@squirkle-ui/map 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +41 -2
- package/dist/styles.css +80 -0
- package/dist/types/SqMap.vue.d.ts +535 -0
- package/dist/types/adapters.d.ts +44 -0
- package/dist/types/cluster.d.ts +40 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/markers.d.ts +26 -0
- package/dist/vue2/index.cjs +459 -0
- package/dist/vue2/index.cjs.map +1 -0
- package/dist/vue2/index.mjs +437 -0
- package/dist/vue2/index.mjs.map +1 -0
- package/dist/vue3/index.cjs +522 -0
- package/dist/vue3/index.cjs.map +1 -0
- package/dist/vue3/index.mjs +500 -0
- package/dist/vue3/index.mjs.map +1 -0
- package/dist/vue3/styles.css +80 -0
- package/package.json +75 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BBox } from './cluster';
|
|
2
|
+
export type LatLng = [number, number];
|
|
3
|
+
export interface MarkerHandle {
|
|
4
|
+
remove: () => void;
|
|
5
|
+
}
|
|
6
|
+
export interface MapAdapter {
|
|
7
|
+
readonly engine: 'vector' | 'raster';
|
|
8
|
+
/** The underlying maplibregl.Map or L.Map. */
|
|
9
|
+
readonly native: unknown;
|
|
10
|
+
getCenter: () => LatLng;
|
|
11
|
+
getZoom: () => number;
|
|
12
|
+
getBounds: () => BBox;
|
|
13
|
+
setView: (center: LatLng, zoom: number, animate?: boolean) => void;
|
|
14
|
+
fitPoints: (points: LatLng[], padding: number, maxZoom: number) => void;
|
|
15
|
+
zoomIn: () => void;
|
|
16
|
+
zoomOut: () => void;
|
|
17
|
+
addMarker: (el: HTMLElement, at: LatLng, anchor: 'bottom' | 'center') => MarkerHandle;
|
|
18
|
+
openPopup: (at: LatLng, content: HTMLElement, offset: number) => void;
|
|
19
|
+
setStyle: (style: string | object) => void;
|
|
20
|
+
on: (event: 'moveend' | 'click' | 'error', cb: (payload?: unknown) => void) => void;
|
|
21
|
+
destroy: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Where MapLibre loads its web worker from. Bundlers don't copy `maplibre-gl-worker.mjs` on their own, so apps pass
|
|
25
|
+
* a bundler-resolved URL, e.g. `import url from 'maplibre-gl/dist/maplibre-gl-worker.mjs?url'` (Vite). Applied lazily.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setMapWorkerUrl(url: string | null): void;
|
|
28
|
+
/** Keyless vector styles (verified 2026-10-24). */
|
|
29
|
+
export declare const MAP_STYLES: Record<string, string>;
|
|
30
|
+
/** `auto` follows the app theme; names map to MAP_STYLES; anything else is passed through (URL or style object). */
|
|
31
|
+
export declare function resolveMapStyle(style: string | object, dark: boolean): string | object;
|
|
32
|
+
export declare function createVectorAdapter(el: HTMLElement, opts: {
|
|
33
|
+
center: LatLng;
|
|
34
|
+
zoom: number;
|
|
35
|
+
style: string | object;
|
|
36
|
+
scrollZoom: boolean;
|
|
37
|
+
}): Promise<MapAdapter>;
|
|
38
|
+
export declare function createRasterAdapter(el: HTMLElement, opts: {
|
|
39
|
+
center: LatLng;
|
|
40
|
+
zoom: number;
|
|
41
|
+
tileUrl: string;
|
|
42
|
+
attribution: string;
|
|
43
|
+
scrollZoom: boolean;
|
|
44
|
+
}): Promise<MapAdapter>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Engine-independent marker clustering (supercluster). Pure functions — unit-tested in test/cluster.test.ts. */
|
|
2
|
+
import Supercluster from 'supercluster';
|
|
3
|
+
export interface ClusterOptions {
|
|
4
|
+
/** Cluster radius in screen pixels (default 60). */
|
|
5
|
+
radius?: number;
|
|
6
|
+
/** Zoom level above which points are never clustered (default 16). */
|
|
7
|
+
maxZoom?: number;
|
|
8
|
+
/** Minimum points to form a cluster (default 2). */
|
|
9
|
+
minPoints?: number;
|
|
10
|
+
}
|
|
11
|
+
export type ClusterIndex = Supercluster<{
|
|
12
|
+
i: number;
|
|
13
|
+
}>;
|
|
14
|
+
export type ClusterItem = {
|
|
15
|
+
kind: 'cluster';
|
|
16
|
+
id: number;
|
|
17
|
+
lat: number;
|
|
18
|
+
lng: number;
|
|
19
|
+
count: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'point';
|
|
22
|
+
index: number;
|
|
23
|
+
lat: number;
|
|
24
|
+
lng: number;
|
|
25
|
+
};
|
|
26
|
+
/** West, south, east, north in degrees. */
|
|
27
|
+
export type BBox = [number, number, number, number];
|
|
28
|
+
/** `tileSize` is the engine's tile size in px (MapLibre 512, Leaflet 256) so `radius` stays in screen pixels. */
|
|
29
|
+
export declare function createClusterIndex(points: {
|
|
30
|
+
lat: number;
|
|
31
|
+
lng: number;
|
|
32
|
+
}[], opts?: ClusterOptions, tileSize?: number): ClusterIndex;
|
|
33
|
+
/** Clusters and single points visible in `bbox` at `zoom`. */
|
|
34
|
+
export declare function clustersFor(index: ClusterIndex, bbox: BBox, zoom: number): ClusterItem[];
|
|
35
|
+
/** Zoom level at which a cluster splits apart. */
|
|
36
|
+
export declare const expansionZoom: (index: ClusterIndex, clusterId: number) => number;
|
|
37
|
+
/** Indices (into the original points array) of every point inside a cluster. */
|
|
38
|
+
export declare const clusterLeaves: (index: ClusterIndex, clusterId: number) => number[];
|
|
39
|
+
/** Grows a bbox by `ratio` on each side so markers just outside the view are ready before they scroll in. */
|
|
40
|
+
export declare function padBBox([w, s, e, n]: BBox, ratio?: number): BBox;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import SqMap from './SqMap.vue';
|
|
2
|
+
export { SqMap };
|
|
3
|
+
export { MAP_STYLES, resolveMapStyle, setMapWorkerUrl, createVectorAdapter, createRasterAdapter, type MapAdapter, type LatLng } from './adapters';
|
|
4
|
+
export { markerElement, clusterElement, type MapMarker, type MarkerShape } from './markers';
|
|
5
|
+
export { createClusterIndex, clustersFor, clusterLeaves, expansionZoom, type ClusterOptions, type ClusterItem } from './cluster';
|
|
6
|
+
export declare const componentNames: readonly ["SqMap"];
|
|
7
|
+
export default SqMap;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type MarkerShape = 'pin' | 'dot' | 'squircle' | 'avatar';
|
|
2
|
+
export interface MapMarker {
|
|
3
|
+
id?: string | number;
|
|
4
|
+
lat: number;
|
|
5
|
+
lng: number;
|
|
6
|
+
title?: string;
|
|
7
|
+
popup?: string;
|
|
8
|
+
/** Theme color name or CSS color. */
|
|
9
|
+
color?: string;
|
|
10
|
+
/** Short text inside the marker (e.g. "3", "$"). */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Built-in or Iconify icon name, e.g. `lucide:coffee`. */
|
|
13
|
+
icon?: string;
|
|
14
|
+
/** Image URL for the `avatar` shape. */
|
|
15
|
+
image?: string;
|
|
16
|
+
shape?: MarkerShape;
|
|
17
|
+
/** Marker size in px (default 30). */
|
|
18
|
+
size?: number;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** Anchor point used by the engines: pins stand on their tip, other shapes are centred. */
|
|
22
|
+
export declare const markerAnchor: (shape: MarkerShape) => "bottom" | "center";
|
|
23
|
+
/** Builds a marker element with DOM APIs (text is never parsed as HTML). */
|
|
24
|
+
export declare function markerElement(m: MapMarker, defaults?: Partial<MapMarker>): HTMLElement;
|
|
25
|
+
/** Squircle bubble sized by the number of points it holds. */
|
|
26
|
+
export declare function clusterElement(count: number, color?: string): HTMLElement;
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
25
|
+
const vue = require("vue");
|
|
26
|
+
const core = require("@squirkle-ui/core");
|
|
27
|
+
const ui = require("@squirkle-ui/ui/vue2");
|
|
28
|
+
const Supercluster = require("supercluster");
|
|
29
|
+
let workerUrl = null;
|
|
30
|
+
function setMapWorkerUrl(url) {
|
|
31
|
+
workerUrl = url;
|
|
32
|
+
}
|
|
33
|
+
const MAP_STYLES = {
|
|
34
|
+
positron: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
|
|
35
|
+
voyager: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",
|
|
36
|
+
"dark-matter": "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
|
|
37
|
+
liberty: "https://tiles.openfreemap.org/styles/liberty",
|
|
38
|
+
bright: "https://tiles.openfreemap.org/styles/bright",
|
|
39
|
+
"openfreemap-dark": "https://tiles.openfreemap.org/styles/dark",
|
|
40
|
+
fiord: "https://tiles.openfreemap.org/styles/fiord"
|
|
41
|
+
};
|
|
42
|
+
function resolveMapStyle(style, dark) {
|
|
43
|
+
var _a;
|
|
44
|
+
if (typeof style !== "string") return style;
|
|
45
|
+
if (style === "auto") return dark ? MAP_STYLES["dark-matter"] : MAP_STYLES.positron;
|
|
46
|
+
return (_a = MAP_STYLES[style]) != null ? _a : style;
|
|
47
|
+
}
|
|
48
|
+
const defaultOf = (mod) => {
|
|
49
|
+
var _a;
|
|
50
|
+
return (_a = mod.default) != null ? _a : mod;
|
|
51
|
+
};
|
|
52
|
+
async function createVectorAdapter(el, opts) {
|
|
53
|
+
const [mod] = await Promise.all([import("maplibre-gl"), import("maplibre-gl/dist/maplibre-gl.css")]);
|
|
54
|
+
const ml = defaultOf(mod);
|
|
55
|
+
if (workerUrl) ml.setWorkerUrl(workerUrl);
|
|
56
|
+
const map = new ml.Map({
|
|
57
|
+
container: el,
|
|
58
|
+
style: opts.style,
|
|
59
|
+
center: [opts.center[1], opts.center[0]],
|
|
60
|
+
zoom: opts.zoom,
|
|
61
|
+
attributionControl: { compact: true },
|
|
62
|
+
scrollZoom: opts.scrollZoom,
|
|
63
|
+
cooperativeGestures: false
|
|
64
|
+
});
|
|
65
|
+
let popup = null;
|
|
66
|
+
return {
|
|
67
|
+
engine: "vector",
|
|
68
|
+
native: map,
|
|
69
|
+
getCenter: () => {
|
|
70
|
+
const c = map.getCenter();
|
|
71
|
+
return [c.lat, c.lng];
|
|
72
|
+
},
|
|
73
|
+
getZoom: () => map.getZoom(),
|
|
74
|
+
getBounds: () => {
|
|
75
|
+
const b = map.getBounds();
|
|
76
|
+
return [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()];
|
|
77
|
+
},
|
|
78
|
+
setView: (c, z, animate) => animate ? map.easeTo({ center: [c[1], c[0]], zoom: z, duration: 450 }) : map.jumpTo({ center: [c[1], c[0]], zoom: z }),
|
|
79
|
+
fitPoints: (pts, padding, maxZoom) => {
|
|
80
|
+
if (!pts.length) return;
|
|
81
|
+
const b = new ml.LngLatBounds([pts[0][1], pts[0][0]], [pts[0][1], pts[0][0]]);
|
|
82
|
+
pts.forEach((p) => b.extend([p[1], p[0]]));
|
|
83
|
+
map.fitBounds(b, { padding, maxZoom, duration: 0 });
|
|
84
|
+
},
|
|
85
|
+
zoomIn: () => map.zoomIn(),
|
|
86
|
+
zoomOut: () => map.zoomOut(),
|
|
87
|
+
addMarker: (element, at, anchor) => {
|
|
88
|
+
const m = new ml.Marker({ element, anchor }).setLngLat([at[1], at[0]]).addTo(map);
|
|
89
|
+
return { remove: () => m.remove() };
|
|
90
|
+
},
|
|
91
|
+
openPopup: (at, content, offset) => {
|
|
92
|
+
popup == null ? void 0 : popup.remove();
|
|
93
|
+
popup = new ml.Popup({ offset, closeButton: false, className: "sq-map__popup", maxWidth: "280px" }).setLngLat([at[1], at[0]]).setDOMContent(content).addTo(map);
|
|
94
|
+
},
|
|
95
|
+
setStyle: (style) => map.setStyle(style),
|
|
96
|
+
on: (event, cb) => {
|
|
97
|
+
if (event === "click") map.on("click", (e) => cb({ lat: e.lngLat.lat, lng: e.lngLat.lng }));
|
|
98
|
+
else if (event === "error") map.on("error", (e) => {
|
|
99
|
+
var _a;
|
|
100
|
+
return cb((_a = e.error) != null ? _a : e);
|
|
101
|
+
});
|
|
102
|
+
else map.on("moveend", () => cb());
|
|
103
|
+
},
|
|
104
|
+
destroy: () => map.remove()
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function createRasterAdapter(el, opts) {
|
|
108
|
+
const [mod] = await Promise.all([import("leaflet"), import("leaflet/dist/leaflet.css")]);
|
|
109
|
+
const L = defaultOf(mod);
|
|
110
|
+
const map = L.map(el, { center: opts.center, zoom: opts.zoom, zoomControl: false, scrollWheelZoom: opts.scrollZoom });
|
|
111
|
+
const tiles = L.tileLayer(opts.tileUrl, { attribution: opts.attribution, maxZoom: 19 }).addTo(map);
|
|
112
|
+
return {
|
|
113
|
+
engine: "raster",
|
|
114
|
+
native: map,
|
|
115
|
+
getCenter: () => {
|
|
116
|
+
const c = map.getCenter();
|
|
117
|
+
return [c.lat, c.lng];
|
|
118
|
+
},
|
|
119
|
+
getZoom: () => map.getZoom(),
|
|
120
|
+
getBounds: () => {
|
|
121
|
+
const b = map.getBounds();
|
|
122
|
+
return [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()];
|
|
123
|
+
},
|
|
124
|
+
setView: (c, z, animate) => map.setView(c, z, { animate: !!animate }),
|
|
125
|
+
fitPoints: (pts, padding, maxZoom) => pts.length && map.fitBounds(L.latLngBounds(pts), { padding: [padding, padding], maxZoom }),
|
|
126
|
+
zoomIn: () => map.zoomIn(),
|
|
127
|
+
zoomOut: () => map.zoomOut(),
|
|
128
|
+
addMarker: (element, at, anchor) => {
|
|
129
|
+
const wrap = document.createElement("div");
|
|
130
|
+
wrap.className = `sq-map__anchor sq-map__anchor--${anchor}`;
|
|
131
|
+
wrap.appendChild(element);
|
|
132
|
+
const m = L.marker(at, { icon: L.divIcon({ html: wrap, className: "sq-map__marker-wrap", iconSize: [0, 0] }), keyboard: false }).addTo(map);
|
|
133
|
+
return { remove: () => m.remove() };
|
|
134
|
+
},
|
|
135
|
+
openPopup: (at, content, offset) => {
|
|
136
|
+
L.popup({ className: "sq-map__popup", offset: [0, -offset], closeButton: false }).setLatLng(at).setContent(content).openOn(map);
|
|
137
|
+
},
|
|
138
|
+
setStyle: (style) => typeof style === "string" && tiles.setUrl(style),
|
|
139
|
+
on: (event, cb) => {
|
|
140
|
+
if (event === "click") map.on("click", (e) => cb({ lat: e.latlng.lat, lng: e.latlng.lng }));
|
|
141
|
+
else if (event === "error") tiles.on("tileerror", (e) => cb(e));
|
|
142
|
+
else map.on("moveend", () => cb());
|
|
143
|
+
},
|
|
144
|
+
destroy: () => map.remove()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const markerAnchor = (shape) => shape === "pin" ? "bottom" : "center";
|
|
148
|
+
function iconNode(icon) {
|
|
149
|
+
const r = ui.resolveIcon(icon);
|
|
150
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
151
|
+
svg.setAttribute("aria-hidden", "true");
|
|
152
|
+
if (r.kind === "path") {
|
|
153
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
154
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
155
|
+
path.setAttribute("d", r.d);
|
|
156
|
+
svg.appendChild(path);
|
|
157
|
+
return svg;
|
|
158
|
+
}
|
|
159
|
+
if (r.kind === "svg") {
|
|
160
|
+
svg.setAttribute("viewBox", `0 0 ${r.data.width} ${r.data.height}`);
|
|
161
|
+
svg.innerHTML = r.data.body;
|
|
162
|
+
return svg;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function markerElement(m, defaults = {}) {
|
|
167
|
+
var _a, _b, _c, _d, _e, _f;
|
|
168
|
+
const o = { ...defaults, ...m };
|
|
169
|
+
const shape = (_a = o.shape) != null ? _a : "pin";
|
|
170
|
+
const el = document.createElement("div");
|
|
171
|
+
el.className = `sq-map-marker sq-map-marker--${shape}`;
|
|
172
|
+
el.style.setProperty("--sq-pin", (_c = ui.colorValue((_b = o.color) != null ? _b : "primary")) != null ? _c : "var(--sq-primary)");
|
|
173
|
+
el.style.setProperty("--sq-pin-size", `${(_d = o.size) != null ? _d : 30}px`);
|
|
174
|
+
el.setAttribute("role", "button");
|
|
175
|
+
el.setAttribute("tabindex", "0");
|
|
176
|
+
el.setAttribute("aria-label", (_f = (_e = o.title) != null ? _e : o.label) != null ? _f : "Marker");
|
|
177
|
+
const body = document.createElement("span");
|
|
178
|
+
body.className = "sq-map-marker__body";
|
|
179
|
+
if (shape === "avatar" && o.image) {
|
|
180
|
+
const img = document.createElement("img");
|
|
181
|
+
img.src = o.image;
|
|
182
|
+
img.alt = "";
|
|
183
|
+
img.loading = "lazy";
|
|
184
|
+
body.appendChild(img);
|
|
185
|
+
} else if (shape !== "dot") {
|
|
186
|
+
const icon = o.icon ? iconNode(o.icon) : null;
|
|
187
|
+
const content = document.createElement("span");
|
|
188
|
+
content.className = "sq-map-marker__content";
|
|
189
|
+
if (icon) content.appendChild(icon);
|
|
190
|
+
else if (o.label) content.textContent = String(o.label);
|
|
191
|
+
else content.classList.add("sq-map-marker__content--empty");
|
|
192
|
+
body.appendChild(content);
|
|
193
|
+
}
|
|
194
|
+
el.appendChild(body);
|
|
195
|
+
return el;
|
|
196
|
+
}
|
|
197
|
+
function clusterElement(count, color) {
|
|
198
|
+
var _a;
|
|
199
|
+
const el = document.createElement("div");
|
|
200
|
+
el.className = "sq-map-cluster";
|
|
201
|
+
const size = Math.round(34 + Math.min(30, Math.log2(count) * 5));
|
|
202
|
+
el.style.setProperty("--sq-cluster-size", `${size}px`);
|
|
203
|
+
el.style.setProperty("--sq-pin", (_a = ui.colorValue(color != null ? color : "primary")) != null ? _a : "var(--sq-primary)");
|
|
204
|
+
el.setAttribute("role", "button");
|
|
205
|
+
el.setAttribute("tabindex", "0");
|
|
206
|
+
el.setAttribute("aria-label", `${count} places — zoom in`);
|
|
207
|
+
const span = document.createElement("span");
|
|
208
|
+
span.textContent = count >= 1e3 ? `${(count / 1e3).toFixed(count >= 1e4 ? 0 : 1)}k` : String(count);
|
|
209
|
+
el.appendChild(span);
|
|
210
|
+
return el;
|
|
211
|
+
}
|
|
212
|
+
function createClusterIndex(points, opts = {}, tileSize = 512) {
|
|
213
|
+
var _a, _b, _c;
|
|
214
|
+
const index = new Supercluster({ radius: (_a = opts.radius) != null ? _a : 60, maxZoom: (_b = opts.maxZoom) != null ? _b : 16, minPoints: (_c = opts.minPoints) != null ? _c : 2, extent: tileSize });
|
|
215
|
+
index.load(points.map((p, i) => ({ type: "Feature", geometry: { type: "Point", coordinates: [p.lng, p.lat] }, properties: { i } })));
|
|
216
|
+
return index;
|
|
217
|
+
}
|
|
218
|
+
function clustersFor(index, bbox, zoom) {
|
|
219
|
+
return index.getClusters(bbox, Math.floor(zoom)).map((f) => {
|
|
220
|
+
const [lng, lat] = f.geometry.coordinates;
|
|
221
|
+
const p = f.properties;
|
|
222
|
+
return p.cluster ? { kind: "cluster", id: p.cluster_id, lat, lng, count: p.point_count } : { kind: "point", index: p.i, lat, lng };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const expansionZoom = (index, clusterId) => index.getClusterExpansionZoom(clusterId);
|
|
226
|
+
const clusterLeaves = (index, clusterId) => index.getLeaves(clusterId, Infinity).map((f) => f.properties.i);
|
|
227
|
+
function padBBox([w, s, e, n], ratio = 0.25) {
|
|
228
|
+
const dx = (e - w) * ratio;
|
|
229
|
+
const dy = (n - s) * ratio;
|
|
230
|
+
return [Math.max(-180, w - dx), Math.max(-85, s - dy), Math.min(180, e + dx), Math.min(85, n + dy)];
|
|
231
|
+
}
|
|
232
|
+
const _sfc_main = vue.defineComponent({
|
|
233
|
+
name: "SqMap",
|
|
234
|
+
components: { SqBtn: ui.SqBtn, SqProgressCircular: ui.SqProgressCircular, SqShape: ui.SqShape },
|
|
235
|
+
props: {
|
|
236
|
+
engine: { type: String, default: "vector" },
|
|
237
|
+
/** Vector style: `auto` (follows light/dark), a preset name (positron, voyager, dark-matter, liberty, bright, openfreemap-dark, fiord), a style URL or a style object. */
|
|
238
|
+
mapStyle: { type: [String, Object], default: "auto" },
|
|
239
|
+
center: { type: Array, default: () => [11.5564, 104.9282] },
|
|
240
|
+
zoom: { type: Number, default: 12 },
|
|
241
|
+
markers: { type: Array, default: () => [] },
|
|
242
|
+
/** Defaults merged into every marker (shape, color, size, icon…). */
|
|
243
|
+
markerDefaults: { type: Object, default: () => ({}) },
|
|
244
|
+
cluster: { type: [Boolean, Object], default: false },
|
|
245
|
+
clusterColor: { type: String, default: "primary" },
|
|
246
|
+
/** Raster engine tile template. */
|
|
247
|
+
tileUrl: { type: String, default: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" },
|
|
248
|
+
attribution: { type: String, default: '© <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors' },
|
|
249
|
+
height: { type: [Number, String], default: 400 },
|
|
250
|
+
rounded: { type: [String, Number], default: "xl" },
|
|
251
|
+
fitMarkers: Boolean,
|
|
252
|
+
scrollWheelZoom: { type: Boolean, default: false },
|
|
253
|
+
label: { type: String, default: "Map" }
|
|
254
|
+
},
|
|
255
|
+
emits: ["update:center", "update:zoom", "click", "marker-click", "cluster-click", "ready", "error"],
|
|
256
|
+
setup(props, { emit, slots }) {
|
|
257
|
+
const el = vue.ref(null);
|
|
258
|
+
const adapter = vue.shallowRef(null);
|
|
259
|
+
const ready = vue.ref(false);
|
|
260
|
+
const inst = vue.getCurrentInstance();
|
|
261
|
+
const handles = /* @__PURE__ */ new Map();
|
|
262
|
+
let index = null;
|
|
263
|
+
let epoch = 0;
|
|
264
|
+
const keyOf = (m, i) => {
|
|
265
|
+
var _a;
|
|
266
|
+
return String((_a = m.id) != null ? _a : i);
|
|
267
|
+
};
|
|
268
|
+
const isDark = vue.computed(() => {
|
|
269
|
+
var _a;
|
|
270
|
+
return !!((_a = ui.config.theme.themes[ui.config.theme.name]) == null ? void 0 : _a.dark);
|
|
271
|
+
});
|
|
272
|
+
const root = () => {
|
|
273
|
+
var _a;
|
|
274
|
+
return (_a = inst == null ? void 0 : inst.proxy) == null ? void 0 : _a.$el;
|
|
275
|
+
};
|
|
276
|
+
const clearMarkers = () => {
|
|
277
|
+
handles.forEach((h) => h.remove());
|
|
278
|
+
handles.clear();
|
|
279
|
+
};
|
|
280
|
+
const pointElement = (m, i) => {
|
|
281
|
+
var _a;
|
|
282
|
+
const tpl = slots.marker ? (_a = root().querySelector(`[data-marker="${CSS.escape(keyOf(m, i))}"]`)) == null ? void 0 : _a.firstElementChild : null;
|
|
283
|
+
const node = tpl ? tpl.cloneNode(true) : markerElement(m, props.markerDefaults);
|
|
284
|
+
if (tpl) node.classList.add("sq-map-marker", "sq-map-marker--custom");
|
|
285
|
+
const open = (e) => {
|
|
286
|
+
var _a2, _b, _c, _d;
|
|
287
|
+
e.stopPropagation();
|
|
288
|
+
emit("marker-click", m);
|
|
289
|
+
const popup = root().querySelector(`[data-popup="${CSS.escape(keyOf(m, i))}"]`);
|
|
290
|
+
if (popup && ((_a2 = popup.textContent) == null ? void 0 : _a2.trim())) (_d = adapter.value) == null ? void 0 : _d.openPopup([m.lat, m.lng], popup.cloneNode(true), ((_c = (_b = m.size) != null ? _b : props.markerDefaults.size) != null ? _c : 30) + 4);
|
|
291
|
+
};
|
|
292
|
+
node.addEventListener("click", open);
|
|
293
|
+
node.addEventListener("keydown", (e) => (e.key === "Enter" || e.key === " ") && open(e));
|
|
294
|
+
return node;
|
|
295
|
+
};
|
|
296
|
+
const shapeOf = (m) => {
|
|
297
|
+
var _a, _b;
|
|
298
|
+
return slots.marker ? "center" : markerAnchor((_b = (_a = m.shape) != null ? _a : props.markerDefaults.shape) != null ? _b : "pin");
|
|
299
|
+
};
|
|
300
|
+
const render2 = () => {
|
|
301
|
+
const a = adapter.value;
|
|
302
|
+
if (!a) return;
|
|
303
|
+
const want = /* @__PURE__ */ new Map();
|
|
304
|
+
if (index) {
|
|
305
|
+
for (const item of clustersFor(index, padBBox(a.getBounds()), a.getZoom())) {
|
|
306
|
+
if (item.kind === "cluster") {
|
|
307
|
+
const key = `c${item.id}`;
|
|
308
|
+
want.set(key, () => {
|
|
309
|
+
const node = clusterElement(item.count, props.clusterColor);
|
|
310
|
+
const zoomIn = (e) => {
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
const idx = index;
|
|
313
|
+
emit("cluster-click", { count: item.count, markers: clusterLeaves(idx, item.id).map((i) => props.markers[i]) });
|
|
314
|
+
a.setView([item.lat, item.lng], Math.min(expansionZoom(idx, item.id), 20), true);
|
|
315
|
+
};
|
|
316
|
+
node.addEventListener("click", zoomIn);
|
|
317
|
+
node.addEventListener("keydown", (e) => (e.key === "Enter" || e.key === " ") && zoomIn(e));
|
|
318
|
+
return a.addMarker(node, [item.lat, item.lng], "center");
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
const m = props.markers[item.index];
|
|
322
|
+
want.set(`p${item.index}`, () => a.addMarker(pointElement(m, item.index), [m.lat, m.lng], shapeOf(m)));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
props.markers.forEach((m, i) => want.set(`p${i}`, () => a.addMarker(pointElement(m, i), [m.lat, m.lng], shapeOf(m))));
|
|
327
|
+
}
|
|
328
|
+
for (const [k, h] of handles) if (!want.has(k)) {
|
|
329
|
+
h.remove();
|
|
330
|
+
handles.delete(k);
|
|
331
|
+
}
|
|
332
|
+
for (const [k, make] of want) if (!handles.has(k)) handles.set(k, make());
|
|
333
|
+
};
|
|
334
|
+
const rebuild = () => {
|
|
335
|
+
clearMarkers();
|
|
336
|
+
index = props.cluster ? createClusterIndex(props.markers, typeof props.cluster === "object" ? props.cluster : {}, props.engine === "raster" ? 256 : 512) : null;
|
|
337
|
+
render2();
|
|
338
|
+
};
|
|
339
|
+
const fit = () => {
|
|
340
|
+
var _a;
|
|
341
|
+
return (_a = adapter.value) == null ? void 0 : _a.fitPoints(props.markers.map((m) => [m.lat, m.lng]), 48, 15);
|
|
342
|
+
};
|
|
343
|
+
const mount = async () => {
|
|
344
|
+
const my = ++epoch;
|
|
345
|
+
if (!el.value) return;
|
|
346
|
+
ready.value = false;
|
|
347
|
+
const a = props.engine === "raster" ? await createRasterAdapter(el.value, { center: props.center, zoom: props.zoom, tileUrl: props.tileUrl, attribution: props.attribution, scrollZoom: props.scrollWheelZoom }) : await createVectorAdapter(el.value, { center: props.center, zoom: props.zoom, style: resolveMapStyle(props.mapStyle, isDark.value), scrollZoom: props.scrollWheelZoom });
|
|
348
|
+
if (my !== epoch) return a.destroy();
|
|
349
|
+
adapter.value = vue.markRaw(a);
|
|
350
|
+
a.on("moveend", () => {
|
|
351
|
+
const c = a.getCenter();
|
|
352
|
+
emit("update:center", c);
|
|
353
|
+
emit("update:zoom", Math.round(a.getZoom() * 100) / 100);
|
|
354
|
+
if (index) render2();
|
|
355
|
+
});
|
|
356
|
+
a.on("click", (p) => emit("click", p));
|
|
357
|
+
a.on("error", (e) => emit("error", e));
|
|
358
|
+
if (props.fitMarkers) fit();
|
|
359
|
+
rebuild();
|
|
360
|
+
ready.value = true;
|
|
361
|
+
emit("ready", a.native);
|
|
362
|
+
};
|
|
363
|
+
const unmountMap = () => {
|
|
364
|
+
var _a;
|
|
365
|
+
clearMarkers();
|
|
366
|
+
(_a = adapter.value) == null ? void 0 : _a.destroy();
|
|
367
|
+
adapter.value = null;
|
|
368
|
+
};
|
|
369
|
+
vue.onMounted(() => {
|
|
370
|
+
if (el.value) return mount();
|
|
371
|
+
const stop = vue.watch(el, (node) => {
|
|
372
|
+
if (node) {
|
|
373
|
+
stop();
|
|
374
|
+
return mount();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
vue.onBeforeUnmount(() => {
|
|
379
|
+
epoch++;
|
|
380
|
+
unmountMap();
|
|
381
|
+
});
|
|
382
|
+
vue.watch(() => props.engine, () => {
|
|
383
|
+
unmountMap();
|
|
384
|
+
mount();
|
|
385
|
+
});
|
|
386
|
+
vue.watch(() => props.markers, () => {
|
|
387
|
+
if (!adapter.value) return;
|
|
388
|
+
if (props.fitMarkers) fit();
|
|
389
|
+
requestAnimationFrame(rebuild);
|
|
390
|
+
}, { deep: true, flush: "post" });
|
|
391
|
+
vue.watch(() => [props.cluster, props.markerDefaults, props.clusterColor], () => adapter.value && rebuild(), { deep: true });
|
|
392
|
+
vue.watch(ui.iconStoreVersion, () => adapter.value && props.markers.some((m) => m.icon || props.markerDefaults.icon) && rebuild());
|
|
393
|
+
vue.watch([() => props.mapStyle, isDark], () => {
|
|
394
|
+
const a = adapter.value;
|
|
395
|
+
if (a && a.engine === "vector") a.setStyle(resolveMapStyle(props.mapStyle, isDark.value));
|
|
396
|
+
});
|
|
397
|
+
vue.watch(() => props.tileUrl, (url) => {
|
|
398
|
+
var _a;
|
|
399
|
+
return ((_a = adapter.value) == null ? void 0 : _a.engine) === "raster" && adapter.value.setStyle(url);
|
|
400
|
+
});
|
|
401
|
+
vue.watch(() => [props.center[0], props.center[1], props.zoom], () => {
|
|
402
|
+
const a = adapter.value;
|
|
403
|
+
if (!a) return;
|
|
404
|
+
const c = a.getCenter();
|
|
405
|
+
if (Math.abs(c[0] - props.center[0]) > 1e-6 || Math.abs(c[1] - props.center[1]) > 1e-6 || Math.abs(a.getZoom() - props.zoom) > 0.01) a.setView(props.center, props.zoom);
|
|
406
|
+
});
|
|
407
|
+
return { el, adapter, ready, keyOf, fit, toUnit: core.toUnit, radiusValue: ui.radiusValue, has: (n) => !!slots[n] };
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
function normalizeComponent(scriptExports, render2, staticRenderFns, functionalTemplate, injectStyles, scopeId, moduleIdentifier, shadowMode) {
|
|
411
|
+
var options = typeof scriptExports === "function" ? scriptExports.options : scriptExports;
|
|
412
|
+
if (render2) {
|
|
413
|
+
options.render = render2;
|
|
414
|
+
options.staticRenderFns = staticRenderFns;
|
|
415
|
+
options._compiled = true;
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
exports: scriptExports,
|
|
419
|
+
options
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
var _sfc_render = function render() {
|
|
423
|
+
var _vm = this, _c = _vm._self._c;
|
|
424
|
+
_vm._self._setupProxy;
|
|
425
|
+
return _c("div", { staticClass: "sq-map sq-surface", style: { height: _vm.toUnit(_vm.height), "--sq-r": _vm.radiusValue(_vm.rounded), "--sq-bg": "var(--sq-surface-variant)", "--sq-bd": "var(--sq-border)", "--sq-bw": "1px" } }, [_c("SqShape", { attrs: { "clip": "" } }), _c("div", { ref: "el", staticClass: "sq-map__canvas", attrs: { "role": "application", "aria-label": _vm.label } }), !_vm.ready ? _c("div", { staticClass: "sq-map__loading" }, [_c("SqProgressCircular", { attrs: { "indeterminate": "", "size": 24, "width": 3 } })], 1) : _vm._e(), _c("div", { staticClass: "sq-map__controls" }, [_c("SqBtn", { attrs: { "icon": "plus", "color": "surface", "variant": "elevated", "size": "small", "aria-label": "Zoom in" }, on: { "click": function($event) {
|
|
426
|
+
_vm.adapter && _vm.adapter.zoomIn();
|
|
427
|
+
} } }), _c("SqBtn", { attrs: { "icon": "minus", "color": "surface", "variant": "elevated", "size": "small", "aria-label": "Zoom out" }, on: { "click": function($event) {
|
|
428
|
+
_vm.adapter && _vm.adapter.zoomOut();
|
|
429
|
+
} } }), _vm.markers.length > 1 ? _c("SqBtn", { attrs: { "icon": "place", "color": "surface", "variant": "elevated", "size": "small", "aria-label": "Fit markers" }, on: { "click": _vm.fit } }) : _vm._e()], 1), _c("div", { staticClass: "sq-map__templates", attrs: { "aria-hidden": "true" } }, [_vm._l(_vm.markers, function(m, i) {
|
|
430
|
+
return _c("div", { key: "p" + _vm.keyOf(m, i), attrs: { "data-popup": _vm.keyOf(m, i) } }, [_vm._t("popup", function() {
|
|
431
|
+
return [m.title ? _c("b", [_vm._v(_vm._s(m.title))]) : _vm._e(), m.popup ? _c("div", [_vm._v(_vm._s(m.popup))]) : _vm._e()];
|
|
432
|
+
}, { "marker": m })], 2);
|
|
433
|
+
}), _vm.has("marker") ? _vm._l(_vm.markers, function(m, i) {
|
|
434
|
+
return _c("div", { key: "m" + _vm.keyOf(m, i), attrs: { "data-marker": _vm.keyOf(m, i) } }, [_vm._t("marker", null, { "marker": m })], 2);
|
|
435
|
+
}) : _vm._e()], 2), _vm._t("default", null, { "map": _vm.adapter ? _vm.adapter.native : null })], 2);
|
|
436
|
+
};
|
|
437
|
+
var _sfc_staticRenderFns = [];
|
|
438
|
+
var __component__ = /* @__PURE__ */ normalizeComponent(
|
|
439
|
+
_sfc_main,
|
|
440
|
+
_sfc_render,
|
|
441
|
+
_sfc_staticRenderFns
|
|
442
|
+
);
|
|
443
|
+
const SqMap = __component__.exports;
|
|
444
|
+
const componentNames = ["SqMap"];
|
|
445
|
+
exports.MAP_STYLES = MAP_STYLES;
|
|
446
|
+
exports.SqMap = SqMap;
|
|
447
|
+
exports.clusterElement = clusterElement;
|
|
448
|
+
exports.clusterLeaves = clusterLeaves;
|
|
449
|
+
exports.clustersFor = clustersFor;
|
|
450
|
+
exports.componentNames = componentNames;
|
|
451
|
+
exports.createClusterIndex = createClusterIndex;
|
|
452
|
+
exports.createRasterAdapter = createRasterAdapter;
|
|
453
|
+
exports.createVectorAdapter = createVectorAdapter;
|
|
454
|
+
exports.default = SqMap;
|
|
455
|
+
exports.expansionZoom = expansionZoom;
|
|
456
|
+
exports.markerElement = markerElement;
|
|
457
|
+
exports.resolveMapStyle = resolveMapStyle;
|
|
458
|
+
exports.setMapWorkerUrl = setMapWorkerUrl;
|
|
459
|
+
//# sourceMappingURL=index.cjs.map
|