This commit is contained in:
Andras Schmelczer 2026-02-10 22:21:15 +00:00
parent 1f68ca0512
commit 3599803589
43 changed files with 3578 additions and 262 deletions

View file

@ -1,62 +1,138 @@
import type { FeatureMeta, FeatureFilters, ViewState } from '../types';
import type { TransportMode, 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;
}
/** Backward compat: parse old comma-packed `f` param */
function parseLegacyFilters(f: string): FeatureFilters | undefined {
const filters: FeatureFilters = {};
for (const segment of f.split(',')) {
const colonIdx = segment.indexOf(':');
if (colonIdx === -1) continue;
const name = segment.substring(0, colonIdx);
const rest = segment.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?: 'pois' | 'properties' | 'area';
travelTime?: TravelTimeInitial;
} {
const params = new URLSearchParams(window.location.search);
const result: ReturnType<typeof parseUrlState> = {};
const v = params.get('v');
if (v) {
const parts = v.split(',').map(Number);
if (parts.length === 3 && parts.every((n) => !isNaN(n))) {
result.viewState = {
latitude: parts[0],
longitude: parts[1],
zoom: parts[2],
pitch: 0,
};
// 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 };
}
}
const f = params.get('f');
if (f) {
const filters: FeatureFilters = {};
for (const segment of f.split(',')) {
const colonIdx = segment.indexOf(':');
if (colonIdx === -1) continue;
const name = segment.substring(0, colonIdx);
const rest = segment.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];
} else {
// Backward compat: old packed `v=lat,lon,zoom`
const v = params.get('v');
if (v) {
const parts = v.split(',').map(Number);
if (parts.length === 3 && parts.every((n) => !isNaN(n))) {
result.viewState = { latitude: parts[0], longitude: parts[1], zoom: parts[2], pitch: 0 };
}
}
if (Object.keys(filters).length > 0) {
result.filters = filters;
}
// Filters: repeated `filter` params
result.filters = parseFilters(params);
if (!result.filters) {
// Backward compat: old packed `f` param
const f = params.get('f');
if (f) result.filters = parseLegacyFilters(f);
}
// POI categories: repeated `poi` params
const poiParams = params.getAll('poi');
if (poiParams.length > 0) {
// Handle both new (repeated params) and old (comma-separated) formats
const categories = poiParams.flatMap((p) => p.split(',')).filter(Boolean);
if (categories.length > 0) {
result.poiCategories = new Set(categories);
}
}
const poi = params.get('poi');
if (poi) {
result.poiCategories = new Set(poi.split(',').filter(Boolean));
// Tab: full name
const tab = params.get('tab');
if (tab === 'properties' || tab === 'pois' || tab === 'area') {
result.tab = tab;
} else if (tab === 'p') {
result.tab = 'properties'; // backward compat
} else if (tab === 'o') {
result.tab = 'pois';
} else if (tab === 'a') {
result.tab = 'area';
}
const tab = params.get('tab');
if (tab === 'p') result.tab = 'properties';
else if (tab === 'o') result.tab = 'pois';
else if (tab === 'a') result.tab = 'area';
// Travel time
const dest = params.get('dest');
if (dest) {
const parts = dest.split(',').map(Number);
if (parts.length === 2 && parts.every((n) => !isNaN(n))) {
const tt: TravelTimeInitial = {
destination: [parts[0], parts[1]],
destinationLabel: params.get('destLabel') || '',
mode: (params.get('tmode') as TransportMode) || 'transit',
};
const ttRange = params.get('tt');
if (ttRange) {
const [min, max] = ttRange.split(':').map(Number);
if (!isNaN(min) && !isNaN(max)) {
tt.timeRange = [min, max];
}
}
result.travelTime = tt;
}
}
return result;
}
@ -66,40 +142,48 @@ export function stateToParams(
filters: FeatureFilters,
features: FeatureMeta[],
selectedPOICategories: Set<string>,
rightPaneTab: 'pois' | 'properties' | 'area'
rightPaneTab: 'pois' | 'properties' | 'area',
travelTime?: { enabled: boolean; destination: [number, number] | null; destinationLabel: string; mode: string; timeRange: [number, number] | null }
): URLSearchParams {
const params = new URLSearchParams();
if (viewState) {
params.set(
'v',
`${viewState.latitude.toFixed(4)},${viewState.longitude.toFixed(4)},${viewState.zoom.toFixed(1)}`
);
params.set('lat', viewState.latitude.toFixed(4));
params.set('lon', viewState.longitude.toFixed(4));
params.set('zoom', viewState.zoom.toFixed(1));
}
const filterEntries = Object.entries(filters);
if (filterEntries.length > 0) {
const filtersStr = filterEntries
.map(([name, value]) => {
const meta = features.find((f) => f.name === name);
if (meta?.type === 'enum') {
return `${name}:${(value as string[]).join('|')}`;
}
const [min, max] = value as [number, number];
return `${name}:${min}:${max}`;
})
.join(',');
params.set('f', filtersStr);
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}`);
}
}
if (selectedPOICategories.size > 0) {
params.set('poi', Array.from(selectedPOICategories).join(','));
for (const category of selectedPOICategories) {
params.append('poi', category);
}
if (rightPaneTab === 'properties') {
params.set('tab', 'p');
params.set('tab', 'properties');
} else if (rightPaneTab === 'area') {
params.set('tab', 'a');
params.set('tab', 'area');
}
if (travelTime?.enabled && travelTime.destination) {
params.set('dest', `${travelTime.destination[0].toFixed(5)},${travelTime.destination[1].toFixed(5)}`);
if (travelTime.destinationLabel) {
params.set('destLabel', travelTime.destinationLabel);
}
if (travelTime.mode !== 'transit') {
params.set('tmode', travelTime.mode);
}
if (travelTime.timeRange) {
params.set('tt', `${travelTime.timeRange[0]}:${travelTime.timeRange[1]}`);
}
}
return params;
@ -109,13 +193,13 @@ export function summarizeParams(queryString: string): string {
const params = new URLSearchParams(queryString);
const parts: string[] = [];
const f = params.get('f');
if (f) {
const filterNames = f
.split(',')
.map((seg) => {
const colonIdx = seg.indexOf(':');
return colonIdx > 0 ? seg.substring(0, colonIdx) : seg;
// New format: repeated `filter` params
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) {
@ -123,11 +207,28 @@ export function summarizeParams(queryString: string): string {
filterNames.length <= 2 ? filterNames.join(', ') : `${filterNames.length} filters`
);
}
} else {
// Backward compat: old packed `f` param
const f = params.get('f');
if (f) {
const filterNames = f
.split(',')
.map((seg) => {
const colonIdx = seg.indexOf(':');
return colonIdx > 0 ? seg.substring(0, colonIdx) : seg;
})
.filter(Boolean);
if (filterNames.length > 0) {
parts.push(
filterNames.length <= 2 ? filterNames.join(', ') : `${filterNames.length} filters`
);
}
}
}
const poi = params.get('poi');
if (poi) {
const count = poi.split(',').filter(Boolean).length;
const poiParams = params.getAll('poi');
if (poiParams.length > 0) {
const count = poiParams.flatMap((p) => p.split(',')).filter(Boolean).length;
if (count > 0) {
parts.push(`${count} POI ${count === 1 ? 'category' : 'categories'}`);
}