これまで
コロナで在宅勤務が続いているため、都会で手狭な家に住んでいるのが苦しくなってきました。週に数日や月に数日しか出社しないなら、通勤に片道2時間ぐらいかかる田舎で暮らすことも現実的かも。(定期代がフルに支給されない)新幹線通勤までも視野に入れれば結構遠くに住めるかも?
今ひとつ土地勘がないので、会社の最寄り駅に平日朝9時に到着するための乗車時間を地図にマッピングして候補地を探していきたい。
ということで、その1とその2では、「駅すぱあとWebサービス フリープラン」を利用して作成しようと思ったのですが挫折しました。
成果物
ぜひこちらを開いてぐりぐりしてみてください。
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)
コード全部はこちら。
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はほとんど書かないので久々に書いてどっと疲れました。。。
でもサンプルコードのコピペを駆使してスライダーでフィルタリングまでできて感動です。
さて、どこに住もうか?