1. Qiita
  2. 投稿
  3. 地図

d3.jsで国土地理院のベクトルタイルを使う

  • 4
    いいね
  • 0
    コメント
に投稿

ニューヨークタイムズがシリアの諸都市の被害状況を可視化した記事を読み、ここに使われている地図はオープンストリートマップのデータをd3.jsで読み込んでいるのではないかと考え、まあ結果的には違ったのですが、国土地理院のベクトルタイルで似たようなものを作れないかと思い、試してみました。

VectorTiles / Mike Bostock's Block

上記のコードを国土地理院のベクトルタイルの仕様に合わせて改造します。 とりあえず、先にデモページとサンプルコードを載せてみます。サンプルコードは上記のコードと比較しながら読むといいと思います。

デモページ

使用ライブラリ

d3.js v4.4.0
d3-tile v0.0.3

html

<html>
<head><script src="https://d3js.org/d3.v4.min.js"></script>
  <!-- d3.js本体に加えて、d3-tileプラグインを読み込みます。 -->
  <script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
</head>
<body>
  <div class="chartcontainer"></div>
</body>
</html>

css

.chartcontainer {
    width: 100%;
}

.map {
    position: relative;
    overflow: hidden;
}

path {
    fill: none;
    stroke-linejoin: round;
    stroke-linecap: round;
}

path.nation {
     stroke: #ccc;
     stroke-width: 4px;
}

path.pref {
     stroke: #ddd;
     stroke-width: 3px;
}

path.minor {
     stroke: #ddd;
}

path.highway {
     stroke: #aaa;
     stroke-width: 6px;
}

path.station {
     stroke: #e6d3ec;
     stroke-width: 3px;
}

path.rail {
     stroke: #777;
     stroke-width: 1px;
}

.info {
  position: absolute;
  bottom: 10px;
  left: 20px;
}

.landmarks {
  font-family: sans-serif;
  font-weight: 600;
  fill: #777;
  font-size:9px;
}

.landmarks.region {
  font-size:20px;
  fill: black;
}

.landmarks.area {
  font-weight: 200;
  fill: #333;
  font-size:16px;
}

JavaScript

var pi = Math.PI;
var tau = 2 * pi;

// 表示するズームレベルとタイルを取得するズームレベルを別個に定義
var zoom = {view: 13.5, tile: 16};

var center = [140.461321, 36.374950];
var width = $(".chartcontainer").width();
var height = 480;

// ズームレベルの差をdzとすると、2^dzを変数magで定義
// 今回の場合は2^(16-14)=2^2=4となる
var mag = Math.pow(2, zoom.tile - zoom.view);

// projectionのスケールは表示するズームレベルを指定
var projection = d3.geoMercator()
    .center(center)
    .scale(256 * Math.pow(2, zoom.view) / tau)
    .translate([width / 2, height / 2]);

var path = d3.geoPath()
    .projection(projection);

// d3.tile()のサイズにmagを掛ける
var tile = d3.tile()
    .size([width * mag, height * mag]);

var map = d3.select(".chartcontainer").append("svg")
    .attr("class", "map")
    .attr("width", width)
    .attr("height", height);

var info = d3.select(".chartcontainer").append("div")
              .attr("class", "info")
              .append("h4");

// geojsonファイルの属性からclassを与える関数
function roadClass(prop) {
    return prop == "国道" ? "nation" :
        prop == "都道府県道" ? "pref" :
        prop == "高速自動車国道等" ? "highway" : "minor";
}

map.selectAll(".tile")
    .data(tile
        .scale(projection.scale() * tau * mag) // magを掛ける
        .translate(projection([0, 0]).map(function(v){return v * mag;}))) //magを掛ける
    .enter().append("g")
    .attr("class","tile")
    .each(function(d) {
        // このgが各タイル座標となる
        var g = d3.select(this);

        // 道路中心線を取得
        d3.json("http://cyberjapandata.gsi.go.jp/xyz/experimental_rdcl/" + d[2] + "/" + d[0] + "/" + d[1] + ".geojson", function(error, json) {
            if (error) throw error;

            g.selectAll(".road")
                .data(json.features.filter(function(d){
                  return d.properties.rnkWidth !== "3m未満";
                })) // 小さい道路をフィルタリング(任意)
                .enter().append("path")
                .attr("class", function(d) {
                    return "road " +roadClass(d.properties.rdCtg);
                })
                .attr("d", path);
        });

        // 鉄道中心線を取得
        d3.json("http://cyberjapandata.gsi.go.jp/xyz/experimental_railcl/" + d[2] + "/" + d[0] + "/" + d[1] + ".geojson", function(error, json) {
            if (error) throw error;
            g.selectAll(".rail")
                .data(json.features)
                .enter().append("path")
                .attr("class", function(d) {
                    return d.properties.snglDbl == "駅部分" ? "station" :
                    "rail";
                })
                .attr("d", path);
        });
    });

  // オーバーレイを作成(任意)
  var layer1 = map.append("g");
  var layer2 = map.append("g");

  d3.json("../data/mito/landmarks.geojson", function(error, json) {
    if (error) throw error;

    var landmarks = layer1.selectAll(".landmarks")
                  .data(json.features)
                  .enter()
                  .append("text")
                  .attr("class", function(d){return "landmarks " + d.properties.type;})
                  .attr("transform", function(d) {
                      return "translate(" + projection(d.geometry.coordinates) + ")";
                  })
                  .attr("text-anchor","middle")
                  .text(function(d){return d.properties.name;});
  });


  d3.json("../data/mito/machinaka.geojson", function(error, json) {
      if (error) throw error;

      var machinaka = layer2.selectAll(".machinaka")
              .data(json.features)
              .enter()
              .append("g")
              .attr("class", "machinaka")
              .attr("transform", function(d) {
                  return "translate(" + projection(d.geometry.coordinates) + ")";
              })
              .on("mouseover", function(d) {
                  d3.select(this).select("circle")
                      .attr("r", 8)
                      .attr("fill", "rgba(255, 145, 0, 0.8)");
                  info.text(d.properties["施設名"]);
              })
              .on("mouseout", function(d) {
                  d3.select(this).select("circle")
                      .attr("r", 5)
                      .attr("fill", "rgba(255, 0, 0, 0.6)");
                  info.text("");
              });

          machinaka.append("circle")
              .attr("r", 5)
              .attr("fill", "rgba(255, 0, 0, 0.6)")
              .attr("stroke", "white");

      });

解説

VectorTiles / Mike Bostock's Blockで使われているMapzenのベクトルタイルとは異なり、国土地理院の道路中心線、鉄道中心線、河川中心線のベクトルタイルはズームレベル16のみで提供されています。d3.tile()で取得できるタイル座標のズームレベルは地図の縮尺に依存するため、ズームレベル16に対応する縮尺を設定しないと地図が白紙になってしまいます。 ですが、ズームレベル16に適した縮尺はそれなりに大縮尺なので、表示する範囲が非常に狭くなってしまします。ズームレベル16のベクトルタイルを取得しつつ、それなりに広域の範囲を表示する方法を考えます。

d3.tile()は、projectionの情報と出力領域のサイズを基に、地図の領域をカバーするタイル座標の配列を返します。この配列をsvgの中の<g>要素とバインドし、d3.json()でベクトルデータを取得していきます。

console.log(map.selectAll(".tile").data());
  // => [[58330,25646,16],[58331,25646,16],[58332,25646,16],...,[58345,25654,16]]
  // タイルのx座標、y座標、ズームレベルで構成された配列の集合を返す

先述の通り、国土地理院の道路中心線ベクトルタイルを取得するには、d3.tile()が返すタイル座標のズームレベルが16に固定されなければなりません。

表示する地図の縮尺と取得するタイルのレベルを補正するために、magという変数を定義します。

var zoom = {view: 14, tile:16};
  var mag = Math.pow(2, zoom.tile - zoom.view);

変数magを使い、ベクトルタイルを取得する際に、実際に表示しているサイズのmag^2倍の大きさの地図の情報をd3.tile()に渡しています。 例えば地図のサイズが400px x 300px、表示したい縮尺がズームレベル14のとき、この地図と同じ範囲のタイルをズームレベル16で取得するには、400 * mag x 300 * magmag = Math.pow(2, 16 - 14) = 4なので1600px x 1200px、ズームレベル16という偽の情報をd3.tile()に渡してやればいいのです。

var tile = d3.tile()
      .size([width * mag, height * mag]);

  map.selectAll(".tile")
      .data(tile.scale(projection.scale() * tau * mag)
                .translate(projection([0, 0]).map(function(v){return v * mag;})))
      .enter().append("g")

ここでtile.translate()が何を意味しているのかいまいち理解してないのですが、とりあえず出てくる数値にmagを掛けるのが正解のようです。

400px x 300px の大きさのsvgに任意のズームレベルで、ズームレベル16のベクトルタイルを描写する場合、zoom.viewzoom.tile<svg>のサイズ、tile.size()magは以下のような関係になります。

zoom.view zoom.tile svg tile.size() mag
13 16 400x300 [3200, 2400] 8
14 16 400x300 [1600, 1200] 4
15 16 400x300 [800, 600] 2
16 16 400x300 [400, 300] 1
17 16 400x300 [200, 150] 0.5

これは地図を表示するズームレベルzoom.viewを13.5のように浮動小数点の値にしてもきちんと動作します。

こうしてズームレベル16のベクトルタイルの読み込みに成功すれば、あとはVectorTiles / Mike Bostock's Blockと同じようにclassとcssでスタイルを整えれば地図が出来上がります。

デモページ

ズームレベル15で提供されている注記ベクトルタイルや、ズームレベル18で提供されている基盤地図情報ベクトルタイルも、以上の方法と同様にzoom.tileの値を固定することで地図の縮尺に影響されることなく利用することができます。

雑記

ベクトルタイルを読んで表示する処理をいちいちクライアント側でさせるのは面倒ですよね。 パンやズーム、インタラクティブな動作を実装しない静的な地図ならサーバー側で予め処理しておくのがいいんですかね。これから勉強します。

タイル地図の仕様やベクトルタイルなどの説明は省いているため、普段タイル地図に触れていない人にはわかりづらい説明になってしまっているとは思います。 以下のリンクを参照ください。