Skip to content

Commit bd62d42

Browse files
committed
Update
1 parent 6e5492c commit bd62d42

File tree

4 files changed

+134
-20
lines changed

4 files changed

+134
-20
lines changed

js/elevation.js

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ export async function preloadTiles(pointsLV95, onProgress) {
129129
}
130130

131131
// =============================================
132-
// Grid creation
132+
// Grid creation (orientation-aligned)
133133
// =============================================
134+
135+
/** Ray-casting point-in-polygon test */
134136
function pointInPolygon(x, y, polygon) {
135137
let inside = false;
136138
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
@@ -143,33 +145,129 @@ function pointInPolygon(x, y, polygon) {
143145
return inside;
144146
}
145147

148+
/** Cross product of vectors OA and OB (for convex hull) */
149+
function cross(o, a, b) {
150+
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
151+
}
152+
153+
/** Monotone chain convex hull — returns points in CCW order, no duplicates */
154+
function convexHull(points) {
155+
const pts = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
156+
const n = pts.length;
157+
if (n <= 2) return pts.slice();
158+
159+
const lower = [];
160+
for (const p of pts) {
161+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
162+
lower.push(p);
163+
}
164+
const upper = [];
165+
for (let i = n - 1; i >= 0; i--) {
166+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pts[i]) <= 0) upper.pop();
167+
upper.push(pts[i]);
168+
}
169+
lower.pop();
170+
upper.pop();
171+
return lower.concat(upper);
172+
}
173+
146174
/**
147-
* Create a grid of sample points inside a polygon (LV95 coordinates).
148-
* Uses axis-aligned grid at GRID_SPACING (1m).
175+
* Minimum area bounding rectangle via rotating calipers on the convex hull.
176+
* Returns the rotation angle (radians) of the longest edge.
177+
*/
178+
function getBuildingAngle(coordsLV95) {
179+
const hull = convexHull(coordsLV95);
180+
if (hull.length < 3) return 0;
181+
182+
let bestArea = Infinity;
183+
let bestAngle = 0;
184+
let bestLongestEdgeAngle = 0;
185+
186+
for (let i = 0; i < hull.length; i++) {
187+
const j = (i + 1) % hull.length;
188+
const edgeAngle = Math.atan2(hull[j][1] - hull[i][1], hull[j][0] - hull[i][0]);
189+
const cos = Math.cos(-edgeAngle), sin = Math.sin(-edgeAngle);
190+
191+
// Rotate all hull points to align this edge with x-axis
192+
let rxMin = Infinity, rxMax = -Infinity, ryMin = Infinity, ryMax = -Infinity;
193+
for (const p of hull) {
194+
const rx = p[0] * cos - p[1] * sin;
195+
const ry = p[0] * sin + p[1] * cos;
196+
if (rx < rxMin) rxMin = rx;
197+
if (rx > rxMax) rxMax = rx;
198+
if (ry < ryMin) ryMin = ry;
199+
if (ry > ryMax) ryMax = ry;
200+
}
201+
202+
const area = (rxMax - rxMin) * (ryMax - ryMin);
203+
if (area < bestArea) {
204+
bestArea = area;
205+
bestAngle = edgeAngle;
206+
// Determine longest edge of this bounding rectangle
207+
const w = rxMax - rxMin, h = ryMax - ryMin;
208+
bestLongestEdgeAngle = w >= h ? edgeAngle : edgeAngle + Math.PI / 2;
209+
}
210+
}
211+
212+
return bestLongestEdgeAngle;
213+
}
214+
215+
/**
216+
* Create grid points aligned to the building's orientation.
217+
*
218+
* 1. Compute orientation from minimum area bounding rectangle
219+
* 2. Rotate polygon to align with axes
220+
* 3. Generate regular grid in rotated space
221+
* 4. Filter points inside the polygon
222+
* 5. Rotate points back to original orientation
149223
*/
150224
export function createGridPoints(coordsLV95, spacing = GRID_SPACING) {
225+
// Compute centroid
226+
let cx = 0, cy = 0;
227+
for (const pt of coordsLV95) { cx += pt[0]; cy += pt[1]; }
228+
cx /= coordsLV95.length;
229+
cy /= coordsLV95.length;
230+
231+
// Get building orientation angle
232+
const angle = getBuildingAngle(coordsLV95);
233+
const cosA = Math.cos(-angle), sinA = Math.sin(-angle);
234+
235+
// Rotate polygon to align with axes (around centroid)
236+
const rotated = coordsLV95.map((p) => {
237+
const dx = p[0] - cx, dy = p[1] - cy;
238+
return [cx + dx * cosA - dy * sinA, cy + dx * sinA + dy * cosA];
239+
});
240+
241+
// Bounding box of rotated polygon, snapped to grid
151242
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
152-
for (const pt of coordsLV95) {
243+
for (const pt of rotated) {
153244
if (pt[0] < minX) minX = pt[0];
154245
if (pt[0] > maxX) maxX = pt[0];
155246
if (pt[1] < minY) minY = pt[1];
156247
if (pt[1] > maxY) maxY = pt[1];
157248
}
249+
minX = Math.floor(minX / spacing) * spacing;
250+
minY = Math.floor(minY / spacing) * spacing;
251+
maxX = Math.ceil(maxX / spacing) * spacing;
252+
maxY = Math.ceil(maxY / spacing) * spacing;
158253

254+
// Generate grid in rotated space, filter by polygon containment
255+
const cosB = Math.cos(angle), sinB = Math.sin(angle);
159256
const points = [];
257+
160258
for (let gx = minX + spacing / 2; gx < maxX; gx += spacing) {
161259
for (let gy = minY + spacing / 2; gy < maxY; gy += spacing) {
162-
if (pointInPolygon(gx, gy, coordsLV95)) {
163-
points.push([gx, gy]);
260+
if (pointInPolygon(gx, gy, rotated)) {
261+
// Rotate back to original orientation
262+
const dx = gx - cx, dy = gy - cy;
263+
points.push([cx + dx * cosB - dy * sinB, cy + dx * sinB + dy * cosB]);
164264
}
165265
}
166266
}
167267

168268
// Fallback: centroid if grid is empty (very small footprint)
169269
if (points.length === 0) {
170-
let cx = 0, cy = 0;
171-
for (const pt of coordsLV95) { cx += pt[0]; cy += pt[1]; }
172-
points.push([cx / coordsLV95.length, cy / coordsLV95.length]);
270+
points.push([cx, cy]);
173271
}
174272
return points;
175273
}
@@ -198,6 +296,7 @@ export function polygonAreaLV95(ring) {
198296
* @returns {object|null} Volume result or null if no data
199297
*/
200298
export function computeVolumeSync(coordsLV95) {
299+
const gridAngle = getBuildingAngle(coordsLV95);
201300
const gridPoints = createGridPoints(coordsLV95);
202301
if (gridPoints.length === 0) return null;
203302

@@ -278,6 +377,7 @@ export function computeVolumeSync(coordsLV95) {
278377
grid_points: dtmValues.length,
279378
grid_cells: cells,
280379
grid_spacing: GRID_SPACING,
380+
grid_angle: gridAngle,
281381
};
282382
}
283383

js/main.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ function cacheProgressEls() {
127127
function updateProgress(progress, startTime) {
128128
if (!progressEls.barFill) cacheProgressEls();
129129

130-
const { processed, total, succeeded, failed } = progress;
130+
const { processed, total, succeeded, failed, noFootprint, noHeight, currentId } = progress;
131131
const pct = total > 0 ? ((processed / total) * 100).toFixed(1) : 0;
132132

133133
progressEls.barFill.style.width = `${pct}%`;
@@ -141,9 +141,15 @@ function updateProgress(progress, startTime) {
141141
const etaMin = Math.floor(etaSeconds / 60);
142142
const etaSec = etaSeconds % 60;
143143
progressEls.eta.textContent = processed < total
144-
? `Geschatzt: ${etaMin}min ${etaSec}s verbleibend`
144+
? `Geschätzt: ${etaMin}min ${etaSec}s verbleibend`
145145
: "Abschluss...";
146-
progressEls.stats.textContent = `Erfolgreich: ${succeeded} | Fehlgeschlagen: ${failed}`;
146+
147+
// Detailed breakdown
148+
const parts = [`Erfolgreich: ${succeeded}`];
149+
if (noFootprint) parts.push(`Kein Grundriss: ${noFootprint}`);
150+
if (noHeight) parts.push(`Keine Höhe: ${noHeight}`);
151+
if (failed - (noFootprint || 0) - (noHeight || 0) > 0) parts.push(`Fehler: ${failed - noFootprint - noHeight}`);
152+
progressEls.stats.textContent = parts.join(" | ");
147153
}
148154

149155
function showResults() {

js/map.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ function ensureGridCellsConverted() {
1717
if (gridCellsGeoJSON || !rawGridCells || rawGridCells.length === 0) return;
1818
const half = GRID_SPACING / 2;
1919
const features = rawGridCells.map((c) => {
20-
const sw = fromLV95(c.x - half, c.y - half);
21-
const se = fromLV95(c.x + half, c.y - half);
22-
const ne = fromLV95(c.x + half, c.y + half);
23-
const nw = fromLV95(c.x - half, c.y + half);
20+
const cos = Math.cos(c.angle), sin = Math.sin(c.angle);
21+
// Compute 4 corners of the rotated square in LV95
22+
const corners = [[-half, -half], [half, -half], [half, half], [-half, half]].map(([dx, dy]) =>
23+
fromLV95(c.x + dx * cos - dy * sin, c.y + dx * sin + dy * cos)
24+
);
25+
corners.push(corners[0]); // close ring
2426
return {
2527
type: "Feature",
26-
geometry: { type: "Polygon", coordinates: [[sw, se, ne, nw, sw]] },
28+
geometry: { type: "Polygon", coordinates: [corners] },
2729
properties: { h: Math.round(c.h * 10) / 10 },
2830
};
2931
});
@@ -147,10 +149,13 @@ export function plotResults(data) {
147149
});
148150
}
149151

150-
// Grid cells — store raw LV95 data, convert lazily on first toggle
152+
// Grid cells — store raw LV95 data with angle, convert lazily on first toggle
151153
rawGridCells = data.buildings
152154
.filter((b) => b.grid_cells)
153-
.flatMap((b) => b.grid_cells);
155+
.flatMap((b) => {
156+
const angle = b.grid_angle || 0;
157+
return b.grid_cells.map((c) => ({ ...c, angle }));
158+
});
154159
gridCellsGeoJSON = null;
155160

156161
const emptyGeoJSON = { type: "FeatureCollection", features: [] };

js/processor.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export async function processRows(rows, onProgress) {
2525
cancelled = false;
2626
const total = rows.length;
2727
let processed = 0, succeeded = 0, failed = 0;
28+
let noFootprint = 0, noHeight = 0;
2829

2930
async function processOne(row) {
3031
if (cancelled) return null;
@@ -72,6 +73,7 @@ export async function processRows(rows, onProgress) {
7273
if (!footprint) {
7374
result.status = STATUS.NO_FOOTPRINT;
7475
failed++;
76+
noFootprint++;
7577
return result;
7678
}
7779

@@ -92,6 +94,7 @@ export async function processRows(rows, onProgress) {
9294
result.status = STATUS.NO_HEIGHT_DATA;
9395
result.area_footprint_m2 = Math.round(polygonAreaLV95(lv95Coords) * 100) / 100;
9496
failed++;
97+
noHeight++;
9598
return result;
9699
}
97100

@@ -171,7 +174,7 @@ export async function processRows(rows, onProgress) {
171174
results[idx] = r;
172175
processed++;
173176
releaseSlot();
174-
onProgress({ processed, total, succeeded, failed });
177+
onProgress({ processed, total, succeeded, failed, noFootprint, noHeight, currentId: rows[idx]?.id || "" });
175178
});
176179
allPromises.push(promise);
177180
}

0 commit comments

Comments
 (0)