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

OneBusAway / wayfinder / 18135387953

30 Sep 2025 03:36PM UTC coverage: 74.689% (-0.2%) from 74.92%
18135387953

push

github

web-flow
Merge pull request #269 from OneBusAway/refactor/optimize-map-marker-system

Refactor/optimize-map-marker-system

839 of 957 branches covered (87.67%)

Branch coverage included in aggregate %.

0 of 62 new or added lines in 3 files covered. (0.0%)

4 existing lines in 1 file now uncovered.

6671 of 9098 relevant lines covered (73.32%)

1.92 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

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

13
        import { faBus } from '@fortawesome/free-solid-svg-icons';
14
        import { RouteType, routePriorities, prioritizedRouteTypeForDisplay } from '$config/routeConfig';
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
         */
25

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

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

44
        function cacheKey(zoomLevel, boundingBox) {
×
NEW
45
                const multiplier = 100; // 2 decimal places
×
NEW
46
                const north = Math.round(boundingBox.north * multiplier);
×
NEW
47
                const south = Math.round(boundingBox.south * multiplier);
×
NEW
48
                const east = Math.round(boundingBox.east * multiplier);
×
NEW
49
                const west = Math.round(boundingBox.west * multiplier);
×
50

NEW
51
                return `${north}_${south}_${east}_${west}_${zoomLevel}`;
×
52
        }
53

54
        function getBoundingBox() {
×
55
                if (!mapProvider) {
×
56
                        throw new Error('Map provider is not initialized');
×
57
                }
58
                return mapProvider.getBoundingBox();
×
59
        }
60

61
        async function loadStopsForLocation(lat, lng, zoomLevel, firstCall = false) {
×
62
                if (firstCall) {
×
63
                        const response = await fetch(`/api/oba/stops-for-location?lat=${lat}&lng=${lng}&radius=2500`);
×
64
                        if (!response.ok) {
×
65
                                throw new Error('Failed to fetch locations');
×
66
                        }
67
                        return await response.json();
×
68
                }
69

70
                const boundingBox = getBoundingBox();
×
71
                const key = cacheKey(zoomLevel, boundingBox);
×
72

73
                if (stopsCache.has(key)) {
×
74
                        console.debug('Stop cache hit: ', key);
×
75
                        return stopsCache.get(key);
×
76
                } else {
77
                        console.debug('Stop cache miss: ', key);
×
78
                }
79

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

84
                if (!response.ok) {
×
85
                        throw new Error('Failed to fetch locations');
×
86
                }
87

88
                const stopsForLocation = await response.json();
×
89
                stopsCache.set(key, stopsForLocation);
×
90

91
                return stopsForLocation;
×
92
        }
93

94
        async function initMap() {
×
95
                try {
96
                        await mapProvider.initMap(mapElement, {
×
97
                                lat: Number(initialLat),
×
98
                                lng: Number(initialLng)
×
99
                        });
100

101
                        mapInstance = mapProvider;
×
102

103
                        await loadStopsAndAddMarkers(initialLat, initialLng, true);
×
104

105
                        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) {
×
111
                                        return;
112
                                }
113
                                await loadStopsAndAddMarkers(center.lat, center.lng, false, zoomLevel);
×
114
                        }, 300);
×
115

116
                        mapProvider.eventListeners(mapInstance, debouncedLoadMarkers);
×
117

118
                        if (browser) {
×
119
                                window.addEventListener('themeChange', handleThemeChange);
×
120
                        }
121
                } catch (error) {
×
122
                        console.error('Error initializing map:', error);
×
123
                }
124
        }
125

126
        async function loadStopsAndAddMarkers(lat, lng, firstCall = false, zoomLevel = 15) {
×
127
                const stopsData = await loadStopsForLocation(lat, lng, zoomLevel, firstCall);
×
128
                const newStops = stopsData.data.list;
×
129
                const routeReference = stopsData.data.references.routes || [];
×
130

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

133
                // merge the stops routeIds with the route data and deduplicate efficiently
134
                newStops.forEach((stop) => {
×
NEW
135
                        if (!allStopsMap.has(stop.id)) {
×
NEW
136
                                stop.routes =
×
NEW
137
                                        stop.routeIds?.map((routeId) => routeLookup.get(routeId)).filter(Boolean) || [];
×
NEW
138
                                allStopsMap.set(stop.id, stop);
×
139
                        }
140
                });
141

NEW
142
                allStops = Array.from(allStopsMap.values());
×
143
        }
144

145
        function clearAllMarkers() {
×
NEW
146
                if (mapInstance && mapInstance.clearAllStopMarkers) {
×
NEW
147
                        mapInstance.clearAllStopMarkers();
×
148
                }
149
        }
150

151
        function updateMarkers() {
×
152
                if (!selectedRoute && !isTripPlanModeActive) {
×
NEW
153
                        batchAddMarkers(allStops);
×
154
                }
155
        }
156

157
        // Batch operation to add multiple markers efficiently
NEW
158
        function batchAddMarkers(stops) {
×
NEW
159
                const stopsToAdd = stops.filter((s) => !mapInstance.hasMarker(s.id));
×
160

NEW
161
                if (stopsToAdd.length === 0) return;
×
162

163
                // Group DOM operations to minimize reflows/repaints
NEW
164
                requestAnimationFrame(() => {
×
NEW
165
                        stopsToAdd.forEach((s) => addMarker(s));
×
166
                });
167
        }
168

169
        function addMarker(s) {
×
170
                if (!mapInstance) {
×
171
                        console.error('Map not initialized yet');
×
172
                        return;
173
                }
174

175
                // Delegate marker existence check to provider
NEW
176
                if (mapInstance.hasMarker(s.id)) {
×
177
                        return;
178
                }
179

180
                let icon = faBus;
×
181

182
                if (s.routes && s.routes.length > 0) {
×
NEW
183
                        const routeTypes = s.routes.map((r) => r.type);
×
NEW
184
                        let prioritizedType = RouteType.UNKNOWN;
×
185

186
                        // Optimized priority lookup - check highest priority first
NEW
187
                        for (const priority of routePriorities) {
×
NEW
188
                                if (routeTypes.includes(priority)) {
×
NEW
189
                                        prioritizedType = priority;
×
190
                                        break;
191
                                }
192
                        }
193

UNCOV
194
                        icon = prioritizedRouteTypeForDisplay(prioritizedType);
×
195
                }
196

197
                const markerObj = mapInstance.addMarker({
×
198
                        position: { lat: s.lat, lng: s.lon },
×
199
                        icon: icon,
×
200
                        stop: s,
×
201
                        onClick: () => {
×
202
                                handleStopMarkerSelect(s);
×
203
                        }
204
                });
205

NEW
206
                return markerObj;
×
207
        }
208

209
        function handleThemeChange(event) {
×
210
                const { darkMode } = event.detail;
×
211
                mapInstance.setTheme(darkMode ? 'dark' : 'light');
×
212
        }
213

214
        function handleLocationObtained(latitude, longitude) {
×
215
                mapInstance.setCenter({ lat: latitude, lng: longitude });
×
216
                mapInstance.addUserLocationMarker({ lat: latitude, lng: longitude });
×
217
                userLocation.set({ lat: latitude, lng: longitude });
×
218
        }
219

220
        // Store event handlers for proper cleanup
NEW
221
        let planTripHandler, tabSwitchHandler;
×
222

223
        onMount(async () => {
×
224
                await initMap();
×
225
                isMapLoaded.set(true);
×
226
                if (browser) {
×
227
                        const darkMode = document.documentElement.classList.contains('dark');
×
228

229
                        // Store handlers for cleanup
NEW
230
                        planTripHandler = () => {
×
UNCOV
231
                                isTripPlanModeActive = true;
×
232
                        };
NEW
233
                        tabSwitchHandler = () => {
×
UNCOV
234
                                isTripPlanModeActive = false;
×
235
                        };
236

NEW
237
                        window.addEventListener('planTripTabClicked', planTripHandler);
×
NEW
238
                        window.addEventListener('tabSwitched', tabSwitchHandler);
×
239

240
                        const event = new CustomEvent('themeChange', { detail: { darkMode } });
×
241
                        window.dispatchEvent(event);
×
242
                }
243
        });
244

245
        onDestroy(() => {
×
246
                if (browser) {
×
247
                        window.removeEventListener('themeChange', handleThemeChange);
×
248

NEW
249
                        if (planTripHandler) window.removeEventListener('planTripTabClicked', planTripHandler);
×
NEW
250
                        if (tabSwitchHandler) window.removeEventListener('tabSwitched', tabSwitchHandler);
×
251
                }
252

NEW
253
                clearAllMarkers();
×
254

NEW
255
                allStopsMap.clear();
×
NEW
256
                stopsCache.clear();
×
257
        });
258
        $effect(() => {
×
259
                if (selectedRoute) {
×
260
                        clearAllMarkers();
×
261
                        updateMarkers();
×
262
                } else if (!isTripPlanModeActive) {
×
NEW
263
                        batchAddMarkers(allStops);
×
264
                }
265
        });
266
        $effect(() => {
×
267
                if (isTripPlanModeActive) {
×
268
                        clearAllMarkers();
×
269
                }
270
        });
271
</script>
272

273
<div class="map-container">
274
        <div id="map" bind:this={mapElement}></div>
×
275

276
        {#if selectedTrip && showRouteMap}
×
277
                <RouteMap mapProvider={mapInstance} tripId={selectedTrip.tripId} />
×
278
        {/if}
279
</div>
280

281
<div class="controls">
282
        <LocationButton {handleLocationObtained} />
283
</div>
284

285
<style>
286
        .map-container {
287
                position: relative;
288
                height: 100%;
289
                width: 100%;
290
                z-index: 1;
291
        }
292
        #map {
293
                height: 100%;
294
                width: 100%;
295
        }
296
</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