import { isSourceGisSourceId, type WaterNetworkSourceId } from "../map/sources"; import { formatValue } from "./format-value"; export type LocalizedFeatureProperty = { key: string; label: string; 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[]; }; export type FeatureInsightMetric = { label: string; value: string; }; const PROPERTY_LABELS: Record = { fid: "GeoServer 要素 ID", id: "编号", scada_id: "编号 UUID", swmm_node: "SWMM 节点", topology_order: "拓扑序", distance_to_wwtp_m: "距处理厂距离", upstream_node_count: "上游节点数", upstream_sensors: "上游传感器", downstream_path: "下游路径", x: "X 坐标", y: "Y 坐标", cod_mg_l: "COD", 氨氮_mg_l: "氨氮", 电导率_us_cm: "电导率", 液位_m: "液位", 流量_m3_s: "流量", gid: "要素编号", name: "名称", code: "编码", type: "类型", device_type: "设备类型", node1: "起点节点", node2: "终点节点", start_node_id: "起点节点", end_node_id: "终点节点", from_node: "起点节点", to_node: "终点节点", diameter: "管径", dn: "管径", length: "长度", roughness: "粗糙系数", shape: "断面形状", max_depth: "最大深度", invert_elevation: "井底高程", v_type: "阀门类型", valve_type: "阀门类型", setting: "阀门设定", minor_loss: "局部损失系数", orifice_type: "孔口类型", discharge_coeff: "流量系数", gated: "闸门", offset_height: "偏移高度", geom1: "几何参数 1", geom2: "几何参数 2", pump_curve: "泵曲线", startup_depth: "启动水深", shutoff_depth: "停泵水深", outfall_type: "排放类型", stage_data: "阶段数据", status: "状态", head: "水头", pattern: "模式", pattern_id: "模式", head_curve_id: "扬程曲线", volume_curve_id: "容积曲线", init_level: "初始水位", min_level: "最低水位", max_level: "最高水位", initial_level: "初始水位", minimum_level: "最低水位", maximum_level: "最高水位", minimum_volume: "最小容积", overflow: "允许溢流", power: "功率", speed: "转速倍率", material: "材质", elevation: "高程", demand: "需水量", base_demand: "基础需水量", demands: "需水配置", pressure: "压力", flow: "流量", velocity: "流速", zone: "分区", district: "片区", source: "来源", updated_at: "更新时间", point_id: "测点 ID", point_external_id: "测点编号", point_name: "测点名称", associated_element_id: "关联资产", node_id: "关联节点", link_id: "关联连线", api_query_id: "采集接口", measurement_unit: "计量单位", transmission_mode: "传输方式", transmission_frequency: "传输频率", reliability: "可靠性", latest_value: "最新监测值", latest_value_with_unit: "最新监测值", latest_observed_at: "监测时间", latest_value_source: "数值来源", device_id: "设备 ID", device_external_id: "设备编号", device_type_id: "设备类型 ID", device_type_name: "设备类型", device_icon_code: "设备图标编码", active: "运行状态", external_id: "设备编号", junction_id: "关联节点", kind: "设备类型", location_source: "位置来源", site_external_id: "站点编号", synced_at: "同步时间", source_fid: "源要素编号", source_object_id: "源对象编号", source_layer_name: "源图层", dataset_title: "数据集", source_date: "数据日期", monitoring_point_no: "监测点序号", selection_method: "选点依据", minimum_diameter: "关联管径下限", source_analysis: "来源分析", run_id: "分析运行编号" }; const PROPERTY_ORDER: Record = { fid: 9, id: 10, scada_id: 10, swmm_node: 13, topology_order: 14, distance_to_wwtp_m: 15, upstream_node_count: 16, upstream_sensors: 17, downstream_path: 18, cod_mg_l: 20, 氨氮_mg_l: 21, 电导率_us_cm: 22, 液位_m: 23, 流量_m3_s: 24, gid: 11, name: 12, code: 13, diameter: 20, 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, start_node_id: 24, node2: 25, to_node: 25, end_node_id: 25, elevation: 30, demand: 31, base_demand: 31, pressure: 32, flow: 33, velocity: 34, status: 90 }; const STATUS_LABELS: Record = { on: "运行", off: "停止", active: "运行中", inactive: "停用", online: "在线", offline: "离线", open: "开启", closed: "关闭", normal: "正常", warning: "告警", critical: "严重" }; const VALVE_TYPE_LABELS: Record = { prv: "减压阀", psv: "持压阀", pbv: "压降控制阀", fcv: "流量控制阀", tcv: "节流控制阀", gpv: "通用阀", cv: "止回阀" }; const MATERIAL_LABELS: Record = { di: "球墨铸铁", ci: "铸铁", steel: "钢管", st: "钢管", pvc: "PVC", pe: "PE", hdpe: "HDPE", concrete: "混凝土", rc: "钢筋混凝土" }; const ASSET_TYPE_LABELS: Record = { pipe: "管线", conduit: "管线", junction: "节点", node: "节点", valve: "阀门", reservoir: "水库", tank: "水箱", pump: "泵站", scada: "SCADA", pressure: "压力监测", pipe_flow: "流量监测" }; const TRANSMISSION_MODE_LABELS: Record = { wired: "有线", wire: "有线", cable: "有线", wireless: "无线", cellular: "蜂窝网络", mobile: "移动网络", gprs: "GPRS", "4g": "4G", "5g": "5G", nb_iot: "NB-IoT", nbiot: "NB-IoT", lora: "LoRa", realtime: "实时", real_time: "实时", non_realtime: "非实时", non_real_time: "非实时" }; const RELIABILITY_LABELS: Record = { high: "高", medium: "中", mid: "中", low: "低", good: "良好", fair: "一般", poor: "较差" }; const PATTERN_LABELS: Record = { none: "无", null: "无", default: "默认" }; const OUTFALL_TYPE_LABELS: Record = { free: "自由出流" }; const GATED_LABELS: Record = { no: "否", false: "否", "0": "否", yes: "是", true: "是", "1": "是" }; const SCADA_KIND_LABELS: Record = { water_quality: "水质监测", radar_level: "雷达液位", ultrasonic_flow: "超声流量", integrated_monitoring: "综合监测点" }; export const SCADA_METRIC_PROPERTY_KEYS = [ "COD_mg_L", "氨氮_mg_L", "电导率_uS_cm", "液位_m", "流量_m3_s" ] as const; const PROPERTY_UNITS: Record = { cod_mg_l: "mg/L", 氨氮_mg_l: "mg/L", 电导率_us_cm: "μS/cm", 液位_m: "m", 流量_m3_s: "m³/s" }; const FEATURE_PANEL_PROPERTY_KEYS: Partial> = { pipes: ["id", "start_node_id", "end_node_id", "diameter", "length", "roughness", "status"], junctions: ["id", "elevation", "base_demand"], valves: ["id", "start_node_id", "end_node_id", "diameter", "valve_type", "setting", "minor_loss"], reservoirs: ["id", "head", "pattern_id"], scada: [ "device_id", "device_type", "latest_value_with_unit", "latest_observed_at", "node_id", "link_id", "api_query_id", "measurement_unit", "transmission_mode", "transmission_frequency", "reliability" ], pumps: ["id", "start_node_id", "end_node_id", "power", "head_curve_id", "speed", "pattern_id"], tanks: ["id", "elevation", "initial_level", "minimum_level", "maximum_level", "diameter", "minimum_volume", "volume_curve_id", "overflow"], water_quality_monitoring_points: [ "id", "monitoring_point_no", "node_id", "selection_method", "minimum_diameter", "elevation", "source_analysis" ] }; const SOURCE_GIS_PANEL_PROPERTY_KEYS = [ "id", "source_layer_name", "source_date", "source_object_id", "diameter", "length", "dataset_title" ] as const; const FEATURE_PANEL_BADGE_KEYS: Partial> = { pipes: "status", valves: "setting" }; type FeatureInsightMetricDefinition = { label: string; key?: string; value?: string; }; const FEATURE_INSIGHT_METRICS: Partial> = { pipes: [ { label: "管径", key: "diameter" }, { label: "长度", key: "length" }, { label: "粗糙系数", key: "roughness" }, { label: "状态", key: "status" } ], junctions: [ { label: "基础需水量", key: "base_demand" }, { label: "高程", key: "elevation" }, { label: "X 坐标", key: "x" }, { label: "Y 坐标", key: "y" } ], valves: [ { label: "管径", key: "diameter" }, { label: "类型", key: "valve_type" }, { label: "设定", key: "setting" }, { label: "局部损失", key: "minor_loss" } ], reservoirs: [ { label: "水头", key: "head" }, { label: "模式", key: "pattern_id" }, { label: "X 坐标", key: "x" }, { label: "Y 坐标", key: "y" } ], scada: [ { label: "最新监测值", key: "latest_value_with_unit" }, { label: "监测时间", key: "latest_observed_at" }, { label: "关联节点", key: "node_id" }, { label: "关联连线", key: "link_id" }, { label: "类型", key: "device_type" } ], pumps: [ { label: "功率", key: "power" }, { label: "扬程曲线", key: "head_curve_id" }, { label: "转速倍率", key: "speed" }, { label: "模式", key: "pattern_id" } ], tanks: [ { label: "高程", key: "elevation" }, { label: "初始水位", key: "initial_level" }, { label: "最低水位", key: "minimum_level" }, { label: "最高水位", key: "maximum_level" } ], water_quality_monitoring_points: [ { label: "监测点序号", key: "monitoring_point_no" }, { label: "关联节点", key: "node_id" }, { label: "关联管径下限", key: "minimum_diameter" }, { label: "选点依据", value: "灵敏度分析" } ] }; export function getLocalizedFeatureProperties(properties: Record): LocalizedFeatureProperty[] { return Object.entries(properties).filter(([key]) => normalizePropertyKey(key) !== "cluster_size").map(([key, value]) => ({ key, label: getFeaturePropertyLabel(key), value: formatFeaturePropertyValue(key, value) })).sort((first, second) => getPropertyOrder(first.key) - getPropertyOrder(second.key)); } export function getFeaturePanelProperties( layer: WaterNetworkSourceId, properties: Record, featureId?: string ): LocalizedFeatureProperty[] { const keys = FEATURE_PANEL_PROPERTY_KEYS[layer] ?? (isSourceGisSourceId(layer) ? SOURCE_GIS_PANEL_PROPERTY_KEYS : ["id"]); return keys.map((key) => { const propertyValue = getPropertyValue(properties, key); const value = key === "id" || key === "scada_id" || key === "device_id" ? propertyValue ?? featureId : propertyValue; return { key, label: getFeaturePropertyLabel(key), value: formatFeaturePropertyValue(key, value) }; }); } export function getScadaMetricProperties( properties: Record ): LocalizedFeatureProperty[] { return SCADA_METRIC_PROPERTY_KEYS.map((key) => ({ key, label: getFeaturePropertyLabel(key), value: formatFeaturePropertyValue(key, getPropertyValue(properties, key)) })); } export function getFeatureInsightMetrics( layer: WaterNetworkSourceId, properties: Record ): FeatureInsightMetric[] { const definitions = FEATURE_INSIGHT_METRICS[layer] ?? (isSourceGisSourceId(layer) ? [ { label: "源图层", key: "source_layer_name" }, { label: "源要素", key: "source_fid" }, { label: "数据日期", key: "source_date" } ] : []); return definitions.map((metric) => ({ label: metric.label, value: metric.key ? formatFeaturePropertyValue(metric.key, getPropertyValue(properties, metric.key)) : metric.value ?? "暂无" })); } export function getFeaturePanelModel( layer: WaterNetworkSourceId, properties: Record, featureId?: string ): FeaturePanelModel { const entries = getFeaturePanelProperties(layer, properties, featureId); const idKey = layer === "scada" ? "device_id" : "id"; const id = entries.find((entry) => entry.key === idKey)?.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 !== idKey && entry.key !== badgeKey) }; } export function getFeaturePropertyLabel(key: string) { const normalizedKey = normalizePropertyKey(key); return PROPERTY_LABELS[normalizedKey] ?? key; } export function formatFeaturePropertyValue(key: string, value: unknown) { const normalizedKey = normalizePropertyKey(key); if (typeof value === "string") { const mappedValue = getMappedPropertyValue(normalizedKey, value); if (mappedValue) return mappedValue; } if (normalizedKey === "outfall_type" && typeof value === "string") { return OUTFALL_TYPE_LABELS[value.toLowerCase()] ?? value; } if (["gated", "overflow"].includes(normalizedKey) && value !== null && value !== undefined && value !== "") { return GATED_LABELS[String(value).toLowerCase()] ?? formatValue(value); } if (normalizedKey === "active" && typeof value === "boolean") { return value ? "在线" : "离线"; } if (normalizedKey === "latest_observed_at" && typeof value === "string") { const timestamp = Date.parse(value); if (Number.isFinite(timestamp)) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).format(new Date(timestamp)); } } if (normalizedKey === "latest_value_source" && value === "monitored_value") { return "SCADA 原始监测值"; } if (normalizedKey === "location_source" && value === "density_cluster_demo") { return "密度聚类模拟位置"; } if (normalizedKey === "location_source" && value === "uniform_farthest_point_demo") { return "空间均匀采样模拟位置"; } if (normalizedKey === "selection_method" && value === "sensitivity") { return "灵敏度分析"; } if (normalizedKey === "kind" && typeof value === "string") { return SCADA_KIND_LABELS[value] ?? value; } const unit = PROPERTY_UNITS[normalizedKey]; if (unit && value !== null && value !== undefined && value !== "") { return `${formatValue(value)} ${unit}`; } if (["diameter", "dn", "minimum_diameter"].includes(normalizedKey) && value !== null && value !== undefined && value !== "") { const diameterInMillimeters = typeof value === "number" ? value : Number(value); return Number.isFinite(diameterInMillimeters) ? `${formatValue(diameterInMillimeters)} mm` : formatValue(value); } if (["length", "elevation", "invert_elevation", "max_depth", "startup_depth", "shutoff_depth", "distance_to_wwtp_m", "initial_level", "minimum_level", "maximum_level"].includes(normalizedKey) && value !== null && value !== undefined && value !== "") { return `${formatValue(value)} m`; } return formatValue(value); } function getPropertyOrder(key: string) { return PROPERTY_ORDER[normalizePropertyKey(key)] ?? 100; } function normalizePropertyKey(key: string) { return key.trim().replaceAll("-", "_").toLowerCase(); } function getPropertyValue(properties: Record, key: string) { if (Object.prototype.hasOwnProperty.call(properties, key)) { return properties[key]; } const normalizedKey = normalizePropertyKey(key); const matchingKey = Object.keys(properties).find((propertyKey) => normalizePropertyKey(propertyKey) === normalizedKey); return matchingKey ? properties[matchingKey] : undefined; } function getMappedPropertyValue(normalizedKey: string, value: string) { const normalizedValue = normalizePropertyValue(value); if (isStatusLikeKey(normalizedKey)) { return STATUS_LABELS[normalizedValue]; } if (normalizedKey === "v_type" || normalizedKey === "valve_type") { return VALVE_TYPE_LABELS[normalizedValue]; } if (normalizedKey === "material") { return MATERIAL_LABELS[normalizedValue]; } if (normalizedKey === "type" || normalizedKey === "device_type") { return ASSET_TYPE_LABELS[normalizedValue]; } if (normalizedKey === "transmission_mode") { return TRANSMISSION_MODE_LABELS[normalizedValue]; } if (normalizedKey === "reliability") { return RELIABILITY_LABELS[normalizedValue]; } if (normalizedKey === "pattern" || normalizedKey === "pattern_id") { return PATTERN_LABELS[normalizedValue]; } return undefined; } function normalizePropertyValue(value: string) { return value.trim().replaceAll("-", "_").replaceAll(" ", "_").toLowerCase(); } function isStatusLikeKey(normalizedKey: string) { return normalizedKey === "status" || normalizedKey === "setting"; } function getFeaturePanelBadge( layer: WaterNetworkSourceId, value: string ): FeaturePanelBadge { 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" }; }