@superrouteca/smap 1.1.2 → 1.1.6

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,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.6 — 2026-10-28
4
+
5
+ - Plan editor: `map.planEditor` lets a person edit a plan of routes on the map — drag a stop within its route or onto
6
+ another route, take stops off, place new orders at the positions the service ranks best and choose another of them,
7
+ lock stops and refine routes around them, undo and redo. Every edit is a call of the Open Platform's plan editing
8
+ with the whole plan, and its answer replaces the routes, distances, times and violations drawn; while a stop is
9
+ dragged, the plan with the stop there is priced as a preview. The editor opens a distance snapshot for faster
10
+ edits and opens a new one by itself when it expired. Credentials are a short-lived plan editor token that your own
11
+ server mints (the token string or the token answer as it is; renewed before it expires and after an expired
12
+ session) or a map key with plan editing switched on, both sent as a bearer token; refusals arrive as errors with
13
+ their reason. A ready-made panel shows routes, totals, violations, undo and redo, the
14
+ insert positions and errors in 14 languages; it can be restyled or left out for a panel of your own.
15
+ `sMap.PlanEditor` is the same editor without a map.
16
+ - `planEditorParts`: the parts a plan editor on another map library is built from — the features to draw, where a
17
+ dropped stop goes, a drag with its preview, hit testing in pixels, and the panel with its stylesheet. The tool
18
+ adapters use them.
19
+ - Map tools: the tools control is now the last button of the map's toolbar by default — the corner holding the zoom
20
+ buttons or compass, top left when there is none — and stays last when controls join that corner later. A corner
21
+ given in `tools.control` is kept as before. While its panel is open its corner is drawn above the map's other
22
+ corners, so nothing on the map covers it, and the panel opens towards the middle of the map.
23
+ - Map tools: data layers can be tools. `addTool(id, { layer: 'stops' })` or `{ layer: ['stops', 'stop-labels'] }`
24
+ shows and hides the layers with the tool, in the tools control and with `setToolVisible` / `setToolsVisible`, and
25
+ they are listed by `tools()` and saved through `onToolsChange` like every other tool. A layer that is not on the map
26
+ is left alone.
27
+
28
+ ## 1.1.5 — 2026-10-28
29
+
30
+ - Map tools: the controls and panels floating on a map can be shown and hidden one by one or all at once. Register
31
+ each with `addTool` — a page element (an orders list, a legend, a search box), an engine control, or a zoom, locate,
32
+ full screen or scale control the SDK makes — and switch them with `setToolVisible` and `setToolsVisible`. A tools
33
+ control (one button, top right by default) lists every tool with *Show all* and *Hide all*. A hidden tool keeps its
34
+ state and comes back as it was. The map data credit and the sMap badge always stay. The starting state is given in
35
+ `tools.hidden` and every change is reported to `onToolsChange`, so the page decides whether and where to keep it.
36
+ A map without tools is unchanged.
37
+
3
38
  ## 1.1.2 — 2026-10-27
4
39
 
5
40
  - The version follows the shared 1.1.2 release; no change of its own.
package/README.md CHANGED
@@ -201,6 +201,136 @@ 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, the last of the map's toolbar) that
207
+ shows or hides each 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.addTool('stops', { layer: ['stops', 'stop-labels'] }, { label: 'Stops' }); // data layers
216
+ map.onToolsChange((e) => save({ hidden: e.hidden }));
217
+
218
+ map.setToolVisible('legend', false);
219
+ map.setToolsVisible(true);
220
+ map.tools(); // [{ id: 'zoom', label: 'Zoom', visible: true }, …]
221
+ ```
222
+
223
+ - A tool is a page element, an engine control (`onAdd` / `onRemove`), or a kind the SDK makes: `navigation`,
224
+ `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
225
+ it. Ids are 1–64 letters, digits, `-`, `_` or `.`; adding an id again replaces the first.
226
+ - A hidden tool is not drawn and keeps its state (an open list stays open, typed text stays). Hiding a switch does not
227
+ turn off what it switches.
228
+ - The map data credit, the sMap badge and the tools control are never tools: *Hide all* leaves them.
229
+ - Data layers are tools too: `{ layer: 'stops' }` or `{ layer: ['stops', 'stop-labels'] }` names layers of
230
+ `addPoints` / `addLines` / `addPolygons` / `addData` / `addStyleLayer` and the external layers, or of the basemap.
231
+ Hiding the tool hides them (layout visibility), showing it shows them. A layer added later under an id of a hidden
232
+ tool starts hidden; an id without a layer (removed, or not added yet) is left alone. Removing a hidden data-layer
233
+ tool shows its layers again.
234
+ - Where the tools control sits (`tools.control`): by default (`'toolbar'`) it is the last button of the map's toolbar,
235
+ the corner holding the zoom buttons or compass (top left when there is none), and stays last when controls join
236
+ that corner later; a corner (`'top-right'`, …) places it there, as in 1.1.5. While its panel is open its corner is
237
+ drawn above the map's other corners, and the panel opens towards the middle of the map.
238
+ - `tools.control: false` leaves the tools control out (make your own buttons with the calls above);
239
+ `tools.labels: { button, showAll, hideAll }` translates it.
240
+ - The SDK keeps no state: give the starting state in `tools.hidden` and keep `onToolsChange`'s `hidden` where you
241
+ like (the page's storage, your server).
242
+ - The tools control joins the map with the first tool; a map without tools is unchanged.
243
+
244
+ ## Plan editor: editing routes on the map
245
+
246
+ `map.planEditor(options)` lets a person edit a plan of routes on the map: drag a stop within its route or onto another
247
+ route, take stops off, place new orders at the positions the service ranks best, refine routes around locked stops,
248
+ and undo or redo. Every edit is one call of the Open Platform's plan editing (`plan.resequence`, `plan.move`,
249
+ `plan.remove`, `plan.insert`, `plan.refine`, `plan.recost`) with the whole plan; the answer replaces the plan drawn,
250
+ its distances, times and violations. Nothing is stored on a server: keep the plan from the `change` event.
251
+
252
+ ```js
253
+ const editor = map.planEditor({
254
+ // A short-lived plan editor token from your own server, or { key: 'smk_…' }: a map key with plan editing switched on.
255
+ credentials: { token: () => fetch('/plan-token').then((r) => r.json()) },
256
+ plan: {
257
+ region: 'NA', profile: 'car',
258
+ vehicles: [{ id: 'van-1', start: { lat: 45.5017, lng: -73.5673 }, return_to_start: true }],
259
+ jobs: [{ id: 'order-1001', location: { lat: 45.5088, lng: -73.554 }, service_seconds: 300 }],
260
+ routes: [{ vehicle_id: 'van-1', stops: ['order-1001'] }],
261
+ },
262
+ language: 'fr', // the panel's language; default the map's
263
+ });
264
+
265
+ editor.on('change', (e) => save(e.plan)); // e.plan: vehicles, jobs, routes, unassigned
266
+ editor.on('error', (e) => report(e.error.code));
267
+ editor.on('busy', (e) => spinner(e.busy));
268
+
269
+ await editor.insert({ id: 'order-2001', location: { lat: 45.52, lng: -73.58 } });
270
+ editor.choices(); // the ranked positions answered for it
271
+ await editor.choose('order-2001', 1); // take the second one instead
272
+ editor.lock(['order-1001']);
273
+ await editor.refine({ vehicleIds: ['van-1'] });
274
+ await editor.undo();
275
+ editor.destroy();
276
+ ```
277
+
278
+ | Call | What it does |
279
+ |---|---|
280
+ | `setPlan(plan)` / `getPlan()` / `getResult()` | Replace the plan (the history starts again; it is priced), read it, read its prices |
281
+ | `resequence(vehicleId, stops)` | A route's stops in a new order (`plan.resequence`) |
282
+ | `move(stops, vehicleId, position?)` | Stops to a route at a position from 1, counted without them; none: last (`plan.move`) |
283
+ | `remove(stops)` | Stops off their routes, to the unassigned (`plan.remove`) |
284
+ | `dropStop(stopId, vehicleId, position)` | A stop dropped before the stop at `position` as drawn: a resequence within its route, a move otherwise |
285
+ | `insert(jobs, { limit, objective, nearest }?)` | New jobs, or unassigned jobs by id, at their best positions (`plan.insert`); `choices()` and `choose(job, i)` |
286
+ | `refine({ vehicleIds, freeze }?)` | Re-orders routes around the locked stops (`lock`, `unlock`), kept only when better (`plan.refine`) |
287
+ | `undo()` / `redo()` | Back and forward through the edits; an answered plan is shown again without a call |
288
+ | `preview(routes)` / `clearPreview()` | Prices other routes without changing the plan; the map does this while a stop is dragged |
289
+ | `state()`, `on` / `off` (`change`, `busy`, `error`, `snapshot`, `preview`, `destroy`), `destroy()` | |
290
+
291
+ - **On the map**: the routes are drawn as layers `plan:legs`, `plan:starts`, `plan:stops` and `plan:unassigned`
292
+ (`id` changes the prefix, `colors` the route colours). Drag a stop onto another stop to put it before that stop, or
293
+ onto a route's line to put it into that leg. While a stop is dragged, the plan with the stop at that place is priced
294
+ and shown in the panel as a preview (`preview: false` turns it off).
295
+ - **The panel** is the map tool `plan-editor` (top left; `panel: { position, label }`): the total and each route's
296
+ distance, driving time and stops, violations, the stops with their arrival, *Lock* and *Remove*, the unassigned
297
+ jobs with *Place*, the positions an insert answered with *Choose*, undo / redo / refine, and errors. Stops can be
298
+ dragged between its lists too. It speaks English, Simplified and Traditional Chinese, German, Spanish, French,
299
+ Italian, Dutch, Portuguese, Serbian, Hungarian, Polish, Slovak and Czech; `labels` replaces any text
300
+ (`sMap.planTexts` lists them), and every colour is in the stylesheet under `.smap-plan-*`. `panel: false` leaves it
301
+ out, for a panel of your own built on `state()` and the events.
302
+ - **Credentials**: your own server mints the token with its API token and hands the answer on; the browser never
303
+ holds the API token. Both credentials are sent as `Authorization: Bearer …`, and the Open Platform answers the
304
+ page's origin (CORS) with `X-Plan-Matrix`, `Retry-After` and `X-RateLimit-*` readable.
305
+
306
+ ```js
307
+ // On your server (Node.js shown): POST /v1/plan/token with the API token.
308
+ app.get('/plan-token', async (req, res) => {
309
+ const r = await fetch('https://open.superroute.ca/v1/plan/token', {
310
+ method: 'POST',
311
+ headers: { Authorization: 'Bearer ' + process.env.SUPERROUTE_API_TOKEN, 'Content-Type': 'application/json' },
312
+ body: JSON.stringify({ ttl_seconds: 900, origins: ['dispatch.example.com'] }),
313
+ });
314
+ res.status(r.status).json((await r.json()).data); // { access_token: 'sr_pet_…', expires_at, expires_in, scopes, origins }
315
+ });
316
+ ```
317
+
318
+ The provider may return the token string or that answer; the editor asks again 30 seconds before `expires_at`, and
319
+ once more after a `401 SESSION_EXPIRED` before repeating the call. Every other refusal is an `error` with its
320
+ `code` and `reason` and is not repeated: `401 SESSION_REVOKED` (`key_revoked`, `entitlements_changed`: mint a new
321
+ token), `401 TOKEN_INVALID`, and `403 NOT_AUTHORIZED` (`origin_not_allowed`, `no_plan_capability`,
322
+ `plan_editing_off`, `key_off`, `plan_routes_only`). A map key needs *Allow plan editing* switched on and the page's
323
+ origin on its allowlist. In an app shell (macOS, Windows) pass `app: 'apple:<bundle id>'` or `'windows:<id>'`: it
324
+ is sent as `X-SMap-App`.
325
+ - **Distance snapshot**: the editor opens one (`POST /v1/plan/matrix`) and names it in every call, so legs measured
326
+ once are not measured again. When an answer says the snapshot expired (`X-Plan-Matrix: expired`, or a `410`), it
327
+ opens a new one and carries on; a snapshot that cannot be opened never stops an edit. `destroy()` closes it.
328
+ - **Calls in flight**: a newer edit cancels the call before it. An edit you can see the outcome of (a move, an order,
329
+ a removal) is drawn at once and priced when the answer arrives; `pending` in the `change` event says it is not
330
+ priced yet. A refused edit is taken back and reported as `error`.
331
+ - `new sMap.PlanEditor(options)` is the same editor without a map, for an interface of your own.
332
+ - `examples/plan-editor.html` runs the editor against a stand-in of the service in the page (build first).
333
+
204
334
  ## Style layers and sources
205
335
 
206
336
  Sources and layers of the Style Specification v8 are passed to the map as they are, so a layer copied from Mapbox
@@ -404,7 +534,8 @@ its bottom-right corner, left of the attribution (above it when the map is too n
404
534
  ## Where the code comes from
405
535
 
406
536
  The sources are in `src/` (the Web SDK `smap.js` and `smap.css`, the parts that need no map `smap-interop.js`:
407
- formats, basemap options, slots, URLs, PMTiles; the JS client `smap-client.js` and its types).
537
+ formats, basemap options, slots, URLs, PMTiles; the plan editor `smap-plan.js`; the JS client `smap-client.js` and its
538
+ types).
408
539
  `npm run build` writes `dist/` from them and the rendering engine (an exact devDependency); `dist/SOURCE.json`
409
540
  records the version, the engine and the hash of every file. `dist/` is committed and never edited by hand. The type
410
541
  declarations of the map SDK are in `types/`.
package/dist/SOURCE.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
- "sdkVersion": "1.1.2",
2
+ "sdkVersion": "1.1.6",
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": "1d0821da7bf4c5891a67336750974fd66109bb148f6dc8b248f398ebc1d7dfd9",
6
+ "smap.css": "d82adcd1b8e2b5d1a6ebe0ee648bc24eb84471bc956e3c8ac3cc7dc1f447d96f",
7
+ "smap-client.js": "ef0b0915f86ec57c4cb654f01db4530e75123101d595b1e718db4b645097856b",
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": "bd333a886917566235b970f6a2709acb004670fdb5040c9a9380ad3b654dd923",
10
+ "smap.cjs": "ed8a33d2457eb243d59a50cc70fb92f1e59382c8f97520ed98ede99adf24d303",
11
+ "smap-client.cjs": "f5aa773fbaf1cfe46a3e37de246c9918609729b3d624aa624f380466ef56bb6f",
12
+ "smap-client.mjs": "6b895d27a13b026ac5e5aecdb608495f3337cda0650a7b2cceaaad8d54c032bd",
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.6 — 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.6';
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.6 — 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.6';
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.6 — 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.6';
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