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.empty": "Keine Geometrie im gewählten Bereich gefunden.",
|
||||||
"ctxImport.importError": "Import fehlgeschlagen.",
|
"ctxImport.importError": "Import fehlgeschlagen.",
|
||||||
"ctxImport.imported": "{count} Objekte in {sets} Sätzen importiert.",
|
"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 ─────────────────────────────────────────────────────────────
|
// ── Raum-Befehl ─────────────────────────────────────────────────────────────
|
||||||
"cmd.room.label": "Raum",
|
"cmd.room.label": "Raum",
|
||||||
|
|||||||
@@ -1294,6 +1294,8 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"ctxImport.empty": "No geometry found in the selected area.",
|
"ctxImport.empty": "No geometry found in the selected area.",
|
||||||
"ctxImport.importError": "Import failed.",
|
"ctxImport.importError": "Import failed.",
|
||||||
"ctxImport.imported": "Imported {count} objects in {sets} sets.",
|
"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.
|
// Room command.
|
||||||
"cmd.room.label": "Room",
|
"cmd.room.label": "Room",
|
||||||
|
|||||||
+36
-13
@@ -75,26 +75,49 @@ export function pickAsset(item: StacItem, priority: string[]): StacAsset | null
|
|||||||
return pool[0] ?? 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
|
/** Lädt ein Asset als Text; entpackt automatisch, wenn es ein `.zip` ist
|
||||||
* (liefert den Inhalt der ERSTEN enthaltenen Datei mit einer der `innerExt`-
|
* (liefert den Inhalt der ERSTEN enthaltenen Datei mit einer der `innerExt`-
|
||||||
* Endungen). `null` bei Fehler, fehlendem Inhalt im ZIP, oder wenn das Asset
|
* 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(
|
export async function downloadAssetText(
|
||||||
asset: StacAsset,
|
asset: StacAsset,
|
||||||
innerExt: string[],
|
innerExt: string[],
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (asset.size != null && asset.size > MAX_ASSET_BYTES) return null;
|
if (asset.size != null && asset.size > MAX_ASSET_BYTES) return null;
|
||||||
const res = await fetch(viaProxy(asset.href));
|
try {
|
||||||
if (!res.ok) return null;
|
const res = await fetch(viaProxy(asset.href));
|
||||||
if (asset.href.toLowerCase().endsWith(".zip")) {
|
if (!res.ok) return null;
|
||||||
const buf = await res.arrayBuffer();
|
if (asset.href.toLowerCase().endsWith(".zip")) {
|
||||||
if (buf.byteLength > MAX_ASSET_BYTES) return null;
|
const buf = await res.arrayBuffer();
|
||||||
const zip = await JSZip.loadAsync(buf);
|
if (buf.byteLength > MAX_ASSET_BYTES) return null;
|
||||||
const entry = Object.values(zip.files).find(
|
const zip = await JSZip.loadAsync(buf);
|
||||||
(f) => !f.dir && innerExt.some((ext) => f.name.toLowerCase().endsWith(ext)),
|
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");
|
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[];
|
meshes: ImportedMesh[];
|
||||||
/** Tatsächlich gelieferte Version (nach evtl. v3→v2-Fallback). */
|
/** Tatsächlich gelieferte Version (nach evtl. v3→v2-Fallback). */
|
||||||
versionUsed: BuildingsVersion;
|
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 origin = originIn ?? makeOrigin(center);
|
||||||
const bboxWgs = lv95BboxToWgs84(bboxAround(center, radius));
|
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 items = await stacQuery(collectionId, bboxWgs, ASSET_PRIORITY);
|
||||||
const meshes: ImportedMesh[] = [];
|
const meshes: ImportedMesh[] = [];
|
||||||
|
let skipped = 0;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const asset = pickAsset(item, ASSET_PRIORITY);
|
const asset = pickAsset(item, ASSET_PRIORITY);
|
||||||
if (!asset) continue;
|
if (!asset) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const text = await downloadAssetText(asset, [".dxf"]);
|
const text = await downloadAssetText(asset, [".dxf"]);
|
||||||
if (!text) continue;
|
if (!text) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const parsed = parseDxf(text);
|
const parsed = parseDxf(text);
|
||||||
for (const m of parsed.meshes) meshes.push(shiftMeshToOrigin(m, origin, item.id));
|
for (const m of parsed.meshes) meshes.push(shiftMeshToOrigin(m, origin, item.id));
|
||||||
}
|
}
|
||||||
return meshes;
|
return { meshes, skipped };
|
||||||
};
|
};
|
||||||
|
|
||||||
if (version === "v3") {
|
if (version === "v3") {
|
||||||
const v3 = await tryCollection(COLLECTION_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);
|
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
|
// Echte swissBUILDINGS3D-Meshes (v2/v3) landen direkt als ContextObject
|
||||||
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
// (kein Footprint+Box-Umweg über GeoFeature/featuresToContextObjects).
|
||||||
const extraObjs: ContextObject[] = [];
|
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") {
|
if (sources.swissBuildings === "flach") {
|
||||||
const r = await fetchBuildings(center, radius, origin);
|
const r = await fetchBuildings(center, radius, origin);
|
||||||
@@ -149,6 +153,7 @@ export function ContextImportDialog({
|
|||||||
);
|
);
|
||||||
origin = r.origin;
|
origin = r.origin;
|
||||||
extraObjs.push(...r.meshes);
|
extraObjs.push(...r.meshes);
|
||||||
|
buildingsSkipped = r.skippedTiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
const osmSel: OsmSelection = {
|
const osmSel: OsmSelection = {
|
||||||
@@ -186,7 +191,11 @@ export function ContextImportDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (all.length === 0 && !terrain && extraObjs.length === 0) {
|
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);
|
setStatus(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -196,10 +205,16 @@ export function ContextImportDialog({
|
|||||||
if (terrain) objs.push(terrain);
|
if (terrain) objs.push(terrain);
|
||||||
onImport(objs);
|
onImport(objs);
|
||||||
setStatus(
|
setStatus(
|
||||||
t("ctxImport.imported", {
|
buildingsSkipped > 0
|
||||||
count: all.length + extraObjs.length,
|
? t("ctxImport.importedWithSkipped", {
|
||||||
sets: objs.length,
|
count: all.length + extraObjs.length,
|
||||||
}),
|
sets: objs.length,
|
||||||
|
skipped: buildingsSkipped,
|
||||||
|
})
|
||||||
|
: t("ctxImport.imported", {
|
||||||
|
count: all.length + extraObjs.length,
|
||||||
|
sets: objs.length,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
onClose();
|
onClose();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user