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/UniLeafletMap.vue CHANGED
@@ -49,52 +49,6 @@
49
49
  </view>
50
50
  </template>
51
51
 
52
- <script lang="ts">
53
- export default {
54
- methods: {
55
- onRender_MapMove(this: any, val: any) {
56
- const center = val?.center || val;
57
- const zoom = val?.zoom;
58
- if (center) {
59
- this.$emit('update:center', center);
60
- this.$emit('move', { center, zoom });
61
- }
62
- },
63
- onRender_MapMoveEnd(this: any, val: any) {
64
- const center = val?.center || val;
65
- const zoom = val?.zoom;
66
- if (center) {
67
- this.$emit('update:center', center);
68
- this.$emit('moveend', { center, zoom });
69
- }
70
- },
71
- onRender_MapZoom(this: any, z: any) {
72
- this.$emit('update:zoom', z);
73
- this.$emit('zoom', z);
74
- },
75
- onRender_MapZoomEnd(this: any, z: any) {
76
- this.$emit('update:zoom', z);
77
- this.$emit('zoomend', z);
78
- },
79
- onRender_MapClick(this: any, val: any) {
80
- const latLng = val?.latLng || val;
81
- const point = val?.point || { x: 0, y: 0 };
82
- this.$emit('click', { latLng, point });
83
- },
84
- onRender_OverlayClick(this: any, val: any) {
85
- this.$emit('overlay-click', val);
86
- if (val && val.type === 'marker') this.$emit('marker-click', val.data);
87
- if (val && val.type === 'polyline') this.$emit('polyline-click', val.data);
88
- if (val && val.type === 'polygon') this.$emit('polygon-click', val.data);
89
- if (val && val.type === 'circle') this.$emit('circle-click', val.data);
90
- },
91
- onRender_Ready(this: any) {
92
- this.$emit('ready', this);
93
- },
94
- },
95
- };
96
- </script>
97
-
98
52
  <script setup lang="ts">
99
53
  import {
100
54
  ref,
@@ -116,6 +70,13 @@ import type {
116
70
  CircleOptions,
117
71
  MapOverlays,
118
72
  OverlayClickEvent,
73
+ FitBoundsOptions,
74
+ LatLngBoundsExpression,
75
+ MapBounds,
76
+ ClusterOptions,
77
+ ClusterGroup,
78
+ GeoJsonStyle,
79
+ GeoJsonStyleOptions,
119
80
  } from './types';
120
81
  import { createMapEngine } from './engine/factory';
121
82
 
@@ -168,6 +129,30 @@ const props = defineProps({
168
129
  type: Object as PropType<MapOverlays>,
169
130
  default: () => undefined,
170
131
  },
132
+ enableCluster: {
133
+ type: Boolean,
134
+ default: false,
135
+ },
136
+ clusterRadius: {
137
+ type: Number,
138
+ default: 60,
139
+ },
140
+ clusterMaxZoom: {
141
+ type: Number,
142
+ default: 17,
143
+ },
144
+ clusterOptions: {
145
+ type: Object as PropType<ClusterOptions>,
146
+ default: () => undefined,
147
+ },
148
+ geojson: {
149
+ type: [Object, Array] as PropType<any>,
150
+ default: () => null,
151
+ },
152
+ geojsonStyle: {
153
+ type: [Object, Function] as PropType<GeoJsonStyle>,
154
+ default: () => undefined,
155
+ },
171
156
  width: {
172
157
  type: String,
173
158
  default: '100%',
@@ -192,9 +177,11 @@ const emit = defineEmits<{
192
177
  (e: 'zoomend', val: number): void;
193
178
  (e: 'click', val: { latLng: LatLngTuple; point: Point }): void;
194
179
  (e: 'marker-click', val: MarkerOptions): void;
180
+ (e: 'cluster-click', val: ClusterGroup): void;
195
181
  (e: 'polyline-click', val: PolylineOptions): void;
196
182
  (e: 'polygon-click', val: PolygonOptions): void;
197
183
  (e: 'circle-click', val: CircleOptions): void;
184
+ (e: 'geojson-click', val: any): void;
198
185
  (e: 'overlay-click', val: OverlayClickEvent): void;
199
186
  }>();
200
187
 
@@ -208,6 +195,22 @@ let engine: IMapEngine | null = null;
208
195
  let isInternalUpdating = false;
209
196
 
210
197
  // Compute payload for renderjs on App-Plus
198
+ const fitBoundsAction = ref<{
199
+ bounds: any;
200
+ options?: FitBoundsOptions;
201
+ timestamp: number;
202
+ } | null>(null);
203
+
204
+ const geojsonRevision = ref(0);
205
+
206
+ watch(
207
+ [() => props.geojson, () => props.overlays?.geojson],
208
+ () => {
209
+ geojsonRevision.value++;
210
+ },
211
+ { deep: true }
212
+ );
213
+
211
214
  const mapPropsPayload = computed(() => ({
212
215
  appMapId,
213
216
  center: props.center,
@@ -222,6 +225,14 @@ const mapPropsPayload = computed(() => ({
222
225
  polygons: props.polygons,
223
226
  circles: props.circles,
224
227
  overlays: props.overlays,
228
+ geojson: props.geojson,
229
+ geojsonStyle: props.geojsonStyle,
230
+ geojsonRevision: geojsonRevision.value,
231
+ enableCluster: props.enableCluster,
232
+ clusterRadius: props.clusterRadius,
233
+ clusterMaxZoom: props.clusterMaxZoom,
234
+ clusterOptions: props.clusterOptions,
235
+ fitBoundsAction: fitBoundsAction.value,
225
236
  }));
226
237
 
227
238
  // Touch & Mouse Wheel handlers for Canvas/Mini-program
@@ -332,29 +343,87 @@ watch(
332
343
  }
333
344
  );
334
345
 
335
- // Overlays Watchers
336
- function syncCombinedOverlays() {
337
- if (!engine) return;
338
- const combined: MapOverlays = {
339
- ...(props.overlays || {}),
340
- };
341
- if (props.markers !== undefined) combined.markers = props.markers;
342
- if (props.polylines !== undefined) combined.polylines = props.polylines;
343
- if (props.polygons !== undefined) combined.polygons = props.polygons;
344
- if (props.circles !== undefined) combined.circles = props.circles;
345
- engine.setOverlays(combined);
346
- }
346
+ // Overlays Watchers - Granular independent watches to prevent cross-layer invalidation
347
+ watch(
348
+ () => props.polygons,
349
+ (newPolygons) => {
350
+ if (!engine) return;
351
+ if (newPolygons !== undefined && typeof (engine as any).setPolygons === 'function') {
352
+ (engine as any).setPolygons(newPolygons);
353
+ }
354
+ },
355
+ { deep: true }
356
+ );
357
+
358
+ watch(
359
+ () => props.polylines,
360
+ (newPolylines) => {
361
+ if (!engine) return;
362
+ if (newPolylines !== undefined && typeof (engine as any).setPolylines === 'function') {
363
+ (engine as any).setPolylines(newPolylines);
364
+ }
365
+ },
366
+ { deep: true }
367
+ );
368
+
369
+ watch(
370
+ () => props.circles,
371
+ (newCircles) => {
372
+ if (!engine) return;
373
+ if (newCircles !== undefined && typeof (engine as any).setCircles === 'function') {
374
+ (engine as any).setCircles(newCircles);
375
+ }
376
+ },
377
+ { deep: true }
378
+ );
379
+
380
+ watch(
381
+ () => props.markers,
382
+ (newMarkers) => {
383
+ if (!engine) return;
384
+ if (newMarkers !== undefined && typeof (engine as any).setMarkers === 'function') {
385
+ (engine as any).setMarkers(newMarkers);
386
+ }
387
+ },
388
+ { deep: true }
389
+ );
390
+
391
+ watch(
392
+ [() => props.geojson, () => props.geojsonStyle],
393
+ ([newData, newStyle]) => {
394
+ if (!engine) return;
395
+ if (newData !== undefined && typeof (engine as any).setGeoJSON === 'function') {
396
+ (engine as any).setGeoJSON(newData, newStyle);
397
+ }
398
+ },
399
+ { deep: true }
400
+ );
401
+
402
+ watch(
403
+ () => props.overlays,
404
+ (newOverlays) => {
405
+ if (!engine || !newOverlays) return;
406
+ engine.setOverlays(newOverlays);
407
+ },
408
+ { deep: true }
409
+ );
347
410
 
348
411
  watch(
349
412
  [
350
- () => props.overlays,
351
- () => props.markers,
352
- () => props.polylines,
353
- () => props.polygons,
354
- () => props.circles,
413
+ () => props.enableCluster,
414
+ () => props.clusterRadius,
415
+ () => props.clusterMaxZoom,
416
+ () => props.clusterOptions,
355
417
  ],
356
418
  () => {
357
- syncCombinedOverlays();
419
+ const opts = props.clusterOptions || {
420
+ enable: props.enableCluster,
421
+ clusterRadius: props.clusterRadius,
422
+ clusterMaxZoom: props.clusterMaxZoom,
423
+ };
424
+ if (engine && typeof (engine as any).setClusterOptions === 'function') {
425
+ (engine as any).setClusterOptions(opts);
426
+ }
358
427
  },
359
428
  { deep: true }
360
429
  );
@@ -441,6 +510,7 @@ function onOverlayClick(event: OverlayClickEvent) {
441
510
  if (event.type === 'polyline') emit('polyline-click', event.data as PolylineOptions);
442
511
  if (event.type === 'polygon') emit('polygon-click', event.data as PolygonOptions);
443
512
  if (event.type === 'circle') emit('circle-click', event.data as CircleOptions);
513
+ if (event.type === 'geojson') emit('geojson-click', event.data);
444
514
  }
445
515
 
446
516
  // Handler when renderjs is ready on App-Plus
@@ -457,6 +527,8 @@ onMounted(async () => {
457
527
  if (props.polylines) initialOverlays.polylines = props.polylines;
458
528
  if (props.polygons) initialOverlays.polygons = props.polygons;
459
529
  if (props.circles) initialOverlays.circles = props.circles;
530
+ if (props.geojson) initialOverlays.geojson = props.geojson;
531
+ if (props.geojsonStyle) initialOverlays.geojsonStyle = props.geojsonStyle;
460
532
 
461
533
  const commonOptions = {
462
534
  center: props.center,
@@ -467,6 +539,8 @@ onMounted(async () => {
467
539
  subdomains: props.subdomains,
468
540
  layers: props.layers,
469
541
  overlays: initialOverlays,
542
+ geojson: props.geojson,
543
+ geojsonStyle: props.geojsonStyle,
470
544
  showControls: props.showControls,
471
545
  onMove: onMapMove,
472
546
  onMoveEnd: onMapMoveEnd,
@@ -474,6 +548,12 @@ onMounted(async () => {
474
548
  onZoomEnd: onMapZoomEnd,
475
549
  onClick: onMapClick,
476
550
  onOverlayClick: onOverlayClick,
551
+ clusterOptions: props.clusterOptions || {
552
+ enable: props.enableCluster,
553
+ clusterRadius: props.clusterRadius,
554
+ clusterMaxZoom: props.clusterMaxZoom,
555
+ },
556
+ onClusterClick: (cluster: ClusterGroup) => emit('cluster-click', cluster),
477
557
  };
478
558
 
479
559
  // #ifdef H5
@@ -544,6 +624,102 @@ onUnmounted(() => {
544
624
  });
545
625
 
546
626
 
627
+ function resolveTargetBounds(target: any) {
628
+ if (target === 'markers' || target === 'points') {
629
+ return props.markers || props.overlays?.markers || [];
630
+ }
631
+ if (target === 'polylines' || target === 'lines') {
632
+ return props.polylines || props.overlays?.polylines || [];
633
+ }
634
+ if (target === 'polygons') {
635
+ return props.polygons || props.overlays?.polygons || [];
636
+ }
637
+ if (target === 'circles') {
638
+ return props.circles || props.overlays?.circles || [];
639
+ }
640
+ if (target === 'geojson') {
641
+ return props.geojson || props.overlays?.geojson || [];
642
+ }
643
+ if (!target || target === 'all' || target === 'active') {
644
+ return {
645
+ markers: props.markers || props.overlays?.markers,
646
+ polylines: props.polylines || props.overlays?.polylines,
647
+ polygons: props.polygons || props.overlays?.polygons,
648
+ circles: props.circles || props.overlays?.circles,
649
+ geojson: props.geojson || props.overlays?.geojson,
650
+ };
651
+ }
652
+ return target;
653
+ }
654
+
655
+ function fitBounds(
656
+ bounds?: LatLngBoundsExpression | LatLngTuple[] | MapBounds | 'markers' | 'polylines' | 'polygons' | 'all' | 'points' | 'lines',
657
+ options?: { padding?: [number, number]; maxZoom?: number; animate?: boolean; duration?: number }
658
+ ) {
659
+ const resolved = resolveTargetBounds(bounds);
660
+ // #ifndef APP-PLUS
661
+ if (engine && typeof (engine as any).fitBounds === 'function') {
662
+ (engine as any).fitBounds(resolved, options);
663
+ }
664
+ // #endif
665
+
666
+ // #ifdef APP-PLUS
667
+ fitBoundsAction.value = {
668
+ bounds: resolved,
669
+ options,
670
+ timestamp: Date.now(),
671
+ };
672
+ // #endif
673
+ }
674
+
675
+ if (instance && instance.proxy) {
676
+ const proxy = instance.proxy as any;
677
+ proxy.doFitBounds = fitBounds;
678
+ proxy.fitBounds = fitBounds;
679
+ proxy.onRender_MapMove = (val: any) => {
680
+ const center = val?.center || val;
681
+ const zoom = val?.zoom;
682
+ if (center) {
683
+ emit('update:center', center);
684
+ emit('move', { center, zoom });
685
+ }
686
+ };
687
+ proxy.onRender_MapMoveEnd = (val: any) => {
688
+ const center = val?.center || val;
689
+ const zoom = val?.zoom;
690
+ if (center) {
691
+ emit('update:center', center);
692
+ emit('moveend', { center, zoom });
693
+ }
694
+ };
695
+ proxy.onRender_MapZoom = (z: any) => {
696
+ emit('update:zoom', z);
697
+ emit('zoom', z);
698
+ };
699
+ proxy.onRender_MapZoomEnd = (z: any) => {
700
+ emit('update:zoom', z);
701
+ emit('zoomend', z);
702
+ };
703
+ proxy.onRender_MapClick = (val: any) => {
704
+ const latLng = val?.latLng || val;
705
+ const point = val?.point || { x: 0, y: 0 };
706
+ emit('click', { latLng, point });
707
+ };
708
+ proxy.onRender_OverlayClick = (val: any) => {
709
+ emit('overlay-click', val);
710
+ if (val && val.type === 'marker') emit('marker-click', val.data);
711
+ if (val && val.type === 'polyline') emit('polyline-click', val.data);
712
+ if (val && val.type === 'polygon') emit('polygon-click', val.data);
713
+ if (val && val.type === 'circle') emit('circle-click', val.data);
714
+ };
715
+ proxy.onRender_ClusterClick = (val: any) => {
716
+ emit('cluster-click', val);
717
+ };
718
+ proxy.onRender_Ready = () => {
719
+ emit('ready', proxy);
720
+ };
721
+ }
722
+
547
723
  // Unified Exposed methods
548
724
  defineExpose({
549
725
  setCenter(center: LatLngTuple, animate = true) {
@@ -561,6 +737,12 @@ defineExpose({
561
737
  panTo(center: LatLngTuple, duration = 300) {
562
738
  engine?.panTo(center, duration);
563
739
  },
740
+ fitBounds(
741
+ bounds: LatLngBoundsExpression | LatLngTuple[] | MapBounds,
742
+ options?: { padding?: [number, number]; maxZoom?: number; animate?: boolean; duration?: number }
743
+ ) {
744
+ fitBounds(bounds, options);
745
+ },
564
746
  setTileUrl(url: string, subdomains?: string[]) {
565
747
  engine?.setTileUrl(url, subdomains);
566
748
  },
@@ -582,9 +764,20 @@ defineExpose({
582
764
  setCircles(circles: CircleOptions[]) {
583
765
  engine?.setCircles(circles);
584
766
  },
767
+ setGeoJSON(data: any, style?: GeoJsonStyle) {
768
+ geojsonRevision.value++;
769
+ if (engine && typeof (engine as any).setGeoJSON === 'function') {
770
+ (engine as any).setGeoJSON(data, style);
771
+ }
772
+ },
585
773
  clearOverlays() {
586
774
  engine?.clearOverlays();
587
775
  },
776
+ setClusterOptions(options: ClusterOptions) {
777
+ if (engine && typeof (engine as any).setClusterOptions === 'function') {
778
+ (engine as any).setClusterOptions(options);
779
+ }
780
+ },
588
781
  resize(width?: number, height?: number) {
589
782
  engine?.resize(width, height);
590
783
  },
@@ -745,8 +938,183 @@ defineExpose({
745
938
  .uni-custom-html-icon,
746
939
  .uni-custom-img-icon,
747
940
  .uni-custom-icon,
748
- .uni-default-pin {
941
+ .uni-default-pin,
942
+ .uni-cluster-marker-icon,
943
+ .uni-avatar-marker-icon {
749
944
  background: transparent !important;
750
945
  border: none !important;
751
946
  }
947
+
948
+ .uni-cluster-bubble {
949
+ position: relative !important;
950
+ width: 44px !important;
951
+ height: 44px !important;
952
+ border-radius: 50% !important;
953
+ background: #ffffff !important;
954
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22), 0 0 0 3px #3b82f6 !important;
955
+ display: flex !important;
956
+ align-items: center !important;
957
+ justify-content: center !important;
958
+ cursor: pointer !important;
959
+ user-select: none !important;
960
+ box-sizing: border-box !important;
961
+ transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
962
+ }
963
+
964
+ .uni-cluster-bubble:hover {
965
+ transform: scale(1.1) !important;
966
+ }
967
+
968
+ .uni-cluster-bubble .uni-cluster-icon {
969
+ width: 26px !important;
970
+ height: 26px !important;
971
+ display: flex !important;
972
+ align-items: center !important;
973
+ justify-content: center !important;
974
+ border-radius: 50% !important;
975
+ overflow: hidden !important;
976
+ }
977
+
978
+ .uni-cluster-bubble .uni-cluster-badge {
979
+ position: absolute !important;
980
+ top: -6px !important;
981
+ right: -8px !important;
982
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
983
+ color: #ffffff !important;
984
+ font-size: 11px !important;
985
+ font-weight: 700 !important;
986
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
987
+ height: 18px !important;
988
+ min-width: 18px !important;
989
+ padding: 0 5px !important;
990
+ border-radius: 10px !important;
991
+ display: flex !important;
992
+ align-items: center !important;
993
+ justify-content: center !important;
994
+ border: 2px solid #ffffff !important;
995
+ box-shadow: 0 2px 6px rgba(220, 38, 38, 0.45) !important;
996
+ box-sizing: border-box !important;
997
+ white-space: nowrap !important;
998
+ pointer-events: none !important;
999
+ }
1000
+
1001
+ /* Flowing polyline animation */
1002
+ @keyframes uni-polyline-flowing {
1003
+ from {
1004
+ stroke-dashoffset: 40px;
1005
+ }
1006
+ to {
1007
+ stroke-dashoffset: 0px;
1008
+ }
1009
+ }
1010
+
1011
+ .uni-flowing-polyline {
1012
+ animation: uni-polyline-flowing 1.5s linear infinite;
1013
+ }
1014
+
1015
+ /* Avatar Bubble Marker Styles */
1016
+ .uni-avatar-marker-wrapper {
1017
+ position: relative !important;
1018
+ display: flex !important;
1019
+ flex-direction: column !important;
1020
+ align-items: center !important;
1021
+ justify-content: flex-start !important;
1022
+ cursor: pointer !important;
1023
+ user-select: none !important;
1024
+ }
1025
+
1026
+ .uni-avatar-bubble {
1027
+ position: relative !important;
1028
+ border-radius: 50% !important;
1029
+ background-color: #ffffff !important;
1030
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) !important;
1031
+ display: flex !important;
1032
+ align-items: center !important;
1033
+ justify-content: center !important;
1034
+ box-sizing: border-box !important;
1035
+ overflow: visible !important;
1036
+ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
1037
+ }
1038
+
1039
+ .uni-avatar-marker-wrapper:hover .uni-avatar-bubble {
1040
+ transform: scale(1.12) !important;
1041
+ }
1042
+
1043
+ .uni-avatar-img {
1044
+ width: 100% !important;
1045
+ height: 100% !important;
1046
+ border-radius: 50% !important;
1047
+ object-fit: cover !important;
1048
+ display: block !important;
1049
+ pointer-events: none !important;
1050
+ }
1051
+
1052
+ .uni-avatar-arrow {
1053
+ position: absolute !important;
1054
+ bottom: -6px !important;
1055
+ left: 50% !important;
1056
+ transform: translateX(-50%) !important;
1057
+ width: 0 !important;
1058
+ height: 0 !important;
1059
+ border-left: 5px solid transparent !important;
1060
+ border-right: 5px solid transparent !important;
1061
+ border-top-width: 6px !important;
1062
+ border-top-style: solid !important;
1063
+ z-index: 1 !important;
1064
+ filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.2)) !important;
1065
+ }
1066
+
1067
+ .uni-avatar-badge {
1068
+ position: absolute !important;
1069
+ top: -2px !important;
1070
+ right: -2px !important;
1071
+ background: #ffffff !important;
1072
+ color: #1e293b !important;
1073
+ font-size: 8px !important;
1074
+ font-weight: 700 !important;
1075
+ line-height: 1 !important;
1076
+ height: 12px !important;
1077
+ min-width: 12px !important;
1078
+ padding: 0 2px !important;
1079
+ border-radius: 6px !important;
1080
+ display: flex !important;
1081
+ align-items: center !important;
1082
+ justify-content: center !important;
1083
+ border: 1px solid rgba(0, 0, 0, 0.08) !important;
1084
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25) !important;
1085
+ box-sizing: border-box !important;
1086
+ white-space: nowrap !important;
1087
+ z-index: 10 !important;
1088
+ pointer-events: none !important;
1089
+ }
1090
+
1091
+ @keyframes uni-avatar-pulse {
1092
+ 0% {
1093
+ transform: scale(1);
1094
+ opacity: 0.85;
1095
+ }
1096
+ 70% {
1097
+ transform: scale(1.6);
1098
+ opacity: 0;
1099
+ }
1100
+ 100% {
1101
+ transform: scale(1.6);
1102
+ opacity: 0;
1103
+ }
1104
+ }
1105
+
1106
+ .uni-avatar-pulse {
1107
+ position: absolute !important;
1108
+ top: 0 !important;
1109
+ left: 0 !important;
1110
+ width: 100% !important;
1111
+ height: 100% !important;
1112
+ border-radius: 50% !important;
1113
+ box-sizing: border-box !important;
1114
+ border-style: solid !important;
1115
+ border-width: 3px !important;
1116
+ animation: uni-avatar-pulse 1.8s cubic-bezier(0.24, 0, 0.38, 1) infinite !important;
1117
+ pointer-events: none !important;
1118
+ z-index: 0 !important;
1119
+ }
752
1120
  </style>