世界の測量

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

オンラインデータのクロスオリジンプロキシdejimaの作成

前回のleaflet-omnivoreの検証で気がついたことは、オンラインデータについて、フォーマットの問題はクライアントサイドで片付きつつあるのに対して、クロスドメイン問題は、CORS設定の普及が間に合っていないという問題である。この問題を当面回避するために、CORS設定をonにして中継するだけのサーバをherokuに書いてみた。

URL及び動作例

URL は http://dejima.herokuapp.com/?url=<URL> である。例えば、http://dejima.herokuapp.com/?url=https://raw.githubusercontent.com/shimizu/dataSet/master/fukui_kindergarten/fukui_kindergarten.geojson といった中継をさせていただくことができる。

プログラム

app.rb

非常に大雑把なプログラムだ。

require 'bundler/setup'
require 'sinatra/base'
require 'open-uri'

class App < Sinatra::Base
  get '/' do
    begin
      open(params['url']) do |input|
        content_type input.content_type
        headers 'Access-Control-Allow-Origin' => '*'
        input.read
      end
    rescue
      status 500
      "don't steal data."
    end
  end
end

config.ru

$:.unshift(File.dirname(__FILE__))
require 'app'
run App

Gemfile

source "https://rubygems.org"
gem "sinatra"

Procfile

web: bundle exec rackup config.ru -p $PORT

leaflet-omnivore テスト

leaflet-omnivore の考え方を理解するために、テストをしてみた。

結果

leaflet-omnivore

解説

omnivore.csv(
  'fukui_kindergarten.csv', 
  {latfield: '緯度', lonfield: '経度', delimiter: ','},
  L.geoJson(null, {
    pointToLayer: function(feature, latlng) {
      return L.marker(latlng, {icon: icon});
    },
    onEachFeature: function(feature, layer) {
      layer.bindPopup(feature.properties['保育園名']);
    }
  })
).addTo(map);

https://github.com/mapbox/leaflet-omnivorehttp://leafletjs.com/examples/geojson.html を見て頂ければ、特に解説は必要ないかもしれない。
omnivore.csv の第一引数は CSV データのURL、第二引数は csv2geojson のオプションであり、第三引数は、マーカーのオプションを明記した L.geoJSON を customLayer として指定している。

分析

  • 実は、CORSできるかどうかが(古くて新しい)課題。いまさらJSONPに戻る訳にはいかない(参考:Why JSONP is a terrible idea and I will never use it again)であろうが、実際にCORSを有効にしているサイトは少なく、データをオンラインでオープンアクセスしてくれるすべての方にCORSを有効にしてもらうことは当面難しいかもしれない。でも、オープンデータのサイトは、その定義からして、CORSを有効にしてもらってもいいのではないか。

sotm-us-2014-dc-springmeyer.pdf からの抜き書き

sotm-us-2014-dc-springmeyer.pdf からの抜き書きを試みる。

ベクトルタイルとは何か

画像タイルと同じようなもの
  • キャッシュが容易であり、高速配信できる。
  • アドレッシングスキームは画像タイルと同じ。
  • 多数の複雑なレイヤを(一種類のタイルで)表現することもできる。
  • 100%オープンソース(で実現できるもの)なので、自分で作ってみることができる。
画像タイルよりも良いもの
  • 幾何形状、道路名、建物高などソースデータを含めることができる。
  • 非常にコンパクト!地球全体をUSBスティックに収めることもできる。
  • クライアントサイド(js)でもサーバサイド(c++)でも高速にパーズできるよう設計されている。
利点・将来展望
  • 高速で効率的で劇的にカスタム化可能なデータ共有ができる!
  • ベクトルタイル+OpenGL/WebGLレンダリングこそが未来!
  • みんながそれぞれお好みのベクトルタイルを配信する近未来を想像する

構造の詳細

(省略)

いくつかのコンセプト

  • OVERZOOMING(低いズームレベルのタイルを使いまわす)
  • COMPOSITING(複数種類のタイルを一種類のタイルに束ねる)

ベクトルタイル作成のワークフロー

統計

530MBのShapefileをz=0〜10でエクスポートするのにMBPで8分程度。100万タイル程度出力されているので、秒間2000タイル程度。MBTilesデータサイズは187MB程度。

バイナリベクトルタイルのウェブ地図対応状況

http://vector.io/vector-map/ に学ばせて頂き、バイナリベクトルタイル(MVT; Mapnik Vector Tiles)のウェブ地図(Leaflet)対応状況を確認してみた。

結果

MVT on Leaflet 0.8
WebGLが必須となっている等するため、ブラウザによっては動かない場合もあると思う。

解説

ソースの解説を試みる。

<link rel="stylesheet" href="leaflet-0.8-dev.css">
中略
<script src="leaflet-0.8-dev.js"></script>

Leaflet の開発版 0.8 を使っている。leafletjs.com でのCDNホストはされていないので、vector.ioにホストされているものの写しを頂いている。

<script src="vector-map.debug.js"></script>
<script src="leaflet-hash.js"></script>

vector-map.debug.js がベクトルタイルの実装の多くを入れているようである。但し、これだけではない。leaflet-hash.js は、私が常用している、現在位置及び現在ズームレベルがURLに反映されるようにする拡張である。0.7で動くものがそのまま0.8-devでも動く。

function resizeMap () {
  document.getElementById('map').style.width = window.innerWidth + 'px';
  document.getElementById('map').style.height = window.innerHeight + 'px';
  map.invalidateSize(false);
}
window.addEventListener('resize', resizeMap);
resizeMap();

ここはおまじない的に頂いてきている。これを削除すると動かない。本来、Leafletの中に内包されるべき部分であるように思われる。invalidateSizeは、Leaflet 0.7にも存在するメソッド。

var layer = L.vectorTileLayer({
  vectorRenderer: '',
  vectorTileSource: {type: 'MapboxTileSource',
    url: 'http://a.tiles.mapbox.com/v3/mapbox.mapbox-streets-v4/' + 
      '{z}/{x}/{y}.vector.pbf',
    max_zoom: 14},
  vectorLayers: 'gl_layers_mvt.js',
  vectorStyles: 'gl_styles.js',
  numWorkers: 1,
  attribution: 'Map data &copy; OpenStreetMap contributors / ' + 
    'original code: http://vector.io/vector-map/',
  unloadInvisibleTiles: false,
  updateWhenIdle: false
});

ここが具体の読み込み設定部分なる。vectorRendererは、削除すると動かなかった。vectorTileSourceがデータソースの指定になっている。vectorLayersが指しているgl_layers_mvt.jsは、データの読み込み方を指定しているように見える。vectorStylesが指しているgl_styles.jsは、読み込んだデータのレイヤごとの色設定を指定しているように見える。しかし、すべての配色設定を示せていないように見える。例えば、背景色の設定は、WebGL環境のデフォルトか何かに委ねられているように見える。

window.addEventListener('load', function () {
  layer.addTo(map);
  frameLoop();
});

function frame(){layer.render();}
function frameLoop () {
  frame();
  requestAnimationFrame(frameLoop);
}

(function requestAnimationFrameCompatibility () {
    if (window.requestAnimationFrame == undefined) {
        window.requestAnimationFrame = (function () {
            return (
                window.requestAnimationFrame       ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame    ||
                window.oRequestAnimationFrame      ||
                window.msRequestAnimationFrame     ||
                function (callback) {
                   window.setTimeout(callback, 1000 / 60);
                }
            );
        })();
    }
}());

やや特有なことを書いているようにみえる。本来、Leafletの中に内包されるべき部分であるように思われる。

分析

http://vector.io/vector-map/ と比べると比較的地に足のついたユースケースを志向しているkともあり、L.VectorTileLayer について、やや先進的なvectorTileSourceとやや保守的なvectorRendererの組み合わせを試してみたいところであるが、L.VectorTileLayer が幅をもって実装されているかどうか等をつかむことはまだできていない。調査が尽くせていないところではあるが、直感的には、今すぐに採択すべき技術であるというよりは、引き続き観察を続けて、適切な時期に採択すべきようにしておく技術であるように思える。
他方、MVT形式については、かなり高速であるように思えた。

地理空間情報クリアリングハウスのCKAN APIを叩いてみる Pt. 2

地理空間情報クリアリングハウスのCKAN APIを叩いてみる Pt. 1 - 世界の測量の続き。

Get a full JSON representation of a dataset, resource or other object (cnt'd)

http://ckan.gsi.go.jp/api/3/action/tag_show?id=気象

特定タグのデータセットのリストを取り出す。ここで、地理空間情報クリアリングハウスではタグの値に日本語を用いているところがひとつの懸案点であるが、モダンブラウザでは難なくクリア。ただし、検索結果を日本語で見たいのでスクリプトを組んでみる。ruby で。

# 04.rb CC0
require 'uri'
require 'open-uri'
require 'json'

url = URI.escape('http://ckan.gsi.go.jp/api/3/action/tag_show?id=気象')
print JSON.pretty_generate(JSON.parse(open(url).read)['result'])

require 'uri' して、URI.escape をかけたところがポイント。

$ ruby 04.rb
{
  "vocabulary_id": null,
  "packages": [
    {
      "owner_org": "a3584284-76c1-45be-874a-18a8b2eea606",
      "maintainer": null,
      "relationships_as_object": [

... 膨大なので途中省略 ...

  "display_name": "気象",
  "id": "cdeb2a73-6ce7-46a7-bca6-9023749bc1b8",
  "name": "気象"
}

OK。

Search for packages or resources matching a query

http://ckan.gsi.go.jp/api/3/action/package_search?q=福江

# 05.rb CC0
require 'uri'
require 'open-uri'
require 'json'

url = URI.escape('http://ckan.gsi.go.jp/api/3/action/package_search?q=福江')
print JSON.pretty_generate(JSON.parse(open(url).read))

結果はOKであるが、長いので省略。

http://ckan.gsi.go.jp/api/3/action/resource_search?query=title:2万5千分1地形図

エラーが出る。409 Conflict が返る。研究が必要。以下は、スクリプト(クエリ内容が少し違う)。

# 06.rb CC0
require 'uri'
require 'open-uri'
require 'json'

url = URI.escape('http://ckan.gsi.go.jp/api/3/action/' +
  'resource_search?query=title:2万5千分1地形図 白野江')
print JSON.pretty_generate(JSON.parse(open(url).read))

Get an activity stream of recently changed datasets on a site

http://ckan.gsi.go.jp/api/3/action/recently_changed_packages_activity_list

こんな感じで取れる。

# 07.rb CC0
require 'uri'
require 'open-uri'
require 'json'

url = URI.escape(
  'http://ckan.gsi.go.jp/api/3/action/recently_changed_packages_activity_list')
print JSON.pretty_generate(JSON.parse(open(url).read))

地理空間情報クリアリングハウスのCKAN APIを叩いてみる Pt. 1

地理空間情報クリアリングハウスにはAPIが用意されている。http://docs.ckan.org/en/latest/api/index.htmlを参考に、試しに叩いてみる。

Get JSON-formatted lists of a site’s datasets, groups or other CKAN objects

http://ckan.gsi.go.jp/api/3/action/package_list

データセットのリストを取得する。大量のidが出てくるので、数えてみよう。ruby で。

# 01.rb CC0
require 'open-uri'
require 'json'

url = 'http://ckan.gsi.go.jp/api/3/action/package_list'
p JSON.parse(open(url).read)['result'].size
$ ruby 01.rb
67144

なるほど、http://ckan.gsi.go.jp/dataset で表示されている「67,144 件のデータセットが見つかりました」と整合している。

http://ckan.gsi.go.jp/api/3/action/group_list

この結果は、要するに

{"success": true, "result": []} 

である。これは、http://ckan.gsi.go.jp/group にあるとおり、「地理空間情報クリアリングハウス(試行版)では、現在のところグループ機能については使っておりません。」であるためであろう。

http://ckan.gsi.go.jp/api/3/action/tag_list

タグリストを取得する。この結果では、日本語がUnicodeエスケープされていて、ブラウザの直接表示ではよくわからない。デコードしてみよう。ruby で。

# 02.rb CC0
require 'open-uri'
require 'json'

url = 'http://ckan.gsi.go.jp/api/3/action/tag_list'
p JSON.parse(open(url).read)['result']
$ ruby 02.rb 
["位置", "健康", "全地球基本地図画像", "公共事業_通信", "土地台帳計画", "地球科学の情報", "境界", "大洋", "構造物", "気象", "環境", "生物相", "社会", "経済", "農業", "運輸", "陸水", "高さ"]

OK。rubyjson gem を使えば、Unicode エスケープのデコードは、自動的に行われるので意識しなくても良いということだ。ところで、この分類は、JMP 2.0 に規定されているものだ。

Get a full JSON representation of a dataset, resource or other object

http://ckan.gsi.go.jp/api/3/action/package_show?id=02og5y53

上記の中のidの値は、http://ckan.gsi.go.jp/api/3/action/package_list の result の中から適当に持ってきたもの。この結果も当然 Unicode エスケープされているので、分かるように書き出してみよう。ruby で。

# 03.rb CC0
require 'open-uri'
require 'json'

url = 'http://ckan.gsi.go.jp/api/3/action/package_show?id=02og5y53'
print JSON.pretty_generate(JSON.parse(open(url).read)['result'])
$ ruby 03.rb
{
  "license_title": "ライセンスが指定されていません",
  "maintainer": null,
  "relationships_as_object": [

  ],
  "private": false,
  "maintainer_email": null,
  "revision_timestamp": "2014-03-13T16:31:51.996500",
  "id": "306b40a3-1187-4164-9176-7e2819130d3a",
  "metadata_created": "2014-03-13T16:31:51.996500",
  "owner_org": "58918f38-4259-4e68-aaea-ae9ab815a21b",
  "metadata_modified": "2014-03-14T06:38:10.634074",
  "author": null,
  "author_email": null,
  "state": "active",
  "version": null,
  "license_id": "notspecified",
  "type": "dataset",
  "resources": [
    {
      "resource_group_id": "95840f3a-55ac-48ca-86c0-3ee23bcf9388",
      "cache_last_updated": null,
      "revision_timestamp": "2014-03-13T16:31:57.645488",
      "webstore_last_updated": null,
      "id": "e8dfa627-cae9-487f-948c-a638e9da9d24",
      "size": null,
      "state": "active",
      "hash": "",
      "description": "",
      "format": "XML",
      "tracking_summary": {
        "total": 0,
        "recent": 0
      },
      "mimetype_inner": null,
      "mimetype": null,
      "cache_url": null,
      "name": "115-07-02-04",
      "created": "2014-03-14T01:31:57.663548",
      "url": "http://ckan.gsi.go.jp///storage/f/2014-03-14T013152/02og5y53_resource.xml",
      "webstore_url": null,
      "last_modified": null,
      "position": 0,
      "revision_id": "f7ebb94b-8c7e-45ec-8c47-43c8c58c6b89",
      "resource_type": "file.upload"
    }
  ],
  "num_resources": 1,
  "tags": [
    {
      "vocabulary_id": null,
      "display_name": "地球科学の情報",
      "name": "地球科学の情報",
      "revision_timestamp": "2014-03-13T16:31:51.996500",
      "state": "active",
      "id": "e02a306e-c919-4ba8-bbc1-2c1608479b89"
    }
  ],
  "tracking_summary": {
    "total": 0,
    "recent": 0
  },
  "groups": [

  ],
  "relationships_as_subject": [

  ],
  "num_tags": 1,
  "name": "02og5y53",
  "isopen": false,
  "url": null,
  "notes": "##要約\n2万5千分1地形図 仁方\n##刊行日\n1970-03-30",
  "title": "2万5千分1地形図 仁方",
  "extras": [
    {
      "value": "{\"type\": \"Polygon\", \"coordinates\": [[[132.62244216549399, 34.169907390855798], [132.622439647009, 34.253230852851701], [132.747428462747, 34.253233468975701], [132.747430992234, 34.169910001425997]]]}",
      "key": "spatial",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "115-07-02-04",
      "key": "ファイル識別子",
      "__extras": {
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a",
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a"
      }
    },
    {
      "value": "JMP2.0",
      "key": "メタデータ規格",
      "__extras": {
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a",
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a"
      }
    },
    {
      "value": "地球科学の情報",
      "key": "主題分類",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "{\"organisationName\": \"国土地理院地理空間情報部情報サービス課\", \"contactInfo\": {\"address\": {\"country\": \"jpn\", \"electronicMailAddress\": \"seika@gsi.go.jp\"}}, \"role\": \"007\"}",
      "key": "問合せ先(責任者)",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "shiftJIS",
      "key": "文字集合",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "2012-07-24",
      "key": "日付",
      "__extras": {
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a",
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a"
      }
    },
    {
      "value": "日本語",
      "key": "言語",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "{\"keyword\": [\"2万5千分1地形図\", \"仁方\", \"115-7-2-4\", \"基本図\"], \"type\": \"005\"}",
      "key": "記述的キーワード",
      "__extras": {
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a",
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a"
      }
    },
    {
      "value": "shiftJIS",
      "key": "識別情報-文字集合",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "{\"EX_GeographicBoundingBox\": {\"extentTypeCode\": \"1\", \"extentReferenceSystem\": {\"authority\": {\"title\": \"測量法施行令(昭和24年政令第322号)(日本測地系)\", \"date\": {\"date\": \"1949-08-31\", \"dateType\": \"001\"}}, \"code\": \"TD / (B,L)\"}, \"westBoundLongitude\": \"132.625000\", \"eastBoundLongitude\": \"132.750000\", \"southBoundLatitude\": \"34.166667\", \"northBoundLatitude\": \"34.250000\"}}",
      "key": "識別情報-範囲1-地理要素1",
      "__extras": {
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a",
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
      }
    },
    {
      "value": "日本語",
      "key": "識別情報-言語",
      "__extras": {
        "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a",
        "package_id": "306b40a3-1187-4164-9176-7e2819130d3a"
      }
    }
  ],
  "organization": {
    "description": "",
    "title": "国土地理院",
    "created": "2014-03-12T10:21:23.872343",
    "approval_status": "approved",
    "revision_timestamp": "2014-03-14T06:38:10.634074",
    "is_organization": true,
    "state": "active",
    "image_url": "http://www.gsi.go.jp/common/000051403.gif",
    "revision_id": "8d4ee750-d7ca-4847-94a5-f977485ef01c",
    "type": "organization",
    "id": "58918f38-4259-4e68-aaea-ae9ab815a21b",
    "name": "gsi"
  },
  "revision_id": "75c08c7f-73f5-4f05-b2b1-7946d456394a"
}

とりあえず Pt.1 はここまで。引き続き API guide — CKAN 2.2a documentation を舐める形で叩いてみたい。ruby で。