いざとなれば会社に通える田舎を探す(その3 できた)

#これまで

コロナで在宅勤務が続いているため、都会で手狭な家に住んでいるのが苦しくなってきました。週に数日や月に数日しか出社しないなら、通勤に片道2時間ぐらいかかる田舎で暮らすことも現実的かも。（定期代がフルに支給されない）新幹線通勤までも視野に入れれば結構遠くに住めるかも？

今ひとつ土地勘がないので、会社の最寄り駅に平日朝9時に到着するための乗車時間を地図にマッピングして候補地を探していきたい。

ということで、[その１](https://qiita.com/aerialist/items/6fe3ad2590c218274c55)と[その２](https://qiita.com/aerialist/items/31ba60ac5289f9d23772)では、「駅すぱあとWebサービス フリープラン」を利用して作成しようと思ったのですが挫折しました。

# 成果物

ついにやりたいことがほぼできました。こんな感じ。
![nihonbashi180_5fps.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138680/2a32754f-37ed-5fdb-b83f-dd8763cfcd0a.gif)

[ぜひこちらを開いて](https://s3inaka.s3-ap-northeast-1.amazonaws.com/nihonbashi180.html)ぐりぐりしてみてください。
[3Dマップバージョン](https://s3inaka.s3-ap-northeast-1.amazonaws.com/nihonbashi180_3d.html)はさらに面白いです。（Ctl押しながらマウスでぐりぐりすると3次元になる）

# NAVITIME Reachableはフリーミアムな経路検索API

NavitimeがRakutenRapid経由で提供してくれている[NAVITIME Reachable](https://api.rakuten.net/navitimejapan-navitimejapan/api/navitime-reachable)がまさに探し求めていたものでした。探索開始地点と時間を指定すると、到達できる駅を返してくれる。ありがたく使わせていただきます。

# Navitime reachableで駅一覧を取得しgeojsonにする

検索開始地点として日本橋駅の緯度経度を指定して、180分以内にたどり着ける駅を検索します。

```python
url = "https://navitime-reachable.p.rapidapi.com/reachable_transit"
nihonbashi_loc = "35.682143,139.774678"
querystring = {
    "start":nihonbashi_loc,
    "term":"180",
    "term_from":"0",
    "walk_speed":"5",
    "transit_limit":"30",
    "offset":"0", # max is 1000... why??
    "limit":"2000",
    #"options":"node_detail",
    "datum":"wgs84",
    "coord_unit":"degree",
}
headers = {
    'x-rapidapi-host': "navitime-reachable.p.rapidapi.com",
    'x-rapidapi-key': rapidapi_key
    }

response = requests.request("GET", url, headers=headers, params=querystring)
```

返ってきた駅情報JSONを扱いやすくフラットにしてからPandas.DataFrameにぶちこむ。
ホントはJSONから直接geojsonにすればいいのだろうけど、mapboxglの df_to_geojson 関数が便利なのでありがたく使わせてもらう。あとでmapboxで使うためにgeojsonをファイルに書き出しておく。

```python
nihonbashi = response.json()
items = [flatten(item) for item in nihonbashi['items']]
df = pd.DataFrame(items)
points = df_to_geojson(df, 
                      properties=['name', 'node_id', 'time', 'transit_count'],
                      lat='coord_lat',
                      lon='coord_lon',
                      precision=6)

with open("navitime_reachable_nihonbashi_180.geosjon", 'w') as outfile:
     geojson.dump(points, outfile)
```

コード全部はこちら。

```python:navitime_to_geojson.py
import requests
import pandas as pd
import geojson
from mapboxgl.utils import df_to_geojson

#mapbox_token = ''
#rapidapi_key = ''
from secret import mapbox_token, rapidapi_key

from collections import MutableMapping 
def flatten(d, parent_key='', sep='_'):
    """
    flatten dictionary
    https://stackoverflow.com/questions/6027558/flatten-nested-dictionaries-compressing-keys
    """
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, MutableMapping):
            items.extend(flatten(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

url = "https://navitime-reachable.p.rapidapi.com/reachable_transit"
nihonbashi_loc = "35.682143,139.774678"
querystring = {
    "start":nihonbashi_loc,
    "term":"180",
    "term_from":"0",
    "walk_speed":"5",
    "transit_limit":"30",
    "offset":"0", # max is 1000... why??
    "limit":"2000",
    #"options":"node_detail",
    "datum":"wgs84",
    "coord_unit":"degree",
}
headers = {
    'x-rapidapi-host': "navitime-reachable.p.rapidapi.com",
    'x-rapidapi-key': rapidapi_key
    }

response = requests.request("GET", url, headers=headers, params=querystring)
nihonbashi = response.json()
items = [flatten(item) for item in nihonbashi['items']]
df = pd.DataFrame(items)
points = df_to_geojson(df, 
                      properties=['name', 'node_id', 'time', 'transit_count'],
                      lat='coord_lat',
                      lon='coord_lon',
                      precision=6)

with open("navitime_reachable_nihonbashi_180.geosjon", 'w') as outfile:
     geojson.dump(points, outfile)
```

# mapboxを使ったhtmlを書く

mapboxglを使って地図表示までさせても良かったのですが、今回は時間を指定するUIを作ったりアニメーションさせたりしたかったので頑張ってHTML+javascript+cssを書いてみました。

出来上がりは[こちら](https://s3inaka.s3-ap-northeast-1.amazonaws.com/nihonbashi180.html)を覗いてください。

bodyの中のHTMLはこれだけ。マップを置くためのDIVと、アニメーション再生・停止のアイコンのSVG2つ、そして時間を指定するスライダー。

```html
    <div id='map'></div>
    <div class="map-overlay top">
        <div class="map-overlay-inner">
        <label>Max time (minutes): <span id="slider-value">180</span></label>
        <svg xmlns="http://www.w3.org/2000/svg" id="play_btn" width="16" height="16" fill="currentColor" class="bi bi-play-btn" viewBox="0 0 16 16">
            <path d="M6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z"/>
            <path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
        </svg>
        <svg xmlns="http://www.w3.org/2000/svg" id="pause_btn" width="16" height="16" fill="currentColor" class="bi bi-pause-btn" viewBox="0 0 16 16" style="display: none;">
            <path d="M6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5zm3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5z"/>
            <path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/>
        </svg>
        <input
        id="slider"
        type="range"
        min="5"
        max="180"
        step="1"
        value="180"
        />
        </div>
    </div>
```

mapbox-gl-jsを使ってmapboxの機能を使っていきます。
せっかく田舎を探すのでoutdoorsのスタイルを指定。起点の日本橋駅にはマーカーを立てる。

```javascript
    var nihonbashi = [139.773389,35.682004];
    var src_nihonbashi;
    var map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/outdoors-v11', // stylesheet location
        center: nihonbashi, // starting position [lng, lat]
        zoom: 7 // starting zoom
    });
    var marker = new mapboxgl.Marker()
        .setLngLat(nihonbashi)
        .addTo(map);
```

駅のgeojsonファイルはあらかじめS3にアップしてアクセスできるようにしておきます。
map.addSourceで登録し、各駅はmap.addLayerで丸として表現します。

```javascript
        map.addSource('src_nihonbashi', {
            type: 'geojson',
            data: 'https://s3inaka.s3-ap-northeast-1.amazonaws.com/navitime_reachable_nihonbashi_180.geosjon'
        });
        // for debug
        src_nihonbashi = map.querySourceFeatures("src_nihonbashi");

        map.addLayer({
            'id': 'places',
            'type': 'circle',
            'source': 'src_nihonbashi',
            'paint': {
                //'circle-blur': 0.6,
                'circle-color': [
                    'step',
                    ['get', 'time'],
                    '#4daf4a',
                    30,
                    '#377eb8',
                    60,
                    '#e41a1c'
                ]
            }
        });
```

各駅の丸にマウスを載せたときにその駅の情報を表示するようにします。mapboxgl.Popupを使う。

```javascript
        // Create a popup, but don't add it to the map yet.
        var popup = new mapboxgl.Popup({
            closeButton: false,
            closeOnClick: false
        });
        
        map.on('mouseenter', 'places', function (e) {
            // Change the cursor style as a UI indicator.
            map.getCanvas().style.cursor = 'pointer';
            
            var coordinates = e.features[0].geometry.coordinates.slice();
            //var description = e.features[0].properties.name;
            var description = "<strong>"
                            + e.features[0].properties.name
                            + "</strong><ul>"
                            + "<li>Time: " + e.features[0].properties.time + "</li>"
                            + "<li>Transit count: " + e.features[0].properties.transit_count + "</li>"
                            + "</ul>";
            
            // Ensure that if the map is zoomed out such that multiple
            // copies of the feature are visible, the popup appears
            // over the copy being pointed to.
            while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
            coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
            }
            
            // Populate the popup and set its coordinates
            // based on the feature found.
            popup.setLngLat(coordinates).setHTML(description).addTo(map);
        });
        
        map.on('mouseleave', 'places', function () {
            map.getCanvas().style.cursor = '';
            popup.remove();
        });
```

map.setFilterを使い、スライダーでの時間指定に応じて表示させる駅を変えます。Filterの式をテキストで定義したりしてなかなか驚きでした。

```javascript
        $("#slider").on("input", function(e){
            map.setFilter('places',['<=', ['get', 'time'], parseInt(e.target.value, 10)]);         
            // Value indicator
            sliderValue.textContent = e.target.value;
        });
```

調子に乗ってアニメーション機能もつけてみた。

```javascript
       var intervalId;
        $("#play_btn").click(function() {
            $("#play_btn").hide();
            $("#pause_btn").show();
            intervalId = setInterval(function(){
                var value = parseInt(slider.value, 10);
                if(value<180){
                    value += 1;
                }
                else{
                    value = 0;
                }
                $("#slider").val(value).trigger("input");
            }, 100);
        });
        $("#pause_btn").click(function() {
            clearInterval(intervalId);
            $("#play_btn").show();
            $("#pause_btn").hide();
        });
```

やれやれ。普段Javascriptはほとんど書かないので久々に書いてどっと疲れました。。。
でもサンプルコードのコピペを駆使してスライダーでフィルタリングまでできて感動です。

さて、どこに住もうか？