Mapbox地図にある地点を中心に距離別の円を表示させてみる

Table of Contents

Table of Contents


Mapboxを使って作成した地図の中に、ある地点から半径◯KMで円を作る場合はよくあると思います。
中心点の座標と半径を入力して、円を作成してもらう方法を調べまして、Mapbox+Turf.jsで出来ました。

これから、今回の実装方法を紹介します。
まずは、今回の実装結果です。

画面結果

Map_With_Circles

機能

  • 地図の中心点から半径0.5KM、1KM、2KM、3KMの円を一点鎖線で表示する。

(コード上には、線のスタイルを調整は可能になる。)

  • 地図の左上にあるメニューから半径の表示・非表示を制御できるようになっている。

ソースコード

今回実装したコードは下記となります。

<html>
  <head>
    <meta charset="utf8">
    <title>Mapbox map with distance circles</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
    <link href="https://api.mapbox.com/mapbox-gl-js/v2.4.1/mapbox-gl.css" rel="stylesheet">
    <script src="https://api.mapbox.com/mapbox-gl-js/v2.4.1/mapbox-gl.js"></script>
    <script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
  </head>
  <body>
    <div id="map"></div>
    <nav id="filter-group" class="filter-group"></nav>
  </body>
</html>
<style>
body {
  margin: 0;
  padding: 0;
}
#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}
.filter-group {
  font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
  font-weight: 600;
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
  border-radius: 3px;
  width: 120px;
  color: #fff;
}

.filter-group input[type='checkbox']:first-child + label {
  border-radius: 3px 3px 0 0;
}

.filter-group label:last-child {
  border-radius: 0 0 3px 3px;
  border: none;
}

.filter-group input[type='checkbox'] {
  display: none;
}

.filter-group input[type='checkbox'] + label {
  background-color: #3386c0;
  display: block;
  cursor: pointer;
  padding: 10px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}

.filter-group input[type='checkbox'] + label {
  background-color: #3386c0;
  text-transform: capitalize;
}

.filter-group input[type='checkbox'] + label:hover,
.filter-group input[type='checkbox']:checked + label {
  background-color: #4ea0da;
}

.filter-group input[type='checkbox']:checked + label:before {
  content: '✔';
  margin-right: 5px;
}
</style>
<script>
  mapboxgl.accessToken = 'Your_Mapbox_Access_Token'
  const center = {lng: -77.04, lat: 38.907}
  const map = new mapboxgl.Map({
    container: 'map',
    style: "mapbox://styles/mapbox/streets-v10",
    center: [center.lng, center.lat],
    zoom: 13,
    scrollZoom: false,
    minZoom: 12,
    maxZoom: 14
  });
  map.addControl(new mapboxgl.NavigationControl());
  // スケール表示
  map.addControl(new mapboxgl.ScaleControl({
    maxWidth: 200,
    unit: 'metric'
  }));
  map.on('load', () => {
    let circleOption = {
      center: center,
      paint: {
        "line-color": '#000',
        "line-width": 4,
        "line-dasharray": [6, 3, 2, 3]
      }
    }
    drawCircles(map, circleOption, [0.5, 1, 2, 3])
  })

  function drawCircles(map, circleOption, radiusList) {
    let sourceId = 'distance'
    let center = circleOption["center"]
    let paint = circleOption["paint"] ?? {
      "line-color": '#f30',
      "line-width": 2,
      "line-dasharray": [6, 3, 2, 3]
    }
    let features = radiusList.map(radius => {
      let point = {
        'type': 'Feature',
        'properties': {
          'distance': radius
        },
        'geometry': {
          'type': 'Point',
          'coordinates': [center["lng"], center["lat"]]
        }
      }
      return turf.buffer(point, radius, { units: 'kilometers' })
    })
    let sourceData = { 'type': 'FeatureCollection', 'features': features }
    map.addSource(sourceId, { 'type': 'geojson', 'data': sourceData })
    const filterGroup = document.getElementById('filter-group');
    if (filterGroup) {
      filterGroup.innerHTML = "" // cleat all nodes in #filterGroup
    }
    radiusList.forEach(radius => {
      let layerId = `circle_r${radius}_c${center["lng"]}-${center["lat"]}`
      let layer = map.getLayer(layerId)
      if (!layer) {
        map.addLayer({
          id: layerId,
          type: "line",
          source: "distance",
          layout: {},
          paint: paint,
          filter: ['==', 'distance', radius]
        })
        const input = document.createElement('input')
        input.type = 'checkbox'
        input.id = layerId
        input.checked = true
        input.addEventListener('change', (e) => {
          map.setLayoutProperty(layerId, 'visibility', e.target.checked ? 'visible' : 'none')
        })
        filterGroup.appendChild(input)
        const label = document.createElement('label')
        label.setAttribute('for', layerId)
        label.textContent = `${radius}km`
        filterGroup.appendChild(label)
      }
    })
  }

</script>

説明

Mapboxの使い方に慣れている方はこちらのをみたらすぐにわかると思います。

今回の実装もその例を参考して完成したものです。

MapboxとTurf.jsの導入

<link href="https://api.mapbox.com/mapbox-gl-js/v2.4.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.4.1/mapbox-gl.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>

Mapboxの地図作成

const map = new mapboxgl.Map({
    container: 'map',
    style: "mapbox://styles/mapbox/streets-v10",
    center: [center.lng, center.lat],
    zoom: 13,
    scrollZoom: false,
    minZoom: 12,
    maxZoom: 14
  });

Mapbox地図上に円の作成

turf.buffer関数を使えば、円を構成するデータを作成出来ます。
関数の1番目の引数は、データの形式と円の中心を設定します。
2番目の引数は半径で、3番目は半径の単位です。

円の構成データがあれば、あとはMapboxの描画機能を活用することです。
addSource関数で構成データを地図に設定して、addLayer関数でレイヤーを追加します。

本実装では、複数の円を作成しますため、loopを使いますが、一つの円を描画するコードは下記の例となります。 注意:一つのレイアーに一つの円を作成します。

let point = {
  'type': 'Feature',
  'properties': {
    'distance': radius
  },
  'geometry': {
    'type': 'Point',
    'coordinates': [center["lng"], center["lat"]]
  }
}
let feature = turf.buffer(point, radius, { units: 'kilometers' })

let sourceData = { 'type': 'FeatureCollection', 'features': features }
map.addSource(sourceId, { 'type': 'geojson', 'data': sourceData })

let layerId = `circle_r${radius}_c${center["lng"]}-${center["lat"]}`
let layer = map.getLayer(layerId)
map.addLayer({
  id: layerId,
  type: "line",
  source: "distance",
  layout: {},
  paint: paint,
  filter: ['==', 'distance', radius]
})

円の表示・非表示の制御

制御UIはHTMLのnav要素になります。
最初はnavの中身が何もないですが、円を作成していく際に中身のinput、labelが作成されます。

一つの円に対して、一つのcheckbox inputと一つのlabelがあります。
表示制御のため、checkbox inputのchangeイベントを監視してます。
変更がありましたら、setLayoutProperty関数で円のレイアーを表示(visible)や非表示(none)を処理しています。

const filterGroup = document.getElementById('filter-group');
if (filterGroup) {
  filterGroup.innerHTML = "" // cleat all nodes in #filterGroup
}
radiusList.forEach(radius => {
  let layerId = `circle_r${radius}_c${center["lng"]}-${center["lat"]}`
  let layer = map.getLayer(layerId)
  if (!layer) {
    map.addLayer({
      id: layerId,
      type: "line",
      source: "distance",
      layout: {},
      paint: paint,
      filter: ['==', 'distance', radius]
    })
    const input = document.createElement('input')
    input.type = 'checkbox'
    input.id = layerId
    input.checked = true
    input.addEventListener('change', (e) => {
      map.setLayoutProperty(layerId, 'visibility', e.target.checked ? 'visible' : 'none')
    })
    filterGroup.appendChild(input)
    const label = document.createElement('label')
    label.setAttribute('for', layerId)
    label.textContent = `${radius}km`
    filterGroup.appendChild(label)
  }
})

実装要点の説明は以上です。

Mapboxにはsourcelayerの概念がありまして、初めてMapboxを使う人にとって少しややこしいかもしれません。
でも、その二つは今回の実装には重要なポイントですので、もし理解出来てないなら、Mapboxの公式サイトの例を参考してみて、先に使い方を理解した方が良いと思います。

参考