使用Mapbox取得兩地的行車路徑資訊

Table of Contents

Table of Contents

目的

簡單來說,利用地圖服務(Mapbox)在地圖上顯示兩地的行車路徑。
但是要在地圖上畫出路徑,需要取得兩地的經緯度,另外為了讓使用者可以輸入關鍵字來找到地點的經緯度,使用了MapboxGeocoder來快速完成關鍵字查詢跟經緯度的取得。

Mapbox

Mapbox是一個相當出色的地圖服務。
它提供了Javascript的程式庫,使用它可以在網頁上顯示地圖,並且繪製出路線。
(另外也有iOS跟Android的程式庫)

使用Mapbox時,需要先註冊一個帳號,然後取得一組預設的Access token,用此Access token才能使用該服務的API。

導入Mapbox

官方文件 Installation

這裡介紹使用CDN的方式。

html網頁檔案的中加入js、css的檔案路徑。

<script src='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />

html網頁檔案的中設定地圖。

<div id='map'></div>
<script>
mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';
const map = new mapboxgl.Map({
    container: 'map', // container ID
    style: 'mapbox://styles/mapbox/streets-v11', // style URL
    center: [-74.5, 40], // starting position [lng, lat]
    zoom: 9 // starting zoom
});
</script>

完成上面的設定就能在網頁上顯示地圖。

使用MapboxGeocoder

加入目的地跟出發地,兩個MapboxGeocoder。

在中需要加入mapbox-gl-geocoder的js跟css檔案。

<head>
    <meta name='viewport' content='width=device-width, initial-scale=1' />
    <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />
    <script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.min.js"></script>
    <link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.css" type="text/css">
    <style>
        .geocoder-row {
        display: flex;
        align-items: center;
        }
    </style>
</head>
<div id='map'></div>
<div class="geocoder-row">
  <div id="geocoder-start"></div>
  <div id="geocoder-end"></div>
</div>
<div id="instructions"></div>
<script>
    mapboxgl.accessToken = 'YOUR_ACCESS_TOKEN';
    let start = [121.4210707, 25.08534], end
    // map
    const map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/streets-v11',
        center: start,
        zoom: 9
    });
    const geocoderStart = new Geocoder({
        placeholder: '出發地',
        accessToken: mapboxgl.accessToken,
        countries: 'tw',
        language: 'zh',
        types: 'country,region,place,postcode,locality,neighborhood,address,poi',
        mapboxgl: mapboxgl
    });
    geocoderStart.onAdd(map)
    geocoderStart.addTo('#geocoder-start');
    const geocoderEnd = new Geocoder({
        placeholder: '目的地',
        accessToken: mapboxgl.accessToken,
        countries: 'tw',
        language: 'zh',
        types: 'country,region,place,postcode,locality,neighborhood,address,poi',
        mapboxgl: mapboxgl,
    });
    geocoderEnd.onAdd(map)
    geocoderEnd.addTo('#geocoder-end');
</script>

繪出路線

繪出路線的時機在於出發地跟目的地的經緯度都決定的時候。
mapbox-gl-geocoder的輸入完成時執行路線繪製getRoute

async function getRoute() {
    if (!start || !end) {
      return
    }
    const query = await fetch(
        `https://api.mapbox.com/directions/v5/mapbox/driving/${start[0]},${start[1]};${end[0]},${end[1]}?steps=true&geometries=geojson&access_token=${mapboxgl.accessToken}`,
        { method: 'GET' }
    );
    const json = await query.json();
    const data = json.routes[0];
    const route = data.geometry.coordinates;
    const geojson = {
        type: 'Feature',
        properties: {},
        geometry: {
        type: 'LineString',
        coordinates: route
        }
    };
    // if the route already exists on the map, we'll reset it using setData
    if (map.getSource('route')) {
        map.getSource('route').setData(geojson);
    }
    // otherwise, we'll make a new request
    else {
        map.addLayer({
        id: 'route',
        type: 'line',
        source: {
            type: 'geojson',
            data: geojson
        },
        layout: {
            'line-join': 'round',
            'line-cap': 'round'
        },
        paint: {
            'line-color': '#3887be',
            'line-width': 5,
            'line-opacity': 0.75
        }
        });
    }
    const instructions = document.getElementById('instructions')
    console.log(Math.floor(data.distance)/1000 + 'km')
    let roundedKm = Math.round(data.distance/10000) * 10
    instructions.innerHTML = `
          <p style="font-weight: bold">距離: <span style="border-bottom: 5px double black; font-size: 22px;">${roundedKm} km</span></p>`
    fitBounds()
}
function fitBounds() {
    const bounds = new mapboxgl.LngLatBounds(
      start,
      end
    )
    map.fitBounds(bounds, {
      padding: 60
    });
}

mapbox-gl-geocoder的輸入完成事件(result)中加入getRoutepointStart處理。輸入清除事件(clear)中執行清除。
getRoute:繪制路線。
pointStartpointEnd:標出地點。

geocoderStart.on('result', (e) => {
    start = e.result.center
    pointStart(start)
    getRoute()
});
geocoderStart.on('clear', () => {
    strat = null
})
geocoderEnd.on('result', (e) => {
    end = e.result.center
    pointEnd(end)
    getRoute()
});
geocoderEnd.on('clear', () => {
    end = null
})
function pointStart(lnglat) {
    const data = {
          type: 'FeatureCollection',
          features: [
              {
                  type: 'Feature',
                  properties: {},
                  geometry: {
                      type: 'Point',
                      coordinates: lnglat
                  }
              }
          ]
    };
    if (map.getLayer('start')) {
        map.getSource('start').setData(data);
    } else {
        map.addLayer({
            id: 'start',
            type: 'circle',
            source: {
                type: 'geojson',
                data: data
            },
            paint: {
                'circle-radius': 10,
                'circle-color': '#3887be'
            }
        });
    }
}
function pointEnd(lnglat) {
    const endData = {
          type: 'FeatureCollection',
          features: [
              {
                  type: 'Feature',
                  properties: {},
                  geometry: {
                      type: 'Point',
                      coordinates: lnglat
                  }
              }
          ]
    };
    if (map.getLayer('end')) {
        map.getSource('end').setData(endData);
    } else {
        map.addLayer({
            id: 'end',
            type: 'circle',
            source: {
                type: 'geojson',
                data: endData
            },
            paint: {
                'circle-radius': 10,
                'circle-color': '#f30'
            }
        });
    }
}

結果

result