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/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 lLine = L.polyline(line.latLngs, {
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: line.dashArray ? line.dashArray.join(',') : undefined,
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
- if (overlays.markers) {
478
- for (const m of overlays.markers) {
479
- let icon;
480
- const size = m.icon && m.icon.size ? m.icon.size : [32, 32];
481
- const anchor = m.icon && m.icon.anchor ? m.icon.anchor : [size[0] / 2, size[1]];
482
-
483
- const tooltipAnchor = [size[0] / 2 - anchor[0], -anchor[1]];
484
-
485
- if (m.icon && m.icon.svg) {
486
- let svgStr = m.icon.svg.trim();
487
- if (svgStr.indexOf('xmlns=') === -1) {
488
- svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
489
- }
490
- icon = L.divIcon({
491
- className: 'uni-custom-svg-icon',
492
- 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>',
493
- iconSize: size,
494
- iconAnchor: anchor,
495
- tooltipAnchor: tooltipAnchor,
496
- });
497
- } else if (m.icon && m.icon.html) {
498
- icon = L.divIcon({
499
- className: 'uni-custom-html-icon',
500
- html: m.icon.html,
501
- iconSize: size,
502
- iconAnchor: anchor,
503
- tooltipAnchor: tooltipAnchor,
504
- });
505
- } else if (m.icon && m.icon.url) {
506
- let finalUrl = m.icon.url;
507
- if (finalUrl && typeof finalUrl === 'string' && !finalUrl.startsWith('data:') && !finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
508
- // Strip leading slash so that local resources resolve relative to app bundle www root in App-Plus
509
- if (finalUrl.startsWith('/')) {
510
- finalUrl = finalUrl.slice(1);
511
- }
512
- }
513
- 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));" />';
514
- icon = L.divIcon({
515
- className: 'uni-custom-img-icon',
516
- html: html,
517
- iconSize: size,
518
- iconAnchor: anchor,
519
- tooltipAnchor: tooltipAnchor,
520
- });
521
- } else if (m.icon && m.icon.text) {
522
- const fontSize = Math.round(size[0] * 0.72);
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
- const lMarker = L.marker([m.latLng[0], m.latLng[1]], {
546
- icon: icon,
547
- interactive: true,
548
- }).addTo(this.overlayGroup);
549
- if (m.label) {
550
- const labelText = typeof m.label === 'string' ? m.label : m.label.text;
551
- const customOffset = typeof m.label === 'object' && m.label.offset ? m.label.offset : undefined;
552
- let offset = [0, 0];
553
- if (customOffset) {
554
- const ox = customOffset[0] || 0;
555
- const oy = customOffset[1] || 0;
556
- if (Math.abs(oy) <= 10) {
557
- offset = [ox, oy];
558
- } else {
559
- offset = [ox, 0];
560
- }
561
- }
562
- lMarker.bindTooltip(labelText, {
563
- permanent: true,
564
- direction: 'top',
565
- offset: offset,
566
- className: 'uni-overlay-label',
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
- lMarker.on('click', (e) => {
570
- if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
571
- this.$ownerInstance && this.$ownerInstance.callMethod('onRender_OverlayClick', { type: 'marker', data: m });
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
  };