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

OneBusAway / wayfinder / 18113598733

29 Sep 2025 11:25PM UTC coverage: 74.631% (-0.2%) from 74.84%
18113598733

Pull #269

github

web-flow
Merge 67c25088d into bd19185af
Pull Request #269: Refactor/optimize-map-marker-system

829 of 946 branches covered (87.63%)

Branch coverage included in aggregate %.

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

2 existing lines in 1 file now uncovered.

6646 of 9070 relevant lines covered (73.27%)

1.83 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 = stop.routeIds?.map((routeId) => routeLookup.get(routeId)).filter(Boolean) || [];
×
NEW
137
                                allStopsMap.set(stop.id, stop);
×
138
                        }
139
                });
140

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

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

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

156
        // Batch operation to add multiple markers efficiently
NEW
157
        function batchAddMarkers(stops) {
×
NEW
158
                const stopsToAdd = stops.filter(s => !mapInstance.hasMarker(s.id));
×
159
                
NEW
160
                if (stopsToAdd.length === 0) return;
×
161
                
162
                // Group DOM operations to minimize reflows/repaints
NEW
163
                requestAnimationFrame(() => {
×
NEW
164
                        stopsToAdd.forEach(s => addMarker(s));
×
165
                });
166
        }
167

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

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

179
                let icon = faBus;
×
180

181
                if (s.routes && s.routes.length > 0) {
×
NEW
182
                        const routeTypes = s.routes.map((r) => r.type);
×
NEW
183
                        let prioritizedType = RouteType.UNKNOWN;
×
184
                        
185
                        // Optimized priority lookup - check highest priority first
NEW
186
                        for (const priority of routePriorities) {
×
NEW
187
                                if (routeTypes.includes(priority)) {
×
NEW
188
                                        prioritizedType = priority;
×
189
                                        break;
190
                                }
191
                        }
192
                        
UNCOV
193
                        icon = prioritizedRouteTypeForDisplay(prioritizedType);
×
194
                }
195

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

NEW
205
                return markerObj;
×
206
        }
207

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

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

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

222
        onMount(async () => {
×
223
                await initMap();
×
224
                isMapLoaded.set(true);
×
225
                if (browser) {
×
226
                        const darkMode = document.documentElement.classList.contains('dark');
×
227
                        
228
                        // Store handlers for cleanup
NEW
229
                        planTripHandler = () => { isTripPlanModeActive = true; };
×
NEW
230
                        tabSwitchHandler = () => { isTripPlanModeActive = false; };
×
231
                        
NEW
232
                        window.addEventListener('planTripTabClicked', planTripHandler);
×
NEW
233
                        window.addEventListener('tabSwitched', tabSwitchHandler);
×
234
                        
235
                        const event = new CustomEvent('themeChange', { detail: { darkMode } });
×
236
                        window.dispatchEvent(event);
×
237
                }
238
        });
239

240
        onDestroy(() => {
×
241
                if (browser) {
×
242
                        window.removeEventListener('themeChange', handleThemeChange);
×
243
                
NEW
244
                        if (planTripHandler) window.removeEventListener('planTripTabClicked', planTripHandler);
×
NEW
245
                        if (tabSwitchHandler) window.removeEventListener('tabSwitched', tabSwitchHandler);
×
246
                }
247

NEW
248
                clearAllMarkers();
×
249
                
NEW
250
                allStopsMap.clear();
×
NEW
251
                stopsCache.clear();
×
252
        });
253
        $effect(() => {
×
254
                if (selectedRoute) {
×
255
                        clearAllMarkers();
×
256
                        updateMarkers();
×
257
                } else if (!isTripPlanModeActive) {
×
NEW
258
                        batchAddMarkers(allStops);
×
259
                }
260
        });
261
        $effect(() => {
×
262
                if (isTripPlanModeActive) {
×
263
                        clearAllMarkers();
×
264
                }
265
        });
266
</script>
267

268
<div class="map-container">
269
        <div id="map" bind:this={mapElement}></div>
×
270

271
        {#if selectedTrip && showRouteMap}
×
272
                <RouteMap mapProvider={mapInstance} tripId={selectedTrip.tripId} />
×
273
        {/if}
274
</div>
275

276
<div class="controls">
277
        <LocationButton {handleLocationObtained} />
278
</div>
279

280
<style>
281
        .map-container {
282
                position: relative;
283
                height: 100%;
284
                width: 100%;
285
                z-index: 1;
286
        }
287
        #map {
288
                height: 100%;
289
                width: 100%;
290
        }
291
</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