@accelint/map-toolkit 0.1.0 → 0.2.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/CHANGELOG.md +29 -0
- package/README.md +91 -18
- package/catalog-info.yaml +13 -10
- package/dist/deckgl/base-map/index.d.ts +91 -8
- package/dist/deckgl/base-map/index.js +26 -20
- package/dist/deckgl/base-map/index.js.map +1 -1
- package/dist/deckgl/base-map/provider.d.ts +133 -0
- package/dist/deckgl/base-map/provider.js +22 -0
- package/dist/deckgl/base-map/provider.js.map +1 -0
- package/dist/deckgl/base-map/types.d.ts +40 -0
- package/dist/deckgl/index.d.ts +3 -2
- package/dist/deckgl/index.js +1 -1
- package/dist/deckgl/text-layer/character-sets.d.ts +20 -0
- package/dist/deckgl/text-layer/character-sets.js +36 -0
- package/dist/deckgl/text-layer/character-sets.js.map +1 -0
- package/dist/deckgl/text-layer/default-settings.d.ts +5 -0
- package/dist/deckgl/text-layer/default-settings.js +23 -0
- package/dist/deckgl/text-layer/default-settings.js.map +1 -0
- package/dist/deckgl/text-layer/fiber.d.ts +31 -0
- package/dist/deckgl/text-layer/fiber.js +6 -0
- package/dist/deckgl/text-layer/fiber.js.map +1 -0
- package/dist/deckgl/text-layer/index.d.ts +43 -0
- package/dist/deckgl/text-layer/index.js +33 -0
- package/dist/deckgl/text-layer/index.js.map +1 -0
- package/dist/decorators/deckgl.js +3 -1
- package/dist/decorators/deckgl.js.map +1 -1
- package/dist/map-mode/events.d.ts +37 -0
- package/dist/map-mode/events.js +15 -0
- package/dist/map-mode/events.js.map +1 -0
- package/dist/map-mode/index.d.ts +6 -0
- package/dist/map-mode/index.js +5 -0
- package/dist/map-mode/index.js.map +1 -0
- package/dist/map-mode/store.d.ts +122 -0
- package/dist/map-mode/store.js +327 -0
- package/dist/map-mode/store.js.map +1 -0
- package/dist/map-mode/types.d.ts +83 -0
- package/dist/map-mode/types.js +3 -0
- package/dist/map-mode/types.js.map +1 -0
- package/dist/map-mode/use-map-mode.d.ts +54 -0
- package/dist/map-mode/use-map-mode.js +31 -0
- package/dist/map-mode/use-map-mode.js.map +1 -0
- package/dist/maplibre/hooks/use-maplibre.d.ts +34 -0
- package/dist/maplibre/hooks/use-maplibre.js +3 -2
- package/dist/maplibre/hooks/use-maplibre.js.map +1 -1
- package/dist/metafile-esm.json +1 -1
- package/package.json +43 -36
- package/dist/test/setup.d.ts +0 -2
- package/dist/test/setup.js +0 -11
- package/dist/test/setup.js.map +0 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { Broadcast } from '@accelint/bus';
|
|
2
|
+
import { uuid } from '@accelint/core';
|
|
3
|
+
import { MapModeEvents } from './events.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MODE = "default";
|
|
6
|
+
const mapModeBus = Broadcast.getInstance();
|
|
7
|
+
class MapModeStore {
|
|
8
|
+
constructor(id) {
|
|
9
|
+
this.id = id;
|
|
10
|
+
this.id = id;
|
|
11
|
+
this.setupEventListeners();
|
|
12
|
+
}
|
|
13
|
+
mode = DEFAULT_MODE;
|
|
14
|
+
defaultMode = DEFAULT_MODE;
|
|
15
|
+
modeOwners = /* @__PURE__ */ new Map();
|
|
16
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
17
|
+
listeners = /* @__PURE__ */ new Set();
|
|
18
|
+
bus = mapModeBus;
|
|
19
|
+
unsubscribers = [];
|
|
20
|
+
/**
|
|
21
|
+
* Get current mode snapshot (for useSyncExternalStore)
|
|
22
|
+
*/
|
|
23
|
+
getSnapshot = () => {
|
|
24
|
+
return this.mode;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to mode changes (for useSyncExternalStore)
|
|
28
|
+
*/
|
|
29
|
+
subscribe = (listener) => {
|
|
30
|
+
this.listeners.add(listener);
|
|
31
|
+
return () => {
|
|
32
|
+
this.listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Request a mode change
|
|
37
|
+
*
|
|
38
|
+
* If the mode change can be auto-accepted (no ownership conflicts), the mode changes immediately.
|
|
39
|
+
* Otherwise, an authorization request is emitted and stored as a pending request.
|
|
40
|
+
*
|
|
41
|
+
* **Important**: If the requester already has a pending authorization request, it will be replaced
|
|
42
|
+
* with this new request. Only one pending request per requester is maintained at a time.
|
|
43
|
+
*
|
|
44
|
+
* @param desiredMode - The mode to switch to (automatically trimmed of whitespace)
|
|
45
|
+
* @param requestOwner - Unique identifier of the component requesting the change (automatically trimmed of whitespace)
|
|
46
|
+
* @throws Error if either parameter is empty or whitespace-only
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* // First request from 'drawing-tool'
|
|
51
|
+
* store.requestModeChange('drawing', 'drawing-tool');
|
|
52
|
+
* // → Creates pending request with authId 'abc-123'
|
|
53
|
+
*
|
|
54
|
+
* // Second request from same 'drawing-tool' before first is resolved
|
|
55
|
+
* store.requestModeChange('measuring', 'drawing-tool');
|
|
56
|
+
* // → Replaces pending request, new authId 'def-456', old 'abc-123' is discarded
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
requestModeChange = (desiredMode, requestOwner) => {
|
|
60
|
+
const trimmedDesiredMode = desiredMode.trim();
|
|
61
|
+
const trimmedRequestOwner = requestOwner.trim();
|
|
62
|
+
if (!trimmedDesiredMode) {
|
|
63
|
+
throw new Error("requestModeChange requires non-empty desiredMode");
|
|
64
|
+
}
|
|
65
|
+
if (!trimmedRequestOwner) {
|
|
66
|
+
throw new Error("requestModeChange requires non-empty requestOwner");
|
|
67
|
+
}
|
|
68
|
+
this.bus.emit(MapModeEvents.changeRequest, {
|
|
69
|
+
desiredMode: trimmedDesiredMode,
|
|
70
|
+
owner: trimmedRequestOwner,
|
|
71
|
+
id: this.id
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Notify all subscribers of state change
|
|
76
|
+
*/
|
|
77
|
+
notify() {
|
|
78
|
+
for (const listener of this.listeners) {
|
|
79
|
+
listener();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Setup event listeners for bus events
|
|
84
|
+
*
|
|
85
|
+
* Note: Event listeners remain active even after early returns in handlers.
|
|
86
|
+
* This is by design - cleanup happens in destroy() which is called automatically
|
|
87
|
+
* by MapProvider on unmount. Consumers don't need to manually manage cleanup.
|
|
88
|
+
*/
|
|
89
|
+
setupEventListeners() {
|
|
90
|
+
const unsubRequest = this.bus.on(MapModeEvents.changeRequest, (event) => {
|
|
91
|
+
const { desiredMode, owner: requestOwner, id } = event.payload;
|
|
92
|
+
if (id !== this.id || desiredMode === this.mode) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.handleModeChangeRequest(desiredMode, requestOwner);
|
|
96
|
+
});
|
|
97
|
+
this.unsubscribers.push(unsubRequest);
|
|
98
|
+
const unsubDecision = this.bus.on(MapModeEvents.changeDecision, (event) => {
|
|
99
|
+
const { id, approved, authId, owner } = event.payload;
|
|
100
|
+
if (id !== this.id) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.handleAuthorizationDecision({ approved, authId, owner });
|
|
104
|
+
});
|
|
105
|
+
this.unsubscribers.push(unsubDecision);
|
|
106
|
+
const unsubChanged = this.bus.on(MapModeEvents.changed, (event) => {
|
|
107
|
+
const { currentMode, previousMode, id } = event.payload;
|
|
108
|
+
if (id !== this.id) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (currentMode === this.defaultMode && this.pendingRequests.size > 0) {
|
|
112
|
+
this.handlePendingRequestsOnDefaultMode(previousMode);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
this.unsubscribers.push(unsubChanged);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Determine if a mode change request should be auto-accepted without authorization
|
|
119
|
+
*/
|
|
120
|
+
shouldAutoAcceptRequest(desiredMode, requestOwner, currentModeOwner, desiredModeOwner) {
|
|
121
|
+
if (desiredMode === this.defaultMode && requestOwner === currentModeOwner) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (requestOwner === currentModeOwner) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (!(currentModeOwner || desiredModeOwner)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (this.mode === this.defaultMode && requestOwner === desiredModeOwner) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Handle mode change request logic
|
|
137
|
+
*/
|
|
138
|
+
handleModeChangeRequest(desiredMode, requestOwner) {
|
|
139
|
+
const currentModeOwner = this.modeOwners.get(this.mode);
|
|
140
|
+
const desiredModeOwner = this.modeOwners.get(desiredMode);
|
|
141
|
+
if (this.shouldAutoAcceptRequest(
|
|
142
|
+
desiredMode,
|
|
143
|
+
requestOwner,
|
|
144
|
+
currentModeOwner,
|
|
145
|
+
desiredModeOwner
|
|
146
|
+
)) {
|
|
147
|
+
this.setMode(desiredMode);
|
|
148
|
+
if (desiredMode !== this.defaultMode && !desiredModeOwner) {
|
|
149
|
+
this.modeOwners.set(desiredMode, requestOwner);
|
|
150
|
+
}
|
|
151
|
+
this.pendingRequests.delete(requestOwner);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const authId = uuid();
|
|
155
|
+
this.pendingRequests.set(requestOwner, {
|
|
156
|
+
authId,
|
|
157
|
+
desiredMode,
|
|
158
|
+
currentMode: this.mode,
|
|
159
|
+
requestOwner
|
|
160
|
+
});
|
|
161
|
+
this.bus.emit(MapModeEvents.changeAuthorization, {
|
|
162
|
+
authId,
|
|
163
|
+
desiredMode,
|
|
164
|
+
currentMode: this.mode,
|
|
165
|
+
id: this.id
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Handle authorization decision
|
|
170
|
+
*
|
|
171
|
+
* Processes approval/rejection decisions from mode owners. Only the current mode's owner
|
|
172
|
+
* can make authorization decisions. If a decision comes from a non-owner, a warning is
|
|
173
|
+
* logged and the decision is ignored to prevent unauthorized mode changes.
|
|
174
|
+
*
|
|
175
|
+
* @param payload - The authorization decision containing authId, approved status, and owner
|
|
176
|
+
*/
|
|
177
|
+
handleAuthorizationDecision(payload) {
|
|
178
|
+
const { approved, authId, owner: decisionOwner } = payload;
|
|
179
|
+
const currentModeOwner = this.modeOwners.get(this.mode);
|
|
180
|
+
if (decisionOwner !== currentModeOwner) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`[MapMode] Authorization decision from "${decisionOwner}" ignored - not the owner of mode "${this.mode}" (owner: ${currentModeOwner || "none"})`
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let matchingRequestOwner = null;
|
|
187
|
+
let matchingRequest = null;
|
|
188
|
+
for (const [requestOwner, request] of this.pendingRequests.entries()) {
|
|
189
|
+
if (request.authId === authId) {
|
|
190
|
+
matchingRequestOwner = requestOwner;
|
|
191
|
+
matchingRequest = request;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!(matchingRequest && matchingRequestOwner)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (approved) {
|
|
199
|
+
this.approveRequestAndRejectOthers(
|
|
200
|
+
matchingRequest,
|
|
201
|
+
authId,
|
|
202
|
+
decisionOwner,
|
|
203
|
+
"",
|
|
204
|
+
false
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
this.pendingRequests.delete(matchingRequestOwner);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Approve a request and reject all others
|
|
212
|
+
*/
|
|
213
|
+
approveRequestAndRejectOthers(approvedRequest, excludeAuthId, decisionOwner, reason, emitApproval) {
|
|
214
|
+
const requestsToReject = [];
|
|
215
|
+
for (const request of this.pendingRequests.values()) {
|
|
216
|
+
if (request.authId !== excludeAuthId) {
|
|
217
|
+
requestsToReject.push(request);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.pendingRequests.clear();
|
|
221
|
+
this.setMode(approvedRequest.desiredMode);
|
|
222
|
+
if (approvedRequest.desiredMode !== this.defaultMode) {
|
|
223
|
+
this.modeOwners.set(
|
|
224
|
+
approvedRequest.desiredMode,
|
|
225
|
+
approvedRequest.requestOwner
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (emitApproval) {
|
|
229
|
+
this.bus.emit(MapModeEvents.changeDecision, {
|
|
230
|
+
authId: approvedRequest.authId,
|
|
231
|
+
approved: true,
|
|
232
|
+
owner: decisionOwner,
|
|
233
|
+
reason,
|
|
234
|
+
id: this.id
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
for (const request of requestsToReject) {
|
|
238
|
+
this.bus.emit(MapModeEvents.changeDecision, {
|
|
239
|
+
authId: request.authId,
|
|
240
|
+
approved: false,
|
|
241
|
+
owner: decisionOwner,
|
|
242
|
+
reason: "Request auto-rejected because another request was approved",
|
|
243
|
+
id: this.id
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Handle pending requests when returning to default mode
|
|
249
|
+
*/
|
|
250
|
+
handlePendingRequestsOnDefaultMode(previousMode) {
|
|
251
|
+
const firstEntry = Array.from(this.pendingRequests.values())[0];
|
|
252
|
+
if (!firstEntry) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const previousModeOwner = this.modeOwners.get(previousMode);
|
|
256
|
+
if (!previousModeOwner) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (firstEntry.desiredMode === this.defaultMode) {
|
|
260
|
+
const allRequests = Array.from(this.pendingRequests.values());
|
|
261
|
+
this.pendingRequests.clear();
|
|
262
|
+
for (const request of allRequests) {
|
|
263
|
+
this.bus.emit(MapModeEvents.changeDecision, {
|
|
264
|
+
authId: request.authId,
|
|
265
|
+
approved: false,
|
|
266
|
+
owner: previousModeOwner,
|
|
267
|
+
reason: "Request rejected - already in requested mode",
|
|
268
|
+
id: this.id
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
this.approveRequestAndRejectOthers(
|
|
273
|
+
firstEntry,
|
|
274
|
+
firstEntry.authId,
|
|
275
|
+
previousModeOwner,
|
|
276
|
+
"Auto-accepted when mode owner returned to default",
|
|
277
|
+
true
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Set mode and notify listeners
|
|
283
|
+
*/
|
|
284
|
+
setMode(newMode) {
|
|
285
|
+
const previousMode = this.mode;
|
|
286
|
+
this.mode = newMode;
|
|
287
|
+
this.bus.emit(MapModeEvents.changed, {
|
|
288
|
+
previousMode,
|
|
289
|
+
currentMode: newMode,
|
|
290
|
+
id: this.id
|
|
291
|
+
});
|
|
292
|
+
this.notify();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Clean up store resources
|
|
296
|
+
*/
|
|
297
|
+
destroy() {
|
|
298
|
+
for (const unsubscribe of this.unsubscribers) {
|
|
299
|
+
unsubscribe();
|
|
300
|
+
}
|
|
301
|
+
this.unsubscribers.length = 0;
|
|
302
|
+
this.modeOwners.clear();
|
|
303
|
+
this.pendingRequests.clear();
|
|
304
|
+
this.listeners.clear();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const storeRegistry = /* @__PURE__ */ new Map();
|
|
308
|
+
function getOrCreateStore(id) {
|
|
309
|
+
if (!storeRegistry.has(id)) {
|
|
310
|
+
storeRegistry.set(id, new MapModeStore(id));
|
|
311
|
+
}
|
|
312
|
+
return storeRegistry.get(id);
|
|
313
|
+
}
|
|
314
|
+
function destroyStore(id) {
|
|
315
|
+
const store = storeRegistry.get(id);
|
|
316
|
+
if (store) {
|
|
317
|
+
store.destroy();
|
|
318
|
+
storeRegistry.delete(id);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function getStore(id) {
|
|
322
|
+
return storeRegistry.get(id);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export { MapModeStore, destroyStore, getOrCreateStore, getStore };
|
|
326
|
+
//# sourceMappingURL=store.js.map
|
|
327
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/map-mode/store.ts"],"names":[],"mappings":";;;;AAkBA,MAAM,YAAA,GAAe,SAAA;AAMrB,MAAM,UAAA,GAAa,UAAU,WAAA,EAA8B;AA6BpD,MAAM,YAAA,CAAa;AAAA,EASxB,YAA6B,EAAA,EAAc;AAAd,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAC3B,IAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AAEV,IAAA,IAAA,CAAK,mBAAA,EAAoB;AAAA,EAC3B;AAAA,EAZQ,IAAA,GAAO,YAAA;AAAA,EACE,WAAA,GAAc,YAAA;AAAA,EACd,UAAA,uBAAiB,GAAA,EAAoB;AAAA,EACrC,eAAA,uBAAsB,GAAA,EAA4B;AAAA,EAClD,SAAA,uBAAgB,GAAA,EAAgB;AAAA,EAChC,GAAA,GAAM,UAAA;AAAA,EACN,gBAAmC,EAAC;AAAA;AAAA;AAAA;AAAA,EAWrD,cAAc,MAAc;AAC1B,IAAA,OAAO,IAAA,CAAK,IAAA;AAAA,EACd,CAAA;AAAA;AAAA;AAAA;AAAA,EAKA,SAAA,GAAY,CAAC,QAAA,KAAuC;AAClD,IAAA,IAAA,CAAK,SAAA,CAAU,IAAI,QAAQ,CAAA;AAC3B,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,CAAK,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,IAChC,CAAA;AAAA,EACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BA,iBAAA,GAAoB,CAAC,WAAA,EAAqB,YAAA,KAA+B;AACvE,IAAA,MAAM,kBAAA,GAAqB,YAAY,IAAA,EAAK;AAC5C,IAAA,MAAM,mBAAA,GAAsB,aAAa,IAAA,EAAK;AAE9C,IAAA,IAAI,CAAC,kBAAA,EAAoB;AACvB,MAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,IACpE;AACA,IAAA,IAAI,CAAC,mBAAA,EAAqB;AACxB,MAAA,MAAM,IAAI,MAAM,mDAAmD,CAAA;AAAA,IACrE;AAEA,IAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,aAAA,CAAc,aAAA,EAAe;AAAA,MACzC,WAAA,EAAa,kBAAA;AAAA,MACb,KAAA,EAAO,mBAAA;AAAA,MACP,IAAI,IAAA,CAAK;AAAA,KACV,CAAA;AAAA,EACH,CAAA;AAAA;AAAA;AAAA;AAAA,EAKQ,MAAA,GAAe;AACrB,IAAA,KAAA,MAAW,QAAA,IAAY,KAAK,SAAA,EAAW;AACrC,MAAA,QAAA,EAAS;AAAA,IACX;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,mBAAA,GAA4B;AAElC,IAAA,MAAM,eAAe,IAAA,CAAK,GAAA,CAAI,GAAG,aAAA,CAAc,aAAA,EAAe,CAAC,KAAA,KAAU;AACvE,MAAA,MAAM,EAAE,WAAA,EAAa,KAAA,EAAO,YAAA,EAAc,EAAA,KAAO,KAAA,CAAM,OAAA;AAGvD,MAAA,IAAI,EAAA,KAAO,IAAA,CAAK,EAAA,IAAM,WAAA,KAAgB,KAAK,IAAA,EAAM;AAC/C,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,CAAK,uBAAA,CAAwB,aAAa,YAAY,CAAA;AAAA,IACxD,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,aAAA,CAAc,KAAK,YAAY,CAAA;AAGpC,IAAA,MAAM,gBAAgB,IAAA,CAAK,GAAA,CAAI,GAAG,aAAA,CAAc,cAAA,EAAgB,CAAC,KAAA,KAAU;AACzE,MAAA,MAAM,EAAE,EAAA,EAAI,QAAA,EAAU,MAAA,EAAQ,KAAA,KAAU,KAAA,CAAM,OAAA;AAG9C,MAAA,IAAI,EAAA,KAAO,KAAK,EAAA,EAAI;AAClB,QAAA;AAAA,MACF;AAEA,MAAA,IAAA,CAAK,2BAAA,CAA4B,EAAE,QAAA,EAAU,MAAA,EAAQ,OAAO,CAAA;AAAA,IAC9D,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,aAAA,CAAc,KAAK,aAAa,CAAA;AAGrC,IAAA,MAAM,eAAe,IAAA,CAAK,GAAA,CAAI,GAAG,aAAA,CAAc,OAAA,EAAS,CAAC,KAAA,KAAU;AACjE,MAAA,MAAM,EAAE,WAAA,EAAa,YAAA,EAAc,EAAA,KAAO,KAAA,CAAM,OAAA;AAGhD,MAAA,IAAI,EAAA,KAAO,KAAK,EAAA,EAAI;AAClB,QAAA;AAAA,MACF;AAGA,MAAA,IAAI,gBAAgB,IAAA,CAAK,WAAA,IAAe,IAAA,CAAK,eAAA,CAAgB,OAAO,CAAA,EAAG;AACrE,QAAA,IAAA,CAAK,mCAAmC,YAAY,CAAA;AAAA,MACtD;AAAA,IACF,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,aAAA,CAAc,KAAK,YAAY,CAAA;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKQ,uBAAA,CACN,WAAA,EACA,YAAA,EACA,gBAAA,EACA,gBAAA,EACS;AAET,IAAA,IAAI,WAAA,KAAgB,IAAA,CAAK,WAAA,IAAe,YAAA,KAAiB,gBAAA,EAAkB;AACzE,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,IAAI,iBAAiB,gBAAA,EAAkB;AACrC,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,IAAI,EAAE,oBAAoB,gBAAA,CAAA,EAAmB;AAC3C,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,IAAI,IAAA,CAAK,IAAA,KAAS,IAAA,CAAK,WAAA,IAAe,iBAAiB,gBAAA,EAAkB;AACvE,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO,KAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,uBAAA,CACN,aACA,YAAA,EACM;AACN,IAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,KAAK,IAAI,CAAA;AACtD,IAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,WAAW,CAAA;AAGxD,IAAA,IACE,IAAA,CAAK,uBAAA;AAAA,MACH,WAAA;AAAA,MACA,YAAA;AAAA,MACA,gBAAA;AAAA,MACA;AAAA,KACF,EACA;AACA,MAAA,IAAA,CAAK,QAAQ,WAAW,CAAA;AAGxB,MAAA,IAAI,WAAA,KAAgB,IAAA,CAAK,WAAA,IAAe,CAAC,gBAAA,EAAkB;AACzD,QAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,WAAA,EAAa,YAAY,CAAA;AAAA,MAC/C;AAGA,MAAA,IAAA,CAAK,eAAA,CAAgB,OAAO,YAAY,CAAA;AACxC,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,SAAS,IAAA,EAAK;AAEpB,IAAA,IAAA,CAAK,eAAA,CAAgB,IAAI,YAAA,EAAc;AAAA,MACrC,MAAA;AAAA,MACA,WAAA;AAAA,MACA,aAAa,IAAA,CAAK,IAAA;AAAA,MAClB;AAAA,KACD,CAAA;AAED,IAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,aAAA,CAAc,mBAAA,EAAqB;AAAA,MAC/C,MAAA;AAAA,MACA,WAAA;AAAA,MACA,aAAa,IAAA,CAAK,IAAA;AAAA,MAClB,IAAI,IAAA,CAAK;AAAA,KACV,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWQ,4BAA4B,OAAA,EAI3B;AACP,IAAA,MAAM,EAAE,QAAA,EAAU,MAAA,EAAQ,KAAA,EAAO,eAAc,GAAI,OAAA;AAInD,IAAA,MAAM,gBAAA,GAAmB,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,KAAK,IAAI,CAAA;AACtD,IAAA,IAAI,kBAAkB,gBAAA,EAAkB;AACtC,MAAA,OAAA,CAAQ,IAAA;AAAA,QACN,0CAA0C,aAAa,CAAA,mCAAA,EAAsC,KAAK,IAAI,CAAA,UAAA,EAAa,oBAAoB,MAAM,CAAA,CAAA;AAAA,OAC/I;AACA,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,oBAAA,GAAsC,IAAA;AAC1C,IAAA,IAAI,eAAA,GAAyC,IAAA;AAE7C,IAAA,KAAA,MAAW,CAAC,YAAA,EAAc,OAAO,KAAK,IAAA,CAAK,eAAA,CAAgB,SAAQ,EAAG;AACpE,MAAA,IAAI,OAAA,CAAQ,WAAW,MAAA,EAAQ;AAC7B,QAAA,oBAAA,GAAuB,YAAA;AACvB,QAAA,eAAA,GAAkB,OAAA;AAClB,QAAA;AAAA,MACF;AAAA,IACF;AAEA,IAAA,IAAI,EAAE,mBAAmB,oBAAA,CAAA,EAAuB;AAC9C,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,IAAA,CAAK,6BAAA;AAAA,QACH,eAAA;AAAA,QACA,MAAA;AAAA,QACA,aAAA;AAAA,QACA,EAAA;AAAA,QACA;AAAA,OACF;AAAA,IACF,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,eAAA,CAAgB,OAAO,oBAAoB,CAAA;AAAA,IAClD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,6BAAA,CACN,eAAA,EACA,aAAA,EACA,aAAA,EACA,QACA,YAAA,EACM;AAEN,IAAA,MAAM,mBAAqC,EAAC;AAC5C,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,eAAA,CAAgB,MAAA,EAAO,EAAG;AACnD,MAAA,IAAI,OAAA,CAAQ,WAAW,aAAA,EAAe;AACpC,QAAA,gBAAA,CAAiB,KAAK,OAAO,CAAA;AAAA,MAC/B;AAAA,IACF;AAGA,IAAA,IAAA,CAAK,gBAAgB,KAAA,EAAM;AAG3B,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,WAAW,CAAA;AAGxC,IAAA,IAAI,eAAA,CAAgB,WAAA,KAAgB,IAAA,CAAK,WAAA,EAAa;AACpD,MAAA,IAAA,CAAK,UAAA,CAAW,GAAA;AAAA,QACd,eAAA,CAAgB,WAAA;AAAA,QAChB,eAAA,CAAgB;AAAA,OAClB;AAAA,IACF;AAGA,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,aAAA,CAAc,cAAA,EAAgB;AAAA,QAC1C,QAAQ,eAAA,CAAgB,MAAA;AAAA,QACxB,QAAA,EAAU,IAAA;AAAA,QACV,KAAA,EAAO,aAAA;AAAA,QACP,MAAA;AAAA,QACA,IAAI,IAAA,CAAK;AAAA,OACV,CAAA;AAAA,IACH;AAGA,IAAA,KAAA,MAAW,WAAW,gBAAA,EAAkB;AACtC,MAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,aAAA,CAAc,cAAA,EAAgB;AAAA,QAC1C,QAAQ,OAAA,CAAQ,MAAA;AAAA,QAChB,QAAA,EAAU,KAAA;AAAA,QACV,KAAA,EAAO,aAAA;AAAA,QACP,MAAA,EAAQ,4DAAA;AAAA,QACR,IAAI,IAAA,CAAK;AAAA,OACV,CAAA;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,mCAAmC,YAAA,EAA4B;AACrE,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAgB,MAAA,EAAQ,EAAE,CAAC,CAAA;AAC9D,IAAA,IAAI,CAAC,UAAA,EAAY;AACf,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,iBAAA,GAAoB,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,YAAY,CAAA;AAE1D,IAAA,IAAI,CAAC,iBAAA,EAAmB;AACtB,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,UAAA,CAAW,WAAA,KAAgB,IAAA,CAAK,WAAA,EAAa;AAC/C,MAAA,MAAM,cAAc,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,eAAA,CAAgB,QAAQ,CAAA;AAC5D,MAAA,IAAA,CAAK,gBAAgB,KAAA,EAAM;AAE3B,MAAA,KAAA,MAAW,WAAW,WAAA,EAAa;AACjC,QAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,aAAA,CAAc,cAAA,EAAgB;AAAA,UAC1C,QAAQ,OAAA,CAAQ,MAAA;AAAA,UAChB,QAAA,EAAU,KAAA;AAAA,UACV,KAAA,EAAO,iBAAA;AAAA,UACP,MAAA,EAAQ,8CAAA;AAAA,UACR,IAAI,IAAA,CAAK;AAAA,SAC0B,CAAA;AAAA,MACvC;AAAA,IACF,CAAA,MAAO;AAEL,MAAA,IAAA,CAAK,6BAAA;AAAA,QACH,UAAA;AAAA,QACA,UAAA,CAAW,MAAA;AAAA,QACX,iBAAA;AAAA,QACA,mDAAA;AAAA,QACA;AAAA,OACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,QAAQ,OAAA,EAAuB;AACrC,IAAA,MAAM,eAAe,IAAA,CAAK,IAAA;AAC1B,IAAA,IAAA,CAAK,IAAA,GAAO,OAAA;AAEZ,IAAA,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,aAAA,CAAc,OAAA,EAAS;AAAA,MACnC,YAAA;AAAA,MACA,WAAA,EAAa,OAAA;AAAA,MACb,IAAI,IAAA,CAAK;AAAA,KACV,CAAA;AAED,IAAA,IAAA,CAAK,MAAA,EAAO;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,OAAA,GAAgB;AAEd,IAAA,KAAA,MAAW,WAAA,IAAe,KAAK,aAAA,EAAe;AAC5C,MAAA,WAAA,EAAY;AAAA,IACd;AACA,IAAA,IAAA,CAAK,cAAc,MAAA,GAAS,CAAA;AAG5B,IAAA,IAAA,CAAK,WAAW,KAAA,EAAM;AACtB,IAAA,IAAA,CAAK,gBAAgB,KAAA,EAAM;AAC3B,IAAA,IAAA,CAAK,UAAU,KAAA,EAAM;AAAA,EACvB;AACF;AAKA,MAAM,aAAA,uBAAoB,GAAA,EAA4B;AAK/C,SAAS,iBAAiB,EAAA,EAA4B;AAC3D,EAAA,IAAI,CAAC,aAAA,CAAc,GAAA,CAAI,EAAE,CAAA,EAAG;AAC1B,IAAA,aAAA,CAAc,GAAA,CAAI,EAAA,EAAI,IAAI,YAAA,CAAa,EAAE,CAAC,CAAA;AAAA,EAC5C;AAEA,EAAA,OAAO,aAAA,CAAc,IAAI,EAAE,CAAA;AAC7B;AAKO,SAAS,aAAa,EAAA,EAAoB;AAC/C,EAAA,MAAM,KAAA,GAAQ,aAAA,CAAc,GAAA,CAAI,EAAE,CAAA;AAClC,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,KAAA,CAAM,OAAA,EAAQ;AACd,IAAA,aAAA,CAAc,OAAO,EAAE,CAAA;AAAA,EACzB;AACF;AAKO,SAAS,SAAS,EAAA,EAAwC;AAC/D,EAAA,OAAO,aAAA,CAAc,IAAI,EAAE,CAAA;AAC7B","file":"store.js","sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { Broadcast } from '@accelint/bus';\nimport { uuid } from '@accelint/core';\nimport { MapModeEvents } from './events';\nimport type { UniqueId } from '@accelint/core';\nimport type { MapModeEventType, ModeChangeDecisionPayload } from './types';\n\nconst DEFAULT_MODE = 'default';\n\n/**\n * Typed event bus instance for map mode events.\n * Provides type-safe event emission and listening for all map mode state changes.\n */\nconst mapModeBus = Broadcast.getInstance<MapModeEventType>();\n\n/**\n * Internal type for tracking pending authorization requests.\n * @internal\n */\ntype PendingRequest = {\n authId: string;\n desiredMode: string;\n currentMode: string;\n requestOwner: string;\n};\n\n/**\n * External store for managing map mode state.\n *\n * This store implements the observable pattern for use with React's `useSyncExternalStore` hook.\n * It manages all mode state, ownership tracking, authorization flow, and event bus communication\n * outside of React's component tree.\n *\n * Each store instance is identified by a unique `id` and operates independently,\n * enabling scenarios with multiple isolated map instances (e.g., main map + minimap).\n * Stores communicate via the event bus and filter events by `id` to ensure isolation.\n *\n * The store always initializes in 'default' mode and does not accept a custom default mode.\n *\n * @see {getOrCreateStore} - Creates or retrieves a store for a given map instance\n * @see {destroyStore} - Destroys a store and cleans up its resources\n */\nexport class MapModeStore {\n private mode = DEFAULT_MODE;\n private readonly defaultMode = DEFAULT_MODE;\n private readonly modeOwners = new Map<string, string>();\n private readonly pendingRequests = new Map<string, PendingRequest>();\n private readonly listeners = new Set<() => void>();\n private readonly bus = mapModeBus;\n private readonly unsubscribers: Array<() => void> = [];\n\n constructor(private readonly id: UniqueId) {\n this.id = id;\n // Subscribe to bus events\n this.setupEventListeners();\n }\n\n /**\n * Get current mode snapshot (for useSyncExternalStore)\n */\n getSnapshot = (): string => {\n return this.mode;\n };\n\n /**\n * Subscribe to mode changes (for useSyncExternalStore)\n */\n subscribe = (listener: () => void): (() => void) => {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n /**\n * Request a mode change\n *\n * If the mode change can be auto-accepted (no ownership conflicts), the mode changes immediately.\n * Otherwise, an authorization request is emitted and stored as a pending request.\n *\n * **Important**: If the requester already has a pending authorization request, it will be replaced\n * with this new request. Only one pending request per requester is maintained at a time.\n *\n * @param desiredMode - The mode to switch to (automatically trimmed of whitespace)\n * @param requestOwner - Unique identifier of the component requesting the change (automatically trimmed of whitespace)\n * @throws Error if either parameter is empty or whitespace-only\n *\n * @example\n * ```ts\n * // First request from 'drawing-tool'\n * store.requestModeChange('drawing', 'drawing-tool');\n * // → Creates pending request with authId 'abc-123'\n *\n * // Second request from same 'drawing-tool' before first is resolved\n * store.requestModeChange('measuring', 'drawing-tool');\n * // → Replaces pending request, new authId 'def-456', old 'abc-123' is discarded\n * ```\n */\n requestModeChange = (desiredMode: string, requestOwner: string): void => {\n const trimmedDesiredMode = desiredMode.trim();\n const trimmedRequestOwner = requestOwner.trim();\n\n if (!trimmedDesiredMode) {\n throw new Error('requestModeChange requires non-empty desiredMode');\n }\n if (!trimmedRequestOwner) {\n throw new Error('requestModeChange requires non-empty requestOwner');\n }\n\n this.bus.emit(MapModeEvents.changeRequest, {\n desiredMode: trimmedDesiredMode,\n owner: trimmedRequestOwner,\n id: this.id,\n });\n };\n\n /**\n * Notify all subscribers of state change\n */\n private notify(): void {\n for (const listener of this.listeners) {\n listener();\n }\n }\n\n /**\n * Setup event listeners for bus events\n *\n * Note: Event listeners remain active even after early returns in handlers.\n * This is by design - cleanup happens in destroy() which is called automatically\n * by MapProvider on unmount. Consumers don't need to manually manage cleanup.\n */\n private setupEventListeners(): void {\n // Listen for mode change requests\n const unsubRequest = this.bus.on(MapModeEvents.changeRequest, (event) => {\n const { desiredMode, owner: requestOwner, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== this.id || desiredMode === this.mode) {\n return;\n }\n\n this.handleModeChangeRequest(desiredMode, requestOwner);\n });\n this.unsubscribers.push(unsubRequest);\n\n // Listen for authorization decisions\n const unsubDecision = this.bus.on(MapModeEvents.changeDecision, (event) => {\n const { id, approved, authId, owner } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== this.id) {\n return;\n }\n\n this.handleAuthorizationDecision({ approved, authId, owner });\n });\n this.unsubscribers.push(unsubDecision);\n\n // Listen for mode changes to handle pending requests\n const unsubChanged = this.bus.on(MapModeEvents.changed, (event) => {\n const { currentMode, previousMode, id } = event.payload;\n\n // Filter: only handle if targeted at this map\n if (id !== this.id) {\n return;\n }\n\n // When mode owner changes to default mode, handle pending requests\n if (currentMode === this.defaultMode && this.pendingRequests.size > 0) {\n this.handlePendingRequestsOnDefaultMode(previousMode);\n }\n });\n this.unsubscribers.push(unsubChanged);\n }\n\n /**\n * Determine if a mode change request should be auto-accepted without authorization\n */\n private shouldAutoAcceptRequest(\n desiredMode: string,\n requestOwner: string,\n currentModeOwner: string | undefined,\n desiredModeOwner: string | undefined,\n ): boolean {\n // Owner returning to default mode\n if (desiredMode === this.defaultMode && requestOwner === currentModeOwner) {\n return true;\n }\n\n // Owner switching between their own modes\n if (requestOwner === currentModeOwner) {\n return true;\n }\n\n // No ownership conflicts exist\n if (!(currentModeOwner || desiredModeOwner)) {\n return true;\n }\n\n // Entering an owned mode from default mode\n if (this.mode === this.defaultMode && requestOwner === desiredModeOwner) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Handle mode change request logic\n */\n private handleModeChangeRequest(\n desiredMode: string,\n requestOwner: string,\n ): void {\n const currentModeOwner = this.modeOwners.get(this.mode);\n const desiredModeOwner = this.modeOwners.get(desiredMode);\n\n // Check if this request should be auto-accepted\n if (\n this.shouldAutoAcceptRequest(\n desiredMode,\n requestOwner,\n currentModeOwner,\n desiredModeOwner,\n )\n ) {\n this.setMode(desiredMode);\n\n // Store the desired mode's owner unless it's default\n if (desiredMode !== this.defaultMode && !desiredModeOwner) {\n this.modeOwners.set(desiredMode, requestOwner);\n }\n\n // Clear requester's pending request since mode changed successfully\n this.pendingRequests.delete(requestOwner);\n return;\n }\n\n // Otherwise, send authorization request\n const authId = uuid();\n\n this.pendingRequests.set(requestOwner, {\n authId,\n desiredMode,\n currentMode: this.mode,\n requestOwner,\n });\n\n this.bus.emit(MapModeEvents.changeAuthorization, {\n authId,\n desiredMode,\n currentMode: this.mode,\n id: this.id,\n });\n }\n\n /**\n * Handle authorization decision\n *\n * Processes approval/rejection decisions from mode owners. Only the current mode's owner\n * can make authorization decisions. If a decision comes from a non-owner, a warning is\n * logged and the decision is ignored to prevent unauthorized mode changes.\n *\n * @param payload - The authorization decision containing authId, approved status, and owner\n */\n private handleAuthorizationDecision(payload: {\n approved: boolean;\n authId: string;\n owner: string;\n }): void {\n const { approved, authId, owner: decisionOwner } = payload;\n\n // Verify decision is from current mode's owner\n // Logs a warning if unauthorized component attempts to make decisions\n const currentModeOwner = this.modeOwners.get(this.mode);\n if (decisionOwner !== currentModeOwner) {\n console.warn(\n `[MapMode] Authorization decision from \"${decisionOwner}\" ignored - not the owner of mode \"${this.mode}\" (owner: ${currentModeOwner || 'none'})`,\n );\n return;\n }\n\n // Find the request with matching authId\n let matchingRequestOwner: string | null = null;\n let matchingRequest: PendingRequest | null = null;\n\n for (const [requestOwner, request] of this.pendingRequests.entries()) {\n if (request.authId === authId) {\n matchingRequestOwner = requestOwner;\n matchingRequest = request;\n break;\n }\n }\n\n if (!(matchingRequest && matchingRequestOwner)) {\n return;\n }\n\n if (approved) {\n this.approveRequestAndRejectOthers(\n matchingRequest,\n authId,\n decisionOwner,\n '',\n false,\n );\n } else {\n this.pendingRequests.delete(matchingRequestOwner);\n }\n }\n\n /**\n * Approve a request and reject all others\n */\n private approveRequestAndRejectOthers(\n approvedRequest: PendingRequest,\n excludeAuthId: string,\n decisionOwner: string,\n reason: string,\n emitApproval: boolean,\n ): void {\n // Collect all other pending requests to emit rejections for\n const requestsToReject: PendingRequest[] = [];\n for (const request of this.pendingRequests.values()) {\n if (request.authId !== excludeAuthId) {\n requestsToReject.push(request);\n }\n }\n\n // Clear all pending requests BEFORE changing mode\n this.pendingRequests.clear();\n\n // Change mode\n this.setMode(approvedRequest.desiredMode);\n\n // Store the new mode's owner (unless it's default mode)\n if (approvedRequest.desiredMode !== this.defaultMode) {\n this.modeOwners.set(\n approvedRequest.desiredMode,\n approvedRequest.requestOwner,\n );\n }\n\n // Emit approval decision if requested\n if (emitApproval) {\n this.bus.emit(MapModeEvents.changeDecision, {\n authId: approvedRequest.authId,\n approved: true,\n owner: decisionOwner,\n reason,\n id: this.id,\n });\n }\n\n // Emit rejection events for all other pending requests\n for (const request of requestsToReject) {\n this.bus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: decisionOwner,\n reason: 'Request auto-rejected because another request was approved',\n id: this.id,\n });\n }\n }\n\n /**\n * Handle pending requests when returning to default mode\n */\n private handlePendingRequestsOnDefaultMode(previousMode: string): void {\n const firstEntry = Array.from(this.pendingRequests.values())[0];\n if (!firstEntry) {\n return;\n }\n\n const previousModeOwner = this.modeOwners.get(previousMode);\n\n if (!previousModeOwner) {\n return;\n }\n\n // If the first pending request is for default mode, reject all requests\n if (firstEntry.desiredMode === this.defaultMode) {\n const allRequests = Array.from(this.pendingRequests.values());\n this.pendingRequests.clear();\n\n for (const request of allRequests) {\n this.bus.emit(MapModeEvents.changeDecision, {\n authId: request.authId,\n approved: false,\n owner: previousModeOwner,\n reason: 'Request rejected - already in requested mode',\n id: this.id,\n } satisfies ModeChangeDecisionPayload);\n }\n } else {\n // Auto-accept the first pending request for a different mode\n this.approveRequestAndRejectOthers(\n firstEntry,\n firstEntry.authId,\n previousModeOwner,\n 'Auto-accepted when mode owner returned to default',\n true,\n );\n }\n }\n\n /**\n * Set mode and notify listeners\n */\n private setMode(newMode: string): void {\n const previousMode = this.mode;\n this.mode = newMode;\n\n this.bus.emit(MapModeEvents.changed, {\n previousMode,\n currentMode: newMode,\n id: this.id,\n });\n\n this.notify();\n }\n\n /**\n * Clean up store resources\n */\n destroy(): void {\n // Unsubscribe from all bus events\n for (const unsubscribe of this.unsubscribers) {\n unsubscribe();\n }\n this.unsubscribers.length = 0;\n\n // Clear all state\n this.modeOwners.clear();\n this.pendingRequests.clear();\n this.listeners.clear();\n }\n}\n\n/**\n * Global store registry\n */\nconst storeRegistry = new Map<UniqueId, MapModeStore>();\n\n/**\n * Get or create a store for a given map instance\n */\nexport function getOrCreateStore(id: UniqueId): MapModeStore {\n if (!storeRegistry.has(id)) {\n storeRegistry.set(id, new MapModeStore(id));\n }\n // biome-ignore lint/style/noNonNullAssertion: Store guaranteed to exist after has() check above\n return storeRegistry.get(id)!;\n}\n\n/**\n * Destroy and remove a store from the registry\n */\nexport function destroyStore(id: UniqueId): void {\n const store = storeRegistry.get(id);\n if (store) {\n store.destroy();\n storeRegistry.delete(id);\n }\n}\n\n/**\n * Get a store by map ID (for testing/advanced use)\n */\nexport function getStore(id: UniqueId): MapModeStore | undefined {\n return storeRegistry.get(id);\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Payload } from '@accelint/bus';
|
|
2
|
+
import { UniqueId } from '@accelint/core';
|
|
3
|
+
import { MapModeEvents } from './events.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Payload emitted when the map mode has successfully changed.
|
|
7
|
+
*/
|
|
8
|
+
type ModeChangedPayload = {
|
|
9
|
+
/** The mode before the change */
|
|
10
|
+
previousMode: string;
|
|
11
|
+
/** The mode after the change */
|
|
12
|
+
currentMode: string;
|
|
13
|
+
/** The ID of the map this event is for */
|
|
14
|
+
id: UniqueId;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Payload for requesting a map mode change.
|
|
18
|
+
* This initiates the mode change flow.
|
|
19
|
+
*/
|
|
20
|
+
type ModeChangeRequestPayload = {
|
|
21
|
+
/** The mode being requested */
|
|
22
|
+
desiredMode: string;
|
|
23
|
+
/** The identifier of the component requesting the mode change */
|
|
24
|
+
owner: string;
|
|
25
|
+
/** The ID of the map this event is for */
|
|
26
|
+
id: UniqueId;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Payload emitted when a mode change requires authorization.
|
|
30
|
+
* This is sent to the current mode owner to approve or reject the request.
|
|
31
|
+
*/
|
|
32
|
+
type ModeChangeAuthorizationPayload = {
|
|
33
|
+
/** Unique identifier for this authorization request */
|
|
34
|
+
authId: string;
|
|
35
|
+
/** The mode being requested */
|
|
36
|
+
desiredMode: string;
|
|
37
|
+
/** The current active mode */
|
|
38
|
+
currentMode: string;
|
|
39
|
+
/** The ID of the map this event is for */
|
|
40
|
+
id: UniqueId;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Payload for an authorization decision from the current mode owner.
|
|
44
|
+
* This completes the authorization flow.
|
|
45
|
+
*/
|
|
46
|
+
type ModeChangeDecisionPayload = {
|
|
47
|
+
/** The authId from the corresponding authorization request */
|
|
48
|
+
authId: string;
|
|
49
|
+
/** Whether the mode change was approved */
|
|
50
|
+
approved: boolean;
|
|
51
|
+
/** The identifier of the component making the decision (must be current mode owner) */
|
|
52
|
+
owner: string;
|
|
53
|
+
/** Optional reason for rejection */
|
|
54
|
+
reason?: string;
|
|
55
|
+
/** The ID of the map this event is for */
|
|
56
|
+
id: UniqueId;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Event type for mode change notifications.
|
|
60
|
+
* Emitted when the map mode has successfully changed.
|
|
61
|
+
*/
|
|
62
|
+
type ModeChangedEvent = Payload<typeof MapModeEvents.changed, ModeChangedPayload>;
|
|
63
|
+
/**
|
|
64
|
+
* Event type for mode change requests.
|
|
65
|
+
* Emitted when a component requests a mode change.
|
|
66
|
+
*/
|
|
67
|
+
type ModeChangeRequestEvent = Payload<typeof MapModeEvents.changeRequest, ModeChangeRequestPayload>;
|
|
68
|
+
/**
|
|
69
|
+
* Event type for mode change authorization requests.
|
|
70
|
+
* Emitted when a mode change requires approval from the current mode owner.
|
|
71
|
+
*/
|
|
72
|
+
type ModeChangeAuthorizationEvent = Payload<typeof MapModeEvents.changeAuthorization, ModeChangeAuthorizationPayload>;
|
|
73
|
+
/**
|
|
74
|
+
* Event type for authorization decisions.
|
|
75
|
+
* Emitted when the current mode owner approves or rejects a mode change request.
|
|
76
|
+
*/
|
|
77
|
+
type ModeChangeDecisionEvent = Payload<typeof MapModeEvents.changeDecision, ModeChangeDecisionPayload>;
|
|
78
|
+
/**
|
|
79
|
+
* Union type of all map mode event types that can be emitted through the event bus.
|
|
80
|
+
*/
|
|
81
|
+
type MapModeEventType = ModeChangedEvent | ModeChangeRequestEvent | ModeChangeAuthorizationEvent | ModeChangeDecisionEvent;
|
|
82
|
+
|
|
83
|
+
export type { MapModeEventType, ModeChangeAuthorizationEvent, ModeChangeAuthorizationPayload, ModeChangeDecisionEvent, ModeChangeDecisionPayload, ModeChangeRequestEvent, ModeChangeRequestPayload, ModeChangedEvent, ModeChangedPayload };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"types.js"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { UniqueId } from '@accelint/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return value for the useMapMode hook
|
|
5
|
+
*/
|
|
6
|
+
type UseMapModeReturn = {
|
|
7
|
+
/** The current active map mode */
|
|
8
|
+
mode: string;
|
|
9
|
+
/** Function to request a mode change with ownership */
|
|
10
|
+
requestModeChange: (desiredMode: string, requestOwner: string) => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Hook to access the map mode state and actions.
|
|
14
|
+
*
|
|
15
|
+
* This hook uses `useSyncExternalStore` to subscribe to the external `MapModeStore`,
|
|
16
|
+
* providing concurrent-safe mode state updates. The hybrid architecture separates:
|
|
17
|
+
* - Map instance identity (from `MapContext` or parameter)
|
|
18
|
+
* - Mode state management (from `MapModeStore` via `useSyncExternalStore`)
|
|
19
|
+
*
|
|
20
|
+
* @param id - Optional map instance ID. If not provided, will use the ID from `MapContext`.
|
|
21
|
+
* @returns The current map mode and requestModeChange function
|
|
22
|
+
* @throws Error if no `id` is provided and hook is used outside of `MapProvider`
|
|
23
|
+
* @throws Error if store doesn't exist for the given map ID
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* // Inside MapProvider (within BaseMap children) - uses context
|
|
28
|
+
* // Only Deck.gl layer components can be children
|
|
29
|
+
* function CustomDeckLayer() {
|
|
30
|
+
* const { mode, requestModeChange } = useMapMode();
|
|
31
|
+
*
|
|
32
|
+
* const handleClick = (info: PickingInfo) => {
|
|
33
|
+
* requestModeChange('editing', 'custom-layer-id');
|
|
34
|
+
* };
|
|
35
|
+
*
|
|
36
|
+
* return <ScatterplotLayer onClick={handleClick} />;
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* // Outside MapProvider - pass id directly
|
|
43
|
+
* function ExternalControl({ mapId }: { mapId: UniqueId }) {
|
|
44
|
+
* const { mode, requestModeChange } = useMapMode(mapId);
|
|
45
|
+
*
|
|
46
|
+
* return <button onClick={() => requestModeChange('default', 'external')}>
|
|
47
|
+
* Reset to Default (current: {mode})
|
|
48
|
+
* </button>;
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function useMapMode(id?: UniqueId): UseMapModeReturn;
|
|
53
|
+
|
|
54
|
+
export { type UseMapModeReturn, useMapMode };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useContext, useSyncExternalStore, useMemo } from 'react';
|
|
2
|
+
import { MapContext } from '../deckgl/base-map/provider.js';
|
|
3
|
+
import { getStore } from './store.js';
|
|
4
|
+
|
|
5
|
+
function useMapMode(id) {
|
|
6
|
+
const contextId = useContext(MapContext);
|
|
7
|
+
const actualId = id ?? contextId;
|
|
8
|
+
if (!actualId) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"useMapMode requires either an id parameter or to be used within a MapProvider"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const store = getStore(actualId);
|
|
14
|
+
if (!store) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`MapModeStore not found for map instance: ${actualId}. Ensure a store has been created for this map instance (e.g., via MapProvider or getOrCreateStore).`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const mode = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
20
|
+
return useMemo(
|
|
21
|
+
() => ({
|
|
22
|
+
mode,
|
|
23
|
+
requestModeChange: store.requestModeChange
|
|
24
|
+
}),
|
|
25
|
+
[mode, store]
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { useMapMode };
|
|
30
|
+
//# sourceMappingURL=use-map-mode.js.map
|
|
31
|
+
//# sourceMappingURL=use-map-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/map-mode/use-map-mode.ts"],"names":[],"mappings":";;;;AAmEO,SAAS,WAAW,EAAA,EAAiC;AAC1D,EAAA,MAAM,SAAA,GAAY,WAAW,UAAU,CAAA;AACvC,EAAA,MAAM,WAAW,EAAA,IAAM,SAAA;AAEvB,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAGA,EAAA,MAAM,KAAA,GAAQ,SAAS,QAAQ,CAAA;AAE/B,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,4CAA4C,QAAQ,CAAA,oGAAA;AAAA,KACtD;AAAA,EACF;AAGA,EAAA,MAAM,IAAA,GAAO,oBAAA,CAAqB,KAAA,CAAM,SAAA,EAAW,MAAM,WAAW,CAAA;AAGpE,EAAA,OAAO,OAAA;AAAA,IACL,OAAO;AAAA,MACL,IAAA;AAAA,MACA,mBAAmB,KAAA,CAAM;AAAA,KAC3B,CAAA;AAAA,IACA,CAAC,MAAM,KAAK;AAAA,GACd;AACF","file":"use-map-mode.js","sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { useContext, useMemo, useSyncExternalStore } from 'react';\nimport { MapContext } from '../deckgl/base-map/provider';\nimport { getStore } from './store';\nimport type { UniqueId } from '@accelint/core';\n\n/**\n * Return value for the useMapMode hook\n */\nexport type UseMapModeReturn = {\n /** The current active map mode */\n mode: string;\n /** Function to request a mode change with ownership */\n requestModeChange: (desiredMode: string, requestOwner: string) => void;\n};\n\n/**\n * Hook to access the map mode state and actions.\n *\n * This hook uses `useSyncExternalStore` to subscribe to the external `MapModeStore`,\n * providing concurrent-safe mode state updates. The hybrid architecture separates:\n * - Map instance identity (from `MapContext` or parameter)\n * - Mode state management (from `MapModeStore` via `useSyncExternalStore`)\n *\n * @param id - Optional map instance ID. If not provided, will use the ID from `MapContext`.\n * @returns The current map mode and requestModeChange function\n * @throws Error if no `id` is provided and hook is used outside of `MapProvider`\n * @throws Error if store doesn't exist for the given map ID\n *\n * @example\n * ```tsx\n * // Inside MapProvider (within BaseMap children) - uses context\n * // Only Deck.gl layer components can be children\n * function CustomDeckLayer() {\n * const { mode, requestModeChange } = useMapMode();\n *\n * const handleClick = (info: PickingInfo) => {\n * requestModeChange('editing', 'custom-layer-id');\n * };\n *\n * return <ScatterplotLayer onClick={handleClick} />;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Outside MapProvider - pass id directly\n * function ExternalControl({ mapId }: { mapId: UniqueId }) {\n * const { mode, requestModeChange } = useMapMode(mapId);\n *\n * return <button onClick={() => requestModeChange('default', 'external')}>\n * Reset to Default (current: {mode})\n * </button>;\n * }\n * ```\n */\nexport function useMapMode(id?: UniqueId): UseMapModeReturn {\n const contextId = useContext(MapContext);\n const actualId = id ?? contextId;\n\n if (!actualId) {\n throw new Error(\n 'useMapMode requires either an id parameter or to be used within a MapProvider',\n );\n }\n\n // Get the store for this map instance\n const store = getStore(actualId);\n\n if (!store) {\n throw new Error(\n `MapModeStore not found for map instance: ${actualId}. Ensure a store has been created for this map instance (e.g., via MapProvider or getOrCreateStore).`,\n );\n }\n\n // Subscribe to store using useSyncExternalStore\n const mode = useSyncExternalStore(store.subscribe, store.getSnapshot);\n\n // Memoize the return value to prevent unnecessary re-renders\n return useMemo(\n () => ({\n mode,\n requestModeChange: store.requestModeChange,\n }),\n [mode, store],\n );\n}\n"]}
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { IControl, MapOptions, Map } from 'maplibre-gl';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Hook to integrate a MapLibre GL map with a Deck.gl instance.
|
|
5
|
+
*
|
|
6
|
+
* This hook manages the lifecycle of a MapLibre map, including initialization,
|
|
7
|
+
* style updates, and cleanup. It ensures the Deck.gl control is properly added
|
|
8
|
+
* to the map and handles cleanup when the component unmounts.
|
|
9
|
+
*
|
|
10
|
+
* @param deck - The Deck.gl IControl instance to add to the map
|
|
11
|
+
* @param styleUrl - The MapLibre style URL to use for the map
|
|
12
|
+
* @param options - MapLibre map options (container, center, zoom, etc.)
|
|
13
|
+
* @returns The MapLibre map instance, or null if not yet initialized
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* function MapComponent() {
|
|
18
|
+
* const deckglInstance = useDeckgl();
|
|
19
|
+
* const container = useId();
|
|
20
|
+
*
|
|
21
|
+
* const mapOptions = useMemo(() => ({
|
|
22
|
+
* container,
|
|
23
|
+
* center: [-122.4, 37.8],
|
|
24
|
+
* zoom: 12,
|
|
25
|
+
* }), [container]);
|
|
26
|
+
*
|
|
27
|
+
* useMapLibre(
|
|
28
|
+
* deckglInstance as IControl,
|
|
29
|
+
* 'https://tiles.example.com/style.json',
|
|
30
|
+
* mapOptions
|
|
31
|
+
* );
|
|
32
|
+
*
|
|
33
|
+
* return <div id={container} />;
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
3
37
|
declare function useMapLibre(deck: IControl | null, styleUrl: string, options: MapOptions): Map | null;
|
|
4
38
|
|
|
5
39
|
export { useMapLibre };
|
|
@@ -3,11 +3,12 @@ import { useRef, useEffect } from 'react';
|
|
|
3
3
|
|
|
4
4
|
function useMapLibre(deck, styleUrl, options) {
|
|
5
5
|
const mapRef = useRef(null);
|
|
6
|
+
const optionsRef = useRef(options);
|
|
6
7
|
const styleRef = useRef(styleUrl);
|
|
7
8
|
useEffect(() => {
|
|
8
9
|
if (deck && !mapRef.current) {
|
|
9
10
|
mapRef.current = new Map({
|
|
10
|
-
...
|
|
11
|
+
...optionsRef.current,
|
|
11
12
|
style: styleRef.current
|
|
12
13
|
});
|
|
13
14
|
mapRef.current.once("style.load", () => {
|
|
@@ -22,7 +23,7 @@ function useMapLibre(deck, styleUrl, options) {
|
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
|
-
}, [deck
|
|
26
|
+
}, [deck]);
|
|
26
27
|
useEffect(() => {
|
|
27
28
|
if (mapRef.current) {
|
|
28
29
|
mapRef.current.setStyle(styleUrl);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/maplibre/hooks/use-maplibre.ts"],"names":["MapLibre"],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../../../src/maplibre/hooks/use-maplibre.ts"],"names":["MapLibre"],"mappings":";;;AAiDO,SAAS,WAAA,CACd,IAAA,EACA,QAAA,EACA,OAAA,EACA;AACA,EAAA,MAAM,MAAA,GAAS,OAAwB,IAAI,CAAA;AAG3C,EAAA,MAAM,UAAA,GAAa,OAAO,OAAO,CAAA;AAEjC,EAAA,MAAM,QAAA,GAAW,OAAO,QAAQ,CAAA;AAGhC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,IAAA,IAAQ,CAAC,MAAA,CAAO,OAAA,EAAS;AAC3B,MAAA,MAAA,CAAO,OAAA,GAAU,IAAIA,GAAA,CAAS;AAAA,QAC5B,GAAG,UAAA,CAAW,OAAA;AAAA,QACd,OAAO,QAAA,CAAS;AAAA,OACjB,CAAA;AAED,MAAA,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,YAAA,EAAc,MAAM;AACtC,QAAA,MAAA,CAAO,OAAA,EAAS,aAAA,CAAc,EAAE,IAAA,EAAM,YAAY,CAAA;AAClD,QAAA,MAAA,CAAO,OAAA,EAAS,WAAW,IAAI,CAAA;AAAA,MACjC,CAAC,CAAA;AAED,MAAA,OAAO,MAAM;AACX,QAAA,IAAI,OAAO,OAAA,EAAS;AAClB,UAAA,MAAA,CAAO,OAAA,CAAQ,cAAc,IAAI,CAAA;AACjC,UAAA,MAAA,CAAO,QAAQ,MAAA,EAAO;AACtB,UAAA,MAAA,CAAO,OAAA,GAAU,IAAA;AAAA,QACnB;AAAA,MACF,CAAA;AAAA,IACF;AAAA,EACF,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAGT,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,OAAA,EAAS;AAClB,MAAA,MAAA,CAAO,OAAA,CAAQ,SAAS,QAAQ,CAAA;AAAA,IAClC;AAAA,EACF,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,OAAO,MAAA,CAAO,OAAA;AAChB","file":"use-maplibre.js","sourcesContent":["/*\n * Copyright 2025 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { type IControl, Map as MapLibre, type MapOptions } from 'maplibre-gl';\nimport { useEffect, useRef } from 'react';\n\n/**\n * Hook to integrate a MapLibre GL map with a Deck.gl instance.\n *\n * This hook manages the lifecycle of a MapLibre map, including initialization,\n * style updates, and cleanup. It ensures the Deck.gl control is properly added\n * to the map and handles cleanup when the component unmounts.\n *\n * @param deck - The Deck.gl IControl instance to add to the map\n * @param styleUrl - The MapLibre style URL to use for the map\n * @param options - MapLibre map options (container, center, zoom, etc.)\n * @returns The MapLibre map instance, or null if not yet initialized\n *\n * @example\n * ```tsx\n * function MapComponent() {\n * const deckglInstance = useDeckgl();\n * const container = useId();\n *\n * const mapOptions = useMemo(() => ({\n * container,\n * center: [-122.4, 37.8],\n * zoom: 12,\n * }), [container]);\n *\n * useMapLibre(\n * deckglInstance as IControl,\n * 'https://tiles.example.com/style.json',\n * mapOptions\n * );\n *\n * return <div id={container} />;\n * }\n * ```\n */\nexport function useMapLibre(\n deck: IControl | null,\n styleUrl: string,\n options: MapOptions,\n) {\n const mapRef = useRef<MapLibre | null>(null);\n // Using a ref for options to avoid re-creating the map when options object reference changes\n // The map is only created once on mount, options changes after that are ignored\n const optionsRef = useRef(options);\n // using a ref in the initial setup so that it doesn't cause a re-run of the effect on change\n const styleRef = useRef(styleUrl);\n\n // Initialize MapLibre instance once\n useEffect(() => {\n if (deck && !mapRef.current) {\n mapRef.current = new MapLibre({\n ...optionsRef.current,\n style: styleRef.current,\n });\n\n mapRef.current.once('style.load', () => {\n mapRef.current?.setProjection({ type: 'mercator' });\n mapRef.current?.addControl(deck);\n });\n\n return () => {\n if (mapRef.current) {\n mapRef.current.removeControl(deck);\n mapRef.current.remove();\n mapRef.current = null;\n }\n };\n }\n }, [deck]);\n\n // Update style when it changes\n useEffect(() => {\n if (mapRef.current) {\n mapRef.current.setStyle(styleUrl);\n }\n }, [styleUrl]);\n\n return mapRef.current;\n}\n"]}
|