maplibre-gl-basemap-control 0.2.2 → 0.4.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.
@@ -2,20 +2,23 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  const DEFAULT_BASEMAP_PROVIDERS = [
5
- { id: "openstreetmap", name: "OpenStreetMap", category: "Community" },
6
- { id: "google", name: "Google", category: "General" },
5
+ { id: "amazon", name: "Amazon Location", category: "General" },
7
6
  { id: "carto", name: "Carto", category: "General" },
8
7
  { id: "cyclosm", name: "CyclOSM", category: "Cycling" },
9
8
  { id: "esri", name: "ESRI", category: "Imagery" },
9
+ { id: "google", name: "Google", category: "General" },
10
+ { id: "mapbox", name: "Mapbox", category: "General" },
11
+ { id: "maptiler", name: "MapTiler", category: "General" },
10
12
  { id: "nasa-gibs", name: "NASA GIBS", category: "Imagery" },
13
+ { id: "nlmaps", name: "nlmaps", category: "Regional" },
14
+ { id: "openfreemap", name: "OpenFreeMap", category: "Vector Styles" },
11
15
  { id: "openrailwaymap", name: "OpenRailwayMap", category: "Transport" },
16
+ { id: "openstreetmap", name: "OpenStreetMap", category: "Community" },
12
17
  { id: "opentopomap", name: "OpenTopoMap", category: "Terrain" },
13
- { id: "openfreemap", name: "OpenFreeMap", category: "Vector Styles" },
14
18
  { id: "swisstopo", name: "Swiss Federal Geoportal", category: "Regional" },
15
19
  { id: "topplusopen", name: "TopPlusOpen", category: "Regional" },
16
20
  { id: "usgs", name: "USGS", category: "United States" },
17
- { id: "waymarkedtrails", name: "Waymarked Trails", category: "Outdoor" },
18
- { id: "nlmaps", name: "nlmaps", category: "Regional" }
21
+ { id: "waymarkedtrails", name: "Waymarked Trails", category: "Outdoor" }
19
22
  ];
20
23
  function rasterBasemap({
21
24
  id,
@@ -45,8 +48,99 @@ function rasterBasemap({
45
48
  tags
46
49
  };
47
50
  }
51
+ function styleBasemap({
52
+ id,
53
+ name,
54
+ provider,
55
+ category,
56
+ description,
57
+ attribution,
58
+ url,
59
+ view,
60
+ tags = []
61
+ }) {
62
+ return {
63
+ id,
64
+ name,
65
+ provider,
66
+ type: "style",
67
+ category,
68
+ description,
69
+ attribution,
70
+ source: {
71
+ type: "style",
72
+ url
73
+ },
74
+ view,
75
+ tags
76
+ };
77
+ }
78
+ function mapTilerStyleBasemap({
79
+ id,
80
+ name,
81
+ mapId,
82
+ category,
83
+ description,
84
+ url,
85
+ tags = []
86
+ }) {
87
+ return styleBasemap({
88
+ id,
89
+ name,
90
+ provider: "maptiler",
91
+ category,
92
+ description,
93
+ attribution: MAPTILER_ATTRIBUTION,
94
+ url: url ?? `https://api.maptiler.com/maps/${mapId}/style.json?key={api-key}`,
95
+ tags: ["maptiler", "vector", "style", ...tags]
96
+ });
97
+ }
98
+ function amazonStyleBasemap({
99
+ id,
100
+ name,
101
+ mapStyle,
102
+ category,
103
+ description,
104
+ tags = []
105
+ }) {
106
+ return styleBasemap({
107
+ id,
108
+ name,
109
+ provider: "amazon",
110
+ category,
111
+ description,
112
+ attribution: AMAZON_ATTRIBUTION,
113
+ url: `https://maps.geo.{aws-region}.amazonaws.com/v2/styles/${mapStyle}/descriptor?key={api-key}`,
114
+ tags: ["amazon", "aws", "location", "style", ...tags]
115
+ });
116
+ }
117
+ function mapboxStyleBasemap({
118
+ id,
119
+ name,
120
+ styleId,
121
+ category,
122
+ description,
123
+ tags = []
124
+ }) {
125
+ return styleBasemap({
126
+ id,
127
+ name,
128
+ provider: "mapbox",
129
+ category,
130
+ description,
131
+ attribution: MAPBOX_ATTRIBUTION,
132
+ url: `https://api.mapbox.com/styles/v1/mapbox/${styleId}?access_token={api-key}`,
133
+ tags: ["mapbox", "vector", "style", ...tags]
134
+ });
135
+ }
136
+ function sortProviders(providers) {
137
+ return [...providers].sort((a, b) => a.name.localeCompare(b.name));
138
+ }
139
+ const AMAZON_ATTRIBUTION = "© Amazon Location Service";
48
140
  const CARTO_ATTRIBUTION = "© OpenStreetMap contributors © CARTO";
49
141
  const ESRI_ATTRIBUTION = "Tiles © Esri and the GIS User Community";
142
+ const MAPBOX_ATTRIBUTION = "© Mapbox © OpenStreetMap contributors";
143
+ const MAPTILER_ATTRIBUTION = "© MapTiler © OpenStreetMap contributors";
50
144
  const OSM_ATTRIBUTION = "© OpenStreetMap contributors";
51
145
  const OPENFREEMAP_ATTRIBUTION = "OpenFreeMap © OpenMapTiles Data from OpenStreetMap";
52
146
  const DEFAULT_BASEMAPS = [
@@ -138,6 +232,216 @@ const DEFAULT_BASEMAPS = [
138
232
  maxzoom: 20,
139
233
  tags: ["google", "hybrid", "imagery"]
140
234
  }),
235
+ amazonStyleBasemap({
236
+ id: "amazon-standard",
237
+ name: "Amazon Standard",
238
+ mapStyle: "Standard",
239
+ category: "Street",
240
+ description: "Amazon Location general-purpose vector map style.",
241
+ tags: ["standard", "street"]
242
+ }),
243
+ amazonStyleBasemap({
244
+ id: "amazon-monochrome",
245
+ name: "Amazon Monochrome",
246
+ mapStyle: "Monochrome",
247
+ category: "Light",
248
+ description: "Amazon Location grey scale map style for overlays.",
249
+ tags: ["monochrome", "light", "dataviz"]
250
+ }),
251
+ amazonStyleBasemap({
252
+ id: "amazon-hybrid",
253
+ name: "Amazon Hybrid",
254
+ mapStyle: "Hybrid",
255
+ category: "Imagery",
256
+ description: "Amazon Location satellite imagery with road and label overlay.",
257
+ tags: ["hybrid", "satellite", "imagery", "labels"]
258
+ }),
259
+ amazonStyleBasemap({
260
+ id: "amazon-satellite",
261
+ name: "Amazon Satellite",
262
+ mapStyle: "Satellite",
263
+ category: "Imagery",
264
+ description: "Amazon Location satellite imagery map style.",
265
+ tags: ["satellite", "imagery"]
266
+ }),
267
+ mapboxStyleBasemap({
268
+ id: "mapbox-streets",
269
+ name: "Mapbox Streets",
270
+ styleId: "streets-v12",
271
+ category: "Street",
272
+ description: "Mapbox general-purpose street map style.",
273
+ tags: ["streets", "street"]
274
+ }),
275
+ mapboxStyleBasemap({
276
+ id: "mapbox-outdoors",
277
+ name: "Mapbox Outdoors",
278
+ styleId: "outdoors-v12",
279
+ category: "Outdoor",
280
+ description: "Mapbox outdoor recreation and terrain style.",
281
+ tags: ["outdoors", "terrain", "recreation"]
282
+ }),
283
+ mapboxStyleBasemap({
284
+ id: "mapbox-light",
285
+ name: "Mapbox Light",
286
+ styleId: "light-v11",
287
+ category: "Light",
288
+ description: "Light Mapbox style for data overlays.",
289
+ tags: ["light", "dataviz"]
290
+ }),
291
+ mapboxStyleBasemap({
292
+ id: "mapbox-dark",
293
+ name: "Mapbox Dark",
294
+ styleId: "dark-v11",
295
+ category: "Dark",
296
+ description: "Dark Mapbox style for high-contrast overlays.",
297
+ tags: ["dark"]
298
+ }),
299
+ mapboxStyleBasemap({
300
+ id: "mapbox-satellite",
301
+ name: "Mapbox Satellite",
302
+ styleId: "satellite-v9",
303
+ category: "Imagery",
304
+ description: "Mapbox satellite imagery style.",
305
+ tags: ["satellite", "imagery"]
306
+ }),
307
+ mapboxStyleBasemap({
308
+ id: "mapbox-satellite-streets",
309
+ name: "Mapbox Satellite Streets",
310
+ styleId: "satellite-streets-v12",
311
+ category: "Imagery",
312
+ description: "Mapbox satellite imagery with streets and labels.",
313
+ tags: ["satellite", "streets", "imagery", "labels"]
314
+ }),
315
+ mapboxStyleBasemap({
316
+ id: "mapbox-navigation-day",
317
+ name: "Mapbox Navigation Day",
318
+ styleId: "navigation-day-v1",
319
+ category: "Navigation",
320
+ description: "Mapbox daytime navigation style.",
321
+ tags: ["navigation", "day"]
322
+ }),
323
+ mapboxStyleBasemap({
324
+ id: "mapbox-navigation-night",
325
+ name: "Mapbox Navigation Night",
326
+ styleId: "navigation-night-v1",
327
+ category: "Navigation",
328
+ description: "Mapbox nighttime navigation style.",
329
+ tags: ["navigation", "night", "dark"]
330
+ }),
331
+ mapTilerStyleBasemap({
332
+ id: "maptiler-aquarelle",
333
+ name: "MapTiler Aquarelle",
334
+ mapId: "aquarelle-v4",
335
+ category: "Artistic",
336
+ description: "Watercolor-style vector basemap.",
337
+ tags: ["aquarelle", "watercolor", "artistic"]
338
+ }),
339
+ mapTilerStyleBasemap({
340
+ id: "maptiler-backdrop",
341
+ name: "MapTiler Backdrop",
342
+ mapId: "backdrop-v4",
343
+ category: "Light",
344
+ description: "Subtle monochrome context map.",
345
+ tags: ["backdrop", "monochrome", "context"]
346
+ }),
347
+ mapTilerStyleBasemap({
348
+ id: "maptiler-base",
349
+ name: "MapTiler Base",
350
+ mapId: "base-v4",
351
+ category: "Street",
352
+ description: "General-purpose vector basemap.",
353
+ tags: ["base", "street"]
354
+ }),
355
+ mapTilerStyleBasemap({
356
+ id: "maptiler-dataviz",
357
+ name: "MapTiler Dataviz",
358
+ mapId: "dataviz-v4",
359
+ category: "Light",
360
+ description: "Minimal basemap for data visualization and overlays.",
361
+ tags: ["dataviz", "data", "light"]
362
+ }),
363
+ mapTilerStyleBasemap({
364
+ id: "maptiler-landscape",
365
+ name: "MapTiler Landscape",
366
+ mapId: "landscape-v4",
367
+ category: "Terrain",
368
+ description: "Light terrain and hillshade map.",
369
+ tags: ["landscape", "terrain", "hillshade"]
370
+ }),
371
+ mapTilerStyleBasemap({
372
+ id: "maptiler-ocean",
373
+ name: "MapTiler Ocean",
374
+ mapId: "ocean-v4",
375
+ category: "Marine",
376
+ description: "Ocean bathymetry and marine-focused basemap.",
377
+ tags: ["ocean", "marine", "bathymetry"]
378
+ }),
379
+ mapTilerStyleBasemap({
380
+ id: "maptiler-openstreetmap",
381
+ name: "MapTiler OpenStreetMap",
382
+ mapId: "openstreetmap",
383
+ category: "Street",
384
+ description: "OpenStreetMap style hosted by MapTiler.",
385
+ url: "https://api.maptiler.com/maps/openstreetmap/style.json?key",
386
+ tags: ["openstreetmap", "osm", "street"]
387
+ }),
388
+ mapTilerStyleBasemap({
389
+ id: "maptiler-outdoor",
390
+ name: "MapTiler Outdoor",
391
+ mapId: "outdoor-v4",
392
+ category: "Outdoor",
393
+ description: "Outdoor recreation basemap with terrain detail.",
394
+ tags: ["outdoor", "terrain", "recreation"]
395
+ }),
396
+ mapTilerStyleBasemap({
397
+ id: "maptiler-satellite-hybrid",
398
+ name: "MapTiler Satellite Hybrid",
399
+ mapId: "hybrid-v4",
400
+ category: "Imagery",
401
+ description: "Satellite imagery with labels, roads, and borders.",
402
+ tags: ["satellite", "hybrid", "imagery", "labels"]
403
+ }),
404
+ mapTilerStyleBasemap({
405
+ id: "maptiler-satellite-plain",
406
+ name: "MapTiler Satellite Plain",
407
+ mapId: "satellite-v4",
408
+ category: "Imagery",
409
+ description: "Satellite imagery without the hybrid reference overlay.",
410
+ tags: ["satellite", "imagery"]
411
+ }),
412
+ mapTilerStyleBasemap({
413
+ id: "maptiler-streets",
414
+ name: "MapTiler Streets",
415
+ mapId: "streets-v4",
416
+ category: "Street",
417
+ description: "Detailed street map for general navigation.",
418
+ tags: ["streets", "street", "navigation"]
419
+ }),
420
+ mapTilerStyleBasemap({
421
+ id: "maptiler-toner",
422
+ name: "MapTiler Toner",
423
+ mapId: "toner-v2",
424
+ category: "Dark",
425
+ description: "High-contrast monochrome basemap.",
426
+ url: "https://api.maptiler.com/maps/toner-v2/style.json?key=",
427
+ tags: ["toner", "monochrome", "contrast"]
428
+ }),
429
+ mapTilerStyleBasemap({
430
+ id: "maptiler-topo",
431
+ name: "MapTiler Topo",
432
+ mapId: "topo-v4",
433
+ category: "Terrain",
434
+ description: "Topographic basemap with contours and terrain context.",
435
+ tags: ["topo", "topographic", "terrain"]
436
+ }),
437
+ mapTilerStyleBasemap({
438
+ id: "maptiler-winter",
439
+ name: "MapTiler Winter",
440
+ mapId: "winter-v4",
441
+ category: "Outdoor",
442
+ description: "Winter sports and mountain activity basemap.",
443
+ tags: ["winter", "skiing", "outdoor"]
444
+ }),
141
445
  rasterBasemap({
142
446
  id: "carto-positron",
143
447
  name: "Carto Positron",
@@ -750,7 +1054,7 @@ function combineProviders(defaults, custom = []) {
750
1054
  const byId = /* @__PURE__ */ new Map();
751
1055
  defaults.forEach((provider) => byId.set(provider.id, provider));
752
1056
  custom.forEach((provider) => byId.set(provider.id, provider));
753
- return [...byId.values()];
1057
+ return sortProviders([...byId.values()]);
754
1058
  }
755
1059
  function resolveBasemapProviders(basemaps, customProviders = [], includeDefaultProviders = true) {
756
1060
  const byId = /* @__PURE__ */ new Map();
@@ -763,7 +1067,7 @@ function resolveBasemapProviders(basemaps, customProviders = [], includeDefaultP
763
1067
  byId.set(basemap.provider, { id: basemap.provider, name: basemap.provider });
764
1068
  }
765
1069
  });
766
- return [...byId.values()];
1070
+ return sortProviders([...byId.values()]);
767
1071
  }
768
1072
  function createBasemapCatalog(customBasemaps = [], includeDefaultBasemaps = true) {
769
1073
  const basemaps = includeDefaultBasemaps ? [...DEFAULT_BASEMAPS, ...customBasemaps] : [...customBasemaps];
@@ -797,10 +1101,21 @@ const DEFAULT_OPTIONS = {
797
1101
  title: "Basemaps",
798
1102
  panelWidth: 340,
799
1103
  className: "",
800
- includeDefaultBasemaps: true
1104
+ includeDefaultBasemaps: true,
1105
+ allowMultiple: false,
1106
+ showMultipleToggle: true,
1107
+ resizable: true
801
1108
  };
802
1109
  const CONTROL_SOURCE_PREFIX = "maplibre-basemap-control-source";
803
1110
  const CONTROL_LAYER_PREFIX = "";
1111
+ const API_KEY_PLACEHOLDER = "{api-key}";
1112
+ const MAPTILER_API_KEY_EXAMPLE = "YOUR_MAPTILER_API_KEY";
1113
+ const MAPTILER_API_KEY_QUERY = "?key";
1114
+ const MAPTILER_API_KEY_EMPTY_QUERY = "?key=";
1115
+ const AWS_REGION_PLACEHOLDER = "{aws-region}";
1116
+ const MIN_PANEL_WIDTH = 240;
1117
+ const MIN_PANEL_HEIGHT = 200;
1118
+ const PANEL_VIEWPORT_MARGIN = 12;
804
1119
  class BasemapControl {
805
1120
  constructor(options) {
806
1121
  __publicField(this, "_map");
@@ -813,12 +1128,49 @@ class BasemapControl {
813
1128
  __publicField(this, "_basemaps");
814
1129
  __publicField(this, "_providers");
815
1130
  __publicField(this, "_eventHandlers", new globalThis.Map());
816
- __publicField(this, "_managedSourceIds", []);
817
- __publicField(this, "_managedLayerIds", []);
1131
+ // Active managed raster basemaps keyed by basemap id, in insertion order. In
1132
+ // single mode this holds at most one entry; in multiple mode it tracks every
1133
+ // stacked raster overlay so each can be added or removed independently.
1134
+ __publicField(this, "_managedRasters", new globalThis.Map());
1135
+ __publicField(this, "_mapTilerApiKey", "");
1136
+ __publicField(this, "_amazonApiKey", "");
1137
+ __publicField(this, "_awsRegion", "");
1138
+ __publicField(this, "_mapboxAccessToken", "");
1139
+ __publicField(this, "_providerSettingsCollapsed", true);
818
1140
  __publicField(this, "_resizeHandler", null);
819
1141
  __publicField(this, "_mapResizeHandler", null);
820
- __publicField(this, "_clickOutsideHandler", null);
1142
+ __publicField(this, "_resizeAnchor", null);
1143
+ __publicField(this, "_resizeHandleEl", null);
1144
+ __publicField(this, "_onResizeMove", (event) => {
1145
+ if (!this._panel || !this._resizeAnchor) return;
1146
+ const { x, y, isLeft, isTop } = this._resizeAnchor;
1147
+ const bounds = this._getResizeBounds();
1148
+ const rawWidth = isLeft ? event.clientX - x : x - event.clientX;
1149
+ const rawHeight = isTop ? event.clientY - y : y - event.clientY;
1150
+ const width = this._clampSize(rawWidth, MIN_PANEL_WIDTH, bounds.maxWidth);
1151
+ const height = this._clampSize(rawHeight, MIN_PANEL_HEIGHT, bounds.maxHeight);
1152
+ this._panel.style.width = `${width}px`;
1153
+ this._panel.style.height = `${height}px`;
1154
+ this._panel.classList.add("is-resized");
1155
+ this._state = { ...this._state, panelWidth: width, panelHeight: height };
1156
+ this._updatePanelPosition();
1157
+ });
1158
+ __publicField(this, "_onResizeEnd", (event) => {
1159
+ var _a, _b;
1160
+ window.removeEventListener("pointermove", this._onResizeMove);
1161
+ window.removeEventListener("pointerup", this._onResizeEnd);
1162
+ window.removeEventListener("pointercancel", this._onResizeEnd);
1163
+ (_b = (_a = this._resizeHandleEl) == null ? void 0 : _a.releasePointerCapture) == null ? void 0 : _b.call(_a, event.pointerId);
1164
+ this._resizeHandleEl = null;
1165
+ if (!this._resizeAnchor) return;
1166
+ this._resizeAnchor = null;
1167
+ this._emit({ type: "statechange", state: this.getState() });
1168
+ });
821
1169
  this._options = { ...DEFAULT_OPTIONS, ...options };
1170
+ this._mapTilerApiKey = (options == null ? void 0 : options.mapTilerApiKey) ?? "";
1171
+ this._amazonApiKey = (options == null ? void 0 : options.amazonApiKey) ?? "";
1172
+ this._awsRegion = (options == null ? void 0 : options.awsRegion) ?? "us-east-1";
1173
+ this._mapboxAccessToken = (options == null ? void 0 : options.mapboxAccessToken) ?? "";
822
1174
  this._basemaps = createBasemapCatalog(
823
1175
  options == null ? void 0 : options.basemaps,
824
1176
  this._options.includeDefaultBasemaps
@@ -832,6 +1184,8 @@ class BasemapControl {
832
1184
  collapsed: this._options.collapsed,
833
1185
  panelWidth: this._options.panelWidth,
834
1186
  activeBasemapId: this._options.defaultBasemapId,
1187
+ activeBasemapIds: this._options.defaultBasemapId ? [this._options.defaultBasemapId] : [],
1188
+ allowMultiple: this._options.allowMultiple,
835
1189
  query: "",
836
1190
  providerFilter: "",
837
1191
  categoryFilter: "",
@@ -852,7 +1206,8 @@ class BasemapControl {
852
1206
  }
853
1207
  this._renderContent();
854
1208
  if (this._state.activeBasemapId) {
855
- this._selectBasemap(this._state.activeBasemapId);
1209
+ this.setBasemap(this._state.activeBasemapId).catch(() => {
1210
+ });
856
1211
  }
857
1212
  return this._container;
858
1213
  }
@@ -866,10 +1221,11 @@ class BasemapControl {
866
1221
  this._map.off("resize", this._mapResizeHandler);
867
1222
  this._mapResizeHandler = null;
868
1223
  }
869
- if (this._clickOutsideHandler) {
870
- document.removeEventListener("click", this._clickOutsideHandler);
871
- this._clickOutsideHandler = null;
872
- }
1224
+ window.removeEventListener("pointermove", this._onResizeMove);
1225
+ window.removeEventListener("pointerup", this._onResizeEnd);
1226
+ window.removeEventListener("pointercancel", this._onResizeEnd);
1227
+ this._resizeAnchor = null;
1228
+ this._resizeHandleEl = null;
873
1229
  (_b = (_a = this._panel) == null ? void 0 : _a.parentNode) == null ? void 0 : _b.removeChild(this._panel);
874
1230
  (_d = (_c = this._container) == null ? void 0 : _c.parentNode) == null ? void 0 : _d.removeChild(this._container);
875
1231
  this._map = void 0;
@@ -884,9 +1240,24 @@ class BasemapControl {
884
1240
  }
885
1241
  setState(newState) {
886
1242
  this._state = { ...this._state, ...newState };
1243
+ this._syncActiveBasemapState(newState);
887
1244
  this._renderContent();
888
1245
  this._emit({ type: "statechange", state: this.getState() });
889
1246
  }
1247
+ // Keeps `activeBasemapId` and `activeBasemapIds` consistent when a caller
1248
+ // updates only one of them through setState (the React wrapper and external
1249
+ // consumers commonly set just `activeBasemapId`).
1250
+ _syncActiveBasemapState(applied) {
1251
+ const hasId = "activeBasemapId" in applied;
1252
+ const hasIds = "activeBasemapIds" in applied;
1253
+ if (hasId && !hasIds) {
1254
+ const id = this._state.activeBasemapId;
1255
+ this._state = { ...this._state, activeBasemapIds: id ? [id] : [] };
1256
+ } else if (hasIds && !hasId) {
1257
+ const ids = this._state.activeBasemapIds;
1258
+ this._state = { ...this._state, activeBasemapId: ids[ids.length - 1] };
1259
+ }
1260
+ }
890
1261
  getBasemaps() {
891
1262
  return [...this._basemaps];
892
1263
  }
@@ -897,16 +1268,87 @@ class BasemapControl {
897
1268
  this._options.providers,
898
1269
  this._options.includeDefaultBasemaps
899
1270
  );
900
- if (this._state.activeBasemapId && !this._basemaps.some((basemap) => basemap.id === this._state.activeBasemapId)) {
901
- this._state = { ...this._state, activeBasemapId: void 0 };
902
- }
1271
+ const activeBasemapIds = this._state.activeBasemapIds.filter(
1272
+ (id) => this._basemaps.some((basemap) => basemap.id === id)
1273
+ );
1274
+ this._state = {
1275
+ ...this._state,
1276
+ activeBasemapIds,
1277
+ activeBasemapId: this._state.activeBasemapId && activeBasemapIds.includes(this._state.activeBasemapId) ? this._state.activeBasemapId : activeBasemapIds[activeBasemapIds.length - 1]
1278
+ };
1279
+ this._renderContent();
1280
+ this._emit({ type: "statechange", state: this.getState() });
1281
+ }
1282
+ setMapTilerApiKey(apiKey) {
1283
+ this._mapTilerApiKey = apiKey;
1284
+ this._state = { ...this._state, error: void 0 };
1285
+ this._renderContent();
1286
+ this._emit({ type: "statechange", state: this.getState() });
1287
+ }
1288
+ setAmazonCredentials(apiKey, awsRegion = this._awsRegion) {
1289
+ this._amazonApiKey = apiKey;
1290
+ this._awsRegion = awsRegion;
1291
+ this._state = { ...this._state, error: void 0 };
1292
+ this._renderContent();
1293
+ this._emit({ type: "statechange", state: this.getState() });
1294
+ }
1295
+ setMapboxAccessToken(accessToken) {
1296
+ this._mapboxAccessToken = accessToken;
1297
+ this._state = { ...this._state, error: void 0 };
903
1298
  this._renderContent();
904
1299
  this._emit({ type: "statechange", state: this.getState() });
905
1300
  }
906
1301
  getActiveBasemap() {
907
1302
  return this._basemaps.find((basemap) => basemap.id === this._state.activeBasemapId);
908
1303
  }
1304
+ getActiveBasemaps() {
1305
+ return this._state.activeBasemapIds.map((id) => this._basemaps.find((basemap) => basemap.id === id)).filter((basemap) => Boolean(basemap));
1306
+ }
1307
+ isBasemapActive(id) {
1308
+ return this._state.activeBasemapIds.includes(id);
1309
+ }
1310
+ // Apply a basemap, replacing any previously active basemaps.
909
1311
  async setBasemap(id) {
1312
+ return this._applyBasemapChange(id, "replace");
1313
+ }
1314
+ // Add a raster basemap as an additional overlay without removing the active
1315
+ // ones. Style basemaps cannot stack, so they always replace the map style.
1316
+ async addBasemap(id) {
1317
+ return this._applyBasemapChange(id, "add");
1318
+ }
1319
+ // Remove a previously added managed raster basemap from the map.
1320
+ async removeBasemap(id) {
1321
+ const basemap = this._basemaps.find((candidate) => candidate.id === id);
1322
+ if (!basemap) {
1323
+ const error = new Error(`Basemap "${id}" was not found.`);
1324
+ this._handleError(error);
1325
+ throw error;
1326
+ }
1327
+ if (!this._map) {
1328
+ const error = new Error("BasemapControl must be added to a map before removing a basemap.");
1329
+ this._handleError(error, basemap);
1330
+ throw error;
1331
+ }
1332
+ const managedRaster = this._removeManagedRaster(id);
1333
+ const activeBasemapIds = this._state.activeBasemapIds.filter((value) => value !== id);
1334
+ this._state = {
1335
+ ...this._state,
1336
+ activeBasemapIds,
1337
+ activeBasemapId: activeBasemapIds[activeBasemapIds.length - 1],
1338
+ error: void 0
1339
+ };
1340
+ this._renderContent(true);
1341
+ this._emit({ type: "basemapremove", state: this.getState(), basemap, managedRaster });
1342
+ this._emit({ type: "statechange", state: this.getState() });
1343
+ }
1344
+ // Add the basemap if it is not active, otherwise remove it.
1345
+ async toggleBasemap(id) {
1346
+ if (this.isBasemapActive(id)) {
1347
+ return this.removeBasemap(id);
1348
+ }
1349
+ return this.addBasemap(id);
1350
+ }
1351
+ async _applyBasemapChange(id, mode) {
910
1352
  const basemap = this._basemaps.find((candidate) => candidate.id === id);
911
1353
  if (!basemap) {
912
1354
  const error = new Error(`Basemap "${id}" was not found.`);
@@ -918,46 +1360,69 @@ class BasemapControl {
918
1360
  this._handleError(error, basemap);
919
1361
  throw error;
920
1362
  }
1363
+ const isRaster = basemap.source.type === "raster";
1364
+ const effectiveMode = isRaster ? mode : "replace";
921
1365
  this._state = { ...this._state, loading: true, error: void 0 };
922
- this._renderContent();
1366
+ this._renderContent(true);
923
1367
  this._emit({ type: "statechange", state: this.getState() });
924
1368
  try {
925
1369
  let managedRaster;
926
- if (basemap.source.type === "raster") {
1370
+ if (isRaster) {
927
1371
  await this._waitForStyleReady();
928
- this._removeManagedBasemap();
1372
+ if (effectiveMode === "replace") {
1373
+ this._removeManagedBasemap();
1374
+ } else {
1375
+ this._removeManagedRaster(basemap.id);
1376
+ }
929
1377
  managedRaster = this._addRasterBasemap(basemap);
930
1378
  } else {
1379
+ const styleUrl = this._resolveStyleUrl(basemap);
1380
+ const styleOptions = this._getStyleOptions(basemap);
931
1381
  this._removeManagedBasemap();
932
- this._map.setStyle(basemap.source.url);
1382
+ if (styleOptions) {
1383
+ this._map.setStyle(styleUrl, styleOptions);
1384
+ } else {
1385
+ this._map.setStyle(styleUrl);
1386
+ }
933
1387
  }
934
1388
  this._applyBasemapView(basemap);
1389
+ const activeBasemapIds = effectiveMode === "add" ? [...this._state.activeBasemapIds.filter((value) => value !== basemap.id), basemap.id] : [basemap.id];
935
1390
  this._state = {
936
1391
  ...this._state,
937
1392
  activeBasemapId: basemap.id,
1393
+ activeBasemapIds,
938
1394
  loading: false,
939
1395
  error: void 0
940
1396
  };
941
- this._renderContent();
1397
+ this._renderContent(true);
942
1398
  this._emit({
943
1399
  type: "basemapchange",
944
1400
  state: this.getState(),
945
1401
  basemap,
946
- managedRaster
1402
+ managedRaster,
1403
+ mode: effectiveMode
947
1404
  });
948
1405
  this._emit({ type: "statechange", state: this.getState() });
949
1406
  } catch (cause) {
950
1407
  const error = cause instanceof Error ? cause : new Error(String(cause));
951
1408
  this._state = { ...this._state, loading: false, error: error.message };
952
- this._renderContent();
1409
+ this._renderContent(true);
953
1410
  this._handleError(error, basemap);
954
1411
  throw error;
955
1412
  }
956
1413
  }
957
- // setBasemap rethrows after emitting the error event so callers that await
958
- // it can react. Internal fire-and-forget callers use this instead to avoid
1414
+ // The public mutators rethrow after emitting the error event so callers that
1415
+ // await them can react. The panel's click handler uses this instead to avoid
959
1416
  // unhandled promise rejections; the failure is still reported via `error`.
1417
+ // In multiple mode a raster basemap click toggles the overlay on or off.
960
1418
  _selectBasemap(id) {
1419
+ const basemap = this._basemaps.find((candidate) => candidate.id === id);
1420
+ const isRaster = (basemap == null ? void 0 : basemap.source.type) === "raster";
1421
+ if (this._state.allowMultiple && isRaster) {
1422
+ this.toggleBasemap(id).catch(() => {
1423
+ });
1424
+ return;
1425
+ }
961
1426
  this.setBasemap(id).catch(() => {
962
1427
  });
963
1428
  }
@@ -1029,7 +1494,11 @@ class BasemapControl {
1029
1494
  _createPanel() {
1030
1495
  const panel = document.createElement("div");
1031
1496
  panel.className = "basemap-control-panel";
1032
- panel.style.width = `${this._options.panelWidth}px`;
1497
+ panel.style.width = `${this._state.panelWidth}px`;
1498
+ if (this._state.panelHeight !== void 0) {
1499
+ panel.style.height = `${this._state.panelHeight}px`;
1500
+ panel.classList.add("is-resized");
1501
+ }
1033
1502
  const header = document.createElement("div");
1034
1503
  header.className = "basemap-control-header";
1035
1504
  const title = document.createElement("span");
@@ -1047,18 +1516,37 @@ class BasemapControl {
1047
1516
  this._content.className = "basemap-control-content";
1048
1517
  panel.appendChild(header);
1049
1518
  panel.appendChild(this._content);
1519
+ if (this._options.resizable) {
1520
+ panel.appendChild(this._createResizeHandle("bottom-left"));
1521
+ panel.appendChild(this._createResizeHandle("bottom-right"));
1522
+ }
1050
1523
  return panel;
1051
1524
  }
1052
- _renderContent() {
1525
+ _createResizeHandle(corner) {
1526
+ const handle = document.createElement("div");
1527
+ handle.className = `basemap-control-resize-handle basemap-control-resize-${corner}`;
1528
+ handle.setAttribute("aria-hidden", "true");
1529
+ handle.addEventListener("pointerdown", (event) => this._startResize(event));
1530
+ return handle;
1531
+ }
1532
+ _renderContent(preserveResultsScroll = false) {
1533
+ var _a;
1053
1534
  if (!this._content) return;
1054
1535
  const categories = getBasemapCategories(this._basemaps);
1055
1536
  const results = this._getFilteredBasemaps();
1537
+ const previousResultsScrollTop = preserveResultsScroll ? (_a = this._content.querySelector(".basemap-control-results")) == null ? void 0 : _a.scrollTop : void 0;
1056
1538
  this._content.replaceChildren(
1057
1539
  this._createSearchRow(),
1540
+ ...this._options.showMultipleToggle ? [this._createMultipleToggleRow()] : [],
1541
+ ...this._hasProviderSettings() ? [this._createProviderSettingsSection()] : [],
1058
1542
  this._createFilterRow(this._providers, categories),
1059
1543
  this._createStatus(results.length),
1060
1544
  this._createResults(results)
1061
1545
  );
1546
+ if (previousResultsScrollTop !== void 0) {
1547
+ const resultsElement = this._content.querySelector(".basemap-control-results");
1548
+ if (resultsElement) resultsElement.scrollTop = previousResultsScrollTop;
1549
+ }
1062
1550
  }
1063
1551
  _renderFilteredResults() {
1064
1552
  var _a, _b;
@@ -1085,6 +1573,25 @@ class BasemapControl {
1085
1573
  row.appendChild(this._createBeforeIdInput());
1086
1574
  return row;
1087
1575
  }
1576
+ _createMultipleToggleRow() {
1577
+ const label = document.createElement("label");
1578
+ label.className = "basemap-control-multiple-toggle";
1579
+ const checkbox = document.createElement("input");
1580
+ checkbox.type = "checkbox";
1581
+ checkbox.className = "basemap-control-multiple-checkbox";
1582
+ checkbox.checked = this._state.allowMultiple;
1583
+ checkbox.setAttribute("aria-label", "Add basemaps instead of replacing");
1584
+ checkbox.addEventListener("change", () => {
1585
+ this._state = { ...this._state, allowMultiple: checkbox.checked };
1586
+ this._emit({ type: "statechange", state: this.getState() });
1587
+ });
1588
+ const text = document.createElement("span");
1589
+ text.className = "basemap-control-multiple-toggle-text";
1590
+ text.textContent = "Add basemaps (stack instead of replace)";
1591
+ label.appendChild(checkbox);
1592
+ label.appendChild(text);
1593
+ return label;
1594
+ }
1088
1595
  _createSearchInput() {
1089
1596
  const wrapper = document.createElement("div");
1090
1597
  wrapper.className = "basemap-control-search";
@@ -1118,6 +1625,106 @@ class BasemapControl {
1118
1625
  wrapper.appendChild(input);
1119
1626
  return wrapper;
1120
1627
  }
1628
+ _createProviderSettingsSection() {
1629
+ const details = document.createElement("details");
1630
+ details.className = "basemap-control-provider-settings";
1631
+ details.open = !this._providerSettingsCollapsed;
1632
+ details.addEventListener("toggle", () => {
1633
+ this._providerSettingsCollapsed = !details.open;
1634
+ });
1635
+ const summary = document.createElement("summary");
1636
+ summary.className = "basemap-control-provider-settings-summary";
1637
+ summary.textContent = "Provider settings";
1638
+ details.appendChild(summary);
1639
+ const fields = document.createElement("div");
1640
+ fields.className = "basemap-control-provider-settings-fields";
1641
+ if (this._hasMapTilerBasemaps()) {
1642
+ fields.appendChild(
1643
+ this._createProviderSettingsInput({
1644
+ className: "basemap-control-maptiler-key",
1645
+ type: "password",
1646
+ placeholder: "MapTiler API key",
1647
+ ariaLabel: "MapTiler API key",
1648
+ value: this._mapTilerApiKey,
1649
+ onInput: (value) => {
1650
+ this._mapTilerApiKey = value;
1651
+ }
1652
+ })
1653
+ );
1654
+ }
1655
+ if (this._hasMapboxBasemaps()) {
1656
+ fields.appendChild(
1657
+ this._createProviderSettingsInput({
1658
+ className: "basemap-control-mapbox-token",
1659
+ type: "password",
1660
+ placeholder: "Mapbox access token",
1661
+ ariaLabel: "Mapbox access token",
1662
+ value: this._mapboxAccessToken,
1663
+ onInput: (value) => {
1664
+ this._mapboxAccessToken = value;
1665
+ }
1666
+ })
1667
+ );
1668
+ }
1669
+ if (this._hasAmazonBasemaps()) {
1670
+ fields.appendChild(
1671
+ this._createProviderSettingsInput({
1672
+ className: "basemap-control-amazon-key",
1673
+ type: "password",
1674
+ placeholder: "Amazon API key",
1675
+ ariaLabel: "Amazon API key",
1676
+ value: this._amazonApiKey,
1677
+ onInput: (value) => {
1678
+ this._amazonApiKey = value;
1679
+ }
1680
+ })
1681
+ );
1682
+ fields.appendChild(
1683
+ this._createProviderSettingsInput({
1684
+ className: "basemap-control-aws-region",
1685
+ type: "text",
1686
+ placeholder: "AWS region",
1687
+ ariaLabel: "AWS region",
1688
+ value: this._awsRegion,
1689
+ onInput: (value) => {
1690
+ this._awsRegion = value;
1691
+ }
1692
+ })
1693
+ );
1694
+ }
1695
+ details.appendChild(fields);
1696
+ return details;
1697
+ }
1698
+ _createProviderSettingsInput({
1699
+ className,
1700
+ type,
1701
+ placeholder,
1702
+ ariaLabel,
1703
+ value,
1704
+ onInput
1705
+ }) {
1706
+ const wrapper = document.createElement("div");
1707
+ wrapper.className = className;
1708
+ const input = document.createElement("input");
1709
+ input.className = "basemap-control-input";
1710
+ input.type = type;
1711
+ input.placeholder = placeholder;
1712
+ input.value = value;
1713
+ input.name = ariaLabel.toLowerCase().replace(/\s+/g, "-");
1714
+ input.autocomplete = "new-password";
1715
+ input.autocapitalize = "none";
1716
+ input.spellcheck = false;
1717
+ input.setAttribute("autocorrect", "off");
1718
+ input.setAttribute("aria-label", ariaLabel);
1719
+ input.addEventListener("input", () => {
1720
+ onInput(input.value);
1721
+ this._state = { ...this._state, error: void 0 };
1722
+ this._renderFilteredResults();
1723
+ this._emit({ type: "statechange", state: this.getState() });
1724
+ });
1725
+ wrapper.appendChild(input);
1726
+ return wrapper;
1727
+ }
1121
1728
  _createFilterRow(providers, categories) {
1122
1729
  const row = document.createElement("div");
1123
1730
  row.className = "basemap-control-filters";
@@ -1193,7 +1800,7 @@ class BasemapControl {
1193
1800
  row.type = "button";
1194
1801
  row.className = "basemap-control-result";
1195
1802
  row.dataset.basemapId = basemap.id;
1196
- if (basemap.id === this._state.activeBasemapId) {
1803
+ if (this._state.activeBasemapIds.includes(basemap.id)) {
1197
1804
  row.classList.add("is-active");
1198
1805
  row.setAttribute("aria-current", "true");
1199
1806
  }
@@ -1235,6 +1842,132 @@ class BasemapControl {
1235
1842
  template.innerHTML = value;
1236
1843
  return template.content.textContent ?? value;
1237
1844
  }
1845
+ _hasMapTilerBasemaps() {
1846
+ return this._basemaps.some((basemap) => basemap.provider === "maptiler");
1847
+ }
1848
+ _hasAmazonBasemaps() {
1849
+ return this._basemaps.some((basemap) => basemap.provider === "amazon");
1850
+ }
1851
+ _hasMapboxBasemaps() {
1852
+ return this._basemaps.some((basemap) => basemap.provider === "mapbox");
1853
+ }
1854
+ _hasProviderSettings() {
1855
+ return this._hasMapTilerBasemaps() || this._hasAmazonBasemaps() || this._hasMapboxBasemaps();
1856
+ }
1857
+ _resolveStyleUrl(basemap) {
1858
+ if (basemap.source.type === "raster") {
1859
+ throw new Error(`Basemap "${basemap.id}" is not a style basemap.`);
1860
+ }
1861
+ const url = basemap.source.url;
1862
+ if (basemap.provider === "amazon") {
1863
+ return this._resolveAmazonStyleUrl(url);
1864
+ }
1865
+ if (basemap.provider === "maptiler") {
1866
+ return this._resolveMapTilerStyleUrl(url);
1867
+ }
1868
+ if (basemap.provider === "mapbox") {
1869
+ return this._resolveMapboxStyleUrl(url);
1870
+ }
1871
+ return url;
1872
+ }
1873
+ _getStyleOptions(basemap) {
1874
+ if (basemap.provider === "mapbox") {
1875
+ return {
1876
+ validate: false,
1877
+ transformStyle: (_previousStyle, nextStyle) => this._transformMapboxStyle(nextStyle, this._mapboxAccessToken.trim())
1878
+ };
1879
+ }
1880
+ return void 0;
1881
+ }
1882
+ _transformMapboxStyle(style, accessToken) {
1883
+ const encodedAccessToken = encodeURIComponent(accessToken);
1884
+ const sources = Object.fromEntries(
1885
+ Object.entries(style.sources).map(([id, source]) => {
1886
+ if ("url" in source && typeof source.url === "string") {
1887
+ return [id, { ...source, url: this._resolveMapboxInternalUrl(source.url, encodedAccessToken) }];
1888
+ }
1889
+ return [id, source];
1890
+ })
1891
+ );
1892
+ return {
1893
+ ...style,
1894
+ glyphs: typeof style.glyphs === "string" ? this._resolveMapboxInternalUrl(style.glyphs, encodedAccessToken) : style.glyphs,
1895
+ sprite: this._resolveMapboxSprite(style.sprite, encodedAccessToken),
1896
+ projection: { type: "mercator" },
1897
+ sources
1898
+ };
1899
+ }
1900
+ _resolveMapboxSprite(sprite, encodedAccessToken) {
1901
+ if (typeof sprite === "string") {
1902
+ return this._resolveMapboxInternalUrl(sprite, encodedAccessToken);
1903
+ }
1904
+ if (Array.isArray(sprite)) {
1905
+ return sprite.map((item) => ({
1906
+ ...item,
1907
+ url: this._resolveMapboxInternalUrl(item.url, encodedAccessToken)
1908
+ }));
1909
+ }
1910
+ return sprite;
1911
+ }
1912
+ _resolveMapboxInternalUrl(url, encodedAccessToken) {
1913
+ if (url.startsWith("mapbox://sprites/")) {
1914
+ const [, owner, styleId] = /^mapbox:\/\/sprites\/([^/]+)\/(.+)$/.exec(url) ?? [];
1915
+ if (owner && styleId) {
1916
+ return `https://api.mapbox.com/styles/v1/${owner}/${styleId}/sprite?access_token=${encodedAccessToken}`;
1917
+ }
1918
+ }
1919
+ if (url.startsWith("mapbox://fonts/")) {
1920
+ const [, owner, fontPath] = /^mapbox:\/\/fonts\/([^/]+)\/(.+)$/.exec(url) ?? [];
1921
+ if (owner && fontPath) {
1922
+ return `https://api.mapbox.com/fonts/v1/${owner}/${fontPath}?access_token=${encodedAccessToken}`;
1923
+ }
1924
+ }
1925
+ if (url.startsWith("mapbox://")) {
1926
+ const tileset = url.replace(/^mapbox:\/\//, "");
1927
+ return `https://api.mapbox.com/v4/${tileset}.json?secure&access_token=${encodedAccessToken}`;
1928
+ }
1929
+ return url;
1930
+ }
1931
+ _resolveMapTilerStyleUrl(url) {
1932
+ const needsMapTilerKey = url.includes(API_KEY_PLACEHOLDER) || url.includes(MAPTILER_API_KEY_EXAMPLE) || url.endsWith(MAPTILER_API_KEY_QUERY) || url.endsWith(MAPTILER_API_KEY_EMPTY_QUERY);
1933
+ if (!needsMapTilerKey) return url;
1934
+ const apiKey = this._mapTilerApiKey.trim();
1935
+ if (!apiKey) {
1936
+ throw new Error("Enter a MapTiler API key before applying this basemap.");
1937
+ }
1938
+ const encodedApiKey = encodeURIComponent(apiKey);
1939
+ if (url.endsWith(MAPTILER_API_KEY_QUERY)) {
1940
+ return `${url}=${encodedApiKey}`;
1941
+ }
1942
+ if (url.endsWith(MAPTILER_API_KEY_EMPTY_QUERY)) {
1943
+ return `${url}${encodedApiKey}`;
1944
+ }
1945
+ return url.split(API_KEY_PLACEHOLDER).join(encodedApiKey).split(MAPTILER_API_KEY_EXAMPLE).join(encodedApiKey);
1946
+ }
1947
+ _resolveAmazonStyleUrl(url) {
1948
+ const apiKey = this._amazonApiKey.trim();
1949
+ const awsRegion = this._awsRegion.trim();
1950
+ if (url.includes(API_KEY_PLACEHOLDER) && !apiKey) {
1951
+ throw new Error("Enter an Amazon API key before applying this basemap.");
1952
+ }
1953
+ if (url.includes(AWS_REGION_PLACEHOLDER) && !awsRegion) {
1954
+ throw new Error("Enter an AWS region before applying this basemap.");
1955
+ }
1956
+ return url.split(AWS_REGION_PLACEHOLDER).join(awsRegion).split(API_KEY_PLACEHOLDER).join(encodeURIComponent(apiKey));
1957
+ }
1958
+ _resolveMapboxStyleUrl(url) {
1959
+ const accessToken = this._mapboxAccessToken.trim();
1960
+ if (url.includes(API_KEY_PLACEHOLDER) && !accessToken) {
1961
+ throw new Error("Enter a Mapbox access token before applying this basemap.");
1962
+ }
1963
+ if (this._isUrlLikeCredential(accessToken)) {
1964
+ throw new Error("Enter a valid Mapbox access token, not a URL.");
1965
+ }
1966
+ return url.split(API_KEY_PLACEHOLDER).join(encodeURIComponent(accessToken));
1967
+ }
1968
+ _isUrlLikeCredential(value) {
1969
+ return /^https?:\/\//i.test(value);
1970
+ }
1238
1971
  _addRasterBasemap(basemap) {
1239
1972
  if (!this._map || basemap.source.type !== "raster") return void 0;
1240
1973
  const sourceId = `${CONTROL_SOURCE_PREFIX}-${basemap.id}`;
@@ -1256,13 +1989,9 @@ class BasemapControl {
1256
1989
  };
1257
1990
  this._map.addSource(sourceId, source);
1258
1991
  this._map.addLayer(layer, beforeId);
1259
- this._managedSourceIds = [sourceId];
1260
- this._managedLayerIds = [layerId];
1261
- return {
1262
- sourceId,
1263
- layerId,
1264
- beforeId
1265
- };
1992
+ const managed = { sourceId, layerId, beforeId };
1993
+ this._managedRasters.set(basemap.id, managed);
1994
+ return managed;
1266
1995
  }
1267
1996
  _waitForStyleReady() {
1268
1997
  if (!this._map) return Promise.resolve();
@@ -1271,32 +2000,43 @@ class BasemapControl {
1271
2000
  }
1272
2001
  return new Promise((resolve) => {
1273
2002
  var _a, _b;
2003
+ let settled = false;
2004
+ let timer;
1274
2005
  const done = () => {
1275
2006
  var _a2, _b2;
2007
+ if (settled) return;
2008
+ settled = true;
2009
+ if (timer !== void 0) clearTimeout(timer);
1276
2010
  (_a2 = this._map) == null ? void 0 : _a2.off("load", done);
1277
2011
  (_b2 = this._map) == null ? void 0 : _b2.off("style.load", done);
1278
2012
  resolve();
1279
2013
  };
2014
+ timer = setTimeout(done, 1500);
1280
2015
  (_a = this._map) == null ? void 0 : _a.once("load", done);
1281
2016
  (_b = this._map) == null ? void 0 : _b.once("style.load", done);
1282
2017
  });
1283
2018
  }
2019
+ // Remove every managed raster basemap from the map.
1284
2020
  _removeManagedBasemap() {
1285
2021
  if (!this._map) return;
1286
- [...this._managedLayerIds].reverse().forEach((layerId) => {
1287
- var _a;
1288
- if ((_a = this._map) == null ? void 0 : _a.getLayer(layerId)) {
1289
- this._map.removeLayer(layerId);
1290
- }
1291
- });
1292
- [...this._managedSourceIds].reverse().forEach((sourceId) => {
1293
- var _a;
1294
- if ((_a = this._map) == null ? void 0 : _a.getSource(sourceId)) {
1295
- this._map.removeSource(sourceId);
1296
- }
1297
- });
1298
- this._managedLayerIds = [];
1299
- this._managedSourceIds = [];
2022
+ for (const id of [...this._managedRasters.keys()]) {
2023
+ this._removeManagedRaster(id);
2024
+ }
2025
+ }
2026
+ // Remove a single managed raster basemap by its basemap id, returning the
2027
+ // managed source/layer record that was removed (if any).
2028
+ _removeManagedRaster(id) {
2029
+ var _a, _b;
2030
+ const managed = this._managedRasters.get(id);
2031
+ if (!managed) return void 0;
2032
+ if ((_a = this._map) == null ? void 0 : _a.getLayer(managed.layerId)) {
2033
+ this._map.removeLayer(managed.layerId);
2034
+ }
2035
+ if ((_b = this._map) == null ? void 0 : _b.getSource(managed.sourceId)) {
2036
+ this._map.removeSource(managed.sourceId);
2037
+ }
2038
+ this._managedRasters.delete(id);
2039
+ return managed;
1300
2040
  }
1301
2041
  _applyBasemapView(basemap) {
1302
2042
  if (!this._map || !basemap.view) return;
@@ -1314,13 +2054,6 @@ class BasemapControl {
1314
2054
  }
1315
2055
  _setupEventListeners() {
1316
2056
  var _a;
1317
- this._clickOutsideHandler = (e) => {
1318
- const target = e.target;
1319
- if (this._container && this._panel && !this._container.contains(target) && !this._panel.contains(target)) {
1320
- this.collapse();
1321
- }
1322
- };
1323
- document.addEventListener("click", this._clickOutsideHandler);
1324
2057
  this._resizeHandler = () => {
1325
2058
  if (!this._state.collapsed) this._updatePanelPosition();
1326
2059
  };
@@ -1375,6 +2108,44 @@ class BasemapControl {
1375
2108
  break;
1376
2109
  }
1377
2110
  }
2111
+ // Begin a panel resize from a bottom corner. The panel stays pinned to its
2112
+ // toggle button, so resizing is measured from the button-anchored corner:
2113
+ // dragging either bottom handle grows or shrinks the panel away from it.
2114
+ _startResize(event) {
2115
+ var _a;
2116
+ if (!this._panel) return;
2117
+ event.preventDefault();
2118
+ event.stopPropagation();
2119
+ const position = this._getControlPosition();
2120
+ const isLeft = position === "top-left" || position === "bottom-left";
2121
+ const isTop = position === "top-left" || position === "top-right";
2122
+ const rect = this._panel.getBoundingClientRect();
2123
+ this._resizeAnchor = {
2124
+ x: isLeft ? rect.left : rect.right,
2125
+ y: isTop ? rect.top : rect.bottom,
2126
+ isLeft,
2127
+ isTop
2128
+ };
2129
+ const handle = event.currentTarget;
2130
+ this._resizeHandleEl = handle;
2131
+ (_a = handle.setPointerCapture) == null ? void 0 : _a.call(handle, event.pointerId);
2132
+ window.addEventListener("pointermove", this._onResizeMove);
2133
+ window.addEventListener("pointerup", this._onResizeEnd);
2134
+ window.addEventListener("pointercancel", this._onResizeEnd);
2135
+ }
2136
+ _getResizeBounds() {
2137
+ var _a;
2138
+ const mapRect = (_a = this._mapContainer) == null ? void 0 : _a.getBoundingClientRect();
2139
+ const availableWidth = ((mapRect == null ? void 0 : mapRect.width) ?? window.innerWidth) - PANEL_VIEWPORT_MARGIN * 2;
2140
+ const availableHeight = ((mapRect == null ? void 0 : mapRect.height) ?? window.innerHeight) - PANEL_VIEWPORT_MARGIN * 2;
2141
+ return {
2142
+ maxWidth: Math.max(MIN_PANEL_WIDTH, availableWidth),
2143
+ maxHeight: Math.max(MIN_PANEL_HEIGHT, availableHeight)
2144
+ };
2145
+ }
2146
+ _clampSize(value, min, max) {
2147
+ return Math.round(Math.min(Math.max(value, min), max));
2148
+ }
1378
2149
  }
1379
2150
  export {
1380
2151
  BasemapControl as B,
@@ -1386,4 +2157,4 @@ export {
1386
2157
  getBasemapCategories as g,
1387
2158
  resolveBasemapProviders as r
1388
2159
  };
1389
- //# sourceMappingURL=BasemapControl-BBPUTp-_.js.map
2160
+ //# sourceMappingURL=BasemapControl-DCusTnvr.js.map