トップ地図アプリGIS > OSMファイルの概略

OSMファイルの概略

1.はじめに[2015.4.26]

OSMに触れて2か月以上たって徐々に理解が深まってきた。

日本全土では OSMファイルは約 22GB と巨大である。node要素も 1億個を超える。 PGISなども少し動かしてみたが、処理時間がかかりすぎ、進捗率も表示されないので極めて使いづらい。

当面の目的は、駅、バス停、店、学校、公共施設等の数を使って、繁華街から山間部までを4段階に分けることである。 それぞれの事物について、大体の位置がわかればよい。

あれこれ調べたが、 このような目的のためには高度なデータベースシステムを使うのではなく、 目的にあわせたシンプルなシステムを作った方がいいという結論に至った。

全1億個の node要素の位置のCSVファイル出力も行ってみた。ファイルサイズは 3.25GB となった。

しかし、実際にはごく一部の node要素の位置しか必要ではない。 いま一度原点に立ち返り、もっとシンプルな方法を考えよう。

2.OSMファイルの構造

下に文献[1]の記載例を示す。

<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="OpenStreetMap server">
  <bounds minlat="51.5073601795557" minlon="-0.108157396316528" maxlat="51.5076406454029" maxlon="-0.107599496841431"/>
  <node id="319408586" lat="51.5074089" lon="-0.1080108" version="1" changeset="440330" user="smsm1" uid="6871" visible="true" timestamp="2008-12-17T01:18:42Z"/>
  <node id="319408587" lat="51.5074343" lon="-0.1081264" version="1" changeset="440330" user="smsm1" uid="6871" visible="true" timestamp="2008-12-17T01:18:42Z"/>
  <node id="275452090" lat="51.5075933" lon="-0.1076186" version="3" changeset="2980587" user="nickb" uid="1697" visible="true" timestamp="2009-10-29T12:14:35Z">
    <tag k="name" v="Jam's Sandwich Bar"/>
    <tag k="amenity" v="cafe"/>
  </node>
  <node id="304994980" lat="51.5074645" lon="-0.1075735" version="2" changeset="388960" user="BiIbo" uid="3516" visible="true" timestamp="2009-02-13T00:41:47Z">
    <tag k="barrier" v="gate"/>
  </node>
  <node id="304994981" lat="51.5074723" lon="-0.1075014" version="1" changeset="374143" user="Matt" uid="70" visible="true" timestamp="2008-10-16T16:35:57Z"/>
  <node id="304994979" lat="51.507406" lon="-0.1083348" version="4" changeset="2114003" user="jamicu" uid="38244" visible="true" timestamp="2009-08-12T01:33:32Z"/>
  <way id="27776903" visible="true" timestamp="2009-05-31T13:39:15Z" version="3" changeset="1368552" user="Matt" uid="70">
    <nd ref="304994979"/>
    <nd ref="319408587"/>
    <nd ref="319408586"/>
    <nd ref="304994980"/>
    <nd ref="304994981"/>
    <tag k="access" v="private"/>
    <tag k="highway" v="service"/>
  </way>
</osm>

ここには親要素としては node要素と way要素しかないが、下に示すように、OSMファイルにはもう一つ relation要素がある。

        <way id="303325454" version="1">
                <nd ref="3076796320"/>
                <nd ref="3076796328"/>
                <nd ref="3076796329"/>
                <nd ref="3076796322"/>
                <nd ref="3076796320"/>
                <tag k="name" v="Amida-do belfry"/>
                <tag k="amenity" v="place_of_worship"/>
                <tag k="building" v="yes"/>
                <tag k="religion" v="buddhist"/>
                <tag k="denomination" v="Tendai"/>
        </way>

        <relation id="4038873" version="1">
                <member type="way" ref="303325445" role="inner"/>
                <member type="way" ref="303325447" role="inner"/>
                <member type="way" ref="303325448" role="outer"/>
                <tag k="type" v="multipolygon"/>
                <tag k="amenity" v="place_of_worship"/>
                <tag k="building" v="yes"/>
                <tag k="religion" v="buddhist"/>
                <tag k="denomination" v="Tendai"/>
        </relation>

relation要素の子要素memberは type属性によりway要素かnode要素である。 way要素の子要素ndは node要素である。

tag要素はnode要素、way要素、relation要素いずれについても子要素となりうる。 日本全土ではなく東京と近辺の1GB強のOSMファイルについてそのことを確認した。

例えば神社や寺(amenity:place_of_worship)はほとんどが node要素で表されているが、 上の例のように、way要素で寺社の形を多角形で表したり、 relation要素で更に複雑な形を複数の多角形で表したりする。

ひとつのway要素やrelation要素直属のtag要素は、node要素と同じく 精々ひとつの事物しか含まないものと考えていいだろう。

way要素には複数のnode要素を含むことができるため、一つの way要素には複数の事物を含むことができる。

relation要素には複数のmember要素(多分node要素かway要素)を含むから当然relation要素には複数の事物を含むことができる。

上記1GB強のOSMファイルでは Node数=862万、Way数=128万、Relation数=5,435 であった。 スコア計算では除外しているノードもある。また、厄介なことに、OSMファイルには古い情報も含まれている。 正確には、バージョンチェックなどがいるが、今回は、少々の誤差はあってもいいので、簡単な方法で重複を除いた。 この結果、無効が約 1.4万ノードあった。有効が約 65万ノードであり、これでスコア計算をした。

relation要素が直属のtagにより事物を表わさない場合、member要素である node要素または way要素に含む 事物の位置は、node要素または way要素の処理で行えるため、relation要素としての処理は一切いらない。

先に示した例のように、relation要素が直属のtagにより事物を表わしている場合、位置の特定が必要となる。 この場合、最初の member要素で位置を決めることにする。 type="way" の場合、その wayの最初の node要素で位置を決める。

OSMファイルでは node, way, relation順に並んでいるから、どのway要素が relationで参照されるか分からない。

1回目のパスでは、relation要素のみの処理を行い、参照する node要素のid または way要素のid のみをファイル出力する。

2回目のパスでは、way要素のみの処理を行う。way要素に事物を表す tag要素があれば、先頭のnode要素の id を出力する。 relation要素から参照されるway要素の場合、同様に先頭のnode要素の id を出力する。

3回目のパスでは、node要素自体に事物を表す tag要素があれば lon, lat, key, value, name を出力する。 2回目のパスで出力された id に対しても同様の項目を出力する。 このため、1回目、2回目のパスでは id 以外に key, value, name も出力しておく必要がある。

1回目、2回目で出力されるファイルのサイズはさほど大きくないため、メモリに読み込んで処理を行う。 最終ファイルも日本全土に対して精々40MB~50MBである。

3.OSMパーサー

今回使用したパーサーを下に示す。

OSMファイルは node, way, relation の順に並んでいる。処理はその逆で relation, way, node の順となる。 従って、1回目のパスで、way および relation の先頭を記憶しておき、 2回目のパスでは wayの先頭からrelationの先頭まで、 3回目のパスでは ファイルの先頭からwayの先頭まで、とした方がスキップ処理はいらなくなる。

しかし、このプログラムは滅多に使わないので、その配慮はしていない。 また、relation, way の処理では、高速化のために行の先頭の4バイトを切り出しているが、 node の処理ではそうしていない。 これは、最初に node処理だけのプログラムを作っており、 後から relation, way の処理を追加した。 node 処理を書き換えするのは面倒なため、以前のプログラムをそのまま使用した。 このため、統一性のないプログラムとなっている。

当初 XMLTextReader を用いたが、階層処理が分かりにくく、実行時間的にも有利な面が感じられなかったため、 使用をやめ、独自プログラムとした。

プログラムから分かるように全てのオブジェクトを抽出しているわけではなく、 絞り込みを行っている。

relation要素は800個程度であるが、way要素は多く、抽出されたオブジェクトは合わせて 12.7万個になった。 一方、node要素から抽出されたオブジェクトは 57.4万個となった。

using System;
using System.Collections.Generic;       // List, Dictionary
using System.IO;
using System.Text;

class Tag {
    public string k, v;

    public Tag(string line) {
        int ixBgn = line.IndexOf("k=");
        int ixEnd = line.IndexOf("\"", ixBgn+3);
        k = ixBgn > 0 ? line.Substring(ixBgn+3, ixEnd-ixBgn-3) : null;
        ixBgn = line.IndexOf("v=");
        ixEnd = line.IndexOf("\"", ixBgn+3);
        v = ixBgn > 0 ? line.Substring(ixBgn+3, ixEnd-ixBgn-3) : null;
    }
}

class Member { // <member type="way" ref="24604723" role=""/>
    public string type, refer, role;

    public Member(string line) {
        int ixBgn = line.IndexOf("type=");
        int ixEnd = line.IndexOf("\"", ixBgn+6);
        type = ixBgn > 0 ? line.Substring(ixBgn+6, ixEnd-ixBgn-6) : null;
        ixBgn = line.IndexOf("ref=");
        ixEnd = line.IndexOf("\"", ixBgn+5);
        refer = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : null;
        ixBgn = line.IndexOf("role=");
        ixEnd = line.IndexOf("\"", ixBgn+6);
        role = ixBgn > 0 ? line.Substring(ixBgn+6, ixEnd-ixBgn-6) : null;
    }
}

class Nd { // <nd ref="3076796320"/>
    public string refer;

    public Nd(string line) {
        int ixBgn = line.IndexOf("ref=");
        int ixEnd = line.IndexOf("\"", ixBgn+5);
        refer = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : null;
    }
}

class OSMParser {
    const string FileOSM = "c:/osm/japan-latest.osm";
    const string FileRelation = "c:/gisdata/relations.csv";
    const string FileWay = "c:/gisdata/ways.csv";
    const string FileNode = "c:/gisdata/nodes1.csv";
    const string FileWRNode = "c:/gisdata/wrnodes.csv";
    static Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");

    void ParseRelation() {
        int cntLine = 0;
        int cntRel = 0;
        StreamWriter writer = new StreamWriter(FileRelation, false, sjisEnc);
        using (StreamReader reader = new StreamReader(FileOSM)) {
            string line;
            Member memb = null;
            string key = null, value = null, nameja = "", name = "";
            bool fRel = false;
            while ((line=reader.ReadLine()) != null) {
                if (++cntLine%1000000 == 0) Console.WriteLine(cntLine + " rel=" + cntRel);
                line = line.Trim();
                string head = line.Length >= 4 ? line.Substring(0, 4) : line;
                if (head == "<rel") {           // <relation ... >
                    cntRel++;
                    fRel = true;
                    memb = null;
                    key = null;
                    value = null; 
                    nameja = "";
                    name = "";
                } else if (head == "</re") {    // </relation>
                    // relation要素の出力処理
                    fRel = false;
                    if (memb != null && key != null) {
                        writer.WriteLine(memb.type + "," + memb.refer + "," + key + "," 
                                        + value + ',' + (nameja.Length > 0 ? nameja : name));
                    }
                } else if (!fRel) {
                    ;
                } else {        // relation要素の処理
                    if (head == "<mem") {
                        if (memb == null) memb = new Member(line);      // 先頭メンバー
                    } else if (head == "<tag") {
                        Tag tag = new Tag(line);
                        if (key == null) {
                            if (tag.k == "amenity" || tag.k == "leisure" || 
                                tag.k == "tourism" || tag.k == "shop") {
                                key = tag.k;
                                value = tag.v;
                            } 
                        }
                        if (tag.k == "name:ja") nameja = tag.v;
                        else if (tag.k == "name") name = tag.v;
                    } else {
                        Console.WriteLine("Error: " + line);
                    }
                }
            }
            reader.Close();
        }
        writer.Close();
        Console.WriteLine(cntLine + " rel=" + cntRel);
    }


    void ParseWay() {   // <way id="303325454" version="1">
        StreamWriter writer = new StreamWriter(FileWay, false, sjisEnc);
        Dictionary<string, string[]> dictWay = new Dictionary<string, string[]>();

        using (StreamReader reader = new StreamReader(FileRelation,sjisEnc)) {
            string line;    // way,217546982,amenity,embassy,中国大使館
            while ((line=reader.ReadLine()) != null) {
                string[] items = line.Split(',');
                if (items[0] == "node") {
                    writer.WriteLine(line.Substring(5));
                } else {        // "way" の場合
                    dictWay[items[1]] = items;
                }
            }
        }

        int cntLine = 0;
        int cntWay = 0;
        using (StreamReader reader = new StreamReader(FileOSM)) {
            string line;
            Nd nd = null;
            string key = null, value = null, nameja = "", name = "";
            bool fWay = false;
            while ((line=reader.ReadLine()) != null) {
                if (++cntLine%1000000 == 0) Console.WriteLine(cntLine + " way=" + cntWay);
                line = line.Trim();
                string head = line.Length >= 4 ? line.Substring(0, 4) : line;
                if (head == "<way") {           // <way ... >
                    cntWay++;
                    fWay = true;
                    nd = null;
                    key = null;
                    value = null; 
                    nameja = "";
                    name = "";
                } else if (head == "</wa") {    // </way>
                    // way要素の出力処理
                    fWay = false;
                    if (nd != null && key != null) {
                        writer.WriteLine(nd.refer + "," + key + "," + value + ',' 
                                                  + (nameja.Length > 0 ? nameja : name));
                    }
                } else if (!fWay) {
                    ;
                } else {        // way要素の処理
                    if (head == "<nd ") {
                        if (nd == null) nd = new Nd(line);      // 先頭メンバー
                    } else if (head == "<tag") {
                        Tag tag = new Tag(line);
                        if (key == null) {
                            if (tag.k == "amenity" || tag.k == "leisure" || 
                                tag.k == "tourism" || tag.k == "shop") {
                                key = tag.k;
                                value = tag.v;
                            } 
                        }
                        if (tag.k == "name:ja") nameja = tag.v;
                        else if (tag.k == "name") name = tag.v;
                    } else {
                        Console.WriteLine("Error: " + line);
                    }
                }
            }
            reader.Close();
        }
        writer.Close();
        Console.WriteLine(cntLine + " way=" + cntWay);
    }

    void ParseNode() {  // <way id="303325454" version="1">
        string[] keys = {
            "highway", "railway", "waterway", "aerialway", "aeroway", "amenity", "shop", 
            "tourism", "sport", "historic", "public_transport", "leisure", 
            "building", "natural", "landuse", "place", "craft", "office", "man_made",
        };
        StreamWriter wrNode = new StreamWriter(FileNode, false, sjisEnc);
        StreamWriter wrWRNode = new StreamWriter(FileWRNode, false, sjisEnc);
        Dictionary<string, string[]> dictNode = new Dictionary<string, string[]>();

        using (StreamReader reader = new StreamReader(FileWay,sjisEnc)) {
            string line;    // 1078757228,leisure,park,あらやしき公園
            while ((line=reader.ReadLine()) != null) {
                string[] items = line.Split(',');
                dictNode[items[0]] = items;
            }
        }

        int cntNode = 0;
        using (StreamReader reader = new StreamReader(FileOSM)) {
            string line;  // <node id="252997573" lat="35.0579000" lon="135.8739898"/>
            while ((line=reader.ReadLine()) != null) {
                string lineNode = line.Trim();
                if (!lineNode.StartsWith("<node")) continue;
                bool fSgl = lineNode.EndsWith("/>");
                int ixBgn = lineNode.IndexOf("id=");
                int ixEnd = lineNode.IndexOf("\"", ixBgn+4);
                string id = ixBgn > 0 ? lineNode.Substring(ixBgn+4, ixEnd-ixBgn-4) : null;
                ixBgn = line.IndexOf("lat=");
                ixEnd = line.IndexOf("\"", ixBgn+5);
                string lat = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : "";
                ixBgn = line.IndexOf("lon=");
                ixEnd = line.IndexOf("\"", ixBgn+5);
                string lon = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : "";

                if (dictNode.ContainsKey(id)) {
                    string[] items = dictNode[id]; 
                    string r = lon + "," + lat + "," + items[1] + "," + items[2] + "," + items[3];
                    wrWRNode.WriteLine(r);
                }
                if (++cntNode%1000000 == 0) Console.WriteLine(cntNode);

                if (fSgl) continue;
                Dictionary<string,string> dictTag = new Dictionary<string,string>();
                string name = "";
                while ((line=reader.ReadLine()) != null) {
                    line = line.Trim();
                    if (line == "</node>") break;
                    Tag tag = new Tag(line);
                    if (!dictTag.ContainsKey(tag.k)) {
                        dictTag.Add(tag.k, tag.v);
                    }
                    if (tag.k == "name:ja" || tag.k == "name" || tag.k == "name:en") {
                        name = tag.v;
                    }
                }

                string rec = null;
                foreach (string kw in keys) {
                    if (dictTag.ContainsKey(kw)) {
                        rec = lon + "," + lat + "," + kw + "," + dictTag[kw] + "," + name;
                        wrNode.WriteLine(rec);
                        break;
                    }
                }
            }
            reader.Close();
        }
        wrNode.Close();
        wrWRNode.Close();
    }

    static void Main() {
        OSMParser parser = new OSMParser();
        parser.ParseRelation();
        parser.ParseWay();
        parser.ParseNode();
    }
}

これらのオブジェクトの位置から、次のようにして、 Zoom = 16 におけるタイル領域のスコアを求めた。

自タイルだけでなく、周辺のタイルに存在するオブジェクトも考慮して、 タイルのスコアを求めている。

このスコアが 230以上, 50以上, 5以上、5未満 によって4段階に分けた。 海は海岸近辺を除き、最初から除外している。 スコア5未満は主に山林であり、OSMファイルにおける オブジェクトがない過疎地も含まれる。

    static double Lon2X(double lon, int zoom) {
        return (lon + 180.0) / 360.0 * (1 << zoom);
    }

    static double Lat2Y(double lat, int zoom) {
        return (1.0 - Math.Log(Math.Tan(lat * Math.PI / 180.0) + 
                1.0 / Math.Cos(lat * Math.PI / 180.0)) / Math.PI) / 2.0 * (1 << zoom);
    }

        // Zoom = 16 のタイル領域のスコアを求める
        int Zoom = 16;
        Node[] nodes = Node.loadNodes();
        int xMin = int.MaxValue, xMax = int.MinValue;
        int yMin = int.MaxValue, yMax = int.MinValue;
        for (int n = 0; n < nodes.Length; n++) {
            if (!nodes[n].fValid) continue;
            double lon = nodes[n].lon;
            double lat = nodes[n].lat;
            if (lon == 0) continue;
            int x = (int)Lon2X(lon, Zoom);
            int y = (int)Lat2Y(lat, Zoom);
            if (x < xMin) xMin = x;
            if (x > xMax) xMax = x;
            if (y < yMin) yMin = y;
            if (y > yMax) yMax = y;
        }

        xMax++;
        yMax++;
        float[,] counts = new float[xMax-xMin, yMax-yMin];
        for (int n = 0; n < nodes.Length; n++) {
            if (!nodes[n].fValid) continue;
            double lon = nodes[n].lon;
            double lat = nodes[n].lat;
            if (lon == 0) continue;
            int x = (int)Lon2X(lon, Zoom);
            int y = (int)Lat2Y(lat, Zoom);
            if (nodes[n].key == "score") {
                counts[x-xMin, y-yMin] += int.Parse(nodes[n].value);
            } else if (nodes[n].value=="place_of_worship") {
                counts[x-xMin, y-yMin] += 3;
            } else if (nodes[n].value=="museum") {
                counts[x-xMin, y-yMin] += 3;
            } else if (nodes[n].value=="park") {
                counts[x-xMin, y-yMin] += 3;
            } else if (nodes[n].value=="station") {
                counts[x-xMin, y-yMin] += 3;
            } else if (nodes[n].value=="school") {
                counts[x-xMin, y-yMin] += 3;
            } else if (nodes[n].value=="university") {
                counts[x-xMin, y-yMin] += 5;
            } else {
                counts[x-xMin, y-yMin] += 1;
            }
        }

        // 3x3タイルのノード数を加算する
        float[,] scores = new float[xMax-xMin, yMax-yMin];
        for (int x = xMin; x < xMax; x++) {
            for (int y = yMin; y < yMax; y++) {
                float score = 0;//counts[x-xMin,y-yMin];
                for (int i = -1; i <= 1; i++) {
                    for (int j = -1; j <= 1; j++) {
                        if (xMin <= x+i && x+i < xMax &&
                            yMin <= y+j && y+j < yMax) { 
                            score += counts[x-xMin+i,y-yMin+j];
                        }
                    }
                }
                scores[x-xMin,y-yMin] = score;
            }
        }

スコアが 230以上, 50以上, 5以上の比率は全土の 3%、10%、30% である。 230以上(3%)、50~229(7%)、5~49(20%) を 赤、青、緑 で表した結果を下に示す。

首都圏、関西圏、中京圏などの都市部は赤となっている。 殆どの県の主要都市の中心市街地も赤となっている。

OSMでは 赤、青、緑、なし を 最高Zoom = 18, 17, 16, 15 とする。 国土地理院の場合、Zoom15 のファイルを待たず、 全土に対して Zoom16 を持つ方が所要ストレージサイズが小さくて済む。

OSMの場合、山は川や道路がなければ、海と同じである(色は異なる)。 このため、Zoom=15 までで十分である。 しかし、Zoom=16以上のファイルを持ったとしても、ファイルサイズは小さいため、実際上の負担増は小さい。

山歩きをするような場合、国土地理院の地図は等高線が描かれており、OSMに比べれば、 細い道まで描かれているケースが多いため、有用である。 ただし、山間部のファイルサイズは OSM よりはるかに大きい。

山といっても、高尾山や比叡山延暦寺などポピュラーな場所は登録情報が多く、最高Zoom は 18 または 17 となる。

OSMファイルでは登録情報が少ない場所のスコアを上げる機能も備えた。

4.地図システム

現在の地図システムは既に1、2か月使用しており、特に問題は生じていない。 しかし、とりあえず、作ったものであり、洗練されたものではない。そのため、そのソースプログラムは載せていない。

ハイキングなどの使用では、Zoomの値を変更したり、スクロールすることはあまりない。 従って、現状でもさほど困らないが、欲を言えば、スクロールがもっと高速で滑らかな方が望ましい。

上記のスコア計算に用いている情報はバス停情報など一部地図表示でも用いている。 この表示時間を短縮する必要がある。(これは簡単であり、すぐ対処できた。)

現在はスクロールによって新たなタイル画像が必要になったら、その時点で画像ファイルの読み込みや タイル画像の縮小(Zoom16のファイル4枚からZoom15のタイル画像を作る)や 拡大(Zoom16ファイルの1/4を拡大してZoom17のタイル画像を作るなど)を行っている。

スクロールを滑らかにするには、表示とファイルの読み込み、拡大縮小処理を分離(マルチスレッド化)して、 極力、表示に必要な部分よりも一回り広い範囲のタイル画像をキャッシュに置くようにすればよい。

マルチスレッドでは排他制御が必要となる分難しいが、表示と読み込みが分離される点では分かりやすくなる。


現在のキャッシュ効果を活かしてのマルチスレッド化は簡単でなさそう。 プログラムを大きく変えるのではなく、ちょっとした工夫で対応できないか検討したい。

どうも読み込みに時間がかかっているのではなく、表示に時間がかかっているようだ。 タイル画像をスクロール毎に再描画していることが原因か、それともその他の情報の描画に時間がかかるのか 明らかにしよう。

また、そもそも表示に時間がかかるのではなく、全く別の理由、例えば、移動の検出に問題があるのかも知れない。 思い込みで突っ走らず、冷静に判断したい。


1日経ってから、 改めて、Google Map と自作システムのスクロール感覚を比較してみた。 Google Map の場合、ネットアクセスに時間がかかることから、読み込みが必要な時はスクロールが止まる。 自作システムでもファイル読み込みが発生するとき、スクロールが一瞬引っかかる。 このような読み込みがいらないときのスクロールはGoogle Map と自作システムはほぼ同じであった。

従って、自作システムの表示方法に問題があるとは言えないようだ。 改良を急ぐのではなく、よりよい方法を思いついたときに、試みることにする。

5.OSMマップの情報の詳しさ

OSMは発展途上にあり、現時点では情報量不足が否めない。 学校、鉄道、郵便局などは国土地理院などがデジタルデータを公開しており、このデータを取り込んでいるため、 概ね完備している。

バス停についても本数が多い都市のバス停はチェックした範囲では完備している。 しかし、地方や観光地など1日数本のバス路線は載っていない。 チェックした路線は、Google Mapでも載っていなかった。

Google Mapではコンビニや小さな店まで余さず掲載されているが、OSMには大きな店以外殆どのっていない。 コンビニについても、チェックした範囲では2割前後しか載っていなかった。 コンビニの数は多いので、それでもそれなりに役に立つ。

著名なお寺や神社は OSM にも載っているが、小さなお寺や神社はほとんど載っていない。 Google Mapでは田舎の末寺や小さな神社まで余さず掲載されている。

結局のところ、OSMの情報はGoogle Mapなどと比較すると桁違いに少ないと言える。 このため、ハイキングに出かけたときなどでは、OSMマップと国土地理院の地図を併用している。 OSMマップは車道が中心で、見やすい。 通常は OSMマップを見て、細い道は、国土地理院の地図に切り替えて確認している。

OSMの場合、郊外の地図を Zoom 16, 17, 18 と拡大してみても、表示される情報が増えるわけではないので、 持つ意味があまりない。

個人使用ではダウンロードが許される、より詳しい情報を持つ地図があれば、それを併用するほうがいい。

6.抽出の見直し

Zoom 16 の範囲はそれほど増やさなくてもよいが、Zoom 17, 18 の範囲は増やした方がよさそう。

抽出情報は引き続き見直したい。building=yes は名前なしを除外する。

way, relation では building=yes が抽出できていない。

        <way id="192411926" version="1">
                <nd ref="2029729947"/>
                <nd ref="2029729951"/>
                <nd ref="2029729945"/>
                <nd ref="2029729943"/>
                <nd ref="2029729947"/>
                <tag k="name" v="ふるさとパレス"/>
                <tag k="building" v="yes"/>
        </way>

way, relationでは抽出条件

    if (tag.k=="amenity" || tag.k=="leisure" || tag.k=="tourism" || tag.k=="shop") {
としているのをnodeタグ処理に合わせる。ノードファイルが大きくなりすぎるのを避けるため、 現在、ダウンロードプログラムで行っている絞り込みをパーサーに移す。

修正後のプログラムを下に示す。必要に応じて、今後も見直しを行う。

using System;
using System.Collections.Generic;       // List, Dictionary
using System.IO;
using System.Text;

class Tag {
    public string k, v;

    public Tag(string line) {
        int ixBgn = line.IndexOf("k=");
        int ixEnd = line.IndexOf("\"", ixBgn+3);
        k = ixBgn > 0 ? line.Substring(ixBgn+3, ixEnd-ixBgn-3) : null;
        ixBgn = line.IndexOf("v=");
        ixEnd = line.IndexOf("\"", ixBgn+3);
        v = ixBgn > 0 ? line.Substring(ixBgn+3, ixEnd-ixBgn-3) : null;
    }
}

class Member { // <member type="way" ref="24604723" role=""/>
    public string type, refer, role;

    public Member(string line) {
        int ixBgn = line.IndexOf("type=");
        int ixEnd = line.IndexOf("\"", ixBgn+6);
        type = ixBgn > 0 ? line.Substring(ixBgn+6, ixEnd-ixBgn-6) : null;
        ixBgn = line.IndexOf("ref=");
        ixEnd = line.IndexOf("\"", ixBgn+5);
        refer = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : null;
        ixBgn = line.IndexOf("role=");
        ixEnd = line.IndexOf("\"", ixBgn+6);
        role = ixBgn > 0 ? line.Substring(ixBgn+6, ixEnd-ixBgn-6) : null;
    }
}

class Nd { // <nd ref="3076796320"/>
    public string refer;

    public Nd(string line) {
        int ixBgn = line.IndexOf("ref=");
        int ixEnd = line.IndexOf("\"", ixBgn+5);
        refer = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : null;
    }
}

class OSMParser {
    const string FileOSM = "c:/osm/japan-latest.osm";
    const string FileRelation = "c:/gisdata/relations.csv";
    const string FileWay = "c:/gisdata/ways.csv";
    const string FileNode = "c:/gisdata/nodes1.csv";
    const string FileWRNode = "c:/gisdata/wrnodes.csv";
    static Encoding sjisEnc = Encoding.GetEncoding("Shift_JIS");
    static string[] keys = {
        "highway", "railway", "waterway", "aerialway", "aeroway", "amenity", "shop", 
        "tourism", "sport", "historic", "public_transport", "leisure", "building",
        "natural", "landuse", "place", "craft", "office", "man_made"
    };

    void ParseRelation() {
        int cntLine = 0;
        int cntRel = 0;
        StreamWriter writer = new StreamWriter(FileRelation, false, sjisEnc);
        using (StreamReader reader = new StreamReader(FileOSM)) {
            string line;
            Member memb = null;
            string key = null, value = null, nameja = "", name = "";
            bool fRel = false;
            while ((line=reader.ReadLine()) != null) {
                if (++cntLine%1000000 == 0) Console.WriteLine(cntLine + " rel=" + cntRel);
                line = line.Trim();
                string head = line.Length >= 4 ? line.Substring(0, 4) : line;
                if (head == "<rel") {           // <relation ... >
                    cntRel++;
                    fRel = true;
                    memb = null;
                    key = null;
                    value = null; 
                    nameja = "";
                    name = "";
                } else if (head == "</re") {    // </relation>
                    // relation要素の出力処理
                    fRel = false;
                    if (nameja.Length > 0) name = nameja;       // namejaを優先する
                    if (memb != null && Select(key,value,name)
                        && key != "highway" && key != "waterway") {
                        writer.WriteLine(memb.type + "," + memb.refer + "," + key + "," 
                                        + value + ',' + name);
                    }
                } else if (!fRel) {
                    ;
                } else {        // relation要素の処理
                    if (head == "<mem") {
                        if (memb == null) memb = new Member(line);      // 先頭メンバー
                    } else if (head == "<tag") {
                        Tag tag = new Tag(line);
                        if (key == null) {
                            foreach (string kw in keys) {
                                if (tag.k == kw) {
                                    key   = tag.k;
                                    value = tag.v;
                                    break;
                                }
                            }
                        }
                        if (tag.k == "name:ja") nameja = tag.v;
                        else if (tag.k == "name") name = tag.v;
                    } else {
                        Console.WriteLine("Error: " + line);
                    }
                }
            }
            reader.Close();
        }
        writer.Close();
        Console.WriteLine(cntLine + " rel=" + cntRel);
    }

    bool Select(string key, string value, string name) {
        if (key == null) return false;
        if (key=="power" && value=="tower") return false;
        if (key=="highway" && name=="") return false;
        if (key=="waterway" && name=="") return false;
        if (key=="landuse" && name=="") return false;
        if (key=="natural" && name=="") return false;
        if (key=="natural" && value=="peak") return false;
        if (key=="railway" && value != "station") return false;
        if (key=="building" && value=="yes" && name=="") return false;
        return true;
    }

    void ParseWay() {   // <way id="303325454" version="1">
        StreamWriter writer = new StreamWriter(FileWay, false, sjisEnc);
        Dictionary<string, string[]> dictWay = new Dictionary<string, string[]>();

        using (StreamReader reader = new StreamReader(FileRelation,sjisEnc)) {
            string line;    // way,217546982,amenity,embassy,中国大使館
            while ((line=reader.ReadLine()) != null) {
                string[] items = line.Split(',');
                if (items[0] == "node") {
                    writer.WriteLine(line.Substring(5));
                } else {        // "way" の場合
                    dictWay[items[1]] = items;
                }
            }
        }

        int cntLine = 0;
        int cntWay = 0;
        using (StreamReader reader = new StreamReader(FileOSM)) {
            string line;
            Nd nd = null;
            string key = null, value = null, nameja = "", name = "";
            bool fWay = false;
            while ((line=reader.ReadLine()) != null) {
                if (++cntLine%1000000 == 0) Console.WriteLine(cntLine + " way=" + cntWay);
                line = line.Trim();
                string head = line.Length >= 4 ? line.Substring(0, 4) : line;
                if (head == "<way") {           // <way ... >
                    cntWay++;
                    fWay = true;
                    nd = null;
                    key = null;
                    value = null; 
                    nameja = "";
                    name = "";
                } else if (head == "</wa") {    // </way>
                    // way要素の出力処理
                    fWay = false;
                    if (nameja.Length > 0) name = nameja;       // name:jaを優先する
                    if (nd != null && Select(key,value,name) 
                        && key != "highway" && key != "waterway") {
                        writer.WriteLine(nd.refer + "," + key + "," + value + ',' + name);
                    }
                } else if (!fWay) {
                    ;
                } else {        // way要素の処理
                    if (head == "<nd ") {
                        if (nd == null) nd = new Nd(line);      // 先頭メンバー
                    } else if (head == "<tag") {
                        Tag tag = new Tag(line);
                        if (key == null) {
                            foreach (string kw in keys) {
                                if (tag.k == kw) {
                                    key   = tag.k;
                                    value = tag.v;
                                    break;
                                }
                            }
                        }
                        if (tag.k == "name:ja") nameja = tag.v;
                        else if (tag.k == "name") name = tag.v;
                    } else {
                        Console.WriteLine("Error: " + line);
                    }
                }
            }
            reader.Close();
        }
        writer.Close();
        Console.WriteLine(cntLine + " way=" + cntWay);
    }

    void ParseNode() {  // <way id="303325454" version="1">
        StreamWriter wrNode = new StreamWriter(FileNode, false, sjisEnc);
        StreamWriter wrWRNode = new StreamWriter(FileWRNode, false, sjisEnc);
        Dictionary<string, string[]> dictNode = new Dictionary<string, string[]>();

        using (StreamReader reader = new StreamReader(FileWay,sjisEnc)) {
            string line;    // 1078757228,leisure,park,あらやしき公園
            while ((line=reader.ReadLine()) != null) {
                string[] items = line.Split(',');
                dictNode[items[0]] = items;
            }
        }

        int cntNode = 0;
        using (StreamReader reader = new StreamReader(FileOSM)) {
            string line;  // <node id="252997573" lat="35.0579000" lon="135.8739898"/>
            while ((line=reader.ReadLine()) != null) {
                string lineNode = line.Trim();
                if (!lineNode.StartsWith("<node")) continue;
                bool fSgl = lineNode.EndsWith("/>");
                int ixBgn = lineNode.IndexOf("id=");
                int ixEnd = lineNode.IndexOf("\"", ixBgn+4);
                string id = ixBgn > 0 ? lineNode.Substring(ixBgn+4, ixEnd-ixBgn-4) : null;
                ixBgn = line.IndexOf("lat=");
                ixEnd = line.IndexOf("\"", ixBgn+5);
                string lat = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : "";
                ixBgn = line.IndexOf("lon=");
                ixEnd = line.IndexOf("\"", ixBgn+5);
                string lon = ixBgn > 0 ? line.Substring(ixBgn+5, ixEnd-ixBgn-5) : "";

                if (dictNode.ContainsKey(id)) {
                    string[] items = dictNode[id]; 
                    string r = lon + "," + lat + "," + items[1] + "," + items[2] + "," + items[3];
                    wrWRNode.WriteLine(r);
                }
                if (++cntNode%1000000 == 0) Console.WriteLine(cntNode);

                if (fSgl) continue;
                Dictionary<string,string> dictTag = new Dictionary<string,string>();
                string name = "";
                while ((line=reader.ReadLine()) != null) {
                    line = line.Trim();
                    if (line == "</node>") break;
                    Tag tag = new Tag(line);
                    if (!dictTag.ContainsKey(tag.k)) {
                        dictTag.Add(tag.k, tag.v);
                    }
                    if (tag.k == "name:ja" || tag.k == "name" || tag.k == "name:en") {
                        name = tag.v;
                    }
                }

                string rec = null;
                foreach (string kw in keys) {
                    if (dictTag.ContainsKey(kw)) {
                        string val = dictTag[kw];
                        if (Select(kw, val, name)) {
                            rec = lon + "," + lat + "," + kw + "," + val + "," + name;
                            wrNode.WriteLine(rec);
                        }
                        break;
                    }
                }
            }
            reader.Close();
        }
        wrNode.Close();
        wrWRNode.Close();
    }

    static void Main() {
        OSMParser parser = new OSMParser();
        parser.ParseRelation();
        parser.ParseWay();
        parser.ParseNode();
    }
}

この変更により、wayおよびrelation要素によるオブジェクトが大幅に増加し、35万件となった。 一方、node要素自体によるオブジェクトは43万件である。無効ノードを除くと、合わせて 77万件である。 これまでの結果より10%強の増加である。

Zoomレベル 16, 17, 18 のタイル数は 52万、72万、85万から 53万、74万、90万に増加した。 パーセンテージとしては、2%, 3%, 5% 増である。

Zoomレベル 17, 18 のタイル数はもう少し増やしたい。

7.日本の領域

文献[2]では日本の領域を下図に示す多角形で表している。 一部、他国が含まれるかも知れないが、多分、漏れはないのであろう。

この多角形は下の様に定義されている。 日本を矩形で表そうとすると、他国が多く含まれるので、 この多角形を参考にしよう。

none 1 
1.538901E+02 2.638211E+01 
1.321529E+02 2.646809E+01 
1.316915E+02 2.120992E+01 
1.225954E+02 2.351966E+01 
1.225607E+02 2.584146E+01 
1.288145E+02 3.474835E+01 
1.293966E+02 3.509403E+01 
1.353079E+02 3.754740E+01 
1.405769E+02 4.570648E+01 
1.491891E+02 4.580245E+01 
1.538901E+02 2.638211E+01 
END END 

この多角形と外接する矩形は (1.225607E+02, 2.120992E+01)、 (1.538901E+02, 4.580245E+01) である。 タイル画像は (zoom, X, Y)で表される。

次のような関数によって、(lon, lat) を (x, y) に変換できる。

    double Lon2X(double lon, int zoom) {
        return (lon + 180.0) / 360.0 * (1 << zoom);
    }

    double Lat2Y(double lat, int zoom) {
        return (1.0 - Math.Log(Math.Tan(lat * Math.PI/180.0) + 
                1.0 / Math.Cos(lat * Math.PI/180.0)) / Math.PI) / 2.0 * (1 << zoom);
    }

タイル画像の処理では、多角形をXY座標で表現し、外接矩形を (Xmin, Ymin, Xmax, Ymax) で表すと 対象範囲は次のようになる。

for (int x = Xmin; x <= Xmax; x++) {
    for (int y = Ymin; y <= Ymax; y++) {
        if (タイル(x,y)と多角形に重なりがある) {
            タイル(x,y) の処理;
        }
    }
}

C# では矩形に対しては Rectangle.IntersectsWithメソッドがある。 しかし、残念ながら矩形と多角形とのIntersectsWithメソッドは見つからなかった。

凸多角形にある点が含まれるかどうかは、比較的簡単に判定できるはずなので、 クラスライブラリにあってもよさそうな気がする。 もう少し探して見つからなければ、ネットの事例[3],[4],[5]を参考にしてプログラムを書こう。

タイルの4隅のいずれかが多角形に含まれるかどうかを判定することになる。

    static bool IsInPolygon(Point[] poly, Point point) {
        var coef = poly.Skip(1).Select((p, i) => 
                       (point.Y - poly[i].Y)*(p.X - poly[i].X) 
                     - (point.X - poly[i].X)*(p.Y - poly[i].Y)).ToList();
        if (coef.Any(p => p == 0)) return true;
        for (int i = 1; i < coef.Count(); i++) {
            if (coef[i] * coef[i-1] < 0) return false;
        }
        return true;
    }
static bool PointInPolygon(Point p, Point[] poly) {
    Point p1, p2;
    bool inside = false;

    if (poly.Length < 3) { return inside; }
    Point oldPoint = new Point(poly[poly.Length-1].X, poly[poly.Length-1].Y);
    for (int i = 0; i < poly.Length; i++) {
        Point newPoint = new Point(poly[i].X, poly[i].Y);
        if (newPoint.X > oldPoint.X) {
            p1 = oldPoint;
            p2 = newPoint;
        } else {
            p1 = newPoint;
            p2 = oldPoint;
        }
        if ((newPoint.X < p.X) == (p.X <= oldPoint.X)
                && ((long)p.Y - (long)p1.Y) * (long)(p2.X - p1.X)
                 < ((long)p2.Y - (long)p1.Y) * (long)(p.X - p1.X)) {
            inside = !inside;
        }
        oldPoint = newPoint;
    }
    return inside;
}
int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy) {
  int i, j, c = 0;
  for (i = 0, j = nvert-1; i < nvert; j = i++) {
    if ( ((verty[i]>testy) != (verty[j]>testy)) &&
	 (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) ) {
       c = !c;
    }
  }
  return c;
}
//nvert:  Number of vertices in the polygon. Whether to repeat the first vertex at the end is discussed below.  
//vertx, verty:  Arrays containing the x- and y-coordinates of the polygon's vertices.  
//testx, testy:  X- and y-coordinate of the test point.  

地図システムでは、極座標をX-Y座標に変換する処理も含まれるため、どこかでミスったかも知れないが、 最初のIsInPolygonメソッドではおかしな結果となった。 PointInPolygonメソッドによる結果はまだ精査していないが、ざっと見た感じでは問題ないと思われる。 とりあえずはPointInPolygonメソッドを使ってみる。

ズームレベルが小さい範囲ではこの多角形の全領域に対してダウンロードすることに問題はないが、 ズームレベルが大きくなると、ファイル数が巨大となるため、処理時間が膨大となる。

そのため、これまでの処理では、 例えば、ズームレベル10で海であったタイルは、ズームレベル11では4タイルに分かれるが全て海と考えて 処理を省略してきた。

また、国土地理院の地図では、ズームレベルが大きくなると、沿岸部分を除き海のタイル画像は存在しない。 したがって、これを読み込もうとすると、エラーとなる。これを避けるためにも、無駄な読み込みをすべきでない。

但し、海であっても、航路などが描かれていることがあり、ズームレベルが大きくなると、 今までになかった文字情報などが表示されているケースがごくまれにあるかも知れない。 このようなタイル画像の保存が漏れる可能性はある。

個人利用では、海を精査することはないため、このようなケースが実際にあるかどうかも未確認であり、 特に不都合は感じていない。

A.リファレンス

[1] JA:OSM XML
[2] Download OpenStreetMap data for this region: Japan
[3] C# Point in polygon
[4] Determine if the point is in the polygon, C#
[5] PNPOLY - Point Inclusion in Polygon Test