NTTドコモR&Dの技術ブログです。

MapLibre GL JSおよびTurf.jsを使った位置情報分析

NTTドコモの石黒です。今回はMapLibreとTurf.jsを使って地図のWebアプリを作って、その上で位置情報の分析をやっていきたいと思います。

MapLibreについて

Webサイトに地図を使ったWebアプリを作って公開できるソフトウェアライブラリです。Mapbox GL JSからフォークされたオープンソースプロジェクトです。

どんなことができるのか

JavascriptでMapbox GL JS Likeなコードを書くことで、地図アプリを動かすことができます。地図の表示だけではなく、重畳するデータの読み込みや可視化についても様々な機能を利用することができます。Javascriptで書けるので、コードが比較的シンプルになると思います。

私はこれまで、Leaflet + Python Flaskで地図アプリを作ったり、QGISで分析した結果をWebアプリ化する事が多かったので、もっと気楽に使える新しい選択肢が出てきたという感想です。

ローカルでのWebアプリの実行

htmlファイルを作って、Webブラウザで読み込むと全ての機能は使えないので、Webサーバーを立てます。

ここでは簡易的な表示の確認ができれば十分ですが、↓をConsoleで実行することでサーバーを立てる方法であったり、

python -m http.server 8000

GitHub PagesのようなWebサーバーに配置するなど、方法は様々あるので、いずれかの方法で解決します。

それでは、実際のコーディングに移ります。 まずはMapboxの実装を確認し、次にMapLibreへの移植を行います。

Mapboxでの地図表示

Mapboxを使う場合には、Mapbox Access Tokenが必要になるため公式サイトで発行します。

<html>
  <body>
    <script src='https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.js'></script>
    <link href='https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl.css' rel='stylesheet' />

    <div id="map" style="width: 600; height: 400;"></div>
    <script>
      mapboxgl.accessToken = 'あなたのMapbox Access Tokenを入れること';
      const map = new mapboxgl.Map({
          container: 'map',
          style: 'mapbox://styles/mapbox/streets-v12',
          center: [-73.954397, 40.751650],
          zoom: 11
      });
    </script>
  </body>
</html>

Mapboxの描画結果

MapLibreでの地図表示

次はMapLibreでの地図表示を行います。今回のチュートリアルでMapboxと異なる部分はここだけです。

この部分を変更すれば、以降の分析や可視化については、どちらのライブラリを利用することでも実装できます。

<html>
  <body>
    <script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
    <link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />

    <div id="map" style="width: 600; height: 400;"></div>
    <script>
      var map = new maplibregl.Map({
          container: 'map',
          // style: 'https://demotiles.maplibre.org/styles/osm-bright-gl-style/style.json',
          // 実際には上記1行で、よく使われるstyleを読み込めるが、
          // 残念ながら筆者の環境だと何故かstyleが読み込めなかったため直接記述する
          style: {
              "version": 8,
              "sources": {
                  "OSM": {
                      "type": "raster",
                      "tiles": ['http://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
                      "tileSize": 256
                  }
              },
              "layers": [{
                  "id": "OSM",
                  "type": "raster",
                  "source": "OSM",
                  "minzoom": 0,
                  "maxzoom": 22
              }]
          },
          center: [-73.954397, 40.751650],
          zoom: 11
      });
    </script>
  </body>
</html>

MapLibreの描画結果

当たっているstyleが異なるため、地図のデザインがMapboxとはやや異なった形となります。styleを変更することで、地図の表示方法は様々デザインすることができます。

このように、Mapboxから、コードはほとんど変更無く利用を始めることができます。Open Sourceプロジェクトである利点として、MapboxのAccess Token利用に伴う課金が発生しないため、気軽に使えて嬉しいです。

MapLibreへのJSONデータの読み込み

次にMapLibreを使って地図上へのマーカー表示を行ってみます。ここでは、New YorkのCitibikeのデータセットを使って、シェアサイクルポートの可視化を行います。

https://citibikenyc.com/system-data

元データはCSVとなっています。ただし、Javascriptで外部から位置情報データを読み込むには、JSONを使うと便利です。

シェアサイクルの位置情報を利用しようと思うのですが、緯度経度の情報はデフォルトではジオメトリの形式にはなっていません。

そこで以下の前処理によるデータ整形を行いました。こちらの処理方法については今回の説明からは省略させていただきます。

  • PostgreSQL + PostGISのDBにCSVファイルをロードし、緯度経度から測地系の変換やジオメトリ情報付加する
  • PostGISを使ってベクトルレイヤーとしてQGISにテーブルを読み込んで、GeoJSONへの変換を行う

前処理後のJSONデータ

{
"type": "FeatureCollection",
"name": "nyc_bikeshare_port",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "station_id": 72, "station_name": "W 52 St & 11 Ave", "station_lat": 40.76727216, "station_lon": -73.99392888 }, "geometry": { "type": "Point", "coordinates": [ -73.99392888, 40.76727216 ] } },
{ "type": "Feature", "properties": { "station_id": 79, "station_name": "Franklin St & W Broadway", "station_lat": 40.71911552, "station_lon": -74.00666661 }, "geometry": { "type": "Point", "coordinates": [ -74.00666661, 40.71911552 ] } },
{ "type": "Feature", "properties": { "station_id": 82, "station_name": "St James Pl & Pearl St", "station_lat": 40.71117416, "station_lon": -74.00016545 }, "geometry": { "type": "Point", "coordinates": [ -74.00016545, 40.71117416 ] } },
{ "type": "Feature", "properties": { "station_id": 83, "station_name": "Atlantic Ave & Fort Greene Pl", "station_lat": 40.68382604, "station_lon": -73.97632328 }, "geometry": { "type": "Point", "coordinates": [ -73.97632328, 40.68382604 ] } },
{ "type": "Feature", "properties": { "station_id": 116, "station_name": "W 17 St & 8 Ave", "station_lat": 40.74177603, "station_lon": -74.00149746 }, "geometry": { "type": "Point", "coordinates": [ -74.00149746, 40.74177603 ] } },
...

地図へのマーカーの可視化

JSONファイルを読み込み、地図へのマーカー可視化を行います。今回はjQueryを使ってJSONファイルの読み込みを行ってみました。 読み込み後、特別なパース処理なども不要で、すぐに可視化をすることができます。

<script src='https://code.jquery.com/jquery-3.7.1.slim.min.js'></script> // jQueryを読み出す

// ...省略

map.on('load', () => {
    var url = './data/nyc_bikeshare_port.geojson';
    $.getJSON(url, function(data){  // jQueryでJSONファイル読み出しを行う
        var bicycles = data;
        map.addLayer({
            id: 'bicycle',
            type: 'circle',
            source: {
                type: 'geojson',
                data: bicycles
            },
            paint: {
                'circle-radius': 3,
                'circle-color': 'red'
            }
        });
    });
});

地図へのマーカー可視化の結果

後段で色情報を付けて区別するため、今回はアイコンなどは使わずシンプルな色付きドットで可視化します。

Turf.jsによる空間分析

ここまでで、MapLibreにデータを読み出して地図上への可視化を行うことができるようになりました。

MapLibreは地図可視化のための様々な機能をもっています。しかし、位置情報・空間情報の分析用途として用いる場合、Javascript側で空間演算ができるとコードもシステムもシンプルになりとても便利です。

それを実現するためのライブラリがTurf.jsです。Turf.jsにはポリゴンの位置関係・集合関係に関わる演算のための基本機能が一通り実装されており、ちょっとした分析を行う上では十分な機能を有しています。

そこで、次はTurf.jsを使った位置情報分析とMapLibreを組み合わせて、位置情報分析と可視化を実施してみましょう。

Turf.jsを用いた空間クラスタリング

今回はシンプルにKMeansとDBSCANによるクラスタリングとその結果の可視化の表示を行ってみます。

KMeansの実行と可視化

まず、KMeansを実装します。短いコードでクラスタリングから可視化まで実現できていることが確認できます。

<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>

// ...省略

map.on('load', () => {
      var url = './data/nyc_bikeshare_port.geojson';
      $.getJSON(url, function(data){
          var cluster_size = 15;
          var options = {numberOfClusters: cluster_size};
          var clustered = turf.clustersKmeans(data, options); // Kmeansの実行

          const color_array = ['aqua', 'blue', 'fuchsia', 'gray', 'yellow',
                   'green', 'lime', 'maroon', 'navy', 'olive',
                   'purple', 'red', 'silver', 'teal', 'white']; // blackを除く基本色とする

          for (let i = 0; i < cluster_size; i++) {
                  map.addLayer({
                      id: 'bicycle_clusters_' + String(i+1),
                      type: 'circle',
                      source: {
                      type: 'geojson',
                      data: clustered
                      },
                      paint: {
                      'circle-radius': 3,
                      'circle-color': color_array[i%15]
                      },
                      filter: ['==', 'cluster', i] // clusterのクラスID毎に色を変えて可視化する
                  });
          }
        })
});

KMeansの可視化結果

DBSCANの実行と可視化

次にDBSCAN。DBSCANではクラスタの通常クラスとノイズクラスの要素が別々に管理されるため、KMeansに比べて少しだけコードのボリュームが増えます。とはいえ十分にシンプルな実装と言えると思います。

<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>

// ...省略

map.on('load', () => {
      var url = './data/nyc_bikeshare_port.geojson';
      $.getJSON(url, function(data){

          var bicycles = data;
          var maxDistance = 0.3;
          var clustered = turf.clustersDbscan(data, maxDistance);
          var cluster_size = 100;

          const color_array = ['aqua', 'blue', 'fuchsia', 'gray', 'yellow',
                   'green', 'lime', 'maroon', 'navy', 'olive',
                   'purple', 'red', 'silver', 'teal', 'white'];

          for (let i = 0; i < cluster_size; i++) {
                  map.addLayer({
                      id: 'bicycle_clusters_' + String(i+1),
                      type: 'circle',
                      source: {
                      type: 'geojson',
                      data: clustered
                      },
                      paint: {
                      'circle-radius': 3,
                      'circle-color': color_array[i%15]
                      },
                      filter: ['==', 'cluster', i] // clusterのクラスID毎に色を変えて可視化する
                  });
          }
          
          map.addLayer({
                  id: 'bicycle_clusters_noise',
                  type: 'circle',
                  source: {
                      type: 'geojson',
                      data: clustered
                  },
                  paint: {
                      'circle-radius': 3,
                      'circle-color': 'black'
                  },
                  filter: ['==', 'dbscan', 'noise']
          // DBSCANのノイズクラスには、cluster要素が振られず、dbscan要素でnoiseというフラグが付く
                });
        })
});

DBSCANの可視化結果

おわりに

このようにMapLibre GL JSとTurf.jsを使って、Javascriptによる位置情報分析のチュートリアルを行いました。

どちらのライブラリも試してみれば簡単に使えるものであり、ちょっとした地図Webアプリのデモを作ることが目的であれば、是非利用を検討してみるととても良いと思います。

それでは、ご拝読ありがとうございました。