分析 Flight Animation 这个 demo,官网介绍是:
This example shows how to use postrender and vectorContext to animate flights. A great circle arc between two airports is calculated using arc.js and then the flight paths are animated with postrender. The flight data is provided by OpenFlights (a simplified data set from the Mapbox.js documentation is used). 此示例展示了如何使用 postrender 和 vectorContext 模拟航线动画。使用 arc.js 来计算了两个机场之间的弧线并使用 postrender 模拟航线路径动画。航线的数据来自于 OpenFlights(Mapbox.js 文档里面也使用了这个简单的数据)。
定义基本结构 先展示地图基本结构,与之前的主要区别是引入了一个 arc.js ,用来生成弧线:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <link rel ="stylesheet" href ="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css" type ="text/css" /> <script src ="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js" > </script > <script src ="https://api.mapbox.com/mapbox.js/plugins/arc.js/v0.1.0/arc.js" > </script > <title > Flight Animation(航线动画)</title > <style > html, body, .map { height: 100%; width: 100%; } </style > </head > <body > <div id ="map" class ="map" > </div > <script type ="text/javascript" > let tileLayer = new ol.layer.Tile({ source: new ol.source.Stamen({ layer: "toner" }) }); let map = new ol.Map({ layers: [tileLayer], target: "map" , view: new ol.View({ center: [0, 0], zoom: 2 }) }); </script > </body > </html >
生成航线 生成航线的方式与常规一致,生成style
、source
和layer
对象,然后再map.addLayer()
把图层添加给地图。与之前 demo 的主要区别在于flightsSource
中的loader
属性:
关于loader
的介绍 :
The loader function used to load features, from a remote source for example. If this is not set and url
is set, the source will create and use an XHR feature loader.
loader 函数被用来从远程资源处加载要素,如果 loader 属性没有设置但是 url 属性已设置,source 就会创建并使用一个 XHR 要素加载器。
在使用的时候如代码所示,就是在回调函数中:请求数据源=>处理数据=>设置逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 let style = new ol.style.Style({ stroke: new ol.style.Stroke({ color: "#EAE911" , width: 2 }) }); let flightsSource = new ol.source.Vector({ wrapX: false , attributions: "Flight data by " + '<a href="http://openflights.org/data.html">OpenFlights</a>,' , loader: () => { let url = "https://openlayers.org/en/latest/examples/data/openflights/flights.json" ; fetch(url) .then(response => { return response.json(); }) .then(json => { let flightsData = json.flights; for (let i = 0 ; i < flightsData.length; i++) { let flight = flightsData[i]; let from = flight[0 ]; let to = flight[1 ]; let arcGenerator = new arc.GreatCircle( { x : from [1 ], y : from [0 ] }, { x : to[1 ], y : to[0 ] } ); let arcLine = arcGenerator.Arc(100 , { offset : 10 }); if (arcLine.geometries.length === 1 ) { let line = new ol.geom.LineString(arcLine.geometries[0 ].coords); line.transform("EPSG:4326" , "EPSG:3857" ); let feature = new ol.Feature({ geometry: line, finished: false }); addLater(feature, i * 50 ); } } tileLayer.on("postrender" , animateFlights); }); } }); let flightsLayer = new ol.layer.Vector({ source: flightsSource, style: feature => { if (feature.get("finished" )) { return style; } else { return null ; } } }); map.addLayer(flightsLayer);
请求数据 请求数据时使用的是 ES6 中的fetch ,使用Promise
封装请求,请求到的数据格式如下所示,只截取了前三个示例,数据结构非常清晰,flights
是一个航线数组,每个里面都包含起点和终点的经纬度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "flights" : [ [ [43.449928 , 39.956589 ], [55.606186 , 49.278728 ] ], [ [55.34 , 52.06 ], [45.034689 , 39.170539 ] ], [ [55.012622 , 82.650656 ], [52.268028 , 104.388975 ] ] ] }
生成 LineString 对象 拿到数据之后利用arc.js 来生成弧线,打印一下arcGenerator
和arcLine
,如下图所示,arcGenerator
中包含了弧线的起点和终点的经纬度和屏幕坐标,arcLine
生成了一个集合体对象。在arcLine
生成的对象中可以看到,LineString
的coords
是一个长度为 100 的数组,arcGenerator.Arc(100, { offset: 10 })
中的 100 就是代表把弧线分成 100 个直线来实现。
把arcLine
生成的几何体对象转化为ol.geom.LineString
对象并改为 3857 坐标系,接下来生成要素的时候同时赋值一个finished
属性为false
。
然后执行addLater(feature, i * 50)
和tileLayer.on("postrender", animateFlights)
,这两句中的addLater
和animateFlights
两个函数是为了航线的动画实现而构建的。
核心动画逻辑函数 addLater addLater
是添加要素的方法,代码如下所示,利用setTimeOut
分时间差逐次添加航线的轨迹,营造出一种航线非同步发出的的真实感。并给每个要素设置一个start
起始时间,用来给后面的逻辑计算动画用时。
1 2 3 4 5 6 7 8 9 10 11 12 13 let addLater = (feature, timeout ) => { window .setTimeout(() => { feature.set("start" , new Date ().getTime()); flightsSource.addFeature(feature); }, timeout); };
animateFlights animateFlights
是动画执行逻辑的方法。如下所示,首先定义了一个常量pointsPerMs
,这个常量的意义是没 ms 走过多少个点。前面已经把弧线分成了 100 段,pointsPerMs
为 0.1 就是代表着 1ms 走 0.1 段,也就是 1s 走 100 段,所以动画正好 1s 完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const pointsPerMs = 0.1 ;let animateFlights = event => { let vectorContext = ol.render.getVectorContext(event); let frameState = event.frameState; vectorContext.setStyle(style); let features = flightsSource.getFeatures(); for (let i = 0 ; i < features.length; i++) { let feature = features[i]; if (!feature.get("finished" )) { let coords = feature.getGeometry().getCoordinates(); let elapsedTime = frameState.time - feature.get("start" ); let elapsedPoints = elapsedTime * pointsPerMs; if (elapsedPoints >= coords.length) { feature.set("finished" , true ); } let maxIndex = Math .min(elapsedPoints, coords.length); let currentLine = new ol.geom.LineString(coords.slice(0 , maxIndex)); vectorContext.drawGeometry(currentLine); } } map.render(); };
动画实现的方式也和之前几个实例基本一致,获取了矢量上下文和当前帧以后,先判断要素的finished
属性是否为false
,然后用当前帧时间和要素的start
属性来计算动画已执行时间和已执行的段数(此时如果 100 段都执行完就设置finished
为true
),最后把当前帧的应该显示到的线段绘制出来并执行map.render()
继续渲染地图。
源码 最后展示一下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <link rel ="stylesheet" href ="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/css/ol.css" type ="text/css" /> <script src ="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.1.1/build/ol.js" > </script > <script src ="https://api.mapbox.com/mapbox.js/plugins/arc.js/v0.1.0/arc.js" > </script > <title > Flight Animation(航线动画)</title > <style > html, body, .map { height: 100%; width: 100%; } </style > </head > <body > <div id ="map" class ="map" > </div > <script type ="text/javascript" > let tileLayer = new ol.layer.Tile({ source: new ol.source.Stamen({ layer: "toner" }) }); let map = new ol.Map({ layers: [tileLayer], target: "map" , view: new ol.View({ center: [0, 0], zoom: 2 }) }); let style = new ol.style.Style({ stroke: new ol.style.Stroke({ color: "#EAE911" , width: 2 }) }); let flightsSource = new ol.source.Vector({ wrapX: false , attributions: "Flight data by " + '<a href ="http://openflights.org/data.html" > OpenFlights</a > ,', loader: () => { let url = "https://openlayers.org/en/latest/examples/data/openflights/flights.json" ; fetch(url) .then(response => { return response.json(); }) .then(json => { let flightsData = json.flights; for (let i = 0 ; i < flightsData.length; i++) { let flight = flightsData[i]; let from = flight[0 ]; let to = flight[1 ]; let arcGenerator = new arc.GreatCircle( { x : from [1 ], y : from [0 ] }, { x: to[1], y: to[0] } ); let arcLine = arcGenerator.Arc(100 , { offset : 10 }); if (arcLine.geometries.length === 1) { let line = new ol.geom.LineString( arcLine.geometries[0].coords ); line.transform("EPSG:4326" , "EPSG:3857" ); let feature = new ol.Feature({ geometry: line, finished: false }); addLater(feature, i * 50); } } tileLayer.on("postrender" , animateFlights); }); } }); let flightsLayer = new ol.layer.Vector({ source: flightsSource, style: feature => { if (feature.get("finished" )) { return style; } else { return null ; } } }); map.addLayer(flightsLayer); const pointsPerMs = 0.1 ; /** * @description: 绘制航线动画的核心函数 * @param {type} event 回调参数 * @return : null */ let animateFlights = event => { let vectorContext = ol.render.getVectorContext(event); let frameState = event.frameState; vectorContext.setStyle(style); let features = flightsSource.getFeatures(); for (let i = 0 ; i < features.length; i++) { let feature = features[i]; if (!feature.get("finished" )) { let coords = feature.getGeometry().getCoordinates(); let elapsedTime = frameState.time - feature.get("start" ); let elapsedPoints = elapsedTime * pointsPerMs; if (elapsedPoints >= coords.length) { feature.set("finished" , true ); } let maxIndex = Math .min(elapsedPoints, coords.length); let currentLine = new ol.geom.LineString(coords.slice(0 , maxIndex)); vectorContext.drawGeometry(currentLine); } } map.render(); }; /** * @description: 添加要素的函数, * @param {type} feature 要素 * @param {Number } timeout 延迟时间 * @return : null */ let addLater = (feature, timeout ) => { window .setTimeout(() => { feature.set("start" , new Date ().getTime()); flightsSource.addFeature(feature); }, timeout); }; </script > </body > </html >