@superrouteca/smap-openlayers 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 +7 -0
- package/README.md +34 -0
- package/package.json +2 -2
- package/src/index.js +4 -3
- package/src/plan.js +156 -0
- package/types/index.d.ts +42 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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 an OpenLayers map — drag a stop within its
|
|
6
|
+
route or onto another route (priced as a preview while dragging), take stops off, place new orders, refine and undo
|
|
7
|
+
or redo, each a call of the Open Platform's plan editing. The routes are drawn as a vector layer and the ready-made
|
|
8
|
+
panel in 14 languages sits in a corner as a control.
|
|
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
|
@@ -136,6 +136,40 @@ A layer's opacity, zoom range and the source's attributions come along. Without
|
|
|
136
136
|
top, in the order added. `addOpenLayersLayers` skips an sMap basemap already in the OpenLayers map and lists what it
|
|
137
137
|
could not add in `skipped`.
|
|
138
138
|
|
|
139
|
+
## Plan editor
|
|
140
|
+
|
|
141
|
+
`addPlanEditor(map, options)` lets a person edit a plan of routes on an OpenLayers map: drag a stop within its route
|
|
142
|
+
or onto another route, take stops off, place new orders at the positions the service ranks best, refine routes around
|
|
143
|
+
locked stops, and undo or redo. Each edit is one call of the Open Platform's plan editing with the whole plan; the
|
|
144
|
+
answer replaces the routes, distances, times and violations drawn. Nothing is kept on a server: keep the plan from the
|
|
145
|
+
`change` event. The editor is the Web SDK's (`PlanEditor` of `@superrouteca/smap`), so its calls, events and
|
|
146
|
+
credentials are the same (see the Web SDK README, "Plan editor").
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { addPlanEditor } from '@superrouteca/smap-openlayers';
|
|
150
|
+
|
|
151
|
+
const plan = addPlanEditor(map, {
|
|
152
|
+
// Your own server mints the token with POST /v1/plan/token; or { key: 'smk_…' } with plan editing switched on.
|
|
153
|
+
credentials: { token: () => fetch('/plan-token').then((r) => r.json()) },
|
|
154
|
+
plan: { region: 'NA', profile: 'car', vehicles, jobs, routes },
|
|
155
|
+
language: 'fr',
|
|
156
|
+
});
|
|
157
|
+
plan.editor.on('change', (e) => save(e.plan));
|
|
158
|
+
// later
|
|
159
|
+
plan.remove();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `plan.layer` is a vector layer with the legs, the vehicles' starts, the unassigned jobs and the stops (numbered in
|
|
163
|
+
route order, outlined dark red with a violation, near black when locked); `features()` gives them as GeoJSON.
|
|
164
|
+
- `plan.interaction` drags a stop or an unassigned job: drop it on a stop to put it before that stop, on a route's line
|
|
165
|
+
to 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
|
|
166
|
+
plan with the stop at the target is priced as a preview (`preview: false` turns that off).
|
|
167
|
+
- `plan.control` holds the ready-made panel (top right; `panel: { position, className, labels, label }`, or
|
|
168
|
+
`panel: false`): routes and totals, violations, undo / redo / refine, the unassigned jobs and insert positions, in
|
|
169
|
+
14 languages. Its stylesheet is added to the page unless `css: false`.
|
|
170
|
+
- `editor: myEditor` binds an editor you made; `remove()` then leaves it running. `id` marks the features, `colors`
|
|
171
|
+
sets the route colours. `editor.destroy()` also removes everything from the map.
|
|
172
|
+
|
|
139
173
|
## Example
|
|
140
174
|
|
|
141
175
|
`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-openlayers",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "sMap for OpenLayers: maps from Superroute's sMap service as an OpenLayers layer (vector or raster), and OpenLayers sources 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.
|
|
62
|
+
"@superrouteca/smap": "1.1.6"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"jsdom": "30.1.3",
|
package/src/index.js
CHANGED
|
@@ -12,11 +12,12 @@ import * as sMapModule from '@superrouteca/smap';
|
|
|
12
12
|
import { SMapAccess, checkStyle, rasterPath, stylePath, FLAVORS, DEFAULT_BASE } from './common/access.js';
|
|
13
13
|
import { OSM_ATTRIBUTION, createBadge } from './common/badge.js';
|
|
14
14
|
import { createEmbedded, hasWebGL2 } from './common/embed.js';
|
|
15
|
+
import { addPlanEditor } from './plan.js';
|
|
15
16
|
|
|
16
17
|
const SDK = sMapModule.default && sMapModule.default.Map ? sMapModule.default : sMapModule;
|
|
17
18
|
|
|
18
|
-
export const version = '1.1.
|
|
19
|
-
export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION, hasWebGL2 };
|
|
19
|
+
export const version = '1.1.6';
|
|
20
|
+
export { FLAVORS as styles, checkStyle, rasterPath, stylePath, SMapAccess, DEFAULT_BASE, OSM_ATTRIBUTION, hasWebGL2, addPlanEditor };
|
|
20
21
|
|
|
21
22
|
/** OpenLayers layer options; everything else of SMapLayer's options goes to the sMap map. */
|
|
22
23
|
const LAYER_OPTIONS = ['className', 'opacity', 'visible', 'extent', 'zIndex', 'minResolution', 'maxResolution', 'minZoom', 'maxZoom', 'properties', 'map'];
|
|
@@ -322,5 +323,5 @@ export function smapBasemap(options) {
|
|
|
322
323
|
return new SMapLayer(o);
|
|
323
324
|
}
|
|
324
325
|
|
|
325
|
-
const SMapOpenLayers = { version, styles: FLAVORS, SMapLayer, smapLayer, SMapRasterSource, smapRasterSource, SMapRasterLayer, smapRasterLayer, smapBasemap, SMapBadge, SMapAccess, hasWebGL2, rasterPath, checkStyle };
|
|
326
|
+
const SMapOpenLayers = { version, styles: FLAVORS, SMapLayer, smapLayer, SMapRasterSource, smapRasterSource, SMapRasterLayer, smapRasterLayer, smapBasemap, SMapBadge, SMapAccess, hasWebGL2, rasterPath, checkStyle, addPlanEditor };
|
|
326
327
|
export default SMapOpenLayers;
|
package/src/plan.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// The plan editor (Superroute OPEN-ROUTE-EDIT E10) on an OpenLayers map: the plan drawn as a vector layer, stops
|
|
2
|
+
// dragged within and between routes through a pointer interaction, and the ready-made panel as an OpenLayers control.
|
|
3
|
+
// The editor itself (state, calls, undo / redo, the panel) is the Web SDK's: PlanEditor and planEditorParts.
|
|
4
|
+
import VectorLayer from 'ol/layer/Vector.js';
|
|
5
|
+
import VectorSource from 'ol/source/Vector.js';
|
|
6
|
+
import GeoJSON from 'ol/format/GeoJSON.js';
|
|
7
|
+
import PointerInteraction from 'ol/interaction/Pointer.js';
|
|
8
|
+
import Control from 'ol/control/Control.js';
|
|
9
|
+
import { Style, Stroke, Fill, Circle as CircleStyle, Text } from 'ol/style.js';
|
|
10
|
+
import { fromLonLat } from 'ol/proj.js';
|
|
11
|
+
import * as sMapModule from '@superrouteca/smap';
|
|
12
|
+
|
|
13
|
+
const SDK = sMapModule.default && sMapModule.default.PlanEditor ? sMapModule.default : sMapModule;
|
|
14
|
+
|
|
15
|
+
/** Options of the binding, not of the editor. */
|
|
16
|
+
const OWN = ['editor', 'id', 'colors', 'preview', 'language', 'labels', 'panel', 'css', 'zIndex', 'plan'];
|
|
17
|
+
const CORNERS = { 'top-left': { top: '8px', left: '8px' }, 'top-right': { top: '8px', right: '8px' }, 'bottom-left': { bottom: '24px', left: '8px' }, 'bottom-right': { bottom: '24px', right: '8px' } };
|
|
18
|
+
|
|
19
|
+
function editorOptions(options) {
|
|
20
|
+
const out = {};
|
|
21
|
+
Object.keys(options).forEach(function (k) { if (OWN.indexOf(k) < 0) { out[k] = options[k]; } });
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function styleFor(feature, cache) {
|
|
26
|
+
const p = feature.getProperties();
|
|
27
|
+
const key = [p.kind, p.color, p.border, p.label].join('|');
|
|
28
|
+
if (cache[key]) { return cache[key]; }
|
|
29
|
+
let style;
|
|
30
|
+
if (p.kind === 'leg') {
|
|
31
|
+
style = [new Style({ stroke: new Stroke({ color: '#ffffff', width: 7 }) }), new Style({ stroke: new Stroke({ color: p.color, width: 4 }) })];
|
|
32
|
+
} else if (p.kind === 'stop') {
|
|
33
|
+
style = new Style({
|
|
34
|
+
image: new CircleStyle({ radius: 11, fill: new Fill({ color: p.color }), stroke: new Stroke({ color: p.border || p.color, width: 3 }) }),
|
|
35
|
+
text: new Text({ text: String(p.label || ''), fill: new Fill({ color: '#ffffff' }), font: 'bold 11px sans-serif' }),
|
|
36
|
+
zIndex: 3,
|
|
37
|
+
});
|
|
38
|
+
} else if (p.kind === 'unassigned') {
|
|
39
|
+
style = new Style({ image: new CircleStyle({ radius: 8, fill: new Fill({ color: '#9ca3af' }), stroke: new Stroke({ color: '#ffffff', width: 2 }) }), zIndex: 2 });
|
|
40
|
+
} else {
|
|
41
|
+
style = new Style({ image: new CircleStyle({ radius: 8, fill: new Fill({ color: p.color }), stroke: new Stroke({ color: '#ffffff', width: 2 }) }), zIndex: 1 });
|
|
42
|
+
}
|
|
43
|
+
cache[key] = style;
|
|
44
|
+
return style;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A plan editor on an OpenLayers map.
|
|
49
|
+
* const plan = addPlanEditor(map, { credentials: { token: getPlanToken }, plan, language: 'fr' });
|
|
50
|
+
* plan.editor.on('change', (e) => save(e.plan));
|
|
51
|
+
* Options: every PlanEditor option, or `editor` (an editor of your own to bind), plus `id` (default 'plan'),
|
|
52
|
+
* `colors`, `preview` (default true), `language`, `labels`, `panel` (true | false | { position, className, labels,
|
|
53
|
+
* label }), `css` (default true: the panel's stylesheet is added to the page), `zIndex` (of the layer).
|
|
54
|
+
* @returns {{ editor, layer, interaction, control, panel, features(): Object, remove(): void }}
|
|
55
|
+
*/
|
|
56
|
+
export function addPlanEditor(map, options) {
|
|
57
|
+
options = options || {};
|
|
58
|
+
if (!map || typeof map.addLayer !== 'function') { throw new Error('sMap: addPlanEditor(map, options) needs an OpenLayers map'); }
|
|
59
|
+
const parts = SDK.planEditorParts;
|
|
60
|
+
const own = !options.editor;
|
|
61
|
+
const editor = options.editor || new SDK.PlanEditor(Object.assign(editorOptions(options), { plan: null }));
|
|
62
|
+
const id = options.id || 'plan';
|
|
63
|
+
const doc = (map.getTargetElement && map.getTargetElement() && map.getTargetElement().ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
64
|
+
const format = new GeoJSON();
|
|
65
|
+
const source = new VectorSource();
|
|
66
|
+
const cache = {};
|
|
67
|
+
const layer = new VectorLayer({ source, style: function (f) { return styleFor(f, cache); }, zIndex: options.zIndex === undefined ? 10 : options.zIndex, properties: { smapPlan: id } });
|
|
68
|
+
const drag = parts.planDrag(editor, { preview: options.preview !== false });
|
|
69
|
+
let removed = false;
|
|
70
|
+
|
|
71
|
+
function features() { return parts.planFeatures(editor, id, options.colors); }
|
|
72
|
+
function draw() {
|
|
73
|
+
if (removed) { return; }
|
|
74
|
+
const f = features();
|
|
75
|
+
const all = [].concat(f.legs, f.starts, f.unassigned, f.stops);
|
|
76
|
+
source.clear(true);
|
|
77
|
+
source.addFeatures(format.readFeatures({ type: 'FeatureCollection', features: all }, { featureProjection: 'EPSG:3857' }));
|
|
78
|
+
}
|
|
79
|
+
// OpenLayers' own hit detection (forEachFeatureAtPixel) reads the rendered canvas; the plan's features are tested
|
|
80
|
+
// in pixels here instead, so it also works before a frame is drawn (and under test without a canvas).
|
|
81
|
+
function hitsAt(pixel) {
|
|
82
|
+
const point = { x: pixel[0], y: pixel[1] };
|
|
83
|
+
const project = function (lngLat) { const px = map.getPixelFromCoordinate(fromLonLat(lngLat)); return px ? { x: px[0], y: px[1] } : { x: Infinity, y: Infinity }; };
|
|
84
|
+
return parts.featuresAt(features(), project, point);
|
|
85
|
+
}
|
|
86
|
+
function dragged() {
|
|
87
|
+
const stopId = drag.dragging();
|
|
88
|
+
return source.getFeatures().filter(function (x) { const p = x.getProperties(); return (p.kind === 'stop' || p.kind === 'unassigned') && p.id === stopId; })[0] || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const interaction = new PointerInteraction({
|
|
92
|
+
handleDownEvent: function (e) {
|
|
93
|
+
if (removed || !e.pixel) { return false; }
|
|
94
|
+
const hit = hitsAt(e.pixel).filter(function (f) { return f.properties.kind === 'stop' || f.properties.kind === 'unassigned'; })[0];
|
|
95
|
+
if (!hit) { return false; }
|
|
96
|
+
drag.start(hit.properties.id, { x: e.pixel[0], y: e.pixel[1] });
|
|
97
|
+
return true; // the drag is this interaction's: the map does not pan
|
|
98
|
+
},
|
|
99
|
+
handleDragEvent: function (e) {
|
|
100
|
+
if (!drag.dragging() || !e.pixel) { return; }
|
|
101
|
+
drag.move(hitsAt(e.pixel), { x: e.pixel[0], y: e.pixel[1] });
|
|
102
|
+
const f = drag.moved() ? dragged() : null;
|
|
103
|
+
if (f && e.coordinate) { f.getGeometry().setCoordinates(e.coordinate); }
|
|
104
|
+
},
|
|
105
|
+
handleUpEvent: function (e) {
|
|
106
|
+
if (!drag.dragging()) { return false; }
|
|
107
|
+
const moved = drag.moved();
|
|
108
|
+
drag.end(e && e.pixel ? hitsAt(e.pixel) : null);
|
|
109
|
+
if (moved) { draw(); } // the pin goes back to its place until the answer moves it
|
|
110
|
+
return false;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let control = null;
|
|
115
|
+
let panel = null;
|
|
116
|
+
if (options.panel !== false && doc) {
|
|
117
|
+
const po = typeof options.panel === 'object' && options.panel ? options.panel : {};
|
|
118
|
+
if (options.css !== false) { parts.injectCss(doc); }
|
|
119
|
+
const language = options.language || (typeof navigator !== 'undefined' && navigator.language) || 'en';
|
|
120
|
+
panel = parts.createPanel(editor, { language, labels: po.labels || options.labels, document: doc, colors: options.colors, className: po.className });
|
|
121
|
+
const position = CORNERS[po.position] ? po.position : 'top-right';
|
|
122
|
+
const element = doc.createElement('div');
|
|
123
|
+
element.className = 'smap-plan-control smap-plan-' + position + ' ol-unselectable';
|
|
124
|
+
element.setAttribute('aria-label', po.label || parts.textsFor(language, po.labels || options.labels).editor);
|
|
125
|
+
Object.assign(element.style, { position: 'absolute' }, CORNERS[position]);
|
|
126
|
+
element.appendChild(panel.element);
|
|
127
|
+
control = new Control({ element });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const onChange = function () { draw(); };
|
|
131
|
+
editor.on('change', onChange);
|
|
132
|
+
const gone = function () { handle.remove(); };
|
|
133
|
+
editor.on('destroy', gone);
|
|
134
|
+
map.addLayer(layer);
|
|
135
|
+
map.addInteraction(interaction);
|
|
136
|
+
if (control) { map.addControl(control); }
|
|
137
|
+
draw();
|
|
138
|
+
if (own && options.plan) { editor.setPlan(options.plan).catch(function () {}); }
|
|
139
|
+
|
|
140
|
+
const handle = {
|
|
141
|
+
editor, layer, interaction, control, panel, features,
|
|
142
|
+
remove: function () {
|
|
143
|
+
if (removed) { return; }
|
|
144
|
+
removed = true;
|
|
145
|
+
drag.cancel();
|
|
146
|
+
editor.off('change', onChange);
|
|
147
|
+
editor.off('destroy', gone);
|
|
148
|
+
map.removeLayer(layer);
|
|
149
|
+
map.removeInteraction(interaction);
|
|
150
|
+
if (control) { map.removeControl(control); }
|
|
151
|
+
if (panel) { panel.destroy(); }
|
|
152
|
+
if (own) { editor.destroy(); }
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
return handle;
|
|
156
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -4,7 +4,11 @@ import TileLayer from 'ol/layer/Tile.js';
|
|
|
4
4
|
import XYZ from 'ol/source/XYZ.js';
|
|
5
5
|
import Control from 'ol/control/Control.js';
|
|
6
6
|
import type { Options as XYZOptions } from 'ol/source/XYZ.js';
|
|
7
|
-
import type { Map as SMapMap, MapOptions as SMapMapOptions, StyleFlavor } from '@superrouteca/smap';
|
|
7
|
+
import type { Map as SMapMap, MapOptions as SMapMapOptions, StyleFlavor, PlanEditor, PlanEditorOptions, PlanFeatures, PlanPanel, PlanTextKey } from '@superrouteca/smap';
|
|
8
|
+
import type OlMap from 'ol/Map.js';
|
|
9
|
+
import type VectorLayer from 'ol/layer/Vector.js';
|
|
10
|
+
import type VectorSource from 'ol/source/Vector.js';
|
|
11
|
+
import type PointerInteraction from 'ol/interaction/Pointer.js';
|
|
8
12
|
import type SMapClient from '@superrouteca/smap/client';
|
|
9
13
|
|
|
10
14
|
export type { StyleFlavor };
|
|
@@ -164,6 +168,42 @@ export declare const DEFAULT_BASE: string;
|
|
|
164
168
|
export declare const OSM_ATTRIBUTION: string;
|
|
165
169
|
export declare const FORWARDED: string[];
|
|
166
170
|
|
|
171
|
+
/** Options of addPlanEditor: the editor's (or an editor of your own as `editor`) and the binding's. */
|
|
172
|
+
export type PlanEditorBindingOptions = (PlanEditorOptions | (Partial<PlanEditorOptions> & { editor: PlanEditor })) & {
|
|
173
|
+
/** Marks the features (`properties.smap_plan`); default 'plan'. */
|
|
174
|
+
id?: string;
|
|
175
|
+
/** Route colours, one per vehicle in the plan's order (repeated). */
|
|
176
|
+
colors?: string[];
|
|
177
|
+
/** Price the drop target while a stop is dragged (default true). */
|
|
178
|
+
preview?: boolean;
|
|
179
|
+
/** The panel's language (any BCP-47 tag; default the browser's). */
|
|
180
|
+
language?: string;
|
|
181
|
+
labels?: Partial<Record<PlanTextKey, string>>;
|
|
182
|
+
/** The ready-made panel (default true): false for none, or its corner, class, texts and label. */
|
|
183
|
+
panel?: boolean | { position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; className?: string; labels?: Partial<Record<PlanTextKey, string>>; label?: string };
|
|
184
|
+
/** Add the panel's stylesheet to the page (default true). */
|
|
185
|
+
css?: boolean;
|
|
186
|
+
/** The vector layer's z-index (default 10). */
|
|
187
|
+
zIndex?: number;
|
|
188
|
+
};
|
|
189
|
+
/** A plan editor on an OpenLayers map. */
|
|
190
|
+
export interface OpenLayersPlanEditor {
|
|
191
|
+
editor: PlanEditor;
|
|
192
|
+
/** The plan: legs, starts, unassigned jobs and stops, in the view's projection. */
|
|
193
|
+
layer: VectorLayer<VectorSource>;
|
|
194
|
+
/** Drags stops and unassigned jobs; the map does not pan while one is dragged. */
|
|
195
|
+
interaction: PointerInteraction;
|
|
196
|
+
/** The panel as a control; null with `panel: false`. */
|
|
197
|
+
control: Control | null;
|
|
198
|
+
panel: PlanPanel | null;
|
|
199
|
+
/** The features drawn, in longitude / latitude. */
|
|
200
|
+
features(): PlanFeatures;
|
|
201
|
+
/** Takes the layer, interaction and control off the map; ends the editor when this call made it. */
|
|
202
|
+
remove(): void;
|
|
203
|
+
}
|
|
204
|
+
/** A plan editor on an OpenLayers map: the plan drawn, stops dragged within and between routes, the panel. */
|
|
205
|
+
export declare function addPlanEditor(map: OlMap, options: PlanEditorBindingOptions): OpenLayersPlanEditor;
|
|
206
|
+
|
|
167
207
|
declare const SMapOpenLayers: {
|
|
168
208
|
version: string;
|
|
169
209
|
styles: StyleFlavor[];
|
|
@@ -179,5 +219,6 @@ declare const SMapOpenLayers: {
|
|
|
179
219
|
hasWebGL2: typeof hasWebGL2;
|
|
180
220
|
rasterPath: typeof rasterPath;
|
|
181
221
|
checkStyle: typeof checkStyle;
|
|
222
|
+
addPlanEditor: typeof addPlanEditor;
|
|
182
223
|
};
|
|
183
224
|
export default SMapOpenLayers;
|