@superrouteca/smap-leaflet 1.1.5 → 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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.6 — 2026-10-28
4
+
5
+ - Plan editor: `smapPlanEditor()` lets a person edit a plan of routes in Leaflet — drag a stop's pin onto another stop
6
+ or onto a route's line, with the new times, distance and violations priced while dragging — with the ready-made
7
+ panel (routes, totals, violations, undo and redo, refine, insert positions, 14 languages) as a Leaflet control.
8
+ Every edit is a call of the Open Platform's plan editing; the new plan is reported through the editor's events.
9
+
3
10
  ## 1.1.5 — 2026-10-28
4
11
 
5
12
  - The version follows the shared 1.1.5 release; no change of its own.
package/README.md CHANGED
@@ -146,6 +146,35 @@ clustering.
146
146
 
147
147
  `addTo()` takes `L.map`, a layer group, or the sMap map itself.
148
148
 
149
+ ## Plan editor
150
+
151
+ `smapPlanEditor(options)` lets a person edit a plan of routes in Leaflet: the routes are drawn as Leaflet layers, a
152
+ stop's pin is dragged (Leaflet's own marker dragging) onto another stop to put it before that stop, or onto a
153
+ route's line to put it into that leg, and the ready-made panel (routes, totals, violations, undo / redo, refine,
154
+ insert positions, in 14 languages) is a Leaflet control. Every edit is a call of the Open Platform's plan editing,
155
+ made by the sMap plan editor; while a pin is dragged, the plan with the stop there is priced and shown in the panel.
156
+
157
+ ```js
158
+ import { smapPlanEditor } from '@superrouteca/smap-leaflet';
159
+
160
+ const plan = smapPlanEditor({
161
+ // Your server mints a short-lived token with POST /v1/plan/token; or { key: 'smk_…' } with plan editing on.
162
+ credentials: { token: () => fetch('/plan-token').then((r) => r.json()) },
163
+ plan: { region: 'NA', profile: 'car', vehicles, jobs, routes },
164
+ language: 'fr',
165
+ panel: { position: 'topright' },
166
+ }).addTo(map);
167
+
168
+ plan.editor.on('change', (e) => save(e.plan)); // the editor: insert, refine, undo, redo, lock, …
169
+ plan.remove(); // off the map (add it again later)
170
+ plan.destroy(); // for good: an editor the layer made is destroyed too
171
+ ```
172
+
173
+ Options: every option of the sMap plan editor (`credentials`, `baseUrl`, `region`, `profile`, `snapshot`,
174
+ `insertLimit`, …) or `editor` (one you made, never destroyed by the layer), plus `colors`, `preview` (default true),
175
+ `language`, `labels`, `panel` (`false` for a panel of your own) and `css` (the panel's stylesheet is added to the page
176
+ unless `false`). `features()` gives the features drawn. The editor itself is described in the sMap Web SDK's README.
177
+
149
178
  ## Example
150
179
 
151
180
  `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-leaflet",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "sMap for Leaflet: maps from Superroute's sMap service as a Leaflet layer (vector or raster), and Leaflet-style calls on an sMap map.",
5
5
  "license": "MIT",
6
6
  "author": "Goodneo Solutions",
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "dependencies": {
61
- "@superrouteca/smap": "1.1.5"
61
+ "@superrouteca/smap": "1.1.6"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/leaflet": "1.9.22",
package/src/compat.js CHANGED
@@ -820,7 +820,7 @@ export function createLeafletCompat(smap) {
820
820
  const tileLayer = function (url, options) { return new TileLayer(url, options); };
821
821
  tileLayer.wms = function (url, options) { return new WmsLayer(url, options); };
822
822
  const ns = {
823
- version: '1.1.5',
823
+ version: '1.1.6',
824
824
  map: map,
825
825
  marker: function (ll, o) { return new Marker(ll, o); },
826
826
  circleMarker: function (ll, o) { return new CircleMarker(ll, o); },
package/src/index.js CHANGED
@@ -11,7 +11,7 @@ import { createEmbedded, hasWebGL2 } from './common/embed.js';
11
11
  const L = LeafletModule.default && LeafletModule.default.Layer ? LeafletModule.default : LeafletModule;
12
12
  const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
13
13
 
14
- export const version = '1.1.5';
14
+ export const version = '1.1.6';
15
15
  export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION };
16
16
 
17
17
  /** Options that belong to Leaflet, not to the sMap map. */
@@ -299,6 +299,8 @@ export function smapBasemap(options) {
299
299
  }
300
300
 
301
301
  export { hasWebGL2 };
302
+ export { SMapPlanEditor, SMapPlanPanel, smapPlanEditor } from './plan.js';
303
+ import { SMapPlanEditor, smapPlanEditor } from './plan.js';
302
304
 
303
- const SMapLeaflet = { version, styles: FLAVORS, SMapLayer, smapLayer, SMapRasterLayer, smapRasterLayer, smapBasemap, SMapBadge, SMapAccess, hasWebGL2, rasterPath, checkStyle };
305
+ const SMapLeaflet = { version, styles: FLAVORS, SMapLayer, smapLayer, SMapRasterLayer, smapRasterLayer, smapBasemap, SMapBadge, SMapAccess, hasWebGL2, rasterPath, checkStyle, SMapPlanEditor, smapPlanEditor };
304
306
  export default SMapLeaflet;
package/src/plan.js ADDED
@@ -0,0 +1,169 @@
1
+ // @superrouteca/smap-leaflet — the plan editor in Leaflet: the plan's routes drawn as Leaflet layers, stops dragged
2
+ // with Leaflet's own marker dragging onto another stop or a route's line, and the ready-made panel as a Leaflet
3
+ // control. Every edit is a plan.* call of the Open Platform made by the sMap plan editor (@superrouteca/smap).
4
+ import * as LeafletModule from 'leaflet';
5
+ import * as sMapModule from '@superrouteca/smap';
6
+
7
+ const L = LeafletModule.default && LeafletModule.default.Layer ? LeafletModule.default : LeafletModule;
8
+ const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
9
+
10
+ /** Options that belong to the editor (the rest are the binding's). */
11
+ const EDITOR_OPTIONS = ['credentials', 'baseUrl', 'fetch', 'app', 'region', 'profile', 'snapshot', 'geometry', 'insertLimit', 'objective'];
12
+
13
+ function parts() {
14
+ if (!SDK.planEditorParts || !SDK.PlanEditor) { throw new Error('sMap: this @superrouteca/smap has no plan editor; update it'); }
15
+ return SDK.planEditorParts;
16
+ }
17
+
18
+ /** A corner as Leaflet names it ('top-right' and 'topright' both work). */
19
+ function corner(position) { return String(position || 'topright').replace('-', ''); }
20
+
21
+ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]); }
22
+
23
+ /** The ready-made plan panel as a Leaflet control. */
24
+ export const SMapPlanPanel = L.Control.extend({
25
+ options: { position: 'topright' },
26
+ initialize: function (panel, options) { L.setOptions(this, options); this._panel = panel; },
27
+ onAdd: function () {
28
+ const el = this._panel.element;
29
+ // The map neither pans nor zooms from inside the panel.
30
+ L.DomEvent.disableClickPropagation(el);
31
+ L.DomEvent.disableScrollPropagation(el);
32
+ return el;
33
+ },
34
+ onRemove: function () {},
35
+ });
36
+
37
+ /**
38
+ * The plan editor as a Leaflet layer. Options: every PlanEditor option (credentials, baseUrl, fetch, app, plan,
39
+ * region, profile, snapshot, geometry, insertLimit, objective) or `editor` (one you made), plus `id` (default 'plan'),
40
+ * `colors`, `preview` (default true), `language`, `labels`, `panel` (true | false | { position, className, labels })
41
+ * and `css` (add the panel's stylesheet to the page; default true).
42
+ *
43
+ * const plan = smapPlanEditor({ credentials: { token: getPlanToken }, plan }).addTo(map);
44
+ * plan.editor.on('change', (e) => save(e.plan));
45
+ */
46
+ export const SMapPlanEditor = L.Layer.extend({
47
+ initialize: function (options) {
48
+ options = options || {};
49
+ const P = parts();
50
+ this._o = options;
51
+ this._id = options.id || 'plan';
52
+ this._own = !options.editor;
53
+ if (options.editor) {
54
+ this.editor = options.editor;
55
+ } else {
56
+ const eo = {};
57
+ EDITOR_OPTIONS.forEach(function (k) { if (options[k] !== undefined) { eo[k] = options[k]; } });
58
+ this.editor = new SDK.PlanEditor(eo);
59
+ }
60
+ this.panel = null;
61
+ this._control = null;
62
+ this._group = L.layerGroup();
63
+ this._drag = P.planDrag(this.editor, { preview: options.preview !== false });
64
+ this._destroyed = false;
65
+ const self = this;
66
+ this._onChange = function () { if (self._map) { self._draw(); } };
67
+ this._onDestroy = function () { self._destroyed = true; self._own = false; self.remove(); };
68
+ this.editor.on('change', this._onChange);
69
+ this.editor.on('destroy', this._onDestroy);
70
+ if (this._own && options.plan) { this.editor.setPlan(options.plan).catch(function () {}); }
71
+ },
72
+
73
+ onAdd: function (map) {
74
+ const P = parts();
75
+ this._group.addTo(map);
76
+ this._draw();
77
+ const o = this._o;
78
+ if (o.panel !== false && !this._destroyed) {
79
+ const po = typeof o.panel === 'object' && o.panel ? o.panel : {};
80
+ const doc = map.getContainer().ownerDocument;
81
+ if (o.css !== false) { P.injectCss(doc); }
82
+ this.panel = P.createPanel(this.editor, {
83
+ language: o.language || (typeof navigator !== 'undefined' && navigator.language) || 'en',
84
+ labels: po.labels || o.labels, document: doc, colors: o.colors, className: po.className,
85
+ });
86
+ this._control = new SMapPlanPanel(this.panel, { position: corner(po.position) }).addTo(map);
87
+ }
88
+ },
89
+
90
+ onRemove: function (map) {
91
+ this._drag.cancel();
92
+ if (this._control) { this._control.remove(); this._control = null; }
93
+ if (this.panel) { this.panel.destroy(); this.panel = null; }
94
+ map.removeLayer(this._group);
95
+ this._group.clearLayers();
96
+ },
97
+
98
+ /** The features drawn: { legs, stops, unassigned, starts } as GeoJSON features. */
99
+ features: function () { return parts().planFeatures(this.editor, this._id, this._o.colors); },
100
+
101
+ /** Takes the layer and its panel off the map for good; an editor the layer made is destroyed with it. */
102
+ destroy: function () {
103
+ this.remove();
104
+ this.editor.off('change', this._onChange);
105
+ this.editor.off('destroy', this._onDestroy);
106
+ if (this._own) { this._own = false; this.editor.destroy(); }
107
+ return this;
108
+ },
109
+
110
+ _draw: function () {
111
+ const map = this._map;
112
+ if (!map) { return; }
113
+ const f = this.features();
114
+ const group = this._group;
115
+ group.clearLayers();
116
+ const ll = (c) => [c[1], c[0]];
117
+ f.legs.forEach(function (x) {
118
+ group.addLayer(L.polyline(x.geometry.coordinates.map(ll), { color: x.properties.color, weight: 4, opacity: 0.9, interactive: false }));
119
+ });
120
+ f.starts.forEach(function (x) {
121
+ group.addLayer(L.circleMarker(ll(x.geometry.coordinates), { radius: 7, color: '#ffffff', weight: 2, fillColor: x.properties.color, fillOpacity: 1, interactive: false }));
122
+ });
123
+ const self = this;
124
+ const pin = function (x, html, size) {
125
+ const m = L.marker(ll(x.geometry.coordinates), {
126
+ draggable: true, keyboard: false, autoPan: true,
127
+ icon: L.divIcon({ className: 'smap-plan-pin', html: html, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }),
128
+ });
129
+ m.feature = x;
130
+ self._bindDrag(m, f);
131
+ group.addLayer(m);
132
+ };
133
+ f.unassigned.forEach(function (x) {
134
+ pin(x, '<span style="display:block;width:14px;height:14px;border-radius:50%;background:#9ca3af;border:2px solid #fff;box-shadow:0 0 0 1px rgba(15,23,42,.3)"></span>', 18);
135
+ });
136
+ f.stops.forEach(function (x) {
137
+ const p = x.properties;
138
+ pin(x, '<span style="display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;' +
139
+ 'background:' + escapeHtml(p.color) + ';border:2px solid ' + escapeHtml(p.border) + ';color:#fff;' +
140
+ 'font:600 11px/1 -apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,Arial,sans-serif">' + escapeHtml(p.label) + '</span>', 24);
141
+ });
142
+ },
143
+
144
+ // A pin dragged with Leaflet's marker dragging; the target is the stop or the line under the pin.
145
+ _bindDrag: function (marker, features) {
146
+ const self = this;
147
+ const id = marker.feature.properties.id;
148
+ const hitsAt = function (latlng) {
149
+ const map = self._map;
150
+ const point = map.latLngToContainerPoint(latlng);
151
+ const project = function (c) { return map.latLngToContainerPoint([c[1], c[0]]); };
152
+ return { point: point, hits: parts().featuresAt(features, project, point) };
153
+ };
154
+ marker.on('dragstart', function () { self._drag.start(id, self._map.latLngToContainerPoint(marker.getLatLng())); });
155
+ marker.on('drag', function (e) {
156
+ const at = hitsAt(e.latlng || marker.getLatLng());
157
+ self._drag.move(at.hits, at.point);
158
+ });
159
+ marker.on('dragend', function () {
160
+ const at = hitsAt(marker.getLatLng());
161
+ self._drag.move(at.hits, at.point);
162
+ // The pin goes back to its stop unless the drop changed the plan (which draws it anew).
163
+ self._drag.end(at.hits).then(function (done) { if (!done && self._map) { self._draw(); } });
164
+ });
165
+ },
166
+ });
167
+
168
+ /** The plan editor as a Leaflet layer (see SMapPlanEditor). */
169
+ export function smapPlanEditor(options) { return new SMapPlanEditor(options); }
package/types/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Types of @superrouteca/smap-leaflet.
2
2
  import * as L from 'leaflet';
3
- import type { Map as SMapMap, MapOptions as SMapMapOptions, StyleFlavor } from '@superrouteca/smap';
3
+ import type { Map as SMapMap, MapOptions as SMapMapOptions, StyleFlavor, PlanEditor, PlanEditorOptions, PlanFeatures, PlanPanel, PlanTextKey } from '@superrouteca/smap';
4
4
  import type SMapClient from '@superrouteca/smap/client';
5
5
 
6
6
  export type { StyleFlavor };
@@ -135,6 +135,46 @@ export declare const OSM_ATTRIBUTION: string;
135
135
  /** The sMap map calls an SMapLayer forwards. */
136
136
  export declare const FORWARDED: string[];
137
137
 
138
+
139
+ // ------------------------------------------------------------------ plan editor
140
+
141
+ /** The plan editor's options in Leaflet: the editor's (or an editor of your own) and how it is shown. */
142
+ export interface SMapPlanEditorOptions extends Partial<Omit<PlanEditorOptions, 'credentials'>> {
143
+ /** Required unless `editor` is given. */
144
+ credentials?: PlanEditorOptions['credentials'];
145
+ /** An editor you made; the layer binds it and never destroys it. */
146
+ editor?: PlanEditor;
147
+ /** Marks the features drawn (`smap_plan`); default 'plan'. */
148
+ id?: string;
149
+ /** Route colours, one per vehicle in the plan's order (repeated). */
150
+ colors?: string[];
151
+ /** Price the drop target while a pin is dragged (default true). */
152
+ preview?: boolean;
153
+ /** The panel's language (default the browser's); any BCP-47 tag, English when none of the 14 fits. */
154
+ language?: string;
155
+ labels?: Partial<Record<PlanTextKey, string>>;
156
+ /** The ready-made panel as a Leaflet control (default true, top right). */
157
+ panel?: boolean | { position?: L.ControlPosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; className?: string; labels?: Partial<Record<PlanTextKey, string>> };
158
+ /** Add the panel's stylesheet to the page (default true). */
159
+ css?: boolean;
160
+ }
161
+
162
+ /** The plan editor as a Leaflet layer: routes as Leaflet layers, stops dragged with Leaflet's marker dragging. */
163
+ export declare class SMapPlanEditor extends L.Layer {
164
+ constructor(options: SMapPlanEditorOptions);
165
+ readonly editor: PlanEditor;
166
+ /** The ready-made panel while the layer is on a map (null with `panel: false`). */
167
+ readonly panel: PlanPanel | null;
168
+ features(): PlanFeatures;
169
+ /** Off the map for good; an editor the layer made is destroyed with it. */
170
+ destroy(): this;
171
+ }
172
+ /** The ready-made plan panel as a Leaflet control. */
173
+ export declare class SMapPlanPanel extends L.Control {
174
+ constructor(panel: PlanPanel, options?: L.ControlOptions);
175
+ }
176
+ export declare function smapPlanEditor(options: SMapPlanEditorOptions): SMapPlanEditor;
177
+
138
178
  declare const SMapLeaflet: {
139
179
  version: string;
140
180
  styles: StyleFlavor[];
@@ -148,5 +188,7 @@ declare const SMapLeaflet: {
148
188
  hasWebGL2: typeof hasWebGL2;
149
189
  rasterPath: typeof rasterPath;
150
190
  checkStyle: typeof checkStyle;
191
+ SMapPlanEditor: typeof SMapPlanEditor;
192
+ smapPlanEditor: typeof smapPlanEditor;
151
193
  };
152
194
  export default SMapLeaflet;