はじめに
昔、Mapbox GL JS が v2 になって Mapbox 社のトークンが必須になった際、ベクトルタイルを消費する別のプラットフォームとして OpenLayers が使えないか、考えたことがありました。
当時、手探りで地理院地図Vector のベクトルタイルを表示させてみたのですが、最近、古いレポジトリ整理で目につきましたので、せっかくですから、Mapbox GL JS との比較もしながら、記事として残してみたいと思います。(ただし、パフォーマンスの比較試験やチューニング等は行っていませんので、あくまで記述方法に限定した記事となります。)
デモサイト
OpenLayers を Hosted build で利用する
当時見つけた OpenLayers の導入方法の記事は、ほとんど npm での導入方法についてばかりでした。
Node で開発するのが推奨されてはおりますが、ビルド等の環境構築を考えずに、サクッと作って GitHub Pages へ上げたかったので、ブラウザに直接読み込んで、開発することとしました。
公式の Get the Code のページを参照して、以下のタグを挿入します。
以下は当時のものですが、現在は、v7.1.0 が最新です。
<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で関数等を指定して読み込んでいたので、ブラウザ上で一括で読み込んだ場合に、どのように各関数を指定すればよいか手間取りました。
結局、試行錯誤しつつ、以下のようにすれば動くことが分かりました。
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
)を受け取れるようです。
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 の条件式で地物のフィルタができる
たとえば、道路の「通常部」のラインデータの一部を表示させようとすると、以下のような形(一部省略)になります。
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 で条件を書けるのはありがたく感じます。
[ "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
)から判断する関数を準備しておくことができます。
const contourLWidth = (f) => {
const keikyoku = +f.properties_.alti % 50;
if(keikyoku == 0){
return 1;
}else{
return 0.5;
}
}
そして、このデータを ol.style.Style
の中で利用することができます。
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
の値が小さいほど、描画順を上にするとシンプルに記述できます。
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が存在しないときの例外処理を行う関数です。
const checkProp = (prop, d=0) => {
if(prop){
return prop;
}else{
return d;
}
}
Sprite の読み込み
Mapbox GL JS では、Sprite 形式の画像データをアイコンとして利用できるようにしてくれますが、OpenLayers では、そのような機能をぱっと見つけられなかったので自作してみました。
Mapbox Style で使う Sprite 画像には、画像本体と、画像の位置とアイコン名の対応を示した JSON ファイルがセットであります。
ですので、まず、以下の通り、JSON を読み込んで、その情報と、Sprite 画像を返す関数を作成しました。
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 を利用したのは浅はかだったかもしれません。)
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
内で呼び出して、画面上へ表示させます。以下は例です。
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 でのスタイリングの正解を研究してもよいかもしれません。
参考文献