🙄

PMTilesに複数のレイヤを格納して、maplibre-gl-jsで重ねて表示する

2024/07/22に公開

こんにちは

Fusicの石橋です

https://zenn.dev/fusic/articles/0872bc742ad163

前回の記事でgeojsonからPMTilesを作成してS3に設置
その後、maplibre-gl-jsから可視化するところまで実装しました

今回は、複数のレイヤ(複数のgeojsonファイル)をPMTilesに格納し
maplibre-gl-js側でそれぞれを重ねて表示する実装をしていきます

データ準備

前回は行政区画だけでしたが、今回はそれに追加してハザードマップを追加してみます

ハザードマップは全国データは無く、各都道府県レベルのデータでした

今回は福岡県のデータを使います

展開した後のデータを見てみると、各市レベルでファイルになっていることが分かりますね
今回はとりあえず一つのファイルにまとめちゃいます

こんな感じでパッと結合してfukuoka-hazard-map.geojsonというファイルにしちゃいます
(ハザードマップってかなり細かいレベルで作られてて結構大きなデータ量になるんですね。ファイル構成の理由が分かった気がする)

import json
import pandas as pd
import geopandas as gpd
from pathlib import Path

# 入力ディレクトリと出力ファイルパスを設定
input_directory = Path("../data/hazard-map/A31-21_40_GML/03_浸水継続時間/GEOJSON")
output_file = Path("fukuoka-hazard-map.geojson")

# GeoJSONファイルのリストを取得
geojson_files = list(input_directory.glob("*.geojson"))

# すべてのGeoJSONファイルを読み込んでリストに追加
gdfs = [gpd.read_file(geojson_file) for geojson_file in geojson_files]

# リスト内のすべてのGeoDataFrameを結合
merged_gdf = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True))

# 結合したデータを新しいGeoJSONファイルとして保存
merged_gdf.to_file(output_file, driver="GeoJSON")

print(f"結合したファイルを {output_file} に保存しました。")

PMTilesの作成

前回同様geojsonからPMTilesを作成します

tippecanoe --no-tile-compression -z15 -Z2 -o output.pmtiles gyousei-kukaku/japan/N03-23_230101.geojson hazard-map/fukuoka-hazard-map.geojson
  • 上限のズームレベルを前回よりちょっと上げました
  • ファイルを2つ指定している

というのが前回のコマンドとの差分になるかと

このように複数ファイルの入力を受け付けてくれます

オプションコマンドでレイヤ名を指定することもできるそうですが、特に何も指定しなければファイル名がそのままレイヤ名になるそうです

また、複数ファイルを一つのレイヤにまとめることとかもできるそうです

作成したpmtilesのファイルは前回同様S3に設置しておきます

js側での実装

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 = "[PMTIlesのS3パス]";

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>'
          },
          "fukuoka-hazardmap": {
            "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",
            },
          },
          {
            "id": "hazard-map",
            "source": "fukuoka-hazardmap",
            "source-layer": "fukuokahazardmap",
            "type": "fill",
            "paint": {
              "fill-color": "#ff0000",
              "fill-opacity": 0.4,
              "fill-outline-color": "#0000ff",
            },
          },
        ]
      }
    });
  }, [lat, lon, zoom]);

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

export default MapComponent;

こんな感じで表示できました!

ただ結構重いですねw

ハザードマップはプロパティにレベルが格納されているので、そのレベルごとに前処理をして分けておくといいかもしれませんね

(浸水リスク、低、中、高とかのそれぞれのレイヤを作っておくと表示非表示とかが簡単そうだし)

とまあ、こんな感じでできました!

Fusic 技術ブログ

Discussion