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

OneBusAway / wayfinder / 13379133338

17 Feb 2025 09:55PM UTC coverage: 11.598% (+2.1%) from 9.509%
13379133338

push

github

web-flow
Merge pull request #176 from OneBusAway/feat/analytics

Add plausible analytics

91 of 186 branches covered (48.92%)

Branch coverage included in aggregate %.

81 of 196 new or added lines in 11 files covered. (41.33%)

1 existing line in 1 file now uncovered.

319 of 3349 relevant lines covered (9.53%)

0.38 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

41
        let markers = [];
×
42
        let stopsCache = new Map();
×
43

44
        function cacheKey(zoomLevel, boundingBox) {
×
45
                const decimalPlaces = 2; // 2 decimal places equals between 0.5 and 1.1 km depending on where you are in the world.
×
46
                const roundedBox = {
×
47
                        north: boundingBox.north.toFixed(decimalPlaces),
×
48
                        south: boundingBox.south.toFixed(decimalPlaces),
×
49
                        east: boundingBox.east.toFixed(decimalPlaces),
×
50
                        west: boundingBox.west.toFixed(decimalPlaces)
×
51
                };
52

53
                return `${roundedBox.north}_${roundedBox.south}_${roundedBox.east}_${roundedBox.west}_${zoomLevel}`;
×
54
        }
55

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

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

72
                const boundingBox = getBoundingBox();
×
73
                const key = cacheKey(zoomLevel, boundingBox);
×
74

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

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

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

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

93
                return stopsForLocation;
×
94
        }
95

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

103
                        mapInstance = mapProvider;
×
104

105
                        await loadStopsAndAddMarkers(initialLat, initialLng, true);
×
106

107
                        const debouncedLoadMarkers = debounce(async () => {
×
108
                                const center = mapInstance.getCenter();
×
109
                                const zoomLevel = mapInstance.map.getZoom();
×
110

111
                                // 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
112
                                if (selectedRoute || showRoute || isTripPlanModeActive) {
×
113
                                        return;
114
                                }
115
                                await loadStopsAndAddMarkers(center.lat, center.lng, false, zoomLevel);
×
116
                        }, 300);
×
117

118
                        mapProvider.eventListeners(mapInstance, debouncedLoadMarkers);
×
119

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

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

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

135
                // merge the stops routeIds with the route data
136
                newStops.forEach((stop) => {
×
137
                        stop.routes = stop.routeIds.map((routeId) => routeLookup.get(routeId)).filter(Boolean);
×
138
                });
139

140
                allStops = [...new Map([...allStops, ...newStops].map((stop) => [stop.id, stop])).values()];
×
141
        }
142

143
        function clearAllMarkers() {
×
144
                markers.forEach((markerObj) => {
×
145
                        mapInstance.removeMarker(markerObj);
×
146
                });
147
                markers = [];
×
148
        }
149

150
        function updateMarkers() {
×
151
                if (!selectedRoute && !isTripPlanModeActive) {
×
152
                        allStops.forEach((s) => addMarker(s));
×
153
                }
154
        }
155

156
        function addMarker(s) {
×
157
                if (!mapInstance) {
×
158
                        console.error('Map not initialized yet');
×
159
                        return;
160
                }
161

162
                // // check if the marker already exists
163
                const existingMarker = markers.find((marker) => marker.stop.id === s.id);
×
164

165
                // if it does, don't add it again
166
                if (existingMarker) {
×
167
                        return;
168
                }
169

170
                let icon = faBus;
×
171

172
                if (s.routes && s.routes.length > 0) {
×
173
                        const routeTypes = new Set(s.routes.map((r) => r.type));
×
174
                        let prioritizedType = routePriorities.find((type) => routeTypes.has(type));
×
175
                        if (prioritizedType === undefined) {
×
176
                                prioritizedType = RouteType.UNKNOWN;
×
177
                        }
178
                        icon = prioritizedRouteTypeForDisplay(prioritizedType);
×
179
                }
180

181
                const markerObj = mapInstance.addMarker({
×
182
                        position: { lat: s.lat, lng: s.lon },
×
183
                        icon: icon,
×
184
                        stop: s,
×
185
                        onClick: () => {
×
186
                                handleStopMarkerSelect(s);
×
187
                        }
188
                });
189

190
                markerObj.stop = s;
×
191
                markers.push(markerObj);
×
192
        }
193

194
        function handleThemeChange(event) {
×
195
                const { darkMode } = event.detail;
×
196
                mapInstance.setTheme(darkMode ? 'dark' : 'light');
×
197
        }
198

199
        function handleLocationObtained(latitude, longitude) {
×
200
                mapInstance.setCenter({ lat: latitude, lng: longitude });
×
201
                mapInstance.addUserLocationMarker({ lat: latitude, lng: longitude });
×
NEW
202
                userLocation.set({ lat: latitude, lng: longitude });
×
203
        }
204

205
        onMount(async () => {
×
206
                await initMap();
×
207
                isMapLoaded.set(true);
×
208
                if (browser) {
×
209
                        const darkMode = document.documentElement.classList.contains('dark');
×
210
                        window.addEventListener('planTripTabClicked', () => {
×
211
                                isTripPlanModeActive = true;
×
212
                        });
213
                        window.addEventListener('tabSwitched', () => {
×
214
                                isTripPlanModeActive = false;
×
215
                        });
216
                        const event = new CustomEvent('themeChange', { detail: { darkMode } });
×
217
                        window.dispatchEvent(event);
×
218
                }
219
        });
220

221
        onDestroy(() => {
×
222
                if (browser) {
×
223
                        window.removeEventListener('themeChange', handleThemeChange);
×
224
                }
225
                markers.forEach(({ markerObj, element }) => {
×
226
                        mapProvider.removeMarker(markerObj);
×
227
                        if (element && element.parentNode) {
×
228
                                element.parentNode.removeChild(element);
×
229
                        }
230
                });
231
        });
232
        $effect(() => {
×
233
                if (selectedRoute) {
×
234
                        clearAllMarkers();
×
235
                        updateMarkers();
×
236
                } else if (!isTripPlanModeActive) {
×
237
                        allStops.forEach((s) => addMarker(s));
×
238
                }
239
        });
240
        $effect(() => {
×
241
                if (isTripPlanModeActive) {
×
242
                        clearAllMarkers();
×
243
                }
244
        });
245
</script>
246

247
<div class="map-container">
248
        <div id="map" bind:this={mapElement}></div>
×
249

250
        {#if selectedTrip && showRouteMap}
×
251
                <RouteMap mapProvider={mapInstance} tripId={selectedTrip.tripId} />
×
252
        {/if}
253
</div>
254

255
<div class="controls">
256
        <LocationButton {handleLocationObtained} />
257
</div>
258

259
<style>
260
        .map-container {
261
                position: relative;
262
                height: 100%;
263
                width: 100%;
264
                z-index: 1;
265
        }
266
        #map {
267
                height: 100%;
268
                width: 100%;
269
        }
270
</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