// Shared placeholder data for TreeGuard screens. // Numbers match the wireframe: 219 total, Clear 184 / Warning 23 / Alert 12. const RISK = { alarm: { label: 'Alarm', color: 'var(--status-alarm)', bg: 'var(--status-alarm-bg)', tier: 1 }, alert: { label: 'Alert', color: 'var(--status-alert)', bg: 'var(--status-alert-bg)', tier: 2 }, stable: { label: 'Stable', color: 'var(--status-stable)', bg: 'var(--status-stable-bg)', tier: 3 }, }; const KPIS = { total: 219, alarm: 12, alert: 23, stable: 184, }; // Span-level KPIs across the three risk perspectives. // Total spans on the line: 23. Each perspective classifies every span as alarm/alert/stable. // `worstCase` is the general roll-up across perspectives — a span lands in // alarm if ANY perspective flags it alarm; alert if any flags alert; otherwise stable. const SPAN_KPIS = { total: 23, worstCase: { alarm: 5, alert: 7, stable: 11 }, perspectives: [ { id: 'growth', label: 'Vegetation height', sub: 'Projected encroachment over the next analysis cycle', alarm: 3, alert: 5, stable: 15, }, { id: 'fall', label: 'Tree-fall risk', sub: 'Trees within fall-radius of the conductor', alarm: 4, alert: 6, stable: 13, }, { id: 'clearance', label: 'Minimum clearance', sub: 'Spans currently breaching regulatory clearance', alarm: 2, alert: 3, stable: 18, }, ], }; // Critical assets — assets (spans) that carry at least one Alarm-tier tree. const CRITICAL_ASSETS = [ { id: 'span-14', label: 'Span 14 — Torre 13 → Torre 14', alarms: 4, alerts: 2 }, { id: 'span-8', label: 'Span 8 — Torre 8 → Torre 9', alarms: 4, alerts: 5 }, { id: 'span-21', label: 'Span 21 — Torre 20 → Torre 21', alarms: 2, alerts: 4 }, { id: 'span-9', label: 'Span 9 — Torre 9 → Torre 10', alarms: 2, alerts: 1 }, ]; // Alarm trees — the canonical high-risk set. ONE per hot span so the dashboard // High-Risk Trees table maps 1:1 to the alarm markers on the Tree Fall map. // `spanNum` keys the map lookup; the map positions the marker procedurally but // uses this record's id / height / dist / species so tooltips match the table. // dist (d) is kept ≤ 40 m so the tree classifies as Alarm at the default 50 m corridor. const ALARM_TREES = [ { idNum: 40, id: '#40', spanNum: 14, span: 'Span 14', h: 20.0, d: 6.3, fall: 22.0, species: 'Pine', risk: 'alarm', lat: '40.5301', lng: '-7.2638' }, { idNum: 41, id: '#41', spanNum: 8, span: 'Span 8', h: 21.5, d: 3.7, fall: 23.2, species: 'Cork Oak', risk: 'alarm', lat: '40.5274', lng: '-7.2589' }, { idNum: 42, id: '#42', spanNum: 21, span: 'Span 21', h: 18.1, d: 2.8, fall: 19.0, species: 'Chestnut', risk: 'alarm', lat: '40.5342', lng: '-7.2713' }, { idNum: 43, id: '#43', spanNum: 1, span: 'Span 1', h: 24.5, d: 2.8, fall: 27.0, species: 'Eucalyptus', risk: 'alarm', lat: '40.5248', lng: '-7.2541' }, { idNum: 44, id: '#44', spanNum: 4, span: 'Span 4', h: 17.2, d: 4.1, fall: 18.4, species: 'Pine', risk: 'alarm', lat: '40.5259', lng: '-7.2563' }, { idNum: 45, id: '#45', spanNum: 5, span: 'Span 5', h: 22.0, d: 3.2, fall: 23.5, species: 'Eucalyptus', risk: 'alarm', lat: '40.5263', lng: '-7.2571' }, { idNum: 46, id: '#46', spanNum: 7, span: 'Span 7', h: 19.6, d: 5.5, fall: 20.7, species: 'Cork Oak', risk: 'alarm', lat: '40.5270', lng: '-7.2585' }, { idNum: 47, id: '#47', spanNum: 10, span: 'Span 10', h: 16.8, d: 6.2, fall: 17.9, species: 'Pine', risk: 'alarm', lat: '40.5290', lng: '-7.2620' }, { idNum: 48, id: '#48', spanNum: 16, span: 'Span 16', h: 23.3, d: 1.8, fall: 24.8, species: 'Eucalyptus', risk: 'alarm', lat: '40.5318', lng: '-7.2671' }, { idNum: 49, id: '#49', spanNum: 18, span: 'Span 18', h: 15.4, d: 4.7, fall: 16.5, species: 'Chestnut', risk: 'alarm', lat: '40.5329', lng: '-7.2692' }, { idNum: 50, id: '#50', spanNum: 19, span: 'Span 19', h: 18.7, d: 3.9, fall: 19.8, species: 'Pine', risk: 'alarm', lat: '40.5335', lng: '-7.2702' }, { idNum: 51, id: '#51', spanNum: 22, span: 'Span 22', h: 20.9, d: 2.4, fall: 22.3, species: 'Cork Oak', risk: 'alarm', lat: '40.5349', lng: '-7.2724' }, ]; // High-risk trees table = the alarm set, sorted by fall radius (most severe first). const HIGH_RISK_TREES = ALARM_TREES .map(t => ({ id: t.id, h: t.h, d: t.d, fall: t.fall, species: t.species, risk: t.risk, span: t.span, lat: t.lat, lng: t.lng })) .sort((a, b) => b.fall - a.fall); const INTERVENTIONS = [ { date: '25 Apr 2026', type: 'scheduled', action: 'Tree trimming', asset: 'Span 8', contractor: 'Silva Verde Lda.' }, { date: '28 Apr 2026', type: 'scheduled', action: 'Tree removal', asset: 'Span 21', contractor: 'ForestCare' }, { date: '3 May 2026', type: 'scheduled', action: 'Tree trimming', asset: 'Span 9', contractor: 'Silva Verde Lda.' }, { date: '8 May 2026', type: 'scheduled', action: 'Corridor clearance', asset: 'Span 14', contractor: 'ForestCare' }, { date: '12 May 2026', type: 'scheduled', action: 'Tree removal', asset: 'Span 14', contractor: 'Silva Verde Lda.' }, // Completed interventions carry a verified cut status (post-survey audit): // cut → tree fully removed // partial → trimming only / partial clearance // no-cut → contractor flagged but not actioned (re-survey needed) { date: '15 Mar 2026', type: 'completed', action: 'Tree removal', asset: 'Span 14', contractor: 'ForestCare', status: 'cut', trees: 7 }, { date: '28 Feb 2026', type: 'completed', action: 'Corridor clearance',asset: 'Span 7', contractor: 'Silva Verde Lda.', status: 'cut', trees: 5 }, { date: '14 Feb 2026', type: 'completed', action: 'Tree trimming', asset: 'Span 3', contractor: 'ForestCare', status: 'partial', trees: 4 }, { date: '30 Jan 2026', type: 'completed', action: 'Tree removal', asset: 'Span 18', contractor: 'Silva Verde Lda.', status: 'cut', trees: 6 }, { date: '22 Jan 2026', type: 'completed', action: 'Tree trimming', asset: 'Span 11', contractor: 'ForestCare', status: 'partial', trees: 5 }, { date: '10 Jan 2026', type: 'completed', action: 'Corridor clearance',asset: 'Span 5', contractor: 'Silva Verde Lda.', status: 'no-cut', trees: 3 }, { date: '18 Dec 2025', type: 'completed', action: 'Tree removal', asset: 'Span 2', contractor: 'ForestCare', status: 'cut', trees: 4 }, { date: '4 Dec 2025', type: 'completed', action: 'Tree trimming', asset: 'Span 17', contractor: 'Silva Verde Lda.', status: 'partial', trees: 4 }, { date: '21 Nov 2025', type: 'completed', action: 'Corridor clearance',asset: 'Span 14', contractor: 'ForestCare', status: 'no-cut', trees: 3 }, ]; // Cut-status taxonomy used by the Interventions map view + table column. const CUT_STATUS = { 'cut': { label: 'Cut', color: 'var(--status-stable)', bg: 'var(--status-stable-bg)' }, 'partial': { label: 'Partial cut', color: 'var(--status-alert)', bg: 'var(--status-alert-bg)' }, 'no-cut': { label: 'No cut', color: 'var(--status-alarm)', bg: 'var(--status-alarm-bg)' }, }; // Analysis history for the date dropdown. const ANALYSIS_DATES = [ '12 Mar 2026', '14 Feb 2026', '18 Jan 2026', '11 Dec 2025', '5 Nov 2025', ]; // ───────────────────────────────────────────────────────────────────────────── // TREE CLUSTERS (TreeGuard 1.1) // ───────────────────────────────────────────────────────────────────────────── // Spotlite resolves vegetation as CLUSTERS of neighbouring trees, not isolated // stems. On the map a cluster renders as the union of its canopies (a silhouette // seen from above); the dashboard tabulates the cluster summaries. This array is // the SINGLE source of truth — the map projects each member onto the corridor // geometry, the dashboard reads the derived fields, so the two never drift. // // A cluster's risk tier is the WORST tier of any member tree, evaluated against // the active Safety Corridor half-width C (default 50 m): // member dist ≤ C → tree stands INSIDE the corridor → Alarm // member (dist − fall) ≤ C → its fall radius reaches the corridor → Alert // otherwise → trunk + fall radius both clear → Clear // Per member: da = along-span metres from the cluster anchor (signed) // dist = perpendicular metres from the conductor centre-line // fall = fall radius (≈ tree height), crownR = crown radius (m) // The map reads (da, dist) to place the canopy; classification uses (dist, fall). const _clrng = (() => { let s = 0x5eed42; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return (s >>> 0) / 0xFFFFFFFF; }; })(); const _CL_SPECIES = ['Pine', 'Cork Oak', 'Eucalyptus', 'Chestnut']; // Cluster anchors. `kind` only shapes how members are seeded; the live tier is // always derived from member geometry so the corridor slider re-classifies them. // t = position of the cluster centroid along its span (0..1) // side = which flank of the corridor (+1 / −1) // base = perpendicular metres from the line to the NEAR edge of the cluster // depth= perpendicular metres the stand extends outward from `base` // n = member tree count const _CLUSTER_SEEDS = [ // Alarm clusters — straddle the corridor edge: near trees inside, far trees clear. { span: 14, t: 0.46, side: 1, base: 34, depth: 52, n: 12, kind: 'alarm' }, { span: 8, t: 0.54, side: -1, base: 30, depth: 58, n: 15, kind: 'alarm' }, { span: 21, t: 0.42, side: 1, base: 38, depth: 46, n: 10, kind: 'alarm' }, { span: 9, t: 0.58, side: -1, base: 40, depth: 44, n: 9, kind: 'alarm' }, { span: 16, t: 0.50, side: 1, base: 32, depth: 50, n: 11, kind: 'alarm' }, // Alert clusters — trunks clear of the corridor, near canopies' fall radius reaches in. { span: 3, t: 0.46, side: -1, base: 54, depth: 40, n: 8, kind: 'alert' }, { span: 5, t: 0.55, side: 1, base: 56, depth: 44, n: 10, kind: 'alert' }, { span: 11, t: 0.40, side: -1, base: 58, depth: 38, n: 7, kind: 'alert' }, { span: 12, t: 0.60, side: 1, base: 55, depth: 42, n: 9, kind: 'alert' }, { span: 18, t: 0.48, side: -1, base: 57, depth: 40, n: 8, kind: 'alert' }, { span: 19, t: 0.44, side: 1, base: 59, depth: 36, n: 7, kind: 'alert' }, // Clear clusters — whole stand outside the corridor + fall reach. { span: 2, t: 0.50, side: 1, base: 92, depth: 30, n: 7, kind: 'stable' }, { span: 6, t: 0.50, side: -1, base: 104, depth: 32, n: 6, kind: 'stable' }, { span: 13, t: 0.50, side: 1, base: 116, depth: 34, n: 8, kind: 'stable' }, { span: 17, t: 0.50, side: -1, base: 98, depth: 28, n: 6, kind: 'stable' }, { span: 22, t: 0.50, side: 1, base: 110, depth: 32, n: 7, kind: 'stable' }, ]; const _tierOf = (dist, fall, C) => (dist <= C ? 'alarm' : (dist - fall <= C ? 'alert' : 'stable')); const _TIER_RANK = { stable: 0, alert: 1, alarm: 2 }; const BASE_CORRIDOR_M = 50; const CLUSTERS = _CLUSTER_SEEDS.map((seed, ci) => { const letter = String.fromCharCode(65 + ci % 26); const members = []; for (let j = 0; j < seed.n; j++) { // Tight along the span (≤ ±16 m) so the stand reads as one canopy mass, // but deep perpendicular so its near edge can breach while the far edge stays clear. const da = (_clrng() - 0.5) * Math.min(seed.depth, 32); const dp = _clrng() * seed.depth; // 0 … depth, from the near edge outward const dist = Math.max(2, +(seed.base + dp).toFixed(1)); const h = seed.kind === 'alarm' ? 16 + _clrng() * 9 : seed.kind === 'alert' ? 13 + _clrng() * 8 : 8 + _clrng() * 8; const fall = +(h * 1.08).toFixed(1); const crownR = +(3.5 + _clrng() * 3.5).toFixed(1); members.push({ da: +da.toFixed(1), dist, h: +h.toFixed(1), fall, crownR, species: _CL_SPECIES[Math.floor(_clrng() * _CL_SPECIES.length)], }); } members.sort((a, b) => a.dist - b.dist); // nearest first // Derived summary at the baseline corridor. let worst = 'stable'; members.forEach(m => { const t = _tierOf(m.dist, m.fall, BASE_CORRIDOR_M); m.tier = t; if (_TIER_RANK[t] > _TIER_RANK[worst]) worst = t; }); const breaching = members.filter(m => m.tier !== 'stable').length; const closest = members[0]; // Dominant species = most frequent among members. const counts = {}; members.forEach(m => { counts[m.species] = (counts[m.species] || 0) + 1; }); const dominantSpecies = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0]; const tallest = members.reduce((mx, m) => Math.max(mx, m.h), 0); return { id: `CL-${seed.span}${letter}`, label: `Cluster ${seed.span}·${letter}`, spanNum: seed.span, span: `Span ${seed.span}`, anchorT: seed.t, side: seed.side, tier: worst, treeCount: seed.n, breaching, closestDist: closest.dist, dominantSpecies, tallest: +tallest.toFixed(1), fall: closest.fall, lat: (40.5248 + seed.span * 0.0009 + ci * 0.00012).toFixed(4), lng: (-7.2541 - seed.span * 0.0008 - ci * 0.00010).toFixed(4), members, }; }); // Cluster KPIs (totals used by panels / list counts). const CLUSTER_KPIS = { total: CLUSTERS.length, alarm: CLUSTERS.filter(c => c.tier === 'alarm').length, alert: CLUSTERS.filter(c => c.tier === 'alert').length, stable: CLUSTERS.filter(c => c.tier === 'stable').length, trees: CLUSTERS.reduce((s, c) => s + c.treeCount, 0), }; Object.assign(window, { RISK, KPIS, SPAN_KPIS, CRITICAL_ASSETS, ALARM_TREES, HIGH_RISK_TREES, INTERVENTIONS, CUT_STATUS, ANALYSIS_DATES, CLUSTERS, CLUSTER_KPIS });