183 lines
5.5 KiB
TypeScript
183 lines
5.5 KiB
TypeScript
import type { FeatureMeta, FeatureFilters, ViewState } from '../types';
|
|
import {
|
|
TRANSPORT_MODES,
|
|
type TransportMode,
|
|
type TravelTimeEntry,
|
|
type TravelTimeInitial,
|
|
} from '../hooks/useTravelTime';
|
|
|
|
function parseFilters(params: URLSearchParams): FeatureFilters | undefined {
|
|
const filterParams = params.getAll('filter');
|
|
if (filterParams.length === 0) return undefined;
|
|
|
|
const filters: FeatureFilters = {};
|
|
for (const entry of filterParams) {
|
|
const colonIdx = entry.indexOf(':');
|
|
if (colonIdx === -1) continue;
|
|
const name = entry.substring(0, colonIdx);
|
|
const rest = entry.substring(colonIdx + 1);
|
|
if (rest.includes(':')) {
|
|
const [minStr, maxStr] = rest.split(':');
|
|
const min = Number(minStr);
|
|
const max = Number(maxStr);
|
|
if (!isNaN(min) && !isNaN(max)) {
|
|
filters[name] = [min, max];
|
|
}
|
|
} else if (rest.includes('|')) {
|
|
filters[name] = rest.split('|');
|
|
} else {
|
|
filters[name] = [rest];
|
|
}
|
|
}
|
|
return Object.keys(filters).length > 0 ? filters : undefined;
|
|
}
|
|
|
|
export function parseUrlState(): {
|
|
viewState?: ViewState;
|
|
filters?: FeatureFilters;
|
|
poiCategories?: Set<string>;
|
|
tab?: 'properties' | 'area';
|
|
travelTime?: TravelTimeInitial;
|
|
} {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const result: ReturnType<typeof parseUrlState> = {};
|
|
|
|
// View state: separate lat/lon/zoom params
|
|
const lat = params.get('lat');
|
|
const lon = params.get('lon');
|
|
const zoom = params.get('zoom');
|
|
if (lat && lon && zoom) {
|
|
const latN = Number(lat);
|
|
const lonN = Number(lon);
|
|
const zoomN = Number(zoom);
|
|
if (!isNaN(latN) && !isNaN(lonN) && !isNaN(zoomN)) {
|
|
result.viewState = { latitude: latN, longitude: lonN, zoom: zoomN, pitch: 0 };
|
|
}
|
|
}
|
|
|
|
// Filters: repeated `filter` params
|
|
result.filters = parseFilters(params);
|
|
|
|
// POI categories: repeated `poi` params
|
|
const poiParams = params.getAll('poi');
|
|
if (poiParams.length > 0) {
|
|
result.poiCategories = new Set(poiParams.filter(Boolean));
|
|
}
|
|
|
|
// Tab: full name
|
|
const tab = params.get('tab');
|
|
if (tab === 'properties' || tab === 'area') {
|
|
result.tab = tab;
|
|
}
|
|
|
|
// Travel time: repeated `tt` params
|
|
// Format: mode:slug:label or mode:slug:label:b or mode:slug:label:min:max or mode:slug:label:b:min:max
|
|
const ttParams = params.getAll('tt');
|
|
if (ttParams.length > 0) {
|
|
const entries: TravelTimeEntry[] = [];
|
|
for (const tt of ttParams) {
|
|
const parts = tt.split(':');
|
|
if (parts.length < 3) continue;
|
|
const mode = parts[0] as TransportMode;
|
|
if (!TRANSPORT_MODES.includes(mode)) continue;
|
|
const slug = parts[1];
|
|
const label = decodeURIComponent(parts[2]);
|
|
const useBest = parts.length >= 4 && parts[3] === 'b';
|
|
const rangeOffset = useBest ? 1 : 0;
|
|
let timeRange: [number, number] | null = null;
|
|
if (parts.length >= 5 + rangeOffset) {
|
|
const min = Number(parts[3 + rangeOffset]);
|
|
const max = Number(parts[4 + rangeOffset]);
|
|
if (!isNaN(min) && !isNaN(max)) {
|
|
timeRange = [min, max];
|
|
}
|
|
}
|
|
entries.push({ mode, slug, label, timeRange, useBest });
|
|
}
|
|
if (entries.length > 0) {
|
|
result.travelTime = { entries };
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function stateToParams(
|
|
viewState: { latitude: number; longitude: number; zoom: number } | null,
|
|
filters: FeatureFilters,
|
|
features: FeatureMeta[],
|
|
selectedPOICategories: Set<string>,
|
|
rightPaneTab: 'properties' | 'area',
|
|
travelTimeEntries?: TravelTimeEntry[]
|
|
): URLSearchParams {
|
|
const params = new URLSearchParams();
|
|
|
|
if (viewState) {
|
|
params.set('lat', viewState.latitude.toFixed(4));
|
|
params.set('lon', viewState.longitude.toFixed(4));
|
|
params.set('zoom', viewState.zoom.toFixed(1));
|
|
}
|
|
|
|
for (const [name, value] of Object.entries(filters)) {
|
|
const meta = features.find((f) => f.name === name);
|
|
if (meta?.type === 'enum') {
|
|
params.append('filter', `${name}:${(value as string[]).join('|')}`);
|
|
} else {
|
|
const [min, max] = value as [number, number];
|
|
params.append('filter', `${name}:${min}:${max}`);
|
|
}
|
|
}
|
|
|
|
for (const category of selectedPOICategories) {
|
|
params.append('poi', category);
|
|
}
|
|
|
|
if (rightPaneTab === 'properties') {
|
|
params.set('tab', 'properties');
|
|
}
|
|
|
|
// Travel time: repeated `tt` params
|
|
if (travelTimeEntries) {
|
|
for (const entry of travelTimeEntries) {
|
|
if (!entry.slug) continue;
|
|
let val = `${entry.mode}:${entry.slug}:${encodeURIComponent(entry.label)}`;
|
|
if (entry.useBest) val += ':b';
|
|
if (entry.timeRange) {
|
|
val += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
|
|
}
|
|
params.append('tt', val);
|
|
}
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
export function summarizeParams(queryString: string): string {
|
|
const params = new URLSearchParams(queryString);
|
|
const parts: string[] = [];
|
|
|
|
const filterParams = params.getAll('filter');
|
|
if (filterParams.length > 0) {
|
|
const filterNames = filterParams
|
|
.map((entry) => {
|
|
const colonIdx = entry.indexOf(':');
|
|
return colonIdx > 0 ? entry.substring(0, colonIdx) : entry;
|
|
})
|
|
.filter(Boolean);
|
|
if (filterNames.length > 0) {
|
|
parts.push(
|
|
filterNames.length <= 2 ? filterNames.join(', ') : `${filterNames.length} filters`
|
|
);
|
|
}
|
|
}
|
|
|
|
const poiParams = params.getAll('poi');
|
|
if (poiParams.length > 0) {
|
|
const count = poiParams.filter(Boolean).length;
|
|
if (count > 0) {
|
|
parts.push(`${count} POI ${count === 1 ? 'category' : 'categories'}`);
|
|
}
|
|
}
|
|
|
|
return parts.length > 0 ? parts.join(' + ') : 'No filters';
|
|
}
|