Skip to content

Commit c31370d

Browse files
committed
added A*, the SHAP and AADT features
1 parent 00a13a9 commit c31370d

5 files changed

Lines changed: 167 additions & 9 deletions

File tree

backend/astar_route.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
SafeWay A* safer route computation.
3+
Separated from main.py so the logic is self-contained and testable.
4+
"""
5+
from __future__ import annotations
6+
import sys
7+
from pathlib import Path
8+
9+
# Ensure the backend directory is on sys.path for sibling imports
10+
_backend_dir = str(Path(__file__).resolve().parent)
11+
if _backend_dir not in sys.path:
12+
sys.path.insert(0, _backend_dir)
13+
14+
15+
def compute_astar_route(
16+
origin_lat: float,
17+
origin_lng: float,
18+
dest_lat: float,
19+
dest_lng: float,
20+
departure_hour: int | None = None,
21+
) -> dict | None:
22+
"""
23+
Compute a safety-optimized A* route between two lat/lng points.
24+
25+
Returns a route dict with the same shape as Google route entries, or None
26+
if the route cannot be computed (e.g. graph not loaded, same origin/dest).
27+
"""
28+
try:
29+
import osmnx as ox
30+
from risk_cache import get_prepared_graph, score_coordinates
31+
from model.route_scoring import (
32+
find_safer_route,
33+
path_to_coordinates,
34+
encode_polyline,
35+
estimate_route_aadt,
36+
)
37+
38+
G = get_prepared_graph()
39+
orig_node = ox.distance.nearest_nodes(G, X=origin_lng, Y=origin_lat)
40+
dest_node = ox.distance.nearest_nodes(G, X=dest_lng, Y=dest_lat)
41+
if orig_node == dest_node:
42+
return None
43+
44+
astar = find_safer_route(G, orig_node, dest_node, beta=0.5)
45+
safe_coords = path_to_coordinates(G, astar["safe_path"])
46+
if len(safe_coords) < 2:
47+
return None
48+
49+
safe_polyline = encode_polyline(safe_coords)
50+
safety = score_coordinates(safe_coords, sample_every=3, departure_hour=departure_hour)
51+
aadt = estimate_route_aadt(G, astar["safe_path"])
52+
53+
return {
54+
"distance_meters": astar["safe_distance_m"],
55+
"duration": f"{astar['safe_time_secs']}s",
56+
"polyline": safe_polyline,
57+
"coordinates": safe_coords,
58+
"safety_score": safety.get("score"),
59+
"safety_label": safety.get("label", "unknown"),
60+
"route_source": "safeway",
61+
"risk_per_km": safety.get("risk_per_km"),
62+
"total_exposure": safety.get("total_exposure"),
63+
"route_km": safety.get("route_km"),
64+
"n_high_risk": safety.get("n_high_risk", 0),
65+
"top_risk_factors": safety.get("top_risk_factors", []),
66+
"time_band": safety.get("time_band"),
67+
"segment_risks": safety.get("segment_risks", []),
68+
"high_risk_coords": safety.get("high_risk_coords", []),
69+
"aadt_avg": aadt.get("aadt_avg"),
70+
"aadt_max": aadt.get("aadt_max"),
71+
"time_penalty_pct": astar["time_penalty_pct"],
72+
"risk_reduction_pct": astar["risk_reduction_pct"],
73+
}
74+
except Exception as e:
75+
print(f"[astar] route computation failed: {e}", flush=True)
76+
return None

backend/main.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,9 @@ def compute_route(payload: RouteRequest):
442442
"top_risk_factors": safety.get("top_risk_factors", []),
443443
"time_band": safety.get("time_band"),
444444
"segment_risks": safety.get("segment_risks", []),
445+
"high_risk_coords": safety.get("high_risk_coords", []),
446+
"aadt_avg": safety.get("aadt_avg"),
447+
"aadt_max": safety.get("aadt_max"),
445448
}
446449
except Exception as score_err:
447450
print(f"[route] score_coordinates failed for route: {score_err}", flush=True)
@@ -470,9 +473,15 @@ def compute_route(payload: RouteRequest):
470473
"route_source": "google",
471474
})
472475

473-
# ── SafeWay A* safer route — temporarily disabled ──────────────────
474-
# TODO: re-enable once Google routes + scoring are confirmed working
475-
pass
476+
# ── SafeWay A* safer route ──────────────────────────────────────────
477+
from astar_route import compute_astar_route
478+
astar_result = compute_astar_route(
479+
payload.origin.lat, payload.origin.lng,
480+
payload.destination.lat, payload.destination.lng,
481+
departure_hour=payload.departure_hour,
482+
)
483+
if astar_result:
484+
result_routes.append(astar_result)
476485

477486
# Sort: SafeWay route first, then by safety score ascending (safest first)
478487
result_routes.sort(key=lambda r: (

backend/risk_cache.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,38 @@ def score_coordinates(
394394
"risk": round(seg_risk, 1),
395395
})
396396

397+
# Coordinates of high-risk nodes for ⚠️ map markers
398+
high_risk_coords = []
399+
for nid, r in zip(nearest, risks):
400+
if r > 66:
401+
try:
402+
high_risk_coords.append({
403+
"latitude": G.nodes[nid]["y"],
404+
"longitude": G.nodes[nid]["x"],
405+
})
406+
except Exception:
407+
pass
408+
409+
# AADT estimate from edge highway types at sampled nodes
410+
_ROAD_CLASS = {
411+
"residential": 1, "living_street": 1, "unclassified": 1,
412+
"tertiary": 2, "tertiary_link": 2,
413+
"secondary": 3, "secondary_link": 3,
414+
"primary": 4, "primary_link": 4,
415+
"trunk": 5, "trunk_link": 5,
416+
"motorway": 6, "motorway_link": 6,
417+
}
418+
_AADT_PROXY = {1: 1_000, 2: 5_000, 3: 15_000, 4: 25_000, 5: 40_000, 6: 60_000}
419+
aadt_values = []
420+
for nid in nearest:
421+
for _, _, ed in list(G.edges(nid, data=True))[:2]:
422+
hw = ed.get("highway", "residential")
423+
if isinstance(hw, list):
424+
hw = hw[0] if hw else "residential"
425+
aadt_values.append(_AADT_PROXY.get(_ROAD_CLASS.get(hw, 1), 1_000))
426+
aadt_avg = round(sum(aadt_values) / len(aadt_values)) if aadt_values else None
427+
aadt_max = max(aadt_values) if aadt_values else None
428+
397429
return {
398430
"score": score,
399431
"label": label,
@@ -406,4 +438,7 @@ def score_coordinates(
406438
"top_risk_factors": top_risk_factors,
407439
"time_band": time_band,
408440
"segment_risks": segment_risks,
441+
"high_risk_coords": high_risk_coords,
442+
"aadt_avg": aadt_avg,
443+
"aadt_max": aadt_max,
409444
}

frontend/app/directions.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ interface ModeRouteData {
7878
topRiskFactors?: { factor: string; weight: number }[] | { label: string; count: number; pct: number }[];
7979
timeBand?: string;
8080
segmentRisks?: number[];
81+
highRiskCoords?: Array<{ latitude: number; longitude: number }>;
8182
aadtAvg?: number;
8283
aadtMax?: number;
8384
timePenaltyPct?: number;
@@ -95,6 +96,7 @@ interface ModeRoutes {
9596
topRiskFactors?: any[];
9697
timeBand?: string;
9798
segmentRisks?: number[];
99+
highRiskCoords?: Array<{ latitude: number; longitude: number }>;
98100
aadtAvg?: number;
99101
aadtMax?: number;
100102
timePenaltyPct?: number;
@@ -186,6 +188,7 @@ function mapSafetyRoutesToAlternatives(safetyRoutes: SafetyRoute[]): RouteAltRow
186188
topRiskFactors: r.top_risk_factors,
187189
timeBand: r.time_band,
188190
segmentRisks: r.segment_risks,
191+
highRiskCoords: r.high_risk_coords ?? [],
189192
aadtAvg: r.aadt_avg,
190193
aadtMax: r.aadt_max,
191194
timePenaltyPct: r.time_penalty_pct,
@@ -211,6 +214,7 @@ function primaryToAlternativeRow(primary: ModeRouteData, index = 0): RouteAltRow
211214
topRiskFactors: primary.topRiskFactors,
212215
timeBand: primary.timeBand,
213216
segmentRisks: primary.segmentRisks,
217+
highRiskCoords: primary.highRiskCoords,
214218
aadtAvg: primary.aadtAvg,
215219
aadtMax: primary.aadtMax,
216220
timePenaltyPct: primary.timePenaltyPct,
@@ -429,8 +433,22 @@ function RouteDetailsModal({
429433
)}
430434
<Marker coordinate={{ latitude: destLat, longitude: destLng }} pinColor="#FF4444" />
431435
{activeData?.coords?.length ? (
432-
<Polyline coordinates={activeData.coords} strokeColor={activeData.routeSource === 'safeway' ? '#1ABC93' : '#4A90E2'} strokeWidth={4} />
436+
(activeData.segmentRisks as any[])?.length
437+
? (activeData.segmentRisks as any[]).map((seg: any, si: number) => (
438+
<Polyline
439+
key={`modal-seg-${si}`}
440+
coordinates={[seg.start, seg.end]}
441+
strokeColor={seg.risk > 66 ? '#FF4444' : seg.risk > 33 ? '#FFA500' : '#1ABC93'}
442+
strokeWidth={4}
443+
/>
444+
))
445+
: <Polyline coordinates={activeData.coords} strokeColor={activeData.routeSource === 'safeway' ? '#1ABC93' : '#4A90E2'} strokeWidth={4} />
433446
) : null}
447+
{(activeData?.highRiskCoords as any[] ?? []).map((coord: any, i: number) => (
448+
<Marker key={`modal-hs-${i}`} coordinate={coord} anchor={{ x: 0.5, y: 1.0 }} tracksViewChanges={false}>
449+
<Text style={{ fontSize: 14 }}>⚠️</Text>
450+
</Marker>
451+
))}
434452
</MapView>
435453
{activeData && (
436454
<View style={dm.routeTimeBubble}>
@@ -782,7 +800,7 @@ export default function DirectionsScreen() {
782800
safetyScore: primary.safetyScore, safetyLabel: primary.safetyLabel, routeSource: primary.routeSource,
783801
riskPerKm: primary.riskPerKm, nHighRisk: primary.nHighRisk, routeKm: primary.routeKm,
784802
topRiskFactors: primary.topRiskFactors, timeBand: primary.timeBand,
785-
segmentRisks: primary.segmentRisks, aadtAvg: primary.aadtAvg, aadtMax: primary.aadtMax,
803+
segmentRisks: primary.segmentRisks, highRiskCoords: primary.highRiskCoords, aadtAvg: primary.aadtAvg, aadtMax: primary.aadtMax,
786804
timePenaltyPct: primary.timePenaltyPct, riskReductionPct: primary.riskReductionPct,
787805
},
788806
alternatives: alts,
@@ -819,7 +837,7 @@ export default function DirectionsScreen() {
819837
safetyScore: primary.safetyScore, safetyLabel: primary.safetyLabel, routeSource: primary.routeSource,
820838
riskPerKm: primary.riskPerKm, nHighRisk: primary.nHighRisk, routeKm: primary.routeKm,
821839
topRiskFactors: primary.topRiskFactors, timeBand: primary.timeBand,
822-
segmentRisks: primary.segmentRisks, aadtAvg: primary.aadtAvg, aadtMax: primary.aadtMax,
840+
segmentRisks: primary.segmentRisks, highRiskCoords: primary.highRiskCoords, aadtAvg: primary.aadtAvg, aadtMax: primary.aadtMax,
823841
timePenaltyPct: primary.timePenaltyPct, riskReductionPct: primary.riskReductionPct,
824842
},
825843
alternatives: alts,
@@ -842,7 +860,7 @@ export default function DirectionsScreen() {
842860
safetyScore: primary.safetyScore, safetyLabel: primary.safetyLabel, routeSource: primary.routeSource,
843861
riskPerKm: primary.riskPerKm, nHighRisk: primary.nHighRisk, routeKm: primary.routeKm,
844862
topRiskFactors: primary.topRiskFactors, timeBand: primary.timeBand,
845-
segmentRisks: primary.segmentRisks, aadtAvg: primary.aadtAvg, aadtMax: primary.aadtMax,
863+
segmentRisks: primary.segmentRisks, highRiskCoords: primary.highRiskCoords, aadtAvg: primary.aadtAvg, aadtMax: primary.aadtMax,
846864
timePenaltyPct: primary.timePenaltyPct, riskReductionPct: primary.riskReductionPct,
847865
},
848866
alternatives: alts,
@@ -981,7 +999,8 @@ export default function DirectionsScreen() {
981999
safetyScore: selectedAlt.safetyScore, safetyLabel: selectedAlt.safetyLabel, routeSource: selectedAlt.routeSource,
9821000
riskPerKm: selectedAlt.riskPerKm, nHighRisk: selectedAlt.nHighRisk, routeKm: selectedAlt.routeKm,
9831001
topRiskFactors: selectedAlt.topRiskFactors, timeBand: selectedAlt.timeBand,
984-
segmentRisks: selectedAlt.segmentRisks, aadtAvg: selectedAlt.aadtAvg, aadtMax: selectedAlt.aadtMax,
1002+
segmentRisks: selectedAlt.segmentRisks, highRiskCoords: selectedAlt.highRiskCoords,
1003+
aadtAvg: selectedAlt.aadtAvg, aadtMax: selectedAlt.aadtMax,
9851004
timePenaltyPct: selectedAlt.timePenaltyPct, riskReductionPct: selectedAlt.riskReductionPct,
9861005
}
9871006
: modeData?.primary ?? null;
@@ -1020,6 +1039,19 @@ export default function DirectionsScreen() {
10201039
{alternatives.map((alt, i) => {
10211040
if (!alt.coords?.length) return null;
10221041
const isSelected = i === selectedRouteIndex;
1042+
const segs = alt.segmentRisks as any[] | undefined;
1043+
if (isSelected && segs?.length) {
1044+
return segs.map((seg: any, si: number) => (
1045+
<Polyline
1046+
key={`${travelMode}-alt-${i}-seg-${si}`}
1047+
coordinates={[seg.start, seg.end]}
1048+
strokeColor={seg.risk > 66 ? '#FF4444' : seg.risk > 33 ? '#FFA500' : '#1ABC93'}
1049+
strokeWidth={5}
1050+
tappable
1051+
onPress={() => setSelectedRouteIndex(i)}
1052+
/>
1053+
));
1054+
}
10231055
return (
10241056
<Polyline
10251057
key={`${travelMode}-alt-${i}`}
@@ -1034,6 +1066,11 @@ export default function DirectionsScreen() {
10341066
{alternatives.length === 0 && activeData?.coords?.length ? (
10351067
<Polyline key={travelMode} coordinates={activeData.coords} strokeColor="#4A90E2" strokeWidth={5} />
10361068
) : null}
1069+
{(alternatives[selectedRouteIndex]?.highRiskCoords as any[] ?? []).map((coord: any, i: number) => (
1070+
<Marker key={`hs-${i}`} coordinate={coord} anchor={{ x: 0.5, y: 1.0 }} tracksViewChanges={false}>
1071+
<Text style={{ fontSize: 16 }}>⚠️</Text>
1072+
</Marker>
1073+
))}
10371074
{heatmapFilter !== 'off' && crashPoints.length > 0 && (
10381075
<Heatmap points={crashPoints} opacity={0.72} radius={20}
10391076
gradient={{ colors: ['#00E5FF', '#FFD600', '#FF1744'], startPoints: [0.1, 0.5, 1.0], colorMapSize: 256 }}

frontend/lib/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ export type SafetyRoute = {
225225
n_high_risk?: number;
226226
top_risk_factors?: { factor: string; weight: number }[];
227227
time_band?: string;
228-
segment_risks?: number[];
228+
segment_risks?: any[];
229+
high_risk_coords?: Array<{ latitude: number; longitude: number }>;
229230
time_penalty_pct?: number;
230231
risk_reduction_pct?: number;
231232
aadt_avg?: number;

0 commit comments

Comments
 (0)