levelBased ramp + engagement: - Ramp foot is now max(startLevel, holdLevel) — was max(startLevel, inflowLevel). inflowLevel is basin geometry, not a control setpoint; the implicit hold zone it created was causing pumps to "start at inflowLevel" instead of startLevel. - New optional `holdLevel` config (defaults to startLevel = no hold band). When raised, pumps engage at startLevel and hold at 0 % = MGC flow.min across [startLevel, holdLevel], then ramp 0..100 % to maxLevel. - Engagement decided in run() (not in `_applyMachineGroupLevelControl`): rising-edge hysteresis arming gates a clean turnOff early-return. Once armed, the helper always forwards setDemand(pct, '%') — 0 % legitimately means "engaged at min flow", no more soft-turnOff at the boundary. - Disengagement paths (minLevel hard-stop, stopLevel falling-edge, pre-arming idle) now all clear the shifted-ramp hysteresis state too. - Threshold validator drops the startLevel ≤ inflowLevel rule; adds startLevel ≤ holdLevel < maxLevel (only checked when holdLevel is explicitly set, so default-null doesn't false-flag). MGC unit math: - Replace direct group.handleInput(percent) with group.setDemand(pct, '%') in _applyMachineGroupLevelControl. The percent → m³/s resolution now lives in MGC.setDemand (committed separately in the MGC submodule). FlowAggregator variant picking: - New _pickFlowSum() helper mirrors selectBestNetFlow's variant precedence (measured first, then predicted) and resolves each side independently. Realistic mixed case — real measured upstream sensor + predicted pump outflow — now feeds the predicted-volume integrator. Was reading only `flow.predicted.*` so a real upstream sensor (which writes `flow.measured.*`) never moved the level. Editor: - New `holdLevel` and `deadZoneKeepAlivePercent` defaults + side-panel input rows in the levelbased mode preview. - Add the missing `ps-mode-line-holdLevel` SVG marker (was declared in the side-panel coupling but the SVG element didn't exist, so the dashed line never rendered). - Relax stopLevel marker gate so it renders for any non-negative typed value — start/stop ordering is the ribbon's job, not the marker's (was hiding the line whenever startLevel was momentarily smaller). - Add holdLevel to the marker loop in mode-preview so changes track. - Add stopLevel + holdLevel + maxLevel to all three bindRedraw lists (basin-diagram, mode-preview, bounds.apply) so the SVG, validation ribbon, and HTML5 min/max attrs update on every edit. - Initialise stopLevel + holdLevel + deadZoneKeepAlivePercent inputs in oneditprepare so reopening the editor shows the saved values. - nodeClass passes holdLevel + deadZoneKeepAlivePercent into the domain config. Tests: - New test/basic/_probe_upstream_emit.test.js: confirms the parent surfaces flow.measured.upstream.* on Port 0 after a measurement child write — pins the previously-invisible measured variant flow. - flowAggregator.basic.test.js: two new regression cases — measured inflow when predicted side is empty, and the measured-in / predicted-out mixed case. - control-levelBased.basic.test.js: new cases for the holdLevel hold band, the [stopLevel, startLevel] keep-alive, the engagement gate, and the "0 % at startLevel = setDemand" contract. - specificClass.test.js: zone tests adjusted to the new ramp foot. Shifted-ramp tests pin holdLevel = 3 explicitly so their legacy arithmetic (ramp foot at inflowLevel) stays self-consistent. - shifted-ramp-end-to-end.test.js: same holdLevel pin for the same reason. Packaging: - Add .gitignore + .npmignore so the published tarball drops the wiki/, simulations/, test/, tools/, .claude/ etc. The pack went from 1.5 MB (72 files) to ~57 KB (30 files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
594 lines
32 KiB
HTML
594 lines
32 KiB
HTML
<!--
|
||
| 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/bounds.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: 50 }, // m³, total empty basin
|
||
basinHeight: { value: 4 }, // m, floor to top
|
||
inflowLevel: { value: 1.5 }, // m, bottom/invert of inlet pipe above floor
|
||
outflowLevel: { value: 0.2 }, // m, top of outlet/suction pipe above floor
|
||
overflowLevel: { value: 3.8 }, // 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 },
|
||
shiftArmPercent: { value: 95 },
|
||
startLevel: { value: 1 }, // m, pump-on threshold (engagement edge)
|
||
stopLevel: { value: 0.5 }, // m, pump-off threshold (hysteresis fall-back)
|
||
holdLevel: { value: 1 }, // m, ramp 0%-foot; defaults to startLevel (= no hold zone)
|
||
deadZoneKeepAlivePercent: { value: 1 }, // % emitted across [stopLevel, startLevel] keep-alive band
|
||
minLevel: { value: 0.3 }, // m, hard-stop (just above outflow pipe top)
|
||
maxLevel: { value: 3.8 }, // m, 100% demand saturation
|
||
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>
|
||
<div id="ps-basin-validation" style="display:none;color:#C0392B;font-size:11px;margin:0 0 8px 0;border:1px solid #C0392B;border-radius:3px;padding:6px 8px;background:#FDECEA;"></div>
|
||
|
||
<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:28px; align-items:flex-start; margin:0 0 14px 0; }
|
||
.ps-diag-side { width: 220px; flex: 0 0 220px; display:flex; flex-direction:column; gap:6px; }
|
||
.ps-diag-side .ps-row {
|
||
display:grid; grid-template-columns: minmax(0,1fr) 70px 16px; 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;
|
||
min-width:0;
|
||
}
|
||
.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">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:360px;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 — shifted right (x=145, width=110) to give the inlet
|
||
sub-label "bottom of pipe" room on the left without clipping.
|
||
Threshold tick lines extend 5 px outside the tank walls. -->
|
||
<rect x="145" y="40" width="110" height="340" fill="#F0F8FF" stroke="#333" stroke-width="1.5" />
|
||
<rect id="ps-deadvol" x="146" width="108" fill="#AACCE0" />
|
||
|
||
<!-- Mid-tank zone labels — centred at x=200 (tank centre). -->
|
||
<text id="ps-zone-spare" x="200" text-anchor="middle" fill="#B78200" font-size="10" font-style="italic" visibility="hidden">Spare</text>
|
||
<text id="ps-zone-sewage" x="200" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Sewage + buffer</text>
|
||
<text id="ps-zone-buffer1" x="200" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Buffer</text>
|
||
<text id="ps-zone-buffer2" x="200" text-anchor="middle" fill="#1F4E79" font-size="10" font-style="italic" visibility="hidden">Buffer</text>
|
||
<text id="ps-zone-dead" x="200" text-anchor="middle" fill="#444" font-size="10" font-style="italic" visibility="hidden">Dead vol</text>
|
||
|
||
<!-- basinHeight tick at tank rim (y=40, static). -->
|
||
<line id="ps-line-basinHeight" x1="140" y1="40" x2="260" y2="40" stroke="#333" stroke-width="1.5" />
|
||
<text id="ps-label-basinHeight" x="265" y="44" fill="#333">basinHeight</text>
|
||
|
||
<line id="ps-line-overflowLevel" x1="140" x2="260" stroke="#C0392B" stroke-dasharray="4 2" stroke-width="1.5" />
|
||
<text id="ps-label-overflowLevel" x="265" fill="#C0392B">overflowLevel</text>
|
||
|
||
<line id="ps-line-highVolumeSafetyLevel" x1="140" x2="260" stroke="#D68910" stroke-dasharray="1 2" stroke-width="1" opacity="0.7" />
|
||
<text id="ps-label-highVolumeSafetyLevel" x="265" fill="#D68910" font-size="10" font-style="italic">highVolSafety</text>
|
||
|
||
<line id="ps-line-inflowLevelGuide" x1="145" x2="255" stroke="#1F4E79" stroke-dasharray="2 3" stroke-width="1" opacity="0.55" />
|
||
<text id="ps-label-inflowLevelGuide" x="265" fill="#1F4E79" font-size="10" font-style="italic">inlet invert</text>
|
||
|
||
<line id="ps-line-inflowLevel" x1="85" x2="145" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
||
<text id="ps-label-inflowLevel" x="80" text-anchor="end" fill="#1F4E79" font-weight="bold">Inlet</text>
|
||
<text id="ps-sub-inflowLevel" x="80" text-anchor="end" fill="#777" font-size="9">bottom of pipe</text>
|
||
|
||
<line id="ps-line-dryRunLevel" x1="140" x2="260" stroke="#C0392B" stroke-dasharray="1 2" stroke-width="1" opacity="0.6" />
|
||
<text id="ps-label-dryRunLevel" x="265" fill="#C0392B" font-size="10" font-style="italic">dryRunLevel</text>
|
||
|
||
<line id="ps-line-outflowLevel" x1="255" x2="295" stroke="#1F4E79" stroke-width="2" marker-end="url(#ps-arrow)" />
|
||
<text id="ps-label-outflowLevel" x="300" fill="#1F4E79" font-weight="bold">Outlet</text>
|
||
<text id="ps-sub-outflowLevel" x="300" fill="#777" font-size="9">top of pipe</text>
|
||
|
||
<!-- Floor / datum — datum label sits BELOW the tank (y=395) so it
|
||
never collides with the Outlet / top-of-pipe sub-label when
|
||
outflowLevel is near the floor. -->
|
||
<line x1="140" y1="380" x2="260" y2="380" stroke="#000" stroke-width="2" />
|
||
<text x="200" y="395" text-anchor="middle" fill="#000" font-size="10">0 m (datum)</text>
|
||
|
||
<!-- Ordering-warning ribbon -->
|
||
<text id="ps-warning" x="200" 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" data-stroke="#7D3C98" data-couples-line="ps-mode-line-stopLevel">
|
||
<div><label>stopLevel</label><div class="ps-sub">pump-off threshold (optional, ≤ startLevel)</div></div>
|
||
<input type="number" id="node-input-stopLevel" min="0" step="0.01" />
|
||
<span class="ps-unit">m</span>
|
||
</div>
|
||
<div class="ps-row" data-stroke="#27AE60" data-couples-line="ps-mode-line-holdLevel">
|
||
<div><label>holdLevel</label><div class="ps-sub">0 % ramp foot — leave at startLevel for no hold band</div></div>
|
||
<input type="number" id="node-input-holdLevel" 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">held output drops here</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" id="ps-shiftArmPercent-row" data-stroke="#D68910" data-couples-line="ps-mode-line-armPercent" style="display:none;">
|
||
<div><label>shiftArmPercent</label><div class="ps-sub">arms when output % crosses this</div></div>
|
||
<input type="number" id="node-input-shiftArmPercent" min="0" max="100" step="1" />
|
||
<span class="ps-unit">%</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-stopLevel" y1="24" y2="140" stroke="#7D3C98" stroke-dasharray="2 2" />
|
||
<line id="ps-mode-line-holdLevel" y1="24" y2="140" stroke="#27AE60" 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;" />
|
||
<!-- Horizontal arming-% line — y is set DYNAMICALLY by the JS to the
|
||
shiftArmPercent value (in plot-y space). Spans full plot width. -->
|
||
<line id="ps-mode-line-armPercent" x1="52" x2="392" stroke="#D68910" stroke-dasharray="4 3" stroke-width="1" opacity="0.7" style="display:none;" />
|
||
<text id="ps-mode-label-armPercent" x="394" text-anchor="start" fill="#D68910" font-size="9" style="display:none;">arm%</text>
|
||
<!-- Axis labels under the plot were removed — they crowded each other
|
||
when levels were close. Identification comes from the line colour
|
||
(matched to the side-panel input row) and hover-coupling. -->
|
||
<!-- Empty <text> stubs kept for the redraw loop's getElementById calls
|
||
(cheaper than guarding each one). They're hidden via display:none. -->
|
||
<text id="ps-mode-label-dryRunLevel" style="display:none;"></text>
|
||
<text id="ps-mode-label-startLevel" style="display:none;"></text>
|
||
<text id="ps-mode-label-stopLevel" style="display:none;"></text>
|
||
<text id="ps-mode-label-inflowLevel" style="display:none;"></text>
|
||
<text id="ps-mode-label-maxLevel" style="display:none;"></text>
|
||
<text id="ps-mode-label-overflowLevel" style="display:none;"></text>
|
||
<text id="ps-mode-label-shiftLevel" style="display:none;"></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 (held @100% then ramp shift→start)</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>
|