More
This commit is contained in:
parent
1f68ca0512
commit
3599803589
43 changed files with 3578 additions and 262 deletions
|
|
@ -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'}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue