Small changes
This commit is contained in:
parent
eae78df3ca
commit
593f380581
7 changed files with 48 additions and 48 deletions
|
|
@ -46,16 +46,22 @@ const ROUTE_COLORS: Record<string, { color: string; darkText?: boolean }> = {
|
|||
|
||||
const NON_TUBE_NAMES = new Set(['DLR', 'London Overground', 'Elizabeth line']);
|
||||
|
||||
/** Strip trailing parenthesized GTFS route IDs and NaPTAN stop codes (e.g. "(6757261)", "(9400ZZLUCGT1)") */
|
||||
function stripId(label: string): string {
|
||||
return label.replace(/\s+\([A-Za-z0-9]+\)$/, '');
|
||||
}
|
||||
|
||||
function getRouteDisplay(mode: string): { label: string; color: string; darkText: boolean } {
|
||||
const known = ROUTE_COLORS[mode];
|
||||
const clean = stripId(mode);
|
||||
const known = ROUTE_COLORS[clean];
|
||||
if (known) {
|
||||
const label = NON_TUBE_NAMES.has(mode) || mode.includes('line') ? mode : `${mode} line`;
|
||||
const label = NON_TUBE_NAMES.has(clean) || clean.includes('line') ? clean : `${clean} line`;
|
||||
return { label, color: known.color, darkText: !!known.darkText };
|
||||
}
|
||||
if (/^\d+[A-Za-z]?$/.test(mode.trim())) {
|
||||
return { label: `Bus ${mode}`, color: '#0d9488', darkText: false };
|
||||
if (/^\d+[A-Za-z]?$/.test(clean.trim())) {
|
||||
return { label: `Bus ${clean}`, color: '#0d9488', darkText: false };
|
||||
}
|
||||
return { label: mode, color: '#6b7280', darkText: false };
|
||||
return { label: clean, color: '#6b7280', darkText: false };
|
||||
}
|
||||
|
||||
function invertLegs(legs: JourneyLeg[]): JourneyLeg[] {
|
||||
|
|
@ -120,8 +126,8 @@ function TimelineLeg({ leg, isLast }: { leg: JourneyLeg; isLast: boolean }) {
|
|||
<span className="text-[11px] text-warm-500 dark:text-warm-400">{leg.minutes} min</span>
|
||||
</div>
|
||||
{leg.from && leg.to && (
|
||||
<div className="text-[11px] text-warm-600 dark:text-warm-300 mt-0.5 truncate">
|
||||
{leg.from} → {leg.to}
|
||||
<div className="text-[11px] text-warm-600 dark:text-warm-300 mt-0.5">
|
||||
{stripId(leg.from)} → {stripId(leg.to)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import LocationSearch, { type SearchedLocation } from './LocationSearch';
|
|||
import MapLegend from './MapLegend';
|
||||
import HoverCard from './HoverCard';
|
||||
import type { FeatureFilters } from '../../types';
|
||||
import { useDeckLayers, osmIdToUrl } from '../../hooks/useDeckLayers';
|
||||
import { useDeckLayers } from '../../hooks/useDeckLayers';
|
||||
import { MODE_LABELS, type TravelTimeEntry } from '../../hooks/useTravelTime';
|
||||
|
||||
interface MapProps {
|
||||
|
|
@ -295,16 +295,6 @@ export default memo(function Map({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{osmIdToUrl(popupInfo.id) && (
|
||||
<a
|
||||
href={osmIdToUrl(popupInfo.id)!}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 text-xs mt-1 block pointer-events-auto"
|
||||
>
|
||||
View on OSM
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ export default function MapPage({
|
|||
features,
|
||||
viewFeature,
|
||||
activeFeature,
|
||||
dragValue,
|
||||
travelTimeEntries: travelTime.entries,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -30,15 +30,6 @@ import {
|
|||
} from './useTravelTime';
|
||||
import { MarchingAntsExtension } from '../lib/MarchingAntsExtension';
|
||||
|
||||
/** Convert POI id (e.g. "n12345") to OpenStreetMap URL */
|
||||
function osmIdToUrl(id: string): string | null {
|
||||
const match = id.match(/^([nwr])(\d+)$/);
|
||||
if (!match) return null;
|
||||
const typeMap: Record<string, string> = { n: 'node', w: 'way', r: 'relation' };
|
||||
return `https://www.openstreetmap.org/${typeMap[match[1]]}/${match[2]}`;
|
||||
}
|
||||
|
||||
export { osmIdToUrl };
|
||||
|
||||
interface UseDeckLayersProps {
|
||||
data: HexagonData[];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import type { FeatureMeta, FeatureFilters } from '../types';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
|
||||
|
|
@ -12,6 +12,9 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
const [activeFeature, setActiveFeature] = useState<string | null>(null);
|
||||
const [dragValue, setDragValue] = useState<[number, number] | null>(null);
|
||||
const [pinnedFeature, setPinnedFeature] = useState<string | null>(null);
|
||||
const pendingDragRef = useRef<string | null>(null);
|
||||
const dragActiveRef = useRef<string | null>(null);
|
||||
const dragValueRef = useRef<[number, number] | null>(null);
|
||||
|
||||
const enabledFeatures = useMemo(() => new Set(Object.keys(filters)), [filters]);
|
||||
|
||||
|
|
@ -60,30 +63,46 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
(name: string) => {
|
||||
const meta = features.find((f) => f.name === name);
|
||||
if (meta?.type === 'enum') return;
|
||||
setActiveFeature(name);
|
||||
const fval = filters[name];
|
||||
setDragValue(fval && !Array.isArray(fval[0]) ? (fval as [number, number]) : null);
|
||||
pendingDragRef.current = name;
|
||||
},
|
||||
[filters, features]
|
||||
[features]
|
||||
);
|
||||
|
||||
const handleDragChange = useCallback((value: [number, number]) => {
|
||||
if (pendingDragRef.current) {
|
||||
setActiveFeature(pendingDragRef.current);
|
||||
dragActiveRef.current = pendingDragRef.current;
|
||||
pendingDragRef.current = null;
|
||||
}
|
||||
setDragValue(value);
|
||||
dragValueRef.current = value;
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
if (activeFeature && dragValue) {
|
||||
setFilters((prev) => ({ ...prev, [activeFeature]: dragValue }));
|
||||
if (pendingDragRef.current) {
|
||||
// Click without drag — no state was changed, just clear the ref
|
||||
pendingDragRef.current = null;
|
||||
return;
|
||||
}
|
||||
const af = dragActiveRef.current;
|
||||
const dv = dragValueRef.current;
|
||||
if (af && dv) {
|
||||
setFilters((prev) => ({ ...prev, [af]: dv }));
|
||||
}
|
||||
setActiveFeature(null);
|
||||
setDragValue(null);
|
||||
}, [activeFeature, dragValue]);
|
||||
dragActiveRef.current = null;
|
||||
dragValueRef.current = null;
|
||||
}, []);
|
||||
|
||||
const handleSetFilters = useCallback((newFilters: FeatureFilters) => {
|
||||
setFilters(newFilters);
|
||||
setActiveFeature(null);
|
||||
setDragValue(null);
|
||||
setPinnedFeature(null);
|
||||
pendingDragRef.current = null;
|
||||
dragActiveRef.current = null;
|
||||
dragValueRef.current = null;
|
||||
}, []);
|
||||
|
||||
const handleTogglePin = useCallback((name: string) => {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ interface UseMapDataOptions {
|
|||
features: FeatureMeta[];
|
||||
viewFeature: string | null;
|
||||
activeFeature: string | null;
|
||||
dragValue: [number, number] | null;
|
||||
travelTimeEntries: TravelTimeEntry[];
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +39,6 @@ export function useMapData({
|
|||
features,
|
||||
viewFeature,
|
||||
activeFeature,
|
||||
dragValue,
|
||||
travelTimeEntries,
|
||||
}: UseMapDataOptions) {
|
||||
const [rawData, setRawData] = useState<HexagonData[]>([]);
|
||||
|
|
@ -75,17 +73,15 @@ export function useMapData({
|
|||
[filters, features]
|
||||
);
|
||||
|
||||
// Build the travel param string from entries with destinations
|
||||
// Format: mode:slug|mode:slug:best or mode:slug:min:max|mode:slug:best:min:max
|
||||
// Build the travel param string from entries with destinations.
|
||||
// timeRange is NOT included — range filtering is handled purely client-side
|
||||
// (dimming in useDeckLayers) so slider changes never trigger server refetches.
|
||||
const travelParam = useMemo((): string => {
|
||||
const segments: string[] = [];
|
||||
for (const entry of travelTimeEntries) {
|
||||
if (!entry.slug) continue;
|
||||
let seg = `${entry.mode}:${entry.slug}`;
|
||||
if (entry.useBest) seg += ':best';
|
||||
if (entry.timeRange) {
|
||||
seg += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
|
||||
}
|
||||
segments.push(seg);
|
||||
}
|
||||
return segments.join('|');
|
||||
|
|
@ -259,7 +255,6 @@ export function useMapData({
|
|||
if (!isTravelTime) {
|
||||
const meta = features.find((f) => f.name === viewFeature);
|
||||
if (!meta || meta.type === 'enum') return null;
|
||||
if (activeFeature && !dragHexData && !dragPostcodeData) return null;
|
||||
}
|
||||
|
||||
const vals: number[] = [];
|
||||
|
|
@ -294,7 +289,7 @@ export function useMapData({
|
|||
percentile(vals, COLOR_RANGE_LOW_PERCENTILE),
|
||||
percentile(vals, COLOR_RANGE_HIGH_PERCENTILE),
|
||||
];
|
||||
}, [viewFeature, data, dragHexData, dragPostcodeData, effectivePostcodeData, usePostcodeView, features, activeFeature, bounds]);
|
||||
}, [viewFeature, data, effectivePostcodeData, usePostcodeView, features, bounds]);
|
||||
|
||||
// Color range for the legend and hex coloring
|
||||
const colorRange = useMemo((): [number, number] | null => {
|
||||
|
|
@ -311,10 +306,9 @@ export function useMapData({
|
|||
return [0, meta.values.length - 1];
|
||||
}
|
||||
if (dataRange) return dataRange;
|
||||
if (activeFeature && dragValue) return dragValue;
|
||||
if (meta.min != null && meta.max != null) return [meta.min, meta.max];
|
||||
return null;
|
||||
}, [viewFeature, features, dataRange, activeFeature, dragValue]);
|
||||
}, [viewFeature, features, dataRange]);
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue