d3.js
位置情報
地図
geojson

D3で世界地図を描画してみる

More than 1 year has passed since last update.

はじめに

最近になってd3jsを触り始めて楽しさを再認識しました。
世界地図をブラウザでぐりぐりするのが目標です。
開発環境はMacOSです。

最終目標までの道は遠いです。
QGISもnpmだってインストールしてゴリゴリ進めます。

この辺りを大分参考にさせていただきました。

http://www.tam-tam.co.jp/tipsnote/javascript/post6174.html
http://y-uti.hatenablog.jp/entry/2014/02/09/071953
http://qiita.com/sawamur@github/items/ec32237bcbaaba94108d
http://ja.d3js.node.ws/blocks/mike/map/
http://blog.hitsuji.me/make-topojson-file-and-show-map/

やること

  1. シェープファイルの取得
  2. Shape→GeoJSON
  3. GeoJSON→topoJSON
  4. d3jsで地図を表示

使うもの

  • d3.js
  • QJIS
  • GDAL
  • node.js

地図データの取得

シェープファイルと言われる地図データを取得します。
ここを参考にさせていただきました。
http://y-uti.hatenablog.jp/entry/2014/02/09/071953

シェープファイルとは

シェープファイルというのは、おおまかに言うと

  • ポリゴンと点のようなエリアや地点を表すベクター情報のファイル(.shp)
  • 国名、県名、番号、あるいは、トイレ、井戸というような属性データ(.dbf)
  • それらの対応関係を示すファイル(.shx)

のセットでできている地図データの形式です。多分。

http://www.pasco.co.jp/recommend/word/word028/
http://www.esrij.com/getting-started/learn-more/shapefile/

NatualEarthからダウンロード

シェープファイルはNaturalEarthというところから落としてきます。
こんなリッチなデータをフリーで使わせていただけるなんてすごすぎる。
http://www.naturalearthdata.com/

今回はグリグリするのが目的で、そこまで詳細な地図は必要ないかなという判断で
国境の世界地図のシェープファイルをダウンロードしました。
800KBのzipなのでダウンロードは一瞬。

Medium scale data, 1:50m > Caltural > Admin 0 – Countries > Download countries

落としてきたzipは解凍。

QGISのインストール

こちらにYosemiteにQGISインストールするまでについての
記事を書きましたので参考になりましたら是非。

ちなみに、私もQGISで中身を見て気づきましたが
県境データだと思っていたこれはアメリカとオーストラリアとブラジルしか県境がありませんでした。

Medium scale data, 1:50m > Caltural > Admin 1 - States, provinces > Download states and province

QGISで見るとこんなかんじです。
スクリーンショット 2015-08-11 14.46.13.png

紫芋っぽい色がAdmin 0 – Countries
ミントっぽい色がAdmin 1 - States, provinces
です。

Shape→GeoJSON

GeoJSONとは?

geojsonの例

{ "type": "FeatureCollection",
  "features": [
    { "type": "Feature",
      "geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
      "properties": {"prop0": "value0"}
      },
    { "type": "Feature",
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
          ]
        },
      "properties": {
        "prop0": "value0",
        "prop1": 0.0
        }
      },
    { "type": "Feature",
       "geometry": {
         "type": "Polygon",
         "coordinates": [
           [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
             [100.0, 1.0], [100.0, 0.0] ]
           ]
       },
       "properties": {
         "prop0": "value0",
         "prop1": {"this": "that"}
         }
       }
     ]
   }

こんな形式で地理的データ構造をエンコードするためのフォーマットです。
多分色んな所で使えて便利なので位置情報クラスタの常識なんだと思います。

ちなみに、今回利用するd3.jsで地図を描画できるのはGeoJSON形式とTopoJSON形式です。

http://blog.qaramell.com/?p=8191

GDALのインストール

Shape形式の地図データをGeoJSON形式に変換するためにogr2ogrというツールを利用します。
ogr2ogrはGDALにバンドルされているのでGDALをインストールします。

このあたりに詳しく書いてあります。
http://qiita.com/sawamur@github/items/ec32237bcbaaba94108d
http://www.geopacific.org/opensourcegis/gdal-ogr/ogr-commands

QGISインストール済みの方

GDALはQGISのインストールのために必要なので、すでに入っているかもしれません。
もし「すでに入れたはずなのにコマンドラインツールが見つからないよ!」という方がいらっしゃったら
パスが通ってないせいかもしれないので、以下を.bash_profileに追記してパスを通しましょう。

export PATH=/Library/Frameworks/GDAL.framework/Programs:$PATH

QGIS未インストールの方

まだ入れていない場合は、今回はMacなのでbrewからGDALをインストールすると簡単かもしれません。

GDALをインストール

$ brew install gdal

これで完了。

ogr2ogrでGeoJSONに変換

shp→geojsonに変換

$ cd ne_50m_admin_0_countries
$ ogr2ogr -f GeoJSON countries.geojson ne_50m_admin_0_countries.shp

GeoJSON形式にフォーマットを指定して変換するだけです。
今回利用したシェープファイルは小さかったので0.5秒くらいで終わります。

参考にさせていただいたこちらには、
出力ファイルがsjisになるので気をつけて。
と書いてありましたが、私の手元ではutf8で出力されていました。ラッキー。

GeoJSON→topoJSON

topoJSONとは?

こちらで 「データの変換」 の章に以下の様な説明がありました。
GeoJSONをより小さく、効率的にするために、トポロジーをエンコードして拡張した。
という感じでしょうか。

TopoJSON(※) は GeoJSON の拡張形式であり、トポロジーをエンコードしたものです。座標計算に固定精度エンコーディングを用いることにより TopoJSON ファイルのサイズは GeoJSON よりずっと小さくなります。今作っている地図は、GeoJSON では 536KB ですが、TopoJSON ではたったの 80KB、つまり 85% も削減されています(gzip圧縮後の比較でもずっと小さくなります)。単にディスク必要量を 削減するだけではなく、TopoJSON に含まれるトポロジー情報を使えば、境界線の自動計算や こうした面白いアプリケーションを作ることもできます。

topojson(npm)をインストール

GeoJSON→topoJsonの変換にはnode.jsにバンドルされているtopoJSONのコンバータを利用します。
そのために、node.jsのパッケージ管理ツールであるnpmをインストールして、
npmからtopojsonをインストールします。

node.jsのパッケージ管理はnodebrewというのが流行っているらしいです。
ここを参考にさせていただきました。
http://qiita.com/ryosy383/items/2ea9c92a792302b159b0

nodebrewをインストール(すぐ終わった

$ brew install nodebrew

node.jsの最新版をインストール(結構かかった

$ nodebrew install latest

latest版を利用する設定

$ nodebrew use latest

nodeのパスを通す。
viで.bash_profileに以下を追記する。

export PATH=$PATH:$HOME/.nodebrew/current/bin

npmが使えるようになったか確認のためにヘルプを見る

$ npm help

これでnpmが使えるようになる!

最後にtopojsonのインストール

$ npm install -g topojson

topojsonがインストールできたか確認

$ which topojson

準備完了!

GeoJSON→topoJSON

topojsonに変換(一瞬

$ cd ne_50m_admin_0_countries
$ topojson --id-property su_a3 -p NAME=name -p name -o worldmap.topojson countries.geojson 

これでokです。

topojsonはいくつかのgeoJSONを結合して1つのjsonにすることもできるので
最後にgeoJSONの引数を幾つか追加すると結合されたtopoJSONを取得できます。

今回は1つのgeoJSONから1つのtopoJSONを生成。

d3.jsで地図を表示

やっときました!
ここからが本番

ここからはこのチュートリアルが手腕をふるいます。
素晴らしいチュートリアル。
詳しいことはだいたい説明してくれているので端折ります。

index.htmlを配置

いい感じのディレクトリを作成して、index.htmlを配置します。

$ mkdir worldmap
$ cd worldmap

index.html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>world map</title>
        <style>

/* CSS goes here. */

        </style>
    </head>
    <body>
        hello world map
        <script src="http://d3js.org/d3.v3.min.js"></script>
        <script src="http://d3js.org/topojson.v0.min.js"></script>
        <script>

/* JavaScript goes here. */

        </script>
    </body>
</html>

ローカルでWebサーバを起動

Webブラウザではローカルのjsonなどを利用するためにはwebサーバを起動する必要があります。
(詳しくはクロスドメインで検索)
http://blog.qaramell.com/?p=11971

ここではMacではお手軽で有名なpythonさんでワンライナーでローカルにWebサーバを起動します。

$ cd worldmap
$ phthon -m SimpleHTTPServer 8008 &

たったこれだけで、
ローカルホストにWebサーバが起動されて、さっき作った /wolrdmap/index.html を
ブラウザで確認できます!

http://localhost:8008

まだこれでは"hello world map"と表示されるだけですが、もうすこしのはず。

topoJSONを描画してみる

index.htmlがあるディレクトリにdataディレクトリを作成して生成したtopojsonを配置しました。

$ cd worldmap
$ cd mkdir data
$ cp (さっき生成したところ)/ne_50m_admin_0_countries/worldmap.topojson ./data/

topojsonを配置したら
/* JavaScript goes here. */
のところにこんなかんじで書いてみます。

var width = 1600;
    height = 900;

var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);


/* JavaScript goes here. */
d3.json("data/worldmap.topojson", function(error, worldmap) {
    svg.append("path")
      .datum(topojson.object(worldmap, worldmap.objects.countries))
      .attr("d", d3.geo.path().projection(d3.geo.mercator()));
});

これで更新してみるとこんな感じ!

スクリーンショット 2015-08-11 15.37.09.png

70度より北らへんがスパット切れちゃってるので、
場所を動かしたりスケールを変えたりしましょう。

国ごとのパスを分割する

さっきの状態ではすべてのパスがひとつなぎになってしまっているので
国ごとに色を変えるみたいなことができません。

なので次のように変更します。
ついでにprojectionでスケールも変えてみました。

var width = 1600;
height = 900;

var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);


/* JavaScript goes here. */
d3.json("data/worldmap.topojson", function(error, worldmap) {

    // topojsonからgeojsonオブジェクトを生成
    var countries = topojson.object(worldmap, worldmap.objects.countries);

    // 投影方法
    var projection = d3.geo.mercator()
        .scale(150)
        .translate( [width / 2, height / 2] );

    // svgのパス要素
    var path = d3.geo.path()
        .projection( projection );

    // 各国のパスを作成
    svg.selectAll(".countries")
        .data( countries.geometries )
        .enter()
        .append("path")
        .attr("class", function(d) { return "country " + d.id; } )
        .attr("d", path)
        .style("fill", function(d){return rand_color();} );

});


function rand_color(){
    var r = Math.floor( Math.random() * 255 ).toString(16);
    var g = Math.floor( Math.random() * 255 ).toString(16);
    var b = Math.floor( Math.random() * 255 ).toString(16);

    return "#" + r + g + b;
}


こんなかんじにすると国のパスが1つづつに別れて便利になりました。

スクリーンショット 2015-08-11 17.02.14.png