uni-leaflet 1.0.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 ADDED
@@ -0,0 +1,550 @@
1
+ import L from 'leaflet';
2
+
3
+ // Inject essential Leaflet layout CSS into Webview document
4
+ function ensureLeafletStyles() {
5
+ if (typeof document === 'undefined') return;
6
+ if (document.getElementById('uni-leaflet-core-styles')) return;
7
+
8
+ const style = document.createElement('style');
9
+ style.id = 'uni-leaflet-core-styles';
10
+ style.innerHTML = `
11
+ .leaflet-pane,
12
+ .leaflet-tile,
13
+ .leaflet-marker-icon,
14
+ .leaflet-marker-shadow,
15
+ .leaflet-tile-container,
16
+ .leaflet-pane > svg,
17
+ .leaflet-pane > canvas,
18
+ .leaflet-zoom-box,
19
+ .leaflet-image-layer,
20
+ .leaflet-layer {
21
+ position: absolute !important;
22
+ left: 0 !important;
23
+ top: 0 !important;
24
+ }
25
+ .leaflet-container {
26
+ overflow: hidden !important;
27
+ width: 100% !important;
28
+ height: 100% !important;
29
+ position: relative !important;
30
+ touch-action: none !important;
31
+ -webkit-tap-highlight-color: transparent !important;
32
+ user-select: none !important;
33
+ -webkit-user-select: none !important;
34
+ background: #f2efe9 !important;
35
+ cursor: grab;
36
+ }
37
+ .leaflet-tile,
38
+ .leaflet-marker-icon,
39
+ .leaflet-marker-shadow {
40
+ user-select: none !important;
41
+ -webkit-user-select: none !important;
42
+ -webkit-user-drag: none !important;
43
+ }
44
+ .leaflet-tile {
45
+ filter: inherit !important;
46
+ visibility: hidden;
47
+ max-width: none !important;
48
+ max-height: none !important;
49
+ width: 256px !important;
50
+ height: 256px !important;
51
+ padding: 0 !important;
52
+ margin: 0 !important;
53
+ border: none !important;
54
+ box-sizing: content-box !important;
55
+ pointer-events: none !important;
56
+ }
57
+ .leaflet-tile-loaded {
58
+ visibility: inherit !important;
59
+ }
60
+ .leaflet-tile-container {
61
+ pointer-events: none !important;
62
+ }
63
+ .leaflet-zoom-animated {
64
+ transform-origin: 0 0 !important;
65
+ -webkit-transform-origin: 0 0 !important;
66
+ }
67
+ .leaflet-pane { z-index: 400; }
68
+ .leaflet-tile-pane { z-index: 200; pointer-events: none !important; }
69
+ .leaflet-overlay-pane { z-index: 400; pointer-events: none !important; }
70
+ .leaflet-shadow-pane { z-index: 500; pointer-events: none !important; }
71
+ .leaflet-marker-pane { z-index: 600; pointer-events: none !important; }
72
+ .leaflet-tooltip-pane { z-index: 650; pointer-events: none !important; }
73
+ .leaflet-popup-pane { z-index: 700; }
74
+ .leaflet-map-pane canvas { z-index: 100; }
75
+ .leaflet-map-pane svg { z-index: 200; pointer-events: none !important; }
76
+
77
+ /* Interactive Elements & Clicks */
78
+ .leaflet-interactive,
79
+ .leaflet-pane > svg path.leaflet-interactive,
80
+ .leaflet-pane > svg polygon.leaflet-interactive,
81
+ .leaflet-pane > svg polyline.leaflet-interactive,
82
+ .leaflet-pane > svg circle.leaflet-interactive,
83
+ .leaflet-marker-pane .leaflet-marker-icon {
84
+ pointer-events: auto !important;
85
+ cursor: pointer !important;
86
+ }
87
+
88
+ .uni-overlay-label {
89
+ background-color: rgba(255, 255, 255, 0.95) !important;
90
+ border: 1px solid #cbd5e1 !important;
91
+ border-radius: 6px !important;
92
+ padding: 3px 8px !important;
93
+ font-size: 12px !important;
94
+ font-weight: 600 !important;
95
+ color: #1e293b !important;
96
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important;
97
+ white-space: nowrap !important;
98
+ pointer-events: none !important;
99
+ }
100
+ .leaflet-tooltip {
101
+ position: absolute;
102
+ padding: 4px 8px;
103
+ background-color: #fff;
104
+ border: 1px solid #fff;
105
+ border-radius: 4px;
106
+ color: #222;
107
+ white-space: nowrap;
108
+ user-select: none;
109
+ pointer-events: none;
110
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
111
+ }
112
+ .leaflet-tooltip-top:before,
113
+ .leaflet-tooltip-bottom:before,
114
+ .leaflet-tooltip-left:before,
115
+ .leaflet-tooltip-right:before {
116
+ position: absolute;
117
+ pointer-events: none;
118
+ border: 6px solid transparent;
119
+ background: transparent;
120
+ content: "";
121
+ }
122
+ .leaflet-tooltip-top:before {
123
+ bottom: 0;
124
+ margin-bottom: -12px;
125
+ border-top-color: #cbd5e1;
126
+ }
127
+ .leaflet-tooltip-bottom:before {
128
+ top: 0;
129
+ margin-top: -12px;
130
+ border-bottom-color: #cbd5e1;
131
+ }
132
+ .uni-emoji-marker-icon,
133
+ .uni-custom-svg-icon,
134
+ .uni-custom-html-icon,
135
+ .uni-custom-img-icon,
136
+ .uni-custom-icon,
137
+ .uni-default-pin {
138
+ background: transparent !important;
139
+ border: none !important;
140
+ }
141
+ `;
142
+ document.head.appendChild(style);
143
+ }
144
+
145
+ export default {
146
+ data() {
147
+ return {
148
+ map: null,
149
+ tileLayers: [],
150
+ overlayGroup: null,
151
+ isReady: false,
152
+ cachedProps: null,
153
+ isUserInteracting: false,
154
+ lastReportedCenter: null,
155
+ lastReportedZoom: null,
156
+ moveThrottleTimer: null,
157
+ currentLayersKey: '',
158
+ currentOverlaysKey: '',
159
+ };
160
+ },
161
+ mounted() {
162
+ ensureLeafletStyles();
163
+ this.initLeaflet();
164
+ },
165
+ beforeDestroy() {
166
+ if (this.moveThrottleTimer) {
167
+ clearTimeout(this.moveThrottleTimer);
168
+ this.moveThrottleTimer = null;
169
+ }
170
+ if (this.map) {
171
+ this.map.remove();
172
+ this.map = null;
173
+ }
174
+ },
175
+ methods: {
176
+ initLeaflet() {
177
+ if (this.map) return;
178
+ ensureLeafletStyles();
179
+
180
+ const id = (this.prop && this.prop.appMapId) || (this.cachedProps && this.cachedProps.appMapId);
181
+ const container = (id && document.getElementById(id)) || (this.$el && this.$el.querySelector ? this.$el.querySelector('.uni-leaflet-h5-container') : this.$el);
182
+ if (!container) {
183
+ setTimeout(() => this.initLeaflet(), 60);
184
+ return;
185
+ }
186
+
187
+ const p = this.cachedProps || this.prop || {};
188
+ const center = p.center || [39.9042, 116.4074];
189
+ const zoom = p.zoom || 13;
190
+ const minZoom = p.minZoom ?? 3;
191
+ const maxZoom = p.maxZoom ?? 18;
192
+
193
+ try {
194
+ this.map = L.map(container, {
195
+ center: [center[0], center[1]],
196
+ zoom,
197
+ minZoom,
198
+ maxZoom,
199
+ zoomControl: false,
200
+ attributionControl: false,
201
+ tap: false, // Avoid synthetic tap conflicts with pinch-to-zoom on mobile Webview
202
+ touchZoom: true, // Smooth native multi-touch pinch-to-zoom
203
+ bounceAtZoomLimits: false,
204
+ zoomAnimation: true,
205
+ fadeAnimation: true,
206
+ markerZoomAnimation: true,
207
+ });
208
+
209
+ try {
210
+ this.map.createPane('overlayTilePane');
211
+ const overlayPane = this.map.getPane('overlayTilePane');
212
+ if (overlayPane) {
213
+ overlayPane.style.zIndex = '350';
214
+ overlayPane.style.pointerEvents = 'none';
215
+ }
216
+ } catch (e) {}
217
+
218
+ this.overlayGroup = L.layerGroup().addTo(this.map);
219
+
220
+ // Track user drag/pinch gestures to prevent ping-pong feedback loop
221
+ this.map.on('movestart zoomstart dragstart', () => {
222
+ this.isUserInteracting = true;
223
+ });
224
+
225
+ this.map.on('move', () => {
226
+ const c = this.map.getCenter();
227
+ const z = this.map.getZoom();
228
+ this.lastReportedCenter = [c.lat, c.lng];
229
+ this.lastReportedZoom = z;
230
+
231
+ if (!this.moveThrottleTimer) {
232
+ this.moveThrottleTimer = setTimeout(() => {
233
+ this.moveThrottleTimer = null;
234
+ if (this.map) {
235
+ const cur = this.map.getCenter();
236
+ const curZ = this.map.getZoom();
237
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_MapMove', { center: [cur.lat, cur.lng], zoom: curZ });
238
+ }
239
+ }, 100);
240
+ }
241
+ });
242
+
243
+ this.map.on('moveend zoomend dragend', () => {
244
+ if (this.moveThrottleTimer) {
245
+ clearTimeout(this.moveThrottleTimer);
246
+ this.moveThrottleTimer = null;
247
+ }
248
+ const c = this.map.getCenter();
249
+ const z = this.map.getZoom();
250
+ this.lastReportedCenter = [c.lat, c.lng];
251
+ this.lastReportedZoom = z;
252
+
253
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_MapMoveEnd', { center: [c.lat, c.lng], zoom: z });
254
+
255
+ setTimeout(() => {
256
+ this.isUserInteracting = false;
257
+ }, 150);
258
+ });
259
+
260
+ this.map.on('zoom', () => {
261
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_MapZoom', this.map.getZoom());
262
+ });
263
+
264
+ this.map.on('zoomend', () => {
265
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_MapZoomEnd', this.map.getZoom());
266
+ });
267
+
268
+ this.map.on('click', (e) => {
269
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_MapClick', {
270
+ latLng: [e.latlng.lat, e.latlng.lng],
271
+ point: { x: e.containerPoint.x, y: e.containerPoint.y }
272
+ });
273
+ });
274
+
275
+ this.isReady = true;
276
+ this.applyProps(p);
277
+
278
+ // Invalidate size on multiple next frames to sync with Webview layout
279
+ if (typeof requestAnimationFrame !== 'undefined') {
280
+ requestAnimationFrame(() => {
281
+ if (this.map) this.map.invalidateSize(true);
282
+ });
283
+ }
284
+ setTimeout(() => {
285
+ if (this.map) this.map.invalidateSize(true);
286
+ }, 120);
287
+ setTimeout(() => {
288
+ if (this.map) this.map.invalidateSize(true);
289
+ }, 300);
290
+
291
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_Ready');
292
+ } catch (err) {
293
+ console.error('Leaflet init in renderjs error:', err);
294
+ }
295
+ },
296
+ updateMapProps(newVal) {
297
+ if (!newVal) return;
298
+ this.cachedProps = newVal;
299
+ if (!this.map || !this.isReady) {
300
+ this.initLeaflet();
301
+ return;
302
+ }
303
+ this.applyProps(newVal);
304
+ },
305
+ applyProps(p) {
306
+ if (!this.map || !p) return;
307
+
308
+ // Anti-feedback loop: Don't setView if the user is actively dragging/zooming or if this is an echo
309
+ if (p.center && p.zoom !== undefined) {
310
+ const curCenter = this.map.getCenter();
311
+ const curZoom = this.map.getZoom();
312
+
313
+ const isEcho =
314
+ this.lastReportedCenter &&
315
+ Math.abs(this.lastReportedCenter[0] - p.center[0]) < 1e-4 &&
316
+ Math.abs(this.lastReportedCenter[1] - p.center[1]) < 1e-4 &&
317
+ this.lastReportedZoom !== null &&
318
+ Math.abs(this.lastReportedZoom - p.zoom) < 0.1;
319
+
320
+ if (!this.isUserInteracting && !isEcho) {
321
+ const centerDiff =
322
+ Math.abs(curCenter.lat - p.center[0]) > 1e-4 ||
323
+ Math.abs(curCenter.lng - p.center[1]) > 1e-4;
324
+ const zoomDiff = Math.abs(curZoom - p.zoom) > 0.05;
325
+
326
+ if (centerDiff || zoomDiff) {
327
+ this.map.setView([p.center[0], p.center[1]], p.zoom, { animate: false });
328
+ }
329
+ }
330
+ }
331
+
332
+ // Only rebuild tile layers when tile URLs / configs actually change
333
+ const layersToApply = (p.layers && p.layers.length > 0)
334
+ ? p.layers
335
+ : (p.tileUrl ? [{ url: p.tileUrl, subdomains: p.subdomains }] : []);
336
+ const layersKey = JSON.stringify(layersToApply);
337
+ if (this.currentLayersKey !== layersKey) {
338
+ this.currentLayersKey = layersKey;
339
+ this.setTileLayers(layersToApply);
340
+ }
341
+
342
+ // Only rebuild vector overlays when overlay data actually changes
343
+ const overlaysToApply = {
344
+ markers: p.markers,
345
+ polylines: p.polylines,
346
+ polygons: p.polygons,
347
+ circles: p.circles,
348
+ ...(p.overlays || {}),
349
+ };
350
+ const overlaysKey = JSON.stringify(overlaysToApply);
351
+ if (this.currentOverlaysKey !== overlaysKey) {
352
+ this.currentOverlaysKey = overlaysKey;
353
+ this.renderOverlays(overlaysToApply);
354
+ }
355
+ },
356
+ setTileLayers(layers) {
357
+ for (const l of this.tileLayers) {
358
+ try {
359
+ this.map.removeLayer(l);
360
+ } catch (e) {}
361
+ }
362
+ this.tileLayers = [];
363
+
364
+ for (let i = 0; i < layers.length; i++) {
365
+ const cfg = typeof layers[i] === 'string' ? { url: layers[i] } : layers[i];
366
+ const subdomains = cfg.subdomains || ['a', 'b', 'c'];
367
+ const isOverlay = (cfg.zIndex && cfg.zIndex >= 10) || (i > 0);
368
+ const tile = L.tileLayer(cfg.url, {
369
+ subdomains,
370
+ opacity: cfg.opacity !== undefined ? cfg.opacity : 1,
371
+ zIndex: cfg.zIndex || (i + 1),
372
+ pane: isOverlay ? 'overlayTilePane' : 'tilePane',
373
+ tileSize: 256,
374
+ });
375
+ tile.addTo(this.map);
376
+ this.tileLayers.push(tile);
377
+ }
378
+ },
379
+ renderOverlays(overlays) {
380
+ if (!this.overlayGroup) return;
381
+ this.overlayGroup.clearLayers();
382
+
383
+ // Polygons
384
+ if (overlays.polygons) {
385
+ for (const poly of overlays.polygons) {
386
+ if (!poly.latLngs || poly.latLngs.length < 3) continue;
387
+ const lPoly = L.polygon(poly.latLngs, {
388
+ color: poly.color || '#ef4444',
389
+ weight: poly.width || 2,
390
+ fillColor: poly.fillColor || '#ef4444',
391
+ fillOpacity: poly.fillOpacity ?? 0.25,
392
+ interactive: true,
393
+ }).addTo(this.overlayGroup);
394
+ if (poly.label) {
395
+ const labelText = typeof poly.label === 'string' ? poly.label : poly.label.text;
396
+ lPoly.bindTooltip(labelText, { permanent: true, direction: 'center', className: 'uni-overlay-label' });
397
+ }
398
+ lPoly.on('click', (e) => {
399
+ if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
400
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_OverlayClick', { type: 'polygon', data: poly });
401
+ });
402
+ }
403
+ }
404
+
405
+ // Circles
406
+ if (overlays.circles) {
407
+ for (const c of overlays.circles) {
408
+ const lCircle = L.circle([c.latLng[0], c.latLng[1]], {
409
+ radius: c.radius,
410
+ color: c.color || '#10b981',
411
+ weight: c.width || 2,
412
+ fillColor: c.fillColor || c.color || '#10b981',
413
+ fillOpacity: c.fillOpacity ?? 0.2,
414
+ interactive: true,
415
+ }).addTo(this.overlayGroup);
416
+ if (c.label) {
417
+ const labelText = typeof c.label === 'string' ? c.label : c.label.text;
418
+ lCircle.bindTooltip(labelText, { permanent: true, direction: 'center', className: 'uni-overlay-label' });
419
+ }
420
+ lCircle.on('click', (e) => {
421
+ if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
422
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_OverlayClick', { type: 'circle', data: c });
423
+ });
424
+ }
425
+ }
426
+
427
+ // Polylines
428
+ if (overlays.polylines) {
429
+ for (const line of overlays.polylines) {
430
+ if (!line.latLngs || line.latLngs.length < 2) continue;
431
+ const lLine = L.polyline(line.latLngs, {
432
+ color: line.color || '#3b82f6',
433
+ weight: line.width || 4,
434
+ opacity: line.opacity ?? 1,
435
+ dashArray: line.dashArray ? line.dashArray.join(',') : undefined,
436
+ interactive: true,
437
+ }).addTo(this.overlayGroup);
438
+ if (line.label) {
439
+ const labelText = typeof line.label === 'string' ? line.label : line.label.text;
440
+ lLine.bindTooltip(labelText, { permanent: true, direction: 'center', className: 'uni-overlay-label' });
441
+ }
442
+ lLine.on('click', (e) => {
443
+ if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
444
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_OverlayClick', { type: 'polyline', data: line });
445
+ });
446
+ }
447
+ }
448
+
449
+ // Markers
450
+ if (overlays.markers) {
451
+ for (const m of overlays.markers) {
452
+ let icon;
453
+ const size = m.icon && m.icon.size ? m.icon.size : [32, 32];
454
+ const anchor = m.icon && m.icon.anchor ? m.icon.anchor : [size[0] / 2, size[1]];
455
+
456
+ const tooltipAnchor = [size[0] / 2 - anchor[0], -anchor[1]];
457
+
458
+ if (m.icon && m.icon.svg) {
459
+ let svgStr = m.icon.svg.trim();
460
+ if (svgStr.indexOf('xmlns=') === -1) {
461
+ svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
462
+ }
463
+ icon = L.divIcon({
464
+ className: 'uni-custom-svg-icon',
465
+ 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>',
466
+ iconSize: size,
467
+ iconAnchor: anchor,
468
+ tooltipAnchor: tooltipAnchor,
469
+ });
470
+ } else if (m.icon && m.icon.html) {
471
+ icon = L.divIcon({
472
+ className: 'uni-custom-html-icon',
473
+ html: m.icon.html,
474
+ iconSize: size,
475
+ iconAnchor: anchor,
476
+ tooltipAnchor: tooltipAnchor,
477
+ });
478
+ } else if (m.icon && m.icon.url) {
479
+ let finalUrl = m.icon.url;
480
+ if (finalUrl && typeof finalUrl === 'string' && !finalUrl.startsWith('data:') && !finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
481
+ // Strip leading slash so that local resources resolve relative to app bundle www root in App-Plus
482
+ if (finalUrl.startsWith('/')) {
483
+ finalUrl = finalUrl.slice(1);
484
+ }
485
+ }
486
+ 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));" />';
487
+ icon = L.divIcon({
488
+ className: 'uni-custom-img-icon',
489
+ html: html,
490
+ iconSize: size,
491
+ iconAnchor: anchor,
492
+ tooltipAnchor: tooltipAnchor,
493
+ });
494
+ } else if (m.icon && m.icon.text) {
495
+ const fontSize = Math.round(size[0] * 0.72);
496
+ icon = L.divIcon({
497
+ className: 'uni-emoji-marker-icon',
498
+ 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>',
499
+ iconSize: size,
500
+ iconAnchor: anchor,
501
+ tooltipAnchor: tooltipAnchor,
502
+ });
503
+ } else {
504
+ const pinColor = (m.icon && m.icon.color) ? m.icon.color : '#ef4444';
505
+ 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));">' +
506
+ '<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 + '"/>' +
507
+ '<circle cx="12" cy="11" r="4.5" fill="#ffffff"/>' +
508
+ '</svg>';
509
+ icon = L.divIcon({
510
+ html: html,
511
+ className: 'uni-default-pin',
512
+ iconSize: size,
513
+ iconAnchor: anchor,
514
+ tooltipAnchor: tooltipAnchor,
515
+ });
516
+ }
517
+
518
+ const lMarker = L.marker([m.latLng[0], m.latLng[1]], {
519
+ icon: icon,
520
+ interactive: true,
521
+ }).addTo(this.overlayGroup);
522
+ if (m.label) {
523
+ const labelText = typeof m.label === 'string' ? m.label : m.label.text;
524
+ const customOffset = typeof m.label === 'object' && m.label.offset ? m.label.offset : undefined;
525
+ let offset = [0, -2];
526
+ if (customOffset) {
527
+ const ox = customOffset[0] || 0;
528
+ const oy = customOffset[1] || 0;
529
+ if (Math.abs(oy) <= 10) {
530
+ offset = [ox, -2 + oy];
531
+ } else {
532
+ offset = [ox, -2];
533
+ }
534
+ }
535
+ lMarker.bindTooltip(labelText, {
536
+ permanent: true,
537
+ direction: 'top',
538
+ offset: offset,
539
+ className: 'uni-overlay-label',
540
+ });
541
+ }
542
+ lMarker.on('click', (e) => {
543
+ if (e && e.originalEvent) L.DomEvent.stopPropagation(e);
544
+ this.$ownerInstance && this.$ownerInstance.callMethod('onRender_OverlayClick', { type: 'marker', data: m });
545
+ });
546
+ }
547
+ }
548
+ }
549
+ }
550
+ };