|  | 
| 10 | 10 | 	import LocationButton from '$lib/LocationButton/LocationButton.svelte'; | 
| 11 | 11 | 	import RouteMap from './RouteMap.svelte'; | 
| 12 | 12 | 
 | 
| 13 |  | -	import { faBus } from '@fortawesome/free-solid-svg-icons'; | 
| 14 |  | -	import { RouteType, routePriorities, prioritizedRouteTypeForDisplay } from '$config/routeConfig'; | 
| 15 | 13 | 	import { isMapLoaded } from '$src/stores/mapStore'; | 
| 16 | 14 | 	import { userLocation } from '$src/stores/userLocationStore'; | 
| 17 | 15 | 	/** | 
|  | 
| 21 | 19 | 	 * @property {boolean} [showRoute] | 
| 22 | 20 | 	 * @property {boolean} [showRouteMap] | 
| 23 | 21 | 	 * @property {any} [mapProvider] | 
|  | 22 | +	 * @property {any} [stop] - Currently selected stop to preserve visual context | 
| 24 | 23 | 	 */ | 
| 25 | 24 | 
 | 
| 26 | 25 | 	/** @type {Props} */ | 
| 27 | 26 | 	let { | 
| 28 | 27 | 		handleStopMarkerSelect, | 
| 29 | 28 | 		selectedTrip = null, | 
| 30 | 29 | 		selectedRoute = null, | 
| 31 |  | -		showRoute = false, | 
|  | 30 | +		isRouteSelected = false, | 
| 32 | 31 | 		showRouteMap = false, | 
| 33 |  | -		mapProvider = null | 
|  | 32 | +		mapProvider = null, | 
|  | 33 | +		stop = null | 
| 34 | 34 | 	} = $props(); | 
| 35 | 35 | 
 | 
| 36 | 36 | 	let isTripPlanModeActive = $state(false); | 
|  | 
| 41 | 41 | 	let allStopsMap = new Map(); | 
| 42 | 42 | 	let stopsCache = new Map(); | 
| 43 | 43 | 
 | 
|  | 44 | +	const Modes = { | 
|  | 45 | +		NORMAL: 'normal', | 
|  | 46 | +		TRIP_PLAN: 'tripPlan', | 
|  | 47 | +		ROUTE: 'route' | 
|  | 48 | +	}; | 
|  | 49 | +
 | 
|  | 50 | +	let mapMode = $state(Modes.NORMAL); | 
|  | 51 | +	let modeChangeTimeout = null; | 
|  | 52 | +
 | 
|  | 53 | +	$effect(() => { | 
|  | 54 | +		let newMode; | 
|  | 55 | +		if (isTripPlanModeActive) { | 
|  | 56 | +			newMode = Modes.TRIP_PLAN; | 
|  | 57 | +		} else if (selectedRoute || isRouteSelected || showRouteMap || selectedTrip) { | 
|  | 58 | +			newMode = Modes.ROUTE; | 
|  | 59 | +		} else { | 
|  | 60 | +			newMode = Modes.NORMAL; | 
|  | 61 | +		} | 
|  | 62 | +		if (modeChangeTimeout) { | 
|  | 63 | +			clearTimeout(modeChangeTimeout); | 
|  | 64 | +		} | 
|  | 65 | +		if (mapMode === Modes.ROUTE && newMode === Modes.NORMAL) { | 
|  | 66 | +			modeChangeTimeout = setTimeout(() => { | 
|  | 67 | +				mapMode = newMode; | 
|  | 68 | +			}, 100); | 
|  | 69 | +		} else if (mapMode !== newMode) { | 
|  | 70 | +			mapMode = newMode; | 
|  | 71 | +		} | 
|  | 72 | +	}); | 
|  | 73 | +
 | 
|  | 74 | +	$effect(() => { | 
|  | 75 | +		if (!mapInstance) return; | 
|  | 76 | +		if (mapMode === Modes.NORMAL) { | 
|  | 77 | +			batchAddMarkers(allStops); | 
|  | 78 | +		} else { | 
|  | 79 | +			clearAllMarkers(); | 
|  | 80 | +		} | 
|  | 81 | +	}); | 
|  | 82 | +
 | 
| 44 | 83 | 	function cacheKey(zoomLevel, boundingBox) { | 
| 45 | 84 | 		const multiplier = 100; // 2 decimal places | 
| 46 | 85 | 		const north = Math.round(boundingBox.north * multiplier); | 
|  | 
| 103 | 142 | 			await loadStopsAndAddMarkers(initialLat, initialLng, true); | 
| 104 | 143 | 
 | 
| 105 | 144 | 			const debouncedLoadMarkers = debounce(async () => { | 
| 106 |  | -				const center = mapInstance.getCenter(); | 
| 107 |  | -				const zoomLevel = mapInstance.map.getZoom(); | 
| 108 |  | -
 | 
| 109 |  | -				// Prevent fetching stops in the background when a route is selected or trip plan mode is active, we only fetch stops when we are see other stops | 
| 110 |  | -				if (selectedRoute || showRoute || isTripPlanModeActive) { | 
|  | 145 | +				if (mapMode !== Modes.NORMAL) { | 
| 111 | 146 | 					return; | 
| 112 | 147 | 				} | 
|  | 148 | +
 | 
|  | 149 | +				const center = mapInstance.getCenter(); | 
|  | 150 | +				const zoomLevel = mapInstance.map.getZoom(); | 
| 113 | 151 | 				await loadStopsAndAddMarkers(center.lat, center.lng, false, zoomLevel); | 
| 114 | 152 | 			}, 300); | 
| 115 | 153 | 
 | 
|  | 
| 148 | 186 | 		} | 
| 149 | 187 | 	} | 
| 150 | 188 | 
 | 
| 151 |  | -	function updateMarkers() { | 
| 152 |  | -		if (!selectedRoute && !isTripPlanModeActive) { | 
| 153 |  | -			batchAddMarkers(allStops); | 
| 154 |  | -		} | 
| 155 |  | -	} | 
| 156 |  | -
 | 
| 157 | 189 | 	// Batch operation to add multiple markers efficiently | 
| 158 | 190 | 	function batchAddMarkers(stops) { | 
| 159 | 191 | 		const stopsToAdd = stops.filter((s) => !mapInstance.hasMarker(s.id)); | 
| 160 | 192 | 
 | 
| 161 |  | -		if (stopsToAdd.length === 0) return; | 
|  | 193 | +		if (stopsToAdd.length === 0) { | 
|  | 194 | +			return; | 
|  | 195 | +		} | 
| 162 | 196 | 
 | 
| 163 | 197 | 		// Group DOM operations to minimize reflows/repaints | 
| 164 | 198 | 		requestAnimationFrame(() => { | 
|  | 
| 172 | 206 | 			return; | 
| 173 | 207 | 		} | 
| 174 | 208 | 
 | 
| 175 |  | -		// Delegate marker existence check to provider | 
| 176 | 209 | 		if (mapInstance.hasMarker(s.id)) { | 
| 177 | 210 | 			return; | 
| 178 | 211 | 		} | 
| 179 | 212 | 
 | 
| 180 |  | -		let icon = faBus; | 
| 181 |  | -
 | 
| 182 |  | -		if (s.routes && s.routes.length > 0) { | 
| 183 |  | -			const routeTypes = s.routes.map((r) => r.type); | 
| 184 |  | -			let prioritizedType = RouteType.UNKNOWN; | 
| 185 |  | -
 | 
| 186 |  | -			// Optimized priority lookup - check highest priority first | 
| 187 |  | -			for (const priority of routePriorities) { | 
| 188 |  | -				if (routeTypes.includes(priority)) { | 
| 189 |  | -					prioritizedType = priority; | 
| 190 |  | -					break; | 
| 191 |  | -				} | 
| 192 |  | -			} | 
| 193 |  | -
 | 
| 194 |  | -			icon = prioritizedRouteTypeForDisplay(prioritizedType); | 
| 195 |  | -		} | 
|  | 213 | +		// Check if this marker should be highlighted (if it's the currently selected stop) | 
|  | 214 | +		const shouldHighlight = stop && s.id === stop.id; | 
| 196 | 215 | 
 | 
| 197 | 216 | 		const markerObj = mapInstance.addMarker({ | 
| 198 | 217 | 			position: { lat: s.lat, lng: s.lon }, | 
| 199 |  | -			icon: icon, | 
| 200 | 218 | 			stop: s, | 
|  | 219 | +			isHighlighted: shouldHighlight, | 
| 201 | 220 | 			onClick: () => { | 
| 202 | 221 | 				handleStopMarkerSelect(s); | 
| 203 | 222 | 			} | 
|  | 
| 250 | 269 | 			if (tabSwitchHandler) window.removeEventListener('tabSwitched', tabSwitchHandler); | 
| 251 | 270 | 		} | 
| 252 | 271 | 
 | 
|  | 272 | +		if (modeChangeTimeout) { | 
|  | 273 | +			clearTimeout(modeChangeTimeout); | 
|  | 274 | +		} | 
|  | 275 | +
 | 
| 253 | 276 | 		clearAllMarkers(); | 
| 254 | 277 | 
 | 
| 255 | 278 | 		allStopsMap.clear(); | 
| 256 | 279 | 		stopsCache.clear(); | 
| 257 | 280 | 	}); | 
| 258 |  | -	$effect(() => { | 
| 259 |  | -		if (selectedRoute) { | 
| 260 |  | -			clearAllMarkers(); | 
| 261 |  | -			updateMarkers(); | 
| 262 |  | -		} else if (!isTripPlanModeActive) { | 
| 263 |  | -			batchAddMarkers(allStops); | 
| 264 |  | -		} | 
| 265 |  | -	}); | 
| 266 |  | -	$effect(() => { | 
| 267 |  | -		if (isTripPlanModeActive) { | 
| 268 |  | -			clearAllMarkers(); | 
| 269 |  | -		} | 
| 270 |  | -	}); | 
| 271 | 281 | </script> | 
| 272 | 282 | 
 | 
| 273 | 283 | <div class="map-container"> | 
| 274 | 284 | 	<div id="map" bind:this={mapElement}></div> | 
| 275 | 285 | 
 | 
| 276 | 286 | 	{#if selectedTrip && showRouteMap} | 
| 277 |  | -		<RouteMap mapProvider={mapInstance} tripId={selectedTrip.tripId} /> | 
|  | 287 | +		<RouteMap mapProvider={mapInstance} tripId={selectedTrip.tripId} currentSelectedStop={stop} /> | 
| 278 | 288 | 	{/if} | 
| 279 | 289 | </div> | 
| 280 | 290 | 
 | 
|  | 
0 commit comments