世界の測量

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

ベクトルタイル・いちさと(小学校区)Pt. 5・クリップトTopoJSON

TopoJSONを使うと良いよという助言を頂いたので、TopoJSONタイルにチャレンジしてみた。

結果

※見た目は従前と変わらないが、データが軽くなったのでレスポンスがよくなっている。
f:id:hfu:20140109045153p:plain
http://handygeospatial.github.io/mapsites/2014/01/06/clipped_topojson.html

TopoJSONデータの作成

TopoJSONデータの作成は、mbostockさんのtopojsonコマンドラインツールを使う。

$ npm install -g topojson

でインストールが行われる。コマンドラインパラメータは、Command Line Reference · mbostock/topojson Wiki · GitHubで解説されている。この解説を参考にしながら、これまでに作ったGeoJSONタイルを変換するアプローチで、次のスクリプト(効率悪い...)で変換を行った。

# topo.rb CC0
require 'find'
require 'fileutils'

count = 0
{'school_district' => 'topojson_clipped',
 'school_district_unclipped' => 'topojson_unclipped'}.each {|src, dst|
  Find.find(src) {|src_path|
    dst_path = src_path.sub('.geojson', '.topojson').sub(src, dst)
    FileUtils.mkdir(dst_path) if File.directory?(src_path) and !File.exist?(dst_path)
    if /geojson$/.match src_path
      json = `/usr/local/share/npm/bin/topojson -p -- es=#{src_path} | jq --ascii-output --compact-output '.' > #{dst_path}`
      count += 1
      print "#{count}:@#{dst}" + json + "\n"
    end
  }
}

topojson コマンドの「-p」オプションで属性をすべてコピーするようにした。

また、topojson コマンドが吐く JSON は日本語文字列をUnicodeエスケープしないので、jq コマンドを使ってUnicodeエスケープするようにした。あわせて、コンパクト化もjqに委ねた。

作成したTopoJSONタイルは、次の場所にホストしている。

サイト

TopoJSONタイルサイトの作成にあたっては、NelsonMinarさんの次のサイトを参考にした。

<!doctype html>
<html>
<head>
  <!-- thx https://gist.github.com/NelsonMinar/5851197 -->
  <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>ベクトルタイル・いちさと(小学校区)Clipped TopoJSON</title>
  <link rel='stylesheet' href='http://cdn.leafletjs.com/leaflet-0.7/leaflet.css'>
  <script src='http://cdn.leafletjs.com/leaflet-0.7/leaflet.js'></script>
  <script src="http://handygeospatial.github.io/mapsites/2014/01/01/leaflet-hash.js"></script>
  <script src="http://d3js.org/d3.v3.min.js"></script>
  <script src="http://d3js.org/topojson.v1.min.js"></script>
  <script src="TileLayer.d3_topoJSON.js"></script>
  <style>
  html, body, #mapdiv {margin: 0; padding: 0; width: 100%; height: 100%;}
  path {stroke-linejoin: round; stroke-linecap: round; fill: none;}
  path.es {stroke: #00f; fill: #00f; strokeOpacity: 0.3; fill-opacity: 0.2;}
  </style>
</head>
<body>
  <div id='mapdiv'/>
  <script>
map = new L.Map('mapdiv', {zoom: 13, center: [35.6822, 139.7386]});
hash = new L.Hash(map);

// this is important. see https://gist.github.com/NelsonMinar/5851197
new L.geoJson({"type": "LineString","coordinates":[[0,0],[0,0]]}).addTo(map);

map.addLayer(new L.TileLayer(
  'http://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
  {attribution: '地理院タイル', minZoom: 13, maxZoom: 13}));

map.addLayer(new L.TileLayer.d3_topoJSON(
  'http://www.handygeospatial.info/2014/01/06/' + 
    'topojson_clipped/{z}/{x}/{y}.topojson',
  {class: 'es',
   attribution: '国土数値情報(小学校区データ) 国土交通省', 
   minZoom: 13, maxZoom: 13,
  }));
  </script>
</body>
</html> 
/* Experimental vector tile layer for Leaflet
 * Uses D3 to render TopoJSON. Derived from a GeoJSON thing that was
 * Originally by Ziggy Jonsson: http://bl.ocks.org/ZJONSSON/5602552
 * Reworked by Nelson Minar: http://bl.ocks.org/NelsonMinar/5624141
 *
 * Todo:
 *   Make this work even if <svg> isn't in the DOM yet
 *   Make this work for tile types that aren't FeatureCollection
 *   Match D3 idioms for .classed(), .style(), etc
 *   Work on allowing feature popups, etc.
 */
// with a minor change by hfu 20140107 (for(key in tjData.objects))

L.TileLayer.d3_topoJSON =  L.TileLayer.extend({
  onAdd : function(map) {
    L.TileLayer.prototype.onAdd.call(this,map);
    this._path = d3.geo.path().projection(function(d) {
      var point = map.latLngToLayerPoint(new L.LatLng(d[1],d[0]));
      return [point.x,point.y];
    });
    this.on("tileunload",function(d) {
      if (d.tile.xhr) d.tile.xhr.abort();
        if (d.tile.nodes) d.tile.nodes.remove();
        d.tile.nodes = null;
        d.tile.xhr = null;
      });
  },
  _loadTile : function(tile,tilePoint) {
    var self = this;
    this._adjustTilePoint(tilePoint);
    if (!tile.nodes && !tile.xhr) {
      tile.xhr = d3.json(this.getTileUrl(tilePoint),function(error, tjData) {
        if (error) {
          console.log(error);
        } else {
          for(var key in tjData.objects) {
            var geoJson = topojson.feature(tjData, tjData.objects[key]);
            tile.xhr = null;
            tile.nodes = d3.select(map._container).select("svg").append("g");
            tile.nodes.selectAll("path")
              .data(geoJson.features).enter()
            .append("path")
              .attr("d", self._path)
              .attr("class", self.options.class)
              .attr("style", self.options.style)
              .append("title").text(function(d) {
                var s = '';
                for(key in d.properties) {
                  s += key + ': ' + d.properties[key] + '\n';
                }
                return s;});
          }
        }
      });
    }
  }
});

感想

TopoJSONを使うためにd3への依存性が強化されたことにより、結果としてd3的にSVGがブラウザネイティブで見えるようになってきた。これは正しい方向性であり、SVG符号化法にまつわる制約をTopoJSONで回避しつつ、SVGの強力な機能をウェブ地図に持ってくるよい方法である。まずは、次のドキュメントをよく身につけたい。
D3.js と TopoJSON で地図を作る
その前に、アンプリップトを試す必要もあるだろう。