Skip to content

Commit 048d609

Browse files
authored
Merge pull request #7 from bbl-dres/claude/add-map-clustering-LEYV5
Add map point clustering for zoomed-out building overview
2 parents 77cdc33 + 386739d commit 048d609

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ <h2>Gebäude werden analysiert...</h2>
175175
<input type="checkbox" class="active-layer-checkbox" checked id="layer-toggle-labels">
176176
<span class="active-layer-title">Beschriftungen</span>
177177
</label>
178+
<label class="active-layer-item">
179+
<input type="checkbox" class="active-layer-checkbox" checked id="layer-toggle-clusters">
180+
<span class="active-layer-title">Clustering</span>
181+
</label>
178182
</div>
179183
<div class="layer-group">
180184
<div class="layer-group-label">Externe Karten</div>

js/map.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,21 @@ const GRID_PAINT = {
3636
"fill-extrusion-opacity": 0.85,
3737
};
3838

39+
// Cluster style constants
40+
const CLUSTER_COLORS = [
41+
[100, "#e74c3c"], // 100+ buildings: red
42+
[20, "#f39c12"], // 20–99: orange
43+
[0, "#3498db"], // 0–19: blue
44+
];
45+
const CLUSTER_RADII = [
46+
[100, 30],
47+
[20, 24],
48+
[0, 18],
49+
];
50+
3951
let map = null;
4052
let buildingsGeoJSON = null;
53+
let clusterGeoJSON = null;
4154
let gridCellsGeoJSON = null;
4255
let rawGridCells = null;
4356
let callbacks = {};
@@ -105,6 +118,106 @@ export async function initMap(containerId, cbs) {
105118
document.getElementById("style-switcher")?.classList.add("visible");
106119
}
107120

121+
/** Build a point FeatureCollection of building centroids for clustering */
122+
function buildClusterPoints(buildings) {
123+
const pts = buildings
124+
.filter((b) => b.geometry)
125+
.map((b, i) => {
126+
const centroid = turf.centroid({ type: "Feature", geometry: b.geometry });
127+
return {
128+
type: "Feature",
129+
geometry: centroid.geometry,
130+
properties: {
131+
_index: i,
132+
id: b.input_id,
133+
status: b.status,
134+
},
135+
};
136+
});
137+
return { type: "FeatureCollection", features: pts };
138+
}
139+
140+
/** Add cluster source and layers to the map */
141+
function addClusterLayers(visible) {
142+
if (!clusterGeoJSON) return;
143+
const vis = visible ? "visible" : "none";
144+
145+
if (!map.getSource("buildings-clustered")) {
146+
map.addSource("buildings-clustered", {
147+
type: "geojson",
148+
data: clusterGeoJSON,
149+
cluster: true,
150+
clusterMaxZoom: 14,
151+
clusterRadius: 50,
152+
});
153+
}
154+
155+
// Cluster circles
156+
if (!map.getLayer("clusters")) {
157+
map.addLayer({
158+
id: "clusters",
159+
type: "circle",
160+
source: "buildings-clustered",
161+
filter: ["has", "point_count"],
162+
layout: { visibility: vis },
163+
paint: {
164+
"circle-color": [
165+
"step", ["get", "point_count"],
166+
CLUSTER_COLORS[2][1], CLUSTER_COLORS[1][0],
167+
CLUSTER_COLORS[1][1], CLUSTER_COLORS[0][0],
168+
CLUSTER_COLORS[0][1],
169+
],
170+
"circle-radius": [
171+
"step", ["get", "point_count"],
172+
CLUSTER_RADII[2][1], CLUSTER_RADII[1][0],
173+
CLUSTER_RADII[1][1], CLUSTER_RADII[0][0],
174+
CLUSTER_RADII[0][1],
175+
],
176+
"circle-stroke-width": 2,
177+
"circle-stroke-color": "#fff",
178+
},
179+
});
180+
}
181+
182+
// Cluster count labels
183+
if (!map.getLayer("cluster-count")) {
184+
map.addLayer({
185+
id: "cluster-count",
186+
type: "symbol",
187+
source: "buildings-clustered",
188+
filter: ["has", "point_count"],
189+
layout: {
190+
visibility: vis,
191+
"text-field": ["get", "point_count_abbreviated"],
192+
"text-size": 12,
193+
"text-allow-overlap": true,
194+
},
195+
paint: {
196+
"text-color": "#fff",
197+
},
198+
});
199+
}
200+
201+
// Unclustered individual points
202+
if (!map.getLayer("unclustered-point")) {
203+
map.addLayer({
204+
id: "unclustered-point",
205+
type: "circle",
206+
source: "buildings-clustered",
207+
filter: ["!", ["has", "point_count"]],
208+
layout: { visibility: vis },
209+
paint: {
210+
"circle-color": [
211+
"case", ["==", ["get", "status"], "success"], "#3498db", "#95a5a6",
212+
],
213+
"circle-radius": 6,
214+
"circle-stroke-width": 1.5,
215+
"circle-stroke-color": "#fff",
216+
},
217+
});
218+
}
219+
}
220+
108221
export function plotResults(data) {
109222
if (!map || !data || !data.buildings) return;
110223

@@ -131,6 +244,9 @@ export function plotResults(data) {
131244

132245
buildingsGeoJSON = { type: "FeatureCollection", features };
133246

247+
// Build cluster point data
248+
clusterGeoJSON = buildClusterPoints(data.buildings);
249+
134250
// Add source
135251
if (map.getSource("buildings")) {
136252
map.getSource("buildings").setData(buildingsGeoJSON);
@@ -158,6 +274,10 @@ export function plotResults(data) {
158274
});
159275
}
160276

277+
// Cluster layers
278+
const clusterToggle = document.getElementById("layer-toggle-clusters");
279+
addClusterLayers(!clusterToggle || clusterToggle.checked);
280+
161281
// Grid cells — store raw LV95 data with angle, convert lazily on first toggle
162282
rawGridCells = data.buildings
163283
.filter((b) => b.grid_cells)
@@ -229,6 +349,27 @@ export function plotResults(data) {
229349
map.on("mouseenter", "buildings-3d", () => { map.getCanvas().style.cursor = "pointer"; });
230350
map.on("mouseleave", "buildings-3d", () => { map.getCanvas().style.cursor = ""; });
231351

352+
// Cluster click → zoom to expand
353+
map.on("click", "clusters", (e) => {
354+
const cluster = e.features[0];
355+
map.getSource("buildings-clustered").getClusterExpansionZoom(cluster.properties.cluster_id, (err, zoom) => {
356+
if (err) return;
357+
map.easeTo({ center: cluster.geometry.coordinates, zoom: zoom + 0.5 });
358+
});
359+
});
360+
map.on("mouseenter", "clusters", () => { map.getCanvas().style.cursor = "pointer"; });
361+
map.on("mouseleave", "clusters", () => { map.getCanvas().style.cursor = ""; });
362+
363+
// Unclustered point click → fly to building
364+
map.on("click", "unclustered-point", (e) => {
365+
if (!e.features.length) return;
366+
const idx = e.features[0].properties._index;
367+
map.flyTo({ center: e.lngLat, zoom: 17 });
368+
if (callbacks.onBuildingSelect) callbacks.onBuildingSelect(idx);
369+
});
370+
map.on("mouseenter", "unclustered-point", () => { map.getCanvas().style.cursor = "pointer"; });
371+
map.on("mouseleave", "unclustered-point", () => { map.getCanvas().style.cursor = ""; });
372+
232373
// Layer toggles
233374
document.getElementById("layer-toggle-footprints")?.addEventListener("change", (e) => {
234375
if (map.getLayer("buildings-outline")) {
@@ -253,6 +394,12 @@ export function plotResults(data) {
253394
map.setLayoutProperty("buildings-labels", "visibility", e.target.checked ? "visible" : "none");
254395
}
255396
});
397+
document.getElementById("layer-toggle-clusters")?.addEventListener("change", (e) => {
398+
const vis = e.target.checked ? "visible" : "none";
399+
for (const id of ["clusters", "cluster-count", "unclustered-point"]) {
400+
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", vis);
401+
}
402+
});
256403

257404
// AV cadastral overlay
258405
document.getElementById("layer-toggle-av")?.addEventListener("change", (e) => {
@@ -323,6 +470,7 @@ function initBasemapSwitcher() {
323470
const visBuildings = document.getElementById("layer-toggle-buildings")?.checked ? "visible" : "none";
324471
const visLabels = document.getElementById("layer-toggle-labels")?.checked ? "visible" : "none";
325472
const visGrid = document.getElementById("layer-toggle-grid")?.checked ? "visible" : "none";
473+
const visClusters = document.getElementById("layer-toggle-clusters")?.checked !== false;
326474

327475
map.addSource("buildings", { type: "geojson", data: savedData });
328476
map.addLayer({ id: "buildings-3d", type: "fill-extrusion", source: "buildings",
@@ -335,6 +483,9 @@ function initBasemapSwitcher() {
335483
map.addSource("grid-cells", { type: "geojson", data: gridData });
336484
map.addLayer({ id: "grid-cells-3d", type: "fill-extrusion", source: "grid-cells",
337485
layout: { visibility: visGrid }, paint: GRID_PAINT });
486+
487+
// Re-add cluster layers
488+
addClusterLayers(visClusters);
338489
}
339490
});
340491

0 commit comments

Comments
 (0)