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

OneBusAway / wayfinder / 23676368898

28 Mar 2026 03:20AM UTC coverage: 74.321%. First build
23676368898

Pull #453

github

web-flow
Merge ccd597435 into 019d5a038
Pull Request #453: Fix critical dependency vulnerabilities + Svelte 5 compatibility fixes

1946 of 2131 branches covered (91.32%)

Branch coverage included in aggregate %.

17 of 47 new or added lines in 25 files covered. (36.17%)

11266 of 15646 relevant lines covered (72.01%)

5.09 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 { SvelteMap } from 'svelte/reactivity';
5
        import {
6
                PUBLIC_OBA_REGION_CENTER_LAT as initialLat,
7
                PUBLIC_OBA_REGION_CENTER_LNG as initialLng
8
        } from '$env/static/public';
9
        import { env } from '$env/dynamic/public';
10

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

134
                return stopsForLocation;
×
135
        }
×
136

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

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

148
                        mapInstance = mapProvider;
×
149

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

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

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

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

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

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

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

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

NEW
189
                const routeLookup = new SvelteMap(routeReference.map((route) => [route.id, route]));
×
190

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

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

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

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

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

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

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

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

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

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

245
                return markerObj;
×
246
        }
×
247

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

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

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

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

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

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

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

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

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

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

296
                clearAllMarkers();
×
297

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

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

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

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

315
<style>
316
        .map-container {
317
                position: relative;
318
                height: 100%;
319
                width: 100%;
320
                z-index: 1;
321
        }
322
        #map {
323
                height: 100%;
324
                width: 100%;
325
        }
326
</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