@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/README.md +81 -6
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1546 -123
- package/dist/index.umd.js +1 -1
- package/dist/types/basemaps/index.d.ts +5 -1
- package/dist/types/basemaps/tianditu.d.ts +7 -4
- package/dist/types/effects/geometry.d.ts +2 -0
- package/dist/types/effects/index.d.ts +2 -2
- package/dist/types/effects/low-altitude.d.ts +327 -12
- package/dist/types/index.d.ts +2 -2
- package/dist/types/map/create-map.d.ts +4 -0
- 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 = (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
|
|
300
|
-
if (!waitingForStyle
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
430
|
+
ensureRender();
|
|
431
|
+
};
|
|
432
|
+
const requestRender = () => {
|
|
433
|
+
if (removed) {
|
|
434
|
+
return;
|
|
321
435
|
}
|
|
322
|
-
|
|
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
|
|
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 ??
|
|
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:
|
|
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 ??
|
|
617
|
-
const edgeOpacity = options.gradient.edgeOpacity ??
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
"
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
"
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
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,
|