OSMを使って現在地の都道府県市町村を取得する

#はじめに

今作ってるゲームで位置情報を使いたいと考えていて、ただ、厳密に位置を使うのではなくて市町村レベルの情報が取得できればいいなと思って調べたので、整理も兼て書いておきます。

OSMでできないかと思い調べると、Nominatimを使うことでできるようです。
今回はブラウザで動作させているので、WebAPIのGeo Location APIと組み合わせることで実現ができました。

[GitHubにソース](https://github.com/project-dev/geo)を公開しています。

# Geo Location API

ブラウザで位置情報を扱うことのできるAPIです。
使い方は結構簡単でした。

位置情報を取得するAPIは2つあります。
呼び出した時点の現在の位置を取得する Geolocation.getCurrentPosition()
定期的に現在地を取得する Geolocation.watchPosition()

今回は、Geolocation.getCurrentPosition() を使いました

[MDN](https://developer.mozilla.org/ja/docs/Web/API/Geolocation/getCurrentPosition)を見ると

getCurrentPosition(success[, error[, [options]])

となっています。
というわけで以下のように実装してみました。
オプションはMDNの説明を参照してください。

```javascript
// navigator.geolocationをチェックして、GetLocation APIをサポートしているかの確認
var isGeolocSupport
if (navigator.geolocation) {
    isGeolocSupport = true;
} else {
    isGeolocSupport = false;
}

document.addEventListener('DOMContentLoaded', function() {
    if(isGeolocSupport){
        navigator.geolocation.getCurrentPosition(function(position){
            // 位置情報が取得できた場合
            var request = new XMLHttpRequest();
            var lat = position.coords.latitude;
            var lon = position.coords.longitude;
            // 何か処理する
        },
        function(error){
            // 取得できない場合
        },{
            // オプション
            enableHighAccuracy : true,
            timeout : 30 * 1000,
            maximumAge : 60 * 1000
        });
    }
});
```

まぁ、こんな感じで簡単に現在地を取得できます。
ちなみに、ブラウザでアクセスすると、以下のように確認ダイアログが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/e0f98a75-259f-02e2-d5f6-010aa4faedae.png)

# Nominatim

私自身、機能を把握しきれていませんが、Wikiを見ると
OSMデータを名前や住所で探したり、OSMポイントの住所を合成して生成する（逆ジオコーディング）ツール
と説明されています。
検索とかに使えるAPIのようです。

このNominatimを使い、現在位置から住所を取得します。
Nominatimには search, reverse, lookup などが用意されています。
今回は緯度経度から住所を取得したいので、reverseを利用します。
今回は以下のオプションで利用ています。

|パラメタ|値|説明|
|-|-|-|
|format|json|json形式で結果を取得する|
|zoom|18|家/建物レベルを指定|
|addressdetails|1|要素内で細分化した住所を含む(この指定の必要があるかは未検証)|

戻ってくるjsonですが、addressに住所の情報が入ってきます。
住所の情報は地域で入ってくるキーが異なるようです。
そのあたりを考慮して結果を取得する必要があります。

|キー|説明|
|-|-|
|province|都道府県|
|state|州|
|region|地方|
|county|郡|
|city|市町村|
|district|？？？|
|suburb|区|
|town|町|
|village|村|
|neighbourhood|小字 / 字 / 丁 / 丁目 / 島嶼部|
|road|道？|

で、その辺を考慮して実装していますが・・・ダサい。
まぁ、それはおいておいて、これで都道府県市町村を取得できます。

```javascript
var request = new XMLHttpRequest();
request.onload = function () {
    var data = this.response;
    var result = document.getElementById('result');

    var addr1 = "";
    var addr2 = "";
    if(data != null && data.address != null){
        if(data.address.province){
            addr1 = data.address.province;
        }
        if(data.address.state){
            if(!addr1){
                addr1 = data.address.state;
            }else if(!addr2){
                addr2 = data.address.state;
            }
        }
        if(data.address.region){
            if(!addr1){
                addr1 = data.address.region;
            }else if(!addr2){
                addr2 = data.address.region;
            }
        }
        if(data.address.county){
            if(!addr1){
                addr1 = data.address.county;
            }else if(!addr2){
                addr2 = data.address.county;
            }
        }
        if(data.address.city){
            if(!addr1){
                addr1 = data.address.city;
            }else if(!addr2){
                addr2 = data.address.city;
            }
        }
        if(data.address.district){
            if(!addr1){
                addr1 = data.address.district;
            }else if(!addr2){
                addr2 = data.address.district;
            }
        }
        if(data.address.suburb){
            if(!addr1){
                addr1 = data.address.suburb;
            }else if(!addr2){
                addr2 = data.address.suburb;
            }
        }
        if(data.address.town){
            if(!addr1){
                addr1 = data.address.town;
            }else if(!addr2){
                addr2 = data.address.town;
            }
        }
        if(data.address.village){
            if(!addr1){
                addr1 = data.address.village;
            }else if(!addr2){
                addr2 = data.address.village;
            }
        }
        if(data.address.neighbourhood){
            if(!addr1){
                addr1 = data.address.neighbourhood;
            }else if(!addr2){
                addr2 = data.address.neighbourhood;
            }
        }
        if(data.address.road){
            if(!addr1){
                addr1 = data.address.road;
            }else if(!addr2){
                addr2 = data.address.road;
            }
        }
    }
    result.innerText = addr1 + addr2;
}   
var url = "https://nominatim.openstreetmap.org/reverse?"
        + "format=json"
        + "&lat=" + lat
        + "&lon=" + lon
        + "&zoom=18"
        + "&addressdetails=1";
request.open('GET', url, true);
request.responseType = 'json';
request.send();
```

# ついでに近辺の情報も取得したい

欲が出て、周辺の情報も取得したくなりました。
その場合、Overpass APIを使うことで実現できます。
Overpass APIは利用するサーバーにより条件があったります。
詳しくは [JA:Overpass API - OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/JA:Overpass_API) を参照してください。

今回は、現在の位置を中心に、50kmの範囲にあるコンビニを取得してみました。
緯度経度を距離に換算する情報は [緯度経度のメートル換算 - teratail](https://teratail.com/questions/102891) を参考にさせていただきました。

Overpass APIにリクエストする場合、JSONで値を送ることができないようで、formデータとして送信します。
(もし、JSONで送る方法があるならコメントで教えてくださいm(_ _)m )
いろいろ指定できるのですが、実際に利用する前に [overpass turbo](http://overpass-turbo.eu/) でパラメタを指定してどんな値が返ってくるのかを確認できます。
なので、ここで動いた条件を、formデータとして利用すると良いと思います。

```javascript
var url = "https://overpass-api.de/api/interpreter?" ;
// 0.00001 が 1m
var areaLen = 0.00001 * 50 * 1000;
var formData = "data="
        + "[out:json][timeout:25];"
        + "("
        + 'node["shop"="convenience"](' + (lat + areaLen * -1) + ',' + (lon + areaLen * -1) + ',' + (lat + areaLen) + ',' + (lon  + areaLen) + ');'
        + ");"
        + "out body;";
var overpass = new XMLHttpRequest();
var logArea = document.getElementById("log");
logArea.value = "";
overpass.onload = function () {
    var data = this.response;
    var logArea = document.getElementById("log");
    data.elements.forEach(poi => {
        logArea.value += poi.tags.name + "\r\n";
    });
}
overpass.open('POST', url, true);
overpass.responseType = 'json';
overpass.send(formData);
```
実行するとこんな感じになります。
50kmは広すぎですね(縦横100kmになるからなぁ・・・)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/2d8717cf-8210-35be-3aaa-727091f3dfbc.png)


# 最後に

どうやってこれを使っていくかは、まだ検討段階です。
IngressやポケGoのように割と精度の必要となる位置情報ゲームではなく、駅メモのような、一番近いものにアクセスするようなものを作る場合には、こういった使い方が生かせるかもしれないと思います。

# 参考
- [projectdev/geo - GitHub](https://github.com/project-dev/geo)
- [位置情報 API - MDN](https://developer.mozilla.org/ja/docs/Web/API/Geolocation_API)
- [JA:Nominatim - OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/JA:Nominatim)
- [overpass turbo](http://overpass-turbo.eu/)
- [JA:Overpass API - OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/JA:Overpass_API)
- [JA:Overpass turbo - OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/JA:Overpass_turbo)
- [JA:住所 - OpenStreetMap Wiki](https://wiki.openstreetmap.org/wiki/JA:%E4%BD%8F%E6%89%80)
- [緯度経度のメートル換算 - teratail](https://teratail.com/questions/102891)