Skip to content

Commit 38c404d

Browse files
add route summary on user hover a route
1 parent e57d97a commit 38c404d

File tree

1 file changed

+124
-1
lines changed

1 file changed

+124
-1
lines changed

src/map/index.tsx

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Map, {
1515
NavigationControl,
1616
} from 'react-map-gl/maplibre';
1717
import type { MaplibreTerradrawControl } from '@watergis/maplibre-gl-terradraw';
18+
import type maplibregl from 'maplibre-gl';
1819
import 'maplibre-gl/dist/maplibre-gl.css';
1920

2021
import axios from 'axios';
@@ -34,6 +35,7 @@ import {
3435
buildLocateRequest,
3536
} from '@/utils/valhalla';
3637
import { buildHeightgraphData } from '@/utils/heightgraph';
38+
import { formatDuration } from '@/utils/date-time';
3739
import HeightGraph from '@/components/heightgraph';
3840
import { DrawControl } from './draw-control';
3941
import './map.css';
@@ -45,7 +47,7 @@ import type { ThunkDispatch } from 'redux-thunk';
4547
import type { DirectionsState } from '@/reducers/directions';
4648
import type { IsochroneState } from '@/reducers/isochrones';
4749
import type { Profile } from '@/reducers/common';
48-
import type { ParsedDirectionsGeometry } from '@/common/types';
50+
import type { ParsedDirectionsGeometry, Summary } from '@/common/types';
4951
import 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

Comments
 (0)