トップMy OpenStreetMap > 陸地ポリゴン
OSMデータから陸地ポリゴン構築
OSM海岸線パーサー(by SAX)

陸地ポリゴン

Overpass API で得た海岸線データ(wayオブジェクト) と Geofabrik からダウンロードした europe-latest.osm.pbf などから陸地ポリゴンを作り出すことを試みたが、データ取得日時のずれから、 wayオブジェクトで参照している node の一部が *.osm.pbf には存在しない。

Overpass APIでは、世界規模の海岸線データを node情報を含めて一度に取り出すことはできない。 分割した場合には、時間的なずれがあるため、繋ぎ合わせで問題が出てくる可能性が高い。

Geofabrik の場合、全ての *.osm.pbf がある時刻の planet.osm から切り出されたものと期待する。 もし、europe-latest.osm.pbf と asia-latest.osm.pbf の元となる OSMデータの時刻が異なるならば europe と asia にまたがる海岸線データがぴったり繋がらないケースが出てくるだろう。

たとえ時刻に食い違いはなくても、海岸線データにエラーがあった場合は更新しないといった 対策は必要であるが、まずは、 *.osm.pbf から海岸線wayデータを抽出してみよう。 [2023.1.27]

はじめに

OpenStreetMapの陸地のレンダリングは一般にはOSMデータから作り出されたシェイプファイル[3]を使う。

自作OSM地図では、現在は Mapnik を使わないため、シェイプファイルのままでは使えない。 これまでは、このシェイプファイルをPostgreSQL/PostGISデータベースに一旦インポートして、 OpenGIS の WKT(Well Known Text)形式でエクスポートしたものを使ってきた。

手間はかかるが、プログラム的には楽である。

今回は、より深く OpenStreetMap を理解するために、 OSMデータから直接陸地ポリゴンデータを取り出すことを試みた。

Overpass APIにより海岸線データを得る

世界全体の海岸線データを Overpass APIで得ることができる[5]。

wayオブジェクト natural=coastline については以下のようにして、Overpass API で入手できる。 2GB近いが、9分弱で得られた。

日本全体の osmファイルは 40GB近いので、それに比べれば、5%程度に過ぎないので、 パソコンで解析できる。

c:\gisa>curl --globoff -o c:/osm/world_coastline.xml -d @c:/osm/overpass_coastline.xml http://overpass-api.de/api/interpreter
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1953M    0 1953M    0   176  3764k      0 --:--:--  0:08:51 --:--:-- 4379k
<osm-script timeout="900" element-limit="2150000000"><!-- 60 minutes、約 2GB -->
  <query type="way">
    <has-kv k="natural" v="coastline"/>
  </query>
  <print/>
</osm-script>

このクエリで得られるのは次のような wayオブジェクトのみである。 狭い範囲であれば、参照されている node オブジェクトも得ることができるが、 世界全体に対しては不可能である。

<way id="131026052" [後略] >
<nd ref="1442787079"/>
<nd ref="1442787122"/>
<nd ref="1442787154"/>
<nd ref="1442787072"/>
<nd ref="1442787061"/>
<nd ref="1442787096"/>
<nd ref="1442787105"/>
<nd ref="1442787157"/>
<nd ref="1442787160"/>
<nd ref="1442787150"/>
<nd ref="1442787034"/>
<tag k="natural" v="coastline"/>

海岸線wayを構成する全nodeの Id を得る

<nd ref="1442787150"> から数値 1442787150 を取り出すのは簡単であり、SAX を使うまでもない。

    static void loadCoastlineNodes(String path) {
        num_nodes = 0;
        try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(new FileInputStream(path))) ) {
            String line;
            while ((line = br.readLine()) != null) {
                int ix = line.indexOf('<');
                if (ix < 0) continue;
                String s = line.substring(ix);      // 行先頭のスペースを削除する
                if (s.startsWith("
                    int bgn = 9;
                    int end = s.indexOf("\"", bgn);
                    String ref = s.substring(bgn, end);
                    long idRef = Long.parseLong(ref);
                    nodes[num_nodes++] = idRef;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

取り出された node ID は約 71M であり、時間は1分とかからない。

node の座標値を得る

Overpass APIといえども、一挙に 71M ノードの情報は得られない。 分割すれば Overpass API で入手できるが、Overpass API の場合、 使用制限があるため、あれこれ試行錯誤できない。

今回は、Geofabrikサイトから latest-europe.osm.pbf、latest-asia.osm.pbf など合わせて8つの pbf ファイルから ノード情報を取り出した。

pbfファイルでも合計約 50GB となり、解凍すると1TB 近くにもなるため、 下のようにパイプ接続で node の座標値を取り出す。

osmosis でも解凍できるが、時間がかかりすぎる。

osmconvert d:/downloads/europe-latest.osm.pbf | java LandPolygon europe

プログラム自体は比較的簡単である。LandPolygon は wayセクションのデータしか使用せず、 先に終了するため、osmconvert は最後は write error となる。 LandPolygonの処理が終わった時点で強制終了させてよい。

/*
javac LandPolygon.java

osmosis  --rbf d:\downloads\europe-latest.osm.pbf  --wx file=- | java LandPolygon europe
osmconvert  d:/downloads/europe-latest.osm.pbf | java LandPolygon europe
*/

import java.io.*;
import java.util.*;
import java.time.*;

public class LandPolygon {
    static int E7 = 10000000;
    static long[] nodes = new long[80000000];
    static int num_nodes;
    static int ixNode;

    static void loadCoastlineNodes(String path) {
        num_nodes = 0;
        try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(new FileInputStream(path))) ) {
            String line;
            while ((line = br.readLine()) != null) {
                int ix = line.indexOf('<');
                if (ix < 0) continue;
                String s = line.substring(ix);      // 行先頭のスペースを削除する
                if (s.startsWith("<nd ")) {    // <nd ref="7943616013"/>
                    int bgn = 9;
                    int end = s.indexOf("\"", bgn);
                    String ref = s.substring(bgn, end);
                    long idRef = Long.parseLong(ref);
                    nodes[num_nodes++] = idRef;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void parseXML(String name) {
        char quote = '"';
        int cntFound = 0;
        String pathOut = "c:/osm/" + name + "_idlonlat.dat";
        long idCurr = nodes[0]; // 最初に探すノード
        ixNode = 0;
        // 標準入力
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
             DataOutputStream dos = new DataOutputStream(new FileOutputStream(pathOut))) {
            String line;
            while ((line = br.readLine()) != null && ixNode < num_nodes) {
                int ix = line.indexOf('<');
                if (ix < 0) continue;
                String s = line.substring(ix);  // 行先頭のスペースを削除する
                if (s.startsWith("<node ")) {   // nodeオブジェクト
                    int bgnId = 10;  // <node id="31252942" lat="35.6528795" lon="139.7573991" ... >
                    int endId = s.indexOf(quote, bgnId);
                    String strId = s.substring(bgnId, endId);
                    long idNode = Long.parseLong(strId);

                    while (idNode > idCurr) {
                        if (ixNode >= num_nodes) return;
                        idCurr = nodes[ixNode++]; 
                    }

                    if (idNode < idCurr) {  
                        continue;
                    } else if (idNode == idCurr) {  // 海岸線ノード
                        cntFound++;
                        idCurr = nodes[ixNode++];   // 次に探すノード
                        int bgnLon = s.indexOf("lon=", endId);
                        int endLon = s.indexOf(quote, bgnLon+5);
                        String strLon = s.substring(bgnLon+5, endLon);
                        int bgnLat = s.indexOf("lat=", endId);
                        int endLat = s.indexOf(quote, bgnLat+5);
                        String strLat = s.substring(bgnLat+5, endLat);
                        int lon = (int)(Double.parseDouble(strLon)*E7);
                        int lat = (int)(Double.parseDouble(strLat)*E7);
                        dos.writeLong(idNode);
                        dos.writeInt(lon);
                        dos.writeInt(lat);
                        //System.out.printf("%d: %d %d\n", idNode, lon, lat);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        Instant instant = Instant.ofEpochSecond(start/1000);
        ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
        System.out.println(zonedDateTime);

        loadCoastlineNodes("c:/osm/world_coastline00.xml");
        System.out.println("海岸線ノード数=" + num_nodes);
        Arrays.sort(nodes, 0, num_nodes-1);
        System.out.println("並び替え(昇順)終了");
        parseXML(args[0]);
        System.out.printf("実行時間=%.1f分\n",
            (System.currentTimeMillis() - start)/60000.0 );
    }

}

出力データは node id(8バイト)、lon(4バイト)、lat(4バイト)のバイナリレコードファイルである。

CPU負荷やディスク(SSD)負荷は低いので、同時に2,3ジョブを実行することができる。 全体では 実行時間は 2.5~3 時間となった。

高速化の余地はあるが、陸地ポリゴンデータの更新は精々年に1、2回に過ぎないので、 プログラムを複雑にしない方がよいと思われる。

海岸線データの繋ぎ合わせとポリゴン出力

繋ぎ合わせは、node id のままで実行できる。出力するときに、id を座標値に変える。 最終的にはバイナリレコード形式とするが、まずは、PostGISのエクスポートと同じ WKT形式とする。

元々OSMデータ構造はシンプルであるが、今回は way データのみであるから、SAXも使わず、 自前でパースする。

リファレンス

[1] OSM陸地ポリゴンをOSMデータから構築する
[2] OSM海岸線パーサー(by SAX)
[3] Data Derived from OpenStreetMap for Download
[4] 空間オブジェクトの表現: OpenGIS WKT形式
[5] OSM Overpass API
[6] OpenStreetMapのデータ構造