OpenLayers 显示世界时间

关注我,带你一起学GIS ^

注:当前使用的是 ol [9.2.4] 版本,天地图使用的key请到天地图官网申请,并替换为自己的key

前言

在现代地图应用中,时间信息的可视化是一个重要且有趣的功能。通过结合地理空间与时间数据,我们可以直观地展示全球不同地区的时间差异,例如时区分布、昼夜变化等。OpenLayers 作为一款强大的开源地图库,提供了灵活的 API 和丰富的功能,能够轻松实现世界时间的动态展示。本文将介绍如何利用 OpenLayers 实现世界时间的可视化,并通过示例展示如何根据时间偏移量动态调整地图元素的样式(如不透明度),从而为用户提供直观的时间分布信息。

在OpenLayers官方例子中,发现一个有趣的案例,可以通过时间偏移量进行图层样式调整。感觉比较有实践意义,推荐大家尝试以下。

详情可参考官网:https://openlayers.org/en/v9.2.4/examples/kml-timezones.html

1. 时间样式函数

该样式函数基于当地中午时间的偏移量设置多边形样式,根据时间大小计算要素的透明度。

/**
 * @description:样式计算函数
 * 基于当地中午时间的偏移量计算多边形的透明度。例如,如果所在时区当前时间为中午的话不透明为0.75;
 * 而所在时区当前显示时间为午夜的话不透明度为0
 * 需要注意的是,此计算未考虑夏令时,因此请不要将其用于规划假期。
 */
const styleFunction = function (feature) {
    const tzOffset = feature.get("tz-offset")
    const local = new Date()
    local.setTime(
        local.getTime() + (local.getTimezoneOffset() + (tzOffset || 0)) * 6000
    )
    // 从中午开始的偏移量
    let delta = Math.abs(12 - (local.getHours() + local.getMinutes() / 60))
    if (delta > 12) {
        delta = 24 - delta
    }
    const opacity = 0.75 * (1 - delta / 12)

    return new ol.style.Style({
        fill: new ol.style.Fill({
            color: [0xff, 0xff, 0x33, opacity],
        }),
        stroke: new ol.style.Stroke({
            color: '#ffffff',
        })
    })
}

以上styleFunction变量为样式函数,具有一个feature参数。样式函数在OpenLayers开发中具有重要作用,通常用于设置文本标注样式,可根据业务需求进行字段展示,最后返回一个Style对象。样式函数中delta变量为时间偏移量,该值用于控制要素的不透明度。

2. 加载时区图层

使用矢量数据源和矢量图层加载时区数据。例子中该矢量数据格式为KML,所以使用ol.format.KML类进行加载。在KML对象中将extractStyles属性设置为false,即不从KML图层中提取样式,默认为true

// 加载时区图层
const worldVector = new ol.layer.Vector({
    source: new ol.source.Vector({
        url: "../../data/kml/timezones.kml",
        format: new ol.format.KML({
            // 不提取KML样式
            extractStyles: false
        })
    }),
    style: styleFunction
})

3. 解析世界时间

在以下代码中使用正则表达式匹配字符串末尾的时间偏移量格式,并将其转换为以分钟为单位的值。

// 解析时间
function parseOffsetFromUTC(name) {
    const match = name.match(/([+-]?)(d{2}):(d{2})$/)
    if (!match) {
        return null
    }
    const sign = match[1] === "-" ? -1 : 1
    const hours = Number(match[2])
    const minutes = Number(match[3])
    // 将时间转换为分钟
    return sign * (60 * hours * minutes)
}

name.match(/([+-]?)(\d{2}):(\d{2})$/)该正则表达式分解如下:

  • ([+-]?):匹配一个可选的 +- 符号,表示偏移方向。
  • (d{2}):匹配两位数字,表示小时部分。
  • ::匹配冒号分隔符。
  • (d{2}):匹配两位数字,表示分钟部分。
  • `:确保匹配的是字符串的末尾。

4. 监听时区图层

监听featuresloadend数据源事件,当图层要素加载完成后,遍历所有要素,并赋予要素属性"tz-offset"

worldVector.getSource().on("featuresloadend"function (evt) {
    evt.features.forEach(feature => {
        const tzOffset = parseOffsetFromUTC(feature.get("name"))
        // 设置"tz-offset"属性
        feature.set("tz-offset", tzOffset, true)
    })
})
map.addLayer(worldVector)

5. 显示图层信息

displayFeatureInfo函数用于显示要素信息。在该函数中,使用forEachFeatureAtPixel方法查找最近的地图要素,如果查询到了矢量要素则显示信息弹出框,否则隐藏。

let currentFeature
const displayFeatureInfo = function (pixel, target) {
    const feature = target.closest('.ol-control')
        ? undefined
        : map.forEachFeatureAtPixel(pixel, function (feature) {
            return feature;
        });
    if (feature) {
        info.style.left = pixel[0] + "px"
        info.style.top = pixel[1] + "px"
        if (feature !== currentFeature) {
            tooltip.setContent({ '.tooltip-inner': feature.get('name') });
        }
        if (currentFeature) {
            tooltip.update()
        } else {
            tooltip.show()
        }
    } else {
        tooltip.hide()
    }
    currentFeature = feature
}

在以下代码中,通过监听地图事件显示和隐藏信息弹出框。

// 监听地图移动事件
map.on('pointermove'function (evt) {
    if (evt.dragging) {
        tooltip.hide();
        currentFeature = undefined;
        return;
    }
    const pixel = map.getEventPixel(evt.originalEvent);
    displayFeatureInfo(pixel, evt.originalEvent.target);
});
//  监听地图点击事件
map.on("click"function (evt) {
    displayFeatureInfo(evt.pixel, evt.originalEvent.target)
})
// 监听地图元素移除事件
map.getTargetElement().addEventListener("pointerleave"function (evt) {
    tooltip.hide()
    currentFeature = undefined
})

6. 完整代码

其中libs文件夹下的包需要更换为自己下载的本地包或者引用在线资源。

<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>OpenLayers 显示世界时间</title>
    <meta charset="utf-8" />

    <link rel="stylesheet" href="../../libs/css/ol9.2.4.css">

    <link rel="stylesheet" href="../../libs/css/bootstrap5.2.0.min.css">
    <script src="../../libs/js/bootstrap5.2.0.bundle.min.js"></script>

    <script src="../../js/config.js"></script>
    <script src="../../libs/js/ol9.2.4.js"></script>
    <style>
        * {
            padding: 0;
            margin: 0;
            font-size: 14px;
            font-family: '微软雅黑';
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

        #map {
            position: absolute;
            top: 50px;
            bottom: 0;
            width: 100%;
        }

        #top-content {
            position: absolute;
            width: 100%;
            height: 50px;
            line-height: 50px;
            background: linear-gradient(135deg, #ff00cc, #ffcc00, #00ffcc, #ff0066);
            color: #fff;
            text-align: center;
            font-size: 32px;
        }

        #top-content span {
            font-size: 32px;
        }

        #info {
            position: absolute;
            height: 1px;
            width: 1px;
            z-index: 100;
        }
    </style>
</head>

<body>
    <div id="top-content">
        <span>OpenLayers 显示世界时间</span>
    </div>
    <div id="map" title="">
        <div id="info"></div>
    </div>
</body>

</html>

<script>
    //==============================================================================//
    //============================天地图服务参数简单介绍==============================//
    //================================vec:矢量图层==================================//
    //================================img:影像图层==================================//
    //================================cva:注记图层==================================//
    //======================其中:_c表示经纬度投影,_w表示球面墨卡托投影================//
    //==============================================================================//
    const TDTImgLayer = new ol.layer.Tile({
        title: "天地图影像图层",
        source: new ol.source.XYZ({
            url: "http://t0.tianditu.com/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" + TDTTOKEN,
            attibutions: "天地图影像描述",
            crossOrigin: "anoymous",
            wrapX: true
        })
    })
    const TDTImgCvaLayer = new ol.layer.Tile({
        title: "天地图影像注记图层",
        source: new ol.source.XYZ({
            url: "http://t0.tianditu.com/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=" + TDTTOKEN,
            attibutions: "天地图注记描述",
            crossOrigin: "anoymous",
            wrapX: true
        })
    })
    const map = new ol.Map({
        target: "map",
        loadTilesWhileInteracting: true,
        view: new ol.View({
            center: [102.845864, 25.421639],
            zoom: 2,
            worldsWrap: true,
            minZoom: 1,
            maxZoom: 20,
            projection: "EPSG:4326",
        }),
        layers: [TDTImgLayer, TDTImgCvaLayer],
        // 地图默认控件
        controls: ol.control.defaults.defaults({
            zoom: false,
            attribution: false,
            rotate: false
        })
    })

    /**
     * @description:样式计算函数
     * 基于当地中午时间的偏移量计算多边形的透明度。例如,如果所在时区当前时间为中午的话不透明为0.75;
     * 而所在时区当前显示时间为午夜的话不透明度为0
     * 需要注意的是,此计算未考虑夏令时,因此请不要将其用于规划假期。
     */
    const styleFunction = function (feature) {
        const tzOffset = feature.get("tz-offset")
        const local = new Date()
        local.setTime(
            local.getTime() + (local.getTimezoneOffset() + (tzOffset || 0)) * 6000
        )
        // 从中午开始的偏移量
        let delta = Math.abs(12 - (local.getHours() + local.getMinutes() / 60))
        if (delta > 12) {
            delta = 24 - delta
        }
        const opacity = 0.75 * (1 - delta / 12)

        return new ol.style.Style({
            fill: new ol.style.Fill({
                color: [0xff, 0xff, 0x33, opacity],
            }),
            stroke: new ol.style.Stroke({
                color: '#ffffff',
            })
        })
    }

    // 加载时区图层
    const worldVector = new ol.layer.Vector({
        source: new ol.source.Vector({
            url: "../../data/kml/timezones.kml",
            format: new ol.format.KML({
                extractStyles: false
            })
        }),
        style: styleFunction
    })

    // 解析时间
    function parseOffsetFromUTC(name) {
        const match = name.match(/([+-]?)(\d{2}):(\d{2})$/)
        if (!match) {
            return null
        }
        const sign = match[1] === "-" ? -1 : 1
        const hours = Number(match[2])
        const minutes = Number(match[3])
        // 将时间转换为分钟
        return sign * (60 * hours * minutes)
    }

    worldVector.getSource().on("featuresloadend"function (evt) {
        evt.features.forEach(feature => {
            const tzOffset = parseOffsetFromUTC(feature.get("name"))
            // 设置"tz-offset"属性
            feature.set("tz-offset", tzOffset, true)
        })
    })
    map.addLayer(worldVector)

    const info = document.getElementById('info');
    info.style.pointerEvents = 'none';
    const tooltip = new bootstrap.Tooltip(info, {
        animation: false,
        customClass: 'pe-none',
        offset: [0, 5],
        title: '-',
        trigger: 'manual',
    });

    let currentFeature
    const displayFeatureInfo = function (pixel, target) {
        const feature = target.closest('.ol-control')
            ? undefined
            : map.forEachFeatureAtPixel(pixel, function (feature) {
                return feature;
            });
        if (feature) {
            info.style.left = pixel[0] + "px"
            info.style.top = pixel[1] + "px"
            if (feature !== currentFeature) {
                tooltip.setContent({ '.tooltip-inner': feature.get('name') });
            }
            if (currentFeature) {
                tooltip.update()
            } else {
                tooltip.show()
            }
        } else {
            tooltip.hide()
        }
        currentFeature = feature
    }

    // 监听地图移动事件
    map.on('pointermove'function (evt) {
        if (evt.dragging) {
            tooltip.hide();
            currentFeature = undefined;
            return;
        }
        const pixel = map.getEventPixel(evt.originalEvent);
        displayFeatureInfo(pixel, evt.originalEvent.target);
    });
    //  监听地图点击事件
    map.on("click"function (evt) {
        displayFeatureInfo(evt.pixel, evt.originalEvent.target)
    })
    // 监听地图元素移除事件
    map.getTargetElement().addEventListener("pointerleave"function (evt) {
        tooltip.hide()
        currentFeature = undefined
    })
</script>

OpenLayers示例数据下载,请在公众号后台回复:ol数据

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路公众号已经接入了智能助手,欢迎大家前来提问。

欢迎访问我的博客网站-长谈GIShttp://shanhaitalk.com

都看到这了,不要忘记点赞、收藏+关注 

本号不定时更新有关 GIS开发  相关内容,欢迎关注 

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部