diff --git a/src/index.html b/src/index.html
index 72927aa..f610721 100644
--- a/src/index.html
+++ b/src/index.html
@@ -951,7 +951,7 @@ function vehState(p) {
let tkMap = null, tkPopup = null, tkLivePoll = null, tkClosureChart = null;
const tkMarkers = new Map(); // imei → maplibregl.Marker
-const tkLayerState = { open: true, closed: false, vehicles: true };
+const tkLayerState = { open: true, closed: true, vehicles: true };
let incData = null, incDropdownsInit = false, vehCount = 0;
// ── INC helpers ───────────────────────────────────────────────────────────
@@ -1007,6 +1007,51 @@ function addPinImages() {
if (!tkMap.hasImage(id)) tkMap.addImage(id, pinImageData(fill), { pixelRatio: 2 });
}
+// Co-located declutter ("fan-out"). Cluster-centroid tickets share an EXACT coordinate,
+// so the open layer would sit on top of (hide) the faded closed pins at the same point.
+// Pins that share a coordinate are fanned into a small deterministic sunflower spiral
+// around it — computed across BOTH layers so an open and a closed ticket separate — so
+// active (vivid) and closed (faded) are both visible. Offsets are cosmetic (a few tens
+// to ~hundreds of m around the cluster centroid; popups already flag 'approx').
+const JITTER_BASE_DEG = 0.00035; // innermost step (~38 m); grows ~sqrt(i)
+const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
+function fanOutColocated(openFC, closedFC) {
+ const groups = new Map();
+ const add = (f, layer) => {
+ const c = f && f.geometry && f.geometry.coordinates; if (!c) return;
+ const key = c[0].toFixed(6) + ',' + c[1].toFixed(6);
+ let arr = groups.get(key); if (!arr) { arr = []; groups.set(key, arr); }
+ arr.push({ f, layer });
+ };
+ ((openFC && openFC.features) || []).forEach((f) => add(f, 'open'));
+ ((closedFC && closedFC.features) || []).forEach((f) => add(f, 'closed'));
+ const outOpen = [], outClosed = [];
+ for (const items of groups.values()) {
+ if (items.length > 1) // stable order so pins don't jump between refreshes
+ items.sort((a, b) => String((a.f.properties || {}).ticket_id || '')
+ .localeCompare(String((b.f.properties || {}).ticket_id || '')));
+ items.forEach((it, i) => {
+ const c = it.f.geometry.coordinates;
+ let coords = c;
+ if (items.length > 1 && i > 0) { // i=0 stays on the true centroid
+ const r = JITTER_BASE_DEG * Math.sqrt(i), a = i * GOLDEN_ANGLE;
+ const latRad = c[1] * Math.PI / 180;
+ coords = [c[0] + (r * Math.cos(a)) / Math.max(Math.cos(latRad), 0.2), c[1] + r * Math.sin(a)];
+ }
+ (it.layer === 'open' ? outOpen : outClosed).push(
+ { type: 'Feature', properties: it.f.properties, geometry: { type: 'Point', coordinates: coords } });
+ });
+ }
+ return { open: { type: 'FeatureCollection', features: outOpen },
+ closed: { type: 'FeatureCollection', features: outClosed } };
+}
+function renderIncMap() {
+ if (!tkMap || !incData) return;
+ const { open, closed } = fanOutColocated(incData.open, incData.closed);
+ if (tkMap.getSource('inc-open')) tkMap.getSource('inc-open').setData(open);
+ if (tkMap.getSource('inc-closed')) tkMap.getSource('inc-closed').setData(closed);
+}
+
function initIncMap() {
if (tkMap) { tkMap.resize(); return; } // already built — just fix sizing
@@ -1062,10 +1107,7 @@ function initIncMap() {
tkMap.on('mousemove', id, (e) => showIncPopup(e.features[0], id === 'inc-closed'));
}
tkMap.on('zoom', updateVehScale); updateVehScale();
- if (incData) { // INC data may have arrived before the basemap finished loading
- tkMap.getSource('inc-open').setData(incData.open || EMPTY_FC);
- tkMap.getSource('inc-closed').setData(incData.closed || EMPTY_FC);
- }
+ if (incData) renderIncMap(); // INC data may have arrived before the basemap loaded
loadLive();
tkLivePoll = setInterval(loadLive, LIVE_POLL_MS);
});
@@ -1154,8 +1196,7 @@ async function loadInc() {
renderIncMetrics(j.metrics, j.freshness);
renderIncTables(j.metrics);
renderClosureChart(j.metrics && j.metrics.closure_rate);
- if (tkMap && tkMap.getSource('inc-open')) tkMap.getSource('inc-open').setData(j.open || EMPTY_FC);
- if (tkMap && tkMap.getSource('inc-closed')) tkMap.getSource('inc-closed').setData(j.closed || EMPTY_FC);
+ renderIncMap(); // fan out co-located pins so open (vivid) + closed (faded) both show
buildIncLayers();
} catch (e) {
console.error(e);