フリーダムの日記

GIS(地理情報システム)を中心に技術的なことを書いています。

全国市区町村界データを Tippecanoe で Mapbox Vector Tile に変換して、ArcGIS API for JavaScript で表示

はじめに

Mapbox 社がオープンソースとして提供しているツール ”Tippecanoe” (発音は ”ティピカヌー”) を使用して、全国市区町村界データのシェープファイルを MVT 化して、Esri 社の ArcGIS API for JavaScript で表示してみましたので、その手順について紹介したいと思います。

今回以下のステップでそれぞれ構築しました。

Tippecanoe

Tippecanoe を使用して、mapbox vector tile に変換する場合に、シェープファイルを GeoJSON に変換する必要があります。そして、変換した GeoJSON を使用して、ベクタータイルを作成します。

ArcGIS API for JavaScript で作成したアプリは以下で確認することができます。 valuecreation.github.io

ArcGIS API for JavaScript

全国市区町村界データは ESRIジャパンが提供しているデータを使用しました。 www.esrij.com

Tippecanoe とは

Tippecanoe は、Mapbox 社が提供しているオープンソースソフトウェアで、GeoJSON ファイルを mbtiles 形式に変換することができます。 実際に Mapbox 社のサービスでも使用されているそうです。 github.com

ベクタータイルの作成

1. GDAL の org2ogr で GeoJSON に変換

ベクタータイルの作成には、全国市区町村界データのシェープファイルを GeoJSON に変換する必要がありますので、GDAL というフリーの GIS ツールを利用しました。GDAL は GIS データを扱う上でとても便利なツールです。データフォーマットの確認やデータ変換など多くの機能に対応しています。

www.gdal.org

GDAL の org2ogr を利用することで変換が可能です。

$ ogr2ogr -f GeoJSON -t_srs crs:84 japan_ver821.geojson  japan_ver821.shp

ogr2ogr では上記のコマンドを実行することで、japan_ver821.shp の シェープファイル形式から japan_ver821.geojson の GeoJSON へと変換できます。

今回は GDAL 本体はインストールしないで、GDAL の Docker イメージも公開されていますので Docker コマンドを使用して変換しました。GDAL の Docker イメージを作成後、以下のコマンドを実行することで GeoJSON に変換することができます。

docker pull osgeo/gdal:alpine-small-latest
docker run --rm -v /Users:/Users osgeo/gdal:alpine-small-latest ogr2ogr -f GeoJSON -t_srs crs:84 $PWD/data/japan_ver821.geojson $PWD/data/japan_ver821.shp

GDAL の Docker のイメージについては以下の種類がそれぞれあり、目的に応じてイメージを作成することができます。

github.com

2. Tippecanoe で ベクタータイルを作成

Tippecanoe を使用するために Tippecanoe をGitHub からクローンします。Tippecanoe の使用にも Docker を使用しました。Tippecanoe の Dockerfile が公開されていますので、Git クローンして、docker build コマンドでイメージを作成します。

$ git clone https://github.com/mapbox/tippecanoe.git
$ cd tippecanoe
$ docker build -t tippecanoe:latest .

1 のステップで作成した GeoJSON を使用して、ベクタータイルを作成します。以下の Docker コマンドで tippecanoe を実行することでベクタータイルを作成することができます。

docker run -it --rm -v /Users:/Users tippecanoe:latest tippecanoe -zg --output-to-directory=$PWD/data/zxy --no-tile-compression --coalesce-fraction-as-needed --extend-zooms-if-still-dropping $PWD/data/japan_ver821.geojson

今回 Tippecanoe は、初めて使用しましたので、コマンドで使用するパラメーターには迷いました。パラメーターも幾つかあり、以下のパラメーターを使用することでうまく作成ができました。GitHub に公開されている Tippecanoe には幾つか使用例がありましたので、そちらを参考にしました。

-zg : zg オプションを指定すると、Tippecanoe は元のデータの精度を反映するのに十分な高さの最大ズームレベルを選択するようになります。
--output-to-directory : mbtiles ファイルではなく、指定したディレクトリにタイルを書き込みます。
--no-tile-compression : PBF vector tile データを圧縮しないでください。レンダラーから "Unimplemented type 3" というエラーメッセージが表示される場合は、通常の gzip 圧縮されたタイルではなく、このオプションを使用して圧縮されていないタイルを期待しているためだと思われます。
--coalesce-fraction-as-needed : 各ズームレベルからのフィーチャの一部を他の近くのフィーチャに動的に結合して、大きなタイルを500 Kサイズ制限以下に保ちます。(主にポリゴンに便利です)。
--extend-zooms-if-still-dropping : 高ズームレベルのタイルでさえ大きすぎる場合は、すべての機能を表すことができる1つに到達するまでズームレベルを追加し続けます。

Tippecanoe で使用するコマンドのパラメーターや仕様の詳細については以下が参考になります。 github.com

全国市区町村界データですが、GeoJSON でファイルサイズが 24.8 MB で、ベクタータイルにすることで 4.6 MB になりました。

3. ArcGIS API for JavaScriptベクタータイルを表示

最後に作成したベクタータイルを ArcGIS API for JavaScript で表示してみました。 github.com

表示には、VectorTileLayer クラスを使用することで可能です。 developers.arcgis.com

クラスの使用はとてもシンプルで VectorTileLayer クラスの URL にスタイルを指定するだけです。変換前の GeoJSON も表示してみました。

const tileLayer = new VectorTileLayer({
    title: "全国市区町村界データ (ベクタータイル)",
    url: "./style.json",
    opacity: 0.5
});

const geoJSONLayer = new GeoJSONLayer({
    title: "全国市区町村界データ (GeoJSON)",
    url: "./japan_ver821.geojson",
    opacity: 0.8,
    visible: false
});

全体のコードは以下となります。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />
    <title>
       全国市区町村界データを Tippecanoe で Mapbox Vector tile に変換して、ArcGIS API for JavaScript で表示
    </title>
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
      #info {
        background-color: black;
        opacity: 0.75;
        color: orange;
        font-size: 12pt;
        padding: 8px;
        visibility: hidden;
      }
    </style>

    <link
      rel="stylesheet"
      href="https://js.arcgis.com/4.18/esri/themes/light/main.css"
    />
    <script src="https://js.arcgis.com/4.18/"></script>

    <script>
      require(["esri/Map", "esri/views/MapView", "esri/layers/VectorTileLayer", "esri/layers/GeoJSONLayer", "esri/widgets/LayerList",], function (Map, MapView, VectorTileLayer, GeoJSONLayer, LayerList) {
        
        const tileLayer = new VectorTileLayer({
          title: "全国市区町村界データ (ベクタータイル)",
          url: "./style.json",
          opacity: 0.5
        });
        
        const geoJSONLayer = new GeoJSONLayer({
            title: "全国市区町村界データ (GeoJSON)",
            url: "./japan_ver821.geojson",
            opacity: 0.8,
            visible: false
        });
        
        var map = new Map({
          basemap: "topo-vector",
          layers: [geoJSONLayer, tileLayer]
        });

        var view = new MapView({
          container: "viewDiv",
          map: map,
          zoom: 5,
          center: [139.69167,35.68944] // longitude, latitude
        });

        view.ui.add("info", "bottom-right");

        view
          .when()
          .then(function () {
            var layerList = new LayerList({
              view: view
            });
            // Add widget to the top right corner of the view
            view.ui.add(layerList, "top-right");
            return tileLayer.when();
          })
          .then(function (layer) {
            return view.whenLayerView(layer);
          })
          .then(function (layerView) {
            view.on(["pointer-move", "pointer-down"], eventHandler);

            function eventHandler(event) {
              // only include graphics from tileLayer in the hitTest
              const opts = {
                include: tileLayer
              };
              // the hitTest() checks to see if any graphics from the tileLayer
              // intersect the x, y coordinates of the pointer
              view.hitTest(event, opts).then(getGraphics);
            }

            function getGraphics(response) {
                
                if (response.results.length) {
                  const graphic = response.results[0].graphic;
                  const mapPoint = response.results[0].mapPoint;

                  const attributes = graphic.attributes;

                  const layerName = attributes.layerName;
                  const latitude = mapPoint.latitude;
                  const longitude = mapPoint.longitude;

                  document.getElementById("info").style.visibility = "visible";
                  document.getElementById("name").innerHTML = layerName;
                  document.getElementById("lat").innerHTML =  "緯度: " + latitude;
                  document.getElementById("lon").innerHTML =  "経度: " + longitude;

              } else {
                document.getElementById("info").style.visibility = "hidden";
              }
            }

          });

      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="info">
      <span id="name"></span><br />
      <span id="lat"></span><br />
      <span id="lon"></span>
    </div>
  </body>
</html>

さいごに

前から気になっていた Tippecanoe をやっと触ることができました。すでに多くの方が使用しているのもあり、ネットでも多くの情報がありましたので、無事にベクタータイルを作成することができました。作成したベクタータイルのスタイルがシンプルなので、時間を見つけてもう少し変化(人口別に表示など)を付けるようにできればと思います。