2019-05-25: ベクトルタイルが完成したので、記事を更新しました。
_ _ _ _ __ __ _
| | | | \ | | \ \ / /__ ___| |_ ___ _ __
| | | | \| | \ \ / / _ \/ __| __/ _ \| '__|
| |_| | |\ | \ V / __/ (__| || (_) | |
\___/|_| \_| \_/ \___|\___|\__\___/|_|
_____ _ _ _____ _ _ _ _
|_ _(_) | ___ |_ _|__ ___ | | | _(_) |_
| | | | |/ _ \ | |/ _ \ / _ \| | |/ / | __|
| | | | | __/ | | (_) | (_) | | <| | |_
|_| |_|_|\___| |_|\___/ \___/|_|_|\_\_|\__|
Make the technology the easy part.
動機
情報源は地域におけるデータの活用促進に向けて~政府のオープンデータ政策~なのですが、 IT Dashboard では次の GeoJSON データが使われているようですね。
ここが出典となるサイトであり、ここが利用規約です。「出典:IT Dashboard (https://www.itdashboard.go.jp) 」で自由に使えるはず。
これ、サイズが随分大きいので、ベクトルタイルにしてみたいと思っています。
(実験1) ogr2ogr (2.4.0 以降) で GeoJSON Text Sequence に変換
国連ベクトルタイルツールキット方式で変換する場合、ソースデータは GeoJSON Text Sequence にまず変換します。データをストリームに変換することで、データをマージするとかしないとかった悩みから解放されます。2.4.0 以降の ogr2ogr を使えば、こういったコマンド一発で変換することができます。
ogr2ogr prefecture.geojsons https://www.itdashboard.go.jp/js/data/prefectures.geojson
(実験2) 標準出力にリダイレクト
国連ベクトルタイルツールキット方式のオンザフライスキーマ変換をするには、modify.js を噛ませなければなりません。Tippecanoe の --prefilter はレイヤ名の変更ができませんし、ogr2ogr にはそのようなフィルタ機能はないようですから、フィルタは Node.js で噛ませることにして、ogr2ogr と Tippecanoe は Node.js から spawn して pipe でつなぐことにします。
手始めに、実験1の結果をファイルではなくて標準出力にリダイレクトできることを確かめます。
ogr2ogr -f GeoJSONSeq -lco RS=YES /vsistdout/ https://www.itdashboard.go.jp/js/data/prefectures.geojson
(検討) 成果物の形式
成果物はプレインな未圧縮のベクトルタイルとし、gh-pages でホストすることにしましょう。
Tippecanoe の README.md によると、ベクトルタイルをファイルシステムに書き出すためには --output-to-directory=directory
オプションを使います。また、書き出したベクトルタイルを未圧縮とするためには、--no-tile-compression
オプションを指定します。
(検討) ズームレベル等の割当・レイヤ名の命名
国連ベクトルタイルツールキット方式では、ベクトルタイル生産後にもベクトルタイルサイズの最適化を回しますが、その前に概ねのズームレベル割り当てとレイヤ名の命名を済ませておいた方が楽です。
サイトを見て、ざっくり次のような感じかなと思います。
ソース GeoJSON | レイヤ名 | ジオメトリ型 | minzoom | maxzoom |
---|---|---|---|---|
prefectures.geojson | prefecture | Polygon + MultiPolygon | 2 | 7 |
municipality.geojson | municipality | Polygon + MultiPolygon | 8 | 10 |
municipality_name.geojson | munilabel | Point | 10 | 10 |
個人的なこだわりとしては、次のようなことを考えています。
- レイヤ名は単数形で行きます。
munilabel
という命名には、polylabelというプロダクト名が影響していると思います。
(開発) コードに落とす
次の方針でコードに落とします。
- ogr2ogr で生成した3つのストリームを立て続けに Tippecanoe に与えて一体のベクトルタイルにする。
- 上記 1. の三連ストリームを出す部分をまず先に作って標準出力で確認する。
- そのストリームを Tippecanoe にパイプする。
作成したコードは https://github.com/hfu/autonomy においていきます。
(FIXME: ここの README.md に書いてあるインストールおよび実行の方法を、ここにも書き出しておく。)
(開発中途) 地物ストリームを確認する
地物ストリームを出せるようになった段階でのスクリプトと設定ファイルをコピーしておくと、次の通りでした。
index.js
const config = require('config')
const Parser = require('json-text-sequence').parser
const { spawn } = require('child_process')
const minzoom = config.get('minzoom')
const maxzoom = config.get('maxzoom')
const srcs = config.get('srcs')
const ogr2ogrPath = config.get('ogr2ogrPath')
const downstream = process.stdout
for (const src of srcs) {
const parser = new Parser()
.on('data', f => {
console.log(JSON.stringify(f, null, 2))
})
const ogr2ogr = spawn(ogr2ogrPath, [
'-f', 'GeoJSONSeq',
'-lco', 'RS=YES',
'/vsistdout/',
src.url
])
ogr2ogr.stdout.pipe(parser)
}
config/default.json
{
minzoom: 2
maxzoom: 10
srcs: [
{
url: https://www.itdashboard.go.jp/js/data/prefectures.geojson
layer: prefecture
minzoom: 2
maxzoom: 7
}
{
url: https://www.itdashboard.go.jp/js/data/municipality.geojson
layer: munisipality
minzoom: 8
maxzoom: 10
}
{
url: https://www.itdashboard.go.jp/js/data/municipality_name.geojson
layer: munilabel
minzoom: 10
maxzoom: 10
}
]
ogr2ogrPath: /usr/local/bin/ogr2ogr
}
地物ストリーム
出てきた三連の GeoJSON Text Sequences は、次のようになります。(出典: IT Dashboard)
{
"type": "Feature",
"properties": {
"municipality_code": 15217
},
"geometry": {
"type": "Point",
"coordinates": [
138.1892722,
36.9073484
]
}
}
...
{
"type": "Feature",
"properties": {
"prefectures_code": 32,
"prefectures_name": "島根県"
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[
133.0038269,
36.0329253
],
...
[
133.1517075,
35.2161611
]
]
]
]
}
}
...
{
"type": "Feature",
"properties": {
"municipality_code": 15217,
"submap": 0
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
138.3786956,
36.9804372
],
...
[
138.3786956,
36.9804372
]
]
]
}
}
...
(調整) データを鑑賞し、属性を調整方針を考える
データを見るに、レイヤがまとめられた形でオンラインで提供されるベクトルタイルとしてはどのように属性を調整するべきか考えてみると、私は次のように感じました。
- ベクトルタイルでは、属性にレイヤ名を冠するメリットがありません。属性名はシンプルにしましょう。つまり、
code
やname
のようなシンプルなものにします。 - 地方公共団体コードを数値型で持っていることについては、私は好感が持てます。なぜなら、コードを整数値で表現する場合には正規の形が定義しやすいですが、文字列型の場合には正規の形をわざわざ定義しなければならないからです。数値型なら「ゼロフィルをするかしないか」のようなことで悩む必要がありません。
(文書化) ベクトルタイルスキーマを整理して文書化しておく
上記の調整方針に従って、ベクトルタイルを次のようなスキーマに調整することにします。
prefecture
属性名 | 説明 |
---|---|
code | 整数として表現された市区町村コード |
municipality
属性名 | 説明 |
---|---|
code | 整数として表現された都道府県コード |
name | 都道府県の名称 |
munilabel
属性名 | 説明 |
---|---|
code | 整数として表現された市区町村コード |
submap | あとで調べるべき、謎の整数値 |
(開発) オンザフライベクトルスキーマ調整の部分を作り込む
次のような形で作りこみました。やや面倒な作りこみですが、データがかなり分かりやすくなると私は思っています。
const renameProperties = (f) => {
for (let pair of [
['municipality_code', 'code'],
['prefectures_code', 'code'],
['prefectures_name', 'name'],
]) {
if (f.properties[pair[0]]) {
f.properties[pair[1]] = f.properties[pair[0]]
delete f.properties[pair[0]]
}
}
return f
}
//...
.on('data', f => {
f = renameProperties(f)
f.tippecanoe = {
layer: src.layer,
minzoom: src.minzoom,
maxzoom: src.maxzoom
}
downstream.write(`\x1e${JSON.stringify(f)}\n`)
})
//...
node index.js
の結果を標準出力で確認して、これで大丈夫かな、と思ったら、Tippecanoe
にパイプでつなぎましょう。
(開発) Tippecanoe にパイプして実際のベクトルタイルを得る
ここは、 process.stdout に設定していた downstream を tippecanoe.stdin に書き換えるだけです。Tippecanoe プロセスを作る部分は、次の通りです。
const tippecanoe = spawn(tippecanoePath, [
`--output-to-directory=${dstDir}`,
`--no-tile-compression`,
`--minimum-zoom=${minzoom}`,
`--maximum-zoom=${maxzoom}`
], { stdio: ['pipe', 'inherit', 'inherit'] })
const downstream = tippecanoe.stdin
この形で node index.js
を実行することで、zxy
フォルダにベクトルタイルが作られることになります。git add .; git commit -m update; git push origin master
で GitHub に送り込み、gh-pages
でホストしてもらいます。
(開発) サイトを作る
style.hjson を経由して style.json を書く
Mapbox Style のドキュメントを見ながら style.json を書きますが、JSON を書くストレスを軽減するために、Hjson で書いて、hjson というツールで変換します。変換コマンドは Rakefile に書いています。style.hjson は次の内容です。
{
version: 8
center: [
139.754
35.746
]
zoom: 8.22
sources: {
v: {
type: vector
tiles: [
https://hfu.github.io/autonomy/zxy/{z}/{x}/{y}.pbf
]
attribution: IT Dashboard (source)
minzoom: 2
maxzoom: 10
}
}
sprite: https://hfu.github.io/unite-sprite/sprite
glyphs: https://vectortiles.xyz/fonts/{fontstack}/{range}.pbf
layers: [
{
id: background
type: background
paint: {
background-color: [
rgb
187
222
251
]
}
}
{
id: prefecture
type: fill
source: v
source-layer: prefecture
paint: {
fill-color: [
rgb
245
245
245
]
fill-outline-color: [
rgb
92
99
102
]
}
}
{
id: municipality
type: fill
source: v
source-layer: municipality
paint: {
fill-color: [
rgb
245
245
245
]
fill-outline-color: [
rgb
92
99
102
]
}
}
{
id: munilabel
type: symbol
source: v
source-layer: munilabel
layout: {
text-field: [
get
code
]
text-font: [
sans
]
}
}
]
}
index.html を書く
index.html は、普通に次のような感じです。Mapbox GL JS の v1.0.0 突破、おめでとうございます。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link rel="stylesheet" type="text/css" href="https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css"/>
<style>
body { margin: 0; top: 0; bottom: 0; width: 100%; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.js"></script>
</head>
<body>
<div id="map"></div>
<script src="bundle.js"></script>
</body>
</html>
サイトの開発補助
これまでは、頻繁に git push して gh-pages からサイトを確認していたのですが、最近は budo を使って手元でサイトの確認をするようにしたりしています。こちらも、詳細は Rakefile を参照してください。
完成したサイト
Maputnik で見るには、https://maputnik.github.io/editor/?style=https://hfu.github.io/autonomy/style.json を開いてください。
付録
感想
- 実際の現場では、このドキュメントほどはお行儀よくやりません。とりあえずいい加減にものを作ってしまって、何度でもやりなおすようなアプローチを取り、最後に苦しみながらドキュメントを書く(あるいは、ドキュメントを書き忘れる)ことが多いです。
- 自分で似たミニプロジェクトを重ねながら流していて思ったのですが、部品の共通化というのはプロダクトの信頼性を上げるために重要だと、工学の教科書通りに、改めて思いました。ただ、よい共通部品は抽象的思考の中から上流で設計されるときよりも、並列で流れるミニプロジェクトたちが、周囲の成果を盗むあうときにできるかもしれないと思います。抽象的思考の中から生まれる共通部品は、悪いものではありませんが、費用対効果が説明しづらい場合が多いです。
- 何よりも手を動かすこと、それから、言語にはそれほどこだわらずにドキュメントすることが重要ですね。ドキュメント作業も、プロダクトも重要ですがプロセスも重要なのですね。
本稿のドラフト置き場