fix: restrict map route domain to Chennai region and implement numbered stop markers

This commit is contained in:
Shanmuga Krishnan S M
2026-07-24 09:34:24 +05:30
parent 2295187788
commit 8e87bd72cd
4 changed files with 2492 additions and 675 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -16,23 +16,34 @@ L.Icon.Default.mergeOptions({
shadowUrl: markerShadow, shadowUrl: markerShadow,
}); });
// Domain Boundary validation (Chennai and surrounding route regions)
const MIN_LAT = 12.7;
const MAX_LAT = 13.4;
const MIN_LNG = 79.6;
const MAX_LNG = 80.4;
const isValidCoordinate = (lat?: number, lng?: number): boolean => {
if (lat === undefined || lng === undefined) return false;
return lat >= MIN_LAT && lat <= MAX_LAT && lng >= MIN_LNG && lng <= MAX_LNG;
};
// Custom Premium DivIcons using Tailwind CSS // Custom Premium DivIcons using Tailwind CSS
const startIcon = L.divIcon({ const getStartIcon = (label: string = "1") => L.divIcon({
html: `<div class="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500 border-2 border-white shadow-md text-white text-[10px] font-bold">A</div>`, html: `<div class="flex items-center justify-center w-6 h-6 rounded-full bg-emerald-500 border-2 border-white shadow-md text-white text-[10px] font-extrabold hover:scale-110 transition-transform">${label}</div>`,
className: '', className: '',
iconSize: [24, 24], iconSize: [24, 24],
iconAnchor: [12, 12], iconAnchor: [12, 12],
}); });
const stopIcon = L.divIcon({ const getStopIcon = (num: number) => L.divIcon({
html: `<div class="w-3.5 h-3.5 rounded-full bg-blue-500 border-2 border-white shadow-sm hover:scale-125 transition-transform"></div>`, html: `<div class="flex items-center justify-center w-5.5 h-5.5 rounded-full bg-blue-600 border-2 border-white shadow-md text-white text-[10px] font-bold hover:scale-125 hover:bg-blue-700 transition-all">${num}</div>`,
className: '', className: '',
iconSize: [14, 14], iconSize: [22, 22],
iconAnchor: [7, 7], iconAnchor: [11, 11],
}); });
const campusIcon = L.divIcon({ const campusIcon = L.divIcon({
html: `<div class="flex items-center justify-center w-9 h-9 rounded-full bg-gradient-to-tr from-orange-500 to-amber-500 border-2 border-white shadow-lg text-white text-sm font-bold animate-pulse">🏫</div>`, html: `<div class="flex items-center justify-center w-9 h-9 rounded-full bg-gradient-to-tr from-orange-500 to-amber-500 border-2 border-white shadow-lg text-white text-sm font-bold animate-pulse hover:scale-110 transition-transform">🏫</div>`,
className: '', className: '',
iconSize: [36, 36], iconSize: [36, 36],
iconAnchor: [18, 18], iconAnchor: [18, 18],
@@ -57,27 +68,27 @@ interface BusRouteMapProps {
const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default const DEFAULT_CENTER = [13.0118, 80.0214]; // RIT Campus default
export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapProps) { export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapProps) {
// Collect coordinates for the polyline path // Collect coordinates for the polyline path, filtering out any invalid outliers
const pathCoordinates = useMemo(() => { const pathCoordinates = useMemo(() => {
if (!selectedRoute) return []; if (!selectedRoute) return [];
const coords: [number, number][] = []; const coords: [number, number][] = [];
// Add start stop coords if present // Add start stop coords if valid
if (selectedRoute.from_lat && selectedRoute.from_lng) { if (isValidCoordinate(selectedRoute.from_lat, selectedRoute.from_lng)) {
coords.push([selectedRoute.from_lat, selectedRoute.from_lng]); coords.push([selectedRoute.from_lat!, selectedRoute.from_lng!]);
} }
// Add all intermediary stop coords // Add all intermediary stop coords that are valid
selectedRoute.stops.forEach(stop => { selectedRoute.stops.forEach(stop => {
if (stop.lat && stop.lng) { if (isValidCoordinate(stop.lat, stop.lng)) {
coords.push([stop.lat, stop.lng]); coords.push([stop.lat!, stop.lng!]);
} }
}); });
// Add end stop coords if present // Add end stop coords if valid
if (selectedRoute.to_lat && selectedRoute.to_lng) { if (isValidCoordinate(selectedRoute.to_lat, selectedRoute.to_lng)) {
coords.push([selectedRoute.to_lat, selectedRoute.to_lng]); coords.push([selectedRoute.to_lat!, selectedRoute.to_lng!]);
} }
return coords; return coords;
@@ -89,32 +100,38 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
return pathCoordinates as L.LatLngBoundsExpression; return pathCoordinates as L.LatLngBoundsExpression;
}, [pathCoordinates]); }, [pathCoordinates]);
// Determine which routes to display (either the selected one, or all route start pins as overview) // Determine which markers to display
const renderMarkers = () => { const renderMarkers = () => {
if (selectedRoute) { if (selectedRoute) {
// Filter out stops that do not have valid coordinates
const validStops = selectedRoute.stops.filter(stop => isValidCoordinate(stop.lat, stop.lng));
return ( return (
<> <>
{selectedRoute.stops.map((stop, index) => { {validStops.map((stop, index) => {
const isStart = index === 0; const isStart = index === 0;
const isEnd = index === selectedRoute.stops.length - 1; const isEnd = index === validStops.length - 1;
const isRit = stop.name.toLowerCase().includes("rit"); const isRit = stop.name.toLowerCase().includes("rit");
let currentIcon = stopIcon; let currentIcon = getStopIcon(index + 1);
if (isStart) currentIcon = startIcon; if (isStart) currentIcon = getStartIcon("Start");
if (isRit || isEnd) currentIcon = campusIcon; if (isRit || isEnd) currentIcon = campusIcon;
if (!stop.lat || !stop.lng) return null;
return ( return (
<Marker <Marker
key={`${stop.name}-${index}`} key={`${stop.name}-${index}`}
position={[stop.lat, stop.lng]} position={[stop.lat!, stop.lng!]}
icon={currentIcon} icon={currentIcon}
> >
<Popup> <Popup>
<div className="p-1 font-sans"> <div className="p-1 font-sans">
<h4 className="font-bold text-slate-800 text-xs">{stop.name}</h4> <div className="flex items-center gap-1.5 mb-0.5">
<p className="text-[10px] text-orange-500 font-semibold mt-0.5">Estimated: {stop.time}</p> <span className="text-[10px] bg-slate-100 text-slate-700 px-1.5 py-0.5 rounded font-bold">
Stop #{index + 1}
</span>
<h4 className="font-bold text-slate-800 text-xs">{stop.name}</h4>
</div>
<p className="text-[10px] text-orange-500 font-semibold mt-0.5">Estimated Arrival: {stop.time}</p>
{isStart && <span className="inline-block text-[9px] bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded-full mt-1 font-bold">Departure Stop</span>} {isStart && <span className="inline-block text-[9px] bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded-full mt-1 font-bold">Departure Stop</span>}
{isRit && <span className="inline-block text-[9px] bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded-full mt-1 font-bold">RIT Campus</span>} {isRit && <span className="inline-block text-[9px] bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded-full mt-1 font-bold">RIT Campus</span>}
</div> </div>
@@ -126,7 +143,7 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
); );
} }
// Default view: Draw RIT Campus pin + Start pins for all routes // Default view: Draw RIT Campus pin + Start pins for all routes (only if coordinates are valid)
return ( return (
<> <>
<Marker position={[13.0118, 80.0214]} icon={campusIcon}> <Marker position={[13.0118, 80.0214]} icon={campusIcon}>
@@ -138,12 +155,12 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
</Popup> </Popup>
</Marker> </Marker>
{allRoutes.map((r, i) => { {allRoutes.map((r, i) => {
if (!r.from_lat || !r.from_lng) return null; if (!isValidCoordinate(r.from_lat, r.from_lng)) return null;
return ( return (
<Marker <Marker
key={`start-${r.number}-${i}`} key={`start-${r.number}-${i}`}
position={[r.from_lat, r.from_lng]} position={[r.from_lat!, r.from_lng!]}
icon={startIcon} icon={getStartIcon(r.number)}
> >
<Popup> <Popup>
<div className="p-1 font-sans"> <div className="p-1 font-sans">

View File

@@ -167,10 +167,6 @@
"lat": 13.0118, "lat": 13.0118,
"lng": 80.0214 "lng": 80.0214
}, },
"Ayyampettai": {
"lat": 10.8962881,
"lng": 79.1886395
},
"Ayyangarkulam": { "Ayyangarkulam": {
"lat": 13.0118, "lat": 13.0118,
"lng": 80.0214 "lng": 80.0214
@@ -178,5 +174,473 @@
"Ayyapanthangal": { "Ayyapanthangal": {
"lat": 13.041, "lat": 13.041,
"lng": 80.1412 "lng": 80.1412
},
"B.B.Road": {
"lat": 13.0095255,
"lng": 80.2537619
},
"BDO Office": {
"lat": 12.9022177,
"lng": 80.0257182
},
"Bai Kadai": {
"lat": 13.0198261,
"lng": 80.1430544
},
"Balaji Dental College": {
"lat": 12.9441308,
"lng": 80.208506
},
"Banavaram koot": {
"lat": 13.0118,
"lng": 80.0214
},
"Beach Station": {
"lat": 13.0942257,
"lng": 80.2921033
},
"Bharath College": {
"lat": 12.9079915,
"lng": 80.1394791
},
"Bharathi nagar": {
"lat": 13.1226511,
"lng": 80.2237949
},
"Butt Road": {
"lat": 13.0084,
"lng": 80.1987
},
"Butt road": {
"lat": 13.0084,
"lng": 80.1987
},
"CIT Nagar": {
"lat": 13.0293843,
"lng": 80.2342825
},
"CMBT (Koyambedu)": {
"lat": 13.0678,
"lng": 80.2054
},
"CRP": {
"lat": 13.0118,
"lng": 80.0214
},
"CRPF": {
"lat": 12.9912054,
"lng": 80.1881366
},
"Camp Road ICICI Bank": {
"lat": 13.0118,
"lng": 80.0214
},
"Camp Road Singal": {
"lat": 13.0118,
"lng": 80.0214
},
"Canara Bank": {
"lat": 12.9764294,
"lng": 80.2224084
},
"Central": {
"lat": 13.0827,
"lng": 80.2707
},
"Chembarambakkam": {
"lat": 13.016769,
"lng": 80.1367942
},
"Chengalpattu Bypass": {
"lat": 13.0118,
"lng": 80.0214
},
"Chengalpattu Rattinakinaru": {
"lat": 13.0118,
"lng": 80.0214
},
"Chennai Trade Centre": {
"lat": 13.0142534,
"lng": 80.1905788
},
"Chennirkuppam": {
"lat": 13.0118,
"lng": 80.0214
},
"Cheriyan Hospital": {
"lat": 13.0118,
"lng": 80.0214
},
"Cheyyar-SBI": {
"lat": 13.0118,
"lng": 80.0214
},
"Chinmaiya Nagar": {
"lat": 13.0118,
"lng": 80.0214
},
"Chintadripet (Post Offiec)": {
"lat": 13.0761258,
"lng": 80.270781
},
"Chintamani": {
"lat": 13.0472403,
"lng": 80.1853252
},
"Chinthamani": {
"lat": 13.0410061,
"lng": 80.2373742
},
"Chithukadu Blue": {
"lat": 13.0118,
"lng": 80.0214
},
"Choolai Post Office": {
"lat": 13.0118,
"lng": 80.0214
},
"Choolaimedu": {
"lat": 13.0666515,
"lng": 80.230974
},
"Choolaimedu Subway": {
"lat": 13.0645,
"lng": 80.2224
},
"Choolaimedu bus stop": {
"lat": 13.0666287,
"lng": 80.2305198
},
"Chrompet": {
"lat": 12.961,
"lng": 80.1462
},
"Collector Nagar": {
"lat": 13.0884944,
"lng": 80.1909707
},
"Collector Office": {
"lat": 13.0959177,
"lng": 80.2922668
},
"Collector nagar": {
"lat": 13.0884944,
"lng": 80.1909707
},
"D.R.Super Market": {
"lat": 13.0118,
"lng": 80.0214
},
"D1 Police Station": {
"lat": 13.0683459,
"lng": 80.2135958
},
"Dasprakash": {
"lat": 13.0811,
"lng": 80.2524
},
"Deepam Foods": {
"lat": 13.0118,
"lng": 80.0214
},
"Donbosco": {
"lat": 13.0118,
"lng": 80.0214
},
"E.B": {
"lat": 13.0869258,
"lng": 80.2194441
},
"E.B. Stop": {
"lat": 13.0869258,
"lng": 80.2194441
},
"Eaga Theatre": {
"lat": 13.0118,
"lng": 80.0214
},
"Egambaranathar Koil": {
"lat": 13.0118,
"lng": 80.0214
},
"Egmore": {
"lat": 13.0792,
"lng": 80.2598
},
"Ekkatuthangal": {
"lat": 13.0118,
"lng": 80.0214
},
"Elango Nagar": {
"lat": 12.9715538,
"lng": 80.2523792
},
"Ellaimman Koil": {
"lat": 13.0118,
"lng": 80.0214
},
"Erumaiyur": {
"lat": 12.9547388,
"lng": 80.0815988
},
"Eswar Nagar": {
"lat": 13.0577106,
"lng": 80.2190678
},
"G3 Police Station": {
"lat": 13.0683459,
"lng": 80.2135958
},
"GRT": {
"lat": 12.9826003,
"lng": 80.2181059
},
"Ganga Cinima": {
"lat": 13.0118,
"lng": 80.0214
},
"Gangaiamman koil": {
"lat": 13.0551756,
"lng": 80.1913579
},
"Gerugambakkam": {
"lat": 13.0152345,
"lng": 80.1368538
},
"Golden Flats": {
"lat": 13.0883017,
"lng": 80.1784121
},
"Golden flat": {
"lat": 13.0118,
"lng": 80.0214
},
"Govardhanagari": {
"lat": 13.0118,
"lng": 80.0214
},
"Govarthanagiri Bus Stand": {
"lat": 13.0118,
"lng": 80.0214
},
"Government Hospital": {
"lat": 13.1093369,
"lng": 80.2312849
},
"Gudalore": {
"lat": 13.0118,
"lng": 80.0214
},
"Guduvanchery": {
"lat": 13.0118,
"lng": 80.0214
},
"Guindy": {
"lat": 13.0067,
"lng": 80.2206
},
"HDFC Bank": {
"lat": 13.0308653,
"lng": 80.2455467
},
"HVF": {
"lat": 13.0293266,
"lng": 80.1667524
},
"Housing Board": {
"lat": 13.0084674,
"lng": 80.2475552
},
"ICF Church": {
"lat": 13.0118,
"lng": 80.0214
},
"ICF Signal": {
"lat": 13.1001091,
"lng": 80.2145485
},
"IOB Bank": {
"lat": 13.0224668,
"lng": 80.2230059
},
"Ice House Police Station": {
"lat": 13.052,
"lng": 80.2762
},
"Indian Bank Thiruvninravur": {
"lat": 13.0118,
"lng": 80.0214
},
"Indira Gandhi Nagar": {
"lat": 13.0914233,
"lng": 80.2541906
},
"Indra Nagar (KPM Railwaygate)": {
"lat": 13.0370804,
"lng": 80.1887213
},
"Irungattukottai bus stand": {
"lat": 13.0118,
"lng": 80.0214
},
"Iyyappanthangal": {
"lat": 13.0118,
"lng": 80.0214
},
"JJ Nagar": {
"lat": 13.0790498,
"lng": 80.1719442
},
"JJ Nagar Police Station": {
"lat": 13.0118,
"lng": 80.0214
},
"Janappanchatram Bypass": {
"lat": 13.0118,
"lng": 80.0214
},
"Jaya College": {
"lat": 13.1225267,
"lng": 80.0454078
},
"Jayalakshmi Theatre": {
"lat": 13.0118,
"lng": 80.0214
},
"Jeyachandran": {
"lat": 12.9211296,
"lng": 80.1968403
},
"Joint Office": {
"lat": 13.101916,
"lng": 80.230392
},
"Joshua School": {
"lat": 13.0118,
"lng": 80.0214
},
"Jothi Nagar": {
"lat": 12.9459767,
"lng": 80.239775
},
"K4 Police Station": {
"lat": 13.0925413,
"lng": 80.2179712
},
"KK Nagar": {
"lat": 13.0373,
"lng": 80.2045
},
"KK Nagar ESI": {
"lat": 13.0373,
"lng": 80.2045
},
"Kachapeswarar Koil": {
"lat": 13.0118,
"lng": 80.0214
},
"Kaduveti": {
"lat": 13.0118,
"lng": 80.0214
},
"Kaiveli": {
"lat": 12.9633783,
"lng": 80.2160859
},
"Kakkalur": {
"lat": 13.1305159,
"lng": 79.9243758
},
"Kakkalur Signal": {
"lat": 13.1305159,
"lng": 79.9243758
},
"Kakkan Bridge": {
"lat": 13.0118,
"lng": 80.0214
},
"Kallikuppam": {
"lat": 13.1331405,
"lng": 80.1701786
},
"Kalmandapam": {
"lat": 13.1153712,
"lng": 80.2905548
},
"Kalpana Lamp": {
"lat": 13.0118,
"lng": 80.0214
},
"Kalyan Jewalrs": {
"lat": 13.0118,
"lng": 80.0214
},
"Kamarajapuram": {
"lat": 13.1933419,
"lng": 80.2612852
},
"Kambar Colony": {
"lat": 12.9755337,
"lng": 80.2296597
},
"Ayyampettai": {
"lat": 13.0118,
"lng": 80.0214
},
"Chitteri": {
"lat": 13.0118,
"lng": 80.0214
},
"Ganeshapuram": {
"lat": 13.0118,
"lng": 80.0214
},
"Kamban Nagar": {
"lat": 13.0118,
"lng": 80.0214
},
"Kammal Street": {
"lat": 13.0903441,
"lng": 80.2791264
},
"Kammala street": {
"lat": 13.0118,
"lng": 80.0214
},
"Kankaiyamman Kovil": {
"lat": 13.0118,
"lng": 80.0214
},
"Kannadhasan Nagar": {
"lat": 13.0118,
"lng": 80.0214
},
"Karanodai Bypass": {
"lat": 13.0118,
"lng": 80.0214
},
"Karapakkam": {
"lat": 12.9117073,
"lng": 80.2277203
},
"Karapakkam - TCS": {
"lat": 13.0118,
"lng": 80.0214
},
"Karayanchavadi": {
"lat": 13.0467114,
"lng": 80.1109051
},
"Karima Nagar": {
"lat": 13.0118,
"lng": 80.0214
},
"Kasi Theatre": {
"lat": 13.0305,
"lng": 80.2132
},
"Kasimedu": {
"lat": 13.1364038,
"lng": 80.2958012
} }
} }

View File

@@ -9,6 +9,13 @@ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
BUS_ROUTES_PATH = os.path.abspath(os.path.join(CURRENT_DIR, "..", "backend", "src", "main", "resources", "bus_routes.json")) BUS_ROUTES_PATH = os.path.abspath(os.path.join(CURRENT_DIR, "..", "backend", "src", "main", "resources", "bus_routes.json"))
CACHE_PATH = os.path.join(CURRENT_DIR, "coordinates_cache.json") CACHE_PATH = os.path.join(CURRENT_DIR, "coordinates_cache.json")
# Domain restriction (Chennai, Tiruvallur, Kanchipuram districts)
MIN_LAT, MAX_LAT = 12.7, 13.4
MIN_LNG, MAX_LNG = 79.6, 80.4
def is_valid_coordinate(lat, lng):
return MIN_LAT <= lat <= MAX_LAT and MIN_LNG <= lng <= MAX_LNG
# Predefined coordinates for hard-to-geocode stops or fallback landmarks in Chennai # Predefined coordinates for hard-to-geocode stops or fallback landmarks in Chennai
STATIC_COORDINATES = { STATIC_COORDINATES = {
"RIT Campus": {"lat": 13.0118, "lng": 80.0214}, "RIT Campus": {"lat": 13.0118, "lng": 80.0214},
@@ -68,7 +75,15 @@ def load_cache():
if os.path.exists(CACHE_PATH): if os.path.exists(CACHE_PATH):
try: try:
with open(CACHE_PATH, "r", encoding="utf-8") as f: with open(CACHE_PATH, "r", encoding="utf-8") as f:
return json.load(f) data = json.load(f)
# Clean coordinates that are outside the map route domain
cleaned = {}
for name, coords in data.items():
if is_valid_coordinate(coords["lat"], coords["lng"]):
cleaned[name] = coords
else:
print(f"Removing invalid cache entry for '{name}': {coords}")
return cleaned
except Exception: except Exception:
pass pass
return {} return {}
@@ -114,8 +129,11 @@ def geocode_nominatim(name):
if data: if data:
lat = float(data[0]["lat"]) lat = float(data[0]["lat"])
lng = float(data[0]["lon"]) lng = float(data[0]["lon"])
print(f"-> Found: {lat}, {lng}") if is_valid_coordinate(lat, lng):
return {"lat": lat, "lng": lng} print(f"-> Found within domain: {lat}, {lng}")
return {"lat": lat, "lng": lng}
else:
print(f"-> Found but DISCARDED (outside domain): {lat}, {lng}")
# Rest to respect API limit # Rest to respect API limit
time.sleep(1.2) time.sleep(1.2)
except Exception as e: except Exception as e:
@@ -132,39 +150,7 @@ def geocode_nominatim(name):
print(f"-> No match found for '{name}'. Defaulting to RIT Campus.") print(f"-> No match found for '{name}'. Defaulting to RIT Campus.")
return STATIC_COORDINATES["RIT Campus"] return STATIC_COORDINATES["RIT Campus"]
def geocode_routes(): def update_bus_routes_json(routes, cache):
if not os.path.exists(BUS_ROUTES_PATH):
print(f"Error: {BUS_ROUTES_PATH} does not exist.")
return
with open(BUS_ROUTES_PATH, "r", encoding="utf-8") as f:
routes = json.load(f)
cache = load_cache()
modified = False
# Collect all unique stop names
unique_names = set()
for r in routes:
unique_names.add(r["from"])
unique_names.add(r["to"])
for stop in r.get("stops", []):
unique_names.add(stop["name"])
print(f"Total unique stops to geocode: {len(unique_names)}")
# Geocode each stop
count = 0
for name in sorted(unique_names):
if name not in cache:
coords = geocode_nominatim(name)
cache[name] = coords
count += 1
# Save cache incrementally
if count % 5 == 0:
save_cache(cache)
save_cache(cache)
# Enrich bus_routes.json # Enrich bus_routes.json
for r in routes: for r in routes:
from_coords = cache.get(r["from"], STATIC_COORDINATES["RIT Campus"]) from_coords = cache.get(r["from"], STATIC_COORDINATES["RIT Campus"])
@@ -183,7 +169,49 @@ def geocode_routes():
with open(BUS_ROUTES_PATH, "w", encoding="utf-8") as f: with open(BUS_ROUTES_PATH, "w", encoding="utf-8") as f:
json.dump(routes, f, indent=2) json.dump(routes, f, indent=2)
print("Geocoding process complete! bus_routes.json updated with coordinates.") def geocode_routes():
if not os.path.exists(BUS_ROUTES_PATH):
print(f"Error: {BUS_ROUTES_PATH} does not exist.")
return
with open(BUS_ROUTES_PATH, "r", encoding="utf-8") as f:
routes = json.load(f)
cache = load_cache()
# Save the cleaned cache to start
save_cache(cache)
# Write coordinates to the JSON immediately for any stops that are already in the cache!
update_bus_routes_json(routes, cache)
print("Initial cache applied to bus_routes.json.")
# Collect all unique stop names
unique_names = set()
for r in routes:
unique_names.add(r["from"])
unique_names.add(r["to"])
for stop in r.get("stops", []):
unique_names.add(stop["name"])
print(f"Total unique stops to geocode: {len(unique_names)}")
# Geocode each stop
count = 0
for name in sorted(unique_names):
if name not in cache:
coords = geocode_nominatim(name)
cache[name] = coords
count += 1
# Save cache and update JSON incrementally
save_cache(cache)
update_bus_routes_json(routes, cache)
print(f"Updated routes JSON with '{name}' ({coords['lat']}, {coords['lng']})")
# Final save
save_cache(cache)
update_bus_routes_json(routes, cache)
print("Geocoding process complete! bus_routes.json fully synchronized.")
if __name__ == "__main__": if __name__ == "__main__":
geocode_routes() geocode_routes()