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