Spaces:
Running
Running
"feat(viz): add alt distance and stop rating to map popups"
Browse files- core/visualizers.py +49 -21
core/visualizers.py
CHANGED
|
@@ -152,7 +152,7 @@ def create_animated_map(structured_data=None):
|
|
| 152 |
if "lat" in sl and "lng" in sl:
|
| 153 |
center_lat, center_lon = sl["lat"], sl["lng"]
|
| 154 |
|
| 155 |
-
# 🔥 Map修正 1: height="100%" 讓它自動填滿父容器
|
| 156 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=13, tiles="OpenStreetMap",
|
| 157 |
height="100%",
|
| 158 |
width="100%"
|
|
@@ -173,11 +173,24 @@ def create_animated_map(structured_data=None):
|
|
| 173 |
|
| 174 |
index_to_name = {stop.get("stop_index"): stop.get("location") for stop in timeline}
|
| 175 |
|
| 176 |
-
|
|
|
|
| 177 |
for t in raw_tasks:
|
| 178 |
for cand in t.get("candidates", []):
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
task_id_to_seq = {}
|
| 183 |
for r in route_info:
|
|
@@ -240,21 +253,17 @@ def create_animated_map(structured_data=None):
|
|
| 240 |
|
| 241 |
route_group.add_to(m)
|
| 242 |
|
| 243 |
-
# --- Layer 2: 備用方案 (
|
| 244 |
-
# 🔥 Map修正 2: 不再使用單一的 alt_group,而是為每個任務建立獨立的 FeatureGroup
|
| 245 |
-
|
| 246 |
for idx, task in enumerate(tasks_detail):
|
| 247 |
tid = task.get("task_id")
|
| 248 |
step_seq = task_id_to_seq.get(tid, 0)
|
| 249 |
|
| 250 |
-
# 取得該任務的主題色
|
| 251 |
theme_idx = step_seq % len(THEMES)
|
| 252 |
theme_color, theme_name = THEMES[theme_idx]
|
| 253 |
|
| 254 |
chosen = task.get("chosen_poi", {})
|
| 255 |
alternatives = task.get("alternative_pois", [])
|
| 256 |
|
| 257 |
-
# 如果沒有備選點,就跳過
|
| 258 |
if not chosen or not alternatives:
|
| 259 |
continue
|
| 260 |
|
|
@@ -262,11 +271,10 @@ def create_animated_map(structured_data=None):
|
|
| 262 |
if not center_lat or not center_lng:
|
| 263 |
continue
|
| 264 |
|
| 265 |
-
# 取得主地點名稱
|
| 266 |
-
|
|
|
|
| 267 |
|
| 268 |
-
# 🔥 建立該任務專屬的 Group,並預設顯示 (show=True)
|
| 269 |
-
# 圖層名稱範例: "↳ Alt: Taipei 101"
|
| 270 |
specific_alt_group = folium.FeatureGroup(
|
| 271 |
name=f"↳ Alt: {chosen_name}",
|
| 272 |
show=True
|
|
@@ -277,14 +285,17 @@ def create_animated_map(structured_data=None):
|
|
| 277 |
if alat and alng:
|
| 278 |
bounds.append([alat, alng])
|
| 279 |
|
| 280 |
-
# 虛線連接
|
| 281 |
folium.PolyLine(
|
| 282 |
locations=[[center_lat, center_lng], [alat, alng]],
|
| 283 |
color=theme_color, weight=2, dash_array='5, 5', opacity=0.5
|
| 284 |
).add_to(specific_alt_group)
|
| 285 |
|
| 286 |
-
|
|
|
|
|
|
|
| 287 |
extra_min = alt.get("delta_travel_time_min", 0)
|
|
|
|
|
|
|
| 288 |
|
| 289 |
popup_html = create_popup_html(
|
| 290 |
title="ALTERNATIVE",
|
|
@@ -292,6 +303,7 @@ def create_animated_map(structured_data=None):
|
|
| 292 |
color="gray",
|
| 293 |
metrics={
|
| 294 |
"Add. Time": f"+{extra_min} min",
|
|
|
|
| 295 |
},
|
| 296 |
is_alternative=True
|
| 297 |
)
|
|
@@ -303,7 +315,6 @@ def create_animated_map(structured_data=None):
|
|
| 303 |
tooltip=f"Alt: {poi_name}"
|
| 304 |
).add_to(specific_alt_group)
|
| 305 |
|
| 306 |
-
# 將這個任務的專屬 Group 加入地圖
|
| 307 |
specific_alt_group.add_to(m)
|
| 308 |
|
| 309 |
# --- Layer 3: 主要站點 ---
|
|
@@ -312,6 +323,7 @@ def create_animated_map(structured_data=None):
|
|
| 312 |
for i, stop in enumerate(timeline):
|
| 313 |
coords = stop.get("coordinates", {})
|
| 314 |
lat, lng = coords.get("lat"), coords.get("lng")
|
|
|
|
| 315 |
|
| 316 |
if lat and lng:
|
| 317 |
bounds.append([lat, lng])
|
|
@@ -319,15 +331,31 @@ def create_animated_map(structured_data=None):
|
|
| 319 |
color_code, theme_name = THEMES[theme_idx]
|
| 320 |
loc_name = stop.get("location", "")
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
popup_html = create_popup_html(
|
| 323 |
title=f"STOP {i + 1}",
|
| 324 |
subtitle=loc_name,
|
| 325 |
color=theme_name,
|
| 326 |
-
metrics=
|
| 327 |
-
"Arrival": stop.get("time", ""),
|
| 328 |
-
"Weather": stop.get("weather", "").split(',')[0],
|
| 329 |
-
"AQI": stop.get("aqi", {}).get("label", "").split(' ')[-1]
|
| 330 |
-
}
|
| 331 |
)
|
| 332 |
|
| 333 |
if i == 0:
|
|
|
|
| 152 |
if "lat" in sl and "lng" in sl:
|
| 153 |
center_lat, center_lon = sl["lat"], sl["lng"]
|
| 154 |
|
| 155 |
+
# 🔥 Map修正 1: height="100%" 讓它自動填滿父容器
|
| 156 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=13, tiles="OpenStreetMap",
|
| 157 |
height="100%",
|
| 158 |
width="100%"
|
|
|
|
| 173 |
|
| 174 |
index_to_name = {stop.get("stop_index"): stop.get("location") for stop in timeline}
|
| 175 |
|
| 176 |
+
# 🔥 Data Prep 1: 建立 POI 詳細資訊對照表 (包含 Name 和 Rating)
|
| 177 |
+
poi_ref = {}
|
| 178 |
for t in raw_tasks:
|
| 179 |
for cand in t.get("candidates", []):
|
| 180 |
+
pid = cand.get("poi_id")
|
| 181 |
+
if pid:
|
| 182 |
+
poi_ref[pid] = {
|
| 183 |
+
"name": cand.get("name"),
|
| 184 |
+
"rating": cand.get("rating")
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
# 🔥 Data Prep 2: 建立 Step 到 POI ID 的對照表 (用於將 Timeline 連結到 Rating)
|
| 188 |
+
step_to_poi_id = {}
|
| 189 |
+
for r in route_info:
|
| 190 |
+
step_id = r.get("step")
|
| 191 |
+
pid = r.get("poi_id")
|
| 192 |
+
if step_id is not None and pid:
|
| 193 |
+
step_to_poi_id[step_id] = pid
|
| 194 |
|
| 195 |
task_id_to_seq = {}
|
| 196 |
for r in route_info:
|
|
|
|
| 253 |
|
| 254 |
route_group.add_to(m)
|
| 255 |
|
| 256 |
+
# --- Layer 2: 備用方案 (FeatureGroup 分組) ---
|
|
|
|
|
|
|
| 257 |
for idx, task in enumerate(tasks_detail):
|
| 258 |
tid = task.get("task_id")
|
| 259 |
step_seq = task_id_to_seq.get(tid, 0)
|
| 260 |
|
|
|
|
| 261 |
theme_idx = step_seq % len(THEMES)
|
| 262 |
theme_color, theme_name = THEMES[theme_idx]
|
| 263 |
|
| 264 |
chosen = task.get("chosen_poi", {})
|
| 265 |
alternatives = task.get("alternative_pois", [])
|
| 266 |
|
|
|
|
| 267 |
if not chosen or not alternatives:
|
| 268 |
continue
|
| 269 |
|
|
|
|
| 271 |
if not center_lat or not center_lng:
|
| 272 |
continue
|
| 273 |
|
| 274 |
+
# 取得主地點名稱
|
| 275 |
+
chosen_pid = chosen.get("poi_id")
|
| 276 |
+
chosen_name = poi_ref.get(chosen_pid, {}).get("name", f"Task {idx + 1}")
|
| 277 |
|
|
|
|
|
|
|
| 278 |
specific_alt_group = folium.FeatureGroup(
|
| 279 |
name=f"↳ Alt: {chosen_name}",
|
| 280 |
show=True
|
|
|
|
| 285 |
if alat and alng:
|
| 286 |
bounds.append([alat, alng])
|
| 287 |
|
|
|
|
| 288 |
folium.PolyLine(
|
| 289 |
locations=[[center_lat, center_lng], [alat, alng]],
|
| 290 |
color=theme_color, weight=2, dash_array='5, 5', opacity=0.5
|
| 291 |
).add_to(specific_alt_group)
|
| 292 |
|
| 293 |
+
alt_pid = alt.get("poi_id")
|
| 294 |
+
poi_name = poi_ref.get(alt_pid, {}).get("name", "Alternative Option")
|
| 295 |
+
|
| 296 |
extra_min = alt.get("delta_travel_time_min", 0)
|
| 297 |
+
# 🔥 更新:獲取 delta distance (注意 JSON key 是 'm' 而非 'meters')
|
| 298 |
+
extra_dist = alt.get("delta_travel_distance_m", 0)
|
| 299 |
|
| 300 |
popup_html = create_popup_html(
|
| 301 |
title="ALTERNATIVE",
|
|
|
|
| 303 |
color="gray",
|
| 304 |
metrics={
|
| 305 |
"Add. Time": f"+{extra_min} min",
|
| 306 |
+
"Add. Dist": f"+{extra_dist} m" # 🔥 新增距離顯示
|
| 307 |
},
|
| 308 |
is_alternative=True
|
| 309 |
)
|
|
|
|
| 315 |
tooltip=f"Alt: {poi_name}"
|
| 316 |
).add_to(specific_alt_group)
|
| 317 |
|
|
|
|
| 318 |
specific_alt_group.add_to(m)
|
| 319 |
|
| 320 |
# --- Layer 3: 主要站點 ---
|
|
|
|
| 323 |
for i, stop in enumerate(timeline):
|
| 324 |
coords = stop.get("coordinates", {})
|
| 325 |
lat, lng = coords.get("lat"), coords.get("lng")
|
| 326 |
+
stop_idx = stop.get("stop_index")
|
| 327 |
|
| 328 |
if lat and lng:
|
| 329 |
bounds.append([lat, lng])
|
|
|
|
| 331 |
color_code, theme_name = THEMES[theme_idx]
|
| 332 |
loc_name = stop.get("location", "")
|
| 333 |
|
| 334 |
+
# 🔥 更新:透過 stop_index -> route -> poi_id -> rating 獲取評分
|
| 335 |
+
rating_display = ""
|
| 336 |
+
if stop_idx is not None:
|
| 337 |
+
# 嘗試從 route map 找到 poi_id
|
| 338 |
+
pid = step_to_poi_id.get(stop_idx)
|
| 339 |
+
if pid and pid in poi_ref:
|
| 340 |
+
rating_val = poi_ref[pid].get("rating")
|
| 341 |
+
if rating_val:
|
| 342 |
+
rating_display = f"{rating_val} ⭐"
|
| 343 |
+
|
| 344 |
+
metrics_data = {
|
| 345 |
+
"Arrival": stop.get("time", ""),
|
| 346 |
+
"Weather": stop.get("weather", "").split(',')[0],
|
| 347 |
+
"AQI": stop.get("aqi", {}).get("label", "").split(' ')[-1]
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
# 🔥 新增 Rating 到 Metrics
|
| 351 |
+
if rating_display:
|
| 352 |
+
metrics_data["Rating"] = rating_display
|
| 353 |
+
|
| 354 |
popup_html = create_popup_html(
|
| 355 |
title=f"STOP {i + 1}",
|
| 356 |
subtitle=loc_name,
|
| 357 |
color=theme_name,
|
| 358 |
+
metrics=metrics_data
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
)
|
| 360 |
|
| 361 |
if i == 0:
|