Skip to content

Commit 12fa0bb

Browse files
cosmithclaude
andcommitted
feat(frontend): implement trip route chevron patterns with selection states
- Add custom PNG chevron patterns for trip routes showing direction - Implement selected state pattern when trip popup is open - Replace canvas-generated arrows with asset-based chevron images - Add dynamic pattern switching between normal and selected states - Improve trip line visibility with wider lines and clear directional indicators 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4ce2973 commit 12fa0bb

File tree

4 files changed

+70
-40
lines changed

4 files changed

+70
-40
lines changed
569 Bytes
Loading
586 Bytes
Loading

frontend/src/hooks/useMapLayers.ts

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
import {
1010
getStatusColor,
1111
createStopMarkerElement,
12-
createArrowElement,
1312
createVehicleArrowElement,
1413
} from '../utils/mapStyles'
1514

@@ -20,11 +19,24 @@ export const useMapLayers = (
2019
const vehicleMarkers = useRef<mapboxgl.Marker[]>([])
2120
const tripSources = useRef<string[]>([])
2221
const activePopups = useRef<mapboxgl.Popup[]>([])
22+
const selectedTripId = useRef<number | null>(null)
2323

2424
const closeAllPopups = useCallback(() => {
2525
activePopups.current.forEach((popup) => popup.remove())
2626
activePopups.current = []
27-
}, [])
27+
28+
// Reset selected trip and update all trip patterns
29+
if (selectedTripId.current) {
30+
const prevSelectedId = selectedTripId.current
31+
selectedTripId.current = null
32+
33+
// Update the previously selected trip pattern back to normal
34+
const layerId = `trip-${prevSelectedId}-layer`
35+
if (map.current?.getLayer(layerId)) {
36+
map.current.setPaintProperty(layerId, 'line-pattern', 'trip-chevron')
37+
}
38+
}
39+
}, [map])
2840

2941
// Add click-outside behavior to close all popups
3042
const setupMapClickHandler = useCallback(() => {
@@ -192,45 +204,51 @@ export const useMapLayers = (
192204
})
193205

194206
const statusColor = getStatusColor(trip.status)
207+
const patternId = 'trip-chevron'
208+
209+
// Load chevron pattern images if they don't exist
210+
if (!map.current!.hasImage('trip-chevron')) {
211+
map.current!.loadImage(
212+
'/src/assets/trip-chevron.png',
213+
(error, image) => {
214+
if (error) throw error
215+
if (image && !map.current!.hasImage('trip-chevron')) {
216+
map.current!.addImage('trip-chevron', image, { sdf: false })
217+
}
218+
}
219+
)
220+
}
221+
222+
if (!map.current!.hasImage('trip-chevron-selected')) {
223+
map.current!.loadImage(
224+
'/src/assets/trip-chevron-selected.png',
225+
(error, image) => {
226+
if (error) throw error
227+
if (image && !map.current!.hasImage('trip-chevron-selected')) {
228+
map.current!.addImage('trip-chevron-selected', image, {
229+
sdf: false,
230+
})
231+
}
232+
}
233+
)
234+
}
195235

196236
map.current!.addLayer({
197237
id: `${sourceId}-layer`,
198238
type: 'line',
199239
source: sourceId,
200240
layout: {
201-
'line-join': 'round',
241+
'line-join': 'none',
202242
'line-cap': 'round',
203243
},
204244
paint: {
205245
'line-color': statusColor,
206-
'line-width': 5,
246+
'line-pattern': patternId,
247+
'line-width': 16,
248+
'line-opacity': 1.0,
207249
},
208250
})
209251

210-
// Add directional arrows at midpoints
211-
for (let i = 0; i < coordinates.length - 1; i++) {
212-
const start = coordinates[i]
213-
const end = coordinates[i + 1]
214-
const midPoint = [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]
215-
216-
// Calculate bearing for arrow direction
217-
const bearing =
218-
(Math.atan2(end[1] - start[1], end[0] - start[0]) * 180) / Math.PI
219-
220-
// Create arrow marker
221-
const arrowElement = createArrowElement(statusColor)
222-
arrowElement.style.transform = `rotate(${bearing + 90}deg)`
223-
arrowElement.style.opacity = showTrips ? '0.8' : '0'
224-
arrowElement.classList.add('trip-arrow') // Add class for easier identification
225-
226-
new mapboxgl.Marker({
227-
element: arrowElement,
228-
anchor: 'center',
229-
})
230-
.setLngLat(midPoint as [number, number])
231-
.addTo(map.current!)
232-
}
233-
234252
// Add click handler for trip lines
235253
map.current!.on('click', `${sourceId}-layer`, (e) => {
236254
const coordinates = e.lngLat
@@ -249,11 +267,34 @@ export const useMapLayers = (
249267
closeAllPopups()
250268
activePopups.current.push(popup)
251269

252-
// Remove from active popups when closed
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+
}
280+
281+
// Remove from active popups when closed and reset pattern
253282
popup.on('close', () => {
254283
activePopups.current = activePopups.current.filter(
255284
(p) => p !== popup
256285
)
286+
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+
)
296+
}
297+
}
257298
})
258299

259300
popup.setLngLat(coordinates).addTo(map.current!)

frontend/src/utils/mapStyles.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,6 @@ export const createStopMarkerElement = (
3131
return markerElement
3232
}
3333

34-
export const createArrowElement = (color: string): HTMLDivElement => {
35-
const arrowElement = document.createElement('div')
36-
arrowElement.style.width = '0'
37-
arrowElement.style.height = '0'
38-
arrowElement.style.borderLeft = '8px solid transparent'
39-
arrowElement.style.borderRight = '8px solid transparent'
40-
arrowElement.style.borderBottom = `12px solid ${color}`
41-
arrowElement.style.opacity = '0.8'
42-
return arrowElement
43-
}
44-
4534
export const createVehicleArrowElement = (
4635
isLive: boolean = true,
4736
heading: number = 0

0 commit comments

Comments
 (0)