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.
- package/README.md +304 -5
- package/UniLeafletMap.vue +433 -65
- package/app-render.js +897 -95
- package/engine/canvas-tile-engine.ts +694 -79
- package/engine/factory.ts +15 -0
- package/engine/h5-map-adapter.ts +894 -133
- package/index.ts +4 -0
- package/package.json +1 -1
- package/types.ts +95 -2
- package/utils/bounds.ts +264 -0
- package/utils/cluster.ts +158 -0
- package/utils/curve.ts +86 -0
- package/utils/geojson.ts +175 -0
- package/uni-leaflet-1.0.0.tgz +0 -0
package/engine/h5-map-adapter.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
707
|
+
if (!this.map || !this.L) {
|
|
255
708
|
this.pendingOverlays = overlays;
|
|
256
709
|
return;
|
|
257
710
|
}
|
|
258
711
|
|
|
259
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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.
|
|
383
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
397
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
}
|