@superrouteca/smap 1.1.2 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.5 — 2026-10-28
4
+
5
+ - Map tools: the controls and panels floating on a map can be shown and hidden one by one or all at once. Register
6
+ each with `addTool` — a page element (an orders list, a legend, a search box), an engine control, or a zoom, locate,
7
+ full screen or scale control the SDK makes — and switch them with `setToolVisible` and `setToolsVisible`. A tools
8
+ control (one button, top right by default) lists every tool with *Show all* and *Hide all*. A hidden tool keeps its
9
+ state and comes back as it was. The map data credit and the sMap badge always stay. The starting state is given in
10
+ `tools.hidden` and every change is reported to `onToolsChange`, so the page decides whether and where to keep it.
11
+ A map without tools is unchanged.
12
+
3
13
  ## 1.1.2 — 2026-10-27
4
14
 
5
15
  - The version follows the shared 1.1.2 release; no change of its own.
package/README.md CHANGED
@@ -201,6 +201,36 @@ map.addPoints('stops', stops, { // {lng, lat, stop_number, status_colo
201
201
  Numbers and badges are drawn over all pins of the layer, so where pins overlap a lower pin's number can show through
202
202
  an upper pin.
203
203
 
204
+ ## Map tools: showing and hiding controls and panels
205
+
206
+ Register what floats on the map, and the map gets a tools control (one button, top right) that shows or hides each
207
+ tool, plus *Show all* and *Hide all*:
208
+
209
+ ```js
210
+ const map = new sMap.Map({ container: 'map', style: 'light', tools: { hidden: saved.hidden } });
211
+ map.addTool('zoom', 'navigation', { options: { showCompass: false } }); // made by the SDK
212
+ map.addTool('fullscreen', 'fullscreen');
213
+ map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' });
214
+ map.addTool('orders', document.getElementById('orders'), { label: 'Orders list', position: 'none' }); // stays in the page
215
+ map.onToolsChange((e) => save({ hidden: e.hidden }));
216
+
217
+ map.setToolVisible('legend', false);
218
+ map.setToolsVisible(true);
219
+ map.tools(); // [{ id: 'zoom', label: 'Zoom', visible: true }, …]
220
+ ```
221
+
222
+ - A tool is a page element, an engine control (`onAdd` / `onRemove`), or a kind the SDK makes: `navigation`,
223
+ `geolocate`, `fullscreen`, `scale`, and the mobile SDKs' names `zoom` (zoom buttons only), `compass` and `locate`. `position` is a corner, or `'none'` for an element that stays where the page put
224
+ it. Ids are 1–64 letters, digits, `-`, `_` or `.`; adding an id again replaces the first.
225
+ - A hidden tool is not drawn and keeps its state (an open list stays open, typed text stays). Hiding a switch does not
226
+ turn off what it switches.
227
+ - The map data credit, the sMap badge and the tools control are never tools: *Hide all* leaves them.
228
+ - `tools.control: false` leaves the tools control out (make your own buttons with the calls above);
229
+ `tools.labels: { button, showAll, hideAll }` translates it.
230
+ - The SDK keeps no state: give the starting state in `tools.hidden` and keep `onToolsChange`'s `hidden` where you
231
+ like (the page's storage, your server).
232
+ - The tools control joins the map with the first tool; a map without tools is unchanged.
233
+
204
234
  ## Style layers and sources
205
235
 
206
236
  Sources and layers of the Style Specification v8 are passed to the map as they are, so a layer copied from Mapbox
package/dist/SOURCE.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
- "sdkVersion": "1.1.2",
2
+ "sdkVersion": "1.1.5",
3
3
  "engine": "maplibre-gl 6.16.0",
4
4
  "files": {
5
- "smap.js": "78b2a10a4b7d5843c427d23d68f935a3917faadd62749a25f42f523274774a7b",
6
- "smap.css": "5e3bdcb00b8fc8805f545b76cdeea5e783c89984f48ec2fee66c4319af4ada0b",
7
- "smap-client.js": "02f6359fa4d359f205ce45e597f32f068fdb04448a2200274b166b228cd4eba8",
5
+ "smap.js": "d8658ace857ab90b2f2f3dc0af163104d3231e11c119af4a12a0d88b66f6ecae",
6
+ "smap.css": "acb9e11dca261a1f827e1d073e47e13b112b986ee744a712107ff83276d327e7",
7
+ "smap-client.js": "d101ab143691d1973399fb7b45fe396178dc96471048923077e57ce76b413a4a",
8
8
  "smap-client.d.ts": "5f0df11bf60c5a9ced34522a284de81260c27871678ab772f16da9ccf6883f58",
9
- "smap.mjs": "54a194d152497fd5730dfb55c2588610690d218d98a4c9ee47e065b4f709d612",
10
- "smap.cjs": "9d29085aebf24e10e701574e56459afa4758bcc4f04ea2ff99e4fecc2adc8a5b",
11
- "smap-client.cjs": "603d5603bce2e7d5912123d65c51ec83cd6b4db266a8c6b47a68b4a893621a6a",
12
- "smap-client.mjs": "6d4707ed18d301c24913f3b1d49c88401ff20da5425464b619b94aee42107cbf",
9
+ "smap.mjs": "db3e14cce3abb2f2dfdd17578de122ed7a633f8f885e1f5765dfbb92cf862e87",
10
+ "smap.cjs": "34ec49173c1e97b88510ffcfdf1b2db30fcfe28ccef5aa1e8e311e52f459b994",
11
+ "smap-client.cjs": "45d68a640f686d93e10c98468ed0c384bbc696da51193253217f6476d19ac6ed",
12
+ "smap-client.mjs": "45b241183a128671d0422c3684f5987cc1d1e1223fc1d95bcba16ba955c58862",
13
13
  "smap-client.d.cts": "e0fe4eb54cd66b831116082f42bd2f7de260f9f12a2741f4004f8cf10c252116",
14
14
  "smap-client.d.mts": "96830d316ddb33b4ff8edb8b6f810337479636114f75c4170d95038ffda74103",
15
15
  "vendor-licenses.txt": "ee5fc05a0677eaf69601d2c7db0d9ecd6cc27c3abc1d0733bc9ed34707cf8ef2"
@@ -1,5 +1,5 @@
1
1
  /* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
2
- /*! sMap JS client 1.1.2 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
2
+ /*! sMap JS client 1.1.5 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
3
3
  /*
4
4
  * Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
5
5
  * own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
@@ -19,7 +19,7 @@
19
19
  }(typeof self !== 'undefined' ? self : this, function () {
20
20
  'use strict';
21
21
 
22
- var VERSION = '1.1.2';
22
+ var VERSION = '1.1.5';
23
23
  var NODE_TTL = 3600 * 1000; // the measured order
24
24
  var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
25
25
 
@@ -1,4 +1,4 @@
1
- /*! sMap JS client 1.1.2 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
1
+ /*! sMap JS client 1.1.5 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
2
2
  /*
3
3
  * Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
4
4
  * own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
@@ -18,7 +18,7 @@
18
18
  }(typeof self !== 'undefined' ? self : this, function () {
19
19
  'use strict';
20
20
 
21
- var VERSION = '1.1.2';
21
+ var VERSION = '1.1.5';
22
22
  var NODE_TTL = 3600 * 1000; // the measured order
23
23
  var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
24
24
 
@@ -1,7 +1,7 @@
1
1
  /* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
2
2
  var smapClientModule = { exports: {} };
3
3
  (function (module, exports, define) {
4
- /*! sMap JS client 1.1.2 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
4
+ /*! sMap JS client 1.1.5 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
5
5
  /*
6
6
  * Runs wherever `fetch` exists: browsers, Node.js 18+, workers; elsewhere (mini programs, older runtimes) pass your
7
7
  * own `fetch`. UMD: `window.SMapClient`, `require('smap-client')`, or an AMD module.
@@ -21,7 +21,7 @@ var smapClientModule = { exports: {} };
21
21
  }(typeof self !== 'undefined' ? self : this, function () {
22
22
  'use strict';
23
23
 
24
- var VERSION = '1.1.2';
24
+ var VERSION = '1.1.5';
25
25
  var NODE_TTL = 3600 * 1000; // the measured order
26
26
  var KNOWN_TTL = 30 * 86400 * 1000; // the last node list received, used when none can be read
27
27
 
package/dist/smap.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
2
2
  'use strict';
3
- /*! sMap Web SDK 1.1.2. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
3
+ /*! sMap Web SDK 1.1.5. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
4
4
 
5
5
  var smapCore = null;
6
6
  function smapCreateEngine() {
@@ -28353,7 +28353,7 @@ function smapCreateSdk(engine, interop) {
28353
28353
  })(typeof self !== 'undefined' ? self : this, function (engine, interop) {
28354
28354
  'use strict';
28355
28355
 
28356
- var VERSION = '1.1.2';
28356
+ var VERSION = '1.1.5';
28357
28357
  var FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
28358
28358
  var BASEMAP_SOURCE = 'basemap';
28359
28359
  var BUILDINGS_LAYER = 'smap-buildings-3d';
@@ -28806,11 +28806,20 @@ function smapCreateSdk(engine, interop) {
28806
28806
  // True between a style's load and the next style change. The engine's own isStyleLoaded() turns false while any
28807
28807
  // source is loading data, which would leave layers added meanwhile waiting for a load that never comes.
28808
28808
  this._styleReady = false;
28809
+ // Map tools (M49-M57): the controls and panels floating on the map that the caller registered, in the order added.
28810
+ var toolOptions = options.tools || {};
28811
+ this._tools = [];
28812
+ this._toolsHidden = {};
28813
+ (toolOptions.hidden || []).forEach(function (id) { this._toolsHidden[id] = true; }, this);
28814
+ this._toolsControlAt = toolOptions.control === undefined ? 'top-right' : toolOptions.control;
28815
+ this._toolsLabels = toolOptions.labels || {};
28816
+ this._toolsControl = null;
28817
+ this._toolHandlers = [];
28809
28818
 
28810
28819
  var self = this;
28811
28820
  var engineOptions = {};
28812
28821
  for (var k in options) {
28813
- if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D'].indexOf(k) < 0) {
28822
+ if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D', 'tools'].indexOf(k) < 0) {
28814
28823
  engineOptions[k] = options[k];
28815
28824
  }
28816
28825
  }
@@ -28843,6 +28852,7 @@ function smapCreateSdk(engine, interop) {
28843
28852
  self.engine.addControl(badge, 'bottom-right');
28844
28853
  badge._place();
28845
28854
  }
28855
+ self._mountTools();
28846
28856
  self.engine.on('style.load', function () { self._styleReady = true; self._restore(); });
28847
28857
  self._keyboard3D();
28848
28858
  // One click handler for every layer's popup: the engine's hit test over those layers, the top feature opens.
@@ -29326,6 +29336,249 @@ function smapCreateSdk(engine, interop) {
29326
29336
  SMap.prototype.flyTo = function (o) { this.engine.flyTo(o); return this; };
29327
29337
  SMap.prototype.remove = function () { clearTimeout(this._refreshTimer); if (this.engine) { this.engine.remove(); } };
29328
29338
 
29339
+ // ------------------------------------------------------------------ map tools (M49-M57)
29340
+
29341
+ var TOOL_ID = /^[A-Za-z0-9._-]{1,64}$/;
29342
+ var TOOL_CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
29343
+ // The tools the SDK makes by name: the engine control, its English label (the caller may give another) and corner.
29344
+ var TOOL_KINDS = {
29345
+ navigation: { control: 'NavigationControl', label: 'Zoom', position: 'top-left' },
29346
+ geolocate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
29347
+ fullscreen: { control: 'FullscreenControl', label: 'Full screen', position: 'top-left' },
29348
+ scale: { control: 'ScaleControl', label: 'Scale', position: 'bottom-left' },
29349
+ // The mobile SDKs' names for the same controls, so one tool list serves every platform.
29350
+ zoom: { control: 'NavigationControl', label: 'Zoom', position: 'top-left', options: { showCompass: false } },
29351
+ compass: { control: 'NavigationControl', label: 'Compass', position: 'top-left', options: { showZoom: false } },
29352
+ locate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
29353
+ };
29354
+ var TOOLS_ICON = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false" fill="none" ' +
29355
+ 'stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h10M18 7h2M4 17h2M10 17h10"/>' +
29356
+ '<circle cx="16" cy="7" r="2"/><circle cx="8" cy="17" r="2"/></svg>';
29357
+ var TOOLS_TEXT = { button: 'Map tools', showAll: 'Show all', hideAll: 'Hide all' };
29358
+
29359
+ function isElement(v) { return !!v && typeof v === 'object' && v.nodeType === 1; }
29360
+
29361
+ // An engine control whose element the SDK can find again to show or hide it: the control's own onAdd, its element kept.
29362
+ function toolControl(tool, inner) {
29363
+ return {
29364
+ onAdd: function (m) { tool.el = inner.onAdd(m); return tool.el; },
29365
+ onRemove: function (m) { if (inner.onRemove) { inner.onRemove(m); } else if (tool.el && tool.el.parentNode) { tool.el.parentNode.removeChild(tool.el); } tool.el = null; },
29366
+ getDefaultPosition: function () { return tool.position; },
29367
+ };
29368
+ }
29369
+
29370
+ // A page element placed in a corner of the map: it moves into the corner and back where it was when removed.
29371
+ function elementControl(tool, element) {
29372
+ var home = null;
29373
+ var next = null;
29374
+ return {
29375
+ onAdd: function () {
29376
+ home = element.parentNode;
29377
+ next = element.nextSibling;
29378
+ element.classList.add('maplibregl-ctrl', 'smap-tool');
29379
+ tool.el = element;
29380
+ return element;
29381
+ },
29382
+ onRemove: function () {
29383
+ element.classList.remove('maplibregl-ctrl', 'smap-tool');
29384
+ if (home) { home.insertBefore(element, next && next.parentNode === home ? next : null); } else if (element.parentNode) { element.parentNode.removeChild(element); }
29385
+ tool.el = null;
29386
+ },
29387
+ getDefaultPosition: function () { return tool.position; },
29388
+ };
29389
+ }
29390
+
29391
+ /**
29392
+ * Registers a tool floating on the map, shown or hidden by setToolVisible / setToolsVisible and the tools control.
29393
+ * map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' })
29394
+ * map.addTool('zoom', 'navigation', { options: { showCompass: false } })
29395
+ * @param {string} id 1-64 letters, digits, '-', '_', '.'; adding an id again replaces the first
29396
+ * @param {Element|Object|string} target a page element, an engine control (onAdd / onRemove), or a kind the SDK
29397
+ * makes: 'navigation', 'geolocate', 'fullscreen', 'scale', or the mobile SDKs' 'zoom' (navigation without the
29398
+ * compass), 'compass' (without the zoom buttons) and 'locate' (geolocate)
29399
+ * @param {Object} [o] label (shown on its button), position ('top-left', 'top-right', 'bottom-left', 'bottom-right',
29400
+ * or 'none': an element stays where the page put it), options (of a kind's engine control)
29401
+ */
29402
+ SMap.prototype.addTool = function (id, target, o) {
29403
+ o = o || {};
29404
+ if (typeof id !== 'string' || !TOOL_ID.test(id)) { throw new Error('sMap: a tool id is 1-64 letters, digits, "-", "_" or "."'); }
29405
+ var kind = typeof target === 'string' ? TOOL_KINDS[target] : null;
29406
+ if (typeof target === 'string') {
29407
+ if (!kind) { throw new Error('sMap: unknown tool kind "' + target + '"'); }
29408
+ if (!engine[kind.control]) { throw new Error('sMap: the rendering engine has no ' + kind.control); }
29409
+ } else if (!isElement(target) && !(target && typeof target.onAdd === 'function')) {
29410
+ throw new Error('sMap: a tool is an element, an engine control or a tool kind');
29411
+ }
29412
+ var position = o.position || (kind ? kind.position : (isElement(target) ? (target.parentNode ? 'none' : 'top-left') : 'top-right'));
29413
+ if (position !== 'none' && TOOL_CORNERS.indexOf(position) < 0) { throw new Error('sMap: unknown tool position "' + position + '"'); }
29414
+ if (position === 'none' && !isElement(target)) { throw new Error('sMap: only an element may stay out of the map\'s corners'); }
29415
+ if (this._toolIndex(id) >= 0) { this.removeTool(id); }
29416
+ var tool = { id: id, label: String(o.label || (kind ? kind.label : id)), position: position, el: null, control: null };
29417
+ if (kind) { tool.control = toolControl(tool, new engine[kind.control](o.options || kind.options)); }
29418
+ else if (!isElement(target)) { tool.control = toolControl(tool, target); }
29419
+ else if (position !== 'none') { tool.control = elementControl(tool, target); }
29420
+ else { tool.el = target; }
29421
+ this._tools.push(tool);
29422
+ if (this.engine) { this._mountTool(tool); }
29423
+ this._renderToolsControl();
29424
+ return this;
29425
+ };
29426
+
29427
+ /** Removes a tool: an engine control leaves the map, a page element is shown again where the page put it. */
29428
+ SMap.prototype.removeTool = function (id) {
29429
+ var i = this._toolIndex(id);
29430
+ if (i < 0) { return this; }
29431
+ var tool = this._tools[i];
29432
+ this._tools.splice(i, 1);
29433
+ if (tool.el) { tool.el.classList.remove('smap-tool-hidden'); }
29434
+ if (tool.control && this.engine && tool.mounted) { this.engine.removeControl(tool.control); }
29435
+ this._renderToolsControl();
29436
+ return this;
29437
+ };
29438
+
29439
+ /** Shows or hides one tool. A hidden tool keeps its state and shows again as it was (M57). */
29440
+ SMap.prototype.setToolVisible = function (id, visible) {
29441
+ var tool = this._tools[this._toolIndex(id)];
29442
+ if (!tool || this._toolVisible(tool) === !!visible) { return this; }
29443
+ this._setHidden(tool, !visible);
29444
+ this._renderToolsControl();
29445
+ this._toolsChanged(id);
29446
+ return this;
29447
+ };
29448
+
29449
+ /** Shows or hides every tool. The map data credit, the sMap badge and the tools control always stay (M51). */
29450
+ SMap.prototype.setToolsVisible = function (visible) {
29451
+ var changed = false;
29452
+ this._tools.forEach(function (tool) {
29453
+ if (this._toolVisible(tool) !== !!visible) { this._setHidden(tool, !visible); changed = true; }
29454
+ }, this);
29455
+ if (changed) { this._renderToolsControl(); this._toolsChanged(null); }
29456
+ return this;
29457
+ };
29458
+
29459
+ /** The tools in the order added: [{ id, label, visible }]. */
29460
+ SMap.prototype.tools = function () {
29461
+ return this._tools.map(function (tool) { return { id: tool.id, label: tool.label, visible: this._toolVisible(tool) }; }, this);
29462
+ };
29463
+
29464
+ /** Calls handler({ hidden: [ids], changed: id | null }) after every change; the SDK keeps no state itself (M53). */
29465
+ SMap.prototype.onToolsChange = function (handler) { this._toolHandlers.push(handler); return this; };
29466
+ SMap.prototype.offToolsChange = function (handler) {
29467
+ this._toolHandlers = this._toolHandlers.filter(function (h) { return h !== handler; });
29468
+ return this;
29469
+ };
29470
+
29471
+ SMap.prototype._toolIndex = function (id) {
29472
+ for (var i = 0; i < this._tools.length; i++) { if (this._tools[i].id === id) { return i; } }
29473
+ return -1;
29474
+ };
29475
+ SMap.prototype._toolVisible = function (tool) { return !this._toolsHidden[tool.id]; };
29476
+ SMap.prototype._setHidden = function (tool, hidden) {
29477
+ if (hidden) { this._toolsHidden[tool.id] = true; } else { delete this._toolsHidden[tool.id]; }
29478
+ if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', hidden); }
29479
+ };
29480
+ SMap.prototype._toolsChanged = function (id) {
29481
+ var hidden = this._tools.filter(function (t) { return !this._toolVisible(t); }, this).map(function (t) { return t.id; });
29482
+ this._toolHandlers.slice().forEach(function (h) { h({ hidden: hidden, changed: id }); });
29483
+ };
29484
+
29485
+ SMap.prototype._mountTool = function (tool) {
29486
+ // The tools control joins the map with its first tool, so a map without tools is exactly as before.
29487
+ if (this._toolsControlAt && !this._toolsControl && this.engine.addControl) {
29488
+ this._toolsControl = toolsControl(this);
29489
+ this.engine.addControl(this._toolsControl, this._toolsControlAt);
29490
+ }
29491
+ if (tool.control && this.engine.addControl) { this.engine.addControl(tool.control, tool.position); tool.mounted = true; }
29492
+ if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', !this._toolVisible(tool)); }
29493
+ };
29494
+
29495
+ // Called once the engine exists: every tool registered before it.
29496
+ SMap.prototype._mountTools = function () {
29497
+ this._tools.forEach(function (tool) { this._mountTool(tool); }, this);
29498
+ this._renderToolsControl();
29499
+ };
29500
+ SMap.prototype._renderToolsControl = function () { if (this._toolsControl) { this._toolsControl._render(); } };
29501
+
29502
+ // The tools control: one button that opens a panel with one toggle per tool (pressed = shown), Show all and Hide all.
29503
+ // It is not a tool and is never hidden; it joins the map with the first tool and is not drawn while none is left.
29504
+ function toolsControl(map) {
29505
+ var el = null;
29506
+ var button = null;
29507
+ var panel = null;
29508
+ var drawn = null;
29509
+ var text = {};
29510
+ Object.keys(TOOLS_TEXT).forEach(function (k) { text[k] = map._toolsLabels[k] || TOOLS_TEXT[k]; });
29511
+ function open(on) {
29512
+ panel.hidden = !on;
29513
+ button.setAttribute('aria-expanded', on ? 'true' : 'false');
29514
+ }
29515
+ function close() { if (panel && !panel.hidden) { open(false); } }
29516
+ function key(e) { if (e.key === 'Escape' && !panel.hidden) { open(false); button.focus(); } }
29517
+ function make(doc, tag, className, label) {
29518
+ var b = doc.createElement(tag);
29519
+ b.className = className;
29520
+ if (tag === 'button') { b.type = 'button'; }
29521
+ if (label !== undefined) { b.textContent = label; }
29522
+ return b;
29523
+ }
29524
+ var control = {
29525
+ onAdd: function (m) {
29526
+ var doc = m.getContainer().ownerDocument;
29527
+ el = make(doc, 'div', 'maplibregl-ctrl maplibregl-ctrl-group smap-tools');
29528
+ button = make(doc, 'button', 'smap-tools-button');
29529
+ button.innerHTML = TOOLS_ICON;
29530
+ button.title = text.button;
29531
+ button.setAttribute('aria-label', text.button);
29532
+ button.setAttribute('aria-haspopup', 'true');
29533
+ button.addEventListener('click', function () { open(panel.hidden); });
29534
+ panel = make(doc, 'div', 'smap-tools-panel');
29535
+ panel.setAttribute('role', 'group');
29536
+ panel.setAttribute('aria-label', text.button);
29537
+ el.appendChild(button);
29538
+ el.appendChild(panel);
29539
+ el.addEventListener('keydown', key);
29540
+ open(false);
29541
+ m.on('click', close);
29542
+ return el;
29543
+ },
29544
+ onRemove: function (m) {
29545
+ m.off('click', close);
29546
+ if (el && el.parentNode) { el.parentNode.removeChild(el); }
29547
+ el = null;
29548
+ drawn = null;
29549
+ },
29550
+ _render: function () {
29551
+ if (!el) { return; }
29552
+ var doc = el.ownerDocument;
29553
+ el.classList.toggle('smap-tools-empty', map._tools.length === 0);
29554
+ // The same tools as last time: only the pressed states change, so a toggle keeps the keyboard focus.
29555
+ var signature = JSON.stringify(map._tools.map(function (t) { return [t.id, t.label]; }));
29556
+ if (signature === drawn) {
29557
+ map._tools.forEach(function (tool, i) { panel.children[i].setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false'); });
29558
+ return;
29559
+ }
29560
+ drawn = signature;
29561
+ while (panel.firstChild) { panel.removeChild(panel.firstChild); }
29562
+ map._tools.forEach(function (tool) {
29563
+ var item = make(doc, 'button', 'smap-tools-item', tool.label);
29564
+ item.setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false');
29565
+ item.setAttribute('data-tool', tool.id);
29566
+ item.addEventListener('click', function () { map.setToolVisible(tool.id, !map._toolVisible(tool)); });
29567
+ panel.appendChild(item);
29568
+ });
29569
+ var all = make(doc, 'div', 'smap-tools-all');
29570
+ var show = make(doc, 'button', 'smap-tools-show-all', text.showAll);
29571
+ show.addEventListener('click', function () { map.setToolsVisible(true); });
29572
+ var hide = make(doc, 'button', 'smap-tools-hide-all', text.hideAll);
29573
+ hide.addEventListener('click', function () { map.setToolsVisible(false); });
29574
+ all.appendChild(show);
29575
+ all.appendChild(hide);
29576
+ panel.appendChild(all);
29577
+ },
29578
+ };
29579
+ return control;
29580
+ }
29581
+
29329
29582
  // ------------------------------------------------------------------ Style Specification sources and layers (M24)
29330
29583
 
29331
29584
  function clone(v) { return v === undefined ? undefined : JSON.parse(JSON.stringify(v)); }
@@ -30294,7 +30547,7 @@ function SMapMap(options) {
30294
30547
  SMapMap.prototype = core.Map.prototype;
30295
30548
  return new core.Map(options);
30296
30549
  }
30297
- var smapVersion = "1.1.2";
30550
+ var smapVersion = "1.1.5";
30298
30551
  var smapStyles = ['light', 'white', 'grayscale', 'dark', 'black'];
30299
30552
  /** The bundled rendering engine, for anything the SDK does not wrap (loaded on the first call). */
30300
30553
  function smapGetEngine() { return smapLoad().engine; }
package/dist/smap.css CHANGED
@@ -76,3 +76,32 @@
76
76
  padding: 0 4px; box-sizing: border-box; border: 2px solid; border-radius: 11px; color: #fff; font-size: 10px; font-weight: 700;
77
77
  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); }
78
78
  .smap-chooser-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
79
+
80
+ /* Map tools (M49-M57). A hidden tool is not drawn; its corner closes up. */
81
+ .smap-tool-hidden { display: none !important; }
82
+ .maplibregl-ctrl.smap-tools.smap-tools-empty { display: none; }
83
+ .smap-tools { position: relative; }
84
+ .smap-tools .smap-tools-button { display: flex; align-items: center; justify-content: center; color: #1e293b; }
85
+ .smap-tools .smap-tools-button[aria-expanded="true"] { color: #2563eb; }
86
+ .smap-tools-panel {
87
+ position: absolute; top: 0; z-index: 3; display: flex; flex-direction: column; gap: 4px;
88
+ min-width: 170px; max-height: 60vmin; overflow-y: auto; padding: 8px; box-sizing: border-box;
89
+ border-radius: 8px; background: #ffffff; color: #1e293b; box-shadow: 0 2px 10px rgba(15, 23, 42, 0.18);
90
+ font: 12px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
91
+ }
92
+ .smap-tools-panel[hidden] { display: none; }
93
+ .maplibregl-ctrl-top-right .smap-tools-panel, .maplibregl-ctrl-bottom-right .smap-tools-panel { right: 36px; }
94
+ .maplibregl-ctrl-top-left .smap-tools-panel, .maplibregl-ctrl-bottom-left .smap-tools-panel { left: 36px; }
95
+ .maplibregl-ctrl-bottom-right .smap-tools-panel, .maplibregl-ctrl-bottom-left .smap-tools-panel { top: auto; bottom: 0; }
96
+ .maplibregl-ctrl-group .smap-tools-panel button {
97
+ display: flex; align-items: center; width: 100%; height: auto; min-height: 28px; padding: 4px 8px; box-sizing: border-box;
98
+ border: 1px solid rgba(15, 23, 42, 0.15); border-radius: 6px; background: #ffffff; color: inherit;
99
+ font: inherit; text-align: left; white-space: nowrap; cursor: pointer;
100
+ }
101
+ .maplibregl-ctrl-group .smap-tools-panel button + button { border-top: 1px solid rgba(15, 23, 42, 0.15); }
102
+ .maplibregl-ctrl-group .smap-tools-panel .smap-tools-item[aria-pressed="true"] { border-color: #2563eb; background: #2563eb; color: #ffffff; }
103
+ .maplibregl-ctrl-group .smap-tools-panel button:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }
104
+ .smap-tools-all { display: flex; gap: 4px; margin-top: 4px; padding-top: 6px; border-top: 1px solid rgba(15, 23, 42, 0.12); }
105
+ .maplibregl-ctrl-group .smap-tools-all button { justify-content: center; text-align: center; }
106
+ .smap-style-dark .smap-tools-panel, .smap-style-black .smap-tools-panel { background: #1f2430; color: #e5e7eb; }
107
+ .smap-style-dark .smap-tools-panel button, .smap-style-black .smap-tools-panel button { background: #1f2430; border-color: rgba(229, 231, 235, 0.2); }
package/dist/smap.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! sMap Web SDK 1.1.2. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
1
+ /*! sMap Web SDK 1.1.5. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
2
2
  (function () {
3
3
  function smapCreateEngine() {
4
4
  var SMAP_BASE_URL = (typeof document !== 'undefined' && document.baseURI) || (typeof location !== 'undefined' && location.href) || 'about:blank';
@@ -28350,7 +28350,7 @@ function smapCreateSdk(engine, interop) {
28350
28350
  })(typeof self !== 'undefined' ? self : this, function (engine, interop) {
28351
28351
  'use strict';
28352
28352
 
28353
- var VERSION = '1.1.2';
28353
+ var VERSION = '1.1.5';
28354
28354
  var FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
28355
28355
  var BASEMAP_SOURCE = 'basemap';
28356
28356
  var BUILDINGS_LAYER = 'smap-buildings-3d';
@@ -28803,11 +28803,20 @@ function smapCreateSdk(engine, interop) {
28803
28803
  // True between a style's load and the next style change. The engine's own isStyleLoaded() turns false while any
28804
28804
  // source is loading data, which would leave layers added meanwhile waiting for a load that never comes.
28805
28805
  this._styleReady = false;
28806
+ // Map tools (M49-M57): the controls and panels floating on the map that the caller registered, in the order added.
28807
+ var toolOptions = options.tools || {};
28808
+ this._tools = [];
28809
+ this._toolsHidden = {};
28810
+ (toolOptions.hidden || []).forEach(function (id) { this._toolsHidden[id] = true; }, this);
28811
+ this._toolsControlAt = toolOptions.control === undefined ? 'top-right' : toolOptions.control;
28812
+ this._toolsLabels = toolOptions.labels || {};
28813
+ this._toolsControl = null;
28814
+ this._toolHandlers = [];
28806
28815
 
28807
28816
  var self = this;
28808
28817
  var engineOptions = {};
28809
28818
  for (var k in options) {
28810
- if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D'].indexOf(k) < 0) {
28819
+ if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D', 'tools'].indexOf(k) < 0) {
28811
28820
  engineOptions[k] = options[k];
28812
28821
  }
28813
28822
  }
@@ -28840,6 +28849,7 @@ function smapCreateSdk(engine, interop) {
28840
28849
  self.engine.addControl(badge, 'bottom-right');
28841
28850
  badge._place();
28842
28851
  }
28852
+ self._mountTools();
28843
28853
  self.engine.on('style.load', function () { self._styleReady = true; self._restore(); });
28844
28854
  self._keyboard3D();
28845
28855
  // One click handler for every layer's popup: the engine's hit test over those layers, the top feature opens.
@@ -29323,6 +29333,249 @@ function smapCreateSdk(engine, interop) {
29323
29333
  SMap.prototype.flyTo = function (o) { this.engine.flyTo(o); return this; };
29324
29334
  SMap.prototype.remove = function () { clearTimeout(this._refreshTimer); if (this.engine) { this.engine.remove(); } };
29325
29335
 
29336
+ // ------------------------------------------------------------------ map tools (M49-M57)
29337
+
29338
+ var TOOL_ID = /^[A-Za-z0-9._-]{1,64}$/;
29339
+ var TOOL_CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
29340
+ // The tools the SDK makes by name: the engine control, its English label (the caller may give another) and corner.
29341
+ var TOOL_KINDS = {
29342
+ navigation: { control: 'NavigationControl', label: 'Zoom', position: 'top-left' },
29343
+ geolocate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
29344
+ fullscreen: { control: 'FullscreenControl', label: 'Full screen', position: 'top-left' },
29345
+ scale: { control: 'ScaleControl', label: 'Scale', position: 'bottom-left' },
29346
+ // The mobile SDKs' names for the same controls, so one tool list serves every platform.
29347
+ zoom: { control: 'NavigationControl', label: 'Zoom', position: 'top-left', options: { showCompass: false } },
29348
+ compass: { control: 'NavigationControl', label: 'Compass', position: 'top-left', options: { showZoom: false } },
29349
+ locate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
29350
+ };
29351
+ var TOOLS_ICON = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false" fill="none" ' +
29352
+ 'stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h10M18 7h2M4 17h2M10 17h10"/>' +
29353
+ '<circle cx="16" cy="7" r="2"/><circle cx="8" cy="17" r="2"/></svg>';
29354
+ var TOOLS_TEXT = { button: 'Map tools', showAll: 'Show all', hideAll: 'Hide all' };
29355
+
29356
+ function isElement(v) { return !!v && typeof v === 'object' && v.nodeType === 1; }
29357
+
29358
+ // An engine control whose element the SDK can find again to show or hide it: the control's own onAdd, its element kept.
29359
+ function toolControl(tool, inner) {
29360
+ return {
29361
+ onAdd: function (m) { tool.el = inner.onAdd(m); return tool.el; },
29362
+ onRemove: function (m) { if (inner.onRemove) { inner.onRemove(m); } else if (tool.el && tool.el.parentNode) { tool.el.parentNode.removeChild(tool.el); } tool.el = null; },
29363
+ getDefaultPosition: function () { return tool.position; },
29364
+ };
29365
+ }
29366
+
29367
+ // A page element placed in a corner of the map: it moves into the corner and back where it was when removed.
29368
+ function elementControl(tool, element) {
29369
+ var home = null;
29370
+ var next = null;
29371
+ return {
29372
+ onAdd: function () {
29373
+ home = element.parentNode;
29374
+ next = element.nextSibling;
29375
+ element.classList.add('maplibregl-ctrl', 'smap-tool');
29376
+ tool.el = element;
29377
+ return element;
29378
+ },
29379
+ onRemove: function () {
29380
+ element.classList.remove('maplibregl-ctrl', 'smap-tool');
29381
+ if (home) { home.insertBefore(element, next && next.parentNode === home ? next : null); } else if (element.parentNode) { element.parentNode.removeChild(element); }
29382
+ tool.el = null;
29383
+ },
29384
+ getDefaultPosition: function () { return tool.position; },
29385
+ };
29386
+ }
29387
+
29388
+ /**
29389
+ * Registers a tool floating on the map, shown or hidden by setToolVisible / setToolsVisible and the tools control.
29390
+ * map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' })
29391
+ * map.addTool('zoom', 'navigation', { options: { showCompass: false } })
29392
+ * @param {string} id 1-64 letters, digits, '-', '_', '.'; adding an id again replaces the first
29393
+ * @param {Element|Object|string} target a page element, an engine control (onAdd / onRemove), or a kind the SDK
29394
+ * makes: 'navigation', 'geolocate', 'fullscreen', 'scale', or the mobile SDKs' 'zoom' (navigation without the
29395
+ * compass), 'compass' (without the zoom buttons) and 'locate' (geolocate)
29396
+ * @param {Object} [o] label (shown on its button), position ('top-left', 'top-right', 'bottom-left', 'bottom-right',
29397
+ * or 'none': an element stays where the page put it), options (of a kind's engine control)
29398
+ */
29399
+ SMap.prototype.addTool = function (id, target, o) {
29400
+ o = o || {};
29401
+ if (typeof id !== 'string' || !TOOL_ID.test(id)) { throw new Error('sMap: a tool id is 1-64 letters, digits, "-", "_" or "."'); }
29402
+ var kind = typeof target === 'string' ? TOOL_KINDS[target] : null;
29403
+ if (typeof target === 'string') {
29404
+ if (!kind) { throw new Error('sMap: unknown tool kind "' + target + '"'); }
29405
+ if (!engine[kind.control]) { throw new Error('sMap: the rendering engine has no ' + kind.control); }
29406
+ } else if (!isElement(target) && !(target && typeof target.onAdd === 'function')) {
29407
+ throw new Error('sMap: a tool is an element, an engine control or a tool kind');
29408
+ }
29409
+ var position = o.position || (kind ? kind.position : (isElement(target) ? (target.parentNode ? 'none' : 'top-left') : 'top-right'));
29410
+ if (position !== 'none' && TOOL_CORNERS.indexOf(position) < 0) { throw new Error('sMap: unknown tool position "' + position + '"'); }
29411
+ if (position === 'none' && !isElement(target)) { throw new Error('sMap: only an element may stay out of the map\'s corners'); }
29412
+ if (this._toolIndex(id) >= 0) { this.removeTool(id); }
29413
+ var tool = { id: id, label: String(o.label || (kind ? kind.label : id)), position: position, el: null, control: null };
29414
+ if (kind) { tool.control = toolControl(tool, new engine[kind.control](o.options || kind.options)); }
29415
+ else if (!isElement(target)) { tool.control = toolControl(tool, target); }
29416
+ else if (position !== 'none') { tool.control = elementControl(tool, target); }
29417
+ else { tool.el = target; }
29418
+ this._tools.push(tool);
29419
+ if (this.engine) { this._mountTool(tool); }
29420
+ this._renderToolsControl();
29421
+ return this;
29422
+ };
29423
+
29424
+ /** Removes a tool: an engine control leaves the map, a page element is shown again where the page put it. */
29425
+ SMap.prototype.removeTool = function (id) {
29426
+ var i = this._toolIndex(id);
29427
+ if (i < 0) { return this; }
29428
+ var tool = this._tools[i];
29429
+ this._tools.splice(i, 1);
29430
+ if (tool.el) { tool.el.classList.remove('smap-tool-hidden'); }
29431
+ if (tool.control && this.engine && tool.mounted) { this.engine.removeControl(tool.control); }
29432
+ this._renderToolsControl();
29433
+ return this;
29434
+ };
29435
+
29436
+ /** Shows or hides one tool. A hidden tool keeps its state and shows again as it was (M57). */
29437
+ SMap.prototype.setToolVisible = function (id, visible) {
29438
+ var tool = this._tools[this._toolIndex(id)];
29439
+ if (!tool || this._toolVisible(tool) === !!visible) { return this; }
29440
+ this._setHidden(tool, !visible);
29441
+ this._renderToolsControl();
29442
+ this._toolsChanged(id);
29443
+ return this;
29444
+ };
29445
+
29446
+ /** Shows or hides every tool. The map data credit, the sMap badge and the tools control always stay (M51). */
29447
+ SMap.prototype.setToolsVisible = function (visible) {
29448
+ var changed = false;
29449
+ this._tools.forEach(function (tool) {
29450
+ if (this._toolVisible(tool) !== !!visible) { this._setHidden(tool, !visible); changed = true; }
29451
+ }, this);
29452
+ if (changed) { this._renderToolsControl(); this._toolsChanged(null); }
29453
+ return this;
29454
+ };
29455
+
29456
+ /** The tools in the order added: [{ id, label, visible }]. */
29457
+ SMap.prototype.tools = function () {
29458
+ return this._tools.map(function (tool) { return { id: tool.id, label: tool.label, visible: this._toolVisible(tool) }; }, this);
29459
+ };
29460
+
29461
+ /** Calls handler({ hidden: [ids], changed: id | null }) after every change; the SDK keeps no state itself (M53). */
29462
+ SMap.prototype.onToolsChange = function (handler) { this._toolHandlers.push(handler); return this; };
29463
+ SMap.prototype.offToolsChange = function (handler) {
29464
+ this._toolHandlers = this._toolHandlers.filter(function (h) { return h !== handler; });
29465
+ return this;
29466
+ };
29467
+
29468
+ SMap.prototype._toolIndex = function (id) {
29469
+ for (var i = 0; i < this._tools.length; i++) { if (this._tools[i].id === id) { return i; } }
29470
+ return -1;
29471
+ };
29472
+ SMap.prototype._toolVisible = function (tool) { return !this._toolsHidden[tool.id]; };
29473
+ SMap.prototype._setHidden = function (tool, hidden) {
29474
+ if (hidden) { this._toolsHidden[tool.id] = true; } else { delete this._toolsHidden[tool.id]; }
29475
+ if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', hidden); }
29476
+ };
29477
+ SMap.prototype._toolsChanged = function (id) {
29478
+ var hidden = this._tools.filter(function (t) { return !this._toolVisible(t); }, this).map(function (t) { return t.id; });
29479
+ this._toolHandlers.slice().forEach(function (h) { h({ hidden: hidden, changed: id }); });
29480
+ };
29481
+
29482
+ SMap.prototype._mountTool = function (tool) {
29483
+ // The tools control joins the map with its first tool, so a map without tools is exactly as before.
29484
+ if (this._toolsControlAt && !this._toolsControl && this.engine.addControl) {
29485
+ this._toolsControl = toolsControl(this);
29486
+ this.engine.addControl(this._toolsControl, this._toolsControlAt);
29487
+ }
29488
+ if (tool.control && this.engine.addControl) { this.engine.addControl(tool.control, tool.position); tool.mounted = true; }
29489
+ if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', !this._toolVisible(tool)); }
29490
+ };
29491
+
29492
+ // Called once the engine exists: every tool registered before it.
29493
+ SMap.prototype._mountTools = function () {
29494
+ this._tools.forEach(function (tool) { this._mountTool(tool); }, this);
29495
+ this._renderToolsControl();
29496
+ };
29497
+ SMap.prototype._renderToolsControl = function () { if (this._toolsControl) { this._toolsControl._render(); } };
29498
+
29499
+ // The tools control: one button that opens a panel with one toggle per tool (pressed = shown), Show all and Hide all.
29500
+ // It is not a tool and is never hidden; it joins the map with the first tool and is not drawn while none is left.
29501
+ function toolsControl(map) {
29502
+ var el = null;
29503
+ var button = null;
29504
+ var panel = null;
29505
+ var drawn = null;
29506
+ var text = {};
29507
+ Object.keys(TOOLS_TEXT).forEach(function (k) { text[k] = map._toolsLabels[k] || TOOLS_TEXT[k]; });
29508
+ function open(on) {
29509
+ panel.hidden = !on;
29510
+ button.setAttribute('aria-expanded', on ? 'true' : 'false');
29511
+ }
29512
+ function close() { if (panel && !panel.hidden) { open(false); } }
29513
+ function key(e) { if (e.key === 'Escape' && !panel.hidden) { open(false); button.focus(); } }
29514
+ function make(doc, tag, className, label) {
29515
+ var b = doc.createElement(tag);
29516
+ b.className = className;
29517
+ if (tag === 'button') { b.type = 'button'; }
29518
+ if (label !== undefined) { b.textContent = label; }
29519
+ return b;
29520
+ }
29521
+ var control = {
29522
+ onAdd: function (m) {
29523
+ var doc = m.getContainer().ownerDocument;
29524
+ el = make(doc, 'div', 'maplibregl-ctrl maplibregl-ctrl-group smap-tools');
29525
+ button = make(doc, 'button', 'smap-tools-button');
29526
+ button.innerHTML = TOOLS_ICON;
29527
+ button.title = text.button;
29528
+ button.setAttribute('aria-label', text.button);
29529
+ button.setAttribute('aria-haspopup', 'true');
29530
+ button.addEventListener('click', function () { open(panel.hidden); });
29531
+ panel = make(doc, 'div', 'smap-tools-panel');
29532
+ panel.setAttribute('role', 'group');
29533
+ panel.setAttribute('aria-label', text.button);
29534
+ el.appendChild(button);
29535
+ el.appendChild(panel);
29536
+ el.addEventListener('keydown', key);
29537
+ open(false);
29538
+ m.on('click', close);
29539
+ return el;
29540
+ },
29541
+ onRemove: function (m) {
29542
+ m.off('click', close);
29543
+ if (el && el.parentNode) { el.parentNode.removeChild(el); }
29544
+ el = null;
29545
+ drawn = null;
29546
+ },
29547
+ _render: function () {
29548
+ if (!el) { return; }
29549
+ var doc = el.ownerDocument;
29550
+ el.classList.toggle('smap-tools-empty', map._tools.length === 0);
29551
+ // The same tools as last time: only the pressed states change, so a toggle keeps the keyboard focus.
29552
+ var signature = JSON.stringify(map._tools.map(function (t) { return [t.id, t.label]; }));
29553
+ if (signature === drawn) {
29554
+ map._tools.forEach(function (tool, i) { panel.children[i].setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false'); });
29555
+ return;
29556
+ }
29557
+ drawn = signature;
29558
+ while (panel.firstChild) { panel.removeChild(panel.firstChild); }
29559
+ map._tools.forEach(function (tool) {
29560
+ var item = make(doc, 'button', 'smap-tools-item', tool.label);
29561
+ item.setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false');
29562
+ item.setAttribute('data-tool', tool.id);
29563
+ item.addEventListener('click', function () { map.setToolVisible(tool.id, !map._toolVisible(tool)); });
29564
+ panel.appendChild(item);
29565
+ });
29566
+ var all = make(doc, 'div', 'smap-tools-all');
29567
+ var show = make(doc, 'button', 'smap-tools-show-all', text.showAll);
29568
+ show.addEventListener('click', function () { map.setToolsVisible(true); });
29569
+ var hide = make(doc, 'button', 'smap-tools-hide-all', text.hideAll);
29570
+ hide.addEventListener('click', function () { map.setToolsVisible(false); });
29571
+ all.appendChild(show);
29572
+ all.appendChild(hide);
29573
+ panel.appendChild(all);
29574
+ },
29575
+ };
29576
+ return control;
29577
+ }
29578
+
29326
29579
  // ------------------------------------------------------------------ Style Specification sources and layers (M24)
29327
29580
 
29328
29581
  function clone(v) { return v === undefined ? undefined : JSON.parse(JSON.stringify(v)); }
package/dist/smap.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
2
- /*! sMap Web SDK 1.1.2. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
2
+ /*! sMap Web SDK 1.1.5. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
3
3
 
4
4
  var smapCore = null;
5
5
  function smapCreateEngine() {
@@ -28352,7 +28352,7 @@ function smapCreateSdk(engine, interop) {
28352
28352
  })(typeof self !== 'undefined' ? self : this, function (engine, interop) {
28353
28353
  'use strict';
28354
28354
 
28355
- var VERSION = '1.1.2';
28355
+ var VERSION = '1.1.5';
28356
28356
  var FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
28357
28357
  var BASEMAP_SOURCE = 'basemap';
28358
28358
  var BUILDINGS_LAYER = 'smap-buildings-3d';
@@ -28805,11 +28805,20 @@ function smapCreateSdk(engine, interop) {
28805
28805
  // True between a style's load and the next style change. The engine's own isStyleLoaded() turns false while any
28806
28806
  // source is loading data, which would leave layers added meanwhile waiting for a load that never comes.
28807
28807
  this._styleReady = false;
28808
+ // Map tools (M49-M57): the controls and panels floating on the map that the caller registered, in the order added.
28809
+ var toolOptions = options.tools || {};
28810
+ this._tools = [];
28811
+ this._toolsHidden = {};
28812
+ (toolOptions.hidden || []).forEach(function (id) { this._toolsHidden[id] = true; }, this);
28813
+ this._toolsControlAt = toolOptions.control === undefined ? 'top-right' : toolOptions.control;
28814
+ this._toolsLabels = toolOptions.labels || {};
28815
+ this._toolsControl = null;
28816
+ this._toolHandlers = [];
28808
28817
 
28809
28818
  var self = this;
28810
28819
  var engineOptions = {};
28811
28820
  for (var k in options) {
28812
- if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D'].indexOf(k) < 0) {
28821
+ if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D', 'tools'].indexOf(k) < 0) {
28813
28822
  engineOptions[k] = options[k];
28814
28823
  }
28815
28824
  }
@@ -28842,6 +28851,7 @@ function smapCreateSdk(engine, interop) {
28842
28851
  self.engine.addControl(badge, 'bottom-right');
28843
28852
  badge._place();
28844
28853
  }
28854
+ self._mountTools();
28845
28855
  self.engine.on('style.load', function () { self._styleReady = true; self._restore(); });
28846
28856
  self._keyboard3D();
28847
28857
  // One click handler for every layer's popup: the engine's hit test over those layers, the top feature opens.
@@ -29325,6 +29335,249 @@ function smapCreateSdk(engine, interop) {
29325
29335
  SMap.prototype.flyTo = function (o) { this.engine.flyTo(o); return this; };
29326
29336
  SMap.prototype.remove = function () { clearTimeout(this._refreshTimer); if (this.engine) { this.engine.remove(); } };
29327
29337
 
29338
+ // ------------------------------------------------------------------ map tools (M49-M57)
29339
+
29340
+ var TOOL_ID = /^[A-Za-z0-9._-]{1,64}$/;
29341
+ var TOOL_CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
29342
+ // The tools the SDK makes by name: the engine control, its English label (the caller may give another) and corner.
29343
+ var TOOL_KINDS = {
29344
+ navigation: { control: 'NavigationControl', label: 'Zoom', position: 'top-left' },
29345
+ geolocate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
29346
+ fullscreen: { control: 'FullscreenControl', label: 'Full screen', position: 'top-left' },
29347
+ scale: { control: 'ScaleControl', label: 'Scale', position: 'bottom-left' },
29348
+ // The mobile SDKs' names for the same controls, so one tool list serves every platform.
29349
+ zoom: { control: 'NavigationControl', label: 'Zoom', position: 'top-left', options: { showCompass: false } },
29350
+ compass: { control: 'NavigationControl', label: 'Compass', position: 'top-left', options: { showZoom: false } },
29351
+ locate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
29352
+ };
29353
+ var TOOLS_ICON = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false" fill="none" ' +
29354
+ 'stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h10M18 7h2M4 17h2M10 17h10"/>' +
29355
+ '<circle cx="16" cy="7" r="2"/><circle cx="8" cy="17" r="2"/></svg>';
29356
+ var TOOLS_TEXT = { button: 'Map tools', showAll: 'Show all', hideAll: 'Hide all' };
29357
+
29358
+ function isElement(v) { return !!v && typeof v === 'object' && v.nodeType === 1; }
29359
+
29360
+ // An engine control whose element the SDK can find again to show or hide it: the control's own onAdd, its element kept.
29361
+ function toolControl(tool, inner) {
29362
+ return {
29363
+ onAdd: function (m) { tool.el = inner.onAdd(m); return tool.el; },
29364
+ onRemove: function (m) { if (inner.onRemove) { inner.onRemove(m); } else if (tool.el && tool.el.parentNode) { tool.el.parentNode.removeChild(tool.el); } tool.el = null; },
29365
+ getDefaultPosition: function () { return tool.position; },
29366
+ };
29367
+ }
29368
+
29369
+ // A page element placed in a corner of the map: it moves into the corner and back where it was when removed.
29370
+ function elementControl(tool, element) {
29371
+ var home = null;
29372
+ var next = null;
29373
+ return {
29374
+ onAdd: function () {
29375
+ home = element.parentNode;
29376
+ next = element.nextSibling;
29377
+ element.classList.add('maplibregl-ctrl', 'smap-tool');
29378
+ tool.el = element;
29379
+ return element;
29380
+ },
29381
+ onRemove: function () {
29382
+ element.classList.remove('maplibregl-ctrl', 'smap-tool');
29383
+ if (home) { home.insertBefore(element, next && next.parentNode === home ? next : null); } else if (element.parentNode) { element.parentNode.removeChild(element); }
29384
+ tool.el = null;
29385
+ },
29386
+ getDefaultPosition: function () { return tool.position; },
29387
+ };
29388
+ }
29389
+
29390
+ /**
29391
+ * Registers a tool floating on the map, shown or hidden by setToolVisible / setToolsVisible and the tools control.
29392
+ * map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' })
29393
+ * map.addTool('zoom', 'navigation', { options: { showCompass: false } })
29394
+ * @param {string} id 1-64 letters, digits, '-', '_', '.'; adding an id again replaces the first
29395
+ * @param {Element|Object|string} target a page element, an engine control (onAdd / onRemove), or a kind the SDK
29396
+ * makes: 'navigation', 'geolocate', 'fullscreen', 'scale', or the mobile SDKs' 'zoom' (navigation without the
29397
+ * compass), 'compass' (without the zoom buttons) and 'locate' (geolocate)
29398
+ * @param {Object} [o] label (shown on its button), position ('top-left', 'top-right', 'bottom-left', 'bottom-right',
29399
+ * or 'none': an element stays where the page put it), options (of a kind's engine control)
29400
+ */
29401
+ SMap.prototype.addTool = function (id, target, o) {
29402
+ o = o || {};
29403
+ if (typeof id !== 'string' || !TOOL_ID.test(id)) { throw new Error('sMap: a tool id is 1-64 letters, digits, "-", "_" or "."'); }
29404
+ var kind = typeof target === 'string' ? TOOL_KINDS[target] : null;
29405
+ if (typeof target === 'string') {
29406
+ if (!kind) { throw new Error('sMap: unknown tool kind "' + target + '"'); }
29407
+ if (!engine[kind.control]) { throw new Error('sMap: the rendering engine has no ' + kind.control); }
29408
+ } else if (!isElement(target) && !(target && typeof target.onAdd === 'function')) {
29409
+ throw new Error('sMap: a tool is an element, an engine control or a tool kind');
29410
+ }
29411
+ var position = o.position || (kind ? kind.position : (isElement(target) ? (target.parentNode ? 'none' : 'top-left') : 'top-right'));
29412
+ if (position !== 'none' && TOOL_CORNERS.indexOf(position) < 0) { throw new Error('sMap: unknown tool position "' + position + '"'); }
29413
+ if (position === 'none' && !isElement(target)) { throw new Error('sMap: only an element may stay out of the map\'s corners'); }
29414
+ if (this._toolIndex(id) >= 0) { this.removeTool(id); }
29415
+ var tool = { id: id, label: String(o.label || (kind ? kind.label : id)), position: position, el: null, control: null };
29416
+ if (kind) { tool.control = toolControl(tool, new engine[kind.control](o.options || kind.options)); }
29417
+ else if (!isElement(target)) { tool.control = toolControl(tool, target); }
29418
+ else if (position !== 'none') { tool.control = elementControl(tool, target); }
29419
+ else { tool.el = target; }
29420
+ this._tools.push(tool);
29421
+ if (this.engine) { this._mountTool(tool); }
29422
+ this._renderToolsControl();
29423
+ return this;
29424
+ };
29425
+
29426
+ /** Removes a tool: an engine control leaves the map, a page element is shown again where the page put it. */
29427
+ SMap.prototype.removeTool = function (id) {
29428
+ var i = this._toolIndex(id);
29429
+ if (i < 0) { return this; }
29430
+ var tool = this._tools[i];
29431
+ this._tools.splice(i, 1);
29432
+ if (tool.el) { tool.el.classList.remove('smap-tool-hidden'); }
29433
+ if (tool.control && this.engine && tool.mounted) { this.engine.removeControl(tool.control); }
29434
+ this._renderToolsControl();
29435
+ return this;
29436
+ };
29437
+
29438
+ /** Shows or hides one tool. A hidden tool keeps its state and shows again as it was (M57). */
29439
+ SMap.prototype.setToolVisible = function (id, visible) {
29440
+ var tool = this._tools[this._toolIndex(id)];
29441
+ if (!tool || this._toolVisible(tool) === !!visible) { return this; }
29442
+ this._setHidden(tool, !visible);
29443
+ this._renderToolsControl();
29444
+ this._toolsChanged(id);
29445
+ return this;
29446
+ };
29447
+
29448
+ /** Shows or hides every tool. The map data credit, the sMap badge and the tools control always stay (M51). */
29449
+ SMap.prototype.setToolsVisible = function (visible) {
29450
+ var changed = false;
29451
+ this._tools.forEach(function (tool) {
29452
+ if (this._toolVisible(tool) !== !!visible) { this._setHidden(tool, !visible); changed = true; }
29453
+ }, this);
29454
+ if (changed) { this._renderToolsControl(); this._toolsChanged(null); }
29455
+ return this;
29456
+ };
29457
+
29458
+ /** The tools in the order added: [{ id, label, visible }]. */
29459
+ SMap.prototype.tools = function () {
29460
+ return this._tools.map(function (tool) { return { id: tool.id, label: tool.label, visible: this._toolVisible(tool) }; }, this);
29461
+ };
29462
+
29463
+ /** Calls handler({ hidden: [ids], changed: id | null }) after every change; the SDK keeps no state itself (M53). */
29464
+ SMap.prototype.onToolsChange = function (handler) { this._toolHandlers.push(handler); return this; };
29465
+ SMap.prototype.offToolsChange = function (handler) {
29466
+ this._toolHandlers = this._toolHandlers.filter(function (h) { return h !== handler; });
29467
+ return this;
29468
+ };
29469
+
29470
+ SMap.prototype._toolIndex = function (id) {
29471
+ for (var i = 0; i < this._tools.length; i++) { if (this._tools[i].id === id) { return i; } }
29472
+ return -1;
29473
+ };
29474
+ SMap.prototype._toolVisible = function (tool) { return !this._toolsHidden[tool.id]; };
29475
+ SMap.prototype._setHidden = function (tool, hidden) {
29476
+ if (hidden) { this._toolsHidden[tool.id] = true; } else { delete this._toolsHidden[tool.id]; }
29477
+ if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', hidden); }
29478
+ };
29479
+ SMap.prototype._toolsChanged = function (id) {
29480
+ var hidden = this._tools.filter(function (t) { return !this._toolVisible(t); }, this).map(function (t) { return t.id; });
29481
+ this._toolHandlers.slice().forEach(function (h) { h({ hidden: hidden, changed: id }); });
29482
+ };
29483
+
29484
+ SMap.prototype._mountTool = function (tool) {
29485
+ // The tools control joins the map with its first tool, so a map without tools is exactly as before.
29486
+ if (this._toolsControlAt && !this._toolsControl && this.engine.addControl) {
29487
+ this._toolsControl = toolsControl(this);
29488
+ this.engine.addControl(this._toolsControl, this._toolsControlAt);
29489
+ }
29490
+ if (tool.control && this.engine.addControl) { this.engine.addControl(tool.control, tool.position); tool.mounted = true; }
29491
+ if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', !this._toolVisible(tool)); }
29492
+ };
29493
+
29494
+ // Called once the engine exists: every tool registered before it.
29495
+ SMap.prototype._mountTools = function () {
29496
+ this._tools.forEach(function (tool) { this._mountTool(tool); }, this);
29497
+ this._renderToolsControl();
29498
+ };
29499
+ SMap.prototype._renderToolsControl = function () { if (this._toolsControl) { this._toolsControl._render(); } };
29500
+
29501
+ // The tools control: one button that opens a panel with one toggle per tool (pressed = shown), Show all and Hide all.
29502
+ // It is not a tool and is never hidden; it joins the map with the first tool and is not drawn while none is left.
29503
+ function toolsControl(map) {
29504
+ var el = null;
29505
+ var button = null;
29506
+ var panel = null;
29507
+ var drawn = null;
29508
+ var text = {};
29509
+ Object.keys(TOOLS_TEXT).forEach(function (k) { text[k] = map._toolsLabels[k] || TOOLS_TEXT[k]; });
29510
+ function open(on) {
29511
+ panel.hidden = !on;
29512
+ button.setAttribute('aria-expanded', on ? 'true' : 'false');
29513
+ }
29514
+ function close() { if (panel && !panel.hidden) { open(false); } }
29515
+ function key(e) { if (e.key === 'Escape' && !panel.hidden) { open(false); button.focus(); } }
29516
+ function make(doc, tag, className, label) {
29517
+ var b = doc.createElement(tag);
29518
+ b.className = className;
29519
+ if (tag === 'button') { b.type = 'button'; }
29520
+ if (label !== undefined) { b.textContent = label; }
29521
+ return b;
29522
+ }
29523
+ var control = {
29524
+ onAdd: function (m) {
29525
+ var doc = m.getContainer().ownerDocument;
29526
+ el = make(doc, 'div', 'maplibregl-ctrl maplibregl-ctrl-group smap-tools');
29527
+ button = make(doc, 'button', 'smap-tools-button');
29528
+ button.innerHTML = TOOLS_ICON;
29529
+ button.title = text.button;
29530
+ button.setAttribute('aria-label', text.button);
29531
+ button.setAttribute('aria-haspopup', 'true');
29532
+ button.addEventListener('click', function () { open(panel.hidden); });
29533
+ panel = make(doc, 'div', 'smap-tools-panel');
29534
+ panel.setAttribute('role', 'group');
29535
+ panel.setAttribute('aria-label', text.button);
29536
+ el.appendChild(button);
29537
+ el.appendChild(panel);
29538
+ el.addEventListener('keydown', key);
29539
+ open(false);
29540
+ m.on('click', close);
29541
+ return el;
29542
+ },
29543
+ onRemove: function (m) {
29544
+ m.off('click', close);
29545
+ if (el && el.parentNode) { el.parentNode.removeChild(el); }
29546
+ el = null;
29547
+ drawn = null;
29548
+ },
29549
+ _render: function () {
29550
+ if (!el) { return; }
29551
+ var doc = el.ownerDocument;
29552
+ el.classList.toggle('smap-tools-empty', map._tools.length === 0);
29553
+ // The same tools as last time: only the pressed states change, so a toggle keeps the keyboard focus.
29554
+ var signature = JSON.stringify(map._tools.map(function (t) { return [t.id, t.label]; }));
29555
+ if (signature === drawn) {
29556
+ map._tools.forEach(function (tool, i) { panel.children[i].setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false'); });
29557
+ return;
29558
+ }
29559
+ drawn = signature;
29560
+ while (panel.firstChild) { panel.removeChild(panel.firstChild); }
29561
+ map._tools.forEach(function (tool) {
29562
+ var item = make(doc, 'button', 'smap-tools-item', tool.label);
29563
+ item.setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false');
29564
+ item.setAttribute('data-tool', tool.id);
29565
+ item.addEventListener('click', function () { map.setToolVisible(tool.id, !map._toolVisible(tool)); });
29566
+ panel.appendChild(item);
29567
+ });
29568
+ var all = make(doc, 'div', 'smap-tools-all');
29569
+ var show = make(doc, 'button', 'smap-tools-show-all', text.showAll);
29570
+ show.addEventListener('click', function () { map.setToolsVisible(true); });
29571
+ var hide = make(doc, 'button', 'smap-tools-hide-all', text.hideAll);
29572
+ hide.addEventListener('click', function () { map.setToolsVisible(false); });
29573
+ all.appendChild(show);
29574
+ all.appendChild(hide);
29575
+ panel.appendChild(all);
29576
+ },
29577
+ };
29578
+ return control;
29579
+ }
29580
+
29328
29581
  // ------------------------------------------------------------------ Style Specification sources and layers (M24)
29329
29582
 
29330
29583
  function clone(v) { return v === undefined ? undefined : JSON.parse(JSON.stringify(v)); }
@@ -30293,7 +30546,7 @@ function SMapMap(options) {
30293
30546
  SMapMap.prototype = core.Map.prototype;
30294
30547
  return new core.Map(options);
30295
30548
  }
30296
- var smapVersion = "1.1.2";
30549
+ var smapVersion = "1.1.5";
30297
30550
  var smapStyles = ['light', 'white', 'grayscale', 'dark', 'black'];
30298
30551
  /** The bundled rendering engine, for anything the SDK does not wrap (loaded on the first call). */
30299
30552
  function smapGetEngine() { return smapLoad().engine; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superrouteca/smap",
3
- "version": "1.1.2",
3
+ "version": "1.1.5",
4
4
  "description": "sMap for the web: maps from Superroute's sMap service, the Web SDK (map view) and the JS client (tokens, nearest node, styles, tiles).",
5
5
  "license": "MIT",
6
6
  "author": "Goodneo Solutions",
package/types/smap.d.cts CHANGED
@@ -292,10 +292,39 @@ export interface MapOptions {
292
292
  popup?: PopupOptions;
293
293
  /** Shown while a popup's content is a pending promise (HTML string or DOM node; default pulsing dots). */
294
294
  popupLoading?: string | Node;
295
+ /** Map tools: the starting state, where the tools control sits (or false for none) and its labels. */
296
+ tools?: ToolsOptions;
295
297
  /** Every other option goes to the rendering engine. */
296
298
  [engineOption: string]: unknown;
297
299
  }
298
300
 
301
+ /** A corner of the map, or 'none' for an element that stays where the page put it. */
302
+ export type ToolPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'none';
303
+ /** The tools the SDK makes by name. */
304
+ export type ToolKind = 'navigation' | 'geolocate' | 'fullscreen' | 'scale' | 'zoom' | 'compass' | 'locate';
305
+ export interface ToolOptions {
306
+ /** Shown on the tool's button in the tools control and read by screen readers. */
307
+ label?: string;
308
+ position?: ToolPosition;
309
+ /** Options of a kind's engine control (e.g. `{ showCompass: false }` for navigation). */
310
+ options?: Record<string, unknown>;
311
+ }
312
+ export interface ToolsOptions {
313
+ /** Ids of the tools hidden from the start. */
314
+ hidden?: string[];
315
+ /** Where the tools control sits ('top-right' by default), or false to leave it out. */
316
+ control?: Exclude<ToolPosition, 'none'> | false;
317
+ /** The tools control's texts (English by default). */
318
+ labels?: { button?: string; showAll?: string; hideAll?: string };
319
+ }
320
+ export interface ToolState { id: string; label: string; visible: boolean }
321
+ export interface ToolsChangeEvent {
322
+ /** Ids of the registered tools that are hidden now. */
323
+ hidden: string[];
324
+ /** The tool switched, or null when every tool was switched at once. */
325
+ changed: string | null;
326
+ }
327
+
299
328
  /** addData: how the file is read, then the options of addPoints / addLines / addPolygons for its parts. */
300
329
  export type DataOptions = ReadOptions & PointOptions & LineOptions & PolygonOptions;
301
330
 
@@ -497,6 +526,22 @@ export declare class Map {
497
526
  closePopup(): this;
498
527
  setAutoZoom(on: boolean): this;
499
528
  autoZoom(): boolean;
529
+ /**
530
+ * Registers a tool floating on the map: a page element, an engine control, or a kind the SDK makes. Adding an id
531
+ * again replaces the first. The map data credit, the sMap badge and the tools control are never tools.
532
+ */
533
+ addTool(id: string, target: HTMLElement | { onAdd(map: any): HTMLElement; onRemove?(map: any): void } | ToolKind, options?: ToolOptions): this;
534
+ /** Removes a tool: an engine control leaves the map, a page element is shown again where the page put it. */
535
+ removeTool(id: string): this;
536
+ /** Shows or hides one tool; a hidden tool keeps its state. */
537
+ setToolVisible(id: string, visible: boolean): this;
538
+ /** Shows or hides every tool. */
539
+ setToolsVisible(visible: boolean): this;
540
+ /** The tools in the order added. */
541
+ tools(): ToolState[];
542
+ /** Called after every change; the SDK keeps no state itself. */
543
+ onToolsChange(handler: (event: ToolsChangeEvent) => void): this;
544
+ offToolsChange(handler: (event: ToolsChangeEvent) => void): this;
500
545
  on(type: string, ...args: any[]): this;
501
546
  off(type: string, ...args: any[]): this;
502
547
  once(type: string, ...args: any[]): any;
package/types/smap.d.mts CHANGED
@@ -292,10 +292,39 @@ export interface MapOptions {
292
292
  popup?: PopupOptions;
293
293
  /** Shown while a popup's content is a pending promise (HTML string or DOM node; default pulsing dots). */
294
294
  popupLoading?: string | Node;
295
+ /** Map tools: the starting state, where the tools control sits (or false for none) and its labels. */
296
+ tools?: ToolsOptions;
295
297
  /** Every other option goes to the rendering engine. */
296
298
  [engineOption: string]: unknown;
297
299
  }
298
300
 
301
+ /** A corner of the map, or 'none' for an element that stays where the page put it. */
302
+ export type ToolPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'none';
303
+ /** The tools the SDK makes by name. */
304
+ export type ToolKind = 'navigation' | 'geolocate' | 'fullscreen' | 'scale' | 'zoom' | 'compass' | 'locate';
305
+ export interface ToolOptions {
306
+ /** Shown on the tool's button in the tools control and read by screen readers. */
307
+ label?: string;
308
+ position?: ToolPosition;
309
+ /** Options of a kind's engine control (e.g. `{ showCompass: false }` for navigation). */
310
+ options?: Record<string, unknown>;
311
+ }
312
+ export interface ToolsOptions {
313
+ /** Ids of the tools hidden from the start. */
314
+ hidden?: string[];
315
+ /** Where the tools control sits ('top-right' by default), or false to leave it out. */
316
+ control?: Exclude<ToolPosition, 'none'> | false;
317
+ /** The tools control's texts (English by default). */
318
+ labels?: { button?: string; showAll?: string; hideAll?: string };
319
+ }
320
+ export interface ToolState { id: string; label: string; visible: boolean }
321
+ export interface ToolsChangeEvent {
322
+ /** Ids of the registered tools that are hidden now. */
323
+ hidden: string[];
324
+ /** The tool switched, or null when every tool was switched at once. */
325
+ changed: string | null;
326
+ }
327
+
299
328
  /** addData: how the file is read, then the options of addPoints / addLines / addPolygons for its parts. */
300
329
  export type DataOptions = ReadOptions & PointOptions & LineOptions & PolygonOptions;
301
330
 
@@ -497,6 +526,22 @@ export declare class Map {
497
526
  closePopup(): this;
498
527
  setAutoZoom(on: boolean): this;
499
528
  autoZoom(): boolean;
529
+ /**
530
+ * Registers a tool floating on the map: a page element, an engine control, or a kind the SDK makes. Adding an id
531
+ * again replaces the first. The map data credit, the sMap badge and the tools control are never tools.
532
+ */
533
+ addTool(id: string, target: HTMLElement | { onAdd(map: any): HTMLElement; onRemove?(map: any): void } | ToolKind, options?: ToolOptions): this;
534
+ /** Removes a tool: an engine control leaves the map, a page element is shown again where the page put it. */
535
+ removeTool(id: string): this;
536
+ /** Shows or hides one tool; a hidden tool keeps its state. */
537
+ setToolVisible(id: string, visible: boolean): this;
538
+ /** Shows or hides every tool. */
539
+ setToolsVisible(visible: boolean): this;
540
+ /** The tools in the order added. */
541
+ tools(): ToolState[];
542
+ /** Called after every change; the SDK keeps no state itself. */
543
+ onToolsChange(handler: (event: ToolsChangeEvent) => void): this;
544
+ offToolsChange(handler: (event: ToolsChangeEvent) => void): this;
500
545
  on(type: string, ...args: any[]): this;
501
546
  off(type: string, ...args: any[]): this;
502
547
  once(type: string, ...args: any[]): any;