anymap-ts 0.9.0 → 0.10.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/anymap_ts/static/deckgl.js +1 -1
- package/anymap_ts/static/leaflet.js +1 -1
- package/anymap_ts/static/mapbox.css +1 -1
- package/anymap_ts/static/mapbox.js +26608 -408
- package/anymap_ts/static/maplibre.js +1 -1
- package/anymap_ts/static/openlayers.js +7 -7
- package/package.json +1 -1
- package/src/core/BaseMapRenderer.ts +3 -24
- package/src/mapbox/MapboxRenderer.ts +2006 -80
- package/src/mapbox/index.ts +2 -0
|
@@ -13,21 +13,65 @@ import mapboxgl, {
|
|
|
13
13
|
Marker,
|
|
14
14
|
Popup,
|
|
15
15
|
} from 'mapbox-gl';
|
|
16
|
+
import MapboxDraw from '@mapbox/mapbox-gl-draw';
|
|
17
|
+
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
|
16
18
|
import { MapboxOverlay } from '@deck.gl/mapbox';
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
ArcLayer,
|
|
21
|
+
PointCloudLayer,
|
|
22
|
+
ScatterplotLayer,
|
|
23
|
+
PathLayer,
|
|
24
|
+
PolygonLayer,
|
|
25
|
+
IconLayer,
|
|
26
|
+
TextLayer,
|
|
27
|
+
GeoJsonLayer,
|
|
28
|
+
LineLayer,
|
|
29
|
+
BitmapLayer,
|
|
30
|
+
ColumnLayer,
|
|
31
|
+
GridCellLayer,
|
|
32
|
+
SolidPolygonLayer,
|
|
33
|
+
} from '@deck.gl/layers';
|
|
34
|
+
import {
|
|
35
|
+
HexagonLayer,
|
|
36
|
+
HeatmapLayer,
|
|
37
|
+
GridLayer,
|
|
38
|
+
ContourLayer,
|
|
39
|
+
ScreenGridLayer,
|
|
40
|
+
} from '@deck.gl/aggregation-layers';
|
|
41
|
+
import { TripsLayer } from '@deck.gl/geo-layers';
|
|
42
|
+
import along from '@turf/along';
|
|
43
|
+
import length from '@turf/length';
|
|
44
|
+
import { lineString } from '@turf/helpers';
|
|
18
45
|
import { COGLayer, proj } from '@developmentseed/deck.gl-geotiff';
|
|
19
46
|
import { toProj4 } from 'geotiff-geokeys-to-proj4';
|
|
20
|
-
import {
|
|
47
|
+
import { ZarrLayer } from '@carbonplan/zarr-layer';
|
|
48
|
+
import { BaseMapRenderer } from '../core/BaseMapRenderer';
|
|
21
49
|
import { StateManager } from '../core/StateManager';
|
|
22
50
|
import type { MapWidgetModel } from '../types/anywidget';
|
|
23
51
|
import type {
|
|
24
52
|
ControlPosition,
|
|
25
53
|
FlyToOptions,
|
|
26
54
|
FitBoundsOptions,
|
|
55
|
+
LayerConfig,
|
|
56
|
+
SourceConfig,
|
|
27
57
|
} from '../types/mapbox';
|
|
28
58
|
import type { Feature, FeatureCollection } from 'geojson';
|
|
29
59
|
import { LidarControl } from 'maplibre-gl-lidar';
|
|
30
|
-
import type { LidarControlOptions,
|
|
60
|
+
import type { LidarControlOptions, LidarColorScheme } from '../types/lidar';
|
|
61
|
+
import {
|
|
62
|
+
PMTilesLayerControl,
|
|
63
|
+
CogLayerControl,
|
|
64
|
+
ZarrLayerControl,
|
|
65
|
+
AddVectorControl,
|
|
66
|
+
addControlGrid,
|
|
67
|
+
Colorbar,
|
|
68
|
+
SearchControl,
|
|
69
|
+
MeasureControl,
|
|
70
|
+
PrintControl,
|
|
71
|
+
} from 'maplibre-gl-components';
|
|
72
|
+
import type { ControlGrid } from 'maplibre-gl-components';
|
|
73
|
+
import 'maplibre-gl-components/style.css';
|
|
74
|
+
import { geojson as flatgeobuf } from 'flatgeobuf';
|
|
31
75
|
|
|
32
76
|
/**
|
|
33
77
|
* Parse GeoKeys to proj4 definition for COG reprojection.
|
|
@@ -52,16 +96,78 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
52
96
|
private markersMap: globalThis.Map<string, Marker> = new globalThis.Map();
|
|
53
97
|
private popupsMap: globalThis.Map<string, Popup> = new globalThis.Map();
|
|
54
98
|
private controlsMap: globalThis.Map<string, mapboxgl.IControl> = new globalThis.Map();
|
|
99
|
+
private legendsMap: globalThis.Map<string, HTMLElement> = new globalThis.Map();
|
|
55
100
|
private resizeObserver: ResizeObserver | null = null;
|
|
56
101
|
private resizeDebounceTimer: number | null = null;
|
|
57
102
|
|
|
58
103
|
// Deck.gl overlay for COG layers
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
protected deckOverlay: MapboxOverlay | null = null;
|
|
105
|
+
protected deckLayers: globalThis.Map<string, unknown> = new globalThis.Map();
|
|
106
|
+
|
|
107
|
+
// Zarr layers
|
|
108
|
+
protected zarrLayers: globalThis.Map<string, ZarrLayer> = new globalThis.Map();
|
|
109
|
+
|
|
110
|
+
// Draw control (Mapbox Draw)
|
|
111
|
+
private mapboxDraw: MapboxDraw | null = null;
|
|
112
|
+
|
|
113
|
+
// maplibre-gl-components controls
|
|
114
|
+
protected pmtilesLayerControl: PMTilesLayerControl | null = null;
|
|
115
|
+
protected cogLayerUiControl: CogLayerControl | null = null;
|
|
116
|
+
protected zarrLayerUiControl: ZarrLayerControl | null = null;
|
|
117
|
+
protected addVectorControl: AddVectorControl | null = null;
|
|
118
|
+
protected controlGrid: ControlGrid | null = null;
|
|
119
|
+
|
|
120
|
+
// Colorbar, Search, Measure, Print controls
|
|
121
|
+
protected colorbarsMap: globalThis.Map<string, Colorbar> = new globalThis.Map();
|
|
122
|
+
protected searchControl: SearchControl | null = null;
|
|
123
|
+
protected measureControl: MeasureControl | null = null;
|
|
124
|
+
protected printControl: PrintControl | null = null;
|
|
125
|
+
|
|
126
|
+
// Route animations
|
|
127
|
+
protected animations: globalThis.Map<string, {
|
|
128
|
+
animationId: number;
|
|
129
|
+
marker: Marker;
|
|
130
|
+
isPaused: boolean;
|
|
131
|
+
speed: number;
|
|
132
|
+
startTime: number;
|
|
133
|
+
pausedAt: number;
|
|
134
|
+
duration: number;
|
|
135
|
+
coordinates: [number, number][];
|
|
136
|
+
loop: boolean;
|
|
137
|
+
trailSourceId?: string;
|
|
138
|
+
trailLayerId?: string;
|
|
139
|
+
}> = new globalThis.Map();
|
|
140
|
+
|
|
141
|
+
// Feature hover state tracking
|
|
142
|
+
protected hoveredFeatureId: string | number | null = null;
|
|
143
|
+
protected hoveredLayerId: string | null = null;
|
|
144
|
+
|
|
145
|
+
// Video sources tracking
|
|
146
|
+
protected videoSources: globalThis.Map<string, string> = new globalThis.Map();
|
|
147
|
+
|
|
148
|
+
// Split map state
|
|
149
|
+
private splitMapRight: MapboxMap | null = null;
|
|
150
|
+
private splitMapContainer: HTMLDivElement | null = null;
|
|
151
|
+
private splitSlider: HTMLDivElement | null = null;
|
|
152
|
+
private splitActive: boolean = false;
|
|
153
|
+
|
|
154
|
+
// Tooltip and coordinates
|
|
155
|
+
private tooltipLayerHandlers: Map<string, (e: mapboxgl.MapMouseEvent & { features?: GeoJSON.Feature[] }) => void> = new Map();
|
|
156
|
+
private coordinatesControl: HTMLDivElement | null = null;
|
|
157
|
+
private coordinatesHandler: ((e: mapboxgl.MapMouseEvent) => void) | null = null;
|
|
158
|
+
|
|
159
|
+
// Time slider, opacity slider, style switcher
|
|
160
|
+
private timeSliderContainer: HTMLDivElement | null = null;
|
|
161
|
+
private opacitySliderContainer: Map<string, HTMLDivElement> = new Map();
|
|
162
|
+
private styleSwitcherContainer: HTMLDivElement | null = null;
|
|
163
|
+
|
|
164
|
+
// Swipe map
|
|
165
|
+
private swipeContainer: HTMLDivElement | null = null;
|
|
166
|
+
private swipeHandler: (() => void) | null = null;
|
|
61
167
|
|
|
62
168
|
// LiDAR control
|
|
63
|
-
|
|
64
|
-
|
|
169
|
+
protected lidarControl: LidarControl | null = null;
|
|
170
|
+
protected lidarLayers: globalThis.Map<string, string> = new globalThis.Map();
|
|
65
171
|
|
|
66
172
|
constructor(model: MapWidgetModel, el: HTMLElement) {
|
|
67
173
|
super(model, el);
|
|
@@ -99,6 +205,15 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
99
205
|
await new Promise<void>((resolve) => {
|
|
100
206
|
this.map!.on('load', () => {
|
|
101
207
|
this.isMapReady = true;
|
|
208
|
+
this.restoreState();
|
|
209
|
+
const initProjection = this.model.get('projection') as string;
|
|
210
|
+
if (initProjection && initProjection !== 'mercator') {
|
|
211
|
+
try {
|
|
212
|
+
this.map!.setProjection({ type: initProjection } as unknown as mapboxgl.ProjectionSpecification);
|
|
213
|
+
} catch {
|
|
214
|
+
// Ignore projection errors
|
|
215
|
+
}
|
|
216
|
+
}
|
|
102
217
|
this.processPendingCalls();
|
|
103
218
|
setTimeout(() => {
|
|
104
219
|
if (this.map) {
|
|
@@ -247,23 +362,46 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
247
362
|
|
|
248
363
|
// Raster data
|
|
249
364
|
this.registerMethod('addTileLayer', this.handleAddTileLayer.bind(this));
|
|
365
|
+
this.registerMethod('addImageLayer', this.handleAddImageLayer.bind(this));
|
|
250
366
|
|
|
251
367
|
// Controls
|
|
252
368
|
this.registerMethod('addControl', this.handleAddControl.bind(this));
|
|
253
369
|
this.registerMethod('removeControl', this.handleRemoveControl.bind(this));
|
|
370
|
+
this.registerMethod('addLayerControl', this.handleAddLayerControl.bind(this));
|
|
254
371
|
|
|
255
|
-
//
|
|
372
|
+
// Draw control
|
|
373
|
+
this.registerMethod('addDrawControl', this.handleAddDrawControl.bind(this));
|
|
374
|
+
this.registerMethod('getDrawData', this.handleGetDrawData.bind(this));
|
|
375
|
+
this.registerMethod('loadDrawData', this.handleLoadDrawData.bind(this));
|
|
376
|
+
this.registerMethod('clearDrawData', this.handleClearDrawData.bind(this));
|
|
377
|
+
|
|
378
|
+
// Markers and popups
|
|
256
379
|
this.registerMethod('addMarker', this.handleAddMarker.bind(this));
|
|
380
|
+
this.registerMethod('addMarkers', this.handleAddMarkers.bind(this));
|
|
257
381
|
this.registerMethod('removeMarker', this.handleRemoveMarker.bind(this));
|
|
382
|
+
this.registerMethod('addPopup', this.handleAddPopup.bind(this));
|
|
383
|
+
|
|
384
|
+
// Legend
|
|
385
|
+
this.registerMethod('addLegend', this.handleAddLegend.bind(this));
|
|
386
|
+
this.registerMethod('removeLegend', this.handleRemoveLegend.bind(this));
|
|
387
|
+
this.registerMethod('updateLegend', this.handleUpdateLegend.bind(this));
|
|
258
388
|
|
|
259
389
|
// Terrain (Mapbox-specific)
|
|
260
390
|
this.registerMethod('addTerrain', this.handleAddTerrain.bind(this));
|
|
261
391
|
this.registerMethod('removeTerrain', this.handleRemoveTerrain.bind(this));
|
|
262
392
|
|
|
393
|
+
// Layer management
|
|
394
|
+
this.registerMethod('moveLayer', this.handleMoveLayer.bind(this));
|
|
395
|
+
|
|
263
396
|
// COG layers (deck.gl)
|
|
264
397
|
this.registerMethod('addCOGLayer', this.handleAddCOGLayer.bind(this));
|
|
265
398
|
this.registerMethod('removeCOGLayer', this.handleRemoveCOGLayer.bind(this));
|
|
266
399
|
|
|
400
|
+
// Zarr layers
|
|
401
|
+
this.registerMethod('addZarrLayer', this.handleAddZarrLayer.bind(this));
|
|
402
|
+
this.registerMethod('removeZarrLayer', this.handleRemoveZarrLayer.bind(this));
|
|
403
|
+
this.registerMethod('updateZarrLayer', this.handleUpdateZarrLayer.bind(this));
|
|
404
|
+
|
|
267
405
|
// Arc layers (deck.gl)
|
|
268
406
|
this.registerMethod('addArcLayer', this.handleAddArcLayer.bind(this));
|
|
269
407
|
this.registerMethod('removeArcLayer', this.handleRemoveArcLayer.bind(this));
|
|
@@ -272,6 +410,47 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
272
410
|
this.registerMethod('addPointCloudLayer', this.handleAddPointCloudLayer.bind(this));
|
|
273
411
|
this.registerMethod('removePointCloudLayer', this.handleRemovePointCloudLayer.bind(this));
|
|
274
412
|
|
|
413
|
+
// Additional deck.gl layers
|
|
414
|
+
this.registerMethod('addScatterplotLayer', this.handleAddScatterplotLayer.bind(this));
|
|
415
|
+
this.registerMethod('addPathLayer', this.handleAddPathLayer.bind(this));
|
|
416
|
+
this.registerMethod('addPolygonLayer', this.handleAddPolygonLayer.bind(this));
|
|
417
|
+
this.registerMethod('addHexagonLayer', this.handleAddHexagonLayer.bind(this));
|
|
418
|
+
this.registerMethod('addHeatmapLayer', this.handleAddHeatmapLayer.bind(this));
|
|
419
|
+
this.registerMethod('addGridLayer', this.handleAddGridLayer.bind(this));
|
|
420
|
+
this.registerMethod('addIconLayer', this.handleAddIconLayer.bind(this));
|
|
421
|
+
this.registerMethod('addTextLayer', this.handleAddTextLayer.bind(this));
|
|
422
|
+
this.registerMethod('addGeoJsonLayer', this.handleAddGeoJsonLayer.bind(this));
|
|
423
|
+
this.registerMethod('addContourLayer', this.handleAddContourLayer.bind(this));
|
|
424
|
+
this.registerMethod('addScreenGridLayer', this.handleAddScreenGridLayer.bind(this));
|
|
425
|
+
this.registerMethod('addTripsLayer', this.handleAddTripsLayer.bind(this));
|
|
426
|
+
this.registerMethod('addLineLayer', this.handleAddLineLayer.bind(this));
|
|
427
|
+
this.registerMethod('addDeckGLLayer', this.handleAddDeckGLLayer.bind(this));
|
|
428
|
+
this.registerMethod('removeDeckLayer', this.handleRemoveDeckLayer.bind(this));
|
|
429
|
+
this.registerMethod('setDeckLayerVisibility', this.handleSetDeckLayerVisibility.bind(this));
|
|
430
|
+
this.registerMethod('addBitmapLayer', this.handleAddBitmapLayer.bind(this));
|
|
431
|
+
this.registerMethod('addColumnLayer', this.handleAddColumnLayer.bind(this));
|
|
432
|
+
this.registerMethod('addGridCellLayer', this.handleAddGridCellLayer.bind(this));
|
|
433
|
+
this.registerMethod('addSolidPolygonLayer', this.handleAddSolidPolygonLayer.bind(this));
|
|
434
|
+
|
|
435
|
+
// Native Mapbox features
|
|
436
|
+
this.registerMethod('setProjection', this.handleSetProjection.bind(this));
|
|
437
|
+
this.registerMethod('updateGeoJSONSource', this.handleUpdateGeoJSONSource.bind(this));
|
|
438
|
+
this.registerMethod('addMapImage', this.handleAddMapImage.bind(this));
|
|
439
|
+
this.registerMethod('addTooltip', this.handleAddTooltip.bind(this));
|
|
440
|
+
this.registerMethod('removeTooltip', this.handleRemoveTooltip.bind(this));
|
|
441
|
+
this.registerMethod('addCoordinatesControl', this.handleAddCoordinatesControl.bind(this));
|
|
442
|
+
this.registerMethod('removeCoordinatesControl', this.handleRemoveCoordinatesControl.bind(this));
|
|
443
|
+
this.registerMethod('addTimeSlider', this.handleAddTimeSlider.bind(this));
|
|
444
|
+
this.registerMethod('removeTimeSlider', this.handleRemoveTimeSlider.bind(this));
|
|
445
|
+
this.registerMethod('addSwipeMap', this.handleAddSwipeMap.bind(this));
|
|
446
|
+
this.registerMethod('removeSwipeMap', this.handleRemoveSwipeMap.bind(this));
|
|
447
|
+
this.registerMethod('addOpacitySlider', this.handleAddOpacitySlider.bind(this));
|
|
448
|
+
this.registerMethod('removeOpacitySlider', this.handleRemoveOpacitySlider.bind(this));
|
|
449
|
+
this.registerMethod('addStyleSwitcher', this.handleAddStyleSwitcher.bind(this));
|
|
450
|
+
this.registerMethod('removeStyleSwitcher', this.handleRemoveStyleSwitcher.bind(this));
|
|
451
|
+
this.registerMethod('getVisibleFeatures', this.handleGetVisibleFeatures.bind(this));
|
|
452
|
+
this.registerMethod('getLayerData', this.handleGetLayerData.bind(this));
|
|
453
|
+
|
|
275
454
|
// LiDAR layers (maplibre-gl-lidar)
|
|
276
455
|
this.registerMethod('addLidarControl', this.handleAddLidarControl.bind(this));
|
|
277
456
|
this.registerMethod('addLidarLayer', this.handleAddLidarLayer.bind(this));
|
|
@@ -279,6 +458,66 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
279
458
|
this.registerMethod('setLidarColorScheme', this.handleSetLidarColorScheme.bind(this));
|
|
280
459
|
this.registerMethod('setLidarPointSize', this.handleSetLidarPointSize.bind(this));
|
|
281
460
|
this.registerMethod('setLidarOpacity', this.handleSetLidarOpacity.bind(this));
|
|
461
|
+
|
|
462
|
+
// PMTiles
|
|
463
|
+
this.registerMethod('addPMTilesLayer', this.handleAddPMTilesLayer.bind(this));
|
|
464
|
+
this.registerMethod('removePMTilesLayer', this.handleRemovePMTilesLayer.bind(this));
|
|
465
|
+
this.registerMethod('addPMTilesControl', this.handleAddPMTilesControl.bind(this));
|
|
466
|
+
this.registerMethod('addCogControl', this.handleAddCogControl.bind(this));
|
|
467
|
+
this.registerMethod('addZarrControl', this.handleAddZarrControl.bind(this));
|
|
468
|
+
this.registerMethod('addVectorControl', this.handleAddVectorControl.bind(this));
|
|
469
|
+
this.registerMethod('addControlGrid', this.handleAddControlGrid.bind(this));
|
|
470
|
+
|
|
471
|
+
// Clustering, Choropleth, 3D Buildings
|
|
472
|
+
this.registerMethod('addClusterLayer', this.handleAddClusterLayer.bind(this));
|
|
473
|
+
this.registerMethod('removeClusterLayer', this.handleRemoveClusterLayer.bind(this));
|
|
474
|
+
this.registerMethod('addChoropleth', this.handleAddChoropleth.bind(this));
|
|
475
|
+
this.registerMethod('add3DBuildings', this.handleAdd3DBuildings.bind(this));
|
|
476
|
+
|
|
477
|
+
// Route Animation
|
|
478
|
+
this.registerMethod('animateAlongRoute', this.handleAnimateAlongRoute.bind(this));
|
|
479
|
+
this.registerMethod('stopAnimation', this.handleStopAnimation.bind(this));
|
|
480
|
+
this.registerMethod('pauseAnimation', this.handlePauseAnimation.bind(this));
|
|
481
|
+
this.registerMethod('resumeAnimation', this.handleResumeAnimation.bind(this));
|
|
482
|
+
this.registerMethod('setAnimationSpeed', this.handleSetAnimationSpeed.bind(this));
|
|
483
|
+
|
|
484
|
+
// Feature Hover
|
|
485
|
+
this.registerMethod('addHoverEffect', this.handleAddHoverEffect.bind(this));
|
|
486
|
+
|
|
487
|
+
// Fog (Mapbox uses setFog, not setSky)
|
|
488
|
+
this.registerMethod('setFog', this.handleSetFog.bind(this));
|
|
489
|
+
this.registerMethod('removeFog', this.handleRemoveFog.bind(this));
|
|
490
|
+
|
|
491
|
+
// Feature Query/Filter
|
|
492
|
+
this.registerMethod('setFilter', this.handleSetFilter.bind(this));
|
|
493
|
+
this.registerMethod('queryRenderedFeatures', this.handleQueryRenderedFeatures.bind(this));
|
|
494
|
+
this.registerMethod('querySourceFeatures', this.handleQuerySourceFeatures.bind(this));
|
|
495
|
+
|
|
496
|
+
// Video Layer
|
|
497
|
+
this.registerMethod('addVideoLayer', this.handleAddVideoLayer.bind(this));
|
|
498
|
+
this.registerMethod('removeVideoLayer', this.handleRemoveVideoLayer.bind(this));
|
|
499
|
+
this.registerMethod('playVideo', this.handlePlayVideo.bind(this));
|
|
500
|
+
this.registerMethod('pauseVideo', this.handlePauseVideo.bind(this));
|
|
501
|
+
this.registerMethod('seekVideo', this.handleSeekVideo.bind(this));
|
|
502
|
+
|
|
503
|
+
// Split Map
|
|
504
|
+
this.registerMethod('addSplitMap', this.handleAddSplitMap.bind(this));
|
|
505
|
+
this.registerMethod('removeSplitMap', this.handleRemoveSplitMap.bind(this));
|
|
506
|
+
|
|
507
|
+
// Colorbar, Search, Measure, Print
|
|
508
|
+
this.registerMethod('addColorbar', this.handleAddColorbar.bind(this));
|
|
509
|
+
this.registerMethod('removeColorbar', this.handleRemoveColorbar.bind(this));
|
|
510
|
+
this.registerMethod('updateColorbar', this.handleUpdateColorbar.bind(this));
|
|
511
|
+
this.registerMethod('addSearchControl', this.handleAddSearchControl.bind(this));
|
|
512
|
+
this.registerMethod('removeSearchControl', this.handleRemoveSearchControl.bind(this));
|
|
513
|
+
this.registerMethod('addMeasureControl', this.handleAddMeasureControl.bind(this));
|
|
514
|
+
this.registerMethod('removeMeasureControl', this.handleRemoveMeasureControl.bind(this));
|
|
515
|
+
this.registerMethod('addPrintControl', this.handleAddPrintControl.bind(this));
|
|
516
|
+
this.registerMethod('removePrintControl', this.handleRemovePrintControl.bind(this));
|
|
517
|
+
|
|
518
|
+
// FlatGeobuf
|
|
519
|
+
this.registerMethod('addFlatGeobuf', this.handleAddFlatGeobuf.bind(this));
|
|
520
|
+
this.registerMethod('removeFlatGeobuf', this.handleRemoveFlatGeobuf.bind(this));
|
|
282
521
|
}
|
|
283
522
|
|
|
284
523
|
// -------------------------------------------------------------------------
|
|
@@ -624,6 +863,38 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
624
863
|
}
|
|
625
864
|
}
|
|
626
865
|
|
|
866
|
+
private handleAddImageLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
867
|
+
if (!this.map) return;
|
|
868
|
+
const id = (kwargs.id as string) || `image-${Date.now()}`;
|
|
869
|
+
const url = kwargs.url as string;
|
|
870
|
+
const coordinates = kwargs.coordinates as number[][];
|
|
871
|
+
const opacity = (kwargs.opacity as number) ?? 1.0;
|
|
872
|
+
|
|
873
|
+
if (!url || !coordinates || coordinates.length !== 4) {
|
|
874
|
+
console.error('addImageLayer requires url and 4 corner coordinates');
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const sourceId = `${id}-source`;
|
|
879
|
+
|
|
880
|
+
if (!this.map.getSource(sourceId)) {
|
|
881
|
+
this.map.addSource(sourceId, {
|
|
882
|
+
type: 'image',
|
|
883
|
+
url,
|
|
884
|
+
coordinates: coordinates as [[number, number], [number, number], [number, number], [number, number]],
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (!this.map.getLayer(id)) {
|
|
889
|
+
this.map.addLayer({
|
|
890
|
+
id,
|
|
891
|
+
type: 'raster',
|
|
892
|
+
source: sourceId,
|
|
893
|
+
paint: { 'raster-opacity': opacity },
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
627
898
|
// -------------------------------------------------------------------------
|
|
628
899
|
// Control handlers
|
|
629
900
|
// -------------------------------------------------------------------------
|
|
@@ -684,6 +955,118 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
684
955
|
}
|
|
685
956
|
}
|
|
686
957
|
|
|
958
|
+
private handleAddLayerControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
959
|
+
if (!this.map) return;
|
|
960
|
+
let layers = kwargs.layers as string[] | undefined;
|
|
961
|
+
if (!layers || layers.length === 0) {
|
|
962
|
+
const modelLayers = this.model.get('_layers') || {};
|
|
963
|
+
layers = Object.keys(modelLayers);
|
|
964
|
+
}
|
|
965
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
966
|
+
const collapsed = (kwargs.collapsed as boolean) || false;
|
|
967
|
+
|
|
968
|
+
const container = document.createElement('div');
|
|
969
|
+
container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group anymap-layer-control';
|
|
970
|
+
container.style.cssText = 'padding:8px;background:rgba(255,255,255,0.95);border-radius:4px;max-height:200px;overflow-y:auto;';
|
|
971
|
+
|
|
972
|
+
if (collapsed) {
|
|
973
|
+
container.style.display = 'none';
|
|
974
|
+
const toggle = document.createElement('button');
|
|
975
|
+
toggle.textContent = 'Layers';
|
|
976
|
+
toggle.style.cssText = 'padding:4px 8px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;';
|
|
977
|
+
toggle.addEventListener('click', () => {
|
|
978
|
+
container.style.display = container.style.display === 'none' ? 'block' : 'none';
|
|
979
|
+
});
|
|
980
|
+
const wrapper = document.createElement('div');
|
|
981
|
+
wrapper.className = 'mapboxgl-ctrl mapboxgl-ctrl-group';
|
|
982
|
+
wrapper.appendChild(toggle);
|
|
983
|
+
wrapper.appendChild(container);
|
|
984
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(wrapper);
|
|
985
|
+
} else {
|
|
986
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(container);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const style = this.map.getStyle();
|
|
990
|
+
const layersList = style?.layers || [];
|
|
991
|
+
for (const layer of layersList) {
|
|
992
|
+
if (layers && layers.length > 0 && !layers.includes(layer.id)) continue;
|
|
993
|
+
if (layer.id.startsWith('mapbox-') || layer.id.startsWith('maplibre-')) continue;
|
|
994
|
+
|
|
995
|
+
const row = document.createElement('div');
|
|
996
|
+
row.style.cssText = 'display:flex;align-items:center;margin-bottom:4px;';
|
|
997
|
+
const cb = document.createElement('input');
|
|
998
|
+
cb.type = 'checkbox';
|
|
999
|
+
cb.checked = (layer.layout as Record<string, unknown>)?.['visibility'] !== 'none';
|
|
1000
|
+
cb.addEventListener('change', () => {
|
|
1001
|
+
this.map?.setLayoutProperty(layer.id, 'visibility', cb.checked ? 'visible' : 'none');
|
|
1002
|
+
});
|
|
1003
|
+
const label = document.createElement('span');
|
|
1004
|
+
label.textContent = layer.id;
|
|
1005
|
+
label.style.marginLeft = '6px';
|
|
1006
|
+
row.appendChild(cb);
|
|
1007
|
+
row.appendChild(label);
|
|
1008
|
+
container.appendChild(row);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private handleAddDrawControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1013
|
+
if (!this.map) return;
|
|
1014
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
1015
|
+
|
|
1016
|
+
if (this.mapboxDraw) {
|
|
1017
|
+
this.map.removeControl(this.mapboxDraw as unknown as mapboxgl.IControl);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
this.mapboxDraw = new MapboxDraw({
|
|
1021
|
+
displayControlsDefault: false,
|
|
1022
|
+
controls: {
|
|
1023
|
+
point: true,
|
|
1024
|
+
line_string: true,
|
|
1025
|
+
polygon: true,
|
|
1026
|
+
trash: true,
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
this.map.addControl(this.mapboxDraw as unknown as mapboxgl.IControl, position);
|
|
1030
|
+
|
|
1031
|
+
this.map.on('draw.create', () => this.syncDrawData());
|
|
1032
|
+
this.map.on('draw.update', () => this.syncDrawData());
|
|
1033
|
+
this.map.on('draw.delete', () => this.syncDrawData());
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private syncDrawData(): void {
|
|
1037
|
+
if (!this.mapboxDraw) return;
|
|
1038
|
+
const data = this.mapboxDraw.getAll();
|
|
1039
|
+
this.model.set('_draw_data', data);
|
|
1040
|
+
this.model.save_changes();
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private handleGetDrawData(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1044
|
+
if (!this.mapboxDraw) {
|
|
1045
|
+
this.model.set('_draw_data', { type: 'FeatureCollection', features: [] });
|
|
1046
|
+
this.model.save_changes();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const data = this.mapboxDraw.getAll();
|
|
1050
|
+
this.model.set('_draw_data', data);
|
|
1051
|
+
this.model.save_changes();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
private handleLoadDrawData(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1055
|
+
if (!this.mapboxDraw) {
|
|
1056
|
+
console.warn('Draw control not initialized');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const geojson = args[0] as FeatureCollection;
|
|
1060
|
+
this.mapboxDraw.set(geojson);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
private handleClearDrawData(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1064
|
+
if (!this.mapboxDraw) return;
|
|
1065
|
+
this.mapboxDraw.deleteAll();
|
|
1066
|
+
this.model.set('_draw_data', { type: 'FeatureCollection', features: [] });
|
|
1067
|
+
this.model.save_changes();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
687
1070
|
// -------------------------------------------------------------------------
|
|
688
1071
|
// Terrain handlers (Mapbox-specific)
|
|
689
1072
|
// -------------------------------------------------------------------------
|
|
@@ -734,6 +1117,22 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
734
1117
|
this.markersMap.set(id, marker);
|
|
735
1118
|
}
|
|
736
1119
|
|
|
1120
|
+
private handleAddMarkers(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1121
|
+
if (!this.map) return;
|
|
1122
|
+
const id = (kwargs.id as string) || `markers-${Date.now()}`;
|
|
1123
|
+
const markers = kwargs.markers as Array<{ lngLat: [number, number]; popup?: string; tooltip?: string }>;
|
|
1124
|
+
const color = (kwargs.color as string) || '#3388ff';
|
|
1125
|
+
if (!markers || !Array.isArray(markers)) return;
|
|
1126
|
+
for (let i = 0; i < markers.length; i++) {
|
|
1127
|
+
const m = markers[i];
|
|
1128
|
+
const markerId = `${id}-${i}`;
|
|
1129
|
+
const marker = new Marker({ color }).setLngLat(m.lngLat);
|
|
1130
|
+
if (m.popup) marker.setPopup(new Popup().setHTML(m.popup));
|
|
1131
|
+
marker.addTo(this.map);
|
|
1132
|
+
this.markersMap.set(markerId, marker);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
737
1136
|
private handleRemoveMarker(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
738
1137
|
const [id] = args as [string];
|
|
739
1138
|
const marker = this.markersMap.get(id);
|
|
@@ -743,6 +1142,111 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
743
1142
|
}
|
|
744
1143
|
}
|
|
745
1144
|
|
|
1145
|
+
private handleAddPopup(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1146
|
+
if (!this.map) return;
|
|
1147
|
+
const layerId = kwargs.layerId as string;
|
|
1148
|
+
const properties = kwargs.properties as string[] | undefined;
|
|
1149
|
+
const template = kwargs.template as string | undefined;
|
|
1150
|
+
if (!layerId) return;
|
|
1151
|
+
this.map.on('click', layerId, (e) => {
|
|
1152
|
+
if (!e.features || e.features.length === 0) return;
|
|
1153
|
+
const feature = e.features[0];
|
|
1154
|
+
const props = feature.properties || {};
|
|
1155
|
+
let content: string;
|
|
1156
|
+
if (template) {
|
|
1157
|
+
content = template.replace(/\{(\w+)\}/g, (_, key) => (props[key] !== undefined ? String(props[key]) : ''));
|
|
1158
|
+
} else if (properties) {
|
|
1159
|
+
content = properties.filter((k) => props[k] !== undefined).map((k) => `${k}: ${props[k]}`).join('<br>');
|
|
1160
|
+
} else {
|
|
1161
|
+
content = Object.entries(props).map(([k, v]) => `${k}: ${v}`).join('<br>');
|
|
1162
|
+
}
|
|
1163
|
+
new Popup().setLngLat(e.lngLat).setHTML(content).addTo(this.map!);
|
|
1164
|
+
});
|
|
1165
|
+
this.map.on('mouseenter', layerId, () => { if (this.map) this.map.getCanvas().style.cursor = 'pointer'; });
|
|
1166
|
+
this.map.on('mouseleave', layerId, () => { if (this.map) this.map.getCanvas().style.cursor = ''; });
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private handleAddLegend(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1170
|
+
if (!this.map) return;
|
|
1171
|
+
const legendId = (kwargs.id as string) || `legend-${Date.now()}`;
|
|
1172
|
+
const title = (kwargs.title as string) || 'Legend';
|
|
1173
|
+
const items = (kwargs.items as Array<{ label: string; color: string }>) || [];
|
|
1174
|
+
const position = (kwargs.position as string) || 'bottom-right';
|
|
1175
|
+
if (this.legendsMap.has(legendId)) {
|
|
1176
|
+
const old = this.legendsMap.get(legendId);
|
|
1177
|
+
if (old?.parentNode) old.parentNode.removeChild(old);
|
|
1178
|
+
this.legendsMap.delete(legendId);
|
|
1179
|
+
}
|
|
1180
|
+
const legendDiv = document.createElement('div');
|
|
1181
|
+
legendDiv.id = legendId;
|
|
1182
|
+
legendDiv.className = 'mapboxgl-ctrl legend-control';
|
|
1183
|
+
legendDiv.style.cssText = 'padding:10px 14px;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,0.3);font-size:12px;max-width:200px;background:rgba(255,255,255,0.95);';
|
|
1184
|
+
const titleEl = document.createElement('div');
|
|
1185
|
+
titleEl.style.cssText = 'font-weight:bold;margin-bottom:8px;';
|
|
1186
|
+
titleEl.textContent = title;
|
|
1187
|
+
legendDiv.appendChild(titleEl);
|
|
1188
|
+
for (const item of items) {
|
|
1189
|
+
const row = document.createElement('div');
|
|
1190
|
+
row.style.cssText = 'display:flex;align-items:center;margin-bottom:4px;';
|
|
1191
|
+
const colorBox = document.createElement('span');
|
|
1192
|
+
colorBox.style.cssText = `width:16px;height:16px;background:${item.color};margin-right:8px;border-radius:2px;`;
|
|
1193
|
+
row.appendChild(colorBox);
|
|
1194
|
+
const label = document.createElement('span');
|
|
1195
|
+
label.textContent = item.label;
|
|
1196
|
+
row.appendChild(label);
|
|
1197
|
+
legendDiv.appendChild(row);
|
|
1198
|
+
}
|
|
1199
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(legendDiv);
|
|
1200
|
+
this.legendsMap.set(legendId, legendDiv);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private handleRemoveLegend(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1204
|
+
const legendId = args[0] as string | undefined;
|
|
1205
|
+
if (legendId) {
|
|
1206
|
+
const legendDiv = this.legendsMap.get(legendId);
|
|
1207
|
+
if (legendDiv?.parentNode) legendDiv.parentNode.removeChild(legendDiv);
|
|
1208
|
+
this.legendsMap.delete(legendId);
|
|
1209
|
+
} else {
|
|
1210
|
+
for (const [, div] of this.legendsMap) {
|
|
1211
|
+
if (div.parentNode) div.parentNode.removeChild(div);
|
|
1212
|
+
}
|
|
1213
|
+
this.legendsMap.clear();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private handleUpdateLegend(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1218
|
+
const legendId = (kwargs.id as string) || (args[0] as string);
|
|
1219
|
+
if (!legendId) return;
|
|
1220
|
+
const legendDiv = this.legendsMap.get(legendId);
|
|
1221
|
+
if (!legendDiv) return;
|
|
1222
|
+
const title = kwargs.title as string | undefined;
|
|
1223
|
+
const items = kwargs.items as Array<{ label: string; color: string }> | undefined;
|
|
1224
|
+
if (title) {
|
|
1225
|
+
const titleEl = legendDiv.querySelector('div');
|
|
1226
|
+
if (titleEl) titleEl.textContent = title;
|
|
1227
|
+
}
|
|
1228
|
+
if (items && items.length > 0) {
|
|
1229
|
+
legendDiv.querySelectorAll('div:not(:first-child)').forEach((r) => r.remove());
|
|
1230
|
+
for (const item of items) {
|
|
1231
|
+
const row = document.createElement('div');
|
|
1232
|
+
row.style.cssText = 'display:flex;align-items:center;margin-bottom:4px;';
|
|
1233
|
+
const colorBox = document.createElement('span');
|
|
1234
|
+
colorBox.style.cssText = `width:16px;height:16px;background:${item.color};margin-right:8px;border-radius:2px;`;
|
|
1235
|
+
row.appendChild(colorBox);
|
|
1236
|
+
const label = document.createElement('span');
|
|
1237
|
+
label.textContent = item.label;
|
|
1238
|
+
row.appendChild(label);
|
|
1239
|
+
legendDiv.appendChild(row);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
private handleMoveLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1245
|
+
if (!this.map) return;
|
|
1246
|
+
const [layerId, beforeId] = args as [string, string | undefined];
|
|
1247
|
+
if (layerId && this.map.getLayer(layerId)) this.map.moveLayer(layerId, beforeId);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
746
1250
|
// -------------------------------------------------------------------------
|
|
747
1251
|
// COG layer handlers (deck.gl)
|
|
748
1252
|
// -------------------------------------------------------------------------
|
|
@@ -809,6 +1313,53 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
809
1313
|
this.updateDeckOverlay();
|
|
810
1314
|
}
|
|
811
1315
|
|
|
1316
|
+
// -------------------------------------------------------------------------
|
|
1317
|
+
// Zarr layer handlers
|
|
1318
|
+
// -------------------------------------------------------------------------
|
|
1319
|
+
|
|
1320
|
+
private handleAddZarrLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1321
|
+
if (!this.map) return;
|
|
1322
|
+
const id = (kwargs.id as string) || `zarr-${Date.now()}`;
|
|
1323
|
+
const source = kwargs.source as string;
|
|
1324
|
+
const variable = kwargs.variable as string;
|
|
1325
|
+
const clim = (kwargs.clim as [number, number]) || [0, 100];
|
|
1326
|
+
const colormap = (kwargs.colormap as string[]) || ['#000000', '#ffffff'];
|
|
1327
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
1328
|
+
const layer = new ZarrLayer({
|
|
1329
|
+
id,
|
|
1330
|
+
source,
|
|
1331
|
+
variable,
|
|
1332
|
+
clim,
|
|
1333
|
+
colormap,
|
|
1334
|
+
selector: (kwargs.selector as Record<string, number>) || {},
|
|
1335
|
+
opacity,
|
|
1336
|
+
minzoom: kwargs.minzoom as number,
|
|
1337
|
+
maxzoom: kwargs.maxzoom as number,
|
|
1338
|
+
fillValue: kwargs.fillValue as number,
|
|
1339
|
+
spatialDimensions: kwargs.spatialDimensions as { lat?: string; lon?: string },
|
|
1340
|
+
zarrVersion: kwargs.zarrVersion as 2 | 3 | undefined,
|
|
1341
|
+
bounds: kwargs.bounds as [number, number, number, number] | undefined,
|
|
1342
|
+
});
|
|
1343
|
+
this.map.addLayer(layer as unknown as mapboxgl.CustomLayerInterface);
|
|
1344
|
+
this.zarrLayers.set(id, layer);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private handleRemoveZarrLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1348
|
+
const [id] = args as [string];
|
|
1349
|
+
if (this.map?.getLayer(id)) this.map.removeLayer(id);
|
|
1350
|
+
this.zarrLayers.delete(id);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
private handleUpdateZarrLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1354
|
+
const id = kwargs.id as string;
|
|
1355
|
+
const layer = this.zarrLayers.get(id);
|
|
1356
|
+
if (!layer) return;
|
|
1357
|
+
if (kwargs.selector) layer.setSelector(kwargs.selector as Record<string, number>);
|
|
1358
|
+
if (kwargs.clim) layer.setClim(kwargs.clim as [number, number]);
|
|
1359
|
+
if (kwargs.colormap) layer.setColormap(kwargs.colormap as string[]);
|
|
1360
|
+
if (kwargs.opacity !== undefined) layer.setOpacity(kwargs.opacity as number);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
812
1363
|
// -------------------------------------------------------------------------
|
|
813
1364
|
// Arc layer handlers (deck.gl)
|
|
814
1365
|
// -------------------------------------------------------------------------
|
|
@@ -921,86 +1472,689 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
921
1472
|
}
|
|
922
1473
|
|
|
923
1474
|
// -------------------------------------------------------------------------
|
|
924
|
-
//
|
|
1475
|
+
// Additional deck.gl layer handlers
|
|
925
1476
|
// -------------------------------------------------------------------------
|
|
926
1477
|
|
|
927
|
-
|
|
1478
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1479
|
+
private makeDeckAccessor(value: unknown, defaultProp: string, fallbackFn?: (d: any) => any): any {
|
|
1480
|
+
if (typeof value === 'string') return (d: any) => d[value];
|
|
1481
|
+
if (typeof value === 'function') return value;
|
|
1482
|
+
if (value !== undefined && value !== null) return value;
|
|
1483
|
+
return fallbackFn || ((d: any) => d[defaultProp]);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private handleAddScatterplotLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
928
1487
|
if (!this.map) return;
|
|
1488
|
+
this.initializeDeckOverlay();
|
|
1489
|
+
const id = (kwargs.id as string) || `scatterplot-${Date.now()}`;
|
|
1490
|
+
const data = kwargs.data as unknown[];
|
|
1491
|
+
const layer = new ScatterplotLayer({
|
|
1492
|
+
id,
|
|
1493
|
+
data,
|
|
1494
|
+
pickable: kwargs.pickable !== false,
|
|
1495
|
+
opacity: (kwargs.opacity as number) ?? 0.8,
|
|
1496
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || d.position || [d.lng || d.longitude, d.lat || d.latitude]),
|
|
1497
|
+
getRadius: (kwargs.getRadius as number) ?? (kwargs.radius as number) ?? 5,
|
|
1498
|
+
getFillColor: (kwargs.getFillColor as [number, number, number, number]) ?? [51, 136, 255, 200],
|
|
1499
|
+
} as any);
|
|
1500
|
+
this.deckLayers.set(id, layer);
|
|
1501
|
+
this.updateDeckOverlay();
|
|
1502
|
+
}
|
|
929
1503
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
1504
|
+
private handleAddPathLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1505
|
+
if (!this.map) return;
|
|
1506
|
+
this.initializeDeckOverlay();
|
|
1507
|
+
const id = (kwargs.id as string) || `path-${Date.now()}`;
|
|
1508
|
+
const data = kwargs.data as unknown[];
|
|
1509
|
+
const layer = new PathLayer({
|
|
1510
|
+
id,
|
|
1511
|
+
data,
|
|
1512
|
+
getPath: (kwargs.getPath as (d: unknown) => [number, number][]) ?? ((d: any) => d.path || d.coordinates),
|
|
1513
|
+
getColor: (kwargs.getColor as [number, number, number, number]) ?? [51, 136, 255, 200],
|
|
1514
|
+
getWidth: (kwargs.getWidth as number) ?? 1,
|
|
1515
|
+
} as any);
|
|
1516
|
+
this.deckLayers.set(id, layer);
|
|
1517
|
+
this.updateDeckOverlay();
|
|
1518
|
+
}
|
|
934
1519
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
streamingPointBudget: (kwargs.streamingPointBudget as number) || 5000000,
|
|
950
|
-
};
|
|
1520
|
+
private handleAddPolygonLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1521
|
+
if (!this.map) return;
|
|
1522
|
+
this.initializeDeckOverlay();
|
|
1523
|
+
const id = (kwargs.id as string) || `polygon-${Date.now()}`;
|
|
1524
|
+
const data = kwargs.data as unknown[];
|
|
1525
|
+
const layer = new PolygonLayer({
|
|
1526
|
+
id,
|
|
1527
|
+
data,
|
|
1528
|
+
getPolygon: (kwargs.getPolygon as (d: unknown) => number[][][]) ?? ((d: any) => d.polygon || d.coordinates),
|
|
1529
|
+
getFillColor: (kwargs.getFillColor as [number, number, number, number]) ?? [51, 136, 255, 128],
|
|
1530
|
+
} as any);
|
|
1531
|
+
this.deckLayers.set(id, layer);
|
|
1532
|
+
this.updateDeckOverlay();
|
|
1533
|
+
}
|
|
951
1534
|
|
|
952
|
-
|
|
953
|
-
this.
|
|
954
|
-
this.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1535
|
+
private handleAddHexagonLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1536
|
+
if (!this.map) return;
|
|
1537
|
+
this.initializeDeckOverlay();
|
|
1538
|
+
const id = (kwargs.id as string) || `hexagon-${Date.now()}`;
|
|
1539
|
+
const data = kwargs.data as unknown[];
|
|
1540
|
+
const layer = new HexagonLayer({
|
|
1541
|
+
id,
|
|
1542
|
+
data,
|
|
1543
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1544
|
+
radius: (kwargs.radius as number) ?? 1000,
|
|
1545
|
+
elevationScale: (kwargs.elevationScale as number) ?? 4,
|
|
1546
|
+
} as any);
|
|
1547
|
+
this.deckLayers.set(id, layer);
|
|
1548
|
+
this.updateDeckOverlay();
|
|
1549
|
+
}
|
|
958
1550
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1551
|
+
private handleAddHeatmapLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1552
|
+
if (!this.map) return;
|
|
1553
|
+
this.initializeDeckOverlay();
|
|
1554
|
+
const id = (kwargs.id as string) || `heatmap-${Date.now()}`;
|
|
1555
|
+
const data = kwargs.data as unknown[];
|
|
1556
|
+
const layer = new HeatmapLayer({
|
|
1557
|
+
id,
|
|
1558
|
+
data,
|
|
1559
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1560
|
+
getWeight: (kwargs.getWeight as number) ?? 1,
|
|
1561
|
+
} as any);
|
|
1562
|
+
this.deckLayers.set(id, layer);
|
|
1563
|
+
this.updateDeckOverlay();
|
|
1564
|
+
}
|
|
966
1565
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1566
|
+
private handleAddGridLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1567
|
+
if (!this.map) return;
|
|
1568
|
+
this.initializeDeckOverlay();
|
|
1569
|
+
const id = (kwargs.id as string) || `grid-${Date.now()}`;
|
|
1570
|
+
const data = kwargs.data as unknown[];
|
|
1571
|
+
const layer = new GridLayer({
|
|
1572
|
+
id,
|
|
1573
|
+
data,
|
|
1574
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1575
|
+
cellSize: (kwargs.cellSize as number) ?? 200,
|
|
1576
|
+
} as any);
|
|
1577
|
+
this.deckLayers.set(id, layer);
|
|
1578
|
+
this.updateDeckOverlay();
|
|
974
1579
|
}
|
|
975
1580
|
|
|
976
|
-
private
|
|
1581
|
+
private handleAddIconLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
977
1582
|
if (!this.map) return;
|
|
1583
|
+
this.initializeDeckOverlay();
|
|
1584
|
+
const id = (kwargs.id as string) || `icon-${Date.now()}`;
|
|
1585
|
+
const data = kwargs.data as unknown[];
|
|
1586
|
+
const layer = new IconLayer({
|
|
1587
|
+
id,
|
|
1588
|
+
data,
|
|
1589
|
+
iconAtlas: kwargs.iconAtlas as string,
|
|
1590
|
+
iconMapping: kwargs.iconMapping as Record<string, unknown>,
|
|
1591
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1592
|
+
getIcon: (kwargs.getIcon as (d: unknown) => string) ?? (() => 'marker'),
|
|
1593
|
+
} as any);
|
|
1594
|
+
this.deckLayers.set(id, layer);
|
|
1595
|
+
this.updateDeckOverlay();
|
|
1596
|
+
}
|
|
978
1597
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1598
|
+
private handleAddTextLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1599
|
+
if (!this.map) return;
|
|
1600
|
+
this.initializeDeckOverlay();
|
|
1601
|
+
const id = (kwargs.id as string) || `text-${Date.now()}`;
|
|
1602
|
+
const data = kwargs.data as unknown[];
|
|
1603
|
+
const layer = new TextLayer({
|
|
1604
|
+
id,
|
|
1605
|
+
data,
|
|
1606
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1607
|
+
getText: (kwargs.getText as (d: unknown) => string) ?? ((d: any) => d.text || d.label || ''),
|
|
1608
|
+
} as any);
|
|
1609
|
+
this.deckLayers.set(id, layer);
|
|
1610
|
+
this.updateDeckOverlay();
|
|
1611
|
+
}
|
|
982
1612
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
}
|
|
1613
|
+
private handleAddGeoJsonLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1614
|
+
if (!this.map) return;
|
|
1615
|
+
this.initializeDeckOverlay();
|
|
1616
|
+
const id = (kwargs.id as string) || `geojson-${Date.now()}`;
|
|
1617
|
+
const data = kwargs.data as unknown;
|
|
1618
|
+
const layer = new GeoJsonLayer({
|
|
1619
|
+
id,
|
|
1620
|
+
data,
|
|
1621
|
+
getFillColor: (kwargs.getFillColor as [number, number, number, number]) ?? [51, 136, 255, 128],
|
|
1622
|
+
} as any);
|
|
1623
|
+
this.deckLayers.set(id, layer);
|
|
1624
|
+
this.updateDeckOverlay();
|
|
1625
|
+
}
|
|
987
1626
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1627
|
+
private handleAddContourLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1628
|
+
if (!this.map) return;
|
|
1629
|
+
this.initializeDeckOverlay();
|
|
1630
|
+
const id = (kwargs.id as string) || `contour-${Date.now()}`;
|
|
1631
|
+
const data = kwargs.data as unknown[];
|
|
1632
|
+
const layer = new ContourLayer({
|
|
1633
|
+
id,
|
|
1634
|
+
data,
|
|
1635
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1636
|
+
} as any);
|
|
1637
|
+
this.deckLayers.set(id, layer);
|
|
1638
|
+
this.updateDeckOverlay();
|
|
1639
|
+
}
|
|
1000
1640
|
|
|
1001
|
-
|
|
1641
|
+
private handleAddScreenGridLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1642
|
+
if (!this.map) return;
|
|
1643
|
+
this.initializeDeckOverlay();
|
|
1644
|
+
const id = (kwargs.id as string) || `screengrid-${Date.now()}`;
|
|
1645
|
+
const data = kwargs.data as unknown[];
|
|
1646
|
+
const layer = new ScreenGridLayer({
|
|
1647
|
+
id,
|
|
1648
|
+
data,
|
|
1649
|
+
getPosition: (kwargs.getPosition as (d: unknown) => [number, number]) ?? ((d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1650
|
+
} as any);
|
|
1651
|
+
this.deckLayers.set(id, layer);
|
|
1652
|
+
this.updateDeckOverlay();
|
|
1653
|
+
}
|
|
1002
1654
|
|
|
1003
|
-
|
|
1655
|
+
private handleAddTripsLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1656
|
+
if (!this.map) return;
|
|
1657
|
+
this.initializeDeckOverlay();
|
|
1658
|
+
const id = (kwargs.id as string) || `trips-${Date.now()}`;
|
|
1659
|
+
const data = kwargs.data as unknown[];
|
|
1660
|
+
const layer = new TripsLayer({
|
|
1661
|
+
id,
|
|
1662
|
+
data,
|
|
1663
|
+
getPath: this.makeDeckAccessor(kwargs.getPath, 'waypoints', (d: any) => d.waypoints || d.path),
|
|
1664
|
+
getTimestamps: this.makeDeckAccessor(kwargs.getTimestamps, 'timestamps', (d: any) => d.timestamps),
|
|
1665
|
+
} as any);
|
|
1666
|
+
this.deckLayers.set(id, layer);
|
|
1667
|
+
this.updateDeckOverlay();
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
private handleAddLineLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1671
|
+
if (!this.map) return;
|
|
1672
|
+
this.initializeDeckOverlay();
|
|
1673
|
+
const id = (kwargs.id as string) || `line-${Date.now()}`;
|
|
1674
|
+
const data = kwargs.data as unknown[];
|
|
1675
|
+
const layer = new LineLayer({
|
|
1676
|
+
id,
|
|
1677
|
+
data,
|
|
1678
|
+
getSourcePosition: this.makeDeckAccessor(kwargs.getSourcePosition, 'source', (d: any) => d.source || d.from),
|
|
1679
|
+
getTargetPosition: this.makeDeckAccessor(kwargs.getTargetPosition, 'target', (d: any) => d.target || d.to),
|
|
1680
|
+
getColor: this.makeDeckAccessor(kwargs.getColor, 'color', () => [51, 136, 255, 200]),
|
|
1681
|
+
} as any);
|
|
1682
|
+
this.deckLayers.set(id, layer);
|
|
1683
|
+
this.updateDeckOverlay();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
private handleAddDeckGLLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1687
|
+
const layerType = kwargs.layerType as string;
|
|
1688
|
+
const handlerMap: Record<string, (a: unknown[], k: Record<string, unknown>) => void> = {
|
|
1689
|
+
ScatterplotLayer: this.handleAddScatterplotLayer.bind(this),
|
|
1690
|
+
ArcLayer: this.handleAddArcLayer.bind(this),
|
|
1691
|
+
PathLayer: this.handleAddPathLayer.bind(this),
|
|
1692
|
+
PolygonLayer: this.handleAddPolygonLayer.bind(this),
|
|
1693
|
+
HexagonLayer: this.handleAddHexagonLayer.bind(this),
|
|
1694
|
+
HeatmapLayer: this.handleAddHeatmapLayer.bind(this),
|
|
1695
|
+
GridLayer: this.handleAddGridLayer.bind(this),
|
|
1696
|
+
IconLayer: this.handleAddIconLayer.bind(this),
|
|
1697
|
+
TextLayer: this.handleAddTextLayer.bind(this),
|
|
1698
|
+
GeoJsonLayer: this.handleAddGeoJsonLayer.bind(this),
|
|
1699
|
+
ContourLayer: this.handleAddContourLayer.bind(this),
|
|
1700
|
+
ScreenGridLayer: this.handleAddScreenGridLayer.bind(this),
|
|
1701
|
+
PointCloudLayer: this.handleAddPointCloudLayer.bind(this),
|
|
1702
|
+
TripsLayer: this.handleAddTripsLayer.bind(this),
|
|
1703
|
+
LineLayer: this.handleAddLineLayer.bind(this),
|
|
1704
|
+
BitmapLayer: this.handleAddBitmapLayer.bind(this),
|
|
1705
|
+
ColumnLayer: this.handleAddColumnLayer.bind(this),
|
|
1706
|
+
GridCellLayer: this.handleAddGridCellLayer.bind(this),
|
|
1707
|
+
SolidPolygonLayer: this.handleAddSolidPolygonLayer.bind(this),
|
|
1708
|
+
};
|
|
1709
|
+
const handler = handlerMap[layerType];
|
|
1710
|
+
if (handler) handler(args, kwargs);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
private handleRemoveDeckLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1714
|
+
const id = (args[0] as string) || (kwargs.id as string);
|
|
1715
|
+
if (id) {
|
|
1716
|
+
this.deckLayers.delete(id);
|
|
1717
|
+
this.updateDeckOverlay();
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private handleSetDeckLayerVisibility(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1722
|
+
const id = (args[0] as string) || (kwargs.id as string);
|
|
1723
|
+
const visible = (args[1] as boolean) ?? (kwargs.visible as boolean) ?? true;
|
|
1724
|
+
if (!id) return;
|
|
1725
|
+
const layer = this.deckLayers.get(id) as { clone?: (p: Record<string, unknown>) => unknown } | undefined;
|
|
1726
|
+
if (layer?.clone) {
|
|
1727
|
+
this.deckLayers.set(id, layer.clone({ visible }));
|
|
1728
|
+
this.updateDeckOverlay();
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
private handleAddBitmapLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1733
|
+
if (!this.map) return;
|
|
1734
|
+
this.initializeDeckOverlay();
|
|
1735
|
+
const id = (kwargs.id as string) || `bitmap-${Date.now()}`;
|
|
1736
|
+
const layer = new BitmapLayer({
|
|
1737
|
+
id,
|
|
1738
|
+
image: kwargs.image as string,
|
|
1739
|
+
bounds: kwargs.bounds as [number, number, number, number],
|
|
1740
|
+
});
|
|
1741
|
+
this.deckLayers.set(id, layer);
|
|
1742
|
+
this.updateDeckOverlay();
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
private handleAddColumnLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1746
|
+
if (!this.map) return;
|
|
1747
|
+
this.initializeDeckOverlay();
|
|
1748
|
+
const id = (kwargs.id as string) || `column-${Date.now()}`;
|
|
1749
|
+
const data = kwargs.data as unknown[];
|
|
1750
|
+
const layer = new ColumnLayer({
|
|
1751
|
+
id,
|
|
1752
|
+
data,
|
|
1753
|
+
getPosition: this.makeDeckAccessor(kwargs.getPosition, 'coordinates', (d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1754
|
+
getElevation: this.makeDeckAccessor(kwargs.getElevation, 'elevation', () => 1000),
|
|
1755
|
+
} as any);
|
|
1756
|
+
this.deckLayers.set(id, layer);
|
|
1757
|
+
this.updateDeckOverlay();
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
private handleAddGridCellLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1761
|
+
if (!this.map) return;
|
|
1762
|
+
this.initializeDeckOverlay();
|
|
1763
|
+
const id = (kwargs.id as string) || `gridcell-${Date.now()}`;
|
|
1764
|
+
const data = kwargs.data as unknown[];
|
|
1765
|
+
const layer = new GridCellLayer({
|
|
1766
|
+
id,
|
|
1767
|
+
data,
|
|
1768
|
+
getPosition: this.makeDeckAccessor(kwargs.getPosition, 'coordinates', (d: any) => d.coordinates || [d.lng, d.lat]),
|
|
1769
|
+
} as any);
|
|
1770
|
+
this.deckLayers.set(id, layer);
|
|
1771
|
+
this.updateDeckOverlay();
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
private handleAddSolidPolygonLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1775
|
+
if (!this.map) return;
|
|
1776
|
+
this.initializeDeckOverlay();
|
|
1777
|
+
const id = (kwargs.id as string) || `solidpolygon-${Date.now()}`;
|
|
1778
|
+
const data = kwargs.data as unknown[];
|
|
1779
|
+
const layer = new SolidPolygonLayer({
|
|
1780
|
+
id,
|
|
1781
|
+
data,
|
|
1782
|
+
getPolygon: this.makeDeckAccessor(kwargs.getPolygon, 'polygon', (d: any) => d.polygon || d.coordinates),
|
|
1783
|
+
} as any);
|
|
1784
|
+
this.deckLayers.set(id, layer);
|
|
1785
|
+
this.updateDeckOverlay();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// -------------------------------------------------------------------------
|
|
1789
|
+
// Native Mapbox feature handlers
|
|
1790
|
+
// -------------------------------------------------------------------------
|
|
1791
|
+
|
|
1792
|
+
private handleSetProjection(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1793
|
+
if (!this.map) return;
|
|
1794
|
+
const projection = (kwargs.projection as string) || (args[0] as string) || 'mercator';
|
|
1795
|
+
try {
|
|
1796
|
+
this.map.setProjection({ type: projection } as unknown as mapboxgl.ProjectionSpecification);
|
|
1797
|
+
} catch {
|
|
1798
|
+
// Ignore
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
private handleUpdateGeoJSONSource(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1803
|
+
if (!this.map) return;
|
|
1804
|
+
const sourceId = (kwargs.sourceId as string) || (args[0] as string);
|
|
1805
|
+
const data = kwargs.data;
|
|
1806
|
+
if (!sourceId) return;
|
|
1807
|
+
let source = this.map.getSource(sourceId) as mapboxgl.GeoJSONSource;
|
|
1808
|
+
if (!source && !sourceId.endsWith('-source')) {
|
|
1809
|
+
source = this.map.getSource(sourceId + '-source') as mapboxgl.GeoJSONSource;
|
|
1810
|
+
}
|
|
1811
|
+
if (source?.setData) source.setData(data as GeoJSON.GeoJSON);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
private handleAddMapImage(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1815
|
+
if (!this.map) return;
|
|
1816
|
+
const name = (kwargs.name as string) || (args[0] as string);
|
|
1817
|
+
const url = (kwargs.url as string) || (args[1] as string);
|
|
1818
|
+
if (!name || !url) return;
|
|
1819
|
+
this.map.loadImage(url, (err, image) => {
|
|
1820
|
+
if (err) {
|
|
1821
|
+
console.warn('Failed to load image:', err);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
if (image && !this.map!.hasImage(name)) {
|
|
1825
|
+
this.map!.addImage(name, image);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
private handleAddTooltip(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1831
|
+
if (!this.map) return;
|
|
1832
|
+
const layerId = kwargs.layerId as string;
|
|
1833
|
+
const template = (kwargs.template as string) || '';
|
|
1834
|
+
const properties = kwargs.properties as string[] | undefined;
|
|
1835
|
+
if (!layerId) return;
|
|
1836
|
+
|
|
1837
|
+
if (this.tooltipLayerHandlers.has(layerId)) {
|
|
1838
|
+
const old = this.tooltipLayerHandlers.get(layerId)!;
|
|
1839
|
+
this.map.off('mousemove', layerId, old);
|
|
1840
|
+
this.tooltipLayerHandlers.delete(layerId);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const popup = new Popup({ closeButton: false, closeOnClick: false });
|
|
1844
|
+
const handler = (e: mapboxgl.MapMouseEvent & { features?: GeoJSON.Feature[] }) => {
|
|
1845
|
+
if (!e.features?.length) {
|
|
1846
|
+
popup.remove();
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const props = e.features[0].properties || {};
|
|
1850
|
+
let html = template
|
|
1851
|
+
? template.replace(/\{(\w+)\}/g, (_, k) => (props[k] !== undefined ? String(props[k]) : ''))
|
|
1852
|
+
: Object.entries(props).map(([k, v]) => `<b>${k}:</b> ${v}`).join('<br>');
|
|
1853
|
+
popup.setLngLat(e.lngLat).setHTML(`<div style="font-size:12px">${html}</div>`).addTo(this.map!);
|
|
1854
|
+
};
|
|
1855
|
+
this.map.on('mousemove', layerId, handler);
|
|
1856
|
+
this.map.on('mouseleave', layerId, () => popup.remove());
|
|
1857
|
+
this.tooltipLayerHandlers.set(layerId, handler);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
private handleRemoveTooltip(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1861
|
+
const layerId = kwargs.layerId as string;
|
|
1862
|
+
if (layerId && this.tooltipLayerHandlers.has(layerId)) {
|
|
1863
|
+
const handler = this.tooltipLayerHandlers.get(layerId)!;
|
|
1864
|
+
this.map?.off('mousemove', layerId, handler);
|
|
1865
|
+
this.tooltipLayerHandlers.delete(layerId);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
private handleAddCoordinatesControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1870
|
+
if (!this.map) return;
|
|
1871
|
+
const position = (kwargs.position as string) || 'bottom-left';
|
|
1872
|
+
if (this.coordinatesControl) {
|
|
1873
|
+
this.coordinatesControl.remove();
|
|
1874
|
+
if (this.coordinatesHandler) this.map.off('mousemove', this.coordinatesHandler);
|
|
1875
|
+
}
|
|
1876
|
+
const div = document.createElement('div');
|
|
1877
|
+
div.className = 'mapboxgl-ctrl mapboxgl-ctrl-group anymap-coordinates';
|
|
1878
|
+
div.style.cssText = 'padding:4px 8px;font-size:11px;font-family:monospace;background:rgba(255,255,255,0.9);';
|
|
1879
|
+
div.textContent = 'Lng: 0.0000, Lat: 0.0000';
|
|
1880
|
+
const precision = (kwargs.precision as number) ?? 4;
|
|
1881
|
+
const handler = (e: mapboxgl.MapMouseEvent) => {
|
|
1882
|
+
div.textContent = `Lng: ${e.lngLat.lng.toFixed(precision)}, Lat: ${e.lngLat.lat.toFixed(precision)}`;
|
|
1883
|
+
};
|
|
1884
|
+
this.map.on('mousemove', handler);
|
|
1885
|
+
this.coordinatesHandler = handler;
|
|
1886
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(div);
|
|
1887
|
+
this.coordinatesControl = div;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
private handleRemoveCoordinatesControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1891
|
+
if (this.coordinatesControl) {
|
|
1892
|
+
this.coordinatesControl.remove();
|
|
1893
|
+
this.coordinatesControl = null;
|
|
1894
|
+
}
|
|
1895
|
+
if (this.coordinatesHandler) {
|
|
1896
|
+
this.map?.off('mousemove', this.coordinatesHandler);
|
|
1897
|
+
this.coordinatesHandler = null;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
private handleAddTimeSlider(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1902
|
+
if (!this.map) return;
|
|
1903
|
+
const layerId = kwargs.layerId as string;
|
|
1904
|
+
const property = kwargs.property as string;
|
|
1905
|
+
const min = (kwargs.min as number) ?? 0;
|
|
1906
|
+
const max = (kwargs.max as number) ?? 100;
|
|
1907
|
+
const step = (kwargs.step as number) ?? 1;
|
|
1908
|
+
const position = (kwargs.position as string) || 'bottom-left';
|
|
1909
|
+
|
|
1910
|
+
this.handleRemoveTimeSlider([], {});
|
|
1911
|
+
|
|
1912
|
+
const container = document.createElement('div');
|
|
1913
|
+
container.className = 'mapboxgl-ctrl anymap-time-slider';
|
|
1914
|
+
container.style.cssText = 'padding:10px;background:rgba(255,255,255,0.95);min-width:250px;';
|
|
1915
|
+
|
|
1916
|
+
const label = document.createElement('div');
|
|
1917
|
+
label.textContent = `${kwargs.label || 'Time'}: ${min}`;
|
|
1918
|
+
|
|
1919
|
+
const slider = document.createElement('input');
|
|
1920
|
+
slider.type = 'range';
|
|
1921
|
+
slider.min = String(min);
|
|
1922
|
+
slider.max = String(max);
|
|
1923
|
+
slider.value = String(min);
|
|
1924
|
+
slider.addEventListener('input', () => {
|
|
1925
|
+
const val = Number(slider.value);
|
|
1926
|
+
label.textContent = `${kwargs.label || 'Time'}: ${val}`;
|
|
1927
|
+
if (layerId && property) this.map?.setFilter(layerId, ['<=', property, val]);
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
container.appendChild(label);
|
|
1931
|
+
container.appendChild(slider);
|
|
1932
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(container);
|
|
1933
|
+
this.timeSliderContainer = container;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
private handleRemoveTimeSlider(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1937
|
+
if (this.timeSliderContainer) {
|
|
1938
|
+
this.timeSliderContainer.remove();
|
|
1939
|
+
this.timeSliderContainer = null;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
private handleAddSwipeMap(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1944
|
+
if (!this.map) return;
|
|
1945
|
+
const leftLayer = kwargs.leftLayer as string;
|
|
1946
|
+
const rightLayer = kwargs.rightLayer as string;
|
|
1947
|
+
if (!leftLayer || !rightLayer) return;
|
|
1948
|
+
this.handleRemoveSwipeMap([], {});
|
|
1949
|
+
|
|
1950
|
+
const container = this.map.getContainer();
|
|
1951
|
+
const slider = document.createElement('div');
|
|
1952
|
+
slider.style.cssText = 'position:absolute;top:0;bottom:0;width:4px;background:#fff;cursor:ew-resize;z-index:10;left:50%;';
|
|
1953
|
+
container.appendChild(slider);
|
|
1954
|
+
this.swipeContainer = slider;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
private handleRemoveSwipeMap(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1958
|
+
if (this.swipeContainer) {
|
|
1959
|
+
this.swipeContainer.remove();
|
|
1960
|
+
this.swipeContainer = null;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
private handleAddOpacitySlider(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1965
|
+
if (!this.map) return;
|
|
1966
|
+
const layerId = kwargs.layerId as string;
|
|
1967
|
+
const position = (kwargs.position as string) || 'top-right';
|
|
1968
|
+
if (!layerId) return;
|
|
1969
|
+
|
|
1970
|
+
const container = document.createElement('div');
|
|
1971
|
+
container.className = 'mapboxgl-ctrl anymap-opacity-slider';
|
|
1972
|
+
container.style.cssText = 'padding:8px;background:rgba(255,255,255,0.95);min-width:150px;';
|
|
1973
|
+
|
|
1974
|
+
const label = document.createElement('div');
|
|
1975
|
+
label.textContent = `${kwargs.label || layerId}: 100%`;
|
|
1976
|
+
|
|
1977
|
+
const slider = document.createElement('input');
|
|
1978
|
+
slider.type = 'range';
|
|
1979
|
+
slider.min = '0';
|
|
1980
|
+
slider.max = '100';
|
|
1981
|
+
slider.value = '100';
|
|
1982
|
+
slider.addEventListener('input', () => {
|
|
1983
|
+
const opacity = Number(slider.value) / 100;
|
|
1984
|
+
label.textContent = `${kwargs.label || layerId}: ${slider.value}%`;
|
|
1985
|
+
if (this.map?.getLayer(layerId)) {
|
|
1986
|
+
const layer = this.map.getLayer(layerId);
|
|
1987
|
+
const type = (layer as { type?: string })?.type;
|
|
1988
|
+
const prop = type === 'raster' ? 'raster-opacity' : type === 'fill' ? 'fill-opacity' : type === 'line' ? 'line-opacity' : type === 'circle' ? 'circle-opacity' : null;
|
|
1989
|
+
if (prop) this.map.setPaintProperty(layerId, prop, opacity);
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
container.appendChild(label);
|
|
1994
|
+
container.appendChild(slider);
|
|
1995
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(container);
|
|
1996
|
+
this.opacitySliderContainer.set(layerId, container);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
private handleRemoveOpacitySlider(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2000
|
+
const layerId = kwargs.layerId as string;
|
|
2001
|
+
const el = this.opacitySliderContainer.get(layerId);
|
|
2002
|
+
if (el) {
|
|
2003
|
+
el.remove();
|
|
2004
|
+
this.opacitySliderContainer.delete(layerId);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
private handleAddStyleSwitcher(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2009
|
+
if (!this.map) return;
|
|
2010
|
+
const styles = kwargs.styles as Record<string, string>;
|
|
2011
|
+
const position = (kwargs.position as string) || 'top-right';
|
|
2012
|
+
if (!styles || !Object.keys(styles).length) return;
|
|
2013
|
+
this.handleRemoveStyleSwitcher([], {});
|
|
2014
|
+
|
|
2015
|
+
const container = document.createElement('div');
|
|
2016
|
+
container.className = 'mapboxgl-ctrl anymap-style-switcher';
|
|
2017
|
+
const select = document.createElement('select');
|
|
2018
|
+
for (const [name, url] of Object.entries(styles)) {
|
|
2019
|
+
const opt = document.createElement('option');
|
|
2020
|
+
opt.value = url;
|
|
2021
|
+
opt.textContent = name;
|
|
2022
|
+
select.appendChild(opt);
|
|
2023
|
+
}
|
|
2024
|
+
select.addEventListener('change', () => this.map!.setStyle(select.value));
|
|
2025
|
+
container.appendChild(select);
|
|
2026
|
+
this.map.getContainer().querySelector(`.mapboxgl-ctrl-${position}`)?.appendChild(container);
|
|
2027
|
+
this.styleSwitcherContainer = container;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
private handleRemoveStyleSwitcher(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2031
|
+
if (this.styleSwitcherContainer) {
|
|
2032
|
+
this.styleSwitcherContainer.remove();
|
|
2033
|
+
this.styleSwitcherContainer = null;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
private handleGetVisibleFeatures(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2038
|
+
if (!this.map) return;
|
|
2039
|
+
const layers = kwargs.layers as string[] | undefined;
|
|
2040
|
+
const canvas = this.map.getCanvas();
|
|
2041
|
+
const bbox: [mapboxgl.PointLike, mapboxgl.PointLike] = canvas
|
|
2042
|
+
? [[0, 0], [canvas.width, canvas.height]]
|
|
2043
|
+
: [[0, 0], [256, 256]];
|
|
2044
|
+
const features = layers
|
|
2045
|
+
? this.map.queryRenderedFeatures(bbox, { layers })
|
|
2046
|
+
: this.map.queryRenderedFeatures(bbox);
|
|
2047
|
+
const geojson: FeatureCollection = {
|
|
2048
|
+
type: 'FeatureCollection',
|
|
2049
|
+
features: features.map((f) => ({ type: 'Feature' as const, geometry: f.geometry, properties: f.properties })),
|
|
2050
|
+
};
|
|
2051
|
+
this.model.set('_queried_features', { type: 'visible_features', data: geojson });
|
|
2052
|
+
this.model.save_changes();
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
private handleGetLayerData(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2056
|
+
if (!this.map) return;
|
|
2057
|
+
const sourceId = kwargs.sourceId as string;
|
|
2058
|
+
if (!sourceId) return;
|
|
2059
|
+
let features: GeoJSON.Feature[] = [];
|
|
2060
|
+
try {
|
|
2061
|
+
features = this.map.querySourceFeatures(sourceId);
|
|
2062
|
+
} catch {
|
|
2063
|
+
try {
|
|
2064
|
+
features = this.map.querySourceFeatures(sourceId + '-source');
|
|
2065
|
+
} catch {
|
|
2066
|
+
// ignore
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
const geojson: FeatureCollection = {
|
|
2070
|
+
type: 'FeatureCollection',
|
|
2071
|
+
features: features.map((f) => ({ type: 'Feature' as const, geometry: f.geometry, properties: f.properties })),
|
|
2072
|
+
};
|
|
2073
|
+
this.model.set('_queried_features', { type: 'layer_data', sourceId, data: geojson });
|
|
2074
|
+
this.model.save_changes();
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// -------------------------------------------------------------------------
|
|
2078
|
+
// LiDAR layer handlers (maplibre-gl-lidar)
|
|
2079
|
+
// -------------------------------------------------------------------------
|
|
2080
|
+
|
|
2081
|
+
private handleAddLidarControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2082
|
+
if (!this.map) return;
|
|
2083
|
+
|
|
2084
|
+
if (this.lidarControl) {
|
|
2085
|
+
console.warn('LiDAR control already exists');
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
const options = {
|
|
2090
|
+
position: (kwargs.position as string) || 'top-right',
|
|
2091
|
+
collapsed: kwargs.collapsed !== false,
|
|
2092
|
+
title: (kwargs.title as string) || 'LiDAR Viewer',
|
|
2093
|
+
panelWidth: (kwargs.panelWidth as number) || 365,
|
|
2094
|
+
panelMaxHeight: (kwargs.panelMaxHeight as number) || 600,
|
|
2095
|
+
pointSize: (kwargs.pointSize as number) || 2,
|
|
2096
|
+
opacity: (kwargs.opacity as number) || 1.0,
|
|
2097
|
+
colorScheme: (kwargs.colorScheme as string) || 'elevation',
|
|
2098
|
+
usePercentile: kwargs.usePercentile !== false,
|
|
2099
|
+
pointBudget: (kwargs.pointBudget as number) || 1000000,
|
|
2100
|
+
pickable: kwargs.pickable === true,
|
|
2101
|
+
autoZoom: kwargs.autoZoom !== false,
|
|
2102
|
+
copcLoadingMode: kwargs.copcLoadingMode as 'full' | 'dynamic' | undefined,
|
|
2103
|
+
streamingPointBudget: (kwargs.streamingPointBudget as number) || 5000000,
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
// LidarControl works with both MapLibre and Mapbox GL JS
|
|
2107
|
+
this.lidarControl = new LidarControl(options as LidarControlOptions);
|
|
2108
|
+
this.map.addControl(
|
|
2109
|
+
this.lidarControl as unknown as mapboxgl.IControl,
|
|
2110
|
+
options.position as ControlPosition
|
|
2111
|
+
);
|
|
2112
|
+
|
|
2113
|
+
this.lidarControl.on('load', (event) => {
|
|
2114
|
+
const info = event.pointCloud as { id: string; name: string; pointCount: number; source?: string } | undefined;
|
|
2115
|
+
if (info && 'name' in info) {
|
|
2116
|
+
this.lidarLayers.set(info.id, info.source || '');
|
|
2117
|
+
this.sendEvent('lidar:load', { id: info.id, name: info.name, pointCount: info.pointCount });
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
this.lidarControl.on('unload', (event) => {
|
|
2122
|
+
const pointCloud = event.pointCloud as { id: string } | undefined;
|
|
2123
|
+
if (pointCloud) {
|
|
2124
|
+
this.lidarLayers.delete(pointCloud.id);
|
|
2125
|
+
this.sendEvent('lidar:unload', { id: pointCloud.id });
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
private handleAddLidarLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2131
|
+
if (!this.map) return;
|
|
2132
|
+
|
|
2133
|
+
const source = kwargs.source as string;
|
|
2134
|
+
const name = (kwargs.name as string) || `lidar-${Date.now()}`;
|
|
2135
|
+
const isBase64 = kwargs.isBase64 === true;
|
|
2136
|
+
|
|
2137
|
+
if (!source) {
|
|
2138
|
+
console.error('LiDAR layer requires a source URL or base64 data');
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (!this.lidarControl) {
|
|
2143
|
+
this.lidarControl = new LidarControl({
|
|
2144
|
+
collapsed: true,
|
|
2145
|
+
position: 'top-right',
|
|
2146
|
+
pointSize: (kwargs.pointSize as number) || 2,
|
|
2147
|
+
opacity: (kwargs.opacity as number) || 1.0,
|
|
2148
|
+
colorScheme: (kwargs.colorScheme as string) || 'elevation',
|
|
2149
|
+
usePercentile: kwargs.usePercentile !== false,
|
|
2150
|
+
pointBudget: (kwargs.pointBudget as number) || 1000000,
|
|
2151
|
+
pickable: kwargs.pickable !== false,
|
|
2152
|
+
autoZoom: kwargs.autoZoom !== false,
|
|
2153
|
+
} as LidarControlOptions);
|
|
2154
|
+
|
|
2155
|
+
this.map.addControl(this.lidarControl as unknown as mapboxgl.IControl, 'top-right');
|
|
2156
|
+
|
|
2157
|
+
this.lidarControl.on('load', (event) => {
|
|
1004
2158
|
const info = event.pointCloud as { id: string; name: string; pointCount: number; source?: string } | undefined;
|
|
1005
2159
|
if (info && 'name' in info) {
|
|
1006
2160
|
this.lidarLayers.set(info.id, info.source || '');
|
|
@@ -1098,6 +2252,716 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
1098
2252
|
}
|
|
1099
2253
|
}
|
|
1100
2254
|
|
|
2255
|
+
// -------------------------------------------------------------------------
|
|
2256
|
+
// PMTiles, maplibre-gl-components, clustering, choropleth, 3D buildings
|
|
2257
|
+
// -------------------------------------------------------------------------
|
|
2258
|
+
|
|
2259
|
+
private handleAddPMTilesLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2260
|
+
if (!this.map) return;
|
|
2261
|
+
const url = kwargs.url as string;
|
|
2262
|
+
const layerId = (kwargs.id as string) || `pmtiles-${Date.now()}`;
|
|
2263
|
+
const sourceType = (kwargs.sourceType as string) || 'vector';
|
|
2264
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
2265
|
+
const pmtilesUrl = url.startsWith('pmtiles://') ? url : `pmtiles://${url}`;
|
|
2266
|
+
const sourceId = `${layerId}-source`;
|
|
2267
|
+
|
|
2268
|
+
if (!this.map.getSource(sourceId)) {
|
|
2269
|
+
this.map.addSource(sourceId, { type: sourceType as 'vector' | 'raster', url: pmtilesUrl });
|
|
2270
|
+
}
|
|
2271
|
+
if (!this.map.getLayer(layerId)) {
|
|
2272
|
+
const layerConfig: Record<string, unknown> = {
|
|
2273
|
+
id: layerId,
|
|
2274
|
+
type: sourceType === 'vector' ? 'fill' : 'raster',
|
|
2275
|
+
source: sourceId,
|
|
2276
|
+
paint: sourceType === 'vector' ? { 'fill-color': '#3388ff', 'fill-opacity': opacity } : { 'raster-opacity': opacity },
|
|
2277
|
+
};
|
|
2278
|
+
this.map.addLayer(layerConfig as mapboxgl.AnyLayer);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
private handleRemovePMTilesLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2283
|
+
const [layerId] = args as [string];
|
|
2284
|
+
const sourceId = `${layerId}-source`;
|
|
2285
|
+
if (this.map?.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
2286
|
+
if (this.map?.getSource(sourceId)) this.map.removeSource(sourceId);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
private handleAddPMTilesControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2290
|
+
if (!this.map) return;
|
|
2291
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
2292
|
+
this.pmtilesLayerControl = new PMTilesLayerControl({ collapsed: kwargs.collapsed !== false } as any);
|
|
2293
|
+
this.map.addControl(this.pmtilesLayerControl as unknown as mapboxgl.IControl, position);
|
|
2294
|
+
this.controlsMap.set('pmtiles-control', this.pmtilesLayerControl as unknown as mapboxgl.IControl);
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
private handleAddCogControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2298
|
+
if (!this.map) return;
|
|
2299
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
2300
|
+
this.cogLayerUiControl = new CogLayerControl({ collapsed: kwargs.collapsed !== false } as any);
|
|
2301
|
+
this.map.addControl(this.cogLayerUiControl as unknown as mapboxgl.IControl, position);
|
|
2302
|
+
this.controlsMap.set('cog-control', this.cogLayerUiControl as unknown as mapboxgl.IControl);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
private handleAddZarrControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2306
|
+
if (!this.map) return;
|
|
2307
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
2308
|
+
this.zarrLayerUiControl = new ZarrLayerControl({ collapsed: kwargs.collapsed !== false } as any);
|
|
2309
|
+
this.map.addControl(this.zarrLayerUiControl as unknown as mapboxgl.IControl, position);
|
|
2310
|
+
this.controlsMap.set('zarr-control', this.zarrLayerUiControl as unknown as mapboxgl.IControl);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
private handleAddVectorControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2314
|
+
if (!this.map) return;
|
|
2315
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
2316
|
+
this.addVectorControl = new AddVectorControl({ collapsed: kwargs.collapsed !== false } as any);
|
|
2317
|
+
this.map.addControl(this.addVectorControl as unknown as mapboxgl.IControl, position);
|
|
2318
|
+
this.controlsMap.set('vector-control', this.addVectorControl as unknown as mapboxgl.IControl);
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
private handleAddControlGrid(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2322
|
+
if (!this.map) return;
|
|
2323
|
+
const position = (kwargs.position as string) || 'top-right';
|
|
2324
|
+
this.controlGrid = addControlGrid(this.map as any, { position } as any);
|
|
2325
|
+
this.controlsMap.set('control-grid', this.controlGrid as unknown as mapboxgl.IControl);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
private handleAddClusterLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2329
|
+
if (!this.map) return;
|
|
2330
|
+
const geojson = kwargs.data as GeoJSON.FeatureCollection;
|
|
2331
|
+
const name = (kwargs.name as string) || `cluster-${Date.now()}`;
|
|
2332
|
+
const sourceId = `${name}-source`;
|
|
2333
|
+
|
|
2334
|
+
if (!this.map.getSource(sourceId)) {
|
|
2335
|
+
this.map.addSource(sourceId, {
|
|
2336
|
+
type: 'geojson',
|
|
2337
|
+
data: geojson,
|
|
2338
|
+
cluster: true,
|
|
2339
|
+
clusterMaxZoom: (kwargs.clusterMaxZoom as number) || 14,
|
|
2340
|
+
clusterRadius: (kwargs.clusterRadius as number) || 50,
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
const clusterLayerId = `${name}-clusters`;
|
|
2344
|
+
if (!this.map.getLayer(clusterLayerId)) {
|
|
2345
|
+
this.map.addLayer({
|
|
2346
|
+
id: clusterLayerId,
|
|
2347
|
+
type: 'circle',
|
|
2348
|
+
source: sourceId,
|
|
2349
|
+
filter: ['has', 'point_count'],
|
|
2350
|
+
paint: {
|
|
2351
|
+
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 750, '#f28cb1'],
|
|
2352
|
+
'circle-radius': ['step', ['get', 'point_count'], 15, 100, 20, 750, 25],
|
|
2353
|
+
},
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
const unclusteredId = `${name}-unclustered`;
|
|
2357
|
+
if (!this.map.getLayer(unclusteredId)) {
|
|
2358
|
+
this.map.addLayer({
|
|
2359
|
+
id: unclusteredId,
|
|
2360
|
+
type: 'circle',
|
|
2361
|
+
source: sourceId,
|
|
2362
|
+
filter: ['!', ['has', 'point_count']],
|
|
2363
|
+
paint: {
|
|
2364
|
+
'circle-color': (kwargs.unclusteredColor as string) || '#11b4da',
|
|
2365
|
+
'circle-radius': (kwargs.unclusteredRadius as number) || 8,
|
|
2366
|
+
},
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
private handleRemoveClusterLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2372
|
+
const [layerId] = args as [string];
|
|
2373
|
+
const sourceId = `${layerId}-source`;
|
|
2374
|
+
for (const id of [`${layerId}-clusters`, `${layerId}-cluster-count`, `${layerId}-unclustered`]) {
|
|
2375
|
+
if (this.map?.getLayer(id)) this.map.removeLayer(id);
|
|
2376
|
+
}
|
|
2377
|
+
if (this.map?.getSource(sourceId)) this.map.removeSource(sourceId);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
private handleAddChoropleth(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2381
|
+
if (!this.map) return;
|
|
2382
|
+
const geojson = kwargs.data as GeoJSON.FeatureCollection;
|
|
2383
|
+
const name = (kwargs.name as string) || `choropleth-${Date.now()}`;
|
|
2384
|
+
const sourceId = `${name}-source`;
|
|
2385
|
+
const stepExpression = kwargs.stepExpression as unknown[];
|
|
2386
|
+
|
|
2387
|
+
if (!this.map.getSource(sourceId)) {
|
|
2388
|
+
this.map.addSource(sourceId, { type: 'geojson', data: geojson, generateId: true });
|
|
2389
|
+
}
|
|
2390
|
+
if (!this.map.getLayer(name)) {
|
|
2391
|
+
this.map.addLayer({
|
|
2392
|
+
id: name,
|
|
2393
|
+
type: 'fill',
|
|
2394
|
+
source: sourceId,
|
|
2395
|
+
paint: {
|
|
2396
|
+
'fill-color': (stepExpression as mapboxgl.ExpressionSpecification) || '#3388ff',
|
|
2397
|
+
'fill-opacity': (kwargs.fillOpacity as number) ?? 0.7,
|
|
2398
|
+
},
|
|
2399
|
+
} as mapboxgl.AnyLayer);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
private handleAdd3DBuildings(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2404
|
+
if (!this.map) return;
|
|
2405
|
+
const layerId = (kwargs.layerId as string) || '3d-buildings';
|
|
2406
|
+
const style = this.map.getStyle();
|
|
2407
|
+
let sourceId: string | null = null;
|
|
2408
|
+
for (const [id, src] of Object.entries(style.sources || {})) {
|
|
2409
|
+
if ((src as { type?: string }).type === 'vector') {
|
|
2410
|
+
sourceId = id;
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
if (!sourceId) {
|
|
2415
|
+
sourceId = 'buildings-source';
|
|
2416
|
+
if (!this.map.getSource(sourceId)) {
|
|
2417
|
+
this.map.addSource(sourceId, { type: 'vector', url: 'https://tiles.openfreemap.org/planet' });
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (!this.map.getLayer(layerId)) {
|
|
2421
|
+
this.map.addLayer({
|
|
2422
|
+
id: layerId,
|
|
2423
|
+
source: sourceId,
|
|
2424
|
+
'source-layer': 'building',
|
|
2425
|
+
type: 'fill-extrusion',
|
|
2426
|
+
minzoom: (kwargs.minZoom as number) ?? 14,
|
|
2427
|
+
paint: {
|
|
2428
|
+
'fill-extrusion-color': (kwargs.fillExtrusionColor as string) || '#aaa',
|
|
2429
|
+
'fill-extrusion-height': ['coalesce', ['get', 'render_height'], ['get', 'height'], 10],
|
|
2430
|
+
'fill-extrusion-base': ['coalesce', ['get', 'render_min_height'], 0],
|
|
2431
|
+
'fill-extrusion-opacity': (kwargs.fillExtrusionOpacity as number) ?? 0.6,
|
|
2432
|
+
},
|
|
2433
|
+
} as mapboxgl.AnyLayer);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// -------------------------------------------------------------------------
|
|
2438
|
+
// Route Animation
|
|
2439
|
+
// -------------------------------------------------------------------------
|
|
2440
|
+
|
|
2441
|
+
private handleAnimateAlongRoute(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2442
|
+
if (!this.map) return;
|
|
2443
|
+
const id = kwargs.id as string;
|
|
2444
|
+
const coordinates = kwargs.coordinates as [number, number][];
|
|
2445
|
+
const duration = (kwargs.duration as number) || 10000;
|
|
2446
|
+
const loop = kwargs.loop !== false;
|
|
2447
|
+
const showTrail = kwargs.showTrail === true;
|
|
2448
|
+
|
|
2449
|
+
const line = lineString(coordinates);
|
|
2450
|
+
const routeLength = length(line, { units: 'kilometers' });
|
|
2451
|
+
const marker = new Marker({ color: (kwargs.markerColor as string) || '#3388ff' })
|
|
2452
|
+
.setLngLat(coordinates[0])
|
|
2453
|
+
.addTo(this.map);
|
|
2454
|
+
|
|
2455
|
+
let trailSourceId: string | undefined;
|
|
2456
|
+
let trailLayerId: string | undefined;
|
|
2457
|
+
if (showTrail) {
|
|
2458
|
+
trailSourceId = `${id}-trail-source`;
|
|
2459
|
+
trailLayerId = `${id}-trail`;
|
|
2460
|
+
this.map.addSource(trailSourceId, {
|
|
2461
|
+
type: 'geojson',
|
|
2462
|
+
data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [] } },
|
|
2463
|
+
});
|
|
2464
|
+
this.map.addLayer({
|
|
2465
|
+
id: trailLayerId,
|
|
2466
|
+
type: 'line',
|
|
2467
|
+
source: trailSourceId,
|
|
2468
|
+
paint: { 'line-color': '#3388ff', 'line-width': 3 },
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
const startTime = performance.now();
|
|
2473
|
+
const trailCoords: [number, number][] = [coordinates[0]];
|
|
2474
|
+
|
|
2475
|
+
const animate = (currentTime: number) => {
|
|
2476
|
+
const anim = this.animations.get(id);
|
|
2477
|
+
if (!anim || !this.map) return;
|
|
2478
|
+
if (anim.isPaused) {
|
|
2479
|
+
anim.animationId = requestAnimationFrame(animate);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const elapsed = (currentTime - anim.startTime) * anim.speed;
|
|
2483
|
+
const progress = (elapsed % anim.duration) / anim.duration;
|
|
2484
|
+
const distance = progress * routeLength;
|
|
2485
|
+
const point = along(line, distance, { units: 'kilometers' });
|
|
2486
|
+
const pos = point.geometry.coordinates as [number, number];
|
|
2487
|
+
anim.marker.setLngLat(pos);
|
|
2488
|
+
if (showTrail && trailSourceId) {
|
|
2489
|
+
trailCoords.push(pos);
|
|
2490
|
+
(this.map.getSource(trailSourceId) as mapboxgl.GeoJSONSource)?.setData({
|
|
2491
|
+
type: 'Feature',
|
|
2492
|
+
properties: {},
|
|
2493
|
+
geometry: { type: 'LineString', coordinates: trailCoords },
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
if (elapsed >= anim.duration && !loop) {
|
|
2497
|
+
this.handleStopAnimation([id], {});
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
anim.animationId = requestAnimationFrame(animate);
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
this.animations.set(id, {
|
|
2504
|
+
animationId: requestAnimationFrame(animate),
|
|
2505
|
+
marker,
|
|
2506
|
+
isPaused: false,
|
|
2507
|
+
speed: 1,
|
|
2508
|
+
startTime,
|
|
2509
|
+
pausedAt: 0,
|
|
2510
|
+
duration,
|
|
2511
|
+
coordinates,
|
|
2512
|
+
loop,
|
|
2513
|
+
trailSourceId,
|
|
2514
|
+
trailLayerId,
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
private handleStopAnimation(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2519
|
+
const [id] = args as [string];
|
|
2520
|
+
const anim = this.animations.get(id);
|
|
2521
|
+
if (!anim) return;
|
|
2522
|
+
cancelAnimationFrame(anim.animationId);
|
|
2523
|
+
anim.marker.remove();
|
|
2524
|
+
if (anim.trailLayerId && this.map?.getLayer(anim.trailLayerId)) this.map.removeLayer(anim.trailLayerId);
|
|
2525
|
+
if (anim.trailSourceId && this.map?.getSource(anim.trailSourceId)) this.map.removeSource(anim.trailSourceId);
|
|
2526
|
+
this.animations.delete(id);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
private handlePauseAnimation(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2530
|
+
const [id] = args as [string];
|
|
2531
|
+
const anim = this.animations.get(id);
|
|
2532
|
+
if (anim) {
|
|
2533
|
+
anim.isPaused = true;
|
|
2534
|
+
anim.pausedAt = performance.now();
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
private handleResumeAnimation(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2539
|
+
const [id] = args as [string];
|
|
2540
|
+
const anim = this.animations.get(id);
|
|
2541
|
+
if (anim?.isPaused) {
|
|
2542
|
+
anim.startTime += performance.now() - anim.pausedAt;
|
|
2543
|
+
anim.isPaused = false;
|
|
2544
|
+
anim.pausedAt = 0;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
private handleSetAnimationSpeed(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2549
|
+
const [id, speed] = args as [string, number];
|
|
2550
|
+
const anim = this.animations.get(id);
|
|
2551
|
+
if (anim) anim.speed = speed;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// -------------------------------------------------------------------------
|
|
2555
|
+
// Feature Hover, Fog, Filter, Query
|
|
2556
|
+
// -------------------------------------------------------------------------
|
|
2557
|
+
|
|
2558
|
+
private handleAddHoverEffect(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2559
|
+
if (!this.map) return;
|
|
2560
|
+
const layerId = kwargs.layerId as string;
|
|
2561
|
+
const layer = this.map.getLayer(layerId);
|
|
2562
|
+
if (!layer) return;
|
|
2563
|
+
const sourceId = (layer as { source?: string }).source;
|
|
2564
|
+
if (!sourceId) return;
|
|
2565
|
+
|
|
2566
|
+
this.map.on('mousemove', layerId, (e) => {
|
|
2567
|
+
if (!this.map || !e.features?.length) return;
|
|
2568
|
+
if (this.hoveredFeatureId !== null) {
|
|
2569
|
+
this.map.setFeatureState({ source: sourceId, id: this.hoveredFeatureId }, { hover: false });
|
|
2570
|
+
}
|
|
2571
|
+
const f = e.features[0];
|
|
2572
|
+
this.hoveredFeatureId = (f as GeoJSON.Feature).id ?? null;
|
|
2573
|
+
this.hoveredLayerId = layerId;
|
|
2574
|
+
if (this.hoveredFeatureId !== null) {
|
|
2575
|
+
this.map.setFeatureState({ source: sourceId, id: this.hoveredFeatureId }, { hover: true });
|
|
2576
|
+
}
|
|
2577
|
+
this.map.getCanvas().style.cursor = 'pointer';
|
|
2578
|
+
});
|
|
2579
|
+
this.map.on('mouseleave', layerId, () => {
|
|
2580
|
+
if (this.map && this.hoveredFeatureId !== null) {
|
|
2581
|
+
this.map.setFeatureState({ source: sourceId, id: this.hoveredFeatureId }, { hover: false });
|
|
2582
|
+
}
|
|
2583
|
+
this.hoveredFeatureId = null;
|
|
2584
|
+
this.hoveredLayerId = null;
|
|
2585
|
+
if (this.map) this.map.getCanvas().style.cursor = '';
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
private handleSetFog(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2590
|
+
if (!this.map) return;
|
|
2591
|
+
const fog: mapboxgl.FogSpecification = {
|
|
2592
|
+
range: (kwargs.range as [number, number]) || [0.5, 10],
|
|
2593
|
+
color: (kwargs.color as string) || 'white',
|
|
2594
|
+
'high-color': (kwargs.highColor as string) || '#245cdf',
|
|
2595
|
+
'space-color': (kwargs.spaceColor as string) || '#000000',
|
|
2596
|
+
};
|
|
2597
|
+
this.map.setFog(fog);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
private handleRemoveFog(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2601
|
+
if (!this.map) return;
|
|
2602
|
+
this.map.setFog(null);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
private handleSetFilter(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2606
|
+
if (!this.map) return;
|
|
2607
|
+
const layerId = kwargs.layerId as string;
|
|
2608
|
+
const filter = kwargs.filter as unknown[] | null;
|
|
2609
|
+
if (layerId && this.map.getLayer(layerId)) {
|
|
2610
|
+
this.map.setFilter(layerId, filter as mapboxgl.FilterSpecification | null);
|
|
2611
|
+
this.stateManager.setLayerFilter(layerId, filter);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
private handleQueryRenderedFeatures(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2616
|
+
if (!this.map) return;
|
|
2617
|
+
const geometry = kwargs.geometry as mapboxgl.PointLike | [mapboxgl.PointLike, mapboxgl.PointLike] | undefined;
|
|
2618
|
+
const layers = kwargs.layers as string[] | undefined;
|
|
2619
|
+
const canvas = this.map.getCanvas();
|
|
2620
|
+
const bbox: [mapboxgl.PointLike, mapboxgl.PointLike] = canvas
|
|
2621
|
+
? [[0, 0], [canvas.width, canvas.height]]
|
|
2622
|
+
: [[0, 0], [256, 256]];
|
|
2623
|
+
const features = geometry
|
|
2624
|
+
? this.map.queryRenderedFeatures(geometry, { layers })
|
|
2625
|
+
: this.map.queryRenderedFeatures(bbox, { layers });
|
|
2626
|
+
this.model.set('_queried_features', {
|
|
2627
|
+
type: 'FeatureCollection',
|
|
2628
|
+
features: features.map((f) => ({ type: 'Feature' as const, geometry: f.geometry, properties: f.properties, id: f.id })),
|
|
2629
|
+
});
|
|
2630
|
+
this.model.save_changes();
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
private handleQuerySourceFeatures(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2634
|
+
if (!this.map) return;
|
|
2635
|
+
const sourceId = kwargs.sourceId as string;
|
|
2636
|
+
if (!sourceId) return;
|
|
2637
|
+
const features = this.map.querySourceFeatures(sourceId);
|
|
2638
|
+
this.model.set('_queried_features', {
|
|
2639
|
+
type: 'FeatureCollection',
|
|
2640
|
+
features: features.map((f) => ({ type: 'Feature' as const, geometry: f.geometry, properties: f.properties, id: f.id })),
|
|
2641
|
+
});
|
|
2642
|
+
this.model.save_changes();
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// -------------------------------------------------------------------------
|
|
2646
|
+
// Video Layer
|
|
2647
|
+
// -------------------------------------------------------------------------
|
|
2648
|
+
|
|
2649
|
+
private handleAddVideoLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2650
|
+
if (!this.map) return;
|
|
2651
|
+
const id = (kwargs.id as string) || `video-${Date.now()}`;
|
|
2652
|
+
const urls = kwargs.urls as string[];
|
|
2653
|
+
const coordinates = kwargs.coordinates as number[][];
|
|
2654
|
+
if (!urls?.length || !coordinates || coordinates.length !== 4) return;
|
|
2655
|
+
|
|
2656
|
+
const sourceId = `${id}-source`;
|
|
2657
|
+
this.map.addSource(sourceId, {
|
|
2658
|
+
type: 'video',
|
|
2659
|
+
urls,
|
|
2660
|
+
coordinates: coordinates as [[number, number], [number, number], [number, number], [number, number]],
|
|
2661
|
+
});
|
|
2662
|
+
this.map.addLayer({
|
|
2663
|
+
id,
|
|
2664
|
+
type: 'raster',
|
|
2665
|
+
source: sourceId,
|
|
2666
|
+
paint: { 'raster-opacity': (kwargs.opacity as number) ?? 1 },
|
|
2667
|
+
});
|
|
2668
|
+
this.videoSources.set(id, sourceId);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
private handleRemoveVideoLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2672
|
+
const id = kwargs.id as string;
|
|
2673
|
+
if (!id) return;
|
|
2674
|
+
const sourceId = this.videoSources.get(id) || `${id}-source`;
|
|
2675
|
+
if (this.map?.getLayer(id)) this.map.removeLayer(id);
|
|
2676
|
+
if (this.map?.getSource(sourceId)) this.map.removeSource(sourceId);
|
|
2677
|
+
this.videoSources.delete(id);
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
private handlePlayVideo(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2681
|
+
const id = kwargs.id as string;
|
|
2682
|
+
const sourceId = this.videoSources.get(id) || `${id}-source`;
|
|
2683
|
+
const source = this.map?.getSource(sourceId) as { play?: () => void };
|
|
2684
|
+
if (source?.play) source.play();
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
private handlePauseVideo(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2688
|
+
const id = kwargs.id as string;
|
|
2689
|
+
const sourceId = this.videoSources.get(id) || `${id}-source`;
|
|
2690
|
+
const source = this.map?.getSource(sourceId) as { pause?: () => void };
|
|
2691
|
+
if (source?.pause) source.pause();
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
private handleSeekVideo(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2695
|
+
const id = kwargs.id as string;
|
|
2696
|
+
const time = kwargs.time as number;
|
|
2697
|
+
const sourceId = this.videoSources.get(id) || `${id}-source`;
|
|
2698
|
+
const source = this.map?.getSource(sourceId) as { getVideo?: () => HTMLVideoElement };
|
|
2699
|
+
const video = source?.getVideo?.();
|
|
2700
|
+
if (video) video.currentTime = time;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// -------------------------------------------------------------------------
|
|
2704
|
+
// Split Map
|
|
2705
|
+
// -------------------------------------------------------------------------
|
|
2706
|
+
|
|
2707
|
+
private handleAddSplitMap(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2708
|
+
if (!this.map || !this.mapContainer) return;
|
|
2709
|
+
const leftLayer = kwargs.leftLayer as string;
|
|
2710
|
+
const rightLayer = kwargs.rightLayer as string;
|
|
2711
|
+
if (!leftLayer || !rightLayer) return;
|
|
2712
|
+
if (this.splitActive) this.handleRemoveSplitMap([], {});
|
|
2713
|
+
|
|
2714
|
+
this.splitActive = true;
|
|
2715
|
+
if (this.map.getLayer(rightLayer)) {
|
|
2716
|
+
this.map.setLayoutProperty(rightLayer, 'visibility', 'none');
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
const rightContainer = document.createElement('div');
|
|
2720
|
+
rightContainer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;clip-path:inset(0 0 0 50%);pointer-events:none;';
|
|
2721
|
+
this.mapContainer.appendChild(rightContainer);
|
|
2722
|
+
this.splitMapContainer = rightContainer;
|
|
2723
|
+
|
|
2724
|
+
const style = this.model.get('style');
|
|
2725
|
+
this.splitMapRight = new MapboxMap({
|
|
2726
|
+
container: rightContainer,
|
|
2727
|
+
style: typeof style === 'string' ? style : (style as mapboxgl.StyleSpecification),
|
|
2728
|
+
center: this.map.getCenter(),
|
|
2729
|
+
zoom: this.map.getZoom(),
|
|
2730
|
+
bearing: this.map.getBearing(),
|
|
2731
|
+
pitch: this.map.getPitch(),
|
|
2732
|
+
interactive: false,
|
|
2733
|
+
attributionControl: false,
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
const syncMaps = () => {
|
|
2737
|
+
if (this.map && this.splitMapRight) {
|
|
2738
|
+
this.splitMapRight.jumpTo({
|
|
2739
|
+
center: this.map.getCenter(),
|
|
2740
|
+
zoom: this.map.getZoom(),
|
|
2741
|
+
bearing: this.map.getBearing(),
|
|
2742
|
+
pitch: this.map.getPitch(),
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
this.map.on('move', syncMaps);
|
|
2747
|
+
|
|
2748
|
+
const slider = document.createElement('div');
|
|
2749
|
+
slider.style.cssText = 'position:absolute;top:0;left:50%;width:4px;height:100%;background:#333;cursor:ew-resize;z-index:10;';
|
|
2750
|
+
this.mapContainer.appendChild(slider);
|
|
2751
|
+
this.splitSlider = slider;
|
|
2752
|
+
|
|
2753
|
+
let isDragging = false;
|
|
2754
|
+
const onPointerDown = () => { isDragging = true; };
|
|
2755
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
2756
|
+
if (!isDragging || !this.mapContainer || !this.splitMapContainer) return;
|
|
2757
|
+
const rect = this.mapContainer.getBoundingClientRect();
|
|
2758
|
+
const pct = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
|
|
2759
|
+
slider.style.left = `${pct}%`;
|
|
2760
|
+
this.splitMapContainer.style.clipPath = `inset(0 0 0 ${pct}%)`;
|
|
2761
|
+
};
|
|
2762
|
+
const onPointerUp = () => { isDragging = false; };
|
|
2763
|
+
slider.addEventListener('pointerdown', onPointerDown);
|
|
2764
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
2765
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
2766
|
+
(slider as any)._cleanup = () => {
|
|
2767
|
+
slider.removeEventListener('pointerdown', onPointerDown);
|
|
2768
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
2769
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
2770
|
+
this.map?.off('move', syncMaps);
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
private handleRemoveSplitMap(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2775
|
+
if (!this.splitActive) return;
|
|
2776
|
+
if (this.splitSlider) {
|
|
2777
|
+
(this.splitSlider as any)?._cleanup?.();
|
|
2778
|
+
this.splitSlider.remove();
|
|
2779
|
+
this.splitSlider = null;
|
|
2780
|
+
}
|
|
2781
|
+
if (this.splitMapRight) {
|
|
2782
|
+
this.splitMapRight.remove();
|
|
2783
|
+
this.splitMapRight = null;
|
|
2784
|
+
}
|
|
2785
|
+
if (this.splitMapContainer) {
|
|
2786
|
+
this.splitMapContainer.remove();
|
|
2787
|
+
this.splitMapContainer = null;
|
|
2788
|
+
}
|
|
2789
|
+
this.splitActive = false;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// -------------------------------------------------------------------------
|
|
2793
|
+
// Colorbar, Search, Measure, Print
|
|
2794
|
+
// -------------------------------------------------------------------------
|
|
2795
|
+
|
|
2796
|
+
private handleAddColorbar(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2797
|
+
if (!this.map) return;
|
|
2798
|
+
const colorbarId = (kwargs.colorbarId as string) || `colorbar-${Date.now()}`;
|
|
2799
|
+
const position = (kwargs.position as ControlPosition) || 'bottom-right';
|
|
2800
|
+
const colorbar = new Colorbar({
|
|
2801
|
+
colormap: kwargs.colormap as string[],
|
|
2802
|
+
vmin: kwargs.vmin as number,
|
|
2803
|
+
vmax: kwargs.vmax as number,
|
|
2804
|
+
label: kwargs.label as string,
|
|
2805
|
+
} as any);
|
|
2806
|
+
this.map.addControl(colorbar as unknown as mapboxgl.IControl, position);
|
|
2807
|
+
this.colorbarsMap.set(colorbarId, colorbar);
|
|
2808
|
+
this.controlsMap.set(colorbarId, colorbar as unknown as mapboxgl.IControl);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
private handleRemoveColorbar(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2812
|
+
const colorbarId = (kwargs.colorbarId as string) || (args[0] as string);
|
|
2813
|
+
if (colorbarId) {
|
|
2814
|
+
const cb = this.colorbarsMap.get(colorbarId);
|
|
2815
|
+
if (cb && this.map) {
|
|
2816
|
+
this.map.removeControl(cb as unknown as mapboxgl.IControl);
|
|
2817
|
+
this.colorbarsMap.delete(colorbarId);
|
|
2818
|
+
this.controlsMap.delete(colorbarId);
|
|
2819
|
+
}
|
|
2820
|
+
} else {
|
|
2821
|
+
this.colorbarsMap.forEach((cb) => {
|
|
2822
|
+
if (this.map) this.map.removeControl(cb as unknown as mapboxgl.IControl);
|
|
2823
|
+
});
|
|
2824
|
+
this.colorbarsMap.clear();
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
private handleUpdateColorbar(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2829
|
+
const colorbarId = (kwargs.colorbarId as string) || 'colorbar-0';
|
|
2830
|
+
const colorbar = this.colorbarsMap.get(colorbarId);
|
|
2831
|
+
if (colorbar?.update) colorbar.update(kwargs as any);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
private handleAddSearchControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2835
|
+
if (!this.map) return;
|
|
2836
|
+
const position = (kwargs.position as ControlPosition) || 'top-left';
|
|
2837
|
+
if (this.searchControl) {
|
|
2838
|
+
this.map.removeControl(this.searchControl as unknown as mapboxgl.IControl);
|
|
2839
|
+
this.controlsMap.delete('search-control');
|
|
2840
|
+
}
|
|
2841
|
+
this.searchControl = new SearchControl(kwargs as any);
|
|
2842
|
+
this.map.addControl(this.searchControl as unknown as mapboxgl.IControl, position);
|
|
2843
|
+
this.controlsMap.set('search-control', this.searchControl as unknown as mapboxgl.IControl);
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
private handleRemoveSearchControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2847
|
+
if (this.searchControl && this.map) {
|
|
2848
|
+
this.map.removeControl(this.searchControl as unknown as mapboxgl.IControl);
|
|
2849
|
+
this.controlsMap.delete('search-control');
|
|
2850
|
+
this.searchControl = null;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
private handleAddMeasureControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2855
|
+
if (!this.map) return;
|
|
2856
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
2857
|
+
if (this.measureControl) {
|
|
2858
|
+
this.map.removeControl(this.measureControl as unknown as mapboxgl.IControl);
|
|
2859
|
+
this.controlsMap.delete('measure-control');
|
|
2860
|
+
}
|
|
2861
|
+
this.measureControl = new MeasureControl(kwargs as any);
|
|
2862
|
+
this.map.addControl(this.measureControl as unknown as mapboxgl.IControl, position);
|
|
2863
|
+
this.controlsMap.set('measure-control', this.measureControl as unknown as mapboxgl.IControl);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
private handleRemoveMeasureControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2867
|
+
if (this.measureControl && this.map) {
|
|
2868
|
+
this.map.removeControl(this.measureControl as unknown as mapboxgl.IControl);
|
|
2869
|
+
this.controlsMap.delete('measure-control');
|
|
2870
|
+
this.measureControl = null;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
private handleAddPrintControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2875
|
+
if (!this.map) return;
|
|
2876
|
+
const position = (kwargs.position as ControlPosition) || 'top-right';
|
|
2877
|
+
if (this.printControl) {
|
|
2878
|
+
this.map.removeControl(this.printControl as unknown as mapboxgl.IControl);
|
|
2879
|
+
this.controlsMap.delete('print-control');
|
|
2880
|
+
}
|
|
2881
|
+
this.printControl = new PrintControl(kwargs as any);
|
|
2882
|
+
this.map.addControl(this.printControl as unknown as mapboxgl.IControl, position);
|
|
2883
|
+
this.controlsMap.set('print-control', this.printControl as unknown as mapboxgl.IControl);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
private handleRemovePrintControl(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2887
|
+
if (this.printControl && this.map) {
|
|
2888
|
+
this.map.removeControl(this.printControl as unknown as mapboxgl.IControl);
|
|
2889
|
+
this.controlsMap.delete('print-control');
|
|
2890
|
+
this.printControl = null;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// -------------------------------------------------------------------------
|
|
2895
|
+
// FlatGeobuf
|
|
2896
|
+
// -------------------------------------------------------------------------
|
|
2897
|
+
|
|
2898
|
+
private async handleAddFlatGeobuf(args: unknown[], kwargs: Record<string, unknown>): Promise<void> {
|
|
2899
|
+
if (!this.map) return;
|
|
2900
|
+
const url = kwargs.url as string;
|
|
2901
|
+
const name = (kwargs.name as string) || `flatgeobuf-${Date.now()}`;
|
|
2902
|
+
const layerType = kwargs.layerType as string | undefined;
|
|
2903
|
+
const paint = kwargs.paint as Record<string, unknown> | undefined;
|
|
2904
|
+
const fitBounds = kwargs.fitBounds !== false;
|
|
2905
|
+
|
|
2906
|
+
const sourceId = `${name}-source`;
|
|
2907
|
+
const layerId = name;
|
|
2908
|
+
|
|
2909
|
+
try {
|
|
2910
|
+
const response = await fetch(url);
|
|
2911
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2912
|
+
const features: Feature[] = [];
|
|
2913
|
+
for await (const f of flatgeobuf.deserialize(response.body as ReadableStream)) {
|
|
2914
|
+
features.push(f as Feature);
|
|
2915
|
+
}
|
|
2916
|
+
const fc: FeatureCollection = { type: 'FeatureCollection', features };
|
|
2917
|
+
|
|
2918
|
+
let type = layerType;
|
|
2919
|
+
if (!type && features.length > 0) type = this.inferLayerType(features[0].geometry.type);
|
|
2920
|
+
type = type || 'circle';
|
|
2921
|
+
const layerPaint = paint || this.getDefaultPaint(type);
|
|
2922
|
+
|
|
2923
|
+
if (!this.map.getSource(sourceId)) {
|
|
2924
|
+
this.map.addSource(sourceId, { type: 'geojson', data: fc });
|
|
2925
|
+
}
|
|
2926
|
+
if (!this.map.getLayer(layerId)) {
|
|
2927
|
+
this.map.addLayer({ id: layerId, type: type as any, source: sourceId, paint: layerPaint });
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
if (fitBounds && features.length > 0) {
|
|
2931
|
+
const b = new mapboxgl.LngLatBounds();
|
|
2932
|
+
for (const f of features) {
|
|
2933
|
+
const g = f.geometry;
|
|
2934
|
+
if (g.type === 'Point') b.extend(g.coordinates as [number, number]);
|
|
2935
|
+
else if (g.type === 'LineString' || g.type === 'MultiPoint') {
|
|
2936
|
+
for (const c of g.coordinates) b.extend(c as [number, number]);
|
|
2937
|
+
} else if (g.type === 'Polygon' || g.type === 'MultiLineString') {
|
|
2938
|
+
for (const ring of g.coordinates) {
|
|
2939
|
+
for (const c of ring) b.extend(c as [number, number]);
|
|
2940
|
+
}
|
|
2941
|
+
} else if (g.type === 'MultiPolygon') {
|
|
2942
|
+
for (const poly of g.coordinates) {
|
|
2943
|
+
for (const ring of poly) {
|
|
2944
|
+
for (const c of ring) b.extend(c as [number, number]);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
if (!b.isEmpty()) this.map.fitBounds(b, { padding: 50 });
|
|
2950
|
+
}
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
console.error('Error loading FlatGeobuf:', err);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
private handleRemoveFlatGeobuf(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
2957
|
+
const name = (kwargs.name as string) || (args[0] as string);
|
|
2958
|
+
if (!name) return;
|
|
2959
|
+
const layerId = name;
|
|
2960
|
+
const sourceId = `${name}-source`;
|
|
2961
|
+
if (this.map?.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
2962
|
+
if (this.map?.getSource(sourceId)) this.map.removeSource(sourceId);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
1101
2965
|
// -------------------------------------------------------------------------
|
|
1102
2966
|
// Trait change handlers
|
|
1103
2967
|
// -------------------------------------------------------------------------
|
|
@@ -1128,26 +2992,92 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
1128
2992
|
// -------------------------------------------------------------------------
|
|
1129
2993
|
|
|
1130
2994
|
destroy(): void {
|
|
2995
|
+
if (this.splitActive) this.handleRemoveSplitMap([], {});
|
|
1131
2996
|
this.removeModelListeners();
|
|
1132
2997
|
|
|
1133
2998
|
if (this.resizeDebounceTimer !== null) {
|
|
1134
2999
|
window.clearTimeout(this.resizeDebounceTimer);
|
|
1135
3000
|
this.resizeDebounceTimer = null;
|
|
1136
3001
|
}
|
|
1137
|
-
|
|
1138
3002
|
if (this.resizeObserver) {
|
|
1139
3003
|
this.resizeObserver.disconnect();
|
|
1140
3004
|
this.resizeObserver = null;
|
|
1141
3005
|
}
|
|
1142
3006
|
|
|
1143
|
-
|
|
3007
|
+
if (this.mapboxDraw && this.map) {
|
|
3008
|
+
this.map.removeControl(this.mapboxDraw as unknown as mapboxgl.IControl);
|
|
3009
|
+
this.mapboxDraw = null;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
for (const [, div] of this.legendsMap) {
|
|
3013
|
+
if (div.parentNode) div.parentNode.removeChild(div);
|
|
3014
|
+
}
|
|
3015
|
+
this.legendsMap.clear();
|
|
3016
|
+
|
|
3017
|
+
if (this.timeSliderContainer) {
|
|
3018
|
+
this.timeSliderContainer.remove();
|
|
3019
|
+
this.timeSliderContainer = null;
|
|
3020
|
+
}
|
|
3021
|
+
this.opacitySliderContainer.forEach((el) => el.remove());
|
|
3022
|
+
this.opacitySliderContainer.clear();
|
|
3023
|
+
if (this.styleSwitcherContainer) {
|
|
3024
|
+
this.styleSwitcherContainer.remove();
|
|
3025
|
+
this.styleSwitcherContainer = null;
|
|
3026
|
+
}
|
|
3027
|
+
if (this.swipeContainer) {
|
|
3028
|
+
this.swipeContainer.remove();
|
|
3029
|
+
this.swipeContainer = null;
|
|
3030
|
+
}
|
|
3031
|
+
if (this.coordinatesControl) {
|
|
3032
|
+
this.coordinatesControl.remove();
|
|
3033
|
+
this.coordinatesControl = null;
|
|
3034
|
+
}
|
|
3035
|
+
if (this.coordinatesHandler) {
|
|
3036
|
+
this.map?.off('mousemove', this.coordinatesHandler);
|
|
3037
|
+
this.coordinatesHandler = null;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
this.colorbarsMap.forEach((cb) => {
|
|
3041
|
+
if (this.map) this.map.removeControl(cb as unknown as mapboxgl.IControl);
|
|
3042
|
+
});
|
|
3043
|
+
this.colorbarsMap.clear();
|
|
3044
|
+
if (this.searchControl && this.map) {
|
|
3045
|
+
this.map.removeControl(this.searchControl as unknown as mapboxgl.IControl);
|
|
3046
|
+
this.searchControl = null;
|
|
3047
|
+
}
|
|
3048
|
+
if (this.measureControl && this.map) {
|
|
3049
|
+
this.map.removeControl(this.measureControl as unknown as mapboxgl.IControl);
|
|
3050
|
+
this.measureControl = null;
|
|
3051
|
+
}
|
|
3052
|
+
if (this.printControl && this.map) {
|
|
3053
|
+
this.map.removeControl(this.printControl as unknown as mapboxgl.IControl);
|
|
3054
|
+
this.printControl = null;
|
|
3055
|
+
}
|
|
3056
|
+
if (this.controlGrid && this.map) {
|
|
3057
|
+
this.map.removeControl(this.controlGrid as unknown as mapboxgl.IControl);
|
|
3058
|
+
this.controlGrid = null;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
1144
3061
|
if (this.deckOverlay && this.map) {
|
|
1145
3062
|
this.map.removeControl(this.deckOverlay as unknown as mapboxgl.IControl);
|
|
1146
3063
|
this.deckOverlay = null;
|
|
1147
3064
|
}
|
|
1148
3065
|
this.deckLayers.clear();
|
|
1149
3066
|
|
|
1150
|
-
|
|
3067
|
+
this.animations.forEach((anim, id) => {
|
|
3068
|
+
cancelAnimationFrame(anim.animationId);
|
|
3069
|
+
anim.marker.remove();
|
|
3070
|
+
if (anim.trailLayerId && this.map?.getLayer(anim.trailLayerId)) this.map.removeLayer(anim.trailLayerId);
|
|
3071
|
+
if (anim.trailSourceId && this.map?.getSource(anim.trailSourceId)) this.map.removeSource(anim.trailSourceId);
|
|
3072
|
+
});
|
|
3073
|
+
this.animations.clear();
|
|
3074
|
+
|
|
3075
|
+
this.zarrLayers.forEach((_, id) => {
|
|
3076
|
+
if (this.map?.getLayer(id)) this.map.removeLayer(id);
|
|
3077
|
+
});
|
|
3078
|
+
this.zarrLayers.clear();
|
|
3079
|
+
this.videoSources.clear();
|
|
3080
|
+
|
|
1151
3081
|
if (this.lidarControl && this.map) {
|
|
1152
3082
|
this.map.removeControl(this.lidarControl as unknown as mapboxgl.IControl);
|
|
1153
3083
|
this.lidarControl = null;
|
|
@@ -1156,14 +3086,11 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
1156
3086
|
|
|
1157
3087
|
this.markersMap.forEach((marker) => marker.remove());
|
|
1158
3088
|
this.markersMap.clear();
|
|
1159
|
-
|
|
1160
3089
|
this.popupsMap.forEach((popup) => popup.remove());
|
|
1161
3090
|
this.popupsMap.clear();
|
|
1162
3091
|
|
|
1163
3092
|
this.controlsMap.forEach((control) => {
|
|
1164
|
-
if (this.map)
|
|
1165
|
-
this.map.removeControl(control);
|
|
1166
|
-
}
|
|
3093
|
+
if (this.map) this.map.removeControl(control);
|
|
1167
3094
|
});
|
|
1168
3095
|
this.controlsMap.clear();
|
|
1169
3096
|
|
|
@@ -1171,7 +3098,6 @@ export class MapboxRenderer extends BaseMapRenderer<MapboxMap> {
|
|
|
1171
3098
|
this.map.remove();
|
|
1172
3099
|
this.map = null;
|
|
1173
3100
|
}
|
|
1174
|
-
|
|
1175
3101
|
if (this.mapContainer) {
|
|
1176
3102
|
this.mapContainer.remove();
|
|
1177
3103
|
this.mapContainer = null;
|