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