Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,14 +11,7 @@ import networkx as nx
|
|
| 11 |
from streamlit_folium import st_folium
|
| 12 |
|
| 13 |
def fetch_station_data(station: str, time_limit: int) -> gpd.GeoDataFrame:
|
| 14 |
-
"""
|
| 15 |
-
Fetch station travel-time data via API and return as GeoDataFrame.
|
| 16 |
-
Args:
|
| 17 |
-
station (str): Station name to query.
|
| 18 |
-
time_limit (int): Maximum travel time (minutes).
|
| 19 |
-
Returns:
|
| 20 |
-
gpd.GeoDataFrame: station points with 'travel_time' and geometry.
|
| 21 |
-
"""
|
| 22 |
api_url = (
|
| 23 |
f"https://api.nearby-map.com/search_stations"
|
| 24 |
f"?station={station}&time_limit={time_limit}&max_transfer=2"
|
|
@@ -29,15 +22,12 @@ def fetch_station_data(station: str, time_limit: int) -> gpd.GeoDataFrame:
|
|
| 29 |
return gpd.GeoDataFrame()
|
| 30 |
items = res.json().get("data", [])
|
| 31 |
df = pd.DataFrame(items)
|
| 32 |
-
df.rename(
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
},
|
| 39 |
-
inplace=True,
|
| 40 |
-
)
|
| 41 |
return gpd.GeoDataFrame(
|
| 42 |
df,
|
| 43 |
geometry=gpd.points_from_xy(df.longitude, df.latitude),
|
|
@@ -49,68 +39,73 @@ def build_isochrone_polygons(
|
|
| 49 |
travel_speeds_kmh: float,
|
| 50 |
time_limits: list[int]
|
| 51 |
) -> gpd.GeoDataFrame:
|
| 52 |
-
"""
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
stations_gdf (gpd.GeoDataFrame): Stations with travel_time.
|
| 56 |
-
travel_speeds_kmh (float): Walking speed in km/h.
|
| 57 |
-
time_limits (list[int]): List of total travel times (min).
|
| 58 |
-
Returns:
|
| 59 |
-
gpd.GeoDataFrame: Polygons with 'time' and 'color'.
|
| 60 |
-
"""
|
| 61 |
-
# prepare colormap
|
| 62 |
-
cmap = LinearSegmentedColormap.from_list("iso", ["green","yellow","orange","red"], N=len(time_limits))
|
| 63 |
-
colors = [to_hex(cmap(i)) for i in np.linspace(0,1,len(time_limits))]
|
| 64 |
|
| 65 |
-
def single_isochrone(total_min: int):
|
|
|
|
| 66 |
polys = []
|
| 67 |
for _, row in stations_gdf.iterrows():
|
| 68 |
-
|
| 69 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
continue
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
if not G.edges:
|
| 74 |
continue
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
for u,v,k,d in G.edges(keys=True, data=True):
|
| 79 |
-
d["time"] = d["length"]/mpm
|
| 80 |
-
sub = nx.ego_graph(G, center, radius=rem, distance="time")
|
| 81 |
-
if not sub.edges:
|
| 82 |
continue
|
| 83 |
-
|
| 84 |
-
merged = unary_union(edges.unary_union)
|
| 85 |
-
polys += list(polygonize(merged))
|
| 86 |
if polys:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
{"time": [total_min], "color": [colors[time_limits.index(total_min)]]},
|
| 90 |
-
geometry=[
|
| 91 |
-
crs=
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
return g.to_crs(epsg=4326)
|
| 95 |
return None
|
| 96 |
|
| 97 |
iso_list = [single_isochrone(t) for t in time_limits]
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
def main():
|
|
|
|
| 101 |
st.title("Isochrone Map Demo")
|
| 102 |
-
|
| 103 |
max_time = st.slider("Max travel time (min)", 10, 60, 30, 10)
|
| 104 |
-
step = st.selectbox("Time step (min)", [5,10,15], index=1)
|
| 105 |
-
if st.button("Generate Map"):
|
| 106 |
with st.spinner("Calculating…"):
|
| 107 |
-
stations = fetch_station_data(
|
| 108 |
if stations.empty:
|
| 109 |
st.warning("No station data.")
|
| 110 |
return
|
| 111 |
-
|
| 112 |
-
iso_gdf = build_isochrone_polygons(stations, travel_speeds_kmh=4.8, time_limits=
|
| 113 |
-
|
|
|
|
| 114 |
for _, row in iso_gdf.iterrows():
|
| 115 |
folium.GeoJson(
|
| 116 |
row.geometry,
|
|
@@ -120,13 +115,18 @@ def main():
|
|
| 120 |
"weight": 2,
|
| 121 |
"fillOpacity": 0.2,
|
| 122 |
},
|
| 123 |
-
).add_to(
|
| 124 |
for _, row in stations.iterrows():
|
| 125 |
folium.Marker(
|
| 126 |
[row["latitude"], row["longitude"]],
|
| 127 |
popup=f"{row['station_name']} ({int(row['travel_time'])}分)"
|
| 128 |
-
).add_to(
|
| 129 |
-
st_folium(
|
| 130 |
|
|
|
|
| 131 |
if __name__ == "__main__":
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from streamlit_folium import st_folium
|
| 12 |
|
| 13 |
def fetch_station_data(station: str, time_limit: int) -> gpd.GeoDataFrame:
|
| 14 |
+
"""Fetch station travel-time data via API and return as GeoDataFrame."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
api_url = (
|
| 16 |
f"https://api.nearby-map.com/search_stations"
|
| 17 |
f"?station={station}&time_limit={time_limit}&max_transfer=2"
|
|
|
|
| 22 |
return gpd.GeoDataFrame()
|
| 23 |
items = res.json().get("data", [])
|
| 24 |
df = pd.DataFrame(items)
|
| 25 |
+
df.rename(columns={
|
| 26 |
+
"name": "station_name",
|
| 27 |
+
"time": "travel_time",
|
| 28 |
+
"lat": "latitude",
|
| 29 |
+
"lon": "longitude",
|
| 30 |
+
}, inplace=True)
|
|
|
|
|
|
|
|
|
|
| 31 |
return gpd.GeoDataFrame(
|
| 32 |
df,
|
| 33 |
geometry=gpd.points_from_xy(df.longitude, df.latitude),
|
|
|
|
| 39 |
travel_speeds_kmh: float,
|
| 40 |
time_limits: list[int]
|
| 41 |
) -> gpd.GeoDataFrame:
|
| 42 |
+
"""Generate isochrone polygons for each time limit."""
|
| 43 |
+
cmap = LinearSegmentedColormap.from_list("iso", ["green", "yellow", "orange", "red"], N=len(time_limits))
|
| 44 |
+
colors = [to_hex(cmap(i)) for i in np.linspace(0, 1, len(time_limits))]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
def single_isochrone(total_min: int) -> gpd.GeoDataFrame | None:
|
| 47 |
+
"""Generate a single isochrone for a total travel time."""
|
| 48 |
polys = []
|
| 49 |
for _, row in stations_gdf.iterrows():
|
| 50 |
+
remaining = total_min - row["travel_time"]
|
| 51 |
+
if remaining <= 0:
|
| 52 |
+
continue
|
| 53 |
+
max_distance = remaining * travel_speeds_kmh * 1000 / 60
|
| 54 |
+
graph = ox.graph_from_point(
|
| 55 |
+
(row["latitude"], row["longitude"]),
|
| 56 |
+
dist=max_distance * 1.2,
|
| 57 |
+
network_type="walk"
|
| 58 |
+
)
|
| 59 |
+
if not graph.edges:
|
| 60 |
+
continue
|
| 61 |
+
center_node = ox.nearest_nodes(graph, row["longitude"], row["latitude"])
|
| 62 |
+
graph = ox.project_graph(graph)
|
| 63 |
+
meters_per_minute = travel_speeds_kmh * 1000 / 60
|
| 64 |
+
for u, v, k, data in graph.edges(keys=True, data=True):
|
| 65 |
+
data["time"] = data["length"] / meters_per_minute
|
| 66 |
+
subgraph = nx.ego_graph(graph, center_node, radius=remaining, distance="time")
|
| 67 |
+
if not subgraph.edges:
|
| 68 |
continue
|
| 69 |
+
edges_gdf = ox.graph_to_gdfs(subgraph, nodes=False, edges=True)
|
| 70 |
+
if edges_gdf.empty:
|
|
|
|
| 71 |
continue
|
| 72 |
+
merged_lines = edges_gdf["geometry"].unary_union
|
| 73 |
+
polygons = list(polygonize(merged_lines))
|
| 74 |
+
if not polygons:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
continue
|
| 76 |
+
polys.extend(polygons)
|
|
|
|
|
|
|
| 77 |
if polys:
|
| 78 |
+
unified = unary_union(polys)
|
| 79 |
+
gdf_iso = gpd.GeoDataFrame(
|
| 80 |
{"time": [total_min], "color": [colors[time_limits.index(total_min)]]},
|
| 81 |
+
geometry=[unified],
|
| 82 |
+
crs=graph.graph["crs"]
|
| 83 |
+
)
|
| 84 |
+
return gdf_iso.to_crs(epsg=4326)
|
|
|
|
| 85 |
return None
|
| 86 |
|
| 87 |
iso_list = [single_isochrone(t) for t in time_limits]
|
| 88 |
+
gdfs = [g for g in iso_list if g is not None]
|
| 89 |
+
if gdfs:
|
| 90 |
+
return gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True), crs="EPSG:4326")
|
| 91 |
+
return gpd.GeoDataFrame(columns=["time","color","geometry"], crs="EPSG:4326")
|
| 92 |
|
| 93 |
def main():
|
| 94 |
+
"""Streamlit application entrypoint."""
|
| 95 |
st.title("Isochrone Map Demo")
|
| 96 |
+
station_name = st.text_input("Station name", "溜池山王")
|
| 97 |
max_time = st.slider("Max travel time (min)", 10, 60, 30, 10)
|
| 98 |
+
step = st.selectbox("Time step (min)", [5, 10, 15], index=1)
|
| 99 |
+
if st.button("Generate Map"]):
|
| 100 |
with st.spinner("Calculating…"):
|
| 101 |
+
stations = fetch_station_data(station_name, max_time)
|
| 102 |
if stations.empty:
|
| 103 |
st.warning("No station data.")
|
| 104 |
return
|
| 105 |
+
time_values = list(range(step, max_time + 1, step))
|
| 106 |
+
iso_gdf = build_isochrone_polygons(stations, travel_speeds_kmh=4.8, time_limits=time_values)
|
| 107 |
+
map_center = [stations.geometry.y.mean(), stations.geometry.x.mean()]
|
| 108 |
+
fmap = folium.Map(location=map_center, zoom_start=12)
|
| 109 |
for _, row in iso_gdf.iterrows():
|
| 110 |
folium.GeoJson(
|
| 111 |
row.geometry,
|
|
|
|
| 115 |
"weight": 2,
|
| 116 |
"fillOpacity": 0.2,
|
| 117 |
},
|
| 118 |
+
).add_to(fmap)
|
| 119 |
for _, row in stations.iterrows():
|
| 120 |
folium.Marker(
|
| 121 |
[row["latitude"], row["longitude"]],
|
| 122 |
popup=f"{row['station_name']} ({int(row['travel_time'])}分)"
|
| 123 |
+
).add_to(fmap)
|
| 124 |
+
st_folium(fmap, width=700, height=500)
|
| 125 |
|
| 126 |
+
# Allow script to be run directly with "python app.py"
|
| 127 |
if __name__ == "__main__":
|
| 128 |
+
import sys
|
| 129 |
+
from streamlit.web import cli as stcli
|
| 130 |
+
|
| 131 |
+
sys.argv = ["streamlit", "run", sys.argv[0]]
|
| 132 |
+
sys.exit(stcli.main())
|