@bitalltech-maplibre/core 1.0.0
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 +120 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.esm.js +1046 -0
- package/dist/index.umd.js +1 -0
- package/dist/types/basemaps/index.d.ts +16 -0
- package/dist/types/basemaps/openfreemap.d.ts +11 -0
- package/dist/types/basemaps/tianditu.d.ts +15 -0
- package/dist/types/config.d.ts +8 -0
- package/dist/types/effects/geometry.d.ts +32 -0
- package/dist/types/effects/index.d.ts +2 -0
- package/dist/types/effects/low-altitude.d.ts +126 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/map/create-map.d.ts +11 -0
- package/dist/types/map/index.d.ts +2 -0
- package/package.json +37 -0
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
import maplibregl from "maplibre-gl";
|
|
2
|
+
import { default as default2 } from "maplibre-gl";
|
|
3
|
+
const config = {};
|
|
4
|
+
const setMaplibreToolsConfig = (options) => {
|
|
5
|
+
Object.assign(config, options);
|
|
6
|
+
};
|
|
7
|
+
const getMaplibreToolsConfig = () => ({ ...config });
|
|
8
|
+
const OPENFREEMAP_STYLE_BASE_URL = "https://tiles.openfreemap.org/styles";
|
|
9
|
+
const createOpenFreeMapStyle = (type, label, path) => ({
|
|
10
|
+
type,
|
|
11
|
+
label,
|
|
12
|
+
style: `${OPENFREEMAP_STYLE_BASE_URL}/${path}`
|
|
13
|
+
});
|
|
14
|
+
const OPENFREEMAP_STYLES = {
|
|
15
|
+
"openfreemap-liberty": createOpenFreeMapStyle(
|
|
16
|
+
"openfreemap-liberty",
|
|
17
|
+
"OpenFreeMap Liberty",
|
|
18
|
+
"liberty"
|
|
19
|
+
),
|
|
20
|
+
"openfreemap-bright": createOpenFreeMapStyle(
|
|
21
|
+
"openfreemap-bright",
|
|
22
|
+
"OpenFreeMap Bright",
|
|
23
|
+
"bright"
|
|
24
|
+
),
|
|
25
|
+
"openfreemap-positron": createOpenFreeMapStyle(
|
|
26
|
+
"openfreemap-positron",
|
|
27
|
+
"OpenFreeMap Positron",
|
|
28
|
+
"positron"
|
|
29
|
+
),
|
|
30
|
+
"openfreemap-dark": createOpenFreeMapStyle(
|
|
31
|
+
"openfreemap-dark",
|
|
32
|
+
"OpenFreeMap Dark",
|
|
33
|
+
"dark"
|
|
34
|
+
),
|
|
35
|
+
"openfreemap-fiord": createOpenFreeMapStyle(
|
|
36
|
+
"openfreemap-fiord",
|
|
37
|
+
"OpenFreeMap Fiord",
|
|
38
|
+
"fiord"
|
|
39
|
+
)
|
|
40
|
+
};
|
|
41
|
+
const OPENFREEMAP_STYLE_TYPES = Object.keys(
|
|
42
|
+
OPENFREEMAP_STYLES
|
|
43
|
+
);
|
|
44
|
+
const getOpenFreeMapStyle = (type = "openfreemap-liberty") => OPENFREEMAP_STYLES[type].style;
|
|
45
|
+
const TDT_SUBDOMAINS = ["0", "1", "2", "3", "4", "5", "6", "7"];
|
|
46
|
+
const TDT_ATTRIBUTION = "© 天地图";
|
|
47
|
+
const TDT_MAX_ZOOM = 18;
|
|
48
|
+
const TDT_STYLE_TYPES = ["tdt-image", "tdt-image-label"];
|
|
49
|
+
const getToken = (token) => {
|
|
50
|
+
const tdtToken = token || getMaplibreToolsConfig().tdtToken;
|
|
51
|
+
if (!tdtToken) {
|
|
52
|
+
throw new Error("Tianditu token is required. Pass token or call setMaplibreToolsConfig({ tdtToken }).");
|
|
53
|
+
}
|
|
54
|
+
return tdtToken;
|
|
55
|
+
};
|
|
56
|
+
const createTdtTileUrls = (tileType, token) => {
|
|
57
|
+
const tdtToken = getToken(token);
|
|
58
|
+
return TDT_SUBDOMAINS.map(
|
|
59
|
+
(subdomain) => `https://t${subdomain}.tianditu.gov.cn/DataServer?T=${tileType}&x={x}&y={y}&l={z}&tk=${tdtToken}`
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
const createTiandituRasterSource = (tileType, options = {}) => ({
|
|
63
|
+
type: "raster",
|
|
64
|
+
tiles: createTdtTileUrls(tileType, options.token),
|
|
65
|
+
tileSize: 256,
|
|
66
|
+
minzoom: 0,
|
|
67
|
+
maxzoom: TDT_MAX_ZOOM,
|
|
68
|
+
attribution: TDT_ATTRIBUTION
|
|
69
|
+
});
|
|
70
|
+
const createTiandituImageStyle = (options = {}) => {
|
|
71
|
+
const withLabel = options.label !== false;
|
|
72
|
+
const sources = {
|
|
73
|
+
"tdt-image": createTiandituRasterSource("img_w", options)
|
|
74
|
+
};
|
|
75
|
+
const layers = [
|
|
76
|
+
{
|
|
77
|
+
id: "tdt-image",
|
|
78
|
+
type: "raster",
|
|
79
|
+
source: "tdt-image"
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
if (withLabel) {
|
|
83
|
+
sources["tdt-image-label"] = createTiandituRasterSource("cia_w", options);
|
|
84
|
+
layers.push({
|
|
85
|
+
id: "tdt-image-label",
|
|
86
|
+
type: "raster",
|
|
87
|
+
source: "tdt-image-label"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
version: 8,
|
|
92
|
+
sources,
|
|
93
|
+
layers
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
const BASE_MAP_TYPES = [
|
|
97
|
+
...Object.keys(OPENFREEMAP_STYLES),
|
|
98
|
+
"tdt-image",
|
|
99
|
+
"tdt-image-label"
|
|
100
|
+
];
|
|
101
|
+
const isBaseMapType = (type) => BASE_MAP_TYPES.includes(type);
|
|
102
|
+
const getBaseMapStyle = (type = "openfreemap-liberty", options = {}) => {
|
|
103
|
+
if (type === "tdt-image") {
|
|
104
|
+
return createTiandituImageStyle({ ...options, label: false });
|
|
105
|
+
}
|
|
106
|
+
if (type === "tdt-image-label") {
|
|
107
|
+
return createTiandituImageStyle({ ...options, label: true });
|
|
108
|
+
}
|
|
109
|
+
return getOpenFreeMapStyle(type);
|
|
110
|
+
};
|
|
111
|
+
const setBaseMap = (map, type, options = {}) => {
|
|
112
|
+
const { diff = false, ...styleOptions } = options;
|
|
113
|
+
map.setStyle(getBaseMapStyle(type, styleOptions), { diff });
|
|
114
|
+
};
|
|
115
|
+
const EARTH_RADIUS = 6378137;
|
|
116
|
+
const DEFAULT_SEGMENTS = 64;
|
|
117
|
+
const toRadians = (value) => value * Math.PI / 180;
|
|
118
|
+
const toDegrees = (value) => value * 180 / Math.PI;
|
|
119
|
+
const normalizeLongitude = (value) => {
|
|
120
|
+
if (value > 180) {
|
|
121
|
+
return value - 360;
|
|
122
|
+
}
|
|
123
|
+
if (value < -180) {
|
|
124
|
+
return value + 360;
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
};
|
|
128
|
+
const normalizeAngle = (angle) => (angle % 360 + 360) % 360;
|
|
129
|
+
const getSweepAngle = (startAngle, endAngle) => {
|
|
130
|
+
const sweep = normalizeAngle(endAngle) - normalizeAngle(startAngle);
|
|
131
|
+
return sweep <= 0 ? sweep + 360 : sweep;
|
|
132
|
+
};
|
|
133
|
+
const getDestination = (center, distance, bearing) => {
|
|
134
|
+
const angularDistance = distance / EARTH_RADIUS;
|
|
135
|
+
const bearingRadians = toRadians(bearing);
|
|
136
|
+
const latitudeRadians = toRadians(center[1]);
|
|
137
|
+
const longitudeRadians = toRadians(center[0]);
|
|
138
|
+
const nextLatitude = Math.asin(
|
|
139
|
+
Math.sin(latitudeRadians) * Math.cos(angularDistance) + Math.cos(latitudeRadians) * Math.sin(angularDistance) * Math.cos(bearingRadians)
|
|
140
|
+
);
|
|
141
|
+
const nextLongitude = longitudeRadians + Math.atan2(
|
|
142
|
+
Math.sin(bearingRadians) * Math.sin(angularDistance) * Math.cos(latitudeRadians),
|
|
143
|
+
Math.cos(angularDistance) - Math.sin(latitudeRadians) * Math.sin(nextLatitude)
|
|
144
|
+
);
|
|
145
|
+
return [
|
|
146
|
+
normalizeLongitude(toDegrees(nextLongitude)),
|
|
147
|
+
toDegrees(nextLatitude)
|
|
148
|
+
];
|
|
149
|
+
};
|
|
150
|
+
const createAngleStops = (startAngle, endAngle, segmentCount = DEFAULT_SEGMENTS) => {
|
|
151
|
+
const sweep = getSweepAngle(startAngle, endAngle);
|
|
152
|
+
const segments = Math.max(12, Math.ceil(segmentCount * sweep / 360));
|
|
153
|
+
return Array.from(
|
|
154
|
+
{ length: segments + 1 },
|
|
155
|
+
(_value, index) => normalizeAngle(startAngle + sweep * index / segments)
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
const closeCoordinates = (coordinates) => {
|
|
159
|
+
if (coordinates.length === 0) {
|
|
160
|
+
return coordinates;
|
|
161
|
+
}
|
|
162
|
+
const [firstLongitude, firstLatitude] = coordinates[0];
|
|
163
|
+
const lastCoordinate = coordinates[coordinates.length - 1];
|
|
164
|
+
if (lastCoordinate[0] === firstLongitude && lastCoordinate[1] === firstLatitude) {
|
|
165
|
+
return coordinates;
|
|
166
|
+
}
|
|
167
|
+
return [...coordinates, [firstLongitude, firstLatitude]];
|
|
168
|
+
};
|
|
169
|
+
const createCircleCoordinates = (center, radius, segmentCount = DEFAULT_SEGMENTS) => closeCoordinates(
|
|
170
|
+
Array.from(
|
|
171
|
+
{ length: segmentCount },
|
|
172
|
+
(_value, index) => getDestination(center, radius, 360 * index / segmentCount)
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
const createCirclePolygon = (center, radius, segmentCount = DEFAULT_SEGMENTS) => ({
|
|
176
|
+
type: "Polygon",
|
|
177
|
+
coordinates: [createCircleCoordinates(center, radius, segmentCount)]
|
|
178
|
+
});
|
|
179
|
+
const createCircleLine = (center, radius, segmentCount = DEFAULT_SEGMENTS) => ({
|
|
180
|
+
type: "LineString",
|
|
181
|
+
coordinates: createCircleCoordinates(center, radius, segmentCount)
|
|
182
|
+
});
|
|
183
|
+
const createSectorPolygon = (center, radius, startAngle, endAngle, segmentCount = DEFAULT_SEGMENTS) => {
|
|
184
|
+
const arcCoordinates = createAngleStops(
|
|
185
|
+
startAngle,
|
|
186
|
+
endAngle,
|
|
187
|
+
segmentCount
|
|
188
|
+
).map((bearing) => getDestination(center, radius, bearing));
|
|
189
|
+
return {
|
|
190
|
+
type: "Polygon",
|
|
191
|
+
coordinates: [[center, ...arcCoordinates, center]]
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const createArcLine = (center, radius, startAngle, endAngle, segmentCount = DEFAULT_SEGMENTS) => ({
|
|
195
|
+
type: "LineString",
|
|
196
|
+
coordinates: createAngleStops(startAngle, endAngle, segmentCount).map(
|
|
197
|
+
(bearing) => getDestination(center, radius, bearing)
|
|
198
|
+
)
|
|
199
|
+
});
|
|
200
|
+
const createRayLine = (center, radius, bearing) => ({
|
|
201
|
+
type: "LineString",
|
|
202
|
+
coordinates: [center, getDestination(center, radius, bearing)]
|
|
203
|
+
});
|
|
204
|
+
const createFeature = (geometry, properties) => ({
|
|
205
|
+
type: "Feature",
|
|
206
|
+
geometry,
|
|
207
|
+
properties
|
|
208
|
+
});
|
|
209
|
+
const createFeatureCollection = (features) => ({
|
|
210
|
+
type: "FeatureCollection",
|
|
211
|
+
features
|
|
212
|
+
});
|
|
213
|
+
const removeLayerIfExists = (map, id) => {
|
|
214
|
+
if (map.getLayer(id)) {
|
|
215
|
+
map.removeLayer(id);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const removeSourceIfExists = (map, id) => {
|
|
219
|
+
if (map.getSource(id)) {
|
|
220
|
+
map.removeSource(id);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const setLayersVisibility = (map, layerIds, visible) => {
|
|
224
|
+
const visibility = visible ? "visible" : "none";
|
|
225
|
+
layerIds.forEach((layerId) => {
|
|
226
|
+
if (map.getLayer(layerId)) {
|
|
227
|
+
map.setLayoutProperty(layerId, "visibility", visibility);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const createManagedEffect = (map, initialOptions, renderEffect) => {
|
|
232
|
+
const effectId = initialOptions.id;
|
|
233
|
+
const sourceId = `${effectId}-source`;
|
|
234
|
+
let options = { ...initialOptions };
|
|
235
|
+
let layerIds = [];
|
|
236
|
+
let sourceIds = [sourceId];
|
|
237
|
+
let removed = false;
|
|
238
|
+
let waitingForStyle = false;
|
|
239
|
+
let disposeAnimation;
|
|
240
|
+
const stopAnimation = () => {
|
|
241
|
+
disposeAnimation == null ? void 0 : disposeAnimation();
|
|
242
|
+
disposeAnimation = void 0;
|
|
243
|
+
};
|
|
244
|
+
const clearEffect = () => {
|
|
245
|
+
stopAnimation();
|
|
246
|
+
[...layerIds].reverse().forEach((layerId) => {
|
|
247
|
+
removeLayerIfExists(map, layerId);
|
|
248
|
+
});
|
|
249
|
+
[...sourceIds].reverse().forEach((id) => {
|
|
250
|
+
removeSourceIfExists(map, id);
|
|
251
|
+
});
|
|
252
|
+
layerIds = [];
|
|
253
|
+
sourceIds = [sourceId];
|
|
254
|
+
};
|
|
255
|
+
const setData = (data) => {
|
|
256
|
+
const source = map.getSource(sourceId);
|
|
257
|
+
if (source) {
|
|
258
|
+
source.setData(data);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const renderNow = () => {
|
|
262
|
+
var _a, _b;
|
|
263
|
+
if (removed) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
clearEffect();
|
|
267
|
+
const nextRender = renderEffect(options);
|
|
268
|
+
map.addSource(sourceId, {
|
|
269
|
+
type: "geojson",
|
|
270
|
+
data: nextRender.data
|
|
271
|
+
});
|
|
272
|
+
(_a = nextRender.canvasSources) == null ? void 0 : _a.forEach((canvasSource) => {
|
|
273
|
+
map.addSource(canvasSource.id, canvasSource.source);
|
|
274
|
+
});
|
|
275
|
+
sourceIds = [
|
|
276
|
+
sourceId,
|
|
277
|
+
...((_b = nextRender.canvasSources) == null ? void 0 : _b.map((canvasSource) => canvasSource.id)) || []
|
|
278
|
+
];
|
|
279
|
+
layerIds = nextRender.layers.map((layer) => layer.id);
|
|
280
|
+
nextRender.layers.forEach((layer) => {
|
|
281
|
+
var _a2;
|
|
282
|
+
const layerSource = layer.type === "raster" && ((_a2 = nextRender.canvasSources) == null ? void 0 : _a2[0]) ? nextRender.canvasSources[0].id : sourceId;
|
|
283
|
+
map.addLayer(
|
|
284
|
+
{
|
|
285
|
+
...layer,
|
|
286
|
+
source: layerSource
|
|
287
|
+
},
|
|
288
|
+
options.beforeId
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
setLayersVisibility(map, layerIds, options.visible !== false);
|
|
292
|
+
if (nextRender.startAnimation) {
|
|
293
|
+
disposeAnimation = nextRender.startAnimation({
|
|
294
|
+
getOptions: () => options,
|
|
295
|
+
setData
|
|
296
|
+
}) || void 0;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const handleStyleData = () => {
|
|
300
|
+
if (!waitingForStyle || removed || !map.isStyleLoaded()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
waitingForStyle = false;
|
|
304
|
+
map.off("styledata", handleStyleData);
|
|
305
|
+
renderNow();
|
|
306
|
+
};
|
|
307
|
+
const requestRender = () => {
|
|
308
|
+
if (removed) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!map.isStyleLoaded()) {
|
|
312
|
+
if (!waitingForStyle) {
|
|
313
|
+
waitingForStyle = true;
|
|
314
|
+
map.on("styledata", handleStyleData);
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (waitingForStyle) {
|
|
319
|
+
waitingForStyle = false;
|
|
320
|
+
map.off("styledata", handleStyleData);
|
|
321
|
+
}
|
|
322
|
+
renderNow();
|
|
323
|
+
};
|
|
324
|
+
requestRender();
|
|
325
|
+
return {
|
|
326
|
+
id: effectId,
|
|
327
|
+
update(nextOptions) {
|
|
328
|
+
if (removed) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
options = {
|
|
332
|
+
...options,
|
|
333
|
+
...nextOptions,
|
|
334
|
+
id: effectId
|
|
335
|
+
};
|
|
336
|
+
requestRender();
|
|
337
|
+
},
|
|
338
|
+
show() {
|
|
339
|
+
if (removed) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
options = {
|
|
343
|
+
...options,
|
|
344
|
+
visible: true
|
|
345
|
+
};
|
|
346
|
+
setLayersVisibility(map, layerIds, true);
|
|
347
|
+
},
|
|
348
|
+
hide() {
|
|
349
|
+
if (removed) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
options = {
|
|
353
|
+
...options,
|
|
354
|
+
visible: false
|
|
355
|
+
};
|
|
356
|
+
setLayersVisibility(map, layerIds, false);
|
|
357
|
+
},
|
|
358
|
+
remove() {
|
|
359
|
+
if (removed) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
removed = true;
|
|
363
|
+
if (waitingForStyle) {
|
|
364
|
+
waitingForStyle = false;
|
|
365
|
+
map.off("styledata", handleStyleData);
|
|
366
|
+
}
|
|
367
|
+
clearEffect();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
const createPulseEffectData = (options, phase = 0) => {
|
|
372
|
+
const baseRadius = Math.max(10, options.radius ?? 70);
|
|
373
|
+
const maxRadius = Math.max(baseRadius, options.maxRadius ?? baseRadius * 2.3);
|
|
374
|
+
const pulseCount = Math.max(1, options.pulseCount ?? 2);
|
|
375
|
+
const lineWidth = Math.max(1, options.lineWidth ?? 2);
|
|
376
|
+
const pulseOpacity = options.pulseOpacity ?? 0.9;
|
|
377
|
+
const features = [
|
|
378
|
+
createFeature(createCirclePolygon(options.center, baseRadius), {
|
|
379
|
+
kind: "core"
|
|
380
|
+
})
|
|
381
|
+
];
|
|
382
|
+
for (let index = 0; index < pulseCount; index += 1) {
|
|
383
|
+
const progress = (phase + index / pulseCount) % 1;
|
|
384
|
+
const currentRadius = baseRadius + (maxRadius - baseRadius) * progress;
|
|
385
|
+
features.push(
|
|
386
|
+
createFeature(createCircleLine(options.center, currentRadius), {
|
|
387
|
+
kind: "pulse",
|
|
388
|
+
opacity: pulseOpacity * (1 - progress),
|
|
389
|
+
width: lineWidth * (1 - progress * 0.35)
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return createFeatureCollection(features);
|
|
394
|
+
};
|
|
395
|
+
const createPulseMarkerRender = (options) => {
|
|
396
|
+
const color = options.color || "#ff3b30";
|
|
397
|
+
const fillColor = options.fillColor || color;
|
|
398
|
+
return {
|
|
399
|
+
data: createPulseEffectData(options),
|
|
400
|
+
layers: [
|
|
401
|
+
{
|
|
402
|
+
id: `${options.id}-core`,
|
|
403
|
+
type: "fill",
|
|
404
|
+
filter: ["==", ["get", "kind"], "core"],
|
|
405
|
+
paint: {
|
|
406
|
+
"fill-color": fillColor,
|
|
407
|
+
"fill-opacity": options.fillOpacity ?? 0.28
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: `${options.id}-pulse`,
|
|
412
|
+
type: "line",
|
|
413
|
+
filter: ["==", ["get", "kind"], "pulse"],
|
|
414
|
+
paint: {
|
|
415
|
+
"line-color": color,
|
|
416
|
+
"line-opacity": ["coalesce", ["get", "opacity"], options.pulseOpacity ?? 0.9],
|
|
417
|
+
"line-width": ["coalesce", ["get", "width"], options.lineWidth ?? 2]
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
],
|
|
421
|
+
startAnimation({ getOptions, setData }) {
|
|
422
|
+
const duration = Math.max(400, getOptions().duration ?? 1800);
|
|
423
|
+
let frameId = 0;
|
|
424
|
+
const startTime = performance.now();
|
|
425
|
+
const tick = (timestamp) => {
|
|
426
|
+
const currentOptions = getOptions();
|
|
427
|
+
const phase = (timestamp - startTime) % duration / duration;
|
|
428
|
+
setData(createPulseEffectData(currentOptions, phase));
|
|
429
|
+
frameId = requestAnimationFrame(tick);
|
|
430
|
+
};
|
|
431
|
+
frameId = requestAnimationFrame(tick);
|
|
432
|
+
return () => {
|
|
433
|
+
cancelAnimationFrame(frameId);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
const createRingPulseEffectData = (options, phase = 0) => {
|
|
439
|
+
var _a, _b;
|
|
440
|
+
const baseRadius = Math.max(16, options.radius ?? 110);
|
|
441
|
+
const pulseScale = Math.max(0, options.pulseScale ?? 0.16);
|
|
442
|
+
const pulseProgress = (1 - Math.cos(phase * Math.PI * 2)) / 2;
|
|
443
|
+
const currentRadius = baseRadius * (1 + pulseScale * pulseProgress);
|
|
444
|
+
const haloRadius = currentRadius * 1.28;
|
|
445
|
+
const ringCount = Math.max(1, options.ringCount ?? ((_a = options.radii) == null ? void 0 : _a.length) ?? 3);
|
|
446
|
+
const maxRadius = Math.max(
|
|
447
|
+
currentRadius * 2.2,
|
|
448
|
+
options.maxRadius ?? (((_b = options.radii) == null ? void 0 : _b.length) ? Math.max(...options.radii) : baseRadius * (ringCount * 2.5))
|
|
449
|
+
);
|
|
450
|
+
const ringSpacing = maxRadius / ringCount;
|
|
451
|
+
const features = [
|
|
452
|
+
createFeature(createCirclePolygon(options.center, haloRadius), {
|
|
453
|
+
kind: "halo",
|
|
454
|
+
opacity: (options.haloOpacity ?? 0.16) * (0.75 + pulseProgress * 0.65)
|
|
455
|
+
}),
|
|
456
|
+
createFeature(createCirclePolygon(options.center, currentRadius), {
|
|
457
|
+
kind: "core",
|
|
458
|
+
opacity: (options.fillOpacity ?? 0.28) * (0.88 + pulseProgress * 0.2)
|
|
459
|
+
}),
|
|
460
|
+
createFeature(createCircleLine(options.center, currentRadius * 1.06), {
|
|
461
|
+
kind: "core-ring",
|
|
462
|
+
opacity: (options.lineOpacity ?? 0.95) * (0.8 + pulseProgress * 0.2),
|
|
463
|
+
width: Math.max(1, (options.lineWidth ?? 3) * (0.92 + pulseProgress * 0.12))
|
|
464
|
+
})
|
|
465
|
+
];
|
|
466
|
+
for (let index = 0; index < ringCount; index += 1) {
|
|
467
|
+
const logicalRadius = (index + phase + 1) * ringSpacing % maxRadius || maxRadius;
|
|
468
|
+
const radiusProgress = logicalRadius / maxRadius;
|
|
469
|
+
features.push(
|
|
470
|
+
createFeature(createCircleLine(options.center, logicalRadius), {
|
|
471
|
+
kind: "ring",
|
|
472
|
+
opacity: Math.max(
|
|
473
|
+
0.18,
|
|
474
|
+
(options.lineOpacity ?? 0.95) * (1 - radiusProgress * 0.42)
|
|
475
|
+
),
|
|
476
|
+
width: Math.max(
|
|
477
|
+
1,
|
|
478
|
+
(options.lineWidth ?? 3) * (1 - radiusProgress * 0.18)
|
|
479
|
+
)
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return createFeatureCollection(features);
|
|
484
|
+
};
|
|
485
|
+
const createRingPulseMarkerRender = (options) => {
|
|
486
|
+
const color = options.color || "#ef4444";
|
|
487
|
+
const fillColor = options.fillColor || color;
|
|
488
|
+
return {
|
|
489
|
+
data: createRingPulseEffectData(options),
|
|
490
|
+
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
|
+
{
|
|
501
|
+
id: `${options.id}-core`,
|
|
502
|
+
type: "fill",
|
|
503
|
+
filter: ["==", ["get", "kind"], "core"],
|
|
504
|
+
paint: {
|
|
505
|
+
"fill-color": fillColor,
|
|
506
|
+
"fill-opacity": ["coalesce", ["get", "opacity"], options.fillOpacity ?? 0.28]
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: `${options.id}-rings`,
|
|
511
|
+
type: "line",
|
|
512
|
+
filter: [
|
|
513
|
+
"any",
|
|
514
|
+
["==", ["get", "kind"], "ring"]
|
|
515
|
+
// ["==", ["get", "kind"], "core-ring"],
|
|
516
|
+
],
|
|
517
|
+
paint: {
|
|
518
|
+
"line-color": color,
|
|
519
|
+
"line-opacity": ["coalesce", ["get", "opacity"], options.lineOpacity ?? 0.95],
|
|
520
|
+
"line-width": ["coalesce", ["get", "width"], options.lineWidth ?? 3]
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
],
|
|
524
|
+
startAnimation({ getOptions, setData }) {
|
|
525
|
+
const duration = Math.max(600, getOptions().duration ?? 2e3);
|
|
526
|
+
let frameId = 0;
|
|
527
|
+
const startTime = performance.now();
|
|
528
|
+
const tick = (timestamp) => {
|
|
529
|
+
const currentOptions = getOptions();
|
|
530
|
+
const phase = (timestamp - startTime) % duration / duration;
|
|
531
|
+
setData(createRingPulseEffectData(currentOptions, phase));
|
|
532
|
+
frameId = requestAnimationFrame(tick);
|
|
533
|
+
};
|
|
534
|
+
frameId = requestAnimationFrame(tick);
|
|
535
|
+
return () => {
|
|
536
|
+
cancelAnimationFrame(frameId);
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
};
|
|
541
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
542
|
+
const parseColor = (color) => {
|
|
543
|
+
const value = color.trim();
|
|
544
|
+
const hex = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
545
|
+
if (hex) {
|
|
546
|
+
const source = hex[1];
|
|
547
|
+
const normalized = source.length === 3 ? source.split("").map((item) => item + item).join("") : source;
|
|
548
|
+
return [
|
|
549
|
+
parseInt(normalized.slice(0, 2), 16),
|
|
550
|
+
parseInt(normalized.slice(2, 4), 16),
|
|
551
|
+
parseInt(normalized.slice(4, 6), 16)
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
const rgb = value.match(
|
|
555
|
+
/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i
|
|
556
|
+
);
|
|
557
|
+
if (rgb) {
|
|
558
|
+
return [
|
|
559
|
+
clamp(Number(rgb[1]), 0, 255),
|
|
560
|
+
clamp(Number(rgb[2]), 0, 255),
|
|
561
|
+
clamp(Number(rgb[3]), 0, 255)
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
return void 0;
|
|
565
|
+
};
|
|
566
|
+
const normalizeCanvasAngle = (angle) => (angle % 360 + 360) % 360;
|
|
567
|
+
const getClockwiseSweep = (startAngle, endAngle) => {
|
|
568
|
+
const sweep = normalizeCanvasAngle(endAngle) - normalizeCanvasAngle(startAngle);
|
|
569
|
+
return sweep <= 0 ? sweep + 360 : sweep;
|
|
570
|
+
};
|
|
571
|
+
const getCanvasArcAngle = (bearing) => (bearing - 90) * Math.PI / 180;
|
|
572
|
+
const toRgbaColor = (color, opacity) => {
|
|
573
|
+
const rgb = parseColor(color);
|
|
574
|
+
if (!rgb) {
|
|
575
|
+
return color;
|
|
576
|
+
}
|
|
577
|
+
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp(opacity, 0, 1)})`;
|
|
578
|
+
};
|
|
579
|
+
const createSectorCanvasCoordinates = (center, radius) => {
|
|
580
|
+
const north = getDestination(center, radius, 0)[1];
|
|
581
|
+
const east = getDestination(center, radius, 90)[0];
|
|
582
|
+
const south = getDestination(center, radius, 180)[1];
|
|
583
|
+
const west = getDestination(center, radius, 270)[0];
|
|
584
|
+
return [
|
|
585
|
+
[west, north],
|
|
586
|
+
[east, north],
|
|
587
|
+
[east, south],
|
|
588
|
+
[west, south]
|
|
589
|
+
];
|
|
590
|
+
};
|
|
591
|
+
const createSectorCanvas = () => {
|
|
592
|
+
const canvas = document.createElement("canvas");
|
|
593
|
+
canvas.width = 768;
|
|
594
|
+
canvas.height = 768;
|
|
595
|
+
return canvas;
|
|
596
|
+
};
|
|
597
|
+
const getSectorGradientColors = (options) => {
|
|
598
|
+
var _a;
|
|
599
|
+
const colors = (_a = options.gradient) == null ? void 0 : _a.colors;
|
|
600
|
+
return Array.isArray(colors) && colors.length >= 2 ? colors : void 0;
|
|
601
|
+
};
|
|
602
|
+
const drawSectorGradientCanvas = (canvas, options, angleOffset = 0) => {
|
|
603
|
+
const context = canvas.getContext("2d");
|
|
604
|
+
const colors = getSectorGradientColors(options);
|
|
605
|
+
if (!context || !options.gradient || !colors) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const size = canvas.width;
|
|
609
|
+
const center = size / 2;
|
|
610
|
+
const radius = center - 2;
|
|
611
|
+
const startAngle = options.startAngle + angleOffset;
|
|
612
|
+
const sweep = getClockwiseSweep(startAngle, options.endAngle + angleOffset);
|
|
613
|
+
const startCanvasAngle = getCanvasArcAngle(startAngle);
|
|
614
|
+
const endCanvasAngle = startCanvasAngle + sweep * Math.PI / 180;
|
|
615
|
+
const baseOpacity = options.gradient.opacity ?? options.opacity ?? 0.28;
|
|
616
|
+
const centerOpacity = options.gradient.centerOpacity ?? options.gradient.tailOpacity ?? baseOpacity * 0.25;
|
|
617
|
+
const edgeOpacity = options.gradient.edgeOpacity ?? options.gradient.headOpacity ?? baseOpacity;
|
|
618
|
+
const gradient = context.createRadialGradient(
|
|
619
|
+
center,
|
|
620
|
+
center,
|
|
621
|
+
0,
|
|
622
|
+
center,
|
|
623
|
+
center,
|
|
624
|
+
radius
|
|
625
|
+
);
|
|
626
|
+
colors.forEach((color, index) => {
|
|
627
|
+
const progress = index / Math.max(1, colors.length - 1);
|
|
628
|
+
const opacity = centerOpacity + (edgeOpacity - centerOpacity) * progress;
|
|
629
|
+
gradient.addColorStop(progress, toRgbaColor(color, opacity));
|
|
630
|
+
});
|
|
631
|
+
context.clearRect(0, 0, size, size);
|
|
632
|
+
context.save();
|
|
633
|
+
context.beginPath();
|
|
634
|
+
context.moveTo(center, center);
|
|
635
|
+
context.arc(center, center, radius, startCanvasAngle, endCanvasAngle, false);
|
|
636
|
+
context.closePath();
|
|
637
|
+
context.clip();
|
|
638
|
+
context.fillStyle = gradient;
|
|
639
|
+
context.fillRect(0, 0, size, size);
|
|
640
|
+
context.restore();
|
|
641
|
+
};
|
|
642
|
+
const createSectorGradientCanvas = (options) => {
|
|
643
|
+
if (!getSectorGradientColors(options)) {
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
const canvas = createSectorCanvas();
|
|
647
|
+
drawSectorGradientCanvas(canvas, options);
|
|
648
|
+
return canvas;
|
|
649
|
+
};
|
|
650
|
+
const createSectorFillFeatures = (options, startAngle, endAngle) => {
|
|
651
|
+
if (getSectorGradientColors(options)) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
return [
|
|
655
|
+
createFeature(
|
|
656
|
+
createSectorPolygon(options.center, options.radius, startAngle, endAngle),
|
|
657
|
+
{
|
|
658
|
+
kind: "sector",
|
|
659
|
+
color: options.color || "#ff453a",
|
|
660
|
+
opacity: options.opacity ?? 0.28
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
];
|
|
664
|
+
};
|
|
665
|
+
const createSectorScanData = (options, angleOffset = 0) => {
|
|
666
|
+
const startAngle = options.startAngle + angleOffset;
|
|
667
|
+
const endAngle = options.endAngle + angleOffset;
|
|
668
|
+
const features = [
|
|
669
|
+
...createSectorFillFeatures(options, startAngle, endAngle),
|
|
670
|
+
createFeature(createRayLine(options.center, options.radius, startAngle), {
|
|
671
|
+
kind: "frame"
|
|
672
|
+
}),
|
|
673
|
+
createFeature(createRayLine(options.center, options.radius, endAngle), {
|
|
674
|
+
kind: "frame"
|
|
675
|
+
})
|
|
676
|
+
];
|
|
677
|
+
return createFeatureCollection(features);
|
|
678
|
+
};
|
|
679
|
+
const createSectorScanRender = (options) => {
|
|
680
|
+
const canvas = createSectorGradientCanvas(options);
|
|
681
|
+
const fillLayer = canvas ? {
|
|
682
|
+
id: `${options.id}-sector-fill`,
|
|
683
|
+
type: "raster",
|
|
684
|
+
paint: {
|
|
685
|
+
"raster-opacity": 1,
|
|
686
|
+
"raster-fade-duration": 0
|
|
687
|
+
}
|
|
688
|
+
} : {
|
|
689
|
+
id: `${options.id}-sector-fill`,
|
|
690
|
+
type: "fill",
|
|
691
|
+
filter: ["==", ["get", "kind"], "sector"],
|
|
692
|
+
paint: {
|
|
693
|
+
"fill-color": ["coalesce", ["get", "color"], options.color || "#ff453a"],
|
|
694
|
+
"fill-opacity": ["coalesce", ["get", "opacity"], options.opacity ?? 0.28],
|
|
695
|
+
"fill-antialias": false
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
return {
|
|
699
|
+
data: createSectorScanData(options),
|
|
700
|
+
canvasSources: canvas ? [
|
|
701
|
+
{
|
|
702
|
+
id: `${options.id}-canvas-source`,
|
|
703
|
+
source: {
|
|
704
|
+
type: "canvas",
|
|
705
|
+
canvas,
|
|
706
|
+
coordinates: createSectorCanvasCoordinates(
|
|
707
|
+
options.center,
|
|
708
|
+
options.radius
|
|
709
|
+
),
|
|
710
|
+
animate: Boolean(options.rotationSpeed && options.rotationSpeed !== 0)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
] : void 0,
|
|
714
|
+
layers: [
|
|
715
|
+
fillLayer,
|
|
716
|
+
{
|
|
717
|
+
id: `${options.id}-sector-frame`,
|
|
718
|
+
type: "line",
|
|
719
|
+
filter: ["==", ["get", "kind"], "frame"],
|
|
720
|
+
paint: {
|
|
721
|
+
"line-color": options.outlineColor || options.color || "#ff453a",
|
|
722
|
+
"line-opacity": options.outlineOpacity ?? 0.95,
|
|
723
|
+
"line-width": options.outlineWidth ?? 2
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
],
|
|
727
|
+
startAnimation: options.rotationSpeed && options.rotationSpeed !== 0 ? ({ getOptions, setData }) => {
|
|
728
|
+
const startTime = performance.now();
|
|
729
|
+
let frameId = 0;
|
|
730
|
+
const tick = (timestamp) => {
|
|
731
|
+
const currentOptions = getOptions();
|
|
732
|
+
const angleOffset = (timestamp - startTime) / 1e3 * (currentOptions.rotationSpeed || 0);
|
|
733
|
+
if (canvas) {
|
|
734
|
+
drawSectorGradientCanvas(canvas, currentOptions, angleOffset);
|
|
735
|
+
}
|
|
736
|
+
setData(createSectorScanData(currentOptions, angleOffset));
|
|
737
|
+
frameId = requestAnimationFrame(tick);
|
|
738
|
+
};
|
|
739
|
+
frameId = requestAnimationFrame(tick);
|
|
740
|
+
return () => {
|
|
741
|
+
cancelAnimationFrame(frameId);
|
|
742
|
+
};
|
|
743
|
+
} : void 0
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
const createDirectionalPulseData = (options, leadingDistance) => {
|
|
747
|
+
const radius = Math.max(40, options.radius);
|
|
748
|
+
const direction = options.direction ?? 270;
|
|
749
|
+
const spread = Math.min(180, Math.max(8, options.spread ?? 72));
|
|
750
|
+
const lineCount = Math.max(1, options.lineCount ?? 7);
|
|
751
|
+
const lineSpacing = Math.max(20, options.lineSpacing ?? Math.max(80, radius / 9));
|
|
752
|
+
const phaseDistance = leadingDistance ?? 0;
|
|
753
|
+
const innerRadius = Math.max(
|
|
754
|
+
0,
|
|
755
|
+
Math.min(radius * 0.35, options.innerRadius ?? Math.min(lineSpacing * 0.4, 120))
|
|
756
|
+
);
|
|
757
|
+
const startAngle = direction - spread / 2;
|
|
758
|
+
const endAngle = direction + spread / 2;
|
|
759
|
+
const features = [];
|
|
760
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
761
|
+
const currentRadius = innerRadius + phaseDistance + index * lineSpacing;
|
|
762
|
+
if (currentRadius > radius) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
const distanceProgress = currentRadius / radius;
|
|
766
|
+
const headProgress = 1 - index / Math.max(1, lineCount);
|
|
767
|
+
features.push(
|
|
768
|
+
createFeature(
|
|
769
|
+
createArcLine(options.center, currentRadius, startAngle, endAngle),
|
|
770
|
+
{
|
|
771
|
+
kind: "directional-pulse-line",
|
|
772
|
+
opacity: Math.max(
|
|
773
|
+
0.08,
|
|
774
|
+
(options.opacity ?? 0.92) * (0.42 + headProgress * 0.58) * (1 - distanceProgress * 0.28)
|
|
775
|
+
),
|
|
776
|
+
width: Math.max(
|
|
777
|
+
1,
|
|
778
|
+
(options.lineWidth ?? 2.8) * (0.86 + headProgress * 0.24) * (1 - distanceProgress * 0.08)
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return createFeatureCollection(features);
|
|
785
|
+
};
|
|
786
|
+
const createDirectionalPulseRender = (options) => ({
|
|
787
|
+
data: createDirectionalPulseData(options),
|
|
788
|
+
layers: [
|
|
789
|
+
{
|
|
790
|
+
id: `${options.id}-directional-pulse`,
|
|
791
|
+
type: "line",
|
|
792
|
+
filter: ["==", ["get", "kind"], "directional-pulse-line"],
|
|
793
|
+
layout: {
|
|
794
|
+
"line-cap": "round",
|
|
795
|
+
"line-join": "round"
|
|
796
|
+
},
|
|
797
|
+
paint: {
|
|
798
|
+
"line-color": options.color || "#ef4444",
|
|
799
|
+
"line-opacity": ["coalesce", ["get", "opacity"], options.opacity ?? 0.92],
|
|
800
|
+
"line-width": ["coalesce", ["get", "width"], options.lineWidth ?? 2.8]
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
],
|
|
804
|
+
startAnimation: options.speed && options.speed > 0 ? ({ getOptions, setData }) => {
|
|
805
|
+
const startTime = performance.now();
|
|
806
|
+
let frameId = 0;
|
|
807
|
+
const tick = (timestamp) => {
|
|
808
|
+
const currentOptions = getOptions();
|
|
809
|
+
const radius = Math.max(40, currentOptions.radius);
|
|
810
|
+
const effectiveLineSpacing = Math.max(
|
|
811
|
+
20,
|
|
812
|
+
currentOptions.lineSpacing ?? Math.max(80, radius / 9)
|
|
813
|
+
);
|
|
814
|
+
const leadingDistance = (timestamp - startTime) / 1e3 * (currentOptions.speed ?? 0) % effectiveLineSpacing;
|
|
815
|
+
setData(createDirectionalPulseData(currentOptions, leadingDistance));
|
|
816
|
+
frameId = requestAnimationFrame(tick);
|
|
817
|
+
};
|
|
818
|
+
frameId = requestAnimationFrame(tick);
|
|
819
|
+
return () => {
|
|
820
|
+
cancelAnimationFrame(frameId);
|
|
821
|
+
};
|
|
822
|
+
} : void 0
|
|
823
|
+
});
|
|
824
|
+
const createRadarSweepData = (options, angleOffset = 0) => {
|
|
825
|
+
var _a;
|
|
826
|
+
const ringCount = Math.max(0, options.ringCount ?? 4);
|
|
827
|
+
const trailCount = Math.max(1, options.trailCount ?? 8);
|
|
828
|
+
const sweepAngle = Math.max(12, options.sweepAngle ?? 42);
|
|
829
|
+
const trailGap = sweepAngle * 0.22;
|
|
830
|
+
const tailOpacity = options.tailOpacity ?? 0.04;
|
|
831
|
+
const headOpacity = options.headOpacity ?? Math.max(0.24, options.opacity ?? 0.3);
|
|
832
|
+
const sweepStartAngle = (options.sweepStartAngle ?? 0) + angleOffset;
|
|
833
|
+
const showCrosshair = options.showCrosshair !== false;
|
|
834
|
+
const showDistanceLabels = options.showDistanceLabels !== false;
|
|
835
|
+
const coreRadius = Math.max(
|
|
836
|
+
20,
|
|
837
|
+
options.coreRadius ?? Math.min(160, options.radius * 0.035)
|
|
838
|
+
);
|
|
839
|
+
const features = [];
|
|
840
|
+
for (let index = ringCount; index >= 1; index -= 1) {
|
|
841
|
+
const distance = options.radius * index / ringCount;
|
|
842
|
+
features.push(
|
|
843
|
+
createFeature(
|
|
844
|
+
createCircleLine(options.center, distance),
|
|
845
|
+
{
|
|
846
|
+
kind: "ring"
|
|
847
|
+
}
|
|
848
|
+
)
|
|
849
|
+
);
|
|
850
|
+
if (showDistanceLabels) {
|
|
851
|
+
const text = ((_a = options.distanceLabelFormatter) == null ? void 0 : _a.call(options, distance, index)) ?? `${Math.round(distance)}`;
|
|
852
|
+
[
|
|
853
|
+
{ bearing: 0, textAnchor: "bottom" },
|
|
854
|
+
{ bearing: 90, textAnchor: "left" },
|
|
855
|
+
{ bearing: 180, textAnchor: "top" },
|
|
856
|
+
{ bearing: 270, textAnchor: "right" }
|
|
857
|
+
].forEach((label) => {
|
|
858
|
+
features.push(
|
|
859
|
+
createFeature(
|
|
860
|
+
{
|
|
861
|
+
type: "Point",
|
|
862
|
+
coordinates: getDestination(options.center, distance, label.bearing)
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
kind: "distance-label",
|
|
866
|
+
text,
|
|
867
|
+
textAnchor: label.textAnchor
|
|
868
|
+
}
|
|
869
|
+
)
|
|
870
|
+
);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (showCrosshair) {
|
|
875
|
+
features.push(
|
|
876
|
+
createFeature(
|
|
877
|
+
{
|
|
878
|
+
type: "LineString",
|
|
879
|
+
coordinates: [
|
|
880
|
+
getDestination(options.center, options.radius, 180),
|
|
881
|
+
getDestination(options.center, options.radius, 0)
|
|
882
|
+
]
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
kind: "crosshair"
|
|
886
|
+
}
|
|
887
|
+
),
|
|
888
|
+
createFeature(
|
|
889
|
+
{
|
|
890
|
+
type: "LineString",
|
|
891
|
+
coordinates: [
|
|
892
|
+
getDestination(options.center, options.radius, 270),
|
|
893
|
+
getDestination(options.center, options.radius, 90)
|
|
894
|
+
]
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
kind: "crosshair"
|
|
898
|
+
}
|
|
899
|
+
)
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
features.push(
|
|
903
|
+
createFeature(createCirclePolygon(options.center, coreRadius), {
|
|
904
|
+
kind: "core"
|
|
905
|
+
})
|
|
906
|
+
);
|
|
907
|
+
for (let index = trailCount - 1; index >= 0; index -= 1) {
|
|
908
|
+
const startAngle = sweepStartAngle - trailGap * index;
|
|
909
|
+
const endAngle = startAngle + sweepAngle;
|
|
910
|
+
const progress = (trailCount - index) / trailCount;
|
|
911
|
+
const opacity = tailOpacity + (headOpacity - tailOpacity) * progress * progress;
|
|
912
|
+
features.push(
|
|
913
|
+
createFeature(
|
|
914
|
+
createSectorPolygon(options.center, options.radius, startAngle, endAngle),
|
|
915
|
+
{
|
|
916
|
+
kind: "sweep",
|
|
917
|
+
opacity
|
|
918
|
+
}
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
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
|
+
return createFeatureCollection(features);
|
|
937
|
+
};
|
|
938
|
+
const createRadarSweepRender = (options) => ({
|
|
939
|
+
data: createRadarSweepData(options),
|
|
940
|
+
layers: [
|
|
941
|
+
{
|
|
942
|
+
id: `${options.id}-radar-trail`,
|
|
943
|
+
type: "fill",
|
|
944
|
+
filter: ["==", ["get", "kind"], "sweep"],
|
|
945
|
+
paint: {
|
|
946
|
+
"fill-color": options.color || "#22c55e",
|
|
947
|
+
"fill-opacity": ["coalesce", ["get", "opacity"], options.opacity ?? 0.32]
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
id: `${options.id}-radar-rings`,
|
|
952
|
+
type: "line",
|
|
953
|
+
filter: ["==", ["get", "kind"], "ring"],
|
|
954
|
+
paint: {
|
|
955
|
+
"line-color": options.ringColor || options.color || "#22c55e",
|
|
956
|
+
"line-opacity": options.ringOpacity ?? 0.95,
|
|
957
|
+
"line-width": options.ringWidth ?? 2
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
id: `${options.id}-radar-crosshair`,
|
|
962
|
+
type: "line",
|
|
963
|
+
filter: ["==", ["get", "kind"], "crosshair"],
|
|
964
|
+
paint: {
|
|
965
|
+
"line-color": options.crosshairColor || options.ringColor || options.color || "#22c55e",
|
|
966
|
+
"line-opacity": options.crosshairOpacity ?? 0.5,
|
|
967
|
+
"line-width": options.crosshairWidth ?? 1.4
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
id: `${options.id}-radar-distance-labels`,
|
|
972
|
+
type: "symbol",
|
|
973
|
+
filter: ["==", ["get", "kind"], "distance-label"],
|
|
974
|
+
layout: {
|
|
975
|
+
"text-field": ["get", "text"],
|
|
976
|
+
"text-size": options.distanceLabelSize ?? 12,
|
|
977
|
+
"text-anchor": ["get", "textAnchor"],
|
|
978
|
+
"text-allow-overlap": true,
|
|
979
|
+
"text-ignore-placement": true
|
|
980
|
+
},
|
|
981
|
+
paint: {
|
|
982
|
+
"text-color": options.distanceLabelColor || options.ringColor || options.color || "#22c55e",
|
|
983
|
+
"text-opacity": options.distanceLabelOpacity ?? 0.92,
|
|
984
|
+
"text-halo-color": options.distanceLabelHaloColor || "rgba(15, 23, 42, 0.42)",
|
|
985
|
+
"text-halo-width": 1
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
],
|
|
989
|
+
startAnimation({ getOptions, setData }) {
|
|
990
|
+
const startTime = performance.now();
|
|
991
|
+
let frameId = 0;
|
|
992
|
+
const tick = (timestamp) => {
|
|
993
|
+
const currentOptions = getOptions();
|
|
994
|
+
const angleOffset = (timestamp - startTime) / 1e3 * (currentOptions.rotationSpeed ?? 36);
|
|
995
|
+
setData(createRadarSweepData(currentOptions, angleOffset));
|
|
996
|
+
frameId = requestAnimationFrame(tick);
|
|
997
|
+
};
|
|
998
|
+
frameId = requestAnimationFrame(tick);
|
|
999
|
+
return () => {
|
|
1000
|
+
cancelAnimationFrame(frameId);
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
const addPulseMarker = (map, options) => createManagedEffect(map, options, createPulseMarkerRender);
|
|
1005
|
+
const addRingPulseMarker = (map, options) => createManagedEffect(map, options, createRingPulseMarkerRender);
|
|
1006
|
+
const addSectorScan = (map, options) => createManagedEffect(map, options, createSectorScanRender);
|
|
1007
|
+
const addDirectionalPulse = (map, options) => createManagedEffect(map, options, createDirectionalPulseRender);
|
|
1008
|
+
const addRadarSweep = (map, options) => createManagedEffect(map, options, createRadarSweepRender);
|
|
1009
|
+
const resolveStyle = (style, tdtToken) => {
|
|
1010
|
+
if (!style) {
|
|
1011
|
+
return getBaseMapStyle("openfreemap-liberty");
|
|
1012
|
+
}
|
|
1013
|
+
if (typeof style === "string" && isBaseMapType(style)) {
|
|
1014
|
+
return getBaseMapStyle(style, { token: tdtToken });
|
|
1015
|
+
}
|
|
1016
|
+
return style;
|
|
1017
|
+
};
|
|
1018
|
+
const createMap = (options) => {
|
|
1019
|
+
const { style, tdtToken, ...mapOptions } = options;
|
|
1020
|
+
return new maplibregl.Map({
|
|
1021
|
+
...mapOptions,
|
|
1022
|
+
style: resolveStyle(style, tdtToken)
|
|
1023
|
+
});
|
|
1024
|
+
};
|
|
1025
|
+
export {
|
|
1026
|
+
BASE_MAP_TYPES,
|
|
1027
|
+
OPENFREEMAP_STYLES,
|
|
1028
|
+
OPENFREEMAP_STYLE_BASE_URL,
|
|
1029
|
+
OPENFREEMAP_STYLE_TYPES,
|
|
1030
|
+
TDT_STYLE_TYPES,
|
|
1031
|
+
addDirectionalPulse,
|
|
1032
|
+
addPulseMarker,
|
|
1033
|
+
addRadarSweep,
|
|
1034
|
+
addRingPulseMarker,
|
|
1035
|
+
addSectorScan,
|
|
1036
|
+
createMap,
|
|
1037
|
+
createTiandituImageStyle,
|
|
1038
|
+
createTiandituRasterSource,
|
|
1039
|
+
getBaseMapStyle,
|
|
1040
|
+
getMaplibreToolsConfig,
|
|
1041
|
+
getOpenFreeMapStyle,
|
|
1042
|
+
isBaseMapType,
|
|
1043
|
+
default2 as maplibregl,
|
|
1044
|
+
setBaseMap,
|
|
1045
|
+
setMaplibreToolsConfig
|
|
1046
|
+
};
|