@mapka/maplibre-gl-sdk 0.16.0 → 0.16.3

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.
Files changed (124) hide show
  1. package/README.md +10 -5
  2. package/lib/.buildInfo.json +1 -1
  3. package/lib/components/ImageCarousel.d.ts +7 -0
  4. package/lib/components/ImageCarousel.d.ts.map +1 -0
  5. package/lib/components/ImageCarousel.js +22 -0
  6. package/lib/components/PopupContent.d.ts +5 -1
  7. package/lib/components/PopupContent.d.ts.map +1 -1
  8. package/lib/components/PopupContent.js +15 -37
  9. package/lib/components/PopupDataRows.d.ts +7 -0
  10. package/lib/components/PopupDataRows.d.ts.map +1 -0
  11. package/lib/components/PopupDataRows.js +22 -0
  12. package/lib/components/PopupList.d.ts +10 -0
  13. package/lib/components/PopupList.d.ts.map +1 -0
  14. package/lib/components/PopupList.js +8 -0
  15. package/lib/components/PopupListItem.d.ts +13 -0
  16. package/lib/components/PopupListItem.d.ts.map +1 -0
  17. package/lib/components/PopupListItem.js +11 -0
  18. package/lib/components/icons/ChevronLeftIcon.d.ts +2 -0
  19. package/lib/components/icons/ChevronLeftIcon.d.ts.map +1 -0
  20. package/lib/components/icons/ChevronLeftIcon.js +4 -0
  21. package/lib/components/icons/ChevronRightIcon.d.ts +2 -0
  22. package/lib/components/icons/ChevronRightIcon.d.ts.map +1 -0
  23. package/lib/components/icons/ChevronRightIcon.js +4 -0
  24. package/lib/components/icons/CircleIcon.d.ts.map +1 -0
  25. package/lib/components/icons/CloseIcon.d.ts +2 -0
  26. package/lib/components/icons/CloseIcon.d.ts.map +1 -0
  27. package/lib/components/icons/CloseIcon.js +4 -0
  28. package/lib/components/icons/DownloadIcon.d.ts.map +1 -0
  29. package/lib/components/icons/FreehandIcon.d.ts.map +1 -0
  30. package/lib/components/icons/HeartIcon.d.ts +4 -0
  31. package/lib/components/icons/HeartIcon.d.ts.map +1 -0
  32. package/lib/components/icons/HeartIcon.js +4 -0
  33. package/lib/components/icons/LineIcon.d.ts.map +1 -0
  34. package/lib/components/icons/PencilIcon.d.ts.map +1 -0
  35. package/lib/components/icons/PolygonIcon.d.ts.map +1 -0
  36. package/lib/components/icons/ProgressDownIcon.d.ts +2 -0
  37. package/lib/components/icons/ProgressDownIcon.d.ts.map +1 -0
  38. package/lib/components/icons/RectangleIcon.d.ts.map +1 -0
  39. package/lib/components/icons/SelectIcon.d.ts.map +1 -0
  40. package/lib/components/icons/TrashIcon.d.ts.map +1 -0
  41. package/lib/controls/MapkaDrawControl.js +7 -7
  42. package/lib/controls/MapkaExportControl.js +2 -2
  43. package/lib/map.d.ts +8 -7
  44. package/lib/map.d.ts.map +1 -1
  45. package/lib/map.js +15 -9
  46. package/lib/modules/layerPopup.d.ts +2 -1
  47. package/lib/modules/layerPopup.d.ts.map +1 -1
  48. package/lib/modules/layerPopup.js +22 -15
  49. package/lib/modules/markers.d.ts +5 -0
  50. package/lib/modules/markers.d.ts.map +1 -1
  51. package/lib/modules/markers.js +21 -16
  52. package/lib/modules/popup.d.ts +3 -10
  53. package/lib/modules/popup.d.ts.map +1 -1
  54. package/lib/modules/popup.js +116 -100
  55. package/lib/modules/popupGroups.d.ts +14 -0
  56. package/lib/modules/popupGroups.d.ts.map +1 -0
  57. package/lib/modules/popupGroups.js +130 -0
  58. package/lib/styles.css +1 -0
  59. package/lib/types/popup.d.ts +10 -1
  60. package/lib/types/popup.d.ts.map +1 -1
  61. package/package.json +26 -7
  62. package/src/components/ImageCarousel.css +73 -0
  63. package/src/components/ImageCarousel.tsx +76 -0
  64. package/src/components/PopupContent.css +64 -174
  65. package/src/components/PopupContent.tsx +32 -202
  66. package/src/components/PopupDataRows.css +41 -0
  67. package/src/components/PopupDataRows.tsx +39 -0
  68. package/src/components/PopupList.css +24 -0
  69. package/src/components/PopupList.tsx +27 -0
  70. package/src/components/PopupListItem.css +61 -0
  71. package/src/components/PopupListItem.tsx +40 -0
  72. package/src/components/icons/ChevronLeftIcon.tsx +20 -0
  73. package/src/components/icons/ChevronRightIcon.tsx +20 -0
  74. package/src/components/icons/CloseIcon.tsx +13 -0
  75. package/src/components/icons/HeartIcon.tsx +18 -0
  76. package/src/components/{ProgressDownIcon.tsx → icons/ProgressDownIcon.tsx} +0 -3
  77. package/src/controls/MapkaDrawControl.tsx +7 -7
  78. package/src/controls/MapkaExportControl.tsx +2 -2
  79. package/src/map.ts +22 -20
  80. package/src/modules/layerPopup.ts +32 -21
  81. package/src/modules/markers.ts +26 -16
  82. package/src/modules/popup.tsx +129 -112
  83. package/src/modules/popupGroups.ts +190 -0
  84. package/src/styles.css +4 -0
  85. package/src/types/popup.ts +12 -1
  86. package/lib/components/CircleIcon.d.ts.map +0 -1
  87. package/lib/components/DownloadIcon.d.ts.map +0 -1
  88. package/lib/components/FreehandIcon.d.ts.map +0 -1
  89. package/lib/components/LineIcon.d.ts.map +0 -1
  90. package/lib/components/PencilIcon.d.ts.map +0 -1
  91. package/lib/components/PolygonIcon.d.ts.map +0 -1
  92. package/lib/components/ProgressDownIcon.d.ts +0 -4
  93. package/lib/components/ProgressDownIcon.d.ts.map +0 -1
  94. package/lib/components/RectangleIcon.d.ts.map +0 -1
  95. package/lib/components/SelectIcon.d.ts.map +0 -1
  96. package/lib/components/TrashIcon.d.ts.map +0 -1
  97. /package/lib/components/{CircleIcon.d.ts → icons/CircleIcon.d.ts} +0 -0
  98. /package/lib/components/{CircleIcon.js → icons/CircleIcon.js} +0 -0
  99. /package/lib/components/{DownloadIcon.d.ts → icons/DownloadIcon.d.ts} +0 -0
  100. /package/lib/components/{DownloadIcon.js → icons/DownloadIcon.js} +0 -0
  101. /package/lib/components/{FreehandIcon.d.ts → icons/FreehandIcon.d.ts} +0 -0
  102. /package/lib/components/{FreehandIcon.js → icons/FreehandIcon.js} +0 -0
  103. /package/lib/components/{LineIcon.d.ts → icons/LineIcon.d.ts} +0 -0
  104. /package/lib/components/{LineIcon.js → icons/LineIcon.js} +0 -0
  105. /package/lib/components/{PencilIcon.d.ts → icons/PencilIcon.d.ts} +0 -0
  106. /package/lib/components/{PencilIcon.js → icons/PencilIcon.js} +0 -0
  107. /package/lib/components/{PolygonIcon.d.ts → icons/PolygonIcon.d.ts} +0 -0
  108. /package/lib/components/{PolygonIcon.js → icons/PolygonIcon.js} +0 -0
  109. /package/lib/components/{ProgressDownIcon.js → icons/ProgressDownIcon.js} +0 -0
  110. /package/lib/components/{RectangleIcon.d.ts → icons/RectangleIcon.d.ts} +0 -0
  111. /package/lib/components/{RectangleIcon.js → icons/RectangleIcon.js} +0 -0
  112. /package/lib/components/{SelectIcon.d.ts → icons/SelectIcon.d.ts} +0 -0
  113. /package/lib/components/{SelectIcon.js → icons/SelectIcon.js} +0 -0
  114. /package/lib/components/{TrashIcon.d.ts → icons/TrashIcon.d.ts} +0 -0
  115. /package/lib/components/{TrashIcon.js → icons/TrashIcon.js} +0 -0
  116. /package/src/components/{CircleIcon.tsx → icons/CircleIcon.tsx} +0 -0
  117. /package/src/components/{DownloadIcon.tsx → icons/DownloadIcon.tsx} +0 -0
  118. /package/src/components/{FreehandIcon.tsx → icons/FreehandIcon.tsx} +0 -0
  119. /package/src/components/{LineIcon.tsx → icons/LineIcon.tsx} +0 -0
  120. /package/src/components/{PencilIcon.tsx → icons/PencilIcon.tsx} +0 -0
  121. /package/src/components/{PolygonIcon.tsx → icons/PolygonIcon.tsx} +0 -0
  122. /package/src/components/{RectangleIcon.tsx → icons/RectangleIcon.tsx} +0 -0
  123. /package/src/components/{SelectIcon.tsx → icons/SelectIcon.tsx} +0 -0
  124. /package/src/components/{TrashIcon.tsx → icons/TrashIcon.tsx} +0 -0
@@ -1,167 +1,184 @@
1
1
  import { Popup } from "maplibre-gl";
2
2
  import { PopupContent } from "../components/PopupContent.js";
3
+ import { PopupList } from "../components/PopupList.js";
3
4
  import { render } from "preact";
4
5
  import { remove } from "es-toolkit/array";
5
- import type { MapkaPopupOptions } from "../types/popup.js";
6
- import type { MapkaMap } from "../map.js";
7
- import { isEqual, isPlainObject } from "es-toolkit";
6
+ import { isPlainObject, without } from "es-toolkit";
7
+ import { computePopupGroups } from "./popupGroups.js";
8
+ import type { MapkaPopupOptions, MapkaPopupOptionsResolved } from "../types/popup.js";
9
+ import type { MapkaMap, MapMapkaPopup } from "../map.js";
8
10
 
9
11
  export function getPopupId(popup: { id?: string }) {
10
12
  return popup.id ?? `popup-${crypto.randomUUID()}`;
11
13
  }
12
14
 
13
- export function getOnClose(map: MapkaMap, id: string) {
14
- return () => map.closePopup(id);
15
+ function getOnClose(map: MapkaMap, id: string) {
16
+ return () => closePopupsByIds(map, [id]);
15
17
  }
16
18
 
17
- export function enforceMaxPopups(map: MapkaMap) {
18
- if (map.popups.length > map.maxPopups) {
19
- const popupToRemove = map.popups.shift();
20
- popupToRemove?.popup.remove();
21
- if (isPlainObject(popupToRemove?.options.content)) {
22
- render(null, popupToRemove.container);
19
+ function hasObjectContent(options: MapkaPopupOptions[]) {
20
+ return options.some((opt) => isPlainObject(opt.content));
21
+ }
22
+
23
+ function resolveContentCreators(options: MapkaPopupOptions[]): MapkaPopupOptionsResolved[] {
24
+ return options.map((opt) => {
25
+ const id = getPopupId(opt);
26
+
27
+ if (typeof opt.content === "function") {
28
+ return {
29
+ ...opt,
30
+ id,
31
+ content: opt.content(id),
32
+ };
33
+ } else {
34
+ return {
35
+ ...opt,
36
+ id,
37
+ content: opt.content,
38
+ };
23
39
  }
24
- popupToRemove?.container.remove();
25
- }
40
+ });
26
41
  }
27
42
 
28
- export function openPopup(map: MapkaMap, options: MapkaPopupOptions) {
29
- const { lngLat, content, closeButton, id = getPopupId(options), ...popupOptions } = options;
30
- if (content instanceof HTMLElement) {
31
- const popup = new Popup({
32
- ...popupOptions,
33
- closeButton: false,
34
- closeOnClick: false,
35
- })
36
- .setLngLat(lngLat)
37
- .setDOMContent(content)
38
- .addTo(map);
43
+ function createNewPopup(map: MapkaMap, options: MapkaPopupOptionsResolved[]) {
44
+ const [{ lngLat, id, content, closeButton, ...opts }] = options;
39
45
 
40
- map.popups.push({
41
- container: content,
42
- id,
43
- options,
44
- popup,
45
- });
46
- enforceMaxPopups(map);
47
- return id;
48
- } else if (typeof content === "object") {
49
- const onClose = getOnClose(map, id);
50
- const container = document.createElement("div");
51
- container.classList.add("mapka-popup-container");
52
-
53
- render(<PopupContent {...content} closeButton={closeButton} onClose={onClose} />, container);
54
-
55
- const popup = new Popup({
56
- ...popupOptions,
46
+ const ids = options.map(getPopupId);
47
+ const container = document.createElement("div");
48
+ container.classList.add("mapka-popup-container");
49
+
50
+ let popup: Popup | undefined;
51
+ if (options.length > 1) {
52
+ render(<PopupList items={options} />, container);
53
+
54
+ popup = new Popup({
55
+ ...opts,
57
56
  closeButton: false,
58
- closeOnClick: false,
57
+ closeOnClick: true,
59
58
  })
60
59
  .setLngLat(lngLat)
61
60
  .setDOMContent(container)
62
61
  .addTo(map);
63
-
64
- map.popups.push({
65
- container,
66
- id,
67
- options,
68
- popup,
69
- });
70
- enforceMaxPopups(map);
71
- return id;
72
- } else if (typeof content === "function") {
73
- const newContent = content(id);
74
- return openPopup(map, {
75
- ...options,
76
- content: newContent,
77
- });
62
+ } else {
63
+ if (content instanceof HTMLElement) {
64
+ popup = new Popup({
65
+ ...opts,
66
+ closeButton: false,
67
+ })
68
+ .setLngLat(lngLat)
69
+ .setDOMContent(content)
70
+ .addTo(map);
71
+ } else {
72
+ popup = new Popup({
73
+ ...opts,
74
+ closeButton: false,
75
+ })
76
+ .setLngLat(lngLat)
77
+ .setDOMContent(container)
78
+ .addTo(map);
79
+
80
+ render(<PopupContent {...content} onClose={getOnClose(map, id)} />, container);
81
+ }
78
82
  }
79
83
 
80
- throw new Error("Invalid popup content");
81
- }
84
+ if (!popup) return;
82
85
 
83
- const DEFAULT_POPUP_MAX_WIDTH = "240px";
86
+ map.popups.push({
87
+ container,
88
+ ids,
89
+ options,
90
+ popup,
91
+ });
84
92
 
85
- export function updatePopupBaseOptions(
86
- popup: Popup,
87
- options: MapkaPopupOptions,
88
- newOptions: Omit<MapkaPopupOptions, "content">,
89
- ) {
90
- if (!isEqual(options.maxWidth, newOptions.maxWidth)) {
91
- popup.setMaxWidth(newOptions.maxWidth ?? DEFAULT_POPUP_MAX_WIDTH);
92
- }
93
- if (!isEqual(options.offset, newOptions.offset)) {
94
- popup.setOffset(newOptions.offset);
95
- }
96
- if (!isEqual(options.lngLat, newOptions.lngLat)) {
97
- popup.setLngLat(newOptions.lngLat);
93
+ return ids;
94
+ }
95
+
96
+ export function enforceMaxPopups(map: MapkaMap) {
97
+ if (map.popups.length > map.maxPopups) {
98
+ const popupToRemove = map.popups.shift();
99
+ if (popupToRemove) {
100
+ popupToRemove.popup.remove();
101
+ if (hasObjectContent(popupToRemove.options)) {
102
+ render(null, popupToRemove.container);
103
+ }
104
+ popupToRemove.container.remove();
105
+ }
98
106
  }
99
- return popup;
100
107
  }
101
108
 
102
- export function updatePopup(map: MapkaMap, { content, ...newOptions }: MapkaPopupOptions) {
103
- const id = getPopupId(newOptions);
109
+ export function reconciliatePopups(map: MapkaMap, options: MapkaPopupOptions[] = []) {
110
+ const resolved = resolveContentCreators(options);
111
+ const actions = computePopupGroups(map, resolved);
104
112
 
105
- if (content instanceof HTMLElement) {
106
- const mapkaPopups = map.popups.filter((popup) => popup.id === id);
107
- for (const { popup, options } of mapkaPopups) {
108
- updatePopupBaseOptions(popup, options, newOptions);
109
- popup.setDOMContent(content);
110
- }
111
- } else if (typeof content === "object") {
112
- const onClose = getOnClose(map, id);
113
- const mapkaPopups = map.popups.filter((popup) => popup.id === id);
114
-
115
- for (const { popup, container, options } of mapkaPopups) {
116
- const { closeButton } = options;
117
- render(<PopupContent {...content} closeButton={closeButton} onClose={onClose} />, container);
118
- updatePopupBaseOptions(popup, options, newOptions);
119
- popup.setDOMContent(container);
113
+ for (const action of actions) {
114
+ if (action.type === "close") {
115
+ closePopupsByIds(map, action.ids);
116
+ } else if (action.type === "create") {
117
+ createNewPopup(map, action.options);
120
118
  }
121
- } else if (typeof content === "function") {
122
- const newContent = content(id);
123
- return updatePopup(map, {
124
- ...newOptions,
125
- content: newContent,
126
- });
127
119
  }
120
+
121
+ enforceMaxPopups(map);
122
+
123
+ return resolved.map((opt) => opt.id);
128
124
  }
129
125
 
130
- /**
131
- * Close all popups that have closeOnClick set to true or undefined
132
- */
133
126
  export function closeOnMapClickPopups(map: MapkaMap) {
134
- const popupsToCloseOnMapClick = remove(
135
- map.popups,
136
- (popup) => popup.options.closeOnClick === true || popup.options.closeOnClick === undefined,
137
- );
127
+ const popupsToCloseOnMapClick = remove(map.popups, (popup) => {
128
+ const [first] = popup.options;
129
+ return (
130
+ popup.ids.length > 1 || first?.closeOnClick === true || first?.closeOnClick === undefined
131
+ );
132
+ });
138
133
  for (const popup of popupsToCloseOnMapClick) {
139
134
  popup.popup.remove();
140
- if (isPlainObject(popup.options.content)) {
135
+ if (hasObjectContent(popup.options)) {
141
136
  render(null, popup.container);
142
137
  }
143
138
  popup.container.remove();
144
139
  }
145
140
  }
146
141
 
147
- export function closePopupsById(map: MapkaMap, id: string) {
148
- const removedPopups = remove(map.popups, (popup) => popup.id === id);
149
- for (const popup of removedPopups) {
142
+ export function closePopups(map: MapkaMap) {
143
+ for (const popup of map.popups) {
150
144
  popup.popup.remove();
151
- if (isPlainObject(popup.options.content)) {
145
+ if (hasObjectContent(popup.options)) {
152
146
  render(null, popup.container);
153
147
  }
154
148
  popup.container.remove();
155
149
  }
150
+ map.popups = [];
156
151
  }
157
152
 
158
- export function removePopups(map: MapkaMap) {
153
+ export function closePopupsByIds(map: MapkaMap, ids: string[]) {
154
+ const toClose: MapMapkaPopup[] = [];
155
+ const toReRender: MapMapkaPopup[] = [];
156
+
159
157
  for (const popup of map.popups) {
158
+ const itemIndex = popup.ids.findIndex((id) => ids.includes(id));
159
+ if (itemIndex < 0) continue;
160
+
161
+ if (popup.ids.length === 1) {
162
+ toClose.push(popup);
163
+ } else if (popup.ids.length > 1) {
164
+ popup.ids.splice(itemIndex, 1);
165
+ popup.options.splice(itemIndex, 1);
166
+
167
+ toClose.push(popup);
168
+ toReRender.push(popup);
169
+ }
170
+ }
171
+
172
+ for (const popup of toClose) {
160
173
  popup.popup.remove();
161
- if (isPlainObject(popup.options.content)) {
174
+ if (hasObjectContent(popup.options)) {
162
175
  render(null, popup.container);
163
176
  }
164
177
  popup.container.remove();
178
+ map.popups = without(map.popups, popup);
179
+ }
180
+
181
+ for (const popup of toReRender) {
182
+ createNewPopup(map, popup.options);
165
183
  }
166
- map.popups = [];
167
184
  }
@@ -0,0 +1,190 @@
1
+ import Supercluster from "supercluster";
2
+ import type { MapkaMap, MapMapkaPopup } from "../map.js";
3
+ import type { MapkaPopupOptionsResolved } from "../types/popup.js";
4
+
5
+ export type ClosePopupAction = { type: "close"; ids: string[] };
6
+ export type CreatePopupAction = { type: "create"; options: MapkaPopupOptionsResolved[] };
7
+
8
+ export type PopupGroupAction = ClosePopupAction | CreatePopupAction;
9
+
10
+ interface PopupCluster {
11
+ options: MapkaPopupOptionsResolved[];
12
+ lngLat: number[];
13
+ }
14
+
15
+ interface PopupPointProps {
16
+ index: number;
17
+ }
18
+
19
+ const WORLD_BOUNDS: [number, number, number, number] = [-180, -90, 180, 90];
20
+
21
+ function clustersByLocation(
22
+ previous: MapkaPopupOptionsResolved[],
23
+ newOptions: MapkaPopupOptionsResolved[],
24
+ zoom: number,
25
+ ): PopupCluster[] {
26
+ const all = [...previous, ...newOptions];
27
+
28
+ const features: Supercluster.PointFeature<PopupPointProps>[] = all.map(({ lngLat }, index) => ({
29
+ type: "Feature",
30
+ properties: { index },
31
+ geometry: {
32
+ type: "Point",
33
+ coordinates: lngLat,
34
+ },
35
+ }));
36
+
37
+ const sc = new Supercluster<PopupPointProps>({
38
+ radius: 20,
39
+ extent: 512,
40
+ });
41
+ sc.load(features);
42
+
43
+ const results = sc.getClusters(WORLD_BOUNDS, zoom);
44
+ const clusters: PopupCluster[] = [];
45
+
46
+ for (const { properties, geometry } of results) {
47
+ if ("cluster" in properties) {
48
+ const leaves = sc.getLeaves(properties.cluster_id, Infinity);
49
+ clusters.push({
50
+ options: leaves.map((point) => all[point.properties.index]),
51
+ lngLat: geometry.coordinates,
52
+ });
53
+ } else {
54
+ const opt = all[properties.index];
55
+ clusters.push({
56
+ options: [opt],
57
+ lngLat: opt.lngLat,
58
+ });
59
+ }
60
+ }
61
+
62
+ return clusters;
63
+ }
64
+
65
+ function clustersFromPopups(
66
+ notUpdated: MapkaPopupOptionsResolved[],
67
+ popups: MapMapkaPopup[],
68
+ ): PopupCluster[] {
69
+ const clusters: PopupCluster[] = [];
70
+ for (const popup of popups) {
71
+ clusters.push({
72
+ options: popup.options.filter((opt) => notUpdated.includes(opt)),
73
+ lngLat: popup.popup.getLngLat().toArray(),
74
+ });
75
+ }
76
+ return clusters.filter((cluster) => cluster.options.length > 0);
77
+ }
78
+
79
+ function clusterKey(cluster: PopupCluster): string {
80
+ return cluster.options
81
+ .map((o) => o.id)
82
+ .sort()
83
+ .join("-");
84
+ }
85
+
86
+ function actionsByClustersChanges(
87
+ prev: PopupCluster[],
88
+ next: PopupCluster[],
89
+ ): {
90
+ close: ClosePopupAction[];
91
+ create: CreatePopupAction[];
92
+ } {
93
+ const prevByKey = new Map(prev.map((c) => [clusterKey(c), c]));
94
+ const nextByKey = new Map(next.map((c) => [clusterKey(c), c]));
95
+
96
+ const close: ClosePopupAction[] = [];
97
+ const create: CreatePopupAction[] = [];
98
+
99
+ for (const [key, cluster] of prevByKey) {
100
+ if (!nextByKey.has(key)) {
101
+ close.push({
102
+ type: "close",
103
+ ids: cluster.options.map((o) => o.id),
104
+ });
105
+ }
106
+ }
107
+
108
+ for (const [key, cluster] of nextByKey) {
109
+ if (!prevByKey.has(key)) {
110
+ create.push({
111
+ type: "create",
112
+ options: cluster.options,
113
+ });
114
+ }
115
+ }
116
+
117
+ return { close, create };
118
+ }
119
+
120
+ function actionsByProximity(
121
+ popups: MapMapkaPopup[],
122
+ newOptions: MapkaPopupOptionsResolved[],
123
+ zoom: number,
124
+ ): PopupGroupAction[] {
125
+ const updated: MapkaPopupOptionsResolved[] = [];
126
+ const nonUpdated: MapkaPopupOptionsResolved[] = [];
127
+
128
+ for (const popup of popups) {
129
+ for (const opt of popup.options) {
130
+ if (newOptions.find((o) => o.id === opt.id)) {
131
+ updated.push(opt);
132
+ } else {
133
+ nonUpdated.push(opt);
134
+ }
135
+ }
136
+ }
137
+
138
+ const prevClusters = clustersFromPopups(nonUpdated, popups);
139
+ const nextClusters = clustersByLocation(nonUpdated, newOptions, zoom);
140
+ const { close, create } = actionsByClustersChanges(prevClusters, nextClusters);
141
+
142
+ const updatedIds = updated.map((opt) => opt.id);
143
+ const closeIds = close.flatMap((opt) => opt.ids);
144
+
145
+ return [
146
+ {
147
+ type: "close" as const,
148
+ ids: updatedIds.concat(closeIds),
149
+ },
150
+ ...create,
151
+ ];
152
+ }
153
+
154
+ export function actionsByChanges(
155
+ popups: MapMapkaPopup[],
156
+ newOptions: MapkaPopupOptionsResolved[],
157
+ ): PopupGroupAction[] {
158
+ const updated = [];
159
+
160
+ for (const popup of popups) {
161
+ if (newOptions.find((o) => popup.ids.includes(o.id))) {
162
+ updated.push(popup);
163
+ }
164
+ }
165
+
166
+ return [
167
+ ...updated.map((popup) => ({
168
+ type: "close" as const,
169
+ ids: popup.ids,
170
+ })),
171
+ {
172
+ type: "create" as const,
173
+ options: newOptions,
174
+ },
175
+ ];
176
+ }
177
+
178
+ export function computePopupGroups(
179
+ map: MapkaMap,
180
+ newOptions: MapkaPopupOptionsResolved[],
181
+ ): PopupGroupAction[] {
182
+ const popups = map.getPopups();
183
+ const zoom = map.getZoom();
184
+
185
+ if (map.scrollPopups) {
186
+ return actionsByProximity(popups, newOptions, zoom);
187
+ } else {
188
+ return actionsByChanges(popups, newOptions);
189
+ }
190
+ }
package/src/styles.css CHANGED
@@ -1,4 +1,8 @@
1
1
  @import "../../../node_modules/maplibre-gl/dist/maplibre-gl.css";
2
2
 
3
+ @import "./components/ImageCarousel.css";
4
+ @import "./components/PopupList.css";
5
+ @import "./components/PopupListItem.css";
6
+ @import "./components/PopupDataRows.css";
3
7
  @import "./components/PopupContent.css";
4
8
  @import "./controls/MapkaDrawControl.css";
@@ -5,6 +5,11 @@ export interface MapkaPopupRow {
5
5
  value: unknown;
6
6
  }
7
7
 
8
+ export interface MapkaPopupAction {
9
+ label: string;
10
+ onClick?: () => void;
11
+ }
12
+
8
13
  export type MapkaLayerPopupRow = {
9
14
  name: ExpressionSpecification | string;
10
15
  value: ExpressionSpecification | unknown;
@@ -15,7 +20,8 @@ export interface MapkaPopupContent {
15
20
  description?: string;
16
21
  rows?: MapkaPopupRow[];
17
22
  imageUrls?: string[];
18
- onFavorite?: (id: string) => void;
23
+ /** Primary action button displayed in the popup */
24
+ primaryAction?: MapkaPopupAction;
19
25
  }
20
26
 
21
27
  type CreatePopupElement = (id: string) => HTMLElement;
@@ -34,6 +40,11 @@ export interface MapkaPopupOptions extends PopupOptions {
34
40
  content: MapkaPopupCreator;
35
41
  }
36
42
 
43
+ export interface MapkaPopupOptionsResolved extends MapkaPopupOptions {
44
+ id: string;
45
+ content: HTMLElement | MapkaPopupContent;
46
+ }
47
+
37
48
  export interface MapkaLayerPopupContent {
38
49
  title: ExpressionSpecification | string;
39
50
  description: ExpressionSpecification | string;
@@ -1 +0,0 @@
1
- {"version":3,"file":"CircleIcon.d.ts","sourceRoot":"","sources":["../../src/components/CircleIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,oCAmBtB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"DownloadIcon.d.ts","sourceRoot":"","sources":["../../src/components/DownloadIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,oCAqBxB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"FreehandIcon.d.ts","sourceRoot":"","sources":["../../src/components/FreehandIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,oCAmBxB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"LineIcon.d.ts","sourceRoot":"","sources":["../../src/components/LineIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,oCAqBpB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"PencilIcon.d.ts","sourceRoot":"","sources":["../../src/components/PencilIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,oCAoBtB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"PolygonIcon.d.ts","sourceRoot":"","sources":["../../src/components/PolygonIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,oCA0BvB,CAAC"}
@@ -1,4 +0,0 @@
1
- /** biome-ignore-all lint/correctness/noUnusedImports: <explanation> */
2
- import { h } from "preact";
3
- export declare const ProgressDownIcon: () => h.JSX.Element;
4
- //# sourceMappingURL=ProgressDownIcon.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ProgressDownIcon.d.ts","sourceRoot":"","sources":["../../src/components/ProgressDownIcon.tsx"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAE3B,eAAO,MAAM,gBAAgB,qBAyB5B,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"RectangleIcon.d.ts","sourceRoot":"","sources":["../../src/components/RectangleIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,oCAmBzB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"SelectIcon.d.ts","sourceRoot":"","sources":["../../src/components/SelectIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,oCAoBtB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"TrashIcon.d.ts","sourceRoot":"","sources":["../../src/components/TrashIcon.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,oCAuBrB,CAAC"}