148 lines
6.8 KiB
JavaScript
148 lines
6.8 KiB
JavaScript
|
|
// 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'); }
|
||
|
|
}
|
||
|
|
},
|
||
|
|
};
|
||
|
|
})();
|