MapLibre でタイルを縮小表示させて情報量を増やす


## はじめに

ある地域を俯瞰しようとした場合、市町村界や交通ネットワーク（主要な道路、鉄道・駅、港湾・空港等）、河川・地形等が１枚にまとまって掲載された地図があると便利です。防災・災害対応等で利用する際の背景地図としても、このような情報は有用だと思います。

[地理院地図Vector](https://maps.gsi.go.jp/vector/)/[最適化ベクトルタイル](https://github.com/gsi-cyberjapan/optimal_bvmap)では、ズームレベル（ZL）11まで拡大すると上記のような情報が表示されてきます。一方、私の経験上ですが、複数の市町村を含む地域を24インチの画面に表示したり、A3用紙に印刷しようとすると、地理院地図Vector/最適化ベクトルタイルで ZL10 がちょうどよいと感じることが多いです。たとえば、能登半島地震関連で、能登半島全体をこのサイズに収めたいと思うと、ZL10にする必要があります。

しかしながら、地理院地図Vector/最適化ベクトルタイルで ZL10となると、市町村界や駅名等が表示されません。このままだと、その地域がどこの市町村に属するのか等が分かりにくく、情報量として物足りないと感じることが多いです。

そのため、地理院地図や地理院地図Vector/最適化ベクトルタイル等、地理院のウェブ地図を利用している際、情報量と表示できる領域の観点から、もう１つ上のズームレベルのタイルを縮小表示したくなることが結構あります。

:::note
地理院地図（Leaflet ベース）と地理院地図Vector/最適化ベクトルタイル（Mapbox GL JS/MapLibre GL JS ベース）では、1つのタイルが画面上に表示されるサイズが異なっています。そのため、地理院地図と地理院地図Vector/最適化ベクトルタイルでは、情報量として ZL が１つずれています。この記事では、地理院地図Vector/最適化ベクトルタイルの ZL で説明します。
:::

なお、Leaflet では、[`minNativeZoom`](https://leafletjs.com/reference.html#gridlayer-minnativezoom) という、設定した ZL 以下では、設定された ZL のタイルを縮小して表示するという小さいズームレベル側へのオーバーズーミング機能があります。一方、Mapbox GL JS/MapLibre GL JS では、このような機能がありません。（逆に、１つ下の ZL のタイルを拡大して表示するオーバーズーミングは、Leaflet、Mapbox GL JS/MapLibre GL JS のいずれも可能です。）

そのため、今回の記事では、MapLibre GL JS と最適化ベクトルタイルを用いて、１つ上の ZL を用いて同じ地図範囲を描画させることができないかと取り組んでみました。要は、小さい ZL の方へオーバーズーミングして地図の情報量を上げる方法のご紹介となります。

方針としては、**ブラウザの縮小表示**とベクトルタイルの利点である**地図デザイン（スタイル設定）の変更**によって、疑似的に表現してみたいと思います。

## 最適化ベクトルタイルのZL9～12における情報量

参考に、今回注目する地物について、最適化ベクトルタイルの ZL9～12のタイルへの格納状況を整理しておきます。どのくらいの領域を１枚にカバーしたいか次第ですが、とりあえずZL11の情報量が欲しいところです。

:::note warn 
この表は、自分が観察して作成したもので、国土地理院の正式なドキュメントを参照したわけではないのでご注意ください。
:::

||ZL9|ZL10|ZL11|ZL12|
|:-:|:-:|:-:|:-:|:-:|
|市町村界|×|×|○|○|
|高速道路|○|○|○|○|
|国道|○|○|○|○|
|県道|×|○|○|○|
|その他主要な道路|×|×|×|○|
|IC名|×|○|○|○|
|鉄道|○|○|○|○|
|駅名|×|×|○|○|
|河川名|×|×|○|○|
|空港名|記号のみ|記号のみ|○|○|

## ブラウザの機能で縮小表示

いきなり裏技的なテクニックとなり恐縮ですが、ブラウザの拡大・縮小機能（ショットカットを用いて Ctrl＋マウスホイールで調整できます）を使って50％縮小表示することで、ちょうど ZL が１個分上のタイルを用いて目的の領域を表示させることができます。

![ZL11(縮小)とZL10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/fe415da1-90a7-5d57-cbfd-bb3982744046.png)


たとえば、石川県災害時交通マネジメント会議（能登半島地震）の資料（[PDF資料7ページ目](https://www.hrr.mlit.go.jp/press/2023/1/240112dourobukaigi.pdf#page=7)）では、地理院地図Vector で ZL11（注記や等高線を非表示にしたもの）を縮小表示したと思われるものが利用されています。

![国交省資料.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/1babf29d-d8ac-7063-386b-5bbf7289a586.png)


:::note warn
ブラウザの拡大縮小機能を利用しないで、サイト側の設定として同じような効果を実現するには、CSS の [`scale`](https://developer.mozilla.org/ja/docs/Web/CSS/scale) 等をうまく使う必要がありそうです。
:::

:::note warn
同じ領域を描画するのに、ZL が上のタイルを用いる場合、読み込まれるタイル数はその分増加しますので、通信量やサーバへの負荷にはご注意ください。
:::

しかしながら、当然縮小しているので、文字サイズや道路等の線の太さも小さくなり、見づらくなってしまいます。そのため、今度はベクトルタイルのスタイル設定（Mapbox GL JS/MapLibre GL JS の style.json）へ手を入れることにしました。

## 全スタイルの線幅・文字サイズ等の設定値を大きくする

文字やアイコンのサイズ、道路等の線幅等を大きく表示するためには、単純にこれらのスタイル設定について、地図を縮小した分だけ拡大すればよいと考えられます。

Mapbox GL JS/MapLibre GL JS の スタイル設定では、`"*"` 演算子を用いることで、以下のように a × b の掛け算の処理を行うことができます。

```
["*", a, b]
```

すなわち、もともと設定されているスタイルの表現に対して、上記の `"*"` を用いて２をかけることができれば、50％縮小しても従来のサイズと同じ大きさで表示をすることが可能という発想になります。

しかしながら、そこまで単純にいきませんでした。問題は、`["zoom"]` という表現です。この `["zoom"]` は、`"interpolate"` や `"step"` の表現とともに使われ、ZL に応じて、数値を変化させるのに利用しますが、スタイル設定の中で、一番上の階層に設定された`"interplatate"` や `"step"` の表現の中でしか利用することができません。以下は、MapLibre GL JS の [Style Spec](https://maplibre.org/maplibre-style-spec/expressions/#zoom) での説明です。

>  Note that in style layout and paint properties, ["zoom"] may only appear as the input to a top-level "step" or "interpolate" expression.

そこで、既存の表現を単に `["*", a, b]` の形に置き換えるのではなく、まずはスタイル設定中に `"zoom"` が使われていないかを確認し、使われていなかったら `["*", a, b]` の形に置き換え、使われていた場合、`"interpolate"` や `"step"` の中で記述されている出力設定部分（stop_output）を抽出して `["*", a, b]` の形に置き換えるという処理を行いました。

とりあえず、最適化ベクトルタイルのサンプルスタイルで使われている表現については、上記の処理で対応することができました。コード例は以下の通りです。

```JavaScript
// "zoom" が使われている場合用の関数
// スタイル設定（expression）中の "interpolate" や "step" の中で
// 使われている stop_output を抽出して、["*", a, b] の形に置き換える
const scaleInterpolateStyle = (expression, arr=[], scale) => {
  if(Array.isArray(expression) && expression[0] == "interpolate"){
    for(let i = 3; i < expression.length; i = i + 2){
      expression[i+1] = ["*", scale, expression[i+1]];
    }
    return expression;
  }else if(Array.isArray(expression) && expression[0] == "step"){
    for(let i = 2; i < expression.length; i = i + 2){
      expression[i] = ["*", scale, expression[i]];
    }
    return expression;
  }else if(Array.isArray(expression)){
    expression.forEach( expressionElement => {
      arr.push(scaleInterpolateStyle(expressionElement, [], scale));
    });
    return arr;
  }else{
    return expression;
  }
}

// スタイル設定（expression）中に特定の演算子（keyword、"zoom" 等）が
// 使われていないかを確認する関数
const isKeywordUsed = (expression, keyword) => {
  let _count = 0;
  if(Array.isArray(expression)){
    expression.forEach( expressionElement => {
      _count += isKeywordUsed(expressionElement, keyword);
    });
  }else{
    if(expression == keyword) _count += 1;
  }
  return _count;
}

// スタイル設定（expression）中に、"zoom" が使われていないかを確認し処理を分岐
switchConvertOperation = (expression, scale) => {
  const count = isKeywordUsed(expression, "zoom");
  if(count > 0){
    // "zoom" が使われている場合
    return scaleInterpolateStyle(expression, [], scale);
  }else{
    // "zoom" が使われていない場合
    return ["*", scale, expression];
  }
}
```

以下は、文字サイズや線幅を拡大したもの・していないものの違いです。

![ZL11(縮小+スタイル変更)とZL11(縮小のみ).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/1caa382d-274f-045d-536f-990e6250cdd8.png)

## 結果

ブラウザの縮小表示と地図デザイン（スタイル設定）の変更により、以下のように情報量を増やしつつ、ある程度見やすい地図を表示することができるようになりました。

![ZL11(縮小＋スタイル)とZL10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/a45c8ae7-e8f7-c36a-89ba-35b6ff50d498.png)

https://mghs15.github.io/map-style-for-scaled-viewing/#11/37.1439/136.8506

等高線が目立つので、少し抑えたスタイルも設定してみました。

![スタイル追加修正版.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/a0fd1a41-62f8-cc31-ebaa-c8a3a9be53f1.png)

https://mghs15.github.io/map-style-for-scaled-viewing/?style=basicl#11/37.1439/136.8506

スタイルについてはまだまだ改良の余地があると考えており、例えば以下のような観点で工夫ができそうです。

* 衝突回避処理により、多くの注記が消えてしまうので、地図の目的に応じて、注記の優先度や衝突回避処理の有無を各注記毎に調整する価値がありそう
* 東京などの都市部では、地方に比べて地物の密度が高く、ZL が１つ上のタイルの情報量では、かなりうるさい地図となってしまう
* 単純に線幅を大きくすると、破線間隔も大きくなって破線の表現が破綻する場合がある
* 道路は、大縮尺になると実際の幅に合わせた線幅が設定されているため、これを太くしてしまうと、大縮尺での見た目が劣化する

最適化ベクトルタイルのデータ側への要望として、欲を言えば、もう少し以下のような情報があると役立ちそうかと思いました。（以下の情報は、ZL を少し変えるだけでは実現できなかったり、そもそも地理院地図Vector/最適化ベクトルタイルの中に存在しない情報があります。）

* [20万分の１地勢図](https://geolib.gsi.go.jp/node/2421)のように、市街地が分かるとよい
* 都道府県道の番号も国道のように表示したい
* ZL11は岬や島の名前が主だが、市町村名以外の主要な地名もわかるとよい
* 鉄道駅は、主要駅かその他の駅かを区別できるとメリハリがつく

### レポジトリ

https://github.com/mghs15/map-style-for-scaled-viewing

## おわりに

MapLibre GL JS と最適化ベクトルタイルを用いて、上の ZL のタイルを用いて同じ地図範囲を描画させるオーバーズーミングに取り組んでみました。

今回は、ZL10前後の小～中縮尺の地図を念頭に欲しい情報（地物）を検討していたのですが、改めて考えてみると、紙の20万分の１地勢図にはそのような情報がよくまとまっているのではないかと、その良さを見直しました。

地理院地図・地理院地図Vector 含めたウェブ地図は、紙の地図と比較して、シームレスに動かせ、かつ地図データのアップデート頻度も高いです。最新の地図を使って、自分の目的の領域を切り出せるのは便利ですし、ベクトルタイルの場合は、目的に応じて地図のデザインを変更することもできます。タイルのデータサイズ増加に対する工夫は必要かもしれませんが、紙地図（地勢図）のような情報量がウェブ地図のZL10前後のタイルへも反映されると使い方が大きく広がるのではないかと思います。