「道路を方角ごとに塗り分けると、その街のでき方がわかる :: デイリーポータルZ」 を MapLibre GL と地理院地図Vectorで

# はじめに

以前の記事 [「道路を方角ごとに塗り分けると、その街のでき方がわかる :: デイリーポータルZ」 を Leaflet と地理院地図Vectorで](https://qiita.com/frogcat/items/3d795c5cbe026c372bf4) は [道路を方角ごとに塗り分けると、その街のでき方がわかる :: デイリーポータルZ ](https://dailyportalz.jp/kiji/douro-hougaku-machi-no-dekikata) を Leaflet と地理院地図 Vector で実装してみました。

この記事では [MapLibre GL](https://github.com/maplibre/maplibre-gl-js) と [地理院地図 Vector](https://github.com/gsi-cyberjapan/gsimaps-vector-experiment) で実装してみます。

# できあがり

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82494/8800c42b-f3a3-bce6-7477-abaa80ea543b.png)](https://frogcat.github.io/douro-hougaku-machi-no-dekikata/maplibre.html#13.42/35.68138/139.79123/0/52)

ワーキングデモは上の画像をクリックするか、こちらのリンクからどうぞ。

<https://frogcat.github.io/douro-hougaku-machi-no-dekikata/maplibre.html#13.42/35.68138/139.79123/0/52>

ソースコードはこちらからどうぞ。ライセンスは MIT を設定しています。

<https://github.com/frogcat/douro-hougaku-machi-no-dekikata>


# 解説

以前の記事では mapbox-gl-js v1.x (maplibre-gl-js v1.x) では道路ポリラインに対して角度計算・着色をする適当な方法がなくて難しい、という結論でした。
その後、とある試作で maplibre-gl-js の [Map#queryRendererdFeatures](https://maplibre.org/maplibre-gl-js-docs/api/map/#map#queryrenderedfeatures) を使うことがあって、これ応用すればできるかも、と思って書いてみたのがこの記事です。

例によってソースコードは 130行程度なのでとりあえず貼っておきますね。

```index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>douro-hougaku-machi-no-dekikata-gl</title>
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@1.14.0/dist/maplibre-gl.css" />
  <script src="https://unpkg.com/maplibre-gl@1.14.0/dist/maplibre-gl.js"></script>
  <script src="https://unpkg.com/@turf/turf/turf.min.js"></script>
</head>

<body>
  <div id="map" style="position:absolute;top:0;left:0;bottom:0;right:0;"></div>
  <script>
    const style = {
      "version": 8,
      "glyphs": "https://maps.gsi.go.jp/xyz/noto-jp/{fontstack}/{range}.pbf",
      "sources": {
        "pale": {
          "type": "raster",
          "tiles": [
            "https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png"
          ],
          "tileSize": 256,
          "minzoom": 5,
          "maxzoom": 18,
          "attribution": "<a href='https://maps.gsi.go.jp/development/ichiran.html'>地理院タイル(淡色地図)</a>"
        },
        "experimental_bvmap": {
          "type": "vector",
          "tiles": [
            "https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf"
          ],
          "minzoom": 5,
          "maxzoom": 16,
          "attribution": "<a href='https://github.com/gsi-cyberjapan/gsimaps-vector-experiment'>地理院地図Vector</a>"
        },
        "overlay": {
          "type": "geojson",
          "data": {
            "type": "FeatureCollection",
            "features": []
          }
        }
      },
      "layers": [{
          "id": "pale",
          "type": "raster",
          "source": "pale",
          "minzoom": 2,
          "maxzoom": 18
        },
        {
          "id": "road",
          "type": "line",
          "source": "experimental_bvmap",
          "source-layer": "road",
          "filter": ["==", ["geometry-type"], "LineString"],
          "minzoom": 5,
          "maxzoom": 18,
          "paint": {
            "line-color": "#000000",
            "line-width": 1
          },
          "layout": {
            "line-cap": "round",
            "line-join": "round"
          }
        },
        {
          "id": "overlay",
          "type": "line",
          "source": "overlay",
          "paint": {
            "line-color": ["get", "color"],
            "line-width": ["get", "width"]
          },
          "layout": {
            "line-cap": "round",
            "line-join": "round"
          }
        }
      ]
    };

    const map = new maplibregl.Map({
      container: "map",
      center: [139.8508, 35.416],
      zoom: 9,
      pitch: 60,
      bearing: -30,
      hash: true,
      style: style
    });

    map.on("idle", function() {
      const features = [];
      map.queryRenderedFeatures({
        layers: ["road"]
      }).forEach(f => {
        const g = f.geometry;
        (g.type === "LineString" ? [g.coordinates] : g.coordinates).forEach(c => {
          const head = c[0];
          const tail = c[c.length - 1];
          let bearing = turf.bearing(head, tail);
          if (bearing < 0) bearing += 180;
          if (bearing > 90) bearing -= 90;
          features.push({
            "type": "Feature",
            "properties": {
              "color": `hsl(${45 - bearing*4},90%,45%)`,
              "width": [4, 4, 6, 8, 10, 4, 4][f.properties.rnkWidth]
            },
            "geometry": {
              "type": "LineString",
              "coordinates": c
            }
          });
        });
      });
      map.getSource("overlay").setData({
        "type": "FeatureCollection",
        "features": features
      });
    });
  </script>
</body>

</html>

```

## スタイル構成

ざっくりとこのような構成になっています。

```yaml
sources:
  pale:  # 地理院淡色地図 (ラスタ)
  experimental_bvmap : #地理院地図VECTOR (ベクトルタイル)
  overlay: #空の GeoJSON
layers:
   - 背景画像として pale を表示するレイヤー 
   - experimental_bvmap のうち道路だけを薄灰色で表示する仮表示するレイヤー
   - overlay にセットされた GeoJSON を描画するレイヤー (色、線幅は GeoJSON 内に指定されているものとする)
```

## 処理の流れ

1. `const map = new maplibregl.Map(...)` によって地図を初期化
2. `map.on('idle',...)` によって、地図の描画完了を監視
3. 地図の描画が完了したら `map.queryRenderedFeatures()` によって描画されたものを GeoJSON として取得
4. 取得した GeoJSON (実体は道路ポリライン) に対して角度を計算して、GeoJSON properties に color, weight をセット
5. `map.getSource("overlay",geoJsonFeatureCollection)` によって GeoJSON を更新、描画させる

## その他

- あるベクトルタイルについて、 **LineString だけ表示させたい** ときのフィルタは `"filter": ["==", ["geometry-type"], "LineString"]`。定義は[こちら](https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#geometry-type)
- 上で "LineString" しか指定していないのに、queryRenderedFeatures の返すオブジェクトには MultiLineString が含まれていることがあるので注意。`(g.type === "LineString" ? [g.coordinates] : g.coordinates).forEach(...)` で LineString のジオメトリは配列で囲み、 MultiLineString 相当にして処理させている

# まとめ

描画されたベクトルタイルを queryRenderedFeatures を使って取得、GeoJSON として加工した上で再度地図上に描画させることで、style だけでは表現しづらいスタイリングを実現する手法を紹介しました。

実際に動作させてみると、地図上にモノクロの道路網が表示されて、ちょっと遅れて上にカラーの道路網が表示されるのがわかるかと思います。表示させるタイル量、描画オブジェクト数などいろいろな要因によってこのタイムラグは増減すると思われますが、そういったラグを許容できる要件であればこのような手法の使いみちがあるかもしれません。