Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
33
Help us understand the problem. What is going on with this article?
@aerialist

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

これまで

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

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

ということで、その1その2では、「駅すぱあとWebサービス フリープラン」を利用して作成しようと思ったのですが挫折しました。

成果物

ついにやりたいことがほぼできました。こんな感じ。
nihonbashi180_5fps.gif

ぜひこちらを開いてぐりぐりしてみてください。
3Dマップバージョンはさらに面白いです。(Ctl押しながらマウスでぐりぐりすると3次元になる)

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

NavitimeがRakutenRapid経由で提供してくれているNAVITIME Reachableがまさに探し求めていたものでした。探索開始地点と時間を指定すると、到達できる駅を返してくれる。ありがたく使わせていただきます。

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

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

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をファイルに書き出しておく。

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)

コード全部はこちら。

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を書いてみました。

出来上がりはこちらを覗いてください。

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

    <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のスタイルを指定。起点の日本橋駅にはマーカーを立てる。

    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で丸として表現します。

        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を使う。

        // 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の式をテキストで定義したりしてなかなか驚きでした。

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

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

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

さて、どこに住もうか?

33
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
33
Help us understand the problem. What is going on with this article?