@superrouteca/smap 1.1.5 → 1.1.7

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.7 — 2026-10-28
4
+
5
+ - Plan editor: faster edits through sMap's fast path, with no code change. When your plan editor token answer names a
6
+ fast path, or your map key's configuration on sMap does, every edit goes there and falls back to the Open Platform
7
+ by itself when the fast path asks for it, fails, or does not answer within 2 seconds (5 seconds for a refine). Quota
8
+ and rate-limit refusals stay final. The editor's state and `change` events say which path answered (`path`);
9
+ `fastPath: false` keeps every edit on the Open Platform, and `smapUrl` names the sMap service a map key's
10
+ configuration is read from (the map's own with `map.planEditor`).
11
+
12
+ ## 1.1.6 — 2026-10-28
13
+
14
+ - Plan editor: `map.planEditor` lets a person edit a plan of routes on the map — drag a stop within its route or onto
15
+ another route, take stops off, place new orders at the positions the service ranks best and choose another of them,
16
+ lock stops and refine routes around them, undo and redo. Every edit is a call of the Open Platform's plan editing
17
+ with the whole plan, and its answer replaces the routes, distances, times and violations drawn; while a stop is
18
+ dragged, the plan with the stop there is priced as a preview. The editor opens a distance snapshot for faster
19
+ edits and opens a new one by itself when it expired. Credentials are a short-lived plan editor token that your own
20
+ server mints (the token string or the token answer as it is; renewed before it expires and after an expired
21
+ session) or a map key with plan editing switched on, both sent as a bearer token; refusals arrive as errors with
22
+ their reason. A ready-made panel shows routes, totals, violations, undo and redo, the
23
+ insert positions and errors in 14 languages; it can be restyled or left out for a panel of your own.
24
+ `sMap.PlanEditor` is the same editor without a map.
25
+ - `planEditorParts`: the parts a plan editor on another map library is built from — the features to draw, where a
26
+ dropped stop goes, a drag with its preview, hit testing in pixels, and the panel with its stylesheet. The tool
27
+ adapters use them.
28
+ - Map tools: the tools control is now the last button of the map's toolbar by default — the corner holding the zoom
29
+ buttons or compass, top left when there is none — and stays last when controls join that corner later. A corner
30
+ given in `tools.control` is kept as before. While its panel is open its corner is drawn above the map's other
31
+ corners, so nothing on the map covers it, and the panel opens towards the middle of the map.
32
+ - Map tools: data layers can be tools. `addTool(id, { layer: 'stops' })` or `{ layer: ['stops', 'stop-labels'] }`
33
+ shows and hides the layers with the tool, in the tools control and with `setToolVisible` / `setToolsVisible`, and
34
+ they are listed by `tools()` and saved through `onToolsChange` like every other tool. A layer that is not on the map
35
+ is left alone.
36
+
3
37
  ## 1.1.5 — 2026-10-28
4
38
 
5
39
  - Map tools: the controls and panels floating on a map can be shown and hidden one by one or all at once. Register
package/README.md CHANGED
@@ -203,8 +203,8 @@ an upper pin.
203
203
 
204
204
  ## Map tools: showing and hiding controls and panels
205
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*:
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
208
 
209
209
  ```js
210
210
  const map = new sMap.Map({ container: 'map', style: 'light', tools: { hidden: saved.hidden } });
@@ -212,6 +212,7 @@ map.addTool('zoom', 'navigation', { options: { showCompass: false } }); // mad
212
212
  map.addTool('fullscreen', 'fullscreen');
213
213
  map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' });
214
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
215
216
  map.onToolsChange((e) => save({ hidden: e.hidden }));
216
217
 
217
218
  map.setToolVisible('legend', false);
@@ -225,12 +226,121 @@ map.tools(); // [{ id: 'zoom', label: 'Zoom', visible: true }, …]
225
226
  - A hidden tool is not drawn and keeps its state (an open list stays open, typed text stays). Hiding a switch does not
226
227
  turn off what it switches.
227
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.
228
238
  - `tools.control: false` leaves the tools control out (make your own buttons with the calls above);
229
239
  `tools.labels: { button, showAll, hideAll }` translates it.
230
240
  - The SDK keeps no state: give the starting state in `tools.hidden` and keep `onToolsChange`'s `hidden` where you
231
241
  like (the page's storage, your server).
232
242
  - The tools control joins the map with the first tool; a map without tools is unchanged.
233
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
+ - **Fast path** (no code change): when the plan editor token answer carries `fast_path.url`, or the map key's
326
+ configuration on sMap names one (`GET <smapUrl>/plan/v1/config`, read once and kept 10 minutes; `smapUrl` is the
327
+ map's own sMap service with `map.planEditor`, `https://tile.srsr.ca` otherwise), every call goes there
328
+ (`<url>/<same path>`, with the same credential, headers and body) instead of `<baseUrl>/v1/plan/…`. A
329
+ `409 USE_STANDARD_PATH` (or `X-Plan-Path: standard`), any 5xx, a network error or no answer within 2 seconds
330
+ (`fastPathTimeout`; 5 seconds for refine, `fastPathRefineTimeout`) repeats the same call once on the standard path; the next call tries the fast path again.
331
+ `402`, `429` and every other 4xx are final, as on the standard path. A renewed token follows its own answer (the
332
+ fast path may appear or go). `state().path` and the `change` event's `path` say where the last answer came from
333
+ (`'fast'` or `'standard'`); `fastPath: false` keeps every call on the standard path. The snapshot works on both
334
+ paths with the same id.
335
+ - **Distance snapshot**: the editor opens one (`POST /v1/plan/matrix`) and names it in every call, so legs measured
336
+ once are not measured again. When an answer says the snapshot expired (`X-Plan-Matrix: expired`, or a `410`), it
337
+ opens a new one and carries on; a snapshot that cannot be opened never stops an edit. `destroy()` closes it.
338
+ - **Calls in flight**: a newer edit cancels the call before it. An edit you can see the outcome of (a move, an order,
339
+ a removal) is drawn at once and priced when the answer arrives; `pending` in the `change` event says it is not
340
+ priced yet. A refused edit is taken back and reported as `error`.
341
+ - `new sMap.PlanEditor(options)` is the same editor without a map, for an interface of your own.
342
+ - `examples/plan-editor.html` runs the editor against a stand-in of the service in the page (build first).
343
+
234
344
  ## Style layers and sources
235
345
 
236
346
  Sources and layers of the Style Specification v8 are passed to the map as they are, so a layer copied from Mapbox
@@ -434,7 +544,8 @@ its bottom-right corner, left of the attribution (above it when the map is too n
434
544
  ## Where the code comes from
435
545
 
436
546
  The sources are in `src/` (the Web SDK `smap.js` and `smap.css`, the parts that need no map `smap-interop.js`:
437
- formats, basemap options, slots, URLs, PMTiles; the JS client `smap-client.js` and its types).
547
+ formats, basemap options, slots, URLs, PMTiles; the plan editor `smap-plan.js`; the JS client `smap-client.js` and its
548
+ types).
438
549
  `npm run build` writes `dist/` from them and the rendering engine (an exact devDependency); `dist/SOURCE.json`
439
550
  records the version, the engine and the hash of every file. `dist/` is committed and never edited by hand. The type
440
551
  declarations of the map SDK are in `types/`.
package/dist/SOURCE.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
- "sdkVersion": "1.1.5",
2
+ "sdkVersion": "1.1.7",
3
3
  "engine": "maplibre-gl 6.16.0",
4
4
  "files": {
5
- "smap.js": "d8658ace857ab90b2f2f3dc0af163104d3231e11c119af4a12a0d88b66f6ecae",
6
- "smap.css": "acb9e11dca261a1f827e1d073e47e13b112b986ee744a712107ff83276d327e7",
7
- "smap-client.js": "d101ab143691d1973399fb7b45fe396178dc96471048923077e57ce76b413a4a",
5
+ "smap.js": "1e7ba1266c67f860e74a2956521d55f49ea5771e0b1cfa832014433ea0554d25",
6
+ "smap.css": "d82adcd1b8e2b5d1a6ebe0ee648bc24eb84471bc956e3c8ac3cc7dc1f447d96f",
7
+ "smap-client.js": "f845c1d52a6f13bfb7f85cf8690e70320b09278b203b9e6a2cc874eed734eb10",
8
8
  "smap-client.d.ts": "5f0df11bf60c5a9ced34522a284de81260c27871678ab772f16da9ccf6883f58",
9
- "smap.mjs": "db3e14cce3abb2f2dfdd17578de122ed7a633f8f885e1f5765dfbb92cf862e87",
10
- "smap.cjs": "34ec49173c1e97b88510ffcfdf1b2db30fcfe28ccef5aa1e8e311e52f459b994",
11
- "smap-client.cjs": "45d68a640f686d93e10c98468ed0c384bbc696da51193253217f6476d19ac6ed",
12
- "smap-client.mjs": "45b241183a128671d0422c3684f5987cc1d1e1223fc1d95bcba16ba955c58862",
9
+ "smap.mjs": "ff3859691bfaf62d672ec6991eeda530be3316b22413d85c09a4adb7e11529cb",
10
+ "smap.cjs": "74c72a1504a0023d1b3d2613aac1527bc20676a7002c909715e60f798182b5c8",
11
+ "smap-client.cjs": "c1d143a0a9ac8a4cf1abdc7c2c82aec0d063bc51188e3eeb572dc48497475ef3",
12
+ "smap-client.mjs": "69d3e44f0d3fd39c3edbd8be6e66d4ddb625795f356a583dfeea610025cb7bd5",
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.5 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
2
+ /*! sMap JS client 1.1.7 — 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.5';
22
+ var VERSION = '1.1.7';
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.5 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
1
+ /*! sMap JS client 1.1.7 — 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.5';
21
+ var VERSION = '1.1.7';
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.5 — tokens, the nearest node, styles, tiles and raw reads, without a map view. */
4
+ /*! sMap JS client 1.1.7 — 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.5';
24
+ var VERSION = '1.1.7';
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