Files
pumpingStation/pumpingStation.html
znetsixe 2dd419dbf4 Editor: nudge dashed lines themselves, revert tank height
Reverts the tank-bigger approach from last commit. Instead of
scaling the tank and keeping strict proportionality, the dashed
threshold lines are now nudged apart directly so each gets a
guaranteed 36-px vertical gap. Inputs and labels align with the
lines (no more leader lines needed).

Trade-off: the diagram is now an ordered schematic, not a strictly
to-scale rendering. Values are still shown next to each line via
the input boxes, and the value ordering is preserved. For an editor
where the goal is entering parameters, readability wins over scale
fidelity.

Sizing reverted:
  viewBox    620 → 430
  tank h     520 → 340
  botY       560 → 380

Behavior:
  GAP        30 → 36 (more visible space between dashed lines)
  placeItem  takes a single y now (line + input + label + unit
             share it); leader-line mechanism kept as hidden
             plumbing in case we switch back to proportional later

Dead-volume band now anchors to the (possibly-nudged) outflow line
instead of the proportional y so it still visually meets the line
cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:10:23 +02:00

592 lines
28 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 -->
<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, centre of inlet pipe above floor
outflowLevel: { value: 0.2 }, // m, centre of outlet 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 },
enableOverfillProtection: { value: true },
dryRunThresholdPercent: { value: 2 },
overfillThresholdPercent: { value: 98 },
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: "none" },
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() {
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;
// Build the right-column items. basinHeight is pinned at the
// tank rim; others are sorted by their proportional y and then
// pushed apart so every dashed line gets a minimum vertical
// gap for readability. The diagram is a schematic ordered
// list, not a strictly to-scale rendering.
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) },
].filter(it => it.yIdeal != null);
const GAP = 36;
items.sort((a, b) => a.yIdeal - b.yIdeal);
let prev = -Infinity;
for (const it of items) {
if (it.pinned) { it.y = it.yIdeal; prev = it.y; continue; }
it.y = Math.max(it.yIdeal, prev + GAP);
prev = it.y;
}
for (const it of items) placeItem(it.id, it.y);
// 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 ------------------- //
},
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');
},
});
</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). Enter values next to each line the diagram scales to whatever you enter.</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;
}
#ps-basin-diagram input[type=number]:focus { outline: 1px solid #0c99d9; border-color: #0c99d9; }
</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>
<!-- 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" />
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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>
<!-- 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" />
<!-- 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>
<div class="form-row">
<label for="node-input-basinVolume"><i class="fa fa-cube"></i> Basin Volume (m³)</label>
<input type="number" id="node-input-basinVolume" min="0" step="0.1" />
</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="none">None / Manual</option>
<option value="levelbased">Level-based</option>
<option value="flowbased">Flow-based</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>
<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>
<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>
<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>
<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
</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-enableOverfillProtection">
<i class="fa fa-exclamation-triangle"></i> Overfill Protection
</label>
<input type="checkbox" id="node-input-enableOverfillProtection" style="width:20px;vertical-align:baseline;" />
<span>Stop filling when approaching 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>
</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>