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>
592 lines
28 KiB
HTML
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>
|