@powerportalspro/react-maps 0.0.0-stage → 13.0.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/dist/index.js ADDED
@@ -0,0 +1,3603 @@
1
+ import { ensureMapStyles, ensureStyleSheet, AZURE_MAPS_ENDPOINT, azureMapsHeaders } from './chunk-7HDK44TQ.js';
2
+ export { setMapLibreWorkerUrl } from './chunk-7HDK44TQ.js';
3
+ import { boundsOfItems, isValidPosition, approximateBounds, padBounds, boundsContain, toBoundingBox, nominatimGeocoder, esriGeocoder, azureMapsGeocoder } from './chunk-G723VLPV.js';
4
+ import { makeStyles, tokens, shorthands, Button, Spinner, MessageBar, MessageBarBody, Text, mergeClasses, Card, Link, Subtitle2, Popover, PopoverTrigger, Tooltip, PopoverSurface, Label, Field, Dropdown, Option, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions } from '@fluentui/react-components';
5
+ import { usePrefixedT, useLocale, usePowerPortalsPro, useRecordPages, useViewMetadata, useTableMetadata, useT } from '@powerportalspro/react';
6
+ import { SharedTooltip, MapPrintButton, useSavedViews, useMultiTableViews, EVERY_TABLE_VIEW_ID, useMultiTableTableSet, useSavedViewSession, ViewToolbar, ViewFilterMenu, ViewRefreshButton, ViewPicker, MultiTableFilterDialog, SavedViewDialogs, AddressMapServiceContext, GridMapRendererContext, GridNearSearchContext } from '@powerportalspro/react-fluent';
7
+ import { forwardRef, useRef, useState, useMemo, useEffect, useImperativeHandle, useLayoutEffect } from 'react';
8
+ import { Dismiss16Regular, Location24Filled, Location24Regular, MyLocation20Regular } from '@fluentui/react-icons';
9
+ import { ColumnType, resolveColumnDisplayValue, getRecordReferenceId, readColumnRawValue, findRecordPage, recordPageUrl, savedViewControl, SavedViewControlKind, MultiTableViewSchema, MultiTableUses, defaultViewId, isViewHidden, shownViewTables } from '@powerportalspro/core';
10
+ import Supercluster from 'supercluster';
11
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
+
13
+ // src/core/registry.ts
14
+ var REGISTRY = /* @__PURE__ */ Symbol.for("powerportalspro.maps.providers");
15
+ function registry() {
16
+ const global = globalThis;
17
+ return global[REGISTRY] ??= /* @__PURE__ */ new Map();
18
+ }
19
+ function registerMapProvider(provider) {
20
+ registry().set(provider.name, provider);
21
+ }
22
+ var BUILT_IN = {
23
+ azureMaps: () => import(
24
+ /* webpackChunkName: "maplibre" */
25
+ './maplibre-J3XSEXEJ.js'
26
+ ).then((module) => module.mapLibreProvider("azureMaps")),
27
+ esri: () => import(
28
+ /* webpackChunkName: "maplibre" */
29
+ './maplibre-J3XSEXEJ.js'
30
+ ).then((module) => module.mapLibreProvider("esri")),
31
+ openStreetMap: () => import(
32
+ /* webpackChunkName: "maplibre" */
33
+ './maplibre-J3XSEXEJ.js'
34
+ ).then((module) => module.mapLibreProvider("openStreetMap")),
35
+ mapLibre: () => import(
36
+ /* webpackChunkName: "maplibre" */
37
+ './maplibre-J3XSEXEJ.js'
38
+ ).then((module) => module.mapLibreProvider("mapLibre")),
39
+ google: () => import(
40
+ /* webpackChunkName: "google" */
41
+ './google-6EZOUTYI.js'
42
+ ).then((module) => module.googleProvider)
43
+ };
44
+ async function findMapProvider(name) {
45
+ const registered = registry().get(name);
46
+ if (registered) return registered;
47
+ const load = BUILT_IN[name];
48
+ return load ? await load() : void 0;
49
+ }
50
+ var BUILT_IN_MAP_PROVIDERS = Object.freeze(Object.keys(BUILT_IN));
51
+
52
+ // src/core/portal-settings.ts
53
+ var TOKEN_RENEWAL_LEAD = 5 * 6e4;
54
+ var PortalMapSettingsCache = class {
55
+ entries = /* @__PURE__ */ new Map();
56
+ /** The settings for `provider`, or the portal's own provider's, from `load` when none are held. */
57
+ get(provider, load) {
58
+ const key = provider ?? "";
59
+ const held = this.entries.get(key);
60
+ if (held && Date.now() < held.renewAfter) return held.settings;
61
+ const settings = load(provider);
62
+ const entry = { settings, renewAfter: Number.POSITIVE_INFINITY };
63
+ this.entries.set(key, entry);
64
+ settings.then(
65
+ (answer) => {
66
+ if (answer.token) entry.renewAfter = Date.parse(answer.token.expiresOn) - TOKEN_RENEWAL_LEAD;
67
+ if (provider === void 0 && !this.entries.has(answer.provider)) this.entries.set(answer.provider, entry);
68
+ },
69
+ () => {
70
+ for (const [name, held2] of this.entries) if (held2 === entry) this.entries.delete(name);
71
+ }
72
+ );
73
+ return settings;
74
+ }
75
+ };
76
+ async function resolveProviderSettings(page, load) {
77
+ const asked = page?.name;
78
+ const name = asked === void 0 ? void 0 : BUILT_IN_MAP_PROVIDERS.find((p) => p.toLowerCase() === asked.toLowerCase()) ?? asked;
79
+ if (!load || name !== void 0 && !BUILT_IN_MAP_PROVIDERS.includes(name)) return page && name ? { ...page, name } : { name: "openStreetMap" };
80
+ const { provider, token, apiKey, ...portal } = await load(name);
81
+ const settings = { ...defined(portal), ...defined(page ?? {}), name: name ?? provider };
82
+ if (apiKey) settings.apiKey = apiKey;
83
+ if (token) {
84
+ const providerName = settings.name;
85
+ settings.getToken = async () => {
86
+ const answer = await load(providerName);
87
+ if (!answer.token) throw new Error("The portal didn't issue an Azure Maps token.");
88
+ return answer.token;
89
+ };
90
+ }
91
+ return settings;
92
+ }
93
+ function defined(values) {
94
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== null && value !== void 0));
95
+ }
96
+
97
+ // src/core/address-search.ts
98
+ var MIN_SUGGEST_LENGTH = 3;
99
+ var SUGGEST_DELAY = 250;
100
+ var SEARCH_ICON = '<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path fill="currentColor" d="M8.5 3a5.5 5.5 0 0 1 4.23 9.02l4.12 4.13a.5.5 0 0 1-.63.76l-.07-.06-4.13-4.12A5.5 5.5 0 1 1 8.5 3Zm0 1a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z"/></svg>';
101
+ var DISMISS_ICON = '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="m2.59 2.72.06-.07a.5.5 0 0 1 .63-.06l.07.06L8 7.29l4.65-4.64a.5.5 0 0 1 .7.7L8.71 8l4.64 4.65c.18.17.2.44.06.63l-.06.07a.5.5 0 0 1-.63.06l-.07-.06L8 8.71l-4.65 4.64a.5.5 0 0 1-.7-.7L7.29 8 2.65 3.35a.5.5 0 0 1-.06-.63l.06-.07-.06.07Z"/></svg>';
102
+ var nextId = 0;
103
+ var AddressSearchBox = class {
104
+ constructor(document, host, labels) {
105
+ this.host = host;
106
+ this.labels = labels;
107
+ this.element = document.createElement("div");
108
+ this.element.className = "ppp-map-search";
109
+ const field = document.createElement("div");
110
+ field.className = "ppp-map-search-field";
111
+ const icon = document.createElement("span");
112
+ icon.className = "ppp-map-search-icon";
113
+ icon.innerHTML = SEARCH_ICON;
114
+ this.input = document.createElement("input");
115
+ this.input.type = "text";
116
+ this.input.className = "ppp-map-search-input";
117
+ this.input.autocomplete = "off";
118
+ this.input.spellcheck = false;
119
+ this.input.setAttribute("role", "combobox");
120
+ this.input.setAttribute("aria-autocomplete", "list");
121
+ this.input.setAttribute("aria-expanded", "false");
122
+ this.input.setAttribute("aria-controls", this.listId);
123
+ this.clearButton = document.createElement("button");
124
+ this.clearButton.type = "button";
125
+ this.clearButton.className = "ppp-map-search-clear";
126
+ this.clearButton.innerHTML = DISMISS_ICON;
127
+ this.clearButton.hidden = true;
128
+ field.append(icon, this.input, this.clearButton);
129
+ this.panel = document.createElement("div");
130
+ this.panel.className = "ppp-map-search-panel";
131
+ this.panel.hidden = true;
132
+ this.list = document.createElement("ul");
133
+ this.list.className = "ppp-map-search-list";
134
+ this.list.id = this.listId;
135
+ this.list.setAttribute("role", "listbox");
136
+ this.status = document.createElement("div");
137
+ this.status.className = "ppp-map-search-status";
138
+ this.status.setAttribute("role", "status");
139
+ this.panel.append(this.list, this.status);
140
+ this.element.append(field, this.panel);
141
+ this.setLabels(labels);
142
+ this.input.addEventListener("input", () => this.typed());
143
+ this.input.addEventListener("keydown", (event) => this.keyDown(event));
144
+ this.input.addEventListener("focus", () => {
145
+ if (this.suggestions.length && this.suggestedFor === this.text) this.open();
146
+ });
147
+ this.element.addEventListener("focusout", (event) => {
148
+ if (!this.element.contains(event.relatedTarget)) this.close();
149
+ });
150
+ this.clearButton.addEventListener("click", () => {
151
+ this.clear();
152
+ this.input.focus();
153
+ });
154
+ this.list.addEventListener("mousedown", (event) => event.preventDefault());
155
+ this.list.addEventListener("click", (event) => {
156
+ const option = event.target.closest('[role="option"]');
157
+ const index = option ? Number(option.getAttribute("data-index")) : -1;
158
+ if (index >= 0) void this.choose(index);
159
+ });
160
+ }
161
+ host;
162
+ labels;
163
+ element;
164
+ input;
165
+ clearButton;
166
+ panel;
167
+ list;
168
+ status;
169
+ listId = `ppp-map-search-${++nextId}`;
170
+ suggestions = [];
171
+ // The text the suggestions are for: Enter on it goes to the first, rather than searching again.
172
+ suggestedFor = null;
173
+ active = -1;
174
+ request = null;
175
+ timer;
176
+ disposed = false;
177
+ /** The text in the box. */
178
+ get text() {
179
+ return this.input.value.trim();
180
+ }
181
+ setLabels(labels) {
182
+ this.labels = labels;
183
+ this.input.placeholder = labels.placeholder;
184
+ this.input.setAttribute("aria-label", labels.placeholder);
185
+ this.list.setAttribute("aria-label", labels.placeholder);
186
+ this.clearButton.setAttribute("aria-label", labels.clear);
187
+ this.clearButton.dataset.pppTooltip = labels.clear;
188
+ }
189
+ /**
190
+ * Shows a place the map went to from code, as though it had been chosen here: its label in the box,
191
+ * which Enter goes back to and the clear button takes off the map.
192
+ */
193
+ showChosen(label) {
194
+ this.cancel();
195
+ this.input.value = label;
196
+ this.clearButton.hidden = false;
197
+ this.suggestions = [];
198
+ this.suggestedFor = null;
199
+ this.close();
200
+ }
201
+ /** Forgets the places listed, which a geocoder that replaced the one that found them can't go to. */
202
+ forgetSuggestions() {
203
+ this.cancel();
204
+ this.suggestions = [];
205
+ this.suggestedFor = null;
206
+ this.close();
207
+ }
208
+ /** Empties the box and takes the place found off the map. */
209
+ clear() {
210
+ this.cancel();
211
+ this.input.value = "";
212
+ this.clearButton.hidden = true;
213
+ this.suggestions = [];
214
+ this.suggestedFor = null;
215
+ this.close();
216
+ this.host.clear();
217
+ }
218
+ dispose() {
219
+ this.disposed = true;
220
+ this.cancel();
221
+ this.element.remove();
222
+ }
223
+ typed() {
224
+ this.clearButton.hidden = !this.input.value;
225
+ this.cancel();
226
+ this.active = -1;
227
+ if (!this.host.suggestsAsYouType() || this.text.length < MIN_SUGGEST_LENGTH) {
228
+ this.suggestions = [];
229
+ this.suggestedFor = null;
230
+ this.close();
231
+ return;
232
+ }
233
+ const text = this.text;
234
+ this.timer = setTimeout(() => void this.run(text, false), SUGGEST_DELAY);
235
+ }
236
+ keyDown(event) {
237
+ const count = this.suggestions.length;
238
+ switch (event.key) {
239
+ case "ArrowDown":
240
+ if (!count) return;
241
+ event.preventDefault();
242
+ this.open();
243
+ this.setActive((this.active + 1) % count);
244
+ break;
245
+ case "ArrowUp":
246
+ if (!count) return;
247
+ event.preventDefault();
248
+ this.open();
249
+ this.setActive(this.active <= 0 ? count - 1 : this.active - 1);
250
+ break;
251
+ case "Enter":
252
+ event.preventDefault();
253
+ if (!this.panel.hidden && this.active >= 0) void this.choose(this.active);
254
+ else void this.submit();
255
+ break;
256
+ case "Escape":
257
+ if (!this.panel.hidden) {
258
+ event.preventDefault();
259
+ event.stopPropagation();
260
+ this.close();
261
+ } else if (this.input.value) {
262
+ event.preventDefault();
263
+ event.stopPropagation();
264
+ this.clear();
265
+ }
266
+ break;
267
+ }
268
+ }
269
+ // Enter with no suggestion picked: the first place found for the text, searched for if it hasn't been.
270
+ async submit() {
271
+ const text = this.text;
272
+ if (!text) return false;
273
+ this.cancel();
274
+ const found = this.suggestedFor === text && this.suggestions.length ? this.suggestions : await this.run(text, true);
275
+ if (!found?.length) return false;
276
+ return this.choose(0);
277
+ }
278
+ // The places for `text`: listed as they come, or, asked for with Enter, with a note while they come.
279
+ async run(text, submitted) {
280
+ this.cancel();
281
+ const request = new AbortController();
282
+ this.request = request;
283
+ if (submitted) this.showStatus(this.labels.searching);
284
+ let found;
285
+ try {
286
+ found = await this.host.suggest(text, request.signal);
287
+ } catch (error) {
288
+ if (request.signal.aborted || this.disposed) return null;
289
+ this.suggestions = [];
290
+ this.suggestedFor = null;
291
+ this.renderList();
292
+ this.showStatus(this.labels.failed);
293
+ console.warn("The map's address search failed.", error);
294
+ return null;
295
+ }
296
+ if (request.signal.aborted || this.disposed) return null;
297
+ this.request = null;
298
+ this.suggestions = found;
299
+ this.suggestedFor = text;
300
+ this.active = -1;
301
+ this.renderList();
302
+ if (found.length) {
303
+ this.status.textContent = "";
304
+ if (!submitted) this.open();
305
+ } else {
306
+ this.showStatus(this.labels.noResults);
307
+ }
308
+ return found;
309
+ }
310
+ async choose(index) {
311
+ const suggestion = this.suggestions[index];
312
+ if (!suggestion) return false;
313
+ this.cancel();
314
+ this.input.value = suggestion.detail ? `${suggestion.text}, ${suggestion.detail}` : suggestion.text;
315
+ this.clearButton.hidden = false;
316
+ this.suggestions = [suggestion];
317
+ this.suggestedFor = this.text;
318
+ this.close();
319
+ try {
320
+ if (await this.host.choose(suggestion)) return true;
321
+ if (!this.disposed) this.showStatus(this.labels.noResults);
322
+ } catch (error) {
323
+ if (this.disposed) return false;
324
+ this.showStatus(this.labels.failed);
325
+ console.warn("The map's address search couldn't find the place chosen.", error);
326
+ }
327
+ return false;
328
+ }
329
+ cancel() {
330
+ clearTimeout(this.timer);
331
+ this.request?.abort();
332
+ this.request = null;
333
+ }
334
+ renderList() {
335
+ const document = this.element.ownerDocument;
336
+ this.list.replaceChildren(
337
+ ...this.suggestions.map((suggestion, index) => {
338
+ const option = document.createElement("li");
339
+ option.id = `${this.listId}-${index}`;
340
+ option.className = "ppp-map-search-option";
341
+ option.setAttribute("role", "option");
342
+ option.setAttribute("aria-selected", "false");
343
+ option.setAttribute("data-index", `${index}`);
344
+ const text = document.createElement("span");
345
+ text.className = "ppp-map-search-text";
346
+ text.textContent = suggestion.text;
347
+ option.append(text);
348
+ if (suggestion.detail) {
349
+ const detail = document.createElement("span");
350
+ detail.className = "ppp-map-search-detail";
351
+ detail.textContent = suggestion.detail;
352
+ option.append(detail);
353
+ }
354
+ return option;
355
+ })
356
+ );
357
+ this.list.hidden = this.suggestions.length === 0;
358
+ this.setActive(-1);
359
+ }
360
+ setActive(index) {
361
+ this.active = index;
362
+ this.list.querySelectorAll('[role="option"]').forEach((option, i) => {
363
+ option.setAttribute("aria-selected", `${i === index}`);
364
+ option.toggleAttribute("data-active", i === index);
365
+ });
366
+ if (index >= 0) {
367
+ this.input.setAttribute("aria-activedescendant", `${this.listId}-${index}`);
368
+ this.list.children[index]?.scrollIntoView?.({ block: "nearest" });
369
+ } else {
370
+ this.input.removeAttribute("aria-activedescendant");
371
+ }
372
+ }
373
+ showStatus(text) {
374
+ this.status.textContent = text;
375
+ this.status.hidden = false;
376
+ this.list.hidden = true;
377
+ this.panel.hidden = false;
378
+ this.input.setAttribute("aria-expanded", "false");
379
+ }
380
+ open() {
381
+ if (!this.suggestions.length) return;
382
+ this.list.hidden = false;
383
+ this.status.textContent = "";
384
+ this.status.hidden = true;
385
+ this.panel.hidden = false;
386
+ this.input.setAttribute("aria-expanded", "true");
387
+ }
388
+ close() {
389
+ this.panel.hidden = true;
390
+ this.status.textContent = "";
391
+ this.input.setAttribute("aria-expanded", "false");
392
+ this.setActive(-1);
393
+ }
394
+ };
395
+
396
+ // src/core/theme.ts
397
+ var DEFAULT_ITEM_COLOR = "var(--colorBrandBackground, #0f6cbd)";
398
+ function resolveColor(value, scope) {
399
+ const probe = scope.ownerDocument.createElement("span");
400
+ probe.style.color = value;
401
+ if (!probe.style.color) return null;
402
+ probe.style.display = "none";
403
+ scope.appendChild(probe);
404
+ try {
405
+ const color = getComputedStyle(probe).color;
406
+ return color && !color.includes("var(") ? color : null;
407
+ } finally {
408
+ probe.remove();
409
+ }
410
+ }
411
+ function channels(color) {
412
+ const match = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(color.trim());
413
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
414
+ }
415
+ function isDarkTheme(element) {
416
+ const background = getComputedStyle(element).getPropertyValue("--colorNeutralBackground1").trim();
417
+ if (!background) return false;
418
+ const rgb = channels(resolveColor(background, element) ?? "");
419
+ if (!rgb) return false;
420
+ const [r, g, b] = rgb.map((channel) => {
421
+ const value = channel / 255;
422
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
423
+ });
424
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.2;
425
+ }
426
+ function watchTheme(element, onChange) {
427
+ let pending = false;
428
+ const notify = () => {
429
+ if (pending) return;
430
+ pending = true;
431
+ requestAnimationFrame(() => {
432
+ pending = false;
433
+ onChange();
434
+ });
435
+ };
436
+ const observer = new MutationObserver(notify);
437
+ for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
438
+ observer.observe(ancestor, { attributes: true });
439
+ }
440
+ const scheme = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
441
+ scheme?.addEventListener("change", notify);
442
+ return () => {
443
+ observer.disconnect();
444
+ scheme?.removeEventListener("change", notify);
445
+ };
446
+ }
447
+
448
+ // src/core/markers.ts
449
+ var CLUSTER_RADIUS = 60;
450
+ var CLUSTER_EXTENT = 256;
451
+ var CLUSTER_MAX_ZOOM = 17;
452
+ var PIN_PATH = "M12 1C5.9 1 1 5.9 1 12c0 8.3 11 19 11 19s11-10.7 11-19C23 5.9 18.1 1 12 1z";
453
+ var PIN_SVG = `<svg viewBox="0 0 24 32" width="24" height="32" aria-hidden="true" focusable="false"><path d="${PIN_PATH}" class="ppp-map-marker-pin"/><circle cx="12" cy="12" r="4.25" class="ppp-map-marker-dot"/></svg>`;
454
+ var MarkerLayer = class {
455
+ constructor(surface, document, options) {
456
+ this.surface = surface;
457
+ this.document = document;
458
+ this.options = options;
459
+ }
460
+ surface;
461
+ document;
462
+ options;
463
+ markers = [];
464
+ index = null;
465
+ shown = /* @__PURE__ */ new Map();
466
+ selectedId = null;
467
+ /** Replaces the markers, clustered or not. */
468
+ setMarkers(markers, cluster) {
469
+ this.markers = markers.filter((marker) => isValidPosition(marker.position));
470
+ this.index = cluster ? new Supercluster({
471
+ radius: CLUSTER_RADIUS,
472
+ extent: CLUSTER_EXTENT,
473
+ maxZoom: CLUSTER_MAX_ZOOM,
474
+ // A cluster of one kind of marker — a view's rows, drawn in their color — is drawn in it too.
475
+ map: (properties) => ({ color: this.markers[properties.index]?.color ?? "" }),
476
+ reduce: (cluster2, properties) => {
477
+ if (cluster2.color !== properties.color) cluster2.color = null;
478
+ }
479
+ }).load(
480
+ this.markers.map((marker, index) => ({
481
+ type: "Feature",
482
+ properties: { index },
483
+ geometry: { type: "Point", coordinates: [marker.position.longitude, marker.position.latitude] }
484
+ }))
485
+ ) : null;
486
+ this.clear();
487
+ this.refresh();
488
+ }
489
+ /** Draws on a new surface: the same provider's map drawn again. */
490
+ moveTo(surface) {
491
+ this.clear();
492
+ this.surface = surface;
493
+ this.refresh();
494
+ }
495
+ /** Shows what the current view holds. */
496
+ refresh() {
497
+ const view = this.surface.getView();
498
+ const wanted = /* @__PURE__ */ new Map();
499
+ const marks = /* @__PURE__ */ new Map();
500
+ for (const entry of this.visible(view)) {
501
+ wanted.set(entry.key, entry.create);
502
+ marks.set(entry.key, entry.mark);
503
+ }
504
+ for (const [key, shown] of this.shown) {
505
+ if (!wanted.has(key)) {
506
+ shown.overlay.remove();
507
+ this.shown.delete(key);
508
+ }
509
+ }
510
+ for (const [key, create] of wanted) {
511
+ if (this.shown.has(key)) continue;
512
+ const element = create();
513
+ const mark = marks.get(key);
514
+ const anchor = mark.kind === "cluster" ? "center" : "bottom";
515
+ this.shown.set(key, { element, mark, overlay: this.surface.addOverlay(element, mark.position, anchor) });
516
+ }
517
+ }
518
+ /** The pins and clusters on the map now, as its image draws them. */
519
+ marks() {
520
+ return [...this.shown.values()].map((shown) => shown.mark);
521
+ }
522
+ /** Marks the marker of that id as the one whose popup is open; null for none. */
523
+ setSelected(id) {
524
+ this.selectedId = id;
525
+ for (const [key, shown] of this.shown) {
526
+ if (key.startsWith("m:")) shown.element.toggleAttribute("data-selected", key === `m:${id}`);
527
+ }
528
+ }
529
+ /** The element of the marker of that id, when it is shown on its own — not in a cluster. */
530
+ elementOf(id) {
531
+ return this.shown.get(`m:${id}`)?.element ?? null;
532
+ }
533
+ dispose() {
534
+ this.clear();
535
+ this.markers = [];
536
+ this.index = null;
537
+ }
538
+ clear() {
539
+ for (const shown of this.shown.values()) shown.overlay.remove();
540
+ this.shown.clear();
541
+ }
542
+ // What to show for the view: clusters and lone markers from the index, or, without one, every
543
+ // marker near the view. A little beyond it either way, so a short pan finds them already there.
544
+ *visible(view) {
545
+ const area = padBounds(view.bounds, 0.25);
546
+ if (!this.index) {
547
+ for (const marker of this.markers) {
548
+ if (boundsContain(area, marker.position)) {
549
+ yield { key: `m:${marker.id}`, mark: markerMark(marker), create: () => this.markerElement(marker) };
550
+ }
551
+ }
552
+ return;
553
+ }
554
+ const [west, south, east, north] = toBoundingBox(area);
555
+ const zoom = Math.max(0, Math.floor(view.zoom));
556
+ const boxes = west <= east ? [[west, south, east, north]] : [[west, south, 180, north], [-180, south, east, north]];
557
+ for (const box of boxes) {
558
+ for (const feature of this.index.getClusters(box, zoom)) {
559
+ const [longitude, latitude] = feature.geometry.coordinates;
560
+ const position = { latitude, longitude };
561
+ const properties = feature.properties;
562
+ if ("cluster" in properties && properties.cluster) {
563
+ const id = properties.cluster_id;
564
+ const count = properties.point_count;
565
+ yield {
566
+ key: `c:${id}`,
567
+ mark: {
568
+ kind: "cluster",
569
+ position,
570
+ color: properties.color || DEFAULT_ITEM_COLOR,
571
+ text: clusterText(count),
572
+ size: clusterSize(count)
573
+ },
574
+ create: () => this.clusterElement(id, count, position, properties.color)
575
+ };
576
+ } else {
577
+ const marker = this.markers[properties.index];
578
+ yield { key: `m:${marker.id}`, mark: markerMark(marker), create: () => this.markerElement(marker) };
579
+ }
580
+ }
581
+ }
582
+ }
583
+ markerElement(marker) {
584
+ const element = this.document.createElement("button");
585
+ element.type = "button";
586
+ element.className = "ppp-map-marker";
587
+ element.innerHTML = PIN_SVG;
588
+ element.style.setProperty("--ppp-map-item-color", marker.color || DEFAULT_ITEM_COLOR);
589
+ const label = markerLabel(marker);
590
+ if (label) {
591
+ const text = this.document.createElement("span");
592
+ text.className = "ppp-map-marker-label";
593
+ text.dataset.length = String(Math.min(label.length, 5));
594
+ text.textContent = label;
595
+ element.appendChild(text);
596
+ element.toggleAttribute("data-labeled", true);
597
+ }
598
+ if (marker.title) {
599
+ element.dataset.pppTooltip = marker.title;
600
+ element.setAttribute("aria-label", marker.title);
601
+ }
602
+ element.toggleAttribute("data-selected", marker.id === this.selectedId);
603
+ element.addEventListener("click", (event) => {
604
+ event.stopPropagation();
605
+ this.options.onMarkerClick(marker);
606
+ });
607
+ return element;
608
+ }
609
+ clusterElement(clusterId, count, position, color) {
610
+ const element = this.document.createElement("button");
611
+ element.type = "button";
612
+ element.className = "ppp-map-cluster";
613
+ element.textContent = clusterText(count);
614
+ element.dataset.size = clusterSize(count);
615
+ if (color) element.style.setProperty("--ppp-map-item-color", color);
616
+ const label = this.options.clusterLabel(count);
617
+ element.dataset.pppTooltip = label;
618
+ element.setAttribute("aria-label", label);
619
+ element.addEventListener("click", (event) => {
620
+ event.stopPropagation();
621
+ this.clusterClicked(clusterId, position);
622
+ });
623
+ return element;
624
+ }
625
+ clusterClicked(clusterId, position) {
626
+ if (!this.index) return;
627
+ const zoom = this.index.getClusterExpansionZoom(clusterId);
628
+ const leaves = this.index.getLeaves(clusterId, Infinity).map((leaf) => this.markers[leaf.properties.index]);
629
+ const first = leaves[0]?.position;
630
+ const together = leaves.every(
631
+ (marker) => marker.position.latitude === first?.latitude && marker.position.longitude === first?.longitude
632
+ );
633
+ this.options.onClusterClick(together && first ? first : position, together ? null : zoom, leaves);
634
+ }
635
+ };
636
+ var clusterText = (count) => count > 999 ? `${Math.floor(count / 1e3)}k` : String(count);
637
+ var clusterSize = (count) => count < 10 ? "small" : count < 100 ? "medium" : "large";
638
+ function markerLabel(marker) {
639
+ return marker.label?.trim() || void 0;
640
+ }
641
+ function markerMark(marker) {
642
+ const label = markerLabel(marker);
643
+ return { kind: "marker", position: marker.position, color: marker.color || DEFAULT_ITEM_COLOR, ...label && { label } };
644
+ }
645
+
646
+ // src/core/popup-content.ts
647
+ var BLOCKS = /* @__PURE__ */ new Set([
648
+ "address",
649
+ "article",
650
+ "blockquote",
651
+ "dd",
652
+ "div",
653
+ "dl",
654
+ "dt",
655
+ "figcaption",
656
+ "figure",
657
+ "footer",
658
+ "h1",
659
+ "h2",
660
+ "h3",
661
+ "h4",
662
+ "h5",
663
+ "h6",
664
+ "header",
665
+ "li",
666
+ "ol",
667
+ "p",
668
+ "section",
669
+ "table",
670
+ "tr",
671
+ "ul"
672
+ ]);
673
+ var BLOCK_DISPLAYS = /* @__PURE__ */ new Set(["block", "flex", "grid", "list-item", "table", "table-row", "flow-root"]);
674
+ var SKIPPED = /* @__PURE__ */ new Set(["button", "fluent-button", "img", "picture", "script", "style", "svg", "template"]);
675
+ var UNREAD_INPUTS = /* @__PURE__ */ new Set(["button", "checkbox", "file", "hidden", "image", "password", "radio", "reset", "submit"]);
676
+ var clean = (text) => text.replace(/\s+/g, " ").trim();
677
+ var displayOf = (element) => {
678
+ const view = element.ownerDocument.defaultView;
679
+ return view?.getComputedStyle ? view.getComputedStyle(element) : null;
680
+ };
681
+ function readPopupContent(element) {
682
+ const lines = [];
683
+ const details = [];
684
+ let title;
685
+ let line = "";
686
+ let label;
687
+ const endLine = (into) => {
688
+ const text = clean(line);
689
+ line = "";
690
+ if (!text) return;
691
+ if (into === lines && title === void 0 && lines.length === 0 && details.length === 0) title = text;
692
+ else into.push(text);
693
+ };
694
+ const linesOf = (node) => {
695
+ const own = [];
696
+ const before = line;
697
+ line = "";
698
+ walk(node, own);
699
+ const text = clean(line);
700
+ if (text) own.push(text);
701
+ line = before;
702
+ return own;
703
+ };
704
+ const walk = (node, into) => {
705
+ for (const child of Array.from(node.childNodes)) {
706
+ if (child.nodeType === child.TEXT_NODE) {
707
+ line += child.textContent ?? "";
708
+ continue;
709
+ }
710
+ if (child.nodeType !== child.ELEMENT_NODE) continue;
711
+ const element2 = child;
712
+ const tag = element2.tagName.toLowerCase();
713
+ if (SKIPPED.has(tag) || element2.getAttribute("role") === "button" || element2.hasAttribute("hidden") || element2.getAttribute("aria-hidden") === "true")
714
+ continue;
715
+ const style = displayOf(element2);
716
+ if (style && (style.display === "none" || style.visibility === "hidden")) continue;
717
+ if (tag === "select") {
718
+ line += ` ${Array.from(element2.selectedOptions, (option) => option.text).join(", ")} `;
719
+ } else if (tag === "textarea" || tag === "input" && !UNREAD_INPUTS.has(element2.type)) {
720
+ line += ` ${element2.value} `;
721
+ } else if (tag === "br") {
722
+ endLine(into);
723
+ } else if (tag === "dt") {
724
+ endLine(into);
725
+ label = linesOf(element2).join(" ");
726
+ } else if (tag === "dd") {
727
+ endLine(into);
728
+ const value = linesOf(element2).join(" \xB7 ");
729
+ if (value) details.push({ label: label ?? "", value });
730
+ label = void 0;
731
+ } else if (BLOCKS.has(tag) || style !== null && BLOCK_DISPLAYS.has(style.display)) {
732
+ endLine(into);
733
+ walk(element2, into);
734
+ endLine(into);
735
+ } else {
736
+ walk(element2, into);
737
+ }
738
+ }
739
+ };
740
+ walk(element, lines);
741
+ endLine(lines);
742
+ return { ...title && { title }, ...lines.length && { lines }, ...details.length && { details } };
743
+ }
744
+
745
+ // src/core/popup-snapshot.ts
746
+ var MARGIN = 24;
747
+ var LEFT_OUT = ".ppp-map-popup-close";
748
+ var XHTML = "http://www.w3.org/1999/xhtml";
749
+ var INHERITED = /* @__PURE__ */ new Set([
750
+ "color",
751
+ "cursor",
752
+ "direction",
753
+ "fill",
754
+ "font-family",
755
+ "font-feature-settings",
756
+ "font-kerning",
757
+ "font-size",
758
+ "font-stretch",
759
+ "font-style",
760
+ "font-variant",
761
+ "font-variation-settings",
762
+ "font-weight",
763
+ "hyphens",
764
+ "letter-spacing",
765
+ "line-height",
766
+ "list-style-position",
767
+ "list-style-type",
768
+ "overflow-wrap",
769
+ "quotes",
770
+ "stroke",
771
+ "tab-size",
772
+ "text-align",
773
+ "text-indent",
774
+ "text-rendering",
775
+ "text-shadow",
776
+ "text-transform",
777
+ "visibility",
778
+ "white-space",
779
+ "word-break",
780
+ "word-spacing",
781
+ "writing-mode",
782
+ "-webkit-font-smoothing"
783
+ ]);
784
+ var IMAGE_PROPERTIES = ["background-image", "mask-image", "-webkit-mask-image", "list-style-image", "border-image-source"];
785
+ async function snapshotElement(element, frame, ratio) {
786
+ const view = element.ownerDocument.defaultView;
787
+ if (!view || !("decode" in view.HTMLImageElement.prototype)) return null;
788
+ const box = element.getBoundingClientRect();
789
+ if (!box.width || !box.height) return null;
790
+ try {
791
+ if (loadsWhatAPictureCant(element, view)) return null;
792
+ const scale = frame.scale || 1;
793
+ const size = { width: box.width / scale, height: box.height / scale };
794
+ const width = size.width + 2 * MARGIN;
795
+ const height = size.height + 2 * MARGIN;
796
+ const image = new view.Image();
797
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(snapshotMarkup(element, size, ratio))}`;
798
+ await image.decode();
799
+ if (!readable(image, element.ownerDocument)) return null;
800
+ return {
801
+ image,
802
+ left: (box.left - frame.left) / scale - MARGIN,
803
+ top: (box.top - frame.top) / scale - MARGIN,
804
+ width,
805
+ height
806
+ };
807
+ } catch {
808
+ return null;
809
+ }
810
+ }
811
+ function snapshotMarkup(element, size, ratio) {
812
+ const view = element.ownerDocument.defaultView;
813
+ const copy = copyWithStyles(element, view, null);
814
+ if (copy instanceof view.Element) {
815
+ const style = copy.style;
816
+ for (const name of ["position", "left", "top", "right", "bottom", "transform", "translate", "margin"])
817
+ style.removeProperty(name);
818
+ style.setProperty("width", `${size.width}px`);
819
+ style.setProperty("height", `${size.height}px`);
820
+ style.setProperty("box-sizing", "border-box");
821
+ }
822
+ const width = size.width + 2 * MARGIN;
823
+ const height = size.height + 2 * MARGIN;
824
+ const frame = element.ownerDocument.createElementNS(XHTML, "div");
825
+ frame.setAttribute("style", `padding: ${MARGIN}px;`);
826
+ frame.appendChild(copy);
827
+ const markup = new view.XMLSerializer().serializeToString(frame);
828
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width * ratio}" height="${height * ratio}" viewBox="0 0 ${width} ${height}"><foreignObject x="0" y="0" width="${width}" height="${height}">${markup}</foreignObject></svg>`;
829
+ }
830
+ function copyWithStyles(source, view, parent) {
831
+ if (source.nodeType === source.TEXT_NODE) return source.cloneNode();
832
+ if (!(source instanceof view.Element)) return source.ownerDocument.createTextNode("");
833
+ const tag = source.tagName.toLowerCase();
834
+ if (tag === "script" || tag === "style" || tag === "template") return source.ownerDocument.createTextNode("");
835
+ const style = view.getComputedStyle(source);
836
+ const copy = tag === "canvas" ? imageOf(source) : source.cloneNode(false);
837
+ copy.setAttribute("style", cssText(style, parent));
838
+ copy.removeAttribute("class");
839
+ copy.removeAttribute("id");
840
+ if (source.matches(LEFT_OUT)) {
841
+ copy.style.setProperty("visibility", "hidden");
842
+ return copy;
843
+ }
844
+ if (source instanceof view.HTMLImageElement) copy.setAttribute("src", dataUrlOf(source) ?? "");
845
+ if (source instanceof view.HTMLInputElement) {
846
+ copy.setAttribute("value", source.value);
847
+ if (source.checked) copy.setAttribute("checked", "");
848
+ else copy.removeAttribute("checked");
849
+ }
850
+ if (source instanceof view.HTMLTextAreaElement) copy.textContent = source.value;
851
+ if (tag === "canvas" || source instanceof view.HTMLTextAreaElement) return copy;
852
+ const before = pseudoElement(source, "::before", view, style);
853
+ if (before) copy.appendChild(before);
854
+ for (const child of shownChildren(source, view)) copy.appendChild(copyWithStyles(child, view, style));
855
+ const after = pseudoElement(source, "::after", view, style);
856
+ if (after) copy.appendChild(after);
857
+ if (source instanceof view.HTMLSelectElement) {
858
+ const copied = copy.querySelectorAll("option");
859
+ Array.from(source.options).forEach((option, index) => {
860
+ if (option.selected) copied[index]?.setAttribute("selected", "");
861
+ else copied[index]?.removeAttribute("selected");
862
+ });
863
+ }
864
+ return copy;
865
+ }
866
+ function shownChildren(element, view) {
867
+ const own = element.shadowRoot ? Array.from(element.shadowRoot.childNodes) : Array.from(element.childNodes);
868
+ return own.flatMap((child) => {
869
+ if (!(child instanceof view.HTMLSlotElement)) return [child];
870
+ const assigned = child.assignedNodes({ flatten: true });
871
+ return assigned.length ? assigned : Array.from(child.childNodes);
872
+ });
873
+ }
874
+ function pseudoElement(element, which, view, parent) {
875
+ const style = view.getComputedStyle(element, which);
876
+ const content = style.getPropertyValue("content");
877
+ if (!content || content === "none" || content === "normal") return null;
878
+ const span = element.ownerDocument.createElementNS(XHTML, "span");
879
+ span.setAttribute("style", cssText(style, parent));
880
+ const text = /^(["'])(.*)\1$/.exec(content.trim());
881
+ if (text) span.textContent = text[2].replace(/\\(.)/g, "$1");
882
+ return span;
883
+ }
884
+ function cssText(style, parent) {
885
+ let text = "";
886
+ for (let index = 0; index < style.length; index++) {
887
+ const name = style[index];
888
+ const value = style.getPropertyValue(name);
889
+ if (parent && INHERITED.has(name) && parent.getPropertyValue(name) === value) continue;
890
+ text += `${name}: ${value};`;
891
+ }
892
+ return text;
893
+ }
894
+ function loadsWhatAPictureCant(element, view) {
895
+ const fonts = element.ownerDocument.fonts;
896
+ const webFonts = /* @__PURE__ */ new Set();
897
+ fonts?.forEach((face) => {
898
+ if (face.status !== "error") webFonts.add(face.family.replace(/["']/g, "").trim().toLowerCase());
899
+ });
900
+ const loads = (node) => {
901
+ const style = view.getComputedStyle(node);
902
+ if (style.display === "none") return false;
903
+ if (IMAGE_PROPERTIES.some((name) => style.getPropertyValue(name).includes("url("))) return true;
904
+ const family = style.fontFamily.split(",")[0]?.replace(/["']/g, "").trim().toLowerCase();
905
+ if (family && webFonts.has(family)) return true;
906
+ return shownChildren(node, view).some((child) => child instanceof view.Element && !child.matches(LEFT_OUT) && loads(child));
907
+ };
908
+ return loads(element);
909
+ }
910
+ function imageOf(canvas) {
911
+ const image = canvas.ownerDocument.createElementNS(XHTML, "img");
912
+ try {
913
+ image.setAttribute("src", canvas.toDataURL());
914
+ } catch {
915
+ }
916
+ return image;
917
+ }
918
+ function dataUrlOf(image) {
919
+ if (!image.complete || !image.naturalWidth) return null;
920
+ try {
921
+ const canvas = image.ownerDocument.createElement("canvas");
922
+ canvas.width = image.naturalWidth;
923
+ canvas.height = image.naturalHeight;
924
+ canvas.getContext("2d")?.drawImage(image, 0, 0);
925
+ return canvas.toDataURL();
926
+ } catch {
927
+ return null;
928
+ }
929
+ }
930
+ function readable(image, document) {
931
+ try {
932
+ const canvas = document.createElement("canvas");
933
+ canvas.width = 1;
934
+ canvas.height = 1;
935
+ const context = canvas.getContext("2d");
936
+ if (!context) return false;
937
+ context.drawImage(image, 0, 0, 1, 1);
938
+ context.getImageData(0, 0, 1, 1);
939
+ return true;
940
+ } catch {
941
+ return false;
942
+ }
943
+ }
944
+
945
+ // src/core/print.ts
946
+ var PIN_WIDTH = 24;
947
+ var PIN_HEIGHT = 32;
948
+ var PIN_HEAD = { x: 12, y: 12, radius: 4.25 };
949
+ var CLUSTER_DIAMETER = { small: 32, medium: 38, large: 44 };
950
+ var LABEL_FONT_SIZE = [11, 11, 11, 9, 7.5, 6];
951
+ var CORNER_MARGIN = 8;
952
+ function drawMapImage(parts) {
953
+ const canvas = parts.map.ownerDocument.createElement("canvas");
954
+ canvas.width = parts.map.width;
955
+ canvas.height = parts.map.height;
956
+ const context = canvas.getContext("2d");
957
+ if (!context) throw new Error("The browser can't draw the map's image.");
958
+ context.drawImage(parts.map, 0, 0);
959
+ const ratio = parts.width > 0 ? parts.map.width / parts.width : 1;
960
+ context.scale(ratio, ratio);
961
+ for (const mark of parts.marks) {
962
+ if (mark.kind === "cluster") drawCluster(context, mark, parts, ratio);
963
+ else drawPin(context, mark.point, mark.color, parts, ratio, mark.label);
964
+ }
965
+ if (parts.place) drawPin(context, parts.place, parts.colors.place, parts, ratio);
966
+ if (parts.legend?.length) drawLegend(context, parts.legend, parts);
967
+ if (parts.attribution) drawAttribution(context, parts.attribution, parts);
968
+ if (parts.popup) drawPopup(context, parts.popup, parts, ratio);
969
+ return canvas;
970
+ }
971
+ function withShadow(context, ratio, blur, draw) {
972
+ context.save();
973
+ context.shadowColor = "rgba(0, 0, 0, 0.35)";
974
+ context.shadowBlur = blur * ratio;
975
+ context.shadowOffsetY = ratio;
976
+ draw();
977
+ context.restore();
978
+ }
979
+ function drawPin(context, point, color, parts, ratio, label) {
980
+ context.save();
981
+ context.translate(point.x - PIN_WIDTH / 2, point.y - PIN_HEIGHT);
982
+ const path = new Path2D(PIN_PATH);
983
+ withShadow(context, ratio, 2, () => {
984
+ context.fillStyle = color;
985
+ context.fill(path);
986
+ });
987
+ context.lineWidth = 1.5;
988
+ context.strokeStyle = parts.colors.background;
989
+ context.stroke(path);
990
+ if (label) {
991
+ const size = LABEL_FONT_SIZE[Math.min(label.length, LABEL_FONT_SIZE.length - 1)];
992
+ context.font = `700 ${size}px ${parts.font}`;
993
+ context.fillStyle = parts.colors.onColor;
994
+ context.textAlign = "center";
995
+ context.textBaseline = "middle";
996
+ context.fillText(label, PIN_HEAD.x, PIN_HEAD.y + 0.5, PIN_WIDTH - 4);
997
+ } else {
998
+ context.beginPath();
999
+ context.arc(PIN_HEAD.x, PIN_HEAD.y, PIN_HEAD.radius, 0, Math.PI * 2);
1000
+ context.fillStyle = parts.colors.background;
1001
+ context.fill();
1002
+ }
1003
+ context.restore();
1004
+ }
1005
+ function drawCluster(context, mark, parts, ratio) {
1006
+ const height = CLUSTER_DIAMETER[mark.size];
1007
+ context.save();
1008
+ context.font = `600 ${mark.size === "large" ? 14 : 12}px ${parts.font}`;
1009
+ const width = Math.max(height, context.measureText(mark.text).width + 16);
1010
+ const left = mark.point.x - width / 2;
1011
+ const top = mark.point.y - height / 2;
1012
+ context.beginPath();
1013
+ context.roundRect(left, top, width, height, height / 2);
1014
+ withShadow(context, ratio, 2, () => {
1015
+ context.fillStyle = mark.color;
1016
+ context.fill();
1017
+ });
1018
+ context.lineWidth = 2;
1019
+ context.strokeStyle = parts.colors.background;
1020
+ context.stroke();
1021
+ context.fillStyle = parts.colors.onColor;
1022
+ context.textAlign = "center";
1023
+ context.textBaseline = "middle";
1024
+ context.fillText(mark.text, mark.point.x, mark.point.y + 0.5);
1025
+ context.restore();
1026
+ }
1027
+ function drawLegend(context, legend, parts) {
1028
+ const dot = 10;
1029
+ const gap = 4;
1030
+ const itemGap = 12;
1031
+ const lineHeight = 16;
1032
+ const padding = { x: 8, y: 4 };
1033
+ const maxWidth = parts.width - 2 * CORNER_MARGIN - 2 * padding.x;
1034
+ context.save();
1035
+ context.font = `400 12px ${parts.font}`;
1036
+ const lines = [[]];
1037
+ let lineWidth = 0;
1038
+ for (const entry of legend) {
1039
+ const width = dot + gap + context.measureText(entry.label).width;
1040
+ const line = lines.at(-1);
1041
+ if (line.length && lineWidth + itemGap + width > maxWidth) {
1042
+ lines.push([]);
1043
+ lineWidth = 0;
1044
+ }
1045
+ lines.at(-1).push({ ...entry, width });
1046
+ lineWidth += (lines.at(-1).length > 1 ? itemGap : 0) + width;
1047
+ }
1048
+ const widest = Math.max(...lines.map((line) => line.reduce((sum, item, index) => sum + item.width + (index ? itemGap : 0), 0)));
1049
+ const boxWidth = Math.min(widest, maxWidth) + 2 * padding.x;
1050
+ const boxHeight = lines.length * lineHeight + 2 * padding.y;
1051
+ const left = CORNER_MARGIN;
1052
+ const top = parts.height - CORNER_MARGIN - boxHeight;
1053
+ context.beginPath();
1054
+ context.roundRect(left, top, boxWidth, boxHeight, 4);
1055
+ context.fillStyle = parts.colors.background;
1056
+ context.fill();
1057
+ context.textBaseline = "middle";
1058
+ lines.forEach((line, row) => {
1059
+ let x = left + padding.x;
1060
+ const y = top + padding.y + row * lineHeight + lineHeight / 2;
1061
+ for (const item of line) {
1062
+ context.beginPath();
1063
+ context.arc(x + dot / 2, y, dot / 2, 0, Math.PI * 2);
1064
+ context.fillStyle = item.color;
1065
+ context.fill();
1066
+ context.fillStyle = parts.colors.foreground;
1067
+ context.fillText(item.label, x + dot + gap, y + 0.5, Math.max(0, left + boxWidth - padding.x - (x + dot + gap)));
1068
+ x += item.width + itemGap;
1069
+ }
1070
+ });
1071
+ context.restore();
1072
+ }
1073
+ function drawAttribution(context, text, parts) {
1074
+ const padding = { x: 5, y: 2 };
1075
+ const lineHeight = 13;
1076
+ context.save();
1077
+ context.font = `400 10px ${parts.font}`;
1078
+ const maxWidth = parts.width - 2 * CORNER_MARGIN - 2 * padding.x;
1079
+ const lines = [];
1080
+ for (const word of text.split(/\s+/).filter(Boolean)) {
1081
+ const last = lines.at(-1);
1082
+ if (last !== void 0 && context.measureText(`${last} ${word}`).width <= maxWidth) lines[lines.length - 1] = `${last} ${word}`;
1083
+ else lines.push(word);
1084
+ }
1085
+ const width = Math.min(maxWidth, Math.max(...lines.map((line) => context.measureText(line).width))) + 2 * padding.x;
1086
+ const height = lines.length * lineHeight + 2 * padding.y;
1087
+ const left = parts.width - CORNER_MARGIN / 2 - width;
1088
+ const top = parts.height - CORNER_MARGIN / 2 - height;
1089
+ context.globalAlpha = 0.85;
1090
+ context.fillStyle = parts.colors.background;
1091
+ context.fillRect(left, top, width, height);
1092
+ context.globalAlpha = 1;
1093
+ context.fillStyle = parts.colors.foreground;
1094
+ context.textAlign = "right";
1095
+ context.textBaseline = "middle";
1096
+ lines.forEach(
1097
+ (line, index) => context.fillText(line, left + width - padding.x, top + padding.y + index * lineHeight + lineHeight / 2, maxWidth)
1098
+ );
1099
+ context.restore();
1100
+ }
1101
+ var POPUP = { minWidth: 200, maxWidth: 340, maxHeight: 360, padding: { x: 16, y: 12 }, radius: 12, gap: 8, columnGap: 12 };
1102
+ var POPUP_TITLE = { size: 14, lineHeight: 20, maxLines: 3 };
1103
+ var POPUP_FIELD = { size: 12, lineHeight: 16, maxLines: 3 };
1104
+ var POPUP_ITEM = { size: 13, lineHeight: 24 };
1105
+ function drawPopup(context, popup, parts, ratio) {
1106
+ if (popup.snapshot) {
1107
+ const { image, left: left2, top: top2, width: width2, height: height2 } = popup.snapshot;
1108
+ context.drawImage(image, left2, top2, width2, height2);
1109
+ return;
1110
+ }
1111
+ const { content } = popup;
1112
+ const inner = POPUP.maxWidth - 2 * POPUP.padding.x;
1113
+ const fonts = {
1114
+ title: `600 ${POPUP_TITLE.size}px ${parts.font}`,
1115
+ field: `400 ${POPUP_FIELD.size}px ${parts.font}`,
1116
+ item: `400 ${POPUP_ITEM.size}px ${parts.font}`
1117
+ };
1118
+ context.save();
1119
+ context.font = fonts.title;
1120
+ const title = content.title ? wrapText(context, content.title, inner, POPUP_TITLE.maxLines) : [];
1121
+ let used = Math.max(0, ...title.map((line) => context.measureText(line).width));
1122
+ context.font = fonts.field;
1123
+ let text = (content.lines ?? []).filter((line) => line.trim()).flatMap((line) => wrapText(context, line, inner, POPUP_FIELD.maxLines));
1124
+ if (text.length) used = Math.max(used, ...text.map((line) => context.measureText(line).width));
1125
+ const details = (content.details ?? []).filter((detail) => detail.value.trim());
1126
+ const labelWidth = Math.min(inner * 0.4, Math.max(0, ...details.map((detail) => context.measureText(detail.label).width)));
1127
+ const valueWidth = inner - labelWidth - POPUP.columnGap;
1128
+ const rows = details.map((detail) => ({
1129
+ label: fitText(context, detail.label, labelWidth),
1130
+ lines: wrapText(context, detail.value, valueWidth, POPUP_FIELD.maxLines)
1131
+ }));
1132
+ if (rows.length) {
1133
+ const widest = Math.max(...rows.flatMap((row) => row.lines.map((line) => context.measureText(line).width)));
1134
+ used = Math.max(used, labelWidth + POPUP.columnGap + widest);
1135
+ }
1136
+ context.font = fonts.item;
1137
+ let items = (content.items ?? []).map((item) => fitText(context, item, inner));
1138
+ if (items.length) used = Math.max(used, ...items.map((item) => context.measureText(item).width));
1139
+ let room = POPUP.maxHeight - 2 * POPUP.padding.y - title.length * POPUP_TITLE.lineHeight;
1140
+ context.font = fonts.field;
1141
+ let cut = false;
1142
+ if (text.length && title.length) room -= POPUP.gap;
1143
+ const textFit = Math.max(0, Math.floor(room / POPUP_FIELD.lineHeight));
1144
+ if (text.length > textFit) {
1145
+ cut = true;
1146
+ text = text.slice(0, textFit);
1147
+ if (text.length) text[text.length - 1] = fitText(context, text.at(-1), inner, true);
1148
+ }
1149
+ room -= text.length * POPUP_FIELD.lineHeight;
1150
+ if (cut) rows.length = 0;
1151
+ if (rows.length && (title.length || text.length)) room -= POPUP.gap;
1152
+ for (let index = 0; index < rows.length && !cut; index++) {
1153
+ const row = rows[index];
1154
+ const fit = Math.min(row.lines.length, Math.floor(room / POPUP_FIELD.lineHeight));
1155
+ if (fit < row.lines.length) {
1156
+ cut = true;
1157
+ row.lines = row.lines.slice(0, fit);
1158
+ rows.length = fit > 0 ? index + 1 : index;
1159
+ }
1160
+ room -= fit * POPUP_FIELD.lineHeight;
1161
+ }
1162
+ if (cut) {
1163
+ const last = rows.at(-1);
1164
+ if (last) last.lines[last.lines.length - 1] = fitText(context, last.lines.at(-1), valueWidth, true);
1165
+ items = [];
1166
+ }
1167
+ if (items.length && (title.length || text.length || rows.length)) room -= POPUP.gap;
1168
+ const itemsFit = Math.max(0, Math.floor(room / POPUP_ITEM.lineHeight));
1169
+ if (items.length > itemsFit) items = itemsFit > 0 ? [...items.slice(0, itemsFit - 1), "\u2026"] : [];
1170
+ const sections = [title.length, text.length, rows.length, items.length].filter(Boolean).length;
1171
+ if (sections === 0) {
1172
+ context.restore();
1173
+ return;
1174
+ }
1175
+ const width = Math.min(POPUP.maxWidth, Math.max(POPUP.minWidth, used + 2 * POPUP.padding.x));
1176
+ const height = 2 * POPUP.padding.y + title.length * POPUP_TITLE.lineHeight + text.length * POPUP_FIELD.lineHeight + rows.reduce((sum, row) => sum + row.lines.length * POPUP_FIELD.lineHeight, 0) + items.length * POPUP_ITEM.lineHeight + (sections - 1) * POPUP.gap;
1177
+ const left = Math.max(CORNER_MARGIN, Math.min(parts.width - CORNER_MARGIN - width, popup.point.x - width / 2));
1178
+ const top = Math.max(CORNER_MARGIN, popup.point.y - popup.offset - height);
1179
+ context.beginPath();
1180
+ context.roundRect(left, top, width, height, POPUP.radius);
1181
+ withShadow(context, ratio, 10, () => {
1182
+ context.fillStyle = parts.colors.background;
1183
+ context.fill();
1184
+ });
1185
+ context.lineWidth = 1;
1186
+ context.strokeStyle = parts.colors.stroke;
1187
+ context.stroke();
1188
+ context.textBaseline = "middle";
1189
+ const x = left + POPUP.padding.x;
1190
+ let y = top + POPUP.padding.y;
1191
+ context.fillStyle = parts.colors.foreground;
1192
+ context.font = fonts.title;
1193
+ for (const line of title) {
1194
+ context.fillText(line, x, y + POPUP_TITLE.lineHeight / 2);
1195
+ y += POPUP_TITLE.lineHeight;
1196
+ }
1197
+ if (title.length && (text.length || rows.length || items.length)) y += POPUP.gap;
1198
+ context.font = fonts.field;
1199
+ for (const line of text) {
1200
+ context.fillText(line, x, y + POPUP_FIELD.lineHeight / 2);
1201
+ y += POPUP_FIELD.lineHeight;
1202
+ }
1203
+ if (text.length && (rows.length || items.length)) y += POPUP.gap;
1204
+ for (const row of rows) {
1205
+ context.fillStyle = parts.colors.secondary;
1206
+ context.fillText(row.label, x, y + POPUP_FIELD.lineHeight / 2);
1207
+ context.fillStyle = parts.colors.foreground;
1208
+ for (const line of row.lines) {
1209
+ context.fillText(line, x + labelWidth + POPUP.columnGap, y + POPUP_FIELD.lineHeight / 2);
1210
+ y += POPUP_FIELD.lineHeight;
1211
+ }
1212
+ }
1213
+ if (rows.length && items.length) y += POPUP.gap;
1214
+ context.font = fonts.item;
1215
+ context.fillStyle = parts.colors.foreground;
1216
+ for (const item of items) {
1217
+ context.fillText(item, x, y + POPUP_ITEM.lineHeight / 2);
1218
+ y += POPUP_ITEM.lineHeight;
1219
+ }
1220
+ context.restore();
1221
+ }
1222
+ function wrapText(context, text, maxWidth, maxLines) {
1223
+ const lines = [];
1224
+ const runsOn = /* @__PURE__ */ new Set();
1225
+ let line = "";
1226
+ for (const word of text.split(/\s+/).filter(Boolean)) {
1227
+ const joined = line ? `${line} ${word}` : word;
1228
+ if (context.measureText(joined).width <= maxWidth) {
1229
+ line = joined;
1230
+ continue;
1231
+ }
1232
+ if (line) lines.push(line);
1233
+ let rest = word;
1234
+ while (rest.length > 1 && context.measureText(rest).width > maxWidth) {
1235
+ let end = rest.length - 1;
1236
+ while (end > 1 && context.measureText(rest.slice(0, end)).width > maxWidth) end--;
1237
+ lines.push(rest.slice(0, end));
1238
+ runsOn.add(lines.length - 1);
1239
+ rest = rest.slice(end);
1240
+ }
1241
+ line = rest;
1242
+ }
1243
+ if (line) lines.push(line);
1244
+ if (lines.length <= maxLines) return lines;
1245
+ const kept = lines.slice(0, maxLines);
1246
+ const joiner = runsOn.has(maxLines - 1) ? "" : " ";
1247
+ kept[maxLines - 1] = fitText(context, `${kept[maxLines - 1]}${joiner}${lines[maxLines]}`, maxWidth, true);
1248
+ return kept;
1249
+ }
1250
+ function fitText(context, text, maxWidth, cut = false) {
1251
+ if (!cut && context.measureText(text).width <= maxWidth) return text;
1252
+ let end = text.length;
1253
+ while (end > 0 && context.measureText(`${text.slice(0, end).trimEnd()}\u2026`).width > maxWidth) end--;
1254
+ return `${text.slice(0, end).trimEnd()}\u2026`;
1255
+ }
1256
+ async function saveCanvas(canvas, fileName) {
1257
+ const blob = await canvasBlob(canvas);
1258
+ const document = canvas.ownerDocument;
1259
+ const url = URL.createObjectURL(blob);
1260
+ const link = document.createElement("a");
1261
+ link.href = url;
1262
+ link.download = fileName;
1263
+ link.style.display = "none";
1264
+ document.body.appendChild(link);
1265
+ link.click();
1266
+ link.remove();
1267
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
1268
+ }
1269
+ function canvasBlob(canvas) {
1270
+ return new Promise(
1271
+ (resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error("The map's image couldn't be made.")), "image/png")
1272
+ );
1273
+ }
1274
+ var PRINT_SHEET_CSS = `
1275
+ .ppp-map-print-sheet { color-scheme: light; color: CanvasText; font-size: 10pt; line-height: 1.35; print-color-adjust: exact; -webkit-print-color-adjust: exact; }
1276
+ .ppp-map-print-title { margin: 0 0 8pt; font-size: 15pt; font-weight: 600; }
1277
+ .ppp-map-print-image { display: block; width: 100%; height: auto; break-inside: avoid; }
1278
+ .ppp-map-print-entries { list-style: none; margin: 10pt 0 0; padding: 0; columns: 2 220pt; column-gap: 18pt; }
1279
+ .ppp-map-print-entry { display: flex; gap: 8pt; padding: 4pt 0; border-bottom: 0.5pt solid GrayText; break-inside: avoid; }
1280
+ .ppp-map-print-badge {
1281
+ flex: none; display: flex; align-items: center; justify-content: center; box-sizing: border-box;
1282
+ min-width: 18pt; height: 18pt; padding: 0 4pt; border-radius: 9pt; font-size: 8pt; font-weight: 700;
1283
+ }
1284
+ .ppp-map-print-bullet { flex: none; width: 8pt; height: 8pt; margin-top: 3pt; border-radius: 4pt; }
1285
+ .ppp-map-print-name { font-weight: 600; overflow-wrap: anywhere; }
1286
+ .ppp-map-print-details { display: grid; grid-template-columns: auto 1fr; column-gap: 6pt; margin: 1pt 0 0; font-size: 8.5pt; }
1287
+ .ppp-map-print-details dt { color: GrayText; }
1288
+ .ppp-map-print-details dd { margin: 0; overflow-wrap: anywhere; }
1289
+ `;
1290
+ function titleElement(document, sheet) {
1291
+ if (!sheet.title?.trim()) return null;
1292
+ const heading = document.createElement("h1");
1293
+ heading.className = "ppp-map-print-title";
1294
+ heading.textContent = sheet.title.trim();
1295
+ return heading;
1296
+ }
1297
+ function entriesElement(document, sheet) {
1298
+ if (!sheet.entries?.length) return null;
1299
+ const list = document.createElement("ol");
1300
+ list.className = "ppp-map-print-entries";
1301
+ for (const entry of sheet.entries) {
1302
+ const item = document.createElement("li");
1303
+ item.className = "ppp-map-print-entry";
1304
+ const badge = document.createElement("span");
1305
+ badge.style.backgroundColor = entry.color || sheet.badgeColor;
1306
+ if (entry.label) {
1307
+ badge.className = "ppp-map-print-badge";
1308
+ badge.style.color = sheet.onColor;
1309
+ badge.textContent = entry.label;
1310
+ } else {
1311
+ badge.className = "ppp-map-print-bullet";
1312
+ }
1313
+ item.appendChild(badge);
1314
+ const body = document.createElement("div");
1315
+ const name = document.createElement("div");
1316
+ name.className = "ppp-map-print-name";
1317
+ name.textContent = entry.title;
1318
+ body.appendChild(name);
1319
+ const details = (entry.details ?? []).filter((detail) => detail.value.trim());
1320
+ if (details.length) {
1321
+ const list2 = document.createElement("dl");
1322
+ list2.className = "ppp-map-print-details";
1323
+ for (const detail of details) {
1324
+ const term = document.createElement("dt");
1325
+ term.textContent = detail.label;
1326
+ const value = document.createElement("dd");
1327
+ value.textContent = detail.value;
1328
+ list2.append(term, value);
1329
+ }
1330
+ body.appendChild(list2);
1331
+ }
1332
+ item.appendChild(body);
1333
+ list.appendChild(item);
1334
+ }
1335
+ return list;
1336
+ }
1337
+ async function printImage(ownerDocument, image, sheet) {
1338
+ for (const old of ownerDocument.querySelectorAll("iframe.ppp-map-print-frame")) removeFrame(old);
1339
+ const frame = ownerDocument.createElement("iframe");
1340
+ frame.className = "ppp-map-print-frame";
1341
+ frame.setAttribute("aria-hidden", "true");
1342
+ frame.tabIndex = -1;
1343
+ frame.style.cssText = "position: fixed; right: 0; bottom: 0; width: 0; height: 0; border: 0;";
1344
+ const url = URL.createObjectURL(image);
1345
+ frame.dataset.url = url;
1346
+ ownerDocument.body.appendChild(frame);
1347
+ const window = frame.contentWindow;
1348
+ const document = frame.contentDocument;
1349
+ if (!window || !document) {
1350
+ removeFrame(frame);
1351
+ throw new Error("The map couldn't be printed.");
1352
+ }
1353
+ document.documentElement.lang = sheet.language;
1354
+ if (ownerDocument.documentElement.dir) document.documentElement.dir = ownerDocument.documentElement.dir;
1355
+ document.title = sheet.title?.trim() || ownerDocument.title;
1356
+ const style = document.createElement("style");
1357
+ style.textContent = `@page { margin: 12mm; } body { margin: 0; font-family: ${sheet.font}; }${PRINT_SHEET_CSS}`;
1358
+ document.head.appendChild(style);
1359
+ const body = document.createElement("div");
1360
+ body.className = "ppp-map-print-sheet";
1361
+ const heading = titleElement(document, sheet);
1362
+ if (heading) body.appendChild(heading);
1363
+ const img = document.createElement("img");
1364
+ img.className = "ppp-map-print-image";
1365
+ img.alt = sheet.title?.trim() ?? "";
1366
+ img.src = url;
1367
+ body.appendChild(img);
1368
+ const entries = entriesElement(document, sheet);
1369
+ if (entries) body.appendChild(entries);
1370
+ document.body.appendChild(body);
1371
+ if (typeof img.decode === "function") await img.decode().catch(() => void 0);
1372
+ window.addEventListener("afterprint", () => removeFrame(frame), { once: true });
1373
+ window.focus();
1374
+ window.print();
1375
+ }
1376
+ function removeFrame(frame) {
1377
+ if (frame.dataset.url) URL.revokeObjectURL(frame.dataset.url);
1378
+ frame.remove();
1379
+ }
1380
+ var PAPER_WIDTH = 680;
1381
+ function printPage(target, sheet) {
1382
+ const document = target.ownerDocument;
1383
+ const window = document.defaultView;
1384
+ if (!window) throw new Error("The map couldn't be printed.");
1385
+ ensureStyleSheet(document, "ppp-map-print-styles", `@media print { ${PRINT_SHEET_CSS} }`);
1386
+ const path = [];
1387
+ for (let element = target.parentElement; element && element !== document.documentElement; element = element.parentElement) {
1388
+ element.setAttribute("data-ppp-map-print-path", "");
1389
+ path.push(element);
1390
+ }
1391
+ const { width, height } = target.getBoundingClientRect();
1392
+ const style = { width: target.style.width, height: target.style.height };
1393
+ const scale = width > PAPER_WIDTH ? PAPER_WIDTH / width : 1;
1394
+ target.style.width = `${width}px`;
1395
+ target.style.height = `${height}px`;
1396
+ target.classList.add("ppp-map-print-target");
1397
+ const printStyle = document.createElement("style");
1398
+ printStyle.textContent = `@media print { html.ppp-map-printing .ppp-map-print-target { transform: scale(${scale}); transform-origin: top left; margin-bottom: ${height * (scale - 1)}px !important; } }`;
1399
+ document.head.appendChild(printStyle);
1400
+ const sheets = [];
1401
+ const heading = titleElement(document, sheet);
1402
+ if (heading) sheets.push(wrapSheet(document, heading, (element) => target.before(element)));
1403
+ const entries = entriesElement(document, sheet);
1404
+ if (entries) sheets.push(wrapSheet(document, entries, (element) => target.after(element)));
1405
+ document.documentElement.classList.add("ppp-map-printing");
1406
+ const restore = () => {
1407
+ document.documentElement.classList.remove("ppp-map-printing");
1408
+ for (const element of path) element.removeAttribute("data-ppp-map-print-path");
1409
+ for (const element of sheets) element.remove();
1410
+ printStyle.remove();
1411
+ target.classList.remove("ppp-map-print-target");
1412
+ target.style.width = style.width;
1413
+ target.style.height = style.height;
1414
+ };
1415
+ window.addEventListener("afterprint", restore, { once: true });
1416
+ window.print();
1417
+ }
1418
+ function wrapSheet(document, content, place) {
1419
+ const sheet = document.createElement("div");
1420
+ sheet.className = "ppp-map-print-sheet";
1421
+ sheet.setAttribute("aria-hidden", "true");
1422
+ sheet.appendChild(content);
1423
+ place(sheet);
1424
+ return sheet;
1425
+ }
1426
+
1427
+ // src/core/map-controller.ts
1428
+ var WORLD_CAMERA = { center: { latitude: 20, longitude: 0 }, zoom: 1.5 };
1429
+ var FIT_MAX_ZOOM = 15;
1430
+ var FIT_PADDING = 48;
1431
+ var PLACE_ZOOM = 16;
1432
+ var PLACE_MAX_ZOOM = 17;
1433
+ var IMAGE_PIXEL_RATIO = 2;
1434
+ var IMAGE_MAX_SIZE = 4096;
1435
+ var PLACE_COLOR = "var(--colorPaletteRedBackground3, #d13438)";
1436
+ var MARKER_POPUP_OFFSET = 38;
1437
+ var SHAPE_POPUP_OFFSET = 8;
1438
+ var MapController = class _MapController {
1439
+ constructor(container, surfaceElement, options, events) {
1440
+ this.container = container;
1441
+ this.surfaceElement = surfaceElement;
1442
+ this.options = options;
1443
+ this.events = events;
1444
+ this.dark = options.usePortalTheme && isDarkTheme(container);
1445
+ this.stopWatching = watchTheme(container, () => this.themeChanged());
1446
+ this.resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.resized());
1447
+ this.resizeObserver?.observe(container);
1448
+ this.popupObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.requestKeepInView());
1449
+ }
1450
+ container;
1451
+ surfaceElement;
1452
+ options;
1453
+ events;
1454
+ surface = null;
1455
+ markers = null;
1456
+ items = [];
1457
+ shapes = [];
1458
+ popup = null;
1459
+ dark = false;
1460
+ disposed = false;
1461
+ // A fit asked for while the map had no size — drawn in a tab not yet shown — made when it gets one.
1462
+ fitPending = false;
1463
+ fitPendingIds;
1464
+ // Counts draws, so one overtaken by another — the theme flipping again while Google's map is
1465
+ // being made again — throws its map away rather than leaving two.
1466
+ drawGeneration = 0;
1467
+ // The shapes and their colors as last handed to the surface: a theme check that changed neither
1468
+ // costs nothing.
1469
+ drawnShapes = null;
1470
+ stopWatching;
1471
+ resizeObserver;
1472
+ // The open popup's size: content that comes after it opens — figures replacing a spinner — can
1473
+ // grow it past the map's edge, so it is kept in view again whenever it changes.
1474
+ popupObserver;
1475
+ // Whether the map is moving — a pan keeping the popup in view among others — and whether the popup
1476
+ // is to be checked again once it stops: measured mid-move, it would be panned for twice over.
1477
+ moving = false;
1478
+ keepInViewPending = false;
1479
+ search = null;
1480
+ // The geocoder the box's suggestions came from: one drawn in its place can't go to them.
1481
+ searchGeocoder = null;
1482
+ // The place the address search went to, pinned until the search is cleared — or the place of the
1483
+ // address the options name, until they name none.
1484
+ place = null;
1485
+ // Finding the address the options name; given up when it changes.
1486
+ addressRequest = null;
1487
+ // Where the address the options name was found: pinned again once a search's place in its stead
1488
+ // is cleared.
1489
+ addressPlace = null;
1490
+ /**
1491
+ * Draws a map into `container`, which it fills: an element of the host's, positioned and sized.
1492
+ * Resolves once the provider's map is up; a provider that fails reports through `events.error`
1493
+ * and leaves the container empty.
1494
+ */
1495
+ static async create(container, options, items, events) {
1496
+ ensureMapStyles(container.ownerDocument);
1497
+ const surfaceElement = container.ownerDocument.createElement("div");
1498
+ surfaceElement.className = "ppp-map-surface";
1499
+ container.prepend(surfaceElement);
1500
+ const controller = new _MapController(container, surfaceElement, options, events);
1501
+ controller.items = [...items];
1502
+ await controller.draw(options.camera ?? null);
1503
+ void controller.showAddress(false);
1504
+ return controller;
1505
+ }
1506
+ /** The provider's own map object, once drawn. */
1507
+ get native() {
1508
+ return this.surface?.native ?? null;
1509
+ }
1510
+ /** Replaces the items. `fit` moves the view to show them all. */
1511
+ setItems(items, fit) {
1512
+ this.items = [...items];
1513
+ this.applyItems();
1514
+ if (fit) this.fitToItems();
1515
+ }
1516
+ /** Applies changed options. A new provider draws the map again; anything else changes it in place. */
1517
+ async setOptions(options) {
1518
+ const previous = this.options;
1519
+ this.options = options;
1520
+ const addressChanged = (previous.address?.trim() ?? "") !== (options.address?.trim() ?? "");
1521
+ if (JSON.stringify(previous.provider ?? null) !== JSON.stringify(options.provider ?? null) || JSON.stringify(previous.nativeOptions ?? null) !== JSON.stringify(options.nativeOptions ?? null) || previous.language !== options.language) {
1522
+ const searching = this.addressRequest !== null;
1523
+ this.addressRequest?.abort();
1524
+ this.addressRequest = null;
1525
+ await this.redraw();
1526
+ if (addressChanged || searching) void this.showAddress(true);
1527
+ return;
1528
+ }
1529
+ if (addressChanged) void this.showAddress(true);
1530
+ if (previous.cluster !== options.cluster) this.applyItems();
1531
+ this.applySearch();
1532
+ if (options.gestureHelp && JSON.stringify(previous.gestureHelp ?? null) !== JSON.stringify(options.gestureHelp)) {
1533
+ this.surface?.setGestureHelp?.(options.gestureHelp);
1534
+ }
1535
+ const dark = options.usePortalTheme && isDarkTheme(this.container);
1536
+ if (previous.style !== options.style || dark !== this.dark) {
1537
+ this.dark = dark;
1538
+ if (this.surface && !this.surface.setStyle(options.style, dark)) await this.redraw();
1539
+ }
1540
+ }
1541
+ getView() {
1542
+ return this.surface?.getView() ?? null;
1543
+ }
1544
+ setCamera(camera, animate = true) {
1545
+ this.fitPending = false;
1546
+ this.surface?.setCamera(camera, animate);
1547
+ }
1548
+ /**
1549
+ * Moves the view to show every item — or, given `ids` (not empty), the items of those ids — or to
1550
+ * the whole world without any.
1551
+ */
1552
+ fitToItems(ids) {
1553
+ if (!this.surface) return;
1554
+ if (!this.container.clientWidth || !this.container.clientHeight) {
1555
+ this.fitPending = true;
1556
+ this.fitPendingIds = ids;
1557
+ return;
1558
+ }
1559
+ this.fitPending = false;
1560
+ this.fitPendingIds = void 0;
1561
+ const wanted = ids?.length ? new Set(ids) : null;
1562
+ const bounds = boundsOfItems(wanted ? this.items.filter((item) => wanted.has(item.id)) : this.items);
1563
+ if (!bounds) {
1564
+ this.surface.setCamera(WORLD_CAMERA, false);
1565
+ } else if (bounds.north === bounds.south && bounds.east === bounds.west) {
1566
+ this.surface.setCamera({ center: { latitude: bounds.north, longitude: bounds.east }, zoom: FIT_MAX_ZOOM }, false);
1567
+ } else {
1568
+ this.surface.fitBounds(bounds, FIT_PADDING, FIT_MAX_ZOOM);
1569
+ }
1570
+ }
1571
+ /** The place the address search last went to; null before one, or once it is cleared. */
1572
+ get foundPlace() {
1573
+ return this.place?.place ?? null;
1574
+ }
1575
+ /**
1576
+ * Finds `text` with the provider's search and goes there, as the address search does with Enter:
1577
+ * the first place found, pinned, shown in the search box and reported as the box reports one. Null
1578
+ * when nothing is found, or the provider has no search.
1579
+ */
1580
+ async goToAddress(text, signal) {
1581
+ const place = await this.findAddress(text, signal);
1582
+ if (!place || this.disposed) return place;
1583
+ this.showPlace(place, { animate: true, fromAddress: false });
1584
+ this.search?.showChosen(place.label);
1585
+ this.events.addressSelected?.(place);
1586
+ return place;
1587
+ }
1588
+ /** Takes the place found off the map. */
1589
+ clearPlace() {
1590
+ this.place?.overlay?.remove();
1591
+ this.place = null;
1592
+ }
1593
+ /** Empties the address search and takes the place it found off the map, as its clear button does. */
1594
+ clearAddress() {
1595
+ if (this.search) {
1596
+ this.search.clear();
1597
+ return;
1598
+ }
1599
+ this.clearSearchPlace();
1600
+ }
1601
+ // Takes the place the address search went to off the map, and says so. The address the options
1602
+ // name is the page's, not the search's: it stays.
1603
+ clearSearchPlace() {
1604
+ if (!this.place || this.place.fromAddress) return;
1605
+ this.clearPlace();
1606
+ if (this.addressPlace) this.showPlace(this.addressPlace, { move: false, fromAddress: true });
1607
+ this.events.addressSelected?.(null);
1608
+ }
1609
+ /**
1610
+ * Whether the map can be made into an image — saved, or printed as one: its provider's map can be
1611
+ * copied. Google's can't; it is printed as it is on the page.
1612
+ */
1613
+ get canSaveImage() {
1614
+ return typeof this.surface?.capture === "function";
1615
+ }
1616
+ /**
1617
+ * The map as an image, as it shows now: the provider's map once its tiles are in, the pins and
1618
+ * clusters over it, the place found, `legend` in the bottom left corner, the provider's attribution
1619
+ * in the bottom right and, while a popup is open, `popup` where it is. Null before the map is
1620
+ * drawn, or when its provider's map can't be copied (`canSaveImage`).
1621
+ */
1622
+ async toImage(legend, popup) {
1623
+ const surface = this.surface;
1624
+ if (!surface?.capture || !this.markers) return null;
1625
+ const width = this.container.clientWidth;
1626
+ const height = this.container.clientHeight;
1627
+ if (!width || !height) return null;
1628
+ const pixelRatio = Math.min(
1629
+ Math.max(IMAGE_PIXEL_RATIO, globalThis.devicePixelRatio || 1),
1630
+ IMAGE_MAX_SIZE / Math.max(width, height)
1631
+ );
1632
+ const map = await surface.capture(pixelRatio);
1633
+ if (this.disposed || this.surface !== surface || !this.markers) return null;
1634
+ const color = this.colorResolver();
1635
+ const colors = this.imageColors(color);
1636
+ return drawMapImage({
1637
+ map,
1638
+ width,
1639
+ height,
1640
+ marks: this.markers.marks().map((mark) => ({
1641
+ ...mark,
1642
+ color: color(mark.color) ?? color(DEFAULT_ITEM_COLOR) ?? "#0f6cbd",
1643
+ point: surface.project(mark.position)
1644
+ })),
1645
+ place: this.place?.overlay && isValidPosition(this.place.place.position) ? surface.project(this.place.place.position) : null,
1646
+ legend: (legend ?? []).map((entry) => ({ label: entry.label, color: color(entry.color) ?? colors.foreground })),
1647
+ attribution: surface.attributionText?.() ?? "",
1648
+ popup: await this.printedPopup(popup, width, height, pixelRatio),
1649
+ colors,
1650
+ font: this.font()
1651
+ });
1652
+ }
1653
+ /**
1654
+ * Saves the map's image (`toImage`) as a PNG file of that name. False when the map can't be made
1655
+ * into one.
1656
+ */
1657
+ async saveImage(fileName = "map", legend, popup) {
1658
+ const image = await this.toImage(legend, popup);
1659
+ if (!image) return false;
1660
+ await saveCanvas(image, pngFileName(fileName));
1661
+ return true;
1662
+ }
1663
+ /**
1664
+ * Prints the map with `options.title` above it and `options.entries` listed under it: its image
1665
+ * (`toImage`), with the legend, in a frame of its own — or, for a provider whose map can't be
1666
+ * copied, the map as it is on the page, the rest of the page left out.
1667
+ */
1668
+ async print(options = {}) {
1669
+ if (!this.surface) return;
1670
+ const color = this.colorResolver();
1671
+ const sheet = {
1672
+ ...options,
1673
+ ...options.entries && {
1674
+ entries: options.entries.map((entry) => ({ ...entry, ...entry.color && { color: color(entry.color) ?? entry.color } }))
1675
+ },
1676
+ language: this.options.language,
1677
+ font: this.font(),
1678
+ onColor: this.imageColors(color).onColor,
1679
+ badgeColor: color(DEFAULT_ITEM_COLOR) ?? "#0f6cbd"
1680
+ };
1681
+ const image = await this.toImage(options.legend, options.popup);
1682
+ if (this.disposed) return;
1683
+ if (image) {
1684
+ await printImage(this.container.ownerDocument, await canvasBlob(image), sheet);
1685
+ } else {
1686
+ printPage(this.container.parentElement ?? this.container, sheet);
1687
+ }
1688
+ }
1689
+ // The popup open on the map, as the image draws it: none once its point is panned off the map,
1690
+ // where the page shows none either. A picture of it as the page shows it, where the browser can make
1691
+ // one; else a card of what it says — the page's own content read from the popup as it is now.
1692
+ async printedPopup(content, width, height, ratio) {
1693
+ const open = this.popup;
1694
+ if (!content || !open || !this.surface) return null;
1695
+ const point = this.surface.project(open.position);
1696
+ if (point.x < 0 || point.y < 0 || point.x > width || point.y > height) return null;
1697
+ const card = open.element.querySelector(".ppp-map-popup") ?? open.element;
1698
+ const snapshot = await snapshotElement(card, this.snapshotFrame(), ratio);
1699
+ if (this.disposed || this.popup !== open) return null;
1700
+ const shown = content.fromPage ? readPopupContent(open.element) : content;
1701
+ return { content: shown, point, offset: open.offset, ...snapshot && { snapshot } };
1702
+ }
1703
+ // Where the map's own pixels start on the page — inside its border — and how much the page scales
1704
+ // them: the image is drawn in the map's own pixels, as the pins are projected.
1705
+ snapshotFrame() {
1706
+ const box = this.container.getBoundingClientRect();
1707
+ const scale = this.container.offsetWidth ? box.width / this.container.offsetWidth : 1;
1708
+ return { left: box.left + this.container.clientLeft * scale, top: box.top + this.container.clientTop * scale, scale };
1709
+ }
1710
+ // Each color resolved where the map is, once however many marks share it.
1711
+ colorResolver() {
1712
+ const resolved = /* @__PURE__ */ new Map();
1713
+ return (value) => {
1714
+ if (!resolved.has(value)) resolved.set(value, resolveColor(value, this.container));
1715
+ return resolved.get(value);
1716
+ };
1717
+ }
1718
+ imageColors(color) {
1719
+ return {
1720
+ background: color("var(--colorNeutralBackground1, #ffffff)") ?? "#ffffff",
1721
+ foreground: color("var(--colorNeutralForeground1, #242424)") ?? "#242424",
1722
+ secondary: color("var(--colorNeutralForeground3, #616161)") ?? "#616161",
1723
+ stroke: color("var(--colorNeutralStroke2, #e0e0e0)") ?? "#e0e0e0",
1724
+ onColor: color("var(--colorNeutralForegroundOnBrand, #ffffff)") ?? "#ffffff",
1725
+ place: color(PLACE_COLOR) ?? "#d13438"
1726
+ };
1727
+ }
1728
+ font() {
1729
+ return getComputedStyle(this.container).fontFamily || "'Segoe UI', sans-serif";
1730
+ }
1731
+ /** Marks the marker of that id as the one whose popup is open; null for none. */
1732
+ select(id) {
1733
+ this.markers?.setSelected(id);
1734
+ }
1735
+ /**
1736
+ * Keeps the host's popup element above `position` as the map moves — above the pin for a marker,
1737
+ * just above the point for anything else — and pans the map so it is all in view.
1738
+ */
1739
+ showPopup(element, position, kind) {
1740
+ if (!isValidPosition(position)) return;
1741
+ this.popup = { element, position, offset: kind === "marker" ? MARKER_POPUP_OFFSET : SHAPE_POPUP_OFFSET };
1742
+ element.hidden = false;
1743
+ this.placePopup();
1744
+ this.keepInViewPending = false;
1745
+ this.popupObserver?.disconnect();
1746
+ if (this.popupObserver) {
1747
+ this.popupObserver.observe(element);
1748
+ } else {
1749
+ requestAnimationFrame(() => {
1750
+ if (this.popup?.element === element) this.keepPopupInView();
1751
+ });
1752
+ }
1753
+ }
1754
+ hidePopup() {
1755
+ if (this.popup) this.popup.element.hidden = true;
1756
+ this.popup = null;
1757
+ this.popupObserver?.disconnect();
1758
+ }
1759
+ /** Puts keyboard focus on the marker of that id, when it is shown on its own. */
1760
+ focusMarker(id) {
1761
+ this.markers?.elementOf(id)?.focus();
1762
+ }
1763
+ dispose() {
1764
+ this.disposed = true;
1765
+ this.addressRequest?.abort();
1766
+ this.addressRequest = null;
1767
+ this.search?.dispose();
1768
+ this.search = null;
1769
+ this.clearPlace();
1770
+ this.stopWatching();
1771
+ this.resizeObserver?.disconnect();
1772
+ this.popupObserver?.disconnect();
1773
+ this.markers?.dispose();
1774
+ this.surface?.dispose();
1775
+ this.surface = null;
1776
+ this.surfaceElement.remove();
1777
+ }
1778
+ async draw(camera) {
1779
+ const generation = ++this.drawGeneration;
1780
+ const overtaken = () => this.disposed || generation !== this.drawGeneration;
1781
+ let settings;
1782
+ let provider;
1783
+ try {
1784
+ settings = await resolveProviderSettings(this.options.provider, this.options.portalSettings);
1785
+ if (overtaken()) return;
1786
+ provider = await findMapProvider(settings.name);
1787
+ } catch (error) {
1788
+ if (!overtaken()) this.events.error?.("failed", error instanceof Error ? error.message : String(error));
1789
+ return;
1790
+ }
1791
+ if (overtaken()) return;
1792
+ if (!provider) {
1793
+ this.events.error?.("unknown-provider", settings.name);
1794
+ return;
1795
+ }
1796
+ const events = {
1797
+ click: (position) => this.events.mapClick?.({ position }),
1798
+ shapeClick: (id, position) => this.events.itemClick?.({ id, position }),
1799
+ viewChanging: () => {
1800
+ this.moving = true;
1801
+ this.placePopup();
1802
+ },
1803
+ redrawn: () => this.placePopup(),
1804
+ viewChanged: () => {
1805
+ this.moving = false;
1806
+ this.markers?.refresh();
1807
+ this.placePopup();
1808
+ if (this.keepInViewPending) {
1809
+ this.keepInViewPending = false;
1810
+ this.keepPopupInView();
1811
+ }
1812
+ const view = this.surface?.getView();
1813
+ if (view) this.events.viewChanged?.(view);
1814
+ },
1815
+ error: (message) => this.events.error?.("failed", message)
1816
+ };
1817
+ const element = this.container.ownerDocument.createElement("div");
1818
+ element.style.width = "100%";
1819
+ element.style.height = "100%";
1820
+ this.surfaceElement.replaceChildren(element);
1821
+ let surface;
1822
+ try {
1823
+ surface = await provider.create({
1824
+ container: element,
1825
+ settings,
1826
+ style: this.options.style,
1827
+ dark: this.dark,
1828
+ language: this.options.language,
1829
+ camera: camera ?? WORLD_CAMERA,
1830
+ ...this.options.nativeOptions ? { nativeOptions: this.options.nativeOptions } : {},
1831
+ ...this.options.gestureHelp ? { gestureHelp: this.options.gestureHelp } : {},
1832
+ events
1833
+ });
1834
+ } catch (error) {
1835
+ if (!overtaken()) this.events.error?.("failed", error instanceof Error ? error.message : String(error));
1836
+ return;
1837
+ }
1838
+ if (overtaken()) {
1839
+ surface.dispose();
1840
+ return;
1841
+ }
1842
+ this.surface = surface;
1843
+ this.drawnShapes = null;
1844
+ if (this.place) this.showPlace(this.place.place, { move: false, fromAddress: this.place.fromAddress });
1845
+ this.applySearch();
1846
+ if (this.markers) {
1847
+ this.markers.moveTo(surface);
1848
+ } else {
1849
+ this.markers = new MarkerLayer(surface, this.container.ownerDocument, {
1850
+ clusterLabel: (count) => this.options.clusterLabel(count),
1851
+ onMarkerClick: (marker) => this.events.itemClick?.({ id: marker.id, position: marker.position }),
1852
+ onClusterClick: (position, zoom, markers) => this.clusterClicked(position, zoom, markers)
1853
+ });
1854
+ }
1855
+ this.applyItems();
1856
+ if (!camera) this.fitToItems();
1857
+ }
1858
+ // The address search: shown while asked for and the provider has a search, its words kept current.
1859
+ // Taken away, it takes its place off the map with it; while the map is being drawn again it waits,
1860
+ // its text kept, for the map it searches.
1861
+ applySearch() {
1862
+ if (!this.surface) return;
1863
+ const options = this.options.addressSearch;
1864
+ const geocoder = this.surface.geocoder ?? null;
1865
+ if (!options || !geocoder) {
1866
+ this.search?.dispose();
1867
+ this.search = null;
1868
+ this.searchGeocoder = null;
1869
+ this.clearSearchPlace();
1870
+ return;
1871
+ }
1872
+ if (this.search) {
1873
+ this.search.setLabels(options);
1874
+ if (geocoder !== this.searchGeocoder) this.search.forgetSuggestions();
1875
+ this.searchGeocoder = geocoder;
1876
+ return;
1877
+ }
1878
+ this.searchGeocoder = geocoder;
1879
+ this.search = new AddressSearchBox(
1880
+ this.container.ownerDocument,
1881
+ {
1882
+ suggestsAsYouType: () => this.surface?.geocoder?.suggestsAsYouType ?? false,
1883
+ suggest: (text, signal) => this.suggestAddresses(text, signal),
1884
+ choose: (suggestion) => this.chooseAddress(suggestion),
1885
+ clear: () => this.clearSearchPlace()
1886
+ },
1887
+ options
1888
+ );
1889
+ this.container.append(this.search.element);
1890
+ }
1891
+ addressQuery(view) {
1892
+ return { view, countries: this.options.addressSearch?.countries ?? [], language: this.options.language };
1893
+ }
1894
+ async suggestAddresses(text, signal) {
1895
+ const geocoder = this.surface?.geocoder;
1896
+ const view = this.surface?.getView();
1897
+ if (!geocoder || !view) return [];
1898
+ return geocoder.suggest({ ...this.addressQuery(view), text, signal });
1899
+ }
1900
+ async chooseAddress(suggestion) {
1901
+ const place = await this.surface?.geocoder?.resolve(suggestion);
1902
+ if (!place || this.disposed) return false;
1903
+ this.showPlace(place, { animate: true, fromAddress: false });
1904
+ this.events.addressSelected?.(place);
1905
+ return true;
1906
+ }
1907
+ // The first place the provider's search finds for `text`. Null when it finds none, or there is
1908
+ // no search.
1909
+ async findAddress(text, signal) {
1910
+ const geocoder = this.surface?.geocoder;
1911
+ if (!geocoder || !text.trim()) return null;
1912
+ const view = this.surface?.getView() ?? {
1913
+ ...WORLD_CAMERA,
1914
+ bounds: approximateBounds(WORLD_CAMERA.center, WORLD_CAMERA.zoom, this.container.clientWidth || 800, this.container.clientHeight || 600)
1915
+ };
1916
+ const [first] = await geocoder.suggest({ ...this.addressQuery(view), text: text.trim(), ...signal ? { signal } : {} });
1917
+ return first ? await geocoder.resolve(first, signal) : null;
1918
+ }
1919
+ // Finds the address the options name and pins it, the search before it given up; the place it
1920
+ // pinned goes when the address does, or when the new one isn't found. `animate` moves the view
1921
+ // there gradually — not as the map is first drawn, when it starts out looking at the world.
1922
+ async showAddress(animate) {
1923
+ this.addressRequest?.abort();
1924
+ this.addressRequest = null;
1925
+ if (!this.surface || this.disposed) return;
1926
+ const text = this.options.address?.trim();
1927
+ this.addressPlace = null;
1928
+ if (!text) {
1929
+ if (this.place?.fromAddress) this.clearPlace();
1930
+ return;
1931
+ }
1932
+ const request = new AbortController();
1933
+ this.addressRequest = request;
1934
+ this.events.addressStatus?.("searching");
1935
+ let status;
1936
+ try {
1937
+ const place = this.surface.geocoder ? await this.findAddress(text, request.signal) : null;
1938
+ if (request.signal.aborted || this.disposed) return;
1939
+ if (place) {
1940
+ this.addressPlace = place;
1941
+ this.showPlace(place, { animate, fromAddress: true });
1942
+ } else if (this.place?.fromAddress) {
1943
+ this.clearPlace();
1944
+ }
1945
+ status = place ? "found" : this.surface?.geocoder ? "not-found" : "failed";
1946
+ } catch {
1947
+ if (request.signal.aborted || this.disposed) return;
1948
+ if (this.place?.fromAddress) this.clearPlace();
1949
+ status = "failed";
1950
+ } finally {
1951
+ if (this.addressRequest === request) this.addressRequest = null;
1952
+ }
1953
+ this.events.addressStatus?.(status);
1954
+ }
1955
+ // Pins the place found — the one place, replacing any before it — and, unless `move` is false,
1956
+ // brings it into view: its area fitted, or the streets around a point.
1957
+ showPlace(place, { move = true, animate = false, fromAddress }) {
1958
+ this.place?.overlay?.remove();
1959
+ this.place = { place, overlay: null, fromAddress };
1960
+ if (!this.surface || !isValidPosition(place.position)) return;
1961
+ const element = this.container.ownerDocument.createElement("div");
1962
+ element.className = "ppp-map-place";
1963
+ element.innerHTML = PIN_SVG;
1964
+ element.setAttribute("role", "img");
1965
+ element.setAttribute("aria-label", place.label);
1966
+ element.dataset.pppTooltip = place.label;
1967
+ this.place.overlay = this.surface.addOverlay(element, place.position, "bottom");
1968
+ if (!move) return;
1969
+ this.fitPending = false;
1970
+ if (place.bounds) this.surface.fitBounds(place.bounds, FIT_PADDING, PLACE_MAX_ZOOM);
1971
+ else this.surface.setCamera({ center: place.position, zoom: PLACE_ZOOM }, animate);
1972
+ }
1973
+ // The same provider's map drawn again where it was looking — for a change it can't make in place.
1974
+ async redraw() {
1975
+ const camera = this.surface?.getView() ?? this.options.camera ?? null;
1976
+ this.place?.overlay?.remove();
1977
+ if (this.place) this.place.overlay = null;
1978
+ this.surface?.dispose();
1979
+ this.surface = null;
1980
+ this.dark = this.options.usePortalTheme && isDarkTheme(this.container);
1981
+ await this.draw(camera ? { center: camera.center, zoom: camera.zoom } : null);
1982
+ }
1983
+ applyItems() {
1984
+ if (!this.surface || !this.markers) return;
1985
+ const markers = [];
1986
+ this.shapes = [];
1987
+ for (const item of this.items) {
1988
+ if (item.kind === "marker") markers.push(item);
1989
+ else this.shapes.push(item);
1990
+ }
1991
+ this.markers.setMarkers(markers, this.options.cluster);
1992
+ this.drawnShapes = null;
1993
+ this.drawShapes();
1994
+ }
1995
+ // A surface draws on a canvas or with the provider's own shapes, which take a color, not a CSS
1996
+ // variable: each is resolved where the map is, and again when the theme changes.
1997
+ drawShapes() {
1998
+ if (!this.surface) return;
1999
+ const resolved = /* @__PURE__ */ new Map();
2000
+ const resolve = (color) => {
2001
+ if (!resolved.has(color)) resolved.set(color, resolveColor(color, this.container));
2002
+ return resolved.get(color);
2003
+ };
2004
+ const fallback = resolve(DEFAULT_ITEM_COLOR) ?? "#0f6cbd";
2005
+ const colors = this.shapes.map((shape) => shape.color && resolve(shape.color) || fallback);
2006
+ const key = colors.join("|");
2007
+ if (this.drawnShapes?.shapes === this.shapes && this.drawnShapes.colors === key) return;
2008
+ this.drawnShapes = { shapes: this.shapes, colors: key };
2009
+ this.surface.setShapes(this.shapes.map((shape, index) => ({ ...shape, color: colors[index] })));
2010
+ }
2011
+ clusterClicked(position, zoom, markers) {
2012
+ if (zoom !== null && this.surface) {
2013
+ this.surface.setCamera({ center: position, zoom }, true);
2014
+ return;
2015
+ }
2016
+ this.events.clusterClick?.({ ids: markers.map((marker) => marker.id), position });
2017
+ }
2018
+ resized() {
2019
+ this.surface?.resize();
2020
+ if (this.fitPending) this.fitToItems(this.fitPendingIds);
2021
+ }
2022
+ themeChanged() {
2023
+ if (this.disposed) return;
2024
+ this.drawShapes();
2025
+ const dark = this.options.usePortalTheme && isDarkTheme(this.container);
2026
+ if (dark === this.dark || !this.surface) return;
2027
+ this.dark = dark;
2028
+ if (!this.surface.setStyle(this.options.style, dark)) void this.redraw();
2029
+ }
2030
+ placePopup() {
2031
+ if (!this.popup || !this.surface) return;
2032
+ const point = this.surface.project(this.popup.position);
2033
+ const style = this.popup.element.style;
2034
+ style.left = `${point.x}px`;
2035
+ style.top = `${point.y - this.popup.offset}px`;
2036
+ style.transform = "translate(-50%, -100%)";
2037
+ }
2038
+ // Keeps the popup in view now, or once the map stops moving.
2039
+ requestKeepInView() {
2040
+ if (this.moving) this.keepInViewPending = true;
2041
+ else this.keepPopupInView();
2042
+ }
2043
+ keepPopupInView() {
2044
+ if (!this.popup || !this.surface) return;
2045
+ const margin = 12;
2046
+ const popup = this.popup.element.getBoundingClientRect();
2047
+ const map = this.container.getBoundingClientRect();
2048
+ let x = 0;
2049
+ let y = 0;
2050
+ if (popup.top < map.top + margin) y = popup.top - map.top - margin;
2051
+ if (popup.left < map.left + margin) x = popup.left - map.left - margin;
2052
+ else if (popup.right > map.right - margin) x = popup.right - map.right + margin;
2053
+ if (x !== 0 || y !== 0) this.surface.panBy(x, y);
2054
+ }
2055
+ };
2056
+ function pngFileName(name) {
2057
+ const cleaned = name.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim().replace(/\.png$/i, "") || "map";
2058
+ return `${cleaned}.png`;
2059
+ }
2060
+
2061
+ // src/core/near.ts
2062
+ var DistanceUnits = { Miles: 0, Kilometers: 1 };
2063
+ var DEFAULT_NEAR_DISTANCES = Object.freeze([1, 5, 10, 25, 50, 100]);
2064
+ var DEFAULT_NEAR_DISTANCE = 25;
2065
+ function metersPer(unit) {
2066
+ return unit === DistanceUnits.Kilometers ? 1e3 : 1609.344;
2067
+ }
2068
+ function formatDistance(value, language) {
2069
+ return value.toLocaleString(language, { maximumFractionDigits: value < 10 ? 1 : 0 });
2070
+ }
2071
+ var WORLD_VIEW = {
2072
+ center: { latitude: 0, longitude: 0 },
2073
+ zoom: 1,
2074
+ bounds: { north: 85, south: -85, east: 180, west: -180 }
2075
+ };
2076
+ async function placeFinder(settings, language, document) {
2077
+ switch (settings.name) {
2078
+ case "azureMaps": {
2079
+ const getToken = settings.getToken;
2080
+ if (!getToken) return null;
2081
+ let token = null;
2082
+ const signIn = async () => {
2083
+ const expiresOn = token ? Date.parse(token.expiresOn) : Number.NaN;
2084
+ if (!token || Number.isFinite(expiresOn) && expiresOn - Date.now() < 5 * 6e4) token = await getToken();
2085
+ };
2086
+ const search = azureMapsGeocoder(AZURE_MAPS_ENDPOINT, () => azureMapsHeaders(token), settings.view);
2087
+ return {
2088
+ suggestsAsYouType: search.suggestsAsYouType,
2089
+ suggest: async (query) => {
2090
+ await signIn();
2091
+ return search.suggest(query);
2092
+ },
2093
+ resolve: async (suggestion, signal) => {
2094
+ await signIn();
2095
+ return search.resolve(suggestion, signal);
2096
+ }
2097
+ };
2098
+ }
2099
+ case "esri":
2100
+ return esriGeocoder(settings.apiKey ?? "");
2101
+ case "google": {
2102
+ const { googlePlaceFinder } = await import(
2103
+ /* webpackChunkName: "google" */
2104
+ './google-6EZOUTYI.js'
2105
+ );
2106
+ return googlePlaceFinder(settings.apiKey ?? "", language, document);
2107
+ }
2108
+ case "openStreetMap":
2109
+ case "mapLibre":
2110
+ return nominatimGeocoder(settings.geocoderUrl || void 0);
2111
+ default:
2112
+ return null;
2113
+ }
2114
+ }
2115
+ function locate(navigator = globalThis.navigator) {
2116
+ return new Promise((resolve, reject) => {
2117
+ if (!navigator?.geolocation) {
2118
+ reject(new Error("The browser can't tell where it is."));
2119
+ return;
2120
+ }
2121
+ navigator.geolocation.getCurrentPosition(
2122
+ ({ coords }) => resolve({ latitude: coords.latitude, longitude: coords.longitude }),
2123
+ (error) => reject(new Error(error.message)),
2124
+ { enableHighAccuracy: false, timeout: 15e3, maximumAge: 6e4 }
2125
+ );
2126
+ });
2127
+ }
2128
+ var NEAR_CSS = `
2129
+ .ppp-near-place-search.ppp-map-search { position: relative; top: auto; left: auto; z-index: auto; width: 100%; }
2130
+ .ppp-near-place-search .ppp-map-search-field { box-shadow: none; }
2131
+ .ppp-near-place-search .ppp-map-search-panel { position: absolute; left: 0; right: 0; z-index: 1; }
2132
+ `;
2133
+ function mountPlaceSearch(container, options) {
2134
+ const document = container.ownerDocument;
2135
+ ensureMapStyles(document);
2136
+ ensureStyleSheet(document, "ppp-near-styles", NEAR_CSS);
2137
+ const { finder } = options;
2138
+ const box = new AddressSearchBox(
2139
+ document,
2140
+ {
2141
+ suggestsAsYouType: () => finder?.suggestsAsYouType ?? false,
2142
+ suggest: (text, signal) => finder ? finder.suggest({ text, view: WORLD_VIEW, countries: options.countries ?? [], language: options.language, signal }) : Promise.resolve([]),
2143
+ choose: async (suggestion) => {
2144
+ const place = finder ? await finder.resolve(suggestion) : null;
2145
+ if (!place) return false;
2146
+ options.chosen(place);
2147
+ return true;
2148
+ },
2149
+ clear: () => options.cleared?.()
2150
+ },
2151
+ options.labels
2152
+ );
2153
+ box.element.classList.add("ppp-near-place-search");
2154
+ container.appendChild(box.element);
2155
+ return box;
2156
+ }
2157
+
2158
+ // src/portal-settings-cache.ts
2159
+ var portalSettingsCaches = /* @__PURE__ */ new WeakMap();
2160
+ function portalSettingsCache(ppp) {
2161
+ let cache = portalSettingsCaches.get(ppp);
2162
+ if (!cache) portalSettingsCaches.set(ppp, cache = new PortalMapSettingsCache());
2163
+ return cache;
2164
+ }
2165
+ var useStyles = makeStyles({
2166
+ surface: {
2167
+ display: "flex",
2168
+ flexDirection: "column",
2169
+ rowGap: tokens.spacingVerticalM,
2170
+ width: `min(340px, calc(100vw - ${tokens.spacingHorizontalXXXL}))`,
2171
+ boxSizing: "border-box"
2172
+ },
2173
+ location: {
2174
+ display: "flex",
2175
+ alignItems: "center",
2176
+ columnGap: tokens.spacingHorizontalS,
2177
+ flexWrap: "wrap"
2178
+ },
2179
+ failure: {
2180
+ color: tokens.colorPaletteRedForeground1
2181
+ },
2182
+ measures: {
2183
+ display: "grid",
2184
+ gridTemplateColumns: "1fr 1fr",
2185
+ columnGap: tokens.spacingHorizontalM
2186
+ },
2187
+ dropdown: {
2188
+ minWidth: 0
2189
+ },
2190
+ actions: {
2191
+ display: "flex",
2192
+ justifyContent: "flex-end"
2193
+ }
2194
+ });
2195
+ var NEAR_CIRCLE_ID = "ppp-near-circle";
2196
+ function nearCircleId(near) {
2197
+ return `${NEAR_CIRCLE_ID}:${near.latitude},${near.longitude},${near.distance},${near.unit}`;
2198
+ }
2199
+ var NEAR_COLOR = "var(--colorPaletteRedBackground3, #d13438)";
2200
+ function nearPlaceItems(near) {
2201
+ const center = { latitude: near.latitude, longitude: near.longitude };
2202
+ return [
2203
+ { kind: "circle", id: nearCircleId(near), center, radius: near.distance * metersPer(near.unit), color: NEAR_COLOR },
2204
+ { kind: "marker", id: "ppp-near-place", position: center, title: near.label, color: NEAR_COLOR }
2205
+ ];
2206
+ }
2207
+ var finders = /* @__PURE__ */ new Map();
2208
+ function NearSearch({ value, onChange, distances, defaultDistance, defaultUnit, provider, countries, className }) {
2209
+ const styles = useStyles();
2210
+ const t = usePrefixedT("app.components.map");
2211
+ const { ppp } = usePowerPortalsPro();
2212
+ const { locale } = useLocale();
2213
+ const offered = distances && distances.length > 0 ? distances : DEFAULT_NEAR_DISTANCES;
2214
+ const startDistance = defaultDistance ?? (offered.includes(DEFAULT_NEAR_DISTANCE) ? DEFAULT_NEAR_DISTANCE : offered[0]);
2215
+ const [open, setOpen] = useState(false);
2216
+ const [distance, setDistance] = useState(value?.distance ?? startDistance);
2217
+ const [unit, setUnit] = useState(value?.unit ?? defaultUnit ?? DistanceUnits.Miles);
2218
+ useEffect(() => {
2219
+ if (value) {
2220
+ setDistance(value.distance);
2221
+ setUnit(value.unit);
2222
+ }
2223
+ }, [value]);
2224
+ const [locating, setLocating] = useState(false);
2225
+ const [locationFailed, setLocationFailed] = useState(false);
2226
+ const distanceText = (miles, inUnit) => t(inUnit === DistanceUnits.Kilometers ? "distance-kilometers" : "distance-miles", [formatDistance(miles, locale)]);
2227
+ const choices = offered.includes(distance) ? offered : [...offered, distance].sort((a, b) => a - b);
2228
+ const latest = useRef({ distance, unit, onChange });
2229
+ latest.current = { distance, unit, onChange };
2230
+ const boxHost = useRef(null);
2231
+ const providerKey = provider ? JSON.stringify(provider) : "";
2232
+ const countriesKey = (countries ?? []).join(",");
2233
+ const label = value?.label ?? null;
2234
+ useEffect(() => {
2235
+ const host = boxHost.current;
2236
+ if (!open || !host) return;
2237
+ let finder = finders.get(providerKey);
2238
+ if (!finder) {
2239
+ finder = resolveProviderSettings(provider, (name) => portalSettingsCache(ppp).get(name, (asked) => ppp.getMapSettingsAsync(asked))).then((settings) => placeFinder(settings, locale, host.ownerDocument)).catch((error) => {
2240
+ finders.delete(providerKey);
2241
+ console.warn("[NearSearch] The map provider's address search couldn't be set up.", error);
2242
+ return null;
2243
+ });
2244
+ finders.set(providerKey, finder);
2245
+ }
2246
+ let box = null;
2247
+ let gone = false;
2248
+ void finder.then((found) => {
2249
+ if (gone) return;
2250
+ box = mountPlaceSearch(host, {
2251
+ finder: found,
2252
+ language: locale,
2253
+ countries: countriesKey ? countriesKey.split(",") : [],
2254
+ labels: {
2255
+ placeholder: t("search-placeholder"),
2256
+ clear: t("search-clear"),
2257
+ searching: t("searching"),
2258
+ noResults: t("search-no-results"),
2259
+ failed: t("search-failed")
2260
+ },
2261
+ chosen: (place) => {
2262
+ const { distance: chosenDistance, unit: chosenUnit, onChange: change } = latest.current;
2263
+ change({
2264
+ label: place.label,
2265
+ latitude: place.position.latitude,
2266
+ longitude: place.position.longitude,
2267
+ distance: chosenDistance,
2268
+ unit: chosenUnit
2269
+ });
2270
+ setOpen(false);
2271
+ }
2272
+ });
2273
+ if (label) box.showChosen(label);
2274
+ });
2275
+ return () => {
2276
+ gone = true;
2277
+ box?.dispose();
2278
+ };
2279
+ }, [open, ppp, providerKey, countriesKey, locale]);
2280
+ const findMyLocation = () => {
2281
+ setLocating(true);
2282
+ setLocationFailed(false);
2283
+ locate().then(
2284
+ (position) => {
2285
+ setLocating(false);
2286
+ onChange({ label: t("near-my-place"), ...position, distance, unit });
2287
+ setOpen(false);
2288
+ },
2289
+ () => {
2290
+ setLocating(false);
2291
+ setLocationFailed(true);
2292
+ }
2293
+ );
2294
+ };
2295
+ const measure = (nextDistance, nextUnit) => {
2296
+ setDistance(nextDistance);
2297
+ setUnit(nextUnit);
2298
+ if (value) onChange({ ...value, distance: nextDistance, unit: nextUnit });
2299
+ };
2300
+ const tooltip = value ? t("near-active", [distanceText(value.distance, value.unit), value.label]) : t("near-title");
2301
+ return /* @__PURE__ */ jsxs(Popover, { positioning: "below-end", withArrow: true, trapFocus: true, open, onOpenChange: (_, data) => setOpen(data.open), children: [
2302
+ /* @__PURE__ */ jsx(PopoverTrigger, { disableButtonEnhancement: true, children: /* @__PURE__ */ jsx(Tooltip, { content: tooltip, relationship: "label", withArrow: true, children: /* @__PURE__ */ jsx(
2303
+ Button,
2304
+ {
2305
+ className: mergeClasses("ppp-near-search", className),
2306
+ appearance: value ? "primary" : "subtle",
2307
+ icon: value ? /* @__PURE__ */ jsx(Location24Filled, {}) : /* @__PURE__ */ jsx(Location24Regular, {}),
2308
+ children: value ? distanceText(value.distance, value.unit) : void 0
2309
+ }
2310
+ ) }) }),
2311
+ /* @__PURE__ */ jsxs(PopoverSurface, { className: mergeClasses("ppp-near-search-panel", styles.surface), "aria-label": t("near-title"), children: [
2312
+ /* @__PURE__ */ jsxs("div", { children: [
2313
+ /* @__PURE__ */ jsx(Label, { children: t("near-place") }),
2314
+ /* @__PURE__ */ jsx("div", { ref: boxHost, className: "ppp-near-search-place" })
2315
+ ] }),
2316
+ /* @__PURE__ */ jsxs("div", { className: styles.location, children: [
2317
+ /* @__PURE__ */ jsx(Button, { appearance: "subtle", icon: /* @__PURE__ */ jsx(MyLocation20Regular, {}), disabled: locating, onClick: findMyLocation, children: t("near-my-location") }),
2318
+ locating && /* @__PURE__ */ jsx(Spinner, { size: "extra-tiny", label: t("near-locating") })
2319
+ ] }),
2320
+ locationFailed && /* @__PURE__ */ jsx(Text, { size: 200, className: styles.failure, role: "alert", children: t("near-location-failed") }),
2321
+ /* @__PURE__ */ jsxs("div", { className: styles.measures, children: [
2322
+ /* @__PURE__ */ jsx(Field, { label: t("near-distance"), children: /* @__PURE__ */ jsx(
2323
+ Dropdown,
2324
+ {
2325
+ className: mergeClasses("ppp-near-search-distance", styles.dropdown),
2326
+ value: distanceText(distance, unit),
2327
+ selectedOptions: [String(distance)],
2328
+ onOptionSelect: (_, data) => data.optionValue && measure(Number(data.optionValue), unit),
2329
+ children: choices.map((choice) => /* @__PURE__ */ jsx(Option, { value: String(choice), children: distanceText(choice, unit) }, choice))
2330
+ }
2331
+ ) }),
2332
+ /* @__PURE__ */ jsx(Field, { label: t("near-unit"), children: /* @__PURE__ */ jsxs(
2333
+ Dropdown,
2334
+ {
2335
+ className: mergeClasses("ppp-near-search-unit", styles.dropdown),
2336
+ value: unit === DistanceUnits.Kilometers ? t("near-kilometers") : t("near-miles"),
2337
+ selectedOptions: [String(unit)],
2338
+ onOptionSelect: (_, data) => data.optionValue && measure(distance, Number(data.optionValue)),
2339
+ children: [
2340
+ /* @__PURE__ */ jsx(Option, { value: String(DistanceUnits.Miles), children: t("near-miles") }),
2341
+ /* @__PURE__ */ jsx(Option, { value: String(DistanceUnits.Kilometers), children: t("near-kilometers") })
2342
+ ]
2343
+ }
2344
+ ) })
2345
+ ] }),
2346
+ value && /* @__PURE__ */ jsx("div", { className: styles.actions, children: /* @__PURE__ */ jsx(
2347
+ Button,
2348
+ {
2349
+ className: "ppp-near-search-clear",
2350
+ icon: /* @__PURE__ */ jsx(Dismiss16Regular, {}),
2351
+ onClick: () => {
2352
+ onChange(null);
2353
+ setOpen(false);
2354
+ },
2355
+ children: t("near-clear")
2356
+ }
2357
+ ) })
2358
+ ] })
2359
+ ] });
2360
+ }
2361
+ var OPTION_COLORS = [
2362
+ "var(--colorPaletteBerryBackground3, #c239b3)",
2363
+ "var(--colorPaletteTealBackground3, #038387)",
2364
+ "var(--colorPaletteDarkOrangeBackground3, #da3b01)",
2365
+ "var(--colorPaletteCornflowerBackground3, #4f6bed)",
2366
+ "var(--colorPaletteGreenBackground3, #107c10)",
2367
+ "var(--colorPaletteMarigoldBackground3, #eaa300)",
2368
+ "var(--colorPaletteGrapeBackground3, #881798)",
2369
+ "var(--colorPaletteRedBackground3, #d13438)",
2370
+ "var(--colorPaletteSteelBackground3, #005b70)",
2371
+ "var(--colorPaletteLightGreenBackground3, #13a10e)"
2372
+ ];
2373
+ var NO_VALUE_COLOR = "var(--colorNeutralForeground4, #707070)";
2374
+ var COLORING_COLUMNS = /* @__PURE__ */ new Set([ColumnType.Boolean, ColumnType.Choice, ColumnType.State, ColumnType.Status]);
2375
+ function colorOptions(column) {
2376
+ if (!column || !COLORING_COLUMNS.has(column.$type ?? -1) || !("choices" in column) || !column.choices) return null;
2377
+ return column.choices.map((choice) => ({ value: Number(choice.value), ...choice.color ? { color: choice.color } : {} }));
2378
+ }
2379
+ function optionValue(record, column) {
2380
+ const value = readColumnRawValue(record, column);
2381
+ if (typeof value === "boolean") return value ? 1 : 0;
2382
+ const number = typeof value === "string" && value !== "" ? Number(value) : value;
2383
+ return typeof number === "number" && Number.isFinite(number) ? number : null;
2384
+ }
2385
+ function optionColor(options, value) {
2386
+ if (value === null) return NO_VALUE_COLOR;
2387
+ const index = options.findIndex((option) => option.value === value);
2388
+ if (index < 0) return OPTION_COLORS[Math.abs(value) % OPTION_COLORS.length];
2389
+ return options[index].color ?? OPTION_COLORS[index % OPTION_COLORS.length];
2390
+ }
2391
+ function optionOrder(options, value) {
2392
+ if (value === null) return [2, 0];
2393
+ const index = options.findIndex((option) => option.value === value);
2394
+ return index < 0 ? [1, value] : [0, index];
2395
+ }
2396
+ var DEFAULT_LATITUDE_COLUMN = "address1_latitude";
2397
+ var DEFAULT_LONGITUDE_COLUMN = "address1_longitude";
2398
+ var DEFAULT_MAX_ROWS = 1e3;
2399
+ function toNumber(value) {
2400
+ const number = typeof value === "string" ? Number(value) : value;
2401
+ return typeof number === "number" && Number.isFinite(number) ? number : null;
2402
+ }
2403
+ function recordMarkers(response, options) {
2404
+ const columns = response.columns ?? [];
2405
+ const markers = [];
2406
+ for (const record of response.tableRecords ?? []) {
2407
+ const latitude = toNumber(readColumnRawValue(record, options.latitudeColumn));
2408
+ const longitude = toNumber(readColumnRawValue(record, options.longitudeColumn));
2409
+ if (latitude === null || longitude === null) continue;
2410
+ const title = options.titleColumn ? resolveColumnDisplayValue(record, options.titleColumn) : null;
2411
+ const color = options.color(record);
2412
+ const url = options.url(record);
2413
+ markers.push({
2414
+ kind: "marker",
2415
+ id: getRecordReferenceId(record) ?? `${markers.length}`,
2416
+ position: { latitude, longitude },
2417
+ ...title ? { title } : {},
2418
+ ...color ? { color } : {},
2419
+ ...url ? { url } : {},
2420
+ data: { record, columns }
2421
+ });
2422
+ }
2423
+ return markers;
2424
+ }
2425
+ function recordUrl(url, record) {
2426
+ if (typeof url === "function") return url(record);
2427
+ const id = getRecordReferenceId(record);
2428
+ return url && id ? url.replaceAll("{0}", id).replaceAll("{1}", record.tableName) : void 0;
2429
+ }
2430
+ function coordinatesFilterXml(latitudeColumn, longitudeColumn) {
2431
+ const condition = (column) => `<condition attribute="${column.replace(/"/g, "&quot;")}" operator="not-null" />`;
2432
+ return `<filter type="and">${condition(latitudeColumn)}${condition(longitudeColumn)}</filter>`;
2433
+ }
2434
+ function fetchXmlTable(fetchXml) {
2435
+ return /<entity\s+[^>]*name\s*=\s*["']([^"']+)["']/i.exec(fetchXml)?.[1] ?? null;
2436
+ }
2437
+ var TABLE_COLORS = ["var(--colorBrandBackground, #0f6cbd)", ...OPTION_COLORS];
2438
+ var EMPTY = { places: [] };
2439
+ var NO_PLACES = [];
2440
+ function useMapTables(options) {
2441
+ const { enabled, tables, defaultTables, defaultViewId: defaultViewId$1, viewIds, regarding, allowFiltering, allowSavedViews, savedViewsKey, signInUrl, title, toolbarActions } = options;
2442
+ const [near, setNear] = useState(null);
2443
+ const viewIdsKey = viewIds?.join(",") ?? null;
2444
+ const t = usePrefixedT("app.components.map");
2445
+ const { ppp, authState } = usePowerPortalsPro();
2446
+ const control = useMemo(
2447
+ () => savedViewControl(SavedViewControlKind.Map, allowSavedViews ? savedViewsKey : void 0),
2448
+ [allowSavedViews, savedViewsKey]
2449
+ );
2450
+ const saved = useSavedViews({ enabled: enabled && allowSavedViews, tableName: MultiTableViewSchema.table, control });
2451
+ const multiSaved = useMultiTableViews({ enabled: enabled && allowSavedViews && saved.available, control });
2452
+ const awaitsSavedViews = allowSavedViews && (authState.status === "loading" || saved.loading || multiSaved.loading);
2453
+ const [chosen, setChosen] = useState(null);
2454
+ const [opened, setOpened] = useState(EVERY_TABLE_VIEW_ID);
2455
+ const tableSetViewId = chosen ?? opened;
2456
+ const tableSet = useMultiTableTableSet({ use: MultiTableUses.Map, viewId: tableSetViewId, tables, defaultTables: defaultTables ?? [] });
2457
+ const registered = tableSet.registered;
2458
+ useEffect(() => {
2459
+ if (enabled && !registered) void tableSet.registeredAsync().catch(() => void 0);
2460
+ }, [enabled, registered]);
2461
+ const findSavedView = (id) => multiSaved.views.find((x) => x.id.toLowerCase() === id.toLowerCase());
2462
+ const offered = allowSavedViews && saved.available;
2463
+ const { views, allViews } = useMemo(() => {
2464
+ if (!registered) return { views: [], allViews: [] };
2465
+ const everyTable = { id: EVERY_TABLE_VIEW_ID, name: t("every-table"), multiTableViewId: null, tables: null };
2466
+ const registeredView = (view) => ({
2467
+ id: view.id,
2468
+ name: view.name,
2469
+ multiTableViewId: view.id,
2470
+ tables: null
2471
+ });
2472
+ const named = viewIdsKey === null ? null : viewIdsKey.split(",").map((id) => registered.views.find((view) => view.id.toLowerCase() === id.toLowerCase())).filter((view) => view !== void 0).map(registeredView);
2473
+ const listed = named === null ? [everyTable, ...registered.views.map(registeredView)] : named.length > 0 ? named : [everyTable];
2474
+ if (offered) for (const own of multiSaved.views) listed.push({ id: own.id, name: own.name, multiTableViewId: null, tables: [...own.tables] });
2475
+ const first = offered ? defaultViewId(saved.settings, listed.map((x) => x.id), control) : void 0;
2476
+ const ordered = first ? [...listed.filter((x) => x.id === first), ...listed.filter((x) => x.id !== first)] : listed;
2477
+ const shownOnes = ordered.filter((x) => x.id === chosen || !offered || !isViewHidden(saved.settings, x.id, control));
2478
+ return { views: shownOnes.length > 0 ? shownOnes : ordered, allViews: listed };
2479
+ }, [registered, offered, multiSaved.views, saved.settings, control, chosen, viewIdsKey]);
2480
+ const userDefault = offered ? defaultViewId(saved.settings, views.map((x) => x.id), control) : void 0;
2481
+ const shownView = views.find((x) => x.id === chosen) ?? views.find((x) => x.id === userDefault) ?? views.find((x) => x.id === defaultViewId$1) ?? views[0] ?? null;
2482
+ useEffect(() => {
2483
+ if (!chosen && shownView && shownView.id !== opened) setOpened(shownView.id);
2484
+ });
2485
+ const known = registered && shownView && shownView.id === tableSetViewId ? tableSet.tablesOf(shownView.tables, shownView.multiTableViewId) : null;
2486
+ useEffect(() => {
2487
+ if (known && known.join(",") !== tableSet.known.join(",")) tableSet.setKnown(tableSetViewId, known);
2488
+ });
2489
+ const filter = tableSet.filter;
2490
+ const shownTables = tableSet.shown;
2491
+ const [refreshes, setRefreshes] = useState(0);
2492
+ const [read, setRead] = useState(null);
2493
+ const [failed, setFailed] = useState(false);
2494
+ const request = enabled && shownView && known && known.join(",") === tableSet.known.join(",") && !awaitsSavedViews ? {
2495
+ viewId: filter ? null : shownView.multiTableViewId,
2496
+ tables: filter ? [...filter.tables] : shownView.tables ? [...shownView.tables] : null,
2497
+ includeTables: [...shownTables],
2498
+ regarding: regarding ? { tableName: regarding.tableName, id: regarding.id } : null,
2499
+ near: near ? { latitude: near.latitude, longitude: near.longitude, distance: near.distance, unit: near.unit } : null
2500
+ } : null;
2501
+ const requestKey = request ? JSON.stringify([request, refreshes]) : null;
2502
+ const latestRequest = useRef(request);
2503
+ latestRequest.current = request;
2504
+ useEffect(() => {
2505
+ const asked = latestRequest.current;
2506
+ if (!asked || !requestKey) return;
2507
+ setFailed(false);
2508
+ if ((asked.includeTables ?? []).length === 0) {
2509
+ setRead({ key: requestKey, response: EMPTY });
2510
+ return;
2511
+ }
2512
+ const controller = new AbortController();
2513
+ ppp.loadMultiTablePlacesAsync(asked, controller.signal).then(
2514
+ (response2) => {
2515
+ if (!controller.signal.aborted) setRead({ key: requestKey, response: response2 });
2516
+ },
2517
+ (error) => {
2518
+ if (controller.signal.aborted) return;
2519
+ console.warn("[Map] The map's tables could not be read.", error);
2520
+ setFailed(true);
2521
+ setRead({ key: requestKey, response: EMPTY });
2522
+ }
2523
+ );
2524
+ return () => controller.abort();
2525
+ }, [ppp, requestKey]);
2526
+ const response = read?.key === requestKey ? read.response : null;
2527
+ const loading = enabled && !response && !failed;
2528
+ const tableLabel = (table) => tableSet.tableLabel(table);
2529
+ const tableColor = (table) => {
2530
+ const color = tableSet.infoOf(table)?.definition?.color;
2531
+ if (color) return color;
2532
+ const index = Math.max(0, tableSet.known.findIndex((x) => x.toLowerCase() === table.toLowerCase()));
2533
+ return TABLE_COLORS[index % TABLE_COLORS.length];
2534
+ };
2535
+ const failedTables = new Set((response?.failures ?? []).map((x) => x.table.toLowerCase()));
2536
+ const truncated = new Set((response?.truncated ?? []).map((x) => x.toLowerCase()));
2537
+ const legend = !enabled || shownTables.length < 2 && failedTables.size === 0 && truncated.size === 0 ? [] : shownTables.map((table) => ({
2538
+ table,
2539
+ label: tableLabel(table),
2540
+ color: tableColor(table),
2541
+ note: failedTables.has(table.toLowerCase()) ? t("table-failed") : truncated.has(table.toLowerCase()) ? t("table-truncated") : null
2542
+ }));
2543
+ const viewTables = () => {
2544
+ if (!shownView) return [];
2545
+ if (shownView.tables) return [...shownView.tables];
2546
+ if (shownView.multiTableViewId) return [...registered?.views.find((x) => x.id === shownView.multiTableViewId)?.tables ?? []];
2547
+ return tableSet.known.map((table) => ({ table }));
2548
+ };
2549
+ const showsFilterButton = allowFiltering && viewTables().length > 0;
2550
+ const openFilter = async () => {
2551
+ const view = shownView;
2552
+ const tablesOfIt = viewTables();
2553
+ if (!view || tablesOfIt.length === 0) return;
2554
+ try {
2555
+ tableSet.openFilter(view.id, await tableSet.filterQueries(tablesOfIt), tableSet.known, tableLabel);
2556
+ } catch (error) {
2557
+ console.warn("[Map] The filters of the view shown could not be read.", error);
2558
+ }
2559
+ };
2560
+ const currentTables = () => shownView ? shownViewTables(filter ? filter.tables : viewTables(), tableSet.known, tableSet.shown, (table) => !!tableSet.infoOf(table)?.isActivity) : [];
2561
+ const viewChanged = !!filter || tableSet.tablesChanged;
2562
+ const shownSaved = shownView ? findSavedView(shownView.id) : void 0;
2563
+ const choose = (id) => {
2564
+ setChosen(id);
2565
+ tableSet.reset();
2566
+ };
2567
+ const viewName = (id) => allViews.find((x) => x.id.toLowerCase() === id.toLowerCase())?.name ?? findSavedView(id)?.name ?? t("every-table");
2568
+ const session = useSavedViewSession({
2569
+ saved,
2570
+ signInUrl,
2571
+ operations: {
2572
+ shownViewId: shownView?.id,
2573
+ savedViewName: (id) => findSavedView(id)?.name,
2574
+ managedViewName: viewName,
2575
+ managedViews: () => allViews.map((view) => ({
2576
+ id: view.id,
2577
+ name: view.name,
2578
+ isSaved: !!findSavedView(view.id),
2579
+ isDefault: defaultViewId(saved.settings, allViews.map((x) => x.id), control) === view.id,
2580
+ isHidden: isViewHidden(saved.settings, view.id, control),
2581
+ isShown: view.id === shownView?.id
2582
+ })),
2583
+ save: async () => {
2584
+ const tablesNow = currentTables();
2585
+ if (!shownSaved || tablesNow.length === 0) return;
2586
+ await multiSaved.update(shownSaved.id, { tables: tablesNow });
2587
+ choose(shownSaved.id);
2588
+ },
2589
+ saveAs: async (name) => {
2590
+ const tablesNow = currentTables();
2591
+ if (tablesNow.length === 0) return;
2592
+ const view = await multiSaved.create(name, tablesNow, {});
2593
+ choose(view.id);
2594
+ },
2595
+ clone: async (id, name) => {
2596
+ const view = allViews.find((x) => x.id === id);
2597
+ const tablesOfIt = findSavedView(id)?.tables ?? (view?.multiTableViewId ? registered?.views.find((x) => x.id === view.multiTableViewId)?.tables : null) ?? (view ? (registered?.tables ?? []).filter((x) => !tableSet.allowed || tableSet.allowed.has(x.table.toLowerCase())).map((x) => ({ table: x.table })) : []);
2598
+ if (tablesOfIt.length === 0) return;
2599
+ await multiSaved.create(name, tablesOfIt, {});
2600
+ },
2601
+ rename: async (id, name) => void await multiSaved.update(id, { name }),
2602
+ remove: async (id) => {
2603
+ await multiSaved.remove(id);
2604
+ await saved.reloadSettings();
2605
+ if (shownView?.id === id) choose(null);
2606
+ },
2607
+ openView: (id) => {
2608
+ if (id !== shownView?.id) choose(id);
2609
+ }
2610
+ }
2611
+ });
2612
+ const toolbar = enabled ? /* @__PURE__ */ jsxs(
2613
+ ViewToolbar,
2614
+ {
2615
+ className: "ppp-map-header",
2616
+ ariaLabel: t("toolbar"),
2617
+ start: (
2618
+ // A choice of one where the user saves views: the picker is how they reach theirs.
2619
+ views.length > (session.showsMenu ? 0 : 1) ? /* @__PURE__ */ jsx(
2620
+ ViewPicker,
2621
+ {
2622
+ className: "ppp-map-view-picker",
2623
+ options: views.map((view) => ({ id: view.id, label: view.name })),
2624
+ value: shownView?.id,
2625
+ onChange: choose,
2626
+ isUserView: (id) => !!findSavedView(id)
2627
+ }
2628
+ ) : title ? /* @__PURE__ */ jsx(Subtitle2, { as: "h2", children: title }) : null
2629
+ ),
2630
+ children: [
2631
+ options.allowNearSearch && /* @__PURE__ */ jsx(
2632
+ NearSearch,
2633
+ {
2634
+ value: near,
2635
+ onChange: setNear,
2636
+ distances: options.nearDistances,
2637
+ defaultDistance: options.nearDistance,
2638
+ defaultUnit: options.nearDistanceUnit,
2639
+ provider: options.provider,
2640
+ countries: options.addressSearchCountries
2641
+ }
2642
+ ),
2643
+ toolbarActions,
2644
+ /* @__PURE__ */ jsx(
2645
+ ViewFilterMenu,
2646
+ {
2647
+ changed: viewChanged,
2648
+ onEditFilters: showsFilterButton ? () => void openFilter() : void 0,
2649
+ tables: tableSet.menuTables(tableLabel),
2650
+ onTableShownChange: tableSet.showTable,
2651
+ savedViews: session.menu({
2652
+ canSave: !!shownSaved && viewChanged,
2653
+ canSaveAs: currentTables().length > 0 && viewChanged,
2654
+ canSetDefault: !!shownView && userDefault !== shownView.id
2655
+ })
2656
+ }
2657
+ ),
2658
+ /* @__PURE__ */ jsx(
2659
+ ViewRefreshButton,
2660
+ {
2661
+ onClick: () => {
2662
+ tableSet.forget();
2663
+ setRefreshes((x) => x + 1);
2664
+ }
2665
+ }
2666
+ )
2667
+ ]
2668
+ }
2669
+ ) : null;
2670
+ const dialogs = enabled ? /* @__PURE__ */ jsxs(Fragment, { children: [
2671
+ /* @__PURE__ */ jsx(
2672
+ MultiTableFilterDialog,
2673
+ {
2674
+ open: tableSet.filterDialog !== null,
2675
+ tabs: tableSet.filterDialog?.tabs ?? [],
2676
+ viewTabs: tableSet.filterDialog?.viewTabs,
2677
+ onApply: (criteria, restoreView) => void tableSet.applyFilter(criteria, restoreView),
2678
+ onCancel: tableSet.closeFilter
2679
+ }
2680
+ ),
2681
+ /* @__PURE__ */ jsx(SavedViewDialogs, { session })
2682
+ ] }) : null;
2683
+ return {
2684
+ places: response?.places ?? NO_PLACES,
2685
+ loading,
2686
+ failed,
2687
+ // A new place to be near, or distance, is a new extent too.
2688
+ fitKey: `${shownView?.id ?? ""}|${regarding?.id ?? ""}|${JSON.stringify(filter?.tables ?? null)}|${near ? JSON.stringify([near.latitude, near.longitude, near.distance, near.unit]) : ""}`,
2689
+ colors: Object.fromEntries(tableSet.known.map((table) => [table, tableColor(table)])),
2690
+ legend,
2691
+ near: enabled ? near : null,
2692
+ toolbar,
2693
+ dialogs
2694
+ };
2695
+ }
2696
+ function MapTablesBar({ onState, ...options }) {
2697
+ const tables = useMapTables({ ...options, enabled: true });
2698
+ const { places, loading, failed, fitKey, colors, legend, near } = tables;
2699
+ const key = JSON.stringify([loading, failed, fitKey, colors, legend, near]);
2700
+ const latest = useRef(onState);
2701
+ latest.current = onState;
2702
+ useEffect(() => {
2703
+ latest.current({ places, loading, failed, fitKey, colors, legend, near });
2704
+ }, [places, key]);
2705
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2706
+ tables.toolbar,
2707
+ tables.dialogs
2708
+ ] });
2709
+ }
2710
+ var STRINGS_PREFIX = "app.components.map";
2711
+ function itemClick(item, position) {
2712
+ let prevented = false;
2713
+ return {
2714
+ item,
2715
+ position,
2716
+ preventDefault: () => {
2717
+ prevented = true;
2718
+ },
2719
+ get defaultPrevented() {
2720
+ return prevented;
2721
+ }
2722
+ };
2723
+ }
2724
+ var useStyles2 = makeStyles({
2725
+ root: {
2726
+ position: "relative",
2727
+ // Lets the map shrink inside a flex or grid parent, as the Chart's wrapper does.
2728
+ minWidth: 0,
2729
+ overflow: "hidden",
2730
+ borderRadius: tokens.borderRadiusMedium,
2731
+ backgroundColor: tokens.colorNeutralBackground3
2732
+ },
2733
+ overlay: {
2734
+ position: "absolute",
2735
+ inset: 0,
2736
+ display: "flex",
2737
+ alignItems: "center",
2738
+ justifyContent: "center",
2739
+ zIndex: 3,
2740
+ pointerEvents: "none"
2741
+ },
2742
+ message: {
2743
+ position: "absolute",
2744
+ top: tokens.spacingVerticalS,
2745
+ left: tokens.spacingHorizontalS,
2746
+ right: tokens.spacingHorizontalS,
2747
+ zIndex: 3
2748
+ },
2749
+ printButton: {
2750
+ boxShadow: tokens.shadow4
2751
+ },
2752
+ // The legend and the note over the bottom left corner, clear of a provider's logo there.
2753
+ corner: {
2754
+ position: "absolute",
2755
+ left: tokens.spacingHorizontalS,
2756
+ bottom: `calc(${tokens.spacingVerticalXXL} + ${tokens.spacingVerticalS})`,
2757
+ zIndex: 1,
2758
+ display: "flex",
2759
+ flexDirection: "column",
2760
+ alignItems: "flex-start",
2761
+ gap: tokens.spacingVerticalXS,
2762
+ maxWidth: `calc(100% - 2 * ${tokens.spacingHorizontalS})`,
2763
+ pointerEvents: "none"
2764
+ },
2765
+ note: {
2766
+ padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalS}`,
2767
+ borderRadius: tokens.borderRadiusMedium,
2768
+ backgroundColor: tokens.colorNeutralBackground1,
2769
+ boxShadow: tokens.shadow4
2770
+ },
2771
+ legend: {
2772
+ display: "flex",
2773
+ flexWrap: "wrap",
2774
+ columnGap: tokens.spacingHorizontalM,
2775
+ rowGap: tokens.spacingVerticalXXS,
2776
+ margin: 0,
2777
+ padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
2778
+ listStyleType: "none",
2779
+ borderRadius: tokens.borderRadiusMedium,
2780
+ backgroundColor: tokens.colorNeutralBackground1,
2781
+ boxShadow: tokens.shadow4
2782
+ },
2783
+ legendItem: {
2784
+ display: "inline-flex",
2785
+ alignItems: "center",
2786
+ gap: tokens.spacingHorizontalXS
2787
+ },
2788
+ legendNote: {
2789
+ color: tokens.colorNeutralForeground3
2790
+ },
2791
+ frame: {
2792
+ display: "flex",
2793
+ flexDirection: "column",
2794
+ gap: tokens.spacingVerticalS,
2795
+ minWidth: 0
2796
+ },
2797
+ legendDot: {
2798
+ flexShrink: 0,
2799
+ width: "10px",
2800
+ height: "10px",
2801
+ borderRadius: tokens.borderRadiusCircular
2802
+ },
2803
+ // A card that stands off the map in either theme: room inside, rounder corners, and a hairline
2804
+ // border as well as the shadow, which a dark map swallows.
2805
+ popup: {
2806
+ ...shorthands.padding(tokens.spacingVerticalM, tokens.spacingHorizontalL),
2807
+ ...shorthands.border(tokens.strokeWidthThin, "solid", tokens.colorNeutralStroke2),
2808
+ // Fluent's 2XLarge radius, which this version of its React theme doesn't define yet.
2809
+ borderRadius: "var(--borderRadius2XLarge, 12px)",
2810
+ width: "max-content",
2811
+ maxWidth: "340px",
2812
+ minWidth: "200px",
2813
+ maxHeight: "360px",
2814
+ overflowY: "auto",
2815
+ boxShadow: tokens.shadow28
2816
+ },
2817
+ popupHeader: {
2818
+ display: "flex",
2819
+ alignItems: "flex-start",
2820
+ gap: tokens.spacingHorizontalS
2821
+ },
2822
+ popupTitle: {
2823
+ flexGrow: 1,
2824
+ minWidth: 0,
2825
+ overflowWrap: "anywhere"
2826
+ },
2827
+ popupBody: {
2828
+ flexGrow: 1,
2829
+ minWidth: 0,
2830
+ overflowWrap: "anywhere"
2831
+ },
2832
+ fields: {
2833
+ display: "grid",
2834
+ gridTemplateColumns: "auto 1fr",
2835
+ columnGap: tokens.spacingHorizontalM,
2836
+ rowGap: tokens.spacingVerticalXXS,
2837
+ margin: 0
2838
+ },
2839
+ fieldName: {
2840
+ color: tokens.colorNeutralForeground3
2841
+ },
2842
+ fieldValue: {
2843
+ margin: 0,
2844
+ overflowWrap: "anywhere"
2845
+ },
2846
+ list: {
2847
+ display: "flex",
2848
+ flexDirection: "column",
2849
+ alignItems: "stretch",
2850
+ gap: tokens.spacingVerticalXXS,
2851
+ margin: 0,
2852
+ padding: 0,
2853
+ listStyleType: "none"
2854
+ },
2855
+ listButton: {
2856
+ justifyContent: "flex-start",
2857
+ width: "100%"
2858
+ }
2859
+ });
2860
+ var NO_TABLES = { places: [], loading: false, failed: false, fitKey: "", colors: {}, legend: [], near: null };
2861
+ var TABLES_LOADING = { ...NO_TABLES, loading: true };
2862
+ var cssLength = (value) => typeof value === "number" ? `${value}px` : value;
2863
+ function itemKind(item) {
2864
+ return item.kind === "marker" ? "marker" : "shape";
2865
+ }
2866
+ function isRecord(data) {
2867
+ return !!data && typeof data === "object" && "record" in data && "columns" in data;
2868
+ }
2869
+ function MapImpl({
2870
+ provider,
2871
+ items,
2872
+ viewId,
2873
+ fetchXml,
2874
+ filters,
2875
+ latitudeColumn = DEFAULT_LATITUDE_COLUMN,
2876
+ longitudeColumn = DEFAULT_LONGITUDE_COLUMN,
2877
+ titleColumn,
2878
+ recordColor,
2879
+ colorColumn,
2880
+ showLegend = true,
2881
+ recordUrl: recordUrl2,
2882
+ showRecordLinks = true,
2883
+ onNavigate,
2884
+ showAddressSearch = false,
2885
+ addressSearchCountries,
2886
+ address,
2887
+ fitItemIds,
2888
+ multiTable = false,
2889
+ tables,
2890
+ defaultTables,
2891
+ defaultViewId,
2892
+ viewIds,
2893
+ regarding,
2894
+ allowFiltering = false,
2895
+ allowSavedViews = false,
2896
+ savedViewsKey,
2897
+ signInUrl = "Account/Login",
2898
+ title,
2899
+ allowNearSearch = false,
2900
+ nearDistances,
2901
+ nearDistance,
2902
+ nearDistanceUnit,
2903
+ maxRows = DEFAULT_MAX_ROWS,
2904
+ mapStyle = "road",
2905
+ center,
2906
+ zoom,
2907
+ cluster = true,
2908
+ showPrintButton = false,
2909
+ printTitle,
2910
+ printEntries,
2911
+ onPrintChange,
2912
+ usePortalTheme = true,
2913
+ showPopups = true,
2914
+ renderPopup,
2915
+ nativeOptions,
2916
+ height = "400px",
2917
+ width = "100%",
2918
+ className,
2919
+ style,
2920
+ onMapClick,
2921
+ onItemClick,
2922
+ onAddressSelected,
2923
+ onViewChanged,
2924
+ onError
2925
+ }, ref) {
2926
+ const styles = useStyles2();
2927
+ const t = usePrefixedT(STRINGS_PREFIX);
2928
+ const { locale } = useLocale();
2929
+ const { ppp } = usePowerPortalsPro();
2930
+ const hostRef = useRef(null);
2931
+ const popupRef = useRef(null);
2932
+ const rootRef = useRef(null);
2933
+ const controllerRef = useRef(null);
2934
+ const [ready, setReady] = useState(false);
2935
+ const [failure, setFailure] = useState(null);
2936
+ const [addressStatus, setAddressStatus] = useState(null);
2937
+ const [popup, setPopup] = useState(null);
2938
+ const [canSaveImage, setCanSaveImage] = useState(false);
2939
+ const [printing, setPrinting] = useState(false);
2940
+ const usesRecordPages = showRecordLinks && !recordUrl2;
2941
+ const recordPages = useRecordPages({ enabled: usesRecordPages && (multiTable || !!viewId || !!fetchXml) });
2942
+ const recordPageOf = (row) => {
2943
+ const id = row.place?.id ?? row.record.id;
2944
+ const page = usesRecordPages && id ? findRecordPage(recordPages, row.place?.table ?? row.record.tableName) : void 0;
2945
+ return page && id ? recordPageUrl(page, id) : void 0;
2946
+ };
2947
+ const latest = useRef({ onMapClick, onItemClick, onAddressSelected, onViewChanged, onError, onNavigate, showPopups, t });
2948
+ latest.current = { onMapClick, onItemClick, onAddressSelected, onViewChanged, onError, onNavigate, showPopups, t };
2949
+ const viewMetadata = useViewMetadata(viewId ?? "", { enabled: !!viewId });
2950
+ const table = viewId ? viewMetadata.data?.tableName : fetchXml ? fetchXmlTable(fetchXml) ?? void 0 : void 0;
2951
+ const tableMetadata = useTableMetadata(table ?? "", { enabled: !!table && (!titleColumn || !!colorColumn) });
2952
+ const recordTitleColumn = titleColumn ?? tableMetadata.data?.primaryNameColumn;
2953
+ const colorChoices = useMemo(() => {
2954
+ if (!colorColumn) return null;
2955
+ const column = tableMetadata.data?.columns.find((c) => c.columnName.toLowerCase() === colorColumn.toLowerCase());
2956
+ return colorOptions(column);
2957
+ }, [colorColumn, tableMetadata.data]);
2958
+ const titleSettled = !!titleColumn || !!viewId && !!viewMetadata.error || (table ? tableMetadata.data !== void 0 || tableMetadata.error !== void 0 : !viewId);
2959
+ const request = useMemo(() => {
2960
+ if (!viewId && !fetchXml || !titleSettled) return null;
2961
+ return {
2962
+ ...viewId ? { viewId } : { fetchXml },
2963
+ pageSize: Math.max(1, Math.min(maxRows, 5e3)),
2964
+ filters: [
2965
+ ...filters ?? [],
2966
+ { $type: "FetchXmlFilter", filterXml: coordinatesFilterXml(latitudeColumn, longitudeColumn) }
2967
+ ],
2968
+ additionalColumns: [
2969
+ latitudeColumn,
2970
+ longitudeColumn,
2971
+ ...recordTitleColumn ? [recordTitleColumn] : [],
2972
+ ...colorColumn ? [colorColumn] : []
2973
+ ]
2974
+ };
2975
+ }, [viewId, fetchXml, filters, maxRows, latitudeColumn, longitudeColumn, recordTitleColumn, colorColumn, titleSettled]);
2976
+ const requestKey = request ? JSON.stringify(request) : null;
2977
+ const [rows, setRows] = useState(null);
2978
+ const [rowsFailed, setRowsFailed] = useState(false);
2979
+ useEffect(() => {
2980
+ if (!request || !requestKey) {
2981
+ setRows(null);
2982
+ setRowsFailed(false);
2983
+ return;
2984
+ }
2985
+ const controller = new AbortController();
2986
+ setRowsFailed(false);
2987
+ ppp.loadQueryAsync(request, controller.signal).then(
2988
+ (response) => {
2989
+ if (!controller.signal.aborted) setRows({ key: requestKey, response });
2990
+ },
2991
+ (error) => {
2992
+ if (controller.signal.aborted) return;
2993
+ setRowsFailed(true);
2994
+ latest.current.onError?.(error instanceof Error ? error.message : String(error));
2995
+ }
2996
+ );
2997
+ return () => controller.abort();
2998
+ }, [ppp, requestKey]);
2999
+ const currentRows = rows?.key === requestKey ? rows : null;
3000
+ const colorOf = (record) => colorColumn && colorChoices ? optionColor(colorChoices, optionValue(record, colorColumn)) : typeof recordColor === "function" ? recordColor(record) : recordColor;
3001
+ const rowRecords = currentRows?.response.tableRecords ?? [];
3002
+ const colorsKey = typeof recordColor === "function" && !(colorColumn && colorChoices) ? rowRecords.map((r) => colorOf(r) ?? "").join("\n") : null;
3003
+ const urlsKey = typeof recordUrl2 === "function" ? rowRecords.map((r) => recordUrl(recordUrl2, r) ?? "").join("\n") : null;
3004
+ const rowFunctions = useRef({ colorOf, recordUrl: recordUrl2 });
3005
+ rowFunctions.current = { colorOf, recordUrl: recordUrl2 };
3006
+ const recordItems = useMemo(
3007
+ () => currentRows ? recordMarkers(currentRows.response, {
3008
+ latitudeColumn,
3009
+ longitudeColumn,
3010
+ titleColumn: recordTitleColumn,
3011
+ color: (record) => rowFunctions.current.colorOf(record),
3012
+ url: (record) => recordUrl(rowFunctions.current.recordUrl, record)
3013
+ }) : [],
3014
+ [
3015
+ currentRows,
3016
+ latitudeColumn,
3017
+ longitudeColumn,
3018
+ recordTitleColumn,
3019
+ typeof recordColor === "function" ? colorsKey : recordColor,
3020
+ colorColumn,
3021
+ colorChoices,
3022
+ typeof recordUrl2 === "function" ? urlsKey : recordUrl2
3023
+ ]
3024
+ );
3025
+ const legend = useMemo(() => {
3026
+ if (!colorColumn || !colorChoices || !showLegend) return [];
3027
+ const entries = new globalThis.Map();
3028
+ for (const marker of recordItems) {
3029
+ const record = marker.data.record;
3030
+ const value = optionValue(record, colorColumn);
3031
+ if (entries.has(value)) continue;
3032
+ const label = value === null ? t("no-value") : resolveColumnDisplayValue(record, colorColumn) || `${value}`;
3033
+ entries.set(value, { label, color: optionColor(colorChoices, value) });
3034
+ }
3035
+ return [...entries].sort(([a], [b]) => {
3036
+ const [x, y] = [optionOrder(colorChoices, a), optionOrder(colorChoices, b)];
3037
+ return x[0] - y[0] || x[1] - y[1];
3038
+ }).map(([value, entry]) => ({ value, ...entry }));
3039
+ }, [colorColumn, colorChoices, showLegend, recordItems, t]);
3040
+ const [tablesRead, setTablesRead] = useState(null);
3041
+ const mapTables = multiTable ? tablesRead ?? TABLES_LOADING : NO_TABLES;
3042
+ const tableColorOf = (table2) => mapTables.colors[table2] ?? mapTables.colors[table2.toLowerCase()] ?? void 0;
3043
+ const placeColorsKey = JSON.stringify(mapTables.colors);
3044
+ const placeItems = useMemo(
3045
+ () => mapTables.places.map((place) => {
3046
+ const color = (typeof recordColor === "function" ? recordColor(place.record) : void 0) ?? tableColorOf(place.table);
3047
+ const url = recordUrl(recordUrl2, place.record);
3048
+ return {
3049
+ kind: "marker",
3050
+ id: `${place.table}:${place.id}`,
3051
+ position: { latitude: Number(place.latitude), longitude: Number(place.longitude) },
3052
+ ...place.title.value ? { title: place.title.value } : {},
3053
+ ...color ? { color } : {},
3054
+ ...url ? { url } : {},
3055
+ data: { record: place.record, columns: [], place }
3056
+ };
3057
+ }),
3058
+ // The colors are in their key; a row function's answers change with the rows.
3059
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3060
+ [mapTables.places, placeColorsKey, typeof recordColor === "function" ? colorsKey : null, typeof recordUrl2 === "function" ? urlsKey : recordUrl2]
3061
+ );
3062
+ const nearKey = mapTables.near ? JSON.stringify(mapTables.near) : null;
3063
+ const nearItems = useMemo(
3064
+ () => mapTables.near ? nearPlaceItems(mapTables.near) : [],
3065
+ // The place is in its key.
3066
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3067
+ [nearKey]
3068
+ );
3069
+ const allItems = useMemo(
3070
+ () => [...items ?? [], ...recordItems, ...placeItems, ...nearItems],
3071
+ [items, recordItems, placeItems, nearItems]
3072
+ );
3073
+ const itemsById = useMemo(() => new globalThis.Map(allItems.map((item) => [item.id, item])), [allItems]);
3074
+ const itemsRef = useRef(itemsById);
3075
+ itemsRef.current = itemsById;
3076
+ const allItemsRef = useRef(allItems);
3077
+ allItemsRef.current = allItems;
3078
+ const camera = center ? { center, zoom: zoom ?? 12 } : void 0;
3079
+ const options = {
3080
+ provider,
3081
+ // The provider's keys and the settings the page leaves unset come from the portal.
3082
+ portalSettings: (name) => portalSettingsCache(ppp).get(name, (asked) => ppp.getMapSettingsAsync(asked)),
3083
+ style: mapStyle,
3084
+ camera,
3085
+ cluster,
3086
+ usePortalTheme,
3087
+ language: locale,
3088
+ nativeOptions,
3089
+ clusterLabel: (count) => latest.current.t("cluster", [count]),
3090
+ gestureHelp: { windows: t("scroll-zoom"), mac: t("scroll-zoom-mac"), mobile: t("touch-pan") },
3091
+ addressSearch: showAddressSearch ? {
3092
+ placeholder: t("search-placeholder"),
3093
+ clear: t("search-clear"),
3094
+ searching: t("searching"),
3095
+ noResults: t("search-no-results"),
3096
+ failed: t("search-failed"),
3097
+ countries: addressSearchCountries
3098
+ } : null,
3099
+ address
3100
+ };
3101
+ const optionsRef = useRef(options);
3102
+ optionsRef.current = options;
3103
+ const openItem = (id, position) => {
3104
+ const item = itemsRef.current.get(id);
3105
+ if (!item) return;
3106
+ setPopup({ kind: "item", item, position: (item.kind === "marker" ? item.position : popupPosition(item)) });
3107
+ };
3108
+ const optionsKey = JSON.stringify([
3109
+ provider ?? null,
3110
+ mapStyle,
3111
+ cluster,
3112
+ usePortalTheme,
3113
+ locale,
3114
+ nativeOptions ?? null,
3115
+ // Its words too, which can come after the map is drawn, as the strings load.
3116
+ options.addressSearch,
3117
+ options.gestureHelp,
3118
+ address?.trim() ?? ""
3119
+ ]);
3120
+ const optionsKeyRef = useRef(optionsKey);
3121
+ optionsKeyRef.current = optionsKey;
3122
+ const appliedOptions = useRef(null);
3123
+ useEffect(() => {
3124
+ const container = hostRef.current;
3125
+ if (!container) return;
3126
+ let disposed = false;
3127
+ let created = null;
3128
+ appliedOptions.current = optionsKeyRef.current;
3129
+ void MapController.create(container, optionsRef.current, allItemsRef.current, {
3130
+ mapClick: (event) => {
3131
+ setPopup(null);
3132
+ latest.current.onMapClick?.(event);
3133
+ },
3134
+ itemClick: ({ id, position }) => {
3135
+ if (id.startsWith(NEAR_CIRCLE_ID)) {
3136
+ setPopup(null);
3137
+ latest.current.onMapClick?.({ position });
3138
+ return;
3139
+ }
3140
+ const item = itemsRef.current.get(id);
3141
+ if (!item) return;
3142
+ const event = itemClick(item, position);
3143
+ latest.current.onItemClick?.(event);
3144
+ if (latest.current.showPopups && !event.defaultPrevented) setPopup({ kind: "item", item, position });
3145
+ },
3146
+ // Listed even with popups off: markers at one place can be told apart no other way.
3147
+ clusterClick: ({ ids, position }) => {
3148
+ const listed = ids.map((id) => itemsRef.current.get(id)).filter((item) => item !== void 0);
3149
+ if (listed.length) setPopup({ kind: "list", items: listed, position });
3150
+ },
3151
+ viewChanged: (view) => latest.current.onViewChanged?.(view),
3152
+ addressSelected: (place) => latest.current.onAddressSelected?.(place),
3153
+ addressStatus: setAddressStatus,
3154
+ error: (kind, message) => {
3155
+ setFailure({ kind, message });
3156
+ latest.current.onError?.(message);
3157
+ }
3158
+ }).then(
3159
+ (controller) => {
3160
+ created = controller;
3161
+ if (disposed) {
3162
+ controller.dispose();
3163
+ return;
3164
+ }
3165
+ controllerRef.current = controller;
3166
+ setCanSaveImage(controller.canSaveImage);
3167
+ setReady(true);
3168
+ },
3169
+ (error) => {
3170
+ if (disposed) return;
3171
+ const message = error instanceof Error ? error.message : String(error);
3172
+ setFailure({ kind: "failed", message });
3173
+ latest.current.onError?.(message);
3174
+ }
3175
+ );
3176
+ return () => {
3177
+ disposed = true;
3178
+ created?.dispose();
3179
+ controllerRef.current = null;
3180
+ setReady(false);
3181
+ };
3182
+ }, []);
3183
+ const fittedSource = useRef(null);
3184
+ const rowsSettled = (!requestKey || !!currentRows || rowsFailed) && (!multiTable || !mapTables.loading);
3185
+ const fitKey = fitItemIds ? fitItemIds.join(",") : null;
3186
+ const fitItemIdsRef = useRef(fitItemIds);
3187
+ fitItemIdsRef.current = fitItemIds;
3188
+ const fittedKey = useRef(null);
3189
+ const fittedAmong = useRef(/* @__PURE__ */ new Set());
3190
+ useEffect(() => {
3191
+ const controller = controllerRef.current;
3192
+ if (!controller) return;
3193
+ if (fitItemIdsRef.current) {
3194
+ controller.setItems(allItems, false);
3195
+ const gone = fittedAmong.current.size === 0 || [...fittedAmong.current].some((id) => !itemsById.has(id));
3196
+ if (gone) {
3197
+ controller.fitToItems(fitItemIdsRef.current);
3198
+ fittedKey.current = fitKey;
3199
+ fittedAmong.current = new Set(itemsById.keys());
3200
+ }
3201
+ } else {
3202
+ const source = rowsSettled ? `${requestKey ?? ""}|${multiTable ? mapTables.fitKey : ""}|${allItems.length > 0}` : null;
3203
+ const fit = (!center || !!mapTables.near) && allItems.length > 0 && source !== null && fittedSource.current !== source;
3204
+ if (fit) fittedSource.current = source;
3205
+ controller.setItems(allItems, fit);
3206
+ }
3207
+ setPopup((open) => {
3208
+ if (open?.kind === "item") return itemsById.has(open.item.id) ? open : null;
3209
+ if (open?.kind === "list") return open.items.every((item) => itemsById.has(item.id)) ? open : null;
3210
+ return open;
3211
+ });
3212
+ }, [ready, allItems, itemsById, requestKey, rowsSettled]);
3213
+ useEffect(() => {
3214
+ if (!ready || fitKey === null || fitKey === fittedKey.current) return;
3215
+ fittedKey.current = fitKey;
3216
+ fittedAmong.current = new Set(itemsRef.current.keys());
3217
+ controllerRef.current?.fitToItems(fitItemIdsRef.current);
3218
+ }, [ready, fitKey]);
3219
+ useEffect(() => {
3220
+ if (!ready || optionsKey === appliedOptions.current) return;
3221
+ appliedOptions.current = optionsKey;
3222
+ setFailure(null);
3223
+ const controller = controllerRef.current;
3224
+ const update = () => controller && setCanSaveImage(controller.canSaveImage);
3225
+ void controller?.setOptions(optionsRef.current).then(update, update);
3226
+ }, [ready, optionsKey]);
3227
+ const cameraKey = camera ? JSON.stringify(camera) : null;
3228
+ const shownCamera = useRef(cameraKey);
3229
+ useEffect(() => {
3230
+ if (!ready || !camera || cameraKey === shownCamera.current) return;
3231
+ shownCamera.current = cameraKey;
3232
+ controllerRef.current?.setCamera(camera, true);
3233
+ }, [ready, cameraKey]);
3234
+ const printLegend = [
3235
+ ...mapTables.legend.map((entry) => ({ label: entry.label, color: entry.color })),
3236
+ ...legend.map((entry) => ({ label: entry.label, color: entry.color }))
3237
+ ];
3238
+ const printRef = useRef({ legend: printLegend, title: printTitle ?? title, entries: printEntries, onError });
3239
+ printRef.current = { legend: printLegend, title: printTitle ?? title, entries: printEntries, onError };
3240
+ const printPopupRef = useRef(() => void 0);
3241
+ const printingRef = useRef(false);
3242
+ const runPrintJob = async (job) => {
3243
+ const controller = controllerRef.current;
3244
+ if (!controller || printingRef.current) return void 0;
3245
+ printingRef.current = true;
3246
+ setPrinting(true);
3247
+ try {
3248
+ return await job(controller);
3249
+ } catch (error) {
3250
+ printRef.current.onError?.(error instanceof Error ? error.message : String(error));
3251
+ return void 0;
3252
+ } finally {
3253
+ printingRef.current = false;
3254
+ setPrinting(false);
3255
+ }
3256
+ };
3257
+ const printMap = () => runPrintJob((controller) => {
3258
+ const { legend: shown, title: heading, entries: listed } = printRef.current;
3259
+ const entries = typeof listed === "function" ? listed() : listed;
3260
+ const popup2 = printPopupRef.current();
3261
+ return controller.print({
3262
+ ...heading && { title: heading },
3263
+ ...shown.length && { legend: shown },
3264
+ ...entries?.length && { entries },
3265
+ ...popup2 && { popup: popup2 }
3266
+ });
3267
+ }).then(() => void 0);
3268
+ const saveMapImage = async (fileName) => await runPrintJob(
3269
+ (controller) => controller.saveImage(fileName ?? printRef.current.title ?? "map", printRef.current.legend, printPopupRef.current())
3270
+ ) ?? false;
3271
+ const printable = ready && !failure;
3272
+ const onPrintChangeRef = useRef(onPrintChange);
3273
+ onPrintChangeRef.current = onPrintChange;
3274
+ const reportsPrint = !!onPrintChange;
3275
+ useEffect(() => {
3276
+ onPrintChangeRef.current?.(
3277
+ printable ? { canSaveImage, printing, print: () => void printMap(), saveImage: () => void saveMapImage() } : null
3278
+ );
3279
+ }, [printable, canSaveImage, printing, reportsPrint]);
3280
+ useEffect(() => () => onPrintChangeRef.current?.(null), []);
3281
+ useImperativeHandle(
3282
+ ref,
3283
+ () => ({
3284
+ setCamera: (target, animate = true) => controllerRef.current?.setCamera(target, animate),
3285
+ fitToItems: (ids) => controllerRef.current?.fitToItems(ids),
3286
+ openPopup: (id) => openItem(id),
3287
+ closePopup: () => setPopup(null),
3288
+ goToAddress: async (text) => await controllerRef.current?.goToAddress(text) ?? null,
3289
+ clearAddress: () => controllerRef.current?.clearAddress(),
3290
+ getView: () => controllerRef.current?.getView() ?? null,
3291
+ print: printMap,
3292
+ saveImage: saveMapImage,
3293
+ get native() {
3294
+ return controllerRef.current?.native ?? null;
3295
+ }
3296
+ }),
3297
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3298
+ []
3299
+ );
3300
+ const closePopup = () => {
3301
+ const id = popup?.kind === "item" ? popup.item.id : null;
3302
+ setPopup(null);
3303
+ if (id) controllerRef.current?.focusMarker(id);
3304
+ };
3305
+ const onPopupKeyDown = (event) => {
3306
+ if (event.key === "Escape") {
3307
+ event.stopPropagation();
3308
+ closePopup();
3309
+ }
3310
+ };
3311
+ const popupContent = (() => {
3312
+ if (!popup) return null;
3313
+ if (popup.kind === "list") {
3314
+ return /* @__PURE__ */ jsx(PopupCard, { title: t("cluster-list", [popup.items.length]), closeLabel: t("close"), onClose: closePopup, children: /* @__PURE__ */ jsx("ul", { className: styles.list, children: popup.items.map((item2) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3315
+ Button,
3316
+ {
3317
+ appearance: "subtle",
3318
+ size: "small",
3319
+ className: styles.listButton,
3320
+ onClick: () => {
3321
+ const event = itemClick(item2, popup.position);
3322
+ latest.current.onItemClick?.(event);
3323
+ setPopup(latest.current.showPopups && !event.defaultPrevented ? { kind: "item", item: item2, position: popup.position } : null);
3324
+ },
3325
+ children: item2.title || t("untitled")
3326
+ }
3327
+ ) }, item2.id)) }) });
3328
+ }
3329
+ const item = popup.item;
3330
+ const custom = renderPopup?.(item);
3331
+ if (renderPopup && (custom === null || custom === void 0 || custom === false)) return null;
3332
+ if (custom !== void 0 && custom !== null) {
3333
+ return /* @__PURE__ */ jsx(PopupCard, { label: item.title, closeLabel: t("close"), onClose: closePopup, children: custom });
3334
+ }
3335
+ const href = item.url ?? (isRecord(item.data) ? recordPageOf(item.data) : void 0);
3336
+ const link = href ? { href, onNavigate: (url) => latest.current.onNavigate?.(url), navigates: !!onNavigate } : void 0;
3337
+ if (isRecord(item.data)) {
3338
+ return /* @__PURE__ */ jsx(PopupCard, { title: item.title || t("untitled"), link, closeLabel: t("close"), onClose: closePopup, children: /* @__PURE__ */ jsx(
3339
+ PopupFields,
3340
+ {
3341
+ fields: popupFields(item.data, recordTitleColumn),
3342
+ distance: item.data.place?.distance != null && mapTables.near ? t("distance-away", [
3343
+ t(mapTables.near.unit === DistanceUnits.Kilometers ? "distance-kilometers" : "distance-miles", [
3344
+ formatDistance(Number(item.data.place.distance), locale)
3345
+ ])
3346
+ ]) : void 0
3347
+ }
3348
+ ) });
3349
+ }
3350
+ return item.title ? /* @__PURE__ */ jsx(PopupCard, { title: item.title, link, closeLabel: t("close"), onClose: closePopup }) : null;
3351
+ })();
3352
+ const shownPopup = popup && popupContent ? popup : null;
3353
+ printPopupRef.current = () => {
3354
+ if (!shownPopup) return void 0;
3355
+ if (shownPopup.kind === "list") {
3356
+ return { title: t("cluster-list", [shownPopup.items.length]), items: shownPopup.items.map((item2) => item2.title || t("untitled")) };
3357
+ }
3358
+ const item = shownPopup.item;
3359
+ if (renderPopup) return { fromPage: true };
3360
+ if (isRecord(item.data)) {
3361
+ const details = popupFields(item.data, recordTitleColumn).map(({ label, value }) => ({ label, value }));
3362
+ return { title: item.title || t("untitled"), ...details.length && { details } };
3363
+ }
3364
+ return item.title ? { title: item.title } : void 0;
3365
+ };
3366
+ useLayoutEffect(() => {
3367
+ const controller = controllerRef.current;
3368
+ const element = popupRef.current;
3369
+ if (!controller || !element) return;
3370
+ if (!shownPopup) {
3371
+ controller.hidePopup();
3372
+ controller.select(null);
3373
+ return;
3374
+ }
3375
+ const kind = shownPopup.kind === "item" ? itemKind(shownPopup.item) : "marker";
3376
+ controller.showPopup(element, shownPopup.position, kind);
3377
+ controller.select(shownPopup.kind === "item" ? shownPopup.item.id : null);
3378
+ }, [shownPopup, ready]);
3379
+ const truncated = currentRows?.response.pagingInfo?.moreRecords === true;
3380
+ const failureText = failure ? failure.kind === "unknown-provider" ? t("unknown-provider", [failure.message]) : t("failed") : null;
3381
+ const shownAddressStatus = address?.trim() ? addressStatus : null;
3382
+ const printButton = (inToolbar) => showPrintButton && printable && /* @__PURE__ */ jsx(
3383
+ MapPrintButton,
3384
+ {
3385
+ inToolbar,
3386
+ ...!inToolbar && { className: styles.printButton },
3387
+ canSaveImage,
3388
+ disabled: printing,
3389
+ onPrint: () => void printMap(),
3390
+ onSaveImage: () => void saveMapImage()
3391
+ }
3392
+ );
3393
+ const printsOverMap = showPrintButton && !multiTable;
3394
+ const mapElement = /* @__PURE__ */ jsxs(
3395
+ "div",
3396
+ {
3397
+ ref: rootRef,
3398
+ className: mergeClasses("ppp-map", printsOverMap && "ppp-map-printable", styles.root, className),
3399
+ style: { height: cssLength(height), width: cssLength(width), ...style },
3400
+ children: [
3401
+ /* @__PURE__ */ jsx(SharedTooltip, { containerRef: rootRef }),
3402
+ /* @__PURE__ */ jsx("div", { ref: hostRef, className: "ppp-map-host" }),
3403
+ printsOverMap && printable && /* @__PURE__ */ jsx("div", { className: "ppp-map-print", children: printButton(false) }),
3404
+ /* @__PURE__ */ jsx("div", { className: "ppp-map-popup-layer", children: /* @__PURE__ */ jsx(
3405
+ "div",
3406
+ {
3407
+ ref: popupRef,
3408
+ className: "ppp-map-popup-anchor",
3409
+ hidden: !popupContent,
3410
+ onKeyDown: onPopupKeyDown,
3411
+ onClick: (event) => event.stopPropagation(),
3412
+ children: popupContent
3413
+ }
3414
+ ) }),
3415
+ (!ready || shownAddressStatus === "searching") && !failure && /* @__PURE__ */ jsx("div", { className: styles.overlay, children: /* @__PURE__ */ jsx(Spinner, { size: "small", label: ready ? t("finding-address") : t("loading") }) }),
3416
+ failureText || rowsFailed || mapTables.failed ? /* @__PURE__ */ jsx("div", { className: styles.message, children: /* @__PURE__ */ jsx(MessageBar, { intent: "error", children: /* @__PURE__ */ jsx(MessageBarBody, { children: failureText ?? t("rows-failed") }) }) }) : (shownAddressStatus === "not-found" || shownAddressStatus === "failed") && /* @__PURE__ */ jsx("div", { className: styles.message, children: /* @__PURE__ */ jsx(MessageBar, { intent: shownAddressStatus === "failed" ? "error" : "warning", children: /* @__PURE__ */ jsx(MessageBarBody, { children: t(shownAddressStatus === "failed" ? "address-failed" : "address-not-found") }) }) }),
3417
+ (legend.length > 0 || mapTables.legend.length > 0 || truncated) && /* @__PURE__ */ jsxs("div", { className: styles.corner, children: [
3418
+ mapTables.legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: mergeClasses("ppp-map-legend", styles.legend), "aria-label": t("legend"), children: mapTables.legend.map((entry) => /* @__PURE__ */ jsxs("li", { className: styles.legendItem, children: [
3419
+ /* @__PURE__ */ jsx("span", { className: styles.legendDot, style: { backgroundColor: entry.color } }),
3420
+ /* @__PURE__ */ jsx(Text, { size: 200, children: entry.label }),
3421
+ entry.note && /* @__PURE__ */ jsx(Text, { size: 200, className: styles.legendNote, children: entry.note })
3422
+ ] }, entry.table)) }),
3423
+ legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: mergeClasses("ppp-map-legend", styles.legend), "aria-label": t("legend"), children: legend.map((entry) => /* @__PURE__ */ jsxs("li", { className: styles.legendItem, children: [
3424
+ /* @__PURE__ */ jsx("span", { className: styles.legendDot, style: { backgroundColor: entry.color } }),
3425
+ /* @__PURE__ */ jsx(Text, { size: 200, children: entry.label })
3426
+ ] }, entry.value ?? "none")) }),
3427
+ truncated && /* @__PURE__ */ jsx(Text, { size: 200, className: styles.note, children: t("rows-truncated", [recordItems.length]) })
3428
+ ] })
3429
+ ]
3430
+ }
3431
+ );
3432
+ if (!multiTable) return mapElement;
3433
+ return /* @__PURE__ */ jsxs("div", { className: mergeClasses("ppp-map-frame", styles.frame), children: [
3434
+ /* @__PURE__ */ jsx(
3435
+ MapTablesBar,
3436
+ {
3437
+ tables,
3438
+ defaultTables,
3439
+ defaultViewId,
3440
+ viewIds,
3441
+ regarding,
3442
+ allowFiltering,
3443
+ allowSavedViews,
3444
+ savedViewsKey,
3445
+ signInUrl,
3446
+ title,
3447
+ allowNearSearch,
3448
+ nearDistances,
3449
+ nearDistance,
3450
+ nearDistanceUnit,
3451
+ provider,
3452
+ addressSearchCountries,
3453
+ toolbarActions: printButton(true),
3454
+ onState: setTablesRead
3455
+ }
3456
+ ),
3457
+ mapElement
3458
+ ] });
3459
+ }
3460
+ function popupPosition(item) {
3461
+ if (item.kind === "circle") return item.center;
3462
+ if (item.kind === "marker") return item.position;
3463
+ const path = item.path;
3464
+ const latitude = path.reduce((sum, position) => sum + position.latitude, 0) / Math.max(1, path.length);
3465
+ const longitude = path.reduce((sum, position) => sum + position.longitude, 0) / Math.max(1, path.length);
3466
+ return { latitude, longitude };
3467
+ }
3468
+ function PopupCard({
3469
+ title,
3470
+ link,
3471
+ label,
3472
+ closeLabel,
3473
+ onClose,
3474
+ children
3475
+ }) {
3476
+ const styles = useStyles2();
3477
+ const onLinkClick = (event) => {
3478
+ if (!link?.navigates || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3479
+ event.preventDefault();
3480
+ link.onNavigate(link.href);
3481
+ };
3482
+ return /* @__PURE__ */ jsxs(Card, { className: mergeClasses("ppp-map-popup", styles.popup), size: "small", role: "dialog", "aria-label": title ?? label, children: [
3483
+ /* @__PURE__ */ jsxs("div", { className: styles.popupHeader, children: [
3484
+ title ? /* @__PURE__ */ jsx("div", { className: styles.popupTitle, children: link ? /* @__PURE__ */ jsx(Link, { href: link.href, onClick: onLinkClick, children: /* @__PURE__ */ jsx(Text, { weight: "semibold", children: title }) }) : /* @__PURE__ */ jsx(Text, { weight: "semibold", children: title }) }) : /* @__PURE__ */ jsx("div", { className: styles.popupBody, children }),
3485
+ /* @__PURE__ */ jsx(
3486
+ Button,
3487
+ {
3488
+ className: "ppp-map-popup-close",
3489
+ appearance: "subtle",
3490
+ size: "small",
3491
+ icon: /* @__PURE__ */ jsx(Dismiss16Regular, {}),
3492
+ "aria-label": closeLabel,
3493
+ "data-ppp-tooltip": closeLabel,
3494
+ onClick: onClose
3495
+ }
3496
+ )
3497
+ ] }),
3498
+ title ? children : null
3499
+ ] });
3500
+ }
3501
+ function popupFields(record, titleColumn) {
3502
+ const fields = record.place ? (record.place.details ?? []).map((field) => ({ key: field.column, label: field.label, value: field.value })) : record.columns.filter((column) => column.columnName !== titleColumn).map((column) => ({
3503
+ key: column.columnName,
3504
+ label: column.displayName,
3505
+ value: resolveColumnDisplayValue(record.record, column.columnName)
3506
+ }));
3507
+ return fields.filter((field) => !!field.value?.trim()).slice(0, 8);
3508
+ }
3509
+ function PopupFields({ fields, distance }) {
3510
+ const styles = useStyles2();
3511
+ if (!fields.length && !distance) return null;
3512
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3513
+ distance && /* @__PURE__ */ jsx(Text, { size: 200, className: mergeClasses("ppp-map-popup-distance", styles.fieldName), children: distance }),
3514
+ fields.length > 0 && /* @__PURE__ */ jsx("dl", { className: styles.fields, children: fields.map((field) => /* @__PURE__ */ jsxs("div", { style: { display: "contents" }, children: [
3515
+ /* @__PURE__ */ jsx("dt", { className: styles.fieldName, children: /* @__PURE__ */ jsx(Text, { size: 200, children: field.label }) }),
3516
+ /* @__PURE__ */ jsx("dd", { className: styles.fieldValue, children: /* @__PURE__ */ jsx(Text, { size: 200, children: field.value }) })
3517
+ ] }, field.key)) })
3518
+ ] });
3519
+ }
3520
+ var Map2 = forwardRef(MapImpl);
3521
+ var PLACE_PIN_COLOR = "var(--colorPaletteRedBackground3, #d13438)";
3522
+ function GridMap({ pins, focusedIds, onPinClick, onNavigate, onPrintChange, printTitle, printEntries, near, onViewChange }) {
3523
+ const focusKey = focusedIds.join(",");
3524
+ const pinsKey = JSON.stringify(pins);
3525
+ const nearKey = near ? JSON.stringify(near) : "";
3526
+ const items = useMemo(() => {
3527
+ const focused = new Set(focusedIds);
3528
+ const markers = pins.map((pin) => ({
3529
+ kind: "marker",
3530
+ id: pin.id,
3531
+ position: { latitude: pin.latitude, longitude: pin.longitude },
3532
+ ...pin.title && { title: pin.title },
3533
+ ...pin.url && { url: pin.url },
3534
+ ...pin.label && { label: pin.label },
3535
+ ...focused.has(pin.id) && { color: PLACE_PIN_COLOR }
3536
+ }));
3537
+ return near ? [...markers, ...nearPlaceItems(near)] : markers;
3538
+ }, [pinsKey, focusKey, nearKey]);
3539
+ const fitItemIds = useMemo(
3540
+ () => focusedIds.length > 0 ? [...focusedIds] : near ? [nearCircleId(near)] : [],
3541
+ [focusKey, nearKey]
3542
+ // eslint-disable-line react-hooks/exhaustive-deps
3543
+ );
3544
+ return /* @__PURE__ */ jsx(
3545
+ Map2,
3546
+ {
3547
+ className: "ppp-grid-map",
3548
+ items,
3549
+ fitItemIds,
3550
+ height: "100%",
3551
+ onItemClick: (event) => onPinClick(event.item.id),
3552
+ ...onNavigate && { onNavigate },
3553
+ ...onPrintChange && { onPrintChange },
3554
+ ...printTitle && { printTitle },
3555
+ ...printEntries && { printEntries },
3556
+ ...onViewChange && { onViewChanged: (view) => onViewChange(view.bounds) }
3557
+ }
3558
+ );
3559
+ }
3560
+ var useStyles3 = makeStyles({
3561
+ surface: {
3562
+ width: `min(800px, calc(100vw - ${tokens.spacingHorizontalXXXL}))`,
3563
+ maxWidth: "none"
3564
+ },
3565
+ content: {
3566
+ display: "flex",
3567
+ flexDirection: "column",
3568
+ rowGap: tokens.spacingVerticalS
3569
+ },
3570
+ address: {
3571
+ color: tokens.colorNeutralForeground2
3572
+ }
3573
+ });
3574
+ function MapsProvider({ children }) {
3575
+ const [request, setRequest] = useState(null);
3576
+ const service = useMemo(() => ({ show: setRequest }), []);
3577
+ return /* @__PURE__ */ jsx(AddressMapServiceContext.Provider, { value: service, children: /* @__PURE__ */ jsx(GridMapRendererContext.Provider, { value: GridMap, children: /* @__PURE__ */ jsxs(GridNearSearchContext.Provider, { value: NearSearch, children: [
3578
+ children,
3579
+ request && /* @__PURE__ */ jsx(AddressMapDialog, { request, onClose: () => setRequest(null) })
3580
+ ] }) }) });
3581
+ }
3582
+ function AddressMapDialog({ request, onClose }) {
3583
+ const styles = useStyles3();
3584
+ const t = useT();
3585
+ const oneLine = request.lines.join(", ");
3586
+ const { position } = request;
3587
+ const items = useMemo(
3588
+ () => position ? [{ kind: "marker", id: "address", position, title: oneLine || request.query, color: PLACE_PIN_COLOR }] : [],
3589
+ [position, oneLine, request.query]
3590
+ );
3591
+ return /* @__PURE__ */ jsx(Dialog, { open: true, onOpenChange: (_, data) => data.open ? void 0 : onClose(), children: /* @__PURE__ */ jsx(DialogSurface, { className: `ppp-address-map-dialog ${styles.surface}`, children: /* @__PURE__ */ jsxs(DialogBody, { children: [
3592
+ /* @__PURE__ */ jsx(DialogTitle, { children: request.title }),
3593
+ /* @__PURE__ */ jsxs(DialogContent, { className: styles.content, children: [
3594
+ oneLine && /* @__PURE__ */ jsx(Text, { className: styles.address, children: oneLine }),
3595
+ /* @__PURE__ */ jsx(Map2, { items, ...position ? {} : { address: request.query }, height: "min(60vh, 480px)" })
3596
+ ] }),
3597
+ /* @__PURE__ */ jsx(DialogActions, { children: /* @__PURE__ */ jsx(Button, { appearance: "primary", onClick: onClose, children: t("app.buttons.close.label") }) })
3598
+ ] }) }) });
3599
+ }
3600
+
3601
+ export { BUILT_IN_MAP_PROVIDERS, DEFAULT_LATITUDE_COLUMN, DEFAULT_LONGITUDE_COLUMN, DEFAULT_MAX_ROWS, DEFAULT_NEAR_DISTANCE, DEFAULT_NEAR_DISTANCES, DistanceUnits, Map2 as Map, MapsProvider, NearSearch, registerMapProvider };
3602
+ //# sourceMappingURL=index.js.map
3603
+ //# sourceMappingURL=index.js.map