anymap-ts 0.10.1 → 0.12.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/cesium.css +1 -0
- package/anymap_ts/static/cesium.js +16543 -1
- package/anymap_ts/static/leaflet.css +1 -1
- package/anymap_ts/static/leaflet.js +1 -1
- package/anymap_ts/static/openlayers.css +1 -1
- package/anymap_ts/static/openlayers.js +602 -7
- package/anymap_ts/static/potree.js +4424 -117
- package/package.json +6 -3
- package/src/cesium/index.ts +43 -99
- package/src/leaflet/LeafletRenderer.ts +684 -113
- package/src/leaflet/index.ts +23 -0
- package/src/leaflet/leaflet-overrides.css +31 -0
- package/src/leaflet/leaflet-setup.ts +13 -0
- package/src/openlayers/OpenLayersRenderer.ts +1275 -181
- package/src/openlayers/index.ts +1 -0
- package/src/potree/index.ts +411 -187
- package/src/styles/openlayers.css +39 -0
- package/src/types/potree.ts +0 -21
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
* src/leaflet directory due to tsconfig baseUrl resolution.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
// @ts-ignore - Import from explicit ESM path to avoid baseUrl conflict
|
|
11
|
-
import * as L from 'leaflet/dist/leaflet-src.esm.js';
|
|
9
|
+
import { L } from './leaflet-setup';
|
|
12
10
|
|
|
13
|
-
// Re-export types for convenience
|
|
14
11
|
type LeafletMap = L.Map;
|
|
15
12
|
type TileLayer = L.TileLayer;
|
|
16
13
|
type Marker = L.Marker;
|
|
@@ -33,6 +30,7 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
33
30
|
private markersMap: globalThis.Map<string, Marker> = new globalThis.Map();
|
|
34
31
|
private popupsMap: globalThis.Map<string, Popup> = new globalThis.Map();
|
|
35
32
|
private controlsMap: globalThis.Map<string, Control> = new globalThis.Map();
|
|
33
|
+
private layerControl: L.Control.Layers | null = null;
|
|
36
34
|
private resizeObserver: ResizeObserver | null = null;
|
|
37
35
|
private resizeDebounceTimer: number | null = null;
|
|
38
36
|
|
|
@@ -42,40 +40,22 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
42
40
|
this.registerMethods();
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
/**
|
|
46
|
-
* Initialize the Leaflet map.
|
|
47
|
-
*/
|
|
48
43
|
async initialize(): Promise<void> {
|
|
49
|
-
// Create container
|
|
50
44
|
this.createMapContainer();
|
|
51
|
-
|
|
52
|
-
// Create map
|
|
53
45
|
this.map = this.createMap();
|
|
54
|
-
|
|
55
|
-
// Set up listeners
|
|
56
46
|
this.setupModelListeners();
|
|
57
47
|
this.setupMapEvents();
|
|
58
|
-
|
|
59
|
-
// Set up resize observer
|
|
60
48
|
this.setupResizeObserver();
|
|
61
|
-
|
|
62
|
-
// Process any JS calls that were made before listeners were set up
|
|
63
49
|
this.processJsCalls();
|
|
64
|
-
|
|
65
|
-
// Mark as ready (Leaflet doesn't have a load event like MapLibre)
|
|
66
50
|
this.isMapReady = true;
|
|
67
51
|
this.processPendingCalls();
|
|
68
52
|
}
|
|
69
53
|
|
|
70
|
-
/**
|
|
71
|
-
* Set up resize observer.
|
|
72
|
-
*/
|
|
73
54
|
private setupResizeObserver(): void {
|
|
74
55
|
if (!this.mapContainer || !this.map) return;
|
|
75
56
|
|
|
76
57
|
this.resizeObserver = new ResizeObserver(() => {
|
|
77
58
|
if (this.map) {
|
|
78
|
-
// Debounce resize to prevent flickering during window resize
|
|
79
59
|
if (this.resizeDebounceTimer !== null) {
|
|
80
60
|
window.clearTimeout(this.resizeDebounceTimer);
|
|
81
61
|
}
|
|
@@ -92,31 +72,23 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
92
72
|
this.resizeObserver.observe(this.el);
|
|
93
73
|
}
|
|
94
74
|
|
|
95
|
-
/**
|
|
96
|
-
* Create the Leaflet map instance.
|
|
97
|
-
*/
|
|
98
75
|
protected createMap(): LeafletMap {
|
|
99
|
-
// Leaflet uses [lat, lng] but we receive [lng, lat] from Python
|
|
100
76
|
const center = this.model.get('center') as [number, number];
|
|
101
77
|
const zoom = this.model.get('zoom');
|
|
102
78
|
|
|
103
79
|
const map = L.map(this.mapContainer!, {
|
|
104
|
-
center: [center[1], center[0]],
|
|
80
|
+
center: [center[1], center[0]],
|
|
105
81
|
zoom,
|
|
106
|
-
zoomControl: false,
|
|
82
|
+
zoomControl: false,
|
|
107
83
|
attributionControl: false,
|
|
108
84
|
});
|
|
109
85
|
|
|
110
86
|
return map;
|
|
111
87
|
}
|
|
112
88
|
|
|
113
|
-
/**
|
|
114
|
-
* Set up map event listeners.
|
|
115
|
-
*/
|
|
116
89
|
private setupMapEvents(): void {
|
|
117
90
|
if (!this.map) return;
|
|
118
91
|
|
|
119
|
-
// Click event
|
|
120
92
|
this.map.on('click', (e: L.LeafletMouseEvent) => {
|
|
121
93
|
this.model.set('clicked', {
|
|
122
94
|
lng: e.latlng.lng,
|
|
@@ -130,7 +102,6 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
130
102
|
this.model.save_changes();
|
|
131
103
|
});
|
|
132
104
|
|
|
133
|
-
// Move end event
|
|
134
105
|
this.map.on('moveend', () => {
|
|
135
106
|
if (!this.map) return;
|
|
136
107
|
const center = this.map.getCenter();
|
|
@@ -159,16 +130,12 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
159
130
|
});
|
|
160
131
|
});
|
|
161
132
|
|
|
162
|
-
// Zoom end event
|
|
163
133
|
this.map.on('zoomend', () => {
|
|
164
134
|
if (!this.map) return;
|
|
165
135
|
this.sendEvent('zoomend', { zoom: this.map.getZoom() });
|
|
166
136
|
});
|
|
167
137
|
}
|
|
168
138
|
|
|
169
|
-
/**
|
|
170
|
-
* Register all method handlers.
|
|
171
|
-
*/
|
|
172
139
|
private registerMethods(): void {
|
|
173
140
|
// Map navigation
|
|
174
141
|
this.registerMethod('setCenter', this.handleSetCenter.bind(this));
|
|
@@ -180,6 +147,10 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
180
147
|
this.registerMethod('addTileLayer', this.handleAddTileLayer.bind(this));
|
|
181
148
|
this.registerMethod('removeTileLayer', this.handleRemoveTileLayer.bind(this));
|
|
182
149
|
|
|
150
|
+
// WMS layers
|
|
151
|
+
this.registerMethod('addWMSLayer', this.handleAddWMSLayer.bind(this));
|
|
152
|
+
this.registerMethod('removeWMSLayer', this.handleRemoveWMSLayer.bind(this));
|
|
153
|
+
|
|
183
154
|
// GeoJSON
|
|
184
155
|
this.registerMethod('addGeoJSON', this.handleAddGeoJSON.bind(this));
|
|
185
156
|
this.registerMethod('removeGeoJSON', this.handleRemoveGeoJSON.bind(this));
|
|
@@ -198,7 +169,34 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
198
169
|
|
|
199
170
|
// Markers
|
|
200
171
|
this.registerMethod('addMarker', this.handleAddMarker.bind(this));
|
|
172
|
+
this.registerMethod('addMarkers', this.handleAddMarkers.bind(this));
|
|
201
173
|
this.registerMethod('removeMarker', this.handleRemoveMarker.bind(this));
|
|
174
|
+
|
|
175
|
+
// Shapes
|
|
176
|
+
this.registerMethod('addCircleMarker', this.handleAddCircleMarker.bind(this));
|
|
177
|
+
this.registerMethod('addCircle', this.handleAddCircle.bind(this));
|
|
178
|
+
this.registerMethod('addPolyline', this.handleAddPolyline.bind(this));
|
|
179
|
+
this.registerMethod('addPolygon', this.handleAddPolygon.bind(this));
|
|
180
|
+
this.registerMethod('addRectangle', this.handleAddRectangle.bind(this));
|
|
181
|
+
|
|
182
|
+
// Overlays
|
|
183
|
+
this.registerMethod('addImageOverlay', this.handleAddImageOverlay.bind(this));
|
|
184
|
+
this.registerMethod('addVideoOverlay', this.handleAddVideoOverlay.bind(this));
|
|
185
|
+
|
|
186
|
+
// Heatmap
|
|
187
|
+
this.registerMethod('addHeatmap', this.handleAddHeatmap.bind(this));
|
|
188
|
+
this.registerMethod('removeHeatmap', this.handleRemoveHeatmap.bind(this));
|
|
189
|
+
|
|
190
|
+
// Choropleth
|
|
191
|
+
this.registerMethod('addChoropleth', this.handleAddChoropleth.bind(this));
|
|
192
|
+
|
|
193
|
+
// Popups & Tooltips
|
|
194
|
+
this.registerMethod('addPopup', this.handleAddPopup.bind(this));
|
|
195
|
+
this.registerMethod('removePopup', this.handleRemovePopup.bind(this));
|
|
196
|
+
|
|
197
|
+
// Legend
|
|
198
|
+
this.registerMethod('addLegend', this.handleAddLegend.bind(this));
|
|
199
|
+
this.registerMethod('removeLegend', this.handleRemoveLegend.bind(this));
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
// -------------------------------------------------------------------------
|
|
@@ -223,9 +221,7 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
223
221
|
const zoom = kwargs.zoom as number | undefined;
|
|
224
222
|
const duration = (kwargs.duration as number) || 2000;
|
|
225
223
|
|
|
226
|
-
const options = {
|
|
227
|
-
duration: duration / 1000, // Leaflet uses seconds
|
|
228
|
-
};
|
|
224
|
+
const options = { duration: duration / 1000 };
|
|
229
225
|
|
|
230
226
|
if (zoom !== undefined) {
|
|
231
227
|
this.map.flyTo([lat, lng], zoom, options);
|
|
@@ -241,8 +237,8 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
241
237
|
const duration = (kwargs.duration as number) || 1000;
|
|
242
238
|
|
|
243
239
|
const leafletBounds = L.latLngBounds(
|
|
244
|
-
[bounds[1], bounds[0]],
|
|
245
|
-
[bounds[3], bounds[2]]
|
|
240
|
+
[bounds[1], bounds[0]],
|
|
241
|
+
[bounds[3], bounds[2]]
|
|
246
242
|
);
|
|
247
243
|
|
|
248
244
|
this.map.fitBounds(leafletBounds, {
|
|
@@ -263,23 +259,20 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
263
259
|
const attribution = (kwargs.attribution as string) || '';
|
|
264
260
|
const minZoom = (kwargs.minZoom as number) || 0;
|
|
265
261
|
const maxZoom = (kwargs.maxZoom as number) || 22;
|
|
266
|
-
const opacity = (kwargs.opacity as number)
|
|
267
|
-
|
|
268
|
-
const tileLayer = L.tileLayer(url, {
|
|
269
|
-
attribution,
|
|
270
|
-
minZoom,
|
|
271
|
-
maxZoom,
|
|
272
|
-
opacity,
|
|
273
|
-
});
|
|
262
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
274
263
|
|
|
264
|
+
const tileLayer = L.tileLayer(url, { attribution, minZoom, maxZoom, opacity });
|
|
275
265
|
tileLayer.addTo(this.map);
|
|
276
266
|
this.layersMap.set(name, tileLayer);
|
|
267
|
+
|
|
268
|
+
if (this.layerControl) {
|
|
269
|
+
this.layerControl.addOverlay(tileLayer, name);
|
|
270
|
+
}
|
|
277
271
|
}
|
|
278
272
|
|
|
279
273
|
private handleRemoveTileLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
280
274
|
if (!this.map) return;
|
|
281
275
|
const [name] = args as [string];
|
|
282
|
-
|
|
283
276
|
const layer = this.layersMap.get(name);
|
|
284
277
|
if (layer) {
|
|
285
278
|
this.map.removeLayer(layer);
|
|
@@ -287,6 +280,59 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
287
280
|
}
|
|
288
281
|
}
|
|
289
282
|
|
|
283
|
+
// -------------------------------------------------------------------------
|
|
284
|
+
// WMS layer handlers
|
|
285
|
+
// -------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
private handleAddWMSLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
288
|
+
if (!this.map) return;
|
|
289
|
+
const [url] = args as [string];
|
|
290
|
+
const name = (kwargs.name as string) || `wms-${Date.now()}`;
|
|
291
|
+
const layers = (kwargs.layers as string) || '';
|
|
292
|
+
const format = (kwargs.format as string) || 'image/png';
|
|
293
|
+
const transparent = kwargs.transparent !== false;
|
|
294
|
+
const attribution = (kwargs.attribution as string) || '';
|
|
295
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
296
|
+
const crs = kwargs.crs as string | undefined;
|
|
297
|
+
const styles = (kwargs.styles as string) || '';
|
|
298
|
+
const version = (kwargs.version as string) || '1.1.1';
|
|
299
|
+
const uppercase = kwargs.uppercase !== false;
|
|
300
|
+
|
|
301
|
+
const wmsOptions: Record<string, unknown> = {
|
|
302
|
+
layers,
|
|
303
|
+
format,
|
|
304
|
+
transparent,
|
|
305
|
+
attribution,
|
|
306
|
+
opacity,
|
|
307
|
+
styles,
|
|
308
|
+
version,
|
|
309
|
+
uppercase,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (crs) {
|
|
313
|
+
const crsMap: Record<string, L.CRS> = {
|
|
314
|
+
'EPSG:3857': L.CRS.EPSG3857,
|
|
315
|
+
'EPSG:4326': L.CRS.EPSG4326,
|
|
316
|
+
'EPSG:3395': L.CRS.EPSG3395,
|
|
317
|
+
};
|
|
318
|
+
if (crsMap[crs]) {
|
|
319
|
+
wmsOptions.crs = crsMap[crs];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const wmsLayer = L.tileLayer.wms(url, wmsOptions as any);
|
|
324
|
+
wmsLayer.addTo(this.map);
|
|
325
|
+
this.layersMap.set(name, wmsLayer);
|
|
326
|
+
|
|
327
|
+
if (this.layerControl) {
|
|
328
|
+
this.layerControl.addOverlay(wmsLayer, name);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private handleRemoveWMSLayer(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
333
|
+
this.handleRemoveLayer(args, kwargs);
|
|
334
|
+
}
|
|
335
|
+
|
|
290
336
|
// -------------------------------------------------------------------------
|
|
291
337
|
// Basemap handlers
|
|
292
338
|
// -------------------------------------------------------------------------
|
|
@@ -297,21 +343,19 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
297
343
|
const name = (kwargs.name as string) || 'basemap';
|
|
298
344
|
const attribution = (kwargs.attribution as string) || '';
|
|
299
345
|
|
|
300
|
-
// Remove existing basemap with same name
|
|
301
346
|
const existingLayer = this.layersMap.get(`basemap-${name}`);
|
|
302
347
|
if (existingLayer) {
|
|
303
348
|
this.map.removeLayer(existingLayer);
|
|
304
349
|
}
|
|
305
350
|
|
|
306
|
-
const tileLayer = L.tileLayer(url, {
|
|
307
|
-
attribution,
|
|
308
|
-
maxZoom: 22,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// Add to bottom of layer stack
|
|
351
|
+
const tileLayer = L.tileLayer(url, { attribution, maxZoom: 22 });
|
|
312
352
|
tileLayer.addTo(this.map);
|
|
313
353
|
tileLayer.bringToBack();
|
|
314
354
|
this.layersMap.set(`basemap-${name}`, tileLayer);
|
|
355
|
+
|
|
356
|
+
if (this.layerControl) {
|
|
357
|
+
this.layerControl.addBaseLayer(tileLayer, name);
|
|
358
|
+
}
|
|
315
359
|
}
|
|
316
360
|
|
|
317
361
|
// -------------------------------------------------------------------------
|
|
@@ -325,14 +369,12 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
325
369
|
const name = kwargs.name as string;
|
|
326
370
|
const style = kwargs.style as Record<string, unknown> | undefined;
|
|
327
371
|
const fitBounds = kwargs.fitBounds !== false;
|
|
372
|
+
const popupProperties = kwargs.popupProperties as string[] | boolean | undefined;
|
|
373
|
+
const tooltipProperty = kwargs.tooltipProperty as string | undefined;
|
|
328
374
|
|
|
329
|
-
// Create GeoJSON layer
|
|
330
375
|
const geoJsonLayer = L.geoJSON(geojson as any, {
|
|
331
376
|
style: (feature: GeoJSON.Feature | undefined) => {
|
|
332
|
-
if (style)
|
|
333
|
-
return style;
|
|
334
|
-
}
|
|
335
|
-
// Default styles based on geometry type
|
|
377
|
+
if (style) return style;
|
|
336
378
|
const geomType = feature?.geometry?.type || 'Point';
|
|
337
379
|
return this.getDefaultStyle(geomType);
|
|
338
380
|
},
|
|
@@ -340,12 +382,34 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
340
382
|
const s = style || this.getDefaultStyle('Point');
|
|
341
383
|
return L.circleMarker(latlng, s as any);
|
|
342
384
|
},
|
|
385
|
+
onEachFeature: (feature: GeoJSON.Feature, layer: L.Layer) => {
|
|
386
|
+
const props = feature.properties || {};
|
|
387
|
+
|
|
388
|
+
if (popupProperties) {
|
|
389
|
+
let html = '<div class="anymap-popup">';
|
|
390
|
+
const keys = popupProperties === true ? Object.keys(props) : popupProperties;
|
|
391
|
+
for (const key of keys) {
|
|
392
|
+
if (props[key] !== undefined && props[key] !== null) {
|
|
393
|
+
html += `<b>${key}:</b> ${props[key]}<br>`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
html += '</div>';
|
|
397
|
+
layer.bindPopup(html);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (tooltipProperty && props[tooltipProperty] !== undefined) {
|
|
401
|
+
layer.bindTooltip(String(props[tooltipProperty]), { sticky: true });
|
|
402
|
+
}
|
|
403
|
+
},
|
|
343
404
|
});
|
|
344
405
|
|
|
345
406
|
geoJsonLayer.addTo(this.map);
|
|
346
407
|
this.layersMap.set(name, geoJsonLayer);
|
|
347
408
|
|
|
348
|
-
|
|
409
|
+
if (this.layerControl) {
|
|
410
|
+
this.layerControl.addOverlay(geoJsonLayer, name);
|
|
411
|
+
}
|
|
412
|
+
|
|
349
413
|
if (fitBounds && kwargs.bounds) {
|
|
350
414
|
const bounds = kwargs.bounds as [number, number, number, number];
|
|
351
415
|
const leafletBounds = L.latLngBounds(
|
|
@@ -364,7 +428,6 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
364
428
|
private handleRemoveGeoJSON(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
365
429
|
if (!this.map) return;
|
|
366
430
|
const [name] = args as [string];
|
|
367
|
-
|
|
368
431
|
const layer = this.layersMap.get(name);
|
|
369
432
|
if (layer) {
|
|
370
433
|
this.map.removeLayer(layer);
|
|
@@ -374,46 +437,12 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
374
437
|
|
|
375
438
|
private getDefaultStyle(geometryType: string): Record<string, unknown> {
|
|
376
439
|
const defaults: Record<string, Record<string, unknown>> = {
|
|
377
|
-
Point: {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
fillOpacity: 0.8,
|
|
384
|
-
},
|
|
385
|
-
MultiPoint: {
|
|
386
|
-
radius: 8,
|
|
387
|
-
fillColor: '#3388ff',
|
|
388
|
-
color: '#ffffff',
|
|
389
|
-
weight: 2,
|
|
390
|
-
opacity: 1,
|
|
391
|
-
fillOpacity: 0.8,
|
|
392
|
-
},
|
|
393
|
-
LineString: {
|
|
394
|
-
color: '#3388ff',
|
|
395
|
-
weight: 3,
|
|
396
|
-
opacity: 0.8,
|
|
397
|
-
},
|
|
398
|
-
MultiLineString: {
|
|
399
|
-
color: '#3388ff',
|
|
400
|
-
weight: 3,
|
|
401
|
-
opacity: 0.8,
|
|
402
|
-
},
|
|
403
|
-
Polygon: {
|
|
404
|
-
fillColor: '#3388ff',
|
|
405
|
-
color: '#0000ff',
|
|
406
|
-
weight: 2,
|
|
407
|
-
opacity: 1,
|
|
408
|
-
fillOpacity: 0.5,
|
|
409
|
-
},
|
|
410
|
-
MultiPolygon: {
|
|
411
|
-
fillColor: '#3388ff',
|
|
412
|
-
color: '#0000ff',
|
|
413
|
-
weight: 2,
|
|
414
|
-
opacity: 1,
|
|
415
|
-
fillOpacity: 0.5,
|
|
416
|
-
},
|
|
440
|
+
Point: { radius: 8, fillColor: '#3388ff', color: '#ffffff', weight: 2, opacity: 1, fillOpacity: 0.8 },
|
|
441
|
+
MultiPoint: { radius: 8, fillColor: '#3388ff', color: '#ffffff', weight: 2, opacity: 1, fillOpacity: 0.8 },
|
|
442
|
+
LineString: { color: '#3388ff', weight: 3, opacity: 0.8 },
|
|
443
|
+
MultiLineString: { color: '#3388ff', weight: 3, opacity: 0.8 },
|
|
444
|
+
Polygon: { fillColor: '#3388ff', color: '#0000ff', weight: 2, opacity: 1, fillOpacity: 0.5 },
|
|
445
|
+
MultiPolygon: { fillColor: '#3388ff', color: '#0000ff', weight: 2, opacity: 1, fillOpacity: 0.5 },
|
|
417
446
|
};
|
|
418
447
|
return defaults[geometryType] || defaults.Point;
|
|
419
448
|
}
|
|
@@ -428,6 +457,9 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
428
457
|
|
|
429
458
|
const layer = this.layersMap.get(layerId);
|
|
430
459
|
if (layer) {
|
|
460
|
+
if (this.layerControl) {
|
|
461
|
+
this.layerControl.removeLayer(layer);
|
|
462
|
+
}
|
|
431
463
|
this.map.removeLayer(layer);
|
|
432
464
|
this.layersMap.delete(layerId);
|
|
433
465
|
}
|
|
@@ -482,8 +514,7 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
482
514
|
case 'attribution':
|
|
483
515
|
control = L.control.attribution({ position });
|
|
484
516
|
break;
|
|
485
|
-
case 'layers':
|
|
486
|
-
// Layer control needs baseLayers and overlays
|
|
517
|
+
case 'layers': {
|
|
487
518
|
const baseLayers: Record<string, TileLayer> = {};
|
|
488
519
|
const overlays: Record<string, L.Layer> = {};
|
|
489
520
|
this.layersMap.forEach((layer, name) => {
|
|
@@ -493,8 +524,13 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
493
524
|
overlays[name] = layer;
|
|
494
525
|
}
|
|
495
526
|
});
|
|
496
|
-
|
|
527
|
+
const layersControl = L.control.layers(baseLayers, overlays, {
|
|
528
|
+
position, collapsed: kwargs.collapsed !== false,
|
|
529
|
+
});
|
|
530
|
+
this.layerControl = layersControl;
|
|
531
|
+
control = layersControl;
|
|
497
532
|
break;
|
|
533
|
+
}
|
|
498
534
|
}
|
|
499
535
|
|
|
500
536
|
if (control) {
|
|
@@ -513,6 +549,9 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
513
549
|
this.map.removeControl(control);
|
|
514
550
|
this.controlsMap.delete(controlType);
|
|
515
551
|
this.stateManager.removeControl(controlType);
|
|
552
|
+
if (controlType === 'layers') {
|
|
553
|
+
this.layerControl = null;
|
|
554
|
+
}
|
|
516
555
|
}
|
|
517
556
|
}
|
|
518
557
|
|
|
@@ -537,19 +576,84 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
537
576
|
private handleAddMarker(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
538
577
|
if (!this.map) return;
|
|
539
578
|
const [lng, lat] = args as [number, number];
|
|
540
|
-
const id = (kwargs.id as string) || `marker-${Date.now()}`;
|
|
579
|
+
const id = (kwargs.id as string) || `marker-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
541
580
|
const popup = kwargs.popup as string | undefined;
|
|
581
|
+
const tooltip = kwargs.tooltip as string | undefined;
|
|
582
|
+
const draggable = kwargs.draggable as boolean | undefined;
|
|
583
|
+
const opacity = kwargs.opacity as number | undefined;
|
|
584
|
+
const iconUrl = kwargs.iconUrl as string | undefined;
|
|
585
|
+
const iconSize = kwargs.iconSize as [number, number] | undefined;
|
|
586
|
+
const iconAnchor = kwargs.iconAnchor as [number, number] | undefined;
|
|
587
|
+
|
|
588
|
+
const markerOptions: L.MarkerOptions = {};
|
|
589
|
+
|
|
590
|
+
if (draggable) markerOptions.draggable = true;
|
|
591
|
+
if (opacity !== undefined) markerOptions.opacity = opacity;
|
|
592
|
+
|
|
593
|
+
if (iconUrl) {
|
|
594
|
+
markerOptions.icon = L.icon({
|
|
595
|
+
iconUrl,
|
|
596
|
+
iconSize: iconSize || [25, 41],
|
|
597
|
+
iconAnchor: iconAnchor || [12, 41],
|
|
598
|
+
popupAnchor: [1, -34],
|
|
599
|
+
});
|
|
600
|
+
}
|
|
542
601
|
|
|
543
|
-
const marker = L.marker([lat, lng]);
|
|
602
|
+
const marker = L.marker([lat, lng], markerOptions);
|
|
544
603
|
|
|
545
|
-
if (popup)
|
|
546
|
-
|
|
604
|
+
if (popup) marker.bindPopup(popup);
|
|
605
|
+
if (tooltip) marker.bindTooltip(tooltip);
|
|
606
|
+
|
|
607
|
+
if (draggable) {
|
|
608
|
+
marker.on('dragend', () => {
|
|
609
|
+
const pos = marker.getLatLng();
|
|
610
|
+
this.sendEvent('marker_dragend', { id, lngLat: [pos.lng, pos.lat] });
|
|
611
|
+
});
|
|
547
612
|
}
|
|
548
613
|
|
|
549
614
|
marker.addTo(this.map);
|
|
550
615
|
this.markersMap.set(id, marker);
|
|
551
616
|
}
|
|
552
617
|
|
|
618
|
+
private handleAddMarkers(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
619
|
+
if (!this.map) return;
|
|
620
|
+
const data = kwargs.data as Array<Record<string, unknown>>;
|
|
621
|
+
const name = (kwargs.name as string) || `markers-${Date.now()}`;
|
|
622
|
+
|
|
623
|
+
const markerGroup = L.layerGroup();
|
|
624
|
+
|
|
625
|
+
for (const item of data) {
|
|
626
|
+
const lng = item.lng as number;
|
|
627
|
+
const lat = item.lat as number;
|
|
628
|
+
const popup = item.popup as string | undefined;
|
|
629
|
+
const tooltip = item.tooltip as string | undefined;
|
|
630
|
+
const iconUrl = item.iconUrl as string | undefined;
|
|
631
|
+
const iconSize = item.iconSize as [number, number] | undefined;
|
|
632
|
+
|
|
633
|
+
const opts: L.MarkerOptions = {};
|
|
634
|
+
if (iconUrl) {
|
|
635
|
+
opts.icon = L.icon({
|
|
636
|
+
iconUrl,
|
|
637
|
+
iconSize: iconSize || [25, 41],
|
|
638
|
+
iconAnchor: [12, 41],
|
|
639
|
+
popupAnchor: [1, -34],
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const marker = L.marker([lat, lng], opts);
|
|
644
|
+
if (popup) marker.bindPopup(popup);
|
|
645
|
+
if (tooltip) marker.bindTooltip(tooltip);
|
|
646
|
+
markerGroup.addLayer(marker);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
markerGroup.addTo(this.map);
|
|
650
|
+
this.layersMap.set(name, markerGroup);
|
|
651
|
+
|
|
652
|
+
if (this.layerControl) {
|
|
653
|
+
this.layerControl.addOverlay(markerGroup, name);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
553
657
|
private handleRemoveMarker(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
554
658
|
if (!this.map) return;
|
|
555
659
|
const [id] = args as [string];
|
|
@@ -561,6 +665,473 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
561
665
|
}
|
|
562
666
|
}
|
|
563
667
|
|
|
668
|
+
// -------------------------------------------------------------------------
|
|
669
|
+
// Shape handlers (circles, polylines, polygons, rectangles)
|
|
670
|
+
// -------------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
private handleAddCircleMarker(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
673
|
+
if (!this.map) return;
|
|
674
|
+
const [lng, lat] = args as [number, number];
|
|
675
|
+
const name = (kwargs.name as string) || `circle-marker-${Date.now()}`;
|
|
676
|
+
const radius = (kwargs.radius as number) || 10;
|
|
677
|
+
const color = (kwargs.color as string) || '#3388ff';
|
|
678
|
+
const fillColor = (kwargs.fillColor as string) || color;
|
|
679
|
+
const fillOpacity = (kwargs.fillOpacity as number) ?? 0.5;
|
|
680
|
+
const weight = (kwargs.weight as number) || 2;
|
|
681
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
682
|
+
const popup = kwargs.popup as string | undefined;
|
|
683
|
+
const tooltip = kwargs.tooltip as string | undefined;
|
|
684
|
+
|
|
685
|
+
const cm = L.circleMarker([lat, lng], {
|
|
686
|
+
radius, color, fillColor, fillOpacity, weight, opacity,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
if (popup) cm.bindPopup(popup);
|
|
690
|
+
if (tooltip) cm.bindTooltip(tooltip);
|
|
691
|
+
|
|
692
|
+
cm.addTo(this.map);
|
|
693
|
+
this.layersMap.set(name, cm);
|
|
694
|
+
|
|
695
|
+
if (this.layerControl) {
|
|
696
|
+
this.layerControl.addOverlay(cm, name);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private handleAddCircle(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
701
|
+
if (!this.map) return;
|
|
702
|
+
const [lng, lat] = args as [number, number];
|
|
703
|
+
const name = (kwargs.name as string) || `circle-${Date.now()}`;
|
|
704
|
+
const radius = (kwargs.radius as number) || 1000;
|
|
705
|
+
const color = (kwargs.color as string) || '#3388ff';
|
|
706
|
+
const fillColor = (kwargs.fillColor as string) || color;
|
|
707
|
+
const fillOpacity = (kwargs.fillOpacity as number) ?? 0.2;
|
|
708
|
+
const weight = (kwargs.weight as number) || 2;
|
|
709
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
710
|
+
const popup = kwargs.popup as string | undefined;
|
|
711
|
+
const tooltip = kwargs.tooltip as string | undefined;
|
|
712
|
+
|
|
713
|
+
const circle = L.circle([lat, lng], {
|
|
714
|
+
radius, color, fillColor, fillOpacity, weight, opacity,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
if (popup) circle.bindPopup(popup);
|
|
718
|
+
if (tooltip) circle.bindTooltip(tooltip);
|
|
719
|
+
|
|
720
|
+
circle.addTo(this.map);
|
|
721
|
+
this.layersMap.set(name, circle);
|
|
722
|
+
|
|
723
|
+
if (this.layerControl) {
|
|
724
|
+
this.layerControl.addOverlay(circle, name);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private handleAddPolyline(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
729
|
+
if (!this.map) return;
|
|
730
|
+
const coords = kwargs.coordinates as number[][];
|
|
731
|
+
const name = (kwargs.name as string) || `polyline-${Date.now()}`;
|
|
732
|
+
const color = (kwargs.color as string) || '#3388ff';
|
|
733
|
+
const weight = (kwargs.weight as number) || 3;
|
|
734
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
735
|
+
const dashArray = kwargs.dashArray as string | undefined;
|
|
736
|
+
const popup = kwargs.popup as string | undefined;
|
|
737
|
+
const tooltip = kwargs.tooltip as string | undefined;
|
|
738
|
+
const fitBounds = kwargs.fitBounds as boolean | undefined;
|
|
739
|
+
|
|
740
|
+
const latLngs = coords.map(([lng, lat]) => [lat, lng] as [number, number]);
|
|
741
|
+
const opts: L.PolylineOptions = { color, weight, opacity };
|
|
742
|
+
if (dashArray) opts.dashArray = dashArray;
|
|
743
|
+
|
|
744
|
+
const polyline = L.polyline(latLngs, opts);
|
|
745
|
+
|
|
746
|
+
if (popup) polyline.bindPopup(popup);
|
|
747
|
+
if (tooltip) polyline.bindTooltip(tooltip);
|
|
748
|
+
|
|
749
|
+
polyline.addTo(this.map);
|
|
750
|
+
this.layersMap.set(name, polyline);
|
|
751
|
+
|
|
752
|
+
if (fitBounds) {
|
|
753
|
+
this.map.fitBounds(polyline.getBounds(), { padding: [50, 50] });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (this.layerControl) {
|
|
757
|
+
this.layerControl.addOverlay(polyline, name);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private handleAddPolygon(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
762
|
+
if (!this.map) return;
|
|
763
|
+
const coords = kwargs.coordinates as number[][];
|
|
764
|
+
const name = (kwargs.name as string) || `polygon-${Date.now()}`;
|
|
765
|
+
const color = (kwargs.color as string) || '#3388ff';
|
|
766
|
+
const fillColor = (kwargs.fillColor as string) || color;
|
|
767
|
+
const fillOpacity = (kwargs.fillOpacity as number) ?? 0.5;
|
|
768
|
+
const weight = (kwargs.weight as number) || 2;
|
|
769
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
770
|
+
const popup = kwargs.popup as string | undefined;
|
|
771
|
+
const tooltip = kwargs.tooltip as string | undefined;
|
|
772
|
+
const fitBounds = kwargs.fitBounds as boolean | undefined;
|
|
773
|
+
|
|
774
|
+
const latLngs = coords.map(([lng, lat]) => [lat, lng] as [number, number]);
|
|
775
|
+
|
|
776
|
+
const polygon = L.polygon(latLngs, { color, fillColor, fillOpacity, weight, opacity });
|
|
777
|
+
|
|
778
|
+
if (popup) polygon.bindPopup(popup);
|
|
779
|
+
if (tooltip) polygon.bindTooltip(tooltip);
|
|
780
|
+
|
|
781
|
+
polygon.addTo(this.map);
|
|
782
|
+
this.layersMap.set(name, polygon);
|
|
783
|
+
|
|
784
|
+
if (fitBounds) {
|
|
785
|
+
this.map.fitBounds(polygon.getBounds(), { padding: [50, 50] });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (this.layerControl) {
|
|
789
|
+
this.layerControl.addOverlay(polygon, name);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private handleAddRectangle(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
794
|
+
if (!this.map) return;
|
|
795
|
+
const bounds = kwargs.bounds as [number, number, number, number];
|
|
796
|
+
const name = (kwargs.name as string) || `rectangle-${Date.now()}`;
|
|
797
|
+
const color = (kwargs.color as string) || '#3388ff';
|
|
798
|
+
const fillColor = (kwargs.fillColor as string) || color;
|
|
799
|
+
const fillOpacity = (kwargs.fillOpacity as number) ?? 0.2;
|
|
800
|
+
const weight = (kwargs.weight as number) || 2;
|
|
801
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
802
|
+
const popup = kwargs.popup as string | undefined;
|
|
803
|
+
const tooltip = kwargs.tooltip as string | undefined;
|
|
804
|
+
|
|
805
|
+
const leafletBounds = L.latLngBounds(
|
|
806
|
+
[bounds[1], bounds[0]],
|
|
807
|
+
[bounds[3], bounds[2]]
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
const rect = L.rectangle(leafletBounds, { color, fillColor, fillOpacity, weight, opacity });
|
|
811
|
+
|
|
812
|
+
if (popup) rect.bindPopup(popup);
|
|
813
|
+
if (tooltip) rect.bindTooltip(tooltip);
|
|
814
|
+
|
|
815
|
+
rect.addTo(this.map);
|
|
816
|
+
this.layersMap.set(name, rect);
|
|
817
|
+
|
|
818
|
+
if (this.layerControl) {
|
|
819
|
+
this.layerControl.addOverlay(rect, name);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// -------------------------------------------------------------------------
|
|
824
|
+
// Image & Video overlay handlers
|
|
825
|
+
// -------------------------------------------------------------------------
|
|
826
|
+
|
|
827
|
+
private handleAddImageOverlay(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
828
|
+
if (!this.map) return;
|
|
829
|
+
const [url] = args as [string];
|
|
830
|
+
const bounds = kwargs.bounds as [number, number, number, number];
|
|
831
|
+
const name = (kwargs.name as string) || `image-${Date.now()}`;
|
|
832
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
833
|
+
const interactive = kwargs.interactive as boolean | undefined;
|
|
834
|
+
|
|
835
|
+
const leafletBounds = L.latLngBounds(
|
|
836
|
+
[bounds[1], bounds[0]],
|
|
837
|
+
[bounds[3], bounds[2]]
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
const imageOverlay = L.imageOverlay(url, leafletBounds, {
|
|
841
|
+
opacity,
|
|
842
|
+
interactive: interactive || false,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
imageOverlay.addTo(this.map);
|
|
846
|
+
this.layersMap.set(name, imageOverlay);
|
|
847
|
+
|
|
848
|
+
if (this.layerControl) {
|
|
849
|
+
this.layerControl.addOverlay(imageOverlay, name);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private handleAddVideoOverlay(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
854
|
+
if (!this.map) return;
|
|
855
|
+
const urls = kwargs.url as string | string[];
|
|
856
|
+
const bounds = kwargs.bounds as [number, number, number, number];
|
|
857
|
+
const name = (kwargs.name as string) || `video-${Date.now()}`;
|
|
858
|
+
const opacity = (kwargs.opacity as number) ?? 1;
|
|
859
|
+
const autoplay = kwargs.autoplay !== false;
|
|
860
|
+
const loop = kwargs.loop !== false;
|
|
861
|
+
const muted = kwargs.muted !== false;
|
|
862
|
+
|
|
863
|
+
const leafletBounds = L.latLngBounds(
|
|
864
|
+
[bounds[1], bounds[0]],
|
|
865
|
+
[bounds[3], bounds[2]]
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
const videoOverlay = L.videoOverlay(urls as string, leafletBounds, {
|
|
869
|
+
opacity,
|
|
870
|
+
autoplay,
|
|
871
|
+
loop,
|
|
872
|
+
muted,
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
videoOverlay.addTo(this.map);
|
|
876
|
+
this.layersMap.set(name, videoOverlay);
|
|
877
|
+
|
|
878
|
+
if (this.layerControl) {
|
|
879
|
+
this.layerControl.addOverlay(videoOverlay, name);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// -------------------------------------------------------------------------
|
|
884
|
+
// Heatmap handlers (via leaflet.heat)
|
|
885
|
+
// -------------------------------------------------------------------------
|
|
886
|
+
|
|
887
|
+
private handleAddHeatmap(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
888
|
+
if (!this.map) return;
|
|
889
|
+
const data = kwargs.data as number[][];
|
|
890
|
+
const name = (kwargs.name as string) || `heatmap-${Date.now()}`;
|
|
891
|
+
const radius = (kwargs.radius as number) || 25;
|
|
892
|
+
const blur = (kwargs.blur as number) || 15;
|
|
893
|
+
const maxZoom = (kwargs.maxZoom as number) || 18;
|
|
894
|
+
const max = (kwargs.max as number) || 1.0;
|
|
895
|
+
const minOpacity = (kwargs.minOpacity as number) || 0.05;
|
|
896
|
+
const gradient = kwargs.gradient as Record<string, string> | undefined;
|
|
897
|
+
|
|
898
|
+
// leaflet.heat expects [lat, lng, intensity] arrays
|
|
899
|
+
const heatData = data.map((point) => {
|
|
900
|
+
if (point.length >= 3) {
|
|
901
|
+
return [point[1], point[0], point[2]];
|
|
902
|
+
}
|
|
903
|
+
return [point[1], point[0]];
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const options: Record<string, unknown> = {
|
|
907
|
+
radius,
|
|
908
|
+
blur,
|
|
909
|
+
maxZoom,
|
|
910
|
+
max,
|
|
911
|
+
minOpacity,
|
|
912
|
+
};
|
|
913
|
+
if (gradient) options.gradient = gradient;
|
|
914
|
+
|
|
915
|
+
// @ts-ignore - L.heatLayer is added by leaflet.heat plugin
|
|
916
|
+
const heatLayer = (L as any).heatLayer(heatData, options);
|
|
917
|
+
heatLayer.addTo(this.map);
|
|
918
|
+
this.layersMap.set(name, heatLayer);
|
|
919
|
+
|
|
920
|
+
if (this.layerControl) {
|
|
921
|
+
this.layerControl.addOverlay(heatLayer, name);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private handleRemoveHeatmap(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
926
|
+
this.handleRemoveLayer(args, kwargs);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// -------------------------------------------------------------------------
|
|
930
|
+
// Choropleth handler
|
|
931
|
+
// -------------------------------------------------------------------------
|
|
932
|
+
|
|
933
|
+
private handleAddChoropleth(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
934
|
+
if (!this.map) return;
|
|
935
|
+
|
|
936
|
+
const geojson = kwargs.data as FeatureCollection;
|
|
937
|
+
const name = (kwargs.name as string) || `choropleth-${Date.now()}`;
|
|
938
|
+
const valueProperty = kwargs.valueProperty as string;
|
|
939
|
+
const colors = kwargs.colors as string[];
|
|
940
|
+
const thresholds = kwargs.thresholds as number[];
|
|
941
|
+
const fillOpacity = (kwargs.fillOpacity as number) ?? 0.7;
|
|
942
|
+
const lineColor = (kwargs.lineColor as string) || '#ffffff';
|
|
943
|
+
const lineWeight = (kwargs.lineWeight as number) || 2;
|
|
944
|
+
const lineOpacity = (kwargs.lineOpacity as number) ?? 1;
|
|
945
|
+
const popupProperties = kwargs.popupProperties as string[] | boolean | undefined;
|
|
946
|
+
const tooltipProperty = kwargs.tooltipProperty as string | undefined;
|
|
947
|
+
const fitBounds = kwargs.fitBounds !== false;
|
|
948
|
+
const legendTitle = kwargs.legendTitle as string | undefined;
|
|
949
|
+
const legendPosition = kwargs.legendPosition as string | undefined;
|
|
950
|
+
|
|
951
|
+
const getColor = (value: number): string => {
|
|
952
|
+
for (let i = thresholds.length - 1; i >= 0; i--) {
|
|
953
|
+
if (value >= thresholds[i]) return colors[i + 1] || colors[colors.length - 1];
|
|
954
|
+
}
|
|
955
|
+
return colors[0];
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const choroplethLayer = L.geoJSON(geojson as any, {
|
|
959
|
+
style: (feature: any) => {
|
|
960
|
+
const value = feature?.properties?.[valueProperty] ?? 0;
|
|
961
|
+
return {
|
|
962
|
+
fillColor: getColor(value),
|
|
963
|
+
weight: lineWeight,
|
|
964
|
+
opacity: lineOpacity,
|
|
965
|
+
color: lineColor,
|
|
966
|
+
fillOpacity,
|
|
967
|
+
};
|
|
968
|
+
},
|
|
969
|
+
onEachFeature: (feature: any, layer: L.Layer) => {
|
|
970
|
+
const props = feature.properties || {};
|
|
971
|
+
|
|
972
|
+
if (popupProperties) {
|
|
973
|
+
let html = '<div class="anymap-popup">';
|
|
974
|
+
const keys = popupProperties === true ? Object.keys(props) : popupProperties;
|
|
975
|
+
for (const key of keys) {
|
|
976
|
+
if (props[key] !== undefined && props[key] !== null) {
|
|
977
|
+
html += `<b>${key}:</b> ${props[key]}<br>`;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
html += '</div>';
|
|
981
|
+
layer.bindPopup(html);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (tooltipProperty && props[tooltipProperty] !== undefined) {
|
|
985
|
+
layer.bindTooltip(String(props[tooltipProperty]), { sticky: true });
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Highlight on hover
|
|
989
|
+
(layer as any).on({
|
|
990
|
+
mouseover: (e: any) => {
|
|
991
|
+
const target = e.target;
|
|
992
|
+
target.setStyle({
|
|
993
|
+
weight: lineWeight + 2,
|
|
994
|
+
fillOpacity: Math.min(fillOpacity + 0.2, 1),
|
|
995
|
+
});
|
|
996
|
+
target.bringToFront();
|
|
997
|
+
},
|
|
998
|
+
mouseout: (e: any) => {
|
|
999
|
+
choroplethLayer.resetStyle(e.target);
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
choroplethLayer.addTo(this.map);
|
|
1006
|
+
this.layersMap.set(name, choroplethLayer);
|
|
1007
|
+
|
|
1008
|
+
if (fitBounds) {
|
|
1009
|
+
const layerBounds = choroplethLayer.getBounds();
|
|
1010
|
+
if (layerBounds.isValid()) {
|
|
1011
|
+
this.map.fitBounds(layerBounds, { padding: [50, 50] });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (this.layerControl) {
|
|
1016
|
+
this.layerControl.addOverlay(choroplethLayer, name);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (legendTitle && colors && thresholds) {
|
|
1020
|
+
this.createChoroplethLegend(name, legendTitle, colors, thresholds, legendPosition || 'bottomright');
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private createChoroplethLegend(
|
|
1025
|
+
name: string,
|
|
1026
|
+
title: string,
|
|
1027
|
+
colors: string[],
|
|
1028
|
+
thresholds: number[],
|
|
1029
|
+
position: string,
|
|
1030
|
+
): void {
|
|
1031
|
+
if (!this.map) return;
|
|
1032
|
+
|
|
1033
|
+
const LegendControl = L.Control.extend({
|
|
1034
|
+
options: { position: this.convertPosition(position) },
|
|
1035
|
+
onAdd: () => {
|
|
1036
|
+
const div = L.DomUtil.create('div', 'anymap-legend');
|
|
1037
|
+
|
|
1038
|
+
let html = `<div style="font-weight:bold;margin-bottom:6px">${title}</div>`;
|
|
1039
|
+
const numBins = thresholds.length + 1;
|
|
1040
|
+
for (let i = 0; i < numBins; i++) {
|
|
1041
|
+
const color = colors[i] || colors[colors.length - 1];
|
|
1042
|
+
const label = i === 0
|
|
1043
|
+
? `< ${thresholds[0]}`
|
|
1044
|
+
: i < thresholds.length
|
|
1045
|
+
? `${thresholds[i - 1]} – ${thresholds[i]}`
|
|
1046
|
+
: `≥ ${thresholds[thresholds.length - 1]}`;
|
|
1047
|
+
html += `<div style="display:flex;align-items:center;margin:2px 0"><span style="width:18px;height:12px;display:inline-block;background:${color};margin-right:6px;border-radius:2px"></span>${label}</div>`;
|
|
1048
|
+
}
|
|
1049
|
+
div.innerHTML = html;
|
|
1050
|
+
return div;
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const legend = new LegendControl();
|
|
1055
|
+
legend.addTo(this.map);
|
|
1056
|
+
this.controlsMap.set(`legend-${name}`, legend);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// -------------------------------------------------------------------------
|
|
1060
|
+
// Popup handlers
|
|
1061
|
+
// -------------------------------------------------------------------------
|
|
1062
|
+
|
|
1063
|
+
private handleAddPopup(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1064
|
+
if (!this.map) return;
|
|
1065
|
+
const [lng, lat] = args as [number, number];
|
|
1066
|
+
const content = kwargs.content as string;
|
|
1067
|
+
const id = (kwargs.id as string) || `popup-${Date.now()}`;
|
|
1068
|
+
const maxWidth = (kwargs.maxWidth as number) || 300;
|
|
1069
|
+
const closeButton = kwargs.closeButton !== false;
|
|
1070
|
+
|
|
1071
|
+
const popup = L.popup({
|
|
1072
|
+
maxWidth,
|
|
1073
|
+
closeButton,
|
|
1074
|
+
})
|
|
1075
|
+
.setLatLng([lat, lng])
|
|
1076
|
+
.setContent(content)
|
|
1077
|
+
.openOn(this.map);
|
|
1078
|
+
|
|
1079
|
+
this.popupsMap.set(id, popup);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private handleRemovePopup(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1083
|
+
if (!this.map) return;
|
|
1084
|
+
const [id] = args as [string];
|
|
1085
|
+
|
|
1086
|
+
const popup = this.popupsMap.get(id);
|
|
1087
|
+
if (popup) {
|
|
1088
|
+
this.map.closePopup(popup);
|
|
1089
|
+
this.popupsMap.delete(id);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// -------------------------------------------------------------------------
|
|
1094
|
+
// Legend handlers
|
|
1095
|
+
// -------------------------------------------------------------------------
|
|
1096
|
+
|
|
1097
|
+
private handleAddLegend(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1098
|
+
if (!this.map) return;
|
|
1099
|
+
const name = (kwargs.name as string) || 'legend';
|
|
1100
|
+
const title = kwargs.title as string;
|
|
1101
|
+
const items = kwargs.items as Array<{ color: string; label: string }>;
|
|
1102
|
+
const position = (kwargs.position as string) || 'bottomright';
|
|
1103
|
+
|
|
1104
|
+
const LegendControl = L.Control.extend({
|
|
1105
|
+
options: { position: this.convertPosition(position) },
|
|
1106
|
+
onAdd: () => {
|
|
1107
|
+
const div = L.DomUtil.create('div', 'anymap-legend');
|
|
1108
|
+
|
|
1109
|
+
let html = title ? `<div style="font-weight:bold;margin-bottom:6px">${title}</div>` : '';
|
|
1110
|
+
for (const item of items) {
|
|
1111
|
+
html += `<div style="display:flex;align-items:center;margin:2px 0"><span style="width:18px;height:12px;display:inline-block;background:${item.color};margin-right:6px;border-radius:2px"></span>${item.label}</div>`;
|
|
1112
|
+
}
|
|
1113
|
+
div.innerHTML = html;
|
|
1114
|
+
return div;
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const legend = new LegendControl();
|
|
1119
|
+
legend.addTo(this.map);
|
|
1120
|
+
this.controlsMap.set(`legend-${name}`, legend);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
private handleRemoveLegend(args: unknown[], kwargs: Record<string, unknown>): void {
|
|
1124
|
+
if (!this.map) return;
|
|
1125
|
+
const [name] = args as [string];
|
|
1126
|
+
const key = `legend-${name}`;
|
|
1127
|
+
|
|
1128
|
+
const control = this.controlsMap.get(key);
|
|
1129
|
+
if (control) {
|
|
1130
|
+
this.map.removeControl(control);
|
|
1131
|
+
this.controlsMap.delete(key);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
564
1135
|
// -------------------------------------------------------------------------
|
|
565
1136
|
// Trait change handlers
|
|
566
1137
|
// -------------------------------------------------------------------------
|
|
@@ -581,7 +1152,6 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
581
1152
|
|
|
582
1153
|
protected onStyleChange(): void {
|
|
583
1154
|
// Leaflet doesn't have a style concept like MapLibre/Mapbox
|
|
584
|
-
// Style changes would need to be handled per-layer
|
|
585
1155
|
}
|
|
586
1156
|
|
|
587
1157
|
// -------------------------------------------------------------------------
|
|
@@ -620,6 +1190,7 @@ export class LeafletRenderer extends BaseMapRenderer<LeafletMap> {
|
|
|
620
1190
|
}
|
|
621
1191
|
});
|
|
622
1192
|
this.controlsMap.clear();
|
|
1193
|
+
this.layerControl = null;
|
|
623
1194
|
|
|
624
1195
|
if (this.map) {
|
|
625
1196
|
this.map.remove();
|