ここで OSMデータベースとは osm2pgsql で PostgreSQL+PostGIS に取り込んだものを指す。 おそらく、このデータベースが一番広く使われているものと思うが、 他にもいくつかのデータベースが使われている。
このOSMデータベースには低ズームのタイル画像ファイル作成用の planet_osm_roads も ある。低ズームでは広域のデータが必要となるため、 これから述べる三つのテーブルを使うと時間がかかりすぎるため、 必要なデータだけに絞り込んだものである。
中高ズームで使うのは planet_osm_line、planet_osm_point、planet_osm_polygon である。 OSM XMLファイルには node, way, relation があるが、この区分とは異なる。
planet_osm_line には道路や川など、ライン状のデータが格納される。 XMLファイルでは主に way(複数のnodeから成る) であるが、複数の way をつないだ relation も含まれる。
planet_osm_point は店、学校、公園、建物などの位置を点で決めたもので、 XMLファイルでは全てひとつの node である。
planet_osm_polygon は公園のエリアや病院・学校の敷地、建物の形(平面図)などを表すもので、 閉ループの way が主で、複数の way をつなぎ合わせて閉ループを構成する relation も含まれる。
XMLファイルには node, way, relation には osm_id と呼ばれる ID が付与されている。 別々に付与されているため、node, way, relation の osm_id には一致するものがある。
planet_osm_pointテーブルのレコードがもつ osm_id は全て node の osm_id である。
planet_osm_line, planet_osm_polygon テーブルのレコードがもつ osm_id は way かまたは relation の osm_id である。両者を区別するため、way の場合は 正、 relation の場合は 負の値で表される(osm_id は 1 から始まる)。
この osm_id はデータベース・テーブルのレコードを特定するユニークなものかというとそうではない。 例えば
select * from planet_osm_polygon where name='川崎市';を実行すると、該当レコードは二つあり、osm_id は同じである。 これは名前が '川崎市' という一つ relation であり、二つのメンバーで構成される。 面積(way_area)の広い上の行が、川崎市の主エリア、下が、町田市にある麻生区の飛び地である。
osm_id admin_level boundary name population z_order way_area way -2689476 7 administrative 川崎市 1489564 0 3.64339e+008 省略 -2689476 7 administrative 川崎市 1489564 0 2.21018e+006 省略(上とは異なる)
planet_osm_lineも同様であり、例えばバス路線はひとつの way のこともあるが、大抵は、 複数の部分経路をメンバーとする relation である。 データベース上には、同じ relation の osm_id をもつレコードが複数存在する。
ちなみに、バス路線は、独自に way を引くものではなく、道路の断片をそのまま、 バス路線の断片とするものである。したがって、一般には、way は生成されず、relation のみ作成される。
way のバス路線も少しあった。これは、道路と重ねるように、独自に way を引いたものであろう。 極めて稀には、一般の車は通行できないバス専用の道路があるかも知れない。
一番簡単なrelationは、中庭のあるロの字形の建物である。 外の矩形(building=yes)を書いて、中の矩形(area=yes)を書いて結合すれば、外がouter、中が inner の multipolygon になる。
湖の中にある島も同じ。中(inner)が何もなし(area=yes)ではなく、適切なものを描画する。
バス路線を書いた経験もある。これは少し面倒である。 誰かが書いた近所のバス路線に誤りがあるのに気づいているが、直すのが面倒である。 間違った部分を削除して、正しい部分経路を追加すればいいのだが、面倒なので、ほっている。
難しそうなのは行政境界など、広域にわたるもの。上の'川崎市'がそれ。 osm2pgsql が部分境界線をつないで、一つの閉多角形にしてくれる。
シンプルXMLファイルをデータベースに読み込むのは簡単である。 osm2pgsqlの場合、数GBのメモリを使っても、読み込みに数時間かかる。 結構複雑な処理をしているということである。(現在はかなり高速化されている)
osmデータベースをダンプとかリストアにはそれほど時間はかからない。 ダンプは 10分とかからなかった。サイズは 2GB を超えた。
このダンプファイルをタブレットに移し、リストアすればタブレットでも osmデータベースを使える。
以前、ネットで OSMの行政境界は壊れていてつかえないという記事を見たことがある。 大都市や都道府県の境界は大規模であり、無数の部分経路で構成される。 一部の部分経路は道路や公園等の境界と一部重なったとき、過失や誤解で、消去されてしまうことがある。
一部が消されただけでもはや閉路とならない。osm2pgsql は polygon として抽出しない。
例えば、川の riverbank などは、relation を使わず、 無数の wayによるpolygon をつなげていく方が無難である。 消されても、一部が消えるだけで、被害は少ない。
relationによる巨大な polygon は一挙に全滅する恐れがある。
過失や誤解による削除は誰でも冒せるのに対して、 行政境界のような relation をメンテナンスできる人は限られているであろう。
次節で述べるように、ある市に含まれる公園や学校などを抽出するのは簡単である。 しかし、境界線が時折壊されるかも知れないことを承知しておかねばならない。
ある市に含まれる公園は ST_Within関数を使うと抽出できる。市は当然 polygon であるが、 公園は point のケースと polygon のケースがある。このため、クエリが二つになっている。
void SaveCityParks(string city, string dst) { var sql1 = "SELECT inn.sqn, inn.osm_id, inn.my_tag, inn.my_name " + "FROM planet_osm_polygon out, planet_osm_point inn " + "WHERE (inn.my_tag='leisure=park' or inn.my_tag='leisure=playground') " + " and inn.my_name is not null" + " and out.place = 'city' and out.my_name = '" + city + "' " + " and ST_Within(inn.way, out.way);"; var sql2 = "SELECT inn.sqn, inn.osm_id, inn.my_tag, inn.my_name " + "FROM planet_osm_polygon inn, planet_osm_polygon out " + "WHERE (inn.my_tag='leisure=park' or inn.my_tag='leisure=playground') " + " and inn.name is not null and inn.sqn != out.sqn" + " and out.place = 'city' and out.my_name = '" + city + "' " + " and ST_Within(inn.way, out.way);"; SaveCityParks(sql1, BP, city, dst, false); SaveCityParks(sql2, BA, city, dst, true); } void SaveCityParks(string sql, int baseSqn, string city, string path, bool fAppend) { using (StreamWriter writer = new StreamWriter(path, fAppend)) using (var conn = new NpgsqlConnection(connString)) { conn.Open(); var command = new NpgsqlCommand(sql, conn); var dr = command.ExecuteReader(); while (dr.Read()) { int sqn = int.Parse(dr[0].ToString()) + baseSqn; string osm_id = dr[1].ToString(); if (osm_id[0] == '-') osm_id = osm_id.Replace("-", "r"); else osm_id = (baseSqn == BP ? "n" : "a") + osm_id; writer.WriteLine("{0}\t{1}\t{2}\t{3}", sqn, osm_id, dr[2], dr[3]); } } }
同じことを amenity=school についても行った。 川崎市の場合、町田市内に麻生区の飛び地があるが、ここにある小学校も正しく抽出できていた。
他府県に同名の市がある場合、府県名も指定しなければならないが、一つのレコードには city しかない。
横浜市内に鎌倉市の飛び地があるが、この場合、同じ位置に、city とその下位の地名のレコードがある。 mapperが境界relationに県名は登録していないのでわからない。
やはり、ST_Withinを使って、ある県にある市の osm_id を抽出しておき、 市名ではなく osm_id を使って poligon を特定すればいいだろう。
つまり、都道府県名、市名から 市(poligon)の osm_id を抽出する。 次いで、その polygon に含まれる公園のpoint, polygon を抽出する。
Wikiによれば同名の市は府中市(東京都、広島県)、伊達市(北海道、福島県)の二つしかないようだ。 同名の区、郡、町などはもっと多くある。 概ね、都道府県を指定すれば区別できるだろうが、 より下位の地名の場合、都道府県と市町村名の指定がいるケースもあるだろう。
1: 106106816,14500.6,139.5015139,35.36766,place=city,鎌倉市,0,402624,20180305 2: 106106817,14500.6,139.5015139,35.36766,place=9,関谷,0,2480327,20161217
公園は一般には leisure=park であるが、 子供向けの場合 leisure=playground とタグ付けするマッパーもいる。
上のクエリでは公園の中に 名前付きの playground があれば、両方が抽出される。
inn.my_tag='leisure=park' としているところは inn.leisure = 'park' と置き換えてもよい。 厳密には、稀に結果が少し異なることがある。
OSM には leisure, amenity, shop, tourism など数多くのタグがある。 一つの地物に複数のタグを付けてもよい。 地図システムとしては代表的な key を選んでそのアイコンを表示する。
my_tag はあるルールでその代表を選んでいるため、 leisureタグがあっても別のタグが my_tag に設定されるケースも稀にある。
因みに アイスクリーム店は、持ち帰りは shop=ice_cream で、その場で食べる店は amenity=ice_cream であるが、中には amnity=fast_food としているケースもある。
タグの使いわけは、人によって異なるので、OSMデータに手を付けず、 自作地図システム用に、my_tag に極力統一した値を設定している。
上記の ST_Within関数は最初に重複表示の抑止に使用した。 OSM(Open Steet Map)はその名の通り、当初は道路地図であった。 公園、学校、病院などは最初は点データとして描画された。 それが年を経て、公園のエリアや学校・病院の敷地がマッピングされるようになり、 更に進んで、建物が描画されるようになった。
この結果、学校・病院の敷地の名前と元の点データの名前がダブって表示されるようになった。
この見苦しさを回避するために、polygon 内にある同じタグで同じ名前の point データを ST_Within関数で抽出して、pointデータの表示を抑止した。
公園の場合、面積が広いほど、低いズームから公園名を表示し、 字の大きさも広い公園の方を大きくしている。 このため、点データではなく、polygonデータを優先している。
自分にも経験があるが、誤って同じデータを二重登録することがある。 全く同じ位置に登録した場合、一つしか表示されないため、表示の見苦しさはない。 しかし、無駄なデータをメモリに読み込んでいる。
少し、ずれた位置に同じデータを登録した場合、 通常の zoom 15, 16 まででは二重に表示されないが、 zoom 17〜19 では二重表示され見苦しい。
下のクエリは二点間の距離が 30 未満に同じタグ、同じ名前のものがある場合、sqn が小さい方を抽出している。 もちろん、相手の sqn も求めている。
信号機(交差点)やバス停など、近くにあっておかしくないものもこのクエリでは検出される。 クエリでも大まかには絞り込めるだろうが、細かい判別は後でプログラムで行う方がいいだろう。 最終的な重複は1、2万レコードになるだろう。
point を polygon に置き換えれば、polygon の重複を検出できる。 と思ったが、 ST_distance(ST_Transform(p.way,4326), ST_Transform(q.way,4326), false) は二点間の距離計算用であり、polygon間の距離は計算できないようだ。 実行途中で、距離が負になったというエラーがでて、止まった。
おそらく、二重登録は point の方が多いと思うので、 まずは point の二重登録に対処しよう。
polygonの二重登録は実際に目についたとき、対応を検討しよう。
select p.sqn, q.sqn, ST_distance(ST_Transform(p.way,4326), ST_Transform(q.way,4326), false) as dist, p.name, q.name, p.my_tag from planet_osm_point p, planet_osm_point q where ST_distance(ST_Transform(p.way,4326), ST_Transform(q.way,4326), false) < 30 and p.sqn <> q.sqn and p.my_tag = q.my_tag and p.my_name is not null and p.my_name = q.my_name and p.sqn < q.sqn;
改めて考えてみると、二重登録の抽出は PostGIS を使うのではなく、 osmdata.tsvで抽出する方がいいようだ。 点(point)同士、エリア(polygon)同士のダブりチェックが一挙に行える。
距離計算は、メートルではなくて、極座標で充分である。 そもそも地図は同じ10mが画素単位では沖縄より北海道の方がかなり長くなる。
√(x2 + y2) ではなくて、|lon| + |lat| でも、 近接判定は十分である(x,yは距離差。lon,latは極座標差)。
このチェックは表示直前に行うのが効率よい。 事前に、シンプルに比較すると 200万x200万回の比較になる。 メモリ上のデータは lon, lat でソートしてあるので、添え字が近い範囲だけの比較でよいが、 それなりのプログラムがいる。
一方、直前の比較では、現在の画面範囲の表示候補内での比較でよいので、 通常は総当たりでせいぜい、100x100 回の比較ですむので、時間がかからない。
現在の自作地図システムでは、タイル単位で描画を行っている。 次のようにして、現在にタイル(xT,yT)よりも、前後左右に4割ずつ広い範囲にある OSMオブジェクト (の添え字)を抽出している。
int[] osms = OSMData.GetData(zoom, xT-0.4, yT-0.4, xT+1.4, yT+1.4);
タイル境界近辺のアイコンや文字は一つのタイルに収まらず、 二つまたは四つのタイルにまたがることがある。 このため、中心が現在のタイルにはないオブジェクトも抽出して描画を試みる。
描画は既にその場所に他のオブジェクトが描画されており、これと重なる場合には、 描画を行わない。このため、まれに、文字の一部が欠けることがあるが、 ズームアップで重なりが解消するので、それほど支障にならない。
アイコンなど、小さいものは、タイル範囲をはみ出すものは描画しないようにできるが、 文字では抑止されるものが増えすぎる。 広い範囲を取るので、運が悪い位置にあると、ズームアップしても、 なかなか一つのタイルに入りきれない状況が続く。 このため、一部欠けることがあることを許容している。
一度に全タイルを規則正しく描画する場合はまだ対応が楽かもしれないが、 ランダムに描画する場合、複数タイルにわたる文字の描画を完璧に行うのは容易ではない。
OSM標準地図では、表示の不具合をあまり見かけない。 しかし、細かく見ると、道路番号の一部が欠けていることがある。 自分は、当初、Mapnikのバグかと思っていたが、タイル単位の描画では、原理上、 タイルをまたがるアイコンや文字の描画が非常に難しいということである。
プログラムでの二重登録チェックはとりあえず次のようにした。
二重登録で表示を抑止するものは添え字を負に変更する。
アイコンのみの重複は除外した。ベンチ、街路樹など近接して配置されるものが多いためである。 名前はぴったり一致が条件ではなく、少し緩めている。 もっと高度な比較で 90% 以上の近似度といった方が望ましいだろう。ただし、そんなに拘る話でもない。
近接判定の距離は今後必要に応じて調整する。 バス停、駅、踏切、信号など除外するものは setAcceptに登録しておく。今後、増えるかも知れない。
それほど重要ではないが、なんとなく気になる重複表示はこのようにすれば避けられる。
bool Match(string p, string q) { return (p.StartsWith(q) || q.StartsWith(p)); } void CheckDuplication(int[] ixs) { // アイコンのみの重複は除外する double MinDist = 0.002; var setAccept = new HashSet<string>() { "traffic_signals", "bus_stop", "station", "level_crossing" }; for (int i = 0; i < ixs.Length; i++) { OSM p = OSMData.osms[ixs[i]]; bool fDup = false; for (int j = i+1; j < ixs.Length && !fDup; j++) { OSM q = OSMData.osms[ixs[j]]; if (p.name == "" || !Match(p.name, q.name) || p.tag != q.tag) continue; if (setAccept.Contains(p.value)) continue; if (Math.Abs(p.lon-q.lon) + Math.Abs(p.lat-q.lat) > MinDist) continue; fDup = true; break; } if (fDup) ixs[i] = -ixs[i]; } }
データベースに my_tag, my_name カラムを設けたことにより、OSMデータの実態を掴みやすくなった。
そのひとつとして、eleカラムだけ設定されたデータが大量にあることが分かった。 国土地理院地図の三角点は man_made=survey_point であるが、このタグがないのは、 おそらく、国土地理院地図で ・62 のように海抜(標高)だけが書かれた場所に対応しているのだろう。
このような標高は散歩でも参考になるので、無意味ではない。
OSMでは amenity, shop, tourism などが主タグであり、ele はサブタグである。 一般に主タグは一つで、サブタグは複数付けられる。
主タグが複数の場合、どれを優先するか迷うが、主タグがないものは無視される。
この ele だけのデータを活かすには適当な主タグを補うのが一番簡単である。
・62 は地図記号一覧表には「写真測量による標高点」となっている。 現地に行っても何も目印になるようなものはないようだ。 それでは man_made=survey_point とは言えないのだろう。
しかし、何か主タグがいるので、とりあえずは man_made=photogrammetry としておこう。 タグ定義テーブル symbols.csv にはとりあえず黒点・を登録した。