实现效果

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

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

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

创建一个地图容器

引入地图相关对象

1

2

3

4

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';

创建地图对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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方式,这里都介绍下吧:

1

2

3

4

5

6

7

8

// 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为准,现在把线路加载的剩余部分补充完整:

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

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所对应的代码。

准备开车

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

<!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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

{

  "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"

  }

}

 

GitHub 加速计划 / vu / vue
207.55 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:3 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 5 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐