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で得ることができる[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"/>
<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分とかからない。
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も使わず、 自前でパースする。