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:
+36
-13
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user