🗺️

PMTilesのデータをS3に設置して、ブラウザから表示する

2024/07/18に公開

こんにちは
Fusicの石橋です

最近、GIS関連のデータを扱うことが増えてきてPMTilesという面白いデータを知りました

https://docs.protomaps.com/

なんとタイルサーバ無しで(S3などのクラウドストレージをタイルサーバにできるって表現の方が正しいかも)ラスタ、ベクタデータを配信できるとのことでした

フロントエンド側では少し特殊な実装が必要っぽいですが、プラグインなども整備されてきているので手軽に使えるようになってきている印象もあります

今回は、「国土数値情報ダウンロードサイト」
の行政区画の情報(geojson)をPMTilesに変換し、S3に設置してから、JSで表示するところまでを書いていきます

データの準備

国土数値情報ダウンロードサイトからデータをダウンロードします

任意の都道府県だけでもいいでしょうが、部分リクエストをしていい感じにできることを実感するためには全国のデータをダウンロードした方がおもしろいので今回は全国版のデータをダウンロードします

https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03-v3_1.html

zipファイルの中身を確認するとこのようになっています

今回はこの中でgeojsonファイルを使います

geojsonをPMTilesに変換

変換ツールとして今回はtippecanoeを使います

インストール方法はMIERUNEの井口さんが分かりやすい記事を書いてくださっていたのでこちらを参考にインストールしていただければと(私もこの方法でWSLにインストールした)

https://qiita.com/Kanahiro/items/ceeb20c158b4c70b62b6

tippecanoeが使えるようになったら以下のコマンドでpmtilesを作ります

tippecanoe --no-tile-compression -z12 -Z2 -o output.pmtiles N03-23_230101.geojson

tippecanoeのコマンドでややこしいのが

小文字のzが最大ズームレベル
大文字のXが最小ズームレベル

らしいですw

作成したPMTilesをS3に設置する

こちらのドキュメントに書いてある通りです

https://docs.protomaps.com/pmtiles/cloud-storage

今回はS3を使うので、S3の項目を参考にしながら

  • バケットポリシー
  • CORS Config

の設定します

バケットの作成ができたら先ほど作成したPMTilesのデータを設置しましょう

js側の実装

React + typescriptのテンプレートを使っています

map関連のライブラリで

がありますが、PMTilesを読み込もうと思うと

が見つかりました。mapbox-gl-jsでmapbox-pmtilesを使おうと思うとなぜかnode_module配下のコードでエラーが発生してうまく使えませんでした

何かしらうまくやる方法があるのかもしれないですが、まだ解決できていないので詳しい方がいたら教えて欲しいです

maplibre-gl-js + pmtilesのパターンでは成功したので、今回はこちらの構成で表示までやります

App.tsx

import './App.css';
import MapComponent from './MapComponent'


function App() {
  return (
    <div className="App">
      <MapComponent />
    </div>
  );
}

export default App;

MapComponent.tsx

import React, { useRef, useEffect, useState } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { Protocol, PMTiles } from 'pmtiles';

const PMTILES_URL = ""; // アップロードしたS3のURL


const MapComponent: React.FC<{}> = () => {
  const mapContainer = useRef(null);
  const map = useRef<any>(null); 
  const [lat, setLat] = useState(33.5676);
  const [lon, setLon] = useState(130.4102);
  const [zoom, setZoom] = useState(9);

  useEffect(() => {
    if (map.current) return; // initialize map only once

    const protocol = new Protocol();
    maplibregl.addProtocol("pmtiles", protocol.tile);
    const p = new PMTiles(PMTILES_URL);
    protocol.add(p);

    map.current = new maplibregl.Map({
      container: mapContainer.current!,
      center: [lon, lat],
      zoom: zoom,
      style: {
        version: 8,
        sources: {
          "osm": {
            "type": "raster",
            "tiles": ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
            "tileSize": 256,
            "attribution": '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors'
          },
          "gyousei-kukaku": {
            "type": "vector",
            "url": "pmtiles://" + PMTILES_URL,
            "attribution": '© <a href="https://openstreetmap.org">OpenStreetMap</a>'
          }
        },
        layers: [
          {
            "id": "osm-tiles",
            "type": "raster",
            "source": "osm",
            "minzoom": 0,
            "maxzoom": 19,
          },
          {
            "id": "sample-layer",
            "source": "gyousei-kukaku",
            "source-layer": "N0323_230101",
            "type": "fill",
            "paint": {
              "fill-color": "#00ffff",
              "fill-opacity": 0.4,
              "fill-outline-color": "#ff0000",
            }
          },
        ]
      }
    });
  }, [lat, lon, zoom]);

  return (
    <div className="App">
      <div ref={mapContainer} className="map-container" style={{ height: '100vh', width: '100%' }} />
    </div>
  );
};

export default MapComponent;

こんな感じにしてあげると
いい感じに地図の上に行政区画が表示されました!

開発者コンソールでネットワークを監視しながら地図をぐりぐり動かしてみると、output.pmtilesからデータを取ってきているのがわかりますね

ステータスコードをみてみると普段あまり見かけない

Status Code: 206 Partial Content

とあります

これはPMTilesからデータを取得する際にHTTP range requestsという手法をつかうからだそうです
詳しく知りたい方はこちらを見ると良いかと

https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests

おさらい

PMTilesをつかうと、タイルサーバをS3などのクラウドストレージで代用することができる
→ サーバ費用をめっちゃ抑えられる可能性があるかも

PMTilesの中身を更新するのはちょっと難しそう(おそらくgeojson側を更新して、再度PMTilesを作り直してS3に設置しなおすって感じになるのかな?)

更新頻度が高いデータの場合は運用の仕方を事前にちゃんと考えて導入するかしないかは検討しないと行けなさそう

とはいえ、大量のデータを1つのファイルにまとめて、そのファイルの1部分だけを取得していい感じに表示できるなんてすごいなぁ

Fusic 技術ブログ

Discussion