# 示例:最快路线计算

通过指定起点和终点(也可以添加一些途经点),计算一条或多条行进最快的路线。

# 1. 搭建页面框架

首先我们创建一个名为 network-paths.html 的页面,这里我们引入 OpenLayers,在页面上显示一个地图组件,并加载一个开放底图:

<!DOCTYPE html>
<html>
<meta charset="utf-8">

<head>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/css/ol.css" type="text/css">
  <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.5.0/build/ol.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
</head>

<body>
  <div id="app">
    <div id="map" style="width: 600px; height: 600px"></div>
  </div>

  <script>
  var app = new Vue({
    el: '#app',
    data: {
      map: null,
      markerLayer: null,
      markerStyle: null,
      resultLayer: null,
    },

    mounted: function() {
      this.markerLayer = new ol.layer.Vector({
        source: new ol.source.Vector(),
      });

      this.markerStyle = new ol.style.Style({
        image: new ol.style.Icon({
          anchor: [0.5, 1.0],
          src: '../../../../assets/guide/icon-pin.png',
        }),
      });

      this.resultLayer = new ol.layer.Vector({
        source: new ol.source.Vector(),
      });

      this.map = new ol.Map({
        target: 'map',
        layers: [
          new ol.layer.Tile({
            source: new ol.source.OSM(),
          }),
          this.resultLayer,
          this.markerLayer,
        ],
        view: new ol.View({
          center: ol.proj.fromLonLat([7.4126, 43.7407]),
          zoom: 14,
        }),
      });
    }
  });
  </script>
</body>

</html>

# 2. 响应鼠标点击事件

然后,我们在鼠标点击的时候,在地图上添加代表位置的图标。在这个例子里,我们只设置起点和终点,如果超过两个点就清空地图重新开始:

this.map.on('click', function(evt) {
  const markerSource = this.markerLayer.getSource();

  const markerCount = markerSource.getFeatures().length;
  if (markerCount > 1) {
    markerSource.clear();
  }

  const { coordinate } = evt;
  const feature = new ol.Feature({
    geometry: new ol.geom.Point(coordinate),
  });
  feature.setStyle(this.markerStyle);

  markerSource.addFeature(feature);
}.bind(this));

这时,页面会随着鼠标点击,会显示点击的位置:

# 3. 计算两点间的最快路径

当地图上存在两个点的时候,调用 API 计算这两个点之间的最快路径:

const features = markerSource.getFeatures();
if (features.length == 2) {
  const from = features[0].getGeometry();
  const to = features[1].getGeometry();

  fetch('http://localhost:9000/heycloud/api/routing/network/sample/paths', {
      method: 'POST',
      mode: 'cors',
      headers: {
        'content-type': 'application/json',
      },
      body: JSON.stringify({
        'points': [
          from.getFirstCoordinate(),
          to.getFirstCoordinate(),
        ],
        'sr': 'web-mercator',
        'alternatives': 2,
      }),
    })
    .then(resp => resp.json())
    .then(resp => {
      const { waypoints, routes } = resp.result;
    });
}

在这里,我们需要将起点和终点的坐标传入points参数,同时指定了最多返回两条备选路线。在 API 调用结果中,我们能够得到设定点对应的道路节点waypoints以及计算后的路线routes。然后,我们可以将这些对象显示到地图上:

const { waypoints, routes } = resp.result;

const image = new ol.style.Circle({
  radius: 4,
  stroke: new ol.style.Stroke({
    color: '#f00',
    width: 4,
  }),
  fill: new ol.style.Fill({
    color: '#fff',
  }),
});

const waypointStyle = new ol.style.Style({
  image,
});

const routeStyle = new ol.style.Style({
  stroke: new ol.style.Stroke({
    color: '#f00',
    width: 4,
  }),
});

const routeAlterStyle = new ol.style.Style({
  stroke: new ol.style.Stroke({
    color: '#449',
    width: 4,
    lineDash: [6, 12],
  }),
});

for (let i = routes.length - 1; i >= 0; --i) {
  const route = routes[i];

  for (let leg of route.properties.legs) {
    for (let step of leg.steps) {
      const stepFeature = new ol.format.GeoJSON().readFeature(step);
      if (i == 0) {
        stepFeature.setStyle(routeStyle);
      } else {
        stepFeature.setStyle(routeAlterStyle);
      }
      resultSource.addFeature(stepFeature);
    }
  }
}

for (let waypoint of waypoints) {
  const feature = new ol.format.GeoJSON().readFeature(waypoint);
  feature.setStyle(waypointStyle);
  resultSource.addFeature(feature);
}

这样的页面,地图显示的效果如下:

# 4. 显示导航建议

路线计算的结果中,还包含导航建议信息,下面我们在图上显示这些建议。导航建议信息存储在 step 对象内部,在这个例子中,我们只挑选转弯、环岛环路信息进行显示:

const stepManeuverFeature = new ol.format.GeoJSON().readFeature(step.properties.maneuver);
const type = stepManeuverFeature.get('type');
const modifier = stepManeuverFeature.get('modifier');
if (['turn', 'roundabout', 'exit roundabout', 'rotary', 'exit rotary'].includes(type)) {
  resultSource.addFeature(stepManeuverFeature);
}

接下来我们给这些对象添加鼠标响应事件,我们希望在鼠标移动到这些对象上的时候,它们会改变样式:

this.map.on('pointermove', function(evt) {
  let hit = false;

  this.map.forEachFeatureAtPixel(evt.pixel, function(f) {
    hit = true;

    const type = f.get('type');
    if (['turn', 'roundabout', 'exit roundabout', 'rotary', 'exit rotary'].includes(type)) {
      if (this.selected) {
        this.selected.setStyle(undefined);
        this.selected = null;
      }

      this.selected = f;

      const image = new ol.style.Circle({
        radius: 4,
        stroke: new ol.style.Stroke({
          color: '#000',
          width: 4,
        }),
        fill: new ol.style.Fill({
          color: '#fff',
        }),
      });

      const selectStyle = new ol.style.Style({
        image,
      });

      f.setStyle(selectStyle);

      return true;
    }
  }.bind(this));

  this.map.getViewport().style.cursor = hit ? 'pointer' : '';
}.bind(this));

同时,我们还可以在图上显示文本提示信息:

<div id="app" style="position: relative;">
  <div id="map" style="width: 600px; height: 600px"></div>
  <div v-if="hint" style="position: absolute; left: 4px; bottom: 4px; background: #fff; border-radius: 4px; padding: 4px; font-size: 0.8em; color: #666">{{ hint }}</div>
</div>
var app = new Vue({
  computed: {
    hint: function() {
      const f = this.selected;
      if (!f) {
        return;
      }

      const type = f.get('type');
      const modifier = f.get('modifier');

      if (type == 'turn') {
        switch (modifier) {
          case 'uturn':
            return '掉头';
          case 'sharp right':
            return '大右转';
          case 'right':
            return '右转';
          case 'slight right':
            return '小右转';
          case 'straight':
            return '直行';
          case 'sharp left':
            return '大左转';
          case 'left':
            return '左转';
          case 'slight left':
            return '小左转';
        }
      } else if (type == 'roundabout') {
        return '进入环岛';
      } else if (type == 'exit roundabout') {
        return '离开环岛';
      } else if (type == 'rotary') {
        return '进入环路';
      } else if (type == 'exit rotary') {
        return '离开环路';
      }
    },
  },
});

这时,当鼠标移动到导航提示点上时,你可以看到提示点切换了显示样式;同时,在左下方还显示了当前的提示信息: