@fleetbase/fleetops-engine 0.6.44 → 0.6.46
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/addon/components/admin/map-settings.hbs +26 -0
- package/addon/components/admin/map-settings.js +46 -0
- package/addon/components/admin/routing-settings.hbs +11 -0
- package/addon/components/cell/order-route-type.hbs +46 -0
- package/addon/components/cell/order-route-type.js +90 -0
- package/addon/components/cell/place-address.hbs +1 -0
- package/addon/components/display-place.hbs +45 -56
- package/addon/components/display-place.js +21 -0
- package/addon/components/entity/form.hbs +24 -0
- package/addon/components/entity/form.js +30 -0
- package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/leaflet-draw-control.js +1 -1
- package/addon/components/maintenance-schedule/form.js +2 -1
- package/addon/components/map/drawer/driver-listing.js +5 -5
- package/addon/components/map/drawer/geofence-event-listing.js +5 -2
- package/addon/components/map/drawer/place-listing.js +5 -5
- package/addon/components/map/drawer/position-listing.js +102 -21
- package/addon/components/map/drawer/vehicle-listing.js +5 -5
- package/addon/components/map/google-live-map.hbs +9 -0
- package/addon/components/map/google-live-map.js +445 -0
- package/addon/components/map/leaflet-live-map.hbs +186 -142
- package/addon/components/map/leaflet-live-map.js +121 -41
- package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
- package/addon/components/map/toolbar/zones-panel.hbs +9 -9
- package/addon/components/map/toolbar.hbs +3 -3
- package/addon/components/map/toolbar.js +9 -1
- package/addon/components/modals/map-layer-form.hbs +132 -63
- package/addon/components/modals/orchestrator-import.hbs +4 -11
- package/addon/components/modals/orchestrator-import.js +17 -1
- package/addon/components/modals/update-order-activity.hbs +1 -1
- package/addon/components/orchestrator/order-pool.js +3 -2
- package/addon/components/orchestrator-workbench.js +29 -41
- package/addon/components/order/details/detail.js +2 -2
- package/addon/components/order/details/payload.hbs +5 -5
- package/addon/components/order/details/payload.js +18 -1
- package/addon/components/order/details/route.hbs +2 -2
- package/addon/components/order/details/route.js +76 -3
- package/addon/components/order/form/payload.hbs +33 -0
- package/addon/components/order/form/payload.js +1 -0
- package/addon/components/order/form/route.hbs +8 -8
- package/addon/components/order/form/route.js +78 -18
- package/addon/components/order/form/service-rate.hbs +1 -1
- package/addon/components/order/form.js +2 -2
- package/addon/components/order/route-editor.hbs +209 -147
- package/addon/components/order/route-editor.js +143 -102
- package/addon/components/place/address.hbs +1 -0
- package/addon/components/place/address.js +12 -0
- package/addon/components/route-list.hbs +43 -55
- package/addon/components/route-list.js +47 -0
- package/addon/components/route-optimization-wizard-panel.js +3 -4
- package/addon/components/service-rate/details.hbs +6 -6
- package/addon/components/service-rate/form.hbs +28 -15
- package/addon/components/vehicle/card.hbs +4 -4
- package/addon/controllers/maintenance/schedules/index.js +2 -1
- package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
- package/addon/controllers/operations/orders/index/details.js +113 -15
- package/addon/controllers/operations/orders/index/new.js +1 -1
- package/addon/controllers/operations/orders/index.js +8 -1
- package/addon/controllers/operations/service-rates/index/edit.js +1 -0
- package/addon/controllers/operations/service-rates/index/new.js +1 -0
- package/addon/controllers/settings/map.js +97 -0
- package/addon/controllers/settings/routing.js +47 -12
- package/addon/extension.js +11 -0
- package/addon/helpers/f-to-int.js +6 -0
- package/addon/helpers/place-address-html.js +6 -0
- package/addon/instance-initializers/register-google-routes.js +17 -0
- package/addon/instance-initializers/register-osrm.js +5 -0
- package/addon/routes/application.js +9 -3
- package/addon/routes/operations/orchestrator.js +13 -0
- package/addon/routes/operations/orders/index/details/virtual.js +1 -0
- package/addon/routes/operations/orders/index/details.js +47 -24
- package/addon/routes/operations/orders/index/new.js +9 -0
- package/addon/routes/operations/orders/index.js +0 -1
- package/addon/routes/settings/map.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/entity-actions.js +1 -1
- package/addon/services/equipment-actions.js +4 -6
- package/addon/services/geofence-event-bus.js +2 -1
- package/addon/services/geofence.js +204 -168
- package/addon/services/google-routes.js +300 -0
- package/addon/services/leaflet-draw-restriction.js +2 -1
- package/addon/services/leaflet-map-manager.js +21 -0
- package/addon/services/maintenance-actions.js +4 -6
- package/addon/services/map-adapter/google.js +2205 -0
- package/addon/services/map-adapter/leaflet.js +1042 -0
- package/addon/services/map-adapter-interface.js +551 -0
- package/addon/services/map-manager.js +768 -0
- package/addon/services/map-settings.js +75 -0
- package/addon/services/movement-tracker.js +83 -32
- package/addon/services/order-actions.js +11 -1
- package/addon/services/osrm.js +79 -6
- package/addon/services/part-actions.js +4 -6
- package/addon/services/position-playback.js +135 -62
- package/addon/services/route-engine.js +83 -0
- package/addon/services/service-areas.js +8 -1
- package/addon/services/service-rate-actions.js +8 -3
- package/addon/styles/fleetops-engine.css +555 -0
- package/addon/templates/management/vehicles/index.hbs +0 -1
- package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
- package/addon/templates/operations/orders/index/details.hbs +0 -1
- package/addon/templates/settings/map.hbs +53 -0
- package/addon/templates/settings/routing.hbs +15 -4
- package/addon/utils/fleet-ops-options.js +34 -0
- package/addon/utils/has-valid-coordinates.js +6 -0
- package/addon/utils/map-provider-options.js +6 -0
- package/addon/utils/order-route-editing.js +86 -0
- package/addon/utils/order-route-summary.js +20 -0
- package/addon/utils/place-address-html.js +41 -0
- package/addon/utils/route-colors.js +43 -12
- package/addon/utils/route-visualization.js +93 -0
- package/addon/utils/serialize-payload.js +1 -1
- package/addon/utils/vendor-integration.js +2 -1
- package/app/components/admin/map-settings.js +1 -0
- package/app/components/admin/routing-settings.js +1 -0
- package/app/components/cell/order-route-type.js +1 -0
- package/app/components/cell/place-address.js +1 -0
- package/app/components/map/google-live-map.js +1 -0
- package/app/components/place/address.js +1 -0
- package/app/controllers/operations/orders/index/details/virtual.js +1 -0
- package/app/controllers/settings/map.js +1 -0
- package/app/helpers/f-to-int.js +1 -0
- package/app/helpers/place-address-html.js +1 -0
- package/app/routes/settings/map.js +1 -0
- package/app/services/map-adapter/google.js +1 -0
- package/app/services/map-adapter/leaflet.js +1 -0
- package/app/services/map-adapter-interface.js +1 -0
- package/app/services/map-manager.js +1 -0
- package/app/services/map-settings.js +1 -0
- package/app/templates/settings/map.js +1 -0
- package/app/utils/has-valid-coordinates.js +1 -0
- package/app/utils/order-route-editing.js +2 -0
- package/app/utils/order-route-summary.js +1 -0
- package/app/utils/place-address-html.js +1 -0
- package/app/utils/route-colors.js +1 -1
- package/app/utils/route-visualization.js +1 -0
- package/assets/images/drawing-controls-spritesheet.svg +156 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +36 -8
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
- package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +7 -2
- package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
- package/server/src/Http/Filter/OrderFilter.php +2 -1
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
- package/server/src/Http/Resources/v1/Order.php +20 -2
- package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
- package/server/src/Models/Entity.php +7 -0
- package/server/src/Models/Order.php +81 -8
- package/server/src/Models/OrderConfig.php +15 -0
- package/server/src/Models/Payload.php +131 -27
- package/server/src/Models/Place.php +182 -48
- package/server/src/Models/ServiceRate.php +125 -32
- package/server/src/Observers/PurchaseRateObserver.php +3 -5
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
- package/server/src/Rules/ComputableAlgo.php +1 -3
- package/server/src/Support/Algo.php +87 -1
- package/server/src/Support/Utils.php +69 -0
- package/server/src/routes.php +4 -0
- package/translations/en-us.yaml +1937 -1916
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MapManagerService
|
|
3
|
+
*
|
|
4
|
+
* Provider-agnostic central map service for FleetOps. Replaces the
|
|
5
|
+
* Leaflet-specific `leaflet-map-manager` as the single point of contact
|
|
6
|
+
* for all map operations across the application.
|
|
7
|
+
*
|
|
8
|
+
* The service resolves the correct adapter at initialization time based on
|
|
9
|
+
* the configured `mapProvider` setting (default: 'leaflet'). All method
|
|
10
|
+
* calls are delegated to the active adapter, which translates them into
|
|
11
|
+
* the underlying library's API.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* @service mapManager;
|
|
15
|
+
* this.mapManager.flyTo(lat, lng, 15);
|
|
16
|
+
*
|
|
17
|
+
* Switching providers:
|
|
18
|
+
* Set `mapProvider` in config/environment.js or via the settings API.
|
|
19
|
+
* The adapter is resolved once at `initializeMap` time.
|
|
20
|
+
*
|
|
21
|
+
* @module services/map-manager
|
|
22
|
+
*/
|
|
23
|
+
import Service from '@ember/service';
|
|
24
|
+
import { tracked } from '@glimmer/tracking';
|
|
25
|
+
import { isArray } from '@ember/array';
|
|
26
|
+
import { inject as service } from '@ember/service';
|
|
27
|
+
import { getOwner } from '@ember/application';
|
|
28
|
+
import { action } from '@ember/object';
|
|
29
|
+
import { debug } from '@ember/debug';
|
|
30
|
+
import { next } from '@ember/runloop';
|
|
31
|
+
|
|
32
|
+
export default class MapManagerService extends Service {
|
|
33
|
+
@service universe;
|
|
34
|
+
@service notifications;
|
|
35
|
+
@service mapSettings;
|
|
36
|
+
@service routeEngine;
|
|
37
|
+
@service leafletContextmenuManager;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The active map provider adapter instance.
|
|
41
|
+
* @type {MapAdapterInterface}
|
|
42
|
+
*/
|
|
43
|
+
@tracked adapter = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The name of the active provider (e.g. 'leaflet', 'google').
|
|
47
|
+
* @type {string}
|
|
48
|
+
*/
|
|
49
|
+
@tracked providerName = 'leaflet';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reference to the live map component instance.
|
|
53
|
+
* @type {*}
|
|
54
|
+
*/
|
|
55
|
+
@tracked _livemap = null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the map has been fully initialized and is ready for interaction.
|
|
59
|
+
* @type {boolean}
|
|
60
|
+
*/
|
|
61
|
+
@tracked isReady = false;
|
|
62
|
+
@tracked routeControls = new Map();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Internal deferred promise that resolves when the map is ready.
|
|
66
|
+
*/
|
|
67
|
+
#mapReadyPromise = null;
|
|
68
|
+
#resolveMapReady = null;
|
|
69
|
+
|
|
70
|
+
constructor() {
|
|
71
|
+
super(...arguments);
|
|
72
|
+
this.#resetReadyDeferred();
|
|
73
|
+
this.providerName = this.getConfiguredProvider();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Provider Resolution ───────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve and activate the adapter for the given provider name.
|
|
80
|
+
* Falls back to 'leaflet' if the requested provider is not registered.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} [provider='leaflet']
|
|
83
|
+
* @returns {MapAdapterInterface}
|
|
84
|
+
*/
|
|
85
|
+
resolveAdapter(provider = 'leaflet') {
|
|
86
|
+
const owner = getOwner(this);
|
|
87
|
+
const lookupName = `service:map-adapter/${provider}`;
|
|
88
|
+
const previousAdapter = this.adapter;
|
|
89
|
+
let adapter = owner.lookup(lookupName);
|
|
90
|
+
let resolvedProvider = provider;
|
|
91
|
+
|
|
92
|
+
if (!adapter) {
|
|
93
|
+
debug(`[MapManager] No adapter found for provider "${provider}", falling back to "leaflet".`);
|
|
94
|
+
adapter = owner.lookup('service:map-adapter/leaflet');
|
|
95
|
+
resolvedProvider = 'leaflet';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!adapter) {
|
|
99
|
+
throw new Error(`[MapManager] Could not resolve any map adapter. Ensure "service:map-adapter/leaflet" is registered.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (previousAdapter && previousAdapter !== adapter) {
|
|
103
|
+
previousAdapter.destroyMap?.();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.adapter = adapter;
|
|
107
|
+
this.providerName = resolvedProvider;
|
|
108
|
+
debug(`[MapManager] Resolved adapter: ${resolvedProvider}`);
|
|
109
|
+
return adapter;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Determine the configured provider from runtime settings.
|
|
114
|
+
*
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
getConfiguredProvider() {
|
|
118
|
+
return this.mapSettings.mapProvider ?? 'leaflet';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Initialize the map in the given DOM element.
|
|
125
|
+
* Resolves the adapter from config if not already resolved.
|
|
126
|
+
*
|
|
127
|
+
* @param {HTMLElement} element
|
|
128
|
+
* @param {Object} [options={}]
|
|
129
|
+
* @returns {*} The native map instance
|
|
130
|
+
*/
|
|
131
|
+
@action initializeMap(element, options = {}) {
|
|
132
|
+
const provider = options.provider ?? this.getConfiguredProvider();
|
|
133
|
+
|
|
134
|
+
if (!this.adapter || this.providerName !== provider) {
|
|
135
|
+
this.resolveAdapter(provider);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const map = this.adapter.initializeMap(element, {
|
|
139
|
+
...options,
|
|
140
|
+
apiKey: options.apiKey ?? this.mapSettings.googleMapsApiKey,
|
|
141
|
+
mapId: options.mapId ?? this.mapSettings.googleMapsMapId,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return Promise.resolve(map).then((nativeMap) => {
|
|
145
|
+
this.isReady = true;
|
|
146
|
+
this.#resolveMapReady?.(nativeMap);
|
|
147
|
+
debug('[MapManager] Map initialized');
|
|
148
|
+
return nativeMap;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Destroy the map and reset state.
|
|
154
|
+
*/
|
|
155
|
+
@action destroyMap() {
|
|
156
|
+
this.adapter?.destroyMap();
|
|
157
|
+
this.isReady = false;
|
|
158
|
+
this._livemap = null;
|
|
159
|
+
this.#resetReadyDeferred();
|
|
160
|
+
debug('[MapManager] Map destroyed');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Notify the adapter that the container has been resized.
|
|
165
|
+
*/
|
|
166
|
+
@action invalidateSize() {
|
|
167
|
+
this.adapter?.invalidateSize();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set the live map component reference.
|
|
172
|
+
*
|
|
173
|
+
* @param {*} livemap
|
|
174
|
+
*/
|
|
175
|
+
@action setLivemap(livemap) {
|
|
176
|
+
this._livemap = livemap;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wait until the map is fully initialized.
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} [options={}]
|
|
183
|
+
* @param {number} [options.timeoutMs=8000]
|
|
184
|
+
* @returns {Promise<*>}
|
|
185
|
+
*/
|
|
186
|
+
waitForMap({ timeoutMs = 8000 } = {}) {
|
|
187
|
+
if (this.isReady && this.adapter?._map) {
|
|
188
|
+
return Promise.resolve(this.adapter._map);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let to;
|
|
192
|
+
const p = Promise.race([
|
|
193
|
+
this.#mapReadyPromise,
|
|
194
|
+
new Promise((_, rej) => {
|
|
195
|
+
if (timeoutMs != null) {
|
|
196
|
+
to = setTimeout(() => rej(new Error('[MapManager] waitForMap timed out')), timeoutMs);
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
return p.finally(() => {
|
|
202
|
+
if (to) clearTimeout(to);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async ensureInteractive({ timeoutMs = 8000 } = {}) {
|
|
207
|
+
const map = await this.waitForMap({ timeoutMs });
|
|
208
|
+
if (typeof this.adapter?.ensureInteractive === 'function') {
|
|
209
|
+
return this.adapter.ensureInteractive({ timeoutMs, map });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return map;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Viewport ──────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
setCenter(lat, lng, zoom) {
|
|
218
|
+
return this.adapter?.setCenter(lat, lng, zoom);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
flyTo(lat, lng, zoom, options = {}) {
|
|
222
|
+
return this.adapter?.flyTo(lat, lng, zoom, options);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fitBounds(bounds, options = {}) {
|
|
226
|
+
return this.adapter?.fitBounds(bounds, options);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
panTo(lat, lng, options = {}) {
|
|
230
|
+
return this.adapter?.panTo(lat, lng, options);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
zoomIn() {
|
|
234
|
+
return this.adapter?.zoomIn();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
zoomOut() {
|
|
238
|
+
return this.adapter?.zoomOut();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getZoom() {
|
|
242
|
+
return this.adapter?.getZoom();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getCenter() {
|
|
246
|
+
return this.adapter?.getCenter();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getBounds() {
|
|
250
|
+
return this.adapter?.getBounds();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
get livemap() {
|
|
254
|
+
return this._livemap;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── Markers ───────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
addMarker(id, lat, lng, options = {}) {
|
|
260
|
+
return this.adapter?.addMarker(id, lat, lng, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
updateMarkerPosition(id, lat, lng, animated = true, duration = 500) {
|
|
264
|
+
return this.adapter?.updateMarkerPosition(id, lat, lng, animated, duration);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setMarkerRotation(id, degrees) {
|
|
268
|
+
return this.adapter?.setMarkerRotation(id, degrees);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
removeMarker(id) {
|
|
272
|
+
return this.adapter?.removeMarker(id);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getMarker(id) {
|
|
276
|
+
return this.adapter?.getMarker(id) ?? null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
hasMarker(id) {
|
|
280
|
+
return this.adapter?.hasMarker(id) ?? false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Overlays ──────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
addPolygon(id, coordinates, options = {}) {
|
|
286
|
+
return this.adapter?.addPolygon(id, coordinates, options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
addPolyline(id, coordinates, options = {}) {
|
|
290
|
+
return this.adapter?.addPolyline(id, coordinates, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
addCircle(id, lat, lng, radiusMeters, options = {}) {
|
|
294
|
+
return this.adapter?.addCircle(id, lat, lng, radiusMeters, options);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
removeOverlay(id) {
|
|
298
|
+
return this.adapter?.removeOverlay(id);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
clearOverlays() {
|
|
302
|
+
return this.adapter?.clearOverlays();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
getOverlay(id) {
|
|
306
|
+
return this.adapter?.getOverlay(id) ?? null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Routing Controls ─────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
async addRoutingControl(waypoints, options = {}) {
|
|
312
|
+
if (!isArray(waypoints) || waypoints.length === 0) return null;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await this.ensureInteractive({ timeoutMs: options.timeoutMs ?? 8000 });
|
|
316
|
+
const engine = options.engine ?? this.routeEngine.getDisplayEngine('osrm');
|
|
317
|
+
const route =
|
|
318
|
+
waypoints.length === 1
|
|
319
|
+
? {
|
|
320
|
+
engine: typeof engine === 'string' ? engine : this.routeEngine.getDisplayEngine('osrm'),
|
|
321
|
+
waypoints,
|
|
322
|
+
coordinates: [],
|
|
323
|
+
bounds: null,
|
|
324
|
+
summary: { totalDistance: 0, totalTime: 0 },
|
|
325
|
+
legs: [],
|
|
326
|
+
raw: null,
|
|
327
|
+
}
|
|
328
|
+
: await this.routeEngine.compute(engine, waypoints, options);
|
|
329
|
+
const routeControl = await this.adapter?.addRoutingControl?.(route, {
|
|
330
|
+
...options,
|
|
331
|
+
engine,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (routeControl) {
|
|
335
|
+
this.routeControls.set(routeControl.id, routeControl);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (options.position !== false) {
|
|
339
|
+
this.positionWaypoints(route.bounds ?? route.waypoints, {
|
|
340
|
+
...(options.fitOptions ?? {}),
|
|
341
|
+
isBounds: Boolean(route.bounds),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
options.onRouteFound?.(route);
|
|
346
|
+
return routeControl;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
options.onRoutingError?.(error);
|
|
349
|
+
debug(`[MapManager] Routing control failed: ${error.message}`);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async replaceRoutingControl(waypoints, existingHandle, options = {}) {
|
|
355
|
+
const removeOptions = options.removeOptions ?? {};
|
|
356
|
+
if (existingHandle) {
|
|
357
|
+
await this.removeRoutingControl(existingHandle, removeOptions);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return this.addRoutingControl(waypoints, options);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
removeRoutingControl(handle, options = {}) {
|
|
364
|
+
const routeControl = typeof handle === 'string' ? this.routeControls.get(handle) : handle;
|
|
365
|
+
if (!routeControl) return false;
|
|
366
|
+
|
|
367
|
+
const removed = this.adapter?.removeRoutingControl?.(routeControl, options) ?? false;
|
|
368
|
+
this.routeControls.delete(routeControl.id);
|
|
369
|
+
return removed;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
clearRoutingControls(filter = null) {
|
|
373
|
+
for (const handle of this.routeControls.values()) {
|
|
374
|
+
if (typeof filter === 'function' && filter(handle) === false) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.removeRoutingControl(handle);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
clearRoutingControlsByTag(tag) {
|
|
383
|
+
if (!tag) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this.clearRoutingControls((handle) => handle?.tag === tag);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
positionWaypoints(waypointsOrBounds, options = {}) {
|
|
391
|
+
return this.adapter?.positionWaypoints?.(waypointsOrBounds, options);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
focusResource(record, zoom = 16, options = {}) {
|
|
395
|
+
if (!record) return null;
|
|
396
|
+
|
|
397
|
+
const { paddingBottomRight = [0, 0], moveend } = options;
|
|
398
|
+
const layer = record?.leafletLayer ?? this.getMarker(record?.id) ?? this.getOverlay(record?.id);
|
|
399
|
+
const bounds = this.#extractLayerBounds(layer);
|
|
400
|
+
const point = this.#extractLayerPoint(layer) ?? this.#extractRecordPoint(record);
|
|
401
|
+
|
|
402
|
+
if (typeof moveend === 'function') {
|
|
403
|
+
this.once('moveend', () => next(this, moveend));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (bounds) {
|
|
407
|
+
return this.fitBounds(bounds, { paddingBottomRight, maxZoom: zoom, animate: true });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (point) {
|
|
411
|
+
return this.flyTo(point.lat, point.lng, zoom, { animate: true, duration: 0.8 });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Drawing Tools ─────────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
enableDrawingMode(type, options = {}) {
|
|
420
|
+
return this.adapter?.enableDrawingMode(type, options);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
disableDrawingMode() {
|
|
424
|
+
return this.adapter?.disableDrawingMode();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
showDrawControl(config = {}) {
|
|
428
|
+
return this.adapter?.showDrawControl(config);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
hideDrawControl() {
|
|
432
|
+
return this.adapter?.hideDrawControl();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ─── Popups ────────────────────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
openPopup(id, lat, lng, htmlContent) {
|
|
438
|
+
return this.adapter?.openPopup(id, lat, lng, htmlContent);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
closePopup(id) {
|
|
442
|
+
return this.adapter?.closePopup(id);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ─── Context Menus ─────────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
registerContextMenu(target, items) {
|
|
448
|
+
return this.adapter?.registerContextMenu(target, items);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
removeContextMenu(target) {
|
|
452
|
+
return this.adapter?.removeContextMenu(target);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── Events ────────────────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
on(event, handler) {
|
|
458
|
+
return this.adapter?.on(event, handler);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
off(event, handler) {
|
|
462
|
+
return this.adapter?.off(event, handler);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
once(event, handler) {
|
|
466
|
+
return this.adapter?.once(event, handler);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ─── Utilities ─────────────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
distanceBetween(lat1, lng1, lat2, lng2) {
|
|
472
|
+
return this.adapter?.distanceBetween(lat1, lng1, lat2, lng2);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
addGeoJson(id, geojson, options = {}) {
|
|
476
|
+
return this.adapter?.addGeoJson(id, geojson, options);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
removeGeoJson(id) {
|
|
480
|
+
return this.adapter?.removeGeoJson(id);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
setTileLayer(url, options = {}) {
|
|
484
|
+
return this.adapter?.setTileLayer(url, options);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ─── Provider Switching ──────────────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Activate a named provider adapter.
|
|
491
|
+
* Called by the live-map component when the map initialises.
|
|
492
|
+
*
|
|
493
|
+
* @param {string} provider e.g. 'leaflet' | 'google'
|
|
494
|
+
*/
|
|
495
|
+
@action setActiveProvider(provider) {
|
|
496
|
+
if (this.providerName === provider && this.adapter) return;
|
|
497
|
+
this.isReady = false;
|
|
498
|
+
this.#resetReadyDeferred();
|
|
499
|
+
this.resolveAdapter(provider);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Bind a host-created native map instance to the active adapter.
|
|
504
|
+
*
|
|
505
|
+
* @param {*} mapInstance
|
|
506
|
+
*/
|
|
507
|
+
@action setMapInstance(mapInstance) {
|
|
508
|
+
this.adapter?.setMapInstance?.(mapInstance);
|
|
509
|
+
if (mapInstance) {
|
|
510
|
+
this.isReady = true;
|
|
511
|
+
this.#resolveMapReady?.(mapInstance);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Whether the active provider is Google Maps.
|
|
517
|
+
* Used by templates to conditionally render the correct map branch.
|
|
518
|
+
*
|
|
519
|
+
* @type {boolean}
|
|
520
|
+
*/
|
|
521
|
+
get isGoogleMaps() {
|
|
522
|
+
return (this.adapter ? this.providerName : this.getConfiguredProvider()) === 'google';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#extractLayerBounds(layer) {
|
|
526
|
+
if (!layer) return null;
|
|
527
|
+
|
|
528
|
+
if (typeof layer.getBounds === 'function') {
|
|
529
|
+
const bounds = layer.getBounds();
|
|
530
|
+
|
|
531
|
+
if (typeof bounds?.getSouth === 'function') {
|
|
532
|
+
return [
|
|
533
|
+
[bounds.getSouth(), bounds.getWest()],
|
|
534
|
+
[bounds.getNorth(), bounds.getEast()],
|
|
535
|
+
];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (typeof bounds?.getSouthWest === 'function') {
|
|
539
|
+
const sw = bounds.getSouthWest();
|
|
540
|
+
const ne = bounds.getNorthEast();
|
|
541
|
+
return [
|
|
542
|
+
[sw.lat(), sw.lng()],
|
|
543
|
+
[ne.lat(), ne.lng()],
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
#extractLayerPoint(layer) {
|
|
552
|
+
if (!layer) return null;
|
|
553
|
+
|
|
554
|
+
if (typeof layer.getLatLng === 'function') {
|
|
555
|
+
const latlng = layer.getLatLng();
|
|
556
|
+
if (Number.isFinite(latlng?.lat) && Number.isFinite(latlng?.lng)) {
|
|
557
|
+
return { lat: latlng.lat, lng: latlng.lng };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const position = layer.position ?? layer.getPosition?.();
|
|
562
|
+
if (position) {
|
|
563
|
+
const lat = typeof position.lat === 'function' ? position.lat() : position.lat;
|
|
564
|
+
const lng = typeof position.lng === 'function' ? position.lng() : position.lng;
|
|
565
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) {
|
|
566
|
+
return { lat, lng };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#extractRecordPoint(record) {
|
|
574
|
+
const coordinates = record?.location?.coordinates;
|
|
575
|
+
if (isArray(coordinates) && coordinates.length >= 2) {
|
|
576
|
+
const [lng, lat] = coordinates;
|
|
577
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) {
|
|
578
|
+
return { lat, lng };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (Number.isFinite(record?.latitude) && Number.isFinite(record?.longitude)) {
|
|
583
|
+
return { lat: record.latitude, lng: record.longitude };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Pass a live `google.maps.Map` instance directly to the Google adapter.
|
|
591
|
+
* Called by `Map::GoogleLiveMap` after the map element is initialised.
|
|
592
|
+
*
|
|
593
|
+
* @param {google.maps.Map} mapInstance
|
|
594
|
+
*/
|
|
595
|
+
registerMarker(id, markerObject, meta = {}) {
|
|
596
|
+
return this.adapter?.registerMarker?.(id, markerObject, meta);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Register a native polygon object so that geofence and other services
|
|
601
|
+
* can retrieve it by model id.
|
|
602
|
+
*
|
|
603
|
+
* @param {string} id
|
|
604
|
+
* @param {*} polygonObject Native polygon (L.Polygon or google.maps.Polygon)
|
|
605
|
+
* @param {Object} [meta={}]
|
|
606
|
+
*/
|
|
607
|
+
registerPolygon(id, polygonObject, meta = {}) {
|
|
608
|
+
return this.adapter?.registerPolygon?.(id, polygonObject, meta);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Show a polygon by model id.
|
|
613
|
+
*
|
|
614
|
+
* @param {string} id
|
|
615
|
+
*/
|
|
616
|
+
showPolygon(id) {
|
|
617
|
+
return this.adapter?.showPolygon?.(id);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Hide a polygon by model id.
|
|
622
|
+
*
|
|
623
|
+
* @param {string} id
|
|
624
|
+
*/
|
|
625
|
+
hidePolygon(id) {
|
|
626
|
+
return this.adapter?.hidePolygon?.(id);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ─── Context Menu helpers ─────────────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Retrieve the registered context-menu items for a given target key.
|
|
633
|
+
* Used by the Google Maps branch to build floating InfoWindow menus.
|
|
634
|
+
*
|
|
635
|
+
* @param {string} target e.g. 'map', 'driver:DRV-XXXX'
|
|
636
|
+
* @returns {Array}
|
|
637
|
+
*/
|
|
638
|
+
getContextMenuItems(target) {
|
|
639
|
+
const registry = this.leafletContextmenuManager.getRegistry(target);
|
|
640
|
+
const items = registry?.contextmenuItems ?? this.adapter?.getContextMenuItems?.(target) ?? [];
|
|
641
|
+
|
|
642
|
+
return items.map((item) => ({
|
|
643
|
+
...item,
|
|
644
|
+
label: item.label ?? item.text,
|
|
645
|
+
action: item.action ?? item.callback,
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
getComposedContextMenuItems(target) {
|
|
650
|
+
const mapItems = this.getContextMenuItems('map');
|
|
651
|
+
if (!target || target === 'map') {
|
|
652
|
+
return mapItems;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const targetItems = this.getContextMenuItems(target);
|
|
656
|
+
if (!targetItems.length) {
|
|
657
|
+
return mapItems;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!mapItems.length) {
|
|
661
|
+
return targetItems;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const combinedItems = [...mapItems];
|
|
665
|
+
const firstTargetItem = targetItems[0];
|
|
666
|
+
|
|
667
|
+
if (!firstTargetItem?.separator) {
|
|
668
|
+
combinedItems.push({ separator: true });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
combinedItems.push(...targetItems);
|
|
672
|
+
return combinedItems;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
showContextMenu(event, items = []) {
|
|
676
|
+
return this.adapter?.showContextMenu?.(event, items);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
closeContextMenu() {
|
|
680
|
+
return this.adapter?.closeContextMenu?.();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ─── Map control shortcuts ──────────────────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Show the current cursor coordinates in a toast / overlay.
|
|
687
|
+
* Delegates to the active adapter.
|
|
688
|
+
*/
|
|
689
|
+
@action showCoordinates(event) {
|
|
690
|
+
const result = this.adapter?.showCoordinates?.(event);
|
|
691
|
+
if (result && typeof result === 'object' && Number.isFinite(result.lat) && Number.isFinite(result.lng)) {
|
|
692
|
+
this.notifications.info(`${result.lat}, ${result.lng}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Re-centre the map on the user's current location.
|
|
700
|
+
*/
|
|
701
|
+
@action centerMap(event) {
|
|
702
|
+
return this.adapter?.centerMap?.(event);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Toggle the drawing toolbar visibility.
|
|
707
|
+
*/
|
|
708
|
+
@action toggleDrawControl() {
|
|
709
|
+
return this.adapter?.toggleDrawControl?.();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Pan the map by a pixel offset.
|
|
714
|
+
*
|
|
715
|
+
* @param {number} x Horizontal pixel offset
|
|
716
|
+
* @param {number} y Vertical pixel offset (default 0)
|
|
717
|
+
*/
|
|
718
|
+
panBy(x, y = 0) {
|
|
719
|
+
return this.adapter?.panBy?.(x, y);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Edit an existing polygon overlay.
|
|
724
|
+
* Delegates to the adapter's `editPolygon` implementation.
|
|
725
|
+
*
|
|
726
|
+
* @param {*} layer Native polygon object
|
|
727
|
+
* @param {Object} [options={}]
|
|
728
|
+
* @returns {Promise<{ type: 'edited'|'cancel', layer: * }>}
|
|
729
|
+
*/
|
|
730
|
+
editPolygon(layer, options = {}) {
|
|
731
|
+
return this.adapter?.editPolygon?.(layer, options) ?? Promise.resolve({ type: 'unsupported' });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ─── Layer Visibility (delegated to adapter) ───────────────────────────────
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Show a layer by its native object reference.
|
|
738
|
+
* Adapters implement this to show/hide provider-specific overlay objects.
|
|
739
|
+
*
|
|
740
|
+
* @param {*} layer - Native layer object
|
|
741
|
+
* @param {Object} [options={}]
|
|
742
|
+
*/
|
|
743
|
+
showLayer(layer, options = {}) {
|
|
744
|
+
return this.adapter?.showLayer?.(layer, options);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Hide a layer by its native object reference.
|
|
749
|
+
*
|
|
750
|
+
* @param {*} layer - Native layer object
|
|
751
|
+
* @param {Object} [options={}]
|
|
752
|
+
*/
|
|
753
|
+
hideLayer(layer, options = {}) {
|
|
754
|
+
return this.adapter?.hideLayer?.(layer, options);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
removeLayer(layer) {
|
|
758
|
+
return this.adapter?.removeLayer?.(layer);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
#resetReadyDeferred() {
|
|
764
|
+
this.#mapReadyPromise = new Promise((resolve) => {
|
|
765
|
+
this.#resolveMapReady = resolve;
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|