トップOpenStreetMap > OSM Node座標値のファイル管理
OSM Node座標値のファイル管理

はじめに

OSMのデータ構造[1]は分かりやすい。しかし、lineデータや polygonデータのノード(頂点)データは ノード番号であって、座標値ではない。

狭い範囲の地図データであれば、node の座標値をメモリ上で管理できるが、 日本地図とか世界地図ではノード数が多すぎて、数GBのメモリには載りきれない。

5年ほど使ってきたプログラムでは、日本全体のノード座標値データを複数のブロックに分けて、 圧縮してメモリに載せている。一部のブロックだけをキャッシュとして、解凍している。

最終的にはこの方法を継続して使用することになるかも知れないが、できれば、 もっとシンプルな方法でノードの座標値を管理したい。

ノード座標値データをシンプルな構造でファイル管理する

ファイル読み込みにより多少パフォーマンスが悪くなってもよい。 ノードID、経度、緯度を単純なバイナリ配列(ノードID順)とする。 ノードID の上位何ビットかを ブロックID として、ブロック別のファイルとする。

ノードIDは 8バイトであるが、ブロックID を使えば、ファイル上は 4バイトでよい。 ノード当たり、4+4+4 = 12 バイトとなる。

japan.osm のノード数を 250M と見込んだ場合、3GB となる。 これだけならばメモリに載る大きさであるが、wayデータにも大きなメモリが必要となるため、 node で使うメモリは 1~2 GBとするため、 大半はファイル上に置き、一部をキャッシュとして、メモリに置く。

座標値データの探索はバイナリサーチを使う。 Java の場合、Arrays.binarySearchメソッドを使えば、プログラムは極めて簡単で済む。

探索の引数は osm_id であるが、連続番号など、近い番号が続くケースが多いため、 前回と近いときは、バイナリサーチは使わず、前回の位置から順次探索する方が速い。

最初のプログラム

完成後は OSMParser とする予定であるが、完成までは Parser とする。

Parserは基本的には OSM XMLファイルの全行をパースする。 属性については無視するものが多いが、処理上のロスは小さいと考え、まずは SAX APIを使ってみる。

処理時間がかかりすぎるようであれば、自前のパーサに置き換える。

最初は node セクションの処理だけのため、プログラムは次のようになる。

japan.osm の空読み時間は 9分 となった。自前プログラムとしなくても大丈夫であろう。 因みに、現在の OSMParserの実行時間は全体では 38分であり、 特に、wayオブジェクトの処理に時間がかかっている。

public class Parser extends DefaultHandler {

    @Override
    public void startElement (String namespaceURI, String localName, String qName, Attributes atts ) {
        if ("node".equals(qName)) {   // nodeオブジェクトの処理
         //   System.out.println( "id=" + atts.getValue("id") );
         //   System.out.println( "lon=" + atts.getValue("lon") );
         //   System.out.println( "lat=" + atts.getValue("lat") );
        } else if ("way".equals(qName)) {
            // パース終了
        }
    }

    public static void main( String[] args ) throws Exception {
        long start = System.currentTimeMillis();
        String src = args.length >= 1 ? args[0] : "c:/osm/japan.osm";
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
        Parser handler = new Parser();
        parser.parse(Paths.get(src).toFile(), handler);
        System.out.printf("実行時間: %.2f分\n", (System.currentTimeMillis()-start)/60000.0);
    }
}
c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m OSMParser japan.osm norm

最大Node Id = 10,231,804,665
最大Way  Id =  1,118,748,330
最大Rel  Id =     14,958,014
minPolys = 3
maxPolys = 358
空読み時間 = 4.08分
nodeパース時間 = 4.93分
wayパース時間 = 27.50分
relation処理時間 = 1.14分
Tagコード変換時間 = 0.00分
レコード書き込み時間 = 13.03分
OSMParser実行時間 = 37.73分
レコード数 34607040/34607040, 最大レコード長 4390116B(4287.2KB), ファイルサイズ 3810.2MB
 maxOuter: 548708(85F64), maxInner: 4793(12B9)

ノード座標値の処理

まず、ID と極座標を取り出し、整数化する。実行時間は 9.33分となった。

    final double E7 = 10000000.0;

    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        if ("node".equals(qName)) {   // nodeオブジェクトの処理
            long id = Long.parseLong(atts.getValue("id"));
            int lon = (int)(Double.parseDouble(atts.getValue("lon")) * E7);
            int lat = (int)(Double.parseDouble(atts.getValue("lat")) * E7);
        } else if ("way".equals(qName)) {
            // パース終了
        }
    }

ブロック毎に sub id、lon、lat を出力するようにした。実行時間は 11.2分となった。 ブロック数は 605、総ファイルサイズは 2.54GB となった。

import java.io.*;
import java.nio.file.Paths;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class Parser extends DefaultHandler {
    final double E7 = 10000000.0;
    final int BlockBits = 24;       // Block番号 id >> BlockBits
    final int BlockSize = 5000000;

    int currBlock = -1;
    int lastBlock = -1;
    int cntNodes = 0;
    int maxNodes = 0;

    int[] sids = new int[BlockSize];
    int[] lons = new int[BlockSize];
    int[] lats = new int[BlockSize];

    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        if ("node".equals(qName)) {   // nodeオブジェクトの処理
            long id = Long.parseLong(atts.getValue("id"));
            int blockNumber = (int)(id >> BlockBits);
            if (blockNumber != currBlock) {     // 新しいブロックが始まった
                if (lastBlock >= 0) {           // 前のブロックをファイルに書き込む
                    String path = "c:/osm/sidlonlats/" + lastBlock + ".dat";
                    try (DataOutputStream dos = new DataOutputStream(
                            new BufferedOutputStream(new FileOutputStream(path)))) {
                        for (int i = 0; i < cntNodes; i++) {
                            dos.writeInt(sids[i]);
                            dos.writeInt(lons[i]);
                            dos.writeInt(lats[i]);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if (cntNodes > maxNodes) maxNodes = cntNodes;
                    System.out.printf("%d %d/%d\n", lastBlock, cntNodes, maxNodes);
                    cntNodes = 0;
                }
                lastBlock = currBlock;
                currBlock = blockNumber;
            }
            sids[cntNodes] = (int)(id & ((1<= 1 ? args[0] : "c:/osm/japan.osm";
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
        Parser handler = new Parser();
        parser.parse(Paths.get(src).toFile(), handler);
        System.out.printf("実行時間: %.2f分\n", (System.currentTimeMillis()-start)/60000.0);
    }
}

最終的には nodeセクションでは nodeの座標値データの出力だけではなく、タグをパースして、 ポイントレコードを出力するが、その前にパフォーマンスの見通しを付けるために、 wayセクションの処理を行う。

ノード座標値の取り出し

次に、ノード座標値の取り出しだけを行った。ノード座標値データは一部だけをメモリに置き、 必要に応じてスワップする仕組みにしているが、今のところ、全てをメモリに置かないと、 スワップに時間がかかり過ぎる。

kanto.osm および japan.osm に対する実行時間を下に示す。

前半の時間が長いのは、ファイルへの書き込みとファイルからの読み込みに時間がかかるためである。

ファイル管理を全面的にやめれば、プログラムはもっと簡単になり、実行時間も短縮する。 現在にOSMの日本地図データでは 5GB の主メモリが使える場合は問題はないが、 もっと大きなデータを扱う場合には、何らかの方法でデータサイズを縮小しないとメモリ不足になる。

スワップによるパフォーマンス劣化はこれほど大きいとは思わなかった。 どこかに不備があるかも知れない。

c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m Parser c:/osm/kanto.osm
100万 189950106 1.69分
200万 385886896 1.82分
300万 558946275 1.92分
400万 725347405 2.02分
500万 849865383 2.11分
600万 1012313038 2.21分
実行時間: 2.30分

c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m Parser
100万 109558932 8.48分
200万 118896760 8.68分
300万 129656349 8.87分
400万 151799302 9.10分
500万 176647489 9.29分
600万 199159815 9.46分
700万 237257311 9.64分
800万 297680074 9.76分
900万 361676392 9.89分
1000万 389144232 10.00分
1100万 420773372 10.11分
1200万 449643588 10.22分
1300万 483282560 10.32分
1400万 523962051 10.43分
1500万 554035212 10.53分
1600万 577758689 10.63分
1700万 603957342 10.73分
1800万 625593406 10.83分
1900万 665255659 10.93分
2000万 704605019 11.04分
2100万 733490874 11.13分
2200万 756457716 11.23分
2300万 790622198 11.34分
2400万 829932859 11.44分
2500万 881160441 11.54分
2600万 930982044 11.64分
2700万 974634995 11.74分
2800万 1006947494 11.84分
2900万 1041709924 11.93分
3000万 1081645714 12.03分
3100万 1117508196 12.13分
実行時間: 12.17分
// javac Parser.java OSMLib.java Tag.java 
// java -Dfile.encoding=UTF-8 -Xmx5000m Parser c:/osm/japan.osm

import java.io.*;
import java.nio.file.*;
import java.util.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;


class NodeCache<K,V> extends LinkedHashMap<K,V> {
    protected int limit = 700;

    public NodeCache() {
        super(501, 0.75F, true);    // true: LRU, false: FIFO
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > limit;
    }
}

class NodeBlock {
    final static int BlockBits = 24;       // Block番号 id >> BlockBits
    final static int BlockSize = 5000000;

    static NodeCache<Integer,NodeBlock> cacheNodes = new NodeCache<>();

    int[] sids;
    long[] lonlats;

    int sidLast = -1, ixLast = -1, blkLast = -1;

    public NodeBlock(int size) {
        sids = new int[size];
        lonlats = new long[size];
    }

    static int getBlockNumber(long id) { return (int)(id >> BlockBits); }
    static int getSubId(long id) { return (int)(id & ((long)(1<<BlockBits) - 1)); }

    static long getLonLat(long id) {
        int blockNumber = getBlockNumber(id);
        int sid = getSubId(id);     // sub id

        NodeBlock blk = cacheNodes.get(blockNumber);
        if (blk == null) {
            blk = load(blockNumber);
            if (blk == null) {
                System.out.printf("no file: blockNumber=%d\n", blockNumber);
                return Long.MIN_VALUE;   // エラー
            }
            cacheNodes.put(blockNumber, blk);
        }

        if (Math.abs(sid - blk.sidLast) < 10 && blk.blkLast == blockNumber) {
            if (sid > blk.sidLast) {
                for (int ix = blk.ixLast; blk.sids[ix] <= sid; ix++) {
                    if (blk.sids[ix] == sid) {
                        blk.sidLast = sid;
                        blk.ixLast = ix;
                        blk.blkLast = blockNumber;
                        return blk.lonlats[ix];
                    }
                }
            } else if (sid < blk.sidLast) {
                for (int ix = blk.ixLast; blk.sids[ix] >= sid; ix--) {
                    if (blk.sids[ix] == sid) {
                        blk.sidLast = sid;
                        blk.ixLast = ix;
                        blk.blkLast = blockNumber;
                        return blk.lonlats[ix];
                    }
                }
            }
        }

        int ix = Arrays.binarySearch(blk.sids, sid);
        if (ix >= 0) {
            blk.sidLast = sid;
            blk.ixLast = ix;
            blk.blkLast = blockNumber;
        } else {
            System.out.printf("no! id=%x blk=%x sid=%x\n", id, blockNumber, sid);
        }
        return ix >= 0 ? blk.lonlats[ix] : Long.MIN_VALUE;
    }

    static long fileSize(String path) {
        try { return Files.size(Paths.get(path)); }
        catch (Exception e) { e.printStackTrace(); return 0;  }
    }

    static NodeBlock load(int block_id) {
        String path = Parser.dirSidLonLat + block_id + ".dat";
        int num_nodes = (int)(fileSize(path)/12);
        if (num_nodes == 0) return null;
        NodeBlock blk = new NodeBlock(num_nodes);
        try (DataInputStream dis = new DataInputStream(
               new BufferedInputStream(new FileInputStream(path)))) {
            for (int i = 0; i < num_nodes; i++) {
               blk.sids[i] = dis.readInt();
               blk.lonlats[i] = dis.readLong();
           }
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return blk;
    } 
}


public class Parser extends DefaultHandler {
    final double E7 = 10000000.0;
    final static String dirSidLonLat = "c:/osm/sidlonlats/";
    static long start = System.currentTimeMillis();

    int currBlock=-1;
    int cntNodes=0, maxNodes=0;
    boolean fNodeSection = false;
    boolean fWaySection = false;
    int cntWay = 0;
    int[] sids = new int[NodeBlock.BlockSize];  // ノード座標値のパースで使用
    long[] lonlats = new long[NodeBlock.BlockSize];

    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        if (!fWaySection && "node".equals(qName)) {
            if (!fNodeSection) fNodeSection = true; 
            procNode(atts); // nodeオブジェクトの処理
        } else if ("way".equals(qName)) {
            long id = Long.parseLong(atts.getValue("id"));
            if (fNodeSection) {
                fNodeSection = false;   // nodeセクションは終わった
                procNodeEnd();  // nodeセクションの後処理
            }
            if (!fWaySection) {
                fWaySection = true;
            }
            if (++cntWay % 1000000 == 0) {
                System.out.printf("%d万 %d %.2f分\n", cntWay/10000, id,
                             (System.currentTimeMillis() - start)/60000.0);
            }
        } else if (fWaySection && "nd".equals(qName)) {
            long nid = Long.parseLong(atts.getValue("ref"));
            long lonlat = NodeBlock.getLonLat(nid);
            //System.out.printf("%d %d %d %d\n", 
            //  NodeBlock.getBlockNumber(nid), nid, lonlat>>32, lonlat&0xffffffffL);
        }
    }

    void writeNodeBlock(int blockNumber, int[] sids, long[] lonlats, int num_nodes) {
        String path = dirSidLonLat + blockNumber + ".dat";
        try (DataOutputStream dos = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(path)))) {
            for (int i = 0; i < num_nodes; i++) {
                dos.writeInt(sids[i]);
                dos.writeLong(lonlats[i]);
            }
        } catch (Exception e) {
             e.printStackTrace();
        }
    }

    void procNode(Attributes atts) {
        long id = Long.parseLong(atts.getValue("id"));
        int blockNumber = NodeBlock.getBlockNumber(id);
        if (blockNumber != currBlock) {     // 新しいブロックが始まった
            if (currBlock >= 0) {           // 前のブロックをファイルに書き込む
                writeNodeBlock(currBlock, sids, lonlats, cntNodes);
                if (cntNodes > maxNodes) maxNodes = cntNodes;
                cntNodes = 0;
            }
            currBlock = blockNumber;
        }
        sids[cntNodes] = NodeBlock.getSubId(id);
        int lon = (int)(Double.parseDouble(atts.getValue("lon")) * E7);
        int lat = (int)(Double.parseDouble(atts.getValue("lat")) * E7);
        lonlats[cntNodes++] = (((long)lon)<<32) + (lat&0xffffffffL);
    }

    void procNodeEnd() {
        writeNodeBlock(currBlock, sids, lonlats, cntNodes);
    }

    public static void main( String[] args ) throws Exception {
        long start = System.currentTimeMillis();
        String src = args.length >= 1 ? args[0] : "c:/osm/japan.osm";
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
        Parser handler = new Parser();
        parser.parse(Paths.get(src).toFile(), handler);
        System.out.printf("実行時間: %.2f分\n", (System.currentTimeMillis()-start)/60000.0);
    }
}

おわりに

スワップが起きる状態では予想以上に時間がかかる。wayセクションの処理をスワップが少なるように、 複数パスで行うなど、何らかの工夫が必要である。

ブロック内の検索はハッシュは要らないことが分かった。

リファレンス

[1] OpenStreetMapのデータ構造
[2] 【Java】binarySearchを使って配列(Array)の要素から値を検索する!
[3] SAXによるXML文書の操作
[4] LinkedHashMapを単純なLRU/FIFOキャッシュとして使う