トップ地図システム基礎技術 > 行政境界線
行政境界線

はじめに

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地図に境界線を描画して、OSMの boundary=administaitive と比較した。 OSM地図(日本の場合)の境界線は当初はこの国土数値情報をインポートしたようである。 その後、商用使用が禁止されたため、現在は、インポートを禁止されている。

精度が高いことを期待したが、残念ながらそうでもなかった。都市計画区域内の標準偏差は2.5m以内であったとしても、 最大誤差はもっと大きい。

OSM地図の場合、インポート後に年月を経て、手作業で修正が行われたようである。 ざっと見た限りでは、OSMの方が精度が高い。ただし、誤まって消された所があるかもしれない。 一から、手作業で書いた境界線(東京都23区の境界線)では、大きく間違えているところがあった。 恐らくマッパーの先入観で間違えたのであろう。

要するに、全体的には、OSM地図の境界線の方が精度が高い。 しかし、場所によっては、OSM地図の境界線は大きく間違っていたり、誤って、消されている可能性がある。

おわりに

取り急ぎ、全都道府県の境界線データをダウンロードする必要はない。 気になったところだけ、ダウンロードして、境界線を比較する。

OSMデータから抽出した主要境界線は別ファイルとして管理する。誤って、消された境界線があった場合、 保存された境界線データを使用する。

座標値列データは差分コードにより、4割前後縮小される。通常はメモリに常駐させる必要はないが、 統計処理などで、全国の主要境界線データを使う場合、必要なメモリは多分 30MB程度で済むであろう。

リファレンス

[1] 利用上の注意(位置精度等について)
[2] 基盤地図情報の整備・更新・提供のあり方について
[3] 国土数値情報ダウンロードサイト: 行政区域データ
[4] 地図データ用クラウドデータベースと作るWebアプリ

バイナリ変換プログラム

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");    // 神奈川県
    }
}