• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OneBusAway / wayfinder / 22210548225

20 Feb 2026 03:51AM UTC coverage: 79.59% (+1.1%) from 78.449%
22210548225

Pull #346

github

web-flow
Merge 067a2fce8 into b5e7a242f
Pull Request #346: Prep 2026.2 Release

1685 of 1861 branches covered (90.54%)

Branch coverage included in aggregate %.

1106 of 1442 new or added lines in 33 files covered. (76.7%)

3 existing lines in 3 files now uncovered.

10739 of 13749 relevant lines covered (78.11%)

4.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/map/MapView.svelte
1
<script>
×
2
        import { browser } from '$app/environment';
3
        import { onMount, onDestroy } from 'svelte';
4
        import {
5
                PUBLIC_OBA_REGION_CENTER_LAT as initialLat,
6
                PUBLIC_OBA_REGION_CENTER_LNG as initialLng
7
        } from '$env/static/public';
8
        import { env } from '$env/dynamic/public';
9

10
        import { debounce } from '$lib/utils';
11
        import LocationButton from '$lib/LocationButton/LocationButton.svelte';
12
        import RouteMap from './RouteMap.svelte';
13

14
        import { isMapLoaded } from '$src/stores/mapStore';
15
        import { userLocation } from '$src/stores/userLocationStore';
16
        /**
17
         * @typedef {Object} Props
18
         * @property {any} [selectedTrip]
19
         * @property {any} [selectedRoute]
20
         * @property {boolean} [showRoute]
21
         * @property {boolean} [showRouteMap]
22
         * @property {any} [mapProvider]
23
         * @property {any} [stop] - Currently selected stop to preserve visual context
24
         * @property {{ lat: number, lng: number } | null} [initialCoords] - Optional initial coordinates from URL params
25
         */
26

27
        /** @type {Props} */
28
        let {
29
                handleStopMarkerSelect,
30
                selectedTrip = null,
×
31
                selectedRoute = null,
×
32
                isRouteSelected = false,
×
33
                showRouteMap = false,
×
34
                mapProvider = null,
×
35
                stop = null,
×
36
                initialCoords = null
×
37
        } = $props();
38

39
        let isTripPlanModeActive = $state(false);
×
40
        let mapInstance = $state(null);
×
41
        let mapElement = $state();
×
42
        let allStops = $state([]);
×
43
        // O(1) lookup for existing stops
44
        let allStopsMap = new Map();
×
45
        let stopsCache = new Map();
×
46

47
        const Modes = {
×
48
                NORMAL: 'normal',
×
49
                TRIP_PLAN: 'tripPlan',
×
50
                ROUTE: 'route'
×
51
        };
52

53
        let mapMode = $state(Modes.NORMAL);
×
54
        let modeChangeTimeout = null;
×
55

56
        $effect(() => {
×
57
                let newMode;
×
58
                if (isTripPlanModeActive) {
×
59
                        newMode = Modes.TRIP_PLAN;
×
60
                } else if (selectedRoute || isRouteSelected || showRouteMap || selectedTrip) {
×
61
                        newMode = Modes.ROUTE;
×
62
                } else {
63
                        newMode = Modes.NORMAL;
×
64
                }
65
                if (modeChangeTimeout) {
×
66
                        clearTimeout(modeChangeTimeout);
×
67
                }
68
                if (mapMode === Modes.ROUTE && newMode === Modes.NORMAL) {
×
69
                        modeChangeTimeout = setTimeout(() => {
×
70
                                mapMode = newMode;
×
71
                        }, 100);
×
72
                } else if (mapMode !== newMode) {
×
73
                        mapMode = newMode;
×
74
                }
75
        });
76

77
        $effect(() => {
×
78
                if (!mapInstance) return;
×
79
                if (mapMode === Modes.NORMAL) {
×
80
                        batchAddMarkers(allStops);
×
81
                } else {
82
                        clearAllMarkers();
×
83
                }
84
        });
85

86
        function cacheKey(zoomLevel, boundingBox) {
×
87
                const multiplier = 100; // 2 decimal places
×
88
                const north = Math.round(boundingBox.north * multiplier);
×
89
                const south = Math.round(boundingBox.south * multiplier);
×
90
                const east = Math.round(boundingBox.east * multiplier);
×
91
                const west = Math.round(boundingBox.west * multiplier);
×
92

93
                return `${north}_${south}_${east}_${west}_${zoomLevel}`;
×
94
        }
95

96
        function getBoundingBox() {
×
97
                if (!mapProvider) {
×
98
                        throw new Error('Map provider is not initialized');
×
99
                }
100
                return mapProvider.getBoundingBox();
×
101
        }
102

103
        async function loadStopsForLocation(lat, lng, zoomLevel, firstCall = false) {
×
104
                if (firstCall) {
×
105
                        const response = await fetch(`/api/oba/stops-for-location?lat=${lat}&lng=${lng}&radius=2500`);
×
106
                        if (!response.ok) {
×
107
                                throw new Error('Failed to fetch locations');
×
108
                        }
109
                        return await response.json();
×
110
                }
111

112
                const boundingBox = getBoundingBox();
×
113
                const key = cacheKey(zoomLevel, boundingBox);
×
114

115
                if (stopsCache.has(key)) {
×
116
                        console.debug('Stop cache hit: ', key);
×
117
                        return stopsCache.get(key);
×
118
                } else {
119
                        console.debug('Stop cache miss: ', key);
×
120
                }
121

122
                const response = await fetch(
×
123
                        `/api/oba/stops-for-location?lat=${lat}&lng=${lng}&latSpan=${boundingBox.north - boundingBox.south}&lngSpan=${boundingBox.east - boundingBox.west}&radius=1500`
×
124
                );
125

126
                if (!response.ok) {
×
127
                        throw new Error('Failed to fetch locations');
×
128
                }
129

130
                const stopsForLocation = await response.json();
×
131
                stopsCache.set(key, stopsForLocation);
×
132

133
                return stopsForLocation;
×
134
        }
135

136
        async function initMap() {
×
137
                try {
138
                        // Use URL-provided coordinates if available, otherwise use region center
139
                        const mapCenterLat = initialCoords?.lat ?? Number(initialLat);
×
140
                        const mapCenterLng = initialCoords?.lng ?? Number(initialLng);
×
141

142
                        await mapProvider.initMap(mapElement, {
×
143
                                lat: mapCenterLat,
×
144
                                lng: mapCenterLng
×
145
                        });
146

147
                        mapInstance = mapProvider;
×
148

149
                        // If we have initial coordinates from URL, update the user location store
150
                        // and add a user location marker
151
                        if (initialCoords) {
×
152
                                const coords = { lat: mapCenterLat, lng: mapCenterLng };
×
153
                                userLocation.set(coords);
×
154
                                mapInstance.addUserLocationMarker(coords);
×
155
                        }
156

157
                        await loadStopsAndAddMarkers(mapCenterLat, mapCenterLng, true);
×
158

159
                        const debouncedLoadMarkers = debounce(async () => {
×
160
                                if (mapMode !== Modes.NORMAL) {
×
161
                                        return;
162
                                }
163

164
                                const center = mapInstance.getCenter();
×
165
                                const zoomLevel = mapInstance.map.getZoom();
×
166
                                await loadStopsAndAddMarkers(center.lat, center.lng, false, zoomLevel);
×
167
                        }, 300);
×
168

169
                        mapProvider.eventListeners(mapInstance, debouncedLoadMarkers);
×
170

NEW
171
                        if (env.PUBLIC_OTP_SERVER_URL) {
×
NEW
172
                                mapProvider.enableContextMenu();
×
173
                        }
174

175
                        if (browser) {
×
176
                                window.addEventListener('themeChange', handleThemeChange);
×
177
                        }
178
                } catch (error) {
×
179
                        console.error('Error initializing map:', error);
×
180
                }
181
        }
182

183
        async function loadStopsAndAddMarkers(lat, lng, firstCall = false, zoomLevel = 15) {
×
184
                const stopsData = await loadStopsForLocation(lat, lng, zoomLevel, firstCall);
×
185
                const newStops = stopsData.data.list;
×
186
                const routeReference = stopsData.data.references.routes || [];
×
187

188
                const routeLookup = new Map(routeReference.map((route) => [route.id, route]));
×
189

190
                // merge the stops routeIds with the route data and deduplicate efficiently
191
                newStops.forEach((stop) => {
×
192
                        if (!allStopsMap.has(stop.id)) {
×
193
                                stop.routes =
×
194
                                        stop.routeIds?.map((routeId) => routeLookup.get(routeId)).filter(Boolean) || [];
×
195
                                allStopsMap.set(stop.id, stop);
×
196
                        }
197
                });
198

199
                allStops = Array.from(allStopsMap.values());
×
200
        }
201

202
        function clearAllMarkers() {
×
203
                if (mapInstance && mapInstance.clearAllStopMarkers) {
×
204
                        mapInstance.clearAllStopMarkers();
×
205
                }
206
        }
207

208
        // Batch operation to add multiple markers efficiently
209
        function batchAddMarkers(stops) {
×
210
                const stopsToAdd = stops.filter((s) => !mapInstance.hasMarker(s.id));
×
211

212
                if (stopsToAdd.length === 0) {
×
213
                        return;
214
                }
215

216
                // Group DOM operations to minimize reflows/repaints
217
                requestAnimationFrame(() => {
×
218
                        stopsToAdd.forEach((s) => addMarker(s));
×
219
                });
220
        }
221

222
        function addMarker(s) {
×
223
                if (!mapInstance) {
×
224
                        console.error('Map not initialized yet');
×
225
                        return;
226
                }
227

228
                if (mapInstance.hasMarker(s.id)) {
×
229
                        return;
230
                }
231

232
                // Check if this marker should be highlighted (if it's the currently selected stop)
233
                const shouldHighlight = stop && s.id === stop.id;
×
234

235
                const markerObj = mapInstance.addMarker({
×
236
                        position: { lat: s.lat, lng: s.lon },
×
237
                        stop: s,
×
238
                        isHighlighted: shouldHighlight,
×
239
                        onClick: () => {
×
240
                                handleStopMarkerSelect(s);
×
241
                        }
242
                });
243

244
                return markerObj;
×
245
        }
246

247
        function handleThemeChange(event) {
×
248
                const { darkMode } = event.detail;
×
249
                mapInstance.setTheme(darkMode ? 'dark' : 'light');
×
250
        }
251

252
        function handleLocationObtained(latitude, longitude) {
×
253
                mapInstance.setCenter({ lat: latitude, lng: longitude });
×
254
                mapInstance.addUserLocationMarker({ lat: latitude, lng: longitude });
×
255
                userLocation.set({ lat: latitude, lng: longitude });
×
256
        }
257

258
        // Store event handlers for proper cleanup
259
        let planTripHandler, tabSwitchHandler;
×
260

261
        onMount(async () => {
×
262
                await initMap();
×
263
                isMapLoaded.set(true);
×
264
                if (browser) {
×
265
                        const darkMode = document.documentElement.classList.contains('dark');
×
266

267
                        // Store handlers for cleanup
268
                        planTripHandler = () => {
×
269
                                isTripPlanModeActive = true;
×
270
                        };
271
                        tabSwitchHandler = () => {
×
272
                                isTripPlanModeActive = false;
×
273
                        };
274

275
                        window.addEventListener('planTripTabClicked', planTripHandler);
×
276
                        window.addEventListener('tabSwitched', tabSwitchHandler);
×
277

278
                        const event = new CustomEvent('themeChange', { detail: { darkMode } });
×
279
                        window.dispatchEvent(event);
×
280
                }
281
        });
282

283
        onDestroy(() => {
×
284
                if (browser) {
×
285
                        window.removeEventListener('themeChange', handleThemeChange);
×
286

287
                        if (planTripHandler) window.removeEventListener('planTripTabClicked', planTripHandler);
×
288
                        if (tabSwitchHandler) window.removeEventListener('tabSwitched', tabSwitchHandler);
×
289
                }
290

291
                if (modeChangeTimeout) {
×
292
                        clearTimeout(modeChangeTimeout);
×
293
                }
294

295
                clearAllMarkers();
×
296

297
                allStopsMap.clear();
×
298
                stopsCache.clear();
×
299
        });
300
</script>
301

302
<div class="map-container">
303
        <div id="map" bind:this={mapElement}></div>
×
304

305
        {#if selectedTrip && showRouteMap}
×
306
                <RouteMap mapProvider={mapInstance} tripId={selectedTrip.tripId} currentSelectedStop={stop} />
×
307
        {/if}
308
</div>
309

310
<div class="controls">
311
        <LocationButton {handleLocationObtained} />
312
</div>
313

314
<style>
315
        .map-container {
316
                position: relative;
317
                height: 100%;
318
                width: 100%;
319
                z-index: 1;
320
        }
321
        #map {
322
                height: 100%;
323
                width: 100%;
324
        }
325
</style>
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc