@bitalltech-maplibre/core 1.0.0 → 1.0.3

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 (31) hide show
  1. package/README.md +183 -6
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +3021 -139
  4. package/dist/index.umd.js +1 -1
  5. package/dist/types/basemaps/index.d.ts +5 -1
  6. package/dist/types/basemaps/tianditu.d.ts +7 -4
  7. package/dist/types/effects/electronic-fence-common.d.ts +76 -0
  8. package/dist/types/effects/electronic-fence-types.d.ts +131 -0
  9. package/dist/types/effects/geometry.d.ts +2 -0
  10. package/dist/types/effects/index.d.ts +5 -2
  11. package/dist/types/effects/low-altitude/breach-alert.d.ts +61 -0
  12. package/dist/types/effects/low-altitude/breathing-circle.d.ts +5 -0
  13. package/dist/types/effects/low-altitude/coordinated-countermeasure.d.ts +5 -0
  14. package/dist/types/effects/low-altitude/countermeasure-beam.d.ts +5 -0
  15. package/dist/types/effects/low-altitude/defence-circle.d.ts +5 -0
  16. package/dist/types/effects/low-altitude/directional-pulse.d.ts +6 -0
  17. package/dist/types/effects/low-altitude/navigation-spoofing.d.ts +6 -0
  18. package/dist/types/effects/low-altitude/pulse-marker.d.ts +5 -0
  19. package/dist/types/effects/low-altitude/radar-sweep.d.ts +5 -0
  20. package/dist/types/effects/low-altitude/ring-pulse-marker.d.ts +5 -0
  21. package/dist/types/effects/low-altitude/sector-scan.d.ts +5 -0
  22. package/dist/types/effects/low-altitude/shared.d.ts +67 -0
  23. package/dist/types/effects/low-altitude/target-lock.d.ts +4 -0
  24. package/dist/types/effects/low-altitude/types.d.ts +333 -0
  25. package/dist/types/effects/low-altitude/visual.d.ts +15 -0
  26. package/dist/types/effects/low-altitude.d.ts +15 -126
  27. package/dist/types/effects/standard-electronic-fence.d.ts +7 -0
  28. package/dist/types/effects/webgl-electronic-fence.d.ts +7 -0
  29. package/dist/types/index.d.ts +2 -2
  30. package/dist/types/map/create-map.d.ts +6 -1
  31. package/package.json +1 -1
package/dist/index.esm.js CHANGED
@@ -45,7 +45,7 @@ const getOpenFreeMapStyle = (type = "openfreemap-liberty") => OPENFREEMAP_STYLES
45
45
  const TDT_SUBDOMAINS = ["0", "1", "2", "3", "4", "5", "6", "7"];
46
46
  const TDT_ATTRIBUTION = "© 天地图";
47
47
  const TDT_MAX_ZOOM = 18;
48
- const TDT_STYLE_TYPES = ["tdt-image", "tdt-image-label"];
48
+ const TDT_STYLE_TYPES = ["tdt-image", "tdt-image-label", "tdt-mvt", "tdt-mvt-label"];
49
49
  const getToken = (token) => {
50
50
  const tdtToken = token || getMaplibreToolsConfig().tdtToken;
51
51
  if (!tdtToken) {
@@ -67,6 +67,14 @@ const createTiandituRasterSource = (tileType, options = {}) => ({
67
67
  maxzoom: TDT_MAX_ZOOM,
68
68
  attribution: TDT_ATTRIBUTION
69
69
  });
70
+ const createTiandituMvtSource = (options = {}) => ({
71
+ type: "raster",
72
+ tiles: createTdtTileUrls("vec_w", options.token),
73
+ tileSize: 256,
74
+ minzoom: 0,
75
+ maxzoom: TDT_MAX_ZOOM,
76
+ attribution: TDT_ATTRIBUTION
77
+ });
70
78
  const createTiandituImageStyle = (options = {}) => {
71
79
  const withLabel = options.label !== false;
72
80
  const sources = {
@@ -93,10 +101,38 @@ const createTiandituImageStyle = (options = {}) => {
93
101
  layers
94
102
  };
95
103
  };
104
+ const createTiandituMvtStyle = (options = {}) => {
105
+ const withLabel = options.label !== false;
106
+ const sources = {
107
+ "tdt-mvt": createTiandituMvtSource(options)
108
+ };
109
+ const layers = [
110
+ {
111
+ id: "tdt-mvt",
112
+ type: "raster",
113
+ source: "tdt-mvt"
114
+ }
115
+ ];
116
+ if (withLabel) {
117
+ sources["tdt-mvt-label"] = createTiandituRasterSource("cva_w", options);
118
+ layers.push({
119
+ id: "tdt-mvt-label",
120
+ type: "raster",
121
+ source: "tdt-mvt-label"
122
+ });
123
+ }
124
+ return {
125
+ version: 8,
126
+ sources,
127
+ layers
128
+ };
129
+ };
96
130
  const BASE_MAP_TYPES = [
97
131
  ...Object.keys(OPENFREEMAP_STYLES),
98
132
  "tdt-image",
99
- "tdt-image-label"
133
+ "tdt-image-label",
134
+ "tdt-mvt",
135
+ "tdt-mvt-label"
100
136
  ];
101
137
  const isBaseMapType = (type) => BASE_MAP_TYPES.includes(type);
102
138
  const getBaseMapStyle = (type = "openfreemap-liberty", options = {}) => {
@@ -106,6 +142,12 @@ const getBaseMapStyle = (type = "openfreemap-liberty", options = {}) => {
106
142
  if (type === "tdt-image-label") {
107
143
  return createTiandituImageStyle({ ...options, label: true });
108
144
  }
145
+ if (type === "tdt-mvt") {
146
+ return createTiandituMvtStyle({ ...options, label: false });
147
+ }
148
+ if (type === "tdt-mvt-label") {
149
+ return createTiandituMvtStyle({ ...options, label: true });
150
+ }
109
151
  return getOpenFreeMapStyle(type);
110
152
  };
111
153
  const setBaseMap = (map, type, options = {}) => {
@@ -147,6 +189,22 @@ const getDestination = (center, distance, bearing) => {
147
189
  toDegrees(nextLatitude)
148
190
  ];
149
191
  };
192
+ const getDistance = (start, end) => {
193
+ const latitudeDelta = toRadians(end[1] - start[1]);
194
+ const longitudeDelta = toRadians(end[0] - start[0]);
195
+ const startLatitude = toRadians(start[1]);
196
+ const endLatitude = toRadians(end[1]);
197
+ const a = Math.sin(latitudeDelta / 2) * Math.sin(latitudeDelta / 2) + Math.cos(startLatitude) * Math.cos(endLatitude) * Math.sin(longitudeDelta / 2) * Math.sin(longitudeDelta / 2);
198
+ return 2 * EARTH_RADIUS * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
199
+ };
200
+ const getBearing = (start, end) => {
201
+ const startLatitude = toRadians(start[1]);
202
+ const endLatitude = toRadians(end[1]);
203
+ const longitudeDelta = toRadians(end[0] - start[0]);
204
+ const y = Math.sin(longitudeDelta) * Math.cos(endLatitude);
205
+ const x = Math.cos(startLatitude) * Math.sin(endLatitude) - Math.sin(startLatitude) * Math.cos(endLatitude) * Math.cos(longitudeDelta);
206
+ return normalizeAngle(toDegrees(Math.atan2(y, x)));
207
+ };
150
208
  const createAngleStops = (startAngle, endAngle, segmentCount = DEFAULT_SEGMENTS) => {
151
209
  const sweep = getSweepAngle(startAngle, endAngle);
152
210
  const segments = Math.max(12, Math.ceil(segmentCount * sweep / 360));
@@ -201,6 +259,40 @@ const createRayLine = (center, radius, bearing) => ({
201
259
  type: "LineString",
202
260
  coordinates: [center, getDestination(center, radius, bearing)]
203
261
  });
262
+ const createBeamGeometry = (options) => {
263
+ const endCenter = options.target || getDestination(
264
+ options.center,
265
+ options.distance || 0,
266
+ options.bearing || 0
267
+ );
268
+ const beamBearing = options.target !== void 0 ? getBearing(options.center, options.target) : normalizeAngle(options.bearing || 0);
269
+ const startLeft = getDestination(
270
+ options.center,
271
+ options.startWidth / 2,
272
+ beamBearing - 90
273
+ );
274
+ const startRight = getDestination(
275
+ options.center,
276
+ options.startWidth / 2,
277
+ beamBearing + 90
278
+ );
279
+ const endLeft = getDestination(endCenter, options.endWidth / 2, beamBearing - 90);
280
+ const endRight = getDestination(
281
+ endCenter,
282
+ options.endWidth / 2,
283
+ beamBearing + 90
284
+ );
285
+ return {
286
+ polygon: {
287
+ type: "Polygon",
288
+ coordinates: [[startLeft, endLeft, endRight, startRight, startLeft]]
289
+ },
290
+ centerLine: {
291
+ type: "LineString",
292
+ coordinates: [options.center, endCenter]
293
+ }
294
+ };
295
+ };
204
296
  const createFeature = (geometry, properties) => ({
205
297
  type: "Feature",
206
298
  geometry,
@@ -228,6 +320,7 @@ const setLayersVisibility = (map, layerIds, visible) => {
228
320
  }
229
321
  });
230
322
  };
323
+ const isStyleNotReadyError$1 = (error) => error instanceof Error && error.message === "Style is not done loading.";
231
324
  const createManagedEffect = (map, initialOptions, renderEffect) => {
232
325
  const effectId = initialOptions.id;
233
326
  const sourceId = `${effectId}-source`;
@@ -259,7 +352,7 @@ const createManagedEffect = (map, initialOptions, renderEffect) => {
259
352
  }
260
353
  };
261
354
  const renderNow = () => {
262
- var _a, _b;
355
+ var _a, _b, _c;
263
356
  if (removed) {
264
357
  return;
265
358
  }
@@ -289,6 +382,19 @@ const createManagedEffect = (map, initialOptions, renderEffect) => {
289
382
  );
290
383
  });
291
384
  setLayersVisibility(map, layerIds, options.visible !== false);
385
+ (_c = nextRender.canvasSources) == null ? void 0 : _c.forEach((canvasSource) => {
386
+ var _a2;
387
+ const styleAny = map.style;
388
+ const tileManager = (_a2 = styleAny == null ? void 0 : styleAny.tileManagers) == null ? void 0 : _a2[canvasSource.id];
389
+ if (!tileManager || !tileManager._sourceLoaded) {
390
+ return;
391
+ }
392
+ const prevUsed = tileManager.used;
393
+ tileManager.used = true;
394
+ const mapAny = map;
395
+ tileManager.update(mapAny.transform, mapAny.terrain);
396
+ tileManager.used = prevUsed;
397
+ });
292
398
  if (nextRender.startAnimation) {
293
399
  disposeAnimation = nextRender.startAnimation({
294
400
  getOptions: () => options,
@@ -296,30 +402,38 @@ const createManagedEffect = (map, initialOptions, renderEffect) => {
296
402
  }) || void 0;
297
403
  }
298
404
  };
299
- const handleStyleData = () => {
300
- if (!waitingForStyle || removed || !map.isStyleLoaded()) {
405
+ const stopWaitingForStyle = () => {
406
+ if (!waitingForStyle) {
301
407
  return;
302
408
  }
303
409
  waitingForStyle = false;
304
410
  map.off("styledata", handleStyleData);
305
- renderNow();
306
411
  };
307
- const requestRender = () => {
308
- if (removed) {
309
- return;
310
- }
311
- if (!map.isStyleLoaded()) {
412
+ const ensureRender = () => {
413
+ try {
414
+ renderNow();
415
+ stopWaitingForStyle();
416
+ } catch (error) {
417
+ if (!isStyleNotReadyError$1(error)) {
418
+ throw error;
419
+ }
312
420
  if (!waitingForStyle) {
313
421
  waitingForStyle = true;
314
422
  map.on("styledata", handleStyleData);
315
423
  }
424
+ }
425
+ };
426
+ const handleStyleData = () => {
427
+ if (!waitingForStyle || removed) {
316
428
  return;
317
429
  }
318
- if (waitingForStyle) {
319
- waitingForStyle = false;
320
- map.off("styledata", handleStyleData);
430
+ ensureRender();
431
+ };
432
+ const requestRender = () => {
433
+ if (removed) {
434
+ return;
321
435
  }
322
- renderNow();
436
+ ensureRender();
323
437
  };
324
438
  requestRender();
325
439
  return {
@@ -435,27 +549,103 @@ const createPulseMarkerRender = (options) => {
435
549
  }
436
550
  };
437
551
  };
552
+ const addPulseMarker = (map, options) => createManagedEffect(map, options, createPulseMarkerRender);
553
+ const createBreathingCircleData = (options, phase = 0) => {
554
+ const baseRadius = Math.max(1, options.radius ?? 90);
555
+ const minRadius = Math.max(1, options.minRadius ?? baseRadius * 0.78);
556
+ const maxRadius = Math.max(
557
+ minRadius,
558
+ options.maxRadius ?? baseRadius * 1.22
559
+ );
560
+ const progress = (1 - Math.cos(phase * Math.PI * 2)) / 2;
561
+ const currentRadius = minRadius + (maxRadius - minRadius) * progress;
562
+ const fillOpacity = options.fillOpacity ?? 0.28;
563
+ const minFillOpacity = options.minFillOpacity ?? fillOpacity * 0.46;
564
+ const strokeOpacity = options.strokeOpacity ?? 0.82;
565
+ const minStrokeOpacity = options.minStrokeOpacity ?? strokeOpacity * 0.38;
566
+ const inverseProgress = 1 - progress;
567
+ const features = [
568
+ createFeature(createCirclePolygon(options.center, currentRadius), {
569
+ kind: "breathing-fill",
570
+ opacity: minFillOpacity + (fillOpacity - minFillOpacity) * inverseProgress
571
+ })
572
+ ];
573
+ if ((options.strokeWidth ?? 2) > 0) {
574
+ features.push(
575
+ createFeature(createCircleLine(options.center, currentRadius), {
576
+ kind: "breathing-stroke",
577
+ opacity: minStrokeOpacity + (strokeOpacity - minStrokeOpacity) * inverseProgress
578
+ })
579
+ );
580
+ }
581
+ return createFeatureCollection(features);
582
+ };
583
+ const createBreathingCircleRender = (options) => {
584
+ const color = options.color || "#1677ff";
585
+ const fillColor = options.fillColor || color;
586
+ const strokeColor = options.strokeColor || color;
587
+ const strokeWidth = Math.max(0, options.strokeWidth ?? 2);
588
+ return {
589
+ data: createBreathingCircleData(options),
590
+ layers: [
591
+ {
592
+ id: `${options.id}-breathing-fill`,
593
+ type: "fill",
594
+ filter: ["==", ["get", "kind"], "breathing-fill"],
595
+ paint: {
596
+ "fill-color": fillColor,
597
+ "fill-opacity": ["coalesce", ["get", "opacity"], options.fillOpacity ?? 0.28]
598
+ }
599
+ },
600
+ {
601
+ id: `${options.id}-breathing-stroke`,
602
+ type: "line",
603
+ filter: ["==", ["get", "kind"], "breathing-stroke"],
604
+ layout: {
605
+ "line-cap": "round",
606
+ "line-join": "round"
607
+ },
608
+ paint: {
609
+ "line-color": strokeColor,
610
+ "line-opacity": ["coalesce", ["get", "opacity"], options.strokeOpacity ?? 0.82],
611
+ "line-width": strokeWidth
612
+ }
613
+ }
614
+ ],
615
+ startAnimation({ getOptions, setData }) {
616
+ const duration = Math.max(400, getOptions().duration ?? 1800);
617
+ const startTime = performance.now();
618
+ let frameId = 0;
619
+ const tick = (timestamp) => {
620
+ const currentOptions = getOptions();
621
+ const phase = (timestamp - startTime) % duration / duration;
622
+ setData(createBreathingCircleData(currentOptions, phase));
623
+ frameId = requestAnimationFrame(tick);
624
+ };
625
+ frameId = requestAnimationFrame(tick);
626
+ return () => {
627
+ cancelAnimationFrame(frameId);
628
+ };
629
+ }
630
+ };
631
+ };
632
+ const addBreathingCircle = (map, options) => createManagedEffect(map, options, createBreathingCircleRender);
438
633
  const createRingPulseEffectData = (options, phase = 0) => {
439
- var _a, _b;
440
634
  const baseRadius = Math.max(16, options.radius ?? 110);
441
635
  const pulseScale = Math.max(0, options.pulseScale ?? 0.16);
442
636
  const pulseProgress = (1 - Math.cos(phase * Math.PI * 2)) / 2;
443
637
  const currentRadius = baseRadius * (1 + pulseScale * pulseProgress);
444
- const haloRadius = currentRadius * 1.28;
445
- const ringCount = Math.max(1, options.ringCount ?? ((_a = options.radii) == null ? void 0 : _a.length) ?? 3);
638
+ const ringCount = Math.max(1, options.ringCount ?? 3);
446
639
  const maxRadius = Math.max(
447
640
  currentRadius * 2.2,
448
- options.maxRadius ?? (((_b = options.radii) == null ? void 0 : _b.length) ? Math.max(...options.radii) : baseRadius * (ringCount * 2.5))
641
+ options.maxRadius ?? baseRadius * (ringCount * 2.5)
449
642
  );
450
643
  const ringSpacing = maxRadius / ringCount;
644
+ const fillOpacity = options.fillOpacity ?? 0.28;
451
645
  const features = [
452
- createFeature(createCirclePolygon(options.center, haloRadius), {
453
- kind: "halo",
454
- opacity: (options.haloOpacity ?? 0.16) * (0.75 + pulseProgress * 0.65)
455
- }),
456
646
  createFeature(createCirclePolygon(options.center, currentRadius), {
457
647
  kind: "core",
458
- opacity: (options.fillOpacity ?? 0.28) * (0.88 + pulseProgress * 0.2)
648
+ opacity: fillOpacity * (0.88 + pulseProgress * 0.2)
459
649
  }),
460
650
  createFeature(createCircleLine(options.center, currentRadius * 1.06), {
461
651
  kind: "core-ring",
@@ -488,15 +678,6 @@ const createRingPulseMarkerRender = (options) => {
488
678
  return {
489
679
  data: createRingPulseEffectData(options),
490
680
  layers: [
491
- {
492
- id: `${options.id}-halo`,
493
- type: "fill",
494
- filter: ["==", ["get", "kind"], "halo"],
495
- paint: {
496
- "fill-color": fillColor,
497
- "fill-opacity": ["coalesce", ["get", "opacity"], options.haloOpacity ?? 0.16]
498
- }
499
- },
500
681
  {
501
682
  id: `${options.id}-core`,
502
683
  type: "fill",
@@ -538,7 +719,8 @@ const createRingPulseMarkerRender = (options) => {
538
719
  }
539
720
  };
540
721
  };
541
- const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
722
+ const addRingPulseMarker = (map, options) => createManagedEffect(map, options, createRingPulseMarkerRender);
723
+ const clamp$1 = (value, min, max) => Math.min(max, Math.max(min, value));
542
724
  const parseColor = (color) => {
543
725
  const value = color.trim();
544
726
  const hex = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
@@ -556,9 +738,9 @@ const parseColor = (color) => {
556
738
  );
557
739
  if (rgb) {
558
740
  return [
559
- clamp(Number(rgb[1]), 0, 255),
560
- clamp(Number(rgb[2]), 0, 255),
561
- clamp(Number(rgb[3]), 0, 255)
741
+ clamp$1(Number(rgb[1]), 0, 255),
742
+ clamp$1(Number(rgb[2]), 0, 255),
743
+ clamp$1(Number(rgb[3]), 0, 255)
562
744
  ];
563
745
  }
564
746
  return void 0;
@@ -574,7 +756,30 @@ const toRgbaColor = (color, opacity) => {
574
756
  if (!rgb) {
575
757
  return color;
576
758
  }
577
- return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp(opacity, 0, 1)})`;
759
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp$1(opacity, 0, 1)})`;
760
+ };
761
+ const easeOutQuad = (t) => t * (2 - t);
762
+ const lerpRgb = (a, b, t) => [
763
+ Math.round(a[0] + (b[0] - a[0]) * t),
764
+ Math.round(a[1] + (b[1] - a[1]) * t),
765
+ Math.round(a[2] + (b[2] - a[2]) * t)
766
+ ];
767
+ const lerpColorArray = (colors, t) => {
768
+ const last = colors.length - 1;
769
+ if (t <= 0) {
770
+ return colors[0];
771
+ }
772
+ if (t >= last) {
773
+ return colors[last];
774
+ }
775
+ const idx = Math.floor(t);
776
+ const frac = t - idx;
777
+ const [a, b] = [parseColor(colors[idx]), parseColor(colors[idx + 1])];
778
+ if (!a || !b) {
779
+ return colors[Math.round(t)];
780
+ }
781
+ const [r, g, b2] = lerpRgb(a, b, frac);
782
+ return `rgb(${r},${g},${b2})`;
578
783
  };
579
784
  const createSectorCanvasCoordinates = (center, radius) => {
580
785
  const north = getDestination(center, radius, 0)[1];
@@ -613,8 +818,8 @@ const drawSectorGradientCanvas = (canvas, options, angleOffset = 0) => {
613
818
  const startCanvasAngle = getCanvasArcAngle(startAngle);
614
819
  const endCanvasAngle = startCanvasAngle + sweep * Math.PI / 180;
615
820
  const baseOpacity = options.gradient.opacity ?? options.opacity ?? 0.28;
616
- const centerOpacity = options.gradient.centerOpacity ?? options.gradient.tailOpacity ?? baseOpacity * 0.25;
617
- const edgeOpacity = options.gradient.edgeOpacity ?? options.gradient.headOpacity ?? baseOpacity;
821
+ const centerOpacity = options.gradient.centerOpacity ?? baseOpacity * 0.25;
822
+ const edgeOpacity = options.gradient.edgeOpacity ?? baseOpacity;
618
823
  const gradient = context.createRadialGradient(
619
824
  center,
620
825
  center,
@@ -743,6 +948,7 @@ const createSectorScanRender = (options) => {
743
948
  } : void 0
744
949
  };
745
950
  };
951
+ const addSectorScan = (map, options) => createManagedEffect(map, options, createSectorScanRender);
746
952
  const createDirectionalPulseData = (options, leadingDistance) => {
747
953
  const radius = Math.max(40, options.radius);
748
954
  const direction = options.direction ?? 270;
@@ -821,13 +1027,87 @@ const createDirectionalPulseRender = (options) => ({
821
1027
  };
822
1028
  } : void 0
823
1029
  });
1030
+ const addDirectionalPulse = (map, options) => createManagedEffect(map, options, createDirectionalPulseRender);
1031
+ const hasRadarSweepGradient = (options) => {
1032
+ var _a;
1033
+ const colors = (_a = options.gradient) == null ? void 0 : _a.colors;
1034
+ return Array.isArray(colors) && colors.length >= 2;
1035
+ };
1036
+ const createRadarSweepCanvasCoordinates = (center, radius) => {
1037
+ const north = getDestination(center, radius, 0)[1];
1038
+ const east = getDestination(center, radius, 90)[0];
1039
+ const south = getDestination(center, radius, 180)[1];
1040
+ const west = getDestination(center, radius, 270)[0];
1041
+ return [
1042
+ [west, north],
1043
+ [east, north],
1044
+ [east, south],
1045
+ [west, south]
1046
+ ];
1047
+ };
1048
+ const createRadarSweepCanvas = () => {
1049
+ const canvas = document.createElement("canvas");
1050
+ canvas.width = 768;
1051
+ canvas.height = 768;
1052
+ return canvas;
1053
+ };
1054
+ const drawRadarSweepCanvas = (canvas, options, angleOffset = 0) => {
1055
+ const ctx = canvas.getContext("2d");
1056
+ const gradient = options.gradient;
1057
+ const colors = gradient == null ? void 0 : gradient.colors;
1058
+ if (!ctx || !gradient || !colors || colors.length < 2) {
1059
+ return;
1060
+ }
1061
+ const size = canvas.width;
1062
+ const centerXY = size / 2;
1063
+ const drawRadius = centerXY - 2;
1064
+ const sweepAngle = Math.max(12, options.sweepAngle ?? 42);
1065
+ const tailOpacity = options.tailOpacity ?? 0.04;
1066
+ const headOpacity = Math.max(0.24, options.opacity ?? 0.3);
1067
+ const sweepStartAngle = (options.sweepStartAngle ?? 0) + angleOffset;
1068
+ const totalStartAngle = sweepStartAngle;
1069
+ const totalEndAngle = sweepStartAngle + sweepAngle;
1070
+ const totalSpan = sweepAngle;
1071
+ ctx.clearRect(0, 0, size, size);
1072
+ const conicGradient = ctx.createConicGradient(
1073
+ getCanvasArcAngle(totalStartAngle),
1074
+ centerXY,
1075
+ centerXY
1076
+ );
1077
+ const normalizedSpan = totalSpan / 360;
1078
+ const maxOpacity = Math.min(1, headOpacity + 0.08);
1079
+ const stopCount = 128;
1080
+ for (let i = 0; i <= stopCount; i++) {
1081
+ const t = i / stopCount;
1082
+ const eased = easeOutQuad(t);
1083
+ const opacity = tailOpacity + (maxOpacity - tailOpacity) * eased;
1084
+ const ci = t * (colors.length - 1);
1085
+ const color = lerpColorArray(colors, ci);
1086
+ conicGradient.addColorStop(
1087
+ t * normalizedSpan,
1088
+ toRgbaColor(color, Math.max(0, opacity))
1089
+ );
1090
+ }
1091
+ ctx.save();
1092
+ ctx.beginPath();
1093
+ ctx.moveTo(centerXY, centerXY);
1094
+ ctx.arc(
1095
+ centerXY,
1096
+ centerXY,
1097
+ drawRadius,
1098
+ getCanvasArcAngle(totalStartAngle),
1099
+ getCanvasArcAngle(totalEndAngle),
1100
+ false
1101
+ );
1102
+ ctx.closePath();
1103
+ ctx.fillStyle = conicGradient;
1104
+ ctx.fill();
1105
+ ctx.restore();
1106
+ };
824
1107
  const createRadarSweepData = (options, angleOffset = 0) => {
825
1108
  var _a;
826
1109
  const ringCount = Math.max(0, options.ringCount ?? 4);
827
- const trailCount = Math.max(1, options.trailCount ?? 8);
828
1110
  const sweepAngle = Math.max(12, options.sweepAngle ?? 42);
829
- const trailGap = sweepAngle * 0.22;
830
- const tailOpacity = options.tailOpacity ?? 0.04;
831
1111
  const headOpacity = options.headOpacity ?? Math.max(0.24, options.opacity ?? 0.3);
832
1112
  const sweepStartAngle = (options.sweepStartAngle ?? 0) + angleOffset;
833
1113
  const showCrosshair = options.showCrosshair !== false;
@@ -836,6 +1116,7 @@ const createRadarSweepData = (options, angleOffset = 0) => {
836
1116
  20,
837
1117
  options.coreRadius ?? Math.min(160, options.radius * 0.035)
838
1118
  );
1119
+ const useGradient = hasRadarSweepGradient(options);
839
1120
  const features = [];
840
1121
  for (let index = ringCount; index >= 1; index -= 1) {
841
1122
  const distance = options.radius * index / ringCount;
@@ -904,123 +1185,2714 @@ const createRadarSweepData = (options, angleOffset = 0) => {
904
1185
  kind: "core"
905
1186
  })
906
1187
  );
907
- for (let index = trailCount - 1; index >= 0; index -= 1) {
908
- const startAngle = sweepStartAngle - trailGap * index;
909
- const endAngle = startAngle + sweepAngle;
910
- const progress = (trailCount - index) / trailCount;
911
- const opacity = tailOpacity + (headOpacity - tailOpacity) * progress * progress;
1188
+ if (!useGradient) {
912
1189
  features.push(
913
1190
  createFeature(
914
- createSectorPolygon(options.center, options.radius, startAngle, endAngle),
1191
+ createSectorPolygon(
1192
+ options.center,
1193
+ options.radius,
1194
+ sweepStartAngle,
1195
+ sweepStartAngle + sweepAngle
1196
+ ),
915
1197
  {
916
1198
  kind: "sweep",
917
- opacity
1199
+ opacity: headOpacity
918
1200
  }
919
1201
  )
920
1202
  );
921
1203
  }
922
- features.push(
923
- createFeature(
924
- createSectorPolygon(
925
- options.center,
926
- options.radius,
927
- sweepStartAngle + sweepAngle * 0.42,
928
- sweepStartAngle + sweepAngle
929
- ),
930
- {
931
- kind: "head",
932
- opacity: Math.min(1, headOpacity + 0.08)
933
- }
934
- )
935
- );
936
1204
  return createFeatureCollection(features);
937
1205
  };
938
- const createRadarSweepRender = (options) => ({
939
- data: createRadarSweepData(options),
940
- layers: [
941
- {
942
- id: `${options.id}-radar-trail`,
943
- type: "fill",
944
- filter: ["==", ["get", "kind"], "sweep"],
945
- paint: {
946
- "fill-color": options.color || "#22c55e",
947
- "fill-opacity": ["coalesce", ["get", "opacity"], options.opacity ?? 0.32]
948
- }
949
- },
950
- {
951
- id: `${options.id}-radar-rings`,
952
- type: "line",
953
- filter: ["==", ["get", "kind"], "ring"],
954
- paint: {
955
- "line-color": options.ringColor || options.color || "#22c55e",
956
- "line-opacity": options.ringOpacity ?? 0.95,
957
- "line-width": options.ringWidth ?? 2
958
- }
959
- },
960
- {
961
- id: `${options.id}-radar-crosshair`,
962
- type: "line",
963
- filter: ["==", ["get", "kind"], "crosshair"],
964
- paint: {
965
- "line-color": options.crosshairColor || options.ringColor || options.color || "#22c55e",
966
- "line-opacity": options.crosshairOpacity ?? 0.5,
967
- "line-width": options.crosshairWidth ?? 1.4
1206
+ const createRadarSweepRender = (options) => {
1207
+ const useGradient = hasRadarSweepGradient(options);
1208
+ const canvas = useGradient ? createRadarSweepCanvas() : void 0;
1209
+ if (canvas) {
1210
+ drawRadarSweepCanvas(canvas, options);
1211
+ }
1212
+ const sweepLayer = canvas ? {
1213
+ id: `${options.id}-radar-trail`,
1214
+ type: "raster",
1215
+ paint: {
1216
+ "raster-opacity": 1,
1217
+ "raster-fade-duration": 0
1218
+ }
1219
+ } : {
1220
+ id: `${options.id}-radar-trail`,
1221
+ type: "fill",
1222
+ filter: ["==", ["get", "kind"], "sweep"],
1223
+ paint: {
1224
+ "fill-color": options.color || "#22c55e",
1225
+ "fill-opacity": ["coalesce", ["get", "opacity"], options.opacity ?? 0.32]
1226
+ }
1227
+ };
1228
+ return {
1229
+ data: createRadarSweepData(options),
1230
+ canvasSources: canvas ? [
1231
+ {
1232
+ id: `${options.id}-canvas-source`,
1233
+ source: {
1234
+ type: "canvas",
1235
+ canvas,
1236
+ coordinates: createRadarSweepCanvasCoordinates(
1237
+ options.center,
1238
+ options.radius
1239
+ ),
1240
+ animate: true
1241
+ }
968
1242
  }
969
- },
970
- {
971
- id: `${options.id}-radar-distance-labels`,
972
- type: "symbol",
973
- filter: ["==", ["get", "kind"], "distance-label"],
974
- layout: {
975
- "text-field": ["get", "text"],
976
- "text-size": options.distanceLabelSize ?? 12,
977
- "text-anchor": ["get", "textAnchor"],
978
- "text-allow-overlap": true,
979
- "text-ignore-placement": true
1243
+ ] : void 0,
1244
+ layers: [
1245
+ sweepLayer,
1246
+ {
1247
+ id: `${options.id}-radar-rings`,
1248
+ type: "line",
1249
+ filter: ["==", ["get", "kind"], "ring"],
1250
+ paint: {
1251
+ "line-color": options.ringColor || options.color || "#22c55e",
1252
+ "line-opacity": options.ringOpacity ?? 0.95,
1253
+ "line-width": options.ringWidth ?? 2
1254
+ }
980
1255
  },
981
- paint: {
982
- "text-color": options.distanceLabelColor || options.ringColor || options.color || "#22c55e",
983
- "text-opacity": options.distanceLabelOpacity ?? 0.92,
984
- "text-halo-color": options.distanceLabelHaloColor || "rgba(15, 23, 42, 0.42)",
985
- "text-halo-width": 1
1256
+ {
1257
+ id: `${options.id}-radar-crosshair`,
1258
+ type: "line",
1259
+ filter: ["==", ["get", "kind"], "crosshair"],
1260
+ paint: {
1261
+ "line-color": options.crosshairColor || options.ringColor || options.color || "#22c55e",
1262
+ "line-opacity": options.crosshairOpacity ?? 0.5,
1263
+ "line-width": options.crosshairWidth ?? 1.4
1264
+ }
1265
+ },
1266
+ {
1267
+ id: `${options.id}-radar-distance-labels`,
1268
+ type: "symbol",
1269
+ filter: ["==", ["get", "kind"], "distance-label"],
1270
+ layout: {
1271
+ "text-field": ["get", "text"],
1272
+ "text-size": options.distanceLabelSize ?? 12,
1273
+ "text-anchor": ["get", "textAnchor"],
1274
+ "text-allow-overlap": true,
1275
+ "text-ignore-placement": true
1276
+ },
1277
+ paint: {
1278
+ "text-color": options.distanceLabelColor || options.ringColor || options.color || "#22c55e",
1279
+ "text-opacity": options.distanceLabelOpacity ?? 0.92,
1280
+ "text-halo-color": options.distanceLabelHaloColor || "rgba(15, 23, 42, 0.42)",
1281
+ "text-halo-width": 1
1282
+ }
986
1283
  }
987
- }
988
- ],
989
- startAnimation({ getOptions, setData }) {
990
- const startTime = performance.now();
991
- let frameId = 0;
992
- const tick = (timestamp) => {
993
- const currentOptions = getOptions();
994
- const angleOffset = (timestamp - startTime) / 1e3 * (currentOptions.rotationSpeed ?? 36);
995
- setData(createRadarSweepData(currentOptions, angleOffset));
1284
+ ],
1285
+ startAnimation({ getOptions, setData }) {
1286
+ const startTime = performance.now();
1287
+ let frameId = 0;
1288
+ const tick = (timestamp) => {
1289
+ const currentOptions = getOptions();
1290
+ const angleOffset = (timestamp - startTime) / 1e3 * (currentOptions.rotationSpeed ?? 36);
1291
+ if (canvas) {
1292
+ drawRadarSweepCanvas(canvas, currentOptions, angleOffset);
1293
+ }
1294
+ setData(createRadarSweepData(currentOptions, angleOffset));
1295
+ frameId = requestAnimationFrame(tick);
1296
+ };
996
1297
  frameId = requestAnimationFrame(tick);
997
- };
998
- frameId = requestAnimationFrame(tick);
999
- return () => {
1000
- cancelAnimationFrame(frameId);
1001
- };
1002
- }
1003
- });
1004
- const addPulseMarker = (map, options) => createManagedEffect(map, options, createPulseMarkerRender);
1005
- const addRingPulseMarker = (map, options) => createManagedEffect(map, options, createRingPulseMarkerRender);
1006
- const addSectorScan = (map, options) => createManagedEffect(map, options, createSectorScanRender);
1007
- const addDirectionalPulse = (map, options) => createManagedEffect(map, options, createDirectionalPulseRender);
1298
+ return () => {
1299
+ cancelAnimationFrame(frameId);
1300
+ };
1301
+ }
1302
+ };
1303
+ };
1008
1304
  const addRadarSweep = (map, options) => createManagedEffect(map, options, createRadarSweepRender);
1009
- const resolveStyle = (style, tdtToken) => {
1010
- if (!style) {
1011
- return getBaseMapStyle("openfreemap-liberty");
1012
- }
1013
- if (typeof style === "string" && isBaseMapType(style)) {
1014
- return getBaseMapStyle(style, { token: tdtToken });
1015
- }
1305
+ const createDefenceCircleCanvas = () => {
1306
+ const canvas = document.createElement("canvas");
1307
+ canvas.width = 512;
1308
+ canvas.height = 512;
1309
+ return canvas;
1310
+ };
1311
+ const createDefenceCircleCanvasCoordinates = (center, radius) => {
1312
+ const north = getDestination(center, radius, 0)[1];
1313
+ const east = getDestination(center, radius, 90)[0];
1314
+ const south = getDestination(center, radius, 180)[1];
1315
+ const west = getDestination(center, radius, 270)[0];
1316
+ return [
1317
+ [west, north],
1318
+ [east, north],
1319
+ [east, south],
1320
+ [west, south]
1321
+ ];
1322
+ };
1323
+ const drawDefenceCircleCanvas = (canvas, options) => {
1324
+ const ctx = canvas.getContext("2d");
1325
+ if (!ctx) {
1326
+ return;
1327
+ }
1328
+ const size = canvas.width;
1329
+ const centerXY = size / 2;
1330
+ const drawRadius = centerXY - 1;
1331
+ const color = options.color || "#06c536";
1332
+ const fillColor = options.fillColor || color;
1333
+ const borderColor = options.borderColor || color;
1334
+ const fillOpacity = options.fillOpacity ?? 0.06;
1335
+ const borderOpacity = options.borderOpacity ?? 0.92;
1336
+ ctx.clearRect(0, 0, size, size);
1337
+ ctx.save();
1338
+ ctx.beginPath();
1339
+ ctx.arc(centerXY, centerXY, drawRadius, 0, Math.PI * 2);
1340
+ ctx.clip();
1341
+ const gradient = ctx.createRadialGradient(
1342
+ centerXY,
1343
+ centerXY,
1344
+ 0,
1345
+ centerXY,
1346
+ centerXY,
1347
+ drawRadius
1348
+ );
1349
+ const edgeGlowOpacity = Math.min(0.8, fillOpacity + (borderOpacity - fillOpacity) * 0.85);
1350
+ gradient.addColorStop(0, toRgbaColor(fillColor, fillOpacity));
1351
+ gradient.addColorStop(0.85, toRgbaColor(fillColor, fillOpacity));
1352
+ gradient.addColorStop(0.92, toRgbaColor(borderColor, fillOpacity + (edgeGlowOpacity - fillOpacity) * 0.3));
1353
+ gradient.addColorStop(0.96, toRgbaColor(borderColor, fillOpacity + (edgeGlowOpacity - fillOpacity) * 0.7));
1354
+ gradient.addColorStop(1, toRgbaColor(borderColor, edgeGlowOpacity));
1355
+ ctx.fillStyle = gradient;
1356
+ ctx.fillRect(0, 0, size, size);
1357
+ ctx.restore();
1358
+ };
1359
+ const createDefenceCircleRender = (options) => {
1360
+ const canvas = createDefenceCircleCanvas();
1361
+ drawDefenceCircleCanvas(canvas, options);
1362
+ const borderColor = options.borderColor || options.color || "#06c536";
1363
+ return {
1364
+ data: createFeatureCollection([
1365
+ createFeature(createCircleLine(options.center, options.radius), {
1366
+ kind: "defence-border"
1367
+ })
1368
+ ]),
1369
+ canvasSources: [
1370
+ {
1371
+ id: `${options.id}-canvas-source`,
1372
+ source: {
1373
+ type: "canvas",
1374
+ canvas,
1375
+ coordinates: createDefenceCircleCanvasCoordinates(
1376
+ options.center,
1377
+ options.radius
1378
+ ),
1379
+ animate: true
1380
+ }
1381
+ }
1382
+ ],
1383
+ layers: [
1384
+ {
1385
+ id: `${options.id}-defence-circle`,
1386
+ type: "raster",
1387
+ paint: {
1388
+ "raster-opacity": 1,
1389
+ "raster-fade-duration": 0
1390
+ }
1391
+ },
1392
+ {
1393
+ id: `${options.id}-defence-border`,
1394
+ type: "line",
1395
+ filter: ["==", ["get", "kind"], "defence-border"],
1396
+ paint: {
1397
+ "line-color": borderColor,
1398
+ "line-opacity": options.borderOpacity ?? 0.92,
1399
+ "line-width": options.borderWidth ?? 2
1400
+ }
1401
+ }
1402
+ ]
1403
+ };
1404
+ };
1405
+ const addDefenceCircle = (map, options) => createManagedEffect(map, options, createDefenceCircleRender);
1406
+ const createCountermeasureBeamData = (options, elapsed = 0) => {
1407
+ const sourceWidth = Math.max(1, options.sourceWidth ?? 220);
1408
+ const targetWidth = Math.max(1, options.targetWidth ?? 45);
1409
+ const beam = createBeamGeometry({
1410
+ center: options.center,
1411
+ target: options.target,
1412
+ startWidth: sourceWidth,
1413
+ endWidth: targetWidth
1414
+ });
1415
+ const features = [
1416
+ createFeature(beam.polygon, { kind: "countermeasure-channel" }),
1417
+ createFeature(beam.centerLine, { kind: "countermeasure-line" })
1418
+ ];
1419
+ const distance = getDistance(options.center, options.target);
1420
+ const bearing = getBearing(options.center, options.target);
1421
+ const particleCount = Math.max(0, Math.floor(options.particleCount ?? 18));
1422
+ const particleDuration = Math.max(300, options.particleDuration ?? 1200);
1423
+ const particlePhase = elapsed % particleDuration / particleDuration;
1424
+ const particleRadius = Math.max(1, options.particleRadius ?? 2.8);
1425
+ if (distance > 0) {
1426
+ for (let index = 0; index < particleCount; index += 1) {
1427
+ const progress = (particlePhase + index / particleCount) % 1;
1428
+ features.push(
1429
+ createFeature(
1430
+ {
1431
+ type: "Point",
1432
+ coordinates: getDestination(
1433
+ options.center,
1434
+ distance * progress,
1435
+ bearing
1436
+ )
1437
+ },
1438
+ {
1439
+ kind: "countermeasure-particle",
1440
+ opacity: 0.25 + progress * 0.75,
1441
+ radius: particleRadius * (0.7 + progress * 0.3)
1442
+ }
1443
+ )
1444
+ );
1445
+ }
1446
+ }
1447
+ if (options.targetRipple !== false) {
1448
+ const rippleCount = Math.max(1, Math.floor(options.targetRippleCount ?? 2));
1449
+ const rippleRadius = Math.max(10, options.targetRippleRadius ?? 120);
1450
+ const rippleDuration = Math.max(300, options.targetRippleDuration ?? 900);
1451
+ const ripplePhase = elapsed % rippleDuration / rippleDuration;
1452
+ for (let index = 0; index < rippleCount; index += 1) {
1453
+ const progress = (ripplePhase + index / rippleCount) % 1;
1454
+ const radiusProgress = options.mode === "forcedLanding" ? 1 - progress : progress;
1455
+ features.push(
1456
+ createFeature(
1457
+ createCircleLine(
1458
+ options.target,
1459
+ Math.max(2, rippleRadius * radiusProgress)
1460
+ ),
1461
+ {
1462
+ kind: "countermeasure-target-ripple",
1463
+ opacity: Math.sin(Math.PI * progress) * 0.9
1464
+ }
1465
+ )
1466
+ );
1467
+ }
1468
+ }
1469
+ return createFeatureCollection(features);
1470
+ };
1471
+ const createCountermeasureBeamRender = (options) => {
1472
+ const color = options.color ?? "#a855f7";
1473
+ const lineColor = options.lineColor ?? "#f0abfc";
1474
+ const lineWidth = Math.max(1, options.lineWidth ?? 2.5);
1475
+ return {
1476
+ data: createCountermeasureBeamData(options),
1477
+ layers: [
1478
+ {
1479
+ id: `${options.id}-countermeasure-channel`,
1480
+ type: "fill",
1481
+ filter: ["==", ["get", "kind"], "countermeasure-channel"],
1482
+ paint: {
1483
+ "fill-color": options.channelColor ?? color,
1484
+ "fill-opacity": options.channelOpacity ?? 0.16
1485
+ }
1486
+ },
1487
+ {
1488
+ id: `${options.id}-countermeasure-glow`,
1489
+ type: "line",
1490
+ filter: ["==", ["get", "kind"], "countermeasure-line"],
1491
+ layout: {
1492
+ "line-cap": "round"
1493
+ },
1494
+ paint: {
1495
+ "line-color": lineColor,
1496
+ "line-opacity": (options.lineOpacity ?? 0.92) * 0.28,
1497
+ "line-width": lineWidth * 4,
1498
+ "line-blur": lineWidth * 1.4
1499
+ }
1500
+ },
1501
+ {
1502
+ id: `${options.id}-countermeasure-line`,
1503
+ type: "line",
1504
+ filter: ["==", ["get", "kind"], "countermeasure-line"],
1505
+ layout: {
1506
+ "line-cap": "round"
1507
+ },
1508
+ paint: {
1509
+ "line-color": lineColor,
1510
+ "line-opacity": options.lineOpacity ?? 0.92,
1511
+ "line-width": lineWidth
1512
+ }
1513
+ },
1514
+ {
1515
+ id: `${options.id}-countermeasure-target-ripple`,
1516
+ type: "line",
1517
+ filter: [
1518
+ "==",
1519
+ ["get", "kind"],
1520
+ "countermeasure-target-ripple"
1521
+ ],
1522
+ paint: {
1523
+ "line-color": color,
1524
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.9],
1525
+ "line-width": Math.max(1, options.targetRippleWidth ?? 2.5)
1526
+ }
1527
+ },
1528
+ {
1529
+ id: `${options.id}-countermeasure-particle`,
1530
+ type: "circle",
1531
+ filter: ["==", ["get", "kind"], "countermeasure-particle"],
1532
+ paint: {
1533
+ "circle-color": options.particleColor ?? lineColor,
1534
+ "circle-opacity": ["coalesce", ["get", "opacity"], 1],
1535
+ "circle-radius": ["coalesce", ["get", "radius"], 2.8],
1536
+ "circle-blur": 0.18,
1537
+ "circle-stroke-color": "#ffffff",
1538
+ "circle-stroke-opacity": 0.45,
1539
+ "circle-stroke-width": 0.5
1540
+ }
1541
+ }
1542
+ ],
1543
+ startAnimation: ({ getOptions, setData }) => {
1544
+ let frameId = 0;
1545
+ const startTime = performance.now();
1546
+ const tick = (timestamp) => {
1547
+ setData(createCountermeasureBeamData(getOptions(), timestamp - startTime));
1548
+ frameId = requestAnimationFrame(tick);
1549
+ };
1550
+ frameId = requestAnimationFrame(tick);
1551
+ return () => cancelAnimationFrame(frameId);
1552
+ }
1553
+ };
1554
+ };
1555
+ const addCountermeasureBeam = (map, options) => createManagedEffect(map, options, createCountermeasureBeamRender);
1556
+ const getCoordinationColor = (type) => {
1557
+ if (type === "detection") return "#38bdf8";
1558
+ if (type === "command") return "#22d3ee";
1559
+ return "#f97316";
1560
+ };
1561
+ const createCoordinatedCountermeasureData = (options, elapsed = 0) => {
1562
+ const features = [];
1563
+ const nodeKeys = /* @__PURE__ */ new Set();
1564
+ const particleCount = Math.max(0, Math.floor(options.particleCount ?? 5));
1565
+ const particleDuration = Math.max(300, options.particleDuration ?? 1600);
1566
+ const phase = elapsed % particleDuration / particleDuration;
1567
+ const particleRadius = Math.max(1, options.particleRadius ?? 2.5);
1568
+ options.links.forEach((link, linkIndex) => {
1569
+ const color = link.color ?? getCoordinationColor(link.type);
1570
+ const distance = getDistance(link.from, link.to);
1571
+ const bearing = getBearing(link.from, link.to);
1572
+ features.push(
1573
+ createFeature(
1574
+ {
1575
+ type: "LineString",
1576
+ coordinates: [link.from, link.to]
1577
+ },
1578
+ {
1579
+ kind: "coordination-link",
1580
+ linkType: link.type,
1581
+ color
1582
+ }
1583
+ )
1584
+ );
1585
+ if (options.showNodes !== false) {
1586
+ [link.from, link.to].forEach((coordinate) => {
1587
+ const key = `${coordinate[0]},${coordinate[1]}`;
1588
+ if (nodeKeys.has(key)) return;
1589
+ nodeKeys.add(key);
1590
+ features.push(
1591
+ createFeature(
1592
+ { type: "Point", coordinates: coordinate },
1593
+ { kind: "coordination-node", color }
1594
+ )
1595
+ );
1596
+ });
1597
+ }
1598
+ if (distance <= 0) return;
1599
+ for (let index = 0; index < particleCount; index += 1) {
1600
+ const progress = (phase + index / particleCount + linkIndex / options.links.length) % 1;
1601
+ features.push(
1602
+ createFeature(
1603
+ {
1604
+ type: "Point",
1605
+ coordinates: getDestination(link.from, distance * progress, bearing)
1606
+ },
1607
+ {
1608
+ kind: "coordination-particle",
1609
+ color,
1610
+ opacity: 0.35 + progress * 0.65,
1611
+ radius: particleRadius
1612
+ }
1613
+ )
1614
+ );
1615
+ }
1616
+ });
1617
+ if (options.targetRipple !== false) {
1618
+ const duration = Math.max(300, options.targetRippleDuration ?? 1200);
1619
+ const progress = elapsed % duration / duration;
1620
+ const radius = Math.max(10, options.targetRippleRadius ?? 100) * progress;
1621
+ features.push(
1622
+ createFeature(createCircleLine(options.center, Math.max(2, radius)), {
1623
+ kind: "coordination-target-ripple",
1624
+ opacity: (1 - progress) * 0.9
1625
+ })
1626
+ );
1627
+ }
1628
+ return createFeatureCollection(features);
1629
+ };
1630
+ const createCoordinatedCountermeasureRender = (options) => {
1631
+ const lineWidth = Math.max(1, options.lineWidth ?? 2.2);
1632
+ const lineOpacity = options.lineOpacity ?? 0.9;
1633
+ return {
1634
+ data: createCoordinatedCountermeasureData(options),
1635
+ layers: [
1636
+ {
1637
+ id: `${options.id}-coordination-detection`,
1638
+ type: "line",
1639
+ filter: ["==", ["get", "linkType"], "detection"],
1640
+ paint: {
1641
+ "line-color": ["coalesce", ["get", "color"], "#38bdf8"],
1642
+ "line-opacity": lineOpacity,
1643
+ "line-width": lineWidth,
1644
+ "line-dasharray": [2, 2]
1645
+ }
1646
+ },
1647
+ {
1648
+ id: `${options.id}-coordination-command`,
1649
+ type: "line",
1650
+ filter: ["==", ["get", "linkType"], "command"],
1651
+ paint: {
1652
+ "line-color": ["coalesce", ["get", "color"], "#22d3ee"],
1653
+ "line-opacity": lineOpacity,
1654
+ "line-width": lineWidth,
1655
+ "line-dasharray": [0.5, 1.8]
1656
+ }
1657
+ },
1658
+ {
1659
+ id: `${options.id}-coordination-countermeasure`,
1660
+ type: "line",
1661
+ filter: ["==", ["get", "linkType"], "countermeasure"],
1662
+ paint: {
1663
+ "line-color": ["coalesce", ["get", "color"], "#f97316"],
1664
+ "line-opacity": lineOpacity,
1665
+ "line-width": lineWidth + 0.8
1666
+ }
1667
+ },
1668
+ {
1669
+ id: `${options.id}-coordination-target-ripple`,
1670
+ type: "line",
1671
+ filter: ["==", ["get", "kind"], "coordination-target-ripple"],
1672
+ paint: {
1673
+ "line-color": "#ff3b30",
1674
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.9],
1675
+ "line-width": 2.5
1676
+ }
1677
+ },
1678
+ {
1679
+ id: `${options.id}-coordination-node`,
1680
+ type: "circle",
1681
+ filter: ["==", ["get", "kind"], "coordination-node"],
1682
+ paint: {
1683
+ "circle-color": ["coalesce", ["get", "color"], "#ffffff"],
1684
+ "circle-radius": Math.max(2, options.nodeRadius ?? 5),
1685
+ "circle-stroke-color": "#ffffff",
1686
+ "circle-stroke-width": 1.5
1687
+ }
1688
+ },
1689
+ {
1690
+ id: `${options.id}-coordination-particle`,
1691
+ type: "circle",
1692
+ filter: ["==", ["get", "kind"], "coordination-particle"],
1693
+ paint: {
1694
+ "circle-color": ["coalesce", ["get", "color"], "#ffffff"],
1695
+ "circle-opacity": ["coalesce", ["get", "opacity"], 1],
1696
+ "circle-radius": ["coalesce", ["get", "radius"], 2.5],
1697
+ "circle-blur": 0.1
1698
+ }
1699
+ }
1700
+ ],
1701
+ startAnimation: ({ getOptions, setData }) => {
1702
+ let frameId = 0;
1703
+ const startTime = performance.now();
1704
+ const tick = (timestamp) => {
1705
+ setData(
1706
+ createCoordinatedCountermeasureData(
1707
+ getOptions(),
1708
+ timestamp - startTime
1709
+ )
1710
+ );
1711
+ frameId = requestAnimationFrame(tick);
1712
+ };
1713
+ frameId = requestAnimationFrame(tick);
1714
+ return () => cancelAnimationFrame(frameId);
1715
+ }
1716
+ };
1717
+ };
1718
+ const addCoordinatedCountermeasure = (map, options) => createManagedEffect(map, options, createCoordinatedCountermeasureRender);
1719
+ const createNavigationSpoofingData = (options, elapsed = 0) => {
1720
+ const features = [
1721
+ createFeature(
1722
+ {
1723
+ type: "LineString",
1724
+ coordinates: [options.center, options.spoofedPosition]
1725
+ },
1726
+ { kind: "spoofing-offset" }
1727
+ )
1728
+ ];
1729
+ const distance = getDistance(options.center, options.spoofedPosition);
1730
+ const bearing = getBearing(options.center, options.spoofedPosition);
1731
+ const particleCount = Math.max(0, Math.floor(options.particleCount ?? 10));
1732
+ const particleDuration = Math.max(300, options.particleDuration ?? 1800);
1733
+ const phase = elapsed % particleDuration / particleDuration;
1734
+ if (options.showTruePosition !== false) {
1735
+ features.push(
1736
+ createFeature(
1737
+ { type: "Point", coordinates: options.center },
1738
+ { kind: "spoofing-true-position" }
1739
+ )
1740
+ );
1741
+ }
1742
+ if (options.showSpoofedPosition !== false) {
1743
+ features.push(
1744
+ createFeature(
1745
+ { type: "Point", coordinates: options.spoofedPosition },
1746
+ { kind: "spoofing-ghost" }
1747
+ )
1748
+ );
1749
+ }
1750
+ if (distance > 0) {
1751
+ for (let index = 0; index < particleCount; index += 1) {
1752
+ const progress = (phase + index / particleCount) % 1;
1753
+ features.push(
1754
+ createFeature(
1755
+ {
1756
+ type: "Point",
1757
+ coordinates: getDestination(
1758
+ options.center,
1759
+ distance * progress,
1760
+ bearing
1761
+ )
1762
+ },
1763
+ {
1764
+ kind: "spoofing-particle",
1765
+ opacity: 0.25 + progress * 0.75
1766
+ }
1767
+ )
1768
+ );
1769
+ }
1770
+ }
1771
+ if (options.ripple !== false) {
1772
+ const count = Math.max(1, Math.floor(options.rippleCount ?? 2));
1773
+ const duration = Math.max(300, options.rippleDuration ?? 1200);
1774
+ const radius = Math.max(10, options.rippleRadius ?? 120);
1775
+ const ripplePhase = elapsed % duration / duration;
1776
+ for (let index = 0; index < count; index += 1) {
1777
+ const progress = (ripplePhase + index / count) % 1;
1778
+ features.push(
1779
+ createFeature(
1780
+ createCircleLine(
1781
+ options.spoofedPosition,
1782
+ Math.max(2, radius * progress)
1783
+ ),
1784
+ {
1785
+ kind: "spoofing-ripple",
1786
+ opacity: (1 - progress) * 0.75
1787
+ }
1788
+ )
1789
+ );
1790
+ }
1791
+ }
1792
+ return createFeatureCollection(features);
1793
+ };
1794
+ const createNavigationSpoofingRender = (options) => {
1795
+ const color = options.color ?? "#8b5cf6";
1796
+ return {
1797
+ data: createNavigationSpoofingData(options),
1798
+ layers: [
1799
+ {
1800
+ id: `${options.id}-spoofing-offset`,
1801
+ type: "line",
1802
+ filter: ["==", ["get", "kind"], "spoofing-offset"],
1803
+ layout: { "line-cap": "round" },
1804
+ paint: {
1805
+ "line-color": options.lineColor ?? color,
1806
+ "line-opacity": options.lineOpacity ?? 0.82,
1807
+ "line-width": Math.max(1, options.lineWidth ?? 2),
1808
+ "line-dasharray": [1.2, 2]
1809
+ }
1810
+ },
1811
+ {
1812
+ id: `${options.id}-spoofing-ripple`,
1813
+ type: "line",
1814
+ filter: ["==", ["get", "kind"], "spoofing-ripple"],
1815
+ paint: {
1816
+ "line-color": color,
1817
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.75],
1818
+ "line-width": 2
1819
+ }
1820
+ },
1821
+ {
1822
+ id: `${options.id}-spoofing-true-position`,
1823
+ type: "circle",
1824
+ filter: ["==", ["get", "kind"], "spoofing-true-position"],
1825
+ paint: {
1826
+ "circle-color": "#ffffff",
1827
+ "circle-radius": 5,
1828
+ "circle-stroke-color": color,
1829
+ "circle-stroke-width": 2
1830
+ }
1831
+ },
1832
+ {
1833
+ id: `${options.id}-spoofing-ghost`,
1834
+ type: "circle",
1835
+ filter: ["==", ["get", "kind"], "spoofing-ghost"],
1836
+ paint: {
1837
+ "circle-color": color,
1838
+ "circle-opacity": options.ghostOpacity ?? 0.42,
1839
+ "circle-radius": Math.max(2, options.ghostRadius ?? 8),
1840
+ "circle-stroke-color": "#ffffff",
1841
+ "circle-stroke-opacity": 0.8,
1842
+ "circle-stroke-width": 1.5
1843
+ }
1844
+ },
1845
+ {
1846
+ id: `${options.id}-spoofing-particle`,
1847
+ type: "circle",
1848
+ filter: ["==", ["get", "kind"], "spoofing-particle"],
1849
+ paint: {
1850
+ "circle-color": color,
1851
+ "circle-opacity": ["coalesce", ["get", "opacity"], 1],
1852
+ "circle-radius": Math.max(1, options.particleRadius ?? 2.5),
1853
+ "circle-blur": 0.12
1854
+ }
1855
+ }
1856
+ ],
1857
+ startAnimation: ({ getOptions, setData }) => {
1858
+ let frameId = 0;
1859
+ const startTime = performance.now();
1860
+ const tick = (timestamp) => {
1861
+ setData(createNavigationSpoofingData(getOptions(), timestamp - startTime));
1862
+ frameId = requestAnimationFrame(tick);
1863
+ };
1864
+ frameId = requestAnimationFrame(tick);
1865
+ return () => cancelAnimationFrame(frameId);
1866
+ }
1867
+ };
1868
+ };
1869
+ const addNavigationSpoofing = (map, options) => createManagedEffect(map, options, createNavigationSpoofingRender);
1870
+ const addTargetLock = (map, initialOptions) => {
1871
+ const effectId = initialOptions.id;
1872
+ let options = { ...initialOptions };
1873
+ let removed = false;
1874
+ let rotationAnimation;
1875
+ const element = document.createElement("div");
1876
+ const frame = document.createElement("div");
1877
+ const corners = Array.from(
1878
+ { length: 4 },
1879
+ () => document.createElement("div")
1880
+ );
1881
+ const crosshairArms = Array.from(
1882
+ { length: 4 },
1883
+ () => document.createElement("div")
1884
+ );
1885
+ element.dataset.effectId = effectId;
1886
+ element.setAttribute("aria-hidden", "true");
1887
+ element.style.position = "absolute";
1888
+ element.style.pointerEvents = "none";
1889
+ frame.style.position = "absolute";
1890
+ frame.style.inset = "0";
1891
+ element.appendChild(frame);
1892
+ corners.forEach((corner) => {
1893
+ corner.style.position = "absolute";
1894
+ corner.style.boxSizing = "border-box";
1895
+ frame.appendChild(corner);
1896
+ });
1897
+ crosshairArms.forEach((arm) => {
1898
+ arm.style.position = "absolute";
1899
+ element.appendChild(arm);
1900
+ });
1901
+ const marker = new maplibregl.Marker({ element, anchor: "center" }).setLngLat(options.center).addTo(map);
1902
+ const render = () => {
1903
+ const color = options.color ?? "#ff3b30";
1904
+ const size = Math.max(24, options.size ?? 72);
1905
+ const lineWidth = Math.max(1, options.lineWidth ?? 2);
1906
+ const cornerLength = Math.min(
1907
+ size / 2,
1908
+ Math.max(lineWidth * 2, options.cornerLength ?? 18)
1909
+ );
1910
+ const crosshairLength = Math.max(2, options.crosshairLength ?? 10);
1911
+ const crosshairGap = Math.max(0, options.crosshairGap ?? 5);
1912
+ marker.setLngLat(options.center);
1913
+ element.style.width = `${size}px`;
1914
+ element.style.height = `${size}px`;
1915
+ element.style.display = options.visible === false ? "none" : "block";
1916
+ element.style.opacity = `${Math.min(1, Math.max(0, options.opacity ?? 1))}`;
1917
+ corners.forEach((corner) => {
1918
+ corner.style.width = `${cornerLength}px`;
1919
+ corner.style.height = `${cornerLength}px`;
1920
+ corner.style.border = "0";
1921
+ });
1922
+ Object.assign(corners[0].style, {
1923
+ left: "0",
1924
+ top: "0",
1925
+ borderLeft: `${lineWidth}px solid ${color}`,
1926
+ borderTop: `${lineWidth}px solid ${color}`
1927
+ });
1928
+ Object.assign(corners[1].style, {
1929
+ right: "0",
1930
+ top: "0",
1931
+ borderRight: `${lineWidth}px solid ${color}`,
1932
+ borderTop: `${lineWidth}px solid ${color}`
1933
+ });
1934
+ Object.assign(corners[2].style, {
1935
+ right: "0",
1936
+ bottom: "0",
1937
+ borderRight: `${lineWidth}px solid ${color}`,
1938
+ borderBottom: `${lineWidth}px solid ${color}`
1939
+ });
1940
+ Object.assign(corners[3].style, {
1941
+ left: "0",
1942
+ bottom: "0",
1943
+ borderLeft: `${lineWidth}px solid ${color}`,
1944
+ borderBottom: `${lineWidth}px solid ${color}`
1945
+ });
1946
+ crosshairArms.forEach((arm) => {
1947
+ arm.style.display = options.showCrosshair === false ? "none" : "block";
1948
+ arm.style.background = color;
1949
+ });
1950
+ Object.assign(crosshairArms[0].style, {
1951
+ width: `${lineWidth}px`,
1952
+ height: `${crosshairLength}px`,
1953
+ left: "50%",
1954
+ bottom: `calc(50% + ${crosshairGap}px)`,
1955
+ transform: "translateX(-50%)"
1956
+ });
1957
+ Object.assign(crosshairArms[1].style, {
1958
+ width: `${lineWidth}px`,
1959
+ height: `${crosshairLength}px`,
1960
+ left: "50%",
1961
+ top: `calc(50% + ${crosshairGap}px)`,
1962
+ transform: "translateX(-50%)"
1963
+ });
1964
+ Object.assign(crosshairArms[2].style, {
1965
+ width: `${crosshairLength}px`,
1966
+ height: `${lineWidth}px`,
1967
+ right: `calc(50% + ${crosshairGap}px)`,
1968
+ top: "50%",
1969
+ transform: "translateY(-50%)"
1970
+ });
1971
+ Object.assign(crosshairArms[3].style, {
1972
+ width: `${crosshairLength}px`,
1973
+ height: `${lineWidth}px`,
1974
+ left: `calc(50% + ${crosshairGap}px)`,
1975
+ top: "50%",
1976
+ transform: "translateY(-50%)"
1977
+ });
1978
+ rotationAnimation == null ? void 0 : rotationAnimation.cancel();
1979
+ rotationAnimation = void 0;
1980
+ frame.style.transform = "rotate(0deg)";
1981
+ if (options.rotate !== false) {
1982
+ rotationAnimation = frame.animate(
1983
+ [{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
1984
+ {
1985
+ duration: Math.max(300, options.rotationDuration ?? 2400),
1986
+ iterations: Infinity
1987
+ }
1988
+ );
1989
+ }
1990
+ };
1991
+ render();
1992
+ return {
1993
+ id: effectId,
1994
+ update(nextOptions) {
1995
+ if (removed) return;
1996
+ options = {
1997
+ ...options,
1998
+ ...nextOptions,
1999
+ id: effectId
2000
+ };
2001
+ render();
2002
+ },
2003
+ show() {
2004
+ if (removed) return;
2005
+ options = { ...options, visible: true };
2006
+ element.style.display = "block";
2007
+ },
2008
+ hide() {
2009
+ if (removed) return;
2010
+ options = { ...options, visible: false };
2011
+ element.style.display = "none";
2012
+ },
2013
+ remove() {
2014
+ if (removed) return;
2015
+ removed = true;
2016
+ rotationAnimation == null ? void 0 : rotationAnimation.cancel();
2017
+ rotationAnimation = void 0;
2018
+ marker.remove();
2019
+ }
2020
+ };
2021
+ };
2022
+ const toFencePolygons = (fence) => {
2023
+ if (!fence) {
2024
+ return [];
2025
+ }
2026
+ if (Array.isArray(fence)) {
2027
+ return [{ type: "Polygon", coordinates: fence }];
2028
+ }
2029
+ switch (fence.type) {
2030
+ case "Polygon":
2031
+ return [fence];
2032
+ case "MultiPolygon":
2033
+ return fence.coordinates.map((coordinates) => ({
2034
+ type: "Polygon",
2035
+ coordinates
2036
+ }));
2037
+ case "Feature": {
2038
+ const geometry = fence.geometry;
2039
+ if ((geometry == null ? void 0 : geometry.type) === "Polygon") {
2040
+ return [geometry];
2041
+ }
2042
+ if ((geometry == null ? void 0 : geometry.type) === "MultiPolygon") {
2043
+ return geometry.coordinates.map((coordinates) => ({
2044
+ type: "Polygon",
2045
+ coordinates
2046
+ }));
2047
+ }
2048
+ return [];
2049
+ }
2050
+ case "FeatureCollection": {
2051
+ const polygons = [];
2052
+ for (const feature of fence.features) {
2053
+ const geometry = feature.geometry;
2054
+ if ((geometry == null ? void 0 : geometry.type) === "Polygon") {
2055
+ polygons.push(geometry);
2056
+ } else if ((geometry == null ? void 0 : geometry.type) === "MultiPolygon") {
2057
+ polygons.push(
2058
+ ...geometry.coordinates.map((coordinates) => ({
2059
+ type: "Polygon",
2060
+ coordinates
2061
+ }))
2062
+ );
2063
+ }
2064
+ }
2065
+ return polygons;
2066
+ }
2067
+ default:
2068
+ return [];
2069
+ }
2070
+ };
2071
+ const createFenceCircle = (center, radius, segmentCount) => createCirclePolygon(center, radius, segmentCount);
2072
+ const createRippleData = (center, radius, count, duration, width, elapsed) => {
2073
+ const baseOpacity = 0.95;
2074
+ const features = [];
2075
+ for (let index = 0; index < count; index += 1) {
2076
+ const phase = (elapsed / duration + index / count) % 1;
2077
+ const currentRadius = phase * radius;
2078
+ if (currentRadius < 1) {
2079
+ continue;
2080
+ }
2081
+ features.push(
2082
+ createFeature(createCircleLine(center, currentRadius), {
2083
+ kind: "ring",
2084
+ opacity: baseOpacity * (1 - phase),
2085
+ width: width * (1 - phase * 0.3)
2086
+ })
2087
+ );
2088
+ }
2089
+ return createFeatureCollection(features);
2090
+ };
2091
+ const addBreachAlert = (map, initialOptions) => {
2092
+ const effectId = initialOptions.id;
2093
+ const fenceSourceId = `${effectId}-fence-source`;
2094
+ const fenceFillId = `${effectId}-fence-fill`;
2095
+ const fenceLineId = `${effectId}-fence-line`;
2096
+ const rippleSourceId = `${effectId}-ripple-source`;
2097
+ const rippleLineId = `${effectId}-ripple-line`;
2098
+ let options = { ...initialOptions };
2099
+ let removed = false;
2100
+ let waitingForStyle = false;
2101
+ let fenceReady = false;
2102
+ let active = false;
2103
+ let fenceFlashTimer;
2104
+ let rippleFrameId = 0;
2105
+ let tacticalTimer;
2106
+ let tacticalBox;
2107
+ const resolveFenceColor = () => options.fenceColor ?? "#1677ff";
2108
+ const resolveFenceFillColor = () => options.fenceFillColor ?? resolveFenceColor();
2109
+ const resolveFenceFillOpacity = () => options.fenceFillOpacity ?? 0.1;
2110
+ const resolveFenceBorderWidth = () => options.fenceBorderWidth ?? 2;
2111
+ const resolveFenceBorderOpacity = () => options.fenceBorderOpacity ?? 0.9;
2112
+ const resolveAlertColor = () => options.fenceAlertColor ?? "#ff3b30";
2113
+ const resolveRippleColor = () => options.rippleColor ?? resolveAlertColor();
2114
+ const resolveTacticalColor = () => options.tacticalColor ?? resolveAlertColor();
2115
+ const setRippleData = (data) => {
2116
+ const source = map.getSource(rippleSourceId);
2117
+ if (source) {
2118
+ source.setData(data);
2119
+ }
2120
+ };
2121
+ const clearRipple = () => {
2122
+ if (rippleFrameId) {
2123
+ cancelAnimationFrame(rippleFrameId);
2124
+ rippleFrameId = 0;
2125
+ }
2126
+ setRippleData(createFeatureCollection([]));
2127
+ };
2128
+ const startRipple = (center) => {
2129
+ const radius = Math.max(20, options.rippleRadius ?? 800);
2130
+ const count = Math.max(1, options.rippleCount ?? 3);
2131
+ const duration = Math.max(400, options.rippleDuration ?? 1500);
2132
+ const width = Math.max(1, options.rippleLineWidth ?? 3);
2133
+ const startTime = performance.now();
2134
+ const tick = (timestamp) => {
2135
+ const elapsed = timestamp - startTime;
2136
+ setRippleData(
2137
+ createRippleData(center, radius, count, duration, width, elapsed)
2138
+ );
2139
+ rippleFrameId = requestAnimationFrame(tick);
2140
+ };
2141
+ rippleFrameId = requestAnimationFrame(tick);
2142
+ };
2143
+ const restoreFencePaint = () => {
2144
+ if (!fenceReady) {
2145
+ return;
2146
+ }
2147
+ const color = resolveFenceColor();
2148
+ const fillColor = resolveFenceFillColor();
2149
+ if (map.getLayer(fenceFillId)) {
2150
+ map.setPaintProperty(fenceFillId, "fill-color", fillColor);
2151
+ map.setPaintProperty(fenceFillId, "fill-opacity", resolveFenceFillOpacity());
2152
+ }
2153
+ if (map.getLayer(fenceLineId)) {
2154
+ map.setPaintProperty(fenceLineId, "line-color", color);
2155
+ map.setPaintProperty(fenceLineId, "line-width", resolveFenceBorderWidth());
2156
+ map.setPaintProperty(
2157
+ fenceLineId,
2158
+ "line-opacity",
2159
+ resolveFenceBorderOpacity()
2160
+ );
2161
+ }
2162
+ };
2163
+ const stopFenceFlash = () => {
2164
+ if (fenceFlashTimer) {
2165
+ clearInterval(fenceFlashTimer);
2166
+ fenceFlashTimer = void 0;
2167
+ }
2168
+ };
2169
+ const startFenceFlash = () => {
2170
+ if (!fenceReady) {
2171
+ return;
2172
+ }
2173
+ const alertColor = resolveAlertColor();
2174
+ const interval = Math.max(60, options.fenceFlashInterval ?? 200);
2175
+ let on = true;
2176
+ if (map.getLayer(fenceFillId)) {
2177
+ map.setPaintProperty(fenceFillId, "fill-color", alertColor);
2178
+ map.setPaintProperty(fenceFillId, "fill-opacity", 0.42);
2179
+ }
2180
+ if (map.getLayer(fenceLineId)) {
2181
+ map.setPaintProperty(fenceLineId, "line-color", alertColor);
2182
+ map.setPaintProperty(fenceLineId, "line-opacity", 1);
2183
+ }
2184
+ fenceFlashTimer = setInterval(() => {
2185
+ on = !on;
2186
+ const fillOpacity = on ? 0.42 : 0.08;
2187
+ const lineOpacity = on ? 1 : 0.4;
2188
+ if (map.getLayer(fenceFillId)) {
2189
+ map.setPaintProperty(fenceFillId, "fill-opacity", fillOpacity);
2190
+ }
2191
+ if (map.getLayer(fenceLineId)) {
2192
+ map.setPaintProperty(fenceLineId, "line-opacity", lineOpacity);
2193
+ }
2194
+ }, interval);
2195
+ };
2196
+ const removeTacticalBox = () => {
2197
+ if (tacticalTimer) {
2198
+ clearInterval(tacticalTimer);
2199
+ tacticalTimer = void 0;
2200
+ }
2201
+ if (tacticalBox) {
2202
+ tacticalBox.remove();
2203
+ tacticalBox = void 0;
2204
+ }
2205
+ };
2206
+ const startTactical = () => {
2207
+ const color = resolveTacticalColor();
2208
+ const width = Math.max(1, options.tacticalBorderWidth ?? 4);
2209
+ const inset = options.tacticalScope === "viewport" ? 12 : 0;
2210
+ const interval = Math.max(60, options.fenceFlashInterval ?? 200);
2211
+ const container = map.getContainer();
2212
+ const box = document.createElement("div");
2213
+ box.style.position = "absolute";
2214
+ box.style.inset = `${inset}px`;
2215
+ box.style.border = `${width}px solid ${color}`;
2216
+ box.style.boxShadow = `inset 0 0 ${width * 6}px ${color}`;
2217
+ box.style.pointerEvents = "none";
2218
+ box.style.zIndex = "5";
2219
+ box.style.opacity = "1";
2220
+ container.appendChild(box);
2221
+ tacticalBox = box;
2222
+ let on = true;
2223
+ tacticalTimer = setInterval(() => {
2224
+ on = !on;
2225
+ box.style.opacity = on ? "1" : "0.15";
2226
+ }, interval);
2227
+ };
2228
+ const ensureFenceLayers = () => {
2229
+ const polygons = toFencePolygons(options.fence);
2230
+ if (polygons.length === 0) {
2231
+ return;
2232
+ }
2233
+ const data = createFeatureCollection(
2234
+ polygons.map((polygon) => createFeature(polygon, { kind: "fence" }))
2235
+ );
2236
+ if (!fenceReady) {
2237
+ if (!map.getSource(fenceSourceId)) {
2238
+ map.addSource(fenceSourceId, {
2239
+ type: "geojson",
2240
+ data
2241
+ });
2242
+ } else {
2243
+ map.getSource(fenceSourceId).setData(data);
2244
+ }
2245
+ if (!map.getLayer(fenceFillId)) {
2246
+ map.addLayer(
2247
+ {
2248
+ id: fenceFillId,
2249
+ type: "fill",
2250
+ source: fenceSourceId,
2251
+ filter: ["==", ["get", "kind"], "fence"],
2252
+ paint: {
2253
+ "fill-color": resolveFenceFillColor(),
2254
+ "fill-opacity": resolveFenceFillOpacity()
2255
+ }
2256
+ },
2257
+ options.beforeId
2258
+ );
2259
+ }
2260
+ if (!map.getLayer(fenceLineId)) {
2261
+ map.addLayer(
2262
+ {
2263
+ id: fenceLineId,
2264
+ type: "line",
2265
+ source: fenceSourceId,
2266
+ filter: ["==", ["get", "kind"], "fence"],
2267
+ paint: {
2268
+ "line-color": resolveFenceColor(),
2269
+ "line-width": resolveFenceBorderWidth(),
2270
+ "line-opacity": resolveFenceBorderOpacity()
2271
+ }
2272
+ },
2273
+ options.beforeId
2274
+ );
2275
+ }
2276
+ fenceReady = true;
2277
+ setLayersVisibility(
2278
+ map,
2279
+ [fenceFillId, fenceLineId],
2280
+ options.visible !== false
2281
+ );
2282
+ } else {
2283
+ const source = map.getSource(fenceSourceId);
2284
+ if (source) {
2285
+ source.setData(data);
2286
+ }
2287
+ restoreFencePaint();
2288
+ }
2289
+ };
2290
+ const ensureRippleSource = () => {
2291
+ if (!map.getSource(rippleSourceId)) {
2292
+ map.addSource(rippleSourceId, {
2293
+ type: "geojson",
2294
+ data: createFeatureCollection([])
2295
+ });
2296
+ }
2297
+ if (!map.getLayer(rippleLineId)) {
2298
+ map.addLayer(
2299
+ {
2300
+ id: rippleLineId,
2301
+ type: "line",
2302
+ source: rippleSourceId,
2303
+ filter: ["==", ["get", "kind"], "ring"],
2304
+ paint: {
2305
+ "line-color": resolveRippleColor(),
2306
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.9],
2307
+ "line-width": ["coalesce", ["get", "width"], 3]
2308
+ }
2309
+ },
2310
+ options.beforeId
2311
+ );
2312
+ setLayersVisibility(map, [rippleLineId], options.visible !== false);
2313
+ } else if (map.getLayer(rippleLineId)) {
2314
+ map.setPaintProperty(rippleLineId, "line-color", resolveRippleColor());
2315
+ }
2316
+ };
2317
+ const handleStyleData = () => {
2318
+ if (!waitingForStyle || removed) {
2319
+ return;
2320
+ }
2321
+ renderNow();
2322
+ };
2323
+ const renderNow = () => {
2324
+ if (removed) {
2325
+ return;
2326
+ }
2327
+ try {
2328
+ ensureFenceLayers();
2329
+ ensureRippleSource();
2330
+ waitingForStyle = false;
2331
+ map.off("styledata", handleStyleData);
2332
+ } catch (error) {
2333
+ if (!isStyleNotReadyError$1(error)) {
2334
+ throw error;
2335
+ }
2336
+ if (!waitingForStyle) {
2337
+ waitingForStyle = true;
2338
+ map.on("styledata", handleStyleData);
2339
+ }
2340
+ }
2341
+ };
2342
+ const stopAll = () => {
2343
+ stopFenceFlash();
2344
+ clearRipple();
2345
+ removeTacticalBox();
2346
+ restoreFencePaint();
2347
+ };
2348
+ renderNow();
2349
+ return {
2350
+ id: effectId,
2351
+ update(nextOptions) {
2352
+ if (removed) {
2353
+ return;
2354
+ }
2355
+ options = {
2356
+ ...options,
2357
+ ...nextOptions,
2358
+ id: effectId
2359
+ };
2360
+ renderNow();
2361
+ },
2362
+ show() {
2363
+ if (removed) {
2364
+ return;
2365
+ }
2366
+ options = { ...options, visible: true };
2367
+ setLayersVisibility(map, [fenceFillId, fenceLineId, rippleLineId], true);
2368
+ },
2369
+ hide() {
2370
+ if (removed) {
2371
+ return;
2372
+ }
2373
+ options = { ...options, visible: false };
2374
+ setLayersVisibility(map, [fenceFillId, fenceLineId, rippleLineId], false);
2375
+ },
2376
+ remove() {
2377
+ if (removed) {
2378
+ return;
2379
+ }
2380
+ removed = true;
2381
+ if (waitingForStyle) {
2382
+ waitingForStyle = false;
2383
+ map.off("styledata", handleStyleData);
2384
+ }
2385
+ stopAll();
2386
+ removeLayerIfExists(map, rippleLineId);
2387
+ removeLayerIfExists(map, fenceLineId);
2388
+ removeLayerIfExists(map, fenceFillId);
2389
+ removeSourceIfExists(map, rippleSourceId);
2390
+ removeSourceIfExists(map, fenceSourceId);
2391
+ fenceReady = false;
2392
+ },
2393
+ trigger(point) {
2394
+ if (removed) {
2395
+ return;
2396
+ }
2397
+ if (active) {
2398
+ stopAll();
2399
+ }
2400
+ const center = point ?? options.center;
2401
+ active = true;
2402
+ if (options.fenceFlash !== false) {
2403
+ startFenceFlash();
2404
+ }
2405
+ if (options.ripple !== false) {
2406
+ startRipple(center);
2407
+ }
2408
+ if (options.tacticalFlash) {
2409
+ startTactical();
2410
+ }
2411
+ },
2412
+ reset() {
2413
+ if (removed) {
2414
+ return;
2415
+ }
2416
+ stopAll();
2417
+ active = false;
2418
+ },
2419
+ isActive() {
2420
+ return active;
2421
+ }
2422
+ };
2423
+ };
2424
+ const FENCE_ID_PROPERTY = "__electronic_fence_id";
2425
+ const clone = (value) => JSON.parse(JSON.stringify(value));
2426
+ const isSamePosition = (a, b) => a[0] === b[0] && a[1] === b[1];
2427
+ const normalizeRing = (ring) => {
2428
+ const coordinates = ring.map((position) => {
2429
+ const lng = Number(position[0]);
2430
+ const lat = Number(position[1]);
2431
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
2432
+ throw new Error("电子围栏坐标必须是有限数字。");
2433
+ }
2434
+ return [lng, lat];
2435
+ });
2436
+ if (coordinates.length < 3) {
2437
+ throw new Error("电子围栏的每个环至少需要 3 个不同坐标。");
2438
+ }
2439
+ if (!isSamePosition(coordinates[0], coordinates[coordinates.length - 1])) {
2440
+ coordinates.push([...coordinates[0]]);
2441
+ }
2442
+ if (coordinates.length < 4) {
2443
+ throw new Error("电子围栏 Polygon 环无效。");
2444
+ }
2445
+ return coordinates;
2446
+ };
2447
+ const normalizeFeature = (feature) => {
2448
+ if (typeof feature.id !== "string" || !feature.id.trim()) {
2449
+ throw new Error("电子围栏必须提供非空字符串 id。");
2450
+ }
2451
+ if (!feature.geometry || feature.geometry.type !== "Polygon") {
2452
+ throw new Error(`电子围栏 ${feature.id} 仅支持 Polygon geometry。`);
2453
+ }
2454
+ const properties = clone(feature.properties || {});
2455
+ return {
2456
+ type: "Feature",
2457
+ id: feature.id,
2458
+ geometry: {
2459
+ type: "Polygon",
2460
+ coordinates: feature.geometry.coordinates.map(normalizeRing)
2461
+ },
2462
+ properties
2463
+ };
2464
+ };
2465
+ const mergeFeaturePatch = (feature, patch) => {
2466
+ const currentProperties = feature.properties || {};
2467
+ const nextProperties = patch.properties || {};
2468
+ const nextStyle = nextProperties.style ? {
2469
+ ...currentProperties.style || {},
2470
+ ...nextProperties.style
2471
+ } : currentProperties.style;
2472
+ return normalizeFeature({
2473
+ ...clone(feature),
2474
+ geometry: patch.geometry ? clone(patch.geometry) : clone(feature.geometry),
2475
+ properties: {
2476
+ ...clone(currentProperties),
2477
+ ...clone(nextProperties),
2478
+ ...nextStyle ? { style: nextStyle } : {}
2479
+ }
2480
+ });
2481
+ };
2482
+ class ElectronicFenceStore {
2483
+ constructor() {
2484
+ this.features = /* @__PURE__ */ new Map();
2485
+ this.states = /* @__PURE__ */ new Map();
2486
+ this.activeIds = /* @__PURE__ */ new Set();
2487
+ this.hiddenIds = /* @__PURE__ */ new Set();
2488
+ }
2489
+ setData(data) {
2490
+ const nextFeatures = /* @__PURE__ */ new Map();
2491
+ data.features.forEach((feature) => {
2492
+ const normalized = normalizeFeature(
2493
+ feature
2494
+ );
2495
+ if (nextFeatures.has(normalized.id)) {
2496
+ throw new Error(`电子围栏 id 重复:${normalized.id}`);
2497
+ }
2498
+ nextFeatures.set(normalized.id, normalized);
2499
+ });
2500
+ this.features = nextFeatures;
2501
+ this.states.clear();
2502
+ this.activeIds.clear();
2503
+ this.hiddenIds.clear();
2504
+ }
2505
+ add(feature) {
2506
+ const normalized = normalizeFeature(feature);
2507
+ if (this.features.has(normalized.id)) {
2508
+ throw new Error(`电子围栏 id 已存在:${normalized.id}`);
2509
+ }
2510
+ this.features.set(normalized.id, normalized);
2511
+ return normalized.id;
2512
+ }
2513
+ update(id, patch) {
2514
+ const feature = this.requireFeature(id);
2515
+ this.features.set(id, mergeFeaturePatch(feature, patch));
2516
+ }
2517
+ remove(id) {
2518
+ this.requireFeature(id);
2519
+ this.features.delete(id);
2520
+ this.states.delete(id);
2521
+ this.activeIds.delete(id);
2522
+ this.hiddenIds.delete(id);
2523
+ }
2524
+ clear() {
2525
+ this.features.clear();
2526
+ this.states.clear();
2527
+ this.activeIds.clear();
2528
+ this.hiddenIds.clear();
2529
+ }
2530
+ setActive(id, active) {
2531
+ this.requireFeature(id);
2532
+ if (active) {
2533
+ this.activeIds.add(id);
2534
+ } else {
2535
+ this.activeIds.delete(id);
2536
+ }
2537
+ }
2538
+ setState(id, state) {
2539
+ this.requireFeature(id);
2540
+ if (state !== "normal" && state !== "warning" && state !== "alarm" && state !== "disabled") {
2541
+ throw new Error(`电子围栏 ${id} 的渲染状态无效。`);
2542
+ }
2543
+ if (state === "normal") {
2544
+ this.states.delete(id);
2545
+ } else {
2546
+ this.states.set(id, state);
2547
+ }
2548
+ }
2549
+ setVisible(id, visible) {
2550
+ this.requireFeature(id);
2551
+ if (visible) {
2552
+ this.hiddenIds.delete(id);
2553
+ } else {
2554
+ this.hiddenIds.add(id);
2555
+ }
2556
+ }
2557
+ isActive(id) {
2558
+ return this.activeIds.has(id);
2559
+ }
2560
+ getState(id) {
2561
+ this.requireFeature(id);
2562
+ return this.states.get(id) || "normal";
2563
+ }
2564
+ get(id) {
2565
+ const feature = this.features.get(id);
2566
+ return feature ? clone(feature) : void 0;
2567
+ }
2568
+ values() {
2569
+ return Array.from(this.features.values());
2570
+ }
2571
+ visibleValues() {
2572
+ return this.values().filter((feature) => !this.hiddenIds.has(feature.id));
2573
+ }
2574
+ requireFeature(id) {
2575
+ const feature = this.features.get(id);
2576
+ if (!feature) {
2577
+ throw new Error(`电子围栏不存在:${id}`);
2578
+ }
2579
+ return feature;
2580
+ }
2581
+ }
2582
+ const resolveFiniteNumber = (value, fallback, name, positive = false) => {
2583
+ const resolved = value === void 0 ? fallback : Number(value);
2584
+ if (!Number.isFinite(resolved) || positive && resolved <= 0) {
2585
+ throw new Error(`${name} 必须是${positive ? "大于 0 的" : "有限"}数字。`);
2586
+ }
2587
+ return resolved;
2588
+ };
2589
+ const validateFenceDefaults = (id, defaults) => {
2590
+ if (!id.trim()) {
2591
+ throw new Error("电子围栏插件 id 不能为空。");
2592
+ }
2593
+ resolveFiniteNumber(defaults.baseHeight, 0, "defaults.baseHeight");
2594
+ resolveFiniteNumber(defaults.height, 0, "defaults.height", true);
2595
+ resolveFiniteNumber(defaults.thickness, 1, "defaults.thickness", true);
2596
+ };
2597
+ const resolveFenceDimensions = (defaults) => ({
2598
+ baseHeight: resolveFiniteNumber(
2599
+ defaults.baseHeight,
2600
+ 0,
2601
+ "defaults.baseHeight"
2602
+ ),
2603
+ height: resolveFiniteNumber(
2604
+ defaults.height,
2605
+ 0,
2606
+ "defaults.height",
2607
+ true
2608
+ ),
2609
+ thickness: resolveFiniteNumber(
2610
+ defaults.thickness,
2611
+ 1,
2612
+ "defaults.thickness",
2613
+ true
2614
+ )
2615
+ });
2616
+ const resolveFenceVisualState = (status, active) => {
2617
+ if (status !== "normal") {
2618
+ return status;
2619
+ }
2620
+ return active ? "active" : "normal";
2621
+ };
2622
+ const resolveFenceStyle = (options) => {
2623
+ var _a, _b;
2624
+ const normal = {
2625
+ ...options.defaults.normal,
2626
+ ...((_a = options.overrides) == null ? void 0 : _a.normal) || {}
2627
+ };
2628
+ const stateStyle = options.state === "normal" ? {} : {
2629
+ ...options.defaults[options.state],
2630
+ ...((_b = options.overrides) == null ? void 0 : _b[options.state]) || {}
2631
+ };
2632
+ return {
2633
+ ...normal,
2634
+ ...stateStyle
2635
+ };
2636
+ };
2637
+ const createFenceSegments = (geometry, thickness) => {
2638
+ const halfThickness = thickness / 2;
2639
+ const segments = [];
2640
+ geometry.coordinates.forEach((ring) => {
2641
+ const rawSegments = [];
2642
+ for (let index = 0; index < ring.length - 1; index += 1) {
2643
+ const start = [ring[index][0], ring[index][1]];
2644
+ const end = [ring[index + 1][0], ring[index + 1][1]];
2645
+ const length = getDistance(start, end);
2646
+ if (length < 0.01) {
2647
+ continue;
2648
+ }
2649
+ rawSegments.push({
2650
+ start,
2651
+ end,
2652
+ length,
2653
+ bearing: getBearing(start, end)
2654
+ });
2655
+ }
2656
+ const totalLength = rawSegments.reduce(
2657
+ (sum, segment) => sum + segment.length,
2658
+ 0
2659
+ );
2660
+ let travelled = 0;
2661
+ rawSegments.forEach((segment) => {
2662
+ const extendedStart = getDestination(
2663
+ segment.start,
2664
+ halfThickness,
2665
+ segment.bearing + 180
2666
+ );
2667
+ const extendedEnd = getDestination(
2668
+ segment.end,
2669
+ halfThickness,
2670
+ segment.bearing
2671
+ );
2672
+ segments.push({
2673
+ startLeft: getDestination(
2674
+ extendedStart,
2675
+ halfThickness,
2676
+ segment.bearing - 90
2677
+ ),
2678
+ startRight: getDestination(
2679
+ extendedStart,
2680
+ halfThickness,
2681
+ segment.bearing + 90
2682
+ ),
2683
+ endLeft: getDestination(
2684
+ extendedEnd,
2685
+ halfThickness,
2686
+ segment.bearing - 90
2687
+ ),
2688
+ endRight: getDestination(
2689
+ extendedEnd,
2690
+ halfThickness,
2691
+ segment.bearing + 90
2692
+ ),
2693
+ startProgress: totalLength ? travelled / totalLength : 0,
2694
+ endProgress: totalLength ? (travelled + segment.length) / totalLength : 1
2695
+ });
2696
+ travelled += segment.length;
2697
+ });
2698
+ });
2699
+ return segments;
2700
+ };
2701
+ const createFenceWallMultiPolygon = (segments) => ({
2702
+ type: "MultiPolygon",
2703
+ coordinates: segments.map((segment) => [
2704
+ [
2705
+ segment.startLeft,
2706
+ segment.endLeft,
2707
+ segment.endRight,
2708
+ segment.startRight,
2709
+ segment.startLeft
2710
+ ]
2711
+ ])
2712
+ });
2713
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
2714
+ const parseFenceColor = (value) => {
2715
+ const color = value.trim().toLowerCase();
2716
+ if (color.startsWith("#")) {
2717
+ const hex = color.slice(1);
2718
+ const expanded = hex.length === 3 || hex.length === 4 ? hex.split("").map((character) => character + character).join("") : hex;
2719
+ if (expanded.length === 6 || expanded.length === 8) {
2720
+ const number = Number.parseInt(expanded, 16);
2721
+ if (Number.isFinite(number)) {
2722
+ return [
2723
+ (number >> (expanded.length === 8 ? 24 : 16) & 255) / 255,
2724
+ (number >> (expanded.length === 8 ? 16 : 8) & 255) / 255,
2725
+ (number >> (expanded.length === 8 ? 8 : 0) & 255) / 255,
2726
+ expanded.length === 8 ? (number & 255) / 255 : 1
2727
+ ];
2728
+ }
2729
+ }
2730
+ }
2731
+ const rgbMatch = color.match(/^rgba?\(([^)]+)\)$/);
2732
+ if (rgbMatch) {
2733
+ const parts = rgbMatch[1].split(",").map((part) => Number(part.trim()));
2734
+ if (parts.length >= 3 && parts.every((part) => Number.isFinite(part))) {
2735
+ return [
2736
+ clamp(parts[0] / 255, 0, 1),
2737
+ clamp(parts[1] / 255, 0, 1),
2738
+ clamp(parts[2] / 255, 0, 1),
2739
+ clamp(parts[3] ?? 1, 0, 1)
2740
+ ];
2741
+ }
2742
+ }
2743
+ throw new Error(`暂不支持的围栏颜色格式:${value}`);
2744
+ };
2745
+ const toFenceRgbaCss = (value, opacity = 1) => {
2746
+ const [red, green, blue, alpha] = parseFenceColor(value);
2747
+ return `rgba(${Math.round(red * 255)}, ${Math.round(green * 255)}, ${Math.round(
2748
+ blue * 255
2749
+ )}, ${clamp(alpha * opacity, 0, 1)})`;
2750
+ };
2751
+ const clampFenceNumber = clamp;
2752
+ const createFenceInteractionData = (store) => ({
2753
+ type: "FeatureCollection",
2754
+ features: store.visibleValues().map((feature) => ({
2755
+ type: "Feature",
2756
+ id: `${feature.id}:interaction`,
2757
+ geometry: clone(feature.geometry),
2758
+ properties: {
2759
+ [FENCE_ID_PROPERTY]: feature.id
2760
+ }
2761
+ }))
2762
+ });
2763
+ const bindFenceInteractions = (options) => {
2764
+ let hoveredId;
2765
+ const getHitId = (event) => {
2766
+ var _a;
2767
+ if (!options.map.getLayer(options.layerId)) {
2768
+ return void 0;
2769
+ }
2770
+ const feature = options.map.queryRenderedFeatures(event.point, {
2771
+ layers: [options.layerId]
2772
+ })[0];
2773
+ const id = (_a = feature == null ? void 0 : feature.properties) == null ? void 0 : _a[FENCE_ID_PROPERTY];
2774
+ return typeof id === "string" ? id : void 0;
2775
+ };
2776
+ const fire = (type, id, event) => {
2777
+ const feature = options.store.get(id);
2778
+ if (!feature) {
2779
+ return;
2780
+ }
2781
+ options.map.fire(`fence.${type}`, {
2782
+ instanceId: options.instanceId,
2783
+ renderer: options.renderer,
2784
+ id,
2785
+ feature,
2786
+ originalEvent: event
2787
+ });
2788
+ };
2789
+ const handleClick = (event) => {
2790
+ const id = getHitId(event);
2791
+ if (id) {
2792
+ fire("click", id, event);
2793
+ }
2794
+ };
2795
+ const handleMouseMove = (event) => {
2796
+ const nextId = getHitId(event);
2797
+ if (nextId === hoveredId) {
2798
+ return;
2799
+ }
2800
+ if (hoveredId) {
2801
+ fire("mouseleave", hoveredId, event);
2802
+ }
2803
+ hoveredId = nextId;
2804
+ if (hoveredId) {
2805
+ fire("mouseenter", hoveredId, event);
2806
+ }
2807
+ };
2808
+ const handleMouseOut = (event) => {
2809
+ if (!hoveredId) {
2810
+ return;
2811
+ }
2812
+ fire("mouseleave", hoveredId, event);
2813
+ hoveredId = void 0;
2814
+ };
2815
+ options.map.on("click", handleClick);
2816
+ options.map.on("mousemove", handleMouseMove);
2817
+ options.map.on("mouseout", handleMouseOut);
2818
+ return () => {
2819
+ options.map.off("click", handleClick);
2820
+ options.map.off("mousemove", handleMouseMove);
2821
+ options.map.off("mouseout", handleMouseOut);
2822
+ };
2823
+ };
2824
+ const isStyleNotReadyError = (error) => error instanceof Error && error.message === "Style is not done loading.";
2825
+ const setFenceLayerVisibility = (map, layerIds, visible) => {
2826
+ layerIds.forEach((layerId) => {
2827
+ if (map.getLayer(layerId)) {
2828
+ map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
2829
+ }
2830
+ });
2831
+ };
2832
+ const createElectronicFenceController = (options) => {
2833
+ let destroyed = false;
2834
+ const assertAlive = () => {
2835
+ if (destroyed) {
2836
+ throw new Error(`电子围栏插件 ${options.id} 已销毁。`);
2837
+ }
2838
+ };
2839
+ return {
2840
+ id: options.id,
2841
+ renderer: options.renderer,
2842
+ setData(data) {
2843
+ assertAlive();
2844
+ options.store.setData(data);
2845
+ options.refresh();
2846
+ },
2847
+ add(feature) {
2848
+ assertAlive();
2849
+ const id = options.store.add(feature);
2850
+ options.refresh();
2851
+ return id;
2852
+ },
2853
+ update(id, patch) {
2854
+ assertAlive();
2855
+ options.store.update(id, patch);
2856
+ options.refresh();
2857
+ },
2858
+ removeFeature(id) {
2859
+ assertAlive();
2860
+ options.store.remove(id);
2861
+ options.refresh();
2862
+ },
2863
+ clear() {
2864
+ assertAlive();
2865
+ options.store.clear();
2866
+ options.refresh();
2867
+ },
2868
+ setFeatureState(id, state) {
2869
+ assertAlive();
2870
+ options.store.setState(id, state);
2871
+ options.refresh();
2872
+ },
2873
+ setFeatureActive(id, active) {
2874
+ assertAlive();
2875
+ options.store.setActive(id, active);
2876
+ options.refresh();
2877
+ },
2878
+ setFeatureVisible(id, visible) {
2879
+ assertAlive();
2880
+ options.store.setVisible(id, visible);
2881
+ options.refresh();
2882
+ },
2883
+ show() {
2884
+ assertAlive();
2885
+ options.setPluginVisible(true);
2886
+ },
2887
+ hide() {
2888
+ assertAlive();
2889
+ options.setPluginVisible(false);
2890
+ },
2891
+ destroy() {
2892
+ if (destroyed) {
2893
+ return;
2894
+ }
2895
+ destroyed = true;
2896
+ options.destroy();
2897
+ }
2898
+ };
2899
+ };
2900
+ const KIND_PROPERTY = "__electronic_fence_kind";
2901
+ const COLOR_PROPERTY = "__electronic_fence_color";
2902
+ const OUTLINE_COLOR_PROPERTY = "__electronic_fence_outline_color";
2903
+ const OUTLINE_WIDTH_PROPERTY = "__electronic_fence_outline_width";
2904
+ const BASE_PROPERTY = "__electronic_fence_base";
2905
+ const TOP_PROPERTY = "__electronic_fence_top";
2906
+ const DEFAULT_STYLES$1 = {
2907
+ normal: {
2908
+ color: "#d8b4fe",
2909
+ opacity: 0.28,
2910
+ outlineColor: "#f5e9ff",
2911
+ outlineWidth: 1.5
2912
+ },
2913
+ active: {
2914
+ color: "#c084fc",
2915
+ opacity: 0.42,
2916
+ outlineColor: "#ffffff",
2917
+ outlineWidth: 2.5
2918
+ },
2919
+ warning: {
2920
+ color: "#f59e0b",
2921
+ opacity: 0.38,
2922
+ outlineColor: "#fde68a",
2923
+ outlineWidth: 2
2924
+ },
2925
+ alarm: {
2926
+ color: "#ef4444",
2927
+ opacity: 0.48,
2928
+ outlineColor: "#ffffff",
2929
+ outlineWidth: 2.5
2930
+ },
2931
+ disabled: {
2932
+ color: "#94a3b8",
2933
+ opacity: 0.16,
2934
+ outlineColor: "#cbd5e1",
2935
+ outlineWidth: 1
2936
+ }
2937
+ };
2938
+ const buildStandardFenceData = (store, options) => {
2939
+ const features = [];
2940
+ const dimensions = resolveFenceDimensions(options.defaults);
2941
+ store.visibleValues().forEach((feature) => {
2942
+ const state = resolveFenceVisualState(
2943
+ store.getState(feature.id),
2944
+ store.isActive(feature.id)
2945
+ );
2946
+ const style = resolveFenceStyle({
2947
+ defaults: DEFAULT_STYLES$1,
2948
+ overrides: options.styles,
2949
+ state
2950
+ });
2951
+ const segments = createFenceSegments(feature.geometry, dimensions.thickness);
2952
+ const commonProperties = {
2953
+ [FENCE_ID_PROPERTY]: feature.id,
2954
+ [COLOR_PROPERTY]: toFenceRgbaCss(
2955
+ style.color,
2956
+ clampFenceNumber(style.opacity, 0, 1)
2957
+ ),
2958
+ [OUTLINE_COLOR_PROPERTY]: toFenceRgbaCss(style.outlineColor),
2959
+ [OUTLINE_WIDTH_PROPERTY]: Math.max(0, style.outlineWidth),
2960
+ [BASE_PROPERTY]: dimensions.baseHeight,
2961
+ [TOP_PROPERTY]: dimensions.baseHeight + dimensions.height
2962
+ };
2963
+ if (segments.length) {
2964
+ features.push({
2965
+ type: "Feature",
2966
+ id: `${feature.id}:wall`,
2967
+ geometry: createFenceWallMultiPolygon(segments),
2968
+ properties: {
2969
+ ...commonProperties,
2970
+ [KIND_PROPERTY]: "wall"
2971
+ }
2972
+ });
2973
+ }
2974
+ features.push({
2975
+ type: "Feature",
2976
+ id: `${feature.id}:boundary`,
2977
+ geometry: feature.geometry,
2978
+ properties: {
2979
+ ...commonProperties,
2980
+ [KIND_PROPERTY]: "boundary"
2981
+ }
2982
+ });
2983
+ });
2984
+ return {
2985
+ type: "FeatureCollection",
2986
+ features
2987
+ };
2988
+ };
2989
+ const addStandardElectronicFenceLayer = (map, options) => {
2990
+ validateFenceDefaults(options.id, options.defaults);
2991
+ const store = new ElectronicFenceStore();
2992
+ const sourceId = `${options.id}-standard-source`;
2993
+ const extrusionLayerId = `${options.id}-standard-extrusion`;
2994
+ const outlineLayerId = `${options.id}-standard-outline`;
2995
+ const interactionLayerId = `${options.id}-standard-interaction`;
2996
+ const layerIds = [
2997
+ extrusionLayerId,
2998
+ outlineLayerId,
2999
+ ...options.interactive === false ? [] : [interactionLayerId]
3000
+ ];
3001
+ let data = buildStandardFenceData(store, options);
3002
+ let visible = options.visible !== false;
3003
+ let destroyed = false;
3004
+ const ensureLayers = () => {
3005
+ if (destroyed) {
3006
+ return;
3007
+ }
3008
+ try {
3009
+ if (!map.getSource(sourceId)) {
3010
+ map.addSource(sourceId, {
3011
+ type: "geojson",
3012
+ data
3013
+ });
3014
+ }
3015
+ if (!map.getLayer(extrusionLayerId)) {
3016
+ map.addLayer(
3017
+ {
3018
+ id: extrusionLayerId,
3019
+ type: "fill-extrusion",
3020
+ source: sourceId,
3021
+ filter: ["==", ["get", KIND_PROPERTY], "wall"],
3022
+ paint: {
3023
+ "fill-extrusion-color": ["get", COLOR_PROPERTY],
3024
+ "fill-extrusion-opacity": 1,
3025
+ "fill-extrusion-base": ["get", BASE_PROPERTY],
3026
+ "fill-extrusion-height": ["get", TOP_PROPERTY],
3027
+ "fill-extrusion-vertical-gradient": true
3028
+ }
3029
+ },
3030
+ options.beforeId
3031
+ );
3032
+ }
3033
+ if (!map.getLayer(outlineLayerId)) {
3034
+ map.addLayer(
3035
+ {
3036
+ id: outlineLayerId,
3037
+ type: "line",
3038
+ source: sourceId,
3039
+ filter: ["==", ["get", KIND_PROPERTY], "boundary"],
3040
+ paint: {
3041
+ "line-color": ["get", OUTLINE_COLOR_PROPERTY],
3042
+ "line-width": ["get", OUTLINE_WIDTH_PROPERTY],
3043
+ "line-opacity": 1
3044
+ }
3045
+ },
3046
+ options.beforeId
3047
+ );
3048
+ }
3049
+ if (options.interactive !== false && !map.getLayer(interactionLayerId)) {
3050
+ map.addLayer(
3051
+ {
3052
+ id: interactionLayerId,
3053
+ type: "fill",
3054
+ source: sourceId,
3055
+ filter: ["==", ["get", KIND_PROPERTY], "boundary"],
3056
+ paint: {
3057
+ "fill-color": "#000000",
3058
+ "fill-opacity": 0
3059
+ }
3060
+ },
3061
+ options.beforeId
3062
+ );
3063
+ }
3064
+ setFenceLayerVisibility(map, layerIds, visible);
3065
+ } catch (error) {
3066
+ if (!isStyleNotReadyError(error)) {
3067
+ throw error;
3068
+ }
3069
+ }
3070
+ };
3071
+ const refresh = () => {
3072
+ data = buildStandardFenceData(store, options);
3073
+ const source = map.getSource(sourceId);
3074
+ if (source) {
3075
+ source.setData(data);
3076
+ } else {
3077
+ ensureLayers();
3078
+ }
3079
+ };
3080
+ const handleStyleData = () => {
3081
+ ensureLayers();
3082
+ };
3083
+ map.on("styledata", handleStyleData);
3084
+ ensureLayers();
3085
+ const unbindInteractions = options.interactive === false ? () => void 0 : bindFenceInteractions({
3086
+ map,
3087
+ instanceId: options.id,
3088
+ renderer: "standard",
3089
+ layerId: interactionLayerId,
3090
+ store
3091
+ });
3092
+ return createElectronicFenceController({
3093
+ id: options.id,
3094
+ renderer: "standard",
3095
+ store,
3096
+ refresh,
3097
+ setPluginVisible(nextVisible) {
3098
+ visible = nextVisible;
3099
+ ensureLayers();
3100
+ setFenceLayerVisibility(map, layerIds, visible);
3101
+ },
3102
+ destroy() {
3103
+ destroyed = true;
3104
+ map.off("styledata", handleStyleData);
3105
+ unbindInteractions();
3106
+ [...layerIds].reverse().forEach((layerId) => {
3107
+ if (map.getLayer(layerId)) {
3108
+ map.removeLayer(layerId);
3109
+ }
3110
+ });
3111
+ if (map.getSource(sourceId)) {
3112
+ map.removeSource(sourceId);
3113
+ }
3114
+ }
3115
+ });
3116
+ };
3117
+ const FLOATS_PER_VERTEX = 19;
3118
+ const GLOW_LAYERS = [
3119
+ { thicknessScale: 0.08, opacity: 1 },
3120
+ { thicknessScale: 0.22, opacity: 0.56 },
3121
+ { thicknessScale: 0.45, opacity: 0.28 },
3122
+ { thicknessScale: 0.8, opacity: 0.12 },
3123
+ { thicknessScale: 1.3, opacity: 0.045 }
3124
+ ];
3125
+ const DEFAULT_STYLES = {
3126
+ normal: {
3127
+ color: "#d8b4fe",
3128
+ opacity: 0.2,
3129
+ outlineColor: "#f5e9ff",
3130
+ outlineWidth: 1.5,
3131
+ glowStrength: 1,
3132
+ flowColor: "#ffffff",
3133
+ flowCount: 3,
3134
+ flowLineStyle: "dashed",
3135
+ flowWidth: 0.18
3136
+ },
3137
+ active: {
3138
+ color: "#e9d5ff",
3139
+ opacity: 0.28,
3140
+ outlineColor: "#ffffff",
3141
+ outlineWidth: 2.5,
3142
+ glowStrength: 1.25,
3143
+ flowColor: "#ffffff",
3144
+ flowCount: 3,
3145
+ flowLineStyle: "dashed",
3146
+ flowWidth: 0.2
3147
+ },
3148
+ warning: {
3149
+ color: "#f59e0b",
3150
+ opacity: 0.25,
3151
+ outlineColor: "#fde68a",
3152
+ outlineWidth: 2,
3153
+ glowStrength: 1.1,
3154
+ flowColor: "#ffffff",
3155
+ flowCount: 4,
3156
+ flowLineStyle: "dashed",
3157
+ flowWidth: 0.18
3158
+ },
3159
+ alarm: {
3160
+ color: "#ef4444",
3161
+ opacity: 0.3,
3162
+ outlineColor: "#ffffff",
3163
+ outlineWidth: 2.5,
3164
+ glowStrength: 1.35,
3165
+ flowColor: "#ffffff",
3166
+ flowCount: 4,
3167
+ flowLineStyle: "dashed",
3168
+ flowWidth: 0.2
3169
+ },
3170
+ disabled: {
3171
+ color: "#94a3b8",
3172
+ opacity: 0.12,
3173
+ outlineColor: "#cbd5e1",
3174
+ outlineWidth: 1,
3175
+ glowStrength: 0,
3176
+ flowColor: "#ffffff",
3177
+ flowCount: 0,
3178
+ flowLineStyle: "dashed",
3179
+ flowWidth: 0
3180
+ }
3181
+ };
3182
+ const resolveAnimationOptions = (options) => ({
3183
+ enabled: (options == null ? void 0 : options.enabled) ?? true,
3184
+ fps: Math.round(clampFenceNumber((options == null ? void 0 : options.fps) ?? 30, 1, 60)),
3185
+ speed: Math.max(0, (options == null ? void 0 : options.speed) ?? 0.35),
3186
+ scope: (options == null ? void 0 : options.scope) ?? "all"
3187
+ });
3188
+ const toMercator = (lngLat, altitude) => {
3189
+ const coordinate = maplibregl.MercatorCoordinate.fromLngLat(lngLat, altitude);
3190
+ return [coordinate.x, coordinate.y, coordinate.z];
3191
+ };
3192
+ const subtract = (a, b) => [
3193
+ a[0] - b[0],
3194
+ a[1] - b[1],
3195
+ a[2] - b[2]
3196
+ ];
3197
+ const normalize = (vector) => {
3198
+ const length = Math.hypot(vector[0], vector[1], vector[2]) || 1;
3199
+ return [vector[0] / length, vector[1] / length, vector[2] / length];
3200
+ };
3201
+ const getNormal = (a, b, c) => {
3202
+ const ab = subtract(b, a);
3203
+ const ac = subtract(c, a);
3204
+ return normalize([
3205
+ ab[1] * ac[2] - ab[2] * ac[1],
3206
+ ab[2] * ac[0] - ab[0] * ac[2],
3207
+ ab[0] * ac[1] - ab[1] * ac[0]
3208
+ ]);
3209
+ };
3210
+ const pushVertex = (target, position, normal, heightRatio, pathProgress, style) => {
3211
+ target.push(
3212
+ ...position,
3213
+ ...normal,
3214
+ heightRatio,
3215
+ pathProgress,
3216
+ ...style.color,
3217
+ style.flowColor[0],
3218
+ style.flowColor[1],
3219
+ style.flowColor[2],
3220
+ style.glowStrength,
3221
+ style.flowWidth,
3222
+ style.flowCount,
3223
+ style.flowLineStyle
3224
+ );
3225
+ };
3226
+ const pushQuad = (target, positions, heightRatios, pathProgresses, style) => {
3227
+ const normal = getNormal(positions[0], positions[1], positions[2]);
3228
+ const indices = [0, 1, 2, 0, 2, 3];
3229
+ indices.forEach((index) => {
3230
+ pushVertex(
3231
+ target,
3232
+ positions[index],
3233
+ normal,
3234
+ heightRatios[index],
3235
+ pathProgresses[index],
3236
+ style
3237
+ );
3238
+ });
3239
+ };
3240
+ const appendSegmentPrism = (target, segment, baseHeight, height, style) => {
3241
+ const topHeight = baseHeight + height;
3242
+ const startLeftBottom = toMercator(segment.startLeft, baseHeight);
3243
+ const startRightBottom = toMercator(segment.startRight, baseHeight);
3244
+ const endLeftBottom = toMercator(segment.endLeft, baseHeight);
3245
+ const endRightBottom = toMercator(segment.endRight, baseHeight);
3246
+ const startLeftTop = toMercator(segment.startLeft, topHeight);
3247
+ const startRightTop = toMercator(segment.startRight, topHeight);
3248
+ const endLeftTop = toMercator(segment.endLeft, topHeight);
3249
+ const endRightTop = toMercator(segment.endRight, topHeight);
3250
+ pushQuad(
3251
+ target,
3252
+ [startLeftTop, endLeftTop, endRightTop, startRightTop],
3253
+ [1, 1, 1, 1],
3254
+ [
3255
+ segment.startProgress,
3256
+ segment.endProgress,
3257
+ segment.endProgress,
3258
+ segment.startProgress
3259
+ ],
3260
+ style
3261
+ );
3262
+ pushQuad(
3263
+ target,
3264
+ [startLeftBottom, endLeftBottom, endLeftTop, startLeftTop],
3265
+ [0, 0, 1, 1],
3266
+ [
3267
+ segment.startProgress,
3268
+ segment.endProgress,
3269
+ segment.endProgress,
3270
+ segment.startProgress
3271
+ ],
3272
+ style
3273
+ );
3274
+ pushQuad(
3275
+ target,
3276
+ [endRightBottom, startRightBottom, startRightTop, endRightTop],
3277
+ [0, 0, 1, 1],
3278
+ [
3279
+ segment.endProgress,
3280
+ segment.startProgress,
3281
+ segment.startProgress,
3282
+ segment.endProgress
3283
+ ],
3284
+ style
3285
+ );
3286
+ };
3287
+ const isHighlighted = (state) => state === "active" || state === "warning" || state === "alarm";
3288
+ const createVertexStyle = (style, flowEnabled) => {
3289
+ const color = parseFenceColor(style.color);
3290
+ const flowColor = parseFenceColor(style.flowColor);
3291
+ const flowCount = flowEnabled && style.flowCount > 0 ? Math.round(clampFenceNumber(style.flowCount, 1, 8)) : 0;
3292
+ return {
3293
+ color: [
3294
+ color[0],
3295
+ color[1],
3296
+ color[2],
3297
+ clampFenceNumber(color[3] * style.opacity, 0, 1)
3298
+ ],
3299
+ flowColor,
3300
+ glowStrength: clampFenceNumber(style.glowStrength, 0, 4),
3301
+ flowCount,
3302
+ flowLineStyle: style.flowLineStyle === "solid" ? 0 : 1,
3303
+ flowWidth: flowCount > 0 ? clampFenceNumber(style.flowWidth, 0.01, 0.49) : 0
3304
+ };
3305
+ };
3306
+ const buildWebGLFenceGeometry = (store, options, animation) => {
3307
+ const glowVertexLayers = GLOW_LAYERS.map(() => []);
3308
+ const bodyVertices = [];
3309
+ const dimensions = resolveFenceDimensions(options.defaults);
3310
+ let animated = false;
3311
+ store.visibleValues().forEach((feature) => {
3312
+ const active = store.isActive(feature.id);
3313
+ const state = resolveFenceVisualState(store.getState(feature.id), active);
3314
+ const style = resolveFenceStyle({
3315
+ defaults: DEFAULT_STYLES,
3316
+ overrides: options.styles,
3317
+ state
3318
+ });
3319
+ const flowEnabled = animation.enabled && (animation.scope === "all" || isHighlighted(state));
3320
+ const vertexStyle = createVertexStyle(style, flowEnabled);
3321
+ const bodySegments = createFenceSegments(
3322
+ feature.geometry,
3323
+ dimensions.thickness
3324
+ );
3325
+ bodySegments.forEach((segment) => {
3326
+ appendSegmentPrism(
3327
+ bodyVertices,
3328
+ segment,
3329
+ dimensions.baseHeight,
3330
+ dimensions.height,
3331
+ vertexStyle
3332
+ );
3333
+ });
3334
+ if (vertexStyle.glowStrength > 0) {
3335
+ GLOW_LAYERS.forEach((glowLayer, index) => {
3336
+ const glowThickness = dimensions.thickness * (1 + Math.min(vertexStyle.glowStrength, 2) * glowLayer.thicknessScale);
3337
+ const glowSegments = createFenceSegments(
3338
+ feature.geometry,
3339
+ glowThickness
3340
+ );
3341
+ glowSegments.forEach((segment) => {
3342
+ appendSegmentPrism(
3343
+ glowVertexLayers[index],
3344
+ segment,
3345
+ dimensions.baseHeight,
3346
+ dimensions.height,
3347
+ vertexStyle
3348
+ );
3349
+ });
3350
+ });
3351
+ }
3352
+ animated || (animated = vertexStyle.flowCount > 0 && vertexStyle.flowWidth > 0);
3353
+ });
3354
+ const glowFloatCount = glowVertexLayers.reduce(
3355
+ (sum, layerVertices) => sum + layerVertices.length,
3356
+ 0
3357
+ );
3358
+ const vertices = new Float32Array(glowFloatCount + bodyVertices.length);
3359
+ const glowPasses = [];
3360
+ let floatOffset = 0;
3361
+ glowVertexLayers.forEach((layerVertices, index) => {
3362
+ const count = layerVertices.length / FLOATS_PER_VERTEX;
3363
+ if (count > 0) {
3364
+ glowPasses.push({
3365
+ first: floatOffset / FLOATS_PER_VERTEX,
3366
+ count,
3367
+ opacity: GLOW_LAYERS[index].opacity
3368
+ });
3369
+ }
3370
+ vertices.set(layerVertices, floatOffset);
3371
+ floatOffset += layerVertices.length;
3372
+ });
3373
+ const bodyFirst = floatOffset / FLOATS_PER_VERTEX;
3374
+ vertices.set(bodyVertices, floatOffset);
3375
+ return {
3376
+ vertices,
3377
+ glowPasses,
3378
+ bodyFirst,
3379
+ bodyVertexCount: bodyVertices.length / FLOATS_PER_VERTEX,
3380
+ animated
3381
+ };
3382
+ };
3383
+ const createShader = (gl, type, source) => {
3384
+ const shader = gl.createShader(type);
3385
+ if (!shader) {
3386
+ throw new Error("无法创建电子围栏 WebGL shader。");
3387
+ }
3388
+ gl.shaderSource(shader, source);
3389
+ gl.compileShader(shader);
3390
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
3391
+ const message = gl.getShaderInfoLog(shader) || "未知 shader 编译错误";
3392
+ gl.deleteShader(shader);
3393
+ throw new Error(`电子围栏 WebGL shader 编译失败:${message}`);
3394
+ }
3395
+ return shader;
3396
+ };
3397
+ const createProgram = (gl) => {
3398
+ const isWebGL2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext;
3399
+ const vertexSource = isWebGL2 ? `#version 300 es
3400
+ precision highp float;
3401
+ in vec3 a_position;
3402
+ in vec3 a_normal;
3403
+ in float a_height_ratio;
3404
+ in float a_path_progress;
3405
+ in vec4 a_color;
3406
+ in vec3 a_flow_color;
3407
+ in float a_glow_strength;
3408
+ in float a_flow_width;
3409
+ in float a_flow_count;
3410
+ in float a_flow_line_style;
3411
+ uniform mat4 u_matrix;
3412
+ out vec3 v_normal;
3413
+ out float v_height_ratio;
3414
+ out float v_path_progress;
3415
+ out vec4 v_color;
3416
+ out vec3 v_flow_color;
3417
+ out float v_glow_strength;
3418
+ out float v_flow_width;
3419
+ out float v_flow_count;
3420
+ out float v_flow_line_style;
3421
+ void main() {
3422
+ gl_Position = u_matrix * vec4(a_position, 1.0);
3423
+ v_normal = a_normal;
3424
+ v_height_ratio = a_height_ratio;
3425
+ v_path_progress = a_path_progress;
3426
+ v_color = a_color;
3427
+ v_flow_color = a_flow_color;
3428
+ v_glow_strength = a_glow_strength;
3429
+ v_flow_width = a_flow_width;
3430
+ v_flow_count = a_flow_count;
3431
+ v_flow_line_style = a_flow_line_style;
3432
+ }
3433
+ ` : `
3434
+ precision highp float;
3435
+ attribute vec3 a_position;
3436
+ attribute vec3 a_normal;
3437
+ attribute float a_height_ratio;
3438
+ attribute float a_path_progress;
3439
+ attribute vec4 a_color;
3440
+ attribute vec3 a_flow_color;
3441
+ attribute float a_glow_strength;
3442
+ attribute float a_flow_width;
3443
+ attribute float a_flow_count;
3444
+ attribute float a_flow_line_style;
3445
+ uniform mat4 u_matrix;
3446
+ varying vec3 v_normal;
3447
+ varying float v_height_ratio;
3448
+ varying float v_path_progress;
3449
+ varying vec4 v_color;
3450
+ varying vec3 v_flow_color;
3451
+ varying float v_glow_strength;
3452
+ varying float v_flow_width;
3453
+ varying float v_flow_count;
3454
+ varying float v_flow_line_style;
3455
+ void main() {
3456
+ gl_Position = u_matrix * vec4(a_position, 1.0);
3457
+ v_normal = a_normal;
3458
+ v_height_ratio = a_height_ratio;
3459
+ v_path_progress = a_path_progress;
3460
+ v_color = a_color;
3461
+ v_flow_color = a_flow_color;
3462
+ v_glow_strength = a_glow_strength;
3463
+ v_flow_width = a_flow_width;
3464
+ v_flow_count = a_flow_count;
3465
+ v_flow_line_style = a_flow_line_style;
3466
+ }
3467
+ `;
3468
+ const fragmentBody = `
3469
+ precision highp float;
3470
+ uniform float u_time;
3471
+ uniform float u_speed;
3472
+ uniform float u_animation_enabled;
3473
+ uniform float u_glow_pass;
3474
+ uniform float u_glow_opacity;
3475
+ VARYING vec3 v_normal;
3476
+ VARYING float v_height_ratio;
3477
+ VARYING float v_path_progress;
3478
+ VARYING vec4 v_color;
3479
+ VARYING vec3 v_flow_color;
3480
+ VARYING float v_glow_strength;
3481
+ VARYING float v_flow_width;
3482
+ VARYING float v_flow_count;
3483
+ VARYING float v_flow_line_style;
3484
+ void main() {
3485
+ float bandCore = 0.0;
3486
+ float bandGlow = 0.0;
3487
+ float verticalFace = 1.0 - smoothstep(
3488
+ 0.82,
3489
+ 0.98,
3490
+ abs(normalize(v_normal).z)
3491
+ );
3492
+
3493
+ if (
3494
+ u_animation_enabled > 0.5 &&
3495
+ v_flow_count > 0.5 &&
3496
+ v_flow_width > 0.0001
3497
+ ) {
3498
+ float count = max(v_flow_count, 1.0);
3499
+ float phase = fract(
3500
+ v_height_ratio * count + u_time * u_speed * count
3501
+ );
3502
+ float bandDistance = abs(phase - 0.5);
3503
+ float dashPhase = fract(
3504
+ v_path_progress * 36.0 - u_time * u_speed * 2.4
3505
+ );
3506
+ float dash = smoothstep(0.02, 0.1, dashPhase) *
3507
+ (1.0 - smoothstep(0.62, 0.74, dashPhase));
3508
+ float linePattern = v_flow_line_style > 0.5 ? dash : 1.0;
3509
+
3510
+ bandCore = 1.0 - smoothstep(
3511
+ max(v_flow_width * 0.08, 0.001),
3512
+ max(v_flow_width * 0.42, 0.003),
3513
+ bandDistance
3514
+ );
3515
+ float glowDistance = bandDistance / max(v_flow_width, 0.001);
3516
+ bandGlow = exp(
3517
+ -glowDistance * glowDistance * 2.6
3518
+ );
3519
+ bandCore *= linePattern * verticalFace;
3520
+ bandGlow *= verticalFace;
3521
+ }
3522
+
3523
+ if (u_glow_pass > 0.5) {
3524
+ float glowAlpha = clamp(
3525
+ (v_color.a * 0.003 + bandGlow * 0.085) *
3526
+ v_glow_strength * u_glow_opacity,
3527
+ 0.0,
3528
+ 0.14
3529
+ );
3530
+ OUTPUT_COLOR = vec4(v_color.rgb, glowAlpha);
3531
+ return;
3532
+ }
3533
+
3534
+ float faceLight = 0.88 + 0.12 * abs(normalize(v_normal).z);
3535
+ vec3 bodyColor = v_color.rgb * faceLight;
3536
+ vec3 color = mix(bodyColor, v_flow_color, bandCore);
3537
+ float alpha = clamp(
3538
+ v_color.a + bandCore * 0.68,
3539
+ 0.0,
3540
+ 1.0
3541
+ );
3542
+ OUTPUT_COLOR = vec4(color, alpha);
3543
+ }
3544
+ `;
3545
+ const replaceShaderToken = (source, token, value) => source.split(token).join(value);
3546
+ const fragmentSource = isWebGL2 ? `#version 300 es
3547
+ ${replaceShaderToken(
3548
+ replaceShaderToken(fragmentBody, "VARYING", "in"),
3549
+ "OUTPUT_COLOR",
3550
+ "fragmentColor"
3551
+ ).replace(
3552
+ "precision highp float;",
3553
+ "precision highp float;\n out vec4 fragmentColor;"
3554
+ )}
3555
+ ` : replaceShaderToken(
3556
+ replaceShaderToken(fragmentBody, "VARYING", "varying"),
3557
+ "OUTPUT_COLOR",
3558
+ "gl_FragColor"
3559
+ );
3560
+ const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
3561
+ const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
3562
+ const program = gl.createProgram();
3563
+ if (!program) {
3564
+ gl.deleteShader(vertexShader);
3565
+ gl.deleteShader(fragmentShader);
3566
+ throw new Error("无法创建电子围栏 WebGL program。");
3567
+ }
3568
+ gl.attachShader(program, vertexShader);
3569
+ gl.attachShader(program, fragmentShader);
3570
+ gl.linkProgram(program);
3571
+ gl.deleteShader(vertexShader);
3572
+ gl.deleteShader(fragmentShader);
3573
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
3574
+ const message = gl.getProgramInfoLog(program) || "未知 program 链接错误";
3575
+ gl.deleteProgram(program);
3576
+ throw new Error(`电子围栏 WebGL program 链接失败:${message}`);
3577
+ }
3578
+ return program;
3579
+ };
3580
+ const requireUniform = (gl, program, name) => {
3581
+ const location = gl.getUniformLocation(program, name);
3582
+ if (!location) {
3583
+ throw new Error(`电子围栏 WebGL uniform 不存在:${name}`);
3584
+ }
3585
+ return location;
3586
+ };
3587
+ const createLocations = (gl, program) => ({
3588
+ position: gl.getAttribLocation(program, "a_position"),
3589
+ normal: gl.getAttribLocation(program, "a_normal"),
3590
+ heightRatio: gl.getAttribLocation(program, "a_height_ratio"),
3591
+ pathProgress: gl.getAttribLocation(program, "a_path_progress"),
3592
+ color: gl.getAttribLocation(program, "a_color"),
3593
+ flowColor: gl.getAttribLocation(program, "a_flow_color"),
3594
+ glowStrength: gl.getAttribLocation(program, "a_glow_strength"),
3595
+ flowWidth: gl.getAttribLocation(program, "a_flow_width"),
3596
+ flowCount: gl.getAttribLocation(program, "a_flow_count"),
3597
+ flowLineStyle: gl.getAttribLocation(program, "a_flow_line_style"),
3598
+ matrix: requireUniform(gl, program, "u_matrix"),
3599
+ time: requireUniform(gl, program, "u_time"),
3600
+ speed: requireUniform(gl, program, "u_speed"),
3601
+ animationEnabled: requireUniform(gl, program, "u_animation_enabled"),
3602
+ glowPass: requireUniform(gl, program, "u_glow_pass"),
3603
+ glowOpacity: requireUniform(gl, program, "u_glow_opacity")
3604
+ });
3605
+ class WebGLElectronicFenceCustomLayer {
3606
+ constructor(id, animation) {
3607
+ this.type = "custom";
3608
+ this.renderingMode = "3d";
3609
+ this.geometry = {
3610
+ vertices: new Float32Array(),
3611
+ glowPasses: [],
3612
+ bodyFirst: 0,
3613
+ bodyVertexCount: 0,
3614
+ animated: false
3615
+ };
3616
+ this.visible = true;
3617
+ this.id = id;
3618
+ this.animation = animation;
3619
+ }
3620
+ setGeometry(geometry) {
3621
+ var _a;
3622
+ this.geometry = geometry;
3623
+ this.uploadGeometry();
3624
+ (_a = this.map) == null ? void 0 : _a.triggerRepaint();
3625
+ }
3626
+ setVisible(visible) {
3627
+ var _a;
3628
+ this.visible = visible;
3629
+ (_a = this.map) == null ? void 0 : _a.triggerRepaint();
3630
+ }
3631
+ onAdd(map, gl) {
3632
+ this.map = map;
3633
+ this.gl = gl;
3634
+ this.program = createProgram(gl);
3635
+ this.locations = createLocations(gl, this.program);
3636
+ this.buffer = gl.createBuffer() || void 0;
3637
+ if (!this.buffer) {
3638
+ throw new Error("无法创建电子围栏 WebGL 顶点缓冲。");
3639
+ }
3640
+ this.uploadGeometry(gl);
3641
+ }
3642
+ render(gl, input) {
3643
+ if (!this.visible || !this.program || !this.buffer || !this.locations || !this.geometry.glowPasses.length && !this.geometry.bodyVertexCount) {
3644
+ return;
3645
+ }
3646
+ const stride = FLOATS_PER_VERTEX * Float32Array.BYTES_PER_ELEMENT;
3647
+ const pointer = (location, size, offset) => {
3648
+ gl.enableVertexAttribArray(location);
3649
+ gl.vertexAttribPointer(
3650
+ location,
3651
+ size,
3652
+ gl.FLOAT,
3653
+ false,
3654
+ stride,
3655
+ offset * Float32Array.BYTES_PER_ELEMENT
3656
+ );
3657
+ };
3658
+ gl.useProgram(this.program);
3659
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
3660
+ pointer(this.locations.position, 3, 0);
3661
+ pointer(this.locations.normal, 3, 3);
3662
+ pointer(this.locations.heightRatio, 1, 6);
3663
+ pointer(this.locations.pathProgress, 1, 7);
3664
+ pointer(this.locations.color, 4, 8);
3665
+ pointer(this.locations.flowColor, 3, 12);
3666
+ pointer(this.locations.glowStrength, 1, 15);
3667
+ pointer(this.locations.flowWidth, 1, 16);
3668
+ pointer(this.locations.flowCount, 1, 17);
3669
+ pointer(this.locations.flowLineStyle, 1, 18);
3670
+ gl.uniformMatrix4fv(
3671
+ this.locations.matrix,
3672
+ false,
3673
+ input.defaultProjectionData.mainMatrix
3674
+ );
3675
+ gl.uniform1f(this.locations.time, performance.now() / 1e3);
3676
+ gl.uniform1f(this.locations.speed, this.animation.speed);
3677
+ gl.uniform1f(
3678
+ this.locations.animationEnabled,
3679
+ this.animation.enabled ? 1 : 0
3680
+ );
3681
+ gl.enable(gl.BLEND);
3682
+ gl.enable(gl.DEPTH_TEST);
3683
+ gl.disable(gl.CULL_FACE);
3684
+ gl.depthMask(false);
3685
+ if (this.geometry.glowPasses.length) {
3686
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
3687
+ gl.uniform1f(this.locations.glowPass, 1);
3688
+ this.geometry.glowPasses.forEach((glowPass) => {
3689
+ gl.uniform1f(this.locations.glowOpacity, glowPass.opacity);
3690
+ gl.drawArrays(gl.TRIANGLES, glowPass.first, glowPass.count);
3691
+ });
3692
+ }
3693
+ if (this.geometry.bodyVertexCount) {
3694
+ gl.blendFuncSeparate(
3695
+ gl.SRC_ALPHA,
3696
+ gl.ONE_MINUS_SRC_ALPHA,
3697
+ gl.ONE,
3698
+ gl.ONE_MINUS_SRC_ALPHA
3699
+ );
3700
+ gl.uniform1f(this.locations.glowPass, 0);
3701
+ gl.uniform1f(this.locations.glowOpacity, 0);
3702
+ gl.drawArrays(
3703
+ gl.TRIANGLES,
3704
+ this.geometry.bodyFirst,
3705
+ this.geometry.bodyVertexCount
3706
+ );
3707
+ }
3708
+ gl.depthMask(true);
3709
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
3710
+ }
3711
+ onRemove(_map, gl) {
3712
+ if (this.buffer) {
3713
+ gl.deleteBuffer(this.buffer);
3714
+ }
3715
+ if (this.program) {
3716
+ gl.deleteProgram(this.program);
3717
+ }
3718
+ this.map = void 0;
3719
+ this.gl = void 0;
3720
+ this.buffer = void 0;
3721
+ this.program = void 0;
3722
+ this.locations = void 0;
3723
+ }
3724
+ uploadGeometry(providedGl) {
3725
+ const gl = providedGl || this.gl;
3726
+ if (!gl || !this.buffer) {
3727
+ return;
3728
+ }
3729
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
3730
+ gl.bufferData(gl.ARRAY_BUFFER, this.geometry.vertices, gl.STATIC_DRAW);
3731
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
3732
+ }
3733
+ }
3734
+ const addWebGLElectronicFenceLayer = (map, options) => {
3735
+ validateFenceDefaults(options.id, options.defaults);
3736
+ const animation = resolveAnimationOptions(options.animation);
3737
+ const store = new ElectronicFenceStore();
3738
+ const customLayerId = `${options.id}-webgl`;
3739
+ const interactionSourceId = `${options.id}-webgl-interaction-source`;
3740
+ const interactionLayerId = `${options.id}-webgl-interaction`;
3741
+ const customLayer = new WebGLElectronicFenceCustomLayer(
3742
+ customLayerId,
3743
+ animation
3744
+ );
3745
+ let interactionData = createFenceInteractionData(store);
3746
+ let geometry = buildWebGLFenceGeometry(store, options, animation);
3747
+ let visible = options.visible !== false;
3748
+ let destroyed = false;
3749
+ let animationFrame = 0;
3750
+ let lastRepaintTime = 0;
3751
+ const stopAnimation = () => {
3752
+ if (animationFrame) {
3753
+ cancelAnimationFrame(animationFrame);
3754
+ animationFrame = 0;
3755
+ }
3756
+ };
3757
+ const tick = (timestamp) => {
3758
+ if (destroyed || !visible || !geometry.animated) {
3759
+ animationFrame = 0;
3760
+ return;
3761
+ }
3762
+ if ((typeof document === "undefined" || !document.hidden) && timestamp - lastRepaintTime >= 1e3 / animation.fps) {
3763
+ lastRepaintTime = timestamp;
3764
+ map.triggerRepaint();
3765
+ }
3766
+ animationFrame = requestAnimationFrame(tick);
3767
+ };
3768
+ const updateAnimation = () => {
3769
+ const shouldAnimate = animation.enabled && visible && geometry.animated && !!map.getLayer(customLayerId);
3770
+ if (shouldAnimate && !animationFrame) {
3771
+ lastRepaintTime = 0;
3772
+ animationFrame = requestAnimationFrame(tick);
3773
+ } else if (!shouldAnimate) {
3774
+ stopAnimation();
3775
+ }
3776
+ };
3777
+ const ensureLayers = () => {
3778
+ if (destroyed) {
3779
+ return;
3780
+ }
3781
+ try {
3782
+ if (options.interactive !== false && !map.getSource(interactionSourceId)) {
3783
+ map.addSource(interactionSourceId, {
3784
+ type: "geojson",
3785
+ data: interactionData
3786
+ });
3787
+ }
3788
+ if (!map.getLayer(customLayerId)) {
3789
+ map.addLayer(customLayer, options.beforeId);
3790
+ }
3791
+ if (options.interactive !== false && !map.getLayer(interactionLayerId)) {
3792
+ map.addLayer(
3793
+ {
3794
+ id: interactionLayerId,
3795
+ type: "fill",
3796
+ source: interactionSourceId,
3797
+ paint: {
3798
+ "fill-color": "#000000",
3799
+ "fill-opacity": 0
3800
+ }
3801
+ },
3802
+ options.beforeId
3803
+ );
3804
+ }
3805
+ customLayer.setVisible(visible);
3806
+ setFenceLayerVisibility(
3807
+ map,
3808
+ options.interactive === false ? [] : [interactionLayerId],
3809
+ visible
3810
+ );
3811
+ updateAnimation();
3812
+ } catch (error) {
3813
+ if (!isStyleNotReadyError(error)) {
3814
+ throw error;
3815
+ }
3816
+ }
3817
+ };
3818
+ const refresh = () => {
3819
+ geometry = buildWebGLFenceGeometry(store, options, animation);
3820
+ interactionData = createFenceInteractionData(store);
3821
+ customLayer.setGeometry(geometry);
3822
+ const source = map.getSource(interactionSourceId);
3823
+ if (source) {
3824
+ source.setData(interactionData);
3825
+ }
3826
+ ensureLayers();
3827
+ updateAnimation();
3828
+ };
3829
+ const handleStyleData = () => {
3830
+ ensureLayers();
3831
+ };
3832
+ customLayer.setGeometry(geometry);
3833
+ map.on("styledata", handleStyleData);
3834
+ ensureLayers();
3835
+ const unbindInteractions = options.interactive === false ? () => void 0 : bindFenceInteractions({
3836
+ map,
3837
+ instanceId: options.id,
3838
+ renderer: "webgl",
3839
+ layerId: interactionLayerId,
3840
+ store
3841
+ });
3842
+ return createElectronicFenceController({
3843
+ id: options.id,
3844
+ renderer: "webgl",
3845
+ store,
3846
+ refresh,
3847
+ setPluginVisible(nextVisible) {
3848
+ visible = nextVisible;
3849
+ customLayer.setVisible(visible);
3850
+ ensureLayers();
3851
+ setFenceLayerVisibility(
3852
+ map,
3853
+ options.interactive === false ? [] : [interactionLayerId],
3854
+ visible
3855
+ );
3856
+ updateAnimation();
3857
+ },
3858
+ destroy() {
3859
+ destroyed = true;
3860
+ stopAnimation();
3861
+ map.off("styledata", handleStyleData);
3862
+ unbindInteractions();
3863
+ if (map.getLayer(interactionLayerId)) {
3864
+ map.removeLayer(interactionLayerId);
3865
+ }
3866
+ if (map.getLayer(customLayerId)) {
3867
+ map.removeLayer(customLayerId);
3868
+ }
3869
+ if (map.getSource(interactionSourceId)) {
3870
+ map.removeSource(interactionSourceId);
3871
+ }
3872
+ }
3873
+ });
3874
+ };
3875
+ const resolveStyle = (style, tdtToken) => {
3876
+ if (!style) {
3877
+ return getBaseMapStyle("openfreemap-liberty");
3878
+ }
3879
+ if (typeof style === "string" && isBaseMapType(style)) {
3880
+ return getBaseMapStyle(style, { token: tdtToken });
3881
+ }
1016
3882
  return style;
1017
3883
  };
1018
3884
  const createMap = (options) => {
1019
- const { style, tdtToken, ...mapOptions } = options;
1020
- return new maplibregl.Map({
3885
+ const { style, tdtToken, projection, ...mapOptions } = options;
3886
+ const map = new maplibregl.Map({
1021
3887
  ...mapOptions,
1022
3888
  style: resolveStyle(style, tdtToken)
1023
3889
  });
3890
+ if (projection) {
3891
+ map.on("style.load", () => {
3892
+ map.setProjection({ type: projection });
3893
+ });
3894
+ }
3895
+ return map;
1024
3896
  };
1025
3897
  export {
1026
3898
  BASE_MAP_TYPES,
@@ -1028,11 +3900,21 @@ export {
1028
3900
  OPENFREEMAP_STYLE_BASE_URL,
1029
3901
  OPENFREEMAP_STYLE_TYPES,
1030
3902
  TDT_STYLE_TYPES,
3903
+ addBreachAlert,
3904
+ addBreathingCircle,
3905
+ addCoordinatedCountermeasure,
3906
+ addCountermeasureBeam,
3907
+ addDefenceCircle,
1031
3908
  addDirectionalPulse,
3909
+ addNavigationSpoofing,
1032
3910
  addPulseMarker,
1033
3911
  addRadarSweep,
1034
3912
  addRingPulseMarker,
1035
3913
  addSectorScan,
3914
+ addStandardElectronicFenceLayer,
3915
+ addTargetLock,
3916
+ addWebGLElectronicFenceLayer,
3917
+ createFenceCircle,
1036
3918
  createMap,
1037
3919
  createTiandituImageStyle,
1038
3920
  createTiandituRasterSource,