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/app-render.js
CHANGED
|
@@ -161,20 +161,494 @@ function ensureLeafletStyles() {
|
|
|
161
161
|
.uni-custom-html-icon,
|
|
162
162
|
.uni-custom-img-icon,
|
|
163
163
|
.uni-custom-icon,
|
|
164
|
-
.uni-default-pin
|
|
164
|
+
.uni-default-pin,
|
|
165
|
+
.uni-cluster-marker-icon {
|
|
165
166
|
background: transparent !important;
|
|
166
167
|
border: none !important;
|
|
167
168
|
}
|
|
169
|
+
.uni-cluster-bubble {
|
|
170
|
+
position: relative !important;
|
|
171
|
+
width: 44px !important;
|
|
172
|
+
height: 44px !important;
|
|
173
|
+
border-radius: 50% !important;
|
|
174
|
+
background: #ffffff !important;
|
|
175
|
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22), 0 0 0 3px #3b82f6 !important;
|
|
176
|
+
display: flex !important;
|
|
177
|
+
align-items: center !important;
|
|
178
|
+
justify-content: center !important;
|
|
179
|
+
cursor: pointer !important;
|
|
180
|
+
user-select: none !important;
|
|
181
|
+
box-sizing: border-box !important;
|
|
182
|
+
}
|
|
183
|
+
.uni-cluster-bubble .uni-cluster-icon {
|
|
184
|
+
width: 26px !important;
|
|
185
|
+
height: 26px !important;
|
|
186
|
+
display: flex !important;
|
|
187
|
+
align-items: center !important;
|
|
188
|
+
justify-content: center !important;
|
|
189
|
+
border-radius: 50% !important;
|
|
190
|
+
overflow: hidden !important;
|
|
191
|
+
}
|
|
192
|
+
.uni-cluster-bubble .uni-cluster-badge {
|
|
193
|
+
position: absolute !important;
|
|
194
|
+
top: -6px !important;
|
|
195
|
+
right: -8px !important;
|
|
196
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
|
|
197
|
+
color: #ffffff !important;
|
|
198
|
+
font-size: 11px !important;
|
|
199
|
+
font-weight: 700 !important;
|
|
200
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
|
|
201
|
+
height: 18px !important;
|
|
202
|
+
min-width: 18px !important;
|
|
203
|
+
padding: 0 5px !important;
|
|
204
|
+
border-radius: 10px !important;
|
|
205
|
+
display: flex !important;
|
|
206
|
+
align-items: center !important;
|
|
207
|
+
justify-content: center !important;
|
|
208
|
+
border: 2px solid #ffffff !important;
|
|
209
|
+
box-shadow: 0 2px 6px rgba(220, 38, 38, 0.45) !important;
|
|
210
|
+
box-sizing: border-box !important;
|
|
211
|
+
white-space: nowrap !important;
|
|
212
|
+
pointer-events: none !important;
|
|
213
|
+
}
|
|
214
|
+
@keyframes uni-polyline-flowing {
|
|
215
|
+
from {
|
|
216
|
+
stroke-dashoffset: 40px;
|
|
217
|
+
}
|
|
218
|
+
to {
|
|
219
|
+
stroke-dashoffset: 0px;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
.uni-flowing-polyline {
|
|
223
|
+
animation: uni-polyline-flowing 1.2s linear infinite !important;
|
|
224
|
+
stroke-linecap: round !important;
|
|
225
|
+
stroke-linejoin: round !important;
|
|
226
|
+
}
|
|
227
|
+
.uni-avatar-marker-icon {
|
|
228
|
+
background: transparent !important;
|
|
229
|
+
border: none !important;
|
|
230
|
+
}
|
|
231
|
+
.uni-avatar-marker-wrapper {
|
|
232
|
+
position: relative !important;
|
|
233
|
+
display: flex !important;
|
|
234
|
+
flex-direction: column !important;
|
|
235
|
+
align-items: center !important;
|
|
236
|
+
justify-content: flex-start !important;
|
|
237
|
+
cursor: pointer !important;
|
|
238
|
+
user-select: none !important;
|
|
239
|
+
}
|
|
240
|
+
.uni-avatar-bubble {
|
|
241
|
+
position: relative !important;
|
|
242
|
+
border-radius: 50% !important;
|
|
243
|
+
background-color: #ffffff !important;
|
|
244
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) !important;
|
|
245
|
+
display: flex !important;
|
|
246
|
+
align-items: center !important;
|
|
247
|
+
justify-content: center !important;
|
|
248
|
+
box-sizing: border-box !important;
|
|
249
|
+
overflow: visible !important;
|
|
250
|
+
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
|
251
|
+
}
|
|
252
|
+
.uni-avatar-img {
|
|
253
|
+
width: 100% !important;
|
|
254
|
+
height: 100% !important;
|
|
255
|
+
border-radius: 50% !important;
|
|
256
|
+
object-fit: cover !important;
|
|
257
|
+
display: block !important;
|
|
258
|
+
pointer-events: none !important;
|
|
259
|
+
}
|
|
260
|
+
.uni-avatar-arrow {
|
|
261
|
+
position: absolute !important;
|
|
262
|
+
bottom: -6px !important;
|
|
263
|
+
left: 50% !important;
|
|
264
|
+
transform: translateX(-50%) !important;
|
|
265
|
+
width: 0 !important;
|
|
266
|
+
height: 0 !important;
|
|
267
|
+
border-left: 5px solid transparent !important;
|
|
268
|
+
border-right: 5px solid transparent !important;
|
|
269
|
+
border-top-width: 6px !important;
|
|
270
|
+
border-top-style: solid !important;
|
|
271
|
+
z-index: 1 !important;
|
|
272
|
+
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.2)) !important;
|
|
273
|
+
}
|
|
274
|
+
.uni-avatar-badge {
|
|
275
|
+
position: absolute !important;
|
|
276
|
+
top: -2px !important;
|
|
277
|
+
right: -2px !important;
|
|
278
|
+
background: #ffffff !important;
|
|
279
|
+
color: #1e293b !important;
|
|
280
|
+
font-size: 8px !important;
|
|
281
|
+
font-weight: 700 !important;
|
|
282
|
+
line-height: 1 !important;
|
|
283
|
+
height: 12px !important;
|
|
284
|
+
min-width: 12px !important;
|
|
285
|
+
padding: 0 2px !important;
|
|
286
|
+
border-radius: 6px !important;
|
|
287
|
+
display: flex !important;
|
|
288
|
+
align-items: center !important;
|
|
289
|
+
justify-content: center !important;
|
|
290
|
+
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
|
291
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
|
|
292
|
+
box-sizing: border-box !important;
|
|
293
|
+
white-space: nowrap !important;
|
|
294
|
+
z-index: 10 !important;
|
|
295
|
+
pointer-events: none !important;
|
|
296
|
+
}
|
|
297
|
+
@keyframes uni-avatar-pulse {
|
|
298
|
+
0% {
|
|
299
|
+
transform: scale(1);
|
|
300
|
+
opacity: 0.85;
|
|
301
|
+
}
|
|
302
|
+
70% {
|
|
303
|
+
transform: scale(1.6);
|
|
304
|
+
opacity: 0;
|
|
305
|
+
}
|
|
306
|
+
100% {
|
|
307
|
+
transform: scale(1.6);
|
|
308
|
+
opacity: 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
.uni-avatar-pulse {
|
|
312
|
+
position: absolute !important;
|
|
313
|
+
top: 0 !important;
|
|
314
|
+
left: 0 !important;
|
|
315
|
+
width: 100% !important;
|
|
316
|
+
height: 100% !important;
|
|
317
|
+
border-radius: 50% !important;
|
|
318
|
+
box-sizing: border-box !important;
|
|
319
|
+
border-style: solid !important;
|
|
320
|
+
border-width: 3px !important;
|
|
321
|
+
animation: uni-avatar-pulse 1.8s cubic-bezier(0.24, 0, 0.38, 1) infinite !important;
|
|
322
|
+
pointer-events: none !important;
|
|
323
|
+
z-index: 0 !important;
|
|
324
|
+
}
|
|
168
325
|
`;
|
|
169
326
|
document.head.appendChild(style);
|
|
170
327
|
}
|
|
171
328
|
|
|
329
|
+
function generateCurvedPolyline(latLngs, curvature, segments) {
|
|
330
|
+
if (!latLngs || latLngs.length < 2) return latLngs || [];
|
|
331
|
+
const curv = curvature !== undefined ? curvature : 0.25;
|
|
332
|
+
if (Math.abs(curv) < 1e-4) return latLngs;
|
|
333
|
+
|
|
334
|
+
function arc(p1, p2) {
|
|
335
|
+
const lat1 = p1[0], lng1 = p1[1];
|
|
336
|
+
const lat2 = p2[0], lng2 = p2[1];
|
|
337
|
+
const dLat = lat2 - lat1;
|
|
338
|
+
const dLng = lng2 - lng1;
|
|
339
|
+
const dist = Math.hypot(dLat, dLng);
|
|
340
|
+
if (dist < 1e-7) return [p1, p2];
|
|
341
|
+
const midLat = (lat1 + lat2) / 2;
|
|
342
|
+
const midLng = (lng1 + lng2) / 2;
|
|
343
|
+
const normLat = -dLng / dist;
|
|
344
|
+
const normLng = dLat / dist;
|
|
345
|
+
const ctrlLat = midLat + normLat * dist * curv;
|
|
346
|
+
const ctrlLng = midLng + normLng * dist * curv;
|
|
347
|
+
const pts = [];
|
|
348
|
+
const steps = Math.max(12, segments || 36);
|
|
349
|
+
for (let i = 0; i <= steps; i++) {
|
|
350
|
+
const t = i / steps;
|
|
351
|
+
const invT = 1 - t;
|
|
352
|
+
const lat = invT * invT * lat1 + 2 * invT * t * ctrlLat + t * t * lat2;
|
|
353
|
+
const lng = invT * invT * lng1 + 2 * invT * t * ctrlLng + t * t * lng2;
|
|
354
|
+
pts.push([lat, lng]);
|
|
355
|
+
}
|
|
356
|
+
return pts;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (latLngs.length === 2) return arc(latLngs[0], latLngs[1]);
|
|
360
|
+
|
|
361
|
+
const result = [];
|
|
362
|
+
for (let i = 0; i < latLngs.length - 1; i++) {
|
|
363
|
+
const a = arc(latLngs[i], latLngs[i + 1]);
|
|
364
|
+
if (i > 0) a.shift();
|
|
365
|
+
result.push.apply(result, a);
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function extractGeoJsonLatLngs(geojson) {
|
|
371
|
+
if (!geojson) return [];
|
|
372
|
+
const result = [];
|
|
373
|
+
function parseCoordinates(coords) {
|
|
374
|
+
if (!coords || !Array.isArray(coords)) return;
|
|
375
|
+
if (coords.length >= 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') {
|
|
376
|
+
result.push([coords[1], coords[0]]);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
for (var i = 0; i < coords.length; i++) {
|
|
380
|
+
parseCoordinates(coords[i]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function parseGeometry(geom) {
|
|
384
|
+
if (!geom || !geom.type) return;
|
|
385
|
+
if (geom.type === 'GeometryCollection' && Array.isArray(geom.geometries)) {
|
|
386
|
+
for (var i = 0; i < geom.geometries.length; i++) {
|
|
387
|
+
parseGeometry(geom.geometries[i]);
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (geom.coordinates) parseCoordinates(geom.coordinates);
|
|
392
|
+
}
|
|
393
|
+
function parseFeature(f) {
|
|
394
|
+
if (!f) return;
|
|
395
|
+
if (f.geometry) parseGeometry(f.geometry);
|
|
396
|
+
}
|
|
397
|
+
if (geojson.type === 'FeatureCollection' && Array.isArray(geojson.features)) {
|
|
398
|
+
for (var i = 0; i < geojson.features.length; i++) parseFeature(geojson.features[i]);
|
|
399
|
+
} else if (geojson.type === 'Feature') {
|
|
400
|
+
parseFeature(geojson);
|
|
401
|
+
} else if (geojson.type) {
|
|
402
|
+
parseGeometry(geojson);
|
|
403
|
+
} else if (Array.isArray(geojson)) {
|
|
404
|
+
for (var i = 0; i < geojson.length; i++) {
|
|
405
|
+
result.push.apply(result, extractGeoJsonLatLngs(geojson[i]));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function getGeoJsonFingerprint(data) {
|
|
412
|
+
if (!data) return 'null';
|
|
413
|
+
try {
|
|
414
|
+
return JSON.stringify(data);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
var str = (data.type || '') + '_' + (data.features ? data.features.length : '');
|
|
417
|
+
if (Array.isArray(data.features)) {
|
|
418
|
+
for (var i = 0; i < data.features.length; i++) {
|
|
419
|
+
var f = data.features[i];
|
|
420
|
+
if (f) {
|
|
421
|
+
str += '_' + (f.id || '') + '_' + (f.properties ? f.properties.name || f.properties.adcode || '' : '');
|
|
422
|
+
if (f.geometry && f.geometry.coordinates) {
|
|
423
|
+
str += '_' + JSON.stringify(f.geometry.coordinates);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return str;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function extractLatLngs(bounds) {
|
|
433
|
+
if (!bounds) return [];
|
|
434
|
+
if (bounds.type && (bounds.features || bounds.geometry || bounds.coordinates || bounds.geometries)) {
|
|
435
|
+
return extractGeoJsonLatLngs(bounds);
|
|
436
|
+
}
|
|
437
|
+
if (Array.isArray(bounds) && bounds.length === 2 && typeof bounds[0] === 'number' && typeof bounds[1] === 'number') {
|
|
438
|
+
return [[bounds[0], bounds[1]]];
|
|
439
|
+
}
|
|
440
|
+
if (Array.isArray(bounds)) {
|
|
441
|
+
const list = [];
|
|
442
|
+
for (const item of bounds) {
|
|
443
|
+
if (!item) continue;
|
|
444
|
+
if (Array.isArray(item) && item.length >= 2 && typeof item[0] === 'number' && typeof item[1] === 'number') {
|
|
445
|
+
list.push([item[0], item[1]]);
|
|
446
|
+
} else if (typeof item.lat === 'number' && typeof item.lng === 'number') {
|
|
447
|
+
list.push([item.lat, item.lng]);
|
|
448
|
+
} else if (Array.isArray(item.latLng) && item.latLng.length >= 2) {
|
|
449
|
+
list.push([item.latLng[0], item.latLng[1]]);
|
|
450
|
+
} else if (Array.isArray(item.latLngs)) {
|
|
451
|
+
const pts = item.curved ? generateCurvedPolyline(item.latLngs, item.curvature) : item.latLngs;
|
|
452
|
+
for (var i = 0; i < pts.length; i++) {
|
|
453
|
+
var pt = pts[i];
|
|
454
|
+
if (Array.isArray(pt) && pt.length >= 2 && typeof pt[0] === 'number' && typeof pt[1] === 'number') {
|
|
455
|
+
list.push([pt[0], pt[1]]);
|
|
456
|
+
} else if (pt && typeof pt.lat === 'number' && typeof pt.lng === 'number') {
|
|
457
|
+
list.push([pt.lat, pt.lng]);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else if (item.type && (item.features || item.geometry || item.coordinates)) {
|
|
461
|
+
list.push.apply(list, extractGeoJsonLatLngs(item));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return list;
|
|
465
|
+
}
|
|
466
|
+
if (bounds.markers || bounds.polylines || bounds.polygons || bounds.circles || bounds.geojson) {
|
|
467
|
+
var list = [];
|
|
468
|
+
if (Array.isArray(bounds.markers)) list = list.concat(extractLatLngs(bounds.markers));
|
|
469
|
+
if (Array.isArray(bounds.polylines)) list = list.concat(extractLatLngs(bounds.polylines));
|
|
470
|
+
if (Array.isArray(bounds.polygons)) list = list.concat(extractLatLngs(bounds.polygons));
|
|
471
|
+
if (Array.isArray(bounds.circles)) list = list.concat(extractLatLngs(bounds.circles));
|
|
472
|
+
if (bounds.geojson) list = list.concat(extractGeoJsonLatLngs(bounds.geojson));
|
|
473
|
+
return list;
|
|
474
|
+
}
|
|
475
|
+
if (typeof bounds.getSouthWest === 'function' && typeof bounds.getNorthEast === 'function') {
|
|
476
|
+
const sw = bounds.getSouthWest();
|
|
477
|
+
const ne = bounds.getNorthEast();
|
|
478
|
+
return [[sw.lat, sw.lng], [ne.lat, ne.lng]];
|
|
479
|
+
}
|
|
480
|
+
if (typeof bounds.minLat === 'number' && typeof bounds.maxLat === 'number') {
|
|
481
|
+
return [[bounds.minLat, bounds.minLng], [bounds.maxLat, bounds.maxLng]];
|
|
482
|
+
}
|
|
483
|
+
if (bounds.southWest && bounds.northEast) {
|
|
484
|
+
const sw = bounds.southWest;
|
|
485
|
+
const ne = bounds.northEast;
|
|
486
|
+
return [
|
|
487
|
+
[Array.isArray(sw) ? sw[0] : sw.lat, Array.isArray(sw) ? sw[1] : sw.lng],
|
|
488
|
+
[Array.isArray(ne) ? ne[0] : ne.lat, Array.isArray(ne) ? ne[1] : ne.lng],
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function latLngToWorldPixel(lat, lng, zoom, tileSize) {
|
|
495
|
+
tileSize = tileSize || 256;
|
|
496
|
+
const sinLat = Math.sin((lat * Math.PI) / 180);
|
|
497
|
+
const clampedSin = Math.max(-0.9999, Math.min(0.9999, sinLat));
|
|
498
|
+
const scale = tileSize * Math.pow(2, zoom);
|
|
499
|
+
const x = ((lng + 180) / 360) * scale;
|
|
500
|
+
const y =
|
|
501
|
+
(0.5 - Math.log((1 + clampedSin) / (1 - clampedSin)) / (4 * Math.PI)) *
|
|
502
|
+
scale;
|
|
503
|
+
return { x: x, y: y };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function clusterMarkers(markers, zoom, options) {
|
|
507
|
+
if (!markers || markers.length === 0) return [];
|
|
508
|
+
const radius = options && options.clusterRadius ? options.clusterRadius : 60;
|
|
509
|
+
const maxZoom =
|
|
510
|
+
options && options.clusterMaxZoom !== undefined
|
|
511
|
+
? options.clusterMaxZoom
|
|
512
|
+
: 17;
|
|
513
|
+
const tileSize = (options && options.tileSize) || 256;
|
|
514
|
+
|
|
515
|
+
if (zoom >= maxZoom) {
|
|
516
|
+
return markers.map(function (m) {
|
|
517
|
+
return { isCluster: false, marker: m };
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const pixelMarkers = [];
|
|
522
|
+
for (let i = 0; i < markers.length; i++) {
|
|
523
|
+
const marker = markers[i];
|
|
524
|
+
if (!marker || !marker.latLng) continue;
|
|
525
|
+
const lat = marker.latLng[0];
|
|
526
|
+
const lng = marker.latLng[1];
|
|
527
|
+
if (
|
|
528
|
+
typeof lat !== 'number' ||
|
|
529
|
+
typeof lng !== 'number' ||
|
|
530
|
+
isNaN(lat) ||
|
|
531
|
+
isNaN(lng)
|
|
532
|
+
)
|
|
533
|
+
continue;
|
|
534
|
+
const pt = latLngToWorldPixel(lat, lng, zoom, tileSize);
|
|
535
|
+
pixelMarkers.push({ marker: marker, x: pt.x, y: pt.y, assigned: false });
|
|
536
|
+
}
|
|
537
|
+
if (pixelMarkers.length === 0) return [];
|
|
538
|
+
|
|
539
|
+
const grid = {};
|
|
540
|
+
const cellSize = radius;
|
|
541
|
+
for (let i = 0; i < pixelMarkers.length; i++) {
|
|
542
|
+
const pm = pixelMarkers[i];
|
|
543
|
+
const gx = Math.floor(pm.x / cellSize);
|
|
544
|
+
const gy = Math.floor(pm.y / cellSize);
|
|
545
|
+
const key = gx + '_' + gy;
|
|
546
|
+
if (!grid[key]) grid[key] = [];
|
|
547
|
+
grid[key].push(pm);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const results = [];
|
|
551
|
+
const radiusSq = radius * radius;
|
|
552
|
+
|
|
553
|
+
for (let i = 0; i < pixelMarkers.length; i++) {
|
|
554
|
+
const pm = pixelMarkers[i];
|
|
555
|
+
if (pm.assigned) continue;
|
|
556
|
+
pm.assigned = true;
|
|
557
|
+
const clusterItems = [pm];
|
|
558
|
+
|
|
559
|
+
const gx = Math.floor(pm.x / cellSize);
|
|
560
|
+
const gy = Math.floor(pm.y / cellSize);
|
|
561
|
+
|
|
562
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
563
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
564
|
+
const neighborKey = gx + dx + '_' + (gy + dy);
|
|
565
|
+
const neighborCell = grid[neighborKey];
|
|
566
|
+
if (!neighborCell) continue;
|
|
567
|
+
for (let k = 0; k < neighborCell.length; k++) {
|
|
568
|
+
const neighbor = neighborCell[k];
|
|
569
|
+
if (neighbor.assigned) continue;
|
|
570
|
+
const distSq =
|
|
571
|
+
(neighbor.x - pm.x) * (neighbor.x - pm.x) +
|
|
572
|
+
(neighbor.y - pm.y) * (neighbor.y - pm.y);
|
|
573
|
+
if (distSq <= radiusSq) {
|
|
574
|
+
neighbor.assigned = true;
|
|
575
|
+
clusterItems.push(neighbor);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (clusterItems.length === 1) {
|
|
582
|
+
results.push({ isCluster: false, marker: clusterItems[0].marker });
|
|
583
|
+
} else {
|
|
584
|
+
let sumLat = 0,
|
|
585
|
+
sumLng = 0;
|
|
586
|
+
let minLat = Infinity,
|
|
587
|
+
maxLat = -Infinity,
|
|
588
|
+
minLng = Infinity,
|
|
589
|
+
maxLng = -Infinity;
|
|
590
|
+
const rawMarkers = [];
|
|
591
|
+
for (let j = 0; j < clusterItems.length; j++) {
|
|
592
|
+
const item = clusterItems[j];
|
|
593
|
+
const lat = item.marker.latLng[0];
|
|
594
|
+
const lng = item.marker.latLng[1];
|
|
595
|
+
sumLat += lat;
|
|
596
|
+
sumLng += lng;
|
|
597
|
+
if (lat < minLat) minLat = lat;
|
|
598
|
+
if (lat > maxLat) maxLat = lat;
|
|
599
|
+
if (lng < minLng) minLng = lng;
|
|
600
|
+
if (lng > maxLng) maxLng = lng;
|
|
601
|
+
rawMarkers.push(item.marker);
|
|
602
|
+
}
|
|
603
|
+
const count = clusterItems.length;
|
|
604
|
+
const centerLat = sumLat / count;
|
|
605
|
+
const centerLng = sumLng / count;
|
|
606
|
+
let rep = null;
|
|
607
|
+
for (let j = 0; j < rawMarkers.length; j++) {
|
|
608
|
+
const rm = rawMarkers[j];
|
|
609
|
+
if (rm.icon && (rm.icon.url || rm.icon.svg || rm.icon.text)) {
|
|
610
|
+
rep = rm;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (!rep) rep = rawMarkers[0];
|
|
615
|
+
|
|
616
|
+
results.push({
|
|
617
|
+
isCluster: true,
|
|
618
|
+
cluster: {
|
|
619
|
+
id:
|
|
620
|
+
'cluster_' +
|
|
621
|
+
zoom +
|
|
622
|
+
'_' +
|
|
623
|
+
centerLat.toFixed(5) +
|
|
624
|
+
'_' +
|
|
625
|
+
centerLng.toFixed(5) +
|
|
626
|
+
'_' +
|
|
627
|
+
count,
|
|
628
|
+
count: count,
|
|
629
|
+
center: [centerLat, centerLng],
|
|
630
|
+
bounds: [
|
|
631
|
+
[minLat, minLng],
|
|
632
|
+
[maxLat, maxLng],
|
|
633
|
+
],
|
|
634
|
+
markers: rawMarkers,
|
|
635
|
+
representativeMarker: rep,
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return results;
|
|
641
|
+
}
|
|
642
|
+
|
|
172
643
|
export default {
|
|
173
644
|
data() {
|
|
174
645
|
return {
|
|
175
646
|
map: null,
|
|
176
647
|
tileLayers: [],
|
|
177
648
|
overlayGroup: null,
|
|
649
|
+
markerLayerGroup: null,
|
|
650
|
+
rawMarkers: [],
|
|
651
|
+
clusterOptions: { enable: false, clusterRadius: 60, clusterMaxZoom: 17 },
|
|
178
652
|
isReady: false,
|
|
179
653
|
cachedProps: null,
|
|
180
654
|
isUserInteracting: false,
|
|
@@ -183,6 +657,9 @@ export default {
|
|
|
183
657
|
moveThrottleTimer: null,
|
|
184
658
|
currentLayersKey: '',
|
|
185
659
|
currentOverlaysKey: '',
|
|
660
|
+
currentGeoJsonKey: '',
|
|
661
|
+
geoJsonLayer: null,
|
|
662
|
+
lastFitBoundsTimestamp: 0,
|
|
186
663
|
};
|
|
187
664
|
},
|
|
188
665
|
mounted() {
|
|
@@ -194,7 +671,17 @@ export default {
|
|
|
194
671
|
clearTimeout(this.moveThrottleTimer);
|
|
195
672
|
this.moveThrottleTimer = null;
|
|
196
673
|
}
|
|
674
|
+
if (this.markerLayerGroup) {
|
|
675
|
+
this.markerLayerGroup.clearLayers();
|
|
676
|
+
this.markerLayerGroup = null;
|
|
677
|
+
}
|
|
197
678
|
if (this.map) {
|
|
679
|
+
if (this.geoJsonLayer) {
|
|
680
|
+
try {
|
|
681
|
+
this.map.removeLayer(this.geoJsonLayer);
|
|
682
|
+
} catch (e) {}
|
|
683
|
+
this.geoJsonLayer = null;
|
|
684
|
+
}
|
|
198
685
|
this.map.remove();
|
|
199
686
|
this.map = null;
|
|
200
687
|
}
|
|
@@ -243,6 +730,7 @@ export default {
|
|
|
243
730
|
} catch (e) {}
|
|
244
731
|
|
|
245
732
|
this.overlayGroup = L.layerGroup().addTo(this.map);
|
|
733
|
+
this.markerLayerGroup = L.layerGroup().addTo(this.map);
|
|
246
734
|
|
|
247
735
|
// Track user drag/pinch gestures to prevent ping-pong feedback loop
|
|
248
736
|
this.map.on('movestart zoomstart dragstart', () => {
|
|
@@ -290,6 +778,9 @@ export default {
|
|
|
290
778
|
|
|
291
779
|
this.map.on('zoomend', () => {
|
|
292
780
|
this.$ownerInstance && this.$ownerInstance.callMethod('onRender_MapZoomEnd', this.map.getZoom());
|
|
781
|
+
if (this.clusterOptions && this.clusterOptions.enable && this.rawMarkers && this.rawMarkers.length > 0) {
|
|
782
|
+
this.renderMarkers(this.rawMarkers);
|
|
783
|
+
}
|
|
293
784
|
});
|
|
294
785
|
|
|
295
786
|
this.map.on('click', (e) => {
|
|
@@ -332,6 +823,20 @@ export default {
|
|
|
332
823
|
applyProps(p) {
|
|
333
824
|
if (!this.map || !p) return;
|
|
334
825
|
|
|
826
|
+
if (p.clusterOptions) {
|
|
827
|
+
this.clusterOptions = Object.assign({}, this.clusterOptions, p.clusterOptions);
|
|
828
|
+
} else {
|
|
829
|
+
if (p.enableCluster !== undefined) this.clusterOptions.enable = !!p.enableCluster;
|
|
830
|
+
if (p.clusterRadius !== undefined) this.clusterOptions.clusterRadius = p.clusterRadius;
|
|
831
|
+
if (p.clusterMaxZoom !== undefined) this.clusterOptions.clusterMaxZoom = p.clusterMaxZoom;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Handle fitBounds action if dispatched
|
|
835
|
+
if (p.fitBoundsAction && p.fitBoundsAction.timestamp && p.fitBoundsAction.timestamp !== this.lastFitBoundsTimestamp) {
|
|
836
|
+
this.lastFitBoundsTimestamp = p.fitBoundsAction.timestamp;
|
|
837
|
+
this.fitBounds(p.fitBoundsAction.bounds, p.fitBoundsAction.options);
|
|
838
|
+
}
|
|
839
|
+
|
|
335
840
|
// Anti-feedback loop: Don't setView if the user is actively dragging/zooming or if this is an echo
|
|
336
841
|
if (p.center && p.zoom !== undefined) {
|
|
337
842
|
const curCenter = this.map.getCenter();
|
|
@@ -372,6 +877,7 @@ export default {
|
|
|
372
877
|
polylines: p.polylines,
|
|
373
878
|
polygons: p.polygons,
|
|
374
879
|
circles: p.circles,
|
|
880
|
+
clusterOptions: this.clusterOptions,
|
|
375
881
|
...(p.overlays || {}),
|
|
376
882
|
};
|
|
377
883
|
const overlaysKey = JSON.stringify(overlaysToApply);
|
|
@@ -379,6 +885,19 @@ export default {
|
|
|
379
885
|
this.currentOverlaysKey = overlaysKey;
|
|
380
886
|
this.renderOverlays(overlaysToApply);
|
|
381
887
|
}
|
|
888
|
+
|
|
889
|
+
// GeoJSON rendering
|
|
890
|
+
const geoJsonData = p.geojson || (p.overlays && p.overlays.geojson);
|
|
891
|
+
const geoJsonStyle = p.geojsonStyle || (p.overlays && p.overlays.geojsonStyle);
|
|
892
|
+
const styleKey = typeof geoJsonStyle === 'object' ? JSON.stringify(geoJsonStyle) : String(geoJsonStyle || '');
|
|
893
|
+
const revKey = p.geojsonRevision !== undefined ? String(p.geojsonRevision) : '';
|
|
894
|
+
const dataKey = getGeoJsonFingerprint(geoJsonData);
|
|
895
|
+
const geoJsonKey = revKey + '___' + dataKey + '___' + styleKey;
|
|
896
|
+
|
|
897
|
+
if (this.currentGeoJsonKey !== geoJsonKey) {
|
|
898
|
+
this.currentGeoJsonKey = geoJsonKey;
|
|
899
|
+
this.renderGeoJSON(geoJsonData, geoJsonStyle);
|
|
900
|
+
}
|
|
382
901
|
},
|
|
383
902
|
setTileLayers(layers) {
|
|
384
903
|
for (const l of this.tileLayers) {
|
|
@@ -455,13 +974,30 @@ export default {
|
|
|
455
974
|
if (overlays.polylines) {
|
|
456
975
|
for (const line of overlays.polylines) {
|
|
457
976
|
if (!line.latLngs || line.latLngs.length < 2) continue;
|
|
458
|
-
const
|
|
977
|
+
const finalLatLngs = line.curved ? generateCurvedPolyline(line.latLngs, line.curvature) : line.latLngs;
|
|
978
|
+
const isFlowing = !!line.flowing;
|
|
979
|
+
const dashStr = line.dashArray ? line.dashArray.join(',') : (isFlowing ? '12,12' : undefined);
|
|
980
|
+
const lLine = L.polyline(finalLatLngs, {
|
|
459
981
|
color: line.color || '#3b82f6',
|
|
460
982
|
weight: line.width || 4,
|
|
461
983
|
opacity: line.opacity ?? 1,
|
|
462
|
-
dashArray:
|
|
984
|
+
dashArray: dashStr,
|
|
985
|
+
className: isFlowing ? 'uni-flowing-polyline' : '',
|
|
463
986
|
interactive: true,
|
|
464
987
|
}).addTo(this.overlayGroup);
|
|
988
|
+
|
|
989
|
+
if (isFlowing) {
|
|
990
|
+
const pathEl = lLine.getElement ? lLine.getElement() : lLine._path;
|
|
991
|
+
if (pathEl) {
|
|
992
|
+
const speed = line.flowSpeed || 1;
|
|
993
|
+
const duration = Math.max(0.2, 1.2 / Math.abs(speed));
|
|
994
|
+
pathEl.style.animationDuration = duration + 's';
|
|
995
|
+
if (speed < 0) {
|
|
996
|
+
pathEl.style.animationDirection = 'reverse';
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
465
1001
|
if (line.label) {
|
|
466
1002
|
const labelText = typeof line.label === 'string' ? line.label : line.label.text;
|
|
467
1003
|
lLine.bindTooltip(labelText, { permanent: true, direction: 'center', className: 'uni-overlay-label' });
|
|
@@ -474,104 +1010,370 @@ export default {
|
|
|
474
1010
|
}
|
|
475
1011
|
|
|
476
1012
|
// Markers
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
icon = L.divIcon({
|
|
524
|
-
className: 'uni-emoji-marker-icon',
|
|
525
|
-
html: '<div style="font-size:' + fontSize + 'px;line-height:1;text-align:center;pointer-events:auto;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">' + m.icon.text + '</div>',
|
|
526
|
-
iconSize: size,
|
|
527
|
-
iconAnchor: anchor,
|
|
528
|
-
tooltipAnchor: tooltipAnchor,
|
|
529
|
-
});
|
|
530
|
-
} else {
|
|
531
|
-
const pinColor = (m.icon && m.icon.color) ? m.icon.color : '#ef4444';
|
|
532
|
-
const html = '<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));">' +
|
|
533
|
-
'<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 + '"/>' +
|
|
534
|
-
'<circle cx="12" cy="11" r="4.5" fill="#ffffff"/>' +
|
|
535
|
-
'</svg>';
|
|
536
|
-
icon = L.divIcon({
|
|
537
|
-
html: html,
|
|
538
|
-
className: 'uni-default-pin',
|
|
539
|
-
iconSize: size,
|
|
540
|
-
iconAnchor: anchor,
|
|
541
|
-
tooltipAnchor: tooltipAnchor,
|
|
1013
|
+
this.renderMarkers(overlays.markers || []);
|
|
1014
|
+
},
|
|
1015
|
+
renderGeoJSON(data, style) {
|
|
1016
|
+
if (this.geoJsonLayer && this.map) {
|
|
1017
|
+
try {
|
|
1018
|
+
this.map.removeLayer(this.geoJsonLayer);
|
|
1019
|
+
} catch (e) {}
|
|
1020
|
+
this.geoJsonLayer = null;
|
|
1021
|
+
}
|
|
1022
|
+
if (!data || !this.map) return;
|
|
1023
|
+
|
|
1024
|
+
const defaultStyle = {
|
|
1025
|
+
color: '#3b82f6',
|
|
1026
|
+
weight: 2,
|
|
1027
|
+
opacity: 1,
|
|
1028
|
+
fillColor: '#3b82f6',
|
|
1029
|
+
fillOpacity: 0.25,
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const resolveStyle = (feature) => {
|
|
1033
|
+
let s = {};
|
|
1034
|
+
if (typeof style === 'function') {
|
|
1035
|
+
s = style(feature) || {};
|
|
1036
|
+
} else if (style && typeof style === 'object') {
|
|
1037
|
+
s = style;
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
color: s.color || defaultStyle.color,
|
|
1041
|
+
weight: s.weight !== undefined ? s.weight : defaultStyle.weight,
|
|
1042
|
+
opacity: s.opacity !== undefined ? s.opacity : defaultStyle.opacity,
|
|
1043
|
+
fillColor: s.fillColor || s.color || defaultStyle.fillColor,
|
|
1044
|
+
fillOpacity: s.fillOpacity !== undefined ? s.fillOpacity : defaultStyle.fillOpacity,
|
|
1045
|
+
dashArray: s.dashArray ? (Array.isArray(s.dashArray) ? s.dashArray.join(',') : s.dashArray) : undefined,
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
try {
|
|
1050
|
+
this.geoJsonLayer = L.geoJSON(data, {
|
|
1051
|
+
style: (feature) => resolveStyle(feature),
|
|
1052
|
+
onEachFeature: (feature, layer) => {
|
|
1053
|
+
layer.on('click', (e) => {
|
|
1054
|
+
if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
|
|
1055
|
+
this.$ownerInstance && this.$ownerInstance.callMethod('onRender_OverlayClick', {
|
|
1056
|
+
type: 'geojson',
|
|
1057
|
+
data: feature
|
|
1058
|
+
});
|
|
542
1059
|
});
|
|
543
1060
|
}
|
|
1061
|
+
}).addTo(this.map);
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
console.error('Render GeoJSON in renderjs error:', err);
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
renderMarkers(markers) {
|
|
1067
|
+
this.rawMarkers = markers || [];
|
|
1068
|
+
if (!this.markerLayerGroup) return;
|
|
1069
|
+
this.markerLayerGroup.clearLayers();
|
|
544
1070
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1071
|
+
const enableCluster = this.clusterOptions && this.clusterOptions.enable;
|
|
1072
|
+
if (!enableCluster) {
|
|
1073
|
+
for (let i = 0; i < this.rawMarkers.length; i++) {
|
|
1074
|
+
this.createSingleMarker(this.rawMarkers[i]);
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const curZoom = this.map ? this.map.getZoom() : 13;
|
|
1080
|
+
const results = clusterMarkers(this.rawMarkers, curZoom, this.clusterOptions);
|
|
1081
|
+
|
|
1082
|
+
for (let i = 0; i < results.length; i++) {
|
|
1083
|
+
const item = results[i];
|
|
1084
|
+
if (!item.isCluster) {
|
|
1085
|
+
this.createSingleMarker(item.marker);
|
|
1086
|
+
} else {
|
|
1087
|
+
this.createClusterMarker(item.cluster);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
createSingleMarker(m) {
|
|
1092
|
+
let icon;
|
|
1093
|
+
const size = m.icon && m.icon.size ? m.icon.size : [32, 32];
|
|
1094
|
+
const anchor = m.icon && m.icon.anchor ? m.icon.anchor : [size[0] / 2, size[1]];
|
|
1095
|
+
const tooltipAnchor = [size[0] / 2 - anchor[0], -anchor[1]];
|
|
1096
|
+
|
|
1097
|
+
if (m.icon && m.icon.type === 'avatar') {
|
|
1098
|
+
const avatarSize = m.icon.size || size || [32, 32];
|
|
1099
|
+
const avatarW = avatarSize[0];
|
|
1100
|
+
const avatarH = avatarSize[1];
|
|
1101
|
+
const borderColor = m.icon.borderColor || '#3b82f6';
|
|
1102
|
+
const borderWidth = m.icon.borderWidth !== undefined ? m.icon.borderWidth : (avatarW <= 36 ? 2.5 : 3);
|
|
1103
|
+
const pulseColor = m.icon.pulseColor || borderColor;
|
|
1104
|
+
const badgeText = m.icon.badgeText;
|
|
1105
|
+
const isCustomBg = !!m.icon.badgeColor;
|
|
1106
|
+
const badgeBg = isCustomBg
|
|
1107
|
+
? 'background:' + m.icon.badgeColor + ' !important; color:#ffffff !important; border:1.5px solid #ffffff !important;'
|
|
1108
|
+
: 'background:#ffffff !important; color:#1e293b !important; border:1px solid rgba(0,0,0,0.08) !important;';
|
|
1109
|
+
const hasPulse = !!m.icon.pulse;
|
|
1110
|
+
|
|
1111
|
+
const arrowW = avatarW <= 36 ? 4 : 5;
|
|
1112
|
+
const arrowH = avatarW <= 36 ? 5 : 6;
|
|
1113
|
+
const badgeHeight = avatarW <= 36 ? 12 : Math.max(14, Math.round(avatarW * 0.32));
|
|
1114
|
+
const badgeFontSize = avatarW <= 36 ? 8 : Math.max(9, Math.round(badgeHeight * 0.65));
|
|
1115
|
+
const badgeOffsetTop = avatarW <= 36 ? -2 : -3;
|
|
1116
|
+
const badgeOffsetRight = avatarW <= 36 ? -2 : -4;
|
|
1117
|
+
const badgePadding = avatarW <= 36 ? '0 2px' : '0 3px';
|
|
1118
|
+
|
|
1119
|
+
let imgUrl = m.icon.url || '';
|
|
1120
|
+
if (
|
|
1121
|
+
imgUrl &&
|
|
1122
|
+
typeof imgUrl === 'string' &&
|
|
1123
|
+
!imgUrl.startsWith('data:') &&
|
|
1124
|
+
!imgUrl.startsWith('http://') &&
|
|
1125
|
+
!imgUrl.startsWith('https://')
|
|
1126
|
+
) {
|
|
1127
|
+
if (imgUrl.startsWith('/')) {
|
|
1128
|
+
imgUrl = imgUrl.slice(1);
|
|
568
1129
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const pulseHtml = hasPulse
|
|
1133
|
+
? '<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>'
|
|
1134
|
+
: '';
|
|
1135
|
+
const badgeHtml = badgeText
|
|
1136
|
+
? '<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>'
|
|
1137
|
+
: '';
|
|
1138
|
+
|
|
1139
|
+
const html =
|
|
1140
|
+
'<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;">' +
|
|
1141
|
+
pulseHtml +
|
|
1142
|
+
'<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 + ';">' +
|
|
1143
|
+
'<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;" />' +
|
|
1144
|
+
'<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>' +
|
|
1145
|
+
'</div>' +
|
|
1146
|
+
badgeHtml +
|
|
1147
|
+
'</div>';
|
|
1148
|
+
|
|
1149
|
+
icon = L.divIcon({
|
|
1150
|
+
html: html,
|
|
1151
|
+
className: 'uni-avatar-marker-icon',
|
|
1152
|
+
iconSize: [avatarW, avatarH + arrowH],
|
|
1153
|
+
iconAnchor: m.icon.anchor || [avatarW / 2, avatarH + arrowH],
|
|
1154
|
+
tooltipAnchor: [0, -(avatarH + arrowH + 2)],
|
|
1155
|
+
});
|
|
1156
|
+
} else if (m.icon && m.icon.svg) {
|
|
1157
|
+
let svgStr = m.icon.svg.trim();
|
|
1158
|
+
if (svgStr.indexOf('xmlns=') === -1) {
|
|
1159
|
+
svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
1160
|
+
}
|
|
1161
|
+
icon = L.divIcon({
|
|
1162
|
+
className: 'uni-custom-svg-icon',
|
|
1163
|
+
html:
|
|
1164
|
+
'<div style="width:' +
|
|
1165
|
+
size[0] +
|
|
1166
|
+
'px;height:' +
|
|
1167
|
+
size[1] +
|
|
1168
|
+
'px;display:flex;align-items:center;justify-content:center;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">' +
|
|
1169
|
+
svgStr +
|
|
1170
|
+
'</div>',
|
|
1171
|
+
iconSize: size,
|
|
1172
|
+
iconAnchor: anchor,
|
|
1173
|
+
tooltipAnchor: tooltipAnchor,
|
|
1174
|
+
});
|
|
1175
|
+
} else if (m.icon && m.icon.html) {
|
|
1176
|
+
icon = L.divIcon({
|
|
1177
|
+
className: 'uni-custom-html-icon',
|
|
1178
|
+
html: m.icon.html,
|
|
1179
|
+
iconSize: size,
|
|
1180
|
+
iconAnchor: anchor,
|
|
1181
|
+
tooltipAnchor: tooltipAnchor,
|
|
1182
|
+
});
|
|
1183
|
+
} else if (m.icon && m.icon.url) {
|
|
1184
|
+
let finalUrl = m.icon.url;
|
|
1185
|
+
if (
|
|
1186
|
+
finalUrl &&
|
|
1187
|
+
typeof finalUrl === 'string' &&
|
|
1188
|
+
!finalUrl.startsWith('data:') &&
|
|
1189
|
+
!finalUrl.startsWith('http://') &&
|
|
1190
|
+
!finalUrl.startsWith('https://')
|
|
1191
|
+
) {
|
|
1192
|
+
if (finalUrl.startsWith('/')) {
|
|
1193
|
+
finalUrl = finalUrl.slice(1);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const html =
|
|
1197
|
+
'<img src="' +
|
|
1198
|
+
finalUrl +
|
|
1199
|
+
'" style="width:' +
|
|
1200
|
+
size[0] +
|
|
1201
|
+
'px;height:' +
|
|
1202
|
+
size[1] +
|
|
1203
|
+
'px;display:block;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));" />';
|
|
1204
|
+
icon = L.divIcon({
|
|
1205
|
+
className: 'uni-custom-img-icon',
|
|
1206
|
+
html: html,
|
|
1207
|
+
iconSize: size,
|
|
1208
|
+
iconAnchor: anchor,
|
|
1209
|
+
tooltipAnchor: tooltipAnchor,
|
|
1210
|
+
});
|
|
1211
|
+
} else if (m.icon && m.icon.text) {
|
|
1212
|
+
const fontSize = Math.round(size[0] * 0.72);
|
|
1213
|
+
icon = L.divIcon({
|
|
1214
|
+
className: 'uni-emoji-marker-icon',
|
|
1215
|
+
html:
|
|
1216
|
+
'<div style="font-size:' +
|
|
1217
|
+
fontSize +
|
|
1218
|
+
'px;line-height:1;text-align:center;pointer-events:auto;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">' +
|
|
1219
|
+
m.icon.text +
|
|
1220
|
+
'</div>',
|
|
1221
|
+
iconSize: size,
|
|
1222
|
+
iconAnchor: anchor,
|
|
1223
|
+
tooltipAnchor: tooltipAnchor,
|
|
1224
|
+
});
|
|
1225
|
+
} else {
|
|
1226
|
+
const pinColor = m.icon && m.icon.color ? m.icon.color : '#ef4444';
|
|
1227
|
+
const html =
|
|
1228
|
+
'<svg viewBox="0 0 24 32" width="' +
|
|
1229
|
+
size[0] +
|
|
1230
|
+
'" height="' +
|
|
1231
|
+
size[1] +
|
|
1232
|
+
'" style="filter:drop-shadow(0 2px 4px rgba(0,0,0,0.35));">' +
|
|
1233
|
+
'<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="' +
|
|
1234
|
+
pinColor +
|
|
1235
|
+
'"/>' +
|
|
1236
|
+
'<circle cx="12" cy="11" r="4.5" fill="#ffffff"/>' +
|
|
1237
|
+
'</svg>';
|
|
1238
|
+
icon = L.divIcon({
|
|
1239
|
+
html: html,
|
|
1240
|
+
className: 'uni-default-pin',
|
|
1241
|
+
iconSize: size,
|
|
1242
|
+
iconAnchor: anchor,
|
|
1243
|
+
tooltipAnchor: tooltipAnchor,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const lMarker = L.marker([m.latLng[0], m.latLng[1]], {
|
|
1248
|
+
icon: icon,
|
|
1249
|
+
interactive: true,
|
|
1250
|
+
}).addTo(this.markerLayerGroup);
|
|
1251
|
+
|
|
1252
|
+
if (m.label) {
|
|
1253
|
+
const labelText = typeof m.label === 'string' ? m.label : m.label.text;
|
|
1254
|
+
const customOffset =
|
|
1255
|
+
typeof m.label === 'object' && m.label.offset
|
|
1256
|
+
? m.label.offset
|
|
1257
|
+
: undefined;
|
|
1258
|
+
let offset = [0, 0];
|
|
1259
|
+
if (customOffset) {
|
|
1260
|
+
const ox = customOffset[0] || 0;
|
|
1261
|
+
const oy = customOffset[1] || 0;
|
|
1262
|
+
if (Math.abs(oy) <= 10) {
|
|
1263
|
+
offset = [ox, oy];
|
|
1264
|
+
} else {
|
|
1265
|
+
offset = [ox, 0];
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
lMarker.bindTooltip(labelText, {
|
|
1269
|
+
permanent: true,
|
|
1270
|
+
direction: 'top',
|
|
1271
|
+
offset: offset,
|
|
1272
|
+
className: 'uni-overlay-label',
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
lMarker.on('click', (e) => {
|
|
1277
|
+
if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
|
|
1278
|
+
this.$ownerInstance &&
|
|
1279
|
+
this.$ownerInstance.callMethod('onRender_OverlayClick', {
|
|
1280
|
+
type: 'marker',
|
|
1281
|
+
data: m,
|
|
572
1282
|
});
|
|
1283
|
+
});
|
|
1284
|
+
},
|
|
1285
|
+
createClusterMarker(cluster) {
|
|
1286
|
+
const rep = cluster.representativeMarker;
|
|
1287
|
+
let iconContent = '';
|
|
1288
|
+
|
|
1289
|
+
if (rep && rep.icon && rep.icon.url) {
|
|
1290
|
+
let finalUrl = rep.icon.url;
|
|
1291
|
+
if (
|
|
1292
|
+
finalUrl &&
|
|
1293
|
+
typeof finalUrl === 'string' &&
|
|
1294
|
+
!finalUrl.startsWith('data:') &&
|
|
1295
|
+
!finalUrl.startsWith('http://') &&
|
|
1296
|
+
!finalUrl.startsWith('https://')
|
|
1297
|
+
) {
|
|
1298
|
+
if (finalUrl.startsWith('/')) finalUrl = finalUrl.slice(1);
|
|
573
1299
|
}
|
|
1300
|
+
iconContent =
|
|
1301
|
+
'<img src="' +
|
|
1302
|
+
finalUrl +
|
|
1303
|
+
'" style="width:100%;height:100%;border-radius:50%;object-fit:cover;" />';
|
|
1304
|
+
} else if (rep && rep.icon && rep.icon.text) {
|
|
1305
|
+
iconContent =
|
|
1306
|
+
'<span style="font-size:20px;line-height:1;">' +
|
|
1307
|
+
rep.icon.text +
|
|
1308
|
+
'</span>';
|
|
1309
|
+
} else if (rep && rep.icon && rep.icon.svg) {
|
|
1310
|
+
let svgStr = rep.icon.svg.trim();
|
|
1311
|
+
if (svgStr.indexOf('xmlns=') === -1) {
|
|
1312
|
+
svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
1313
|
+
}
|
|
1314
|
+
iconContent = svgStr;
|
|
1315
|
+
} else {
|
|
1316
|
+
iconContent =
|
|
1317
|
+
'<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>';
|
|
574
1318
|
}
|
|
575
|
-
|
|
1319
|
+
|
|
1320
|
+
const html =
|
|
1321
|
+
'<div class="uni-cluster-bubble">' +
|
|
1322
|
+
'<div class="uni-cluster-icon">' +
|
|
1323
|
+
iconContent +
|
|
1324
|
+
'</div>' +
|
|
1325
|
+
'<div class="uni-cluster-badge">+' +
|
|
1326
|
+
cluster.count +
|
|
1327
|
+
'</div>' +
|
|
1328
|
+
'</div>';
|
|
1329
|
+
|
|
1330
|
+
const icon = L.divIcon({
|
|
1331
|
+
html: html,
|
|
1332
|
+
className: 'uni-cluster-marker-icon',
|
|
1333
|
+
iconSize: [44, 44],
|
|
1334
|
+
iconAnchor: [22, 22],
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
const lCluster = L.marker([cluster.center[0], cluster.center[1]], {
|
|
1338
|
+
icon: icon,
|
|
1339
|
+
interactive: true,
|
|
1340
|
+
zIndexOffset: 1000,
|
|
1341
|
+
}).addTo(this.markerLayerGroup);
|
|
1342
|
+
|
|
1343
|
+
lCluster.on('click', (e) => {
|
|
1344
|
+
if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
|
|
1345
|
+
const bounds = cluster.bounds;
|
|
1346
|
+
const isSameCoord =
|
|
1347
|
+
Math.abs(bounds[0][0] - bounds[1][0]) < 1e-6 &&
|
|
1348
|
+
Math.abs(bounds[0][1] - bounds[1][1]) < 1e-6;
|
|
1349
|
+
|
|
1350
|
+
if (isSameCoord) {
|
|
1351
|
+
this.map.setZoom(Math.min(this.map.getZoom() + 2, 19));
|
|
1352
|
+
this.map.panTo([cluster.center[0], cluster.center[1]]);
|
|
1353
|
+
} else {
|
|
1354
|
+
this.fitBounds(bounds, { padding: [60, 60], animate: true });
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
this.$ownerInstance &&
|
|
1358
|
+
this.$ownerInstance.callMethod('onRender_ClusterClick', cluster);
|
|
1359
|
+
});
|
|
1360
|
+
},
|
|
1361
|
+
fitBounds(bounds, options) {
|
|
1362
|
+
if (!this.map || !bounds) return;
|
|
1363
|
+
try {
|
|
1364
|
+
const latLngs = extractLatLngs(bounds);
|
|
1365
|
+
if (!latLngs || latLngs.length === 0) return;
|
|
1366
|
+
const leafletOptions = {};
|
|
1367
|
+
if (options && options.padding) leafletOptions.padding = options.padding;
|
|
1368
|
+
if (options && options.paddingTopLeft) leafletOptions.paddingTopLeft = options.paddingTopLeft;
|
|
1369
|
+
if (options && options.paddingBottomRight) leafletOptions.paddingBottomRight = options.paddingBottomRight;
|
|
1370
|
+
if (options && options.maxZoom !== undefined) leafletOptions.maxZoom = options.maxZoom;
|
|
1371
|
+
if (options && options.animate !== undefined) leafletOptions.animate = options.animate;
|
|
1372
|
+
if (options && options.duration !== undefined) leafletOptions.duration = options.duration / 1000;
|
|
1373
|
+
this.map.fitBounds(latLngs, leafletOptions);
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
console.error('[uni-leaflet app-render] fitBounds error:', e);
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
576
1378
|
}
|
|
577
1379
|
};
|