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地图实现的。

相关推荐
华洛2 分钟前
聊聊我们公司的AI应用工程师每天都干啥?
前端·javascript·vue.js
江城开朗的豌豆2 分钟前
JavaScript篇:你以为事件循环都一样?浏览器和Node的差别让我栽了跟头!
前端·javascript·面试
技术小丁5 分钟前
使用 HTML +JavaScript 从零构建视频帧提取器
javascript·html·音视频
漫谈网络23 分钟前
TypeScript 编译 ES6+ 语法到兼容的 JavaScript介绍
javascript·typescript·es6
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的日历(Calendar),日历_天气预报日历示例(CalendarView01_18)
前端·javascript·vue.js·ecmascript·deepseek
江城开朗的豌豆1 小时前
JavaScript篇:反柯里化:让函数'反悔'自己的特异功能,回归普通生活!
前端·javascript·面试
江城开朗的豌豆1 小时前
JavaScript篇:数字千分位格式化:从入门到花式炫技
前端·javascript·面试
十年砍柴---小火苗1 小时前
原生js操作元素类名(classList,classList.add...)
javascript·css·css3
JohnYan8 小时前
Bun技术评估 - 04 HTTP Client
javascript·后端·bun
拉不动的猪10 小时前
TS常规面试题1
前端·javascript·面试