@superrouteca/smap 1.1.2 → 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 +35 -0
- package/README.md +132 -1
- package/dist/SOURCE.json +8 -8
- package/dist/smap-client.cjs +2 -2
- package/dist/smap-client.js +2 -2
- package/dist/smap-client.mjs +2 -2
- package/dist/smap.cjs +2020 -13
- package/dist/smap.css +89 -0
- package/dist/smap.js +2009 -10
- package/dist/smap.mjs +2018 -14
- package/package.json +1 -1
- package/types/smap.d.cts +422 -0
- package/types/smap.d.mts +422 -0
package/dist/smap.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Generated by js/build.mjs from js/src (see SOURCE.json). Do not edit. */
|
|
2
2
|
'use strict';
|
|
3
|
-
/*! sMap Web SDK 1.1.
|
|
3
|
+
/*! sMap Web SDK 1.1.6. Bundles maplibre-gl 6.16.0 (BSD-3-Clause), see vendor-licenses.txt. */
|
|
4
4
|
|
|
5
5
|
var smapCore = null;
|
|
6
6
|
function smapCreateEngine() {
|
|
@@ -28328,12 +28328,1634 @@ function smapCreateInterop() {
|
|
|
28328
28328
|
|
|
28329
28329
|
return module.exports;
|
|
28330
28330
|
}
|
|
28331
|
-
function
|
|
28331
|
+
function smapCreatePlan() {
|
|
28332
|
+
var module = { exports: {} };
|
|
28333
|
+
var exports = module.exports;
|
|
28334
|
+
var define;
|
|
28335
|
+
var self = {};
|
|
28336
|
+
/*!
|
|
28337
|
+
* sMap plan editor (OPEN-ROUTE-EDIT E10): edits a plan the caller holds through the Open Platform's plan.* calls.
|
|
28338
|
+
*
|
|
28339
|
+
* // /plan-token on your own server calls POST /v1/plan/token with its API token and hands the answer on.
|
|
28340
|
+
* const editor = map.planEditor({ credentials: { token: () => fetch('/plan-token').then((r) => r.json()) }, plan });
|
|
28341
|
+
* editor.on('change', (e) => save(e.plan));
|
|
28342
|
+
*
|
|
28343
|
+
* The editor keeps no plan on any server: every call carries the whole plan and the answer replaces the plan drawn.
|
|
28344
|
+
* It opens a distance snapshot (POST /v1/plan/matrix) and names it in every call, opening a new one by itself when
|
|
28345
|
+
* the snapshot expired. Three parts, each usable alone:
|
|
28346
|
+
* PlanEditor the state, the calls, undo / redo and the events; needs no map and no page
|
|
28347
|
+
* attachToMap draws the plan on an sMap map and lets stops be dragged within and between routes
|
|
28348
|
+
* createPanel the ready-made panel (routes, totals, violations, undo / redo, insert choices) in 14 languages
|
|
28349
|
+
*/
|
|
28350
|
+
(function (root, factory) {
|
|
28351
|
+
if (typeof module === 'object' && module.exports) {
|
|
28352
|
+
module.exports = factory();
|
|
28353
|
+
} else {
|
|
28354
|
+
root.smapPlan = factory();
|
|
28355
|
+
}
|
|
28356
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
28357
|
+
'use strict';
|
|
28358
|
+
|
|
28359
|
+
// ------------------------------------------------------------------ credentials and transport
|
|
28360
|
+
//
|
|
28361
|
+
// The one place that knows where plan calls go and how they are authorised (Superroute OPEN-ROUTE-EDIT §2e): a
|
|
28362
|
+
// short-lived plan editor token (`sr_pet_…`) that the customer's own server mints with POST /v1/plan/token, or a
|
|
28363
|
+
// map key (`smk_…`) with plan editing switched on. Both are sent as `Authorization: Bearer …`. A browser is known by
|
|
28364
|
+
// its Origin; a native app also names itself with X-SMap-App (and X-SMap-App-Cert on Android).
|
|
28365
|
+
|
|
28366
|
+
var DEFAULT_BASE = 'https://open.superroute.ca';
|
|
28367
|
+
// A token whose expiry can be read is renewed this many seconds before it ends.
|
|
28368
|
+
var RENEW_BEFORE = 30;
|
|
28369
|
+
var TOKEN_PREFIX = 'sr_pet_';
|
|
28370
|
+
|
|
28371
|
+
function base64Json(part) {
|
|
28372
|
+
var b64 = String(part).replace(/-/g, '+').replace(/_/g, '/');
|
|
28373
|
+
while (b64.length % 4) { b64 += '='; }
|
|
28374
|
+
return JSON.parse(atob(b64));
|
|
28375
|
+
}
|
|
28376
|
+
|
|
28377
|
+
/** The expiry (Unix seconds) of a plan editor token (`sr_pet_<payload>.<signature>`) or a JWT-shaped token; 0 if unreadable. */
|
|
28378
|
+
function tokenExpiry(token) {
|
|
28379
|
+
try {
|
|
28380
|
+
var t = String(token);
|
|
28381
|
+
var payload = t.indexOf(TOKEN_PREFIX) === 0 ? t.slice(TOKEN_PREFIX.length).split('.')[0] : t.split('.')[1];
|
|
28382
|
+
return Number(base64Json(payload).exp) || 0;
|
|
28383
|
+
} catch (e) { return 0; }
|
|
28384
|
+
}
|
|
28385
|
+
|
|
28386
|
+
/**
|
|
28387
|
+
* What a token provider gave: the token string, or the token answer of POST /v1/plan/token
|
|
28388
|
+
* ({ access_token, token_type, expires_in, expires_at, scopes, origins }, or that answer's `{ data }` envelope).
|
|
28389
|
+
* Returns { token, exp } with exp in Unix seconds (0 when it cannot be told).
|
|
28390
|
+
*/
|
|
28391
|
+
function readToken(t) {
|
|
28392
|
+
if (t && typeof t === 'object' && t.data && typeof t.data === 'object' && !t.access_token) { t = t.data; }
|
|
28393
|
+
if (typeof t === 'string' && t) { return { token: t, exp: tokenExpiry(t) }; }
|
|
28394
|
+
if (t && typeof t === 'object' && typeof t.access_token === 'string' && t.access_token) {
|
|
28395
|
+
var exp = t.expires_at ? Date.parse(t.expires_at) / 1000 : 0;
|
|
28396
|
+
if (!exp && typeof t.expires_in === 'number') { exp = Date.now() / 1000 + t.expires_in; }
|
|
28397
|
+
return { token: t.access_token, exp: exp || tokenExpiry(t.access_token) };
|
|
28398
|
+
}
|
|
28399
|
+
return null;
|
|
28400
|
+
}
|
|
28401
|
+
|
|
28402
|
+
/** The app headers of a native app: options.app is 'android:<package>' / 'apple:<bundle id>' / 'windows:<id>', or { id, cert }. */
|
|
28403
|
+
function appHeaders(app) {
|
|
28404
|
+
var h = {};
|
|
28405
|
+
if (!app) { return h; }
|
|
28406
|
+
var id = typeof app === 'string' ? app : app.id;
|
|
28407
|
+
if (id) { h['X-SMap-App'] = String(id); }
|
|
28408
|
+
if (app && typeof app === 'object' && app.cert) { h['X-SMap-App-Cert'] = String(app.cert); }
|
|
28409
|
+
return h;
|
|
28410
|
+
}
|
|
28411
|
+
|
|
28412
|
+
/**
|
|
28413
|
+
* Credentials: { token: () => Promise<string | TokenAnswer> } (called at first use, again before the expiry it
|
|
28414
|
+
* gives and after a 401 SESSION_EXPIRED), or { key: 'smk_…' } (a map key with plan editing switched on).
|
|
28415
|
+
*/
|
|
28416
|
+
function credentialsOf(c) {
|
|
28417
|
+
if (c && typeof c.token === 'function') {
|
|
28418
|
+
var token = null;
|
|
28419
|
+
var exp = 0;
|
|
28420
|
+
var pending = null;
|
|
28421
|
+
var fetchToken = function () {
|
|
28422
|
+
if (!pending) {
|
|
28423
|
+
pending = Promise.resolve().then(function () { return c.token(); }).then(function (t) {
|
|
28424
|
+
pending = null;
|
|
28425
|
+
var read = readToken(t);
|
|
28426
|
+
if (!read) { throw new Error('sMap plan editor: the token provider gave no token'); }
|
|
28427
|
+
token = read.token;
|
|
28428
|
+
exp = read.exp;
|
|
28429
|
+
return token;
|
|
28430
|
+
}, function (e) { pending = null; throw e; });
|
|
28431
|
+
}
|
|
28432
|
+
return pending;
|
|
28433
|
+
};
|
|
28434
|
+
return {
|
|
28435
|
+
kind: 'token',
|
|
28436
|
+
headers: function () {
|
|
28437
|
+
var fresh = token && (!exp || exp - Date.now() / 1000 > RENEW_BEFORE);
|
|
28438
|
+
return (fresh ? Promise.resolve(token) : fetchToken()).then(function (t) { return { Authorization: 'Bearer ' + t }; });
|
|
28439
|
+
},
|
|
28440
|
+
// After a 401 SESSION_EXPIRED: forget the token so the next call asks the provider again. True when a retry may help.
|
|
28441
|
+
renew: function () { token = null; exp = 0; return true; },
|
|
28442
|
+
};
|
|
28443
|
+
}
|
|
28444
|
+
if (c && typeof c.key === 'string' && c.key) {
|
|
28445
|
+
return {
|
|
28446
|
+
kind: 'key',
|
|
28447
|
+
headers: function () { return Promise.resolve({ Authorization: 'Bearer ' + c.key }); },
|
|
28448
|
+
renew: function () { return false; },
|
|
28449
|
+
};
|
|
28450
|
+
}
|
|
28451
|
+
throw new Error('sMap plan editor: credentials are { token: () => Promise<string | TokenAnswer> } or { key: string }');
|
|
28452
|
+
}
|
|
28453
|
+
|
|
28454
|
+
/** An error answered by the Open Platform (status, code, reason, details) or raised on the way. */
|
|
28455
|
+
function PlanError(message, o) {
|
|
28456
|
+
var e = new Error(message);
|
|
28457
|
+
e.name = 'PlanError';
|
|
28458
|
+
o = o || {};
|
|
28459
|
+
e.status = o.status || 0;
|
|
28460
|
+
e.code = o.code || null;
|
|
28461
|
+
e.details = o.details || null;
|
|
28462
|
+
e.reason = o.reason || null;
|
|
28463
|
+
e.path = o.path || null;
|
|
28464
|
+
return e;
|
|
28465
|
+
}
|
|
28466
|
+
|
|
28467
|
+
function isAbort(e) { return !!e && (e.name === 'AbortError' || e.cancelled === true); }
|
|
28468
|
+
function cancelled() { var e = new Error('cancelled'); e.name = 'AbortError'; e.cancelled = true; return e; }
|
|
28469
|
+
|
|
28470
|
+
/**
|
|
28471
|
+
* The transport: send(method, path, body, signal) resolves { status, body, data, matrix } (matrix: the X-Plan-Matrix
|
|
28472
|
+
* header) or rejects with a PlanError. A 401 SESSION_EXPIRED with a token provider asks for a new token and repeats
|
|
28473
|
+
* the call once; every other refusal (SESSION_REVOKED, TOKEN_INVALID, 403 NOT_AUTHORIZED with its reason) is raised.
|
|
28474
|
+
*/
|
|
28475
|
+
function createTransport(o) {
|
|
28476
|
+
o = o || {};
|
|
28477
|
+
var base = String(o.baseUrl || DEFAULT_BASE).replace(/\/$/, '');
|
|
28478
|
+
var creds = credentialsOf(o.credentials);
|
|
28479
|
+
var app = appHeaders(o.app);
|
|
28480
|
+
var fetchFn = o.fetch || (typeof fetch === 'function' ? function (url, init) { return fetch(url, init); } : null);
|
|
28481
|
+
if (!fetchFn) { throw new Error('sMap plan editor: no fetch; pass options.fetch'); }
|
|
28482
|
+
|
|
28483
|
+
function send(method, path, body, signal, retried) {
|
|
28484
|
+
return creds.headers().then(function (h) {
|
|
28485
|
+
var headers = { Accept: 'application/json' };
|
|
28486
|
+
Object.keys(app).forEach(function (k) { headers[k] = app[k]; });
|
|
28487
|
+
Object.keys(h).forEach(function (k) { headers[k] = h[k]; });
|
|
28488
|
+
var init = { method: method, headers: headers };
|
|
28489
|
+
if (body !== undefined) { headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); }
|
|
28490
|
+
if (signal) { init.signal = signal; }
|
|
28491
|
+
return fetchFn(base + path, init);
|
|
28492
|
+
}).then(function (res) {
|
|
28493
|
+
return res.text().then(function (text) {
|
|
28494
|
+
var json = null;
|
|
28495
|
+
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
|
|
28496
|
+
var matrix = res.headers && typeof res.headers.get === 'function' ? res.headers.get('X-Plan-Matrix') : null;
|
|
28497
|
+
if (!res.ok) {
|
|
28498
|
+
var err = (json && json.error) || {};
|
|
28499
|
+
if (res.status === 401 && err.code === 'SESSION_EXPIRED' && !retried && creds.renew()) { return send(method, path, body, signal, true); }
|
|
28500
|
+
throw PlanError(err.message || ('HTTP ' + res.status), {
|
|
28501
|
+
status: res.status, code: err.code || null, details: err.details || null, reason: err.reason || (json && json.reason) || null, path: path,
|
|
28502
|
+
});
|
|
28503
|
+
}
|
|
28504
|
+
return { status: res.status, body: json, data: json && json.data, matrix: matrix };
|
|
28505
|
+
});
|
|
28506
|
+
});
|
|
28507
|
+
}
|
|
28508
|
+
return { send: send, kind: creds.kind, baseUrl: base };
|
|
28509
|
+
}
|
|
28510
|
+
|
|
28511
|
+
// ------------------------------------------------------------------ plans
|
|
28512
|
+
|
|
28513
|
+
function clone(v) { return v === undefined ? undefined : JSON.parse(JSON.stringify(v)); }
|
|
28514
|
+
|
|
28515
|
+
function routesOf(routes) {
|
|
28516
|
+
return (routes || []).map(function (r) {
|
|
28517
|
+
var stops = (r.stops || []).map(String);
|
|
28518
|
+
// A route sent with `reverse` is held in driving order, as the plan calls answer it.
|
|
28519
|
+
if (r.reverse) { stops.reverse(); }
|
|
28520
|
+
return { vehicle_id: String(r.vehicle_id), stops: stops };
|
|
28521
|
+
});
|
|
28522
|
+
}
|
|
28523
|
+
|
|
28524
|
+
// Every vehicle keeps a route to drop stops on: an empty one, last, where the plan gives none.
|
|
28525
|
+
function withAllVehicles(routes, vehicles) {
|
|
28526
|
+
var out = routes.slice();
|
|
28527
|
+
vehicles.forEach(function (v) {
|
|
28528
|
+
if (!out.some(function (r) { return r.vehicle_id === String(v.id); })) { out.push({ vehicle_id: String(v.id), stops: [] }); }
|
|
28529
|
+
});
|
|
28530
|
+
return out;
|
|
28531
|
+
}
|
|
28532
|
+
|
|
28533
|
+
function unassignedOf(jobs, routes) {
|
|
28534
|
+
var on = {};
|
|
28535
|
+
routes.forEach(function (r) { r.stops.forEach(function (s) { on[s] = true; }); });
|
|
28536
|
+
return jobs.filter(function (j) { return !on[String(j.id)]; }).map(function (j) { return String(j.id); });
|
|
28537
|
+
}
|
|
28538
|
+
|
|
28539
|
+
function findStop(routes, id) {
|
|
28540
|
+
for (var i = 0; i < routes.length; i++) {
|
|
28541
|
+
var k = routes[i].stops.indexOf(id);
|
|
28542
|
+
if (k >= 0) { return { route: i, vehicle_id: routes[i].vehicle_id, position: k + 1 }; }
|
|
28543
|
+
}
|
|
28544
|
+
return null;
|
|
28545
|
+
}
|
|
28546
|
+
|
|
28547
|
+
function idList(v) { return (Array.isArray(v) ? v : [v]).map(String); }
|
|
28548
|
+
|
|
28549
|
+
/** The routes after giving vehicleId's route the stops in `stops` (every stop of the route once, no other). */
|
|
28550
|
+
function applyResequence(routes, vehicleId, stops) {
|
|
28551
|
+
var out = clone(routes);
|
|
28552
|
+
var r = out.filter(function (x) { return x.vehicle_id === vehicleId; })[0];
|
|
28553
|
+
if (!r) { throw PlanError('sMap plan editor: no route of vehicle ' + vehicleId); }
|
|
28554
|
+
var want = idList(stops);
|
|
28555
|
+
if (want.length !== r.stops.length || want.slice().sort().join('\u0000') !== r.stops.slice().sort().join('\u0000')) {
|
|
28556
|
+
throw PlanError('sMap plan editor: a new order holds every stop of the route once, and no other');
|
|
28557
|
+
}
|
|
28558
|
+
r.stops = want;
|
|
28559
|
+
return out;
|
|
28560
|
+
}
|
|
28561
|
+
|
|
28562
|
+
/** The routes after moving `stops` to vehicleId's route at `position` (from 1, counted without them; none: last). */
|
|
28563
|
+
function applyMove(routes, stops, vehicleId, position) {
|
|
28564
|
+
var out = clone(routes);
|
|
28565
|
+
var ids = idList(stops);
|
|
28566
|
+
out.forEach(function (r) { r.stops = r.stops.filter(function (s) { return ids.indexOf(s) < 0; }); });
|
|
28567
|
+
var target = out.filter(function (x) { return x.vehicle_id === vehicleId; })[0];
|
|
28568
|
+
if (!target) { target = { vehicle_id: vehicleId, stops: [] }; out.push(target); }
|
|
28569
|
+
var at = position === undefined || position === null ? target.stops.length : Math.max(0, Math.min(target.stops.length, position - 1));
|
|
28570
|
+
Array.prototype.splice.apply(target.stops, [at, 0].concat(ids));
|
|
28571
|
+
return out;
|
|
28572
|
+
}
|
|
28573
|
+
|
|
28574
|
+
/** The routes after taking `stops` off their routes (each must be on a route). */
|
|
28575
|
+
function applyRemove(routes, stops) {
|
|
28576
|
+
var ids = idList(stops);
|
|
28577
|
+
ids.forEach(function (id) { if (!findStop(routes, id)) { throw PlanError('sMap plan editor: ' + id + ' is on no route'); } });
|
|
28578
|
+
var out = clone(routes);
|
|
28579
|
+
out.forEach(function (r) { r.stops = r.stops.filter(function (s) { return ids.indexOf(s) < 0; }); });
|
|
28580
|
+
return out;
|
|
28581
|
+
}
|
|
28582
|
+
|
|
28583
|
+
/**
|
|
28584
|
+
* Where a stop dropped before the stop at `position` of vehicleId's route (as drawn, the stop itself included; past
|
|
28585
|
+
* the last stop = after it) ends up: { kind: 'resequence', vehicle_id, stops } within its route, { kind: 'move',
|
|
28586
|
+
* vehicle_id, position } to another route or from the unassigned, or null when nothing changes.
|
|
28587
|
+
*/
|
|
28588
|
+
function dropEdit(routes, stopId, vehicleId, position) {
|
|
28589
|
+
var from = findStop(routes, stopId);
|
|
28590
|
+
var target = routes.filter(function (r) { return r.vehicle_id === vehicleId; })[0];
|
|
28591
|
+
var length = target ? target.stops.length : 0;
|
|
28592
|
+
if (from && from.vehicle_id === vehicleId) {
|
|
28593
|
+
var stops = target.stops.slice();
|
|
28594
|
+
stops.splice(from.position - 1, 1);
|
|
28595
|
+
var at = Math.max(1, Math.min(position, length + 1));
|
|
28596
|
+
if (from.position < at) { at -= 1; }
|
|
28597
|
+
stops.splice(at - 1, 0, stopId);
|
|
28598
|
+
if (stops.join('\u0000') === target.stops.join('\u0000')) { return null; }
|
|
28599
|
+
return { kind: 'resequence', vehicle_id: vehicleId, stops: stops };
|
|
28600
|
+
}
|
|
28601
|
+
var p = Math.max(1, position);
|
|
28602
|
+
return { kind: 'move', vehicle_id: vehicleId, position: p > length ? null : p };
|
|
28603
|
+
}
|
|
28604
|
+
|
|
28605
|
+
/** [lng, lat] of a location given as { lng, lat } / { lon, lat } / [lng, lat]; null when unreadable. */
|
|
28606
|
+
function lngLat(v) {
|
|
28607
|
+
if (!v) { return null; }
|
|
28608
|
+
if (Array.isArray(v)) { return v.length >= 2 ? [Number(v[0]), Number(v[1])] : null; }
|
|
28609
|
+
var lng = v.lng !== undefined ? v.lng : (v.lon !== undefined ? v.lon : v.longitude);
|
|
28610
|
+
var lat = v.lat !== undefined ? v.lat : v.latitude;
|
|
28611
|
+
return lng === undefined || lat === undefined ? null : [Number(lng), Number(lat)];
|
|
28612
|
+
}
|
|
28613
|
+
|
|
28614
|
+
/** An encoded polyline (precision 5, as plan.recost answers legs) as [[lng, lat], …]. */
|
|
28615
|
+
function decodePolyline(text, precision) {
|
|
28616
|
+
var factor = Math.pow(10, precision || 5);
|
|
28617
|
+
var out = [];
|
|
28618
|
+
var index = 0;
|
|
28619
|
+
var lat = 0;
|
|
28620
|
+
var lng = 0;
|
|
28621
|
+
text = String(text);
|
|
28622
|
+
while (index < text.length) {
|
|
28623
|
+
var values = [0, 0];
|
|
28624
|
+
for (var n = 0; n < 2; n++) {
|
|
28625
|
+
var shift = 0;
|
|
28626
|
+
var result = 0;
|
|
28627
|
+
var b;
|
|
28628
|
+
do {
|
|
28629
|
+
b = text.charCodeAt(index++) - 63;
|
|
28630
|
+
result |= (b & 0x1f) << shift;
|
|
28631
|
+
shift += 5;
|
|
28632
|
+
} while (b >= 0x20 && index <= text.length);
|
|
28633
|
+
values[n] = (result & 1) ? ~(result >> 1) : (result >> 1);
|
|
28634
|
+
}
|
|
28635
|
+
lat += values[0];
|
|
28636
|
+
lng += values[1];
|
|
28637
|
+
out.push([lng / factor, lat / factor]);
|
|
28638
|
+
}
|
|
28639
|
+
return out;
|
|
28640
|
+
}
|
|
28641
|
+
|
|
28642
|
+
// ------------------------------------------------------------------ the editor
|
|
28643
|
+
|
|
28644
|
+
var EVENTS = ['change', 'busy', 'error', 'snapshot', 'preview', 'destroy'];
|
|
28645
|
+
var PATHS = {
|
|
28646
|
+
recost: '/v1/plan/recost', resequence: '/v1/plan/resequence', move: '/v1/plan/move', remove: '/v1/plan/remove',
|
|
28647
|
+
insert: '/v1/plan/insert', refine: '/v1/plan/refine', matrix: '/v1/plan/matrix',
|
|
28648
|
+
};
|
|
28649
|
+
// Answer fields that describe the edit itself, beside routes, summary and plan.
|
|
28650
|
+
var ANSWERS = { resequence: 'resequence', move: 'move', remove: 'removed', insert: 'insert', refine: 'refine' };
|
|
28651
|
+
// A snapshot that could not be opened is asked for again after this long; calls go on without one meanwhile.
|
|
28652
|
+
var SNAPSHOT_RETRY_MS = 60000;
|
|
28653
|
+
|
|
28654
|
+
function newController() { return typeof AbortController === 'function' ? new AbortController() : null; }
|
|
28655
|
+
|
|
28656
|
+
/**
|
|
28657
|
+
* The plan editor's state and calls, without a map.
|
|
28658
|
+
* @param {Object} options
|
|
28659
|
+
* @param {Object} options.credentials { token: () => Promise<string | TokenAnswer> } or { key: string }
|
|
28660
|
+
* @param {string|Object} [options.app] a native app's identity: 'android:<package>' / 'apple:<bundle id>' / 'windows:<id>', or { id, cert }
|
|
28661
|
+
* @param {string} [options.baseUrl] the Open Platform (default https://open.superroute.ca)
|
|
28662
|
+
* @param {Function} [options.fetch] fetch to use (default the page's)
|
|
28663
|
+
* @param {Object} [options.plan] the starting plan: { vehicles, jobs, routes, region?, profile?, departed_at? }
|
|
28664
|
+
* @param {string} [options.region] used when the plan names none
|
|
28665
|
+
* @param {string} [options.profile] used when the plan names none
|
|
28666
|
+
* @param {boolean} [options.snapshot=true] open and use a distance snapshot
|
|
28667
|
+
* @param {boolean} [options.geometry=true] ask for each leg's line (for drawing)
|
|
28668
|
+
* @param {number} [options.insertLimit=3] positions offered per inserted job (1-10)
|
|
28669
|
+
* @param {'duration'|'distance'} [options.objective] what insert ranks by (the server's default: duration)
|
|
28670
|
+
*/
|
|
28671
|
+
function PlanEditor(options) {
|
|
28672
|
+
if (!(this instanceof PlanEditor)) { return new PlanEditor(options); }
|
|
28673
|
+
options = options || {};
|
|
28674
|
+
this._transport = createTransport(options);
|
|
28675
|
+
this._options = options;
|
|
28676
|
+
this._snapshotOn = options.snapshot !== false;
|
|
28677
|
+
this._geometry = options.geometry !== false;
|
|
28678
|
+
this._handlers = {};
|
|
28679
|
+
EVENTS.forEach(function (t) { this._handlers[t] = []; }, this);
|
|
28680
|
+
this._history = [];
|
|
28681
|
+
this._at = -1;
|
|
28682
|
+
this._vehicles = [];
|
|
28683
|
+
this._context = {};
|
|
28684
|
+
this._locked = {};
|
|
28685
|
+
this._choices = null;
|
|
28686
|
+
this._last = null;
|
|
28687
|
+
this._inflight = null;
|
|
28688
|
+
this._previewing = null;
|
|
28689
|
+
this._previewResult = null;
|
|
28690
|
+
this._busy = false;
|
|
28691
|
+
this._error = null;
|
|
28692
|
+
this._matrix = null;
|
|
28693
|
+
this._matrixKey = null;
|
|
28694
|
+
this._opening = null;
|
|
28695
|
+
this._openingKey = null;
|
|
28696
|
+
this._matrixRetryAt = 0;
|
|
28697
|
+
this._destroyed = false;
|
|
28698
|
+
if (options.plan) { this.setPlan(options.plan).catch(function () {}); }
|
|
28699
|
+
}
|
|
28700
|
+
|
|
28701
|
+
PlanEditor.prototype.on = function (type, fn) {
|
|
28702
|
+
if (!this._handlers[type]) { throw new Error('sMap plan editor: unknown event "' + type + '"'); }
|
|
28703
|
+
this._handlers[type].push(fn);
|
|
28704
|
+
return this;
|
|
28705
|
+
};
|
|
28706
|
+
PlanEditor.prototype.off = function (type, fn) {
|
|
28707
|
+
if (this._handlers[type]) { this._handlers[type] = this._handlers[type].filter(function (h) { return h !== fn; }); }
|
|
28708
|
+
return this;
|
|
28709
|
+
};
|
|
28710
|
+
PlanEditor.prototype._emit = function (type, payload) {
|
|
28711
|
+
(this._handlers[type] || []).slice().forEach(function (h) {
|
|
28712
|
+
try { h(payload); } catch (e) { if (typeof console !== 'undefined') { console.error(e); } }
|
|
28713
|
+
});
|
|
28714
|
+
};
|
|
28715
|
+
|
|
28716
|
+
/** Replaces the plan: the history starts again from it, and it is priced (plan.recost). */
|
|
28717
|
+
PlanEditor.prototype.setPlan = function (plan) {
|
|
28718
|
+
if (!plan || !Array.isArray(plan.vehicles) || !Array.isArray(plan.jobs)) {
|
|
28719
|
+
return Promise.reject(PlanError('sMap plan editor: a plan has vehicles and jobs'));
|
|
28720
|
+
}
|
|
28721
|
+
this._supersede();
|
|
28722
|
+
this.clearPreview();
|
|
28723
|
+
this._vehicles = clone(plan.vehicles);
|
|
28724
|
+
this._context = {};
|
|
28725
|
+
var o = this._options;
|
|
28726
|
+
var region = plan.region || o.region;
|
|
28727
|
+
var profile = plan.profile || o.profile;
|
|
28728
|
+
if (region) { this._context.region = region; }
|
|
28729
|
+
if (profile) { this._context.profile = profile; }
|
|
28730
|
+
if (plan.departed_at) { this._context.departed_at = plan.departed_at; }
|
|
28731
|
+
var routes = withAllVehicles(routesOf(plan.routes), this._vehicles);
|
|
28732
|
+
var jobs = clone(plan.jobs);
|
|
28733
|
+
var known = {};
|
|
28734
|
+
jobs.forEach(function (j) { known[String(j.id)] = true; });
|
|
28735
|
+
Object.keys(this._locked).forEach(function (id) { if (!known[id]) { delete this._locked[id]; } }, this);
|
|
28736
|
+
this._history = [{ kind: 'set', routes: routes, jobs: jobs, result: null, answer: null }];
|
|
28737
|
+
this._at = 0;
|
|
28738
|
+
this._choices = null;
|
|
28739
|
+
this._last = null;
|
|
28740
|
+
this._error = null;
|
|
28741
|
+
this._changed('set');
|
|
28742
|
+
return this._price(this._history[0]);
|
|
28743
|
+
};
|
|
28744
|
+
|
|
28745
|
+
/** The plan in the shape it is sent: vehicles, jobs, routes (stops in driving order), unassigned and its context. */
|
|
28746
|
+
PlanEditor.prototype.getPlan = function () {
|
|
28747
|
+
var e = this._history[this._at];
|
|
28748
|
+
if (!e) { return null; }
|
|
28749
|
+
var plan = { vehicles: clone(this._vehicles), jobs: clone(e.jobs), routes: clone(e.routes), unassigned: unassignedOf(e.jobs, e.routes) };
|
|
28750
|
+
Object.keys(this._context).forEach(function (k) { plan[k] = this._context[k]; }, this);
|
|
28751
|
+
return plan;
|
|
28752
|
+
};
|
|
28753
|
+
|
|
28754
|
+
/** The plan's prices and judgement as the last answer gave them ({ routes, summary }), or null before one. */
|
|
28755
|
+
PlanEditor.prototype.getResult = function () {
|
|
28756
|
+
var e = this._history[this._at];
|
|
28757
|
+
return e && e.result ? clone(e.result) : null;
|
|
28758
|
+
};
|
|
28759
|
+
|
|
28760
|
+
/** Everything a panel shows, in one object. */
|
|
28761
|
+
PlanEditor.prototype.state = function () {
|
|
28762
|
+
return {
|
|
28763
|
+
plan: this.getPlan(), result: this.getResult(), busy: this._busy, error: this._error, canUndo: this.canUndo(),
|
|
28764
|
+
canRedo: this.canRedo(), choices: clone(this._choices), locked: this.locked(), last: clone(this._last),
|
|
28765
|
+
preview: this._previewResult ? clone(this._previewResult) : null,
|
|
28766
|
+
};
|
|
28767
|
+
};
|
|
28768
|
+
|
|
28769
|
+
PlanEditor.prototype.canUndo = function () { return this._at > 0; };
|
|
28770
|
+
PlanEditor.prototype.canRedo = function () { return this._at >= 0 && this._at < this._history.length - 1; };
|
|
28771
|
+
PlanEditor.prototype.isBusy = function () { return this._busy; };
|
|
28772
|
+
|
|
28773
|
+
/** Gives vehicleId's route the stops in a new driving order (plan.resequence). */
|
|
28774
|
+
PlanEditor.prototype.resequence = function (vehicleId, stops) {
|
|
28775
|
+
vehicleId = String(vehicleId);
|
|
28776
|
+
var order = idList(stops);
|
|
28777
|
+
return this._run('resequence', { vehicle_id: vehicleId, stops: order }, function (routes) { return applyResequence(routes, vehicleId, order); });
|
|
28778
|
+
};
|
|
28779
|
+
|
|
28780
|
+
/** Moves stops to vehicleId's route at `position` (from 1, counted without them; omitted: last) (plan.move). */
|
|
28781
|
+
PlanEditor.prototype.move = function (stops, vehicleId, position) {
|
|
28782
|
+
vehicleId = String(vehicleId);
|
|
28783
|
+
var ids = idList(stops);
|
|
28784
|
+
var body = { vehicle_id: vehicleId, stops: ids };
|
|
28785
|
+
if (position !== undefined && position !== null) { body.position = position; }
|
|
28786
|
+
return this._run('move', body, function (routes) { return applyMove(routes, ids, vehicleId, position); });
|
|
28787
|
+
};
|
|
28788
|
+
|
|
28789
|
+
/** Takes stops off their routes; they become unassigned (plan.remove). */
|
|
28790
|
+
PlanEditor.prototype.remove = function (stops) {
|
|
28791
|
+
var ids = idList(stops);
|
|
28792
|
+
return this._run('remove', { stops: ids }, function (routes) { return applyRemove(routes, ids); });
|
|
28793
|
+
};
|
|
28794
|
+
|
|
28795
|
+
/**
|
|
28796
|
+
* Places a stop where it was dropped: before the stop at `position` of vehicleId's route as drawn (past the last
|
|
28797
|
+
* stop: after it). Within its route this is a resequence, otherwise a move. Resolves null when nothing changes.
|
|
28798
|
+
*/
|
|
28799
|
+
PlanEditor.prototype.dropStop = function (stopId, vehicleId, position) {
|
|
28800
|
+
var e = this._history[this._at];
|
|
28801
|
+
if (!e) { return Promise.resolve(null); }
|
|
28802
|
+
var edit = dropEdit(e.routes, String(stopId), String(vehicleId), position);
|
|
28803
|
+
if (!edit) { return Promise.resolve(null); }
|
|
28804
|
+
return edit.kind === 'resequence' ? this.resequence(edit.vehicle_id, edit.stops) : this.move([String(stopId)], edit.vehicle_id, edit.position);
|
|
28805
|
+
};
|
|
28806
|
+
|
|
28807
|
+
/**
|
|
28808
|
+
* Inserts new jobs (each like a job of the plan, with an id the plan does not have) at their cheapest positions, or
|
|
28809
|
+
* places jobs of the plan that are on no route (given by id) (plan.insert). The ranked positions answered are kept
|
|
28810
|
+
* in choices(); choose() takes another one.
|
|
28811
|
+
* @param {Object|string|Array} jobs
|
|
28812
|
+
* @param {Object} [o] limit (1-10), objective ('duration' | 'distance'), nearest (1-50)
|
|
28813
|
+
*/
|
|
28814
|
+
PlanEditor.prototype.insert = function (jobs, o) {
|
|
28815
|
+
try { return this._insert(jobs, o || {}); } catch (err) { return Promise.reject(err); }
|
|
28816
|
+
};
|
|
28817
|
+
PlanEditor.prototype._insert = function (jobs, o) {
|
|
28818
|
+
var e = this._history[this._at];
|
|
28819
|
+
if (!e) { return Promise.reject(PlanError('sMap plan editor: no plan; call setPlan first')); }
|
|
28820
|
+
var list = Array.isArray(jobs) ? jobs : [jobs];
|
|
28821
|
+
var byId = {};
|
|
28822
|
+
e.jobs.forEach(function (j) { byId[String(j.id)] = j; });
|
|
28823
|
+
var placing = list.map(function (j) {
|
|
28824
|
+
if (typeof j === 'string' || typeof j === 'number') {
|
|
28825
|
+
var known = byId[String(j)];
|
|
28826
|
+
if (!known) { throw PlanError('sMap plan editor: no job ' + j); }
|
|
28827
|
+
if (findStop(e.routes, String(j))) { throw PlanError('sMap plan editor: ' + j + ' is already on a route'); }
|
|
28828
|
+
return clone(known);
|
|
28829
|
+
}
|
|
28830
|
+
return clone(j);
|
|
28831
|
+
});
|
|
28832
|
+
var ids = placing.map(function (j) { return String(j.id); });
|
|
28833
|
+
var rest = e.jobs.filter(function (j) { return ids.indexOf(String(j.id)) < 0; });
|
|
28834
|
+
var body = { insert: placing, limit: o.limit || this._options.insertLimit || 3 };
|
|
28835
|
+
var objective = o.objective || this._options.objective;
|
|
28836
|
+
if (objective) { body.objective = objective; }
|
|
28837
|
+
if (o.nearest) { body.nearest = o.nearest; }
|
|
28838
|
+
return this._run('insert', body, null, { jobs: rest, after: rest.concat(placing) });
|
|
28839
|
+
};
|
|
28840
|
+
|
|
28841
|
+
/** The positions the last insert answered: [{ job, placed, reason?, choices: […], chosen }]; null when none. */
|
|
28842
|
+
PlanEditor.prototype.choices = function () { return clone(this._choices); };
|
|
28843
|
+
PlanEditor.prototype.clearChoices = function () { this._choices = null; this._changed('choices'); return this; };
|
|
28844
|
+
|
|
28845
|
+
/** Puts an inserted job at another of its answered positions (plan.move). */
|
|
28846
|
+
PlanEditor.prototype.choose = function (jobId, index) {
|
|
28847
|
+
jobId = String(jobId);
|
|
28848
|
+
var item = (this._choices || []).filter(function (c) { return c.job === jobId; })[0];
|
|
28849
|
+
var choice = item && item.choices[index];
|
|
28850
|
+
if (!choice) { return Promise.reject(PlanError('sMap plan editor: no such position for ' + jobId)); }
|
|
28851
|
+
var self = this;
|
|
28852
|
+
var kept = this._choices;
|
|
28853
|
+
var before = item.chosen;
|
|
28854
|
+
var moving = this.move([jobId], choice.vehicle_id, choice.position);
|
|
28855
|
+
item.chosen = index;
|
|
28856
|
+
this._choices = kept;
|
|
28857
|
+
this._changed('choices');
|
|
28858
|
+
return moving.then(function (answer) {
|
|
28859
|
+
if (answer) { self._choices = kept; self._changed('choices'); }
|
|
28860
|
+
return answer;
|
|
28861
|
+
}, function (e) { item.chosen = before; self._choices = kept; self._changed('choices'); throw e; });
|
|
28862
|
+
};
|
|
28863
|
+
|
|
28864
|
+
/**
|
|
28865
|
+
* Re-orders the stops of chosen routes around the locked stops (plan.refine). The new order is kept only when the
|
|
28866
|
+
* service finds it better; otherwise the plan stays and `last.answer.changed` is false.
|
|
28867
|
+
* @param {Object} [o] vehicleIds (default every route), freeze (default the locked stops on those routes)
|
|
28868
|
+
*/
|
|
28869
|
+
PlanEditor.prototype.refine = function (o) {
|
|
28870
|
+
o = o || {};
|
|
28871
|
+
var e = this._history[this._at];
|
|
28872
|
+
if (!e) { return Promise.reject(PlanError('sMap plan editor: no plan; call setPlan first')); }
|
|
28873
|
+
var body = {};
|
|
28874
|
+
var vehicles = o.vehicleIds ? idList(o.vehicleIds) : null;
|
|
28875
|
+
if (vehicles) { body.vehicle_ids = vehicles; }
|
|
28876
|
+
var freeze = o.freeze ? idList(o.freeze) : this.locked().filter(function (id) {
|
|
28877
|
+
var at = findStop(e.routes, id);
|
|
28878
|
+
return at && (!vehicles || vehicles.indexOf(at.vehicle_id) >= 0);
|
|
28879
|
+
});
|
|
28880
|
+
if (freeze.length) { body.freeze = freeze; }
|
|
28881
|
+
return this._run('refine', body, null, {});
|
|
28882
|
+
};
|
|
28883
|
+
|
|
28884
|
+
/** Prices the plan again as it is (plan.recost). */
|
|
28885
|
+
PlanEditor.prototype.recost = function () {
|
|
28886
|
+
var e = this._history[this._at];
|
|
28887
|
+
if (!e) { return Promise.resolve(null); }
|
|
28888
|
+
this._supersede();
|
|
28889
|
+
return this._price(e);
|
|
28890
|
+
};
|
|
28891
|
+
|
|
28892
|
+
/** Locks stops so that refine keeps them on their route, in their order. */
|
|
28893
|
+
PlanEditor.prototype.lock = function (stops) { idList(stops).forEach(function (id) { this._locked[id] = true; }, this); this._changed('lock'); return this; };
|
|
28894
|
+
PlanEditor.prototype.unlock = function (stops) { idList(stops).forEach(function (id) { delete this._locked[id]; }, this); this._changed('lock'); return this; };
|
|
28895
|
+
PlanEditor.prototype.locked = function () { return Object.keys(this._locked); };
|
|
28896
|
+
PlanEditor.prototype.isLocked = function (id) { return !!this._locked[String(id)]; };
|
|
28897
|
+
|
|
28898
|
+
/** Goes back one edit. The plan drawn is the one answered then; a plan never priced is priced again. */
|
|
28899
|
+
PlanEditor.prototype.undo = function () {
|
|
28900
|
+
if (!this.canUndo()) { return Promise.resolve(null); }
|
|
28901
|
+
return this._step(-1);
|
|
28902
|
+
};
|
|
28903
|
+
PlanEditor.prototype.redo = function () {
|
|
28904
|
+
if (!this.canRedo()) { return Promise.resolve(null); }
|
|
28905
|
+
return this._step(1);
|
|
28906
|
+
};
|
|
28907
|
+
PlanEditor.prototype._step = function (by) {
|
|
28908
|
+
this._supersede();
|
|
28909
|
+
this.clearPreview();
|
|
28910
|
+
this._at += by;
|
|
28911
|
+
this._choices = null;
|
|
28912
|
+
this._last = null;
|
|
28913
|
+
this._error = null;
|
|
28914
|
+
var e = this._history[this._at];
|
|
28915
|
+
this._changed(by < 0 ? 'undo' : 'redo');
|
|
28916
|
+
return e.result ? Promise.resolve(this.getResult()) : this._price(e);
|
|
28917
|
+
};
|
|
28918
|
+
|
|
28919
|
+
/**
|
|
28920
|
+
* Prices other routes for the current jobs without changing the plan (plan.recost, no geometry), e.g. while a stop
|
|
28921
|
+
* is dragged; a newer preview cancels the one before. Reported as the `preview` event.
|
|
28922
|
+
*/
|
|
28923
|
+
PlanEditor.prototype.preview = function (routes) {
|
|
28924
|
+
var e = this._history[this._at];
|
|
28925
|
+
if (!e || this._destroyed) { return Promise.resolve(null); }
|
|
28926
|
+
this._cancelPreview();
|
|
28927
|
+
var controller = newController();
|
|
28928
|
+
var ticket = { controller: controller };
|
|
28929
|
+
this._previewing = ticket;
|
|
28930
|
+
var body = this._body({ routes: routesOf(routes), jobs: e.jobs }, {}, false);
|
|
28931
|
+
var self = this;
|
|
28932
|
+
return this._call(PATHS.recost, body, controller && controller.signal).then(function (res) {
|
|
28933
|
+
if (self._previewing !== ticket) { return null; }
|
|
28934
|
+
self._previewing = null;
|
|
28935
|
+
self._previewResult = { routes: routesOf(routes), result: { routes: (res.data || {}).routes || [], summary: (res.data || {}).summary || null } };
|
|
28936
|
+
self._emit('preview', clone(self._previewResult));
|
|
28937
|
+
return clone(self._previewResult);
|
|
28938
|
+
}, function (err) {
|
|
28939
|
+
if (self._previewing === ticket) { self._previewing = null; }
|
|
28940
|
+
if (!isAbort(err) && self._previewing === null && !self._destroyed) { self._emit('error', { error: err, kind: 'preview' }); }
|
|
28941
|
+
return null;
|
|
28942
|
+
});
|
|
28943
|
+
};
|
|
28944
|
+
PlanEditor.prototype.clearPreview = function () {
|
|
28945
|
+
var had = this._previewing || this._previewResult;
|
|
28946
|
+
this._cancelPreview();
|
|
28947
|
+
this._previewResult = null;
|
|
28948
|
+
if (had) { this._emit('preview', null); }
|
|
28949
|
+
return this;
|
|
28950
|
+
};
|
|
28951
|
+
PlanEditor.prototype._cancelPreview = function () {
|
|
28952
|
+
if (this._previewing && this._previewing.controller) { this._previewing.controller.abort(); }
|
|
28953
|
+
this._previewing = null;
|
|
28954
|
+
};
|
|
28955
|
+
|
|
28956
|
+
/** Ends the editor: cancels its calls, closes its snapshot, and tells its map layer and panel to leave. */
|
|
28957
|
+
PlanEditor.prototype.destroy = function () {
|
|
28958
|
+
if (this._destroyed) { return; }
|
|
28959
|
+
this._supersede();
|
|
28960
|
+
this._cancelPreview();
|
|
28961
|
+
this._destroyed = true;
|
|
28962
|
+
var matrix = this._matrix;
|
|
28963
|
+
this._matrix = null;
|
|
28964
|
+
if (matrix) { this._transport.send('DELETE', PATHS.matrix + '/' + encodeURIComponent(matrix)).catch(function () {}); }
|
|
28965
|
+
this._emit('destroy', null);
|
|
28966
|
+
EVENTS.forEach(function (t) { this._handlers[t] = []; }, this);
|
|
28967
|
+
};
|
|
28968
|
+
|
|
28969
|
+
// --- internals
|
|
28970
|
+
|
|
28971
|
+
PlanEditor.prototype._changed = function (kind) {
|
|
28972
|
+
var e = this._history[this._at] || {};
|
|
28973
|
+
this._emit('change', {
|
|
28974
|
+
kind: kind, plan: this.getPlan(), result: this.getResult(), answer: this._last ? clone(this._last.answer) : null,
|
|
28975
|
+
pending: !e.result, canUndo: this.canUndo(), canRedo: this.canRedo(),
|
|
28976
|
+
});
|
|
28977
|
+
};
|
|
28978
|
+
|
|
28979
|
+
PlanEditor.prototype._setBusy = function (on) {
|
|
28980
|
+
if (this._busy === on) { return; }
|
|
28981
|
+
this._busy = on;
|
|
28982
|
+
this._emit('busy', { busy: on });
|
|
28983
|
+
};
|
|
28984
|
+
|
|
28985
|
+
// Cancels the call in flight. An edit drawn ahead of its answer stays in the history unpriced, and is priced when
|
|
28986
|
+
// it is shown again; a search (insert, refine) that was cancelled never happened.
|
|
28987
|
+
PlanEditor.prototype._supersede = function () {
|
|
28988
|
+
var t = this._inflight;
|
|
28989
|
+
if (!t) { return; }
|
|
28990
|
+
this._inflight = null;
|
|
28991
|
+
if (t.controller) { t.controller.abort(); }
|
|
28992
|
+
this._setBusy(false);
|
|
28993
|
+
};
|
|
28994
|
+
|
|
28995
|
+
PlanEditor.prototype._body = function (entry, extra, geometry) {
|
|
28996
|
+
// Routes without stops are kept here as drop targets and not sent (an empty route would be priced start to end).
|
|
28997
|
+
var routes = entry.routes.filter(function (r) { return r.stops.length; });
|
|
28998
|
+
if (!routes.length) { routes = entry.routes.slice(0, 1); }
|
|
28999
|
+
var body = { vehicles: this._vehicles, jobs: entry.jobs, routes: routes };
|
|
29000
|
+
Object.keys(this._context).forEach(function (k) { body[k] = this._context[k]; }, this);
|
|
29001
|
+
body.options = { geometry: geometry === undefined ? this._geometry : geometry };
|
|
29002
|
+
Object.keys(extra || {}).forEach(function (k) { body[k] = extra[k]; });
|
|
29003
|
+
return body;
|
|
29004
|
+
};
|
|
29005
|
+
|
|
29006
|
+
// One edit: drawn at once when its outcome is known here (`local`), sent with the plan it changes, and replaced by
|
|
29007
|
+
// the answer. `search` (insert, refine) is pushed to the history only when answered.
|
|
29008
|
+
PlanEditor.prototype._run = function (kind, extra, local, search) {
|
|
29009
|
+
if (this._destroyed) { return Promise.reject(PlanError('sMap plan editor: destroyed')); }
|
|
29010
|
+
var base = this._history[this._at];
|
|
29011
|
+
if (!base) { return Promise.reject(PlanError('sMap plan editor: no plan; call setPlan first')); }
|
|
29012
|
+
this._supersede();
|
|
29013
|
+
this.clearPreview();
|
|
29014
|
+
var entry = null;
|
|
29015
|
+
if (local) {
|
|
29016
|
+
var routes;
|
|
29017
|
+
try { routes = local(base.routes); } catch (err) { return Promise.reject(err); }
|
|
29018
|
+
entry = { kind: kind, routes: routes, jobs: base.jobs, result: null, answer: null };
|
|
29019
|
+
this._history.splice(this._at + 1);
|
|
29020
|
+
this._history.push(entry);
|
|
29021
|
+
this._at = this._history.length - 1;
|
|
29022
|
+
this._choices = null;
|
|
29023
|
+
this._last = null;
|
|
29024
|
+
this._error = null;
|
|
29025
|
+
this._changed(kind);
|
|
29026
|
+
}
|
|
29027
|
+
var body = this._body(search && search.jobs ? { routes: base.routes, jobs: search.jobs } : base, extra);
|
|
29028
|
+
return this._request(kind, PATHS[kind], body, entry, search ? { base: base, jobs: search.after || base.jobs } : null);
|
|
29029
|
+
};
|
|
29030
|
+
|
|
29031
|
+
PlanEditor.prototype._price = function (entry) {
|
|
29032
|
+
return this._request('recost', PATHS.recost, this._body(entry, {}), entry, null);
|
|
29033
|
+
};
|
|
29034
|
+
|
|
29035
|
+
PlanEditor.prototype._request = function (kind, path, body, entry, search) {
|
|
29036
|
+
var self = this;
|
|
29037
|
+
var controller = newController();
|
|
29038
|
+
var ticket = { controller: controller, kind: kind };
|
|
29039
|
+
this._inflight = ticket;
|
|
29040
|
+
this._setBusy(true);
|
|
29041
|
+
return this._call(path, body, controller && controller.signal).then(function (res) {
|
|
29042
|
+
if (self._inflight !== ticket) { return null; }
|
|
29043
|
+
self._inflight = null;
|
|
29044
|
+
var data = res.data || {};
|
|
29045
|
+
var answer = ANSWERS[kind] ? (data[ANSWERS[kind]] === undefined ? null : data[ANSWERS[kind]]) : null;
|
|
29046
|
+
var target = entry;
|
|
29047
|
+
if (search) {
|
|
29048
|
+
if (self._history[self._at] !== search.base) { self._setBusy(false); return null; }
|
|
29049
|
+
if (kind === 'refine' && answer && answer.changed === false) {
|
|
29050
|
+
// The plan as sent stays; its prices are the ones answered.
|
|
29051
|
+
target = search.base;
|
|
29052
|
+
} else {
|
|
29053
|
+
target = { kind: kind, routes: search.base.routes, jobs: clone(search.jobs), result: null, answer: null };
|
|
29054
|
+
self._history.splice(self._at + 1);
|
|
29055
|
+
self._history.push(target);
|
|
29056
|
+
self._at = self._history.length - 1;
|
|
29057
|
+
}
|
|
29058
|
+
}
|
|
29059
|
+
if (data.plan && Array.isArray(data.plan.routes)) { target.routes = withAllVehicles(routesOf(data.plan.routes), self._vehicles); }
|
|
29060
|
+
target.result = { routes: data.routes || [], summary: data.summary || null };
|
|
29061
|
+
if (kind !== 'recost') {
|
|
29062
|
+
target.answer = answer;
|
|
29063
|
+
self._last = { kind: kind, answer: answer };
|
|
29064
|
+
}
|
|
29065
|
+
if (kind === 'insert') {
|
|
29066
|
+
self._choices = (answer || []).map(function (item) {
|
|
29067
|
+
var c = clone(item);
|
|
29068
|
+
c.chosen = c.placed ? 0 : null;
|
|
29069
|
+
return c;
|
|
29070
|
+
});
|
|
29071
|
+
}
|
|
29072
|
+
self._error = null;
|
|
29073
|
+
self._setBusy(false);
|
|
29074
|
+
if (self._history[self._at] === target || kind !== 'recost') { self._changed(kind); }
|
|
29075
|
+
return { kind: kind, plan: self.getPlan(), result: self.getResult(), answer: clone(answer) };
|
|
29076
|
+
}, function (err) {
|
|
29077
|
+
var current = self._inflight === ticket;
|
|
29078
|
+
if (current) { self._inflight = null; self._setBusy(false); }
|
|
29079
|
+
if (isAbort(err) || !current || self._destroyed) { return null; }
|
|
29080
|
+
// A failed edit drawn ahead of its answer is taken back.
|
|
29081
|
+
if (entry && entry !== self._history[0] && kind !== 'recost') {
|
|
29082
|
+
var i = self._history.indexOf(entry);
|
|
29083
|
+
if (i > 0) {
|
|
29084
|
+
self._history.splice(i, 1);
|
|
29085
|
+
if (self._at >= i) { self._at -= 1; }
|
|
29086
|
+
}
|
|
29087
|
+
}
|
|
29088
|
+
self._error = { kind: kind, status: err.status || 0, code: err.code || null, message: err.message, reason: err.reason || null };
|
|
29089
|
+
self._changed('error');
|
|
29090
|
+
self._emit('error', { error: err, kind: kind });
|
|
29091
|
+
throw err;
|
|
29092
|
+
});
|
|
29093
|
+
};
|
|
29094
|
+
|
|
29095
|
+
// A call with the snapshot: opened first when needed, named as `matrix`, and opened again after it expired. A
|
|
29096
|
+
// snapshot never makes a call fail (E2): a 410 is repeated with a new snapshot, then without one.
|
|
29097
|
+
PlanEditor.prototype._call = function (path, body, signal, attempt) {
|
|
29098
|
+
var self = this;
|
|
29099
|
+
attempt = attempt || 0;
|
|
29100
|
+
var opening = attempt >= 2 ? Promise.resolve(null) : this._ensureMatrix();
|
|
29101
|
+
return opening.then(function (matrix) {
|
|
29102
|
+
if (signal && signal.aborted) { throw cancelled(); }
|
|
29103
|
+
if (matrix) { body.matrix = matrix; } else { delete body.matrix; }
|
|
29104
|
+
return self._transport.send('POST', path, body, signal);
|
|
29105
|
+
}).then(function (res) {
|
|
29106
|
+
if (body.matrix) { self._snapshotSeen(res.matrix, body.matrix); }
|
|
29107
|
+
return res;
|
|
29108
|
+
}, function (err) {
|
|
29109
|
+
if (err && err.status === 410 && body.matrix && attempt < 2) {
|
|
29110
|
+
self._dropMatrix(body.matrix, 'expired');
|
|
29111
|
+
return self._call(path, body, signal, attempt + 1);
|
|
29112
|
+
}
|
|
29113
|
+
throw err;
|
|
29114
|
+
});
|
|
29115
|
+
};
|
|
29116
|
+
|
|
29117
|
+
PlanEditor.prototype._snapshotSeen = function (header, sent) {
|
|
29118
|
+
var status = header || 'used';
|
|
29119
|
+
this._emit('snapshot', { status: status, matrix: sent });
|
|
29120
|
+
if (status === 'expired' || status === 'mismatch') {
|
|
29121
|
+
this._dropMatrix(sent, null);
|
|
29122
|
+
// Opened now, for the next call; this call was already answered without it.
|
|
29123
|
+
this._ensureMatrix();
|
|
29124
|
+
}
|
|
29125
|
+
};
|
|
29126
|
+
|
|
29127
|
+
PlanEditor.prototype._dropMatrix = function (id, status) {
|
|
29128
|
+
if (this._matrix === id) { this._matrix = null; this._matrixKey = null; }
|
|
29129
|
+
if (status) { this._emit('snapshot', { status: status, matrix: id }); }
|
|
29130
|
+
};
|
|
29131
|
+
|
|
29132
|
+
PlanEditor.prototype._ensureMatrix = function () {
|
|
29133
|
+
if (!this._snapshotOn || this._destroyed) { return Promise.resolve(null); }
|
|
29134
|
+
var key = (this._context.region || '') + '|' + (this._context.profile || '');
|
|
29135
|
+
if (this._matrix && this._matrixKey === key) { return Promise.resolve(this._matrix); }
|
|
29136
|
+
if (this._opening && this._openingKey === key) { return this._opening; }
|
|
29137
|
+
if (this._matrixRetryAt && Date.now() < this._matrixRetryAt) { return Promise.resolve(null); }
|
|
29138
|
+
var self = this;
|
|
29139
|
+
var old = this._matrix;
|
|
29140
|
+
if (old) {
|
|
29141
|
+
this._matrix = null;
|
|
29142
|
+
this._transport.send('DELETE', PATHS.matrix + '/' + encodeURIComponent(old)).catch(function () {});
|
|
29143
|
+
}
|
|
29144
|
+
var body = {};
|
|
29145
|
+
if (this._context.region) { body.region = this._context.region; }
|
|
29146
|
+
if (this._context.profile) { body.profile = this._context.profile; }
|
|
29147
|
+
this._openingKey = key;
|
|
29148
|
+
var opening = this._transport.send('POST', PATHS.matrix, body).then(function (res) {
|
|
29149
|
+
var id = res.data && res.data.matrix;
|
|
29150
|
+
if (!id) { throw PlanError('sMap plan editor: the snapshot answer has no id'); }
|
|
29151
|
+
if (self._destroyed) {
|
|
29152
|
+
self._transport.send('DELETE', PATHS.matrix + '/' + encodeURIComponent(id)).catch(function () {});
|
|
29153
|
+
return null;
|
|
29154
|
+
}
|
|
29155
|
+
self._matrixRetryAt = 0;
|
|
29156
|
+
if (self._openingKey === key) { self._matrix = id; self._matrixKey = key; }
|
|
29157
|
+
self._emit('snapshot', { status: 'opened', matrix: id, expires_at: res.data.expires_at || null, node: res.data.node || null });
|
|
29158
|
+
return id;
|
|
29159
|
+
}, function (err) {
|
|
29160
|
+
self._matrixRetryAt = Date.now() + SNAPSHOT_RETRY_MS;
|
|
29161
|
+
if (!self._destroyed) { self._emit('snapshot', { status: 'failed', matrix: null, error: err }); }
|
|
29162
|
+
return null;
|
|
29163
|
+
}).then(function (id) {
|
|
29164
|
+
if (self._opening === opening) { self._opening = null; self._openingKey = null; }
|
|
29165
|
+
return id;
|
|
29166
|
+
});
|
|
29167
|
+
this._opening = opening;
|
|
29168
|
+
return opening;
|
|
29169
|
+
};
|
|
29170
|
+
|
|
29171
|
+
// ------------------------------------------------------------------ texts (14 languages)
|
|
29172
|
+
|
|
29173
|
+
var TEXT_KEYS = ['editor', 'busy', 'undo', 'redo', 'refine', 'total', 'distance', 'drivingTime', 'stops', 'violations',
|
|
29174
|
+
'unreachable', 'unassigned', 'noStops', 'place', 'remove', 'lock', 'unlock', 'choicesFor', 'choice', 'added', 'arrival',
|
|
29175
|
+
'choose', 'chosen', 'close', 'notPlaced', 'preview', 'refined', 'notRefined', 'error', 'pending', 'km', 'hours',
|
|
29176
|
+
'minutes', 'rule.capacity', 'rule.max_tasks', 'rule.max_distance', 'rule.max_travel_time', 'rule.vehicle_time_window',
|
|
29177
|
+
'rule.skills', 'rule.time_window', 'rule.unreachable'];
|
|
29178
|
+
|
|
29179
|
+
function texts(list) {
|
|
29180
|
+
var out = {};
|
|
29181
|
+
TEXT_KEYS.forEach(function (k, i) { out[k] = list[i]; });
|
|
29182
|
+
return out;
|
|
29183
|
+
}
|
|
29184
|
+
|
|
29185
|
+
var TEXTS = {
|
|
29186
|
+
en: texts(['Plan editor', 'Updating the plan', 'Undo', 'Redo', 'Refine', 'Total', 'Distance', 'Driving time', 'Stops',
|
|
29187
|
+
'Violations', 'Unreachable legs', 'Unassigned', 'No stops', 'Place', 'Remove', 'Lock', 'Unlock', 'Positions for {job}',
|
|
29188
|
+
'{vehicle}, stop {n}', '+{distance}, +{time}', 'Arrival {time}', 'Choose', 'Chosen', 'Close', 'No position found',
|
|
29189
|
+
'Preview', 'Stops moved: {n}', 'No better order found', 'The plan could not be updated.', 'Not yet priced', '{d} km',
|
|
29190
|
+
'{h} h {m} min', '{m} min', 'Capacity exceeded', 'Too many stops', 'Distance limit exceeded',
|
|
29191
|
+
'Driving time limit exceeded', 'Working hours exceeded', 'Missing skills', 'Time window missed', 'No road to this stop']),
|
|
29192
|
+
'zh-Hans': texts(['计划编辑器', '正在更新计划', '撤销', '重做', '优化', '合计', '距离', '行驶时间', '站点', '违规',
|
|
29193
|
+
'不可达路段', '未分配', '无站点', '安排', '移除', '锁定', '解锁', '{job} 的可选位置', '{vehicle},第 {n} 站',
|
|
29194
|
+
'+{distance},+{time}', '到达 {time}', '选择', '已选', '关闭', '未找到可用位置', '预览', '已移动站点:{n}',
|
|
29195
|
+
'未找到更优的顺序', '计划未能更新。', '尚未计算', '{d} 公里', '{h} 小时 {m} 分钟', '{m} 分钟', '超出载量', '站点过多',
|
|
29196
|
+
'超出距离上限', '超出行驶时间上限', '超出工作时间', '缺少技能', '错过时间窗', '无法到达此站点']),
|
|
29197
|
+
'zh-Hant': texts(['計劃編輯器', '正在更新計劃', '復原', '重做', '優化', '合計', '距離', '行駛時間', '站點', '違規',
|
|
29198
|
+
'不可達路段', '未分配', '無站點', '安排', '移除', '鎖定', '解鎖', '{job} 的可選位置', '{vehicle},第 {n} 站',
|
|
29199
|
+
'+{distance},+{time}', '到達 {time}', '選擇', '已選', '關閉', '未找到可用位置', '預覽', '已移動站點:{n}',
|
|
29200
|
+
'未找到更優的順序', '計劃未能更新。', '尚未計算', '{d} 公里', '{h} 小時 {m} 分鐘', '{m} 分鐘', '超出載量', '站點過多',
|
|
29201
|
+
'超出距離上限', '超出行駛時間上限', '超出工作時間', '缺少技能', '錯過時間窗', '無法到達此站點']),
|
|
29202
|
+
de: texts(['Planeditor', 'Plan wird aktualisiert', 'Rückgängig', 'Wiederholen', 'Optimieren', 'Gesamt', 'Strecke',
|
|
29203
|
+
'Fahrzeit', 'Stopps', 'Verstöße', 'Nicht erreichbare Abschnitte', 'Nicht zugewiesen', 'Keine Stopps', 'Einplanen',
|
|
29204
|
+
'Entfernen', 'Sperren', 'Entsperren', 'Positionen für {job}', '{vehicle}, Stopp {n}', '+{distance}, +{time}',
|
|
29205
|
+
'Ankunft {time}', 'Auswählen', 'Ausgewählt', 'Schließen', 'Keine Position gefunden', 'Vorschau',
|
|
29206
|
+
'Verschobene Stopps: {n}', 'Keine bessere Reihenfolge gefunden', 'Der Plan konnte nicht aktualisiert werden.',
|
|
29207
|
+
'Noch nicht berechnet', '{d} km', '{h} Std. {m} Min.', '{m} Min.', 'Kapazität überschritten', 'Zu viele Stopps',
|
|
29208
|
+
'Streckenlimit überschritten', 'Fahrzeitlimit überschritten', 'Arbeitszeit überschritten', 'Fehlende Fähigkeiten',
|
|
29209
|
+
'Zeitfenster verpasst', 'Kein Weg zu diesem Stopp']),
|
|
29210
|
+
es: texts(['Editor de planes', 'Actualizando el plan', 'Deshacer', 'Rehacer', 'Optimizar', 'Total', 'Distancia',
|
|
29211
|
+
'Tiempo de conducción', 'Paradas', 'Infracciones', 'Tramos inalcanzables', 'Sin asignar', 'Sin paradas', 'Asignar',
|
|
29212
|
+
'Quitar', 'Bloquear', 'Desbloquear', 'Posiciones para {job}', '{vehicle}, parada {n}', '+{distance}, +{time}',
|
|
29213
|
+
'Llegada {time}', 'Elegir', 'Elegida', 'Cerrar', 'No se encontró ninguna posición', 'Vista previa',
|
|
29214
|
+
'Paradas movidas: {n}', 'No se encontró un orden mejor', 'No se pudo actualizar el plan.', 'Aún sin calcular',
|
|
29215
|
+
'{d} km', '{h} h {m} min', '{m} min', 'Capacidad superada', 'Demasiadas paradas', 'Límite de distancia superado',
|
|
29216
|
+
'Límite de tiempo de conducción superado', 'Horario de trabajo superado', 'Faltan habilidades',
|
|
29217
|
+
'Ventana horaria incumplida', 'No hay ruta hasta esta parada']),
|
|
29218
|
+
fr: texts(['Éditeur de plan', 'Mise à jour du plan', 'Annuler', 'Rétablir', 'Optimiser', 'Total', 'Distance',
|
|
29219
|
+
'Temps de conduite', 'Arrêts', 'Infractions', 'Tronçons inaccessibles', 'Non affectés', 'Aucun arrêt', 'Placer',
|
|
29220
|
+
'Retirer', 'Verrouiller', 'Déverrouiller', 'Positions pour {job}', '{vehicle}, arrêt {n}', '+{distance}, +{time}',
|
|
29221
|
+
'Arrivée {time}', 'Choisir', 'Choisie', 'Fermer', 'Aucune position trouvée', 'Aperçu', 'Arrêts déplacés : {n}',
|
|
29222
|
+
'Aucun meilleur ordre trouvé', 'Le plan n’a pas pu être mis à jour.', 'Pas encore calculé', '{d} km',
|
|
29223
|
+
'{h} h {m} min', '{m} min', 'Capacité dépassée', 'Trop d’arrêts', 'Limite de distance dépassée',
|
|
29224
|
+
'Limite de temps de conduite dépassée', 'Horaires de travail dépassés', 'Compétences manquantes',
|
|
29225
|
+
'Créneau horaire manqué', 'Aucune route vers cet arrêt']),
|
|
29226
|
+
it: texts(['Editor del piano', 'Aggiornamento del piano', 'Annulla', 'Ripeti', 'Ottimizza', 'Totale', 'Distanza',
|
|
29227
|
+
'Tempo di guida', 'Fermate', 'Violazioni', 'Tratti non raggiungibili', 'Non assegnate', 'Nessuna fermata', 'Assegna',
|
|
29228
|
+
'Rimuovi', 'Blocca', 'Sblocca', 'Posizioni per {job}', '{vehicle}, fermata {n}', '+{distance}, +{time}',
|
|
29229
|
+
'Arrivo {time}', 'Scegli', 'Scelta', 'Chiudi', 'Nessuna posizione trovata', 'Anteprima', 'Fermate spostate: {n}',
|
|
29230
|
+
'Nessun ordine migliore trovato', 'Impossibile aggiornare il piano.', 'Non ancora calcolato', '{d} km',
|
|
29231
|
+
'{h} h {m} min', '{m} min', 'Capacità superata', 'Troppe fermate', 'Limite di distanza superato',
|
|
29232
|
+
'Limite di tempo di guida superato', 'Orario di lavoro superato', 'Competenze mancanti', 'Finestra oraria mancata',
|
|
29233
|
+
'Nessuna strada per questa fermata']),
|
|
29234
|
+
nl: texts(['Planeditor', 'Plan wordt bijgewerkt', 'Ongedaan maken', 'Opnieuw', 'Optimaliseren', 'Totaal', 'Afstand',
|
|
29235
|
+
'Rijtijd', 'Stops', 'Overtredingen', 'Onbereikbare trajecten', 'Niet toegewezen', 'Geen stops', 'Inplannen',
|
|
29236
|
+
'Verwijderen', 'Vergrendelen', 'Ontgrendelen', 'Posities voor {job}', '{vehicle}, stop {n}', '+{distance}, +{time}',
|
|
29237
|
+
'Aankomst {time}', 'Kiezen', 'Gekozen', 'Sluiten', 'Geen positie gevonden', 'Voorbeeld', 'Verplaatste stops: {n}',
|
|
29238
|
+
'Geen betere volgorde gevonden', 'Het plan kon niet worden bijgewerkt.', 'Nog niet berekend', '{d} km',
|
|
29239
|
+
'{h} u {m} min', '{m} min', 'Capaciteit overschreden', 'Te veel stops', 'Afstandslimiet overschreden',
|
|
29240
|
+
'Rijtijdlimiet overschreden', 'Werktijd overschreden', 'Ontbrekende vaardigheden', 'Tijdvenster gemist',
|
|
29241
|
+
'Geen route naar deze stop']),
|
|
29242
|
+
// European Portuguese.
|
|
29243
|
+
pt: texts(['Editor de planos', 'A atualizar o plano', 'Anular', 'Refazer', 'Otimizar', 'Total', 'Distância',
|
|
29244
|
+
'Tempo de condução', 'Paragens', 'Infrações', 'Troços inacessíveis', 'Não atribuídas', 'Sem paragens', 'Atribuir',
|
|
29245
|
+
'Remover', 'Bloquear', 'Desbloquear', 'Posições para {job}', '{vehicle}, paragem {n}', '+{distance}, +{time}',
|
|
29246
|
+
'Chegada {time}', 'Escolher', 'Escolhida', 'Fechar', 'Nenhuma posição encontrada', 'Pré-visualização',
|
|
29247
|
+
'Paragens movidas: {n}', 'Nenhuma ordem melhor encontrada', 'Não foi possível atualizar o plano.',
|
|
29248
|
+
'Ainda não calculado', '{d} km', '{h} h {m} min', '{m} min', 'Capacidade excedida', 'Demasiadas paragens',
|
|
29249
|
+
'Limite de distância excedido', 'Limite de tempo de condução excedido', 'Horário de trabalho excedido',
|
|
29250
|
+
'Competências em falta', 'Janela horária falhada', 'Sem estrada até esta paragem']),
|
|
29251
|
+
// Serbian in the Latin script.
|
|
29252
|
+
sr: texts(['Uređivač plana', 'Plan se ažurira', 'Opozovi', 'Ponovi', 'Optimizuj', 'Ukupno', 'Rastojanje',
|
|
29253
|
+
'Vreme vožnje', 'Stanice', 'Prekršaji', 'Nedostupne deonice', 'Nedodeljeno', 'Nema stanica', 'Rasporedi', 'Ukloni',
|
|
29254
|
+
'Zaključaj', 'Otključaj', 'Pozicije za {job}', '{vehicle}, stanica {n}', '+{distance}, +{time}', 'Dolazak {time}',
|
|
29255
|
+
'Izaberi', 'Izabrano', 'Zatvori', 'Nije pronađena pozicija', 'Pregled', 'Premeštene stanice: {n}',
|
|
29256
|
+
'Nije pronađen bolji redosled', 'Plan nije mogao da se ažurira.', 'Još nije izračunato', '{d} km',
|
|
29257
|
+
'{h} h {m} min', '{m} min', 'Prekoračen kapacitet', 'Previše stanica', 'Prekoračeno ograničenje rastojanja',
|
|
29258
|
+
'Prekoračeno ograničenje vremena vožnje', 'Prekoračeno radno vreme', 'Nedostaju veštine',
|
|
29259
|
+
'Propušten vremenski prozor', 'Nema puta do ove stanice']),
|
|
29260
|
+
hu: texts(['Tervszerkesztő', 'A terv frissítése folyamatban', 'Visszavonás', 'Újra', 'Optimalizálás', 'Összesen',
|
|
29261
|
+
'Távolság', 'Vezetési idő', 'Megállók', 'Szabálysértések', 'Elérhetetlen szakaszok', 'Nincs hozzárendelve',
|
|
29262
|
+
'Nincsenek megállók', 'Beosztás', 'Eltávolítás', 'Zárolás', 'Feloldás', 'Pozíciók ehhez: {job}',
|
|
29263
|
+
'{vehicle}, {n}. megálló', '+{distance}, +{time}', 'Érkezés: {time}', 'Kiválasztás', 'Kiválasztva', 'Bezárás',
|
|
29264
|
+
'Nem található pozíció', 'Előnézet', 'Áthelyezett megállók: {n}', 'Nem található jobb sorrend',
|
|
29265
|
+
'A tervet nem sikerült frissíteni.', 'Még nincs kiszámítva', '{d} km', '{h} óra {m} perc', '{m} perc',
|
|
29266
|
+
'Kapacitás túllépve', 'Túl sok megálló', 'Távolságkorlát túllépve', 'Vezetésiidő-korlát túllépve',
|
|
29267
|
+
'Munkaidő túllépve', 'Hiányzó képességek', 'Időablak elmulasztva', 'Nincs út ehhez a megállóhoz']),
|
|
29268
|
+
pl: texts(['Edytor planu', 'Aktualizowanie planu', 'Cofnij', 'Ponów', 'Optymalizuj', 'Razem', 'Odległość',
|
|
29269
|
+
'Czas jazdy', 'Przystanki', 'Naruszenia', 'Nieosiągalne odcinki', 'Nieprzypisane', 'Brak przystanków', 'Przydziel',
|
|
29270
|
+
'Usuń', 'Zablokuj', 'Odblokuj', 'Pozycje dla {job}', '{vehicle}, przystanek {n}', '+{distance}, +{time}',
|
|
29271
|
+
'Przyjazd {time}', 'Wybierz', 'Wybrano', 'Zamknij', 'Nie znaleziono pozycji', 'Podgląd',
|
|
29272
|
+
'Przeniesione przystanki: {n}', 'Nie znaleziono lepszej kolejności', 'Nie udało się zaktualizować planu.',
|
|
29273
|
+
'Jeszcze nie obliczono', '{d} km', '{h} godz. {m} min', '{m} min', 'Przekroczona ładowność',
|
|
29274
|
+
'Zbyt wiele przystanków', 'Przekroczony limit odległości', 'Przekroczony limit czasu jazdy',
|
|
29275
|
+
'Przekroczony czas pracy', 'Brak umiejętności', 'Pominięte okno czasowe', 'Brak drogi do tego przystanku']),
|
|
29276
|
+
sk: texts(['Editor plánu', 'Plán sa aktualizuje', 'Späť', 'Znova', 'Optimalizovať', 'Spolu', 'Vzdialenosť',
|
|
29277
|
+
'Čas jazdy', 'Zastávky', 'Porušenia', 'Nedosiahnuteľné úseky', 'Nepriradené', 'Žiadne zastávky', 'Zaradiť',
|
|
29278
|
+
'Odstrániť', 'Uzamknúť', 'Odomknúť', 'Pozície pre {job}', '{vehicle}, zastávka {n}', '+{distance}, +{time}',
|
|
29279
|
+
'Príchod {time}', 'Vybrať', 'Vybrané', 'Zavrieť', 'Nenašla sa žiadna pozícia', 'Náhľad', 'Presunuté zastávky: {n}',
|
|
29280
|
+
'Lepšie poradie sa nenašlo', 'Plán sa nepodarilo aktualizovať.', 'Ešte nevypočítané', '{d} km', '{h} h {m} min',
|
|
29281
|
+
'{m} min', 'Prekročená kapacita', 'Príliš veľa zastávok', 'Prekročený limit vzdialenosti',
|
|
29282
|
+
'Prekročený limit času jazdy', 'Prekročený pracovný čas', 'Chýbajúce zručnosti', 'Zmeškané časové okno',
|
|
29283
|
+
'Žiadna cesta k tejto zastávke']),
|
|
29284
|
+
cs: texts(['Editor plánu', 'Plán se aktualizuje', 'Zpět', 'Znovu', 'Optimalizovat', 'Celkem', 'Vzdálenost',
|
|
29285
|
+
'Doba jízdy', 'Zastávky', 'Porušení', 'Nedosažitelné úseky', 'Nepřiřazené', 'Žádné zastávky', 'Zařadit', 'Odebrat',
|
|
29286
|
+
'Zamknout', 'Odemknout', 'Pozice pro {job}', '{vehicle}, zastávka {n}', '+{distance}, +{time}', 'Příjezd {time}',
|
|
29287
|
+
'Vybrat', 'Vybráno', 'Zavřít', 'Nebyla nalezena žádná pozice', 'Náhled', 'Přesunuté zastávky: {n}',
|
|
29288
|
+
'Lepší pořadí nebylo nalezeno', 'Plán se nepodařilo aktualizovat.', 'Dosud nevypočteno', '{d} km', '{h} h {m} min',
|
|
29289
|
+
'{m} min', 'Překročena kapacita', 'Příliš mnoho zastávek', 'Překročen limit vzdálenosti',
|
|
29290
|
+
'Překročen limit doby jízdy', 'Překročena pracovní doba', 'Chybějící dovednosti', 'Zmeškané časové okno',
|
|
29291
|
+
'K této zastávce nevede cesta']),
|
|
29292
|
+
};
|
|
29293
|
+
var LANGUAGES = Object.keys(TEXTS);
|
|
29294
|
+
|
|
29295
|
+
/** The panel's language for a BCP-47 tag: one of the 14, English when none fits. */
|
|
29296
|
+
function panelLanguage(tag) {
|
|
29297
|
+
var t = String(tag || 'en').toLowerCase();
|
|
29298
|
+
if (/^zh-(hant|tw|hk|mo)/.test(t)) { return 'zh-Hant'; }
|
|
29299
|
+
if (/^zh/.test(t)) { return 'zh-Hans'; }
|
|
29300
|
+
var base = t.split(/[-_]/)[0];
|
|
29301
|
+
return TEXTS[base] ? base : 'en';
|
|
29302
|
+
}
|
|
29303
|
+
|
|
29304
|
+
/** The panel's texts in a language, with the caller's own texts (`labels`) over them. */
|
|
29305
|
+
function textsFor(language, labels) {
|
|
29306
|
+
var out = {};
|
|
29307
|
+
var own = TEXTS[panelLanguage(language)];
|
|
29308
|
+
TEXT_KEYS.forEach(function (k) { out[k] = (labels && labels[k]) || own[k]; });
|
|
29309
|
+
return out;
|
|
29310
|
+
}
|
|
29311
|
+
|
|
29312
|
+
function fill(template, values) {
|
|
29313
|
+
return String(template).replace(/\{(\w+)\}/g, function (m, k) { return values[k] === undefined ? m : String(values[k]); });
|
|
29314
|
+
}
|
|
29315
|
+
|
|
29316
|
+
// ------------------------------------------------------------------ the panel
|
|
29317
|
+
|
|
29318
|
+
// The locale numbers are written in: the panel's Portuguese is European Portuguese, so any `pt` tag writes numbers
|
|
29319
|
+
// as `pt-PT` does (`pt` alone is Brazilian grouping); every other tag is used as given.
|
|
29320
|
+
function numberLocale(language) {
|
|
29321
|
+
return panelLanguage(language) === 'pt' ? 'pt-PT' : language;
|
|
29322
|
+
}
|
|
29323
|
+
|
|
29324
|
+
function numberFormat(language, digits) {
|
|
29325
|
+
try {
|
|
29326
|
+
var nf = new Intl.NumberFormat(numberLocale(language), { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
29327
|
+
return function (v) { return nf.format(v); };
|
|
29328
|
+
} catch (e) { return function (v) { return Number(v).toFixed(digits); }; }
|
|
29329
|
+
}
|
|
29330
|
+
|
|
29331
|
+
function formatters(language, t) {
|
|
29332
|
+
var one = numberFormat(language, 1);
|
|
29333
|
+
return {
|
|
29334
|
+
distance: function (m) { return fill(t.km, { d: one((Number(m) || 0) / 1000) }); },
|
|
29335
|
+
time: function (s) {
|
|
29336
|
+
var min = Math.round((Number(s) || 0) / 60);
|
|
29337
|
+
var h = Math.floor(min / 60);
|
|
29338
|
+
var m = min % 60;
|
|
29339
|
+
return h > 0 ? fill(t.hours, { h: h, m: (m < 10 ? '0' : '') + m }) : fill(t.minutes, { m: m });
|
|
29340
|
+
},
|
|
29341
|
+
// The clock time of an ISO 8601 time with offset, as the answer gives it (the plan's own time zone).
|
|
29342
|
+
clock: function (iso) { var m = /T(\d\d:\d\d)/.exec(String(iso || '')); return m ? m[1] : ''; },
|
|
29343
|
+
};
|
|
29344
|
+
}
|
|
29345
|
+
|
|
29346
|
+
var COLORS = ['#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2', '#db2777', '#65a30d', '#4f46e5', '#b45309'];
|
|
29347
|
+
|
|
29348
|
+
function colorOf(editor, vehicleId, colors) {
|
|
29349
|
+
var list = colors || COLORS;
|
|
29350
|
+
var i = 0;
|
|
29351
|
+
editor._vehicles.some(function (v, k) { if (String(v.id) === vehicleId) { i = k; return true; } return false; });
|
|
29352
|
+
return list[i % list.length];
|
|
29353
|
+
}
|
|
29354
|
+
|
|
29355
|
+
/**
|
|
29356
|
+
* The ready-made panel: routes with their totals and stops, violations, undo / redo / refine, the positions an
|
|
29357
|
+
* insert answered, and errors. Stops are dragged between lists, or onto the unassigned list to take them off.
|
|
29358
|
+
* @param {PlanEditor} editor
|
|
29359
|
+
* @param {Object} [o] language (BCP-47), labels (own texts by key), document, colors, className
|
|
29360
|
+
* @returns {{ element: HTMLElement, setLanguage(language: string): void, render(): void, destroy(): void }}
|
|
29361
|
+
*/
|
|
29362
|
+
function createPanel(editor, o) {
|
|
29363
|
+
o = o || {};
|
|
29364
|
+
var doc = o.document || (typeof document !== 'undefined' ? document : null);
|
|
29365
|
+
if (!doc) { throw new Error('sMap plan editor: the panel needs a document'); }
|
|
29366
|
+
var language = o.language || 'en';
|
|
29367
|
+
var t = textsFor(language, o.labels);
|
|
29368
|
+
var fmt = formatters(language, t);
|
|
29369
|
+
var root = doc.createElement('div');
|
|
29370
|
+
root.className = 'smap-plan-panel' + (o.className ? ' ' + o.className : '');
|
|
29371
|
+
var dragging = null;
|
|
29372
|
+
|
|
29373
|
+
function el(tag, className, text) {
|
|
29374
|
+
var n = doc.createElement(tag);
|
|
29375
|
+
if (className) { n.className = className; }
|
|
29376
|
+
if (text !== undefined && text !== null) { n.textContent = String(text); }
|
|
29377
|
+
return n;
|
|
29378
|
+
}
|
|
29379
|
+
function button(className, text, onClick, disabled) {
|
|
29380
|
+
var b = el('button', 'smap-plan-button ' + className, text);
|
|
29381
|
+
b.type = 'button';
|
|
29382
|
+
if (disabled) { b.disabled = true; }
|
|
29383
|
+
b.addEventListener('click', function () { var p = onClick(); if (p && p.catch) { p.catch(function () {}); } });
|
|
29384
|
+
return b;
|
|
29385
|
+
}
|
|
29386
|
+
function totals(parent, summary, stops) {
|
|
29387
|
+
var dl = el('dl', 'smap-plan-totals');
|
|
29388
|
+
function row(label, value, className) {
|
|
29389
|
+
var w = el('div', className);
|
|
29390
|
+
w.appendChild(el('dt', null, label));
|
|
29391
|
+
w.appendChild(el('dd', null, value));
|
|
29392
|
+
dl.appendChild(w);
|
|
29393
|
+
}
|
|
29394
|
+
row(t.distance, fmt.distance(summary.distance_meters), 'smap-plan-distance');
|
|
29395
|
+
row(t.drivingTime, fmt.time(summary.duration_seconds), 'smap-plan-time');
|
|
29396
|
+
row(t.stops, String(stops), 'smap-plan-stops');
|
|
29397
|
+
if (summary.violations || summary.violation_count) { row(t.violations, String(summary.violations || summary.violation_count), 'smap-plan-violations'); }
|
|
29398
|
+
if (summary.unreachable_legs) { row(t.unreachable, String(summary.unreachable_legs), 'smap-plan-unreachable'); }
|
|
29399
|
+
parent.appendChild(dl);
|
|
29400
|
+
}
|
|
29401
|
+
function ruleText(v) { return t['rule.' + v.rule] || v.message || v.rule; }
|
|
29402
|
+
function dropZone(node, onDrop) {
|
|
29403
|
+
node.addEventListener('dragover', function (e) { if (dragging) { e.preventDefault(); node.classList.add('smap-plan-over'); } });
|
|
29404
|
+
node.addEventListener('dragleave', function () { node.classList.remove('smap-plan-over'); });
|
|
29405
|
+
node.addEventListener('drop', function (e) {
|
|
29406
|
+
node.classList.remove('smap-plan-over');
|
|
29407
|
+
if (!dragging) { return; }
|
|
29408
|
+
e.preventDefault();
|
|
29409
|
+
e.stopPropagation();
|
|
29410
|
+
var id = dragging;
|
|
29411
|
+
dragging = null;
|
|
29412
|
+
var p = onDrop(id);
|
|
29413
|
+
if (p && p.catch) { p.catch(function () {}); }
|
|
29414
|
+
});
|
|
29415
|
+
}
|
|
29416
|
+
function draggable(node, id) {
|
|
29417
|
+
node.draggable = true;
|
|
29418
|
+
node.setAttribute('data-stop', id);
|
|
29419
|
+
node.addEventListener('dragstart', function (e) {
|
|
29420
|
+
dragging = id;
|
|
29421
|
+
if (e.dataTransfer) { try { e.dataTransfer.setData('text/plain', id); e.dataTransfer.effectAllowed = 'move'; } catch (x) { /* older engines */ } }
|
|
29422
|
+
});
|
|
29423
|
+
node.addEventListener('dragend', function () { dragging = null; });
|
|
29424
|
+
}
|
|
29425
|
+
|
|
29426
|
+
function render() {
|
|
29427
|
+
var s = editor.state();
|
|
29428
|
+
while (root.firstChild) { root.removeChild(root.firstChild); }
|
|
29429
|
+
root.setAttribute('role', 'region');
|
|
29430
|
+
root.setAttribute('aria-label', t.editor);
|
|
29431
|
+
root.setAttribute('aria-busy', s.busy ? 'true' : 'false');
|
|
29432
|
+
root.classList.toggle('smap-plan-busy', s.busy);
|
|
29433
|
+
|
|
29434
|
+
var bar = el('div', 'smap-plan-bar');
|
|
29435
|
+
bar.appendChild(button('smap-plan-undo', t.undo, function () { return editor.undo(); }, !s.canUndo));
|
|
29436
|
+
bar.appendChild(button('smap-plan-redo', t.redo, function () { return editor.redo(); }, !s.canRedo));
|
|
29437
|
+
bar.appendChild(button('smap-plan-refine', t.refine, function () { return editor.refine(); }, !s.plan));
|
|
29438
|
+
var status = el('span', 'smap-plan-status', s.busy ? t.busy : '');
|
|
29439
|
+
status.setAttribute('role', 'status');
|
|
29440
|
+
bar.appendChild(status);
|
|
29441
|
+
root.appendChild(bar);
|
|
29442
|
+
|
|
29443
|
+
if (s.error) {
|
|
29444
|
+
var alert = el('div', 'smap-plan-error');
|
|
29445
|
+
alert.setAttribute('role', 'alert');
|
|
29446
|
+
alert.appendChild(el('span', null, t.error + (s.error.code ? ' (' + s.error.code + (s.error.reason ? ', ' + s.error.reason : '') + ')' : '')));
|
|
29447
|
+
alert.appendChild(button('smap-plan-error-close', t.close, function () { editor._error = null; render(); }));
|
|
29448
|
+
root.appendChild(alert);
|
|
29449
|
+
}
|
|
29450
|
+
if (s.last && s.last.kind === 'refine' && s.last.answer) {
|
|
29451
|
+
root.appendChild(el('div', 'smap-plan-note', s.last.answer.changed ? fill(t.refined, { n: s.last.answer.moved || 0 }) : t.notRefined));
|
|
29452
|
+
}
|
|
29453
|
+
if (!s.plan) { return; }
|
|
29454
|
+
|
|
29455
|
+
var priced = {};
|
|
29456
|
+
((s.result && s.result.routes) || []).forEach(function (r) { priced[r.vehicle_id] = r; });
|
|
29457
|
+
var summary = s.result && s.result.summary;
|
|
29458
|
+
var total = el('section', 'smap-plan-total');
|
|
29459
|
+
total.appendChild(el('h3', null, t.total));
|
|
29460
|
+
if (summary) { totals(total, summary, summary.tasks !== undefined ? summary.tasks : 0); } else { total.appendChild(el('p', 'smap-plan-pending', t.pending)); }
|
|
29461
|
+
if (s.preview && s.preview.result && s.preview.result.summary) {
|
|
29462
|
+
var pv = el('div', 'smap-plan-preview');
|
|
29463
|
+
pv.appendChild(el('h4', null, t.preview));
|
|
29464
|
+
totals(pv, s.preview.result.summary, s.preview.result.summary.tasks || 0);
|
|
29465
|
+
total.appendChild(pv);
|
|
29466
|
+
}
|
|
29467
|
+
root.appendChild(total);
|
|
29468
|
+
|
|
29469
|
+
var routes = el('div', 'smap-plan-routes');
|
|
29470
|
+
s.plan.routes.forEach(function (r) {
|
|
29471
|
+
var p = priced[r.vehicle_id];
|
|
29472
|
+
var section = el('section', 'smap-plan-route');
|
|
29473
|
+
section.setAttribute('data-vehicle', r.vehicle_id);
|
|
29474
|
+
var head = el('h3', 'smap-plan-route-head');
|
|
29475
|
+
var swatch = el('span', 'smap-plan-swatch');
|
|
29476
|
+
swatch.style.background = colorOf(editor, r.vehicle_id, o.colors);
|
|
29477
|
+
head.appendChild(swatch);
|
|
29478
|
+
head.appendChild(el('span', null, r.vehicle_id));
|
|
29479
|
+
section.appendChild(head);
|
|
29480
|
+
if (p) {
|
|
29481
|
+
totals(section, { distance_meters: p.distance_meters, duration_seconds: p.duration_seconds, violation_count: p.violation_count, unreachable_legs: p.unreachable_legs }, r.stops.length);
|
|
29482
|
+
if (p.violations && p.violations.length) {
|
|
29483
|
+
var ul = el('ul', 'smap-plan-route-violations');
|
|
29484
|
+
p.violations.forEach(function (v) { ul.appendChild(el('li', null, ruleText(v))); });
|
|
29485
|
+
section.appendChild(ul);
|
|
29486
|
+
}
|
|
29487
|
+
}
|
|
29488
|
+
var byId = {};
|
|
29489
|
+
((p && p.stops) || []).forEach(function (st) { if (st.type === 'job') { byId[st.id] = st; } });
|
|
29490
|
+
var ol = el('ol', 'smap-plan-stops-list');
|
|
29491
|
+
if (!r.stops.length) { ol.appendChild(el('li', 'smap-plan-empty', t.noStops)); }
|
|
29492
|
+
r.stops.forEach(function (id, i) {
|
|
29493
|
+
var st = byId[id];
|
|
29494
|
+
var li = el('li', 'smap-plan-stop' + (editor.isLocked(id) ? ' smap-plan-locked' : ''));
|
|
29495
|
+
draggable(li, id);
|
|
29496
|
+
li.appendChild(el('span', 'smap-plan-seq', String(i + 1)));
|
|
29497
|
+
li.appendChild(el('span', 'smap-plan-id', id));
|
|
29498
|
+
if (st && st.arrival) {
|
|
29499
|
+
// The clock time only, the full text for the pointer and screen readers: a stop row stays on one line.
|
|
29500
|
+
var arrival = el('span', 'smap-plan-arrival', fmt.clock(st.arrival));
|
|
29501
|
+
arrival.title = fill(t.arrival, { time: fmt.clock(st.arrival) });
|
|
29502
|
+
arrival.setAttribute('aria-label', arrival.title);
|
|
29503
|
+
li.appendChild(arrival);
|
|
29504
|
+
}
|
|
29505
|
+
var marks = [];
|
|
29506
|
+
if (st && st.reachable === false) { marks.push(t['rule.unreachable']); }
|
|
29507
|
+
((st && st.violations) || []).forEach(function (v) { marks.push(ruleText(v)); });
|
|
29508
|
+
if (marks.length) { li.appendChild(el('span', 'smap-plan-stop-violations', marks.join(', '))); }
|
|
29509
|
+
var locked = editor.isLocked(id);
|
|
29510
|
+
li.appendChild(button('smap-plan-lock', locked ? t.unlock : t.lock, function () { return locked ? editor.unlock(id) : editor.lock(id); }));
|
|
29511
|
+
li.appendChild(button('smap-plan-remove', t.remove, function () { return editor.remove([id]); }));
|
|
29512
|
+
dropZone(li, function (dragged) { return editor.dropStop(dragged, r.vehicle_id, i + 1); });
|
|
29513
|
+
ol.appendChild(li);
|
|
29514
|
+
});
|
|
29515
|
+
dropZone(ol, function (dragged) { return editor.dropStop(dragged, r.vehicle_id, r.stops.length + 1); });
|
|
29516
|
+
section.appendChild(ol);
|
|
29517
|
+
routes.appendChild(section);
|
|
29518
|
+
});
|
|
29519
|
+
root.appendChild(routes);
|
|
29520
|
+
|
|
29521
|
+
var un = el('section', 'smap-plan-unassigned');
|
|
29522
|
+
un.appendChild(el('h3', null, t.unassigned + ' (' + s.plan.unassigned.length + ')'));
|
|
29523
|
+
var ulu = el('ul', 'smap-plan-unassigned-list');
|
|
29524
|
+
s.plan.unassigned.forEach(function (id) {
|
|
29525
|
+
var li = el('li', 'smap-plan-job');
|
|
29526
|
+
draggable(li, id);
|
|
29527
|
+
li.appendChild(el('span', 'smap-plan-id', id));
|
|
29528
|
+
li.appendChild(button('smap-plan-place', t.place, function () { return editor.insert(id); }));
|
|
29529
|
+
ulu.appendChild(li);
|
|
29530
|
+
});
|
|
29531
|
+
dropZone(un, function (dragged) { return s.plan.unassigned.indexOf(dragged) >= 0 ? null : editor.remove([dragged]); });
|
|
29532
|
+
un.appendChild(ulu);
|
|
29533
|
+
root.appendChild(un);
|
|
29534
|
+
|
|
29535
|
+
if (s.choices && s.choices.length) {
|
|
29536
|
+
var ch = el('section', 'smap-plan-choices');
|
|
29537
|
+
s.choices.forEach(function (item) {
|
|
29538
|
+
ch.appendChild(el('h3', null, fill(t.choicesFor, { job: item.job })));
|
|
29539
|
+
if (!item.placed && !item.choices.length) { ch.appendChild(el('p', 'smap-plan-not-placed', t.notPlaced)); }
|
|
29540
|
+
var list = el('ol', 'smap-plan-choice-list');
|
|
29541
|
+
item.choices.forEach(function (c, i) {
|
|
29542
|
+
var li = el('li', 'smap-plan-choice' + (c.feasible ? '' : ' smap-plan-infeasible'));
|
|
29543
|
+
li.appendChild(el('span', 'smap-plan-choice-where', fill(t.choice, { vehicle: c.vehicle_id, n: c.position })));
|
|
29544
|
+
li.appendChild(el('span', 'smap-plan-choice-added', fill(t.added, { distance: fmt.distance(c.added_distance_meters), time: fmt.time(c.added_duration_seconds) })));
|
|
29545
|
+
if (c.arrival) { li.appendChild(el('span', 'smap-plan-arrival', fill(t.arrival, { time: fmt.clock(c.arrival) }))); }
|
|
29546
|
+
if (!c.feasible && c.violations && c.violations.length) {
|
|
29547
|
+
li.appendChild(el('span', 'smap-plan-stop-violations', c.violations.map(ruleText).join(', ')));
|
|
29548
|
+
}
|
|
29549
|
+
var chosen = item.chosen === i;
|
|
29550
|
+
li.appendChild(button('smap-plan-choose', chosen ? t.chosen : t.choose, function () { return editor.choose(item.job, i); }, chosen));
|
|
29551
|
+
list.appendChild(li);
|
|
29552
|
+
});
|
|
29553
|
+
ch.appendChild(list);
|
|
29554
|
+
});
|
|
29555
|
+
ch.appendChild(button('smap-plan-choices-close', t.close, function () { editor.clearChoices(); }));
|
|
29556
|
+
root.appendChild(ch);
|
|
29557
|
+
}
|
|
29558
|
+
}
|
|
29559
|
+
|
|
29560
|
+
var onAny = function () { render(); };
|
|
29561
|
+
['change', 'busy', 'preview'].forEach(function (type) { editor.on(type, onAny); });
|
|
29562
|
+
var gone = function () { api.destroy(); };
|
|
29563
|
+
editor.on('destroy', gone);
|
|
29564
|
+
render();
|
|
29565
|
+
var api = {
|
|
29566
|
+
element: root,
|
|
29567
|
+
render: render,
|
|
29568
|
+
setLanguage: function (lang) { language = lang; t = textsFor(lang, o.labels); fmt = formatters(lang, t); render(); },
|
|
29569
|
+
destroy: function () {
|
|
29570
|
+
['change', 'busy', 'preview'].forEach(function (type) { editor.off(type, onAny); });
|
|
29571
|
+
editor.off('destroy', gone);
|
|
29572
|
+
if (root.parentNode) { root.parentNode.removeChild(root); }
|
|
29573
|
+
},
|
|
29574
|
+
};
|
|
29575
|
+
return api;
|
|
29576
|
+
}
|
|
29577
|
+
|
|
29578
|
+
// ------------------------------------------------------------------ the map layer (sMap)
|
|
29579
|
+
|
|
29580
|
+
/** The features drawn for the editor's plan: legs, stops, unassigned jobs and vehicle starts. */
|
|
29581
|
+
function planFeatures(editor, id, colors) {
|
|
29582
|
+
var e = editor._history[editor._at];
|
|
29583
|
+
var out = { legs: [], stops: [], unassigned: [], starts: [] };
|
|
29584
|
+
if (!e) { return out; }
|
|
29585
|
+
var jobs = {};
|
|
29586
|
+
e.jobs.forEach(function (j) { jobs[String(j.id)] = j; });
|
|
29587
|
+
var vehicles = {};
|
|
29588
|
+
editor._vehicles.forEach(function (v) { vehicles[String(v.id)] = v; });
|
|
29589
|
+
var priced = {};
|
|
29590
|
+
((e.result && e.result.routes) || []).forEach(function (r) { priced[r.vehicle_id] = r; });
|
|
29591
|
+
function feature(geometry, props) { props.smap_plan = id; return { type: 'Feature', properties: props, geometry: geometry }; }
|
|
29592
|
+
e.routes.forEach(function (r) {
|
|
29593
|
+
var v = vehicles[r.vehicle_id] || {};
|
|
29594
|
+
var color = colorOf(editor, r.vehicle_id, colors);
|
|
29595
|
+
var start = lngLat(v.start);
|
|
29596
|
+
var end = lngLat(v.end) || (v.return_to_start ? start : null);
|
|
29597
|
+
if (start) { out.starts.push(feature({ type: 'Point', coordinates: start }, { kind: 'start', vehicle_id: r.vehicle_id, position: 1, color: color })); }
|
|
29598
|
+
var p = priced[r.vehicle_id];
|
|
29599
|
+
// The answer's legs (with their lines) while the drawn order is the answered one; straight lines before.
|
|
29600
|
+
var answered = p && p.stops && p.stops.filter(function (s) { return s.type === 'job'; }).map(function (s) { return s.id; }).join('\u0000') === r.stops.join('\u0000');
|
|
29601
|
+
var prev = start;
|
|
29602
|
+
var points = r.stops.map(function (s) { return lngLat(jobs[s] && jobs[s].location); });
|
|
29603
|
+
if (answered) {
|
|
29604
|
+
var k = 0;
|
|
29605
|
+
p.stops.forEach(function (st) {
|
|
29606
|
+
if (st.type === 'start') { return; }
|
|
29607
|
+
var position = st.type === 'job' ? (k += 1) : r.stops.length + 1;
|
|
29608
|
+
var to = st.type === 'job' ? points[position - 1] : end;
|
|
29609
|
+
var line = st.leg_geometry ? decodePolyline(st.leg_geometry) : (prev && to ? [prev, to] : null);
|
|
29610
|
+
if (line && line.length > 1) { out.legs.push(feature({ type: 'LineString', coordinates: line }, { kind: 'leg', vehicle_id: r.vehicle_id, position: position, color: color })); }
|
|
29611
|
+
if (to) { prev = to; }
|
|
29612
|
+
});
|
|
29613
|
+
} else {
|
|
29614
|
+
points.concat(end ? [end] : []).forEach(function (to, i) {
|
|
29615
|
+
if (prev && to) { out.legs.push(feature({ type: 'LineString', coordinates: [prev, to] }, { kind: 'leg', vehicle_id: r.vehicle_id, position: i + 1, color: color })); }
|
|
29616
|
+
if (to) { prev = to; }
|
|
29617
|
+
});
|
|
29618
|
+
}
|
|
29619
|
+
var stopInfo = {};
|
|
29620
|
+
((p && p.stops) || []).forEach(function (st) { if (st.type === 'job') { stopInfo[st.id] = st; } });
|
|
29621
|
+
r.stops.forEach(function (s, i) {
|
|
29622
|
+
if (!points[i]) { return; }
|
|
29623
|
+
var st = answered ? stopInfo[s] : null;
|
|
29624
|
+
var violated = !!(st && ((st.violations && st.violations.length) || st.reachable === false));
|
|
29625
|
+
var locked = editor.isLocked(s);
|
|
29626
|
+
out.stops.push(feature({ type: 'Point', coordinates: points[i] }, {
|
|
29627
|
+
kind: 'stop', id: s, vehicle_id: r.vehicle_id, position: i + 1, label: String(i + 1), color: color,
|
|
29628
|
+
border: violated ? '#b91c1c' : (locked ? '#111827' : color), locked: locked, violated: violated,
|
|
29629
|
+
}));
|
|
29630
|
+
});
|
|
29631
|
+
});
|
|
29632
|
+
unassignedOf(e.jobs, e.routes).forEach(function (s) {
|
|
29633
|
+
var at = lngLat(jobs[s] && jobs[s].location);
|
|
29634
|
+
if (at) { out.unassigned.push(feature({ type: 'Point', coordinates: at }, { kind: 'unassigned', id: s, color: '#6b7280' })); }
|
|
29635
|
+
});
|
|
29636
|
+
return out;
|
|
29637
|
+
}
|
|
29638
|
+
|
|
29639
|
+
// Where a drop at these features goes: before a stop, into a leg (before the stop it leads to), or first after a start.
|
|
29640
|
+
function dropTarget(hits, dragged) {
|
|
29641
|
+
for (var i = 0; i < hits.length; i++) {
|
|
29642
|
+
var p = hits[i].properties || {};
|
|
29643
|
+
if (p.kind === 'stop' && p.id !== dragged) { return { vehicle_id: p.vehicle_id, position: Number(p.position) }; }
|
|
29644
|
+
}
|
|
29645
|
+
for (var k = 0; k < hits.length; k++) {
|
|
29646
|
+
var q = hits[k].properties || {};
|
|
29647
|
+
if (q.kind === 'leg' || q.kind === 'start') { return { vehicle_id: q.vehicle_id, position: Number(q.position) }; }
|
|
29648
|
+
}
|
|
29649
|
+
return null;
|
|
29650
|
+
}
|
|
29651
|
+
|
|
29652
|
+
var DRAG_PX = 4;
|
|
29653
|
+
var PREVIEW_MS = 250;
|
|
29654
|
+
|
|
29655
|
+
// ------------------------------------------------------------------ parts for other map libraries
|
|
29656
|
+
//
|
|
29657
|
+
// What a binding on another map library (the tool adapters, a page of your own) needs besides the editor: the
|
|
29658
|
+
// features to draw, where a drop lands, a drag with its preview, the panel and its stylesheet.
|
|
29659
|
+
|
|
29660
|
+
/** The routes after dropping stopId at `target` ({ vehicle_id, position } as dropTarget gives it); null: no change. */
|
|
29661
|
+
function dropRoutes(editor, stopId, target) {
|
|
29662
|
+
var e = editor._history[editor._at];
|
|
29663
|
+
if (!e || !target) { return null; }
|
|
29664
|
+
var id = String(stopId);
|
|
29665
|
+
var edit = dropEdit(e.routes, id, String(target.vehicle_id), Number(target.position));
|
|
29666
|
+
if (!edit) { return null; }
|
|
29667
|
+
return edit.kind === 'resequence' ? applyResequence(e.routes, edit.vehicle_id, edit.stops) : applyMove(e.routes, [id], edit.vehicle_id, edit.position);
|
|
29668
|
+
}
|
|
29669
|
+
|
|
29670
|
+
function segmentDistance(p, a, b) {
|
|
29671
|
+
var dx = b.x - a.x;
|
|
29672
|
+
var dy = b.y - a.y;
|
|
29673
|
+
var l = dx * dx + dy * dy;
|
|
29674
|
+
var t = l ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l)) : 0;
|
|
29675
|
+
var x = a.x + t * dx - p.x;
|
|
29676
|
+
var y = a.y + t * dy - p.y;
|
|
29677
|
+
return Math.sqrt(x * x + y * y);
|
|
29678
|
+
}
|
|
29679
|
+
|
|
29680
|
+
/**
|
|
29681
|
+
* The features within `tolerance` pixels of a screen point, nearest first, for libraries that cannot tell which
|
|
29682
|
+
* drawn feature is under the pointer. `features`: planFeatures' answer or a list; `project([lng, lat])` gives
|
|
29683
|
+
* { x, y } in the same pixels as `point`.
|
|
29684
|
+
* @param {Object} [o] tolerance (default 12 px for points, 8 px for lines)
|
|
29685
|
+
*/
|
|
29686
|
+
function featuresAt(features, project, point, o) {
|
|
29687
|
+
o = o || {};
|
|
29688
|
+
var list = Array.isArray(features) ? features : [].concat(features.stops || [], features.unassigned || [], features.starts || [], features.legs || []);
|
|
29689
|
+
var hits = [];
|
|
29690
|
+
list.forEach(function (f, order) {
|
|
29691
|
+
var g = f && f.geometry;
|
|
29692
|
+
if (!g) { return; }
|
|
29693
|
+
var d = Infinity;
|
|
29694
|
+
if (g.type === 'Point') {
|
|
29695
|
+
var q = project(g.coordinates);
|
|
29696
|
+
d = Math.sqrt((q.x - point.x) * (q.x - point.x) + (q.y - point.y) * (q.y - point.y));
|
|
29697
|
+
if (d > (o.tolerance === undefined ? 12 : o.tolerance)) { return; }
|
|
29698
|
+
} else if (g.type === 'LineString') {
|
|
29699
|
+
var pts = g.coordinates.map(project);
|
|
29700
|
+
for (var i = 1; i < pts.length; i++) { d = Math.min(d, segmentDistance(point, pts[i - 1], pts[i])); }
|
|
29701
|
+
if (d > (o.tolerance === undefined ? 8 : o.tolerance)) { return; }
|
|
29702
|
+
} else { return; }
|
|
29703
|
+
hits.push({ f: f, d: d, order: order });
|
|
29704
|
+
});
|
|
29705
|
+
hits.sort(function (a, b) { return a.d - b.d || a.order - b.order; });
|
|
29706
|
+
return hits.map(function (h) { return h.f; });
|
|
29707
|
+
}
|
|
29708
|
+
|
|
29709
|
+
/**
|
|
29710
|
+
* A stop dragged on a map: start(stopId, point?) when the pointer goes down on a stop or an unassigned job,
|
|
29711
|
+
* move(hits, point?) with the features under the pointer (it becomes a drag past `threshold` pixels, and the plan
|
|
29712
|
+
* with the stop at the target is priced as a preview after `previewMs`), end(hits?) drops it (editor.dropStop; the
|
|
29713
|
+
* last target when no hits are given), cancel() gives it up.
|
|
29714
|
+
* @param {Object} [o] preview (default true), previewMs (250), threshold (4 px)
|
|
29715
|
+
*/
|
|
29716
|
+
function planDrag(editor, o) {
|
|
29717
|
+
o = o || {};
|
|
29718
|
+
var preview = o.preview !== false;
|
|
29719
|
+
var threshold = o.threshold === undefined ? DRAG_PX : o.threshold;
|
|
29720
|
+
var previewMs = o.previewMs === undefined ? PREVIEW_MS : o.previewMs;
|
|
29721
|
+
var drag = null;
|
|
29722
|
+
var timer = null;
|
|
29723
|
+
function stopTimer() { if (timer) { clearTimeout(timer); timer = null; } }
|
|
29724
|
+
return {
|
|
29725
|
+
start: function (stopId, point) {
|
|
29726
|
+
stopTimer();
|
|
29727
|
+
drag = { id: String(stopId), start: point || null, moved: false, target: null, key: null };
|
|
29728
|
+
},
|
|
29729
|
+
move: function (hits, point) {
|
|
29730
|
+
if (!drag) { return null; }
|
|
29731
|
+
if (!drag.moved && point && drag.start && Math.abs(point.x - drag.start.x) + Math.abs(point.y - drag.start.y) < threshold) { return null; }
|
|
29732
|
+
drag.moved = true;
|
|
29733
|
+
var target = dropTarget(hits || [], drag.id);
|
|
29734
|
+
var key = target ? target.vehicle_id + '#' + target.position : null;
|
|
29735
|
+
if (key === drag.key) { return target; }
|
|
29736
|
+
drag.key = key;
|
|
29737
|
+
drag.target = target;
|
|
29738
|
+
stopTimer();
|
|
29739
|
+
if (!preview) { return target; }
|
|
29740
|
+
var routes = dropRoutes(editor, drag.id, target);
|
|
29741
|
+
if (!routes) { editor.clearPreview(); return target; }
|
|
29742
|
+
timer = setTimeout(function () { timer = null; editor.preview(routes); }, previewMs);
|
|
29743
|
+
return target;
|
|
29744
|
+
},
|
|
29745
|
+
end: function (hits) {
|
|
29746
|
+
if (!drag) { return Promise.resolve(null); }
|
|
29747
|
+
var d = drag;
|
|
29748
|
+
drag = null;
|
|
29749
|
+
stopTimer();
|
|
29750
|
+
if (!d.moved) { return Promise.resolve(null); }
|
|
29751
|
+
var target = hits ? dropTarget(hits, d.id) : d.target;
|
|
29752
|
+
editor.clearPreview();
|
|
29753
|
+
if (!target) { return Promise.resolve(null); }
|
|
29754
|
+
return editor.dropStop(d.id, target.vehicle_id, target.position).catch(function () { return null; });
|
|
29755
|
+
},
|
|
29756
|
+
cancel: function () {
|
|
29757
|
+
var d = drag;
|
|
29758
|
+
drag = null;
|
|
29759
|
+
stopTimer();
|
|
29760
|
+
if (d && d.moved) { editor.clearPreview(); }
|
|
29761
|
+
},
|
|
29762
|
+
dragging: function () { return drag ? drag.id : null; },
|
|
29763
|
+
moved: function () { return !!(drag && drag.moved); },
|
|
29764
|
+
target: function () { return drag ? drag.target : null; },
|
|
29765
|
+
};
|
|
29766
|
+
}
|
|
29767
|
+
|
|
29768
|
+
// The panel's stylesheet (the .smap-plan-* rules of smap.css), filled in by js/build.mjs.
|
|
29769
|
+
var PANEL_CSS = "/* Plan editor panel (OPEN-ROUTE-EDIT E10). Restyle through these classes; every colour is set here. */\n.smap-plan-panel {\n width: 320px; max-width: calc(100vw - 20px); max-height: 70vmin; overflow-y: auto; padding: 10px; box-sizing: border-box;\n border-radius: 8px; background: #ffffff; color: #1e293b; box-shadow: 0 2px 10px rgba(15, 23, 42, 0.18);\n font: 12px/1.35 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n}\n.smap-plan-panel h3 { margin: 10px 0 4px; font-size: 13px; font-weight: 600; }\n.smap-plan-panel h4 { margin: 6px 0 2px; font-size: 12px; font-weight: 600; }\n.smap-plan-bar { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }\n.smap-plan-button {\n min-height: 26px; padding: 3px 8px; border: 1px solid rgba(15, 23, 42, 0.18); border-radius: 6px; background: #ffffff;\n color: inherit; font: inherit; cursor: pointer;\n}\n.smap-plan-button:disabled { opacity: 0.45; cursor: default; }\n.smap-plan-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 1px; }\n.smap-plan-status { margin-left: auto; color: #2563eb; }\n.smap-plan-error { display: flex; align-items: center; gap: 6px; margin-top: 8px; padding: 6px 8px; border-radius: 6px; background: #fef2f2; color: #991b1b; }\n.smap-plan-error span { flex: 1 1 auto; }\n.smap-plan-note { margin-top: 8px; color: #475569; }\n.smap-plan-totals { display: flex; flex-wrap: wrap; gap: 2px 12px; margin: 2px 0 4px; }\n.smap-plan-totals div { display: flex; gap: 4px; }\n.smap-plan-totals dt { color: #64748b; }\n.smap-plan-totals dd { margin: 0; font-weight: 600; }\n.smap-plan-violations dd, .smap-plan-unreachable dd, .smap-plan-route-violations, .smap-plan-stop-violations { color: #b91c1c; }\n.smap-plan-preview { margin-top: 4px; padding: 4px 6px; border-radius: 6px; background: #eff6ff; }\n.smap-plan-pending { margin: 2px 0; color: #64748b; }\n.smap-plan-route-head { display: flex; align-items: center; gap: 6px; }\n.smap-plan-swatch { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }\n.smap-plan-route-violations { margin: 2px 0; padding-left: 16px; }\n.smap-plan-stops-list, .smap-plan-unassigned-list, .smap-plan-choice-list { margin: 0; padding: 0; list-style: none; min-height: 8px; }\n.smap-plan-stop, .smap-plan-job, .smap-plan-choice {\n display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; padding: 3px 4px; border-radius: 4px;\n}\n.smap-plan-stop, .smap-plan-job { flex-wrap: nowrap; }\n.smap-plan-stop .smap-plan-id, .smap-plan-job .smap-plan-id { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.smap-plan-stop .smap-plan-arrival { white-space: nowrap; }\n.smap-plan-stop .smap-plan-stop-violations { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }\n.smap-plan-stop, .smap-plan-job { cursor: grab; }\n.smap-plan-stop:hover, .smap-plan-job:hover { background: #f1f5f9; }\n.smap-plan-over { outline: 2px dashed #2563eb; outline-offset: -2px; }\n.smap-plan-seq { min-width: 18px; color: #64748b; text-align: right; }\n.smap-plan-id { font-weight: 600; }\n.smap-plan-arrival { color: #475569; }\n.smap-plan-stop .smap-plan-button, .smap-plan-job .smap-plan-button, .smap-plan-choice .smap-plan-button { min-height: 22px; padding: 1px 6px; }\n.smap-plan-stop .smap-plan-lock { margin-left: auto; }\n.smap-plan-job .smap-plan-place, .smap-plan-choice .smap-plan-choose { margin-left: auto; }\n.smap-plan-locked .smap-plan-id::after { content: \" \\1F512\"; font-weight: 400; }\n.smap-plan-empty { color: #94a3b8; cursor: default; }\n.smap-plan-infeasible .smap-plan-choice-where { color: #b91c1c; }\n.smap-plan-choices { margin-top: 8px; padding-top: 4px; border-top: 1px solid rgba(15, 23, 42, 0.12); }\n.smap-plan-choices-close { margin-top: 6px; }\n.smap-style-dark .smap-plan-panel, .smap-style-black .smap-plan-panel { background: #1f2430; color: #e5e7eb; }\n.smap-style-dark .smap-plan-button, .smap-style-black .smap-plan-button { background: #1f2430; border-color: rgba(229, 231, 235, 0.2); }\n.smap-style-dark .smap-plan-stop:hover, .smap-style-black .smap-plan-stop:hover,\n.smap-style-dark .smap-plan-job:hover, .smap-style-black .smap-plan-job:hover { background: #2b3242; }\n";
|
|
29770
|
+
|
|
29771
|
+
/** Adds the panel's stylesheet to a document once (pages that load smap.css have it already). */
|
|
29772
|
+
function injectCss(doc) {
|
|
29773
|
+
if (!doc || !doc.head || PANEL_CSS.indexOf('__SMAP') === 0 || doc.getElementById('smap-plan-css')) { return; }
|
|
29774
|
+
var style = doc.createElement('style');
|
|
29775
|
+
style.id = 'smap-plan-css';
|
|
29776
|
+
style.textContent = PANEL_CSS;
|
|
29777
|
+
doc.head.appendChild(style);
|
|
29778
|
+
}
|
|
29779
|
+
|
|
29780
|
+
/**
|
|
29781
|
+
* Draws the editor's plan on an sMap map as layers `<id>:legs`, `<id>:starts`, `<id>:stops`, `<id>:unassigned`, and
|
|
29782
|
+
* lets a stop (or an unassigned job) be dragged before another stop or onto a route's line.
|
|
29783
|
+
* @returns {{ remove(): void, features(): Object }}
|
|
29784
|
+
*/
|
|
29785
|
+
function attachToMap(map, editor, o) {
|
|
29786
|
+
o = o || {};
|
|
29787
|
+
var id = o.id || 'plan';
|
|
29788
|
+
var ids = { legs: id + ':legs', starts: id + ':starts', stops: id + ':stops', unassigned: id + ':unassigned', drag: id + ':drag' };
|
|
29789
|
+
var added = false;
|
|
29790
|
+
var removed = false;
|
|
29791
|
+
var drag = planDrag(editor, { preview: o.preview !== false });
|
|
29792
|
+
|
|
29793
|
+
function draw() {
|
|
29794
|
+
if (removed) { return; }
|
|
29795
|
+
var f = planFeatures(editor, id, o.colors);
|
|
29796
|
+
var fc = function (list) { return { type: 'FeatureCollection', features: list }; };
|
|
29797
|
+
if (!added) {
|
|
29798
|
+
map.addLines(ids.legs, fc(f.legs), { color: { property: 'color' }, width: 4, casing: true, arrows: true, zIndex: o.zIndex });
|
|
29799
|
+
map.addPoints(ids.starts, fc(f.starts), { marker: { shape: 'circle', fill: { property: 'color' }, border: '#ffffff', size: 18, text: '', overlap: false } });
|
|
29800
|
+
map.addPoints(ids.unassigned, fc(f.unassigned), { marker: { shape: 'circle', fill: '#9ca3af', border: '#ffffff', size: 18, text: '', overlap: false } });
|
|
29801
|
+
map.addPoints(ids.stops, fc(f.stops), { marker: { shape: 'pin', fill: { property: 'color' }, border: { property: 'border' }, text: { property: 'label' }, overlap: false } });
|
|
29802
|
+
map.addPoints(ids.drag, fc([]), { marker: { shape: 'circle', fill: '#111827', border: '#ffffff', size: 16, text: '', overlap: false } });
|
|
29803
|
+
added = true;
|
|
29804
|
+
} else {
|
|
29805
|
+
map.setData(ids.legs, fc(f.legs));
|
|
29806
|
+
map.setData(ids.starts, fc(f.starts));
|
|
29807
|
+
map.setData(ids.unassigned, fc(f.unassigned));
|
|
29808
|
+
map.setData(ids.stops, fc(f.stops));
|
|
29809
|
+
}
|
|
29810
|
+
}
|
|
29811
|
+
|
|
29812
|
+
function engine() { return map.engine; }
|
|
29813
|
+
function hitsAt(point) {
|
|
29814
|
+
var eng = engine();
|
|
29815
|
+
if (!eng || typeof eng.queryRenderedFeatures !== 'function') { return []; }
|
|
29816
|
+
var box = [[point.x - 6, point.y - 6], [point.x + 6, point.y + 6]];
|
|
29817
|
+
return (eng.queryRenderedFeatures(box) || []).filter(function (f) { return f && f.properties && f.properties.smap_plan === id; });
|
|
29818
|
+
}
|
|
29819
|
+
function setPan(on) {
|
|
29820
|
+
var eng = engine();
|
|
29821
|
+
if (eng && eng.dragPan) { if (on) { eng.dragPan.enable(); } else { eng.dragPan.disable(); } }
|
|
29822
|
+
}
|
|
29823
|
+
|
|
29824
|
+
function down(e) {
|
|
29825
|
+
if (removed || (e.originalEvent && e.originalEvent.button) || !e.point) { return; }
|
|
29826
|
+
var hit = hitsAt(e.point).filter(function (f) { return f.properties.kind === 'stop' || f.properties.kind === 'unassigned'; })[0];
|
|
29827
|
+
if (!hit) { return; }
|
|
29828
|
+
drag.start(hit.properties.id, e.point);
|
|
29829
|
+
setPan(false);
|
|
29830
|
+
if (e.preventDefault) { e.preventDefault(); }
|
|
29831
|
+
}
|
|
29832
|
+
function move(e) {
|
|
29833
|
+
if (!drag.dragging() || !e.point) { return; }
|
|
29834
|
+
drag.move(hitsAt(e.point), e.point);
|
|
29835
|
+
if (drag.moved() && e.lngLat) { map.setData(ids.drag, [{ lng: e.lngLat.lng, lat: e.lngLat.lat }]); }
|
|
29836
|
+
}
|
|
29837
|
+
function up(e) {
|
|
29838
|
+
if (!drag.dragging()) { return; }
|
|
29839
|
+
setPan(true);
|
|
29840
|
+
map.setData(ids.drag, []);
|
|
29841
|
+
drag.end(e && e.point ? hitsAt(e.point) : null);
|
|
29842
|
+
}
|
|
29843
|
+
|
|
29844
|
+
var listening = false;
|
|
29845
|
+
function listen() {
|
|
29846
|
+
var eng = engine();
|
|
29847
|
+
if (!eng || listening || removed) { return; }
|
|
29848
|
+
listening = true;
|
|
29849
|
+
eng.on('mousedown', down);
|
|
29850
|
+
eng.on('mousemove', move);
|
|
29851
|
+
eng.on('mouseup', up);
|
|
29852
|
+
eng.on('touchstart', down);
|
|
29853
|
+
eng.on('touchmove', move);
|
|
29854
|
+
eng.on('touchend', up);
|
|
29855
|
+
}
|
|
29856
|
+
function onChange() { if (ready) { draw(); } }
|
|
29857
|
+
editor.on('change', onChange);
|
|
29858
|
+
var gone = function () { api.remove(); };
|
|
29859
|
+
editor.on('destroy', gone);
|
|
29860
|
+
var ready = false;
|
|
29861
|
+
var started = map.ready && typeof map.ready.then === 'function' ? map.ready : Promise.resolve(map);
|
|
29862
|
+
started.then(function () { ready = true; if (!removed) { draw(); listen(); } });
|
|
29863
|
+
|
|
29864
|
+
var api = {
|
|
29865
|
+
features: function () { return planFeatures(editor, id, o.colors); },
|
|
29866
|
+
remove: function () {
|
|
29867
|
+
if (removed) { return; }
|
|
29868
|
+
removed = true;
|
|
29869
|
+
drag.cancel();
|
|
29870
|
+
editor.off('change', onChange);
|
|
29871
|
+
editor.off('destroy', gone);
|
|
29872
|
+
var eng = engine();
|
|
29873
|
+
if (eng && listening) {
|
|
29874
|
+
eng.off('mousedown', down); eng.off('mousemove', move); eng.off('mouseup', up);
|
|
29875
|
+
eng.off('touchstart', down); eng.off('touchmove', move); eng.off('touchend', up);
|
|
29876
|
+
setPan(true);
|
|
29877
|
+
}
|
|
29878
|
+
if (added) { Object.keys(ids).forEach(function (k) { try { map.removeLayer(ids[k]); } catch (x) { /* already gone */ } }); }
|
|
29879
|
+
},
|
|
29880
|
+
// For tests and adapters: a drag as the map reports it.
|
|
29881
|
+
_down: down, _move: move, _up: up,
|
|
29882
|
+
};
|
|
29883
|
+
return api;
|
|
29884
|
+
}
|
|
29885
|
+
|
|
29886
|
+
/**
|
|
29887
|
+
* An editor on an sMap map: its layers, and the panel as the map tool `plan-editor` (unless `panel: false`).
|
|
29888
|
+
* options as PlanEditor, plus id (layer prefix), colors, preview (default true), panel (true | false |
|
|
29889
|
+
* { position, label, labels, className }), language (default the map's).
|
|
29890
|
+
*/
|
|
29891
|
+
function attach(map, options) {
|
|
29892
|
+
options = options || {};
|
|
29893
|
+
var editor = new PlanEditor(Object.assign({}, options, { plan: null }));
|
|
29894
|
+
var layer = attachToMap(map, editor, options);
|
|
29895
|
+
var toolId = null;
|
|
29896
|
+
var destroyed = false;
|
|
29897
|
+
editor.panel = null;
|
|
29898
|
+
editor.layer = layer;
|
|
29899
|
+
// The panel is made from the map's own page (the engine's container), so it waits for the map's engine.
|
|
29900
|
+
function mountPanel() {
|
|
29901
|
+
if (destroyed || options.panel === false) { return; }
|
|
29902
|
+
var po = typeof options.panel === 'object' && options.panel ? options.panel : {};
|
|
29903
|
+
var container = map.engine && map.engine.getContainer ? map.engine.getContainer() : null;
|
|
29904
|
+
var doc = options.document || (container && container.ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
29905
|
+
if (!doc) { return; }
|
|
29906
|
+
var language = options.language || map._language || 'en';
|
|
29907
|
+
var labels = po.labels || options.labels;
|
|
29908
|
+
editor.panel = createPanel(editor, { language: language, labels: labels, document: doc, colors: options.colors, className: po.className });
|
|
29909
|
+
toolId = po.id || ((options.id || 'plan') + '-editor');
|
|
29910
|
+
map.addTool(toolId, editor.panel.element, { label: po.label || textsFor(language, labels).editor, position: po.position || 'top-left' });
|
|
29911
|
+
}
|
|
29912
|
+
if (map.engine || options.document) { mountPanel(); }
|
|
29913
|
+
else if (map.ready && typeof map.ready.then === 'function') { map.ready.then(mountPanel, function () {}); }
|
|
29914
|
+
var destroy = editor.destroy;
|
|
29915
|
+
editor.destroy = function () {
|
|
29916
|
+
destroyed = true;
|
|
29917
|
+
if (toolId) { try { map.removeTool(toolId); } catch (x) { /* map removed */ } }
|
|
29918
|
+
destroy.call(editor);
|
|
29919
|
+
};
|
|
29920
|
+
if (options.plan) { editor.setPlan(options.plan).catch(function () {}); }
|
|
29921
|
+
return editor;
|
|
29922
|
+
}
|
|
29923
|
+
|
|
29924
|
+
return {
|
|
29925
|
+
PlanEditor: PlanEditor,
|
|
29926
|
+
attach: attach,
|
|
29927
|
+
attachToMap: attachToMap,
|
|
29928
|
+
createPanel: createPanel,
|
|
29929
|
+
createTransport: createTransport,
|
|
29930
|
+
planFeatures: planFeatures,
|
|
29931
|
+
dropEdit: dropEdit,
|
|
29932
|
+
applyResequence: applyResequence,
|
|
29933
|
+
applyMove: applyMove,
|
|
29934
|
+
applyRemove: applyRemove,
|
|
29935
|
+
decodePolyline: decodePolyline,
|
|
29936
|
+
texts: TEXTS,
|
|
29937
|
+
textKeys: TEXT_KEYS,
|
|
29938
|
+
languages: LANGUAGES,
|
|
29939
|
+
panelLanguage: panelLanguage,
|
|
29940
|
+
numberLocale: numberLocale,
|
|
29941
|
+
formatters: formatters,
|
|
29942
|
+
// The parts a binding on another map library builds on (exported as planEditorParts).
|
|
29943
|
+
parts: {
|
|
29944
|
+
planFeatures: planFeatures, dropTarget: dropTarget, dropEdit: dropEdit, dropRoutes: dropRoutes,
|
|
29945
|
+
featuresAt: featuresAt, planDrag: planDrag, createPanel: createPanel, injectCss: injectCss, textsFor: textsFor,
|
|
29946
|
+
panelLanguage: panelLanguage, decodePolyline: decodePolyline, colors: COLORS.slice(),
|
|
29947
|
+
},
|
|
29948
|
+
};
|
|
29949
|
+
});
|
|
29950
|
+
|
|
29951
|
+
return module.exports;
|
|
29952
|
+
}
|
|
29953
|
+
function smapCreateSdk(engine, interop, plan) {
|
|
28332
29954
|
var module = { exports: {} };
|
|
28333
29955
|
var exports = module.exports;
|
|
28334
29956
|
var define;
|
|
28335
29957
|
var require = function (name) { throw new Error('sMap: unexpected require of ' + name); };
|
|
28336
|
-
var self = { maplibregl: engine, smapInterop: interop };
|
|
29958
|
+
var self = { maplibregl: engine, smapInterop: interop, smapPlan: plan };
|
|
28337
29959
|
/*!
|
|
28338
29960
|
* sMap Web SDK — maps from Superroute's sMap service.
|
|
28339
29961
|
*
|
|
@@ -28346,14 +29968,15 @@ function smapCreateSdk(engine, interop) {
|
|
|
28346
29968
|
(function (root, factory) {
|
|
28347
29969
|
if (typeof module === 'object' && module.exports) {
|
|
28348
29970
|
module.exports = factory(root.maplibregl !== undefined ? root.maplibregl : require('maplibre-gl'),
|
|
28349
|
-
root.smapInterop !== undefined ? root.smapInterop : require('./smap-interop.js')
|
|
29971
|
+
root.smapInterop !== undefined ? root.smapInterop : require('./smap-interop.js'),
|
|
29972
|
+
root.smapPlan !== undefined ? root.smapPlan : (typeof require === 'function' ? require('./smap-plan.js') : null));
|
|
28350
29973
|
} else {
|
|
28351
|
-
root.sMap = factory(root.maplibregl, root.smapInterop);
|
|
29974
|
+
root.sMap = factory(root.maplibregl, root.smapInterop, root.smapPlan);
|
|
28352
29975
|
}
|
|
28353
|
-
})(typeof self !== 'undefined' ? self : this, function (engine, interop) {
|
|
29976
|
+
})(typeof self !== 'undefined' ? self : this, function (engine, interop, plan) {
|
|
28354
29977
|
'use strict';
|
|
28355
29978
|
|
|
28356
|
-
var VERSION = '1.1.
|
|
29979
|
+
var VERSION = '1.1.6';
|
|
28357
29980
|
var FLAVORS = ['light', 'white', 'grayscale', 'dark', 'black'];
|
|
28358
29981
|
var BASEMAP_SOURCE = 'basemap';
|
|
28359
29982
|
var BUILDINGS_LAYER = 'smap-buildings-3d';
|
|
@@ -28806,11 +30429,22 @@ function smapCreateSdk(engine, interop) {
|
|
|
28806
30429
|
// True between a style's load and the next style change. The engine's own isStyleLoaded() turns false while any
|
|
28807
30430
|
// source is loading data, which would leave layers added meanwhile waiting for a load that never comes.
|
|
28808
30431
|
this._styleReady = false;
|
|
30432
|
+
// Map tools (M49-M57): the controls and panels floating on the map that the caller registered, in the order added.
|
|
30433
|
+
var toolOptions = options.tools || {};
|
|
30434
|
+
this._tools = [];
|
|
30435
|
+
this._toolsHidden = {};
|
|
30436
|
+
(toolOptions.hidden || []).forEach(function (id) { this._toolsHidden[id] = true; }, this);
|
|
30437
|
+
// 1.1.6: by default the tools control is the last button of the map's toolbar (the corner holding the
|
|
30438
|
+
// navigation control, top left without one); a corner given by the caller is kept as in 1.1.5.
|
|
30439
|
+
this._toolsControlAt = toolOptions.control === undefined || toolOptions.control === true ? 'toolbar' : toolOptions.control;
|
|
30440
|
+
this._toolsLabels = toolOptions.labels || {};
|
|
30441
|
+
this._toolsControl = null;
|
|
30442
|
+
this._toolHandlers = [];
|
|
28809
30443
|
|
|
28810
30444
|
var self = this;
|
|
28811
30445
|
var engineOptions = {};
|
|
28812
30446
|
for (var k in options) {
|
|
28813
|
-
if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D'].indexOf(k) < 0) {
|
|
30447
|
+
if (Object.prototype.hasOwnProperty.call(options, k) && ['token', 'getToken', 'apiKey', 'style', 'language', 'baseUrl', 'projection', 'nearest', 'nodes', 'popup', 'popupLoading', 'autoZoom', 'keyboard3D', 'tools'].indexOf(k) < 0) {
|
|
28814
30448
|
engineOptions[k] = options[k];
|
|
28815
30449
|
}
|
|
28816
30450
|
}
|
|
@@ -28843,7 +30477,8 @@ function smapCreateSdk(engine, interop) {
|
|
|
28843
30477
|
self.engine.addControl(badge, 'bottom-right');
|
|
28844
30478
|
badge._place();
|
|
28845
30479
|
}
|
|
28846
|
-
self.
|
|
30480
|
+
self._mountTools();
|
|
30481
|
+
self.engine.on('style.load', function () { self._styleReady = true; self._restore(); self._hideLayerTools(); });
|
|
28847
30482
|
self._keyboard3D();
|
|
28848
30483
|
// One click handler for every layer's popup: the engine's hit test over those layers, the top feature opens.
|
|
28849
30484
|
self.engine.on('click', function (e) { self._popupClick(e); });
|
|
@@ -29281,6 +30916,7 @@ function smapCreateSdk(engine, interop) {
|
|
|
29281
30916
|
if (!def) { return this; }
|
|
29282
30917
|
def.hidden = !visible;
|
|
29283
30918
|
var m = this.engine;
|
|
30919
|
+
if (!m) { return this; }
|
|
29284
30920
|
def.engineLayers.forEach(function (l) { if (m.getLayer(l)) { m.setLayoutProperty(l, 'visibility', visible ? 'visible' : 'none'); } });
|
|
29285
30921
|
return this;
|
|
29286
30922
|
};
|
|
@@ -29326,6 +30962,365 @@ function smapCreateSdk(engine, interop) {
|
|
|
29326
30962
|
SMap.prototype.flyTo = function (o) { this.engine.flyTo(o); return this; };
|
|
29327
30963
|
SMap.prototype.remove = function () { clearTimeout(this._refreshTimer); if (this.engine) { this.engine.remove(); } };
|
|
29328
30964
|
|
|
30965
|
+
// ------------------------------------------------------------------ map tools (M49-M57)
|
|
30966
|
+
|
|
30967
|
+
var TOOL_ID = /^[A-Za-z0-9._-]{1,64}$/;
|
|
30968
|
+
var TOOL_CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
30969
|
+
// The tools the SDK makes by name: the engine control, its English label (the caller may give another) and corner.
|
|
30970
|
+
var TOOL_KINDS = {
|
|
30971
|
+
navigation: { control: 'NavigationControl', label: 'Zoom', position: 'top-left' },
|
|
30972
|
+
geolocate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
|
|
30973
|
+
fullscreen: { control: 'FullscreenControl', label: 'Full screen', position: 'top-left' },
|
|
30974
|
+
scale: { control: 'ScaleControl', label: 'Scale', position: 'bottom-left' },
|
|
30975
|
+
// The mobile SDKs' names for the same controls, so one tool list serves every platform.
|
|
30976
|
+
zoom: { control: 'NavigationControl', label: 'Zoom', position: 'top-left', options: { showCompass: false } },
|
|
30977
|
+
compass: { control: 'NavigationControl', label: 'Compass', position: 'top-left', options: { showZoom: false } },
|
|
30978
|
+
locate: { control: 'GeolocateControl', label: 'Locate', position: 'top-left' },
|
|
30979
|
+
};
|
|
30980
|
+
var TOOLS_ICON = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false" fill="none" ' +
|
|
30981
|
+
'stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h10M18 7h2M4 17h2M10 17h10"/>' +
|
|
30982
|
+
'<circle cx="16" cy="7" r="2"/><circle cx="8" cy="17" r="2"/></svg>';
|
|
30983
|
+
var TOOLS_TEXT = { button: 'Map tools', showAll: 'Show all', hideAll: 'Hide all' };
|
|
30984
|
+
|
|
30985
|
+
function isElement(v) { return !!v && typeof v === 'object' && v.nodeType === 1; }
|
|
30986
|
+
|
|
30987
|
+
// An engine control whose element the SDK can find again to show or hide it: the control's own onAdd, its element kept.
|
|
30988
|
+
function toolControl(tool, inner) {
|
|
30989
|
+
return {
|
|
30990
|
+
onAdd: function (m) { tool.el = inner.onAdd(m); return tool.el; },
|
|
30991
|
+
onRemove: function (m) { if (inner.onRemove) { inner.onRemove(m); } else if (tool.el && tool.el.parentNode) { tool.el.parentNode.removeChild(tool.el); } tool.el = null; },
|
|
30992
|
+
getDefaultPosition: function () { return tool.position; },
|
|
30993
|
+
};
|
|
30994
|
+
}
|
|
30995
|
+
|
|
30996
|
+
// A page element placed in a corner of the map: it moves into the corner and back where it was when removed.
|
|
30997
|
+
function elementControl(tool, element) {
|
|
30998
|
+
var home = null;
|
|
30999
|
+
var next = null;
|
|
31000
|
+
return {
|
|
31001
|
+
onAdd: function () {
|
|
31002
|
+
home = element.parentNode;
|
|
31003
|
+
next = element.nextSibling;
|
|
31004
|
+
element.classList.add('maplibregl-ctrl', 'smap-tool');
|
|
31005
|
+
tool.el = element;
|
|
31006
|
+
return element;
|
|
31007
|
+
},
|
|
31008
|
+
onRemove: function () {
|
|
31009
|
+
element.classList.remove('maplibregl-ctrl', 'smap-tool');
|
|
31010
|
+
if (home) { home.insertBefore(element, next && next.parentNode === home ? next : null); } else if (element.parentNode) { element.parentNode.removeChild(element); }
|
|
31011
|
+
tool.el = null;
|
|
31012
|
+
},
|
|
31013
|
+
getDefaultPosition: function () { return tool.position; },
|
|
31014
|
+
};
|
|
31015
|
+
}
|
|
31016
|
+
|
|
31017
|
+
/**
|
|
31018
|
+
* Registers a tool floating on the map, shown or hidden by setToolVisible / setToolsVisible and the tools control.
|
|
31019
|
+
* map.addTool('legend', document.getElementById('legend'), { label: 'Legend', position: 'bottom-left' })
|
|
31020
|
+
* map.addTool('zoom', 'navigation', { options: { showCompass: false } })
|
|
31021
|
+
* @param {string} id 1-64 letters, digits, '-', '_', '.'; adding an id again replaces the first
|
|
31022
|
+
* map.addTool('stops', { layer: ['stops', 'stop-labels'] }, { label: 'Stops' })
|
|
31023
|
+
* @param {Element|Object|string} target a page element, an engine control (onAdd / onRemove), a kind the SDK
|
|
31024
|
+
* makes: 'navigation', 'geolocate', 'fullscreen', 'scale', or the mobile SDKs' 'zoom' (navigation without the
|
|
31025
|
+
* compass), 'compass' (without the zoom buttons) and 'locate' (geolocate); or data layers, `{ layer: id }` or
|
|
31026
|
+
* `{ layer: [ids] }`: layers of addPoints / addLines / addPolygons / addData / addStyleLayer and the external
|
|
31027
|
+
* layers, or of the basemap. Hiding the tool hides the layers (layout visibility), showing it shows them; a
|
|
31028
|
+
* layer added later under one of the ids follows a hidden tool, and an id without a layer is left alone.
|
|
31029
|
+
* @param {Object} [o] label (shown on its button), position ('top-left', 'top-right', 'bottom-left', 'bottom-right',
|
|
31030
|
+
* or 'none': an element stays where the page put it), options (of a kind's engine control)
|
|
31031
|
+
*/
|
|
31032
|
+
SMap.prototype.addTool = function (id, target, o) {
|
|
31033
|
+
o = o || {};
|
|
31034
|
+
if (typeof id !== 'string' || !TOOL_ID.test(id)) { throw new Error('sMap: a tool id is 1-64 letters, digits, "-", "_" or "."'); }
|
|
31035
|
+
var kind = typeof target === 'string' ? TOOL_KINDS[target] : null;
|
|
31036
|
+
var layers = layerTarget(target);
|
|
31037
|
+
if (layers) {
|
|
31038
|
+
if (o.position !== undefined && o.position !== 'none') { throw new Error('sMap: a data-layer tool is not placed in a corner'); }
|
|
31039
|
+
if (this._toolIndex(id) >= 0) { this.removeTool(id); }
|
|
31040
|
+
var layerTool = { id: id, label: String(o.label || id), position: 'none', el: null, control: null, layers: layers };
|
|
31041
|
+
this._tools.push(layerTool);
|
|
31042
|
+
if (!this._toolVisible(layerTool)) { this._applyLayerTool(layerTool, false); }
|
|
31043
|
+
if (this.engine) { this._mountTool(layerTool); }
|
|
31044
|
+
this._renderToolsControl();
|
|
31045
|
+
return this;
|
|
31046
|
+
}
|
|
31047
|
+
if (typeof target === 'string') {
|
|
31048
|
+
if (!kind) { throw new Error('sMap: unknown tool kind "' + target + '"'); }
|
|
31049
|
+
if (!engine[kind.control]) { throw new Error('sMap: the rendering engine has no ' + kind.control); }
|
|
31050
|
+
} else if (!isElement(target) && !(target && typeof target.onAdd === 'function')) {
|
|
31051
|
+
throw new Error('sMap: a tool is an element, an engine control or a tool kind');
|
|
31052
|
+
}
|
|
31053
|
+
var position = o.position || (kind ? kind.position : (isElement(target) ? (target.parentNode ? 'none' : 'top-left') : 'top-right'));
|
|
31054
|
+
if (position !== 'none' && TOOL_CORNERS.indexOf(position) < 0) { throw new Error('sMap: unknown tool position "' + position + '"'); }
|
|
31055
|
+
if (position === 'none' && !isElement(target)) { throw new Error('sMap: only an element may stay out of the map\'s corners'); }
|
|
31056
|
+
if (this._toolIndex(id) >= 0) { this.removeTool(id); }
|
|
31057
|
+
var tool = { id: id, label: String(o.label || (kind ? kind.label : id)), position: position, el: null, control: null };
|
|
31058
|
+
if (kind) { tool.control = toolControl(tool, new engine[kind.control](o.options || kind.options)); }
|
|
31059
|
+
else if (!isElement(target)) { tool.control = toolControl(tool, target); }
|
|
31060
|
+
else if (position !== 'none') { tool.control = elementControl(tool, target); }
|
|
31061
|
+
else { tool.el = target; }
|
|
31062
|
+
this._tools.push(tool);
|
|
31063
|
+
if (this.engine) { this._mountTool(tool); }
|
|
31064
|
+
this._renderToolsControl();
|
|
31065
|
+
return this;
|
|
31066
|
+
};
|
|
31067
|
+
|
|
31068
|
+
/** Removes a tool: an engine control leaves the map, a page element is shown again where the page put it. */
|
|
31069
|
+
SMap.prototype.removeTool = function (id) {
|
|
31070
|
+
var i = this._toolIndex(id);
|
|
31071
|
+
if (i < 0) { return this; }
|
|
31072
|
+
var tool = this._tools[i];
|
|
31073
|
+
this._tools.splice(i, 1);
|
|
31074
|
+
if (tool.el) { tool.el.classList.remove('smap-tool-hidden'); }
|
|
31075
|
+
// A data-layer tool leaves its layers shown, as a removed element is shown again.
|
|
31076
|
+
if (tool.layers && !this._toolVisible(tool)) { this._applyLayerTool(tool, true); }
|
|
31077
|
+
if (tool.control && this.engine && tool.mounted) { this.engine.removeControl(tool.control); }
|
|
31078
|
+
this._renderToolsControl();
|
|
31079
|
+
return this;
|
|
31080
|
+
};
|
|
31081
|
+
|
|
31082
|
+
/** Shows or hides one tool. A hidden tool keeps its state and shows again as it was (M57). */
|
|
31083
|
+
SMap.prototype.setToolVisible = function (id, visible) {
|
|
31084
|
+
var tool = this._tools[this._toolIndex(id)];
|
|
31085
|
+
if (!tool || this._toolVisible(tool) === !!visible) { return this; }
|
|
31086
|
+
this._setHidden(tool, !visible);
|
|
31087
|
+
this._renderToolsControl();
|
|
31088
|
+
this._toolsChanged(id);
|
|
31089
|
+
return this;
|
|
31090
|
+
};
|
|
31091
|
+
|
|
31092
|
+
/** Shows or hides every tool. The map data credit, the sMap badge and the tools control always stay (M51). */
|
|
31093
|
+
SMap.prototype.setToolsVisible = function (visible) {
|
|
31094
|
+
var changed = false;
|
|
31095
|
+
this._tools.forEach(function (tool) {
|
|
31096
|
+
if (this._toolVisible(tool) !== !!visible) { this._setHidden(tool, !visible); changed = true; }
|
|
31097
|
+
}, this);
|
|
31098
|
+
if (changed) { this._renderToolsControl(); this._toolsChanged(null); }
|
|
31099
|
+
return this;
|
|
31100
|
+
};
|
|
31101
|
+
|
|
31102
|
+
/** The tools in the order added: [{ id, label, visible }]. */
|
|
31103
|
+
SMap.prototype.tools = function () {
|
|
31104
|
+
return this._tools.map(function (tool) { return { id: tool.id, label: tool.label, visible: this._toolVisible(tool) }; }, this);
|
|
31105
|
+
};
|
|
31106
|
+
|
|
31107
|
+
/** Calls handler({ hidden: [ids], changed: id | null }) after every change; the SDK keeps no state itself (M53). */
|
|
31108
|
+
SMap.prototype.onToolsChange = function (handler) { this._toolHandlers.push(handler); return this; };
|
|
31109
|
+
SMap.prototype.offToolsChange = function (handler) {
|
|
31110
|
+
this._toolHandlers = this._toolHandlers.filter(function (h) { return h !== handler; });
|
|
31111
|
+
return this;
|
|
31112
|
+
};
|
|
31113
|
+
|
|
31114
|
+
SMap.prototype._toolIndex = function (id) {
|
|
31115
|
+
for (var i = 0; i < this._tools.length; i++) { if (this._tools[i].id === id) { return i; } }
|
|
31116
|
+
return -1;
|
|
31117
|
+
};
|
|
31118
|
+
SMap.prototype._toolVisible = function (tool) { return !this._toolsHidden[tool.id]; };
|
|
31119
|
+
SMap.prototype._setHidden = function (tool, hidden) {
|
|
31120
|
+
if (hidden) { this._toolsHidden[tool.id] = true; } else { delete this._toolsHidden[tool.id]; }
|
|
31121
|
+
if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', hidden); }
|
|
31122
|
+
if (tool.layers) { this._applyLayerTool(tool, !hidden); }
|
|
31123
|
+
};
|
|
31124
|
+
|
|
31125
|
+
// { layer: 'id' } or { layer: ['id', ...] }: the layer ids of a data-layer tool, or null for any other target.
|
|
31126
|
+
function layerTarget(target) {
|
|
31127
|
+
if (!target || typeof target !== 'object' || isElement(target) || typeof target.onAdd === 'function' || !('layer' in target)) { return null; }
|
|
31128
|
+
var ids = Array.isArray(target.layer) ? target.layer : [target.layer];
|
|
31129
|
+
if (!ids.length || ids.some(function (x) { return typeof x !== 'string' || !x; })) { throw new Error('sMap: a data-layer tool names one or more layer ids'); }
|
|
31130
|
+
return ids.filter(function (x, i) { return ids.indexOf(x) === i; });
|
|
31131
|
+
}
|
|
31132
|
+
|
|
31133
|
+
// Shows or hides a data-layer tool's layers: the SDK's own through setVisible, any other (the basemap's) through its
|
|
31134
|
+
// layout visibility. A layer that is not there (removed, or not added yet) is left alone.
|
|
31135
|
+
SMap.prototype._applyLayerTool = function (tool, visible) {
|
|
31136
|
+
tool.layers.forEach(function (layerId) {
|
|
31137
|
+
if (this._layers[layerId]) { this.setVisible(layerId, visible); return; }
|
|
31138
|
+
var m = this.engine;
|
|
31139
|
+
if (m && m.getLayer && m.getLayer(layerId)) { this.setLayout(layerId, 'visibility', visible ? 'visible' : 'none'); }
|
|
31140
|
+
}, this);
|
|
31141
|
+
};
|
|
31142
|
+
|
|
31143
|
+
// After a style loads: the basemap layers of hidden data-layer tools are hidden (the SDK's own layers already are).
|
|
31144
|
+
SMap.prototype._hideLayerTools = function () {
|
|
31145
|
+
this._tools.forEach(function (t) { if (t.layers && !this._toolVisible(t)) { this._applyLayerTool(t, false); } }, this);
|
|
31146
|
+
};
|
|
31147
|
+
|
|
31148
|
+
// Is a layer id covered by a hidden data-layer tool? A layer added later under that id starts hidden.
|
|
31149
|
+
SMap.prototype._layerToolHides = function (layerId) {
|
|
31150
|
+
return this._tools.some(function (t) { return t.layers && t.layers.indexOf(layerId) >= 0 && !this._toolVisible(t); }, this);
|
|
31151
|
+
};
|
|
31152
|
+
SMap.prototype._toolsChanged = function (id) {
|
|
31153
|
+
var hidden = this._tools.filter(function (t) { return !this._toolVisible(t); }, this).map(function (t) { return t.id; });
|
|
31154
|
+
this._toolHandlers.slice().forEach(function (h) { h({ hidden: hidden, changed: id }); });
|
|
31155
|
+
};
|
|
31156
|
+
|
|
31157
|
+
SMap.prototype._mountTool = function (tool) {
|
|
31158
|
+
// The tools control joins the map with its first tool, so a map without tools is exactly as before.
|
|
31159
|
+
if (this._toolsControlAt && !this._toolsControl && this.engine.addControl) {
|
|
31160
|
+
this._toolsControl = toolsControl(this);
|
|
31161
|
+
this.engine.addControl(this._toolsControl, this._toolsControlAt === 'toolbar' ? 'top-left' : this._toolsControlAt);
|
|
31162
|
+
if (this._toolsControlAt === 'toolbar') { this._toolsControl._follow(); }
|
|
31163
|
+
}
|
|
31164
|
+
if (tool.control && this.engine.addControl) { this.engine.addControl(tool.control, tool.position); tool.mounted = true; }
|
|
31165
|
+
if (tool.el) { tool.el.classList.toggle('smap-tool-hidden', !this._toolVisible(tool)); }
|
|
31166
|
+
if (this._toolsControl) { this._toolsControl._place(); }
|
|
31167
|
+
};
|
|
31168
|
+
|
|
31169
|
+
// Called once the engine exists: every tool registered before it.
|
|
31170
|
+
SMap.prototype._mountTools = function () {
|
|
31171
|
+
this._tools.forEach(function (tool) { this._mountTool(tool); }, this);
|
|
31172
|
+
this._renderToolsControl();
|
|
31173
|
+
};
|
|
31174
|
+
SMap.prototype._renderToolsControl = function () { if (this._toolsControl) { this._toolsControl._render(); } };
|
|
31175
|
+
|
|
31176
|
+
// The tools control: one button that opens a panel with one toggle per tool (pressed = shown), Show all and Hide all.
|
|
31177
|
+
// It is not a tool and is never hidden; it joins the map with the first tool and is not drawn while none is left.
|
|
31178
|
+
function toolsControl(map) {
|
|
31179
|
+
var el = null;
|
|
31180
|
+
var button = null;
|
|
31181
|
+
var panel = null;
|
|
31182
|
+
var drawn = null;
|
|
31183
|
+
var text = {};
|
|
31184
|
+
Object.keys(TOOLS_TEXT).forEach(function (k) { text[k] = map._toolsLabels[k] || TOOLS_TEXT[k]; });
|
|
31185
|
+
var observer = null;
|
|
31186
|
+
var openCorner = null;
|
|
31187
|
+
function open(on) {
|
|
31188
|
+
panel.hidden = !on;
|
|
31189
|
+
button.setAttribute('aria-expanded', on ? 'true' : 'false');
|
|
31190
|
+
// While the panel is open its corner is drawn above the map's other corners.
|
|
31191
|
+
if (openCorner) { openCorner.classList.remove('smap-tools-open'); openCorner = null; }
|
|
31192
|
+
if (on && el.parentNode) { openCorner = el.parentNode; openCorner.classList.add('smap-tools-open'); }
|
|
31193
|
+
}
|
|
31194
|
+
function cornerOf(node, root) {
|
|
31195
|
+
for (; node && node !== root; node = node.parentNode) {
|
|
31196
|
+
if (node.nodeType === 1 && /\bmaplibregl-ctrl-(top|bottom)-(left|right)\b/.test(node.className)) { return node; }
|
|
31197
|
+
}
|
|
31198
|
+
return null;
|
|
31199
|
+
}
|
|
31200
|
+
// The map's toolbar: the corner holding a navigation control (zoom buttons or compass), top left without one.
|
|
31201
|
+
function toolbarCorner(root) {
|
|
31202
|
+
var navs = root.querySelectorAll('.maplibregl-ctrl-zoom-in, .maplibregl-ctrl-zoom-out, .maplibregl-ctrl-compass');
|
|
31203
|
+
for (var i = 0; i < navs.length; i++) {
|
|
31204
|
+
if (el.contains(navs[i])) { continue; }
|
|
31205
|
+
var c = cornerOf(navs[i], root);
|
|
31206
|
+
if (c) { return c; }
|
|
31207
|
+
}
|
|
31208
|
+
return root.querySelector('.maplibregl-ctrl-top-left');
|
|
31209
|
+
}
|
|
31210
|
+
// Where the control goes in a corner: last, but in a bottom corner before the attribution and the sMap badge,
|
|
31211
|
+
// which keep the map's edge.
|
|
31212
|
+
function edgeOf(corner) {
|
|
31213
|
+
if (!/\bmaplibregl-ctrl-bottom-/.test(corner.className)) { return null; }
|
|
31214
|
+
for (var c = corner.firstElementChild; c; c = c.nextElementSibling) {
|
|
31215
|
+
if (c !== el && (c.classList.contains('maplibregl-ctrl-attrib') || c.classList.contains('smap-badge'))) { return c; }
|
|
31216
|
+
}
|
|
31217
|
+
return null;
|
|
31218
|
+
}
|
|
31219
|
+
function close() { if (panel && !panel.hidden) { open(false); } }
|
|
31220
|
+
function key(e) { if (e.key === 'Escape' && !panel.hidden) { open(false); button.focus(); } }
|
|
31221
|
+
function make(doc, tag, className, label) {
|
|
31222
|
+
var b = doc.createElement(tag);
|
|
31223
|
+
b.className = className;
|
|
31224
|
+
if (tag === 'button') { b.type = 'button'; }
|
|
31225
|
+
if (label !== undefined) { b.textContent = label; }
|
|
31226
|
+
return b;
|
|
31227
|
+
}
|
|
31228
|
+
var control = {
|
|
31229
|
+
onAdd: function (m) {
|
|
31230
|
+
var doc = m.getContainer().ownerDocument;
|
|
31231
|
+
el = make(doc, 'div', 'maplibregl-ctrl maplibregl-ctrl-group smap-tools');
|
|
31232
|
+
button = make(doc, 'button', 'smap-tools-button');
|
|
31233
|
+
button.innerHTML = TOOLS_ICON;
|
|
31234
|
+
button.title = text.button;
|
|
31235
|
+
button.setAttribute('aria-label', text.button);
|
|
31236
|
+
button.setAttribute('aria-haspopup', 'true');
|
|
31237
|
+
button.addEventListener('click', function () { open(panel.hidden); });
|
|
31238
|
+
panel = make(doc, 'div', 'smap-tools-panel');
|
|
31239
|
+
panel.setAttribute('role', 'group');
|
|
31240
|
+
panel.setAttribute('aria-label', text.button);
|
|
31241
|
+
el.appendChild(button);
|
|
31242
|
+
el.appendChild(panel);
|
|
31243
|
+
el.addEventListener('keydown', key);
|
|
31244
|
+
open(false);
|
|
31245
|
+
m.on('click', close);
|
|
31246
|
+
return el;
|
|
31247
|
+
},
|
|
31248
|
+
onRemove: function (m) {
|
|
31249
|
+
m.off('click', close);
|
|
31250
|
+
if (observer) { observer.disconnect(); observer = null; }
|
|
31251
|
+
if (openCorner) { openCorner.classList.remove('smap-tools-open'); openCorner = null; }
|
|
31252
|
+
if (el && el.parentNode) { el.parentNode.removeChild(el); }
|
|
31253
|
+
el = null;
|
|
31254
|
+
drawn = null;
|
|
31255
|
+
},
|
|
31256
|
+
// The default place (1.1.6): the last button of the map's toolbar, kept last when controls join it later.
|
|
31257
|
+
_place: function () {
|
|
31258
|
+
if (!el || map._toolsControlAt !== 'toolbar' || !map.engine || !map.engine.getContainer) { return; }
|
|
31259
|
+
var root = map.engine.getContainer();
|
|
31260
|
+
var corner = toolbarCorner(root);
|
|
31261
|
+
if (!corner) { return; }
|
|
31262
|
+
var edge = edgeOf(corner);
|
|
31263
|
+
var inPlace = el.parentNode === corner && (edge ? el.nextElementSibling === edge : !el.nextElementSibling);
|
|
31264
|
+
if (inPlace) { return; }
|
|
31265
|
+
corner.insertBefore(el, edge);
|
|
31266
|
+
if (openCorner && openCorner !== corner) { openCorner.classList.remove('smap-tools-open'); openCorner = corner; corner.classList.add('smap-tools-open'); }
|
|
31267
|
+
},
|
|
31268
|
+
// Watches the map's corners, so a control the app adds later (to the engine itself too) leaves this one last.
|
|
31269
|
+
_follow: function () {
|
|
31270
|
+
if (observer || !el || !map.engine.getContainer) { return; }
|
|
31271
|
+
var root = map.engine.getContainer();
|
|
31272
|
+
var view = root.ownerDocument && root.ownerDocument.defaultView;
|
|
31273
|
+
var Observer = view && view.MutationObserver;
|
|
31274
|
+
if (!Observer) { return; }
|
|
31275
|
+
var watched = root.querySelector('.maplibregl-control-container') || root;
|
|
31276
|
+
observer = new Observer(function () { control._place(); });
|
|
31277
|
+
observer.observe(watched, { childList: true, subtree: true });
|
|
31278
|
+
},
|
|
31279
|
+
_render: function () {
|
|
31280
|
+
if (!el) { return; }
|
|
31281
|
+
var doc = el.ownerDocument;
|
|
31282
|
+
el.classList.toggle('smap-tools-empty', map._tools.length === 0);
|
|
31283
|
+
// The same tools as last time: only the pressed states change, so a toggle keeps the keyboard focus.
|
|
31284
|
+
var signature = JSON.stringify(map._tools.map(function (t) { return [t.id, t.label]; }));
|
|
31285
|
+
if (signature === drawn) {
|
|
31286
|
+
map._tools.forEach(function (tool, i) { panel.children[i].setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false'); });
|
|
31287
|
+
return;
|
|
31288
|
+
}
|
|
31289
|
+
drawn = signature;
|
|
31290
|
+
while (panel.firstChild) { panel.removeChild(panel.firstChild); }
|
|
31291
|
+
map._tools.forEach(function (tool) {
|
|
31292
|
+
var item = make(doc, 'button', 'smap-tools-item', tool.label);
|
|
31293
|
+
item.setAttribute('aria-pressed', map._toolVisible(tool) ? 'true' : 'false');
|
|
31294
|
+
item.setAttribute('data-tool', tool.id);
|
|
31295
|
+
item.addEventListener('click', function () { map.setToolVisible(tool.id, !map._toolVisible(tool)); });
|
|
31296
|
+
panel.appendChild(item);
|
|
31297
|
+
});
|
|
31298
|
+
var all = make(doc, 'div', 'smap-tools-all');
|
|
31299
|
+
var show = make(doc, 'button', 'smap-tools-show-all', text.showAll);
|
|
31300
|
+
show.addEventListener('click', function () { map.setToolsVisible(true); });
|
|
31301
|
+
var hide = make(doc, 'button', 'smap-tools-hide-all', text.hideAll);
|
|
31302
|
+
hide.addEventListener('click', function () { map.setToolsVisible(false); });
|
|
31303
|
+
all.appendChild(show);
|
|
31304
|
+
all.appendChild(hide);
|
|
31305
|
+
panel.appendChild(all);
|
|
31306
|
+
},
|
|
31307
|
+
};
|
|
31308
|
+
return control;
|
|
31309
|
+
}
|
|
31310
|
+
|
|
31311
|
+
// ------------------------------------------------------------------ plan editor (OPEN-ROUTE-EDIT E10)
|
|
31312
|
+
|
|
31313
|
+
/**
|
|
31314
|
+
* A plan editor on this map: the plan drawn as layers, stops dragged within and between routes, and the ready-made
|
|
31315
|
+
* panel as the map tool `<id>-editor` (unless `panel: false`). Every edit is a plan.* call of the Open Platform.
|
|
31316
|
+
* const editor = map.planEditor({ credentials: { token: () => fetch('/plan-token').then((r) => r.json()) }, plan });
|
|
31317
|
+
* See PlanEditorOptions in the types for every option.
|
|
31318
|
+
*/
|
|
31319
|
+
SMap.prototype.planEditor = function (options) {
|
|
31320
|
+
if (!plan) { throw new Error('sMap: the plan editor is not loaded'); }
|
|
31321
|
+
return plan.attach(this, options);
|
|
31322
|
+
};
|
|
31323
|
+
|
|
29329
31324
|
// ------------------------------------------------------------------ Style Specification sources and layers (M24)
|
|
29330
31325
|
|
|
29331
31326
|
function clone(v) { return v === undefined ? undefined : JSON.parse(JSON.stringify(v)); }
|
|
@@ -29893,6 +31888,7 @@ function smapCreateSdk(engine, interop) {
|
|
|
29893
31888
|
def.popupFn = typeof o.popup === 'string' ? popupTemplate(o.popup) : null;
|
|
29894
31889
|
def.zIndex = def.o.zIndex || 0;
|
|
29895
31890
|
def.engineLayers = [];
|
|
31891
|
+
if (this._layerToolHides(id)) { def.hidden = true; }
|
|
29896
31892
|
this._layers[id] = def;
|
|
29897
31893
|
this._order = this._order.filter(function (x) { return x !== id; });
|
|
29898
31894
|
this._order.push(id);
|
|
@@ -30273,14 +32269,18 @@ function smapCreateSdk(engine, interop) {
|
|
|
30273
32269
|
parseWmtsCapabilities: interop.parseWmtsCapabilities,
|
|
30274
32270
|
PMTilesArchive: interop.PMTilesArchive,
|
|
30275
32271
|
useDeck: interop.useDeck,
|
|
32272
|
+
PlanEditor: plan ? plan.PlanEditor : undefined,
|
|
32273
|
+
planTexts: plan ? plan.texts : undefined,
|
|
32274
|
+
planEditorParts: plan ? plan.parts : undefined,
|
|
30276
32275
|
};
|
|
30277
32276
|
});
|
|
30278
32277
|
|
|
30279
32278
|
return module.exports;
|
|
30280
32279
|
}
|
|
30281
32280
|
var smapInterop = smapCreateInterop();
|
|
32281
|
+
var smapPlan = smapCreatePlan();
|
|
30282
32282
|
function smapLoad() {
|
|
30283
|
-
if (!smapCore) { smapCore = smapCreateSdk(smapCreateEngine(), smapInterop); }
|
|
32283
|
+
if (!smapCore) { smapCore = smapCreateSdk(smapCreateEngine(), smapInterop, smapPlan); }
|
|
30284
32284
|
return smapCore;
|
|
30285
32285
|
}
|
|
30286
32286
|
// Every top-level name starts with smap: the engine and the SDK run inside this scope and must keep seeing the
|
|
@@ -30294,14 +32294,14 @@ function SMapMap(options) {
|
|
|
30294
32294
|
SMapMap.prototype = core.Map.prototype;
|
|
30295
32295
|
return new core.Map(options);
|
|
30296
32296
|
}
|
|
30297
|
-
var smapVersion = "1.1.
|
|
32297
|
+
var smapVersion = "1.1.6";
|
|
30298
32298
|
var smapStyles = ['light', 'white', 'grayscale', 'dark', 'black'];
|
|
30299
32299
|
/** The bundled rendering engine, for anything the SDK does not wrap (loaded on the first call). */
|
|
30300
32300
|
function smapGetEngine() { return smapLoad().engine; }
|
|
30301
32301
|
// Templates need no engine: before the first map they come from an SDK made without one.
|
|
30302
32302
|
var smapNoEngine = null;
|
|
30303
32303
|
function smapPopupTemplate(template) {
|
|
30304
|
-
if (!smapCore && !smapNoEngine) { smapNoEngine = smapCreateSdk(null, smapInterop); }
|
|
32304
|
+
if (!smapCore && !smapNoEngine) { smapNoEngine = smapCreateSdk(null, smapInterop, smapPlan); }
|
|
30305
32305
|
return (smapCore || smapNoEngine).popupTemplate(template);
|
|
30306
32306
|
}
|
|
30307
32307
|
var smapSlots = ['bottom', 'roads', 'labels', 'top'];
|
|
@@ -30316,8 +32316,12 @@ var smap_slotAnchors = smapInterop.slotAnchors;
|
|
|
30316
32316
|
var smap_parseWmtsCapabilities = smapInterop.parseWmtsCapabilities;
|
|
30317
32317
|
var smap_PMTilesArchive = smapInterop.PMTilesArchive;
|
|
30318
32318
|
var smap_useDeck = smapInterop.useDeck;
|
|
32319
|
+
var smapPlanEditor = smapPlan.PlanEditor;
|
|
32320
|
+
var smapPlanTexts = smapPlan.texts;
|
|
32321
|
+
var smapPlanEditorParts = smapPlan.parts;
|
|
30319
32322
|
var smapDefault = { Map: SMapMap, version: smapVersion, styles: smapStyles, getEngine: smapGetEngine, popupTemplate: smapPopupTemplate,
|
|
30320
|
-
slots: smapSlots, formats: smap_formats, convert: smap_convert, rasterUrl: smap_rasterUrl, rasterTileJsonUrl: smap_rasterTileJsonUrl, styleUrl: smap_styleUrl, applyBasemapOverrides: smap_applyBasemapOverrides, basemapClasses: smap_basemapClasses, slotAnchors: smap_slotAnchors, parseWmtsCapabilities: smap_parseWmtsCapabilities, PMTilesArchive: smap_PMTilesArchive, useDeck: smap_useDeck
|
|
32323
|
+
slots: smapSlots, formats: smap_formats, convert: smap_convert, rasterUrl: smap_rasterUrl, rasterTileJsonUrl: smap_rasterTileJsonUrl, styleUrl: smap_styleUrl, applyBasemapOverrides: smap_applyBasemapOverrides, basemapClasses: smap_basemapClasses, slotAnchors: smap_slotAnchors, parseWmtsCapabilities: smap_parseWmtsCapabilities, PMTilesArchive: smap_PMTilesArchive, useDeck: smap_useDeck, PlanEditor: smapPlanEditor, planTexts: smapPlanTexts,
|
|
32324
|
+
planEditorParts: smapPlanEditorParts };
|
|
30321
32325
|
exports.Map = SMapMap;
|
|
30322
32326
|
exports.version = smapVersion;
|
|
30323
32327
|
exports.styles = smapStyles;
|
|
@@ -30335,4 +32339,7 @@ exports.slotAnchors = smap_slotAnchors;
|
|
|
30335
32339
|
exports.parseWmtsCapabilities = smap_parseWmtsCapabilities;
|
|
30336
32340
|
exports.PMTilesArchive = smap_PMTilesArchive;
|
|
30337
32341
|
exports.useDeck = smap_useDeck;
|
|
32342
|
+
exports.PlanEditor = smapPlanEditor;
|
|
32343
|
+
exports.planTexts = smapPlanTexts;
|
|
32344
|
+
exports.planEditorParts = smapPlanEditorParts;
|
|
30338
32345
|
exports.default = smapDefault;
|