Files
pumpingStation/pumpingStation.html
Rene De Ren 8a6ca1baeb 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>
2026-05-05 19:29:34 +02:00

559 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
| S88-niveau | Primair (blokkleur) | Tekstkleur |
| ---------------------- | ------------------- | ---------- |
| **Area** | `#0f52a5` | wit |
| **Process Cell** | `#0c99d9` | wit |
| **Unit** | `#50a8d9` | zwart |
| **Equipment (Module)** | `#86bbdd` | zwart |
| **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 -->
<!-- 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", {
category: "EVOLV",
color: "#0c99d9", // color for the node based on the S88 schema
defaults: {
name: { value: "" },
// Define station-specific properties
simulator: { value: false },
basinVolume: { value: 1 }, // m³, total empty basin
basinHeight: { value: 1 }, // m, floor to top
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
outletPipeDiameter: { value: 0.3 }, // m
pipelineLength: { value: 80 }, // m
maxDischargeHead: { value: 24 }, // m
staticHead: { value: 12 }, // m
maxInflowRate: { value: 200 }, // m³/h
temperatureReferenceDegC: { value: 15 },
timeleftToFullOrEmptyThresholdSeconds:{value:0}, // time threshold to safeguard starting or stopping pumps in seconds
enableDryRunProtection: { value: true },
enableHighVolumeSafety: { value: true },
enableOverfillProtection: { value: true }, // deprecated alias
dryRunThresholdPercent: { value: 2 },
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" },
// Advanced reference information
refHeight: { value: "NAP" }, // reference height
basinBottomRef: { value: 1 }, // absolute elevation of basin floor
//define asset properties
uuid: { value: "" },
supplier: { value: "" },
category: { value: "" },
assetType: { value: "" },
model: { value: "" },
unit: { value: "" },
//logger properties
enableLog: { value: false },
logLevel: { value: "error" },
//physicalAspect
positionVsParent: { value: "" },
positionIcon: { value: "" },
hasDistance: { value: false },
distance: { value: 0 },
distanceUnit: { value: "m" },
distanceDescription: { value: "" },
// control strategy
controlMode: { value: "levelbased" },
levelCurveType: { value: "linear" },
logCurveFactor: { value: 9 },
enableShiftedRamp: { value: false },
shiftLevel: { value: 0 },
startLevel: { value: null },
minLevel: { value: null },
maxLevel: { value: null },
flowSetpoint: { value: null },
flowDeadband: { value: null }
},
inputs: 1,
outputs: 3,
inputLabels: ["Input"],
outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tint",
label: function () {
return this.positionIcon + " PumpingStation";
},
oneditprepare: function () {
window.PSEditor.oneditprepare.call(this);
},
oneditsave: function () {
window.PSEditor.oneditsave.call(this);
},
});
</script>
<!-- Main UI -->
<script type="text/html" data-template-name="pumpingStation">
<h4>Simulation</h4>
<div class="form-row">
<label for="node-input-simulator"><i class="fa fa-play-circle"></i> Simulator</label>
<input type="checkbox" id="node-input-simulator" style="width:20px;vertical-align:baseline;" />
<span>Run station in simulated mode</span>
</div>
<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). Each input on the left controls a line in the diagram on the right hover an input to highlight its line.</p>
<style>
/* 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-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>
<!--
============================================================
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.
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³")
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>.
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"></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" />
<!-- 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>
<!-- 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>
<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>
<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>
<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>
<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>
<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>
<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>
<!-- 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>
<hr>
<h4>Control Strategy</h4>
<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="levelbased">Level-based</option>
<option value="manual">Manual</option>
</select>
</div>
<div id="ps-mode-levelbased" class="ps-mode-section">
<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=inlettop=max. Down curve foot=starttop=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 inletmax</text>
<text id="ps-mode-curve-down-label" x="210" y="205" fill="#D68910" font-size="10" style="display:none;"> shifted startshift</text>
</svg>
</div>
</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 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%;">
<option value="NAP">NAP</option>
</select>
</div>
<hr>
<h4>Safety</h4>
<!-- Safety settings -->
<div class="form-row">
<label for="node-input-enableDryRunProtection">
<i class="fa fa-shield"></i> Dry-run Protection
</label>
<input type="checkbox" id="node-input-enableDryRunProtection" style="width:20px;vertical-align:baseline;" />
<span>Prevent pumps from running on low volume</span>
</div>
<div class="form-row">
<label for="node-input-dryRunThresholdPercent" style="padding-left:20px;">Low Volume Threshold (%)</label>
<input type="number" id="node-input-dryRunThresholdPercent" min="0" max="100" step="0.1" style="width:80px;" />
<span id="derived-dryRunLevel" style="margin-left:8px;color:#777;font-size:12px;"> dryRunLevel m</span>
</div>
<div class="form-row">
<label for="node-input-enableHighVolumeSafety">
<i class="fa fa-exclamation-triangle"></i> High-volume Safety
</label>
<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-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>
<h3>Output Formats</h3>
<div class="form-row">
<label for="node-input-processOutputFormat"><i class="fa fa-random"></i> Process Output</label>
<select id="node-input-processOutputFormat" style="width:60%;">
<option value="process">process</option>
<option value="json">json</option>
<option value="csv">csv</option>
</select>
</div>
<div class="form-row">
<label for="node-input-dbaseOutputFormat"><i class="fa fa-database"></i> Database Output</label>
<select id="node-input-dbaseOutputFormat" style="width:60%;">
<option value="influxdb">influxdb</option>
<option value="json">json</option>
<option value="csv">csv</option>
</select>
</div>
<!-- Shared asset/logger/position menus -->
<div id="asset-fields-placeholder"></div>
<div id="logger-fields-placeholder"></div>
<div id="position-fields-placeholder"></div>
</script>
<script type="text/html" data-help-name="pumpingStation">
</script>