naohiro701 commited on
Commit
975b20f
·
verified ·
1 Parent(s): e4e9640

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +82 -57
app.py CHANGED
@@ -1,17 +1,22 @@
1
  import streamlit as st
2
- import requests
3
- import osmnx as ox
4
- import geopandas as gpd
5
- import folium
6
- from shapely.ops import unary_union, polygonize
7
- import pandas as pd
8
- import numpy as np
9
- from matplotlib.colors import LinearSegmentedColormap, to_hex
10
- 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
  """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"
@@ -34,61 +39,75 @@ def fetch_station_data(station: str, time_limit: int) -> gpd.GeoDataFrame:
34
  crs="EPSG:4326",
35
  )
36
 
37
- def build_isochrone_polygons(
38
- stations_gdf: gpd.GeoDataFrame,
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."""
@@ -103,9 +122,15 @@ def main():
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,
 
1
  import streamlit as st
2
+ import logging
3
+ import warnings
4
+
5
+ # Disable usage statistics collection message
6
+ st.set_option('browser.gatherUsageStats', False)
7
+ # Suppress Streamlit missing ScriptRunContext warnings
8
+ logging.getLogger("streamlit.runtime.scriptrunner_utils").setLevel(logging.ERROR)
9
+ # Suppress all warnings for a cleaner output
10
+ warnings.filterwarnings("ignore")
11
+
12
+ # Lazy import heavy libraries inside functions to speed up startup
13
+
14
+ def fetch_station_data(station: str, time_limit: int):
15
  """Fetch station travel-time data via API and return as GeoDataFrame."""
16
+ import requests
17
+ import pandas as pd
18
+ import geopandas as gpd
19
+
20
  api_url = (
21
  f"https://api.nearby-map.com/search_stations"
22
  f"?station={station}&time_limit={time_limit}&max_transfer=2"
 
39
  crs="EPSG:4326",
40
  )
41
 
42
+
43
+ def build_isochrone_polygons(stations_gdf, travel_speeds_kmh, time_limits):
44
+ """Generate isochrone polygons using a single pre-fetched graph to limit map data retrieval."""
45
+ # Lazy imports
46
+ import pandas as pd
47
+ import numpy as np
48
+ from matplotlib.colors import LinearSegmentedColormap, to_hex
49
+ import networkx as nx
50
+ import osmnx as ox
51
+ import geopandas as gpd
52
+ from shapely.ops import unary_union, polygonize
53
+
54
+ # Determine global max remaining time
55
+ min_travel = stations_gdf['travel_time'].min()
56
+ max_time_lim = max(time_limits)
57
+ max_remaining = max_time_lim - min_travel
58
+ if max_remaining <= 0:
59
+ return gpd.GeoDataFrame(columns=['time','color','geometry'], crs='EPSG:4326')
60
+
61
+ # Compute max distance (meters)
62
+ max_distance = max_remaining * travel_speeds_kmh * 1000 / 60
63
+ # Center at mean coordinates
64
+ center_lat = stations_gdf.geometry.y.mean()
65
+ center_lon = stations_gdf.geometry.x.mean()
66
+
67
+ # Prefetch graph once within bounding radius
68
+ G = ox.graph_from_point((center_lat, center_lon), dist=max_distance * 1.2, network_type='walk')
69
+ # Project and set edge travel time attribute
70
+ G = ox.project_graph(G)
71
+ mpm = travel_speeds_kmh * 1000 / 60
72
+ for u, v, k, data in G.edges(keys=True, data=True):
73
+ data['time'] = data['length'] / mpm
74
+
75
+ # Prepare colormap
76
+ cmap = LinearSegmentedColormap.from_list('iso', ['green','yellow','orange','red'], N=len(time_limits))
77
  colors = [to_hex(cmap(i)) for i in np.linspace(0, 1, len(time_limits))]
78
 
79
+ iso_list = []
80
+ # Generate isochrones for each time limit
81
+ for total_min in time_limits:
82
  polys = []
83
  for _, row in stations_gdf.iterrows():
84
+ rem = total_min - row['travel_time']
85
+ if rem <= 0:
 
 
 
 
 
 
 
 
86
  continue
87
+ # Find nearest node in pre-fetched graph
88
+ node = ox.nearest_nodes(G, row['longitude'], row['latitude'])
89
+ # Extract subgraph reachable within 'rem' minutes
90
+ sub = nx.ego_graph(G, node, radius=rem, distance='time')
91
+ if not sub.edges:
 
 
92
  continue
93
+ edges_gdf = ox.graph_to_gdfs(sub, nodes=False, edges=True)
94
  if edges_gdf.empty:
95
  continue
96
+ merged = edges_gdf['geometry'].unary_union
97
+ polys.extend(polygonize(merged))
 
 
 
98
  if polys:
99
  unified = unary_union(polys)
100
  gdf_iso = gpd.GeoDataFrame(
101
+ {'time': [total_min], 'color': [colors[time_limits.index(total_min)]]},
102
  geometry=[unified],
103
+ crs=G.graph['crs']
104
+ ).to_crs(epsg=4326)
105
+ iso_list.append(gdf_iso)
106
+
107
+ if iso_list:
108
+ return gpd.GeoDataFrame(pd.concat(iso_list, ignore_index=True), crs='EPSG:4326')
109
+ return gpd.GeoDataFrame(columns=['time','color','geometry'], crs='EPSG:4326')
110
 
 
 
 
 
 
111
 
112
  def main():
113
  """Streamlit application entrypoint."""
 
122
  st.warning("No station data.")
123
  return
124
  time_values = list(range(step, max_time + 1, step))
125
+ iso_gdf = build_isochrone_polygons(
126
+ stations, travel_speeds_kmh=4.8, time_limits=time_values
127
+ )
128
+ # Lazy import folium and streamlit_folium
129
+ import folium
130
+ from streamlit_folium import st_folium
131
+
132
+ center = [stations.geometry.y.mean(), stations.geometry.x.mean()]
133
+ fmap = folium.Map(location=center, zoom_start=12)
134
  for _, row in iso_gdf.iterrows():
135
  folium.GeoJson(
136
  row.geometry,