OpenLayers で地理院地図Vector を表示する

## はじめに

昔、Mapbox GL JS が v2 になって Mapbox 社のトークンが必須になった際、ベクトルタイルを消費する別のプラットフォームとして OpenLayers が使えないか、考えたことがありました。

当時、手探りで地理院地図Vector のベクトルタイルを表示させてみたのですが、最近、古いレポジトリ整理で目につきましたので、せっかくですから、Mapbox GL JS との比較もしながら、記事として残してみたいと思います。（ただし、パフォーマンスの比較試験やチューニング等は行っていませんので、あくまで記述方法に限定した記事となります。）

![スクリーンショット 2022-09-12 203931.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842832/2827e22b-2619-5878-5132-692de35c1886.png)

https://github.com/mghs15/gsi-vector-openlayers

### デモサイト

https://mghs15.github.io/gsi-vector-openlayers/

## OpenLayers を Hosted build で利用する

当時見つけた OpenLayers の導入方法の記事は、ほとんど npm での導入方法についてばかりでした。
Node で開発するのが推奨されてはおりますが、ビルド等の環境構築を考えずに、サクッと作って GitHub Pages へ上げたかったので、ブラウザに直接読み込んで、開発することとしました。


公式の [Get the Code](https://openlayers.org/download/) のページを参照して、以下のタグを挿入します。
以下は当時のものですが、現在は、v7.1.0 が最新です。

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.4.3/css/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.4.3/build/ol.js"></script>
```

あとは、チュートリアルに従って、地図を描画するのですが、チュートリアルやドキュメントでは、importで関数等を指定して読み込んでいたので、ブラウザ上で一括で読み込んだ場合に、どのように各関数を指定すればよいか手間取りました。

結局、試行錯誤しつつ、以下のようにすれば動くことが分かりました。

```javascript
const map = new ol.Map({
  target: 'map',
  layers: [
    new ol.layer.VectorTile({
      declutter: true,
      source: new ol.source.VectorTile({
        attributions: '<a href="https://maps.gsi.go.jp/vector/" target="_blank">地理院地図Vector</a>',
        format: new ol.format.MVT({}),
        url:
          'https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf'
      }),
      minZoom: 5,
      maxZoom: 16,
      renderBuffer: 100,
      style: stylingVectorTile //スタイリング用の関数（後述）
    })
  ],
  view: new ol.View({
    center: ol.proj.fromLonLat([139.649019, 35.270359]),
    zoom: 13,
    minZoom: 5,
    maxZoom: 16
  })
});
```

`ol.Map` に

* `target`: コンテナ名
* `layers`: 追加するベクトルタイルの情報
* `view`: 地図の表示に関する情報

を渡すことで、地図の表示ができます。

今回の場合は、ベクトルタイルのなので、`layers` には、`ol.layer.VectorTile` を渡しています。

`ol.layer.VectorTile` には、ベクトルタイルの設定を渡しますが、ここまで来ると、Mapbox GL JS の記述方法と似通ったところが出てきます。

ベクトルタイルの場合、スタイリングが必要なので、そこは別途スタイルを作成する関数（上記コードの`stylingVectorTile`）を作成しました。
次から説明します。

## ベクトルタイルのスタイリング

OpenLayers のベクトルタイルのスタイリングでは、`ol.style.Style` の配列を返すことで実装します。

上記コードの `stylingVectorTile` は、引数として地物データ（以下の `rf`）を受け取れるようです。

```javascript
const stylingVectorTile = (rf, num) => {
  return [
    new ol.style.Style({ ... }),
    new ol.style.Style({ ... }),
    ...
    new ol.style.Style({ ... })
  ]
}
```

以下、個別の `ol.style.Style` の具体的な記述を見ながら、Mapbox GL JS 用のスタイル設定（以下、Mapbox Style と言うことにします。）と比べて、個人的な特筆するべき点を3点挙げます。

### JavaScript の条件式で地物のフィルタができる

たとえば、道路の「通常部」のラインデータの一部を表示させようとすると、以下のような形（一部省略）になります。

```javascript
new ol.style.Style({
  geometry: (f) => {if(
    f.properties_.layer == "road" &&
    f.properties_.ftCode > 2710  &&
    f.properties_.ftCode < 9999  &&
    f.properties_.motorway != 1
  ) return f;},
  stroke: new ol.style.Stroke({
    width: 1,
    lineDash: [2,2]
  })
})
```

このフィルタを Mapbox GL JS で行おうとする場合、以下の通りになります。
JavaScript の方は、普段見慣れているというのもありますが、Mapbox Style では、ネストが多くなると記述が非常に大変になるので、JavaScript で条件を書けるのはありがたく感じます。

```javascript
[ "all", 
  [">", ["get", "ftCode"], 2710],
  ["<", ["get", "ftCode"], 9999],
  ["!=", ["get", "motorway"], 1],
]
```

なお、 Mapbox Style に存在する `source-layer` の指定は、ここでは、if 文によるフィルタの一部となります。
ですので、if 文のフィルタ条件に `source-layer` の指定を含めないこともできるので、Mapbox GL JS ではできない「`source-layer` を超えて一度に同一スタイルを記述する」ということができるようになります。（ただ、そんなデータ設計は、そもそも好ましくないような気がしますが。）


### 複雑なスタイリングを関数に分けることができる

複雑な処理や、ほかのレイヤでも利用するような処理を関数として別の場所にまとめて記載して、`ol.style.Style` 内では、その関数を呼び出して使うということができました。これは便利です。

たとえば、2万5千分の1地形図では、等高線は 10 m おきに引かれますが、50 m おきに計曲線という太い線となります。

このような表現をする際に、事前に線の太さを標高値（`alti`）から判断する関数を準備しておくことができます。

```javascript
const contourLWidth = (f) => {
  const keikyoku = +f.properties_.alti % 50;
  if(keikyoku == 0){
    return 1;
  }else{
    return 0.5;
  }
}
```

そして、このデータを `ol.style.Style` の中で利用することができます。

```javascript
new ol.style.Style({
  geometry: (f) => {if(
    f.properties_.layer == "contour"
  ) return f;},
  stroke: new ol.style.Stroke({
    color: 'rgba(230,230,230,1)',
    width: contourLWidth(rf)
  }),
  zIndex: 10
})
```

### 地物の重なりを制御しやすい

OpenLayers では、地物の重なり順を `zIndex` で指定できるので、少ない記述量で、階層条件を表現できました。

たとえば、道路データは、国道・都道府県道・市町村道、幅員、階層順……と条件分岐が多く、スタイルのレイヤ数が非常に大きくなりがちです。
しかし、`zIndex` を指定することで、たとえば、以下のように `lvOrder` が大きいほど、`rdCtg` の値が小さいほど、描画順を上にするとシンプルに記述できます。

```javascript
new ol.style.Style({
  geometry: (f) => {if(
    f.properties_.layer == "road" &&
    f.properties_.ftCode > 2710  &&
    f.properties_.ftCode < 9999  &&
    f.properties_.motorway != 1
  ) return f;},
  stroke: new ol.style.Stroke({
    color: roadLColor(rf), //roadLColor()は、道路の色分けを行う関数
    width: 1,
    lineDash: [2,2]
  }),
  zIndex: 1000 + checkProp(rf.properties_.lvOrder) + 10 - rf.properties_.rdCtg 
})
```

なお、ここの `checkProp()` は、lvOrderが存在しないときの例外処理を行う関数です。

```javascript
const checkProp = (prop, d=0) => {
  if(prop){
    return prop;
  }else{
    return d; 
  }
}
```

## Sprite の読み込み

Mapbox GL JS では、Sprite 形式の画像データをアイコンとして利用できるようにしてくれますが、OpenLayers では、そのような機能をぱっと見つけられなかったので自作してみました。

Mapbox Style で使う Sprite 画像には、画像本体と、画像の位置とアイコン名の対応を示した JSON ファイルがセットであります。
ですので、まず、以下の通り、JSON を読み込んで、その情報と、Sprite 画像を返す関数を作成しました。

```javascript
const loadJSON = function(path){
  
  let data = "";
  
  const xhr = new XMLHttpRequest();
  xhr.open('GET', path, false);
  xhr.send(null);
  
  if(xhr.status == 200 || xhr.status == 304){
    data = JSON.parse(xhr.responseText);
  }
  
  return data;
}

const spritejson = loadJSON('https://maps.gsi.go.jp/vector/sprite/std.json');

const iconStyleOption = (iconname) => {

  if(spritejson[iconname]){
    const info = spritejson[iconname];
    const style =  {
      src: 'https://maps.gsi.go.jp/vector/sprite/std.png',
      size: [info.width, info.height],
      offset: [info.x, info.y],
      scale: (info.y < 770) ? 0.5 : 0.3 //ここでは、画像内の位置によって、大きさを変更
    };
    
    return style;
  }else{
    return null;
  }
  
}
```

実際に描画するときは、コードからアイコン名を指定するので、以下のような対応させるための関数を作成しました。
（大量にあるので、switch case を利用したのは浅はかだったかもしれません。）

```javascript
const showIcon = (code) => {
  if(!code) return null;
  let iconname = "unknown";
  switch(+code){
    case 1401:
      iconname = "都道府県所在地-20"; break;
    case 1402:
      iconname = "市役所・東京都の区役所-20"; break;
    //（中略）
    default:
      iconname = "指示点"; break;
  }
  return new ol.style.Icon(iconStyleOption(iconname));
}
```

最後に、`ol.style.Style` 内で呼び出して、画面上へ表示させます。以下は例です。

```javascript
new ol.style.Style({
  geometry: (f) => {if(
    f.properties_.layer == "symbol" &&
    ![1401,1402,1403,7101,7102,7103].includes(rf.properties_.ftCode)
  ) return f;},
  image: showIcon(rf.properties_.ftCode),
  zIndex: 100002
})
```

## おわりに

OpenLayers には、Mapbox Style の JSON ファイルを読み込む機能もあるようです（`ol-mapbox-style`）が、上記の Hosted build には含まれていないようです。

また、とりあえず表示させることを優先して何も考えずに、`ol.style.Style` の `geometry` の中で地物のフィルタリングを行っています。しかし、スタイリング用の関数 `stylingVectorTile` 内で地物がどのように描画されるべきが絞り込んで、1つの `ol.style.Style` を返した方が良いような気もしています（確証はないです）。

突貫工事で作ったあと、需要が高まらずに放置していましたが、スタイルの書き方は気に入っています。スタイリングした人が、どんな思いで地図を描画したのか、わかりやすい気がします。

パフォーマンスが優れているのかどうかは微妙ですが、上述の通り、私の記述方法が悪いだけかもしれませんし、そもそも非推奨の Hosted build を使っています。

まず、`ol-mapbox-style` で、Mapbox Style をどのように読んで、どのようにレイヤを追加しているか等、OpenLayers でのスタイリングの正解を研究してもよいかもしれません。

## 参考文献

https://openlayers.org/

https://github.com/mapbox/mapbox-gl-js

https://maps.gsi.go.jp/vector/