feat(workbench): refine feature detail popover
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getFeaturePanelModel, getFeaturePanelProperties } from "./feature-properties";
|
||||
|
||||
describe("feature panel properties", () => {
|
||||
it.each([
|
||||
["pumps", ["id", "status", "startup_depth", "shutoff_depth"]],
|
||||
["conduits", ["id", "length", "diameter"]],
|
||||
["junctions", ["id", "invert_elevation", "max_depth"]],
|
||||
["orifices", ["id", "discharge_coeff", "gated", "shape", "geom1", "geom2"]],
|
||||
["outfalls", ["id", "invert_elevation", "outfall_type"]]
|
||||
] as const)("shows the configured %s fields", (layer, expectedKeys) => {
|
||||
const entries = getFeaturePanelProperties(layer, {}, "feature-id");
|
||||
|
||||
expect(entries.map((entry) => entry.key)).toEqual(expectedKeys);
|
||||
expect(entries[0].value).toBe("feature-id");
|
||||
});
|
||||
|
||||
it("formats conduit diameter from meters to millimeters", () => {
|
||||
const entries = getFeaturePanelProperties("conduits", {
|
||||
id: "C-1",
|
||||
length: 42.5,
|
||||
diameter: 0.6
|
||||
});
|
||||
|
||||
expect(entries.map(({ label, value }) => [label, value])).toEqual([
|
||||
["编号", "C-1"],
|
||||
["长度", "42.50 m"],
|
||||
["管径", "600 mm"]
|
||||
]);
|
||||
});
|
||||
|
||||
it("localizes free outfall type", () => {
|
||||
const entries = getFeaturePanelProperties("outfalls", {
|
||||
id: "OF-1",
|
||||
invert_elevation: -5.67,
|
||||
outfall_type: "FREE"
|
||||
});
|
||||
|
||||
expect(entries.map((entry) => entry.value)).toEqual(["OF-1", "-5.67 m", "自由出流"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
layer: "pumps",
|
||||
properties: { id: "P-1", status: "ON", startup_depth: 1.2, shutoff_depth: 0.4 },
|
||||
badge: { label: "运行", tone: "active" },
|
||||
attributeKeys: ["startup_depth", "shutoff_depth"]
|
||||
},
|
||||
{
|
||||
layer: "orifices",
|
||||
properties: { id: "O-1", gated: "YES", discharge_coeff: 0.65, shape: "RECT_CLOSED" },
|
||||
badge: { label: "有闸门", tone: "info" },
|
||||
attributeKeys: ["discharge_coeff", "shape", "geom1", "geom2"]
|
||||
}
|
||||
] as const)("promotes the $layer badge without repeating it", ({ layer, properties, badge, attributeKeys }) => {
|
||||
const model = getFeaturePanelModel(layer, properties);
|
||||
|
||||
expect(model.id).toBe(properties.id);
|
||||
expect(model.badge).toEqual(badge);
|
||||
expect(model.attributes.map((entry) => entry.key)).toEqual(attributeKeys);
|
||||
expect(model.attributes.every((entry) => entry.value !== properties.id)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the map feature id when the properties omit it", () => {
|
||||
const model = getFeaturePanelModel("junctions", { max_depth: 3.4 }, "map-feature-id");
|
||||
|
||||
expect(model.id).toBe("map-feature-id");
|
||||
expect(model.badge).toBeUndefined();
|
||||
expect(model.attributes.map((entry) => entry.key)).toEqual(["invert_elevation", "max_depth"]);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { WaterNetworkSourceId } from "../map/sources";
|
||||
import { formatValue } from "./format-value";
|
||||
|
||||
export type LocalizedFeatureProperty = {
|
||||
@@ -6,6 +7,19 @@ export type LocalizedFeatureProperty = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type FeaturePanelBadgeTone = "active" | "inactive" | "warning" | "critical" | "info";
|
||||
|
||||
export type FeaturePanelBadge = {
|
||||
label: string;
|
||||
tone: FeaturePanelBadgeTone;
|
||||
};
|
||||
|
||||
export type FeaturePanelModel = {
|
||||
id: string;
|
||||
badge?: FeaturePanelBadge;
|
||||
attributes: LocalizedFeatureProperty[];
|
||||
};
|
||||
|
||||
const PROPERTY_LABELS: Record<string, string> = {
|
||||
id: "编号",
|
||||
gid: "要素编号",
|
||||
@@ -20,6 +34,20 @@ const PROPERTY_LABELS: Record<string, string> = {
|
||||
dn: "管径",
|
||||
length: "长度",
|
||||
roughness: "粗糙系数",
|
||||
shape: "断面形状",
|
||||
max_depth: "最大深度",
|
||||
invert_elevation: "井底高程",
|
||||
orifice_type: "孔口类型",
|
||||
discharge_coeff: "流量系数",
|
||||
gated: "闸门",
|
||||
offset_height: "偏移高度",
|
||||
geom1: "几何参数 1",
|
||||
geom2: "几何参数 2",
|
||||
pump_curve: "泵曲线",
|
||||
startup_depth: "启动水深",
|
||||
shutoff_depth: "停泵水深",
|
||||
outfall_type: "排放类型",
|
||||
stage_data: "阶段数据",
|
||||
status: "状态",
|
||||
material: "材质",
|
||||
elevation: "高程",
|
||||
@@ -42,6 +70,12 @@ const PROPERTY_ORDER: Record<string, number> = {
|
||||
dn: 20,
|
||||
length: 21,
|
||||
roughness: 22,
|
||||
max_depth: 30,
|
||||
invert_elevation: 31,
|
||||
orifice_type: 32,
|
||||
outfall_type: 32,
|
||||
shape: 33,
|
||||
pump_curve: 34,
|
||||
material: 23,
|
||||
node1: 24,
|
||||
from_node: 24,
|
||||
@@ -56,6 +90,8 @@ const PROPERTY_ORDER: Record<string, number> = {
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
on: "运行",
|
||||
off: "停止",
|
||||
active: "运行中",
|
||||
inactive: "停用",
|
||||
online: "在线",
|
||||
@@ -67,6 +103,32 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
critical: "严重"
|
||||
};
|
||||
|
||||
const OUTFALL_TYPE_LABELS: Record<string, string> = {
|
||||
free: "自由出流"
|
||||
};
|
||||
|
||||
const GATED_LABELS: Record<string, string> = {
|
||||
no: "否",
|
||||
false: "否",
|
||||
"0": "否",
|
||||
yes: "是",
|
||||
true: "是",
|
||||
"1": "是"
|
||||
};
|
||||
|
||||
const FEATURE_PANEL_PROPERTY_KEYS: Record<WaterNetworkSourceId, readonly string[]> = {
|
||||
pumps: ["id", "status", "startup_depth", "shutoff_depth"],
|
||||
conduits: ["id", "length", "diameter"],
|
||||
junctions: ["id", "invert_elevation", "max_depth"],
|
||||
orifices: ["id", "discharge_coeff", "gated", "shape", "geom1", "geom2"],
|
||||
outfalls: ["id", "invert_elevation", "outfall_type"]
|
||||
};
|
||||
|
||||
const FEATURE_PANEL_BADGE_KEYS: Partial<Record<WaterNetworkSourceId, string>> = {
|
||||
pumps: "status",
|
||||
orifices: "gated"
|
||||
};
|
||||
|
||||
export function getLocalizedFeatureProperties(properties: Record<string, unknown>): LocalizedFeatureProperty[] {
|
||||
return Object.entries(properties).map(([key, value]) => ({
|
||||
key,
|
||||
@@ -75,6 +137,39 @@ export function getLocalizedFeatureProperties(properties: Record<string, unknown
|
||||
})).sort((first, second) => getPropertyOrder(first.key) - getPropertyOrder(second.key));
|
||||
}
|
||||
|
||||
export function getFeaturePanelProperties(
|
||||
layer: WaterNetworkSourceId,
|
||||
properties: Record<string, unknown>,
|
||||
featureId?: string
|
||||
): LocalizedFeatureProperty[] {
|
||||
return FEATURE_PANEL_PROPERTY_KEYS[layer].map((key) => {
|
||||
const value = key === "id" ? properties[key] ?? featureId : properties[key];
|
||||
|
||||
return {
|
||||
key,
|
||||
label: getFeaturePropertyLabel(key),
|
||||
value: formatFeaturePropertyValue(key, value)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getFeaturePanelModel(
|
||||
layer: WaterNetworkSourceId,
|
||||
properties: Record<string, unknown>,
|
||||
featureId?: string
|
||||
): FeaturePanelModel {
|
||||
const entries = getFeaturePanelProperties(layer, properties, featureId);
|
||||
const id = entries.find((entry) => entry.key === "id")?.value ?? "暂无";
|
||||
const badgeKey = FEATURE_PANEL_BADGE_KEYS[layer];
|
||||
const badgeEntry = badgeKey ? entries.find((entry) => entry.key === badgeKey) : undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
badge: badgeEntry ? getFeaturePanelBadge(layer, badgeEntry.value) : undefined,
|
||||
attributes: entries.filter((entry) => entry.key !== "id" && entry.key !== badgeKey)
|
||||
};
|
||||
}
|
||||
|
||||
export function getFeaturePropertyLabel(key: string) {
|
||||
const normalizedKey = normalizePropertyKey(key);
|
||||
return PROPERTY_LABELS[normalizedKey] ?? key;
|
||||
@@ -87,11 +182,23 @@ export function formatFeaturePropertyValue(key: string, value: unknown) {
|
||||
return STATUS_LABELS[value.toLowerCase()] ?? value;
|
||||
}
|
||||
|
||||
if ((normalizedKey === "diameter" || normalizedKey === "dn") && value !== null && value !== undefined && value !== "") {
|
||||
return `DN${formatValue(value)}`;
|
||||
if (normalizedKey === "outfall_type" && typeof value === "string") {
|
||||
return OUTFALL_TYPE_LABELS[value.toLowerCase()] ?? value;
|
||||
}
|
||||
|
||||
if ((normalizedKey === "length" || normalizedKey === "elevation") && value !== null && value !== undefined && value !== "") {
|
||||
if (normalizedKey === "gated" && value !== null && value !== undefined && value !== "") {
|
||||
return GATED_LABELS[String(value).toLowerCase()] ?? formatValue(value);
|
||||
}
|
||||
|
||||
if ((normalizedKey === "diameter" || normalizedKey === "dn") && value !== null && value !== undefined && value !== "") {
|
||||
const diameterInMeters = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(diameterInMeters)
|
||||
? `${formatValue(diameterInMeters * 1000)} mm`
|
||||
: formatValue(value);
|
||||
}
|
||||
|
||||
if (["length", "elevation", "invert_elevation", "max_depth", "startup_depth", "shutoff_depth"].includes(normalizedKey)
|
||||
&& value !== null && value !== undefined && value !== "") {
|
||||
return `${formatValue(value)} m`;
|
||||
}
|
||||
|
||||
@@ -109,3 +216,36 @@ function normalizePropertyKey(key: string) {
|
||||
function isStatusKey(key: string) {
|
||||
return normalizePropertyKey(key) === "status";
|
||||
}
|
||||
|
||||
function getFeaturePanelBadge(
|
||||
layer: WaterNetworkSourceId,
|
||||
value: string
|
||||
): FeaturePanelBadge {
|
||||
if (layer === "orifices") {
|
||||
return value === "是"
|
||||
? { label: "有闸门", tone: "info" }
|
||||
: value === "否"
|
||||
? { label: "无闸门", tone: "inactive" }
|
||||
: { label: `闸门 ${value}`, tone: "inactive" };
|
||||
}
|
||||
|
||||
const normalizedValue = value.toLowerCase();
|
||||
|
||||
if (["运行", "运行中", "在线", "开启", "正常"].includes(value)) {
|
||||
return { label: value, tone: "active" };
|
||||
}
|
||||
|
||||
if (["停止", "停用", "离线", "关闭"].includes(value)) {
|
||||
return { label: value, tone: "inactive" };
|
||||
}
|
||||
|
||||
if (normalizedValue === "warning" || value === "告警") {
|
||||
return { label: value, tone: "warning" };
|
||||
}
|
||||
|
||||
if (normalizedValue === "critical" || value === "严重") {
|
||||
return { label: value, tone: "critical" };
|
||||
}
|
||||
|
||||
return { label: value, tone: "inactive" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user