4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

IT dashboard の行政界データをベクトルタイルにした

Last updated at Posted at 2019-05-21

2019-05-25: ベクトルタイルが完成したので、記事を更新しました。

 _   _ _   _  __     __        _             
| | | | \ | | \ \   / /__  ___| |_ ___  _ __ 
| | | |  \| |  \ \ / / _ \/ __| __/ _ \| '__|
| |_| | |\  |   \ V /  __/ (__| || (_) | |   
 \___/|_| \_|    \_/ \___|\___|\__\___/|_|   
                                             
 _____ _ _        _____           _ _    _ _   
|_   _(_) | ___  |_   _|__   ___ | | | _(_) |_ 
  | | | | |/ _ \   | |/ _ \ / _ \| | |/ / | __|
  | | | | |  __/   | | (_) | (_) | |   <| | |_ 
  |_| |_|_|\___|   |_|\___/ \___/|_|_|\_\_|\__|

       Make the technology the easy part.

動機

情報源は地域におけるデータの活用促進に向けて~政府のオープンデータ政策~なのですが、 IT Dashboard では次の GeoJSON データが使われているようですね。

  1. prefectures.geojson
  2. municipality.geojson
  3. municipality_name.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

個人的なこだわりとしては、次のようなことを考えています。

  1. レイヤ名は単数形で行きます。

munilabel という命名には、polylabelというプロダクト名が影響していると思います。

(開発) コードに落とす

次の方針でコードに落とします。

  1. ogr2ogr で生成した3つのストリームを立て続けに Tippecanoe に与えて一体のベクトルタイルにする。
  2. 上記 1. の三連ストリームを出す部分をまず先に作って標準出力で確認する。
  3. そのストリームを 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
        ]
      ]
    ]
  }
}
...

(調整) データを鑑賞し、属性を調整方針を考える

データを見るに、レイヤがまとめられた形でオンラインで提供されるベクトルタイルとしてはどのように属性を調整するべきか考えてみると、私は次のように感じました。

  • ベクトルタイルでは、属性にレイヤ名を冠するメリットがありません。属性名はシンプルにしましょう。つまり、codename のようなシンプルなものにします。
  • 地方公共団体コードを数値型で持っていることについては、私は好感が持てます。なぜなら、コードを整数値で表現する場合には正規の形が定義しやすいですが、文字列型の場合には正規の形をわざわざ定義しなければならないからです。数値型なら「ゼロフィルをするかしないか」のようなことで悩む必要がありません。

(文書化) ベクトルタイルスキーマを整理して文書化しておく

上記の調整方針に従って、ベクトルタイルを次のようなスキーマに調整することにします。

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 を開いてください。

付録

感想

  • 実際の現場では、このドキュメントほどはお行儀よくやりません。とりあえずいい加減にものを作ってしまって、何度でもやりなおすようなアプローチを取り、最後に苦しみながらドキュメントを書く(あるいは、ドキュメントを書き忘れる)ことが多いです。
  • 自分で似たミニプロジェクトを重ねながら流していて思ったのですが、部品の共通化というのはプロダクトの信頼性を上げるために重要だと、工学の教科書通りに、改めて思いました。ただ、よい共通部品は抽象的思考の中から上流で設計されるときよりも、並列で流れるミニプロジェクトたちが、周囲の成果を盗むあうときにできるかもしれないと思います。抽象的思考の中から生まれる共通部品は、悪いものではありませんが、費用対効果が説明しづらい場合が多いです。
  • 何よりも手を動かすこと、それから、言語にはそれほどこだわらずにドキュメントすることが重要ですね。ドキュメント作業も、プロダクトも重要ですがプロセスも重要なのですね。

本稿のドラフト置き場

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?