@mapka/maplibre-gl-sdk 0.5.2 → 0.7.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/src/map.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  import * as maplibregl from "maplibre-gl";
2
- import type { RequestTransformFunction, MapOptions } from "maplibre-gl";
3
2
  import { loadLayersIcons } from "./modules/icons.js";
3
+ import { addMarkers, addMarkersStyleDiff } from "./modules/markers.js";
4
+ import type {
5
+ RequestTransformFunction,
6
+ MapStyleImageMissingEvent,
7
+ MapOptions,
8
+ StyleSwapOptions,
9
+ StyleOptions,
10
+ StyleSpecification,
11
+ } from "maplibre-gl";
4
12
 
5
13
  export interface MapkaMapOptions extends MapOptions {
6
14
  apiKey: string;
7
15
  env?: "dev" | "prod" | "local";
8
16
  }
9
17
 
10
- const noopTransformRequest: RequestTransformFunction = (url) => {
18
+ const noopTransformRequest: maplibregl.RequestTransformFunction = (url) => {
11
19
  return {
12
20
  url,
13
21
  };
@@ -27,17 +35,42 @@ const createTransformRequest =
27
35
  return transformRequest ? transformRequest(url) : noopTransformRequest(url);
28
36
  };
29
37
 
38
+ const noopTransformStyle: maplibregl.TransformStyleFunction = (_, next) => {
39
+ return next;
40
+ };
41
+
30
42
  export class MapkaMap extends maplibregl.Map {
31
43
  static env: string = "prod";
32
44
 
33
- public constructor({ transformRequest, apiKey, ...props }: MapkaMapOptions) {
45
+ public constructor({ transformRequest, apiKey, ...options }: MapkaMapOptions) {
34
46
  super({
35
- ...props,
47
+ ...options,
36
48
  transformRequest: createTransformRequest(apiKey, transformRequest),
37
49
  });
38
50
 
39
- super.on("styleimagemissing", (event: maplibregl.MapStyleImageMissingEvent) => {
51
+ super.on("styledata", (_: maplibregl.MapStyleDataEvent) => {
52
+ addMarkers(this);
53
+ });
54
+
55
+ super.on("styleimagemissing", (event: MapStyleImageMissingEvent) => {
40
56
  loadLayersIcons(this, event);
41
57
  });
42
58
  }
59
+
60
+ public setStyle(
61
+ style: string | StyleSpecification,
62
+ options: StyleSwapOptions & StyleOptions = {},
63
+ ) {
64
+ const { transformStyle = noopTransformStyle, ...rest } = options;
65
+
66
+ super.setStyle(style, {
67
+ ...rest,
68
+ transformStyle: (prev, next) => {
69
+ addMarkersStyleDiff(this, next);
70
+ return transformStyle(prev, next);
71
+ },
72
+ });
73
+
74
+ return this;
75
+ }
43
76
  }
@@ -0,0 +1,59 @@
1
+ import * as maplibregl from "maplibre-gl";
2
+ import { get } from "es-toolkit/compat";
3
+ import type { StyleSpecification } from "maplibre-gl";
4
+ import type { MapkaMap } from "../map.js";
5
+ import type { MapkaMarkerOptions } from "../types/marker.js";
6
+ import { showTooltip, hideTooltip } from "./tooltip.js";
7
+
8
+ const prevMarkers = new Set<maplibregl.Marker>();
9
+
10
+ function addMarkersToMap(map: MapkaMap, markers: MapkaMarkerOptions[]) {
11
+ for (const marker of prevMarkers) {
12
+ marker.remove();
13
+ }
14
+ prevMarkers.clear();
15
+
16
+ for (const markerConfig of markers) {
17
+ const { position, color, tooltip } = markerConfig;
18
+ const newMarker = new maplibregl.Marker({
19
+ color,
20
+ })
21
+ .setLngLat(position)
22
+ .addTo(map);
23
+
24
+ if (tooltip?.trigger === "click") {
25
+ const markerElement = newMarker.getElement();
26
+
27
+ markerElement.style.cursor = "pointer";
28
+
29
+ markerElement.addEventListener("click", (e) => {
30
+ e.stopPropagation();
31
+ showTooltip(newMarker, tooltip, map);
32
+ });
33
+ } else if (tooltip?.trigger === "hover") {
34
+ const markerElement = newMarker.getElement();
35
+
36
+ markerElement.addEventListener("mouseenter", (e) => {
37
+ e.stopPropagation();
38
+ showTooltip(newMarker, tooltip, map);
39
+ });
40
+ markerElement.addEventListener("mouseleave", (e) => {
41
+ e.stopPropagation();
42
+ hideTooltip();
43
+ });
44
+ }
45
+
46
+ prevMarkers.add(newMarker);
47
+ }
48
+ }
49
+
50
+ export function addMarkers(map: MapkaMap) {
51
+ const style = map.getStyle();
52
+ const markers = get(style, "metadata.mapka.markers", []) as MapkaMarkerOptions[];
53
+ addMarkersToMap(map, markers);
54
+ }
55
+
56
+ export function addMarkersStyleDiff(map: MapkaMap, next: StyleSpecification) {
57
+ const markers = get(next, "metadata.mapka.markers", []) as MapkaMarkerOptions[];
58
+ addMarkersToMap(map, markers);
59
+ }
@@ -0,0 +1,344 @@
1
+ import * as maplibregl from "maplibre-gl";
2
+ import type { MapkaTooltipOptions } from "../types/marker.js";
3
+
4
+ let currentPopup: maplibregl.Popup | null = null;
5
+ let currentCarouselIndex = 0;
6
+
7
+ /**
8
+ * Creates the tooltip content HTML with Airbnb-style design
9
+ */
10
+ function createTooltipContent(options: MapkaTooltipOptions): HTMLElement {
11
+ const content = document.createElement("div");
12
+ content.className = "mapka-tooltip-content-wrapper";
13
+
14
+ // Add image carousel if images are provided
15
+ if (options.imageUrls && options.imageUrls.length > 0) {
16
+ const carouselContainer = document.createElement("div");
17
+ carouselContainer.className = "mapka-tooltip-carousel";
18
+
19
+ const carouselTrack = document.createElement("div");
20
+ carouselTrack.className = "mapka-tooltip-carousel-track";
21
+
22
+ options.imageUrls.forEach((url, index) => {
23
+ const img = document.createElement("img");
24
+ img.src = url;
25
+ img.alt = options.title || "Marker image";
26
+ img.className = "mapka-tooltip-image";
27
+ if (index === 0) img.classList.add("active");
28
+ carouselTrack.appendChild(img);
29
+ });
30
+
31
+ carouselContainer.appendChild(carouselTrack);
32
+
33
+ // Add close button
34
+ const closeButton = document.createElement("button");
35
+ closeButton.className = "mapka-tooltip-action-btn mapka-tooltip-close";
36
+ closeButton.innerHTML = `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" style="display:block;fill:none;height:16px;width:16px;stroke:currentColor;stroke-width:3;overflow:visible"><path d="m6 6 20 20M26 6 6 26"></path></svg>`;
37
+ closeButton.setAttribute("aria-label", "Close");
38
+ closeButton.addEventListener("click", (e) => {
39
+ e.stopPropagation();
40
+ hideTooltip();
41
+ });
42
+
43
+ carouselContainer.appendChild(closeButton);
44
+
45
+ // Add navigation dots if multiple images
46
+ if (options.imageUrls.length > 1) {
47
+ const dotsContainer = document.createElement("div");
48
+ dotsContainer.className = "mapka-tooltip-dots";
49
+
50
+ options.imageUrls.forEach((_, index) => {
51
+ const dot = document.createElement("button");
52
+ dot.className = "mapka-tooltip-dot";
53
+ if (index === 0) dot.classList.add("active");
54
+ dot.setAttribute("aria-label", `Go to image ${index + 1}`);
55
+ dot.addEventListener("click", (e) => {
56
+ e.stopPropagation();
57
+ showCarouselImage(carouselTrack, dotsContainer, index);
58
+ });
59
+ dotsContainer.appendChild(dot);
60
+ });
61
+
62
+ carouselContainer.appendChild(dotsContainer);
63
+
64
+ // Add prev/next buttons
65
+ const prevButton = document.createElement("button");
66
+ prevButton.className = "mapka-tooltip-carousel-btn mapka-tooltip-carousel-prev";
67
+ prevButton.innerHTML = `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" style="display:block;fill:none;height:12px;width:12px;stroke:currentColor;stroke-width:4;overflow:visible"><path fill="none" d="M20 28 8.7 16.7a1 1 0 0 1 0-1.4L20 4"></path></svg>`;
68
+ prevButton.setAttribute("aria-label", "Previous image");
69
+ prevButton.addEventListener("click", (e) => {
70
+ e.stopPropagation();
71
+ const imageCount = options.imageUrls?.length ?? 0;
72
+ const newIndex = currentCarouselIndex === 0 ? imageCount - 1 : currentCarouselIndex - 1;
73
+ showCarouselImage(carouselTrack, dotsContainer, newIndex);
74
+ });
75
+
76
+ const nextButton = document.createElement("button");
77
+ nextButton.className = "mapka-tooltip-carousel-btn mapka-tooltip-carousel-next";
78
+ nextButton.innerHTML = `<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" style="display:block;fill:none;height:12px;width:12px;stroke:currentColor;stroke-width:4;overflow:visible"><path fill="none" d="m12 4 11.3 11.3a1 1 0 0 1 0 1.4L12 28"></path></svg>`;
79
+ nextButton.setAttribute("aria-label", "Next image");
80
+ nextButton.addEventListener("click", (e) => {
81
+ e.stopPropagation();
82
+ const imageCount = options.imageUrls?.length ?? 0;
83
+ const newIndex = (currentCarouselIndex + 1) % imageCount;
84
+ showCarouselImage(carouselTrack, dotsContainer, newIndex);
85
+ });
86
+
87
+ carouselContainer.appendChild(prevButton);
88
+ carouselContainer.appendChild(nextButton);
89
+ }
90
+
91
+ content.appendChild(carouselContainer);
92
+ }
93
+
94
+ // Add text content section
95
+ const textContent = document.createElement("div");
96
+ textContent.className = "mapka-tooltip-text";
97
+
98
+ if (options.title) {
99
+ const title = document.createElement("h3");
100
+ title.className = "mapka-tooltip-title";
101
+ title.textContent = options.title;
102
+ textContent.appendChild(title);
103
+ }
104
+
105
+ if (options.description) {
106
+ const description = document.createElement("p");
107
+ description.className = "mapka-tooltip-description";
108
+ description.textContent = options.description;
109
+ textContent.appendChild(description);
110
+ }
111
+
112
+ content.appendChild(textContent);
113
+
114
+ return content;
115
+ }
116
+
117
+ /**
118
+ * Shows a specific image in the carousel
119
+ */
120
+ function showCarouselImage(track: HTMLElement, dotsContainer: HTMLElement, index: number) {
121
+ currentCarouselIndex = index;
122
+
123
+ const images = track.querySelectorAll(".mapka-tooltip-image");
124
+ const dots = dotsContainer.querySelectorAll(".mapka-tooltip-dot");
125
+
126
+ images.forEach((img, i) => {
127
+ img.classList.toggle("active", i === index);
128
+ });
129
+
130
+ dots.forEach((dot, i) => {
131
+ dot.classList.toggle("active", i === index);
132
+ });
133
+
134
+ track.style.transform = `translateX(-${index * 100}%)`;
135
+ }
136
+
137
+ /**
138
+ * Injects the required CSS styles for the tooltip
139
+ */
140
+ function injectStyles() {
141
+ if (document.getElementById("mapka-tooltip-styles")) return;
142
+
143
+ const style = document.createElement("style");
144
+ style.id = "mapka-tooltip-styles";
145
+ style.textContent = `
146
+ .maplibregl-popup-content {
147
+ padding: 0 !important;
148
+ border-radius: 12px !important;
149
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important;
150
+ min-width: 280px;
151
+ max-width: 320px;
152
+ }
153
+
154
+ .maplibregl-popup-close-button {
155
+ display: none !important;
156
+ }
157
+
158
+ .mapka-tooltip-content-wrapper {
159
+ border-radius: 12px;
160
+ overflow: hidden;
161
+ }
162
+
163
+ .mapka-tooltip-carousel {
164
+ position: relative;
165
+ width: 100%;
166
+ height: 200px;
167
+ overflow: hidden;
168
+ border-radius: 12px 12px 0 0;
169
+ }
170
+
171
+ .mapka-tooltip-carousel-track {
172
+ display: flex;
173
+ height: 100%;
174
+ transition: transform 0.3s ease;
175
+ }
176
+
177
+ .mapka-tooltip-image {
178
+ min-width: 100%;
179
+ height: 100%;
180
+ object-fit: cover;
181
+ display: none;
182
+ }
183
+
184
+ .mapka-tooltip-image.active {
185
+ display: block;
186
+ }
187
+
188
+ .mapka-tooltip-action-btn {
189
+ position: absolute;
190
+ top: 12px;
191
+ right: 12px;
192
+ background: rgba(255, 255, 255, 0.95);
193
+ border: none;
194
+ border-radius: 50%;
195
+ width: 32px;
196
+ height: 32px;
197
+ cursor: pointer;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
202
+ transition: background 0.2s ease, transform 0.2s ease;
203
+ color: #222;
204
+ z-index: 3;
205
+ }
206
+
207
+ .mapka-tooltip-action-btn:hover {
208
+ background: white;
209
+ transform: scale(1.05);
210
+ }
211
+
212
+ .mapka-tooltip-carousel-btn {
213
+ position: absolute;
214
+ top: 50%;
215
+ transform: translateY(-50%);
216
+ background: rgba(255, 255, 255, 0.95);
217
+ border: none;
218
+ border-radius: 50%;
219
+ width: 32px;
220
+ height: 32px;
221
+ cursor: pointer;
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ z-index: 2;
226
+ transition: background 0.2s ease, transform 0.2s ease;
227
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.18);
228
+ color: #222;
229
+ }
230
+
231
+ .mapka-tooltip-carousel-btn:hover {
232
+ background: white;
233
+ transform: translateY(-50%) scale(1.05);
234
+ }
235
+
236
+ .mapka-tooltip-carousel-prev {
237
+ left: 12px;
238
+ }
239
+
240
+ .mapka-tooltip-carousel-next {
241
+ right: 12px;
242
+ }
243
+
244
+ .mapka-tooltip-dots {
245
+ position: absolute;
246
+ bottom: 12px;
247
+ left: 50%;
248
+ transform: translateX(-50%);
249
+ display: flex;
250
+ gap: 6px;
251
+ z-index: 2;
252
+ }
253
+
254
+ .mapka-tooltip-dot {
255
+ width: 6px;
256
+ height: 6px;
257
+ border-radius: 50%;
258
+ background: rgba(255, 255, 255, 0.6);
259
+ border: none;
260
+ cursor: pointer;
261
+ padding: 0;
262
+ transition: background 0.2s ease, transform 0.2s ease;
263
+ }
264
+
265
+ .mapka-tooltip-dot:hover {
266
+ background: rgba(255, 255, 255, 0.8);
267
+ transform: scale(1.2);
268
+ }
269
+
270
+ .mapka-tooltip-dot.active {
271
+ background: white;
272
+ }
273
+
274
+ .mapka-tooltip-text {
275
+ padding: 12px 16px 16px;
276
+ }
277
+
278
+ .mapka-tooltip-title {
279
+ margin: 0 0 4px 0;
280
+ font-size: 15px;
281
+ font-weight: 600;
282
+ color: #222;
283
+ line-height: 1.3;
284
+ }
285
+
286
+ .mapka-tooltip-description {
287
+ margin: 0;
288
+ font-size: 14px;
289
+ color: #717171;
290
+ line-height: 1.4;
291
+ }
292
+ `;
293
+
294
+ document.head.appendChild(style);
295
+ }
296
+
297
+ /**
298
+ * Shows a tooltip for a marker
299
+ */
300
+ export function showTooltip(
301
+ marker: maplibregl.Marker,
302
+ options: MapkaTooltipOptions,
303
+ map: maplibregl.Map,
304
+ ) {
305
+ // Hide any existing tooltip
306
+ hideTooltip();
307
+
308
+ // Inject styles if not already present
309
+ injectStyles();
310
+
311
+ // Reset carousel index
312
+ currentCarouselIndex = 0;
313
+
314
+ const content = createTooltipContent(options);
315
+
316
+ const popup = new maplibregl.Popup({
317
+ closeButton: true,
318
+ closeOnClick: false,
319
+ maxWidth: "320px",
320
+ offset: 12,
321
+ })
322
+ .setLngLat(marker.getLngLat())
323
+ .setDOMContent(content)
324
+ .addTo(map);
325
+
326
+ currentPopup = popup;
327
+ }
328
+
329
+ /**
330
+ * Hides the current tooltip
331
+ */
332
+ export function hideTooltip() {
333
+ if (currentPopup) {
334
+ currentPopup.remove();
335
+ currentPopup = null;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Gets the current visible popup
341
+ */
342
+ export function getCurrentTooltip(): maplibregl.Popup | null {
343
+ return currentPopup;
344
+ }
@@ -0,0 +1,28 @@
1
+ export interface MapkaLayerConfig {
2
+ /**
3
+ * Layer id, layer can belong only to one group
4
+ */
5
+ value: string;
6
+ }
7
+
8
+ export interface MapkaLayerGroupConfig {
9
+ /**
10
+ * Unique identifier for the group, randomly generated
11
+ */
12
+ value: string;
13
+ /**
14
+ * Human readable label for the group
15
+ */
16
+ label: string;
17
+ /**
18
+ * Icon for the group
19
+ */
20
+ icon: string;
21
+ /**
22
+ * Children of the group
23
+ * This could be other groups or layer items
24
+ */
25
+ children: MapkaLayerTreeConfig;
26
+ }
27
+
28
+ export type MapkaLayerTreeConfig = (MapkaLayerConfig | MapkaLayerGroupConfig)[];
@@ -0,0 +1,13 @@
1
+ export interface MapkaTooltipOptions {
2
+ trigger?: "hover" | "click";
3
+ title?: string;
4
+ description?: string;
5
+ imageUrls?: string[];
6
+ }
7
+
8
+ export interface MapkaMarkerOptions {
9
+ position: [number, number];
10
+ color?: string;
11
+ icon?: string;
12
+ tooltip?: MapkaTooltipOptions;
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { MapkaLayerTreeConfig } from "./layer.js";
2
+ import type { StyleSpecification } from "@maplibre/maplibre-gl-style-spec";
3
+ import type { MapkaMarkerOptions } from "./marker.js";
4
+
5
+ export interface MapkaStyleSpecification extends StyleSpecification {
6
+ metadata?: {
7
+ mapka?: {
8
+ layerGroups?: MapkaLayerTreeConfig;
9
+ markers?: MapkaMarkerOptions[];
10
+ };
11
+ };
12
+ }
package/src/utils/url.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { MapkaMap } from "../map.js";
2
2
 
3
3
  export function getMapkaUrl() {
4
- console.log("MapkaMap.env", MapkaMap.env);
5
4
  if (MapkaMap.env === "dev") {
6
5
  return "https://api.dev.mapka.dev";
7
6
  }