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:
@@ -1306,6 +1306,8 @@ export const de = {
|
||||
"ctxImport.empty": "Keine Geometrie im gewählten Bereich gefunden.",
|
||||
"ctxImport.importError": "Import fehlgeschlagen.",
|
||||
"ctxImport.imported": "{count} Objekte in {sets} Sätzen importiert.",
|
||||
"ctxImport.importedWithSkipped": "{count} Objekte in {sets} Sätzen importiert ({skipped} Gebäude-Kachel(n) übersprungen, zu gross).",
|
||||
"ctxImport.buildingsSkipped": "Keine Gebäude geladen — {count} Kachel(n) waren zu gross (dicht bebautes Gebiet). Kleineren Radius versuchen oder Generation wechseln.",
|
||||
|
||||
// ── Raum-Befehl ─────────────────────────────────────────────────────────────
|
||||
"cmd.room.label": "Raum",
|
||||
|
||||
@@ -1294,6 +1294,8 @@ export const en: Record<TranslationKey, string> = {
|
||||
"ctxImport.empty": "No geometry found in the selected area.",
|
||||
"ctxImport.importError": "Import failed.",
|
||||
"ctxImport.imported": "Imported {count} objects in {sets} sets.",
|
||||
"ctxImport.importedWithSkipped": "Imported {count} objects in {sets} sets ({skipped} building tile(s) skipped, too large).",
|
||||
"ctxImport.buildingsSkipped": "No buildings loaded — {count} tile(s) were too large (dense built-up area). Try a smaller radius or switch generation.",
|
||||
|
||||
// Room command.
|
||||
"cmd.room.label": "Room",
|
||||
|
||||
+26
-3
@@ -75,15 +75,28 @@ 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;
|
||||
try {
|
||||
const res = await fetch(viaProxy(asset.href));
|
||||
if (!res.ok) return null;
|
||||
if (asset.href.toLowerCase().endsWith(".zip")) {
|
||||
@@ -94,7 +107,17 @@ export async function downloadAssetText(
|
||||
(f) => !f.dir && innerExt.some((ext) => f.name.toLowerCase().endsWith(ext)),
|
||||
);
|
||||
if (!entry) return null;
|
||||
return entry.async("text");
|
||||
// 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 };
|
||||
}
|
||||
|
||||
@@ -135,6 +135,10 @@ export function ContextImportDialog({
|
||||
// Echte swissBUILDINGS3D-Meshes (v2/v3) landen direkt als ContextObject
|
||||
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
||||
const extraObjs: ContextObject[] = [];
|
||||
// Übersprungene Kacheln (zu gross entpackt, Netzwerkfehler o. Ä.) —
|
||||
// damit „0 Gebäude" von „X Kacheln übersprungen" unterscheidbar bleibt
|
||||
// statt eines stummen Leer-Ergebnisses (Nutzer-Report).
|
||||
let buildingsSkipped = 0;
|
||||
|
||||
if (sources.swissBuildings === "flach") {
|
||||
const r = await fetchBuildings(center, radius, origin);
|
||||
@@ -149,6 +153,7 @@ export function ContextImportDialog({
|
||||
);
|
||||
origin = r.origin;
|
||||
extraObjs.push(...r.meshes);
|
||||
buildingsSkipped = r.skippedTiles;
|
||||
}
|
||||
|
||||
const osmSel: OsmSelection = {
|
||||
@@ -186,7 +191,11 @@ export function ContextImportDialog({
|
||||
}
|
||||
|
||||
if (all.length === 0 && !terrain && extraObjs.length === 0) {
|
||||
setError(t("ctxImport.empty"));
|
||||
setError(
|
||||
buildingsSkipped > 0
|
||||
? t("ctxImport.buildingsSkipped", { count: buildingsSkipped })
|
||||
: t("ctxImport.empty"),
|
||||
);
|
||||
setStatus(null);
|
||||
return;
|
||||
}
|
||||
@@ -196,7 +205,13 @@ export function ContextImportDialog({
|
||||
if (terrain) objs.push(terrain);
|
||||
onImport(objs);
|
||||
setStatus(
|
||||
t("ctxImport.imported", {
|
||||
buildingsSkipped > 0
|
||||
? t("ctxImport.importedWithSkipped", {
|
||||
count: all.length + extraObjs.length,
|
||||
sets: objs.length,
|
||||
skipped: buildingsSkipped,
|
||||
})
|
||||
: t("ctxImport.imported", {
|
||||
count: all.length + extraObjs.length,
|
||||
sets: objs.length,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user