uni-leaflet 1.1.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.
@@ -0,0 +1,101 @@
1
+ import type {
2
+ IMapEngine,
3
+ LatLngTuple,
4
+ Point,
5
+ TileLayerConfig,
6
+ MapOverlays,
7
+ OverlayClickEvent,
8
+ } from '../types';
9
+
10
+ // #ifdef H5
11
+ import { H5MapAdapter } from './h5-map-adapter';
12
+ // #endif
13
+
14
+ // #ifndef H5
15
+ import { CanvasTileEngine } from './canvas-tile-engine';
16
+ // #endif
17
+
18
+ export interface CreateEngineOptions {
19
+ center: LatLngTuple;
20
+ zoom: number;
21
+ minZoom: number;
22
+ maxZoom: number;
23
+ tileUrl: string;
24
+ subdomains: string[];
25
+ layers?: Array<string | TileLayerConfig>;
26
+ overlays?: MapOverlays;
27
+ showControls?: boolean;
28
+ onMove: (center: LatLngTuple, zoom: number) => void;
29
+ onMoveEnd: (center: LatLngTuple, zoom: number) => void;
30
+ onZoom: (zoom: number) => void;
31
+ onZoomEnd: (zoom: number) => void;
32
+ onClick: (latLng: LatLngTuple, point: Point) => void;
33
+ onOverlayClick?: (event: OverlayClickEvent) => void;
34
+
35
+ // H5 container
36
+ container?: HTMLElement | null;
37
+
38
+ // Mini-program / App canvas
39
+ canvas?: any;
40
+ ctx?: any;
41
+ width?: number;
42
+ height?: number;
43
+ dpr?: number;
44
+ }
45
+
46
+ /**
47
+ * Factory to create platform-specific map engine implementing unified IMapEngine
48
+ */
49
+ export async function createMapEngine(
50
+ options: CreateEngineOptions
51
+ ): Promise<IMapEngine> {
52
+ // #ifdef H5
53
+ const adapter = new H5MapAdapter();
54
+ if (!options.container) {
55
+ throw new Error('H5MapAdapter requires container DOM element');
56
+ }
57
+ await adapter.init({
58
+ container: options.container,
59
+ center: options.center,
60
+ zoom: options.zoom,
61
+ minZoom: options.minZoom,
62
+ maxZoom: options.maxZoom,
63
+ tileUrl: options.tileUrl,
64
+ subdomains: options.subdomains,
65
+ layers: options.layers,
66
+ overlays: options.overlays,
67
+ onMove: options.onMove,
68
+ onMoveEnd: options.onMoveEnd,
69
+ onZoom: options.onZoom,
70
+ onZoomEnd: options.onZoomEnd,
71
+ onClick: options.onClick,
72
+ onOverlayClick: options.onOverlayClick,
73
+ });
74
+ return adapter;
75
+ // #endif
76
+
77
+ // #ifndef H5
78
+ return new CanvasTileEngine({
79
+ canvas: options.canvas,
80
+ ctx: options.ctx,
81
+ width: options.width || 300,
82
+ height: options.height || 300,
83
+ dpr: options.dpr || 1,
84
+ center: options.center,
85
+ zoom: options.zoom,
86
+ minZoom: options.minZoom,
87
+ maxZoom: options.maxZoom,
88
+ tileUrl: options.tileUrl,
89
+ subdomains: options.subdomains,
90
+ layers: options.layers,
91
+ overlays: options.overlays,
92
+ showControls: options.showControls,
93
+ onMove: options.onMove,
94
+ onMoveEnd: options.onMoveEnd,
95
+ onZoom: options.onZoom,
96
+ onZoomEnd: options.onZoomEnd,
97
+ onClick: options.onClick,
98
+ onOverlayClick: options.onOverlayClick,
99
+ });
100
+ // #endif
101
+ }
@@ -0,0 +1,504 @@
1
+ import type {
2
+ IMapEngine,
3
+ LatLngTuple,
4
+ Point,
5
+ TileLayerConfig,
6
+ MarkerOptions,
7
+ PolylineOptions,
8
+ PolygonOptions,
9
+ CircleOptions,
10
+ MapOverlays,
11
+ OverlayClickEvent,
12
+ } from '../types';
13
+
14
+ export interface H5AdapterOptions {
15
+ container: HTMLElement;
16
+ center?: LatLngTuple;
17
+ zoom?: number;
18
+ minZoom?: number;
19
+ maxZoom?: number;
20
+ tileUrl?: string;
21
+ subdomains?: string[];
22
+ layers?: Array<string | TileLayerConfig>;
23
+ overlays?: MapOverlays;
24
+ onMove?: (center: LatLngTuple, zoom: number) => void;
25
+ onMoveEnd?: (center: LatLngTuple, zoom: number) => void;
26
+ onZoom?: (zoom: number) => void;
27
+ onZoomEnd?: (zoom: number) => void;
28
+ onClick?: (latLng: LatLngTuple, point: Point) => void;
29
+ onOverlayClick?: (event: OverlayClickEvent) => void;
30
+ }
31
+
32
+ import * as LModule from 'leaflet';
33
+ import 'leaflet/dist/leaflet.css';
34
+
35
+ function getL(): any {
36
+ if (LModule && typeof (LModule as any).map === 'function') {
37
+ return LModule;
38
+ }
39
+ if (
40
+ LModule &&
41
+ (LModule as any).default &&
42
+ typeof (LModule as any).default.map === 'function'
43
+ ) {
44
+ return (LModule as any).default;
45
+ }
46
+ if (
47
+ typeof window !== 'undefined' &&
48
+ (window as any).L &&
49
+ typeof (window as any).L.map === 'function'
50
+ ) {
51
+ return (window as any).L;
52
+ }
53
+ if (
54
+ typeof globalThis !== 'undefined' &&
55
+ (globalThis as any).L &&
56
+ typeof (globalThis as any).L.map === 'function'
57
+ ) {
58
+ return (globalThis as any).L;
59
+ }
60
+ return (LModule as any)?.default || LModule;
61
+ }
62
+
63
+ export class H5MapAdapter implements IMapEngine {
64
+ private map: any = null;
65
+ private tileLayers: any[] = [];
66
+ private overlayGroup: any = null;
67
+ private L: any = null;
68
+ private pendingLayers?: Array<string | TileLayerConfig>;
69
+ private pendingOverlays?: MapOverlays;
70
+
71
+ public onMove?: (center: LatLngTuple, zoom: number) => void;
72
+ public onMoveEnd?: (center: LatLngTuple, zoom: number) => void;
73
+ public onZoom?: (zoom: number) => void;
74
+ public onZoomEnd?: (zoom: number) => void;
75
+ public onClick?: (latLng: LatLngTuple, point: Point) => void;
76
+ public onOverlayClick?: (event: OverlayClickEvent) => void;
77
+
78
+ public async init(options: H5AdapterOptions): Promise<void> {
79
+ this.onMove = options.onMove;
80
+ this.onMoveEnd = options.onMoveEnd;
81
+ this.onZoom = options.onZoom;
82
+ this.onZoomEnd = options.onZoomEnd;
83
+ this.onClick = options.onClick;
84
+ this.onOverlayClick = options.onOverlayClick;
85
+
86
+ let L = getL();
87
+ if (!L || typeof L.map !== 'function') {
88
+ console.warn('[uni-leaflet] Leaflet map instance not found. Make sure leaflet is installed.');
89
+ }
90
+ this.L = L;
91
+
92
+ const center = options.center || [39.9042, 116.4074];
93
+ const zoom = options.zoom || 13;
94
+ const minZoom = options.minZoom ?? 3;
95
+ const maxZoom = options.maxZoom ?? 18;
96
+
97
+ this.map = L.map(options.container, {
98
+ center: [center[0], center[1]],
99
+ zoom,
100
+ minZoom,
101
+ maxZoom,
102
+ zoomControl: false, // We provide unified UI controls
103
+ attributionControl: false,
104
+ });
105
+
106
+ // Create a dedicated overlay pane for annotation / label layers above tilePane
107
+ try {
108
+ this.map.createPane('overlayTilePane');
109
+ const overlayPane = this.map.getPane('overlayTilePane');
110
+ if (overlayPane) {
111
+ overlayPane.style.zIndex = '350';
112
+ overlayPane.style.pointerEvents = 'none';
113
+ }
114
+ } catch (e) {}
115
+
116
+ // Create overlay layer group for markers, lines, polygons
117
+ this.overlayGroup = L.layerGroup().addTo(this.map);
118
+
119
+ const initialLayers =
120
+ this.pendingLayers ||
121
+ options.layers ||
122
+ [
123
+ {
124
+ url:
125
+ options.tileUrl ||
126
+ 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
127
+ subdomains: options.subdomains || ['a', 'b', 'c'],
128
+ },
129
+ ];
130
+
131
+ this.setLayers(initialLayers);
132
+
133
+ if (this.pendingOverlays || options.overlays) {
134
+ this.setOverlays(this.pendingOverlays || options.overlays || {});
135
+ }
136
+
137
+ // Bind Leaflet events
138
+ this.map.on('move', () => {
139
+ const c = this.map.getCenter();
140
+ const z = this.map.getZoom();
141
+ this.onMove?.([c.lat, c.lng], z);
142
+ });
143
+
144
+ this.map.on('moveend', () => {
145
+ const c = this.map.getCenter();
146
+ const z = this.map.getZoom();
147
+ this.onMoveEnd?.([c.lat, c.lng], z);
148
+ });
149
+
150
+ this.map.on('zoom', () => {
151
+ const z = this.map.getZoom();
152
+ this.onZoom?.(z);
153
+ });
154
+
155
+ this.map.on('zoomend', () => {
156
+ const z = this.map.getZoom();
157
+ this.onZoomEnd?.(z);
158
+ });
159
+
160
+ this.map.on('click', (e: any) => {
161
+ this.onClick?.([e.latlng.lat, e.latlng.lng], {
162
+ x: e.containerPoint.x,
163
+ y: e.containerPoint.y,
164
+ });
165
+ });
166
+
167
+ // Invalidate size in next tick to avoid tile rendering glitches
168
+ setTimeout(() => {
169
+ this.map?.invalidateSize();
170
+ }, 100);
171
+ }
172
+
173
+ public setCenter(center: LatLngTuple, animate = true) {
174
+ if (!this.map) return;
175
+ if (animate) {
176
+ this.map.panTo([center[0], center[1]]);
177
+ } else {
178
+ this.map.setView([center[0], center[1]], this.map.getZoom());
179
+ }
180
+ }
181
+
182
+ public setZoom(zoom: number) {
183
+ if (!this.map) return;
184
+ this.map.setZoom(zoom);
185
+ }
186
+
187
+ public zoomIn() {
188
+ if (!this.map) return;
189
+ this.map.zoomIn();
190
+ }
191
+
192
+ public zoomOut() {
193
+ if (!this.map) return;
194
+ this.map.zoomOut();
195
+ }
196
+
197
+ public panTo(center: LatLngTuple, duration?: number) {
198
+ if (!this.map) return;
199
+ this.map.panTo([center[0], center[1]], {
200
+ duration: duration ? duration / 1000 : 0.5,
201
+ });
202
+ }
203
+
204
+ public setTileUrl(url: string, subdomains?: string[]) {
205
+ this.setLayers([{ url, subdomains }]);
206
+ }
207
+
208
+ public setLayers(layers: Array<string | TileLayerConfig>) {
209
+ if (!this.map || !this.L) {
210
+ this.pendingLayers = layers;
211
+ return;
212
+ }
213
+
214
+ // Remove existing tile layers
215
+ for (const layer of this.tileLayers) {
216
+ try {
217
+ this.map.removeLayer(layer);
218
+ } catch (e) {}
219
+ }
220
+ this.tileLayers = [];
221
+
222
+ // Add new layers
223
+ let index = 0;
224
+ for (const l of layers) {
225
+ const cfg = typeof l === 'string' ? { url: l } : l;
226
+ if (!cfg.url) continue;
227
+
228
+ // Base layer in tilePane (zIndex 200), overlays in overlayTilePane (zIndex 350)
229
+ const isBase = index === 0;
230
+ const pane = isBase ? 'tilePane' : 'overlayTilePane';
231
+ const zIndex = cfg.zIndex ?? (index + 1) * 10;
232
+
233
+ const tileLayer = this.L.tileLayer(cfg.url, {
234
+ subdomains: cfg.subdomains || ['a', 'b', 'c', 'd'],
235
+ opacity: cfg.opacity ?? 1,
236
+ zIndex,
237
+ pane,
238
+ minZoom: cfg.minZoom ?? this.map.getMinZoom?.() ?? 3,
239
+ maxZoom: cfg.maxZoom ?? this.map.getMaxZoom?.() ?? 18,
240
+ }).addTo(this.map);
241
+
242
+ if (typeof tileLayer.setZIndex === 'function') {
243
+ tileLayer.setZIndex(zIndex);
244
+ }
245
+
246
+ this.tileLayers.push(tileLayer);
247
+ index++;
248
+ }
249
+ }
250
+
251
+ // --- Vector Overlays (Markers, Polylines, Polygons, Circles) ---
252
+
253
+ public setOverlays(overlays: MapOverlays) {
254
+ if (!this.map || !this.L || !this.overlayGroup) {
255
+ this.pendingOverlays = overlays;
256
+ return;
257
+ }
258
+
259
+ this.clearOverlays();
260
+
261
+ if (overlays.polygons) {
262
+ this.setPolygons(overlays.polygons);
263
+ }
264
+ if (overlays.polylines) {
265
+ this.setPolylines(overlays.polylines);
266
+ }
267
+ if (overlays.circles) {
268
+ this.setCircles(overlays.circles);
269
+ }
270
+ if (overlays.markers) {
271
+ this.setMarkers(overlays.markers);
272
+ }
273
+ }
274
+
275
+ public setMarkers(markers: MarkerOptions[]) {
276
+ if (!this.map || !this.L || !this.overlayGroup) return;
277
+
278
+ for (const m of markers) {
279
+ let icon = undefined;
280
+ const size = m.icon?.size || [32, 32];
281
+ const anchor = m.icon?.anchor || [size[0] / 2, size[1]];
282
+ const tooltipAnchor: [number, number] = [
283
+ size[0] / 2 - anchor[0],
284
+ -anchor[1],
285
+ ];
286
+
287
+ if (m.icon?.svg) {
288
+ let svgStr = m.icon.svg.trim();
289
+ if (!svgStr.includes('xmlns=')) {
290
+ svgStr = svgStr.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
291
+ }
292
+ const html = `<div style="width:${size[0]}px;height:${size[1]}px;display:flex;align-items:center;justify-content:center;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">${svgStr}</div>`;
293
+ icon = this.L.divIcon({
294
+ html,
295
+ className: 'uni-custom-svg-icon',
296
+ iconSize: size,
297
+ iconAnchor: anchor,
298
+ tooltipAnchor,
299
+ });
300
+ } else if (m.icon?.html) {
301
+ icon = this.L.divIcon({
302
+ html: m.icon.html,
303
+ className: 'uni-custom-html-icon',
304
+ iconSize: size,
305
+ iconAnchor: anchor,
306
+ tooltipAnchor,
307
+ });
308
+ } else if (m.icon?.url) {
309
+ let finalUrl = m.icon.url;
310
+ if (finalUrl && typeof finalUrl === 'string' && !finalUrl.startsWith('data:') && !finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
311
+ if (finalUrl.startsWith('/')) {
312
+ finalUrl = finalUrl.slice(1);
313
+ }
314
+ }
315
+ const html = `<img src="${finalUrl}" style="width:${size[0]}px;height:${size[1]}px;display:block;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));" />`;
316
+ icon = this.L.divIcon({
317
+ className: 'uni-custom-img-icon',
318
+ html,
319
+ iconSize: size,
320
+ iconAnchor: anchor,
321
+ tooltipAnchor,
322
+ });
323
+ } else if (m.icon?.text) {
324
+ const fontSize = Math.round(size[0] * 0.72);
325
+ const html = `<div style="display:flex;align-items:center;justify-content:center;width:${size[0]}px;height:${size[1]}px;font-size:${fontSize}px;filter:drop-shadow(0 2px 4px rgba(0,0,0,0.3));">${m.icon.text}</div>`;
326
+ icon = this.L.divIcon({
327
+ html,
328
+ className: 'uni-custom-icon',
329
+ iconSize: size,
330
+ iconAnchor: anchor,
331
+ tooltipAnchor,
332
+ });
333
+ } else {
334
+ // Sleek default SVG Pin
335
+ const pinColor = m.icon?.color || '#ef4444';
336
+ const html = `
337
+ <svg viewBox="0 0 24 32" width="${size[0]}" height="${size[1]}" style="filter:drop-shadow(0 2px 4px rgba(0,0,0,0.35));">
338
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 9 12 20 12 20s12-11 12-20c0-6.63-5.37-12-12-12z" fill="${pinColor}"/>
339
+ <circle cx="12" cy="11" r="4.5" fill="#ffffff"/>
340
+ </svg>`;
341
+ icon = this.L.divIcon({
342
+ html,
343
+ className: 'uni-default-pin',
344
+ iconSize: size,
345
+ iconAnchor: anchor,
346
+ tooltipAnchor,
347
+ });
348
+ }
349
+
350
+ const leafletMarker = this.L.marker([m.latLng[0], m.latLng[1]], {
351
+ icon,
352
+ title: m.title,
353
+ }).addTo(this.overlayGroup);
354
+
355
+ // Label tooltip
356
+ if (m.label) {
357
+ const labelText = typeof m.label === 'string' ? m.label : m.label.text;
358
+ const customOffset =
359
+ typeof m.label === 'object' && m.label.offset
360
+ ? m.label.offset
361
+ : undefined;
362
+
363
+ let offset: [number, number] = [0, 0];
364
+ if (customOffset) {
365
+ const ox = customOffset[0] || 0;
366
+ const oy = customOffset[1] || 0;
367
+ if (Math.abs(oy) <= 10) {
368
+ offset = [ox, oy];
369
+ } else {
370
+ offset = [ox, 0];
371
+ }
372
+ }
373
+
374
+ leafletMarker.bindTooltip(labelText, {
375
+ permanent: true,
376
+ direction: 'top',
377
+ offset,
378
+ className: 'uni-overlay-label',
379
+ });
380
+ }
381
+
382
+ leafletMarker.on('click', () => {
383
+ this.onOverlayClick?.({ type: 'marker', data: m });
384
+ });
385
+ }
386
+ }
387
+
388
+ public setPolylines(polylines: PolylineOptions[]) {
389
+ if (!this.map || !this.L || !this.overlayGroup) return;
390
+
391
+ for (const p of polylines) {
392
+ const leafletLine = this.L.polyline(p.latLngs, {
393
+ color: p.color || '#3b82f6',
394
+ weight: p.width || 4,
395
+ opacity: p.opacity ?? 1,
396
+ dashArray: p.dashArray?.join(','),
397
+ }).addTo(this.overlayGroup);
398
+
399
+ if (p.label) {
400
+ const labelText = typeof p.label === 'string' ? p.label : p.label.text;
401
+ leafletLine.bindTooltip(labelText, {
402
+ permanent: true,
403
+ direction: 'center',
404
+ className: 'uni-overlay-label',
405
+ });
406
+ }
407
+
408
+ leafletLine.on('click', () => {
409
+ this.onOverlayClick?.({ type: 'polyline', data: p });
410
+ });
411
+ }
412
+ }
413
+
414
+ public setPolygons(polygons: PolygonOptions[]) {
415
+ if (!this.map || !this.L || !this.overlayGroup) return;
416
+
417
+ for (const poly of polygons) {
418
+ const leafletPolygon = this.L.polygon(poly.latLngs, {
419
+ color: poly.color || '#ef4444',
420
+ weight: poly.width || 2,
421
+ fillColor: poly.fillColor || poly.color || '#ef4444',
422
+ fillOpacity: poly.fillOpacity ?? 0.25,
423
+ }).addTo(this.overlayGroup);
424
+
425
+ if (poly.label) {
426
+ const labelText =
427
+ typeof poly.label === 'string' ? poly.label : poly.label.text;
428
+ leafletPolygon.bindTooltip(labelText, {
429
+ permanent: true,
430
+ direction: 'center',
431
+ className: 'uni-overlay-label',
432
+ });
433
+ }
434
+
435
+ leafletPolygon.on('click', () => {
436
+ this.onOverlayClick?.({ type: 'polygon', data: poly });
437
+ });
438
+ }
439
+ }
440
+
441
+ public setCircles(circles: CircleOptions[]) {
442
+ if (!this.map || !this.L || !this.overlayGroup) return;
443
+
444
+ for (const c of circles) {
445
+ const leafletCircle = this.L.circle([c.latLng[0], c.latLng[1]], {
446
+ radius: c.radius,
447
+ color: c.color || '#10b981',
448
+ weight: c.width || 2,
449
+ fillColor: c.fillColor || c.color || '#10b981',
450
+ fillOpacity: c.fillOpacity ?? 0.2,
451
+ }).addTo(this.overlayGroup);
452
+
453
+ if (c.label) {
454
+ const labelText = typeof c.label === 'string' ? c.label : c.label.text;
455
+ leafletCircle.bindTooltip(labelText, {
456
+ permanent: true,
457
+ direction: 'center',
458
+ className: 'uni-overlay-label',
459
+ });
460
+ }
461
+
462
+ leafletCircle.on('click', () => {
463
+ this.onOverlayClick?.({ type: 'circle', data: c });
464
+ });
465
+ }
466
+ }
467
+
468
+ public clearOverlays() {
469
+ if (this.overlayGroup) {
470
+ this.overlayGroup.clearLayers();
471
+ }
472
+ }
473
+
474
+ public getCenter(): LatLngTuple {
475
+ if (!this.map) return [0, 0];
476
+ const c = this.map.getCenter();
477
+ return [c.lat, c.lng];
478
+ }
479
+
480
+ public getZoom(): number {
481
+ return this.map ? this.map.getZoom() : 0;
482
+ }
483
+
484
+ public getLeafletInstance(): any {
485
+ return this.map;
486
+ }
487
+
488
+ public getNativeInstance(): any {
489
+ return this.map;
490
+ }
491
+
492
+ public resize() {
493
+ this.map?.invalidateSize();
494
+ }
495
+
496
+ public destroy() {
497
+ if (this.map) {
498
+ this.map.remove();
499
+ this.map = null;
500
+ this.tileLayers = [];
501
+ this.overlayGroup = null;
502
+ }
503
+ }
504
+ }