OpenLayers地图实现车辆标记点和轨迹回放功能

OpenLayers地图基础功能

前言

接触到openlayers地图的起因是,一些客户设备性能差,使用高德地图非常卡,完全拖不动,又不接受聚合的呈现。试过降低高德地图的版本,2.0.0和1.4.15都用过,对于车辆多轨迹点多的情况还是会很卡,体验感不好。最后讨论决定换成openlayers地图,流畅度体验感确实好很多。当看到openlayers的英文文档内心是崩溃的,但是还好写着写着感觉很顺手,结构也比较清晰。 这里分享一下用到的openlayers的基础功能。


OpenLayers 是一个开源的 ​​JavaScript 地图库​​,用于在网页中展示交互式地图。它支持多种地图源(如Google Maps、Bing Maps、OSM等)和地理数据格式(如GeoJSON、WMS、WFS等),适用于构建专业的GIS(地理信息系统)应用。

1、安装

sh 复制代码
npm i ol -S

2、openlayers组成

📦 Map 地图实例 └── 🧩 Layers 图层(瓦片图层TileLayer-用于地图底图、矢量图层VectorLayer-用于绘制点线面) └── 🧮 Source 数据源(OSM、Vector、XYZ 等) └── 📍 Feature 地图要素(点、线、面) └── 🔷 Geometry 几何对象(Point, LineString, Polygon) └── 🎨 Style 样式系统(Style、Stroke、Fill、Text、Icon) └── 🧭 View 地图视图(中心、缩放) └── 🧰 Controls 控件(缩放、比例尺、全屏等) └── 🤏 Interactions 交互(拖动、绘制、选择)

3、创建地图

(1)这里封装成一个通用方法,方便创建多个实例

javascript 复制代码
/**
 * 创建 OpenLayers 地图实例,图层独立,避免相互影响
 * @param mapID 地图挂载的容器ID
 * @param options 视图参数配置
 */
export async function createOlMap(mapID: string, options = {}) {
    try {
        const map = new Map({
            target: mapID,
            view: new View({
                center: fromLonLat(defaultMapCenter), //中心点坐标
                zoom: defaultMapZoom,   //默认10
                minZoom: 4,
                maxZoom: 18,
                projection: 'EPSG:3857',
                ...options,
            }),
            layers: [standardLayer, satelliteLayer, trafficLayer, satelliteLabelLayer], //图层数组
        });

        // 添加比例尺控件
        const scaleLineControl = new ScaleLine({
            units: 'metric',
            className: 'custom-scale-line',
        });
        map.addControl(scaleLineControl);

        return map;
    } catch (err) {
        return Promise.reject('init map error: ' + err);
    }
}

(2)使用高德地图的瓦片图层 · 路况图层需要叠在标准图层上使用 · 卫星标注图层要和卫星影像图层一起使用,才会有地点信息

javascript 复制代码
/**工厂函数 - 创建高德标准地图图层 */
function createStandardLayer() {
    return new TileLayer({
        source: new XYZ({
            url: 'https://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&z={z}&x={x}&y={y}',
            crossOrigin: 'anonymous',
        }),
        visible: true,  //控制图层是否展示
    });
}

/**工厂函数 - 创建高德卫星影像图层 */
function createSatelliteLayer() {
    return new TileLayer({
        source: new XYZ({
            url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
            crossOrigin: 'anonymous',
        }),
        visible: false,
    });
}

/**工厂函数 - 创建高德卫星标注图层 */
function createSatelliteLabelLayer() {
    return new TileLayer({
        source: new XYZ({
            url: 'https://webst0{1-4}.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}',
            crossOrigin: 'anonymous',
        }),
        visible: false,
    });
}

/**工厂函数 - 创建高德实时路况图层 */
function createTrafficLayer() {
    return new TileLayer({
        source: new XYZ({
            url: `http://tm.amap.com/trafficengine/mapabc/traffictile?v=1.0&=&=&t=1&t=${Date.now()}&x={x}&y={y}&z={z}`,
            tileSize: 256,
        }),
        visible: false,
    });
}

4、给地图添加标记点

有两种方法,一种是使用dom元素标记点用Overlay实现,另一种是矢量图形用Feature,视情况而定。

(1)使用overlay实现(dom元素) 在 OpenLayers 中,​​Overlay(覆盖物)​​ 是一种将 ​​HTML/DOM 元素​​ 绑定到地图特定坐标位置的机制。它不参与地图的实际渲染,而是独立浮动在地图容器之上,适合实现弹窗、自定义标记、测量工具提示等需要复杂 UI 或精准屏幕定位的场景。 以给地图添加车辆标记点为例:

javascript 复制代码
const addMarkers = (truck)=>{
    const dom = getMarkerContentDOM(getMarkerContent(truck));
    // 创建overlay
    const overlay = new Overlay({
        element: dom,
        positioning: 'center-center',
        position: truck.position,
        stopEvent: false
    });
    mapInstance.value!.addOverlay(overlay);
}

(2)使用Feature实现

javascript 复制代码
const addMarker = (item: any)=>{
    const icon = getMarkIcon(item);
    const marker = new Feature({
        geometry: new Point(fromLonLat(item.position)), //fromLonLat是用来转坐标系的,不一定需要
    })

    marker.setStyle(new Style({
        image: new Icon({
            anchor: [0.5, 0.5], // 图标锚点在底部中间
            src: icon, // 图标地址
            scale: getMarkScale(item), // 可调整缩放
            rotation: item.Part=== 1 ? 0 : (item.Direction || 0) * Math.PI / 180 // 角度转弧度
        })
    }))

    const markerLayer = new VectorLayer({ 
        source: new VectorSource({
            features:[marker]
        }),
        zIndex: 200
    });
    mapInstance.addLayer(markerLayer);
}

5、给地图添加路线

openlayers地图的架构感觉比较清晰,就是点线面,往上叠图层就行

javascript 复制代码
const renderRoutes = ()=>{
    if(!trackPath.value?.length) return;
    if(routesLayer){
        routesLayer?.getSource()?.clear();
    }
    const feature = new Feature({
        geometry: new LineString(trackPath.value as any)
    })

    feature.setStyle(new Style({
        stroke: new Stroke({
            color: "#335CFF",
            width: 3,
        })
    }))

    routesLayer = new VectorLayer({
        source: new VectorSource({
            features:[feature]
        }),
        zIndex: 100
    })

    mapInstance.addLayer(routesLayer);
    routesExtent.value = routesLayer.getSource()?.getExtent();
    // 调整视图范围
    mapInstance.getView().fit(routesExtent.value, { padding: [50, 50, 50, 50] });
}

6、绘制多边形图形

javascript 复制代码
const startDrawingPolygon = ()=>{
    // 确保清除之前的绘制交互
    cleanupDrawing();
    const source = polygonLayers.get(polygonType.efence)!.getSource();
    drawInteraction.value = new Draw({
        source: source,
        type: 'Polygon',
        style: new Style({
            fill: new Fill({
                color: '#f1870b40' // 对应 fillOpacity: 0.1
            }),
            stroke: new Stroke({
                color: efenceColor, // 对应 strokeColor
                width: 3,        // 对应 strokeWeight
                lineDash: [10, 5] // 虚线效果,对应 strokeStyle: "dashed"
            })
        })
    })

    // 添加绘制完成回调
    drawInteraction.value.on('drawend', (event) => {
        drawFeature = event.feature;
        drawFeature.setStyle(new Style({
            fill: new Fill({ color: efenceColor + 'CC' }), // 40表示透明度
            stroke: new Stroke({
                color: efenceColor,
                width: 2
            }),
            text: new Text({
                font: '14px Calibri,sans-serif',
                stroke: new Stroke({
                    color: '#fff',
                    width: 3
                })
            })
        }))
        const geometry = drawFeature.getGeometry() as OlPolygon;

        // 获取坐标(EPSG:3857),如需转为经纬度需逆转投影
        const coordinates = geometry.getCoordinates()[0]; // [0] 是外环

        // 转换为经纬度坐标
        const lngLatCoords = coordinates.map(coord => {
            const [lon, lat] = toLonLat(coord);
            return {
                Latitude:  Math.round(lat * 1_000_000),
                Longitude: Math.round(lon * 1_000_000)
            };
        });
    });
    mapInstance.value?.addInteraction(drawInteraction.value as Draw);
}

7、实现动态轨迹回放

静态轨迹回放,就是标记点+路线,这里只展示动态轨迹回放写法

javascript 复制代码
let timer:any;
let lineLayer: VectorLayer | null = null;
let index = 0; //全局播放索引

/** 动态回放 */
const dynamicPlayback = (trigger?: string)=>{
    if(!trackPath.value?.length) return;
    if(!lineLayer){
        lineLayer = new VectorLayer({
            source: new VectorSource(),
            zIndex: 100
        })
        mapInstance.addLayer(lineLayer);
    }

    if (!markerLayer) {
        markerLayer = new VectorLayer({ 
            source: new VectorSource(),
            zIndex: 200
        });
        mapInstance.addLayer(markerLayer);
    }
    //获取轨迹中心点范围
    mapInstance.getView().fit(routesExtent.value, { padding: [50, 50, 50, 50] });

    timer = setInterval(()=>{
        if (index >= locationData.value!.length) return clearInterval(timer);
        const item = locationData.value[index]
        const coord = fromLonLat(locationData.value[index].position);
        const { startFeature, endFeature } = getStartEndMarker(trackPath.value);

        // 添加起点
        if(index === 0 ){
            markerLayer?.getSource()?.addFeature(startFeature);
        }
        // 添加线
        if(index > 0){
            const prevCoord = fromLonLat(locationData.value[index-1].position);
            const lineFeature = new Feature({
                geometry: new LineString([prevCoord,coord])
            })
            lineFeature.setStyle(new Style({
                stroke: new Stroke({
                    color: "#335CFF",
                    width: 3,
                })
            }))

            lineLayer?.getSource()?.addFeature(lineFeature);
        }
        // 添加轨迹点
        const icon = getMarkIcon(item);
        const marker = new Feature({
            geometry: new Point(fromLonLat(item.position)),
            data: item // 存储原始数据以便交互
        })

        marker.setStyle(new Style({
            image: new Icon({
                anchor: [0.5, 0.5], // 图标锚点在底部中间
                src: icon, // 图标地址
                scale: getMarkScale(item), // 可调整缩放
                rotation: item.Part=== 1 ? 0 : (item.Direction || 0) * Math.PI / 180 // 角度转弧度
            })
        }))

        markerLayer?.getSource()?.addFeature(marker);

        // 添加终点
        if(index === locationData.value!.length - 1){
            markerLayer?.getSource()?.addFeature(endFeature);
        }

        index++;
    },100)
}

/**暂停 */
function pause() {
    isPaused.value = true;
    if (timer) {
        clearInterval(timer);
        timer = null;
    }
}

/**继续 */
function resume() {
    isPaused.value = false;
    dynamicPlayback();
}

8、坐标系转换

地球坐标系和火星坐标系互相转换

javascript 复制代码
//wgs84 to gcj02   地球坐标系 转 火星坐标系
export function wgs84_to_gcj02(lng, lat) {
    var dlat = transformlat(lng - 105.0, lat - 35.0);
    var dlng = transformlng(lng - 105.0, lat - 35.0);
    var radlat = (lat / 180.0) * PI;
    var magic = Math.sin(radlat);
    magic = 1 - ee * magic * magic;
    var sqrtmagic = Math.sqrt(magic);
    dlat =
        (dlat * 180.0) /
        (((a * (1 - ee)) / (magic * sqrtmagic)) * PI);
    dlng =
        (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI);
    var mglat = lat + dlat;
    var mglng = lng + dlng;

    return [mglng, mglat];
}

/**
 * GCJ-02 转 WGS84(火星坐标系 → 地球坐标系)
 * @param {number} gcjLng - 火星经度
 * @param {number} gcjLat - 火星纬度
 * @returns {[number, number]} WGS84坐标 [经度, 纬度]
 */
export function gcj02_to_wgs84(lng, lat) {
    const originalLngSign = Math.sign(lng);
    const originalLatSign = Math.sign(lat);
    lat = Math.abs(lat);
    lng = Math.abs(lng);
    let dlat = transformlat(lng - 105.0, lat - 35.0)
    let dlng = transformlng(lng - 105.0, lat - 35.0)
    let radlat = lat / 180.0 * PI
    let magic = Math.sin(radlat)
    magic = 1 - ee * magic * magic
    let sqrtmagic = Math.sqrt(magic)
    dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI)
    dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI)
    let mglat = lat + dlat
    let mglng = lng + dlng
    let lngs = lng * 2 - mglng
    let lats = lat * 2 - mglat
    let finalLng = originalLngSign * lngs;
    let finalLat = originalLatSign * lats;
    
    return [finalLng, finalLat];

}

9、其他

还有一些功能,比如算轨迹的总里程、POI地点搜索、两点间路线规划等我是借助高德的api结合openlayers地图实现的。

相关推荐
Fly-ping5 小时前
【前端】JavaScript 的事件循环 (Event Loop)
开发语言·前端·javascript
在逃的吗喽6 小时前
黑马头条项目详解
前端·javascript·ajax
JHCan3337 小时前
一个没有手动加分号引发的bug
前端·javascript·bug
天涯学馆8 小时前
为什么越来越多开发者偷偷用上了 Svelte?
前端·javascript·svelte
拾光拾趣录8 小时前
为什么浏览器那条“假进度”救不了我们?
前端·javascript·浏览器
香菜狗8 小时前
vue3响应式数据(ref,reactive)详解
前端·javascript·vue.js
油丶酸萝卜别吃9 小时前
SSE与Websocket有什么区别?
前端·javascript·网络·网络协议
27669582929 小时前
拼多多小程序 anti_content 分析
java·javascript·python·node·拼多多·anti-content·anti_content
The_era_achievs_hero9 小时前
uni-appDay02
javascript·vue.js·微信小程序·uni-app
rzl0210 小时前
SpringBoot6-10(黑马)
linux·前端·javascript