Fly map to highlighted day segment on hover

When hovering a day row in the route detail breakdown, the map smoothly
flies to fit that segment's bounds (0.5s animation). On mouse leave it
flies back to the full route bounds. Refactored geometry splitting into
shared helpers used by both DayColoredRoute and FlyToSegment.

Rotation not implemented — Leaflet doesn't support native bearing
rotation. Would require leaflet-rotate plugin or MapLibre GL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-11 00:46:46 +02:00
parent 7029c85dd0
commit b010cd8c59
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9

View file

@ -19,8 +19,60 @@ function FitBounds({ data }: { data: GeoJsonObject }) {
return null;
}
function FlyToSegment({ segments, highlightedDay, fullData }: {
segments: Array<{ coords: number[][] }>;
highlightedDay: number | null | undefined;
fullData: GeoJsonObject;
}) {
const map = useMap();
const fullBoundsRef = useRef<L.LatLngBounds | null>(null);
useEffect(() => {
// Cache full route bounds on first render
if (!fullBoundsRef.current) {
const layer = L.geoJSON(fullData);
fullBoundsRef.current = layer.getBounds();
}
if (highlightedDay != null && highlightedDay >= 1 && highlightedDay <= segments.length) {
const seg = segments[highlightedDay - 1]!;
// coords are [lon, lat] GeoJSON format
const latlngs = seg.coords.map((c) => L.latLng(c[1]!, c[0]!));
const bounds = L.latLngBounds(latlngs);
if (bounds.isValid()) {
map.flyToBounds(bounds, { padding: [40, 40], duration: 0.5 });
}
} else if (fullBoundsRef.current?.isValid()) {
map.flyToBounds(fullBoundsRef.current, { padding: [20, 20], duration: 0.5 });
}
}, [highlightedDay, segments, fullData, map]);
return null;
}
const DAY_COLORS = ["#2563eb", "#8B6D3A", "#059669", "#9333ea", "#dc2626", "#0891b2"];
function extractGeometry(data: GeoJsonObject): number[][] | null {
const coords = (data as { type: string; coordinates?: number[][] }).coordinates
?? ((data as { features?: Array<{ geometry: { coordinates: number[][] } }> }).features?.[0]?.geometry?.coordinates);
return coords && coords.length >= 2 ? coords : null;
}
function splitGeometry(data: GeoJsonObject, numDays: number): Array<{ coords: number[][] }> {
const geometry = extractGeometry(data);
if (!geometry) return [];
const totalPoints = geometry.length;
const pointsPerDay = Math.ceil(totalPoints / numDays);
const segments: Array<{ coords: number[][] }> = [];
for (let d = 0; d < numDays; d++) {
const start = d * pointsPerDay;
const end = Math.min((d + 1) * pointsPerDay + 1, totalPoints);
if (start >= totalPoints) break;
segments.push({ coords: geometry.slice(start, end) });
}
return segments;
}
interface RouteMapProps {
geojson: string;
interactive?: boolean;
@ -55,36 +107,29 @@ export function RouteMapThumbnail({ geojson, interactive, className, dayBreaks,
<GeoJSON data={data} style={{ color: "#2563eb", weight: 3, opacity: 0.8 }} />
)}
<FitBounds data={data} />
{dayBreaks && dayBreaks.length > 0 && (
<FlyToSegment
segments={splitGeometry(data, dayBreaks.length + 1)}
highlightedDay={highlightedDay}
fullData={data}
/>
)}
</MapContainer>
);
}
function DayColoredRoute({ data, dayBreaks, highlightedDay }: { data: GeoJsonObject; dayBreaks: number[]; highlightedDay?: number | null }) {
// Extract coordinates from the GeoJSON LineString
const geometry = (data as { type: string; coordinates?: number[][] }).coordinates
?? ((data as { features?: Array<{ geometry: { coordinates: number[][] } }> }).features?.[0]?.geometry?.coordinates);
if (!geometry || geometry.length < 2) {
const geometry = extractGeometry(data);
if (!geometry) {
return <GeoJSON data={data} style={{ color: "#2563eb", weight: 3, opacity: 0.8 }} />;
}
// Split coordinates into segments at approximate day break points
// dayBreaks are waypoint indices — we split the line evenly since we don't
// have exact waypoint-to-coordinate mapping in the Journal context
const totalPoints = geometry.length;
const numDays = dayBreaks.length + 1;
const pointsPerDay = Math.ceil(totalPoints / numDays);
const segments: Array<{ coords: number[][]; color: string }> = [];
for (let d = 0; d < numDays; d++) {
const start = d * pointsPerDay;
const end = Math.min((d + 1) * pointsPerDay + 1, totalPoints);
if (start >= totalPoints) break;
segments.push({
coords: geometry.slice(start, end),
color: DAY_COLORS[d % DAY_COLORS.length]!,
});
}
const rawSegments = splitGeometry(data, numDays);
const segments = rawSegments.map((seg, i) => ({
...seg,
color: DAY_COLORS[i % DAY_COLORS.length]!,
}));
const isHighlighting = highlightedDay != null;