MariaDBでGeoJsonを扱ってみた

地図アプリを作っていて、緯度経度を扱っているのに、MySQL/MariaDBの空間データ型を扱わないのはもったいないので、触ってみました。
また、地図情報を扱うのに都合がよいファイルフォーマットとして、JSON型のGeoJsonがあるようで、JavascriptやNode.jsで扱いやすそうです。
これを機にGeoJsonファイルビューアを作ってみました。

MariaDBに都道府県や市町村データを登録して、位置情報から都道府県や市町村名を検索できるようなWebAPIをNode.jsで作成します。
地図の描画には、GoogleMap APIを使います。GoogleMap APIでも、GeoJsonファイルをサポートしています。

国土交通省が提供している国土数値情報サイトからいろんなGeoJsonファイルが提供されているので、それを見るだけでも楽しそうです。
今回以下のデータを参照できるようにしました。
・駅
・鉄道ルート
・バス停留所
・バスルート
・高速バス停留所
・高速バスルート

それ以外のJSONは、ご自身でダウンロードして、ビューアに食わせると参照できます。

国土数値情報ダウンロードサイト
https://nlftp.mlit.go.jp/ksj/

ソースコードもろもろは以下に上げておきました。

https://github.com/poruruba/GeoJsonViewer

# GoogleMap APIでGeoJsonを表示する

まずは、GooleMap APIを使って、GeoJsonファイルをブラウザで表示します。
GeoJsonファイルは、以下のサイトにあるjapan.geojsonを使わせていただきました。
https://github.com/dataofjapan/land

このGeoJsonファイルでは、都道府県ごとに県境を囲ってありますので、都道府県の県境を参照できますし、緯度経度からどの都道府県かわかるようになります。

```html:public/index.html
<div id="map" style="height:65vh"></div>

<script src="https://maps.googleapis.com/maps/api/js?key=【APIキー】&callback=initMap" async defer></script>
```

上記をindex.htmlに記載すると、ライブラリのロードが完了したときに、initMap関数を呼んでくるので、そこに初期化処理を記述します。

```js:public/js/start.js
            map = new google.maps.Map(document.getElementById('map'), {
                zoom: 15,
                center: default_latlng,
                draggableCursor: "pointer"
            });
```

```js:public/js/start.js
                var input = {
                    url: "geojson/japan.geojson",
                    method: "GET",
                };
                var result = await do_http(input);
                prefectureFeatures = map.data.addGeoJson(result);
```

do_httpは自作の関数で、指定されたURLとメソッドでダウンロードし、JSONパースします。
JSONをmap.data.addGeoJsonに渡せば完了です。
戻り値をprefectureFeatures に入れているのは、GeoJsonを表示した後に、非表示にするために覚えておきます。
以下は非表示にするときのロジックです。

```js:public/js/start.js
            if( prefectureFeatures)
                for(let feature of prefectureFeatures )
                    map.data.remove(feature);
            prefectureFeatures = null;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c86b01e1-7e30-edc4-1735-e52d6b78ef3a.png)

# MariaDBにGeoJsonの情報を登録する。

MariaDBは、位置情報を扱うための専用のデータ型が定義されています。

https://dev.mysql.com/doc/refman/8.0/ja/spatial-type-overview.html

単一ジオメトリ
・POINT
・LINESTRING
・POLYGON
・GEOMETRY
※GEOMETRYにはそれ以外を格納できます。

コレクション
・MULTIPOINT
・MULTILINESTRING
・MULTIPOLYGON
・GEOMETRYCOLLECTION
※GEOMETRYCOLLECTIONにはそれ以外を格納できます。

そして、MariaDBにも、GeoJsonを扱うための関数が用意されています。

GeoJsonファイルは以下のようなファイルです。

```json
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
              // 任意
            },
            "geometry": {
                "type": "MultiPolygon", // "MultiPolygon" or "Polygon" or "LineString" or "Point" など
                "coordinates": [
                    [
                        [
                            [
                                135.036697387695, // 経度
                                35.537334442138686 // 緯度
                            ],
```

MariaDBは、このうちfeaturesの要素を1つずつ登録します。

japan.geojsonの場合は、GeoJsonファイルは以下のような内容でした。

```json
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "nam": "Kyoto Fu",
                "nam_ja": "京都府",
                "id": 26
            },
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
```

typeは、GeoJsonによってさまざまで混在もあるため、以下のようなスキーマ定義としました。
codeは、都道府県コードです。

```sql
CREATE TABLE `prefecture` (
  `code` int(11) NOT NULL,
  `name` text NOT NULL,
  `polygon` geometrycollection NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

ALTER TABLE `prefecture`
  ADD PRIMARY KEY (`code`);
COMMIT;
```

関数ST_GeomFromGeoJSONが、GeoJsonの文字列を解釈してくれます。


```js:api\controllers\geojson-api\index.js
const mysql = require('mysql2/promise');
const fs = require('fs').promises;

	var result = await fs.readFile("geojson/japan.geojson");
	var json = JSON.parse(result);
	for( const item of json.features){
		var values = [item.id, item.nam_ja, JSON.stringify(item)];
		var sql = "INSERT INTO prefecture (code, name, polygon) VALUES(?, ?, ST_GeomFromGeoJSON(?))";
		try{
			var result = await conn.execute(sql, values);
			console.log("inserted: " + item.nam_ja);
		}catch(error){
			console.error(error);
			break;
		}
	}
```

登録内容を確認したい場合は、ST_AsGeoJSON関数を使うとよいです。47都道府県分が返ってくるかと思います。

```sql
SELECT code, name, ST_AsGeoJSON(polygon) FROM prefecture;
```

# 指定の緯度経度がどの都道府県かを調べる

MariaDBに登録した情報を使って、指定の緯度経度がどの県かを調べます。
まず、指定する緯度経度は、以下の形式で記述する必要があります。

```sql
ST_GeomFromText('POINT(経度 緯度)', 4326)
```

次に、その緯度経度が、prefectureに登録したpolygonの領域に含まれるかどうかを以下のようにして確認します。true/falseが返る関数です。

```sql
ST_Contains(polygon, ST_GeomFromText('POINT(経度 緯度)', 4326))
```

まとめるとこんな感じです。

```js:api\controllers\geojson-api\index.js
		var values = ["POINT(" + body.lng + " " + body.lat+ ")"];
		var [rows, fields] = await conn.query("SELECT code, name FROM prefecture WHERE ST_Contains(polygon, ST_GeomFromText(?, 4326))", values );
		if( rows.length <= 0 )
			throw new Error("prefecture not found");

		return new Response(rows[0]);
```


# 指定の緯度経度の範囲にあるバス停留所を表示する

次に、バス停留所をGoogleMapに表示してみます。
これまで説明したやり方で、GoogleMap APIでGeoJsonファイルを読み出せば、表示できそうです。
ですが、全国にあるバス停留所の数はものすごく、全部を表示していたら重たすぎて固まってしまうでしょう。
そこで、今GoogleMapで見えている緯度経度の範囲内にあるバス停留所に絞って表示するようにしてみます。

まずは、今GoogleMapで見えている緯度経度の範囲は以下のようにしてわかります。

```js:public/js/start.js
            var bounds = map.getBounds();
            var ne = [bounds.getNorthEast().lat(), bounds.getNorthEast().lng()];
            var sw = [bounds.getSouthWest().lat(), bounds.getSouthWest().lng()];
```

右上と左下の緯度経度がわかりました。
これをPolygonとして領域として定義する必要があります。
そのためには、左上、右上、右下、左下、左上 の順に並べます。左上をだぶらせて、閉じた領域であることを示しています。

領域は以下のように定義します。

```sql
ST_GeomFromText('POLYGON((経度1 緯度1) (経度2 緯度2) .... (経度xn 緯度n))', 4326)
```

それを使ってその領域に含まれるものを抽出します。

```sql
ST_Intersects(ST_GeomFromText('POLYGON((緯度1 緯度1) (緯度2 緯度2) .... (緯度n 緯度n))', 4326), polygon)
```

ST_Intersectsは、領域に少しでも重なっていたらtrueとなります。ほかにもST_WithinやST_Containsなどがあります。

まとめるとこんな感じ
ちなみに、```type = ?``` の部分は、同じテーブルに複数の種類のGeoJsonを格納していてそれを区別するためのものです。

```js:api\controllers\geojson-api\index.js
		var polygon = "";
		for( var i = 0 ; i < bounds.length ; i++ ){
			if( i != 0 )
				polygon += ",";
			polygon += bounds[i].lng.toString() + " " + bounds[i].lat.toString();
		}
		if( bounds.length > 0 )
			polygon += "," + bounds[0].lng.toString() + " " + bounds[0].lat.toString();;
		
		var value = ["POLYGON((" + polygon + "))", type];
		var sql = "SELECT property, ST_AsGeoJSON(polygon) as json FROM geojson WHERE ST_Intersects(ST_GeomFromText(?, 4326), polygon) AND type = ?";
		var [rows, fields]  = await conn.execute(sql, value);
```

あとは、この結果をGeoJsonファイル形式にしてクライアント側に返してあげてます。

```js:api\controllers\geojson-api\index.js
		var result = {
			type: "FeatureCollection",
			features: []
		};
		for( let item of rows ){
			var feature = {
				type: "Feature",
				properties: JSON.parse(item.property),
				geometry: JSON.parse(item.json)
			};
			result.features.push(feature);
		};
		
		return new TextResponse("application/json", JSON.stringify(result));
```

以上