Skip to content

Commit b16144f

Browse files
authored
Merge pull request #8 from bbl-dres/claude/analyze-grid-storey-intersection-HEbRv
Add merged storey polygon layer for per-floor 3D visualization
2 parents 048d609 + 13048ba commit b16144f

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ <h2>Gebäude werden analysiert...</h2>
171171
<input type="checkbox" class="active-layer-checkbox" id="layer-toggle-grid">
172172
<span class="active-layer-title">Rasterzellen</span>
173173
</label>
174+
<label class="active-layer-item">
175+
<input type="checkbox" class="active-layer-checkbox" id="layer-toggle-storeys">
176+
<span class="active-layer-title">Geschosspolygone</span>
177+
</label>
174178
<label class="active-layer-item">
175179
<input type="checkbox" class="active-layer-checkbox" checked id="layer-toggle-labels">
176180
<span class="active-layer-title">Beschriftungen</span>

js/map.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ const GRID_PAINT = {
3535
"fill-extrusion-base": 0,
3636
"fill-extrusion-opacity": 0.85,
3737
};
38+
const STOREY_PAINT = {
39+
"fill-extrusion-color": ["interpolate", ["linear"], ["get", "storey"],
40+
1, "#4a90d9", 2, "#3498db", 3, "#2ecc71", 5, "#f39c12", 8, "#e74c3c"],
41+
"fill-extrusion-height": ["get", "top"],
42+
"fill-extrusion-base": ["get", "base"],
43+
"fill-extrusion-opacity": 0.75,
44+
};
3845

3946
// Cluster style constants
4047
const CLUSTER_COLORS = [
@@ -53,6 +60,8 @@ let buildingsGeoJSON = null;
5360
let clusterGeoJSON = null;
5461
let gridCellsGeoJSON = null;
5562
let rawGridCells = null;
63+
let storeyPolygonsGeoJSON = null;
64+
let rawStoreyData = null;
5665
let callbacks = {};
5766
let summaryToggleCb = null;
5867

@@ -79,6 +88,31 @@ function ensureGridCellsConverted() {
7988
}
8089
}
8190

91+
/** Convert raw LV95 storey polygons to WGS84 GeoJSON on first use */
92+
function ensureStoreyPolygonsConverted() {
93+
if (storeyPolygonsGeoJSON || !rawStoreyData || rawStoreyData.length === 0) return;
94+
const features = rawStoreyData.map((sp) => {
95+
// Convert polygon rings from LV95 to WGS84
96+
const geom = sp.polygonLV95;
97+
const convertRing = (ring) => ring.map(([x, y]) => fromLV95(x, y));
98+
let coordinates;
99+
if (geom.type === "MultiPolygon") {
100+
coordinates = geom.coordinates.map((poly) => poly.map(convertRing));
101+
} else {
102+
coordinates = geom.coordinates.map(convertRing);
103+
}
104+
return {
105+
type: "Feature",
106+
geometry: { type: geom.type, coordinates },
107+
properties: { storey: sp.storey, base: sp.base, top: sp.top, buildingIndex: sp.buildingIndex },
108+
};
109+
});
110+
storeyPolygonsGeoJSON = { type: "FeatureCollection", features };
111+
if (map && map.getSource("storey-polygons")) {
112+
map.getSource("storey-polygons").setData(storeyPolygonsGeoJSON);
113+
}
114+
}
115+
82116
export function onSummaryToggle(cb) { summaryToggleCb = cb; }
83117
export function setSummaryToggleVisible(visible) {
84118
const btn = document.getElementById("summary-toggle-btn");
@@ -304,6 +338,81 @@ export function plotResults(data) {
304338
});
305339
}
306340

341+
// Storey polygons — compute merged polygon per storey per building
342+
rawStoreyData = [];
343+
storeyPolygonsGeoJSON = null;
344+
const half = GRID_SPACING / 2;
345+
346+
for (let bi = 0; bi < data.buildings.length; bi++) {
347+
const b = data.buildings[bi];
348+
if (!b.grid_cells || !b.floor_height_used || b.floor_height_used <= 0) continue;
349+
350+
const floorH = b.floor_height_used;
351+
const angle = b.grid_angle || 0;
352+
const cos = Math.cos(angle), sin = Math.sin(angle);
353+
const maxH = Math.max(...b.grid_cells.map((c) => c.h));
354+
const maxStoreys = Math.ceil(maxH / floorH);
355+
356+
for (let s = 1; s <= maxStoreys; s++) {
357+
const threshold = (s - 1) * floorH;
358+
const cellsAtLevel = b.grid_cells.filter((c) => c.h > threshold);
359+
360+
// Skip storey if fewer than 50% of cells reach it (partial roof artefact)
361+
if (cellsAtLevel.length < b.grid_cells.length * 0.5 && s > Math.floor(maxH / floorH)) continue;
362+
if (cellsAtLevel.length === 0) continue;
363+
364+
// Build cell polygons in LV95 for union
365+
const cellFeatures = cellsAtLevel.map((c) => {
366+
const corners = [[-half, -half], [half, -half], [half, half], [-half, half]].map(([dx, dy]) =>
367+
[c.x + dx * cos - dy * sin, c.y + dx * sin + dy * cos]
368+
);
369+
corners.push(corners[0]);
370+
return turf.polygon([corners]);
371+
});
372+
373+
// Union all cell polygons into a single polygon for this storey
374+
let merged;
375+
if (cellFeatures.length === 1) {
376+
merged = cellFeatures[0];
377+
} else {
378+
try {
379+
merged = turf.union(turf.featureCollection(cellFeatures));
380+
} catch (_) {
381+
// If union fails, skip this storey
382+
continue;
383+
}
384+
}
385+
386+
if (merged) {
387+
rawStoreyData.push({
388+
buildingIndex: bi,
389+
storey: s,
390+
polygonLV95: merged.geometry,
391+
base: (s - 1) * floorH,
392+
top: Math.min(s * floorH, maxH),
393+
});
394+
}
395+
}
396+
}
397+
398+
// Storey polygons source + layer (starts empty, converted lazily)
399+
const emptyGeoJSON2 = { type: "FeatureCollection", features: [] };
400+
if (map.getSource("storey-polygons")) {
401+
map.getSource("storey-polygons").setData(emptyGeoJSON2);
402+
} else {
403+
map.addSource("storey-polygons", { type: "geojson", data: emptyGeoJSON2 });
404+
}
405+
406+
if (!map.getLayer("storey-polygons-3d")) {
407+
map.addLayer({
408+
id: "storey-polygons-3d",
409+
type: "fill-extrusion",
410+
source: "storey-polygons",
411+
layout: { visibility: "none" },
412+
paint: STOREY_PAINT,
413+
});
414+
}
415+
307416
// Labels
308417
if (!map.getLayer("buildings-labels")) {
309418
map.addLayer({
@@ -389,6 +498,14 @@ export function plotResults(data) {
389498
map.setLayoutProperty("grid-cells-3d", "visibility", e.target.checked ? "visible" : "none");
390499
}
391500
});
501+
document.getElementById("layer-toggle-storeys")?.addEventListener("change", (e) => {
502+
if (e.target.checked) {
503+
ensureStoreyPolygonsConverted();
504+
}
505+
if (map.getLayer("storey-polygons-3d")) {
506+
map.setLayoutProperty("storey-polygons-3d", "visibility", e.target.checked ? "visible" : "none");
507+
}
508+
});
392509
document.getElementById("layer-toggle-labels")?.addEventListener("change", (e) => {
393510
if (map.getLayer("buildings-labels")) {
394511
map.setLayoutProperty("buildings-labels", "visibility", e.target.checked ? "visible" : "none");
@@ -484,6 +601,12 @@ function initBasemapSwitcher() {
484601
map.addLayer({ id: "grid-cells-3d", type: "fill-extrusion", source: "grid-cells",
485602
layout: { visibility: visGrid }, paint: GRID_PAINT });
486603

604+
const visStoreys = document.getElementById("layer-toggle-storeys")?.checked ? "visible" : "none";
605+
const storeyData = storeyPolygonsGeoJSON || { type: "FeatureCollection", features: [] };
606+
map.addSource("storey-polygons", { type: "geojson", data: storeyData });
607+
map.addLayer({ id: "storey-polygons-3d", type: "fill-extrusion", source: "storey-polygons",
608+
layout: { visibility: visStoreys }, paint: STOREY_PAINT });
609+
487610
// Re-add cluster layers
488611
addClusterLayers(visClusters);
489612
}

0 commit comments

Comments
 (0)