Small changes

This commit is contained in:
Andras Schmelczer 2026-03-12 22:11:33 +00:00
parent eae78df3ca
commit 593f380581
7 changed files with 48 additions and 48 deletions

View file

@ -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>

View file

@ -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>

View file

@ -157,7 +157,6 @@ export default function MapPage({
features,
viewFeature,
activeFeature,
dragValue,
travelTimeEntries: travelTime.entries,
});

View file

@ -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[];

View file

@ -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) => {

View file

@ -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(
({