@superrouteca/smap-deckgl 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 +3 -2
- package/src/index.js +2 -1
- package/src/plan.js +164 -0
- package/types/index.d.ts +39 -0
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(deck, options)` draws a plan of routes as deck.gl layers beside the app's own and lets a
|
|
6
|
+
person drag stops within and between routes, with the stop's new place priced while it is dragged. The ready-made
|
|
7
|
+
panel in 14 languages (routes, totals, violations, undo and redo, insert positions) sits in a corner of the map.
|
|
8
|
+
Every edit is a call of the Open Platform's plan editing.
|
|
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
|
@@ -165,6 +165,40 @@ Then pick a style by its id (`smap-light`, `smap-dark`, `smap-custom-sms_…`),
|
|
|
165
165
|
A map key goes in the style URL itself, so a key is enough even where `transformRequest` is not passed; a token needs
|
|
166
166
|
`transformRequest`. kepler.gl shows the OpenStreetMap credit from the style's sources.
|
|
167
167
|
|
|
168
|
+
## Plan editor
|
|
169
|
+
|
|
170
|
+
`addPlanEditor(deck, options)` lets a person edit a plan of routes on a deck.gl map: drag a stop within its route or
|
|
171
|
+
onto another route, take stops off, place new orders at the positions the service ranks best, refine routes around
|
|
172
|
+
locked stops, and undo or redo. Every edit is one call of the Open Platform's plan editing with the whole plan (see
|
|
173
|
+
the plan editor of `@superrouteca/smap`); nothing is kept on a server, so keep the plan from the `change` event.
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import { Deck } from '@deck.gl/core';
|
|
177
|
+
import { addPlanEditor } from '@superrouteca/smap-deckgl';
|
|
178
|
+
|
|
179
|
+
const deck = new Deck({ parent: document.getElementById('map'), controller: true, initialViewState, layers: [myLayer] });
|
|
180
|
+
const plan = addPlanEditor(deck, {
|
|
181
|
+
// A short-lived plan editor token your own server mints with POST /v1/plan/token, or { key: 'smk_…' }.
|
|
182
|
+
credentials: { token: () => fetch('/plan-token').then((r) => r.json()) },
|
|
183
|
+
plan: { vehicles, jobs, routes },
|
|
184
|
+
language: 'fr',
|
|
185
|
+
});
|
|
186
|
+
plan.editor.on('change', (e) => save(e.plan));
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- **Layers**: `plan:legs`, `plan:starts`, `plan:unassigned` and `plan:stops` (`id` changes the prefix, `colors` the
|
|
190
|
+
route colours), added to the deck's own layers and replaced on every change; the app's layers stay. Pass
|
|
191
|
+
`onLayers(layers)` to place them yourself (for example with `deckLayers(map)` on an sMap map); `plan.layers()`
|
|
192
|
+
gives them at any time.
|
|
193
|
+
- **Dragging** uses deck.gl's drag callbacks on the stop and job layers: drop a stop on another stop to put it before
|
|
194
|
+
that stop, or on a route's line to put it into that leg. The map does not pan while a stop is dragged, and the plan
|
|
195
|
+
with the stop at its target is priced and shown in the panel as a preview (`preview: false` turns it off).
|
|
196
|
+
- **The panel** sits in a corner of the deck.gl container (`panel: { position: 'top-left' }`, default top right; or
|
|
197
|
+
`container`), in 14 languages with your own `labels`; its stylesheet is added to the page (`css: false` leaves it to
|
|
198
|
+
you). `panel: false` leaves it out, for a panel of your own on `plan.editor.state()` and its events.
|
|
199
|
+
- `editor`: draw and edit an editor you made (`new PlanEditor(...)` from `@superrouteca/smap`); `remove()` then leaves
|
|
200
|
+
it running. Otherwise `remove()` also ends the editor, and ending the editor removes everything.
|
|
201
|
+
|
|
168
202
|
## Example
|
|
169
203
|
|
|
170
204
|
[`examples/index.html`](examples/index.html): serve the repository root (`npx serve .`) and open
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superrouteca/smap-deckgl",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "sMap for deck.gl: sMap as the basemap of deck.gl apps (interleaved or drawn by deck.gl), deck.gl layers on sMap maps, and kepler.gl map styles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Goodneo Solutions",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typecheck": "tsc -p test/tsconfig.json"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@superrouteca/smap": "1.1.
|
|
49
|
+
"@superrouteca/smap": "1.1.6"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@deck.gl/core": "^9.0.0",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@deck.gl/layers": "9.4.0",
|
|
60
60
|
"@deck.gl/geo-layers": "9.4.0",
|
|
61
61
|
"@deck.gl/mapbox": "9.4.0",
|
|
62
|
+
"jsdom": "30.1.3",
|
|
62
63
|
"typescript": "6.0.3"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/src/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { createVectorBasemapLayer, createRasterBasemapLayer, tileSubLayers } fro
|
|
|
4
4
|
export { createSMapDeck, deckLayers, registerDeck, addAttribution } from './overlay.js';
|
|
5
5
|
export { pointsLayers, linesLayers, polygonsLayers, popupTooltip, toFeatures, accessor } from './layers.js';
|
|
6
6
|
export { keplerMapStyles, keplerTransformRequest } from './kepler.js';
|
|
7
|
+
export { addPlanEditor } from './plan.js';
|
|
7
8
|
export { parseColor, evaluate, compileStyle, drawTile } from './style.js';
|
|
8
|
-
export const version = '1.1.
|
|
9
|
+
export const version = '1.1.6';
|
package/src/plan.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// The plan editor (OPEN-ROUTE-EDIT E10) on a deck.gl map: the plan drawn as deck.gl layers, stops dragged through
|
|
2
|
+
// deck.gl's own drag callbacks onto another stop or a route's line, and the ready-made panel in a corner of the deck.gl
|
|
3
|
+
// container. Every edit is a plan.* call of the Open Platform made by the SDK's PlanEditor.
|
|
4
|
+
import { PlanEditor, planEditorParts } from '@superrouteca/smap';
|
|
5
|
+
import { linesLayers, pointsLayers } from './layers.js';
|
|
6
|
+
|
|
7
|
+
const EDITOR_OPTIONS = ['credentials', 'baseUrl', 'fetch', 'app', 'region', 'profile', 'snapshot', 'geometry', 'insertLimit', 'objective'];
|
|
8
|
+
const CORNERS = {
|
|
9
|
+
'top-left': 'top:10px;left:10px', 'top-right': 'top:10px;right:10px',
|
|
10
|
+
'bottom-left': 'bottom:10px;left:10px', 'bottom-right': 'bottom:30px;right:10px',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const fc = (features) => ({ type: 'FeatureCollection', features });
|
|
14
|
+
// A feature behind a deck.gl pick: points pick their feature, lines a { path, feature } row.
|
|
15
|
+
const featureOf = (object) => (object && object.feature ? object.feature : object);
|
|
16
|
+
|
|
17
|
+
function withProps(layer, props) {
|
|
18
|
+
return typeof layer.clone === 'function' ? layer.clone(props) : new layer.constructor(layer.props, props);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A plan editor drawn by deck.gl.
|
|
23
|
+
*
|
|
24
|
+
* const plan = addPlanEditor(deck, { credentials: { token: getPlanToken }, plan, language: 'fr' });
|
|
25
|
+
* plan.editor.on('change', (e) => save(e.plan));
|
|
26
|
+
*
|
|
27
|
+
* @param {object} deck a Deck (or anything with setProps / props / pickMultipleObjects, such as a MapboxOverlay)
|
|
28
|
+
* @param {object} options the PlanEditor options or `editor` (an editor of your own), and `id` (layer id prefix,
|
|
29
|
+
* default 'plan'), `colors`, `preview` (default true), `language`, `labels`, `panel` (default true; false, or
|
|
30
|
+
* { position, className, labels, container }), `css` (default true), `container` (where the panel goes; default
|
|
31
|
+
* the deck.gl canvas's parent), `onLayers(layers)` (to place the layers yourself instead of setProps)
|
|
32
|
+
*/
|
|
33
|
+
export function addPlanEditor(deck, options = {}) {
|
|
34
|
+
if (!deck || typeof deck.setProps !== 'function') { throw new Error('sMap: addPlanEditor(deck, options) needs a deck.gl Deck or overlay'); }
|
|
35
|
+
const P = planEditorParts;
|
|
36
|
+
const id = options.id || 'plan';
|
|
37
|
+
const ids = { legs: id + ':legs', starts: id + ':starts', unassigned: id + ':unassigned', stops: id + ':stops', drag: id + ':drag' };
|
|
38
|
+
const own = !options.editor;
|
|
39
|
+
const editor = options.editor || new PlanEditor(Object.fromEntries(Object.entries(options).filter(([k]) => EDITOR_OPTIONS.includes(k))));
|
|
40
|
+
const drag = P.planDrag(editor, { preview: options.preview !== false });
|
|
41
|
+
let removed = false;
|
|
42
|
+
let current = [];
|
|
43
|
+
let dragAt = null;
|
|
44
|
+
let savedController;
|
|
45
|
+
|
|
46
|
+
function setPan(on) {
|
|
47
|
+
const props = deck.props || {};
|
|
48
|
+
if (!on) {
|
|
49
|
+
if (props.controller === undefined || props.controller === null || props.controller === false) { savedController = undefined; return; }
|
|
50
|
+
savedController = props.controller;
|
|
51
|
+
deck.setProps({ controller: Object.assign({}, typeof props.controller === 'object' ? props.controller : {}, { dragPan: false }) });
|
|
52
|
+
} else if (savedController !== undefined) {
|
|
53
|
+
deck.setProps({ controller: savedController });
|
|
54
|
+
savedController = undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hitsAt(x, y) {
|
|
59
|
+
if (typeof deck.pickMultipleObjects !== 'function') { return []; }
|
|
60
|
+
const picked = deck.pickMultipleObjects({ x, y, radius: 6, layerIds: [ids.legs, ids.starts, ids.stops] }) || [];
|
|
61
|
+
return picked.map((p) => featureOf(p.object)).filter((f) => f && f.properties && f.properties.smap_plan === id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dragProps = {
|
|
65
|
+
onDragStart(info) {
|
|
66
|
+
const f = featureOf(info && info.object);
|
|
67
|
+
if (removed || !f || !f.properties || (f.properties.kind !== 'stop' && f.properties.kind !== 'unassigned')) { return false; }
|
|
68
|
+
drag.start(f.properties.id, { x: info.x, y: info.y });
|
|
69
|
+
setPan(false);
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
onDrag(info) {
|
|
73
|
+
if (!drag.dragging()) { return false; }
|
|
74
|
+
drag.move(hitsAt(info.x, info.y), { x: info.x, y: info.y });
|
|
75
|
+
if (drag.moved() && info.coordinate) { dragAt = [info.coordinate[0], info.coordinate[1]]; update(); }
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
onDragEnd(info) {
|
|
79
|
+
if (!drag.dragging()) { return false; }
|
|
80
|
+
setPan(true);
|
|
81
|
+
dragAt = null;
|
|
82
|
+
update();
|
|
83
|
+
drag.end(info && typeof info.x === 'number' ? hitsAt(info.x, info.y) : null);
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function build() {
|
|
89
|
+
const f = P.planFeatures(editor, id, options.colors);
|
|
90
|
+
// The casing comes first, under the line.
|
|
91
|
+
const legs = linesLayers(ids.legs, fc(f.legs), { color: { property: 'color' }, width: 4, casing: true, pickable: true });
|
|
92
|
+
const starts = pointsLayers(ids.starts, fc(f.starts), { marker: { shape: 'circle', fill: { property: 'color' }, border: '#ffffff', size: 18 }, pickable: true });
|
|
93
|
+
const [unassigned, ...unRest] = pointsLayers(ids.unassigned, fc(f.unassigned), { marker: { shape: 'circle', fill: '#9ca3af', border: '#ffffff', size: 18 }, pickable: true });
|
|
94
|
+
const [stops, ...stopRest] = pointsLayers(ids.stops, fc(f.stops), { marker: { shape: 'pin', fill: { property: 'color' }, border: { property: 'border' }, text: { property: 'label' } }, pickable: true });
|
|
95
|
+
const out = legs.concat(starts, [withProps(unassigned, dragProps)], unRest, [withProps(stops, dragProps)], stopRest);
|
|
96
|
+
if (dragAt) {
|
|
97
|
+
out.push(...pointsLayers(ids.drag, [{ lng: dragAt[0], lat: dragAt[1] }], { marker: { shape: 'circle', fill: '#111827', border: '#ffffff', size: 16 } }));
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function update() {
|
|
103
|
+
if (removed) { return; }
|
|
104
|
+
current = build();
|
|
105
|
+
if (typeof options.onLayers === 'function') { options.onLayers(current.slice()); return; }
|
|
106
|
+
const others = ((deck.props && deck.props.layers) || []).filter((l) => !l || !String(l.id).startsWith(id + ':'));
|
|
107
|
+
deck.setProps({ layers: others.concat(current) });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The panel, in a corner of the deck.gl container.
|
|
111
|
+
let panel = null;
|
|
112
|
+
let wrapper = null;
|
|
113
|
+
if (options.panel !== false) {
|
|
114
|
+
const po = typeof options.panel === 'object' && options.panel ? options.panel : {};
|
|
115
|
+
const canvas = typeof deck.getCanvas === 'function' ? deck.getCanvas() : null;
|
|
116
|
+
const container = po.container || options.container || (canvas && canvas.parentElement) || null;
|
|
117
|
+
const doc = (container && container.ownerDocument) || options.document || (typeof document !== 'undefined' ? document : null);
|
|
118
|
+
if (doc) {
|
|
119
|
+
if (options.css !== false) { P.injectCss(doc); }
|
|
120
|
+
panel = P.createPanel(editor, { language: options.language || 'en', labels: po.labels || options.labels, document: doc, colors: options.colors, className: po.className });
|
|
121
|
+
if (container) {
|
|
122
|
+
wrapper = doc.createElement('div');
|
|
123
|
+
wrapper.className = 'smap-deck-plan-editor';
|
|
124
|
+
wrapper.setAttribute('style', 'position:absolute;z-index:2;pointer-events:auto;' + (CORNERS[po.position || 'top-right'] || CORNERS['top-right']));
|
|
125
|
+
wrapper.setAttribute('aria-label', po.label || P.textsFor(options.language || 'en', po.labels || options.labels).editor);
|
|
126
|
+
wrapper.appendChild(panel.element);
|
|
127
|
+
container.appendChild(wrapper);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const onChange = () => update();
|
|
133
|
+
editor.on('change', onChange);
|
|
134
|
+
const gone = () => handle.remove();
|
|
135
|
+
editor.on('destroy', gone);
|
|
136
|
+
|
|
137
|
+
const handle = {
|
|
138
|
+
editor,
|
|
139
|
+
panel,
|
|
140
|
+
/** The plan's deck.gl layers as last drawn. */
|
|
141
|
+
layers: () => current.slice(),
|
|
142
|
+
features: () => P.planFeatures(editor, id, options.colors),
|
|
143
|
+
remove() {
|
|
144
|
+
if (removed) { return; }
|
|
145
|
+
removed = true;
|
|
146
|
+
drag.cancel();
|
|
147
|
+
setPan(true);
|
|
148
|
+
editor.off('change', onChange);
|
|
149
|
+
editor.off('destroy', gone);
|
|
150
|
+
if (typeof options.onLayers === 'function') { options.onLayers([]); } else {
|
|
151
|
+
deck.setProps({ layers: ((deck.props && deck.props.layers) || []).filter((l) => !l || !String(l.id).startsWith(id + ':')) });
|
|
152
|
+
}
|
|
153
|
+
current = [];
|
|
154
|
+
if (panel) { panel.destroy(); }
|
|
155
|
+
if (wrapper && wrapper.parentNode) { wrapper.parentNode.removeChild(wrapper); }
|
|
156
|
+
if (own) { editor.destroy(); }
|
|
157
|
+
},
|
|
158
|
+
// For tests: the drag callbacks the layers carry.
|
|
159
|
+
_drag: dragProps,
|
|
160
|
+
};
|
|
161
|
+
update();
|
|
162
|
+
if (own && options.plan) { editor.setPlan(options.plan).catch(() => {}); }
|
|
163
|
+
return handle;
|
|
164
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
|
3
3
|
import type { MVTLayer, TileLayer } from '@deck.gl/geo-layers';
|
|
4
4
|
import type { MapboxOverlay, MapboxOverlayProps } from '@deck.gl/mapbox';
|
|
5
5
|
import type SMapClient from '@superrouteca/smap/client';
|
|
6
|
+
import type { PlanEditor, PlanEditorOptions, PlanFeatures, PlanPanel, PlanTextKey } from '@superrouteca/smap';
|
|
6
7
|
import type { Map as SMapMap, MapOptions as SMapMapOptions, LayerData, PointOptions, LineOptions, PolygonOptions, GeoJSONFeature, BasemapOverrides, Slot } from '@superrouteca/smap';
|
|
7
8
|
|
|
8
9
|
/** A colour scheme, or a saved custom style as `custom:<id>`. */
|
|
@@ -166,3 +167,41 @@ export declare function drawTile(compiled: CompiledStyle, features: GeoJSONFeatu
|
|
|
166
167
|
extrusions: Array<{ geometry: unknown; color: RGBA; height: number; base: number }>;
|
|
167
168
|
labels: Array<{ position: [number, number]; text: string; color: RGBA; size: number }>;
|
|
168
169
|
};
|
|
170
|
+
|
|
171
|
+
// ------------------------------------------------------------------ plan editor (OPEN-ROUTE-EDIT E10)
|
|
172
|
+
|
|
173
|
+
export interface DeckPlanEditorOptions extends Partial<PlanEditorOptions> {
|
|
174
|
+
/** An editor of your own to draw and edit; otherwise one is made from the editor options (`credentials` needed). */
|
|
175
|
+
editor?: PlanEditor;
|
|
176
|
+
/** Prefix of the layer ids (default 'plan'): `<id>:legs`, `<id>:starts`, `<id>:unassigned`, `<id>:stops`. */
|
|
177
|
+
id?: string;
|
|
178
|
+
/** Route colours, one per vehicle in the plan's order (repeated). */
|
|
179
|
+
colors?: string[];
|
|
180
|
+
/** Price the drop target while a stop is dragged (default true). */
|
|
181
|
+
preview?: boolean;
|
|
182
|
+
/** The panel's language; any BCP-47 tag, English when none of the 14 fits. */
|
|
183
|
+
language?: string;
|
|
184
|
+
labels?: Partial<Record<PlanTextKey, string>>;
|
|
185
|
+
/** The ready-made panel (default true): false for none, or its corner, class, texts and container. */
|
|
186
|
+
panel?: boolean | { position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; className?: string; labels?: Partial<Record<PlanTextKey, string>>; label?: string; container?: HTMLElement };
|
|
187
|
+
/** Add the panel's stylesheet to the page (default true). */
|
|
188
|
+
css?: boolean;
|
|
189
|
+
/** Where the panel goes (default the deck.gl canvas's parent). */
|
|
190
|
+
container?: HTMLElement;
|
|
191
|
+
/** Place the plan's layers yourself (called after every change) instead of the binding setting the deck's layers. */
|
|
192
|
+
onLayers?: (layers: Layer[]) => void;
|
|
193
|
+
}
|
|
194
|
+
export interface DeckPlanEditor {
|
|
195
|
+
editor: PlanEditor;
|
|
196
|
+
panel: PlanPanel | null;
|
|
197
|
+
/** The plan's deck.gl layers as last drawn. */
|
|
198
|
+
layers(): Layer[];
|
|
199
|
+
features(): PlanFeatures;
|
|
200
|
+
/** Takes the layers, drag handling and panel away; destroys the editor only when this binding made it. */
|
|
201
|
+
remove(): void;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A plan editor drawn by deck.gl: the plan as deck.gl layers, stops dragged onto another stop or a route's line, and
|
|
205
|
+
* the ready-made panel in a corner of the deck.gl container.
|
|
206
|
+
*/
|
|
207
|
+
export declare function addPlanEditor(deck: { setProps(props: Record<string, unknown>): void; props?: { layers?: unknown[]; controller?: unknown }; pickMultipleObjects?(o: { x: number; y: number; radius?: number; layerIds?: string[] }): PickingInfo[]; getCanvas?(): HTMLCanvasElement | null }, options: DeckPlanEditorOptions): DeckPlanEditor;
|