@mapka/maplibre-gl-sdk 0.16.4 → 0.17.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 +29 -2
- package/lib/.buildInfo.json +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/modules/icons.d.ts +8 -4
- package/lib/modules/icons.d.ts.map +1 -1
- package/lib/modules/icons.js +69 -31
- package/lib/modules/markers.d.ts.map +1 -1
- package/lib/modules/markers.js +55 -2
- package/lib/styles.css +1 -1
- package/lib/types/marker.d.ts +4 -2
- package/lib/types/marker.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/components/PopupContent.css +5 -5
- package/src/index.ts +2 -0
- package/src/modules/icons.ts +74 -38
- package/src/modules/markers.css +18 -0
- package/src/modules/markers.ts +67 -3
- package/src/styles.css +1 -0
- package/src/types/marker.ts +4 -2
package/lib/types/marker.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { MarkerOptions } from "maplibre-gl";
|
|
2
2
|
import type { MapkaMarkerPopupOptions } from "./popup.js";
|
|
3
|
-
export interface MapkaMarkerOptions extends MarkerOptions {
|
|
3
|
+
export interface MapkaMarkerOptions extends Omit<MarkerOptions, "color"> {
|
|
4
4
|
id?: string;
|
|
5
5
|
lngLat: [number, number];
|
|
6
|
-
|
|
6
|
+
/** Mapka icon id (e.g. `"maki:restaurant"`). Omit to use the default pin. */
|
|
7
7
|
icon?: string;
|
|
8
|
+
/** CSS color applied to the SVG fill. Default pin uses `#3FB1CE` when unset. */
|
|
9
|
+
color?: string;
|
|
8
10
|
popup?: MapkaMarkerPopupOptions;
|
|
9
11
|
}
|
|
10
12
|
//# sourceMappingURL=marker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"marker.d.ts","sourceRoot":"","sources":["../../src/types/marker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,kBAAmB,SAAQ,aAAa;
|
|
1
|
+
{"version":3,"file":"marker.d.ts","sourceRoot":"","sources":["../../src/types/marker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC;IACtE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,uBAAuB,CAAC;CACjC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapka/maplibre-gl-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Mapka JS SDK",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"es-toolkit": "^1.45.1",
|
|
34
34
|
"html-to-image": "^1.11.13",
|
|
35
|
+
"ky": "^2.0.1",
|
|
35
36
|
"maplibre-gl": "^5.22.0",
|
|
36
37
|
"preact": "^10.29.1",
|
|
37
38
|
"supercluster": "^8.0.1",
|
|
@@ -56,5 +57,5 @@
|
|
|
56
57
|
"!**/__tests__/",
|
|
57
58
|
"!lib/buildInfo.json"
|
|
58
59
|
],
|
|
59
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "205ea38efd4f49ec5a2f52267b76d19b9f5f1af9"
|
|
60
61
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* Mapka Popup Styles */
|
|
2
|
-
.mapka-popup-container {
|
|
3
|
-
margin-top: -15px;
|
|
4
|
-
margin-bottom: -15px;
|
|
5
|
-
margin-left: -10px;
|
|
6
|
-
margin-right: -10px;
|
|
2
|
+
.mapka-popup-container {}
|
|
7
3
|
|
|
4
|
+
.maplibregl-popup-content {
|
|
5
|
+
padding: 0;
|
|
6
|
+
margin: 0;
|
|
7
|
+
border-radius: 12px;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
.mapka-popup {
|
package/src/index.ts
CHANGED
package/src/modules/icons.ts
CHANGED
|
@@ -1,57 +1,93 @@
|
|
|
1
|
+
import ky from "ky";
|
|
1
2
|
import { debounce, uniq } from "es-toolkit";
|
|
2
3
|
import { getMapkaUrl } from "../utils/url.js";
|
|
3
4
|
import type { MapkaMap } from "../map.js";
|
|
4
5
|
import type { MapStyleImageMissingEvent } from "maplibre-gl";
|
|
5
6
|
|
|
6
|
-
let missingIcons: string[] = [];
|
|
7
|
-
let loadedIcons: string[] = [];
|
|
8
|
-
|
|
9
7
|
interface Icon {
|
|
10
8
|
id: string;
|
|
11
9
|
svg: string;
|
|
12
10
|
}
|
|
13
11
|
|
|
12
|
+
const svgCache = new Map<string, string>();
|
|
13
|
+
const pendingResolvers = new Map<string, PromiseWithResolvers<string>>();
|
|
14
|
+
let pendingIds: string[] = [];
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* Debounced batch fetch: pulls unique pending ids, hits the icons API
|
|
18
|
+
* in one request, populates svgCache, and resolves waiting callers.
|
|
17
19
|
*/
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
const flushIcons = debounce(() => {
|
|
21
|
+
const idsToFetch = uniq(pendingIds).filter((id) => !svgCache.has(id));
|
|
22
|
+
if (idsToFetch.length === 0) return;
|
|
23
|
+
|
|
24
|
+
pendingIds = [];
|
|
25
|
+
|
|
26
|
+
ky.get(`${getMapkaUrl()}/v1/icons`, {
|
|
27
|
+
searchParams: idsToFetch.map((id) => ["ids", id]),
|
|
28
|
+
})
|
|
29
|
+
.json<Icon[]>()
|
|
30
|
+
.then((data) => {
|
|
31
|
+
for (const { id, svg } of data) {
|
|
32
|
+
svgCache.set(id, svg);
|
|
33
|
+
const resolver = pendingResolvers.get(id);
|
|
34
|
+
if (resolver) {
|
|
35
|
+
resolver.resolve(svg);
|
|
36
|
+
pendingResolvers.delete(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.catch((err) => {
|
|
41
|
+
for (const id of idsToFetch) {
|
|
42
|
+
const resolver = pendingResolvers.get(id);
|
|
43
|
+
if (resolver) {
|
|
44
|
+
resolver.reject(err);
|
|
45
|
+
pendingResolvers.delete(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
39
48
|
});
|
|
40
49
|
}, 50);
|
|
41
50
|
|
|
42
51
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
52
|
+
* Fetch an icon's SVG source from the Mapka API.
|
|
53
|
+
* Batched and cached across callers; concurrent requests for the same
|
|
54
|
+
* id share one fetch.
|
|
55
|
+
*/
|
|
56
|
+
export function loadMarkerIcon(id: string): Promise<string> {
|
|
57
|
+
const cached = svgCache.get(id);
|
|
58
|
+
if (cached) return Promise.resolve(cached);
|
|
59
|
+
|
|
60
|
+
const existing = pendingResolvers.get(id);
|
|
61
|
+
if (existing) return existing.promise;
|
|
62
|
+
|
|
63
|
+
const resolver = Promise.withResolvers<string>();
|
|
64
|
+
pendingResolvers.set(id, resolver);
|
|
65
|
+
pendingIds.push(id);
|
|
66
|
+
flushIcons();
|
|
67
|
+
return resolver.promise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isStyleImage = (id: string) => id.includes(":");
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load any icons that are missing from the map from the Mapka API.
|
|
74
|
+
* @see https://github.com/mapbox/mapbox-gl-js/issues/5529
|
|
47
75
|
*/
|
|
48
76
|
export function loadLayersIcons(map: MapkaMap, event: MapStyleImageMissingEvent) {
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
if (map.hasImage(event.id) || !isStyleImage(event.id)) return;
|
|
78
|
+
|
|
79
|
+
loadMarkerIcon(event.id)
|
|
80
|
+
.then((svg) => {
|
|
81
|
+
if (map.hasImage(event.id)) return;
|
|
82
|
+
const img = new Image(15, 15);
|
|
83
|
+
img.onload = () => {
|
|
84
|
+
if (!map.hasImage(event.id)) {
|
|
85
|
+
map.addImage(event.id, img);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
img.src = `data:image/svg+xml;charset=utf-8;base64,${btoa(svg)}`;
|
|
89
|
+
})
|
|
90
|
+
.catch((err) => {
|
|
91
|
+
map.logger.warn(`[mapka] Failed to load layer icon "${event.id}":`, err);
|
|
92
|
+
});
|
|
57
93
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.mapka-marker-icon {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
pointer-events: auto;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Icon markers (maki / temaki) render in a fixed 32×32 box. */
|
|
9
|
+
.mapka-marker-icon[data-icon-id] {
|
|
10
|
+
width: 32px;
|
|
11
|
+
height: 32px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.mapka-marker-icon[data-icon-id] svg {
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
display: block;
|
|
18
|
+
}
|
package/src/modules/markers.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Marker } from "maplibre-gl";
|
|
2
2
|
import { get } from "es-toolkit/compat";
|
|
3
3
|
import { remove } from "es-toolkit";
|
|
4
|
-
import
|
|
4
|
+
import { loadMarkerIcon } from "./icons.js";
|
|
5
|
+
import type { MarkerOptions, Offset, StyleSpecification } from "maplibre-gl";
|
|
5
6
|
import type { MapkaMap } from "../map.js";
|
|
6
7
|
import type { MapkaMarkerOptions } from "../types/marker.js";
|
|
7
8
|
import type { MapkaMarkerPopupOptions, MapkaPopupOptions } from "../types/popup.js";
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Offset for the default pin so the tip sits on the LngLat anchor point.
|
|
12
|
+
* Value taken from maplibre-gl-js: (shadow translate-y + ellipse cy) - (height/2) ≈ 14.
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_PIN_OFFSET: [number, number] = [0, -14];
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Default marker offset
|
|
11
18
|
* @see https://github.com/maplibre/maplibre-gl-js/blob/master/src/ui/marker.ts#L457
|
|
@@ -110,10 +117,67 @@ function setupMarkerPopupListeners(
|
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Apply `color` (as SVG `fill`) to the fetched icon SVG inside `element`.
|
|
122
|
+
*
|
|
123
|
+
* Override every descendant whose `fill` attribute is set and not `"none"`,
|
|
124
|
+
* then set `fill` on the root `<svg>` itself so paths that omit the
|
|
125
|
+
* attribute (common in maki — they inherit the browser default) pick up the
|
|
126
|
+
* user color via SVG cascading. `fill="none"` is preserved so outline-only
|
|
127
|
+
* shapes keep their transparency.
|
|
128
|
+
*
|
|
129
|
+
* Only invoked for icon markers; the default maplibre pin handles its own
|
|
130
|
+
* color via `MarkerOptions.color`.
|
|
131
|
+
*/
|
|
132
|
+
function applyMarkerColors(element: HTMLElement, color?: string) {
|
|
133
|
+
if (!color) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const el of element.querySelectorAll('[fill]:not([fill="none"])')) {
|
|
137
|
+
el.setAttribute("fill", color);
|
|
138
|
+
}
|
|
139
|
+
const svg = element.querySelector("svg");
|
|
140
|
+
if (svg && !svg.hasAttribute("fill")) {
|
|
141
|
+
svg.setAttribute("fill", color);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createMarkerElement(
|
|
146
|
+
currentMap: MapkaMap,
|
|
147
|
+
options: MapkaMarkerOptions,
|
|
148
|
+
): HTMLElement | undefined {
|
|
149
|
+
const { color, icon } = options;
|
|
150
|
+
if (!icon) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const element = document.createElement("div");
|
|
155
|
+
element.className = "mapka-marker-icon";
|
|
156
|
+
|
|
157
|
+
element.dataset.iconId = icon;
|
|
158
|
+
loadMarkerIcon(icon)
|
|
159
|
+
.then((svg) => {
|
|
160
|
+
element.innerHTML = svg;
|
|
161
|
+
applyMarkerColors(element, color);
|
|
162
|
+
})
|
|
163
|
+
.catch((err) => {
|
|
164
|
+
currentMap.logger.warn(`[mapka] Failed to load marker icon "${icon}":`, err);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return element;
|
|
168
|
+
}
|
|
169
|
+
|
|
113
170
|
export function addMarkers(currentMap: MapkaMap, markersOptions: MapkaMarkerOptions[]) {
|
|
114
171
|
for (const markerOptions of markersOptions) {
|
|
115
|
-
const { lngLat, popup, ...
|
|
116
|
-
|
|
172
|
+
const { lngLat, popup, icon, offset = DEFAULT_PIN_OFFSET, ...rest } = markerOptions;
|
|
173
|
+
|
|
174
|
+
const markerOpts: MarkerOptions = {
|
|
175
|
+
...rest,
|
|
176
|
+
element: createMarkerElement(currentMap, markerOptions),
|
|
177
|
+
offset,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const newMarker = new Marker(markerOpts).setLngLat(lngLat).addTo(currentMap);
|
|
117
181
|
|
|
118
182
|
currentMap.markers.push({
|
|
119
183
|
id: getMarkerId(markerOptions),
|
package/src/styles.css
CHANGED
package/src/types/marker.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { MarkerOptions } from "maplibre-gl";
|
|
2
2
|
import type { MapkaMarkerPopupOptions } from "./popup.js";
|
|
3
3
|
|
|
4
|
-
export interface MapkaMarkerOptions extends MarkerOptions {
|
|
4
|
+
export interface MapkaMarkerOptions extends Omit<MarkerOptions, "color"> {
|
|
5
5
|
id?: string;
|
|
6
6
|
lngLat: [number, number];
|
|
7
|
-
|
|
7
|
+
/** Mapka icon id (e.g. `"maki:restaurant"`). Omit to use the default pin. */
|
|
8
8
|
icon?: string;
|
|
9
|
+
/** CSS color applied to the SVG fill. Default pin uses `#3FB1CE` when unset. */
|
|
10
|
+
color?: string;
|
|
9
11
|
popup?: MapkaMarkerPopupOptions;
|
|
10
12
|
}
|