世界の測量

Sibling of "Relevant, Timely, and Accurate, " but much lighter and shorter ※自らの所属する組織の見解を示すものでない

ベクトルタイル・バスルート

国土数値情報(バスルートデータ)の一部のベクトルタイル化する試行を行った。失敗の多い試行であったが、学習は進めることができたと思う。

結果

f:id:hfu:20140103063045p:plain
http://handygeospatial.github.io/mapsites/2014/01/01/
いくつか、見るに耐える部分を紹介する。

方法

フロントエンド側からバックエンド側に遡る順番で、実現方法を紹介する。

index.html

<!doctype html>
<html>
<head>
  <!-- thx https://github.com/ebrelsford/leaflet-tilelayer-vector -->
  <meta charset='UTF-8'>
  <meta name="viewport" 
   content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="apple-mobile-web-app-capable" content="yes"/>
  <meta name="apple-mobile-web-app-status-bar-style" 
   content="black-translucent" />
  <link rel="apple-touch-icon" 
   href="https://si0.twimg.com/profile_images/666603054/r.png"/>
  <title>vectiles bus route</title>
  <link rel='stylesheet' href='http://cdn.leafletjs.com/leaflet-0.7/leaflet.css'>
  <link rel="stylesheet" href="http://leaflet.github.io/Leaflet.label/leaflet.label.css">
  <script src='http://cdn.leafletjs.com/leaflet-0.7/leaflet.js'></script>
  <script src="http://leaflet.github.io/Leaflet.label/leaflet.label.js"></script>
  <script src="communist.min.js"></script>
  <script src="TileCache.js"></script>
  <script src="TileQueue.js"></script>
  <script src="AbstractWorker.js"></script>
  <script src="CommunistWorker.js"></script>
  <script src="TileLayer.GeoJSON.js"></script>
  <script src="TileLayer.Overzoom.js"></script>
  <script src="Leaflet.label-patch.js"></script>
  <script src="leaflet-hash.js"></script>
  <style>
  html, body, #mapdiv {margin: 0; padding: 0; width: 100%; height: 100%;}
  </style>
</head>
<body>
  <div id='mapdiv'/>
  <script>
icon = L.icon({iconUrl: 'http://handygeospatial.github.io/mapsites/2013/12/13/maki/bus-24.png'});
map = new L.Map('mapdiv', {zoom: 15, center: [35.71725, 139.733747]});
hash = new L.Hash(map);
map.addLayer(new L.TileLayer(
  'http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
  {attribution: '地理院タイル', minZoom: 2}));
map.addLayer(new L.TileLayer.Vector(
  'http://www.handygeospatial.info/2014/01/01/{z}/{x}/{y}.geojson',
  {attribution: '国土数値情報(バスルートデータ) 国土交通省', 
   unloadInvisibleTiles: true, minZoom: 15, 
   serverZooms: [15]},
  {style: { // not used
        "clickable": true,
        "color": "#00F",
        "fillColor": "#00F",
        "weight": 5.0,
        "opacity": 0.3,
        "fillOpacity": 0.2
    },
   onEachFeature: function(feature, layer) {
     layer.bindPopup("事業者名: " + feature.properties.N07_002 + 
                     "<br/>バス系統: " + feature.properties.N07_003);
//     layer.setIcon(icon);
   }
  }));
  </script>
</body>
</html> 

Rakefile(ローカルホストでチェックするためのプログラム)

バス停データを処理した時と同じもの。ローカルファイルシステムでサイトを作成したときに使用するものであり、私は、コンテンツを置いたフォルダから実行している。

require 'rubygems'
require 'sinatra/base'

class App < Sinatra::Base
  get '/' do
    File.open('index.html').read
  end

  get '/*.js' do
    content_type "text/javascript"
    File.open("#{params[:splat][0]}.js").read
  end

  get '/*/*/*.geojson' do
    headers({"Access-Control-Allow-Origin" => "*"})
    path = "#{params[:splat].join('/')}.geojson"
    if File.exist?(path)
      File.open(path).read
    else
      '{"type": "FeatureCollection", "features": []}'
    end
  end
end

task :default do
  App.run!
end

tilestache-seed.py 投入用 Rakefile

作業用のものでかなり生々しいが、そのまま共有する。

task :default do
  sh "tilestache-seed.py -b 24.259007 123.746937 45.522249 145.812026 -c tilestache.cfg -l busroute 15"
end

task :south do
  #sh "tilestache-seed.py -b 24.259007 123.746937 35.522249 145.812026 -c tilestache.cfg -l busroute 15"
  sh "tilestache-seed.py -b 25.522249 123.746937 26.522249 145.812026 -c tilestache.cfg -l busroute 15"
end

task :tokyo do
  sh "tilestache-seed.py -x -b 35.614675 139.711388 35.74982 139.867099 -c tilestache.cfg -l busroute 15"
end

task :minamikanto do
  sh "tilestache-seed.py -b 35.3063 139.3611 36.1531 140.2326 -c tilestache.cfg -l busroute 15"
end

task :kyoto do
  sh "tilestache-seed.py -b 34.7289 134.6680 35.5758 136.3252 -c tilestache.cfg -l busroute 15"
end

tilestache.cfg

{"cache": {
  "name": "Disk",
  "path": "/vagrant/busroute",
  "dirs": "portable",
  "gzip": []
  },
 "layers": {
  "busroute": {
    "provider": {"name": "vector", "driver": "ESRI Shapefile", "verbose": true,
      "parameters": { 
        "file": "N07-11-jgd_utf8_3.shp"},
        "properties": ["N07_001", "N07_002", "N07_003", "N07_004", "N07_005", "N07_006", "N07_007"]
      }
    }
  }
}

convert2.rb

国土数値情報(バスルートデータ)の文字コードUTF-8に変換するもの。機種依存文字への対応などかなり未整理で生々しいものになっているが、そのまま共有する。

なお、tilestache-seed.pyは、つぎのような癖がある。

  • .prjファイルによる座標参照系定義がないとエラーになる。ダウンロードしたままの国土数値情報(バスルートデータ)には.prjファイルがないので、私はQGISを用いて.prjファイルを作成し、変換後ファイルに対応する複製も作成した。
  • .qixファイルによる空間インデックスを活用する。結果的にかなりのスピードアップになる。ダウンロードしたままの国土数値情報(バスルートデータ)には.qixファイルがないので、私はQGISを用いて.qixファイルを作成し、変換後ファイルに対応する複製も作成した。
# -*- coding: utf-8 -*-
# convert2.rb CC0
require 'rubygems'
require 'geo_ruby'
require 'geo_ruby/shp'

include GeoRuby::Shp4r
path = "N07-11-jgd.shp"
ShpFile.open(path) do |src|
  dst_fields = []
  src.fields.each {|field|
    dst_fields << 
      Dbf::Field.new(field.name,
        field.type, field.length * 2, field.decimal)
  } 
  # 結局、長さ2倍拡張した dst_fields は使っていない。
  # 変換エラーが出たため。おそらく256バイトを超えたため。
  ShpFile.create(path.sub('.shp', '_utf8_3.shp'), 
    src.shp_type, src.fields) do |dst|
    dst.transaction do |tr|
      src.each do |r|
        attr = {}
        r.data.attributes.each {|k, v|
          if v.class == String
            # 効率向上の余地は明らかに存在するだろう。
            v.gsub!('~', '-')
            v.gsub!('⇔', '<=>')
            v.gsub!('→', '->')
            v.gsub!('←', '<-')
            v.gsub!('①', '(1)')
            v.gsub!('②', '(2)')
            v.gsub!('③', '(3)')
            v.gsub!('④', '(4)')
            v.gsub!('⑤', '(5)')
            v.gsub!('⑥', '(6)')
            v.gsub!('⑦', '(7)')
            v.gsub!('⑧', '(8)')
            v.gsub!('⑨', '(9)')
            v.gsub!('⑩', '(10)')
            v.gsub!('⑪', '(11)')
            v.gsub!('⑫', '(12)')
            v.gsub!('⑬', '(13)')
            v.gsub!('⑭', '(14)')
            v.gsub!('⑮', '(15)')
            v.gsub!('⑯', '(16)')
            v.gsub!('⑰', '(17)')
            v.gsub!('⑱', '(18)')
            v.gsub!('⑲', '(19)')
            v.gsub!('⑳', '(20)')
            v.gsub!('Ⅰ', 'I')
            v.gsub!('Ⅱ', 'II')
            v.gsub!('Ⅲ', 'III')
            v.gsub!('Ⅳ', 'IV')
            v.gsub!('Ⅴ', 'V')
            v.gsub!('㈱', '(株)')
            v.gsub!('№', 'No.')
            v.gsub!('-', '-')
            v = v[0..39]
            print "#{k}: #{v.encode!('UTF-8', :invalid => :replace)}\n"
          end
          attr[k] = v
        }
        tr.add(ShpRecord.new(r.geometry, attr))
      end
    end
  end
end

分析

日本語問題

日本語処理の問題がブービー・トラップのように散在している状態になっている。機種依存文字が放置されている文字コードおよびデータセットの問題、UTF-8エンコードするとバイト数が約1.5倍になるところ、Shapefileは幅指定の形式でありしかも幅の限界があるという複合問題、そして何より問題であったのは、私がPythonにおける日本語処理を自家薬籠中の物にしていなかったことである。

日本語処理は、適切に行えば問題ないところまできているが、適切なおこない方がいまだ常識化していないことが問題である。

作成の効率化の必要性

日本タイル台帳の必要

バスルートデータのextentをベースにtilestache-seed.pyをかけようとすると、約5000万タイルの作成タスクが発生する。日本はかなり細長く海洋が多い領域であるので、いわゆる海タイルが支配的になってしまう。日本においてタイル作成の手間を5倍ほど効率化する方法として「日本タイル台帳(略称:ニッタイダイ)」を利用する方法があると思われる。日本タイル台帳とは、日本の陸域がかかるウェブメルカトルのタイルのリストである。残念ながら日本タイル台帳はまだ作成していない。

空タイル問題

今回の試行では、初期想定作成対象約5000万タイルのうち、約40万タイルを作成した。作成方針としては、tilestache-seed.pyにプレインに作成させるタスク(結果的に、extentの北西方向からテキストの方向に、つまり一行ずつ北から南に、作成される)と、北限を、日本の東西断面が長いあたりにとった(そののち、北限をかなり南に移した)タスクを基本的に走らせ、それとは別に、まずは概ね山の手線内を、次には南関東をラフにカバーする部分を限定的に作成するタスクを走らせた。その他、京都を含むタイルの作成も投入したが、対象となる14440タイルのうち3900タイル程度しか作成しないところで中断した。

このようにして作成した約40万タイルをS3に上げるに際して、空のタイル(結果的に54バイトになり、このバイト数により空タイルと判定。)を検出して削除したところ、約39万のタイル(98.3%!)が削除された。残ったタイルはわずかに7500タイル程度であった。

これは、今後、タイル作成がいかに効率化できるかということを示すものである。データのあるところにのみタイルを作ればよいのだから、やはりMapReduceのアプローチによるタイル作成を早期に実現することが必要であろう。

国土数値情報(バスルートデータ)の特性

国土数値情報(バスルートデータ)はバスルートの一本一本をデータ化したものであり、今回のデモサイトのような見せ方には、そのままでは必ずしも向いていない*1。本来、そのような部分にも適切な考慮が必要である。

*1:ターミナル付近など、1タイルが数MBになってしまう場所もある。