なんかgoogleのAdSense通ったんで有益な情報載せないとなって。
国土地理院のオープンデータ
国土地理院が公開している(正確には国土地理院のデータを利用して国土交通省国土政策局国土情報課が公開している)「国土数値情報」っていうオープンデータが大量にあって、色々便利なのです。
で、せっかくなのでUnityで読んでみたいな、と。
今回は行政区域データってのを使ってみます。要は県とか市の境界情報がまとまっているデータです。
で、Unityで読んで表示したいな、と。
GeoJsonを読み込もう
このデータ、ダウンロードページに行くと簡単なアンケートに答えるだけで誰でも無料で利用できるんですけど、結構データ形式の情報が無い。そのわりに、割と色々な形式が入ってきます。
どうもメインはXML形式らしいんだけどJSON形式も付いてる。それもこれはGeoJsonっていうデファクトスタンダードっぽい仕様に沿ってるらしい。
データは大量(全国データで600MB)に入っているのだけど、例えば我が故郷伊丹市の部分を抜粋するとこんな感じ。
{
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:EPSG::6668"
}
},
"features": [
{
"type": "Feature",
"properties": {
"N03_001": "兵庫県",
"N03_002": null,
"N03_003": null,
"N03_004": "伊丹市",
"N03_007": "28207"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 135.39466666703311, 34.805916386831825 ],
[ 135.39479666673287, 34.805749162393965 ],
(省略)
[ 135.39447861159874, 34.806172774553943 ],
[ 135.39466666703311, 34.805916386831825 ]
]
]
}
},
{(次の行政区域)}…
]
}
まぁまぁ意味が分かるかな、って感じのデータです。propertiesに謎のコードが入ってますが、これは「シェープファイルの属性について(xlsファイル)」っていうExcelに内容がまとめてあるのでそれを読むと何かわかる。それ以外の部分は、WikipediaのGeoJSONの項目がいい感じにまとめてあるので一読すると良いです。
Unityで処理しよう
せっかくなのでUnityで可視化したい。
JsonUtilityの挫折
UnityにはJsonUtilityっていうAPIが整備されていてJSONをいい感じにシリアライズできる。
が、
辞書のネストは読めても、配列のネストが読めない。
具体的に言うと、こういうの(↓)は読める。
{
"hoge" : 1,
"piyo" : {
"foo" : 2.1,
"bar" : [2.21, 2.22, 2.23]
}
}
こういうクラスを用意してあげれば良い。
[Serializable]
public class hogepiyo
{
public int hoge;
public foobar piyo;
}
[Serializable]
public class foobar
{
public double foo;
public double[] bar;
}
でもこういうの(↓)が読めない。
{
"hoge" : [
[1, 2],
[3, 4],
[5, 6]
]
}
配列の配列がダメらしい。どうもUnityシリアライザーが受け入れてくれないらしい。
(正確には、上みたいなのは”Vector2[]“として受け取ればイケるらしい…?)
今回のGeoJSONだとcoordinatesが三次元配列になっていて、JsonUtilityは諦めるしかない。
MiniJSON
というわけでMiniJSONを使う。
MiniJSONをUnityで使うサンプルコードはQiitaにいい感じの記事があるので、そっち見てもらった方が早い。
まぁザクっと読めば読める。
そのまま読むのがつらいので適当な構造体を用意して突っ込みます。
構造体の定義がこんな感じ。
public struct geoJsonFeatureCollection
{
public string type;
public List<geoJsonFeature> features;
}
public struct geoJsonFeature
{
public string type;
public geoJsonProperties properties;
public geoJsonGeometry geometry;
}
public struct geoJsonProperties
{
public string N03_001; /* 都道府県名 */
public string N03_002; /* 支庁名 */
public string N03_003; /* 郡政令都市 */
public string N03_004; /* 市区町村名 */
public string N03_005; /* 成立年月日 */
public string N03_006; /* 消滅年月日 */
public string N03_007; /* 行政区域コード */
}
public struct geoJsonGeometry
{
public string type;
public List<List<coordinate>> coordinates; /* 閉路ごとにリストが分けられている */
}
public struct coordinate
{
public double latitude; /* 緯度 */
public double longitude; /* 経度 */
}
で、この構造体にデータを突っ込む処理がこんな感じ。
testjsonって変数にjsonの文字列が入ってるもんだと思って。
Dictionary<string, object> featureCollection = Json.Deserialize(testjson) as Dictionary<string, object>;
geoJsonFeatureCollection geodata = new geoJsonFeatureCollection();
geodata.type = (string)featureCollection["type"];
geodata.features = new List<geoJsonFeature>();
foreach (Dictionary<string, object> feature in (List<object>)featureCollection["features"])
{
geoJsonFeature tmpfeature = new geoJsonFeature();
tmpfeature.type = (string)feature["type"];
Dictionary<string, object> properties = (Dictionary<string, object>)feature["properties"];
if (properties.ContainsKey("N03_001")) tmpfeature.properties.N03_001 = (string)properties["N03_001"];
if (properties.ContainsKey("N03_002")) tmpfeature.properties.N03_002 = (string)properties["N03_002"];
if (properties.ContainsKey("N03_003")) tmpfeature.properties.N03_003 = (string)properties["N03_003"];
if (properties.ContainsKey("N03_004")) tmpfeature.properties.N03_004 = (string)properties["N03_004"];
if (properties.ContainsKey("N03_005")) tmpfeature.properties.N03_005 = (string)properties["N03_005"];
if (properties.ContainsKey("N03_006")) tmpfeature.properties.N03_006 = (string)properties["N03_006"];
if (properties.ContainsKey("N03_007")) tmpfeature.properties.N03_007 = (string)properties["N03_007"];
Dictionary<string, object> geometry = (Dictionary<string, object>)feature["geometry"];
tmpfeature.geometry.type = (string)geometry["type"];
tmpfeature.geometry.coordinates = new List<List<coordinate>>();
foreach (List<object> closedLoop in (List<object>)geometry["coordinates"])
{
List<coordinate> tmpcloop = new List<coordinate>();
foreach (List<object> position in closedLoop)
{
coordinate tmppos = new coordinate();
tmppos.latitude = (double)position[0];
tmppos.longitude = (double)position[1];
tmpcloop.Add(tmppos);
}
tmpfeature.geometry.coordinates.Add(tmpcloop);
}
geodata.features.Add(tmpfeature);
}
ただ問題は、日本の市町村の大きさが地球に対して小さすぎるので、緯度経度の数値をfloatに入れると使い物にならない。
なので、floatに入れても値が丸まらないように気をつけつつ処理しないといけない。
たとえばとりあえずDebug.DrawLineに流し込む例はこんな感じ。配列の最初のデータを原点に置いて、緯度・経度を1万倍して表示しています。
void Update()
{
List<coordinate> tmp = geodata.features[0].geometry.coordinates[0];
for (int i = 1; i < tmp.Count; i++)
{
Debug.DrawLine(
new Vector3((float)(tmp[i-1].latitude * 10000 - tmp[0].latitude * 10000), (float)(tmp[i - 1].longitude * 10000 - tmp[0].longitude * 10000)),
new Vector3((float)(tmp[i].latitude * 10000 - tmp[0].latitude * 10000), (float)(tmp[i].longitude * 10000 - tmp[0].longitude * 10000))
);
}
}
これの実行結果がこう。
緯度経度を何も考えずにx,y座標に流しているので縦横比がメチャメチャですが、とりあえずデータが取れているのが分かります。
本当はきちんと座標変換すべきだし、今後このデータを3Dメッシュとして表示させたりしてみたいですね。