vue利用openlayers实现动态轨迹

今天介绍一个有趣的gis小功能:动态轨迹播放!效果就像这样:

GIF_20211118_233238.gif

这效果看着还很丝滑!别急,接下来教你怎么实现。代码示例基于parcel打包工具和es6语法,本文假设你已经掌握相关知识和技巧。

gis初学者可能对openlayers(后面简称ol)不熟悉,这里暂时不介绍ol了,直接上代码,先体验下感觉。

创建一个地图容器

引入地图相关对象

import Map from 'ol/Map';
import View from 'ol/View';
import XYZ from 'ol/source/XYZ';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';

创建地图对象

const center = [-5639523.95, -3501274.52];
const map = new Map({
 target: document.getElementById('map'),
 view: new View({
 center: center,
 zoom: 10,
 minZoom: 2,
 maxZoom: 19,
 }),
 layers: [
 new TileLayer({
 source: new XYZ({
 attributions: attributions,
 url: 'https://api.maptiler.com/maps/hybrid/{z}/{x}/{y}.jpg?key=' + key,
 tileSize: 512,
 }),
 }),
 ],
});

创建一条线路

画一条线路

可以用这个geojson网站随意画一条线,然后把数据内容复制下来,保存为json文件格式,作为图层数据添加到地图容器中。

你可以用异步加载的方式,也可以用require方式,这里都介绍下吧:

// fetch
fetch('data/route.json').then(function (response) {
 response.json().then(function (result) {
 const polyline = result.routes[0].geometry;
 }),
};
// require
var roadData = require('data/route.json')

后面基本一样了,就以fetch为准,现在把线路加载的剩余部分补充完整:

fetch('data/route.json').then(function (response) {
 response.json().then(function (result) {
 const polyline = result.routes[0].geometry;
	// 线路数据坐标系转换
 const route = new Polyline({
 factor: 1e6,
 }).readGeometry(polyline, {
 dataProjection: 'EPSG:4326',
 featureProjection: 'EPSG:3857',
 });
	// 线路图层要素
 const routeFeature = new Feature({
 type: 'route',
 geometry: route,
 });
 // 起点要素
 const startMarker = new Feature({
 type: 'icon',
 geometry: new Point(route.getFirstCoordinate()),
 });
 // 终点要素
 const endMarker = new Feature({
 type: 'icon',
 geometry: new Point(route.getLastCoordinate()),
 });
 // 取起点值
 const position = startMarker.getGeometry().clone();
 // 游标要素
 const geoMarker = new Feature({
 type: 'geoMarker',
 geometry: position,
 });
	// 样式组合
 const styles = {
 // 路线
 'route': new Style({
 stroke: new Stroke({
 width: 6,
 color: [237, 212, 0, 0.8],
 }),
 }),
 'icon': new Style({
 image: new Icon({
 anchor: [0.5, 1],
 src: 'data/icon.png',
 }),
 }),
 'geoMarker': new Style({
 image: new CircleStyle({
 radius: 7,
 fill: new Fill({color: 'black'}),
 stroke: new Stroke({
 color: 'white',
 width: 2,
 }),
 }),
 }),
 };
	// 创建图层并添加以上要素集合
 const vectorLayer = new VectorLayer({
 source: new VectorSource({
 features: [routeFeature, geoMarker, startMarker, endMarker],
 }),
 style: function (feature) {
 return styles[feature.get('type')];
 },
 });
	// 在地图容器中添加图层
 map.addLayer(vectorLayer);

以上代码很完整,我加了注释,整体思路总结如下:

  • 先加载路线数据
  • 构造路线、起始点及游标对应图层要素对象
  • 构造图层并把要素添加进去
  • 在地图容器中添加图层

添加起、终点

这个上面的代码已经包括了,我这里列出来是为了让你更清晰,就是startMarkerendMarker对应的代码。

添加小车

同样的,这里的代码在上面也写过了,就是geoMarker所对应的代码。

准备开车

线路有了,车也有了,现在就到了激动人心的开车时刻了,接下来才是本文最核心的代码!

const speedInput = document.getElementById('speed');
 const startButton = document.getElementById('start-animation');
 let animating = false;
 let distance = 0;
 let lastTime;
 function moveFeature(event) {
 const speed = Number(speedInput.value);
 // 获取当前渲染帧状态时刻
 const time = event.frameState.time;
 // 渲染时刻减去开始播放轨迹的时间
 const elapsedTime = time - lastTime;
 // 求得距离比
 distance = (distance + (speed * elapsedTime) / 1e6) % 2;
 // 刷新上一时刻
 lastTime = time;
	 // 反减可实现反向运动,获取坐标点
 const currentCoordinate = route.getCoordinateAt(
 distance > 1 ? 2 - distance : distance
 );
 position.setCoordinates(currentCoordinate);
 // 获取渲染图层的画布
 const vectorContext = getVectorContext(event);
 vectorContext.setStyle(styles.geoMarker);
 vectorContext.drawGeometry(position);
 map.render();
 }
 function startAnimation() {
 animating = true;
 lastTime = Date.now();
 startButton.textContent = 'Stop Animation';
 vectorLayer.on('postrender', moveFeature);
 // 隐藏小车前一刻位置同时触发事件
 geoMarker.setGeometry(null);
 }
 function stopAnimation() {
 animating = false;
 startButton.textContent = '开车了';
 // 将小车固定在当前位置
 geoMarker.setGeometry(position);
 vectorLayer.un('postrender', moveFeature);
 }
 startButton.addEventListener('click', function () {
 if (animating) {
 stopAnimation();
 } else {
 startAnimation();
 }
 });

简单说下它的原理就是利用postrender事件触发一个函数,这个事件本来是地图渲染结束事件,但是它的回调函数中,小车的坐标位置一直在变,那就会不停地触发地图渲染,当然最终也会触发postrender。这样就实现的小车沿着轨迹的动画效果了。这段代码有点难理解,最好自己尝试体验下,比较难理解部分我都加上了注释。

好了,ol动态巡查已经介绍完了,动手试下吧!看你的车能否开起来?

完整代码

index.html

<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8">
 <title>Marker Animation</title>
 <!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
 <script src="https://unpkg.com/elm-pep"></script>
 <style>
 .map {
 width: 100%;
 height:400px;
 }
 </style>
 </head>
 <body>
 <div id="map" class="map"></div>
 <label for="speed">
 speed: 
 <input id="speed" type="range" min="10" max="999" step="10" value="60">
 </label>
 <button id="start-animation">Start Animation</button>
 <script src="main.js"></script>
 </body>
</html>

main.js

import 'ol/ol.css';
import Feature from 'ol/Feature';
import Map from 'ol/Map';
import Point from 'ol/geom/Point';
import Polyline from 'ol/format/Polyline';
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
import XYZ from 'ol/source/XYZ';
import {
 Circle as CircleStyle,
 Fill,
 Icon,
 Stroke,
 Style,
} from 'ol/style';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import {getVectorContext} from 'ol/render';
const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const attributions =
 '<a href="https://www.maptiler.com/copyright/" rel="external nofollow" target="_blank">&copy; MapTiler</a> ' +
 '<a href="https://www.openstreetmap.org/copyright" rel="external nofollow" target="_blank">&copy; OpenStreetMap contributors</a>';
const center = [-5639523.95, -3501274.52];
const map = new Map({
 target: document.getElementById('map'),
 view: new View({
 center: center,
 zoom: 10,
 minZoom: 2,
 maxZoom: 19,
 }),
 layers: [
 new TileLayer({
 source: new XYZ({
 attributions: attributions,
 url: 'https://api.maptiler.com/maps/hybrid/{z}/{x}/{y}.jpg?key=' + key,
 tileSize: 512,
 }),
 }),
 ],
});
// The polyline string is read from a JSON similiar to those returned
// by directions APIs such as Openrouteservice and Mapbox.
fetch('data/polyline/route.json').then(function (response) {
 response.json().then(function (result) {
 const polyline = result.routes[0].geometry;
 const route = new Polyline({
 factor: 1e6,
 }).readGeometry(polyline, {
 dataProjection: 'EPSG:4326',
 featureProjection: 'EPSG:3857',
 });
 const routeFeature = new Feature({
 type: 'route',
 geometry: route,
 });
 const startMarker = new Feature({
 type: 'icon',
 geometry: new Point(route.getFirstCoordinate()),
 });
 const endMarker = new Feature({
 type: 'icon',
 geometry: new Point(route.getLastCoordinate()),
 });
 const position = startMarker.getGeometry().clone();
 const geoMarker = new Feature({
 type: 'geoMarker',
 geometry: position,
 });
 const styles = {
 'route': new Style({
 stroke: new Stroke({
 width: 6,
 color: [237, 212, 0, 0.8],
 }),
 }),
 'icon': new Style({
 image: new Icon({
 anchor: [0.5, 1],
 src: 'data/icon.png',
 }),
 }),
 'geoMarker': new Style({
 image: new CircleStyle({
 radius: 7,
 fill: new Fill({color: 'black'}),
 stroke: new Stroke({
 color: 'white',
 width: 2,
 }),
 }),
 }),
 };
 const vectorLayer = new VectorLayer({
 source: new VectorSource({
 features: [routeFeature, geoMarker, startMarker, endMarker],
 }),
 style: function (feature) {
 return styles[feature.get('type')];
 },
 });
 map.addLayer(vectorLayer);
 const speedInput = document.getElementById('speed');
 const startButton = document.getElementById('start-animation');
 let animating = false;
 let distance = 0;
 let lastTime;
 function moveFeature(event) {
 const speed = Number(speedInput.value);
 const time = event.frameState.time;
 const elapsedTime = time - lastTime;
 distance = (distance + (speed * elapsedTime) / 1e6) % 2;
 lastTime = time;
 const currentCoordinate = route.getCoordinateAt(
 distance > 1 ? 2 - distance : distance
 );
 position.setCoordinates(currentCoordinate);
 const vectorContext = getVectorContext(event);
 vectorContext.setStyle(styles.geoMarker);
 vectorContext.drawGeometry(position);
 // tell OpenLayers to continue the postrender animation
 map.render();
 }
 function startAnimation() {
 animating = true;
 lastTime = Date.now();
 startButton.textContent = 'Stop Animation';
 vectorLayer.on('postrender', moveFeature);
 geoMarker.setGeometry(null);
 }
 function stopAnimation() {
 animating = false;
 startButton.textContent = '开车了';
 geoMarker.setGeometry(position);
 vectorLayer.un('postrender', moveFeature);
 }
 startButton.addEventListener('click', function () {
 if (animating) {
 stopAnimation();
 } else {
 startAnimation();
 }
 });
 });
});

package.json

{
 "name": "feature-move-animation",
 "dependencies": {
 "ol": "6.9.0"
 },
 "devDependencies": {
 "parcel": "^2.0.0-beta.1"
 },
 "scripts": {
 "start": "parcel index.html",
 "build": "parcel build --public-url . index.html"
 }
}

参考资源:

https://openlayers.org/en/latest/examples/feature-move-animation.html

作者:字节逆旅

%s 个评论

要回复文章请先登录注册