When real wastewater values cluster near the basin floor (minLevel, dryRunLevel, outflowLevel are often within a few cm of each other), the threshold inputs were stacking on top of each other. Now: - Threshold LINE stays at its proportional y on the tank (visual truth: that's where the level actually is). - Input BOX / label / unit are positioned in a nudged right-column stack with a minimum 26-px gap so they never overlap. - A dashed grey leader line connects each line to its input when they had to be pulled apart, so the association stays visible. - Items are sorted by ideal y top-down and nudged downward once; basinHeight is pinned at the rim and acts as the anchor. Also: viewBox extended 430 → 480 so the bottom-of-stack items have room below the tank when the bottom cluster is tight. Warning ribbon moved to y=460 accordingly. No schema change; purely UI layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
605 lines
28 KiB
HTML
605 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 right-column item. yLine is the threshold's true
|
|
// proportional position on the tank; yInput is where the label
|
|
// and input box land (may be nudged away from yLine to avoid
|
|
// overlap with neighbouring items). A dashed leader line is
|
|
// shown only when the two differ by more than a pixel or two.
|
|
const placeItem = (id, yLine, yInput) => {
|
|
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', yLine); line.setAttribute('y2', yLine); }
|
|
if (label) label.setAttribute('y', yInput + 4);
|
|
if (unit) unit.setAttribute('y', yInput + 4);
|
|
if (fo) fo.setAttribute('y', yInput - 11);
|
|
if (sub) sub.setAttribute('y', yInput + 15);
|
|
if (lead) {
|
|
if (Math.abs(yLine - yInput) > 2) {
|
|
lead.setAttribute('x1', 325); lead.setAttribute('y1', yLine);
|
|
lead.setAttribute('x2', 420); lead.setAttribute('y2', yInput);
|
|
lead.setAttribute('visibility', 'visible');
|
|
} else {
|
|
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
|
|
// rim (DIAG.topY); others float on the proportional axis and
|
|
// get nudged apart so the input boxes don't overlap.
|
|
const items = [
|
|
{ id: 'basinHeight', yLine: DIAG.topY, pinned: true },
|
|
{ id: 'overflowLevel', yLine: yForLevel(fNum('overflowLevel'), basinH) },
|
|
{ id: 'maxLevel', yLine: yForLevel(fNum('maxLevel'), basinH) },
|
|
{ id: 'startLevel', yLine: yForLevel(fNum('startLevel'), basinH) },
|
|
{ id: 'minLevel', yLine: yForLevel(fNum('minLevel'), basinH) },
|
|
{ id: 'dryRunLevel', yLine: yForLevel(dryLvl, basinH) },
|
|
{ id: 'outflowLevel', yLine: yForLevel(fNum('outflowLevel'), basinH) },
|
|
].filter(it => it.yLine != null);
|
|
|
|
const GAP = 26;
|
|
items.sort((a, b) => a.yLine - b.yLine);
|
|
let prev = -Infinity;
|
|
for (const it of items) {
|
|
if (it.pinned) { it.yInput = it.yLine; prev = it.yInput; continue; }
|
|
it.yInput = Math.max(it.yLine, prev + GAP);
|
|
prev = it.yInput;
|
|
}
|
|
|
|
// Hide leader lines for items whose input is cleared
|
|
const active = new Set(items.map(it => it.id));
|
|
['overflowLevel','maxLevel','startLevel','minLevel','dryRunLevel','outflowLevel'].forEach(id => {
|
|
if (!active.has(id)) {
|
|
const lead = document.getElementById(`ps-leader-${id}`);
|
|
if (lead) lead.setAttribute('visibility', 'hidden');
|
|
}
|
|
});
|
|
for (const it of items) placeItem(it.id, it.yLine, it.yInput);
|
|
|
|
// 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 outflowLevel down to the floor
|
|
const outflowY = yForLevel(fNum('outflowLevel'), basinH);
|
|
const deadvol = document.getElementById('ps-deadvol');
|
|
if (deadvol && outflowY != null) {
|
|
deadvol.setAttribute('y', outflowY);
|
|
deadvol.setAttribute('height', Math.max(0, DIAG.botY - outflowY));
|
|
}
|
|
|
|
// 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 480"
|
|
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="460" 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>
|