@caoguo/maplibre-pipeline 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/burst/index.cjs +708 -0
- package/dist/burst/index.cjs.map +1 -0
- package/dist/burst/index.js +4 -0
- package/dist/burst/index.js.map +1 -0
- package/dist/burstClass-CGMjcAYy.d.cts +161 -0
- package/dist/burstClass-Cf2p71LE.d.ts +161 -0
- package/dist/chunk-BWQKY35F.js +259 -0
- package/dist/chunk-BWQKY35F.js.map +1 -0
- package/dist/chunk-CDEYJ67B.js +80 -0
- package/dist/chunk-CDEYJ67B.js.map +1 -0
- package/dist/chunk-GCDA7KJM.js +289 -0
- package/dist/chunk-GCDA7KJM.js.map +1 -0
- package/dist/chunk-HCT6NSNS.js +532 -0
- package/dist/chunk-HCT6NSNS.js.map +1 -0
- package/dist/chunk-HIRNJVCV.js +13 -0
- package/dist/chunk-HIRNJVCV.js.map +1 -0
- package/dist/chunk-PJMHTPSA.js +352 -0
- package/dist/chunk-PJMHTPSA.js.map +1 -0
- package/dist/chunk-QVY4WJLM.js +256 -0
- package/dist/chunk-QVY4WJLM.js.map +1 -0
- package/dist/chunk-VA2TQWL3.js +340 -0
- package/dist/chunk-VA2TQWL3.js.map +1 -0
- package/dist/chunk-YBLESEN4.js +133 -0
- package/dist/chunk-YBLESEN4.js.map +1 -0
- package/dist/chunk-YS2ICFVK.js +101 -0
- package/dist/chunk-YS2ICFVK.js.map +1 -0
- package/dist/graph/index.cjs +382 -0
- package/dist/graph/index.cjs.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/health/index.cjs +348 -0
- package/dist/health/index.cjs.map +1 -0
- package/dist/health/index.js +3 -0
- package/dist/health/index.js.map +1 -0
- package/dist/index.cjs +2389 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/leakage/index.cjs +7 -6
- package/dist/leakage/index.cjs.map +1 -1
- package/dist/leakage/index.js +1 -1
- package/dist/topology/index.cjs +113 -14
- package/dist/topology/index.cjs.map +1 -1
- package/dist/topology/index.d.cts +65 -15
- package/dist/topology/index.d.ts +65 -15
- package/dist/topology/index.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { paintPipeBy } from './chunk-QVY4WJLM.js';
|
|
2
|
+
import { upsertSource } from '@caoguo/maplibre';
|
|
3
|
+
|
|
4
|
+
var PipelineTopology = class {
|
|
5
|
+
map;
|
|
6
|
+
dataset;
|
|
7
|
+
colorBy;
|
|
8
|
+
layerPrefix;
|
|
9
|
+
pipelineTypes;
|
|
10
|
+
layerIds = [];
|
|
11
|
+
nodeListeners = /* @__PURE__ */ new Set();
|
|
12
|
+
pipeListeners = /* @__PURE__ */ new Set();
|
|
13
|
+
drillListeners = /* @__PURE__ */ new Set();
|
|
14
|
+
/** 当前钻取区域(null = 全量) */
|
|
15
|
+
currentRegion = null;
|
|
16
|
+
/** 当前分层过滤器 */
|
|
17
|
+
layerFilter = null;
|
|
18
|
+
/** click 事件是否已绑定 */
|
|
19
|
+
clickBound = false;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.map = options.map;
|
|
22
|
+
this.dataset = options.dataset;
|
|
23
|
+
this.colorBy = options.colorBy ?? "type";
|
|
24
|
+
this.pipelineTypes = options.pipelineTypes;
|
|
25
|
+
this.layerPrefix = options.layerPrefix ?? "cg-topo";
|
|
26
|
+
}
|
|
27
|
+
/** 渲染管段+节点到地图 */
|
|
28
|
+
render() {
|
|
29
|
+
this.clear();
|
|
30
|
+
const mlMap = this.map.instance;
|
|
31
|
+
const prefix = this.layerPrefix;
|
|
32
|
+
const pipeGeoJSON = {
|
|
33
|
+
type: "FeatureCollection",
|
|
34
|
+
features: this.dataset.pipes.filter((p) => this.isPipeVisible(p)).flatMap((p) => {
|
|
35
|
+
const from = this.dataset.nodes.find((n) => n.id === p.fromNode);
|
|
36
|
+
const to = this.dataset.nodes.find((n) => n.id === p.toNode);
|
|
37
|
+
if (!from || !to) return [];
|
|
38
|
+
const coords = p.geometry && p.geometry.length >= 2 ? p.geometry : [
|
|
39
|
+
[from.lng, from.lat],
|
|
40
|
+
[to.lng, to.lat]
|
|
41
|
+
];
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
type: "Feature",
|
|
45
|
+
geometry: { type: "LineString", coordinates: coords },
|
|
46
|
+
properties: {
|
|
47
|
+
pipeId: p.id,
|
|
48
|
+
pipelineType: p.pipelineType ?? "",
|
|
49
|
+
diameter: p.properties?.diameter ?? 0,
|
|
50
|
+
status: p.properties?.status ?? "unknown",
|
|
51
|
+
material: p.properties?.material ?? "unknown"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
})
|
|
56
|
+
};
|
|
57
|
+
const nodeGeoJSON = {
|
|
58
|
+
type: "FeatureCollection",
|
|
59
|
+
features: this.dataset.nodes.filter((n) => this.isNodeVisible(n)).map((n) => ({
|
|
60
|
+
type: "Feature",
|
|
61
|
+
geometry: { type: "Point", coordinates: [n.lng, n.lat] },
|
|
62
|
+
properties: { nodeId: n.id, kind: n.kind, pipelineType: n.pipelineType ?? "" }
|
|
63
|
+
}))
|
|
64
|
+
};
|
|
65
|
+
upsertSource(mlMap, `${prefix}-pipes-src`, pipeGeoJSON);
|
|
66
|
+
upsertSource(mlMap, `${prefix}-nodes-src`, nodeGeoJSON);
|
|
67
|
+
mlMap.addLayer({
|
|
68
|
+
id: `${prefix}-pipes-line`,
|
|
69
|
+
type: "line",
|
|
70
|
+
source: `${prefix}-pipes-src`,
|
|
71
|
+
paint: {
|
|
72
|
+
"line-color": paintPipeBy(this.colorBy, { types: this.pipelineTypes }),
|
|
73
|
+
"line-width": 2,
|
|
74
|
+
"line-opacity": 0.85
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
mlMap.addLayer({
|
|
78
|
+
id: `${prefix}-nodes-pt`,
|
|
79
|
+
type: "circle",
|
|
80
|
+
source: `${prefix}-nodes-src`,
|
|
81
|
+
paint: {
|
|
82
|
+
"circle-radius": 4,
|
|
83
|
+
"circle-color": "#f59e0b",
|
|
84
|
+
"circle-stroke-width": 1.5,
|
|
85
|
+
"circle-stroke-color": "#ffffff"
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
this.layerIds.push(`${prefix}-pipes-line`, `${prefix}-nodes-pt`);
|
|
89
|
+
this.bindClickEvents(mlMap, prefix);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 绑定点击事件(设备卡片:点击节点/管段触发订阅回调)。
|
|
93
|
+
* 只绑定一次(render 重建图层后事件仍指向固定 layerId)。
|
|
94
|
+
*/
|
|
95
|
+
bindClickEvents(mlMap, prefix) {
|
|
96
|
+
if (this.clickBound) return;
|
|
97
|
+
this.clickBound = true;
|
|
98
|
+
const nodeById = new Map(this.dataset.nodes.map((n) => [n.id, n]));
|
|
99
|
+
const pipeById = new Map(this.dataset.pipes.map((p) => [p.id, p]));
|
|
100
|
+
mlMap.on("click", `${prefix}-pipes-line`, (e) => {
|
|
101
|
+
const features = e.features ?? [];
|
|
102
|
+
const f = features[0];
|
|
103
|
+
const pipe = f?.properties?.pipeId ? pipeById.get(f.properties.pipeId) : void 0;
|
|
104
|
+
if (pipe) {
|
|
105
|
+
for (const l of this.pipeListeners) l({ pipe });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
mlMap.on("click", `${prefix}-nodes-pt`, (e) => {
|
|
109
|
+
const features = e.features ?? [];
|
|
110
|
+
const f = features[0];
|
|
111
|
+
const node = f?.properties?.nodeId ? nodeById.get(f.properties.nodeId) : void 0;
|
|
112
|
+
if (node) {
|
|
113
|
+
for (const l of this.nodeListeners) l({ node });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/** 切换着色模式 */
|
|
118
|
+
setColorBy(mode) {
|
|
119
|
+
this.colorBy = mode;
|
|
120
|
+
const mlMap = this.map.instance;
|
|
121
|
+
if (mlMap.setPaintProperty) {
|
|
122
|
+
try {
|
|
123
|
+
mlMap.setPaintProperty(
|
|
124
|
+
`${this.layerPrefix}-pipes-line`,
|
|
125
|
+
"line-color",
|
|
126
|
+
paintPipeBy(mode, { types: this.pipelineTypes })
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 清空所有图层 */
|
|
133
|
+
clear() {
|
|
134
|
+
for (const id of this.layerIds) {
|
|
135
|
+
this.map.removeLayer(id);
|
|
136
|
+
}
|
|
137
|
+
this.layerIds = [];
|
|
138
|
+
}
|
|
139
|
+
/** 销毁组件 */
|
|
140
|
+
destroy() {
|
|
141
|
+
this.clear();
|
|
142
|
+
this.nodeListeners.clear();
|
|
143
|
+
this.pipeListeners.clear();
|
|
144
|
+
this.drillListeners.clear();
|
|
145
|
+
}
|
|
146
|
+
/** 订阅节点点击事件 */
|
|
147
|
+
onNodeSelect(fn) {
|
|
148
|
+
this.nodeListeners.add(fn);
|
|
149
|
+
return () => this.nodeListeners.delete(fn);
|
|
150
|
+
}
|
|
151
|
+
/** 订阅管段点击事件 */
|
|
152
|
+
onPipeSelect(fn) {
|
|
153
|
+
this.pipeListeners.add(fn);
|
|
154
|
+
return () => this.pipeListeners.delete(fn);
|
|
155
|
+
}
|
|
156
|
+
/** 订阅钻取事件 */
|
|
157
|
+
onDrillDown(fn) {
|
|
158
|
+
this.drillListeners.add(fn);
|
|
159
|
+
return () => this.drillListeners.delete(fn);
|
|
160
|
+
}
|
|
161
|
+
/** 管段是否可见(管线类型 + 区域钻取 + 分层过滤) */
|
|
162
|
+
isPipeVisible(p) {
|
|
163
|
+
if (this.pipelineTypes?.length && !this.pipelineTypes.includes(p.pipelineType)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (this.currentRegion && p.region !== this.currentRegion) return false;
|
|
167
|
+
const f = this.layerFilter;
|
|
168
|
+
if (f) {
|
|
169
|
+
const props = p.properties ?? {};
|
|
170
|
+
if (f.minDiameter !== void 0 && (props.diameter ?? 0) < f.minDiameter) return false;
|
|
171
|
+
if (f.maxDiameter !== void 0 && (props.diameter ?? Infinity) > f.maxDiameter) return false;
|
|
172
|
+
if (f.material && props.material !== f.material) return false;
|
|
173
|
+
if (f.status && props.status !== f.status) return false;
|
|
174
|
+
if (f.minAgeYears !== void 0 && props.installDate) {
|
|
175
|
+
if (this.ageInYears(props.installDate) < f.minAgeYears) return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
/** 节点是否可见(管线类型 + 区域钻取) */
|
|
181
|
+
isNodeVisible(n) {
|
|
182
|
+
if (this.pipelineTypes?.length && !this.pipelineTypes.includes(n.pipelineType)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
if (this.currentRegion && n.region !== this.currentRegion) return false;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
/** 计算管段使用年限(年) */
|
|
189
|
+
ageInYears(installDate) {
|
|
190
|
+
const d = new Date(installDate).getTime();
|
|
191
|
+
if (Number.isNaN(d)) return 0;
|
|
192
|
+
return (Date.now() - d) / (365.25 * 24 * 3600 * 1e3);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 搜索定位(按编号/地址/区域/类型)。
|
|
196
|
+
* 返回匹配的节点与管段,供上层 flyTo 定位。
|
|
197
|
+
*/
|
|
198
|
+
search(query) {
|
|
199
|
+
const q = query.trim().toLowerCase();
|
|
200
|
+
if (!q) return { nodes: [], pipes: [] };
|
|
201
|
+
const nodes = this.dataset.nodes.filter(
|
|
202
|
+
(n) => n.id.toLowerCase().includes(q) || (n.properties?.code ?? "").toLowerCase().includes(q) || (n.region ?? "").toLowerCase().includes(q) || n.kind.toLowerCase().includes(q)
|
|
203
|
+
);
|
|
204
|
+
const pipes = this.dataset.pipes.filter(
|
|
205
|
+
(p) => p.id.toLowerCase().includes(q) || (p.region ?? "").toLowerCase().includes(q) || (p.properties?.material ?? "").toLowerCase().includes(q)
|
|
206
|
+
);
|
|
207
|
+
return { nodes, pipes };
|
|
208
|
+
}
|
|
209
|
+
/** 层级钻取:下钻到指定区域(过滤渲染 + 触发事件) */
|
|
210
|
+
drillDown(region) {
|
|
211
|
+
const from = this.currentRegion;
|
|
212
|
+
this.currentRegion = region;
|
|
213
|
+
this.render();
|
|
214
|
+
for (const l of this.drillListeners) l({ from, to: region });
|
|
215
|
+
}
|
|
216
|
+
/** 层级钻取:返回上一级(清空区域过滤) */
|
|
217
|
+
drillUp() {
|
|
218
|
+
const from = this.currentRegion;
|
|
219
|
+
this.currentRegion = null;
|
|
220
|
+
this.render();
|
|
221
|
+
for (const l of this.drillListeners) l({ from, to: "" });
|
|
222
|
+
}
|
|
223
|
+
/** 分层控制:按管径/材质/状态/年代过滤 */
|
|
224
|
+
setLayerFilter(filter) {
|
|
225
|
+
this.layerFilter = filter;
|
|
226
|
+
this.render();
|
|
227
|
+
}
|
|
228
|
+
/** 清空分层过滤器 */
|
|
229
|
+
clearLayerFilter() {
|
|
230
|
+
this.layerFilter = null;
|
|
231
|
+
this.render();
|
|
232
|
+
}
|
|
233
|
+
/** 高亮连通路径(基于节点 ID) */
|
|
234
|
+
highlightConnectivity(centerId) {
|
|
235
|
+
const adj = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const p of this.dataset.pipes) {
|
|
237
|
+
if (!adj.has(p.fromNode)) adj.set(p.fromNode, /* @__PURE__ */ new Set());
|
|
238
|
+
if (!adj.has(p.toNode)) adj.set(p.toNode, /* @__PURE__ */ new Set());
|
|
239
|
+
adj.get(p.fromNode).add(p.toNode);
|
|
240
|
+
adj.get(p.toNode).add(p.fromNode);
|
|
241
|
+
}
|
|
242
|
+
const visited = /* @__PURE__ */ new Set();
|
|
243
|
+
const stack = [centerId];
|
|
244
|
+
while (stack.length > 0) {
|
|
245
|
+
const cur = stack.pop();
|
|
246
|
+
if (visited.has(cur)) continue;
|
|
247
|
+
visited.add(cur);
|
|
248
|
+
const neighbors = adj.get(cur) ?? /* @__PURE__ */ new Set();
|
|
249
|
+
for (const n of neighbors) {
|
|
250
|
+
if (!visited.has(n)) stack.push(n);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return visited;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export { PipelineTopology };
|
|
258
|
+
//# sourceMappingURL=chunk-BWQKY35F.js.map
|
|
259
|
+
//# sourceMappingURL=chunk-BWQKY35F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/topology/PipelineTopology.ts"],"names":[],"mappings":"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","file":"chunk-BWQKY35F.js","sourcesContent":["/**\n * PipelineTopology 类(Phase 1 管网拓扑编辑器)\n *\n * 完整实现:\n * - 拓扑渲染(管段/节点按类型着色)\n * - 层级钻取(区域→街道→小区→楼栋,drillDown / drillUp)\n * - 设备卡片(点击节点/管段触发 onNodeSelect / onPipeSelect)\n * - 连通性高亮(选中管线后高亮上下游,highlightConnectivity)\n * - 搜索定位(按编号/地址/区域,search)\n * - 分层控制(按管径/材质/年代/状态,setLayerFilter)\n */\n\nimport type { Map as CaoguoMap } from '@caoguo/maplibre';\nimport { upsertSource } from '@caoguo/maplibre';\nimport type {\n PipelineTopologyDataset,\n ColorByMode,\n PipelineType,\n PipelineNode,\n PipelinePipe,\n NodeSelectEvent,\n PipeSelectEvent,\n DrillDownEvent,\n} from '../types';\nimport { paintPipeBy } from '../style/paintRules';\n\nexport interface PipelineTopologyOptions {\n map: CaoguoMap;\n dataset: PipelineTopologyDataset;\n /** 默认着色模式 */\n colorBy?: ColorByMode;\n /** 仅显示指定管线类型(空 = 全显示) */\n pipelineTypes?: PipelineType[];\n /** 层 ID 前缀 */\n layerPrefix?: string;\n}\n\ntype NodeListener = (e: NodeSelectEvent) => void;\ntype PipeListener = (e: PipeSelectEvent) => void;\ntype DrillListener = (e: DrillDownEvent) => void;\n\n/** 分层控制过滤器(按管径/材质/状态/年代) */\nexport interface LayerFilter {\n /** 最小管径(mm) */\n minDiameter?: number;\n /** 最大管径(mm) */\n maxDiameter?: number;\n /** 管材 */\n material?: string;\n /** 状态 */\n status?: string;\n /** 最小使用年限 */\n minAgeYears?: number;\n}\n\n/** 搜索结果 */\nexport interface SearchResult {\n /** 匹配节点 */\n nodes: PipelineNode[];\n /** 匹配管段 */\n pipes: PipelinePipe[];\n}\n\n/**\n * PipelineTopology 组件(Phase 1 MVP 完整实现)\n *\n * 实现进度:\n * - [x] 渲染数据接入\n * - [x] 层级钻取(drillDown / drillUp)\n * - [x] 设备卡片(点击绑定 onNodeSelect / onPipeSelect)\n * - [x] 连通性高亮(highlightConnectivity)\n * - [x] 搜索定位(search)\n * - [x] 分层控制(setLayerFilter / clearLayerFilter)\n */\nexport class PipelineTopology {\n private map: CaoguoMap;\n private dataset: PipelineTopologyDataset;\n private colorBy: ColorByMode;\n private layerPrefix: string;\n private pipelineTypes?: PipelineType[];\n private layerIds: string[] = [];\n private nodeListeners = new Set<NodeListener>();\n private pipeListeners = new Set<PipeListener>();\n private drillListeners = new Set<DrillListener>();\n /** 当前钻取区域(null = 全量) */\n private currentRegion: string | null = null;\n /** 当前分层过滤器 */\n private layerFilter: LayerFilter | null = null;\n /** click 事件是否已绑定 */\n private clickBound = false;\n\n constructor(options: PipelineTopologyOptions) {\n this.map = options.map;\n this.dataset = options.dataset;\n this.colorBy = options.colorBy ?? 'type';\n this.pipelineTypes = options.pipelineTypes;\n this.layerPrefix = options.layerPrefix ?? 'cg-topo';\n }\n\n /** 渲染管段+节点到地图 */\n render(): void {\n this.clear();\n const mlMap = (this.map as unknown as {\n instance: {\n addSource: (id: string, source: unknown) => void;\n addLayer: (layer: unknown) => void;\n getSource: (id: string) => unknown;\n on: (event: string, layerId: string, cb: (e: unknown) => void) => void;\n };\n }).instance;\n const prefix = this.layerPrefix;\n\n const pipeGeoJSON: GeoJSON.FeatureCollection<GeoJSON.LineString> = {\n type: 'FeatureCollection',\n features: this.dataset.pipes\n .filter((p) => this.isPipeVisible(p))\n .flatMap((p) => {\n const from = this.dataset.nodes.find((n) => n.id === p.fromNode);\n const to = this.dataset.nodes.find((n) => n.id === p.toNode);\n if (!from || !to) return [];\n const coords: [number, number][] =\n p.geometry && p.geometry.length >= 2\n ? (p.geometry as [number, number][])\n : [\n [from.lng, from.lat],\n [to.lng, to.lat],\n ];\n return [\n {\n type: 'Feature' as const,\n geometry: { type: 'LineString' as const, coordinates: coords },\n properties: {\n pipeId: p.id,\n pipelineType: p.pipelineType ?? '',\n diameter: p.properties?.diameter ?? 0,\n status: p.properties?.status ?? 'unknown',\n material: p.properties?.material ?? 'unknown',\n },\n },\n ];\n }),\n };\n\n const nodeGeoJSON: GeoJSON.FeatureCollection<GeoJSON.Point> = {\n type: 'FeatureCollection',\n features: this.dataset.nodes\n .filter((n) => this.isNodeVisible(n))\n .map((n) => ({\n type: 'Feature' as const,\n geometry: { type: 'Point' as const, coordinates: [n.lng, n.lat] },\n properties: { nodeId: n.id, kind: n.kind, pipelineType: n.pipelineType ?? '' },\n })),\n };\n\n // 幂等:层级/着色切换重渲染时 source 可能已存在,先判断避免抛 \"already exists\"\n upsertSource(mlMap, `${prefix}-pipes-src`, pipeGeoJSON);\n upsertSource(mlMap, `${prefix}-nodes-src`, nodeGeoJSON);\n mlMap.addLayer({\n id: `${prefix}-pipes-line`,\n type: 'line',\n source: `${prefix}-pipes-src`,\n paint: {\n 'line-color': paintPipeBy(this.colorBy, { types: this.pipelineTypes }) as never,\n 'line-width': 2,\n 'line-opacity': 0.85,\n },\n });\n mlMap.addLayer({\n id: `${prefix}-nodes-pt`,\n type: 'circle',\n source: `${prefix}-nodes-src`,\n paint: {\n 'circle-radius': 4,\n 'circle-color': '#f59e0b',\n 'circle-stroke-width': 1.5,\n 'circle-stroke-color': '#ffffff',\n },\n });\n this.layerIds.push(`${prefix}-pipes-line`, `${prefix}-nodes-pt`);\n this.bindClickEvents(mlMap, prefix);\n }\n\n /**\n * 绑定点击事件(设备卡片:点击节点/管段触发订阅回调)。\n * 只绑定一次(render 重建图层后事件仍指向固定 layerId)。\n */\n private bindClickEvents(\n mlMap: {\n on: (event: string, layerId: string, cb: (e: unknown) => void) => void;\n },\n prefix: string\n ): void {\n if (this.clickBound) return;\n this.clickBound = true;\n\n const nodeById = new Map(this.dataset.nodes.map((n) => [n.id, n] as const));\n const pipeById = new Map(this.dataset.pipes.map((p) => [p.id, p] as const));\n\n mlMap.on('click', `${prefix}-pipes-line`, (e) => {\n const features = (e as { features?: Array<{ properties?: { pipeId?: string } }> }).features ?? [];\n const f = features[0];\n const pipe = f?.properties?.pipeId ? pipeById.get(f.properties.pipeId) : undefined;\n if (pipe) {\n for (const l of this.pipeListeners) l({ pipe });\n }\n });\n\n mlMap.on('click', `${prefix}-nodes-pt`, (e) => {\n const features = (e as { features?: Array<{ properties?: { nodeId?: string } }> }).features ?? [];\n const f = features[0];\n const node = f?.properties?.nodeId ? nodeById.get(f.properties.nodeId) : undefined;\n if (node) {\n for (const l of this.nodeListeners) l({ node });\n }\n });\n }\n\n /** 切换着色模式 */\n setColorBy(mode: ColorByMode): void {\n this.colorBy = mode;\n const mlMap = (this.map as unknown as {\n instance: { setPaintProperty: (id: string, prop: string, value: unknown) => void };\n }).instance;\n if (mlMap.setPaintProperty) {\n try {\n mlMap.setPaintProperty(\n `${this.layerPrefix}-pipes-line`,\n 'line-color',\n paintPipeBy(mode, { types: this.pipelineTypes }) as never\n );\n } catch {\n // ignore\n }\n }\n }\n\n /** 清空所有图层 */\n clear(): void {\n for (const id of this.layerIds) {\n this.map.removeLayer(id);\n }\n this.layerIds = [];\n }\n\n /** 销毁组件 */\n destroy(): void {\n this.clear();\n this.nodeListeners.clear();\n this.pipeListeners.clear();\n this.drillListeners.clear();\n }\n\n /** 订阅节点点击事件 */\n onNodeSelect(fn: NodeListener): () => void {\n this.nodeListeners.add(fn);\n return () => this.nodeListeners.delete(fn);\n }\n\n /** 订阅管段点击事件 */\n onPipeSelect(fn: PipeListener): () => void {\n this.pipeListeners.add(fn);\n return () => this.pipeListeners.delete(fn);\n }\n\n /** 订阅钻取事件 */\n onDrillDown(fn: DrillListener): () => void {\n this.drillListeners.add(fn);\n return () => this.drillListeners.delete(fn);\n }\n\n /** 管段是否可见(管线类型 + 区域钻取 + 分层过滤) */\n private isPipeVisible(p: PipelinePipe): boolean {\n if (this.pipelineTypes?.length && !this.pipelineTypes.includes(p.pipelineType as PipelineType)) {\n return false;\n }\n if (this.currentRegion && p.region !== this.currentRegion) return false;\n const f = this.layerFilter;\n if (f) {\n const props = p.properties ?? {};\n if (f.minDiameter !== undefined && (props.diameter ?? 0) < f.minDiameter) return false;\n if (f.maxDiameter !== undefined && (props.diameter ?? Infinity) > f.maxDiameter) return false;\n if (f.material && props.material !== f.material) return false;\n if (f.status && props.status !== f.status) return false;\n if (f.minAgeYears !== undefined && props.installDate) {\n if (this.ageInYears(props.installDate) < f.minAgeYears) return false;\n }\n }\n return true;\n }\n\n /** 节点是否可见(管线类型 + 区域钻取) */\n private isNodeVisible(n: PipelineNode): boolean {\n if (this.pipelineTypes?.length && !this.pipelineTypes.includes(n.pipelineType as PipelineType)) {\n return false;\n }\n if (this.currentRegion && n.region !== this.currentRegion) return false;\n return true;\n }\n\n /** 计算管段使用年限(年) */\n private ageInYears(installDate: string): number {\n const d = new Date(installDate).getTime();\n if (Number.isNaN(d)) return 0;\n return (Date.now() - d) / (365.25 * 24 * 3600 * 1000);\n }\n\n /**\n * 搜索定位(按编号/地址/区域/类型)。\n * 返回匹配的节点与管段,供上层 flyTo 定位。\n */\n search(query: string): SearchResult {\n const q = query.trim().toLowerCase();\n if (!q) return { nodes: [], pipes: [] };\n const nodes = this.dataset.nodes.filter(\n (n) =>\n n.id.toLowerCase().includes(q) ||\n (n.properties?.code ?? '').toLowerCase().includes(q) ||\n (n.region ?? '').toLowerCase().includes(q) ||\n n.kind.toLowerCase().includes(q)\n );\n const pipes = this.dataset.pipes.filter(\n (p) =>\n p.id.toLowerCase().includes(q) ||\n (p.region ?? '').toLowerCase().includes(q) ||\n (p.properties?.material ?? '').toLowerCase().includes(q)\n );\n return { nodes, pipes };\n }\n\n /** 层级钻取:下钻到指定区域(过滤渲染 + 触发事件) */\n drillDown(region: string): void {\n const from = this.currentRegion;\n this.currentRegion = region;\n this.render();\n for (const l of this.drillListeners) l({ from, to: region });\n }\n\n /** 层级钻取:返回上一级(清空区域过滤) */\n drillUp(): void {\n const from = this.currentRegion;\n this.currentRegion = null;\n this.render();\n for (const l of this.drillListeners) l({ from, to: '' });\n }\n\n /** 分层控制:按管径/材质/状态/年代过滤 */\n setLayerFilter(filter: LayerFilter): void {\n this.layerFilter = filter;\n this.render();\n }\n\n /** 清空分层过滤器 */\n clearLayerFilter(): void {\n this.layerFilter = null;\n this.render();\n }\n\n /** 高亮连通路径(基于节点 ID) */\n highlightConnectivity(centerId: string): Set<string> {\n const adj = new Map<string, Set<string>>();\n for (const p of this.dataset.pipes) {\n if (!adj.has(p.fromNode)) adj.set(p.fromNode, new Set());\n if (!adj.has(p.toNode)) adj.set(p.toNode, new Set());\n adj.get(p.fromNode)!.add(p.toNode);\n adj.get(p.toNode)!.add(p.fromNode);\n }\n\n const visited = new Set<string>();\n const stack = [centerId];\n while (stack.length > 0) {\n const cur = stack.pop()!;\n if (visited.has(cur)) continue;\n visited.add(cur);\n const neighbors = adj.get(cur) ?? new Set();\n for (const n of neighbors) {\n if (!visited.has(n)) stack.push(n);\n }\n }\n return visited;\n }\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/graph/dfs.ts
|
|
2
|
+
function dfs(adj, start, opts = {}) {
|
|
3
|
+
const visited = [];
|
|
4
|
+
const parents = /* @__PURE__ */ new Map();
|
|
5
|
+
const visitedSet = /* @__PURE__ */ new Set();
|
|
6
|
+
let truncated = false;
|
|
7
|
+
if (!adj.has(start)) {
|
|
8
|
+
return { visited, parents, truncated };
|
|
9
|
+
}
|
|
10
|
+
if (opts.allowStart && !opts.allowStart(start)) {
|
|
11
|
+
return { visited, parents, truncated };
|
|
12
|
+
}
|
|
13
|
+
const stack = [{ id: start, depth: 0 }];
|
|
14
|
+
parents.set(start, null);
|
|
15
|
+
visitedSet.add(start);
|
|
16
|
+
while (stack.length > 0) {
|
|
17
|
+
const { id, depth } = stack.pop();
|
|
18
|
+
visited.push(id);
|
|
19
|
+
if (opts.maxDepth !== void 0 && depth >= opts.maxDepth) {
|
|
20
|
+
truncated = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const edges = adj.get(id) ?? [];
|
|
24
|
+
for (let i = edges.length - 1; i >= 0; i--) {
|
|
25
|
+
const e = edges[i];
|
|
26
|
+
if (opts.allowEdge && !opts.allowEdge(e, id, e.to)) continue;
|
|
27
|
+
if (visitedSet.has(e.to)) continue;
|
|
28
|
+
visitedSet.add(e.to);
|
|
29
|
+
parents.set(e.to, id);
|
|
30
|
+
stack.push({ id: e.to, depth: depth + 1 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { visited, parents, truncated };
|
|
34
|
+
}
|
|
35
|
+
function detectCycles(adj) {
|
|
36
|
+
const cycles = [];
|
|
37
|
+
const visited = /* @__PURE__ */ new Set();
|
|
38
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
39
|
+
const stack = [];
|
|
40
|
+
for (const node of adj.keys()) {
|
|
41
|
+
if (visited.has(node)) continue;
|
|
42
|
+
stack.push({
|
|
43
|
+
id: node,
|
|
44
|
+
iter: (adj.get(node) ?? [])[Symbol.iterator]()
|
|
45
|
+
});
|
|
46
|
+
onStack.add(node);
|
|
47
|
+
visited.add(node);
|
|
48
|
+
while (stack.length > 0) {
|
|
49
|
+
const top = stack[stack.length - 1];
|
|
50
|
+
const next = top.iter.next();
|
|
51
|
+
if (next.done) {
|
|
52
|
+
onStack.delete(top.id);
|
|
53
|
+
stack.pop();
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const nextId = next.value.to;
|
|
57
|
+
if (onStack.has(nextId)) {
|
|
58
|
+
const cycle = [nextId];
|
|
59
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
60
|
+
cycle.push(stack[i].id);
|
|
61
|
+
if (stack[i].id === nextId) break;
|
|
62
|
+
}
|
|
63
|
+
cycle.reverse();
|
|
64
|
+
if (new Set(cycle).size >= 3) cycles.push(cycle);
|
|
65
|
+
} else if (!visited.has(nextId)) {
|
|
66
|
+
visited.add(nextId);
|
|
67
|
+
onStack.add(nextId);
|
|
68
|
+
stack.push({
|
|
69
|
+
id: nextId,
|
|
70
|
+
iter: (adj.get(nextId) ?? [])[Symbol.iterator]()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return cycles;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { detectCycles, dfs };
|
|
79
|
+
//# sourceMappingURL=chunk-CDEYJ67B.js.map
|
|
80
|
+
//# sourceMappingURL=chunk-CDEYJ67B.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graph/dfs.ts"],"names":[],"mappings":"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","file":"chunk-CDEYJ67B.js","sourcesContent":["/**\n * DFS(深度优先遍历)\n *\n * 用途:\n * - 管线编辑后构建新的连通分量\n * - 环检测(爆管推演风险评估)\n * - 上游路径追踪\n */\n\nimport type { AdjacencyList, AdjEdge } from './adjacency';\n\nexport interface DfsOptions {\n /** 最大深度,默认无限制 */\n maxDepth?: number;\n /** 邻居过滤(在遍历孩子前判断) */\n allowEdge?: (edge: AdjEdge, from: string, to: string) => boolean;\n /** 起点过滤器 */\n allowStart?: (node: string) => boolean;\n}\n\nexport interface DfsResult {\n visited: string[];\n parents: Map<string, string | null>;\n /** 是否触发了 maxDepth 截断(true=未完全遍历) */\n truncated: boolean;\n}\n\n/**\n * 深度优先遍历\n */\nexport function dfs(\n adj: AdjacencyList,\n start: string,\n opts: DfsOptions = {}\n): DfsResult {\n const visited: string[] = [];\n const parents = new Map<string, string | null>();\n const visitedSet = new Set<string>();\n let truncated = false;\n\n if (!adj.has(start)) {\n return { visited, parents, truncated };\n }\n if (opts.allowStart && !opts.allowStart(start)) {\n return { visited, parents, truncated };\n }\n\n const stack: Array<{ id: string; depth: number }> = [{ id: start, depth: 0 }];\n parents.set(start, null);\n visitedSet.add(start);\n\n while (stack.length > 0) {\n const { id, depth } = stack.pop()!;\n visited.push(id);\n\n if (opts.maxDepth !== undefined && depth >= opts.maxDepth) {\n truncated = true;\n continue;\n }\n\n const edges = adj.get(id) ?? [];\n // 倒序遍历以保持\"原本右子树先访问\"的 DFS 直观\n for (let i = edges.length - 1; i >= 0; i--) {\n const e = edges[i];\n if (opts.allowEdge && !opts.allowEdge(e, id, e.to)) continue;\n if (visitedSet.has(e.to)) continue;\n visitedSet.add(e.to);\n parents.set(e.to, id);\n stack.push({ id: e.to, depth: depth + 1 });\n }\n }\n\n return { visited, parents, truncated };\n}\n\n/**\n * 检测有向图中的所有环(基于 Tarjan 算法的简化版本)。\n * 返回包含环的所有节点集合(连通分量内存在环时整分量被标记)。\n */\nexport function detectCycles(adj: AdjacencyList): string[][] {\n const cycles: string[][] = [];\n const visited = new Set<string>();\n const onStack = new Set<string>();\n const stack: Array<{ id: string; iter: Iterator<AdjEdge> }> = [];\n\n for (const node of adj.keys()) {\n if (visited.has(node)) continue;\n const localCycle: string[] = [];\n stack.push({\n id: node,\n iter: (adj.get(node) ?? [])[Symbol.iterator](),\n });\n onStack.add(node);\n visited.add(node);\n\n while (stack.length > 0) {\n const top = stack[stack.length - 1];\n const next = top.iter.next();\n if (next.done) {\n onStack.delete(top.id);\n stack.pop();\n continue;\n }\n const nextId = next.value.to;\n if (onStack.has(nextId)) {\n // 找到环:提取 onStack 中从 nextId 到 top.id 的所有节点\n const cycle: string[] = [nextId];\n for (let i = stack.length - 1; i >= 0; i--) {\n cycle.push(stack[i].id);\n if (stack[i].id === nextId) break;\n }\n cycle.reverse();\n // 双向建表会把 s->v1->s 视为 2 节点\"环\"(实质是同一物理边的双向表示),\n // 只保留 ≥3 个不同节点的\"真环\"\n if (new Set(cycle).size >= 3) cycles.push(cycle);\n } else if (!visited.has(nextId)) {\n visited.add(nextId);\n onStack.add(nextId);\n stack.push({\n id: nextId,\n iter: (adj.get(nextId) ?? [])[Symbol.iterator](),\n });\n }\n }\n }\n\n return cycles;\n}\n"]}
|