Skip to content

Commit 9465d7a

Browse files
committed
feat(frontend): enhance MapView with trip details drawer and trip update handling
- Integrated TripDetailsDrawer into MapView for displaying trip details. - Added functionality to refresh trip data upon updates. - Improved map interaction by allowing selection of trips with visual feedback. - Updated useMapLayers to support trip selection and fitting map bounds to selected trips. - Enhanced drawer animation with slide-in effect for better user experience.
1 parent b06b4aa commit 9465d7a

3 files changed

Lines changed: 200 additions & 78 deletions

File tree

frontend/src/components/maps/MapView.tsx

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import React, { useEffect, useState, useRef } from 'react'
22
import mapboxgl from 'mapbox-gl'
3-
import { Box, Text } from '@chakra-ui/react'
3+
import { Box, Text, useDisclosure } from '@chakra-ui/react'
44
import { useMapData } from '../../hooks/useMapData'
55
import { useMapLayers } from '../../hooks/useMapLayers'
6-
import { MapControls } from '../../types/map'
6+
import type { MapControls, Trip } from '../../types/map'
77
import { getMapCustomCSS } from '../../utils/mapStyles'
88
import MapControlsComponent from './MapControls'
9+
import { TripDetailsDrawer } from '../trips/TripDetailsDrawer'
910
import 'mapbox-gl/dist/mapbox-gl.css'
1011

1112
const MapView: React.FC = () => {
1213
const mapContainer = useRef<HTMLDivElement>(null)
1314
const map = useRef<mapboxgl.Map | null>(null)
15+
const [selectedTrip, setSelectedTrip] = useState<Trip | null>(null)
1416

1517
// Custom hooks for data and layer management
16-
const { stops, trips, vehiclePositions, loading, error } = useMapData()
18+
const { stops, trips, vehiclePositions, loading, error, refetchTrips } =
19+
useMapData()
20+
21+
const [shouldRefreshTrips, setShouldRefreshTrips] = useState(false)
22+
23+
const handleTripUpdated = async () => {
24+
setShouldRefreshTrips(true)
25+
await refetchTrips()
26+
}
27+
1728
const {
1829
addMarkersToMap,
1930
addTripsToMap,
@@ -22,7 +33,24 @@ const MapView: React.FC = () => {
2233
toggleTripVisibility,
2334
toggleVehicleVisibility,
2435
setupMapClickHandler,
25-
} = useMapLayers(map)
36+
fitMapToTrip,
37+
resetSelectedTrip,
38+
forceRefreshTrips,
39+
} = useMapLayers(map, (trip: Trip) => {
40+
setSelectedTrip(trip)
41+
onDrawerOpen()
42+
// Fit map to trip with space for drawer after a short delay
43+
setTimeout(() => {
44+
fitMapToTrip(trip, true)
45+
}, 100)
46+
})
47+
48+
// Drawer state
49+
const {
50+
isOpen: isDrawerOpen,
51+
onOpen: onDrawerOpen,
52+
onClose: onDrawerClose,
53+
} = useDisclosure()
2654

2755
// Control states for map layers
2856
const [controls, setControls] = useState<MapControls>({
@@ -36,6 +64,20 @@ const MapView: React.FC = () => {
3664

3765
const mapboxToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
3866

67+
// Effect to force refresh trips when data changes after an update
68+
useEffect(() => {
69+
if (shouldRefreshTrips && mapReady && map.current && trips.length > 0) {
70+
forceRefreshTrips(trips, controls.showTrips)
71+
setShouldRefreshTrips(false)
72+
}
73+
}, [
74+
trips,
75+
shouldRefreshTrips,
76+
mapReady,
77+
forceRefreshTrips,
78+
controls.showTrips,
79+
])
80+
3981
// Handle control changes
4082
const handleControlChange = (control: keyof MapControls, value: boolean) => {
4183
setControls((prev) => ({ ...prev, [control]: value }))
@@ -180,6 +222,17 @@ const MapView: React.FC = () => {
180222
controls={controls}
181223
onControlChange={handleControlChange}
182224
/>
225+
226+
<TripDetailsDrawer
227+
isOpen={isDrawerOpen}
228+
onClose={() => {
229+
setSelectedTrip(null)
230+
resetSelectedTrip()
231+
onDrawerClose()
232+
}}
233+
trip={selectedTrip}
234+
onTripUpdated={handleTripUpdated}
235+
/>
183236
</Box>
184237
)
185238
}

frontend/src/components/trips/TripDetailsDrawer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,13 @@ export const TripDetailsDrawer: React.FC<TripDetailsDrawerProps> = ({
187187
)
188188

189189
return (
190-
<Drawer isOpen={isOpen} placement="right" onClose={onClose} size="xl">
190+
<Drawer
191+
isOpen={isOpen}
192+
placement="right"
193+
onClose={onClose}
194+
size="xl"
195+
motionPreset="slideInRight"
196+
>
191197
<DrawerOverlay />
192198
<DrawerContent>
193199
<DrawerCloseButton />

frontend/src/hooks/useMapLayers.ts

Lines changed: 136 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
} from '../utils/mapStyles'
1414

1515
export const useMapLayers = (
16-
map: React.MutableRefObject<mapboxgl.Map | null>
16+
map: React.MutableRefObject<mapboxgl.Map | null>,
17+
onTripClick?: (trip: Trip) => void
1718
) => {
1819
const stopMarkers = useRef<mapboxgl.Marker[]>([])
1920
const vehicleMarkers = useRef<mapboxgl.Marker[]>([])
@@ -124,33 +125,7 @@ export const useMapLayers = (
124125
(trips: Trip[], showTrips: boolean) => {
125126
if (!map.current) return
126127

127-
// Check if we already have the right number of trip sources
128-
const expectedSources = trips.filter(
129-
(trip) => trip.trip_stops && trip.trip_stops.length >= 2
130-
).length
131-
132-
if (tripSources.current.length === expectedSources) {
133-
// Just toggle visibility if sources already exist
134-
tripSources.current.forEach((sourceId) => {
135-
const layerId = `${sourceId}-layer`
136-
if (map.current?.getLayer(layerId)) {
137-
map.current.setLayoutProperty(
138-
layerId,
139-
'visibility',
140-
showTrips ? 'visible' : 'none'
141-
)
142-
}
143-
})
144-
145-
// Toggle arrow markers visibility
146-
const arrowElements = document.querySelectorAll('.trip-arrow')
147-
arrowElements.forEach((element) => {
148-
;(element as HTMLElement).style.opacity = showTrips ? '0.8' : '0'
149-
})
150-
return
151-
}
152-
153-
// Clear existing trip sources if count doesn't match
128+
// Always clear existing trip sources to ensure fresh data
154129
tripSources.current.forEach((sourceId) => {
155130
if (map.current?.getSource(sourceId)) {
156131
if (map.current?.getLayer(`${sourceId}-layer`)) {
@@ -204,7 +179,11 @@ export const useMapLayers = (
204179
})
205180

206181
const statusColor = getStatusColor(trip.status)
207-
const patternId = 'trip-chevron'
182+
// Use selected pattern if this trip is currently selected
183+
const patternId =
184+
selectedTripId.current === trip.id
185+
? 'trip-chevron-selected'
186+
: 'trip-chevron'
208187

209188
// Load chevron pattern images if they don't exist
210189
if (!map.current!.hasImage('trip-chevron')) {
@@ -240,6 +219,7 @@ export const useMapLayers = (
240219
layout: {
241220
'line-join': 'none',
242221
'line-cap': 'round',
222+
visibility: showTrips ? 'visible' : 'none',
243223
},
244224
paint: {
245225
'line-color': statusColor,
@@ -251,53 +231,71 @@ export const useMapLayers = (
251231

252232
// Add click handler for trip lines
253233
map.current!.on('click', `${sourceId}-layer`, (e) => {
254-
const coordinates = e.lngLat
255-
256-
// Create popup content using the same styling as stops
257-
const popupContent = createTripPopupContent(trip, validStops)
258-
259-
const popup = new mapboxgl.Popup({
260-
offset: 25,
261-
closeButton: true,
262-
closeOnClick: false,
263-
className: 'custom-popup',
264-
}).setHTML(popupContent)
265-
266-
// Close all other popups when this one opens
267-
closeAllPopups()
268-
activePopups.current.push(popup)
269-
270-
// Update selected trip pattern
271-
selectedTripId.current = trip.id
272-
const layerId = `${sourceId}-layer`
273-
if (map.current?.getLayer(layerId)) {
274-
map.current.setPaintProperty(
275-
layerId,
276-
'line-pattern',
277-
'trip-chevron-selected'
278-
)
279-
}
234+
if (onTripClick) {
235+
// Close all popups and update selected trip pattern
236+
closeAllPopups()
237+
selectedTripId.current = trip.id
238+
const layerId = `${sourceId}-layer`
239+
if (map.current?.getLayer(layerId)) {
240+
map.current.setPaintProperty(
241+
layerId,
242+
'line-pattern',
243+
'trip-chevron-selected'
244+
)
245+
}
280246

281-
// Remove from active popups when closed and reset pattern
282-
popup.on('close', () => {
283-
activePopups.current = activePopups.current.filter(
284-
(p) => p !== popup
285-
)
247+
// Call the trip click callback (for drawer)
248+
onTripClick(trip)
249+
} else {
250+
// Fallback to popup behavior
251+
const coordinates = e.lngLat
252+
253+
// Create popup content using the same styling as stops
254+
const popupContent = createTripPopupContent(trip, validStops)
255+
256+
const popup = new mapboxgl.Popup({
257+
offset: 25,
258+
closeButton: true,
259+
closeOnClick: false,
260+
className: 'custom-popup',
261+
}).setHTML(popupContent)
262+
263+
// Close all other popups when this one opens
264+
closeAllPopups()
265+
activePopups.current.push(popup)
266+
267+
// Update selected trip pattern
268+
selectedTripId.current = trip.id
269+
const layerId = `${sourceId}-layer`
270+
if (map.current?.getLayer(layerId)) {
271+
map.current.setPaintProperty(
272+
layerId,
273+
'line-pattern',
274+
'trip-chevron-selected'
275+
)
276+
}
286277

287-
// Reset pattern back to normal when popup closes
288-
if (selectedTripId.current === trip.id) {
289-
selectedTripId.current = null
290-
if (map.current?.getLayer(layerId)) {
291-
map.current.setPaintProperty(
292-
layerId,
293-
'line-pattern',
294-
'trip-chevron'
295-
)
278+
// Remove from active popups when closed and reset pattern
279+
popup.on('close', () => {
280+
activePopups.current = activePopups.current.filter(
281+
(p) => p !== popup
282+
)
283+
284+
// Reset pattern back to normal when popup closes
285+
if (selectedTripId.current === trip.id) {
286+
selectedTripId.current = null
287+
if (map.current?.getLayer(layerId)) {
288+
map.current.setPaintProperty(
289+
layerId,
290+
'line-pattern',
291+
'trip-chevron'
292+
)
293+
}
296294
}
297-
}
298-
})
295+
})
299296

300-
popup.setLngLat(coordinates).addTo(map.current!)
297+
popup.setLngLat(coordinates).addTo(map.current!)
298+
}
301299
})
302300

303301
// Change cursor on hover
@@ -310,7 +308,7 @@ export const useMapLayers = (
310308
})
311309
})
312310
},
313-
[map, closeAllPopups]
311+
[map, closeAllPopups, onTripClick]
314312
)
315313

316314
const toggleStopVisibility = useCallback((showStops: boolean) => {
@@ -440,6 +438,68 @@ export const useMapLayers = (
440438
[map]
441439
)
442440

441+
const fitMapToTrip = useCallback(
442+
(trip: Trip, withDrawerSpace = false) => {
443+
if (!map.current || !trip.trip_stops || trip.trip_stops.length === 0)
444+
return
445+
446+
const bounds = new mapboxgl.LngLatBounds()
447+
448+
// Add all trip stops to bounds
449+
trip.trip_stops.forEach((tripStop) => {
450+
if (tripStop.stop.latitude && tripStop.stop.longitude) {
451+
const lat = parseFloat(tripStop.stop.latitude)
452+
const lng = parseFloat(tripStop.stop.longitude)
453+
bounds.extend([lng, lat])
454+
}
455+
})
456+
457+
// Adjust padding to account for drawer space
458+
const padding = withDrawerSpace
459+
? { top: 100, bottom: 100, left: 100, right: 1000 } // Extra space on right for drawer
460+
: 50
461+
462+
map.current.fitBounds(bounds, {
463+
padding,
464+
maxZoom: 12,
465+
})
466+
},
467+
[map]
468+
)
469+
470+
const resetSelectedTrip = useCallback(() => {
471+
if (selectedTripId.current) {
472+
const tripId = selectedTripId.current
473+
selectedTripId.current = null
474+
475+
const layerId = `trip-${tripId}-layer`
476+
if (map.current?.getLayer(layerId)) {
477+
map.current.setPaintProperty(layerId, 'line-pattern', 'trip-chevron')
478+
}
479+
}
480+
}, [map])
481+
482+
const forceRefreshTrips = useCallback(
483+
(trips: Trip[], showTrips: boolean) => {
484+
if (!map.current) return
485+
486+
// Clear existing trip sources
487+
tripSources.current.forEach((sourceId) => {
488+
if (map.current?.getSource(sourceId)) {
489+
if (map.current?.getLayer(`${sourceId}-layer`)) {
490+
map.current.removeLayer(`${sourceId}-layer`)
491+
}
492+
map.current.removeSource(sourceId)
493+
}
494+
})
495+
tripSources.current = []
496+
497+
// Re-add all trips
498+
addTripsToMap(trips, showTrips)
499+
},
500+
[map, addTripsToMap]
501+
)
502+
443503
return {
444504
addMarkersToMap,
445505
addTripsToMap,
@@ -449,5 +509,8 @@ export const useMapLayers = (
449509
toggleVehicleVisibility,
450510
setupMapClickHandler,
451511
fitMapToStops,
512+
fitMapToTrip,
513+
resetSelectedTrip,
514+
forceRefreshTrips,
452515
}
453516
}

0 commit comments

Comments
 (0)