@superrouteca/smap-arcgis 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 +31 -0
- package/package.json +3 -2
- package/src/index.js +2 -1
- package/src/plan.js +160 -0
- package/types/index.d.ts +37 -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(view, options)` draws a plan of routes as a graphics layer on a MapView 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) is placed in the view's UI.
|
|
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
|
@@ -126,6 +126,37 @@ await addArcGisService(map, 'parcels', 'https://example.com/arcgis/rest/services
|
|
|
126
126
|
- Helpers: `queryFeatures(url, options)`, `serviceInfo(url)`, `parseServiceUrl(url)`, `rendererStyle`,
|
|
127
127
|
`symbolToSimplestyle`.
|
|
128
128
|
|
|
129
|
+
## Plan editor
|
|
130
|
+
|
|
131
|
+
`addPlanEditor(view, options)` lets a person edit a plan of routes on a MapView: drag a stop within its route or
|
|
132
|
+
onto another route, take stops off, place new orders at the positions the service ranks best, refine routes around
|
|
133
|
+
locked stops, and undo or redo. Every edit is one call of the Open Platform's plan editing with the whole plan (see
|
|
134
|
+
the plan editor of `@superrouteca/smap`); nothing is kept on a server, so keep the plan from the `change` event.
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { addPlanEditor } from '@superrouteca/smap-arcgis';
|
|
138
|
+
|
|
139
|
+
const plan = addPlanEditor(view, {
|
|
140
|
+
// A short-lived plan editor token your own server mints with POST /v1/plan/token, or { key: 'smk_…' }.
|
|
141
|
+
credentials: { token: () => fetch('/plan-token').then((r) => r.json()) },
|
|
142
|
+
plan: { vehicles, jobs, routes },
|
|
143
|
+
language: 'fr',
|
|
144
|
+
});
|
|
145
|
+
plan.editor.on('change', (e) => save(e.plan));
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **The layer**: one GraphicsLayer (`plan`, or `id`) on the view's map with the legs, the vehicle starts, the
|
|
149
|
+
unassigned jobs and a numbered pin per stop (`colors` sets the route colours); each graphic's attributes are its
|
|
150
|
+
plan feature's (`kind`, `id`, `vehicle_id`, `position`).
|
|
151
|
+
- **Dragging** uses the view's pointer and drag events and its hit test: drop a stop on another stop to put it before
|
|
152
|
+
that stop, or on a route's line to put it into that leg. The view does not pan while a stop is dragged, and the
|
|
153
|
+
plan with the stop at its target is priced and shown in the panel as a preview (`preview: false` turns it off).
|
|
154
|
+
- **The panel** is added to the view's UI (top right; `panel: { position: 'top-left' }`), in 14 languages with your
|
|
155
|
+
own `labels`; its stylesheet is added to the page (`css: false` leaves it to you). `panel: false` leaves it out, for
|
|
156
|
+
a panel of your own on `plan.editor.state()` and its events.
|
|
157
|
+
- `editor`: draw and edit an editor you made (`new PlanEditor(...)` from `@superrouteca/smap`); `remove()` then leaves
|
|
158
|
+
it running. Otherwise `remove()` also ends the editor, and ending the editor removes everything.
|
|
159
|
+
|
|
129
160
|
## Example
|
|
130
161
|
|
|
131
162
|
[`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-arcgis",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "sMap for the ArcGIS Maps SDK for JavaScript: sMap basemaps (vector and raster), sMap layers as graphics, and ArcGIS REST services on sMap maps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Goodneo Solutions",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"typecheck": "tsc -p test/tsconfig.json"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@superrouteca/smap": "1.1.
|
|
53
|
+
"@superrouteca/smap": "1.1.6"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@arcgis/core": "^5.0.0"
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@arcgis/core": "5.1.27",
|
|
65
65
|
"@maplibre/maplibre-gl-style-spec": "26.4.4",
|
|
66
|
+
"jsdom": "30.1.3",
|
|
66
67
|
"typescript": "6.0.3"
|
|
67
68
|
}
|
|
68
69
|
}
|
package/src/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export { installInterceptor, createVectorTileLayer, createRasterTileLayer, creat
|
|
|
4
4
|
export { toArcGISStyle, styleLanguage, nameExpression } from './style.js';
|
|
5
5
|
export { createGraphicsLayer, symbolFor, toArcGisGeometries, toFeatures, zoomToScale } from './graphics.js';
|
|
6
6
|
export { addArcGisService, queryFeatures, serviceInfo, parseServiceUrl, withToken, symbolToSimplestyle, rendererStyle } from './services.js';
|
|
7
|
-
export
|
|
7
|
+
export { addPlanEditor } from './plan.js';
|
|
8
|
+
export const version = '1.1.6';
|
package/src/plan.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// The plan editor (OPEN-ROUTE-EDIT E10) on an ArcGIS MapView: the plan drawn as a GraphicsLayer, stops dragged with
|
|
2
|
+
// the view's own pointer and drag events and hit test onto another stop or a route's line, and the ready-made panel
|
|
3
|
+
// in the view's UI. Every edit is a plan.* call of the Open Platform made by the SDK's PlanEditor.
|
|
4
|
+
import Graphic from '@arcgis/core/Graphic.js';
|
|
5
|
+
import GraphicsLayer from '@arcgis/core/layers/GraphicsLayer.js';
|
|
6
|
+
import { PlanEditor, planEditorParts } from '@superrouteca/smap';
|
|
7
|
+
import { symbolFor, toArcGisGeometries } from './graphics.js';
|
|
8
|
+
|
|
9
|
+
const EDITOR_OPTIONS = ['credentials', 'baseUrl', 'fetch', 'app', 'region', 'profile', 'snapshot', 'geometry', 'insertLimit', 'objective'];
|
|
10
|
+
|
|
11
|
+
const LEG = { color: { property: 'color' }, width: 4 };
|
|
12
|
+
const START = { marker: { shape: 'circle', fill: { property: 'color' }, border: '#ffffff', size: 18 } };
|
|
13
|
+
const UNASSIGNED = { marker: { shape: 'circle', fill: '#9ca3af', border: '#ffffff', size: 18 } };
|
|
14
|
+
const STOP = { marker: { shape: 'pin', fill: { property: 'color' }, border: { property: 'border' } } };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A plan editor on an ArcGIS MapView.
|
|
18
|
+
*
|
|
19
|
+
* const plan = addPlanEditor(view, { credentials: { token: getPlanToken }, plan, language: 'fr' });
|
|
20
|
+
* plan.editor.on('change', (e) => save(e.plan));
|
|
21
|
+
*
|
|
22
|
+
* @param {object} view a MapView (map, ui, on, hitTest)
|
|
23
|
+
* @param {object} options the PlanEditor options or `editor` (an editor of your own), and `id` (layer id, default
|
|
24
|
+
* 'plan'), `colors`, `preview` (default true), `language`, `labels`, `panel` (default true; false, or
|
|
25
|
+
* { position, className, labels }), `css` (default true)
|
|
26
|
+
*/
|
|
27
|
+
export function addPlanEditor(view, options = {}) {
|
|
28
|
+
if (!view || !view.map || typeof view.on !== 'function') { throw new Error('sMap: addPlanEditor(view, options) needs an ArcGIS MapView with a map'); }
|
|
29
|
+
const P = planEditorParts;
|
|
30
|
+
const id = options.id || 'plan';
|
|
31
|
+
const own = !options.editor;
|
|
32
|
+
const editor = options.editor || new PlanEditor(Object.fromEntries(Object.entries(options).filter(([k]) => EDITOR_OPTIONS.includes(k))));
|
|
33
|
+
const drag = P.planDrag(editor, { preview: options.preview !== false });
|
|
34
|
+
const layer = new GraphicsLayer({ id, title: options.title || id, listMode: 'hide' });
|
|
35
|
+
view.map.add(layer);
|
|
36
|
+
let removed = false;
|
|
37
|
+
let dragGraphic = null;
|
|
38
|
+
|
|
39
|
+
function graphicsOf(feature, kind, o) {
|
|
40
|
+
const props = { ...feature.properties };
|
|
41
|
+
return toArcGisGeometries(feature.geometry).map((geometry) => new Graphic({ geometry, attributes: props, symbol: symbolFor(kind, props, o) }));
|
|
42
|
+
}
|
|
43
|
+
function labelOf(feature) {
|
|
44
|
+
const geometry = toArcGisGeometries(feature.geometry)[0];
|
|
45
|
+
return new Graphic({
|
|
46
|
+
geometry, attributes: { ...feature.properties },
|
|
47
|
+
symbol: { type: 'text', text: feature.properties.label, color: '#ffffff', font: { size: 8, weight: 'bold', family: 'sans-serif' }, yoffset: 15 },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function draw() {
|
|
52
|
+
if (removed) { return; }
|
|
53
|
+
const f = P.planFeatures(editor, id, options.colors);
|
|
54
|
+
const list = [];
|
|
55
|
+
f.legs.forEach((x) => list.push(...graphicsOf(x, 'lines', LEG)));
|
|
56
|
+
f.starts.forEach((x) => list.push(...graphicsOf(x, 'points', START)));
|
|
57
|
+
f.unassigned.forEach((x) => list.push(...graphicsOf(x, 'points', UNASSIGNED)));
|
|
58
|
+
f.stops.forEach((x) => { list.push(...graphicsOf(x, 'points', STOP)); list.push(labelOf(x)); });
|
|
59
|
+
layer.removeAll();
|
|
60
|
+
layer.addMany(list);
|
|
61
|
+
dragGraphic = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function showDrag(mapPoint) {
|
|
65
|
+
if (dragGraphic) { layer.remove(dragGraphic); dragGraphic = null; }
|
|
66
|
+
if (!mapPoint) { return; }
|
|
67
|
+
const geometry = { type: 'point', longitude: mapPoint.longitude, latitude: mapPoint.latitude, spatialReference: { wkid: 4326 } };
|
|
68
|
+
dragGraphic = new Graphic({ geometry, attributes: { smap_plan_drag: true }, symbol: symbolFor('points', {}, { marker: { shape: 'circle', fill: '#111827', border: '#ffffff', size: 16 } }) });
|
|
69
|
+
layer.add(dragGraphic);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The plan features under a screen point, as the view's hit test finds them in this layer.
|
|
73
|
+
function hitsAt(event) {
|
|
74
|
+
return Promise.resolve(view.hitTest(event, { include: [layer] })).then((r) => ((r && r.results) || [])
|
|
75
|
+
.map((x) => x.graphic)
|
|
76
|
+
.filter((g) => g && g.attributes && g.attributes.smap_plan === id && (!g.layer || g.layer === layer) && !(g.symbol && g.symbol.type === 'text'))
|
|
77
|
+
.map((g) => ({ properties: g.attributes })));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Pointer events arrive in order, hit tests answer later: each answer is used only while its press is current.
|
|
81
|
+
let press = 0;
|
|
82
|
+
let pending = Promise.resolve();
|
|
83
|
+
const handles = [
|
|
84
|
+
view.on('pointer-down', (e) => {
|
|
85
|
+
if (removed || (e.button !== undefined && e.button !== 0)) { return; }
|
|
86
|
+
const mine = ++press;
|
|
87
|
+
drag.cancel();
|
|
88
|
+
pending = hitsAt(e).then((hits) => {
|
|
89
|
+
if (mine !== press || removed) { return; }
|
|
90
|
+
const hit = hits.find((h) => h.properties.kind === 'stop' || h.properties.kind === 'unassigned');
|
|
91
|
+
if (hit) { drag.start(hit.properties.id, { x: e.x, y: e.y }); }
|
|
92
|
+
});
|
|
93
|
+
}),
|
|
94
|
+
view.on('drag', (e) => {
|
|
95
|
+
if (removed || !drag.dragging()) { return; }
|
|
96
|
+
// The view does not pan while a stop is dragged.
|
|
97
|
+
if (typeof e.stopPropagation === 'function') { e.stopPropagation(); }
|
|
98
|
+
const mine = press;
|
|
99
|
+
const point = { x: e.x, y: e.y };
|
|
100
|
+
if (e.action === 'end') {
|
|
101
|
+
pending = pending.then(() => hitsAt(e)).then((hits) => {
|
|
102
|
+
if (mine !== press) { return; }
|
|
103
|
+
showDrag(null);
|
|
104
|
+
drag.end(hits);
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
pending = pending.then(() => hitsAt(e)).then((hits) => {
|
|
109
|
+
if (mine !== press || !drag.dragging()) { return; }
|
|
110
|
+
drag.move(hits, point);
|
|
111
|
+
if (drag.moved() && typeof view.toMap === 'function') { showDrag(view.toMap(point)); }
|
|
112
|
+
});
|
|
113
|
+
}),
|
|
114
|
+
view.on('pointer-up', () => {
|
|
115
|
+
// A press that never became a drag.
|
|
116
|
+
pending = pending.then(() => { if (drag.dragging() && !drag.moved()) { drag.cancel(); } });
|
|
117
|
+
}),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// The panel in the view's UI.
|
|
121
|
+
let panel = null;
|
|
122
|
+
if (options.panel !== false) {
|
|
123
|
+
const po = typeof options.panel === 'object' && options.panel ? options.panel : {};
|
|
124
|
+
const container = view.container && view.container.ownerDocument ? view.container : null;
|
|
125
|
+
const doc = (container && container.ownerDocument) || options.document || (typeof document !== 'undefined' ? document : null);
|
|
126
|
+
if (doc) {
|
|
127
|
+
if (options.css !== false) { P.injectCss(doc); }
|
|
128
|
+
panel = P.createPanel(editor, { language: options.language || 'en', labels: po.labels || options.labels, document: doc, colors: options.colors, className: po.className });
|
|
129
|
+
panel.element.setAttribute('aria-label', po.label || P.textsFor(options.language || 'en', po.labels || options.labels).editor);
|
|
130
|
+
view.ui.add(panel.element, po.position || 'top-right');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const onChange = () => draw();
|
|
135
|
+
editor.on('change', onChange);
|
|
136
|
+
const gone = () => handle.remove();
|
|
137
|
+
editor.on('destroy', gone);
|
|
138
|
+
|
|
139
|
+
const handle = {
|
|
140
|
+
editor,
|
|
141
|
+
panel,
|
|
142
|
+
layer,
|
|
143
|
+
features: () => P.planFeatures(editor, id, options.colors),
|
|
144
|
+
remove() {
|
|
145
|
+
if (removed) { return; }
|
|
146
|
+
removed = true;
|
|
147
|
+
press += 1;
|
|
148
|
+
drag.cancel();
|
|
149
|
+
editor.off('change', onChange);
|
|
150
|
+
editor.off('destroy', gone);
|
|
151
|
+
handles.forEach((h) => h && h.remove && h.remove());
|
|
152
|
+
view.map.remove(layer);
|
|
153
|
+
if (panel) { view.ui.remove(panel.element); panel.destroy(); }
|
|
154
|
+
if (own) { editor.destroy(); }
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
draw();
|
|
158
|
+
if (own && options.plan) { editor.setPlan(options.plan).catch(() => {}); }
|
|
159
|
+
return handle;
|
|
160
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js';
|
|
|
4
4
|
import type WebTileLayer from '@arcgis/core/layers/WebTileLayer.js';
|
|
5
5
|
import type GraphicsLayer from '@arcgis/core/layers/GraphicsLayer.js';
|
|
6
6
|
import type SMapClient from '@superrouteca/smap/client';
|
|
7
|
+
import type MapView from '@arcgis/core/views/MapView.js';
|
|
8
|
+
import type { PlanEditor, PlanEditorOptions, PlanFeatures, PlanPanel, PlanTextKey } from '@superrouteca/smap';
|
|
7
9
|
import type { LayerData, PointOptions, LineOptions, PolygonOptions, GeoJSONFeature, BasemapOverrides } from '@superrouteca/smap';
|
|
8
10
|
|
|
9
11
|
export * from './services.js';
|
|
@@ -128,3 +130,38 @@ export declare function symbolFor(kind: GraphicsKind, properties: Record<string,
|
|
|
128
130
|
export declare function toArcGisGeometries(geometry: { type: string; coordinates?: unknown; geometries?: unknown[] } | null | undefined): Array<Record<string, unknown>>;
|
|
129
131
|
export declare function toFeatures(data: LayerData): GeoJSONFeature[];
|
|
130
132
|
export declare function zoomToScale(zoom: number): number;
|
|
133
|
+
|
|
134
|
+
// ------------------------------------------------------------------ plan editor (OPEN-ROUTE-EDIT E10)
|
|
135
|
+
|
|
136
|
+
export interface ArcGisPlanEditorOptions extends Partial<PlanEditorOptions> {
|
|
137
|
+
/** An editor of your own to draw and edit; otherwise one is made from the editor options (`credentials` needed). */
|
|
138
|
+
editor?: PlanEditor;
|
|
139
|
+
/** The GraphicsLayer's id (default 'plan'). */
|
|
140
|
+
id?: string;
|
|
141
|
+
title?: string;
|
|
142
|
+
/** Route colours, one per vehicle in the plan's order (repeated). */
|
|
143
|
+
colors?: string[];
|
|
144
|
+
/** Price the drop target while a stop is dragged (default true). */
|
|
145
|
+
preview?: boolean;
|
|
146
|
+
/** The panel's language; any BCP-47 tag, English when none of the 14 fits. */
|
|
147
|
+
language?: string;
|
|
148
|
+
labels?: Partial<Record<PlanTextKey, string>>;
|
|
149
|
+
/** The ready-made panel in the view's UI (default true, top right): false for none, or its position, class and texts. */
|
|
150
|
+
panel?: boolean | { position?: string; className?: string; labels?: Partial<Record<PlanTextKey, string>>; label?: string };
|
|
151
|
+
/** Add the panel's stylesheet to the page (default true). */
|
|
152
|
+
css?: boolean;
|
|
153
|
+
}
|
|
154
|
+
export interface ArcGisPlanEditor {
|
|
155
|
+
editor: PlanEditor;
|
|
156
|
+
panel: PlanPanel | null;
|
|
157
|
+
/** The plan's graphics; each graphic's attributes are its plan feature's properties. */
|
|
158
|
+
layer: GraphicsLayer;
|
|
159
|
+
features(): PlanFeatures;
|
|
160
|
+
/** Takes the layer, the view events and the panel away; ends the editor only when this binding made it. */
|
|
161
|
+
remove(): void;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A plan editor on an ArcGIS MapView: the plan as a GraphicsLayer, stops dragged onto another stop or a route's line,
|
|
165
|
+
* and the ready-made panel in the view's UI.
|
|
166
|
+
*/
|
|
167
|
+
export declare function addPlanEditor(view: MapView, options: ArcGisPlanEditorOptions): ArcGisPlanEditor;
|