feat(history): align units and history data

This commit is contained in:
2026-09-14 12:30:57 +08:00
parent 35b6819459
commit 08ddb4453d
21 changed files with 1849 additions and 3618 deletions
+41 -6
View File
@@ -1,4 +1,11 @@
import { FLOW_DISPLAY_UNIT, isLpsFlowProperty, toM3h } from "./units";
import {
FLOW_DISPLAY_UNIT,
metricForResultProperty,
networkResultUnitsFromOptions,
toDisplayValue,
toM3h,
toModelDisplayValue,
} from "./units";
describe("flow display units", () => {
it("uses cubic meters per hour as the flow display unit", () => {
@@ -10,10 +17,38 @@ describe("flow display units", () => {
expect(toM3h(10, "L/s")).toBe(36);
});
it("recognizes computed properties that arrive from the backend in L/s", () => {
expect(isLpsFlowProperty("flow")).toBe(true);
expect(isLpsFlowProperty("actual_demand")).toBe(true);
expect(isLpsFlowProperty("actualdemand")).toBe(true);
expect(isLpsFlowProperty("pressure")).toBe(false);
it("centralizes model and SCADA conversion for all UI consumers", () => {
expect(toDisplayValue(2, "flow", "MLD")).toBeCloseTo(83.3333333334);
expect(toDisplayValue(10, "pressure", "KPA")).toBeCloseTo(1.019716213);
expect(toDisplayValue(2, "velocity", "ft/s")).toBeCloseTo(0.6096);
expect(toDisplayValue(36, "flow", "m3/h")).toBe(36);
});
it("maps result properties to their physical metrics", () => {
expect(metricForResultProperty("flow")).toBe("flow");
expect(metricForResultProperty("base_demand")).toBe("flow");
expect(metricForResultProperty("actual_demand")).toBe("flow");
expect(metricForResultProperty("pressure")).toBe("pressure");
expect(metricForResultProperty("velocity")).toBe("velocity");
expect(metricForResultProperty("headloss")).toBeNull();
});
it("resolves legacy and v3 model options in one place", () => {
expect(
networkResultUnitsFromOptions({
FLOW_UNITS: "MLD",
PRESSURE_UNITS: "METERS",
}),
).toEqual({ flow: "MLD", pressure: "METERS", velocity: "m/s" });
expect(networkResultUnitsFromOptions({ UNITS: "GPM", PRESSURE: "PSI" }))
.toEqual({ flow: "GPM", pressure: "PSI", velocity: "ft/s" });
});
it("converts model properties using project result units", () => {
const units = { flow: "MLD", pressure: "KPA", velocity: "ft/s" };
expect(toModelDisplayValue(2, "flow", units)).toBeCloseTo(83.3333333334);
expect(toModelDisplayValue(10, "pressure", units)).toBeCloseTo(1.019716213);
expect(toModelDisplayValue(2, "velocity", units)).toBeCloseTo(0.6096);
expect(toModelDisplayValue(3, "headloss", units)).toBe(3);
});
});
+124 -9
View File
@@ -1,17 +1,132 @@
export const FLOW_DISPLAY_UNIT = "m³/h";
const M3H_FACTOR = 3600;
const LPS_FLOW_PROPERTIES = new Set(["flow", "actual_demand", "actualdemand"]);
export const PRESSURE_DISPLAY_UNIT = "m";
export const VELOCITY_DISPLAY_UNIT = "m/s";
export type MeasurementMetric = "flow" | "pressure" | "velocity";
export interface NetworkResultUnits {
flow: string;
pressure: string;
velocity: string;
}
export const isLpsFlowProperty = (property: string) =>
LPS_FLOW_PROPERTIES.has(property);
export const DEFAULT_NETWORK_RESULT_UNITS: NetworkResultUnits = {
flow: "LPS",
pressure: "MTR",
velocity: "m/s",
};
const M3H_FACTOR = 3600;
const FLOW_PROPERTIES = new Set([
"flow",
"demand",
"base_demand",
"actual_demand",
"actualdemand",
]);
const PRESSURE_PROPERTIES = new Set(["pressure"]);
const VELOCITY_PROPERTIES = new Set(["velocity"]);
const IMPERIAL_FLOW_UNITS = new Set(["CFS", "GPM", "MGD", "IMGD", "AFD"]);
export const metricForResultProperty = (
property: string,
): MeasurementMetric | null => {
const normalizedProperty = property.trim().toLowerCase();
if (FLOW_PROPERTIES.has(normalizedProperty)) return "flow";
if (PRESSURE_PROPERTIES.has(normalizedProperty)) return "pressure";
if (VELOCITY_PROPERTIES.has(normalizedProperty)) return "velocity";
return null;
};
export const networkResultUnitsFromOptions = (
options: Record<string, string>,
): NetworkResultUnits => {
const flow = options.FLOW_UNITS || options.UNITS || "LPS";
const pressure = options.PRESSURE_UNITS || options.PRESSURE || "MTR";
return {
flow,
pressure,
velocity: IMPERIAL_FLOW_UNITS.has(flow.trim().toUpperCase())
? "ft/s"
: "m/s",
};
};
export const toM3h = (value: number, sourceUnit: string = "m³/s") => {
if (!Number.isFinite(value)) return Number.NaN;
const normalizedUnit = sourceUnit.trim().toLowerCase();
if (normalizedUnit === "m³/h") return value;
if (normalizedUnit === "lps" || normalizedUnit === "l/s") return value * 3.6;
if (normalizedUnit === "m³/s") return value * M3H_FACTOR;
return value * M3H_FACTOR;
const normalizedUnit = sourceUnit.trim().toUpperCase().replace("³", "3");
const factors: Record<string, number> = {
CFS: 101.9406477312,
GPM: 0.22712470704,
MGD: 157.725491,
IMGD: 189.4204167,
AFD: 51.39507656,
LPS: 3.6,
"L/S": 3.6,
LPM: 0.06,
MLD: 41.6666666667,
CMH: 1,
"M3/H": 1,
CMD: 1 / 24,
"M3/D": 1 / 24,
"M3/S": M3H_FACTOR,
};
const factor = factors[normalizedUnit];
if (factor === undefined) throw new Error(`不支持的流量单位: ${sourceUnit}`);
return value * factor;
};
export const toMeters = (value: number, sourceUnit: string = "m") => {
if (!Number.isFinite(value)) return Number.NaN;
const normalizedUnit = sourceUnit.trim().toUpperCase();
const factors: Record<string, number> = {
METERS: 1,
METRES: 1,
MTR: 1,
M: 1,
MH2O: 1,
"M H2O": 1,
KPA: 0.1019716213,
PSI: 0.7032496149,
};
const factor = factors[normalizedUnit];
if (factor === undefined) throw new Error(`不支持的压力单位: ${sourceUnit}`);
return value * factor;
};
export const toMetersPerSecond = (
value: number,
sourceUnit: string = "m/s",
) => {
if (!Number.isFinite(value)) return Number.NaN;
const normalizedUnit = sourceUnit.trim().toUpperCase();
if (["M/S", "MPS"].includes(normalizedUnit)) return value;
if (["FT/S", "FPS"].includes(normalizedUnit)) return value * 0.3048;
throw new Error(`不支持的流速单位: ${sourceUnit}`);
};
export const toDisplayValue = (
value: number | null | undefined,
metric: MeasurementMetric,
sourceUnit: string,
) => {
if (value === null || value === undefined) return null;
switch (metric) {
case "flow":
return toM3h(value, sourceUnit);
case "pressure":
return toMeters(value, sourceUnit);
case "velocity":
return toMetersPerSecond(value, sourceUnit);
}
};
export const toModelDisplayValue = (
value: number,
property: string,
units: NetworkResultUnits,
) => {
const metric = metricForResultProperty(property);
if (!metric) return value;
return toDisplayValue(value, metric, units[metric])!;
};
export const toM3s = (value: number, sourceUnit: string = "m³/h") => {