@superrouteca/smap-mapbox 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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.6 — 2026-10-28
4
+
5
+ - Plan editor: `addPlanEditor(map, options)` edits a plan of routes on a Mapbox GL JS map — drag a stop within its
6
+ route or onto another route with mouse or touch (priced as a preview while dragging), take stops off, place new
7
+ orders, refine and undo or redo, each a call of the Open Platform's plan editing. The routes are drawn as layers of
8
+ the map and the ready-made panel in 14 languages sits in a corner as a control.
9
+
10
+ ## 1.1.5 — 2026-10-28
11
+
12
+ - The version follows the shared 1.1.5 release; no change of its own.
13
+
3
14
  ## 1.1.2 — 2026-10-27
4
15
 
5
16
  - The version follows the shared 1.1.2 release; no change of its own.
package/README.md CHANGED
@@ -126,6 +126,41 @@ sMap slot (`bottom` → `bottom`, `middle` → `labels`, `top` → `top`); optio
126
126
  none; without any, a layer draws on top in the order added. `prefix` renames every source and layer; `layers` or
127
127
  `filter` choose them. Icons of the style's own sprite are not loaded (a warning says so).
128
128
 
129
+ ## Plan editor
130
+
131
+ `addPlanEditor(map, options)` lets a person edit a plan of routes on a Mapbox GL JS map (or an `SMapMapbox`): drag a
132
+ stop within its route or onto another route, take stops off, place new orders at the positions the service ranks
133
+ best, refine routes around locked stops, and undo or redo. Each edit is one call of the Open Platform's plan editing
134
+ with the whole plan; the answer replaces the routes, distances, times and violations drawn. Nothing is kept on a
135
+ server: keep the plan from the `change` event. The editor is the Web SDK's (`PlanEditor` of `@superrouteca/smap`), so
136
+ its calls, events and credentials are the same (see the Web SDK README, "Plan editor").
137
+
138
+ ```js
139
+ import { addPlanEditor } from '@superrouteca/smap-mapbox';
140
+
141
+ const plan = addPlanEditor(map, {
142
+ // Your own server mints the token with POST /v1/plan/token; or { key: 'smk_…' } with plan editing switched on.
143
+ credentials: { token: () => fetch('/plan-token').then((r) => r.json()) },
144
+ plan: { region: 'NA', profile: 'car', vehicles, jobs, routes },
145
+ language: 'fr',
146
+ });
147
+ plan.editor.on('change', (e) => save(e.plan));
148
+ // later
149
+ plan.remove();
150
+ ```
151
+
152
+ - The plan is drawn as GeoJSON sources and layers named after `id` (default `plan`): `plan:legs` (with
153
+ `plan:legs-casing` under it), `plan:starts`, `plan:unassigned`, `plan:stops` and `plan:stops-label`; `before` puts
154
+ them under a layer of your style. They are added once the style has loaded (`plan.ready`).
155
+ - Press on a stop or an unassigned job and drag it: drop it on a stop to put it before that stop, on a route's line to
156
+ put it into that leg, or on a vehicle's start to make it the first stop. The map does not pan meanwhile, and the
157
+ plan with the stop at the target is priced as a preview (`preview: false` turns that off). Mouse and touch both work.
158
+ - The ready-made panel is a control (top right; `panel: { position, className, labels, label }`, or `panel: false`):
159
+ routes and totals, violations, undo / redo / refine, the unassigned jobs and insert positions, in 14 languages. Its
160
+ stylesheet is added to the page unless `css: false`.
161
+ - `editor: myEditor` binds an editor you made; `remove()` then leaves it running. `editor.destroy()` also removes
162
+ everything from the map.
163
+
129
164
  ## Example
130
165
 
131
166
  `examples/index.html` shows both ways. Serve the repository root (for example `npx serve .`) and open
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superrouteca/smap-mapbox",
3
- "version": "1.1.2",
3
+ "version": "1.1.6",
4
4
  "description": "sMap for Mapbox GL JS: maps from Superroute's sMap service in Mapbox GL JS, and Mapbox GL JS styles and layers on an sMap map.",
5
5
  "license": "MIT",
6
6
  "author": "Goodneo Solutions",
@@ -59,7 +59,7 @@
59
59
  }
60
60
  },
61
61
  "dependencies": {
62
- "@superrouteca/smap": "1.1.2"
62
+ "@superrouteca/smap": "1.1.6"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/geojson": "7946.0.16",
package/src/index.js CHANGED
@@ -9,11 +9,12 @@ import * as sMapModule from '@superrouteca/smap';
9
9
  import { SMapAccess, checkStyle, rasterPath, stylePath, FLAVORS, DEFAULT_BASE } from './common/access.js';
10
10
  import { OSM_ATTRIBUTION, createBadge, injectCss } from './common/badge.js';
11
11
  import { createEmbedded } from './common/embed.js';
12
+ import { addPlanEditor } from './plan.js';
12
13
 
13
14
  const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
14
15
 
15
- export const version = '1.1.2';
16
- export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION };
16
+ export const version = '1.1.6';
17
+ export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION, addPlanEditor };
17
18
 
18
19
  const ACCESS_OPTIONS = ['baseUrl', 'token', 'getToken', 'apiKey', 'nodes', 'nearest', 'fetch', 'timeoutMs', 'client', 'access'];
19
20
  const OWN_OPTIONS = ['style', 'language', 'basemap', 'buildings3D', 'sdk', 'popup', 'popupLoading', 'autoZoom', 'overlay'];
@@ -471,5 +472,5 @@ export function smapAccess(options) { return new SMapAccess(options); }
471
472
 
472
473
  export { isMapboxSource, OSM_ATTRIBUTION as attribution };
473
474
 
474
- const SMapMapboxModule = { version, styles: FLAVORS, SMapMapbox, createMap, switchBasemap, smapAccess, smapTransformRequest, SMapBadgeControl, SMapAccess, checkStyle, rasterPath };
475
+ const SMapMapboxModule = { version, styles: FLAVORS, SMapMapbox, createMap, switchBasemap, smapAccess, smapTransformRequest, SMapBadgeControl, SMapAccess, checkStyle, rasterPath, addPlanEditor };
475
476
  export default SMapMapboxModule;
package/src/plan.js ADDED
@@ -0,0 +1,191 @@
1
+ // The plan editor (Superroute OPEN-ROUTE-EDIT E10) on a Mapbox GL JS map: the plan drawn as GeoJSON sources and
2
+ // layers, stops dragged within and between routes through the map's layer events, and the ready-made panel as a
3
+ // control. The editor itself (state, calls, undo / redo, the panel) is the Web SDK's: PlanEditor and planEditorParts.
4
+ import * as sMapModule from '@superrouteca/smap';
5
+
6
+ const SDK = sMapModule.default && sMapModule.default.PlanEditor ? sMapModule.default : sMapModule;
7
+
8
+ /** Options of the binding, not of the editor. */
9
+ const OWN = ['editor', 'id', 'colors', 'preview', 'language', 'labels', 'panel', 'css', 'before', 'plan'];
10
+ const POSITIONS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
11
+ const fc = function (list) { return { type: 'FeatureCollection', features: list }; };
12
+
13
+ function editorOptions(options) {
14
+ const out = {};
15
+ Object.keys(options).forEach(function (k) { if (OWN.indexOf(k) < 0) { out[k] = options[k]; } });
16
+ return out;
17
+ }
18
+
19
+ /**
20
+ * A plan editor on a Mapbox GL JS map (or an SMapMapbox: its `map` is used).
21
+ * const plan = addPlanEditor(map, { credentials: { token: getPlanToken }, plan, language: 'fr' });
22
+ * plan.editor.on('change', (e) => save(e.plan));
23
+ * Options: every PlanEditor option, or `editor` (an editor of your own to bind), plus `id` (default 'plan'),
24
+ * `colors`, `preview` (default true), `language`, `labels`, `panel` (true | false | { position, className, labels,
25
+ * label }), `css` (default true: the panel's stylesheet is added to the page), `before` (a layer id to draw under).
26
+ * @returns {{ editor, layerIds: string[], sourceIds: string[], control, panel, ready: Promise, features(): Object, remove(): void }}
27
+ */
28
+ export function addPlanEditor(target, options) {
29
+ options = options || {};
30
+ const map = target && target.map && typeof target.map.addLayer === 'function' ? target.map : target;
31
+ if (!map || typeof map.addLayer !== 'function') { throw new Error('sMap: addPlanEditor(map, options) needs a Mapbox GL JS map'); }
32
+ const parts = SDK.planEditorParts;
33
+ const own = !options.editor;
34
+ const editor = options.editor || new SDK.PlanEditor(Object.assign(editorOptions(options), { plan: null }));
35
+ const id = options.id || 'plan';
36
+ const src = { legs: id + ':legs', starts: id + ':starts', unassigned: id + ':unassigned', stops: id + ':stops' };
37
+ const layers = {
38
+ casing: id + ':legs-casing', legs: id + ':legs', starts: id + ':starts', unassigned: id + ':unassigned', stops: id + ':stops', labels: id + ':stops-label',
39
+ };
40
+ const hitLayers = [layers.stops, layers.unassigned, layers.starts, layers.legs];
41
+ const drag = parts.planDrag(editor, { preview: options.preview !== false });
42
+ const container = map.getContainer ? map.getContainer() : null;
43
+ const doc = (container && container.ownerDocument) || (typeof document !== 'undefined' ? document : null);
44
+ let added = false;
45
+ let removed = false;
46
+ let current = null;
47
+
48
+ function features() { return parts.planFeatures(editor, id, options.colors); }
49
+ function draw() {
50
+ if (removed || !added) { return; }
51
+ current = features();
52
+ Object.keys(src).forEach(function (k) { const s = map.getSource(src[k]); if (s) { s.setData(fc(current[k])); } });
53
+ }
54
+ function addAll() {
55
+ if (removed || added) { return; }
56
+ current = features();
57
+ Object.keys(src).forEach(function (k) { map.addSource(src[k], { type: 'geojson', data: fc(current[k]) }); });
58
+ const before = options.before;
59
+ map.addLayer({ id: layers.casing, type: 'line', source: src.legs, layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': '#ffffff', 'line-width': 7 } }, before);
60
+ map.addLayer({ id: layers.legs, type: 'line', source: src.legs, layout: { 'line-cap': 'round', 'line-join': 'round' }, paint: { 'line-color': ['get', 'color'], 'line-width': 4 } }, before);
61
+ map.addLayer({ id: layers.starts, type: 'circle', source: src.starts, paint: { 'circle-radius': 8, 'circle-color': ['get', 'color'], 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 } }, before);
62
+ map.addLayer({ id: layers.unassigned, type: 'circle', source: src.unassigned, paint: { 'circle-radius': 8, 'circle-color': '#9ca3af', 'circle-stroke-color': '#ffffff', 'circle-stroke-width': 2 } }, before);
63
+ map.addLayer({ id: layers.stops, type: 'circle', source: src.stops, paint: { 'circle-radius': 11, 'circle-color': ['get', 'color'], 'circle-stroke-color': ['get', 'border'], 'circle-stroke-width': 3 } }, before);
64
+ map.addLayer({
65
+ id: layers.labels, type: 'symbol', source: src.stops,
66
+ layout: { 'text-field': ['get', 'label'], 'text-size': 11, 'text-allow-overlap': true, 'text-ignore-placement': true },
67
+ paint: { 'text-color': '#ffffff' },
68
+ }, before);
69
+ added = true;
70
+ listen();
71
+ }
72
+
73
+ function hitsAt(point) {
74
+ if (typeof map.queryRenderedFeatures !== 'function') { return []; }
75
+ const box = [[point.x - 6, point.y - 6], [point.x + 6, point.y + 6]];
76
+ return (map.queryRenderedFeatures(box, { layers: hitLayers }) || []).filter(function (f) { return f && f.properties && f.properties.smap_plan === id; });
77
+ }
78
+ function setPan(on) { if (map.dragPan) { if (on) { map.dragPan.enable(); } else { map.dragPan.disable(); } } }
79
+ // The dragged pin follows the pointer; a redraw puts it back.
80
+ function follow(lngLat) {
81
+ const stopId = drag.dragging();
82
+ if (!current || !lngLat) { return; }
83
+ const kind = current.stops.some(function (f) { return f.properties.id === stopId; }) ? 'stops' : 'unassigned';
84
+ const s = map.getSource(src[kind]);
85
+ if (!s) { return; }
86
+ s.setData(fc(current[kind].map(function (f) {
87
+ return f.properties.id === stopId ? { type: 'Feature', properties: f.properties, geometry: { type: 'Point', coordinates: [lngLat.lng, lngLat.lat] } } : f;
88
+ })));
89
+ }
90
+
91
+ function down(e) {
92
+ if (removed || !e.point || (e.originalEvent && e.originalEvent.button)) { return; }
93
+ const hit = (e.features && e.features.length ? e.features : hitsAt(e.point)).filter(function (f) {
94
+ return f.properties && f.properties.smap_plan === id && (f.properties.kind === 'stop' || f.properties.kind === 'unassigned');
95
+ })[0];
96
+ if (!hit) { return; }
97
+ drag.start(hit.properties.id, e.point);
98
+ setPan(false);
99
+ if (e.preventDefault) { e.preventDefault(); }
100
+ }
101
+ function move(e) {
102
+ if (!drag.dragging() || !e.point) { return; }
103
+ drag.move(hitsAt(e.point), e.point);
104
+ if (drag.moved()) { follow(e.lngLat); }
105
+ }
106
+ function up(e) {
107
+ if (!drag.dragging()) { return; }
108
+ const moved = drag.moved();
109
+ setPan(true);
110
+ drag.end(e && e.point ? hitsAt(e.point) : null);
111
+ if (moved) { draw(); }
112
+ }
113
+ let listening = false;
114
+ function listen() {
115
+ if (listening) { return; }
116
+ listening = true;
117
+ [layers.stops, layers.unassigned].forEach(function (l) { map.on('mousedown', l, down); map.on('touchstart', l, down); });
118
+ map.on('mousemove', move);
119
+ map.on('touchmove', move);
120
+ map.on('mouseup', up);
121
+ map.on('touchend', up);
122
+ }
123
+
124
+ let control = null;
125
+ let panel = null;
126
+ if (options.panel !== false && doc) {
127
+ const po = typeof options.panel === 'object' && options.panel ? options.panel : {};
128
+ if (options.css !== false) { parts.injectCss(doc); }
129
+ const language = options.language || (typeof navigator !== 'undefined' && navigator.language) || 'en';
130
+ panel = parts.createPanel(editor, { language, labels: po.labels || options.labels, document: doc, colors: options.colors, className: po.className });
131
+ const element = doc.createElement('div');
132
+ element.className = 'mapboxgl-ctrl smap-plan-control';
133
+ element.setAttribute('aria-label', po.label || parts.textsFor(language, po.labels || options.labels).editor);
134
+ element.appendChild(panel.element);
135
+ control = {
136
+ onAdd: function () { return element; },
137
+ onRemove: function () { if (element.parentNode) { element.parentNode.removeChild(element); } },
138
+ getDefaultPosition: function () { return 'top-right'; },
139
+ };
140
+ map.addControl(control, POSITIONS.indexOf(po.position) >= 0 ? po.position : 'top-right');
141
+ }
142
+
143
+ const onChange = function () { draw(); };
144
+ editor.on('change', onChange);
145
+ const gone = function () { handle.remove(); };
146
+ editor.on('destroy', gone);
147
+ const loaded = typeof map.isStyleLoaded === 'function' ? map.isStyleLoaded() : true;
148
+ const ready = loaded ? Promise.resolve() : new Promise(function (resolve) {
149
+ let done = false;
150
+ const go = function () { if (!done) { done = true; resolve(); } };
151
+ map.once('style.load', go);
152
+ map.once('load', go);
153
+ });
154
+ if (loaded) { addAll(); } else { ready.then(addAll); }
155
+ // A new style (a scheme or language switch) can leave the plan's sources out: they are added again.
156
+ const restyled = function () {
157
+ if (removed || !added || map.getSource(src.legs)) { return; }
158
+ added = false;
159
+ addAll();
160
+ };
161
+ map.on('style.load', restyled);
162
+ if (own && options.plan) { editor.setPlan(options.plan).catch(function () {}); }
163
+
164
+ const handle = {
165
+ editor, control, panel, features,
166
+ ready: ready.then(function () { return handle; }),
167
+ layerIds: [layers.casing, layers.legs, layers.starts, layers.unassigned, layers.stops, layers.labels],
168
+ sourceIds: [src.legs, src.starts, src.unassigned, src.stops],
169
+ remove: function () {
170
+ if (removed) { return; }
171
+ removed = true;
172
+ drag.cancel();
173
+ setPan(true);
174
+ map.off('style.load', restyled);
175
+ editor.off('change', onChange);
176
+ editor.off('destroy', gone);
177
+ if (listening) {
178
+ [layers.stops, layers.unassigned].forEach(function (l) { map.off('mousedown', l, down); map.off('touchstart', l, down); });
179
+ map.off('mousemove', move); map.off('touchmove', move); map.off('mouseup', up); map.off('touchend', up);
180
+ }
181
+ if (added) {
182
+ handle.layerIds.slice().reverse().forEach(function (l) { if (map.getLayer(l)) { map.removeLayer(l); } });
183
+ handle.sourceIds.forEach(function (s) { if (map.getSource(s)) { map.removeSource(s); } });
184
+ }
185
+ if (control && map.removeControl) { map.removeControl(control); }
186
+ if (panel) { panel.destroy(); }
187
+ if (own) { editor.destroy(); }
188
+ },
189
+ };
190
+ return handle;
191
+ }
package/types/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import type * as mapboxgl from 'mapbox-gl';
3
3
  import type { Map as SMapMap, StyleFlavor, BasemapOverrides, StyleLayer, StyleSource, Slot, PointOptions, LineOptions, PolygonOptions,
4
4
  LayerData, PopupOptions, PopupContent, FitOptions, RasterLayerOptions, VectorTilesOptions, WmsLayerOptions, WmtsLayerOptions, ArcGisLayerOptions,
5
- LayerDescription, DataOptions, FileInput, LngLatLike } from '@superrouteca/smap';
5
+ LayerDescription, DataOptions, FileInput, LngLatLike, PlanEditor, PlanEditorOptions, PlanFeatures, PlanPanel, PlanTextKey } from '@superrouteca/smap';
6
6
  import type SMapClient from '@superrouteca/smap/client';
7
7
 
8
8
  export type { StyleFlavor };
@@ -149,6 +149,43 @@ export declare const OSM_ATTRIBUTION: string;
149
149
  export declare const attribution: string;
150
150
  export declare const OVERLAY_CALLS: string[];
151
151
 
152
+ /** Options of addPlanEditor: the editor's (or an editor of your own as `editor`) and the binding's. */
153
+ export type PlanEditorBindingOptions = (PlanEditorOptions | (Partial<PlanEditorOptions> & { editor: PlanEditor })) & {
154
+ /** Prefix of the source and layer ids, and the features' `smap_plan` (default 'plan'). */
155
+ id?: string;
156
+ /** Route colours, one per vehicle in the plan's order (repeated). */
157
+ colors?: string[];
158
+ /** Price the drop target while a stop is dragged (default true). */
159
+ preview?: boolean;
160
+ /** The panel's language (any BCP-47 tag; default the browser's). */
161
+ language?: string;
162
+ labels?: Partial<Record<PlanTextKey, string>>;
163
+ /** The ready-made panel (default true): false for none, or its corner, class, texts and label. */
164
+ panel?: boolean | { position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; className?: string; labels?: Partial<Record<PlanTextKey, string>>; label?: string };
165
+ /** Add the panel's stylesheet to the page (default true). */
166
+ css?: boolean;
167
+ /** A layer of the map to draw the plan under. */
168
+ before?: string;
169
+ };
170
+ /** A plan editor on a Mapbox GL JS map. */
171
+ export interface MapboxPlanEditor {
172
+ editor: PlanEditor;
173
+ /** `<id>:legs-casing`, `<id>:legs`, `<id>:starts`, `<id>:unassigned`, `<id>:stops`, `<id>:stops-label`. */
174
+ layerIds: string[];
175
+ /** `<id>:legs`, `<id>:starts`, `<id>:unassigned`, `<id>:stops` (GeoJSON). */
176
+ sourceIds: string[];
177
+ /** The panel as a control; null with `panel: false`. */
178
+ control: mapboxgl.IControl | null;
179
+ panel: PlanPanel | null;
180
+ /** Resolves once the layers are on the map (after the style has loaded). */
181
+ ready: Promise<MapboxPlanEditor>;
182
+ features(): PlanFeatures;
183
+ /** Takes the layers, sources, listeners and control off the map; ends the editor when this call made it. */
184
+ remove(): void;
185
+ }
186
+ /** A plan editor on a Mapbox GL JS map (or an SMapMapbox): the plan drawn, stops dragged within and between routes, the panel. */
187
+ export declare function addPlanEditor(map: mapboxgl.Map | SMapMapbox, options: PlanEditorBindingOptions): MapboxPlanEditor;
188
+
152
189
  declare const SMapMapboxModule: {
153
190
  version: string;
154
191
  styles: StyleFlavor[];
@@ -161,5 +198,6 @@ declare const SMapMapboxModule: {
161
198
  SMapAccess: typeof SMapAccess;
162
199
  checkStyle: typeof checkStyle;
163
200
  rasterPath: typeof rasterPath;
201
+ addPlanEditor: typeof addPlanEditor;
164
202
  };
165
203
  export default SMapMapboxModule;