Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js): - Replace direction-based hysteresis with level-armed _shiftArmed state. Arms when level rises past shiftLevel; disarms when level drops below startLevel. While armed, ramp foot moves to startLevel and ramp top to shiftLevel — both ends shift left, then saturate at 100 % up to maxLevel. - _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so the saturation point follows the shift state. - New setManualOutflow mirroring setManualInflow. Adapter (nodeClass.js): - Pipe enableShiftedRamp / shiftLevel through to control.levelbased. - New q_out topic handler. Editor (pumpingStation.html + new src/editor/ modules): - Split monolithic <script> into modules: index.js (helpers), basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js, oneditsave.js — served via /pumpingStation/editor/:file. - Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 % flat from start→inlet, ramp inlet→max, optional shifted-down curve start→shift with 100 % saturation past shift. - Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh / overflow), level markers (dryRun derived, start, inlet, max, shift, overflow), validation ribbon that blocks save on bad ordering. - Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is always visible. - All level inputs moved to a side panel left of each diagram, color- coded to match line strokes; hover-couple highlights the paired SVG line on input focus / mouseover. - Removed UI for non-static parameters: minHeightBasedOn, pipelineLength, maxDischargeHead, staticHead, defaultFluid, maxInflowRate, temperatureReferenceDegC, timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter, outletPipeDiameter, minLevel (now derived = dryRunLevel). - foreignObject inputs in basin SVG removed (single source of truth in side panel). Dashboard example (examples/basic-dashboard.flow.json): - Add manual Q_OUT slider + q_out builder mirroring the existing q_in trio so the basin can be exercised end-to-end without a connected rotating-machine downstream. Tests (test/basic/specificClass.test.js): - Replace direction-shift test with two new cases covering shift-disabled hold-zone behaviour and shift-armed/disarmed transitions through shiftLevel and startLevel boundaries. 53/53 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,17 @@
|
||||
| **Control Module** | `#a9daee` | zwart |
|
||||
|
||||
-->
|
||||
<script src="/pumpingStation/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
|
||||
<script src="/pumpingStation/configData.js"></script> <!-- Load the config script for node information -->
|
||||
<script src="/pumpingStation/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
|
||||
<script src="/pumpingStation/configData.js"></script> <!-- Load the config script for node information -->
|
||||
|
||||
<!-- Editor JS modules — see nodes/pumpingStation/src/editor/. Loaded in
|
||||
dependency order: index.js (namespace + helpers) → diagrams → handlers. -->
|
||||
<script src="/pumpingStation/editor/index.js"></script>
|
||||
<script src="/pumpingStation/editor/basin-diagram.js"></script>
|
||||
<script src="/pumpingStation/editor/mode-preview.js"></script>
|
||||
<script src="/pumpingStation/editor/hover-couple.js"></script>
|
||||
<script src="/pumpingStation/editor/oneditprepare.js"></script>
|
||||
<script src="/pumpingStation/editor/oneditsave.js"></script>
|
||||
|
||||
<script>//test
|
||||
RED.nodes.registerType("pumpingStation", {
|
||||
@@ -22,8 +31,8 @@
|
||||
simulator: { value: false },
|
||||
basinVolume: { value: 1 }, // m³, total empty basin
|
||||
basinHeight: { value: 1 }, // m, floor to top
|
||||
inflowLevel: { value: 0.8 }, // m, centre of inlet pipe above floor
|
||||
outflowLevel: { value: 0.2 }, // m, centre of outlet pipe above floor
|
||||
inflowLevel: { value: 0.8 }, // m, bottom/invert of inlet pipe above floor
|
||||
outflowLevel: { value: 0.2 }, // m, top of outlet/suction pipe above floor
|
||||
overflowLevel: { value: 0.9 }, // m, overflow elevation
|
||||
defaultFluid: { value: "wastewater" },
|
||||
inletPipeDiameter: { value: 0.3 }, // m
|
||||
@@ -35,9 +44,11 @@
|
||||
temperatureReferenceDegC: { value: 15 },
|
||||
timeleftToFullOrEmptyThresholdSeconds:{value:0}, // time threshold to safeguard starting or stopping pumps in seconds
|
||||
enableDryRunProtection: { value: true },
|
||||
enableOverfillProtection: { value: true },
|
||||
enableHighVolumeSafety: { value: true },
|
||||
enableOverfillProtection: { value: true }, // deprecated alias
|
||||
dryRunThresholdPercent: { value: 2 },
|
||||
overfillThresholdPercent: { value: 98 },
|
||||
highVolumeSafetyThresholdPercent: { value: 98 },
|
||||
overfillThresholdPercent: { value: 98 }, // deprecated alias
|
||||
minHeightBasedOn: { value: "outlet" }, // basis for minimum height check: inlet or outlet
|
||||
processOutputFormat: { value: "process" },
|
||||
dbaseOutputFormat: { value: "influxdb" },
|
||||
@@ -67,7 +78,11 @@
|
||||
distanceDescription: { value: "" },
|
||||
|
||||
// control strategy
|
||||
controlMode: { value: "none" },
|
||||
controlMode: { value: "levelbased" },
|
||||
levelCurveType: { value: "linear" },
|
||||
logCurveFactor: { value: 9 },
|
||||
enableShiftedRamp: { value: false },
|
||||
shiftLevel: { value: 0 },
|
||||
startLevel: { value: null },
|
||||
minLevel: { value: null },
|
||||
maxLevel: { value: null },
|
||||
@@ -86,296 +101,11 @@
|
||||
return this.positionIcon + " PumpingStation";
|
||||
},
|
||||
|
||||
oneditprepare: function() {
|
||||
const waitForMenuData = () => {
|
||||
if (window.EVOLV?.nodes?.pumpingStation?.initEditor) {
|
||||
window.EVOLV.nodes.pumpingStation.initEditor(this);
|
||||
} else {
|
||||
setTimeout(waitForMenuData, 50);
|
||||
}
|
||||
};
|
||||
// Wait for the menu data to be ready before initializing the editor
|
||||
waitForMenuData();
|
||||
|
||||
// NODE SPECIFIC
|
||||
document.getElementById("node-input-basinVolume");
|
||||
document.getElementById("node-input-basinHeight");
|
||||
document.getElementById("node-input-inflowLevel");
|
||||
document.getElementById("node-input-outflowLevel");
|
||||
document.getElementById("node-input-overflowLevel");
|
||||
document.getElementById("node-input-refHeight");
|
||||
document.getElementById("node-input-basinBottomRef");
|
||||
|
||||
const refHeightEl = document.getElementById("node-input-refHeight");
|
||||
if (refHeightEl) {
|
||||
refHeightEl.value = this.refHeight || "NAP";
|
||||
}
|
||||
|
||||
const minHeightBasedOnEl = document.getElementById("node-input-minHeightBasedOn");
|
||||
if (minHeightBasedOnEl) {
|
||||
minHeightBasedOnEl.value = this.minHeightBasedOn;
|
||||
}
|
||||
|
||||
const dryRunToggle = document.getElementById("node-input-enableDryRunProtection");
|
||||
const dryRunPercent = document.getElementById("node-input-dryRunThresholdPercent");
|
||||
const overfillToggle = document.getElementById("node-input-enableOverfillProtection");
|
||||
const overfillPercent = document.getElementById("node-input-overfillThresholdPercent");
|
||||
|
||||
const toggleInput = (toggleEl, inputEl) => {
|
||||
if (!toggleEl || !inputEl) { return; }
|
||||
inputEl.disabled = !toggleEl.checked;
|
||||
inputEl.parentElement.classList.toggle('disabled', inputEl.disabled);
|
||||
};
|
||||
|
||||
if (dryRunToggle && dryRunPercent) {
|
||||
dryRunToggle.checked = !!this.enableDryRunProtection;
|
||||
dryRunPercent.value = Number.isFinite(this.dryRunThresholdPercent) ? this.dryRunThresholdPercent : 2;
|
||||
dryRunToggle.addEventListener('change', () => toggleInput(dryRunToggle, dryRunPercent));
|
||||
toggleInput(dryRunToggle, dryRunPercent);
|
||||
}
|
||||
|
||||
if (overfillToggle && overfillPercent) {
|
||||
overfillToggle.checked = !!this.enableOverfillProtection;
|
||||
overfillPercent.value = Number.isFinite(this.overfillThresholdPercent) ? this.overfillThresholdPercent : 98;
|
||||
overfillToggle.addEventListener('change', () => toggleInput(overfillToggle, overfillPercent));
|
||||
toggleInput(overfillToggle, overfillPercent);
|
||||
}
|
||||
|
||||
const timeLeftInput = document.getElementById("node-input-timeleftToFullOrEmptyThresholdSeconds");
|
||||
if (timeLeftInput) {
|
||||
timeLeftInput.value = Number.isFinite(this.timeleftToFullOrEmptyThresholdSeconds)
|
||||
? this.timeleftToFullOrEmptyThresholdSeconds
|
||||
: 0;
|
||||
}
|
||||
|
||||
// control mode toggle UI
|
||||
const toggleModeSections = (val) => {
|
||||
document.querySelectorAll('.ps-mode-section').forEach((el) => el.style.display = 'none');
|
||||
const active = document.getElementById(`ps-mode-${val}`);
|
||||
if (active) active.style.display = '';
|
||||
};
|
||||
|
||||
const modeSelect = document.getElementById('node-input-controlMode');
|
||||
if (modeSelect) {
|
||||
modeSelect.value = this.controlMode || 'none';
|
||||
toggleModeSections(modeSelect.value);
|
||||
modeSelect.addEventListener('change', (e) => toggleModeSections(e.target.value));
|
||||
}
|
||||
|
||||
const setNumberField = (id, val) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = Number.isFinite(val) ? val : '';
|
||||
};
|
||||
|
||||
setNumberField('node-input-startLevel', this.startLevel);
|
||||
setNumberField('node-input-minLevel', this.minLevel);
|
||||
setNumberField('node-input-maxLevel', this.maxLevel);
|
||||
setNumberField('node-input-flowSetpoint', this.flowSetpoint);
|
||||
setNumberField('node-input-flowDeadband', this.flowDeadband);
|
||||
|
||||
// Interactive diagram: place every threshold line/input at its
|
||||
// proportional y on the tank, plus compute derived safety levels
|
||||
// (dryRunLevel, overfillLevel) that are shown both in the diagram
|
||||
// and next to the safety-% fields. Same formulas as
|
||||
// specificClass._validateThresholdOrdering.
|
||||
const DIAG = { topY: 40, botY: 380 };
|
||||
const fNum = (id) => {
|
||||
const v = parseFloat(document.getElementById(`node-input-${id}`)?.value);
|
||||
return Number.isFinite(v) ? v : null;
|
||||
};
|
||||
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.
|
||||
// The diagram is a schematic ordered list (value order is
|
||||
// preserved, but the y-positions are distributed with a
|
||||
// guaranteed minimum gap for readability), not a strictly
|
||||
// proportional rendering.
|
||||
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');
|
||||
};
|
||||
|
||||
const redraw = () => {
|
||||
const basinH = fNum('basinHeight') || 5;
|
||||
|
||||
// Derived safety levels (participate in the right-column stack)
|
||||
const basedOn = document.getElementById('node-input-minHeightBasedOn')?.value || 'outlet';
|
||||
const refLow = basedOn === 'inlet' ? fNum('inflowLevel') : fNum('outflowLevel');
|
||||
const dryPct = fNum('dryRunThresholdPercent');
|
||||
const ovfPct = fNum('overfillThresholdPercent');
|
||||
const ovf = fNum('overflowLevel');
|
||||
const dryLvl = (refLow != null && dryPct != null) ? refLow * (1 + dryPct / 100) : null;
|
||||
const ovfLvl = (ovf != null && ovfPct != null) ? ovf * (ovfPct / 100) : null;
|
||||
|
||||
// Right-column stack. TWO anchors: basinHeight pinned at the
|
||||
// tank rim (top) and outflowLevel pinned at its proportional y
|
||||
// (bottom). Everything between is nudged to maintain a minimum
|
||||
// vertical gap via two passes — top-down from the rim, then
|
||||
// bottom-up from the outlet — so the dashed lines keep their
|
||||
// value-order and outlet stays near the floor where it belongs.
|
||||
const items = [
|
||||
{ id: 'basinHeight', yIdeal: DIAG.topY, pinned: true },
|
||||
{ id: 'overflowLevel', yIdeal: yForLevel(fNum('overflowLevel'), basinH) },
|
||||
{ id: 'maxLevel', yIdeal: yForLevel(fNum('maxLevel'), basinH) },
|
||||
{ id: 'startLevel', yIdeal: yForLevel(fNum('startLevel'), basinH) },
|
||||
{ id: 'minLevel', yIdeal: yForLevel(fNum('minLevel'), 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;
|
||||
// Pass 1: top-down — push DOWN to maintain GAP; pinned items don't move
|
||||
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);
|
||||
}
|
||||
// Pass 2: bottom-up — push UP so outflow's pin propagates up the stack
|
||||
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);
|
||||
|
||||
// Zone labels between adjacent thresholds (italic, centered).
|
||||
// Hidden if either bracketing threshold is missing, or the gap
|
||||
// is too small to read (< 14 px).
|
||||
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', 'maxLevel');
|
||||
placeZone('sewage', 'maxLevel', 'startLevel');
|
||||
placeZone('buffer1', 'startLevel', 'minLevel');
|
||||
placeZone('buffer2', 'minLevel', 'dryRunLevel');
|
||||
// "Dead volume" sits inside the blue band between outflowLevel and the floor
|
||||
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');
|
||||
}
|
||||
|
||||
// Inlet arrow — sole item on the left, no stacking concerns
|
||||
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);
|
||||
}
|
||||
|
||||
// Dead-volume band: from the (possibly-nudged) outflow line
|
||||
// down to the floor. Use the nudged y so the band meets the
|
||||
// outflow line exactly.
|
||||
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));
|
||||
}
|
||||
|
||||
// dryRunLevel label text (derived, read-only)
|
||||
const dryLbl = document.getElementById('ps-label-dryRunLevel');
|
||||
if (dryLbl) dryLbl.textContent = dryLvl != null
|
||||
? `dryRunLevel ≈ ${dryLvl.toFixed(2)} m (safety — from %)`
|
||||
: 'dryRunLevel ≈ — m (safety — from %)';
|
||||
|
||||
// Safety-section readouts (second view, beneath the diagram)
|
||||
const d1 = document.getElementById('derived-dryRunLevel');
|
||||
if (d1) d1.textContent = dryLvl != null ? `→ dryRunLevel ≈ ${dryLvl.toFixed(2)} m` : '→ dryRunLevel ≈ — m';
|
||||
const d2 = document.getElementById('derived-overfillLevel');
|
||||
if (d2) d2.textContent = ovfLvl != null ? `→ overfillLevel ≈ ${ovfLvl.toFixed(2)} m` : '→ overfillLevel ≈ — m';
|
||||
|
||||
// Ordering warning ribbon
|
||||
const warn = document.getElementById('ps-warning');
|
||||
const issues = [];
|
||||
const pairs = [
|
||||
['outflowLevel', 'inflowLevel', '<'],
|
||||
['inflowLevel', 'overflowLevel', '<'],
|
||||
['minLevel', 'startLevel', '<='],
|
||||
['startLevel', 'maxLevel', '<'],
|
||||
['maxLevel', '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'); }
|
||||
}
|
||||
};
|
||||
['basinHeight','overflowLevel','maxLevel','startLevel','minLevel','inflowLevel','outflowLevel',
|
||||
'dryRunThresholdPercent','overfillThresholdPercent','minHeightBasedOn'].forEach((id) => {
|
||||
const el = document.getElementById(`node-input-${id}`);
|
||||
if (el) { el.addEventListener('input', redraw); el.addEventListener('change', redraw); }
|
||||
});
|
||||
setTimeout(redraw, 60);
|
||||
|
||||
//------------------- END OF CUSTOM config UI ELEMENTS ------------------- //
|
||||
oneditprepare: function () {
|
||||
window.PSEditor.oneditprepare.call(this);
|
||||
},
|
||||
oneditsave: function () {
|
||||
const node = this;
|
||||
|
||||
//window.EVOLV?.nodes?.pumpingStation?.assetMenu?.saveEditor?.(node);
|
||||
window.EVOLV?.nodes?.pumpingStation?.loggerMenu?.saveEditor?.(node);
|
||||
window.EVOLV?.nodes?.pumpingStation?.positionMenu?.saveEditor?.(node);
|
||||
|
||||
//node specific
|
||||
node.refHeight = document.getElementById("node-input-refHeight").value || "NAP";
|
||||
node.minHeightBasedOn = document.getElementById("node-input-minHeightBasedOn").value || "outlet";
|
||||
node.simulator = document.getElementById("node-input-simulator").checked;
|
||||
|
||||
["basinVolume","basinHeight","inflowLevel","outflowLevel","overflowLevel","basinBottomRef","timeleftToFullOrEmptyThresholdSeconds","dryRunThresholdPercent","overfillThresholdPercent"]
|
||||
.forEach(field => {
|
||||
node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0;
|
||||
});
|
||||
|
||||
node.refHeight = document.getElementById("node-input-refHeight").value || "";
|
||||
node.enableDryRunProtection = document.getElementById("node-input-enableDryRunProtection").checked;
|
||||
node.enableOverfillProtection = document.getElementById("node-input-enableOverfillProtection").checked;
|
||||
|
||||
// control strategy
|
||||
node.controlMode = document.getElementById('node-input-controlMode').value || 'none';
|
||||
|
||||
const parseNum = (id) => parseFloat(document.getElementById(id)?.value);
|
||||
node.startLevel = parseNum('node-input-startLevel');
|
||||
node.minLevel = parseNum('node-input-minLevel');
|
||||
node.maxLevel = parseNum('node-input-maxLevel');
|
||||
node.flowSetpoint = parseNum('node-input-flowSetpoint');
|
||||
node.flowDeadband = parseNum('node-input-flowDeadband');
|
||||
|
||||
window.PSEditor.oneditsave.call(this);
|
||||
},
|
||||
|
||||
});
|
||||
@@ -395,123 +125,199 @@
|
||||
<hr>
|
||||
|
||||
<h4>Basin parameters</h4>
|
||||
<p style="font-size:12px;color:#777;margin:0 0 8px 0;">Heights are measured from the basin floor (0 m). Enter values next to each line — the diagram scales to whatever you enter.</p>
|
||||
<p style="font-size:12px;color:#777;margin:0 0 8px 0;">Heights are measured from the basin floor (0 m). Each input on the left controls a line in the diagram on the right — hover an input to highlight its line.</p>
|
||||
|
||||
<style>
|
||||
#ps-basin-diagram input[type=number] {
|
||||
width: 100%; height: 20px; box-sizing: border-box;
|
||||
font-size: 11px; padding: 1px 4px; margin: 0;
|
||||
border: 1px solid #ccc; border-radius: 3px; background: #fff;
|
||||
/* Two-column layout: stacked colour-coded inputs on the left,
|
||||
SVG on the right. Hover an input row → its paired SVG line
|
||||
(referenced by data-couples-line) gets a thicker stroke. */
|
||||
.ps-diag { display:flex; gap:14px; align-items:flex-start; margin:0 0 14px 0; }
|
||||
.ps-diag-side { width: 200px; flex: 0 0 200px; display:flex; flex-direction:column; gap:6px; }
|
||||
.ps-diag-side .ps-row {
|
||||
display:grid; grid-template-columns: 1fr 78px 18px; align-items:center;
|
||||
gap:6px; padding:4px 6px 4px 10px; border-left:4px solid #ccc;
|
||||
background:#fafafa; border-radius:3px; font-size:11px; cursor:pointer;
|
||||
}
|
||||
#ps-basin-diagram input[type=number]:focus { outline: 1px solid #0c99d9; border-color: #0c99d9; }
|
||||
.ps-diag-side .ps-row:hover { background:#f0f0f0; }
|
||||
.ps-diag-side .ps-row.ps-readonly { background:#fff; cursor:default; opacity:0.85; }
|
||||
.ps-diag-side .ps-row label { font-weight:600; margin:0; line-height:1.2; }
|
||||
.ps-diag-side .ps-row .ps-sub { grid-column:1; font-size:10px; color:#888; font-weight:400; }
|
||||
.ps-diag-side .ps-row input[type=number] {
|
||||
width:100%; height:22px; box-sizing:border-box; font-size:11px;
|
||||
padding:1px 4px; margin:0; border:1px solid #ccc; border-radius:3px;
|
||||
background:#fff;
|
||||
}
|
||||
.ps-diag-side .ps-row input[type=number]:focus { outline:1px solid #0c99d9; border-color:#0c99d9; }
|
||||
.ps-diag-side .ps-row .ps-readonly-val {
|
||||
font-family:monospace; font-size:11px; color:#666; text-align:right;
|
||||
padding-right:4px;
|
||||
}
|
||||
.ps-diag-side .ps-row .ps-unit { color:#888; font-size:10px; }
|
||||
.ps-diag-svg { flex:1; min-width:0; }
|
||||
/* Border colours matched to each SVG line stroke. */
|
||||
.ps-row[data-stroke="#333"] { border-left-color:#333; }
|
||||
.ps-row[data-stroke="#C0392B"] { border-left-color:#C0392B; }
|
||||
.ps-row[data-stroke="#1E8449"] { border-left-color:#1E8449; }
|
||||
.ps-row[data-stroke="#1F4E79"] { border-left-color:#1F4E79; }
|
||||
.ps-row[data-stroke="#D68910"] { border-left-color:#D68910; }
|
||||
.ps-row[data-stroke="#888"] { border-left-color:#888; }
|
||||
.ps-row[data-stroke="#333"] label { color:#333; }
|
||||
.ps-row[data-stroke="#C0392B"] label { color:#C0392B; }
|
||||
.ps-row[data-stroke="#1E8449"] label { color:#1E8449; }
|
||||
.ps-row[data-stroke="#1F4E79"] label { color:#1F4E79; }
|
||||
.ps-row[data-stroke="#D68910"] label { color:#D68910; }
|
||||
.ps-row[data-stroke="#888"] label { color:#888; }
|
||||
/* Highlight class applied to the SVG line during input row hover. */
|
||||
.ps-line-highlight { stroke-width:3.5 !important; opacity:1 !important; }
|
||||
</style>
|
||||
|
||||
<svg id="ps-basin-diagram" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 430"
|
||||
style="display:block;width:100%;max-width:540px;margin:0 0 12px 0;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
|
||||
font-family="Arial,sans-serif" font-size="11">
|
||||
<defs>
|
||||
<marker id="ps-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1F4E79" />
|
||||
</marker>
|
||||
</defs>
|
||||
<!--
|
||||
============================================================
|
||||
BASIN DIAGRAM (ps-basin-diagram)
|
||||
============================================================
|
||||
Coordinate system: SVG viewBox is 520 (wide) × 430 (tall).
|
||||
Origin (0,0) is top-left. +x goes right. +y goes DOWN.
|
||||
Bigger y = lower on screen.
|
||||
|
||||
<!-- Tank body -->
|
||||
<rect x="200" y="40" width="120" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
|
||||
<!-- Dead-volume band (y + height updated dynamically below outflowLevel) -->
|
||||
<rect id="ps-deadvol" x="201" width="118" fill="#AACCE0" />
|
||||
<!-- basinVolume — pinned above the rim -->
|
||||
<text id="ps-label-basinVolume" x="330" y="19" fill="#333" font-weight="600">basin volume</text>
|
||||
<foreignObject id="ps-fo-basinVolume" x="425" y="4" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-basinVolume" min="0" step="0.1" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-basinVolume" x="500" y="19" fill="#555">m³</text>
|
||||
X-LANES (all viewBox units, edit any of these to shift a column):
|
||||
x ≈ 5..75 left input column (inlet number input)
|
||||
x = 80 inlet unit "m"
|
||||
x = 135 inlet text labels (right-aligned, anchor at x)
|
||||
x = 140..200 inlet arrow (line + arrow head into tank)
|
||||
x = 200..320 tank body (rect.x=200 width=120) — interior 201..319
|
||||
x = 195/325 threshold tick lines (extend 5 px outside tank)
|
||||
x = 260 mid-tank zone labels (centered)
|
||||
x = 320..360 outlet arrow
|
||||
x = 330 right-side label column ("overflowLevel", "Outlet", …)
|
||||
x = 365 outlet sub-text column
|
||||
x = 425..495 right input column (foreignObject inputs, width=70)
|
||||
x = 500 right unit column ("m", "m³")
|
||||
|
||||
<!-- Zone labels (mid-tank italic, positioned dynamically at midpoint between adjacent thresholds) -->
|
||||
<text id="ps-zone-spare" x="260" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare volume before spilling</text>
|
||||
<text id="ps-zone-sewage" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + tank buffer</text>
|
||||
<text id="ps-zone-buffer1" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
|
||||
<text id="ps-zone-buffer2" x="260" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
|
||||
<text id="ps-zone-dead" x="260" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead volume</text>
|
||||
Y-COORDINATES:
|
||||
y = 40 tank rim (basinHeight line)
|
||||
y = 380 tank floor / datum
|
||||
y = 410 ordering warning ribbon
|
||||
y = 19,44 "basin volume" / "basinHeight" labels (static)
|
||||
Threshold rows (overflowLevel, highVolumeSafetyLevel, inflowLevelGuide,
|
||||
dryRunLevel, outflowLevel, basinHeight tick) get y assigned
|
||||
DYNAMICALLY by the redraw() function around line 250-340 below.
|
||||
Their input row may be NUDGED off ideal-y to avoid overlap; a leader
|
||||
line (ps-leader-*) is then drawn between threshold y and input y.
|
||||
Zone-label rows (ps-zone-*) get y assigned dynamically to the midpoint
|
||||
between adjacent thresholds; they hide if the gap is too small.
|
||||
|
||||
HOW TO NUDGE OVERLAPPING LABELS:
|
||||
- For STATIC y values (hardcoded below): edit the inline y attribute.
|
||||
- For DYNAMIC y values: search redraw() for the element id and adjust
|
||||
the layout math (e.g. NUDGE_PX or the threshold-stack ordering).
|
||||
- For x: every label column above can be shifted by editing the inline
|
||||
x attribute on the relevant <text>/<line>/<foreignObject>.
|
||||
|
||||
<!-- basinHeight — always at tank rim (y=40 in viewBox coords) -->
|
||||
<line id="ps-line-basinHeight" x1="195" y1="40" x2="325" y2="40" stroke="#333" stroke-width="1.5" />
|
||||
<text id="ps-label-basinHeight" x="330" y="44" fill="#333">basinHeight</text>
|
||||
<foreignObject id="ps-fo-basinHeight" x="425" y="29" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-basinHeight" min="0" step="0.1" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-basinHeight" x="500" y="44" fill="#555">m</text>
|
||||
Note: dynamic line/label positioning lives in oneditprepare → redraw()
|
||||
further up in this file. Changing only the inline y here will be
|
||||
overridden on first redraw for any element whose id appears in redraw().
|
||||
============================================================
|
||||
-->
|
||||
<div class="ps-diag" id="ps-basin-wrap">
|
||||
<!-- LEFT: stacked colour-coded inputs. Hover a row → its paired SVG
|
||||
line (data-couples-line) is highlighted in the diagram. -->
|
||||
<div class="ps-diag-side">
|
||||
<div class="ps-row" data-stroke="#333" style="cursor:default;">
|
||||
<div><label>basinVolume</label><div class="ps-sub">total empty volume (no marker)</div></div>
|
||||
<input type="number" id="node-input-basinVolume" min="0" step="0.1" />
|
||||
<span class="ps-unit">m³</span>
|
||||
</div>
|
||||
<div class="ps-row" data-stroke="#333" data-couples-line="ps-line-basinHeight">
|
||||
<div><label>basinHeight</label><div class="ps-sub">floor → rim</div></div>
|
||||
<input type="number" id="node-input-basinHeight" min="0" step="0.1" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row" data-stroke="#C0392B" data-couples-line="ps-line-overflowLevel">
|
||||
<div><label>overflowLevel</label><div class="ps-sub">spill height</div></div>
|
||||
<input type="number" id="node-input-overflowLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row ps-readonly" data-stroke="#D68910" data-couples-line="ps-line-highVolumeSafetyLevel">
|
||||
<div><label>highVolumeSafety</label><div class="ps-sub">derived (overflow × %)</div></div>
|
||||
<span id="derived-highVolumeSafetyLevel" class="ps-readonly-val">— m</span>
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row" data-stroke="#1F4E79" data-couples-line="ps-line-inflowLevel">
|
||||
<div><label>inflowLevel</label><div class="ps-sub">bottom of inlet pipe</div></div>
|
||||
<input type="number" id="node-input-inflowLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row ps-readonly" data-stroke="#C0392B" data-couples-line="ps-line-dryRunLevel">
|
||||
<div><label>dryRunLevel</label><div class="ps-sub">derived (outflow × dry%)</div></div>
|
||||
<span id="derived-dryRunLevel" class="ps-readonly-val">— m</span>
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row" data-stroke="#1F4E79" data-couples-line="ps-line-outflowLevel">
|
||||
<div><label>outflowLevel</label><div class="ps-sub">top of outlet pipe</div></div>
|
||||
<input type="number" id="node-input-outflowLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row ps-readonly" data-stroke="#888" style="cursor:default;">
|
||||
<div><label>basinBottomRef</label><div class="ps-sub">floor above NAP (no marker)</div></div>
|
||||
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- RIGHT: SVG. The viewBox is now narrower (320 wide) since the right
|
||||
input column is gone — labels render inside the tank's right margin. -->
|
||||
<svg id="ps-basin-diagram" class="ps-diag-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 430"
|
||||
style="display:block;width:100%;max-width:380px;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
|
||||
font-family="Arial,sans-serif" font-size="11">
|
||||
<defs>
|
||||
<marker id="ps-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#1F4E79" />
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- Tank body — x=120,width=120 (was 200,120 in the old wider viewBox).
|
||||
Threshold tick lines extend a few px outside the tank walls. -->
|
||||
<rect x="120" y="40" width="120" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
|
||||
<rect id="ps-deadvol" x="121" width="118" fill="#AACCE0" />
|
||||
|
||||
<!-- overflowLevel -->
|
||||
<line id="ps-line-overflowLevel" x1="195" x2="325" stroke="#C0392B" stroke-dasharray="4 2" stroke-width="1.5" />
|
||||
<text id="ps-label-overflowLevel" x="330" fill="#C0392B">overflowLevel</text>
|
||||
<foreignObject id="ps-fo-overflowLevel" x="425" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-overflowLevel" min="0" step="0.01" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-overflowLevel" x="500" fill="#555">m</text>
|
||||
<!-- Mid-tank zone labels — centred at x=180. -->
|
||||
<text id="ps-zone-spare" x="180" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare volume before spilling</text>
|
||||
<text id="ps-zone-sewage" x="180" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + tank buffer</text>
|
||||
<text id="ps-zone-buffer1" x="180" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
|
||||
<text id="ps-zone-buffer2" x="180" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Tank buffer</text>
|
||||
<text id="ps-zone-dead" x="180" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead volume</text>
|
||||
|
||||
<!-- maxLevel -->
|
||||
<line id="ps-line-maxLevel" x1="195" x2="325" stroke="#D68910" stroke-dasharray="4 2" stroke-width="1.5" />
|
||||
<text id="ps-label-maxLevel" x="330" fill="#D68910">maxLevel</text>
|
||||
<foreignObject id="ps-fo-maxLevel" x="425" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-maxLevel" min="0" step="0.01" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-maxLevel" x="500" fill="#555">m</text>
|
||||
<!-- basinHeight tick at tank rim (y=40, static). -->
|
||||
<line id="ps-line-basinHeight" x1="115" y1="40" x2="245" y2="40" stroke="#333" stroke-width="1.5" />
|
||||
<text id="ps-label-basinHeight" x="250" y="44" fill="#333">basinHeight</text>
|
||||
|
||||
<!-- startLevel -->
|
||||
<line id="ps-line-startLevel" x1="195" x2="325" stroke="#1E8449" stroke-dasharray="4 2" stroke-width="1.5" />
|
||||
<text id="ps-label-startLevel" x="330" fill="#1E8449">startLevel</text>
|
||||
<foreignObject id="ps-fo-startLevel" x="425" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-startLevel" min="0" step="0.01" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-startLevel" x="500" fill="#555">m</text>
|
||||
<line id="ps-line-overflowLevel" x1="115" x2="245" stroke="#C0392B" stroke-dasharray="4 2" stroke-width="1.5" />
|
||||
<text id="ps-label-overflowLevel" x="250" fill="#C0392B">overflowLevel</text>
|
||||
|
||||
<!-- Inlet — arrow + input on the left -->
|
||||
<line id="ps-line-inflowLevel" x1="140" x2="200" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
||||
<text id="ps-label-inflowLevel" x="135" text-anchor="end" fill="#1F4E79" font-weight="bold">Inlet</text>
|
||||
<text id="ps-sub-inflowLevel" x="135" text-anchor="end" fill="#777" font-size="9">bottom of pipe</text>
|
||||
<foreignObject id="ps-fo-inflowLevel" x="5" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-inflowLevel" min="0" step="0.01" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-inflowLevel" x="80" fill="#555">m</text>
|
||||
<line id="ps-line-highVolumeSafetyLevel" x1="115" x2="245" stroke="#D68910" stroke-dasharray="1 2" stroke-width="1" opacity="0.7" />
|
||||
<text id="ps-label-highVolumeSafetyLevel" x="250" fill="#D68910" font-size="10" font-style="italic">highVolumeSafety</text>
|
||||
|
||||
<!-- minLevel -->
|
||||
<line id="ps-line-minLevel" x1="195" x2="325" stroke="#6C3483" stroke-dasharray="4 2" stroke-width="1.5" />
|
||||
<text id="ps-label-minLevel" x="330" fill="#6C3483">minLevel</text>
|
||||
<foreignObject id="ps-fo-minLevel" x="425" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-minLevel" min="0" step="0.01" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-minLevel" x="500" fill="#555">m</text>
|
||||
<line id="ps-line-inflowLevelGuide" x1="120" x2="240" stroke="#1F4E79" stroke-dasharray="2 3" stroke-width="1" opacity="0.55" />
|
||||
<text id="ps-label-inflowLevelGuide" x="250" fill="#1F4E79" font-size="10" font-style="italic">inlet invert</text>
|
||||
|
||||
<!-- dryRunLevel (derived, read-only) -->
|
||||
<line id="ps-line-dryRunLevel" x1="195" x2="325" stroke="#C0392B" stroke-dasharray="1 2" stroke-width="1" opacity="0.6" />
|
||||
<text id="ps-label-dryRunLevel" x="330" fill="#C0392B" font-size="10" font-style="italic">dryRunLevel ≈ — m (safety — from %)</text>
|
||||
<line id="ps-line-inflowLevel" x1="60" x2="120" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
||||
<text id="ps-label-inflowLevel" x="55" text-anchor="end" fill="#1F4E79" font-weight="bold">Inlet</text>
|
||||
<text id="ps-sub-inflowLevel" x="55" text-anchor="end" fill="#777" font-size="9">bottom of pipe</text>
|
||||
|
||||
<!-- Outlet — arrow on right, input below the threshold column -->
|
||||
<line id="ps-line-outflowLevel" x1="320" x2="360" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
||||
<text id="ps-label-outflowLevel" x="365" fill="#1F4E79" font-weight="bold">Outlet</text>
|
||||
<text id="ps-sub-outflowLevel" x="365" fill="#777" font-size="9">top of pipe</text>
|
||||
<foreignObject id="ps-fo-outflowLevel" x="425" width="70" height="22">
|
||||
<input xmlns="http://www.w3.org/1999/xhtml" type="number" id="node-input-outflowLevel" min="0" step="0.01" />
|
||||
</foreignObject>
|
||||
<text id="ps-unit-outflowLevel" x="500" fill="#555">m</text>
|
||||
<line id="ps-line-dryRunLevel" x1="115" x2="245" stroke="#C0392B" stroke-dasharray="1 2" stroke-width="1" opacity="0.6" />
|
||||
<text id="ps-label-dryRunLevel" x="250" fill="#C0392B" font-size="10" font-style="italic">dryRunLevel</text>
|
||||
|
||||
<!-- Floor / datum -->
|
||||
<line x1="195" y1="380" x2="325" y2="380" stroke="#000" stroke-width="2" />
|
||||
<text x="330" y="384" fill="#000">0 m (datum)</text>
|
||||
<line id="ps-line-outflowLevel" x1="240" x2="280" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
||||
<text id="ps-label-outflowLevel" x="285" fill="#1F4E79" font-weight="bold">Outlet</text>
|
||||
<text id="ps-sub-outflowLevel" x="285" fill="#777" font-size="9">top of pipe</text>
|
||||
|
||||
<!-- Leader lines: shown when the input row had to be nudged off its threshold's ideal y -->
|
||||
<line id="ps-leader-basinHeight" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<line id="ps-leader-overflowLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<line id="ps-leader-maxLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<line id="ps-leader-startLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<line id="ps-leader-minLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<line id="ps-leader-dryRunLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<line id="ps-leader-outflowLevel" x1="0" y1="0" x2="0" y2="0" stroke="#bbb" stroke-width="0.6" stroke-dasharray="2 2" visibility="hidden" />
|
||||
<!-- Floor / datum -->
|
||||
<line x1="115" y1="380" x2="245" y2="380" stroke="#000" stroke-width="2" />
|
||||
<text x="250" y="384" fill="#000">0 m (datum)</text>
|
||||
|
||||
<!-- Ordering-warning ribbon -->
|
||||
<text id="ps-warning" x="180" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Ordering-warning ribbon -->
|
||||
<text id="ps-warning" x="260" y="410" text-anchor="middle" fill="#C0392B" font-size="10" font-style="italic" visibility="hidden"></text>
|
||||
</svg>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -519,39 +325,167 @@
|
||||
<div class="form-row">
|
||||
<label for="node-input-controlMode"><i class="fa fa-sliders"></i> Control mode</label>
|
||||
<select id="node-input-controlMode">
|
||||
<option value="none">None / Manual</option>
|
||||
<option value="levelbased">Level-based</option>
|
||||
<option value="flowbased">Flow-based</option>
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="ps-mode-levelbased" class="ps-mode-section">
|
||||
<p style="font-size:12px;color:#777;margin:0;">Level-based uses <code>minLevel</code> / <code>startLevel</code> / <code>maxLevel</code> from the diagram above.</p>
|
||||
<div class="form-row">
|
||||
<label for="node-input-levelCurveType">Curve</label>
|
||||
<select id="node-input-levelCurveType" style="width:60%;">
|
||||
<option value="linear">Linear</option>
|
||||
<option value="log">Log - fast early response</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" id="ps-log-factor-row" style="display:none;">
|
||||
<label for="node-input-logCurveFactor">Log shape factor</label>
|
||||
<input type="number" id="node-input-logCurveFactor" min="0.001" step="0.1" style="width:100px;" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-enableShiftedRamp" style="width:auto;">
|
||||
<input type="checkbox" id="node-input-enableShiftedRamp" style="width:auto;vertical-align:middle;margin-right:6px;" />
|
||||
Enable shifted ramp (hysteresis)
|
||||
</label>
|
||||
</div>
|
||||
<div id="ps-mode-validation" style="display:none;color:#C0392B;font-size:11px;margin:4px 0 8px 0;border:1px solid #C0392B;border-radius:3px;padding:6px 8px;background:#FDECEA;"></div>
|
||||
<!--
|
||||
============================================================
|
||||
LEVEL-BASED MODE PREVIEW (ps-levelbased-mode-diagram)
|
||||
============================================================
|
||||
Coordinate system: SVG viewBox is 430 (wide) × 185 (tall).
|
||||
Origin (0,0) top-left. +x right. +y DOWN (so y=24 is HIGH on screen,
|
||||
y=158 is at the baseline).
|
||||
|
||||
X-AXIS (level, in viewBox px) — controlled by redrawModeDiagram() in
|
||||
the oneditprepare script above. The function maps the user's
|
||||
startLevel/inflowLevel/maxLevel/shiftLevel onto the px window
|
||||
x0=52 (left axis) → x1=390 (right end of plot).
|
||||
DO NOT hardcode x for ps-mode-line-* / ps-mode-label-*; they're
|
||||
rewritten on every input change.
|
||||
|
||||
Y-AXIS (process demand %):
|
||||
y=24 100% (top of plot)
|
||||
y=140 0% (baseline / x-axis)
|
||||
y=160 OFF baseline (pink dashed)
|
||||
y=180 axis labels under the plot ("dry run","start","inlet","max","overflow","shift")
|
||||
y=205 legend captions (one row, BELOW axis labels — moved here to stop
|
||||
colliding with the title row at y=14)
|
||||
y=14 curve-type title only ("linear curve" / "log curve"), centered.
|
||||
|
||||
WHAT IS STATIC vs DYNAMIC:
|
||||
STATIC (edit inline below): viewBox bounds, axis lines, "0%"/"100%"
|
||||
tick labels, in-plot caption x/y, axis-label y=176.
|
||||
DYNAMIC (edit in JS): every ps-mode-line-*, ps-mode-label-* x;
|
||||
ps-mode-curve-up/down points; visibility of shift elements.
|
||||
|
||||
HOW TO NUDGE OVERLAPPING TEXT:
|
||||
- Move the curve-type caption: edit the x="220" y="18" on
|
||||
#ps-mode-curve-label.
|
||||
- Move axis labels (start/inlet/max/shift) UP or DOWN: edit y="176".
|
||||
(To move them left/right relative to the line, edit redrawModeDiagram
|
||||
in the script — the x is set there.)
|
||||
- Move the legend captions: edit x="280" y="54" / y="72" on
|
||||
#ps-mode-curve-up-label / #ps-mode-curve-down-label.
|
||||
- To resize the plot box, change viewBox + the x0/x1/y0/y1 constants
|
||||
in redrawModeDiagram() to match.
|
||||
============================================================
|
||||
-->
|
||||
<div class="ps-diag" id="ps-mode-wrap">
|
||||
<!-- LEFT side-panel: only the level-based mode's editable inputs +
|
||||
read-only displays for derived/related levels (so user has all
|
||||
level context in one column). Hover-coupled to the SVG markers. -->
|
||||
<div class="ps-diag-side">
|
||||
<div class="ps-row ps-readonly" data-stroke="#C0392B" data-couples-line="ps-mode-line-dryRunLevel">
|
||||
<div><label>dryRunLevel</label><div class="ps-sub">derived</div></div>
|
||||
<span id="ps-mode-readout-dryRun" class="ps-readonly-val">— m</span>
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row" data-stroke="#1E8449" data-couples-line="ps-mode-line-startLevel">
|
||||
<div><label>startLevel</label><div class="ps-sub">pump-on threshold</div></div>
|
||||
<input type="number" id="node-input-startLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row ps-readonly" data-stroke="#1F4E79" data-couples-line="ps-mode-line-inflowLevel">
|
||||
<div><label>inflowLevel</label><div class="ps-sub">from basin above</div></div>
|
||||
<span id="ps-mode-readout-inflow" class="ps-readonly-val">— m</span>
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row" data-stroke="#D68910" data-couples-line="ps-mode-line-maxLevel">
|
||||
<div><label>maxLevel</label><div class="ps-sub">100% saturation</div></div>
|
||||
<input type="number" id="node-input-maxLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row" id="ps-shiftLevel-row" data-stroke="#D68910" data-couples-line="ps-mode-line-shiftLevel" style="display:none;">
|
||||
<div><label>shiftLevel</label><div class="ps-sub">arms hysteresis</div></div>
|
||||
<input type="number" id="node-input-shiftLevel" min="0" step="0.01" />
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
<div class="ps-row ps-readonly" data-stroke="#C0392B" data-couples-line="ps-mode-line-overflowLevel">
|
||||
<div><label>overflowLevel</label><div class="ps-sub">from basin above</div></div>
|
||||
<span id="ps-mode-readout-overflow" class="ps-readonly-val">— m</span>
|
||||
<span class="ps-unit">m</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg id="ps-levelbased-mode-diagram" class="ps-diag-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 430 215"
|
||||
style="display:block;width:100%;max-width:540px;background:#fff;border:1px solid #e5e5e5;border-radius:4px;"
|
||||
font-family="Arial,sans-serif" font-size="11">
|
||||
<!-- ZONE BANDS — drawn FIRST so they sit behind axes and curves.
|
||||
x is set DYNAMICALLY by redrawModeDiagram(); y/height span the full plot (24..160).
|
||||
Order from leftmost to rightmost: dryRun (red) | safetyLow (orange) | safe (green) |
|
||||
safetyHigh (orange) | overflow (red).
|
||||
-->
|
||||
<rect id="ps-zone-dryRun" y="24" height="136" fill="#fdecea" />
|
||||
<rect id="ps-zone-safetyLow" y="24" height="136" fill="#fef5e7" />
|
||||
<rect id="ps-zone-safe" y="24" height="136" fill="#eafaf1" />
|
||||
<rect id="ps-zone-safetyHigh" y="24" height="136" fill="#fef5e7" />
|
||||
<rect id="ps-zone-overflow" y="24" height="136" fill="#fdecea" />
|
||||
<!-- X-axis (0% baseline) at y=140; y axis at x=52 (top y=24). Plot range: y=24..140. -->
|
||||
<line x1="52" y1="140" x2="402" y2="140" stroke="#333" />
|
||||
<line x1="52" y1="140" x2="52" y2="24" stroke="#333" />
|
||||
<!-- OFF tier baseline at y=160 (20px below 0% baseline). pink line drawn dynamically by curve. -->
|
||||
<line x1="52" y1="160" x2="402" y2="160" stroke="#E08080" stroke-dasharray="2 3" />
|
||||
<!-- Y-axis tick labels (x=4, right-aligned via text-anchor="end" at x=50 for tighter alignment). -->
|
||||
<text x="50" y="27" text-anchor="end" fill="#333">100%</text>
|
||||
<text x="50" y="143" text-anchor="end" fill="#333">0%</text>
|
||||
<text x="50" y="163" text-anchor="end" fill="#E08080">OFF</text>
|
||||
<!-- Plot title above 100% line. -->
|
||||
<text id="ps-mode-curve-label" x="220" y="14" text-anchor="middle" fill="#555">linear curve</text>
|
||||
<!-- Curves drawn dynamically. Up curve foot=inlet→top=max. Down curve foot=start→top=shiftLevel (visible when shift enabled). -->
|
||||
<polyline id="ps-mode-curve-up" fill="none" stroke="#1E8449" stroke-width="2.5" points="" />
|
||||
<polyline id="ps-mode-curve-down" fill="none" stroke="#D68910" stroke-width="2" stroke-dasharray="5 3" points="" style="display:none;" />
|
||||
<!-- Vertical level-marker lines — span y=24..140 (top to baseline only, NOT into OFF tier). x set dynamically. -->
|
||||
<line id="ps-mode-line-dryRunLevel" y1="24" y2="140" stroke="#C0392B" stroke-dasharray="2 2" />
|
||||
<line id="ps-mode-line-startLevel" y1="24" y2="140" stroke="#1E8449" stroke-dasharray="2 2" />
|
||||
<line id="ps-mode-line-inflowLevel" y1="24" y2="140" stroke="#1F4E79" stroke-dasharray="2 2" />
|
||||
<line id="ps-mode-line-maxLevel" y1="24" y2="140" stroke="#D68910" stroke-dasharray="2 2" />
|
||||
<line id="ps-mode-line-overflowLevel" y1="24" y2="140" stroke="#C0392B" stroke-dasharray="2 2" />
|
||||
<line id="ps-mode-line-shiftLevel" y1="24" y2="140" stroke="#D68910" stroke-dasharray="2 2" style="display:none;" />
|
||||
<!-- Axis labels — y=180 row sits below the OFF baseline (y=160). x set dynamically. -->
|
||||
<text id="ps-mode-label-dryRunLevel" y="180" text-anchor="middle" fill="#C0392B">dry run</text>
|
||||
<text id="ps-mode-label-startLevel" y="180" text-anchor="middle" fill="#1E8449">start</text>
|
||||
<text id="ps-mode-label-inflowLevel" y="180" text-anchor="middle" fill="#1F4E79">inlet</text>
|
||||
<text id="ps-mode-label-maxLevel" y="180" text-anchor="middle" fill="#D68910">max</text>
|
||||
<text id="ps-mode-label-overflowLevel" y="180" text-anchor="middle" fill="#C0392B">overflow</text>
|
||||
<text id="ps-mode-label-shiftLevel" y="180" text-anchor="middle" fill="#D68910" style="display:none;">shift</text>
|
||||
<!-- Legend captions — placed BELOW the axis labels (y=200) on their own row,
|
||||
so they never collide with the title (y=14). Up-caption left-aligned at
|
||||
x=60; down-caption to its right at x=210. Both font-size 10. -->
|
||||
<text id="ps-mode-curve-up-label" x="60" y="205" fill="#1E8449" font-size="10">— ramp inlet→max</text>
|
||||
<text id="ps-mode-curve-down-label" x="210" y="205" fill="#D68910" font-size="10" style="display:none;">— shifted start→shift</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ps-mode-flowbased" class="ps-mode-section" style="display:none">
|
||||
<div class="form-row">
|
||||
<label for="node-input-flowSetpoint">Flow setpoint</label>
|
||||
<input type="number" id="node-input-flowSetpoint" placeholder="m3/h" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-flowDeadband">Deadband</label>
|
||||
<input type="number" id="node-input-flowDeadband" placeholder="m3/h" />
|
||||
</div>
|
||||
<div id="ps-mode-manual" class="ps-mode-section" style="display:none">
|
||||
<p style="font-size:12px;color:#777;margin:0;">Manual mode accepts external <code>Qd</code> demand commands and does not compute demand from basin level.</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Reference</h4>
|
||||
|
||||
<!-- Reference data -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-minHeightBasedOn"><i class="fa fa-arrows-v"></i> Minimum Height Based On</label>
|
||||
<select id="node-input-minHeightBasedOn" style="width:60%;">
|
||||
<option value="inlet">Inlet Elevation</option>
|
||||
<option value="outlet">Outlet Elevation</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Reference data — basinBottomRef moved into basin side-panel above. -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-refHeight"><i class="fa fa-map-marker"></i> Reference height</label>
|
||||
<select id="node-input-refHeight" style="width:60%;">
|
||||
@@ -559,21 +493,11 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-basinBottomRef"><i class="fa fa-level-down"></i> Basin floor above datum (m)</label>
|
||||
<input type="number" id="node-input-basinBottomRef" step="0.01" />
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Safety</h4>
|
||||
|
||||
<!-- Safety settings -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-timeleftToFullOrEmptyThresholdSeconds"><i class="fa fa-clock-o"></i> Time To Empty/Full (s)</label>
|
||||
<input type="number" id="node-input-timeleftToFullOrEmptyThresholdSeconds" min="0" step="1" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-enableDryRunProtection">
|
||||
<i class="fa fa-shield"></i> Dry-run Protection
|
||||
@@ -588,16 +512,16 @@
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-enableOverfillProtection">
|
||||
<i class="fa fa-exclamation-triangle"></i> Overfill Protection
|
||||
<label for="node-input-enableHighVolumeSafety">
|
||||
<i class="fa fa-exclamation-triangle"></i> High-volume Safety
|
||||
</label>
|
||||
<input type="checkbox" id="node-input-enableOverfillProtection" style="width:20px;vertical-align:baseline;" />
|
||||
<span>Stop filling when approaching overflow</span>
|
||||
<input type="checkbox" id="node-input-enableHighVolumeSafety" style="width:20px;vertical-align:baseline;" />
|
||||
<span>Act before physical overflow</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-overfillThresholdPercent" style="padding-left:20px;">High Volume Threshold (%)</label>
|
||||
<input type="number" id="node-input-overfillThresholdPercent" min="0" max="100" step="0.1" style="width:80px;" />
|
||||
<span id="derived-overfillLevel" style="margin-left:8px;color:#777;font-size:12px;">→ overfillLevel ≈ — m</span>
|
||||
<label for="node-input-highVolumeSafetyThresholdPercent" style="padding-left:20px;">High-volume Safety (%)</label>
|
||||
<input type="number" id="node-input-highVolumeSafetyThresholdPercent" min="0" max="100" step="0.1" style="width:80px;" />
|
||||
<span id="derived-highVolumeSafetyLevel" style="margin-left:8px;color:#777;font-size:12px;">→ highVolumeSafetyLevel ≈ — m</span>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user