swissBUILDINGS3D-Import: Absturz bei riesigen Kacheln behoben, Übersprungene sichtbar gemacht

Nutzer-Report „funktioniert nicht": Der Import stürzte mit einem harten
RangeError ab, sobald eine STAC-Kachel entpackt die maximale JS-String-Länge
überschritt (DXF komprimiert stark — eine Kachel unter dem 150-MB-Limit kann
trotzdem >700 MB unkomprimierten Text ergeben, real reproduziert für ein
dicht bebautes Stadtzentrum). downloadAssetText prüft jetzt zusätzlich die
JSZip-interne unkomprimierte Grössenschätzung VOR dem Entpacken und fängt
verbleibende Fehler (Netzwerk/ZIP/String-Länge) sicher ab, statt zu werfen.

Zu grosse/fehlgeschlagene Kacheln wurden bisher stumm übersprungen (0 Gebäude,
keine Erklärung — sah wie ein Bug aus). fetchBuildings3d liefert jetzt
skippedTiles mit; der Dialog zeigt „X Kachel(n) übersprungen, zu gross" statt
eines wortlosen Leer-Ergebnisses.
This commit is contained in:
2026-07-12 19:15:45 +02:00
parent 7dc8f0d5c6
commit 35a6834072
5 changed files with 85 additions and 24 deletions
+36 -13
View File
@@ -75,26 +75,49 @@ export function pickAsset(item: StacItem, priority: string[]): StacAsset | null
return pool[0] ?? null;
}
/** Grobe jszip-interne Schätzung der UNKOMPRIMIERTEN Grösse eines Zip-Eintrags
* (Bytes), falls verfügbar — `_data.uncompressedSize` ist kein offiziell
* typisiertes API, aber ein stabiles, verbreitet genutztes internes Feld
* (siehe `jszip/lib/compressedObject.js`). `null`, wenn nicht auslesbar. */
function estimateUncompressedSize(entry: JSZip.JSZipObject): number | null {
const data = (entry as unknown as { _data?: { uncompressedSize?: number } })._data;
return typeof data?.uncompressedSize === "number" ? data.uncompressedSize : null;
}
/** Lädt ein Asset als Text; entpackt automatisch, wenn es ein `.zip` ist
* (liefert den Inhalt der ERSTEN enthaltenen Datei mit einer der `innerExt`-
* Endungen). `null` bei Fehler, fehlendem Inhalt im ZIP, oder wenn das Asset
* {@link MAX_ASSET_BYTES} überschreitet. */
* {@link MAX_ASSET_BYTES} überschreitet (komprimiert ODER — DXF/Text
* komprimiert stark — unkomprimiert, s. u.). Wirft NIE (fängt Netzwerk-/
* ZIP-/String-Längen-Fehler ab und liefert `null`), damit eine einzelne
* kaputte/überdimensionierte Kachel nicht den ganzen Import abbricht. */
export async function downloadAssetText(
asset: StacAsset,
innerExt: string[],
): Promise<string | null> {
if (asset.size != null && asset.size > MAX_ASSET_BYTES) return null;
const res = await fetch(viaProxy(asset.href));
if (!res.ok) return null;
if (asset.href.toLowerCase().endsWith(".zip")) {
const buf = await res.arrayBuffer();
if (buf.byteLength > MAX_ASSET_BYTES) return null;
const zip = await JSZip.loadAsync(buf);
const entry = Object.values(zip.files).find(
(f) => !f.dir && innerExt.some((ext) => f.name.toLowerCase().endsWith(ext)),
);
if (!entry) return null;
return entry.async("text");
try {
const res = await fetch(viaProxy(asset.href));
if (!res.ok) return null;
if (asset.href.toLowerCase().endsWith(".zip")) {
const buf = await res.arrayBuffer();
if (buf.byteLength > MAX_ASSET_BYTES) return null;
const zip = await JSZip.loadAsync(buf);
const entry = Object.values(zip.files).find(
(f) => !f.dir && innerExt.some((ext) => f.name.toLowerCase().endsWith(ext)),
);
if (!entry) return null;
// Proaktiv prüfen: DXF/Text komprimiert stark, ein Zip unter dem Limit
// kann trotzdem eine Text-Datei enthalten, die die max. JS-String-Länge
// sprengt (führte zu einem harten Absturz statt einem übersprungenen
// Tile). Bekannte, aber unschätzbare Grösse (`null`) lässt es zu — der
// äussere try/catch fängt den Fall dann trotzdem sicher ab.
const uncompressed = estimateUncompressedSize(entry);
if (uncompressed != null && uncompressed > MAX_ASSET_BYTES) return null;
return await entry.async("text");
}
return await res.text();
} catch {
return null;
}
return res.text();
}
+25 -6
View File
@@ -61,6 +61,14 @@ export interface FetchBuildings3dResult {
meshes: ImportedMesh[];
/** Tatsächlich gelieferte Version (nach evtl. v3→v2-Fallback). */
versionUsed: BuildingsVersion;
/**
* Anzahl STAC-Kacheln, die NICHT geladen werden konnten (zu gross — DXF
* komprimiert stark, eine Kachel unter {@link MAX_ASSET_BYTES} kann
* entpackt trotzdem die max. Textlänge sprengen —, Netzwerkfehler oder
* fehlender Inhalt). Dient der UI, „0 Gebäude" von „X Kacheln übersprungen,
* zu gross" zu unterscheiden statt eines stummen Leer-Ergebnisses.
*/
skippedTiles: number;
}
/**
@@ -82,24 +90,35 @@ export async function fetchBuildings3d(
const origin = originIn ?? makeOrigin(center);
const bboxWgs = lv95BboxToWgs84(bboxAround(center, radius));
const tryCollection = async (collectionId: string): Promise<ImportedMesh[]> => {
const tryCollection = async (
collectionId: string,
): Promise<{ meshes: ImportedMesh[]; skipped: number }> => {
const items = await stacQuery(collectionId, bboxWgs, ASSET_PRIORITY);
const meshes: ImportedMesh[] = [];
let skipped = 0;
for (const item of items) {
const asset = pickAsset(item, ASSET_PRIORITY);
if (!asset) continue;
if (!asset) {
skipped++;
continue;
}
const text = await downloadAssetText(asset, [".dxf"]);
if (!text) continue;
if (!text) {
skipped++;
continue;
}
const parsed = parseDxf(text);
for (const m of parsed.meshes) meshes.push(shiftMeshToOrigin(m, origin, item.id));
}
return meshes;
return { meshes, skipped };
};
if (version === "v3") {
const v3 = await tryCollection(COLLECTION_V3);
if (v3.length > 0) return { origin, meshes: v3, versionUsed: "v3" };
if (v3.meshes.length > 0) {
return { origin, meshes: v3.meshes, versionUsed: "v3", skippedTiles: v3.skipped };
}
}
const v2 = await tryCollection(COLLECTION_V2);
return { origin, meshes: v2, versionUsed: "v2" };
return { origin, meshes: v2.meshes, versionUsed: "v2", skippedTiles: v2.skipped };
}