uni-leaflet 1.1.0 → 1.2.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.
@@ -9,7 +9,18 @@ import type {
9
9
  CircleOptions,
10
10
  MapOverlays,
11
11
  OverlayClickEvent,
12
+ FitBoundsOptions,
13
+ LatLngBoundsExpression,
14
+ MapBounds,
15
+ ClusterOptions,
16
+ ClusterGroup,
17
+ GeoJsonStyle,
18
+ GeoJsonStyleOptions,
12
19
  } from '../types';
20
+ import { extractLatLngs } from '../utils/bounds';
21
+ import { clusterMarkers } from '../utils/cluster';
22
+ import { resolveGeoJsonStyle } from '../utils/geojson';
23
+ import { generateCurvedPolyline } from '../utils/curve';
13
24
 
14
25
  export interface H5AdapterOptions {
15
26
  container: HTMLElement;
@@ -21,52 +32,415 @@ export interface H5AdapterOptions {
21
32
  subdomains?: string[];
22
33
  layers?: Array<string | TileLayerConfig>;
23
34
  overlays?: MapOverlays;
35
+ geojson?: any;
36
+ geojsonStyle?: GeoJsonStyle;
24
37
  onMove?: (center: LatLngTuple, zoom: number) => void;
25
38
  onMoveEnd?: (center: LatLngTuple, zoom: number) => void;
26
39
  onZoom?: (zoom: number) => void;
27
40
  onZoomEnd?: (zoom: number) => void;
28
41
  onClick?: (latLng: LatLngTuple, point: Point) => void;
29
42
  onOverlayClick?: (event: OverlayClickEvent) => void;
43
+ onClusterClick?: (cluster: ClusterGroup) => void;
44
+ clusterOptions?: ClusterOptions;
30
45
  }
31
46
 
32
47
  import * as LModule from 'leaflet';
33
48
  import 'leaflet/dist/leaflet.css';
34
49
 
50
+ function patchLeafletDraggableSafeguards(L: any) {
51
+ if (!L || (L as any).__uni_patched__) return;
52
+ (L as any).__uni_patched__ = true;
53
+
54
+ // 1. Prototype-level defense: Prevent "Cannot read properties of undefined (reading 'baseVal')"
55
+ // When mouse events bubble to document or window, el.classList is undefined in standard DOM,
56
+ // causing Leaflet's internal DomUtil.addClass/getClass to attempt accessing document.className.baseVal.
57
+ if (typeof window !== 'undefined') {
58
+ const emptyClassList = {
59
+ add() {},
60
+ remove() {},
61
+ contains() { return false; },
62
+ toggle() { return false; },
63
+ item() { return null; },
64
+ length: 0,
65
+ value: '',
66
+ };
67
+
68
+ if (typeof Document !== 'undefined') {
69
+ if (!(Document.prototype as any).classList) {
70
+ Object.defineProperty(Document.prototype, 'classList', {
71
+ get() { return emptyClassList; },
72
+ configurable: true,
73
+ });
74
+ }
75
+ if ((Document.prototype as any).className === undefined) {
76
+ Object.defineProperty(Document.prototype, 'className', {
77
+ get() { return ''; },
78
+ set(_val: any) {},
79
+ configurable: true,
80
+ });
81
+ }
82
+ }
83
+
84
+ if (typeof Window !== 'undefined') {
85
+ if (!(Window.prototype as any).classList) {
86
+ Object.defineProperty(Window.prototype, 'classList', {
87
+ get() { return emptyClassList; },
88
+ configurable: true,
89
+ });
90
+ }
91
+ if ((Window.prototype as any).className === undefined) {
92
+ Object.defineProperty(Window.prototype, 'className', {
93
+ get() { return ''; },
94
+ set(_val: any) {},
95
+ configurable: true,
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ // 2. Defend against "Cannot read properties of undefined (reading 'baseVal')" in L.DomUtil
102
+ if (L.DomUtil) {
103
+ const origRemoveClass = L.DomUtil.removeClass;
104
+ const origAddClass = L.DomUtil.addClass;
105
+
106
+ L.DomUtil.getClass = function (el: any) {
107
+ if (!el || typeof el !== 'object') return '';
108
+ if (el.correspondingElement) {
109
+ el = el.correspondingElement;
110
+ }
111
+ if (!el || !el.className) {
112
+ return (el && typeof el.className === 'string') ? el.className : '';
113
+ }
114
+ return el.className.baseVal === undefined ? String(el.className || '') : (el.className.baseVal || '');
115
+ };
116
+
117
+ L.DomUtil.setClass = function (el: any, name: string) {
118
+ if (!el || typeof el !== 'object') return;
119
+ if (!el.className || el.className.baseVal === undefined) {
120
+ if ('className' in el) {
121
+ try { el.className = name; } catch (e) { }
122
+ }
123
+ } else {
124
+ try { el.className.baseVal = name; } catch (e) { }
125
+ }
126
+ };
127
+
128
+ L.DomUtil.removeClass = function (el: any, name: string) {
129
+ if (!el || typeof el !== 'object') return;
130
+ if (el.classList !== undefined) {
131
+ try { el.classList.remove(name); } catch (e) { }
132
+ } else if (el.className) {
133
+ try { origRemoveClass.call(L.DomUtil, el, name); } catch (e) { }
134
+ }
135
+ };
136
+
137
+ L.DomUtil.addClass = function (el: any, name: string) {
138
+ if (!el || typeof el !== 'object') return;
139
+ if (el.classList !== undefined) {
140
+ try { el.classList.add(name); } catch (e) { }
141
+ } else if (el.className) {
142
+ try { origAddClass.call(L.DomUtil, el, name); } catch (e) { }
143
+ }
144
+ };
145
+ }
146
+
147
+ // 3. Defend against dragging lockup when finishDrag or _onMove throws or when dragging is orphaned
148
+ if (L.Draggable && L.Draggable.prototype) {
149
+ const origOnMove = L.Draggable.prototype._onMove;
150
+ if (origOnMove) {
151
+ L.Draggable.prototype._onMove = function (e: any) {
152
+ if (!this._moving && e && (!e.target || !(e.target instanceof Element))) {
153
+ const safeTarget = this._element || (typeof document !== 'undefined' ? document.body : null);
154
+ if (safeTarget) {
155
+ try {
156
+ Object.defineProperty(e, 'target', { value: safeTarget, configurable: true });
157
+ } catch (err) { }
158
+ }
159
+ }
160
+ try {
161
+ origOnMove.call(this, e);
162
+ } catch (err) {
163
+ console.warn('[uni-leaflet] Safe recovery from Draggable _onMove error:', err);
164
+ }
165
+ };
166
+ }
167
+
168
+ const origFinishDrag = L.Draggable.prototype.finishDrag;
169
+ L.Draggable.prototype.finishDrag = function (noInertia: any) {
170
+ try {
171
+ origFinishDrag.call(this, noInertia);
172
+ } catch (err) {
173
+ console.warn('[uni-leaflet] Safe recovery from Draggable finishDrag error:', err);
174
+ } finally {
175
+ this._moving = false;
176
+ L.Draggable._dragging = false;
177
+ }
178
+ };
179
+ }
180
+ }
181
+
35
182
  function getL(): any {
183
+ let L: any = null;
36
184
  if (LModule && typeof (LModule as any).map === 'function') {
37
- return LModule;
38
- }
39
- if (
185
+ L = LModule;
186
+ } else if (
40
187
  LModule &&
41
188
  (LModule as any).default &&
42
189
  typeof (LModule as any).default.map === 'function'
43
190
  ) {
44
- return (LModule as any).default;
45
- }
46
- if (
191
+ L = (LModule as any).default;
192
+ } else if (
47
193
  typeof window !== 'undefined' &&
48
194
  (window as any).L &&
49
195
  typeof (window as any).L.map === 'function'
50
196
  ) {
51
- return (window as any).L;
52
- }
53
- if (
197
+ L = (window as any).L;
198
+ } else if (
54
199
  typeof globalThis !== 'undefined' &&
55
200
  (globalThis as any).L &&
56
201
  typeof (globalThis as any).L.map === 'function'
57
202
  ) {
58
- return (globalThis as any).L;
203
+ L = (globalThis as any).L;
204
+ } else {
205
+ L = (LModule as any)?.default || LModule;
206
+ }
207
+
208
+ if (L) {
209
+ patchLeafletDraggableSafeguards(L);
210
+ }
211
+ return L;
212
+ }
213
+
214
+ function ensureH5GlobalStyles() {
215
+ if (typeof document === 'undefined') return;
216
+ let style = document.getElementById('uni-leaflet-h5-global-styles') as HTMLStyleElement | null;
217
+ if (!style) {
218
+ style = document.createElement('style');
219
+ style.id = 'uni-leaflet-h5-global-styles';
220
+ document.head.appendChild(style);
59
221
  }
60
- return (LModule as any)?.default || LModule;
222
+ style.textContent = `
223
+ .uni-avatar-marker-icon,
224
+ .uni-cluster-marker-icon,
225
+ .uni-emoji-marker-icon,
226
+ .uni-custom-svg-icon,
227
+ .uni-custom-html-icon,
228
+ .uni-custom-img-icon,
229
+ .uni-custom-icon,
230
+ .uni-default-pin {
231
+ background: transparent !important;
232
+ border: none !important;
233
+ }
234
+ .uni-avatar-marker-wrapper {
235
+ position: relative !important;
236
+ display: flex !important;
237
+ flex-direction: column !important;
238
+ align-items: center !important;
239
+ justify-content: flex-start !important;
240
+ cursor: pointer !important;
241
+ user-select: none !important;
242
+ }
243
+ .uni-avatar-bubble {
244
+ position: relative !important;
245
+ border-radius: 50% !important;
246
+ background-color: #ffffff !important;
247
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) !important;
248
+ display: flex !important;
249
+ align-items: center !important;
250
+ justify-content: center !important;
251
+ box-sizing: border-box !important;
252
+ overflow: visible !important;
253
+ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
254
+ }
255
+ .uni-avatar-marker-wrapper:hover .uni-avatar-bubble {
256
+ transform: scale(1.12) !important;
257
+ }
258
+ .uni-avatar-img {
259
+ width: 100% !important;
260
+ height: 100% !important;
261
+ border-radius: 50% !important;
262
+ object-fit: cover !important;
263
+ display: block !important;
264
+ pointer-events: none !important;
265
+ }
266
+ .uni-avatar-arrow {
267
+ position: absolute !important;
268
+ bottom: -6px !important;
269
+ left: 50% !important;
270
+ transform: translateX(-50%) !important;
271
+ width: 0 !important;
272
+ height: 0 !important;
273
+ border-left: 5px solid transparent !important;
274
+ border-right: 5px solid transparent !important;
275
+ border-top-width: 6px !important;
276
+ border-top-style: solid !important;
277
+ z-index: 1 !important;
278
+ filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.2)) !important;
279
+ }
280
+ .uni-avatar-badge {
281
+ position: absolute !important;
282
+ top: -2px !important;
283
+ right: -2px !important;
284
+ background: #ffffff !important;
285
+ color: #1e293b !important;
286
+ font-size: 8px !important;
287
+ font-weight: 700 !important;
288
+ line-height: 1 !important;
289
+ height: 12px !important;
290
+ min-width: 12px !important;
291
+ padding: 0 2px !important;
292
+ border-radius: 6px !important;
293
+ display: flex !important;
294
+ align-items: center !important;
295
+ justify-content: center !important;
296
+ border: 1px solid rgba(0, 0, 0, 0.08) !important;
297
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
298
+ box-sizing: border-box !important;
299
+ white-space: nowrap !important;
300
+ z-index: 10 !important;
301
+ pointer-events: none !important;
302
+ }
303
+ @keyframes uni-avatar-pulse {
304
+ 0% {
305
+ transform: scale(1);
306
+ opacity: 0.85;
307
+ }
308
+ 70% {
309
+ transform: scale(1.6);
310
+ opacity: 0;
311
+ }
312
+ 100% {
313
+ transform: scale(1.6);
314
+ opacity: 0;
315
+ }
316
+ }
317
+ .uni-avatar-pulse {
318
+ position: absolute !important;
319
+ top: 0 !important;
320
+ left: 0 !important;
321
+ width: 100% !important;
322
+ height: 100% !important;
323
+ border-radius: 50% !important;
324
+ box-sizing: border-box !important;
325
+ border-style: solid !important;
326
+ border-width: 3px !important;
327
+ animation: uni-avatar-pulse 1.8s cubic-bezier(0.24, 0, 0.38, 1) infinite !important;
328
+ pointer-events: none !important;
329
+ z-index: 0 !important;
330
+ }
331
+ .uni-cluster-bubble {
332
+ position: relative !important;
333
+ width: 44px !important;
334
+ height: 44px !important;
335
+ border-radius: 50% !important;
336
+ background: #ffffff !important;
337
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22), 0 0 0 3px #3b82f6 !important;
338
+ display: flex !important;
339
+ align-items: center !important;
340
+ justify-content: center !important;
341
+ cursor: pointer !important;
342
+ user-select: none !important;
343
+ box-sizing: border-box !important;
344
+ transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
345
+ }
346
+ .uni-cluster-bubble:hover {
347
+ transform: scale(1.1) !important;
348
+ }
349
+ .uni-cluster-bubble .uni-cluster-icon {
350
+ width: 26px !important;
351
+ height: 26px !important;
352
+ display: flex !important;
353
+ align-items: center !important;
354
+ justify-content: center !important;
355
+ border-radius: 50% !important;
356
+ overflow: hidden !important;
357
+ }
358
+ .uni-cluster-bubble .uni-cluster-badge {
359
+ position: absolute !important;
360
+ top: -6px !important;
361
+ right: -8px !important;
362
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
363
+ color: #ffffff !important;
364
+ font-size: 11px !important;
365
+ font-weight: 700 !important;
366
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
367
+ height: 18px !important;
368
+ min-width: 18px !important;
369
+ padding: 0 5px !important;
370
+ border-radius: 10px !important;
371
+ display: flex !important;
372
+ align-items: center !important;
373
+ justify-content: center !important;
374
+ border: 2px solid #ffffff !important;
375
+ box-shadow: 0 2px 6px rgba(220, 38, 38, 0.45) !important;
376
+ box-sizing: border-box !important;
377
+ white-space: nowrap !important;
378
+ pointer-events: none !important;
379
+ }
380
+ @keyframes uni-polyline-flowing {
381
+ from { stroke-dashoffset: 40px; }
382
+ to { stroke-dashoffset: 0px; }
383
+ }
384
+ .uni-flowing-polyline {
385
+ animation: uni-polyline-flowing 1.2s linear infinite !important;
386
+ stroke-linecap: round !important;
387
+ stroke-linejoin: round !important;
388
+ }
389
+ .leaflet-tooltip.uni-overlay-label {
390
+ position: absolute !important;
391
+ background-color: rgba(255, 255, 255, 0.96) !important;
392
+ border: 1px solid #cbd5e1 !important;
393
+ border-radius: 6px !important;
394
+ padding: 3px 8px !important;
395
+ font-size: 12px !important;
396
+ font-weight: 600 !important;
397
+ color: #1e293b !important;
398
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important;
399
+ white-space: nowrap !important;
400
+ pointer-events: none !important;
401
+ }
402
+ .leaflet-tooltip-top.uni-overlay-label:after {
403
+ bottom: -5px !important;
404
+ border-width: 5px 4px 0 4px !important;
405
+ border-color: rgba(255, 255, 255, 0.96) transparent transparent transparent !important;
406
+ z-index: 2 !important;
407
+ }
408
+ `;
409
+ document.head.appendChild(style);
61
410
  }
62
411
 
63
412
  export class H5MapAdapter implements IMapEngine {
64
413
  private map: any = null;
65
414
  private tileLayers: any[] = [];
66
415
  private overlayGroup: any = null;
416
+ private polygonGroup: any = null;
417
+ private polylineGroup: any = null;
418
+ private circleGroup: any = null;
419
+ private geoJsonGroup: any = null;
420
+ private markerLayerGroup: any = null;
421
+ private rawMarkers: MarkerOptions[] = [];
422
+ private clusterOptions: ClusterOptions = {
423
+ enable: false,
424
+ clusterRadius: 60,
425
+ clusterMaxZoom: 17,
426
+ };
427
+ private geoJsonLayer: any = null;
428
+ private currentGeoJsonData: any = null;
429
+ private currentGeoJsonStyle?: GeoJsonStyle;
430
+ private lastDragTimestamp = 0;
67
431
  private L: any = null;
68
432
  private pendingLayers?: Array<string | TileLayerConfig>;
69
433
  private pendingOverlays?: MapOverlays;
434
+ private releaseWatchdogHandler?: () => void;
435
+
436
+ private isRecentlyDragged(): boolean {
437
+ if (!this.map) return false;
438
+ if (Date.now() - this.lastDragTimestamp < 300) return true;
439
+ if (this.map.dragging && typeof this.map.dragging.moved === 'function') {
440
+ return this.map.dragging.moved();
441
+ }
442
+ return false;
443
+ }
70
444
 
71
445
  public onMove?: (center: LatLngTuple, zoom: number) => void;
72
446
  public onMoveEnd?: (center: LatLngTuple, zoom: number) => void;
@@ -74,14 +448,20 @@ export class H5MapAdapter implements IMapEngine {
74
448
  public onZoomEnd?: (zoom: number) => void;
75
449
  public onClick?: (latLng: LatLngTuple, point: Point) => void;
76
450
  public onOverlayClick?: (event: OverlayClickEvent) => void;
451
+ public onClusterClick?: (cluster: ClusterGroup) => void;
77
452
 
78
453
  public async init(options: H5AdapterOptions): Promise<void> {
454
+ ensureH5GlobalStyles();
79
455
  this.onMove = options.onMove;
80
456
  this.onMoveEnd = options.onMoveEnd;
81
457
  this.onZoom = options.onZoom;
82
458
  this.onZoomEnd = options.onZoomEnd;
83
459
  this.onClick = options.onClick;
84
460
  this.onOverlayClick = options.onOverlayClick;
461
+ this.onClusterClick = options.onClusterClick;
462
+ if (options.clusterOptions) {
463
+ this.clusterOptions = { ...this.clusterOptions, ...options.clusterOptions };
464
+ }
85
465
 
86
466
  let L = getL();
87
467
  if (!L || typeof L.map !== 'function') {
@@ -103,6 +483,10 @@ export class H5MapAdapter implements IMapEngine {
103
483
  attributionControl: false,
104
484
  });
105
485
 
486
+ if (typeof window !== 'undefined') {
487
+ (window as any).__activeMap = this.map;
488
+ }
489
+
106
490
  // Create a dedicated overlay pane for annotation / label layers above tilePane
107
491
  try {
108
492
  this.map.createPane('overlayTilePane');
@@ -111,9 +495,18 @@ export class H5MapAdapter implements IMapEngine {
111
495
  overlayPane.style.zIndex = '350';
112
496
  overlayPane.style.pointerEvents = 'none';
113
497
  }
114
- } catch (e) {}
115
-
116
- // Create overlay layer group for markers, lines, polygons
498
+ } catch (e) { }
499
+
500
+ // Create isolated layer groups in ordered Z-depth hierarchy
501
+ // 1. GeoJSON (boundaries / background districts)
502
+ this.geoJsonGroup = L.layerGroup().addTo(this.map);
503
+ // 2. Vector overlays (Polygons, Circles, Polylines)
504
+ this.polygonGroup = L.layerGroup().addTo(this.map);
505
+ this.circleGroup = L.layerGroup().addTo(this.map);
506
+ this.polylineGroup = L.layerGroup().addTo(this.map);
507
+ // 3. Markers & Clusters on top
508
+ this.markerLayerGroup = L.layerGroup().addTo(this.map);
509
+ // 4. General overlayGroup for backwards-compatibility
117
510
  this.overlayGroup = L.layerGroup().addTo(this.map);
118
511
 
119
512
  const initialLayers =
@@ -134,6 +527,18 @@ export class H5MapAdapter implements IMapEngine {
134
527
  this.setOverlays(this.pendingOverlays || options.overlays || {});
135
528
  }
136
529
 
530
+ if (options.geojson || options.overlays?.geojson) {
531
+ this.setGeoJSON(
532
+ options.geojson || options.overlays?.geojson,
533
+ options.geojsonStyle || options.overlays?.geojsonStyle
534
+ );
535
+ }
536
+
537
+ // Track user drag/move gesture to prevent click misfire on overlays after panning
538
+ this.map.on('movestart dragstart moveend dragend', () => {
539
+ this.lastDragTimestamp = Date.now();
540
+ });
541
+
137
542
  // Bind Leaflet events
138
543
  this.map.on('move', () => {
139
544
  const c = this.map.getCenter();
@@ -155,6 +560,9 @@ export class H5MapAdapter implements IMapEngine {
155
560
  this.map.on('zoomend', () => {
156
561
  const z = this.map.getZoom();
157
562
  this.onZoomEnd?.(z);
563
+ if (this.clusterOptions.enable && this.rawMarkers.length > 0) {
564
+ this.renderMarkers();
565
+ }
158
566
  });
159
567
 
160
568
  this.map.on('click', (e: any) => {
@@ -164,6 +572,21 @@ export class H5MapAdapter implements IMapEngine {
164
572
  });
165
573
  });
166
574
 
575
+ // Global watchdog against orphaned Leaflet drag state (e.g., pointer released outside container or interrupted by DOM shifts)
576
+ if (typeof window !== 'undefined') {
577
+ const releaseGhostDrag = () => {
578
+ if (L && L.Draggable && L.Draggable._dragging) {
579
+ L.Draggable._dragging = false;
580
+ }
581
+ };
582
+ window.addEventListener('mouseup', releaseGhostDrag, true);
583
+ window.addEventListener('touchend', releaseGhostDrag, true);
584
+ this.releaseWatchdogHandler = () => {
585
+ window.removeEventListener('mouseup', releaseGhostDrag, true);
586
+ window.removeEventListener('touchend', releaseGhostDrag, true);
587
+ };
588
+ }
589
+
167
590
  // Invalidate size in next tick to avoid tile rendering glitches
168
591
  setTimeout(() => {
169
592
  this.map?.invalidateSize();
@@ -201,6 +624,36 @@ export class H5MapAdapter implements IMapEngine {
201
624
  });
202
625
  }
203
626
 
627
+ public fitBounds(
628
+ bounds: LatLngBoundsExpression | LatLngTuple[] | MapBounds,
629
+ options?: FitBoundsOptions
630
+ ) {
631
+ if (!this.map) return;
632
+ const latLngs = extractLatLngs(bounds);
633
+ if (!latLngs || latLngs.length === 0) return;
634
+
635
+ const leafletOptions: any = {
636
+ animate: options?.animate !== false,
637
+ };
638
+ if (options?.padding) {
639
+ leafletOptions.padding = options.padding;
640
+ }
641
+ if (options?.paddingTopLeft) {
642
+ leafletOptions.paddingTopLeft = options.paddingTopLeft;
643
+ }
644
+ if (options?.paddingBottomRight) {
645
+ leafletOptions.paddingBottomRight = options.paddingBottomRight;
646
+ }
647
+ if (options?.maxZoom !== undefined) {
648
+ leafletOptions.maxZoom = options.maxZoom;
649
+ }
650
+ if (options?.duration !== undefined) {
651
+ leafletOptions.duration = options.duration / 1000;
652
+ }
653
+
654
+ this.map.fitBounds(latLngs, leafletOptions);
655
+ }
656
+
204
657
  public setTileUrl(url: string, subdomains?: string[]) {
205
658
  this.setLayers([{ url, subdomains }]);
206
659
  }
@@ -215,7 +668,7 @@ export class H5MapAdapter implements IMapEngine {
215
668
  for (const layer of this.tileLayers) {
216
669
  try {
217
670
  this.map.removeLayer(layer);
218
- } catch (e) {}
671
+ } catch (e) { }
219
672
  }
220
673
  this.tileLayers = [];
221
674
 
@@ -251,150 +704,394 @@ export class H5MapAdapter implements IMapEngine {
251
704
  // --- Vector Overlays (Markers, Polylines, Polygons, Circles) ---
252
705
 
253
706
  public setOverlays(overlays: MapOverlays) {
254
- if (!this.map || !this.L || !this.overlayGroup) {
707
+ if (!this.map || !this.L) {
255
708
  this.pendingOverlays = overlays;
256
709
  return;
257
710
  }
258
711
 
259
- this.clearOverlays();
260
-
261
- if (overlays.polygons) {
712
+ if (overlays.polygons !== undefined) {
262
713
  this.setPolygons(overlays.polygons);
263
714
  }
264
- if (overlays.polylines) {
715
+ if (overlays.polylines !== undefined) {
265
716
  this.setPolylines(overlays.polylines);
266
717
  }
267
- if (overlays.circles) {
718
+ if (overlays.circles !== undefined) {
268
719
  this.setCircles(overlays.circles);
269
720
  }
270
- if (overlays.markers) {
721
+ if (overlays.markers !== undefined) {
271
722
  this.setMarkers(overlays.markers);
272
723
  }
724
+ if (overlays.geojson !== undefined) {
725
+ this.setGeoJSON(overlays.geojson, overlays.geojsonStyle);
726
+ }
727
+ }
728
+
729
+ public setGeoJSON(data: any, style?: GeoJsonStyle) {
730
+ this.currentGeoJsonData = data;
731
+ this.currentGeoJsonStyle = style;
732
+
733
+ if (!this.map || !this.L) return;
734
+
735
+ if (!this.geoJsonGroup) {
736
+ this.geoJsonGroup = this.L.layerGroup().addTo(this.map);
737
+ }
738
+ this.geoJsonGroup.clearLayers();
739
+ this.geoJsonLayer = null;
740
+
741
+ if (!data) return;
742
+
743
+ try {
744
+ this.geoJsonLayer = this.L.geoJSON(data, {
745
+ style: (feature: any) => {
746
+ const s = resolveGeoJsonStyle(this.currentGeoJsonStyle, feature, '#3b82f6');
747
+ return {
748
+ color: s.color,
749
+ weight: s.weight,
750
+ opacity: s.opacity,
751
+ fillColor: s.fillColor,
752
+ fillOpacity: s.fillOpacity,
753
+ dashArray: s.dashArray ? s.dashArray.join(',') : undefined,
754
+ };
755
+ },
756
+ onEachFeature: (feature: any, layer: any) => {
757
+ layer.on('click', (e: any) => {
758
+ // Drag protection: do not trigger click if user was dragging/panning the map
759
+ if (this.isRecentlyDragged()) return;
760
+ if (e && e.originalEvent) {
761
+ this.L.DomEvent.stopPropagation(e);
762
+ }
763
+ this.onOverlayClick?.({
764
+ type: 'geojson',
765
+ data: feature,
766
+ });
767
+ });
768
+ },
769
+ }).addTo(this.geoJsonGroup);
770
+ } catch (err) {
771
+ console.error('[uni-leaflet] Error creating GeoJSON layer in H5:', err);
772
+ }
773
+ }
774
+
775
+ public setClusterOptions(options: ClusterOptions) {
776
+ this.clusterOptions = { ...this.clusterOptions, ...options };
777
+ if (this.rawMarkers.length > 0) {
778
+ this.renderMarkers();
779
+ }
273
780
  }
274
781
 
275
782
  public setMarkers(markers: MarkerOptions[]) {
276
- if (!this.map || !this.L || !this.overlayGroup) return;
277
-
278
- for (const m of markers) {
279
- let icon = undefined;
280
- const size = m.icon?.size || [32, 32];
281
- const anchor = m.icon?.anchor || [size[0] / 2, size[1]];
282
- const tooltipAnchor: [number, number] = [
283
- size[0] / 2 - anchor[0],
284
- -anchor[1],
285
- ];
783
+ this.rawMarkers = markers || [];
784
+ if (!this.map || !this.L || !this.markerLayerGroup) return;
785
+ this.renderMarkers();
786
+ }
286
787
 
287
- if (m.icon?.svg) {
288
- let svgStr = m.icon.svg.trim();
289
- if (!svgStr.includes('xmlns=')) {
290
- svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
291
- }
292
- const html = `<div style="width:${size[0]}px;height:${size[1]}px;display:flex;align-items:center;justify-content:center;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">${svgStr}</div>`;
293
- icon = this.L.divIcon({
294
- html,
295
- className: 'uni-custom-svg-icon',
296
- iconSize: size,
297
- iconAnchor: anchor,
298
- tooltipAnchor,
299
- });
300
- } else if (m.icon?.html) {
301
- icon = this.L.divIcon({
302
- html: m.icon.html,
303
- className: 'uni-custom-html-icon',
304
- iconSize: size,
305
- iconAnchor: anchor,
306
- tooltipAnchor,
307
- });
308
- } else if (m.icon?.url) {
309
- let finalUrl = m.icon.url;
310
- if (finalUrl && typeof finalUrl === 'string' && !finalUrl.startsWith('data:') && !finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
311
- if (finalUrl.startsWith('/')) {
312
- finalUrl = finalUrl.slice(1);
313
- }
314
- }
315
- const html = `<img src="${finalUrl}" style="width:${size[0]}px;height:${size[1]}px;display:block;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));" />`;
316
- icon = this.L.divIcon({
317
- className: 'uni-custom-img-icon',
318
- html,
319
- iconSize: size,
320
- iconAnchor: anchor,
321
- tooltipAnchor,
322
- });
323
- } else if (m.icon?.text) {
324
- const fontSize = Math.round(size[0] * 0.72);
325
- const html = `<div style="display:flex;align-items:center;justify-content:center;width:${size[0]}px;height:${size[1]}px;font-size:${fontSize}px;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">${m.icon.text}</div>`;
326
- icon = this.L.divIcon({
327
- html,
328
- className: 'uni-custom-icon',
329
- iconSize: size,
330
- iconAnchor: anchor,
331
- tooltipAnchor,
332
- });
788
+ private renderMarkers() {
789
+ if (!this.markerLayerGroup || !this.L) return;
790
+ this.markerLayerGroup.clearLayers();
791
+
792
+ if (!this.clusterOptions.enable) {
793
+ for (const m of this.rawMarkers) {
794
+ this.createSingleMarker(m);
795
+ }
796
+ return;
797
+ }
798
+
799
+ const currentZoom = this.getZoom();
800
+ const clusterResults = clusterMarkers(
801
+ this.rawMarkers,
802
+ currentZoom,
803
+ this.clusterOptions
804
+ );
805
+
806
+ for (const item of clusterResults) {
807
+ if (!item.isCluster) {
808
+ this.createSingleMarker(item.marker);
333
809
  } else {
334
- // Sleek default SVG Pin
335
- const pinColor = m.icon?.color || '#ef4444';
336
- const html = `
337
- <svg viewBox="0 0 24 32" width="${size[0]}" height="${size[1]}" style="filter:drop-shadow(0 2px 4px rgba(0,0,0,0.35));">
338
- <path d="M12 0C5.37 0 0 5.37 0 12c0 9 12 20 12 20s12-11 12-20c0-6.63-5.37-12-12-12z" fill="${pinColor}"/>
339
- <circle cx="12" cy="11" r="4.5" fill="#ffffff"/>
340
- </svg>`;
341
- icon = this.L.divIcon({
342
- html,
343
- className: 'uni-default-pin',
344
- iconSize: size,
345
- iconAnchor: anchor,
346
- tooltipAnchor,
347
- });
810
+ this.createClusterMarker(item.cluster);
348
811
  }
812
+ }
813
+ }
349
814
 
350
- const leafletMarker = this.L.marker([m.latLng[0], m.latLng[1]], {
351
- icon,
352
- title: m.title,
353
- }).addTo(this.overlayGroup);
354
-
355
- // Label tooltip
356
- if (m.label) {
357
- const labelText = typeof m.label === 'string' ? m.label : m.label.text;
358
- const customOffset =
359
- typeof m.label === 'object' && m.label.offset
360
- ? m.label.offset
361
- : undefined;
362
-
363
- let offset: [number, number] = [0, 0];
364
- if (customOffset) {
365
- const ox = customOffset[0] || 0;
366
- const oy = customOffset[1] || 0;
367
- if (Math.abs(oy) <= 10) {
368
- offset = [ox, oy];
369
- } else {
370
- offset = [ox, 0];
371
- }
815
+ private createSingleMarker(m: MarkerOptions) {
816
+ let icon = undefined;
817
+ const size = m.icon?.size || [32, 32];
818
+ const anchor = m.icon?.anchor || [size[0] / 2, size[1]];
819
+ const tooltipAnchor: [number, number] = [
820
+ size[0] / 2 - anchor[0],
821
+ -anchor[1],
822
+ ];
823
+
824
+ if (m.icon?.type === 'avatar') {
825
+ const avatarSize = m.icon.size || size || [32, 32];
826
+ const avatarW = avatarSize[0];
827
+ const avatarH = avatarSize[1];
828
+ const borderColor = m.icon.borderColor || '#3b82f6';
829
+ const borderWidth = m.icon.borderWidth ?? (avatarW <= 36 ? 2.5 : 3);
830
+ const pulseColor = m.icon.pulseColor || borderColor;
831
+ const badgeText = m.icon.badgeText;
832
+ const isCustomBg = !!m.icon.badgeColor;
833
+ const badgeBg = isCustomBg
834
+ ? `background:${m.icon.badgeColor} !important; color:#ffffff !important; border:1.5px solid #ffffff !important;`
835
+ : 'background:#ffffff !important; color:#1e293b !important; border:1px solid rgba(0,0,0,0.08) !important;';
836
+ const hasPulse = !!m.icon.pulse;
837
+
838
+ const arrowW = avatarW <= 36 ? 4 : 5;
839
+ const arrowH = avatarW <= 36 ? 5 : 6;
840
+ const badgeHeight = avatarW <= 36 ? 12 : Math.max(14, Math.round(avatarW * 0.32));
841
+ const badgeFontSize = avatarW <= 36 ? 8 : Math.max(9, Math.round(badgeHeight * 0.65));
842
+ const badgeOffsetTop = avatarW <= 36 ? -2 : -3;
843
+ const badgeOffsetRight = avatarW <= 36 ? -2 : -4;
844
+ const badgePadding = avatarW <= 36 ? '0 2px' : '0 3px';
845
+
846
+ let imgUrl = m.icon.url || '';
847
+ if (
848
+ imgUrl &&
849
+ typeof imgUrl === 'string' &&
850
+ !imgUrl.startsWith('data:') &&
851
+ !imgUrl.startsWith('http://') &&
852
+ !imgUrl.startsWith('https://')
853
+ ) {
854
+ if (imgUrl.startsWith('/')) {
855
+ imgUrl = imgUrl.slice(1);
372
856
  }
857
+ }
373
858
 
374
- leafletMarker.bindTooltip(labelText, {
375
- permanent: true,
376
- direction: 'top',
377
- offset,
378
- className: 'uni-overlay-label',
379
- });
859
+ const html = `
860
+ <div class="uni-avatar-marker-wrapper" style="position:relative;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;cursor:pointer;user-select:none;width:${avatarW}px;height:${avatarH + arrowH}px;">
861
+ ${hasPulse ? `<div class="uni-avatar-pulse" style="position:absolute;top:0;left:0;width:${avatarW}px !important;height:${avatarH}px !important;border-radius:50%;box-sizing:border-box;border:${borderWidth}px solid ${pulseColor};pointer-events:none;z-index:0;"></div>` : ''}
862
+ <div class="uni-avatar-bubble" style="position:relative;width:${avatarW}px !important;height:${avatarH}px !important;flex-shrink:0;border-radius:50%;background-color:#ffffff;box-shadow:0 4px 12px rgba(0,0,0,0.25);display:flex;align-items:center;justify-content:center;box-sizing:border-box;overflow:visible;border:${borderWidth}px solid ${borderColor};">
863
+ <img class="uni-avatar-img" src="${imgUrl}" style="width:100% !important;height:100% !important;border-radius:50%;object-fit:cover;display:block;pointer-events:none;" />
864
+ <div class="uni-avatar-arrow" style="position:absolute;bottom:-${arrowH}px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:${arrowW}px solid transparent;border-right:${arrowW}px solid transparent;border-top:${arrowH}px solid ${borderColor} !important;border-top-color:${borderColor} !important;z-index:1;filter:drop-shadow(0 2px 2px rgba(0,0,0,0.2));"></div>
865
+ </div>
866
+ ${badgeText ? `<div class="uni-avatar-badge" style="position:absolute;top:${badgeOffsetTop}px;right:${badgeOffsetRight}px;font-size:${badgeFontSize}px;font-weight:700;line-height:1;height:${badgeHeight}px;min-width:${badgeHeight}px;padding:${badgePadding};border-radius:${badgeHeight / 2}px;display:flex;align-items:center;justify-content:center;box-shadow:0 1px 3px rgba(0,0,0,0.25);box-sizing:border-box;white-space:nowrap;z-index:10;pointer-events:none;${badgeBg}">${badgeText}</div>` : ''}
867
+ </div>
868
+ `;
869
+
870
+ icon = this.L.divIcon({
871
+ html,
872
+ className: 'uni-avatar-marker-icon',
873
+ iconSize: [avatarW, avatarH + arrowH],
874
+ iconAnchor: m.icon.anchor || [avatarW / 2, avatarH + arrowH],
875
+ tooltipAnchor: [0, -(avatarH + arrowH + 2)],
876
+ });
877
+ } else if (m.icon?.svg) {
878
+ let svgStr = m.icon.svg.trim();
879
+ if (!svgStr.includes('xmlns=')) {
880
+ svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
881
+ }
882
+ const html = `<div style="width:${size[0]}px;height:${size[1]}px;display:flex;align-items:center;justify-content:center;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">${svgStr}</div>`;
883
+ icon = this.L.divIcon({
884
+ html,
885
+ className: 'uni-custom-svg-icon',
886
+ iconSize: size,
887
+ iconAnchor: anchor,
888
+ tooltipAnchor,
889
+ });
890
+ } else if (m.icon?.html) {
891
+ icon = this.L.divIcon({
892
+ html: m.icon.html,
893
+ className: 'uni-custom-html-icon',
894
+ iconSize: size,
895
+ iconAnchor: anchor,
896
+ tooltipAnchor,
897
+ });
898
+ } else if (m.icon?.url) {
899
+ let finalUrl = m.icon.url;
900
+ if (
901
+ finalUrl &&
902
+ typeof finalUrl === 'string' &&
903
+ !finalUrl.startsWith('data:') &&
904
+ !finalUrl.startsWith('http://') &&
905
+ !finalUrl.startsWith('https://')
906
+ ) {
907
+ if (finalUrl.startsWith('/')) {
908
+ finalUrl = finalUrl.slice(1);
909
+ }
910
+ }
911
+ const html = `<img src="${finalUrl}" style="width:${size[0]}px;height:${size[1]}px;display:block;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));" />`;
912
+ icon = this.L.divIcon({
913
+ className: 'uni-custom-img-icon',
914
+ html,
915
+ iconSize: size,
916
+ iconAnchor: anchor,
917
+ tooltipAnchor,
918
+ });
919
+ } else if (m.icon?.text) {
920
+ const fontSize = Math.round(size[0] * 0.72);
921
+ const html = `<div style="display:flex;align-items:center;justify-content:center;width:${size[0]}px;height:${size[1]}px;font-size:${fontSize}px;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">${m.icon.text}</div>`;
922
+ icon = this.L.divIcon({
923
+ html,
924
+ className: 'uni-custom-icon',
925
+ iconSize: size,
926
+ iconAnchor: anchor,
927
+ tooltipAnchor,
928
+ });
929
+ } else {
930
+ // Sleek default SVG Pin
931
+ const pinColor = m.icon?.color || '#ef4444';
932
+ const html = `
933
+ <svg viewBox="0 0 24 32" width="${size[0]}" height="${size[1]}" style="filter:drop-shadow(0 2px 4px rgba(0,0,0,0.35));">
934
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 9 12 20 12 20s12-11 12-20c0-6.63-5.37-12-12-12z" fill="${pinColor}"/>
935
+ <circle cx="12" cy="11" r="4.5" fill="#ffffff"/>
936
+ </svg>`;
937
+ icon = this.L.divIcon({
938
+ html,
939
+ className: 'uni-default-pin',
940
+ iconSize: size,
941
+ iconAnchor: anchor,
942
+ tooltipAnchor,
943
+ });
944
+ }
945
+
946
+ const leafletMarker = this.L.marker([m.latLng[0], m.latLng[1]], {
947
+ icon,
948
+ title: m.title,
949
+ }).addTo(this.markerLayerGroup);
950
+
951
+ // Label tooltip
952
+ if (m.label) {
953
+ const labelText = typeof m.label === 'string' ? m.label : m.label.text;
954
+ const customOffset =
955
+ typeof m.label === 'object' && m.label.offset
956
+ ? m.label.offset
957
+ : undefined;
958
+
959
+ let offset: [number, number] = [0, 0];
960
+ if (customOffset) {
961
+ const ox = customOffset[0] || 0;
962
+ const oy = customOffset[1] || 0;
963
+ if (Math.abs(oy) <= 10) {
964
+ offset = [ox, oy];
965
+ } else {
966
+ offset = [ox, 0];
967
+ }
380
968
  }
381
969
 
382
- leafletMarker.on('click', () => {
383
- this.onOverlayClick?.({ type: 'marker', data: m });
970
+ leafletMarker.bindTooltip(labelText, {
971
+ permanent: true,
972
+ direction: 'top',
973
+ offset,
974
+ className: 'uni-overlay-label',
384
975
  });
385
976
  }
977
+
978
+ leafletMarker.on('click', () => {
979
+ this.onOverlayClick?.({ type: 'marker', data: m });
980
+ });
981
+ }
982
+
983
+ private createClusterMarker(cluster: ClusterGroup) {
984
+ const rep = cluster.representativeMarker;
985
+ let iconContent = '';
986
+
987
+ if (rep?.icon?.url) {
988
+ let finalUrl = rep.icon.url;
989
+ if (
990
+ finalUrl &&
991
+ typeof finalUrl === 'string' &&
992
+ !finalUrl.startsWith('data:') &&
993
+ !finalUrl.startsWith('http://') &&
994
+ !finalUrl.startsWith('https://')
995
+ ) {
996
+ if (finalUrl.startsWith('/')) {
997
+ finalUrl = finalUrl.slice(1);
998
+ }
999
+ }
1000
+ iconContent = `<img src="${finalUrl}" style="width:100%;height:100%;border-radius:50%;object-fit:cover;" />`;
1001
+ } else if (rep?.icon?.text) {
1002
+ iconContent = `<span style="font-size:20px;line-height:1;">${rep.icon.text}</span>`;
1003
+ } else if (rep?.icon?.svg) {
1004
+ let svgStr = rep.icon.svg.trim();
1005
+ if (!svgStr.includes('xmlns=')) {
1006
+ svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
1007
+ }
1008
+ iconContent = svgStr;
1009
+ } else {
1010
+ iconContent = `<svg viewBox="0 0 24 24" width="22" height="22" fill="#3b82f6"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>`;
1011
+ }
1012
+
1013
+ const html = `
1014
+ <div class="uni-cluster-bubble">
1015
+ <div class="uni-cluster-icon">${iconContent}</div>
1016
+ <div class="uni-cluster-badge">+${cluster.count}</div>
1017
+ </div>
1018
+ `;
1019
+
1020
+ const icon = this.L.divIcon({
1021
+ html,
1022
+ className: 'uni-cluster-marker-icon',
1023
+ iconSize: [44, 44],
1024
+ iconAnchor: [22, 22],
1025
+ });
1026
+
1027
+ const clusterMarker = this.L.marker([cluster.center[0], cluster.center[1]], {
1028
+ icon,
1029
+ zIndexOffset: 1000,
1030
+ }).addTo(this.markerLayerGroup);
1031
+
1032
+ clusterMarker.on('click', (e: any) => {
1033
+ if (e && e.originalEvent) {
1034
+ e.originalEvent.stopPropagation?.();
1035
+ }
1036
+ const bounds = cluster.bounds;
1037
+ const isSameCoord =
1038
+ Math.abs(bounds[0][0] - bounds[1][0]) < 1e-6 &&
1039
+ Math.abs(bounds[0][1] - bounds[1][1]) < 1e-6;
1040
+
1041
+ if (isSameCoord) {
1042
+ this.setZoom(Math.min(this.getZoom() + 2, 19));
1043
+ this.setCenter(cluster.center, true);
1044
+ } else {
1045
+ this.fitBounds(bounds, { padding: [60, 60], animate: true });
1046
+ }
1047
+
1048
+ this.onClusterClick?.(cluster);
1049
+ });
386
1050
  }
387
1051
 
388
1052
  public setPolylines(polylines: PolylineOptions[]) {
389
- if (!this.map || !this.L || !this.overlayGroup) return;
1053
+ if (!this.map || !this.L) return;
1054
+
1055
+ if (!this.polylineGroup) {
1056
+ this.polylineGroup = this.L.layerGroup().addTo(this.map);
1057
+ }
1058
+ this.polylineGroup.clearLayers();
1059
+
1060
+ if (!polylines || !Array.isArray(polylines)) return;
390
1061
 
391
1062
  for (const p of polylines) {
392
- const leafletLine = this.L.polyline(p.latLngs, {
1063
+ if (!p.latLngs || p.latLngs.length < 2) continue;
1064
+
1065
+ const finalLatLngs = p.curved
1066
+ ? generateCurvedPolyline(p.latLngs, p.curvature ?? 0.25)
1067
+ : p.latLngs;
1068
+
1069
+ const isFlowing = !!p.flowing;
1070
+ const dashStr = p.dashArray
1071
+ ? p.dashArray.join(',')
1072
+ : isFlowing
1073
+ ? '12,12'
1074
+ : undefined;
1075
+
1076
+ const leafletLine = this.L.polyline(finalLatLngs, {
393
1077
  color: p.color || '#3b82f6',
394
1078
  weight: p.width || 4,
395
1079
  opacity: p.opacity ?? 1,
396
- dashArray: p.dashArray?.join(','),
397
- }).addTo(this.overlayGroup);
1080
+ dashArray: dashStr,
1081
+ className: isFlowing ? 'uni-flowing-polyline' : '',
1082
+ }).addTo(this.polylineGroup);
1083
+
1084
+ if (isFlowing) {
1085
+ const pathEl = leafletLine.getElement?.() || (leafletLine as any)._path;
1086
+ if (pathEl) {
1087
+ const speed = p.flowSpeed || 1;
1088
+ const duration = Math.max(0.2, 1.2 / Math.abs(speed));
1089
+ pathEl.style.animationDuration = `${duration}s`;
1090
+ if (speed < 0) {
1091
+ pathEl.style.animationDirection = 'reverse';
1092
+ }
1093
+ }
1094
+ }
398
1095
 
399
1096
  if (p.label) {
400
1097
  const labelText = typeof p.label === 'string' ? p.label : p.label.text;
@@ -405,22 +1102,35 @@ export class H5MapAdapter implements IMapEngine {
405
1102
  });
406
1103
  }
407
1104
 
408
- leafletLine.on('click', () => {
1105
+ leafletLine.on('click', (e: any) => {
1106
+ if (this.isRecentlyDragged()) return;
1107
+ if (e && e.originalEvent) {
1108
+ this.L.DomEvent.stopPropagation(e);
1109
+ }
409
1110
  this.onOverlayClick?.({ type: 'polyline', data: p });
410
1111
  });
411
1112
  }
412
1113
  }
413
1114
 
414
1115
  public setPolygons(polygons: PolygonOptions[]) {
415
- if (!this.map || !this.L || !this.overlayGroup) return;
1116
+ if (!this.map || !this.L) return;
1117
+
1118
+ if (!this.polygonGroup) {
1119
+ this.polygonGroup = this.L.layerGroup().addTo(this.map);
1120
+ }
1121
+ this.polygonGroup.clearLayers();
1122
+
1123
+ if (!polygons || !Array.isArray(polygons)) return;
416
1124
 
417
1125
  for (const poly of polygons) {
1126
+ if (!poly.latLngs || poly.latLngs.length < 3) continue;
1127
+
418
1128
  const leafletPolygon = this.L.polygon(poly.latLngs, {
419
1129
  color: poly.color || '#ef4444',
420
1130
  weight: poly.width || 2,
421
1131
  fillColor: poly.fillColor || poly.color || '#ef4444',
422
1132
  fillOpacity: poly.fillOpacity ?? 0.25,
423
- }).addTo(this.overlayGroup);
1133
+ }).addTo(this.polygonGroup);
424
1134
 
425
1135
  if (poly.label) {
426
1136
  const labelText =
@@ -432,14 +1142,25 @@ export class H5MapAdapter implements IMapEngine {
432
1142
  });
433
1143
  }
434
1144
 
435
- leafletPolygon.on('click', () => {
1145
+ leafletPolygon.on('click', (e: any) => {
1146
+ if (this.isRecentlyDragged()) return;
1147
+ if (e && e.originalEvent) {
1148
+ this.L.DomEvent.stopPropagation(e);
1149
+ }
436
1150
  this.onOverlayClick?.({ type: 'polygon', data: poly });
437
1151
  });
438
1152
  }
439
1153
  }
440
1154
 
441
1155
  public setCircles(circles: CircleOptions[]) {
442
- if (!this.map || !this.L || !this.overlayGroup) return;
1156
+ if (!this.map || !this.L) return;
1157
+
1158
+ if (!this.circleGroup) {
1159
+ this.circleGroup = this.L.layerGroup().addTo(this.map);
1160
+ }
1161
+ this.circleGroup.clearLayers();
1162
+
1163
+ if (!circles || !Array.isArray(circles)) return;
443
1164
 
444
1165
  for (const c of circles) {
445
1166
  const leafletCircle = this.L.circle([c.latLng[0], c.latLng[1]], {
@@ -448,7 +1169,7 @@ export class H5MapAdapter implements IMapEngine {
448
1169
  weight: c.width || 2,
449
1170
  fillColor: c.fillColor || c.color || '#10b981',
450
1171
  fillOpacity: c.fillOpacity ?? 0.2,
451
- }).addTo(this.overlayGroup);
1172
+ }).addTo(this.circleGroup);
452
1173
 
453
1174
  if (c.label) {
454
1175
  const labelText = typeof c.label === 'string' ? c.label : c.label.text;
@@ -459,16 +1180,38 @@ export class H5MapAdapter implements IMapEngine {
459
1180
  });
460
1181
  }
461
1182
 
462
- leafletCircle.on('click', () => {
1183
+ leafletCircle.on('click', (e: any) => {
1184
+ if (this.isRecentlyDragged()) return;
1185
+ if (e && e.originalEvent) {
1186
+ this.L.DomEvent.stopPropagation(e);
1187
+ }
463
1188
  this.onOverlayClick?.({ type: 'circle', data: c });
464
1189
  });
465
1190
  }
466
1191
  }
467
1192
 
468
1193
  public clearOverlays() {
1194
+ if (this.polygonGroup) {
1195
+ this.polygonGroup.clearLayers();
1196
+ }
1197
+ if (this.polylineGroup) {
1198
+ this.polylineGroup.clearLayers();
1199
+ }
1200
+ if (this.circleGroup) {
1201
+ this.circleGroup.clearLayers();
1202
+ }
469
1203
  if (this.overlayGroup) {
470
1204
  this.overlayGroup.clearLayers();
471
1205
  }
1206
+ if (this.markerLayerGroup) {
1207
+ this.markerLayerGroup.clearLayers();
1208
+ }
1209
+ if (this.geoJsonGroup) {
1210
+ this.geoJsonGroup.clearLayers();
1211
+ }
1212
+ this.rawMarkers = [];
1213
+ this.geoJsonLayer = null;
1214
+ this.currentGeoJsonData = null;
472
1215
  }
473
1216
 
474
1217
  public getCenter(): LatLngTuple {
@@ -495,10 +1238,28 @@ export class H5MapAdapter implements IMapEngine {
495
1238
 
496
1239
  public destroy() {
497
1240
  if (this.map) {
1241
+ if (typeof window !== 'undefined' && (window as any).__activeMap === this.map) {
1242
+ delete (window as any).__activeMap;
1243
+ }
498
1244
  this.map.remove();
499
1245
  this.map = null;
500
1246
  this.tileLayers = [];
501
1247
  this.overlayGroup = null;
1248
+ this.polygonGroup = null;
1249
+ this.polylineGroup = null;
1250
+ this.circleGroup = null;
1251
+ this.geoJsonGroup = null;
1252
+ if (this.markerLayerGroup) {
1253
+ this.markerLayerGroup.clearLayers();
1254
+ this.markerLayerGroup = null;
1255
+ }
1256
+ this.rawMarkers = [];
1257
+ this.geoJsonLayer = null;
1258
+ this.currentGeoJsonData = null;
1259
+ }
1260
+ if (this.releaseWatchdogHandler) {
1261
+ this.releaseWatchdogHandler();
1262
+ this.releaseWatchdogHandler = undefined;
502
1263
  }
503
1264
  }
504
1265
  }