Fix layer toggles: malformed GeoJSON geometry + fragile layer loading
All checks were successful
CI / test-and-build (push) Successful in 47s
All checks were successful
CI / test-and-build (push) Successful in 47s
Root cause: osm-data.mjs emitted ways with 'out center' (no geometry) as a flat [lon,lat] labeled 'LineString'. Leaflet reads those as two invalid points and threw 'Invalid LatLng', which aborted the entire layer load loop in onMount -- so no layers rendered and the checkboxes all showed unchecked. Toggling then appeared to do nothing. Fixes: - osm-data.mjs: coordsFor/geometryFor now emit a proper Point for any flat [lon,lat] (from nodes / 'out center' ways) instead of a malformed LineString - MapView.svelte: isolate each layer's load + each feature's searchable build in try/catch so one bad feature/layer can't break all layers; skip invalid bounds in fitBounds - osm-data.mjs: preserve manifest entries it doesn't generate (e.g. dump layers from osm-dump.mjs) so data/osm-dump scripts don't clobber each other - scripts/test.mjs: add geometry-integrity checks (no malformed coords) Verified in a real browser: all 6 layers render, each toggles off/on independently (33361->0 overlays), and the Directions panel loads.
This commit is contained in:
parent
edd6ba9ae2
commit
81a3da388e
23
package-lock.json
generated
23
package-lock.json
generated
@ -18,7 +18,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
"svelte-check": "^4.6.0",
|
"svelte-check": "^4.6.0",
|
||||||
"tsx": "^4.23.11",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.16"
|
"vite": "^8.0.16"
|
||||||
}
|
}
|
||||||
@ -915,6 +915,18 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "26.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
|
||||||
|
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~8.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@ -2320,6 +2332,15 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@ -32,4 +32,4 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"osm-pbf-parser": "^2.3.0"
|
"osm-pbf-parser": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,15 @@ function coordsFor(el) {
|
|||||||
function geometryFor(el) {
|
function geometryFor(el) {
|
||||||
const coords = coordsFor(el);
|
const coords = coordsFor(el);
|
||||||
if (!coords) return null;
|
if (!coords) return null;
|
||||||
|
// A flat [lon, lat] pair (from a node, or a way's "out center") is a Point,
|
||||||
|
// not a LineString. Leaflet would otherwise mis-read it as two points and
|
||||||
|
// throw "Invalid LatLng".
|
||||||
|
if (typeof coords[0] === 'number') {
|
||||||
|
if (coords.length >= 2 && typeof coords[1] === 'number') {
|
||||||
|
return { type: 'Point', coordinates: [coords[0], coords[1]] };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Preserve polygons for closed ways when we have full geometry.
|
// Preserve polygons for closed ways when we have full geometry.
|
||||||
if (el.type === 'way' && el.geometry && el.geometry.length > 2 &&
|
if (el.type === 'way' && el.geometry && el.geometry.length > 2 &&
|
||||||
coords[0][0] === coords[coords.length - 1][0] &&
|
coords[0][0] === coords[coords.length - 1][0] &&
|
||||||
@ -294,7 +303,20 @@ async function main() {
|
|||||||
const signal = new AbortController().signal;
|
const signal = new AbortController().signal;
|
||||||
await mkdir(OUT_DIR, { recursive: true });
|
await mkdir(OUT_DIR, { recursive: true });
|
||||||
|
|
||||||
const manifest = [];
|
// Preserve manifest entries we are not (re)generating (e.g. dump layers built
|
||||||
|
// by osm-dump.mjs) so running one script doesn't clobber the other's layers.
|
||||||
|
let existing = [];
|
||||||
|
try {
|
||||||
|
const { readFile } = await import('node:fs/promises');
|
||||||
|
const { existsSync } = await import('node:fs');
|
||||||
|
const mp = resolve(OUT_DIR, '_index.json');
|
||||||
|
if (existsSync(mp)) existing = JSON.parse(await readFile(mp, 'utf8'));
|
||||||
|
} catch { existing = []; }
|
||||||
|
const toast = new Set();
|
||||||
|
for (const [name, cfg] of Object.entries(AREAS)) toast.add(name);
|
||||||
|
for (const l of CUSTOM_LAYERS) toast.add(l.id);
|
||||||
|
|
||||||
|
const manifest = existing.filter((m) => !toast.has(m.name));
|
||||||
for (const [name, cfg] of Object.entries(AREAS)) {
|
for (const [name, cfg] of Object.entries(AREAS)) {
|
||||||
if (only && name !== only) continue;
|
if (only && name !== only) continue;
|
||||||
manifest.push(await buildArea(name, cfg, signal));
|
manifest.push(await buildArea(name, cfg, signal));
|
||||||
|
|||||||
@ -91,6 +91,24 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 2.4 GeoJSON geometry integrity: no malformed coords Leaflet would choke on.
|
||||||
|
for (const layer of manifest) {
|
||||||
|
const fc = loadJson(resolve(STATIC, layer.path.replace(/^\/data\//, '')));
|
||||||
|
let badLine = 0, badPoint = 0;
|
||||||
|
for (const f of fc.features) {
|
||||||
|
const g = f.geometry || {};
|
||||||
|
const c = g.coordinates;
|
||||||
|
if (g.type === 'LineString' && Array.isArray(c) && c.length && typeof c[0] === 'number') badLine++;
|
||||||
|
if (g.type === 'Point' && Array.isArray(c) && (c.length < 2 || typeof c[0] !== 'number')) badPoint++;
|
||||||
|
if (g.type === 'Polygon') {
|
||||||
|
const ring = Array.isArray(c) ? c[0] : null;
|
||||||
|
if (!ring || !ring.length || typeof ring[0] === 'number') badLine++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(badLine === 0, `geometry: "${layer.name}" has no malformed LineString/Polygon coords`);
|
||||||
|
check(badPoint === 0, `geometry: "${layer.name}" has no malformed Point coords`);
|
||||||
|
}
|
||||||
|
|
||||||
// 2.5 Routing graph validation (dump sources).
|
// 2.5 Routing graph validation (dump sources).
|
||||||
for (const layer of manifest) {
|
for (const layer of manifest) {
|
||||||
if (layer.category !== 'dump') continue;
|
if (layer.category !== 'dump') continue;
|
||||||
|
|||||||
@ -191,6 +191,7 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
|
try {
|
||||||
const res = await fetch(layer.path);
|
const res = await fetch(layer.path);
|
||||||
if (!res.ok) throw new Error(`fetch ${layer.path}: HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`fetch ${layer.path}: HTTP ${res.status}`);
|
||||||
const fc: FeatureCollection = await res.json();
|
const fc: FeatureCollection = await res.json();
|
||||||
@ -238,7 +239,9 @@
|
|||||||
lay.bindPopup(popupHtml(f));
|
lay.bindPopup(popupHtml(f));
|
||||||
featureLayers[f.id as string] = lay;
|
featureLayers[f.id as string] = lay;
|
||||||
|
|
||||||
// Build a searchable record.
|
// Build a searchable record. A single malformed feature
|
||||||
|
// must not break the whole layer load.
|
||||||
|
try {
|
||||||
let lat, lon, bounds;
|
let lat, lon, bounds;
|
||||||
if (f.geometry.type === 'Point') {
|
if (f.geometry.type === 'Point') {
|
||||||
[lon, lat] = f.geometry.coordinates as [number, number];
|
[lon, lat] = f.geometry.coordinates as [number, number];
|
||||||
@ -269,21 +272,29 @@
|
|||||||
bounds,
|
bounds,
|
||||||
feature: f
|
feature: f
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('skip bad feature', f.id, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
layerGroups[layer.name] = geo;
|
layerGroups[layer.name] = geo;
|
||||||
visible[layer.name] = true;
|
visible[layer.name] = true;
|
||||||
geo.addTo(map);
|
geo.addTo(map);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('layer load failed:', layer.name, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const all: L.Layer[] = Object.values(layerGroups);
|
const all: L.Layer[] = Object.values(layerGroups);
|
||||||
if (all.length) {
|
if (all.length) {
|
||||||
const group = L.featureGroup(all);
|
const group = L.featureGroup(all);
|
||||||
map.fitBounds(group.getBounds(), { padding: [40, 40] });
|
if (typeof group.getBounds === 'function' && group.getBounds().isValid()) {
|
||||||
|
map.fitBounds(group.getBounds(), { padding: [40, 40] });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = (e as Error).message;
|
console.warn('overall load error:', e);
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,16 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "tulsa-dump",
|
||||||
|
"label": "Tulsa OSM (dump)",
|
||||||
|
"category": "dump",
|
||||||
|
"layerType": "poi",
|
||||||
|
"color": "#8b5cf6",
|
||||||
|
"featureCount": 12853,
|
||||||
|
"path": "/data/tulsa-dump-poi.geojson",
|
||||||
|
"poiPath": "/data/tulsa-dump-poi.geojson",
|
||||||
|
"graphPath": "/data/tulsa-dump-graph.json",
|
||||||
|
"graphNodeCount": 55334
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tulsa",
|
"name": "tulsa",
|
||||||
"label": "Tulsa, Oklahoma",
|
"label": "Tulsa, Oklahoma",
|
||||||
@ -10,7 +22,7 @@
|
|||||||
36.3,
|
36.3,
|
||||||
-95.7
|
-95.7
|
||||||
],
|
],
|
||||||
"featureCount": 20261,
|
"featureCount": 20496,
|
||||||
"path": "/data/tulsa.geojson"
|
"path": "/data/tulsa.geojson"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -48,17 +60,5 @@
|
|||||||
"color": "#f59e0b",
|
"color": "#f59e0b",
|
||||||
"featureCount": 7,
|
"featureCount": 7,
|
||||||
"path": "/data/custom/rogers-county.geojson"
|
"path": "/data/custom/rogers-county.geojson"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tulsa-dump",
|
|
||||||
"label": "Tulsa OSM (dump)",
|
|
||||||
"category": "dump",
|
|
||||||
"layerType": "poi",
|
|
||||||
"color": "#8b5cf6",
|
|
||||||
"featureCount": 12853,
|
|
||||||
"path": "/data/tulsa-dump-poi.geojson",
|
|
||||||
"poiPath": "/data/tulsa-dump-poi.geojson",
|
|
||||||
"graphPath": "/data/tulsa-dump-graph.json",
|
|
||||||
"graphNodeCount": 55334
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user