@superrouteca/smap-arcgis 1.1.5 → 1.1.7

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.7 — 2026-10-28
4
+
5
+ - Plan editor: faster edits through sMap's fast path, with no code change. When your plan editor token answer names a
6
+ fast path, or your map key's configuration on sMap does, every edit goes there and falls back to the Open Platform
7
+ by itself when the fast path asks for it, fails, or does not answer within 2 seconds (5 seconds for a refine). Quota
8
+ and rate-limit refusals stay final. The editor's state says which path answered (`path`).
9
+
10
+ ## 1.1.6 — 2026-10-28
11
+
12
+ - Plan editor: `addPlanEditor(view, options)` draws a plan of routes as a graphics layer on a MapView and lets a
13
+ person drag stops within and between routes, with the stop's new place priced while it is dragged. The ready-made
14
+ panel in 14 languages (routes, totals, violations, undo and redo, insert positions) is placed in the view's UI.
15
+ Every edit is a call of the Open Platform's plan editing.
16
+
3
17
  ## 1.1.5 — 2026-10-28
4
18
 
5
19
  - 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.5",
3
+ "version": "1.1.7",
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.5"
53
+ "@superrouteca/smap": "1.1.7"
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 const version = '1.1.5';
7
+ export { addPlanEditor } from './plan.js';
8
+ export const version = '1.1.7';
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;