// PumpingStation editor — interactive basin SVG (top of the editor). // Places threshold lines, derived safety levels, zone labels, dead-volume // band, and ordering warnings. Same formulas as // specificClass._validateThresholdOrdering. (function () { const ns = window.PSEditor = window.PSEditor || {}; const fNum = (id) => ns.fNum(id); // viewBox y bounds of the tank rect (now 120,40)..(240,380); width // shrunk to 360 in the new side-panel layout. y-bounds unchanged. const DIAG = { topY: 40, botY: 380 }; const yForLevel = (val, basinH) => { if (val == null || !basinH) return null; const y = DIAG.botY - (val / basinH) * (DIAG.botY - DIAG.topY); return Math.max(DIAG.topY - 8, Math.min(DIAG.botY + 8, y)); }; // Place a row — line, label, input, unit all share the same y. const placeItem = (id, y) => { const line = document.getElementById(`ps-line-${id}`); const label = document.getElementById(`ps-label-${id}`); const unit = document.getElementById(`ps-unit-${id}`); const fo = document.getElementById(`ps-fo-${id}`); const sub = document.getElementById(`ps-sub-${id}`); const lead = document.getElementById(`ps-leader-${id}`); if (line) { line.setAttribute('y1', y); line.setAttribute('y2', y); } if (label) label.setAttribute('y', y + 4); if (unit) unit.setAttribute('y', y + 4); if (fo) fo.setAttribute('y', y - 11); if (sub) sub.setAttribute('y', y + 15); if (lead) lead.setAttribute('visibility', 'hidden'); }; ns.basinDiagram = { redraw() { const basinH = fNum('basinHeight') || 5; const refLow = fNum('outflowLevel'); const dryPct = fNum('dryRunThresholdPercent'); const highPct = fNum('highVolumeSafetyThresholdPercent'); const ovf = fNum('overflowLevel'); const dryLvl = (refLow != null && dryPct != null) ? refLow * (1 + dryPct / 100) : null; const highLvl = (ovf != null && highPct != null) ? ovf * (highPct / 100) : null; // Right-column stack. TWO anchors: basinHeight pinned at the rim, // outflowLevel pinned at its proportional y. Two passes (top-down + // bottom-up) maintain a minimum vertical gap. const items = [ { id: 'basinHeight', yIdeal: DIAG.topY, pinned: true }, { id: 'overflowLevel', yIdeal: yForLevel(fNum('overflowLevel'), basinH) }, { id: 'highVolumeSafetyLevel', yIdeal: yForLevel(highLvl, basinH) }, { id: 'inflowLevelGuide', yIdeal: yForLevel(fNum('inflowLevel'), basinH) }, { id: 'dryRunLevel', yIdeal: yForLevel(dryLvl, basinH) }, { id: 'outflowLevel', yIdeal: yForLevel(fNum('outflowLevel'), basinH), pinned: true }, ].filter(it => it.yIdeal != null); const GAP = 36; items.sort((a, b) => a.yIdeal - b.yIdeal); for (const it of items) it.y = it.yIdeal; for (let i = 1; i < items.length; i++) { if (items[i].pinned) continue; items[i].y = Math.max(items[i].y, items[i - 1].y + GAP); } for (let i = items.length - 2; i >= 0; i--) { if (items[i].pinned) continue; items[i].y = Math.min(items[i].y, items[i + 1].y - GAP); } for (const it of items) placeItem(it.id, it.y); const placeZone = (zoneId, topId, botId) => { const el = document.getElementById(`ps-zone-${zoneId}`); if (!el) return; const top = items.find(it => it.id === topId); const bot = items.find(it => it.id === botId); if (!top || !bot || (bot.y - top.y) < 14) { el.setAttribute('visibility', 'hidden'); return; } el.setAttribute('y', (top.y + bot.y) / 2 + 3); el.setAttribute('visibility', 'visible'); }; placeZone('spare', 'overflowLevel', 'highVolumeSafetyLevel'); placeZone('sewage', 'highVolumeSafetyLevel', 'inflowLevelGuide'); placeZone('buffer1', 'inflowLevelGuide', 'dryRunLevel'); placeZone('buffer2', 'dryRunLevel', 'outflowLevel'); const outflowPinned = items.find(it => it.id === 'outflowLevel'); const deadLbl = document.getElementById('ps-zone-dead'); if (deadLbl && outflowPinned && (DIAG.botY - outflowPinned.y) > 14) { deadLbl.setAttribute('y', (outflowPinned.y + DIAG.botY) / 2 + 3); deadLbl.setAttribute('visibility', 'visible'); } else if (deadLbl) { deadLbl.setAttribute('visibility', 'hidden'); } const inflowY = yForLevel(fNum('inflowLevel'), basinH); if (inflowY != null) { const line = document.getElementById('ps-line-inflowLevel'); const lbl = document.getElementById('ps-label-inflowLevel'); const sub = document.getElementById('ps-sub-inflowLevel'); const fo = document.getElementById('ps-fo-inflowLevel'); const unit = document.getElementById('ps-unit-inflowLevel'); if (line) { line.setAttribute('y1', inflowY); line.setAttribute('y2', inflowY); } if (lbl) lbl.setAttribute('y', inflowY - 4); if (sub) sub.setAttribute('y', inflowY + 8); if (fo) fo.setAttribute('y', inflowY - 11); if (unit) unit.setAttribute('y', inflowY + 4); } const outflowItem = items.find(it => it.id === 'outflowLevel'); const deadvol = document.getElementById('ps-deadvol'); if (deadvol && outflowItem) { deadvol.setAttribute('y', outflowItem.y); deadvol.setAttribute('height', Math.max(0, DIAG.botY - outflowItem.y)); } // SVG labels — keep them short, side panel shows the numeric value. const dryLbl = document.getElementById('ps-label-dryRunLevel'); if (dryLbl) dryLbl.textContent = 'dryRunLevel'; const highLbl = document.getElementById('ps-label-highVolumeSafetyLevel'); if (highLbl) highLbl.textContent = 'highVolumeSafety'; // Side-panel read-only displays — number only ("m" is shown in the unit span). const fmt = (v) => Number.isFinite(v) ? v.toFixed(2) : '—'; const d1 = document.getElementById('derived-dryRunLevel'); if (d1) d1.textContent = fmt(dryLvl); const d2 = document.getElementById('derived-highVolumeSafetyLevel'); if (d2) d2.textContent = fmt(highLvl); const warn = document.getElementById('ps-warning'); const issues = []; const pairs = [ ['outflowLevel', 'inflowLevel', '<'], ['inflowLevel', 'overflowLevel', '<'], ]; for (const [a, b, op] of pairs) { const av = fNum(a), bv = fNum(b); if (av == null || bv == null) continue; if (op === '<' ? !(av < bv) : !(av <= bv)) issues.push(`${a} ${op} ${b}`); } if (warn) { if (issues.length) { warn.setAttribute('visibility', 'visible'); warn.textContent = `⚠ Check ordering: ${issues.join(', ')}`; } else { warn.setAttribute('visibility', 'hidden'); } } }, }; })();