@@ -15,6 +15,7 @@ import Map, {
1515 NavigationControl ,
1616} from 'react-map-gl/maplibre' ;
1717import type { MaplibreTerradrawControl } from '@watergis/maplibre-gl-terradraw' ;
18+ import type maplibregl from 'maplibre-gl' ;
1819import 'maplibre-gl/dist/maplibre-gl.css' ;
1920
2021import axios from 'axios' ;
@@ -34,6 +35,7 @@ import {
3435 buildLocateRequest ,
3536} from '@/utils/valhalla' ;
3637import { buildHeightgraphData } from '@/utils/heightgraph' ;
38+ import { formatDuration } from '@/utils/date-time' ;
3739import HeightGraph from '@/components/heightgraph' ;
3840import { DrawControl } from './draw-control' ;
3941import './map.css' ;
@@ -45,7 +47,7 @@ import type { ThunkDispatch } from 'redux-thunk';
4547import type { DirectionsState } from '@/reducers/directions' ;
4648import type { IsochroneState } from '@/reducers/isochrones' ;
4749import type { Profile } from '@/reducers/common' ;
48- import type { ParsedDirectionsGeometry } from '@/common/types' ;
50+ import type { ParsedDirectionsGeometry , Summary } from '@/common/types' ;
4951import type { Feature , FeatureCollection , LineString } from 'geojson' ;
5052
5153// Import the style JSON
@@ -169,6 +171,11 @@ const MapComponent = ({
169171 const [ heightgraphHoverDistance , setHeightgraphHoverDistance ] = useState <
170172 number | null
171173 > ( null ) ;
174+ const [ routeHoverPopup , setRouteHoverPopup ] = useState < {
175+ lng : number ;
176+ lat : number ;
177+ summary : Summary ;
178+ } | null > ( null ) ;
172179 const [ viewState , setViewState ] = useState ( {
173180 longitude : center [ 0 ] ,
174181 latitude : center [ 1 ] ,
@@ -651,6 +658,66 @@ const MapComponent = ({
651658 localStorage . setItem ( 'last_center' , last_center ) ;
652659 } , [ ] ) ;
653660
661+ // Handle route line hover
662+ const onRouteLineHover = useCallback (
663+ ( event : maplibregl . MapLayerMouseEvent ) => {
664+ if ( ! mapRef . current ) return ;
665+
666+ const map = mapRef . current . getMap ( ) ;
667+ map . getCanvas ( ) . style . cursor = 'pointer' ;
668+
669+ const feature = event . features ?. [ 0 ] ;
670+ if ( feature && feature . properties ?. summary ) {
671+ // Parse the summary if it's a string
672+ const summary =
673+ typeof feature . properties . summary === 'string'
674+ ? JSON . parse ( feature . properties . summary )
675+ : feature . properties . summary ;
676+
677+ setRouteHoverPopup ( {
678+ lng : event . lngLat . lng ,
679+ lat : event . lngLat . lat ,
680+ summary : summary as Summary ,
681+ } ) ;
682+ }
683+ } ,
684+ [ ]
685+ ) ;
686+
687+ const handleMouseMove = useCallback (
688+ ( event : maplibregl . MapLayerMouseEvent ) => {
689+ if ( ! mapRef . current || showPopup ) return ; // Don't show if click popup is visible
690+
691+ const features = event . features ;
692+ // Check if we're hovering over the routes-line layer
693+ const isOverRoute =
694+ features &&
695+ features . length > 0 &&
696+ features [ 0 ] ?. layer ?. id === 'routes-line' ;
697+
698+ if ( isOverRoute ) {
699+ onRouteLineHover ( event ) ;
700+ } else {
701+ // Clear popup and cursor when not over route
702+ if ( routeHoverPopup ) {
703+ setRouteHoverPopup ( null ) ;
704+ }
705+ const map = mapRef . current . getMap ( ) ;
706+ if ( map . getCanvas ( ) . style . cursor === 'pointer' ) {
707+ map . getCanvas ( ) . style . cursor = '' ;
708+ }
709+ }
710+ } ,
711+ [ showPopup , routeHoverPopup , onRouteLineHover ]
712+ ) ;
713+
714+ const handleMouseLeave = useCallback ( ( ) => {
715+ if ( ! mapRef . current ) return ;
716+ const map = mapRef . current . getMap ( ) ;
717+ map . getCanvas ( ) . style . cursor = '' ;
718+ setRouteHoverPopup ( null ) ;
719+ } , [ ] ) ;
720+
654721 const MarkerIcon = ( {
655722 color,
656723 number,
@@ -924,6 +991,9 @@ const MapComponent = ({
924991 onMoveEnd = { handleMoveEnd }
925992 onClick = { handleMapClick }
926993 onContextMenu = { handleMapContextMenu }
994+ onMouseMove = { handleMouseMove }
995+ onMouseLeave = { handleMouseLeave }
996+ interactiveLayerIds = { [ 'routes-line' ] }
927997 mapStyle = { mapStyle as unknown as maplibregl . StyleSpecification }
928998 style = { { width : '100%' , height : '100vh' } }
929999 maxBounds = { maxBounds }
@@ -1121,6 +1191,59 @@ const MapComponent = ({
11211191 </ Popup >
11221192 ) }
11231193
1194+ { /* Route hover popup */ }
1195+ { routeHoverPopup && (
1196+ < Popup
1197+ longitude = { routeHoverPopup . lng }
1198+ latitude = { routeHoverPopup . lat }
1199+ anchor = "bottom"
1200+ closeButton = { false }
1201+ closeOnClick = { false }
1202+ maxWidth = "none"
1203+ >
1204+ { /* todo: update styling with tailwind when we migrate to it */ }
1205+ < div style = { { padding : '4px 8px' , minWidth : '120px' } } >
1206+ < div
1207+ style = { {
1208+ fontSize : '11px' ,
1209+ fontWeight : 'bold' ,
1210+ marginBottom : '4px' ,
1211+ color : '#666' ,
1212+ } }
1213+ >
1214+ Route Summary
1215+ </ div >
1216+ < div
1217+ style = { {
1218+ display : 'flex' ,
1219+ alignItems : 'center' ,
1220+ gap : '4px' ,
1221+ marginBottom : '2px' ,
1222+ } }
1223+ >
1224+ < Icon
1225+ name = "arrows alternate horizontal"
1226+ size = "small"
1227+ style = { { margin : 0 } }
1228+ />
1229+ < span style = { { fontSize : '13px' } } >
1230+ { `${ routeHoverPopup . summary . length . toFixed (
1231+ routeHoverPopup . summary . length > 1000 ? 0 : 1
1232+ ) } km`}
1233+ </ span >
1234+ </ div >
1235+ < div
1236+ style = { { display : 'flex' , alignItems : 'center' , gap : '4px' } }
1237+ >
1238+ < Icon name = "clock" size = "small" style = { { margin : 0 } } />
1239+ < span style = { { fontSize : '13px' } } >
1240+ { formatDuration ( routeHoverPopup . summary . time ) }
1241+ </ span >
1242+ </ div >
1243+ </ div >
1244+ </ Popup >
1245+ ) }
1246+
11241247 { /* Brand logos */ }
11251248 < div
11261249 style = { {
0 commit comments