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