6、7年前のことであるが、OSM(OpenStreetMap)の行政境界線の精度が非常に低かった。市街地(神奈川県と東京都の境界)で数10mの誤差があった。
国土地理院地図の場合、ズームレベル15~18に記載される地物の水平精度は、原則として、17.5m以内(標準偏差)となっている[1]。
行政境界線の精度については、基準省令第2条第2項で、都市計画区域内では平面位置の誤差が2.5m以内、都市計画区域外では25m以内と規定されている[2]。?
OSMの行政境界線は誤って変更されたり、消されることもある。
国土数値情報:行政区域データをダウンロードして、OSM地図上に行政境界線を描いて、精度を確認してみたい。
神奈川県のデータをダウンロードした。解凍したxmlファイルは 40MB弱となった。
ファイルの上段に Pointデータがある。市とか町の代表点であろうか?
<jps:GM_Point id="n00012">
<jps:GM_Point.position>
<jps:DirectPosition>
<DirectPosition.coordinate>35.56139800 139.63333100</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:DirectPosition>
</jps:GM_Point.position>
</jps:GM_Point>
境界線データは次のようになっている。idから境界線が何かわかるのであろうが、 まずは、精度を確認したいので、極座標値列データで境界線を描いてみる。
<jps:GM_Curve id="c00234">
<jps:GM_Primitive.proxy idref="c00234"/>
<jps:GM_Primitive.proxy idref="_c00234"/>
<jps:GM_OrientablePrimitive.orientation>+</jps:GM_OrientablePrimitive.orientation>
<jps:GM_OrientablePrimitive.primitive idref="c00234"/>
<jps:GM_Curve.segment>
<jps:GM_LineString>
<jps:GM_CurveSegment.interpolation>linear</jps:GM_CurveSegment.interpolation>
<jps:GM_LineString.controlPoint>
<jps:GM_PointArray>
<GM_PointArray.column>
<jps:GM_Position.indirect>
<GM_PointRef.point idref="n00236"/>
</jps:GM_Position.indirect>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26616500 139.72910900</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26617600 139.72914600</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26617300 139.72916500</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26618900 139.72920300</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26621400 139.72922900</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26623100 139.72925600</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26622800 139.72928400</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26617800 139.72932000</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26613700 139.72934900</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26609200 139.72937300</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26606900 139.72937000</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26604000 139.72932100</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26600700 139.72926200</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26598900 139.72923600</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26599500 139.72921100</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26601000 139.72918000</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26602400 139.72910800</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.direct>
<DirectPosition.coordinate>35.26603900 139.72908700</DirectPosition.coordinate>
<DirectPosition.dimension>2</DirectPosition.dimension>
</jps:GM_Position.direct>
</GM_PointArray.column>
<GM_PointArray.column>
<jps:GM_Position.indirect>
<GM_PointRef.point idref="n00236"/>
</jps:GM_Position.indirect>
</GM_PointArray.column>
</jps:GM_PointArray>
</jps:GM_LineString.controlPoint>
</jps:GM_LineString>
</jps:GM_Curve.segment>
</jps:GM_Curve>
まずは精度のチェックだけなので、当初、直接、xmlファイルを Android に読込むプログラムを作った。 プログラムは動作するが、緯度、経度のパースでしばしばエラーが起きた。
error: text=' 139.55460800' length=13 start=0そのためパソコンで、xmlをバイナリレコードに変換することにした。
ファイル形式は暫定的なものであるが、次のようにした。
緯度、経度は 107をかけて整数化した。
先頭に、そのファイル全体の境界ボックスを置き、次に、レコード数を置いた。
その後ろに、レコード毎の境界ボックス、境界線のノード数、座標値列データを置いた。
minlonAll, minlatAll, maxlonAll, maxlatAll num_records minlon, minlat, maxlon, maxlat num_nodes, lon0, lat0, lon1, lat1, ..., lonL, latL
とりあえず、東京都と神奈川県だけをダウンロードして変換した。実行時間はAndroidタブレットより、1桁速い。 東京は細分化され、島嶼部も多いためであろうか、神奈川県の約2倍のファイルサイズで、レコード数約5倍となった。
東京都 3300records 1.84MB 実行時間: 0.02分 神奈川県 613records 983KB 実行時間: 0.01分
OSM地図に境界線を描画して、OSMの boundary=administaitive と比較した。 OSM地図(日本の場合)の境界線は当初はこの国土数値情報をインポートしたようである。 その後、商用使用が禁止されたため、現在は、インポートを禁止されている。
精度が高いことを期待したが、残念ながらそうでもなかった。都市計画区域内の標準偏差は2.5m以内であったとしても、 最大誤差はもっと大きい。
OSM地図の場合、インポート後に年月を経て、手作業で修正が行われたようである。 ざっと見た限りでは、OSMの方が精度が高い。ただし、誤まって消された所があるかもしれない。 一から、手作業で書いた境界線(東京都23区の境界線)では、大きく間違えているところがあった。 恐らくマッパーの先入観で間違えたのであろう。
要するに、全体的には、OSM地図の境界線の方が精度が高い。 しかし、場所によっては、OSM地図の境界線は大きく間違っていたり、誤って、消されている可能性がある。
取り急ぎ、全都道府県の境界線データをダウンロードする必要はない。 気になったところだけ、ダウンロードして、境界線を比較する。
OSMデータから抽出した主要境界線は別ファイルとして管理する。誤って、消された境界線があった場合、 保存された境界線データを使用する。
座標値列データは差分コードにより、4割前後縮小される。通常はメモリに常駐させる必要はないが、 統計処理などで、全国の主要境界線データを使う場合、必要なメモリは多分 30MB程度で済むであろう。
import java.io.*;
import java.nio.*;
import java.util.*;
import java.nio.file.Paths;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
public class Parser extends DefaultHandler {
static class PointArray {
int minlon, minlat, maxlon, maxlat;
int[] points; // lon, lat
}
static void parseAdmin(String name) throws Exception{
String path = "c:/map/admin/" + name + ".xml";
String dst = "c:/map/adm/" + name + ".dat";
long start = System.currentTimeMillis();
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
Parser handler = new Parser();
parser.parse(Paths.get(path).toFile(), handler);
PointArray[] pa = handler.listPA.toArray(new PointArray[0]);
System.out.println(pa.length + "records");
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(
new FileOutputStream(dst)));
dos.writeInt(handler.minlonAll);
dos.writeInt(handler.minlatAll);
dos.writeInt(handler.maxlonAll);
dos.writeInt(handler.maxlatAll);
dos.writeInt(pa.length);
for (PointArray rec : pa) {
dos.writeInt(rec.minlon);
dos.writeInt(rec.minlat);
dos.writeInt(rec.maxlon);
dos.writeInt(rec.maxlat);
dos.writeInt(rec.points.length/2);
for (int v : rec.points) {
dos.writeInt(v);
}
}
dos.close();
System.out.printf("実行時間: %.2f分\n", (System.currentTimeMillis()-start)/60000.0);
}
List<PointArray> listPA = new ArrayList<>();
List<Integer> list = new ArrayList<>();
int minlon, minlat, maxlon, maxlat;
int minlonAll, minlatAll, maxlonAll, maxlatAll;
Parser() {
minlonAll = minlatAll = Integer.MAX_VALUE;
maxlonAll = maxlatAll = Integer.MIN_VALUE;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes);
if (qName.equals("jps:GM_Curve.segment")) {
//System.out.println("Bgn: " + qName);
list.clear();
minlon = minlat = Integer.MAX_VALUE;
maxlon = maxlat = Integer.MIN_VALUE;
} else if (qName.equals("DirectPosition.coordinate")) {
coord = true;
//System.out.println("coord");
}
}
boolean coord;
public void characters(char[] ch, int start, int length) throws SAXException {
// 6. テキストが出現したなら、char配列をStringにしてフィールドへ保存する
if (coord) {
String text = new String(ch, start, length);
String[] val = text.split(" ");
if (val.length == 2) {
try {
int lon = (int) (Double.parseDouble(val[0]) * 10000000);
int lat = (int) (Double.parseDouble(val[1]) * 10000000);
if (lon < minlon) minlon = lon;
if (lat < minlat) minlat = lat;
if (lon > maxlon) maxlon = lon;
if (lat > maxlat) maxlat = lat;
list.add(lon);
list.add(lat);
//System.out.println(lon + ", " + lat);
} catch (Exception ex) {
System.out.println("error: text='" + text + "' length=" + length + " start=" + start);
ex.printStackTrace();
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
super.endElement(uri, localName, qName);
if (qName.equals("jps:GM_Curve.segment")) {
//System.out.println("End: " + qName);
PointArray pa = new PointArray();
pa.points = list.stream().mapToInt(i -> i).toArray();
pa.minlon = minlon;
pa.minlat = minlat;
pa.maxlon = maxlon;
pa.maxlat = maxlat;
listPA.add(pa);
if (minlon < minlonAll) minlonAll = minlon;
if (minlat < minlatAll) minlatAll = minlat;
if (maxlon > maxlonAll) maxlonAll = maxlon;
if (maxlat > maxlatAll) maxlatAll = maxlat;
} else if (qName.equals("DirectPosition.coordinate")) {
coord = false;
} else if (qName.equals("jps:GM_Curve")) {
}
}
public static void main(String[] args) throws Exception {
parseAdmin("N03-110331_13"); // 東京都
parseAdmin("N03-110331_14"); // 神奈川県
}
}