Files
TREK/client/src/components/Tours/activity/ActivityCompareLegend.tsx
T
Maurice 6a0c54cb2f feat(tours): compare a tour's plan with the track it was actually done with (ACT-02)
Each actual track in a tour's detail now stands beside the plan instead of
on its own: a table names the plan and the actual track as columns, each with
the line it is drawn as on the map, and sets distance, ascent, descent and the
time with breaks side by side. The difference is shown, signed and with an
arrow, only where both values are known and mean the same thing; a planned
total that leaves a stay out reads "at least" and is not subtracted from. A
value one side does not carry is named as missing ("Not in the plan", "Not in
the track", "No time stamps"), never estimated. Below the table, how far the
track left the planned line: its farthest point and the recorded stretches
more than 50 m away, or that there is no line to compare.

On the desktop plan map, "Compare on map" draws the actual track dotted in
the colour of done next to the plan, which stays picked in its own track
colour (and is put back should it be hidden on the map). The recording leaves
the map as a tour of its own meanwhile, so it is never drawn twice or in a
plan's colour. A frosted legend over the map names both lines and ends the
comparison; so do closing the detail, comparing another track, or the track
being unlinked. Comparing only reads: neither the plan nor the track changes.

The track is laid into the day's route lines the map already draws, so both
renderers show it without a layer of their own. distancesToLine in the shared
route corridor measures many points against one line laid flat once; a long
recording is measured in at most 1500 samples.
2026-10-20 15:23:18 +02:00

75 lines
3.1 KiB
TypeScript

import { GitCompareArrows, X } from 'lucide-react';
import type { ReactNode } from 'react';
import { useTranslation } from '../../../i18n';
import { useTourActivityCompareStore } from '../../../store/tourActivityCompareStore';
import { MAP_CARD_GLASS } from '../../Map/mapControlShadow';
import { Eyebrow } from '../../Planner/bookings/bookingParts';
import { fs } from '../../shared/DialogShell';
import { Tooltip } from '../../shared/Tooltip';
import { ACTUAL_TRACK_COLOR, LineSwatch } from './activityCompareParts';
import type { ActivityCompareLegend as Legend } from './useActivityCompareMap';
/** One line of the legend: how it is drawn, what it is, and whose it is. */
function LegendRow({ swatch, label, name }: { swatch: ReactNode; label: string; name: string }) {
return (
<li className="flex min-w-0 items-center gap-2">
<span className="grid h-6 w-8 flex-none place-items-center rounded-full bg-surface-card shadow-sm ring-1 ring-inset ring-edge-faint">
{swatch}
</span>
<span className="flex min-w-0 flex-col">
<Eyebrow>{label}</Eyebrow>
<span className="truncate font-semibold text-content" style={fs(12, 'body')}>
{name}
</span>
</span>
</li>
);
}
/**
* ACT-02 over the plan map: what its two lines are while a tour is compared
* with its actual track, in the frosted look of the map's cards. Closing it
* ends the comparison; it changes nothing else.
*/
export default function ActivityCompareLegend({ legend }: { legend: Legend }) {
const { t } = useTranslation();
const close = t('tours.activity.compare.hide');
return (
<aside
data-testid="activity-compare-legend"
aria-label={t('tours.activity.compare.title')}
className="pointer-events-auto w-[240px] rounded-2xl p-3"
style={MAP_CARD_GLASS}
>
<div className="mb-2 flex items-center gap-1.5">
<GitCompareArrows size={13} strokeWidth={2} className="flex-none text-content-muted" aria-hidden="true" />
<span className="min-w-0 flex-1 truncate font-semibold text-content" style={fs(12.5, 'body')}>
{t('tours.activity.compare.title')}
</span>
<Tooltip label={close}>
<button
type="button"
onClick={() => useTourActivityCompareStore.getState().clear()}
aria-label={close}
className="grid h-6 w-6 flex-none place-items-center rounded-full text-content-faint transition-colors hover:bg-surface-card hover:text-content focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
>
<X size={13} strokeWidth={2.2} />
</button>
</Tooltip>
</div>
<ul className="m-0 flex list-none flex-col gap-2 p-0">
<LegendRow
swatch={<LineSwatch color={legend.planColor} />}
label={t('tours.activity.compare.plan')}
name={legend.planName}
/>
<LegendRow
swatch={<LineSwatch color={ACTUAL_TRACK_COLOR} dotted />}
label={t('tours.activity.badge')}
name={legend.trackName}
/>
</ul>
</aside>
);
}