@bitalltech-maplibre/core 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 = (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(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 {
@@ -436,26 +550,21 @@ const createPulseMarkerRender = (options) => {
436
550
  };
437
551
  };
438
552
  const createRingPulseEffectData = (options, phase = 0) => {
439
- var _a, _b;
440
553
  const baseRadius = Math.max(16, options.radius ?? 110);
441
554
  const pulseScale = Math.max(0, options.pulseScale ?? 0.16);
442
555
  const pulseProgress = (1 - Math.cos(phase * Math.PI * 2)) / 2;
443
556
  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);
557
+ const ringCount = Math.max(1, options.ringCount ?? 3);
446
558
  const maxRadius = Math.max(
447
559
  currentRadius * 2.2,
448
- options.maxRadius ?? (((_b = options.radii) == null ? void 0 : _b.length) ? Math.max(...options.radii) : baseRadius * (ringCount * 2.5))
560
+ options.maxRadius ?? baseRadius * (ringCount * 2.5)
449
561
  );
450
562
  const ringSpacing = maxRadius / ringCount;
563
+ const fillOpacity = options.fillOpacity ?? 0.28;
451
564
  const features = [
452
- createFeature(createCirclePolygon(options.center, haloRadius), {
453
- kind: "halo",
454
- opacity: (options.haloOpacity ?? 0.16) * (0.75 + pulseProgress * 0.65)
455
- }),
456
565
  createFeature(createCirclePolygon(options.center, currentRadius), {
457
566
  kind: "core",
458
- opacity: (options.fillOpacity ?? 0.28) * (0.88 + pulseProgress * 0.2)
567
+ opacity: fillOpacity * (0.88 + pulseProgress * 0.2)
459
568
  }),
460
569
  createFeature(createCircleLine(options.center, currentRadius * 1.06), {
461
570
  kind: "core-ring",
@@ -488,15 +597,6 @@ const createRingPulseMarkerRender = (options) => {
488
597
  return {
489
598
  data: createRingPulseEffectData(options),
490
599
  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
600
  {
501
601
  id: `${options.id}-core`,
502
602
  type: "fill",
@@ -538,6 +638,85 @@ const createRingPulseMarkerRender = (options) => {
538
638
  }
539
639
  };
540
640
  };
641
+ const createBreathingCircleData = (options, phase = 0) => {
642
+ const baseRadius = Math.max(1, options.radius ?? 90);
643
+ const minRadius = Math.max(1, options.minRadius ?? baseRadius * 0.78);
644
+ const maxRadius = Math.max(
645
+ minRadius,
646
+ options.maxRadius ?? baseRadius * 1.22
647
+ );
648
+ const progress = (1 - Math.cos(phase * Math.PI * 2)) / 2;
649
+ const currentRadius = minRadius + (maxRadius - minRadius) * progress;
650
+ const fillOpacity = options.fillOpacity ?? 0.28;
651
+ const minFillOpacity = options.minFillOpacity ?? fillOpacity * 0.46;
652
+ const strokeOpacity = options.strokeOpacity ?? 0.82;
653
+ const minStrokeOpacity = options.minStrokeOpacity ?? strokeOpacity * 0.38;
654
+ const inverseProgress = 1 - progress;
655
+ const features = [
656
+ createFeature(createCirclePolygon(options.center, currentRadius), {
657
+ kind: "breathing-fill",
658
+ opacity: minFillOpacity + (fillOpacity - minFillOpacity) * inverseProgress
659
+ })
660
+ ];
661
+ if ((options.strokeWidth ?? 2) > 0) {
662
+ features.push(
663
+ createFeature(createCircleLine(options.center, currentRadius), {
664
+ kind: "breathing-stroke",
665
+ opacity: minStrokeOpacity + (strokeOpacity - minStrokeOpacity) * inverseProgress
666
+ })
667
+ );
668
+ }
669
+ return createFeatureCollection(features);
670
+ };
671
+ const createBreathingCircleRender = (options) => {
672
+ const color = options.color || "#1677ff";
673
+ const fillColor = options.fillColor || color;
674
+ const strokeColor = options.strokeColor || color;
675
+ const strokeWidth = Math.max(0, options.strokeWidth ?? 2);
676
+ return {
677
+ data: createBreathingCircleData(options),
678
+ layers: [
679
+ {
680
+ id: `${options.id}-breathing-fill`,
681
+ type: "fill",
682
+ filter: ["==", ["get", "kind"], "breathing-fill"],
683
+ paint: {
684
+ "fill-color": fillColor,
685
+ "fill-opacity": ["coalesce", ["get", "opacity"], options.fillOpacity ?? 0.28]
686
+ }
687
+ },
688
+ {
689
+ id: `${options.id}-breathing-stroke`,
690
+ type: "line",
691
+ filter: ["==", ["get", "kind"], "breathing-stroke"],
692
+ layout: {
693
+ "line-cap": "round",
694
+ "line-join": "round"
695
+ },
696
+ paint: {
697
+ "line-color": strokeColor,
698
+ "line-opacity": ["coalesce", ["get", "opacity"], options.strokeOpacity ?? 0.82],
699
+ "line-width": strokeWidth
700
+ }
701
+ }
702
+ ],
703
+ startAnimation({ getOptions, setData }) {
704
+ const duration = Math.max(400, getOptions().duration ?? 1800);
705
+ const startTime = performance.now();
706
+ let frameId = 0;
707
+ const tick = (timestamp) => {
708
+ const currentOptions = getOptions();
709
+ const phase = (timestamp - startTime) % duration / duration;
710
+ setData(createBreathingCircleData(currentOptions, phase));
711
+ frameId = requestAnimationFrame(tick);
712
+ };
713
+ frameId = requestAnimationFrame(tick);
714
+ return () => {
715
+ cancelAnimationFrame(frameId);
716
+ };
717
+ }
718
+ };
719
+ };
541
720
  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
542
721
  const parseColor = (color) => {
543
722
  const value = color.trim();
@@ -576,6 +755,29 @@ const toRgbaColor = (color, opacity) => {
576
755
  }
577
756
  return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp(opacity, 0, 1)})`;
578
757
  };
758
+ const easeOutQuad = (t) => t * (2 - t);
759
+ const lerpRgb = (a, b, t) => [
760
+ Math.round(a[0] + (b[0] - a[0]) * t),
761
+ Math.round(a[1] + (b[1] - a[1]) * t),
762
+ Math.round(a[2] + (b[2] - a[2]) * t)
763
+ ];
764
+ const lerpColorArray = (colors, t) => {
765
+ const last = colors.length - 1;
766
+ if (t <= 0) {
767
+ return colors[0];
768
+ }
769
+ if (t >= last) {
770
+ return colors[last];
771
+ }
772
+ const idx = Math.floor(t);
773
+ const frac = t - idx;
774
+ const [a, b] = [parseColor(colors[idx]), parseColor(colors[idx + 1])];
775
+ if (!a || !b) {
776
+ return colors[Math.round(t)];
777
+ }
778
+ const [r, g, b2] = lerpRgb(a, b, frac);
779
+ return `rgb(${r},${g},${b2})`;
780
+ };
579
781
  const createSectorCanvasCoordinates = (center, radius) => {
580
782
  const north = getDestination(center, radius, 0)[1];
581
783
  const east = getDestination(center, radius, 90)[0];
@@ -613,8 +815,8 @@ const drawSectorGradientCanvas = (canvas, options, angleOffset = 0) => {
613
815
  const startCanvasAngle = getCanvasArcAngle(startAngle);
614
816
  const endCanvasAngle = startCanvasAngle + sweep * Math.PI / 180;
615
817
  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;
818
+ const centerOpacity = options.gradient.centerOpacity ?? baseOpacity * 0.25;
819
+ const edgeOpacity = options.gradient.edgeOpacity ?? baseOpacity;
618
820
  const gradient = context.createRadialGradient(
619
821
  center,
620
822
  center,
@@ -821,13 +1023,86 @@ const createDirectionalPulseRender = (options) => ({
821
1023
  };
822
1024
  } : void 0
823
1025
  });
1026
+ const hasRadarSweepGradient = (options) => {
1027
+ var _a;
1028
+ const colors = (_a = options.gradient) == null ? void 0 : _a.colors;
1029
+ return Array.isArray(colors) && colors.length >= 2;
1030
+ };
1031
+ const createRadarSweepCanvasCoordinates = (center, radius) => {
1032
+ const north = getDestination(center, radius, 0)[1];
1033
+ const east = getDestination(center, radius, 90)[0];
1034
+ const south = getDestination(center, radius, 180)[1];
1035
+ const west = getDestination(center, radius, 270)[0];
1036
+ return [
1037
+ [west, north],
1038
+ [east, north],
1039
+ [east, south],
1040
+ [west, south]
1041
+ ];
1042
+ };
1043
+ const createRadarSweepCanvas = () => {
1044
+ const canvas = document.createElement("canvas");
1045
+ canvas.width = 768;
1046
+ canvas.height = 768;
1047
+ return canvas;
1048
+ };
1049
+ const drawRadarSweepCanvas = (canvas, options, angleOffset = 0) => {
1050
+ const ctx = canvas.getContext("2d");
1051
+ const gradient = options.gradient;
1052
+ const colors = gradient == null ? void 0 : gradient.colors;
1053
+ if (!ctx || !gradient || !colors || colors.length < 2) {
1054
+ return;
1055
+ }
1056
+ const size = canvas.width;
1057
+ const centerXY = size / 2;
1058
+ const drawRadius = centerXY - 2;
1059
+ const sweepAngle = Math.max(12, options.sweepAngle ?? 42);
1060
+ const tailOpacity = options.tailOpacity ?? 0.04;
1061
+ const headOpacity = Math.max(0.24, options.opacity ?? 0.3);
1062
+ const sweepStartAngle = (options.sweepStartAngle ?? 0) + angleOffset;
1063
+ const totalStartAngle = sweepStartAngle;
1064
+ const totalEndAngle = sweepStartAngle + sweepAngle;
1065
+ const totalSpan = sweepAngle;
1066
+ ctx.clearRect(0, 0, size, size);
1067
+ const conicGradient = ctx.createConicGradient(
1068
+ getCanvasArcAngle(totalStartAngle),
1069
+ centerXY,
1070
+ centerXY
1071
+ );
1072
+ const normalizedSpan = totalSpan / 360;
1073
+ const maxOpacity = Math.min(1, headOpacity + 0.08);
1074
+ const stopCount = 128;
1075
+ for (let i = 0; i <= stopCount; i++) {
1076
+ const t = i / stopCount;
1077
+ const eased = easeOutQuad(t);
1078
+ const opacity = tailOpacity + (maxOpacity - tailOpacity) * eased;
1079
+ const ci = t * (colors.length - 1);
1080
+ const color = lerpColorArray(colors, ci);
1081
+ conicGradient.addColorStop(
1082
+ t * normalizedSpan,
1083
+ toRgbaColor(color, Math.max(0, opacity))
1084
+ );
1085
+ }
1086
+ ctx.save();
1087
+ ctx.beginPath();
1088
+ ctx.moveTo(centerXY, centerXY);
1089
+ ctx.arc(
1090
+ centerXY,
1091
+ centerXY,
1092
+ drawRadius,
1093
+ getCanvasArcAngle(totalStartAngle),
1094
+ getCanvasArcAngle(totalEndAngle),
1095
+ false
1096
+ );
1097
+ ctx.closePath();
1098
+ ctx.fillStyle = conicGradient;
1099
+ ctx.fill();
1100
+ ctx.restore();
1101
+ };
824
1102
  const createRadarSweepData = (options, angleOffset = 0) => {
825
1103
  var _a;
826
1104
  const ringCount = Math.max(0, options.ringCount ?? 4);
827
- const trailCount = Math.max(1, options.trailCount ?? 8);
828
1105
  const sweepAngle = Math.max(12, options.sweepAngle ?? 42);
829
- const trailGap = sweepAngle * 0.22;
830
- const tailOpacity = options.tailOpacity ?? 0.04;
831
1106
  const headOpacity = options.headOpacity ?? Math.max(0.24, options.opacity ?? 0.3);
832
1107
  const sweepStartAngle = (options.sweepStartAngle ?? 0) + angleOffset;
833
1108
  const showCrosshair = options.showCrosshair !== false;
@@ -836,6 +1111,7 @@ const createRadarSweepData = (options, angleOffset = 0) => {
836
1111
  20,
837
1112
  options.coreRadius ?? Math.min(160, options.radius * 0.035)
838
1113
  );
1114
+ const useGradient = hasRadarSweepGradient(options);
839
1115
  const features = [];
840
1116
  for (let index = ringCount; index >= 1; index -= 1) {
841
1117
  const distance = options.radius * index / ringCount;
@@ -904,108 +1180,1247 @@ const createRadarSweepData = (options, angleOffset = 0) => {
904
1180
  kind: "core"
905
1181
  })
906
1182
  );
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;
1183
+ if (!useGradient) {
912
1184
  features.push(
913
1185
  createFeature(
914
- createSectorPolygon(options.center, options.radius, startAngle, endAngle),
1186
+ createSectorPolygon(
1187
+ options.center,
1188
+ options.radius,
1189
+ sweepStartAngle,
1190
+ sweepStartAngle + sweepAngle
1191
+ ),
915
1192
  {
916
1193
  kind: "sweep",
917
- opacity
1194
+ opacity: headOpacity
918
1195
  }
919
1196
  )
920
1197
  );
921
1198
  }
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
1199
  return createFeatureCollection(features);
937
1200
  };
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
1201
+ const createRadarSweepRender = (options) => {
1202
+ const useGradient = hasRadarSweepGradient(options);
1203
+ const canvas = useGradient ? createRadarSweepCanvas() : void 0;
1204
+ if (canvas) {
1205
+ drawRadarSweepCanvas(canvas, options);
1206
+ }
1207
+ const sweepLayer = canvas ? {
1208
+ id: `${options.id}-radar-trail`,
1209
+ type: "raster",
1210
+ paint: {
1211
+ "raster-opacity": 1,
1212
+ "raster-fade-duration": 0
1213
+ }
1214
+ } : {
1215
+ id: `${options.id}-radar-trail`,
1216
+ type: "fill",
1217
+ filter: ["==", ["get", "kind"], "sweep"],
1218
+ paint: {
1219
+ "fill-color": options.color || "#22c55e",
1220
+ "fill-opacity": ["coalesce", ["get", "opacity"], options.opacity ?? 0.32]
1221
+ }
1222
+ };
1223
+ return {
1224
+ data: createRadarSweepData(options),
1225
+ canvasSources: canvas ? [
1226
+ {
1227
+ id: `${options.id}-canvas-source`,
1228
+ source: {
1229
+ type: "canvas",
1230
+ canvas,
1231
+ coordinates: createRadarSweepCanvasCoordinates(
1232
+ options.center,
1233
+ options.radius
1234
+ ),
1235
+ animate: true
1236
+ }
968
1237
  }
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
1238
+ ] : void 0,
1239
+ layers: [
1240
+ sweepLayer,
1241
+ {
1242
+ id: `${options.id}-radar-rings`,
1243
+ type: "line",
1244
+ filter: ["==", ["get", "kind"], "ring"],
1245
+ paint: {
1246
+ "line-color": options.ringColor || options.color || "#22c55e",
1247
+ "line-opacity": options.ringOpacity ?? 0.95,
1248
+ "line-width": options.ringWidth ?? 2
1249
+ }
980
1250
  },
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
1251
+ {
1252
+ id: `${options.id}-radar-crosshair`,
1253
+ type: "line",
1254
+ filter: ["==", ["get", "kind"], "crosshair"],
1255
+ paint: {
1256
+ "line-color": options.crosshairColor || options.ringColor || options.color || "#22c55e",
1257
+ "line-opacity": options.crosshairOpacity ?? 0.5,
1258
+ "line-width": options.crosshairWidth ?? 1.4
1259
+ }
1260
+ },
1261
+ {
1262
+ id: `${options.id}-radar-distance-labels`,
1263
+ type: "symbol",
1264
+ filter: ["==", ["get", "kind"], "distance-label"],
1265
+ layout: {
1266
+ "text-field": ["get", "text"],
1267
+ "text-size": options.distanceLabelSize ?? 12,
1268
+ "text-anchor": ["get", "textAnchor"],
1269
+ "text-allow-overlap": true,
1270
+ "text-ignore-placement": true
1271
+ },
1272
+ paint: {
1273
+ "text-color": options.distanceLabelColor || options.ringColor || options.color || "#22c55e",
1274
+ "text-opacity": options.distanceLabelOpacity ?? 0.92,
1275
+ "text-halo-color": options.distanceLabelHaloColor || "rgba(15, 23, 42, 0.42)",
1276
+ "text-halo-width": 1
1277
+ }
986
1278
  }
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));
1279
+ ],
1280
+ startAnimation({ getOptions, setData }) {
1281
+ const startTime = performance.now();
1282
+ let frameId = 0;
1283
+ const tick = (timestamp) => {
1284
+ const currentOptions = getOptions();
1285
+ const angleOffset = (timestamp - startTime) / 1e3 * (currentOptions.rotationSpeed ?? 36);
1286
+ if (canvas) {
1287
+ drawRadarSweepCanvas(canvas, currentOptions, angleOffset);
1288
+ }
1289
+ setData(createRadarSweepData(currentOptions, angleOffset));
1290
+ frameId = requestAnimationFrame(tick);
1291
+ };
996
1292
  frameId = requestAnimationFrame(tick);
997
- };
998
- frameId = requestAnimationFrame(tick);
999
- return () => {
1000
- cancelAnimationFrame(frameId);
1001
- };
1293
+ return () => {
1294
+ cancelAnimationFrame(frameId);
1295
+ };
1296
+ }
1297
+ };
1298
+ };
1299
+ const createDefenceCircleCanvas = () => {
1300
+ const canvas = document.createElement("canvas");
1301
+ canvas.width = 512;
1302
+ canvas.height = 512;
1303
+ return canvas;
1304
+ };
1305
+ const createDefenceCircleCanvasCoordinates = (center, radius) => {
1306
+ const north = getDestination(center, radius, 0)[1];
1307
+ const east = getDestination(center, radius, 90)[0];
1308
+ const south = getDestination(center, radius, 180)[1];
1309
+ const west = getDestination(center, radius, 270)[0];
1310
+ return [
1311
+ [west, north],
1312
+ [east, north],
1313
+ [east, south],
1314
+ [west, south]
1315
+ ];
1316
+ };
1317
+ const drawDefenceCircleCanvas = (canvas, options) => {
1318
+ const ctx = canvas.getContext("2d");
1319
+ if (!ctx) {
1320
+ return;
1002
1321
  }
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);
1322
+ const size = canvas.width;
1323
+ const centerXY = size / 2;
1324
+ const drawRadius = centerXY - 1;
1325
+ const color = options.color || "#06c536";
1326
+ const fillColor = options.fillColor || color;
1327
+ const borderColor = options.borderColor || color;
1328
+ const fillOpacity = options.fillOpacity ?? 0.06;
1329
+ const borderOpacity = options.borderOpacity ?? 0.92;
1330
+ ctx.clearRect(0, 0, size, size);
1331
+ ctx.save();
1332
+ ctx.beginPath();
1333
+ ctx.arc(centerXY, centerXY, drawRadius, 0, Math.PI * 2);
1334
+ ctx.clip();
1335
+ const gradient = ctx.createRadialGradient(
1336
+ centerXY,
1337
+ centerXY,
1338
+ 0,
1339
+ centerXY,
1340
+ centerXY,
1341
+ drawRadius
1342
+ );
1343
+ const edgeGlowOpacity = Math.min(0.8, fillOpacity + (borderOpacity - fillOpacity) * 0.85);
1344
+ gradient.addColorStop(0, toRgbaColor(fillColor, fillOpacity));
1345
+ gradient.addColorStop(0.85, toRgbaColor(fillColor, fillOpacity));
1346
+ gradient.addColorStop(0.92, toRgbaColor(borderColor, fillOpacity + (edgeGlowOpacity - fillOpacity) * 0.3));
1347
+ gradient.addColorStop(0.96, toRgbaColor(borderColor, fillOpacity + (edgeGlowOpacity - fillOpacity) * 0.7));
1348
+ gradient.addColorStop(1, toRgbaColor(borderColor, edgeGlowOpacity));
1349
+ ctx.fillStyle = gradient;
1350
+ ctx.fillRect(0, 0, size, size);
1351
+ ctx.restore();
1352
+ };
1353
+ const createDefenceCircleRender = (options) => {
1354
+ const canvas = createDefenceCircleCanvas();
1355
+ drawDefenceCircleCanvas(canvas, options);
1356
+ const borderColor = options.borderColor || options.color || "#06c536";
1357
+ return {
1358
+ data: createFeatureCollection([
1359
+ createFeature(createCircleLine(options.center, options.radius), {
1360
+ kind: "defence-border"
1361
+ })
1362
+ ]),
1363
+ canvasSources: [
1364
+ {
1365
+ id: `${options.id}-canvas-source`,
1366
+ source: {
1367
+ type: "canvas",
1368
+ canvas,
1369
+ coordinates: createDefenceCircleCanvasCoordinates(
1370
+ options.center,
1371
+ options.radius
1372
+ ),
1373
+ animate: true
1374
+ }
1375
+ }
1376
+ ],
1377
+ layers: [
1378
+ {
1379
+ id: `${options.id}-defence-circle`,
1380
+ type: "raster",
1381
+ paint: {
1382
+ "raster-opacity": 1,
1383
+ "raster-fade-duration": 0
1384
+ }
1385
+ },
1386
+ {
1387
+ id: `${options.id}-defence-border`,
1388
+ type: "line",
1389
+ filter: ["==", ["get", "kind"], "defence-border"],
1390
+ paint: {
1391
+ "line-color": borderColor,
1392
+ "line-opacity": options.borderOpacity ?? 0.92,
1393
+ "line-width": options.borderWidth ?? 2
1394
+ }
1395
+ }
1396
+ ]
1397
+ };
1398
+ };
1399
+ const createCountermeasureBeamData = (options, elapsed = 0) => {
1400
+ const sourceWidth = Math.max(1, options.sourceWidth ?? 220);
1401
+ const targetWidth = Math.max(1, options.targetWidth ?? 45);
1402
+ const beam = createBeamGeometry({
1403
+ center: options.center,
1404
+ target: options.target,
1405
+ startWidth: sourceWidth,
1406
+ endWidth: targetWidth
1407
+ });
1408
+ const features = [
1409
+ createFeature(beam.polygon, { kind: "countermeasure-channel" }),
1410
+ createFeature(beam.centerLine, { kind: "countermeasure-line" })
1411
+ ];
1412
+ const distance = getDistance(options.center, options.target);
1413
+ const bearing = getBearing(options.center, options.target);
1414
+ const particleCount = Math.max(0, Math.floor(options.particleCount ?? 18));
1415
+ const particleDuration = Math.max(300, options.particleDuration ?? 1200);
1416
+ const particlePhase = elapsed % particleDuration / particleDuration;
1417
+ const particleRadius = Math.max(1, options.particleRadius ?? 2.8);
1418
+ if (distance > 0) {
1419
+ for (let index = 0; index < particleCount; index += 1) {
1420
+ const progress = (particlePhase + index / particleCount) % 1;
1421
+ features.push(
1422
+ createFeature(
1423
+ {
1424
+ type: "Point",
1425
+ coordinates: getDestination(
1426
+ options.center,
1427
+ distance * progress,
1428
+ bearing
1429
+ )
1430
+ },
1431
+ {
1432
+ kind: "countermeasure-particle",
1433
+ opacity: 0.25 + progress * 0.75,
1434
+ radius: particleRadius * (0.7 + progress * 0.3)
1435
+ }
1436
+ )
1437
+ );
1438
+ }
1439
+ }
1440
+ if (options.targetRipple !== false) {
1441
+ const rippleCount = Math.max(1, Math.floor(options.targetRippleCount ?? 2));
1442
+ const rippleRadius = Math.max(10, options.targetRippleRadius ?? 120);
1443
+ const rippleDuration = Math.max(300, options.targetRippleDuration ?? 900);
1444
+ const ripplePhase = elapsed % rippleDuration / rippleDuration;
1445
+ for (let index = 0; index < rippleCount; index += 1) {
1446
+ const progress = (ripplePhase + index / rippleCount) % 1;
1447
+ const radiusProgress = options.mode === "forcedLanding" ? 1 - progress : progress;
1448
+ features.push(
1449
+ createFeature(
1450
+ createCircleLine(
1451
+ options.target,
1452
+ Math.max(2, rippleRadius * radiusProgress)
1453
+ ),
1454
+ {
1455
+ kind: "countermeasure-target-ripple",
1456
+ opacity: Math.sin(Math.PI * progress) * 0.9
1457
+ }
1458
+ )
1459
+ );
1460
+ }
1461
+ }
1462
+ return createFeatureCollection(features);
1463
+ };
1464
+ const createCountermeasureBeamRender = (options) => {
1465
+ const color = options.color ?? "#a855f7";
1466
+ const lineColor = options.lineColor ?? "#f0abfc";
1467
+ const lineWidth = Math.max(1, options.lineWidth ?? 2.5);
1468
+ return {
1469
+ data: createCountermeasureBeamData(options),
1470
+ layers: [
1471
+ {
1472
+ id: `${options.id}-countermeasure-channel`,
1473
+ type: "fill",
1474
+ filter: ["==", ["get", "kind"], "countermeasure-channel"],
1475
+ paint: {
1476
+ "fill-color": options.channelColor ?? color,
1477
+ "fill-opacity": options.channelOpacity ?? 0.16
1478
+ }
1479
+ },
1480
+ {
1481
+ id: `${options.id}-countermeasure-glow`,
1482
+ type: "line",
1483
+ filter: ["==", ["get", "kind"], "countermeasure-line"],
1484
+ layout: {
1485
+ "line-cap": "round"
1486
+ },
1487
+ paint: {
1488
+ "line-color": lineColor,
1489
+ "line-opacity": (options.lineOpacity ?? 0.92) * 0.28,
1490
+ "line-width": lineWidth * 4,
1491
+ "line-blur": lineWidth * 1.4
1492
+ }
1493
+ },
1494
+ {
1495
+ id: `${options.id}-countermeasure-line`,
1496
+ type: "line",
1497
+ filter: ["==", ["get", "kind"], "countermeasure-line"],
1498
+ layout: {
1499
+ "line-cap": "round"
1500
+ },
1501
+ paint: {
1502
+ "line-color": lineColor,
1503
+ "line-opacity": options.lineOpacity ?? 0.92,
1504
+ "line-width": lineWidth
1505
+ }
1506
+ },
1507
+ {
1508
+ id: `${options.id}-countermeasure-target-ripple`,
1509
+ type: "line",
1510
+ filter: [
1511
+ "==",
1512
+ ["get", "kind"],
1513
+ "countermeasure-target-ripple"
1514
+ ],
1515
+ paint: {
1516
+ "line-color": color,
1517
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.9],
1518
+ "line-width": Math.max(1, options.targetRippleWidth ?? 2.5)
1519
+ }
1520
+ },
1521
+ {
1522
+ id: `${options.id}-countermeasure-particle`,
1523
+ type: "circle",
1524
+ filter: ["==", ["get", "kind"], "countermeasure-particle"],
1525
+ paint: {
1526
+ "circle-color": options.particleColor ?? lineColor,
1527
+ "circle-opacity": ["coalesce", ["get", "opacity"], 1],
1528
+ "circle-radius": ["coalesce", ["get", "radius"], 2.8],
1529
+ "circle-blur": 0.18,
1530
+ "circle-stroke-color": "#ffffff",
1531
+ "circle-stroke-opacity": 0.45,
1532
+ "circle-stroke-width": 0.5
1533
+ }
1534
+ }
1535
+ ],
1536
+ startAnimation: ({ getOptions, setData }) => {
1537
+ let frameId = 0;
1538
+ const startTime = performance.now();
1539
+ const tick = (timestamp) => {
1540
+ setData(createCountermeasureBeamData(getOptions(), timestamp - startTime));
1541
+ frameId = requestAnimationFrame(tick);
1542
+ };
1543
+ frameId = requestAnimationFrame(tick);
1544
+ return () => cancelAnimationFrame(frameId);
1545
+ }
1546
+ };
1547
+ };
1548
+ const getCoordinationColor = (type) => {
1549
+ if (type === "detection") return "#38bdf8";
1550
+ if (type === "command") return "#22d3ee";
1551
+ return "#f97316";
1552
+ };
1553
+ const createCoordinatedCountermeasureData = (options, elapsed = 0) => {
1554
+ const features = [];
1555
+ const nodeKeys = /* @__PURE__ */ new Set();
1556
+ const particleCount = Math.max(0, Math.floor(options.particleCount ?? 5));
1557
+ const particleDuration = Math.max(300, options.particleDuration ?? 1600);
1558
+ const phase = elapsed % particleDuration / particleDuration;
1559
+ const particleRadius = Math.max(1, options.particleRadius ?? 2.5);
1560
+ options.links.forEach((link, linkIndex) => {
1561
+ const color = link.color ?? getCoordinationColor(link.type);
1562
+ const distance = getDistance(link.from, link.to);
1563
+ const bearing = getBearing(link.from, link.to);
1564
+ features.push(
1565
+ createFeature(
1566
+ {
1567
+ type: "LineString",
1568
+ coordinates: [link.from, link.to]
1569
+ },
1570
+ {
1571
+ kind: "coordination-link",
1572
+ linkType: link.type,
1573
+ color
1574
+ }
1575
+ )
1576
+ );
1577
+ if (options.showNodes !== false) {
1578
+ [link.from, link.to].forEach((coordinate) => {
1579
+ const key = `${coordinate[0]},${coordinate[1]}`;
1580
+ if (nodeKeys.has(key)) return;
1581
+ nodeKeys.add(key);
1582
+ features.push(
1583
+ createFeature(
1584
+ { type: "Point", coordinates: coordinate },
1585
+ { kind: "coordination-node", color }
1586
+ )
1587
+ );
1588
+ });
1589
+ }
1590
+ if (distance <= 0) return;
1591
+ for (let index = 0; index < particleCount; index += 1) {
1592
+ const progress = (phase + index / particleCount + linkIndex / options.links.length) % 1;
1593
+ features.push(
1594
+ createFeature(
1595
+ {
1596
+ type: "Point",
1597
+ coordinates: getDestination(link.from, distance * progress, bearing)
1598
+ },
1599
+ {
1600
+ kind: "coordination-particle",
1601
+ color,
1602
+ opacity: 0.35 + progress * 0.65,
1603
+ radius: particleRadius
1604
+ }
1605
+ )
1606
+ );
1607
+ }
1608
+ });
1609
+ if (options.targetRipple !== false) {
1610
+ const duration = Math.max(300, options.targetRippleDuration ?? 1200);
1611
+ const progress = elapsed % duration / duration;
1612
+ const radius = Math.max(10, options.targetRippleRadius ?? 100) * progress;
1613
+ features.push(
1614
+ createFeature(createCircleLine(options.center, Math.max(2, radius)), {
1615
+ kind: "coordination-target-ripple",
1616
+ opacity: (1 - progress) * 0.9
1617
+ })
1618
+ );
1619
+ }
1620
+ return createFeatureCollection(features);
1621
+ };
1622
+ const createCoordinatedCountermeasureRender = (options) => {
1623
+ const lineWidth = Math.max(1, options.lineWidth ?? 2.2);
1624
+ const lineOpacity = options.lineOpacity ?? 0.9;
1625
+ return {
1626
+ data: createCoordinatedCountermeasureData(options),
1627
+ layers: [
1628
+ {
1629
+ id: `${options.id}-coordination-detection`,
1630
+ type: "line",
1631
+ filter: ["==", ["get", "linkType"], "detection"],
1632
+ paint: {
1633
+ "line-color": ["coalesce", ["get", "color"], "#38bdf8"],
1634
+ "line-opacity": lineOpacity,
1635
+ "line-width": lineWidth,
1636
+ "line-dasharray": [2, 2]
1637
+ }
1638
+ },
1639
+ {
1640
+ id: `${options.id}-coordination-command`,
1641
+ type: "line",
1642
+ filter: ["==", ["get", "linkType"], "command"],
1643
+ paint: {
1644
+ "line-color": ["coalesce", ["get", "color"], "#22d3ee"],
1645
+ "line-opacity": lineOpacity,
1646
+ "line-width": lineWidth,
1647
+ "line-dasharray": [0.5, 1.8]
1648
+ }
1649
+ },
1650
+ {
1651
+ id: `${options.id}-coordination-countermeasure`,
1652
+ type: "line",
1653
+ filter: ["==", ["get", "linkType"], "countermeasure"],
1654
+ paint: {
1655
+ "line-color": ["coalesce", ["get", "color"], "#f97316"],
1656
+ "line-opacity": lineOpacity,
1657
+ "line-width": lineWidth + 0.8
1658
+ }
1659
+ },
1660
+ {
1661
+ id: `${options.id}-coordination-target-ripple`,
1662
+ type: "line",
1663
+ filter: ["==", ["get", "kind"], "coordination-target-ripple"],
1664
+ paint: {
1665
+ "line-color": "#ff3b30",
1666
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.9],
1667
+ "line-width": 2.5
1668
+ }
1669
+ },
1670
+ {
1671
+ id: `${options.id}-coordination-node`,
1672
+ type: "circle",
1673
+ filter: ["==", ["get", "kind"], "coordination-node"],
1674
+ paint: {
1675
+ "circle-color": ["coalesce", ["get", "color"], "#ffffff"],
1676
+ "circle-radius": Math.max(2, options.nodeRadius ?? 5),
1677
+ "circle-stroke-color": "#ffffff",
1678
+ "circle-stroke-width": 1.5
1679
+ }
1680
+ },
1681
+ {
1682
+ id: `${options.id}-coordination-particle`,
1683
+ type: "circle",
1684
+ filter: ["==", ["get", "kind"], "coordination-particle"],
1685
+ paint: {
1686
+ "circle-color": ["coalesce", ["get", "color"], "#ffffff"],
1687
+ "circle-opacity": ["coalesce", ["get", "opacity"], 1],
1688
+ "circle-radius": ["coalesce", ["get", "radius"], 2.5],
1689
+ "circle-blur": 0.1
1690
+ }
1691
+ }
1692
+ ],
1693
+ startAnimation: ({ getOptions, setData }) => {
1694
+ let frameId = 0;
1695
+ const startTime = performance.now();
1696
+ const tick = (timestamp) => {
1697
+ setData(
1698
+ createCoordinatedCountermeasureData(
1699
+ getOptions(),
1700
+ timestamp - startTime
1701
+ )
1702
+ );
1703
+ frameId = requestAnimationFrame(tick);
1704
+ };
1705
+ frameId = requestAnimationFrame(tick);
1706
+ return () => cancelAnimationFrame(frameId);
1707
+ }
1708
+ };
1709
+ };
1710
+ const createNavigationSpoofingData = (options, elapsed = 0) => {
1711
+ const features = [
1712
+ createFeature(
1713
+ {
1714
+ type: "LineString",
1715
+ coordinates: [options.center, options.spoofedPosition]
1716
+ },
1717
+ { kind: "spoofing-offset" }
1718
+ )
1719
+ ];
1720
+ const distance = getDistance(options.center, options.spoofedPosition);
1721
+ const bearing = getBearing(options.center, options.spoofedPosition);
1722
+ const particleCount = Math.max(0, Math.floor(options.particleCount ?? 10));
1723
+ const particleDuration = Math.max(300, options.particleDuration ?? 1800);
1724
+ const phase = elapsed % particleDuration / particleDuration;
1725
+ if (options.showTruePosition !== false) {
1726
+ features.push(
1727
+ createFeature(
1728
+ { type: "Point", coordinates: options.center },
1729
+ { kind: "spoofing-true-position" }
1730
+ )
1731
+ );
1732
+ }
1733
+ if (options.showSpoofedPosition !== false) {
1734
+ features.push(
1735
+ createFeature(
1736
+ { type: "Point", coordinates: options.spoofedPosition },
1737
+ { kind: "spoofing-ghost" }
1738
+ )
1739
+ );
1740
+ }
1741
+ if (distance > 0) {
1742
+ for (let index = 0; index < particleCount; index += 1) {
1743
+ const progress = (phase + index / particleCount) % 1;
1744
+ features.push(
1745
+ createFeature(
1746
+ {
1747
+ type: "Point",
1748
+ coordinates: getDestination(
1749
+ options.center,
1750
+ distance * progress,
1751
+ bearing
1752
+ )
1753
+ },
1754
+ {
1755
+ kind: "spoofing-particle",
1756
+ opacity: 0.25 + progress * 0.75
1757
+ }
1758
+ )
1759
+ );
1760
+ }
1761
+ }
1762
+ if (options.ripple !== false) {
1763
+ const count = Math.max(1, Math.floor(options.rippleCount ?? 2));
1764
+ const duration = Math.max(300, options.rippleDuration ?? 1200);
1765
+ const radius = Math.max(10, options.rippleRadius ?? 120);
1766
+ const ripplePhase = elapsed % duration / duration;
1767
+ for (let index = 0; index < count; index += 1) {
1768
+ const progress = (ripplePhase + index / count) % 1;
1769
+ features.push(
1770
+ createFeature(
1771
+ createCircleLine(
1772
+ options.spoofedPosition,
1773
+ Math.max(2, radius * progress)
1774
+ ),
1775
+ {
1776
+ kind: "spoofing-ripple",
1777
+ opacity: (1 - progress) * 0.75
1778
+ }
1779
+ )
1780
+ );
1781
+ }
1782
+ }
1783
+ return createFeatureCollection(features);
1784
+ };
1785
+ const createNavigationSpoofingRender = (options) => {
1786
+ const color = options.color ?? "#8b5cf6";
1787
+ return {
1788
+ data: createNavigationSpoofingData(options),
1789
+ layers: [
1790
+ {
1791
+ id: `${options.id}-spoofing-offset`,
1792
+ type: "line",
1793
+ filter: ["==", ["get", "kind"], "spoofing-offset"],
1794
+ layout: { "line-cap": "round" },
1795
+ paint: {
1796
+ "line-color": options.lineColor ?? color,
1797
+ "line-opacity": options.lineOpacity ?? 0.82,
1798
+ "line-width": Math.max(1, options.lineWidth ?? 2),
1799
+ "line-dasharray": [1.2, 2]
1800
+ }
1801
+ },
1802
+ {
1803
+ id: `${options.id}-spoofing-ripple`,
1804
+ type: "line",
1805
+ filter: ["==", ["get", "kind"], "spoofing-ripple"],
1806
+ paint: {
1807
+ "line-color": color,
1808
+ "line-opacity": ["coalesce", ["get", "opacity"], 0.75],
1809
+ "line-width": 2
1810
+ }
1811
+ },
1812
+ {
1813
+ id: `${options.id}-spoofing-true-position`,
1814
+ type: "circle",
1815
+ filter: ["==", ["get", "kind"], "spoofing-true-position"],
1816
+ paint: {
1817
+ "circle-color": "#ffffff",
1818
+ "circle-radius": 5,
1819
+ "circle-stroke-color": color,
1820
+ "circle-stroke-width": 2
1821
+ }
1822
+ },
1823
+ {
1824
+ id: `${options.id}-spoofing-ghost`,
1825
+ type: "circle",
1826
+ filter: ["==", ["get", "kind"], "spoofing-ghost"],
1827
+ paint: {
1828
+ "circle-color": color,
1829
+ "circle-opacity": options.ghostOpacity ?? 0.42,
1830
+ "circle-radius": Math.max(2, options.ghostRadius ?? 8),
1831
+ "circle-stroke-color": "#ffffff",
1832
+ "circle-stroke-opacity": 0.8,
1833
+ "circle-stroke-width": 1.5
1834
+ }
1835
+ },
1836
+ {
1837
+ id: `${options.id}-spoofing-particle`,
1838
+ type: "circle",
1839
+ filter: ["==", ["get", "kind"], "spoofing-particle"],
1840
+ paint: {
1841
+ "circle-color": color,
1842
+ "circle-opacity": ["coalesce", ["get", "opacity"], 1],
1843
+ "circle-radius": Math.max(1, options.particleRadius ?? 2.5),
1844
+ "circle-blur": 0.12
1845
+ }
1846
+ }
1847
+ ],
1848
+ startAnimation: ({ getOptions, setData }) => {
1849
+ let frameId = 0;
1850
+ const startTime = performance.now();
1851
+ const tick = (timestamp) => {
1852
+ setData(createNavigationSpoofingData(getOptions(), timestamp - startTime));
1853
+ frameId = requestAnimationFrame(tick);
1854
+ };
1855
+ frameId = requestAnimationFrame(tick);
1856
+ return () => cancelAnimationFrame(frameId);
1857
+ }
1858
+ };
1859
+ };
1860
+ const addPulseMarker = (map, options) => createManagedEffect(map, options, createPulseMarkerRender);
1861
+ const addBreathingCircle = (map, options) => createManagedEffect(map, options, createBreathingCircleRender);
1862
+ const addRingPulseMarker = (map, options) => createManagedEffect(map, options, createRingPulseMarkerRender);
1863
+ const addSectorScan = (map, options) => createManagedEffect(map, options, createSectorScanRender);
1007
1864
  const addDirectionalPulse = (map, options) => createManagedEffect(map, options, createDirectionalPulseRender);
1008
1865
  const addRadarSweep = (map, options) => createManagedEffect(map, options, createRadarSweepRender);
1866
+ const addDefenceCircle = (map, options) => createManagedEffect(map, options, createDefenceCircleRender);
1867
+ const addCountermeasureBeam = (map, options) => createManagedEffect(map, options, createCountermeasureBeamRender);
1868
+ const addCoordinatedCountermeasure = (map, options) => createManagedEffect(map, options, createCoordinatedCountermeasureRender);
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(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
+ };
1009
2424
  const resolveStyle = (style, tdtToken) => {
1010
2425
  if (!style) {
1011
2426
  return getBaseMapStyle("openfreemap-liberty");
@@ -1028,11 +2443,19 @@ export {
1028
2443
  OPENFREEMAP_STYLE_BASE_URL,
1029
2444
  OPENFREEMAP_STYLE_TYPES,
1030
2445
  TDT_STYLE_TYPES,
2446
+ addBreachAlert,
2447
+ addBreathingCircle,
2448
+ addCoordinatedCountermeasure,
2449
+ addCountermeasureBeam,
2450
+ addDefenceCircle,
1031
2451
  addDirectionalPulse,
2452
+ addNavigationSpoofing,
1032
2453
  addPulseMarker,
1033
2454
  addRadarSweep,
1034
2455
  addRingPulseMarker,
1035
2456
  addSectorScan,
2457
+ addTargetLock,
2458
+ createFenceCircle,
1036
2459
  createMap,
1037
2460
  createTiandituImageStyle,
1038
2461
  createTiandituRasterSource,