@caoguo/maplibre-pipeline 0.0.1

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.
Files changed (77) hide show
  1. package/dist/burst/index.cjs +708 -0
  2. package/dist/burst/index.cjs.map +1 -0
  3. package/dist/burst/index.d.cts +74 -0
  4. package/dist/burst/index.d.ts +74 -0
  5. package/dist/burst/index.js +4 -0
  6. package/dist/burst/index.js.map +1 -0
  7. package/dist/burstClass-CGMjcAYy.d.cts +161 -0
  8. package/dist/burstClass-Cf2p71LE.d.ts +161 -0
  9. package/dist/chunk-CDEYJ67B.js +80 -0
  10. package/dist/chunk-CDEYJ67B.js.map +1 -0
  11. package/dist/chunk-GCDA7KJM.js +289 -0
  12. package/dist/chunk-GCDA7KJM.js.map +1 -0
  13. package/dist/chunk-GEJRE2OR.js +351 -0
  14. package/dist/chunk-GEJRE2OR.js.map +1 -0
  15. package/dist/chunk-HCT6NSNS.js +532 -0
  16. package/dist/chunk-HCT6NSNS.js.map +1 -0
  17. package/dist/chunk-HIRNJVCV.js +13 -0
  18. package/dist/chunk-HIRNJVCV.js.map +1 -0
  19. package/dist/chunk-J3AEVXT4.js +164 -0
  20. package/dist/chunk-J3AEVXT4.js.map +1 -0
  21. package/dist/chunk-QVY4WJLM.js +256 -0
  22. package/dist/chunk-QVY4WJLM.js.map +1 -0
  23. package/dist/chunk-VA2TQWL3.js +340 -0
  24. package/dist/chunk-VA2TQWL3.js.map +1 -0
  25. package/dist/chunk-YBLESEN4.js +133 -0
  26. package/dist/chunk-YBLESEN4.js.map +1 -0
  27. package/dist/chunk-YS2ICFVK.js +101 -0
  28. package/dist/chunk-YS2ICFVK.js.map +1 -0
  29. package/dist/graph/index.cjs +382 -0
  30. package/dist/graph/index.cjs.map +1 -0
  31. package/dist/graph/index.d.cts +211 -0
  32. package/dist/graph/index.d.ts +211 -0
  33. package/dist/graph/index.js +4 -0
  34. package/dist/graph/index.js.map +1 -0
  35. package/dist/health/index.cjs +348 -0
  36. package/dist/health/index.cjs.map +1 -0
  37. package/dist/health/index.d.cts +210 -0
  38. package/dist/health/index.d.ts +210 -0
  39. package/dist/health/index.js +3 -0
  40. package/dist/health/index.js.map +1 -0
  41. package/dist/index.cjs +2295 -0
  42. package/dist/index.cjs.map +1 -0
  43. package/dist/index.d.cts +10 -0
  44. package/dist/index.d.ts +10 -0
  45. package/dist/index.js +12 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/leakage/index.cjs +358 -0
  48. package/dist/leakage/index.cjs.map +1 -0
  49. package/dist/leakage/index.d.cts +212 -0
  50. package/dist/leakage/index.d.ts +212 -0
  51. package/dist/leakage/index.js +3 -0
  52. package/dist/leakage/index.js.map +1 -0
  53. package/dist/nlpg/index.cjs +136 -0
  54. package/dist/nlpg/index.cjs.map +1 -0
  55. package/dist/nlpg/index.d.cts +51 -0
  56. package/dist/nlpg/index.d.ts +51 -0
  57. package/dist/nlpg/index.js +3 -0
  58. package/dist/nlpg/index.js.map +1 -0
  59. package/dist/style/index.cjs +379 -0
  60. package/dist/style/index.cjs.map +1 -0
  61. package/dist/style/index.d.cts +119 -0
  62. package/dist/style/index.d.ts +119 -0
  63. package/dist/style/index.js +4 -0
  64. package/dist/style/index.js.map +1 -0
  65. package/dist/topology/index.cjs +317 -0
  66. package/dist/topology/index.cjs.map +1 -0
  67. package/dist/topology/index.d.cts +69 -0
  68. package/dist/topology/index.d.ts +69 -0
  69. package/dist/topology/index.js +4 -0
  70. package/dist/topology/index.js.map +1 -0
  71. package/dist/types/index.cjs +15 -0
  72. package/dist/types/index.cjs.map +1 -0
  73. package/dist/types/index.d.cts +214 -0
  74. package/dist/types/index.d.ts +214 -0
  75. package/dist/types/index.js +3 -0
  76. package/dist/types/index.js.map +1 -0
  77. package/package.json +81 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,2295 @@
1
+ 'use strict';
2
+
3
+ // src/types/index.ts
4
+ var PIPELINE_TYPES = [
5
+ "gas",
6
+ "water",
7
+ "drainage",
8
+ "heating",
9
+ "power",
10
+ "telecom"
11
+ ];
12
+
13
+ // src/style/pipelineTheme.ts
14
+ var PIPELINE_TYPE_COLORS = {
15
+ gas: "#f59e0b",
16
+ // 琥珀(燃气)
17
+ water: "#3b82f6",
18
+ // 蓝(供水)
19
+ drainage: "#6b7280",
20
+ // 灰(排水)
21
+ heating: "#ef4444",
22
+ // 红(供热)
23
+ power: "#8b5cf6",
24
+ // 紫(电力管沟)
25
+ telecom: "#10b981"
26
+ // 绿(通信管沟)
27
+ };
28
+ var PIPELINE_TYPE_LABELS = {
29
+ gas: "\u71C3\u6C14\u7BA1",
30
+ water: "\u4F9B\u6C34\u7BA1",
31
+ drainage: "\u6392\u6C34\u7BA1",
32
+ heating: "\u4F9B\u70ED\u7BA1",
33
+ power: "\u7535\u529B\u7BA1\u6C9F",
34
+ telecom: "\u901A\u4FE1\u7BA1\u6C9F"
35
+ };
36
+ var PIPELINE_TYPE_ICONS = {
37
+ gas: "\u{1F525}",
38
+ water: "\u{1F4A7}",
39
+ drainage: "\u{1F6B0}",
40
+ heating: "\u{1F534}",
41
+ power: "\u26A1",
42
+ telecom: "\u{1F4E1}"
43
+ };
44
+ var PIPE_STATUS_COLORS = {
45
+ normal: "#4ade80",
46
+ // 绿
47
+ aging: "#fbbf24",
48
+ // 黄
49
+ damaged: "#ef4444",
50
+ // 红
51
+ under_repair: "#8b5cf6",
52
+ // 紫
53
+ abandoned: "#4b5563",
54
+ // 深灰
55
+ unknown: "#6b7280"
56
+ // 灰
57
+ };
58
+ var PIPE_STATUS_META = {
59
+ normal: { label: "\u6B63\u5E38", animation: "none" },
60
+ aging: { label: "\u8001\u5316", animation: "none" },
61
+ damaged: { label: "\u635F\u574F", animation: "pulse" },
62
+ under_repair: { label: "\u7EF4\u4FEE\u4E2D", animation: "dashed" },
63
+ abandoned: { label: "\u5E9F\u5F03", animation: "none" },
64
+ unknown: { label: "\u672A\u77E5", animation: "none" }
65
+ };
66
+ var PIPE_MATERIAL_COLORS = {
67
+ cast_iron: "#94a3b8",
68
+ // 蓝灰(旧铸铁)
69
+ ductile_iron: "#0ea5e9",
70
+ // 天蓝(球墨铸铁)
71
+ steel: "#64748b",
72
+ // 钢蓝
73
+ pe: "#22c55e",
74
+ // 绿(PE 现代管材)
75
+ pvc: "#a78bfa",
76
+ // 紫(PVC)
77
+ concrete: "#a8a29e",
78
+ // 米灰(混凝土)
79
+ hdpe: "#16a34a",
80
+ // 深绿(HDPE)
81
+ copper: "#d97706",
82
+ // 橙(铜)
83
+ unknown: "#6b7280"
84
+ };
85
+ var NODE_KIND_ICONS = {
86
+ junction: "M -6 0 L 6 0 M 0 -6 L 0 6",
87
+ // 十字
88
+ valve: "M -8 -4 L 8 4 M 0 -4 L 0 4",
89
+ // X
90
+ pump: "M -6 0 A 6 6 0 1 0 6 0 A 6 6 0 1 0 -6 0",
91
+ // 圆
92
+ meter: "M -6 -6 L 6 -6 L 6 6 L -6 6 Z",
93
+ // 方
94
+ source: "M 0 -10 L 6 6 L -6 6 Z",
95
+ // 三角
96
+ tank: "M -8 -6 Q 0 -10 8 -6 L 8 6 L -8 6 Z",
97
+ // 圆角
98
+ junction_box: "M -7 -4 L 7 -4 L 7 4 L -7 4 Z"
99
+ // 小方
100
+ };
101
+ var NODE_KIND_COLORS = {
102
+ junction: "#94a3b8",
103
+ valve: "#ef4444",
104
+ // 阀门=红(控制关键)
105
+ pump: "#f59e0b",
106
+ meter: "#3b82f6",
107
+ source: "#22c55e",
108
+ tank: "#06b6d4",
109
+ junction_box: "#8b5cf6"
110
+ };
111
+ var HEALTH_LEVEL_COLORS = {
112
+ excellent: "#22c55e",
113
+ // 绿 80-100
114
+ good: "#3b82f6",
115
+ // 蓝 60-80
116
+ fair: "#eab308",
117
+ // 黄 40-60
118
+ poor: "#f97316",
119
+ // 橙 20-40
120
+ critical: "#ef4444"
121
+ // 红 0-20
122
+ };
123
+ var HEALTH_LEVEL_LABELS = {
124
+ excellent: "\u4F18",
125
+ good: "\u826F",
126
+ fair: "\u4E2D",
127
+ poor: "\u5DEE",
128
+ critical: "\u5371"
129
+ };
130
+ function healthLevel(score) {
131
+ if (score >= 80) return "excellent";
132
+ if (score >= 60) return "good";
133
+ if (score >= 40) return "fair";
134
+ if (score >= 20) return "poor";
135
+ return "critical";
136
+ }
137
+ function pipelineTypeColor(type) {
138
+ if (!type) return "#94a3b8";
139
+ return PIPELINE_TYPE_COLORS[type];
140
+ }
141
+
142
+ // src/style/paintRules.ts
143
+ function paintPipeByType(types) {
144
+ if (!types || types.length === 0) {
145
+ return [
146
+ "match",
147
+ ["get", "pipelineType"],
148
+ ...Object.entries(PIPELINE_TYPE_COLORS).flatMap(([k, v]) => [k, v]),
149
+ "#94a3b8"
150
+ ];
151
+ }
152
+ return [
153
+ "match",
154
+ ["get", "pipelineType"],
155
+ ...types.flatMap((t) => [t, PIPELINE_TYPE_COLORS[t]]),
156
+ "#94a3b8"
157
+ ];
158
+ }
159
+ function paintPipeByDiameter() {
160
+ return [
161
+ "interpolate",
162
+ ["linear"],
163
+ ["coalesce", ["get", "diameter"], 0],
164
+ 0,
165
+ "#4ade80",
166
+ // 极细管 绿
167
+ 50,
168
+ "#4ade80",
169
+ // DN50 以下 绿(支管)
170
+ 150,
171
+ "#60a5fa",
172
+ // DN150 蓝(配水管)
173
+ 300,
174
+ "#f59e0b",
175
+ // DN300 黄(干管)
176
+ 600,
177
+ "#ef4444",
178
+ // DN600+ 红(主管)
179
+ 1500,
180
+ "#7f1d1d"
181
+ // 极大管 深红
182
+ ];
183
+ }
184
+ function paintPipeByStatus() {
185
+ return [
186
+ "match",
187
+ ["coalesce", ["get", "status"], "unknown"],
188
+ ...Object.entries(PIPE_STATUS_COLORS).flatMap(([k, v]) => [k, v]),
189
+ "#6b7280"
190
+ ];
191
+ }
192
+ function paintPipeByMaterial() {
193
+ return [
194
+ "match",
195
+ ["coalesce", ["get", "material"], "unknown"],
196
+ ...Object.entries(PIPE_MATERIAL_COLORS).flatMap(([k, v]) => [k, v]),
197
+ "#6b7280"
198
+ ];
199
+ }
200
+ function paintPipeByHealth() {
201
+ return [
202
+ "step",
203
+ ["coalesce", ["get", "healthScore"], 100],
204
+ HEALTH_LEVEL_COLORS.critical,
205
+ // <20
206
+ 20,
207
+ HEALTH_LEVEL_COLORS.poor,
208
+ 40,
209
+ HEALTH_LEVEL_COLORS.fair,
210
+ 60,
211
+ HEALTH_LEVEL_COLORS.good,
212
+ 80,
213
+ HEALTH_LEVEL_COLORS.excellent
214
+ ];
215
+ }
216
+ function paintPipeBy(mode, opts) {
217
+ switch (mode) {
218
+ case "type":
219
+ return paintPipeByType(opts?.types);
220
+ case "diameter":
221
+ return paintPipeByDiameter();
222
+ case "status":
223
+ return paintPipeByStatus();
224
+ case "material":
225
+ return paintPipeByMaterial();
226
+ case "health":
227
+ return paintPipeByHealth();
228
+ case "uniform":
229
+ default:
230
+ return "#60a5fa";
231
+ }
232
+ }
233
+ function paintNodeByKind() {
234
+ return [
235
+ "match",
236
+ ["get", "kind"],
237
+ "junction",
238
+ "#94a3b8",
239
+ "valve",
240
+ "#ef4444",
241
+ "pump",
242
+ "#f59e0b",
243
+ "meter",
244
+ "#3b82f6",
245
+ "source",
246
+ "#22c55e",
247
+ "tank",
248
+ "#06b6d4",
249
+ "junction_box",
250
+ "#8b5cf6",
251
+ "#cbd5e1"
252
+ ];
253
+ }
254
+ function paintPipeWidthByDiameter(minWidth = 1.5, maxWidth = 6) {
255
+ return [
256
+ "interpolate",
257
+ ["linear"],
258
+ ["coalesce", ["get", "diameter"], 100],
259
+ 0,
260
+ minWidth,
261
+ 1500,
262
+ maxWidth
263
+ ];
264
+ }
265
+
266
+ // src/style/legend.ts
267
+ function legendByType() {
268
+ return {
269
+ title: "\u7BA1\u7EBF\u7C7B\u578B",
270
+ items: Object.keys(PIPELINE_TYPE_COLORS).map((k) => ({
271
+ label: PIPELINE_TYPE_LABELS[k],
272
+ color: PIPELINE_TYPE_COLORS[k]
273
+ }))
274
+ };
275
+ }
276
+ function legendByDiameter() {
277
+ return {
278
+ title: "\u7BA1\u5F84\uFF08mm\uFF09",
279
+ items: [
280
+ { label: "\u226450 \u652F\u7BA1", color: "#4ade80" },
281
+ { label: "50-150 \u914D\u6C34/\u914D\u6C14", color: "#60a5fa" },
282
+ { label: "150-300 \u5E72\u7BA1", color: "#f59e0b" },
283
+ { label: "300-600 \u4E3B\u7BA1", color: "#ef4444" },
284
+ { label: "\u2265600 \u4E3B\u5E72", color: "#7f1d1d" }
285
+ ]
286
+ };
287
+ }
288
+ function legendByStatus() {
289
+ return {
290
+ title: "\u7BA1\u6BB5\u72B6\u6001",
291
+ items: Object.keys(PIPE_STATUS_COLORS).map(
292
+ (k) => {
293
+ const meta = PIPE_STATUS_META[k];
294
+ return {
295
+ label: meta.label,
296
+ color: PIPE_STATUS_COLORS[k],
297
+ style: meta.animation === "dashed" ? "dashed" : "solid"
298
+ };
299
+ }
300
+ )
301
+ };
302
+ }
303
+ function legendByMaterial() {
304
+ return {
305
+ title: "\u7BA1\u6750",
306
+ items: Object.entries(PIPE_MATERIAL_COLORS).map(([k, v]) => ({
307
+ label: materialLabel(k),
308
+ color: v
309
+ }))
310
+ };
311
+ }
312
+ function legendByHealth() {
313
+ return {
314
+ title: "\u5065\u5EB7\u7B49\u7EA7",
315
+ items: Object.entries(HEALTH_LEVEL_COLORS).map(([k, v]) => ({
316
+ label: `${HEALTH_LEVEL_LABELS[k]}\uFF08${healthRangeLabel(k)}\uFF09`,
317
+ color: v
318
+ }))
319
+ };
320
+ }
321
+ function materialLabel(k) {
322
+ const map = {
323
+ cast_iron: "\u94F8\u94C1",
324
+ ductile_iron: "\u7403\u58A8\u94F8\u94C1",
325
+ steel: "\u94A2",
326
+ pe: "PE",
327
+ pvc: "PVC",
328
+ concrete: "\u6DF7\u51DD\u571F",
329
+ hdpe: "HDPE",
330
+ copper: "\u94DC",
331
+ unknown: "\u672A\u77E5"
332
+ };
333
+ return map[k] ?? k;
334
+ }
335
+ function healthRangeLabel(k) {
336
+ const map = {
337
+ excellent: "80-100",
338
+ good: "60-80",
339
+ fair: "40-60",
340
+ poor: "20-40",
341
+ critical: "0-20"
342
+ };
343
+ return map[k] ?? "";
344
+ }
345
+ function buildLegend(mode) {
346
+ switch (mode) {
347
+ case "type":
348
+ return legendByType();
349
+ case "diameter":
350
+ return legendByDiameter();
351
+ case "status":
352
+ return legendByStatus();
353
+ case "material":
354
+ return legendByMaterial();
355
+ case "health":
356
+ return legendByHealth();
357
+ default:
358
+ return legendByType();
359
+ }
360
+ }
361
+
362
+ // src/graph/adjacency.ts
363
+ function buildAdjacency(dataset) {
364
+ const adj = /* @__PURE__ */ new Map();
365
+ for (const node of dataset.nodes) {
366
+ if (!adj.has(node.id)) adj.set(node.id, []);
367
+ }
368
+ for (const pipe of dataset.pipes) {
369
+ const len = pipe.length ?? pipeLengthFromGeometry(pipe, dataset.nodes) ?? 0;
370
+ const fromList = adj.get(pipe.fromNode) ?? [];
371
+ fromList.push({
372
+ pipeId: pipe.id,
373
+ to: pipe.toNode,
374
+ length: len
375
+ });
376
+ adj.set(pipe.fromNode, fromList);
377
+ const toList = adj.get(pipe.toNode) ?? [];
378
+ toList.push({
379
+ pipeId: pipe.id,
380
+ to: pipe.fromNode,
381
+ length: len
382
+ });
383
+ adj.set(pipe.toNode, toList);
384
+ }
385
+ return adj;
386
+ }
387
+ function pipeLengthFromGeometry(pipe, nodes) {
388
+ if (pipe.length && pipe.length > 0) return pipe.length;
389
+ if (pipe.geometry && pipe.geometry.length >= 2) {
390
+ let total = 0;
391
+ for (let i = 1; i < pipe.geometry.length; i++) {
392
+ total += haversine(
393
+ pipe.geometry[i - 1][0],
394
+ pipe.geometry[i - 1][1],
395
+ pipe.geometry[i][0],
396
+ pipe.geometry[i][1]
397
+ );
398
+ }
399
+ return total;
400
+ }
401
+ const from = nodes.find((n) => n.id === pipe.fromNode);
402
+ const to = nodes.find((n) => n.id === pipe.toNode);
403
+ if (!from || !to) return 0;
404
+ return haversine(from.lng, from.lat, to.lng, to.lat);
405
+ }
406
+ function haversine(lng1, lat1, lng2, lat2) {
407
+ const R = 6371e3;
408
+ const toRad = (d) => d * Math.PI / 180;
409
+ const dLat = toRad(lat2 - lat1);
410
+ const dLng = toRad(lng2 - lng1);
411
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
412
+ return 2 * R * Math.asin(Math.sqrt(a));
413
+ }
414
+ function neighborIds(adj, nodeId) {
415
+ const list = adj.get(nodeId) ?? [];
416
+ const seen = /* @__PURE__ */ new Set();
417
+ const out = [];
418
+ for (const e of list) {
419
+ if (!seen.has(e.to)) {
420
+ seen.add(e.to);
421
+ out.push(e.to);
422
+ }
423
+ }
424
+ return out;
425
+ }
426
+ function edgesOf(adj, nodeId) {
427
+ return adj.get(nodeId) ?? [];
428
+ }
429
+
430
+ // src/graph/bfs.ts
431
+ function bfs(adj, start, opts = {}) {
432
+ const visited = [];
433
+ const parents = /* @__PURE__ */ new Map();
434
+ const visitedSet = /* @__PURE__ */ new Set();
435
+ if (!adj.has(start)) {
436
+ parents.set(start, null);
437
+ return { visited, parents, start };
438
+ }
439
+ if (opts.allowStart && !opts.allowStart(start)) {
440
+ return { visited: [], parents, start };
441
+ }
442
+ const queue = [{ id: start, depth: 0 }];
443
+ parents.set(start, null);
444
+ visitedSet.add(start);
445
+ visited.push(start);
446
+ let foundEnd;
447
+ while (queue.length > 0) {
448
+ const { id, depth } = queue.shift();
449
+ if (opts.until && opts.until(id)) {
450
+ foundEnd = id;
451
+ break;
452
+ }
453
+ if (opts.maxDepth !== void 0 && depth >= opts.maxDepth) continue;
454
+ const edges = adj.get(id) ?? [];
455
+ for (const e of edges) {
456
+ if (opts.allowEdge && !opts.allowEdge(e, id, e.to)) continue;
457
+ if (visitedSet.has(e.to)) continue;
458
+ visitedSet.add(e.to);
459
+ parents.set(e.to, id);
460
+ visited.push(e.to);
461
+ queue.push({ id: e.to, depth: depth + 1 });
462
+ }
463
+ }
464
+ return { visited, parents, start, end: foundEnd };
465
+ }
466
+ function multiSourceBfs(adj, starts, opts = {}) {
467
+ const visited = [];
468
+ const parents = /* @__PURE__ */ new Map();
469
+ const sourceMap = /* @__PURE__ */ new Map();
470
+ const visitedSet = /* @__PURE__ */ new Set();
471
+ const queue = [];
472
+ for (const s of starts) {
473
+ if (!adj.has(s)) continue;
474
+ if (opts.allowStart && !opts.allowStart(s)) continue;
475
+ if (!visitedSet.has(s)) {
476
+ visitedSet.add(s);
477
+ parents.set(s, null);
478
+ sourceMap.set(s, s);
479
+ visited.push(s);
480
+ queue.push({ id: s, depth: 0 });
481
+ }
482
+ }
483
+ while (queue.length > 0) {
484
+ const { id, depth } = queue.shift();
485
+ if (opts.maxDepth !== void 0 && depth >= opts.maxDepth) continue;
486
+ const edges = adj.get(id) ?? [];
487
+ for (const e of edges) {
488
+ if (opts.allowEdge && !opts.allowEdge(e, id, e.to)) continue;
489
+ if (visitedSet.has(e.to)) continue;
490
+ visitedSet.add(e.to);
491
+ parents.set(e.to, id);
492
+ sourceMap.set(e.to, sourceMap.get(id));
493
+ visited.push(e.to);
494
+ queue.push({ id: e.to, depth: depth + 1 });
495
+ }
496
+ }
497
+ return { visited, parents, start: starts[0] ?? "", sourceMap };
498
+ }
499
+ function reconstructPath(parents, end) {
500
+ const path = [];
501
+ let cur = end;
502
+ while (cur) {
503
+ path.unshift(cur);
504
+ const p = parents.get(cur);
505
+ cur = p ?? null;
506
+ if (cur === null && p === null) {
507
+ if (parents.get(end) === null && path.length === 1) break;
508
+ break;
509
+ }
510
+ }
511
+ if (path[0] !== end) ;
512
+ return path;
513
+ }
514
+
515
+ // src/graph/dfs.ts
516
+ function dfs(adj, start, opts = {}) {
517
+ const visited = [];
518
+ const parents = /* @__PURE__ */ new Map();
519
+ const visitedSet = /* @__PURE__ */ new Set();
520
+ let truncated = false;
521
+ if (!adj.has(start)) {
522
+ return { visited, parents, truncated };
523
+ }
524
+ if (opts.allowStart && !opts.allowStart(start)) {
525
+ return { visited, parents, truncated };
526
+ }
527
+ const stack = [{ id: start, depth: 0 }];
528
+ parents.set(start, null);
529
+ visitedSet.add(start);
530
+ while (stack.length > 0) {
531
+ const { id, depth } = stack.pop();
532
+ visited.push(id);
533
+ if (opts.maxDepth !== void 0 && depth >= opts.maxDepth) {
534
+ truncated = true;
535
+ continue;
536
+ }
537
+ const edges = adj.get(id) ?? [];
538
+ for (let i = edges.length - 1; i >= 0; i--) {
539
+ const e = edges[i];
540
+ if (opts.allowEdge && !opts.allowEdge(e, id, e.to)) continue;
541
+ if (visitedSet.has(e.to)) continue;
542
+ visitedSet.add(e.to);
543
+ parents.set(e.to, id);
544
+ stack.push({ id: e.to, depth: depth + 1 });
545
+ }
546
+ }
547
+ return { visited, parents, truncated };
548
+ }
549
+ function detectCycles(adj) {
550
+ const cycles = [];
551
+ const visited = /* @__PURE__ */ new Set();
552
+ const onStack = /* @__PURE__ */ new Set();
553
+ const stack = [];
554
+ for (const node of adj.keys()) {
555
+ if (visited.has(node)) continue;
556
+ stack.push({
557
+ id: node,
558
+ iter: (adj.get(node) ?? [])[Symbol.iterator]()
559
+ });
560
+ onStack.add(node);
561
+ visited.add(node);
562
+ while (stack.length > 0) {
563
+ const top = stack[stack.length - 1];
564
+ const next = top.iter.next();
565
+ if (next.done) {
566
+ onStack.delete(top.id);
567
+ stack.pop();
568
+ continue;
569
+ }
570
+ const nextId = next.value.to;
571
+ if (onStack.has(nextId)) {
572
+ const cycle = [nextId];
573
+ for (let i = stack.length - 1; i >= 0; i--) {
574
+ cycle.push(stack[i].id);
575
+ if (stack[i].id === nextId) break;
576
+ }
577
+ cycle.reverse();
578
+ if (new Set(cycle).size >= 3) cycles.push(cycle);
579
+ } else if (!visited.has(nextId)) {
580
+ visited.add(nextId);
581
+ onStack.add(nextId);
582
+ stack.push({
583
+ id: nextId,
584
+ iter: (adj.get(nextId) ?? [])[Symbol.iterator]()
585
+ });
586
+ }
587
+ }
588
+ }
589
+ return cycles;
590
+ }
591
+
592
+ // src/graph/connectivity.ts
593
+ function findConnectedComponents(adj, nodes) {
594
+ const visited = /* @__PURE__ */ new Set();
595
+ const components = [];
596
+ const kindById = /* @__PURE__ */ new Map();
597
+ for (const n of nodes) kindById.set(n.id, n.kind);
598
+ for (const nodeId of adj.keys()) {
599
+ if (visited.has(nodeId)) continue;
600
+ const r = bfs(adj, nodeId);
601
+ let hasSource = false;
602
+ for (const id of r.visited) {
603
+ visited.add(id);
604
+ if (kindById.get(id) === "source") hasSource = true;
605
+ }
606
+ components.push({
607
+ nodes: r.visited,
608
+ size: r.visited.length,
609
+ hasSource
610
+ });
611
+ }
612
+ return components;
613
+ }
614
+ function findPaths(adj, start, end, opts = {}) {
615
+ const maxPaths = opts.maxPaths ?? 3;
616
+ const maxDepth = opts.maxDepth ?? 20;
617
+ const all = [];
618
+ const path = [];
619
+ const onPath = /* @__PURE__ */ new Set();
620
+ const dfsLocal = (cur) => {
621
+ if (all.length >= maxPaths) return;
622
+ if (cur === end) {
623
+ all.push([...path]);
624
+ return;
625
+ }
626
+ if (path.length >= maxDepth) return;
627
+ onPath.add(cur);
628
+ path.push(cur);
629
+ const edges = adj.get(cur) ?? [];
630
+ for (const e of edges) {
631
+ if (onPath.has(e.to)) continue;
632
+ dfsLocal(e.to);
633
+ if (all.length >= maxPaths) return;
634
+ }
635
+ path.pop();
636
+ onPath.delete(cur);
637
+ };
638
+ dfsLocal(start);
639
+ return all;
640
+ }
641
+ function findUpstreamNode(adj, startPipeId, dataset, predicate) {
642
+ const pipe = dataset.pipes.find((p) => p.id === startPipeId);
643
+ if (!pipe) return null;
644
+ const nodeById = new Map(dataset.nodes.map((n) => [n.id, n]));
645
+ const startId = pipe.fromNode;
646
+ const startNode = nodeById.get(startId);
647
+ if (!startNode) return null;
648
+ const allowEdge = (e, _from, to) => {
649
+ const targetPipe = dataset.pipes.find((p) => p.id === e.pipeId);
650
+ if (!targetPipe) return false;
651
+ return true;
652
+ };
653
+ const r = bfs(adj, startId, {
654
+ allowEdge,
655
+ until: (n) => {
656
+ const node = nodeById.get(n);
657
+ return node ? predicate(node) : false;
658
+ },
659
+ maxDepth: 50
660
+ });
661
+ if (!r.end) return null;
662
+ return nodeById.get(r.end) ?? null;
663
+ }
664
+ function highlightConnectivity(adj, start) {
665
+ const r = bfs(adj, start, { maxDepth: 64 });
666
+ const nodeIds = new Set(r.visited);
667
+ const edgeIds = /* @__PURE__ */ new Set();
668
+ for (const n of r.visited) {
669
+ const edges = adj.get(n) ?? [];
670
+ for (const e of edges) {
671
+ if (nodeIds.has(e.to)) edgeIds.add(e.pipeId);
672
+ }
673
+ }
674
+ return { nodeIds, edgeIds };
675
+ }
676
+
677
+ // src/graph/shortestPath.ts
678
+ function dijkstra(adj, start, end, opts = {}) {
679
+ if (!adj.has(start) || !adj.has(end)) {
680
+ return { distance: Infinity, path: [], found: false };
681
+ }
682
+ const weight = opts.weight ?? ((e) => e.length || 1);
683
+ const dist = /* @__PURE__ */ new Map();
684
+ const parents = /* @__PURE__ */ new Map();
685
+ const visited = /* @__PURE__ */ new Set();
686
+ const pq = [{ id: start, d: 0 }];
687
+ for (const n of adj.keys()) {
688
+ dist.set(n, Infinity);
689
+ parents.set(n, null);
690
+ }
691
+ dist.set(start, 0);
692
+ while (pq.length > 0) {
693
+ pq.sort((a, b) => a.d - b.d);
694
+ const cur2 = pq.shift();
695
+ if (visited.has(cur2.id)) continue;
696
+ visited.add(cur2.id);
697
+ if (cur2.id === end) break;
698
+ const edges = adj.get(cur2.id) ?? [];
699
+ for (const e of edges) {
700
+ if (visited.has(e.to)) continue;
701
+ const w = weight(e);
702
+ const alt = (dist.get(cur2.id) ?? Infinity) + w;
703
+ if (alt < (dist.get(e.to) ?? Infinity)) {
704
+ dist.set(e.to, alt);
705
+ parents.set(e.to, cur2.id);
706
+ pq.push({ id: e.to, d: alt });
707
+ }
708
+ }
709
+ if (opts.until && opts.until(cur2.id, cur2.d)) break;
710
+ }
711
+ if ((dist.get(end) ?? Infinity) === Infinity) {
712
+ return { distance: Infinity, path: [], found: false };
713
+ }
714
+ const path = [];
715
+ let cur = end;
716
+ while (cur) {
717
+ path.unshift(cur);
718
+ const p = parents.get(cur);
719
+ if (p === void 0) break;
720
+ cur = p;
721
+ }
722
+ return { distance: dist.get(end) ?? 0, path, found: true };
723
+ }
724
+
725
+ // src/topology/PipelineTopology.ts
726
+ var PipelineTopology = class {
727
+ map;
728
+ dataset;
729
+ colorBy;
730
+ layerPrefix;
731
+ pipelineTypes;
732
+ layerIds = [];
733
+ nodeListeners = /* @__PURE__ */ new Set();
734
+ pipeListeners = /* @__PURE__ */ new Set();
735
+ drillListeners = /* @__PURE__ */ new Set();
736
+ constructor(options) {
737
+ this.map = options.map;
738
+ this.dataset = options.dataset;
739
+ this.colorBy = options.colorBy ?? "type";
740
+ this.pipelineTypes = options.pipelineTypes;
741
+ this.layerPrefix = options.layerPrefix ?? "cg-topo";
742
+ }
743
+ /** 渲染管段+节点到地图 */
744
+ render() {
745
+ this.clear();
746
+ const mlMap = this.map.instance;
747
+ const prefix = this.layerPrefix;
748
+ const pipeGeoJSON = {
749
+ type: "FeatureCollection",
750
+ features: this.dataset.pipes.filter((p) => {
751
+ if (this.pipelineTypes?.length && !this.pipelineTypes.includes(p.pipelineType)) {
752
+ return false;
753
+ }
754
+ return true;
755
+ }).flatMap((p) => {
756
+ const from = this.dataset.nodes.find((n) => n.id === p.fromNode);
757
+ const to = this.dataset.nodes.find((n) => n.id === p.toNode);
758
+ if (!from || !to) return [];
759
+ const coords = p.geometry && p.geometry.length >= 2 ? p.geometry : [
760
+ [from.lng, from.lat],
761
+ [to.lng, to.lat]
762
+ ];
763
+ return [
764
+ {
765
+ type: "Feature",
766
+ geometry: { type: "LineString", coordinates: coords },
767
+ properties: {
768
+ pipeId: p.id,
769
+ pipelineType: p.pipelineType ?? "",
770
+ diameter: p.properties?.diameter ?? 0,
771
+ status: p.properties?.status ?? "unknown",
772
+ material: p.properties?.material ?? "unknown"
773
+ }
774
+ }
775
+ ];
776
+ })
777
+ };
778
+ const nodeGeoJSON = {
779
+ type: "FeatureCollection",
780
+ features: this.dataset.nodes.filter((n) => {
781
+ if (this.pipelineTypes?.length && !this.pipelineTypes.includes(n.pipelineType)) {
782
+ return false;
783
+ }
784
+ return true;
785
+ }).map((n) => ({
786
+ type: "Feature",
787
+ geometry: { type: "Point", coordinates: [n.lng, n.lat] },
788
+ properties: { nodeId: n.id, kind: n.kind, pipelineType: n.pipelineType ?? "" }
789
+ }))
790
+ };
791
+ mlMap.addSource(`${prefix}-pipes-src`, pipeGeoJSON);
792
+ mlMap.addSource(`${prefix}-nodes-src`, nodeGeoJSON);
793
+ mlMap.addLayer({
794
+ id: `${prefix}-pipes-line`,
795
+ type: "line",
796
+ source: `${prefix}-pipes-src`,
797
+ paint: {
798
+ "line-color": paintPipeBy(this.colorBy, { types: this.pipelineTypes }),
799
+ "line-width": 2,
800
+ "line-opacity": 0.85
801
+ }
802
+ });
803
+ mlMap.addLayer({
804
+ id: `${prefix}-nodes-pt`,
805
+ type: "circle",
806
+ source: `${prefix}-nodes-src`,
807
+ paint: {
808
+ "circle-radius": 4,
809
+ "circle-color": "#f59e0b",
810
+ "circle-stroke-width": 1.5,
811
+ "circle-stroke-color": "#ffffff"
812
+ }
813
+ });
814
+ this.layerIds.push(`${prefix}-pipes-line`, `${prefix}-nodes-pt`);
815
+ }
816
+ /** 切换着色模式 */
817
+ setColorBy(mode) {
818
+ this.colorBy = mode;
819
+ const mlMap = this.map.instance;
820
+ if (mlMap.setPaintProperty) {
821
+ try {
822
+ mlMap.setPaintProperty(
823
+ `${this.layerPrefix}-pipes-line`,
824
+ "line-color",
825
+ paintPipeBy(mode, { types: this.pipelineTypes })
826
+ );
827
+ } catch {
828
+ }
829
+ }
830
+ }
831
+ /** 清空所有图层 */
832
+ clear() {
833
+ for (const id of this.layerIds) {
834
+ this.map.removeLayer(id);
835
+ }
836
+ this.layerIds = [];
837
+ }
838
+ /** 销毁组件 */
839
+ destroy() {
840
+ this.clear();
841
+ this.nodeListeners.clear();
842
+ this.pipeListeners.clear();
843
+ this.drillListeners.clear();
844
+ }
845
+ /** 订阅节点点击事件 */
846
+ onNodeSelect(fn) {
847
+ this.nodeListeners.add(fn);
848
+ return () => this.nodeListeners.delete(fn);
849
+ }
850
+ /** 订阅管段点击事件 */
851
+ onPipeSelect(fn) {
852
+ this.pipeListeners.add(fn);
853
+ return () => this.pipeListeners.delete(fn);
854
+ }
855
+ /** 订阅钻取事件 */
856
+ onDrillDown(fn) {
857
+ this.drillListeners.add(fn);
858
+ return () => this.drillListeners.delete(fn);
859
+ }
860
+ /** 高亮连通路径(基于节点 ID) */
861
+ highlightConnectivity(centerId) {
862
+ const adj = /* @__PURE__ */ new Map();
863
+ for (const p of this.dataset.pipes) {
864
+ if (!adj.has(p.fromNode)) adj.set(p.fromNode, /* @__PURE__ */ new Set());
865
+ if (!adj.has(p.toNode)) adj.set(p.toNode, /* @__PURE__ */ new Set());
866
+ adj.get(p.fromNode).add(p.toNode);
867
+ adj.get(p.toNode).add(p.fromNode);
868
+ }
869
+ const visited = /* @__PURE__ */ new Set();
870
+ const stack = [centerId];
871
+ while (stack.length > 0) {
872
+ const cur = stack.pop();
873
+ if (visited.has(cur)) continue;
874
+ visited.add(cur);
875
+ const neighbors = adj.get(cur) ?? /* @__PURE__ */ new Set();
876
+ for (const n of neighbors) {
877
+ if (!visited.has(n)) stack.push(n);
878
+ }
879
+ }
880
+ return visited;
881
+ }
882
+ };
883
+
884
+ // src/burst/burstCore.ts
885
+ function simulateBurst(dataset, pipeId, opts = {}) {
886
+ const t0 = typeof performance !== "undefined" ? performance.now() : Date.now();
887
+ const scenario = opts.scenario ?? "gas";
888
+ const skipClosed = opts.skipClosedValves ?? true;
889
+ const maxAlt = opts.maxAlternatives ?? 2;
890
+ const pipe = dataset.pipes.find((p) => p.id === pipeId);
891
+ if (!pipe) {
892
+ throw new Error(`BurstSimulator: pipe '${pipeId}' not found in dataset`);
893
+ }
894
+ const nodeById = new Map(dataset.nodes.map((n) => [n.id, n]));
895
+ const adj = buildAdjacency(dataset);
896
+ const upstreamValves = findValvesFromEndpoint(adj, pipe, dataset, pipe.fromNode, skipClosed);
897
+ const downstreamValves = findValvesFromEndpoint(adj, pipe, dataset, pipe.toNode, skipClosed);
898
+ const candidateClose = mergeCandidateValves(upstreamValves, downstreamValves);
899
+ const closedSet = new Set(candidateClose.map((v) => v.id));
900
+ const downstream = bfs(adj, pipe.toNode, {
901
+ allowEdge: (_e, _f, to) => {
902
+ if (closedSet.has(to)) return false;
903
+ return true;
904
+ },
905
+ maxDepth: 1e3
906
+ });
907
+ const affectedSet = new Set(downstream.visited);
908
+ affectedSet.add(pipe.fromNode);
909
+ const affectedNodes = [];
910
+ for (const id of affectedSet) {
911
+ const n = nodeById.get(id);
912
+ if (n) affectedNodes.push(n);
913
+ }
914
+ const affectedPipes = dataset.pipes.filter(
915
+ (p) => affectedSet.has(p.fromNode) || affectedSet.has(p.toNode)
916
+ );
917
+ const users = dataset.users ?? [];
918
+ const affectedUsers = users.filter((u) => {
919
+ if (!u.nodeId) return false;
920
+ return affectedSet.has(u.nodeId);
921
+ });
922
+ const importantUsers = affectedUsers.filter((u) => u.kind === "important");
923
+ const impactArea = computeImpactArea(affectedNodes, affectedPipes);
924
+ const valvePlan = buildValvePlan(
925
+ adj,
926
+ dataset,
927
+ pipe,
928
+ candidateClose,
929
+ affectedNodes,
930
+ maxAlt,
931
+ scenario
932
+ );
933
+ const durationMs = (typeof performance !== "undefined" ? performance.now() : Date.now()) - t0;
934
+ return {
935
+ pipe,
936
+ affectedNodes,
937
+ affectedPipes,
938
+ affectedUsers,
939
+ affectedUserCount: affectedUsers.length,
940
+ importantUsers,
941
+ impactArea,
942
+ valvePlan,
943
+ durationMs
944
+ };
945
+ }
946
+ function findValvesFromEndpoint(adj, pipe, dataset, startId, skipClosed) {
947
+ const faultPipeId = pipe.id;
948
+ const nodeById = new Map(dataset.nodes.map((n) => [n.id, n]));
949
+ const out = [];
950
+ const endpoint = startId === pipe.fromNode ? "from" : "to";
951
+ const startNode = nodeById.get(startId);
952
+ if (!startNode) return [];
953
+ if (startNode.kind === "valve") {
954
+ if (!skipClosed || startNode.properties?.valveStatus !== "closed") {
955
+ out.push({ node: startNode, hops: 0, distance: 0, endpoint });
956
+ }
957
+ return out;
958
+ }
959
+ const visited = /* @__PURE__ */ new Set();
960
+ const queue = [
961
+ { id: startId, hops: 0, dist: 0 }
962
+ ];
963
+ while (queue.length > 0) {
964
+ const cur = queue.shift();
965
+ if (visited.has(cur.id)) continue;
966
+ visited.add(cur.id);
967
+ const n = nodeById.get(cur.id);
968
+ if (!n) continue;
969
+ if (n.kind === "valve" && (!skipClosed || n.properties?.valveStatus !== "closed")) {
970
+ out.push({ node: n, hops: cur.hops, distance: cur.dist, endpoint });
971
+ return out;
972
+ }
973
+ const edges = adj.get(cur.id) ?? [];
974
+ for (const e of edges) {
975
+ if (e.pipeId === faultPipeId) continue;
976
+ if (visited.has(e.to)) continue;
977
+ queue.push({
978
+ id: e.to,
979
+ hops: cur.hops + 1,
980
+ dist: cur.dist + (e.length || 0)
981
+ });
982
+ }
983
+ }
984
+ return out;
985
+ }
986
+ function mergeCandidateValves(upstream, downstream) {
987
+ const seen = /* @__PURE__ */ new Set();
988
+ const all = [...upstream, ...downstream].sort((a, b) => a.distance - b.distance);
989
+ const out = [];
990
+ for (const c of all) {
991
+ if (seen.has(c.node.id)) continue;
992
+ seen.add(c.node.id);
993
+ out.push(c.node);
994
+ }
995
+ return out;
996
+ }
997
+ function computeImpactArea(nodes, pipes) {
998
+ const features = [];
999
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
1000
+ for (const n of nodes) {
1001
+ features.push({
1002
+ type: "Feature",
1003
+ geometry: { type: "Point", coordinates: [n.lng, n.lat] },
1004
+ properties: { kind: "node" }
1005
+ });
1006
+ }
1007
+ for (const p of pipes) {
1008
+ let coords = [];
1009
+ if (p.geometry && p.geometry.length >= 2) {
1010
+ coords = p.geometry;
1011
+ } else {
1012
+ const from = nodeById.get(p.fromNode);
1013
+ const to = nodeById.get(p.toNode);
1014
+ if (from && to) coords = [[from.lng, from.lat], [to.lng, to.lat]];
1015
+ }
1016
+ if (coords.length >= 2) {
1017
+ features.push({
1018
+ type: "Feature",
1019
+ geometry: { type: "LineString", coordinates: coords },
1020
+ properties: { kind: "pipe" }
1021
+ });
1022
+ }
1023
+ }
1024
+ const hull = convexHull(nodes.map((n) => [n.lng, n.lat]));
1025
+ return { type: "FeatureCollection", features, hull };
1026
+ }
1027
+ function buildValvePlan(adj, dataset, pipe, closeValves, _affectedNodes, maxAlt, scenario) {
1028
+ const closeSet = new Set(closeValves.map((v) => v.id));
1029
+ const openValves = [];
1030
+ for (const n of _affectedNodes) {
1031
+ if (n.kind !== "valve") continue;
1032
+ if (closeSet.has(n.id)) continue;
1033
+ if (n.properties?.valveStatus !== "open") continue;
1034
+ const from = dataset.nodes.find((m) => m.id === pipe.fromNode);
1035
+ const to = dataset.nodes.find((m) => m.id === pipe.toNode);
1036
+ if (!from || !to) continue;
1037
+ const d1 = haversine(n.lng, n.lat, from.lng, from.lat);
1038
+ const d2 = haversine(n.lng, n.lat, to.lng, to.lat);
1039
+ if (Math.min(d1, d2) < 1e3) openValves.push(n);
1040
+ }
1041
+ const alternativePaths = [];
1042
+ if (closeValves.length) {
1043
+ const sources = dataset.nodes.filter((n) => n.kind === "source");
1044
+ for (const s of sources) {
1045
+ const r = dijkstra(adj, closeValves[0].id, s.id, {
1046
+ weight: (e) => closeSet.has(e.to) ? Infinity : e.length || 1
1047
+ });
1048
+ if (r.found) {
1049
+ const paths = findPaths(adj, closeValves[0].id, s.id, {
1050
+ maxPaths: maxAlt,
1051
+ maxDepth: 20
1052
+ });
1053
+ for (const p of paths) alternativePaths.push(p);
1054
+ break;
1055
+ }
1056
+ }
1057
+ }
1058
+ const userCount = (dataset.users ?? []).filter(
1059
+ (u) => _affectedNodes.some((n) => n.id === u.nodeId)
1060
+ ).length;
1061
+ const baseHours = userCount / 200;
1062
+ const scenarioFactor = scenario === "water" ? 1.5 : scenario === "heating" ? 2 : 1;
1063
+ const hours = Math.max(0.5, Math.min(8, baseHours * scenarioFactor));
1064
+ const estimatedShutdownTime = hours >= 1 ? `${hours.toFixed(1)}h` : `${Math.round(hours * 60)}min`;
1065
+ const summary = buildSummary(closeValves, openValves, userCount, scenario);
1066
+ return {
1067
+ closeValves,
1068
+ openValves,
1069
+ alternativePaths,
1070
+ estimatedShutdownTime,
1071
+ summary
1072
+ };
1073
+ }
1074
+ function buildSummary(closeValves, openValves, userCount, scenario) {
1075
+ const labels = {
1076
+ gas: "\u505C\u6C14",
1077
+ water: "\u505C\u6C34",
1078
+ drainage: "\u6392\u6C34\u4E2D\u65AD",
1079
+ heating: "\u505C\u70ED"
1080
+ };
1081
+ const label = labels[scenario] ?? "\u53D7\u5F71\u54CD";
1082
+ const parts = [];
1083
+ parts.push(`\u9884\u8BA1${label}${userCount}\u6237`);
1084
+ if (closeValves.length) {
1085
+ parts.push(`\u5173\u95ED\u9600\u95E8 ${closeValves.map((v) => v.properties?.code ?? v.id).join(", ")}`);
1086
+ } else {
1087
+ parts.push("\u672A\u627E\u5230\u4E0A\u6E38\u9694\u79BB\u9600\u95E8\uFF08\u5EFA\u8BAE\u4EBA\u5DE5\u6307\u5B9A\uFF09");
1088
+ }
1089
+ if (openValves.length) {
1090
+ parts.push(`\u6253\u5F00\u9600\u95E8 ${openValves.map((v) => v.properties?.code ?? v.id).join(", ")}\uFF08\u6CC4\u538B\uFF09`);
1091
+ }
1092
+ return parts.join("\uFF1B");
1093
+ }
1094
+ function convexHull(points) {
1095
+ if (points.length < 3) return [...points];
1096
+ const pts = [...points];
1097
+ pts.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
1098
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
1099
+ const lower = [];
1100
+ for (const p of pts) {
1101
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
1102
+ lower.pop();
1103
+ }
1104
+ lower.push(p);
1105
+ }
1106
+ const upper = [];
1107
+ for (let i = pts.length - 1; i >= 0; i--) {
1108
+ const p = pts[i];
1109
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
1110
+ upper.pop();
1111
+ }
1112
+ upper.push(p);
1113
+ }
1114
+ upper.pop();
1115
+ lower.pop();
1116
+ return lower.concat(upper);
1117
+ }
1118
+ function userSeverity(kind, scale = 1) {
1119
+ const base = {
1120
+ important: 100,
1121
+ industrial: 50,
1122
+ commercial: 20,
1123
+ residential: 1
1124
+ };
1125
+ return (base[kind] ?? 1) * (scale ?? 1);
1126
+ }
1127
+
1128
+ // src/burst/valvePlanner.ts
1129
+ function listCandidateValves(dataset, pipeId, filter) {
1130
+ const pipe = dataset.pipes.find((p) => p.id === pipeId);
1131
+ if (!pipe) return [];
1132
+ const nodeById = new Map(dataset.nodes.map((n) => [n.id, n]));
1133
+ const adj = buildAdjacency(dataset);
1134
+ const visited = /* @__PURE__ */ new Set();
1135
+ const valveCandidates = [];
1136
+ for (const startId of [pipe.fromNode, pipe.toNode]) {
1137
+ const queue = [{ id: startId, d: 0 }];
1138
+ while (queue.length > 0) {
1139
+ const { id, d } = queue.shift();
1140
+ if (visited.has(id)) continue;
1141
+ visited.add(id);
1142
+ const n = nodeById.get(id);
1143
+ if (!n) continue;
1144
+ if (n.kind === "valve" && (!filter || filter(n))) {
1145
+ valveCandidates.push({ n, dist: d });
1146
+ if (d > 0) continue;
1147
+ }
1148
+ const edges = adj.get(id) ?? [];
1149
+ for (const e of edges) {
1150
+ if (e.pipeId === pipeId) continue;
1151
+ if (visited.has(e.to)) continue;
1152
+ queue.push({ id: e.to, d: d + 1 });
1153
+ }
1154
+ }
1155
+ }
1156
+ valveCandidates.sort((a, b) => a.dist - b.dist);
1157
+ return valveCandidates.map((v) => v.n);
1158
+ }
1159
+ function evaluateValvePlan(dataset, pipeId, valvesToClose) {
1160
+ const pipe = dataset.pipes.find((p) => p.id === pipeId);
1161
+ if (!pipe) {
1162
+ return {
1163
+ valves: valvesToClose,
1164
+ isolationCompleteness: Infinity,
1165
+ impactUserCount: 0,
1166
+ operationOrder: []
1167
+ };
1168
+ }
1169
+ const adj = buildAdjacency(dataset);
1170
+ const closedSet = new Set(valvesToClose.map((v) => v.id));
1171
+ new Map(dataset.pipes.map((p) => [p.id, p]));
1172
+ const affectedNodes = bfs(adj, pipe.toNode, {
1173
+ allowEdge: (_e, _f, to) => !closedSet.has(to),
1174
+ maxDepth: 1e3
1175
+ }).visited;
1176
+ const users = (dataset.users ?? []).filter((u) => {
1177
+ if (!u.nodeId) return false;
1178
+ return affectedNodes.includes(u.nodeId);
1179
+ });
1180
+ const sources = dataset.nodes.filter((n) => n.kind === "source");
1181
+ let completeness = Infinity;
1182
+ for (const s of sources) {
1183
+ const rTo = dijkstra(adj, s.id, pipe.toNode, {
1184
+ weight: (e) => closedSet.has(e.to) ? Infinity : e.length || 1
1185
+ });
1186
+ const rFrom = dijkstra(adj, s.id, pipe.fromNode, {
1187
+ weight: (e) => closedSet.has(e.to) ? Infinity : e.length || 1
1188
+ });
1189
+ const reachable = rTo.found ? rTo.distance : Infinity;
1190
+ const reachableFrom = rFrom.found ? rFrom.distance : Infinity;
1191
+ const nearest = Math.min(reachable, reachableFrom);
1192
+ if (nearest < completeness) completeness = nearest;
1193
+ }
1194
+ const order = valvesToClose.map((v) => v.properties?.code ?? v.id).sort();
1195
+ return {
1196
+ valves: valvesToClose,
1197
+ isolationCompleteness: completeness,
1198
+ impactUserCount: users.length,
1199
+ operationOrder: order
1200
+ };
1201
+ }
1202
+
1203
+ // src/burst/impactArea.ts
1204
+ function toMapLayerData(result) {
1205
+ const hull = result.impactArea.hull;
1206
+ const hullPolygon = hull.length >= 3 ? {
1207
+ type: "Feature",
1208
+ geometry: { type: "Polygon", coordinates: [hull] },
1209
+ properties: { score: 1, affected: result.affectedUserCount }
1210
+ } : null;
1211
+ const nodeCoords = /* @__PURE__ */ new Map();
1212
+ for (const n of result.affectedNodes) nodeCoords.set(n.id, n);
1213
+ const affectedPipes = {
1214
+ type: "FeatureCollection",
1215
+ features: result.affectedPipes.map((p) => ({
1216
+ type: "Feature",
1217
+ geometry: pipeToLineString(p, nodeCoords),
1218
+ properties: { pipeId: p.id, scenario: "affected" }
1219
+ })).filter((f) => f.geometry.coordinates.length >= 2)
1220
+ };
1221
+ const affectedNodes = {
1222
+ type: "FeatureCollection",
1223
+ features: result.affectedNodes.map((n) => ({
1224
+ type: "Feature",
1225
+ geometry: { type: "Point", coordinates: [n.lng, n.lat] },
1226
+ properties: { nodeId: n.id, kind: n.kind }
1227
+ }))
1228
+ };
1229
+ return { hullPolygon, affectedPipes, affectedNodes };
1230
+ }
1231
+ function pipeToLineString(p, nodes) {
1232
+ if (p.geometry && p.geometry.length >= 2) {
1233
+ return { type: "LineString", coordinates: p.geometry };
1234
+ }
1235
+ const from = nodes.get(p.fromNode);
1236
+ const to = nodes.get(p.toNode);
1237
+ if (from && to) {
1238
+ return {
1239
+ type: "LineString",
1240
+ coordinates: [
1241
+ [from.lng, from.lat],
1242
+ [to.lng, to.lat]
1243
+ ]
1244
+ };
1245
+ }
1246
+ return { type: "LineString", coordinates: [] };
1247
+ }
1248
+ function impactAreaSquareMeters(hull) {
1249
+ if (hull.length < 3) return 0;
1250
+ const refLat = hull[0][1];
1251
+ const mPerDegLat = 110540;
1252
+ const mPerDegLng = 111320 * Math.cos(refLat * Math.PI / 180);
1253
+ const pts = hull.map(([lng, lat]) => [
1254
+ (lng - hull[0][0]) * mPerDegLng,
1255
+ (lat - hull[0][1]) * mPerDegLat
1256
+ ]);
1257
+ let sum = 0;
1258
+ for (let i = 0; i < pts.length; i++) {
1259
+ const [x1, y1] = pts[i];
1260
+ const [x2, y2] = pts[(i + 1) % pts.length];
1261
+ sum += x1 * y2 - x2 * y1;
1262
+ }
1263
+ return Math.abs(sum) / 2;
1264
+ }
1265
+ function impactCenter(nodes) {
1266
+ if (!nodes.length) return null;
1267
+ let lng = 0;
1268
+ let lat = 0;
1269
+ for (const n of nodes) {
1270
+ lng += n.lng;
1271
+ lat += n.lat;
1272
+ }
1273
+ return [lng / nodes.length, lat / nodes.length];
1274
+ }
1275
+ function nearestNodeOnPipe(pipe, nodes, lng, lat) {
1276
+ const a = nodes.find((n) => n.id === pipe.fromNode);
1277
+ const b = nodes.find((n) => n.id === pipe.toNode);
1278
+ if (!a || !b) return null;
1279
+ const da = haversineApprox(a.lng, a.lat, lng, lat);
1280
+ const db = haversineApprox(b.lng, b.lat, lng, lat);
1281
+ return da < db ? a : b;
1282
+ }
1283
+ function haversineApprox(lng1, lat1, lng2, lat2) {
1284
+ const dx = (lng2 - lng1) * 111320 * Math.cos(lat1 * Math.PI / 180);
1285
+ const dy = (lat2 - lat1) * 110540;
1286
+ return Math.sqrt(dx * dx + dy * dy);
1287
+ }
1288
+
1289
+ // src/burst/burstClass.ts
1290
+ var BurstSimulator = class {
1291
+ map;
1292
+ dataset;
1293
+ scenario;
1294
+ affectedPipePaint;
1295
+ affectedNodePaint;
1296
+ valvePaint;
1297
+ hullPaint;
1298
+ layerPrefix;
1299
+ listeners = /* @__PURE__ */ new Set();
1300
+ lastResult = null;
1301
+ layerIds = [];
1302
+ constructor(options) {
1303
+ this.map = options.map;
1304
+ this.dataset = options.dataset;
1305
+ this.scenario = options.scenario ?? "gas";
1306
+ this.affectedPipePaint = options.affectedPipePaint ?? {
1307
+ "line-color": "#ef4444",
1308
+ "line-width": 4,
1309
+ "line-opacity": 0.85,
1310
+ "line-blur": 1.5
1311
+ };
1312
+ this.affectedNodePaint = options.affectedNodePaint ?? {
1313
+ "circle-radius": 6,
1314
+ "circle-color": "#ef4444",
1315
+ "circle-stroke-width": 2,
1316
+ "circle-stroke-color": "#ffffff"
1317
+ };
1318
+ this.valvePaint = options.valvePaint ?? {
1319
+ "circle-radius": 8,
1320
+ "circle-color": "#fbbf24",
1321
+ "circle-stroke-width": 2,
1322
+ "circle-stroke-color": "#000000"
1323
+ };
1324
+ this.hullPaint = options.hullPaint ?? {
1325
+ "fill-color": "#ef4444",
1326
+ "fill-opacity": 0.15,
1327
+ "fill-outline-color": "#ef4444"
1328
+ };
1329
+ this.layerPrefix = options.layerPrefix ?? "cg-burst";
1330
+ }
1331
+ /** 触发爆管推演 */
1332
+ simulate(pipeId, overrideOpts) {
1333
+ const result = simulateBurst(this.dataset, pipeId, {
1334
+ scenario: overrideOpts?.scenario ?? this.scenario,
1335
+ skipClosedValves: overrideOpts?.skipClosedValves ?? true,
1336
+ maxAlternatives: overrideOpts?.maxAlternatives ?? 2
1337
+ });
1338
+ this.lastResult = result;
1339
+ this.render(result);
1340
+ for (const l of this.listeners) l(result);
1341
+ return result;
1342
+ }
1343
+ /** 清空所有受影响区域图层 */
1344
+ clear() {
1345
+ for (const id of this.layerIds) {
1346
+ this.map.removeLayer(id);
1347
+ }
1348
+ this.layerIds = [];
1349
+ }
1350
+ /** 销毁并清空所有资源 */
1351
+ destroy() {
1352
+ this.clear();
1353
+ this.listeners.clear();
1354
+ this.lastResult = null;
1355
+ }
1356
+ /** 订阅推演结果 */
1357
+ onResult(fn) {
1358
+ this.listeners.add(fn);
1359
+ return () => this.listeners.delete(fn);
1360
+ }
1361
+ /** 取最后一次结果 */
1362
+ getLastResult() {
1363
+ return this.lastResult;
1364
+ }
1365
+ // ------------------------------------------------------
1366
+ // 内部:把结果渲染为 MapLibre 图层
1367
+ // ------------------------------------------------------
1368
+ render(result) {
1369
+ this.clear();
1370
+ const layerData = toMapLayerData(result);
1371
+ const mlMap = this.map.instance;
1372
+ const prefix = this.layerPrefix;
1373
+ const sourcesToAdd = [];
1374
+ const layersToAdd = [];
1375
+ if (layerData.hullPolygon) {
1376
+ sourcesToAdd.push({ id: `${prefix}-hull-src`, data: layerData.hullPolygon });
1377
+ layersToAdd.push({
1378
+ id: `${prefix}-hull-fill`,
1379
+ type: "fill",
1380
+ source: `${prefix}-hull-src`,
1381
+ paint: this.hullPaint
1382
+ });
1383
+ }
1384
+ sourcesToAdd.push({ id: `${prefix}-pipes-src`, data: layerData.affectedPipes });
1385
+ layersToAdd.push({
1386
+ id: `${prefix}-pipes-line`,
1387
+ type: "line",
1388
+ source: `${prefix}-pipes-src`,
1389
+ paint: this.affectedPipePaint
1390
+ });
1391
+ sourcesToAdd.push({ id: `${prefix}-nodes-src`, data: layerData.affectedNodes });
1392
+ layersToAdd.push({
1393
+ id: `${prefix}-nodes-pt`,
1394
+ type: "circle",
1395
+ source: `${prefix}-nodes-src`,
1396
+ paint: this.affectedNodePaint
1397
+ });
1398
+ for (const src of sourcesToAdd) {
1399
+ if (!mlMap.getSource(src.id)) mlMap.addSource(src.id, src.data);
1400
+ }
1401
+ for (const layer of layersToAdd) {
1402
+ try {
1403
+ mlMap.addLayer(layer);
1404
+ this.layerIds.push(layer.id);
1405
+ } catch {
1406
+ }
1407
+ }
1408
+ }
1409
+ };
1410
+
1411
+ // src/leakage/pasquillGifford.ts
1412
+ var TABLE_DAY = [
1413
+ { wind: 2, cls: { strong: "A", moderate: "A", slight: "B" } },
1414
+ { wind: 3, cls: { strong: "A", moderate: "B", slight: "C" } },
1415
+ { wind: 5, cls: { strong: "B", moderate: "B", slight: "C" } },
1416
+ { wind: 6, cls: { strong: "C", moderate: "C", slight: "D" } },
1417
+ { wind: 7, cls: { strong: "C", moderate: "D", slight: "D" } },
1418
+ { wind: 999, cls: { strong: "D", moderate: "D", slight: "D" } }
1419
+ ];
1420
+ var TABLE_NIGHT = [
1421
+ { wind: 2, cls: { overcast: "F", partly: "F", clear: "F" } },
1422
+ { wind: 3, cls: { overcast: "E", partly: "F", clear: "F" } },
1423
+ { wind: 5, cls: { overcast: "D", partly: "E", clear: "F" } },
1424
+ { wind: 6, cls: { overcast: "D", partly: "D", clear: "E" } },
1425
+ { wind: 999, cls: { overcast: "D", partly: "D", clear: "D" } }
1426
+ ];
1427
+ function classifyStability(params) {
1428
+ const table = params.isDaytime ? TABLE_DAY : TABLE_NIGHT;
1429
+ const key = params.isDaytime ? params.daytimeInsolation ?? "moderate" : params.nightCloudCover ?? "partly";
1430
+ for (const row of table) {
1431
+ if (params.windSpeed < row.wind) return row.cls[key] ?? "D";
1432
+ }
1433
+ return "D";
1434
+ }
1435
+ function dispersionCoefficients(stability) {
1436
+ const params = {
1437
+ A: [0.22, 1e-4, 0.2, 0],
1438
+ B: [0.16, 1e-4, 0.12, 0],
1439
+ C: [0.11, 1e-4, 0.08, 2e-4],
1440
+ D: [0.08, 1e-4, 0.06, 15e-4],
1441
+ E: [0.06, 1e-4, 0.03, 3e-4],
1442
+ F: [0.04, 1e-4, 0.016, 3e-4]
1443
+ };
1444
+ const [a, b, c, d] = params[stability];
1445
+ return {
1446
+ sigmaY: (xMeters) => {
1447
+ const xKm = xMeters / 1e3;
1448
+ return a * xKm / Math.sqrt(1 + b * xKm) * 1e3;
1449
+ },
1450
+ sigmaZ: (xMeters) => {
1451
+ const xKm = xMeters / 1e3;
1452
+ return c * xKm / Math.sqrt(1 + d * xKm) * 1e3;
1453
+ }
1454
+ };
1455
+ }
1456
+
1457
+ // src/leakage/gaussianPlume.ts
1458
+ function gaussianPlume(source, params) {
1459
+ const stability = params.stability ?? "D";
1460
+ const { sigmaY, sigmaZ } = dispersionCoefficients(stability);
1461
+ const thresholds = params.thresholds ?? [1e-3, 5e-3, 0.01];
1462
+ const step = params.gridStep ?? 50;
1463
+ const range = params.range ?? 5e3;
1464
+ const contourList = thresholds.map((t) => ({
1465
+ threshold: t,
1466
+ polygon: [],
1467
+ closed: false
1468
+ }));
1469
+ const Q = params.leakRate;
1470
+ const u = params.windSpeed;
1471
+ const H = params.releaseHeight;
1472
+ let maxDownwindDistance = 0;
1473
+ for (const c of contourList) {
1474
+ for (let r = step; r <= range; r += step) {
1475
+ const sy = sigmaY(r);
1476
+ const sz = sigmaZ(r);
1477
+ if (sy <= 0 || sz <= 0) continue;
1478
+ const concCenter = Q / (2 * Math.PI * sy * sz * u);
1479
+ const zTerm = 2 * Math.exp(-(H * H) / (2 * sz * sz));
1480
+ const cMax = concCenter * zTerm;
1481
+ if (cMax >= c.threshold) {
1482
+ if (r > maxDownwindDistance) maxDownwindDistance = r;
1483
+ }
1484
+ }
1485
+ const poly = [];
1486
+ for (let r = step; r <= range; r += step) {
1487
+ const sy = sigmaY(r);
1488
+ if (sy <= 0) continue;
1489
+ const syy = Q / (2 * Math.PI * sy * sigmaZ(r) * u) * 2;
1490
+ if (syy < c.threshold) continue;
1491
+ const ratio = c.threshold * Math.PI * sy * sigmaZ(r) * u / Q;
1492
+ if (ratio >= 1) continue;
1493
+ const yMax = sy * Math.sqrt(-2 * Math.log(ratio));
1494
+ poly.push([r, yMax]);
1495
+ poly.unshift([r, -yMax]);
1496
+ }
1497
+ c.polygon = polyToLngLat(source, params.windDirection, poly);
1498
+ c.closed = poly.length >= 3;
1499
+ }
1500
+ return {
1501
+ source,
1502
+ windDirection: params.windDirection,
1503
+ windSpeed: params.windSpeed,
1504
+ stability,
1505
+ contours: contourList,
1506
+ maxDownwindDistance
1507
+ };
1508
+ }
1509
+ function polyToLngLat(source, windDir, poly) {
1510
+ return poly.map(([r, y]) => {
1511
+ const cosW = Math.cos(windDir);
1512
+ const sinW = Math.sin(windDir);
1513
+ const dx = r * cosW + y * -sinW;
1514
+ const dy = r * sinW + y * cosW;
1515
+ const dLat = dy / 110540;
1516
+ const dLng = dx / (111320 * Math.cos(source.lat * Math.PI / 180));
1517
+ return [source.lng + dLng, source.lat + dLat];
1518
+ });
1519
+ }
1520
+ function classifyFromMeteo(opts) {
1521
+ return classifyStability(opts);
1522
+ }
1523
+
1524
+ // src/leakage/floodFill.ts
1525
+ function simulateFlood(dem, params) {
1526
+ const sourceIdx = lngLatToIndex(dem, params.sourceLng, params.sourceLat);
1527
+ if (sourceIdx.row < 0 || sourceIdx.row >= dem.rows || sourceIdx.col < 0 || sourceIdx.col >= dem.cols) {
1528
+ return {
1529
+ source: { lng: params.sourceLng, lat: params.sourceLat },
1530
+ floodArea: 0,
1531
+ affectedCells: [],
1532
+ hull: [],
1533
+ maxDepth: 0
1534
+ };
1535
+ }
1536
+ const sourceElev = dem.elevations[sourceIdx.row * dem.cols + sourceIdx.col] ?? 0;
1537
+ (params.duration ?? 30 * 60) / 60;
1538
+ params.flowRate ?? 50;
1539
+ const initialLevel = params.initialWaterLevel ?? sourceElev + 0.05;
1540
+ const visited = /* @__PURE__ */ new Set();
1541
+ const affected = [];
1542
+ const queue = [];
1543
+ queue.push({ row: sourceIdx.row, col: sourceIdx.col, level: initialLevel });
1544
+ let maxDepth = 0;
1545
+ while (queue.length > 0) {
1546
+ const cur = queue.shift();
1547
+ const idx = cur.row * dem.cols + cur.col;
1548
+ if (visited.has(idx)) continue;
1549
+ visited.add(idx);
1550
+ if (cur.row < 0 || cur.row >= dem.rows) continue;
1551
+ if (cur.col < 0 || cur.col >= dem.cols) continue;
1552
+ const elev = dem.elevations[idx] ?? 0;
1553
+ const depth = cur.level - elev;
1554
+ if (depth <= 0) continue;
1555
+ if (depth > maxDepth) maxDepth = depth;
1556
+ const lng = dem.west + cur.col * dem.cellSize + dem.cellSize / 2;
1557
+ const lat = dem.south + cur.row * dem.cellSize + dem.cellSize / 2;
1558
+ affected.push({ lng, lat, elevation: elev, waterDepth: depth });
1559
+ const nextLevel = cur.level - 0.01;
1560
+ for (const [dr, dc] of [
1561
+ [0, 1],
1562
+ [0, -1],
1563
+ [1, 0],
1564
+ [-1, 0]
1565
+ ]) {
1566
+ queue.push({ row: cur.row + dr, col: cur.col + dc, level: nextLevel });
1567
+ }
1568
+ }
1569
+ const cellArea = dem.cellSize * 111320 * (dem.cellSize * 110540);
1570
+ const floodArea = affected.length * cellArea;
1571
+ const hull = convexHullLocal(affected.map((c) => [c.lng, c.lat]));
1572
+ return {
1573
+ source: { lng: params.sourceLng, lat: params.sourceLat },
1574
+ floodArea,
1575
+ affectedCells: affected,
1576
+ hull,
1577
+ maxDepth
1578
+ };
1579
+ }
1580
+ function lngLatToIndex(dem, lng, lat) {
1581
+ const col = Math.floor((lng - dem.west) / dem.cellSize);
1582
+ const row = Math.floor((lat - dem.south) / dem.cellSize);
1583
+ return { row, col };
1584
+ }
1585
+ function convexHullLocal(points) {
1586
+ if (points.length < 3) return [...points];
1587
+ const pts = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
1588
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
1589
+ const lower = [];
1590
+ for (const p of pts) {
1591
+ while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
1592
+ lower.push(p);
1593
+ }
1594
+ const upper = [];
1595
+ for (let i = pts.length - 1; i >= 0; i--) {
1596
+ const p = pts[i];
1597
+ while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
1598
+ upper.push(p);
1599
+ }
1600
+ upper.pop();
1601
+ lower.pop();
1602
+ return lower.concat(upper);
1603
+ }
1604
+
1605
+ // src/leakage/LeakagePlume.ts
1606
+ var LeakagePlume = class {
1607
+ map;
1608
+ thresholds;
1609
+ layerPrefix;
1610
+ fillColor;
1611
+ layerIds = [];
1612
+ listeners = /* @__PURE__ */ new Set();
1613
+ lastResult = null;
1614
+ constructor(options) {
1615
+ this.map = options.map;
1616
+ this.thresholds = options.thresholds ?? [1e-3, 5e-3, 0.01];
1617
+ this.layerPrefix = options.layerPrefix ?? "cg-leakage";
1618
+ this.fillColor = options.fillColor ?? "#fb923c";
1619
+ }
1620
+ /** 燃气泄漏扩散模拟 */
1621
+ simulateGas(source, params) {
1622
+ const result = gaussianPlume(source, { ...params, thresholds: this.thresholds });
1623
+ this.lastResult = result;
1624
+ this.renderGas(result);
1625
+ for (const l of this.listeners) l(result);
1626
+ return result;
1627
+ }
1628
+ /** 供水爆管淹没模拟 */
1629
+ simulateFlood(dem, params) {
1630
+ const result = simulateFlood(dem, params);
1631
+ this.lastResult = result;
1632
+ this.renderFlood(result);
1633
+ for (const l of this.listeners) l(result);
1634
+ return result;
1635
+ }
1636
+ /** 清空图层 */
1637
+ clear() {
1638
+ for (const id of this.layerIds) {
1639
+ this.map.removeLayer(id);
1640
+ }
1641
+ this.layerIds = [];
1642
+ }
1643
+ /** 销毁组件 */
1644
+ destroy() {
1645
+ this.clear();
1646
+ this.listeners.clear();
1647
+ this.lastResult = null;
1648
+ }
1649
+ /** 订阅结果 */
1650
+ onResult(fn) {
1651
+ this.listeners.add(fn);
1652
+ return () => this.listeners.delete(fn);
1653
+ }
1654
+ /** 取最后一次结果 */
1655
+ getLastResult() {
1656
+ return this.lastResult;
1657
+ }
1658
+ // --------------------------------------------------
1659
+ // 内部渲染(GeoJSON 多边形叠加)
1660
+ // --------------------------------------------------
1661
+ renderGas(result) {
1662
+ this.renderContours(
1663
+ result.contours.map((c) => ({
1664
+ polygon: c.polygon,
1665
+ threshold: c.threshold
1666
+ })),
1667
+ result.source
1668
+ );
1669
+ }
1670
+ renderFlood(result) {
1671
+ if (result.hull.length < 3) return;
1672
+ const mlMap = this.map.instance;
1673
+ const data = {
1674
+ type: "Feature",
1675
+ geometry: { type: "Polygon", coordinates: [result.hull] },
1676
+ properties: { threshold: 0 }
1677
+ };
1678
+ const id = `${this.layerPrefix}-flood-fill`;
1679
+ if (!mlMap.getSource(id)) mlMap.addSource(id, data);
1680
+ try {
1681
+ mlMap.addLayer({
1682
+ id,
1683
+ type: "fill",
1684
+ source: id,
1685
+ paint: {
1686
+ "fill-color": "#3b82f6",
1687
+ "fill-opacity": 0.3,
1688
+ "fill-outline-color": "#1e40af"
1689
+ }
1690
+ });
1691
+ this.layerIds.push(id);
1692
+ } catch {
1693
+ }
1694
+ }
1695
+ renderContours(contours, source) {
1696
+ const mlMap = this.map.instance;
1697
+ const data = {
1698
+ type: "FeatureCollection",
1699
+ features: contours.filter((c) => c.polygon.length >= 3).map((c) => ({
1700
+ type: "Feature",
1701
+ geometry: { type: "Polygon", coordinates: [c.polygon] },
1702
+ properties: { threshold: c.threshold }
1703
+ }))
1704
+ };
1705
+ const sourceId = `${this.layerPrefix}-gas-src`;
1706
+ const layerId = `${this.layerPrefix}-gas-fill`;
1707
+ if (!mlMap.getSource(sourceId)) mlMap.addSource(sourceId, data);
1708
+ try {
1709
+ mlMap.addLayer({
1710
+ id: layerId,
1711
+ type: "fill",
1712
+ source: sourceId,
1713
+ paint: {
1714
+ "fill-color": [
1715
+ "step",
1716
+ ["get", "threshold"],
1717
+ this.fillColor,
1718
+ 5e-3,
1719
+ "#f97316",
1720
+ 0.01,
1721
+ "#ef4444"
1722
+ ],
1723
+ "fill-opacity": 0.35,
1724
+ "fill-outline-color": this.fillColor
1725
+ }
1726
+ });
1727
+ this.layerIds.push(layerId);
1728
+ } catch {
1729
+ }
1730
+ const ptId = `${this.layerPrefix}-source`;
1731
+ mlMap.addSource(ptId, {
1732
+ type: "FeatureCollection",
1733
+ features: [
1734
+ {
1735
+ type: "Feature",
1736
+ geometry: { type: "Point", coordinates: [source.lng, source.lat] },
1737
+ properties: {}
1738
+ }
1739
+ ]
1740
+ });
1741
+ try {
1742
+ mlMap.addLayer({
1743
+ id: ptId,
1744
+ type: "circle",
1745
+ source: ptId,
1746
+ paint: {
1747
+ "circle-radius": 10,
1748
+ "circle-color": "#fbbf24",
1749
+ "circle-stroke-width": 3,
1750
+ "circle-stroke-color": "#000"
1751
+ }
1752
+ });
1753
+ this.layerIds.push(ptId);
1754
+ } catch {
1755
+ }
1756
+ }
1757
+ };
1758
+
1759
+ // src/health/healthScorer.ts
1760
+ var DEFAULT_WEIGHTS = {
1761
+ age: 0.25,
1762
+ material: 0.2,
1763
+ soil: 0.15,
1764
+ history: 0.2,
1765
+ pressure: 0.1,
1766
+ protection: 0.1
1767
+ };
1768
+ var MATERIAL_RISK = {
1769
+ cast_iron: 0.85,
1770
+ // 铸铁 = 高风险(旧管网)
1771
+ ductile_iron: 0.2,
1772
+ // 球墨铸铁
1773
+ steel: 0.4,
1774
+ pe: 0.1,
1775
+ pvc: 0.15,
1776
+ concrete: 0.3,
1777
+ hdpe: 0.05,
1778
+ copper: 0.1,
1779
+ unknown: 0.5
1780
+ };
1781
+ function scorePipeHealth(input, weights = DEFAULT_WEIGHTS) {
1782
+ const dims = {
1783
+ age: scoreAge(input, weights.age),
1784
+ material: scoreMaterial(input, weights.material),
1785
+ soil: scoreSoil(input, weights.soil),
1786
+ history: scoreHistory(input, weights.history),
1787
+ pressure: scorePressure(input, weights.pressure),
1788
+ protection: scoreProtection(input, weights.protection)
1789
+ };
1790
+ const total = dims.age.score * dims.age.weight + dims.material.score * dims.material.weight + dims.soil.score * dims.soil.weight + dims.history.score * dims.history.weight + dims.pressure.score * dims.pressure.weight + dims.protection.score * dims.protection.weight;
1791
+ let final = total;
1792
+ if (input.status === "damaged") final *= 0.5;
1793
+ else if (input.status === "under_repair") final *= 0.7;
1794
+ else if (input.status === "abandoned") final = 0;
1795
+ return {
1796
+ score: Math.round(final),
1797
+ level: toLevel(final),
1798
+ dimensions: dims
1799
+ };
1800
+ }
1801
+ function toLevel(score) {
1802
+ if (score >= 80) return "excellent";
1803
+ if (score >= 60) return "good";
1804
+ if (score >= 40) return "fair";
1805
+ if (score >= 20) return "poor";
1806
+ return "critical";
1807
+ }
1808
+ function scoreAge(input, weight) {
1809
+ if (!input.installDate) {
1810
+ return { score: 50, weight, reason: "\u672A\u77E5\u5B89\u88C5\u65E5\u671F\uFF0C\u6309\u4E2D\u7B49\u8BA1" };
1811
+ }
1812
+ const ageYears = (Date.now() - new Date(input.installDate).getTime()) / (365.25 * 24 * 3600 * 1e3);
1813
+ if (ageYears < 0) return { score: 100, weight, reason: "\u672A\u6765\u65E5\u671F\uFF0C\u89C6\u4E3A\u4F18\u79C0" };
1814
+ if (ageYears < 5) return { score: 100, weight, reason: "\u65B0\u5EFA\u7BA1\u7EBF (<5\u5E74)" };
1815
+ if (ageYears < 15) return { score: 85, weight, reason: "\u8F83\u65B0 (5-15\u5E74)" };
1816
+ if (ageYears < 25) return { score: 65, weight, reason: "\u4E2D\u9F84 (15-25\u5E74)" };
1817
+ if (ageYears < 40) return { score: 40, weight, reason: "\u8001\u9F84 (25-40\u5E74)" };
1818
+ if (ageYears < 60) return { score: 20, weight, reason: "\u8D85\u671F (>40\u5E74)" };
1819
+ return { score: 5, weight, reason: "\u8D85\u671F (>60\u5E74)" };
1820
+ }
1821
+ function scoreMaterial(input, weight) {
1822
+ if (!input.material) {
1823
+ return { score: 50, weight, reason: "\u672A\u77E5\u6750\u8D28\uFF0C\u6309\u4E2D\u7B49\u8BA1" };
1824
+ }
1825
+ const risk = MATERIAL_RISK[input.material] ?? 0.5;
1826
+ const score = Math.round((1 - risk) * 100);
1827
+ const labels = {
1828
+ cast_iron: "\u94F8\u94C1\uFF08\u9AD8\u98CE\u9669\uFF09",
1829
+ ductile_iron: "\u7403\u58A8\u94F8\u94C1",
1830
+ steel: "\u94A2",
1831
+ pe: "PE\uFF08\u4F4E\u98CE\u9669\uFF09",
1832
+ pvc: "PVC",
1833
+ concrete: "\u6DF7\u51DD\u571F",
1834
+ hdpe: "HDPE\uFF08\u6700\u4F73\uFF09",
1835
+ copper: "\u94DC",
1836
+ unknown: "\u672A\u77E5"
1837
+ };
1838
+ return {
1839
+ score,
1840
+ weight,
1841
+ reason: labels[input.material] ?? input.material
1842
+ };
1843
+ }
1844
+ function scoreSoil(input, weight) {
1845
+ const c = input.soilCorrosion ?? 0.5;
1846
+ const score = Math.round((1 - c) * 100);
1847
+ return {
1848
+ score,
1849
+ weight,
1850
+ reason: c === 0.5 && !input.soilCorrosion ? "\u65E0\u571F\u58E4\u6570\u636E\uFF0C\u6309\u5E73\u5747\u8BA1" : `\u571F\u58E4\u8150\u8680\u6307\u6570 ${c.toFixed(2)}`
1851
+ };
1852
+ }
1853
+ function scoreHistory(input, weight) {
1854
+ const fail = input.failureCount ?? 0;
1855
+ if (fail === 0) return { score: 100, weight, reason: "\u5386\u53F2\u65E0\u6545\u969C" };
1856
+ if (fail === 1) return { score: 75, weight, reason: "\u5386\u53F2 1 \u6B21\u6545\u969C" };
1857
+ if (fail === 2) return { score: 50, weight, reason: "\u5386\u53F2 2 \u6B21\u6545\u969C" };
1858
+ if (fail === 3) return { score: 25, weight, reason: "\u5386\u53F2 3 \u6B21\u6545\u969C" };
1859
+ return { score: 0, weight, reason: `\u5386\u53F2 \u2265${fail} \u6B21\u6545\u969C` };
1860
+ }
1861
+ function scorePressure(input, weight) {
1862
+ if (!input.pressure) {
1863
+ return { score: 75, weight, reason: "\u65E0\u538B\u529B\u6570\u636E\uFF0C\u6309\u4E2D\u7B49\u504F\u597D\u8BA1" };
1864
+ }
1865
+ if (!input.ratedPressure) {
1866
+ return { score: 75, weight, reason: "\u4EC5\u77E5\u5F53\u524D\u538B\u529B" };
1867
+ }
1868
+ const ratio = input.pressure / input.ratedPressure;
1869
+ if (ratio < 0.5) return { score: 60, weight, reason: "\u538B\u529B\u504F\u4F4E (\u4F7F\u7528\u7387 <50%)" };
1870
+ if (ratio < 0.8) return { score: 90, weight, reason: "\u538B\u529B\u6B63\u5E38 (\u4F7F\u7528\u7387 50-80%)" };
1871
+ if (ratio < 1) return { score: 75, weight, reason: "\u538B\u529B\u504F\u9AD8 (\u4F7F\u7528\u7387 80-100%)" };
1872
+ if (ratio < 1.2) return { score: 30, weight, reason: "\u538B\u529B\u8D85\u9650 (100-120%)" };
1873
+ return { score: 0, weight, reason: "\u538B\u529B\u4E25\u91CD\u8D85\u9650 (>120%)" };
1874
+ }
1875
+ function scoreProtection(input, weight) {
1876
+ if (input.hasCathodicProtection === true) {
1877
+ return { score: 90, weight, reason: "\u6709\u9634\u6781\u4FDD\u62A4" };
1878
+ }
1879
+ if (input.hasCathodicProtection === false) {
1880
+ return { score: 30, weight, reason: "\u65E0\u9634\u6781\u4FDD\u62A4" };
1881
+ }
1882
+ return { score: 50, weight, reason: "\u9634\u4FDD\u72B6\u6001\u672A\u77E5" };
1883
+ }
1884
+ function scorePipes(inputs, weights = DEFAULT_WEIGHTS) {
1885
+ return inputs.map((i) => scorePipeHealth(i, weights));
1886
+ }
1887
+
1888
+ // src/health/riskHeatmap.ts
1889
+ function aggregateHeatmap(points, options = {}) {
1890
+ const cell = options.cellSize ?? 500;
1891
+ const agg = options.aggregation ?? "average";
1892
+ if (points.length === 0) return [];
1893
+ const refLat = points[0].lat;
1894
+ const refLng = points[0].lng;
1895
+ const mPerDegLat = 110540;
1896
+ const mPerDegLng = 111320 * Math.cos(refLat * Math.PI / 180);
1897
+ const grid = /* @__PURE__ */ new Map();
1898
+ for (const p of points) {
1899
+ const x = (p.lng - refLng) * mPerDegLng;
1900
+ const y = (p.lat - refLat) * mPerDegLat;
1901
+ const cx = Math.floor(x / cell);
1902
+ const cy = Math.floor(y / cell);
1903
+ const key = `${cx}:${cy}`;
1904
+ const cur = grid.get(key);
1905
+ if (!cur) {
1906
+ grid.set(key, {
1907
+ acc: p.healthScore,
1908
+ count: 1,
1909
+ minS: p.healthScore,
1910
+ maxS: p.healthScore,
1911
+ lng: p.lng,
1912
+ lat: p.lat
1913
+ });
1914
+ } else {
1915
+ cur.acc += p.healthScore;
1916
+ cur.count += 1;
1917
+ if (p.healthScore < cur.minS) cur.minS = p.healthScore;
1918
+ if (p.healthScore > cur.maxS) cur.maxS = p.healthScore;
1919
+ cur.lng = (cur.lng * (cur.count - 1) + p.lng) / cur.count;
1920
+ cur.lat = (cur.lat * (cur.count - 1) + p.lat) / cur.count;
1921
+ }
1922
+ }
1923
+ const cells = [];
1924
+ for (const v of grid.values()) {
1925
+ let score;
1926
+ if (agg === "min") score = v.minS;
1927
+ else if (agg === "max") score = v.maxS;
1928
+ else score = v.acc / v.count;
1929
+ cells.push({
1930
+ lng: v.lng,
1931
+ lat: v.lat,
1932
+ healthScore: Math.round(score),
1933
+ pipeCount: v.count
1934
+ });
1935
+ }
1936
+ return cells;
1937
+ }
1938
+ function heatmapToGeoJSON(cells) {
1939
+ return {
1940
+ type: "FeatureCollection",
1941
+ features: cells.map((c) => ({
1942
+ type: "Feature",
1943
+ geometry: { type: "Point", coordinates: [c.lng, c.lat] },
1944
+ properties: {
1945
+ healthScore: c.healthScore,
1946
+ pipeCount: c.pipeCount
1947
+ }
1948
+ }))
1949
+ };
1950
+ }
1951
+ function prioritizeMaintenance(pipes, topN = 10) {
1952
+ return [...pipes].sort((a, b) => a.healthScore - b.healthScore).slice(0, topN);
1953
+ }
1954
+
1955
+ // src/health/PipelineHealth.ts
1956
+ var PipelineHealth = class {
1957
+ map;
1958
+ dataset;
1959
+ cellSize;
1960
+ layerPrefix;
1961
+ listeners = /* @__PURE__ */ new Set();
1962
+ lastResult = null;
1963
+ layerIds = [];
1964
+ constructor(options) {
1965
+ this.map = options.map;
1966
+ this.dataset = options.dataset;
1967
+ this.cellSize = options.cellSize ?? 500;
1968
+ this.layerPrefix = options.layerPrefix ?? "cg-health";
1969
+ }
1970
+ /** 评估全网管线健康度 */
1971
+ evaluate(weights = DEFAULT_WEIGHTS) {
1972
+ const t0 = typeof performance !== "undefined" ? performance.now() : Date.now();
1973
+ const nodeById = new Map(this.dataset.nodes.map((n) => [n.id, n]));
1974
+ const scores = this.dataset.pipes.map((p) => {
1975
+ const input = {
1976
+ installDate: p.properties?.installDate,
1977
+ material: p.properties?.material,
1978
+ failureCount: void 0,
1979
+ pressure: p.properties?.pressure,
1980
+ ratedPressure: p.properties?.ratedPressure,
1981
+ hasCathodicProtection: void 0,
1982
+ status: p.properties?.status
1983
+ };
1984
+ return { pipeId: p.id, score: scorePipeHealth(input, weights) };
1985
+ });
1986
+ const points = scores.flatMap((s, i) => {
1987
+ const pipe = this.dataset.pipes[i];
1988
+ const from = nodeById.get(pipe.fromNode);
1989
+ const to = nodeById.get(pipe.toNode);
1990
+ if (!from || !to) return [];
1991
+ return [
1992
+ {
1993
+ lng: (from.lng + to.lng) / 2,
1994
+ lat: (from.lat + to.lat) / 2,
1995
+ healthScore: s.score.score
1996
+ }
1997
+ ];
1998
+ });
1999
+ const heatmap = aggregateHeatmap(points, { cellSize: this.cellSize });
2000
+ const maintenance = prioritizeMaintenance(
2001
+ scores.flatMap((s, i) => {
2002
+ const pipe = this.dataset.pipes[i];
2003
+ const from = nodeById.get(pipe.fromNode);
2004
+ const to = nodeById.get(pipe.toNode);
2005
+ if (!from || !to) return [];
2006
+ return [
2007
+ {
2008
+ id: pipe.id,
2009
+ healthScore: s.score.score,
2010
+ lng: (from.lng + to.lng) / 2,
2011
+ lat: (from.lat + to.lat) / 2,
2012
+ label: pipe.id
2013
+ }
2014
+ ];
2015
+ })
2016
+ );
2017
+ const durationMs = (typeof performance !== "undefined" ? performance.now() : Date.now()) - t0;
2018
+ const result = { scores, heatmap, maintenance, durationMs };
2019
+ this.lastResult = result;
2020
+ this.renderHeatmap(result);
2021
+ for (const l of this.listeners) l(result);
2022
+ return result;
2023
+ }
2024
+ /** 清空图层 */
2025
+ clear() {
2026
+ for (const id of this.layerIds) {
2027
+ this.map.removeLayer(id);
2028
+ }
2029
+ this.layerIds = [];
2030
+ }
2031
+ /** 销毁 */
2032
+ destroy() {
2033
+ this.clear();
2034
+ this.listeners.clear();
2035
+ this.lastResult = null;
2036
+ }
2037
+ /** 订阅结果 */
2038
+ onResult(fn) {
2039
+ this.listeners.add(fn);
2040
+ return () => this.listeners.delete(fn);
2041
+ }
2042
+ /** 取最后一次结果 */
2043
+ getLastResult() {
2044
+ return this.lastResult;
2045
+ }
2046
+ renderHeatmap(result) {
2047
+ this.clear();
2048
+ const mlMap = this.map.instance;
2049
+ const data = heatmapToGeoJSON(result.heatmap);
2050
+ const sourceId = `${this.layerPrefix}-heat-src`;
2051
+ const layerId = `${this.layerPrefix}-heat-point`;
2052
+ if (!mlMap.getSource(sourceId)) mlMap.addSource(sourceId, data);
2053
+ try {
2054
+ mlMap.addLayer({
2055
+ id: layerId,
2056
+ type: "heatmap",
2057
+ source: sourceId,
2058
+ paint: {
2059
+ "heatmap-weight": [
2060
+ "interpolate",
2061
+ ["linear"],
2062
+ ["get", "healthScore"],
2063
+ 0,
2064
+ 1,
2065
+ 50,
2066
+ 0.5,
2067
+ 100,
2068
+ 0
2069
+ ],
2070
+ "heatmap-intensity": 1,
2071
+ "heatmap-color": [
2072
+ "interpolate",
2073
+ ["linear"],
2074
+ ["heatmap-density"],
2075
+ 0,
2076
+ "rgba(34,197,94,0)",
2077
+ 0.2,
2078
+ "rgba(34,197,94,0.5)",
2079
+ 0.5,
2080
+ "rgba(245,158,11,0.7)",
2081
+ 0.8,
2082
+ "rgba(239,68,68,0.8)",
2083
+ 1,
2084
+ "rgba(127,29,29,0.9)"
2085
+ ],
2086
+ "heatmap-radius": 25,
2087
+ "heatmap-opacity": 0.7
2088
+ }
2089
+ });
2090
+ this.layerIds.push(layerId);
2091
+ } catch {
2092
+ }
2093
+ }
2094
+ };
2095
+
2096
+ // src/nlpg/pipelineNlp.ts
2097
+ var PATTERNS = [
2098
+ { intent: "burst", re: /(爆管|破裂|泄漏|故障|事故|爆裂)/, confidence: 0.95 },
2099
+ { intent: "valve", re: /(阀门|闸).*?(关闭|关|合)|(?:关闭|关|合).*?(阀门|闸)/, confidence: 0.9 },
2100
+ { intent: "material_age", re: /(\d+)\s*年.*?(铸铁|钢|PE|PVC|铜)/, confidence: 0.85 },
2101
+ { intent: "material_age", re: /(铸铁|钢|PE|PVC|铜).*?(\d+)\s*年/, confidence: 0.85 },
2102
+ { intent: "pressure", re: /压力.*?(低于|高于|小于|大于|<|>|>=|<=|低|高)\s*(\d+\.?\d*)/, confidence: 0.85 },
2103
+ { intent: "nearby", re: /(\d+)\s*(米|m|公里|km).*?(学校|医院|工厂|消防|政府|小区)/, confidence: 0.85 },
2104
+ { intent: "alarm_cluster", re: /报警|告警/, confidence: 0.8 }
2105
+ ];
2106
+ var TYPE_REGEX = [
2107
+ { type: "gas", re: /燃气|煤气|天然气/ },
2108
+ { type: "water", re: /供水|自来水|水管/ },
2109
+ { type: "drainage", re: /排水|污水|雨水/ },
2110
+ { type: "heating", re: /供热|暖气|热力/ },
2111
+ { type: "power", re: /电力|电缆|高压/ },
2112
+ { type: "telecom", re: /通信|光纤|光缆/ }
2113
+ ];
2114
+ var MATERIAL_REGEX = [
2115
+ { key: "cast_iron", cname: "\u94F8\u94C1", re: /铸铁/ },
2116
+ { key: "ductile_iron", cname: "\u7403\u58A8\u94F8\u94C1", re: /球墨/ },
2117
+ { key: "steel", cname: "\u94A2", re: /钢/ },
2118
+ { key: "pe", cname: "PE", re: /PE/ },
2119
+ { key: "pvc", cname: "PVC", re: /PVC/ },
2120
+ { key: "concrete", cname: "\u6DF7\u51DD\u571F", re: /混凝土/ },
2121
+ { key: "hdpe", cname: "HDPE", re: /HDPE/ },
2122
+ { key: "copper", cname: "\u94DC", re: /铜/ }
2123
+ ];
2124
+ var REGION_REGEX = /(朝阳区|海淀区|江岸区|江汉区|硚口区|汉阳区|武昌区|青山区|洪山区|东西湖区|黄陂区|新洲区|江夏区|蔡甸区|汉南区)/;
2125
+ function parsePipelineQuery(query) {
2126
+ let best = { intent: "unknown", confidence: 0 };
2127
+ for (const p of PATTERNS) {
2128
+ const m = query.match(p.re);
2129
+ if (m) {
2130
+ const c = p.confidence * (1 + 0.05 * (m[0].length / query.length));
2131
+ if (c > best.confidence) best = { intent: p.intent, confidence: Math.min(c, 1) };
2132
+ }
2133
+ }
2134
+ const filters = {};
2135
+ for (const t of TYPE_REGEX) {
2136
+ if (t.re.test(query)) {
2137
+ filters.pipelineType = t.type;
2138
+ break;
2139
+ }
2140
+ }
2141
+ for (const m of MATERIAL_REGEX) {
2142
+ if (m.re.test(query)) {
2143
+ filters.material = m.key;
2144
+ break;
2145
+ }
2146
+ }
2147
+ const ageMatch = query.match(/(\d+)\s*年/);
2148
+ if (ageMatch) {
2149
+ const years = parseInt(ageMatch[1]);
2150
+ if (best.intent === "material_age") {
2151
+ if (/超[过于]/.test(query) || /[以]?上/.test(query)) filters.minAgeYears = years;
2152
+ else if (/内|以下|不超/.test(query)) filters.maxAgeYears = years;
2153
+ else filters.minAgeYears = years;
2154
+ } else {
2155
+ filters.minAgeYears = years;
2156
+ }
2157
+ }
2158
+ const pressureMatch = query.match(/(\d+\.?\d*)\s*(MPa|mpa)/);
2159
+ if (pressureMatch) {
2160
+ const v = parseFloat(pressureMatch[1]);
2161
+ if (/低|小于|小于等于|</.test(query)) filters.maxPressure = v;
2162
+ else if (/高|大于|大于等于|>/.test(query)) filters.minPressure = v;
2163
+ else filters.maxPressure = v;
2164
+ } else if (/压力/.test(query)) {
2165
+ filters.maxPressure = 0.2;
2166
+ }
2167
+ const distMatch = query.match(/(\d+)\s*(公里|km|千米|米|m)/);
2168
+ if (distMatch) {
2169
+ let d = parseInt(distMatch[1]);
2170
+ const unit = distMatch[2];
2171
+ if (/公里|km|千米/.test(unit)) d *= 1e3;
2172
+ filters.radius = d;
2173
+ }
2174
+ if (/昨天/.test(query)) filters.timeWindow = "1d";
2175
+ else if (/今天/.test(query)) filters.timeWindow = "1d";
2176
+ else if (/最近一周|过去一周|7\s*天/.test(query)) filters.timeWindow = "7d";
2177
+ else if (/最近一个月/.test(query)) filters.timeWindow = "30d";
2178
+ const regionMatch = query.match(REGION_REGEX);
2179
+ if (regionMatch) filters.region = regionMatch[1];
2180
+ return {
2181
+ intent: best.intent,
2182
+ filters,
2183
+ description: buildDescription(best.intent, filters),
2184
+ confidence: best.confidence
2185
+ };
2186
+ }
2187
+ function buildDescription(intent, f) {
2188
+ const desc = [];
2189
+ if (intent === "burst") desc.push("\u89E6\u53D1\u7206\u7BA1\u63A8\u6F14");
2190
+ else if (intent === "valve") desc.push("\u67E5\u8BE2\u9600\u95E8\u5173\u95ED\u5F71\u54CD");
2191
+ else if (intent === "material_age") {
2192
+ const matName = MATERIAL_REGEX.find((m) => m.key === f.material)?.cname ?? f.material ?? "";
2193
+ desc.push(`\u7B5B\u9009${matName}\u7BA1${f.minAgeYears ?? "?"}\u5E74\u4EE5\u4E0A`);
2194
+ } else if (intent === "pressure") desc.push(`\u7B5B\u9009\u538B\u529B${f.maxPressure ?? ""}MPa\u4EE5\u4E0B`);
2195
+ else if (intent === "nearby") desc.push(`${f.radius}m\u5185\u67E5\u627EPOI`);
2196
+ else if (intent === "alarm_cluster") desc.push(`\u67E5\u8BE2${f.timeWindow ?? "1d"}\u5185\u62A5\u8B66\u805A\u96C6`);
2197
+ if (f.pipelineType) desc.push(`[${f.pipelineType}]`);
2198
+ if (f.region) desc.push(`\u533A\u57DF:${f.region}`);
2199
+ return desc.join(" ");
2200
+ }
2201
+
2202
+ // src/nlpg/pipelineNlpClass.ts
2203
+ var PipelineNlp = class {
2204
+ burstSimulator;
2205
+ onIntent;
2206
+ constructor(options = {}) {
2207
+ this.burstSimulator = options.burstSimulator;
2208
+ this.onIntent = options.onIntent;
2209
+ }
2210
+ /** 解析并按意图触发动作 */
2211
+ query(text) {
2212
+ const result = parsePipelineQuery(text);
2213
+ if (this.onIntent) this.onIntent(result.intent, result);
2214
+ return result;
2215
+ }
2216
+ /** 仅解析不触发动作 */
2217
+ parse(text) {
2218
+ return parsePipelineQuery(text);
2219
+ }
2220
+ /** 关联 BurstSimulator */
2221
+ setBurstSimulator(sim) {
2222
+ this.burstSimulator = sim;
2223
+ }
2224
+ };
2225
+
2226
+ exports.BurstSimulator = BurstSimulator;
2227
+ exports.DEFAULT_WEIGHTS = DEFAULT_WEIGHTS;
2228
+ exports.HEALTH_LEVEL_COLORS = HEALTH_LEVEL_COLORS;
2229
+ exports.HEALTH_LEVEL_LABELS = HEALTH_LEVEL_LABELS;
2230
+ exports.LeakagePlume = LeakagePlume;
2231
+ exports.NODE_KIND_COLORS = NODE_KIND_COLORS;
2232
+ exports.NODE_KIND_ICONS = NODE_KIND_ICONS;
2233
+ exports.PIPELINE_TYPES = PIPELINE_TYPES;
2234
+ exports.PIPELINE_TYPE_COLORS = PIPELINE_TYPE_COLORS;
2235
+ exports.PIPELINE_TYPE_ICONS = PIPELINE_TYPE_ICONS;
2236
+ exports.PIPELINE_TYPE_LABELS = PIPELINE_TYPE_LABELS;
2237
+ exports.PIPE_MATERIAL_COLORS = PIPE_MATERIAL_COLORS;
2238
+ exports.PIPE_STATUS_COLORS = PIPE_STATUS_COLORS;
2239
+ exports.PIPE_STATUS_META = PIPE_STATUS_META;
2240
+ exports.PipelineHealth = PipelineHealth;
2241
+ exports.PipelineNlp = PipelineNlp;
2242
+ exports.PipelineTopology = PipelineTopology;
2243
+ exports.aggregateHeatmap = aggregateHeatmap;
2244
+ exports.bfs = bfs;
2245
+ exports.buildAdjacency = buildAdjacency;
2246
+ exports.buildLegend = buildLegend;
2247
+ exports.classifyFromMeteo = classifyFromMeteo;
2248
+ exports.classifyStability = classifyStability;
2249
+ exports.convexHull = convexHull;
2250
+ exports.detectCycles = detectCycles;
2251
+ exports.dfs = dfs;
2252
+ exports.dijkstra = dijkstra;
2253
+ exports.dispersionCoefficients = dispersionCoefficients;
2254
+ exports.edgesOf = edgesOf;
2255
+ exports.evaluateValvePlan = evaluateValvePlan;
2256
+ exports.findConnectedComponents = findConnectedComponents;
2257
+ exports.findPaths = findPaths;
2258
+ exports.findUpstreamNode = findUpstreamNode;
2259
+ exports.gaussianPlume = gaussianPlume;
2260
+ exports.haversine = haversine;
2261
+ exports.healthLevel = healthLevel;
2262
+ exports.heatmapToGeoJSON = heatmapToGeoJSON;
2263
+ exports.highlightConnectivity = highlightConnectivity;
2264
+ exports.impactAreaSquareMeters = impactAreaSquareMeters;
2265
+ exports.impactCenter = impactCenter;
2266
+ exports.legendByDiameter = legendByDiameter;
2267
+ exports.legendByHealth = legendByHealth;
2268
+ exports.legendByMaterial = legendByMaterial;
2269
+ exports.legendByStatus = legendByStatus;
2270
+ exports.legendByType = legendByType;
2271
+ exports.listCandidateValves = listCandidateValves;
2272
+ exports.multiSourceBfs = multiSourceBfs;
2273
+ exports.nearestNodeOnPipe = nearestNodeOnPipe;
2274
+ exports.neighborIds = neighborIds;
2275
+ exports.paintNodeByKind = paintNodeByKind;
2276
+ exports.paintPipeBy = paintPipeBy;
2277
+ exports.paintPipeByDiameter = paintPipeByDiameter;
2278
+ exports.paintPipeByHealth = paintPipeByHealth;
2279
+ exports.paintPipeByMaterial = paintPipeByMaterial;
2280
+ exports.paintPipeByStatus = paintPipeByStatus;
2281
+ exports.paintPipeByType = paintPipeByType;
2282
+ exports.paintPipeWidthByDiameter = paintPipeWidthByDiameter;
2283
+ exports.parsePipelineQuery = parsePipelineQuery;
2284
+ exports.pipeLengthFromGeometry = pipeLengthFromGeometry;
2285
+ exports.pipelineTypeColor = pipelineTypeColor;
2286
+ exports.prioritizeMaintenance = prioritizeMaintenance;
2287
+ exports.reconstructPath = reconstructPath;
2288
+ exports.scorePipeHealth = scorePipeHealth;
2289
+ exports.scorePipes = scorePipes;
2290
+ exports.simulateBurst = simulateBurst;
2291
+ exports.simulateFlood = simulateFlood;
2292
+ exports.toMapLayerData = toMapLayerData;
2293
+ exports.userSeverity = userSeverity;
2294
+ //# sourceMappingURL=index.cjs.map
2295
+ //# sourceMappingURL=index.cjs.map