@fleetbase/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
466 lines (395 loc) • 17.4 kB
JavaScript
/**
* GeofenceService
*
* Refactored to use the provider-agnostic MapManagerService for all map
* interactions (drawing, fitBounds, showDrawControl, etc.).
*
*/
import Service, { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { debug } from '@ember/debug';
import { dasherize } from '@ember/string';
import { later } from '@ember/runloop';
import toMultiPolygon from '../utils/to-multi-polygon';
export default class GeofenceService extends Service {
mapManager;
store;
modalsManager;
serviceAreas;
serviceAreaActions;
zoneActions;
notifications;
intl;
_draftSession = null;
_draftCreatedHandler = null;
createServiceArea() {
this.notifications.info(this.intl.t('geofence.prompts.use-draw-controls-create-service-area'));
this.#startDraftSession('service-area');
}
createZone(serviceArea) {
this.notifications.info(this.intl.t('geofence.prompts.use-draw-controls-create-zone'));
if (serviceArea.leafletCoordinates) {
this.mapManager.fitBounds(serviceArea.leafletCoordinates, { paddingBottomRight: [300, 0], maxZoom: 15, animate: true });
}
this.#startDraftSession('zone', { serviceArea });
}
toggleDrawControl() {
if (this._draftSession) {
this.#resetDraftSession({ hideControl: true, removeDraft: true });
return;
}
this.notifications.info(this.intl.t('geofence.prompts.use-draw-controls-create-service-area'));
this.#startDraftSession('generic');
}
async editServiceArea(serviceArea) {
const layer = serviceArea?.leafletLayer ?? this.mapManager.getOverlay(serviceArea.id);
if (!layer) {
this.notifications.info(this.intl.t('geofence.prompts.no-layer-found-for-resource', { resource: this.intl.t('resource.service-area') }));
return;
}
try {
this.notifications.info(this.intl.t('geofence.prompts.editing-enabled'));
const result = await this.#editPolygonLayer(layer, { focusBounds: serviceArea.leafletCoordinates });
if (result?.type === 'edited') {
const border = toMultiPolygon(result.geoJson ?? result.toGeoJSON?.());
serviceArea.set('border', border);
await serviceArea.save?.();
this.notifications.success(this.intl.t('geofence.prompts.resource-boundaries-updated', { resource: this.intl.t('resource.service-area') }));
} else {
this.notifications.info(this.intl.t('geofence.prompts.edit-canceled'));
}
} catch (e) {
debug(`editServiceArea error: ${e?.message ?? e}`);
this.notifications.serverError?.(e) || this.notifications.error?.(this.intl.t('geofence.prompts.failed-to-update-resource', { resource: this.intl.t('resource.service-area') }));
}
}
async editZone(zone) {
const layer = zone?.leafletLayer ?? this.mapManager.getOverlay(zone.id);
if (!layer) {
this.notifications.info(this.intl.t('geofence.prompts.no-layer-found-for-resource', { resource: this.intl.t('resource.zone') }));
return;
}
try {
this.notifications.info(this.intl.t('geofence.prompts.editing-enabled'));
const result = await this.#editPolygonLayer(layer, { focusBounds: zone.leafletCoordinates });
if (result?.type === 'edited') {
const border = result.geoJson ?? result.toGeoJSON?.();
zone.set('border', border);
await zone.save?.();
this.notifications.success(this.intl.t('geofence.prompts.resource-boundaries-updated', { resource: this.intl.t('resource.zone') }));
} else {
this.notifications.info(this.intl.t('geofence.prompts.edit-canceled'));
}
} catch (e) {
debug(`editZone error: ${e?.message ?? e}`);
this.notifications.serverError?.(e) || this.notifications.error?.(this.intl.t('geofence.prompts.failed-to-update-resource', { resource: this.intl.t('resource.zone') }));
}
}
showAllServiceAreas() {
this.serviceAreaActions.serviceAreas.forEach((serviceArea) => {
this.#showServiceArea(serviceArea);
});
}
hideAllServiceAreas() {
this.serviceAreaActions.serviceAreas.forEach((serviceArea) => {
this.#hideServiceArea(serviceArea);
});
}
focusServiceArea(serviceArea) {
this.#showServiceArea(serviceArea, { pin: true });
if (serviceArea.leafletCoordinates) {
this.mapManager.fitBounds(serviceArea.leafletCoordinates, { paddingBottomRight: [0, 0], maxZoom: 15, animate: true });
}
}
blurServiceArea(serviceArea) {
this.#showServiceArea(serviceArea, { pin: false });
this.#hideServiceArea(serviceArea);
}
focusZone(zone) {
this.#showZone(zone, { pin: true });
if (zone.leafletCoordinates) {
this.mapManager.fitBounds(zone.leafletCoordinates, { paddingBottomRight: [0, 0], maxZoom: 15, animate: true });
}
}
#startDraftSession(mode, { serviceArea = null } = {}) {
this.#resetDraftSession();
this._draftSession = {
mode,
serviceArea,
draftLayer: null,
};
this.mapManager.showDrawControl({
tools: ['polygon', 'circle', 'rectangle'],
allowEdit: true,
allowDelete: true,
defaultMode: null,
});
this._draftCreatedHandler = (payload) => {
this.#handleDraftCreated(payload);
};
this.mapManager.on('draw:created', this._draftCreatedHandler);
}
#handleDraftCreated({ layer, geoJson }) {
if (!this._draftSession) return;
this._draftSession.draftLayer = layer;
this.mapManager.off('draw:created', this._draftCreatedHandler);
this._draftCreatedHandler = null;
if (this._draftSession.mode === 'service-area') {
const border = toMultiPolygon(geoJson);
this.serviceAreaActions.modal.create(
{ border },
{
decline: (modal) => {
this.#resetDraftSession({ hideControl: true, removeDraft: true });
modal.done?.();
},
},
{
callback: (serviceArea) => {
this.#finishDraftSave((draftLayer) => {
this.#reloadThenFocusServiceArea(serviceArea, { draftLayer });
});
},
refresh: false,
}
);
return;
}
if (this._draftSession.mode === 'zone') {
const serviceArea = this._draftSession.serviceArea;
this.zoneActions.modal.create(
{ border: geoJson, service_area: serviceArea },
{
decline: (modal) => {
this.#resetDraftSession({ hideControl: true, removeDraft: true });
modal.done?.();
},
},
{
callback: (zone) => {
this.#finishDraftSave((draftLayer) => {
this.#reloadThenFocusZone(serviceArea, zone, { draftLayer });
});
},
refresh: false,
}
);
return;
}
this.#openGenericLayerModal(geoJson);
}
#openGenericLayerModal(geoJson) {
this.modalsManager.show('modals/map-layer-form', {
title: 'Create new Layer',
modalClass: 'flb-resource-modal',
acceptButtonText: 'Create',
acceptButtonIcon: 'magic',
declineButtonIcon: 'times',
declineButtonIconPrefix: 'fas',
layerTypes: ['Service Area', 'Zone'],
selectedLayerType: 'Service Area',
serviceAreaTypes: this.serviceAreas.serviceAreaTypes,
layerOptions: {
trigger_on_entry: false,
trigger_on_exit: false,
dwell_threshold_minutes: null,
speed_limit_kmh: null,
description: null,
},
confirm: (modal) => {
modal.startLoading();
const selectedLayerType = modal.getOption('selectedLayerType');
const layerOptions = modal.getOption('layerOptions') ?? {};
const selectedServiceArea = layerOptions.service_area;
if (selectedLayerType === 'Zone' && !selectedServiceArea) {
this.notifications.error('Service Area required to create Zone!');
modal.stopLoading?.();
return;
}
const record = this.store.createRecord(dasherize(selectedLayerType), layerOptions);
if (selectedLayerType === 'Service Area') {
record.setProperties({
border: toMultiPolygon(geoJson),
status: 'active',
});
} else {
record.setProperties({
border: geoJson,
service_area: selectedServiceArea,
});
}
return record.save().then((savedRecord) => {
if (selectedLayerType === 'Service Area') {
this.#finishDraftSave((draftLayer) => {
this.#reloadThenFocusServiceArea(savedRecord, { draftLayer });
});
} else {
this.#finishDraftSave((draftLayer) => {
this.#reloadThenFocusZone(selectedServiceArea, savedRecord, { draftLayer });
});
}
return savedRecord;
});
},
decline: (modal) => {
this.#resetDraftSession({ hideControl: true, removeDraft: true });
modal.done?.();
},
});
}
#resetDraftSession({ hideControl = false, removeDraft = false } = {}) {
if (this._draftCreatedHandler) {
this.mapManager.off('draw:created', this._draftCreatedHandler);
this._draftCreatedHandler = null;
}
const draftLayer = this._draftSession?.draftLayer;
if (removeDraft && draftLayer) {
this.mapManager.removeLayer(draftLayer);
}
if (hideControl) {
this.mapManager.hideDrawControl();
}
this._draftSession = null;
}
#finishDraftSave(callback) {
const draftLayer = this._draftSession?.draftLayer ?? null;
callback?.(draftLayer);
this.#resetDraftSession({ hideControl: true, removeDraft: false });
}
async #editPolygonLayer(originalLayer, { focusBounds = null } = {}) {
return this.mapManager.editPolygon(originalLayer, { focusBounds });
}
#showZone(zone, { pin = false } = {}) {
const overlay = this.mapManager.getOverlay(zone.id);
const layer = overlay ?? zone?.leafletLayer ?? null;
this.#setLayerPinned(layer, pin);
if (overlay) {
this.mapManager.showPolygon(zone.id);
} else if (layer) {
this.mapManager.showLayer?.(layer);
}
}
#includeServiceArea(serviceArea) {
if (!serviceArea?.id) return;
const currentServiceAreas = this.serviceAreaActions.serviceAreas;
if (!currentServiceAreas) {
this.serviceAreaActions.serviceAreas = [serviceArea];
return;
}
const serviceAreas = Array.from(currentServiceAreas);
const exists = serviceAreas.some((existingServiceArea) => existingServiceArea?.id === serviceArea.id);
if (exists) return;
this.serviceAreaActions.serviceAreas = [...serviceAreas, serviceArea];
}
#includeZone(serviceArea, zone) {
if (!serviceArea?.id || !zone?.id) return;
const zones = serviceArea.zones;
if (!zones) {
serviceArea.set?.('zones', [zone]);
this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
return;
}
const exists = Array.from(zones).some((existingZone) => existingZone?.id === zone.id);
if (exists) return;
if (typeof zones.pushObject === 'function') {
zones.pushObject(zone);
} else if (typeof zones.addObject === 'function') {
zones.addObject(zone);
} else if (typeof zones.push === 'function') {
zones.push(zone);
}
this.serviceAreaActions.serviceAreas = Array.from(this.serviceAreaActions.serviceAreas ?? []);
}
#reloadThenFocusServiceArea(serviceArea, options = {}) {
this.#reloadServiceAreas()
.then((serviceAreas) => {
const canonicalServiceArea = this.#findResource(serviceAreas, serviceArea) ?? serviceArea;
this.#focusServiceAreaAfterCreate(canonicalServiceArea, options);
})
.catch(() => {
this.#includeServiceArea(serviceArea);
this.#focusServiceAreaAfterCreate(serviceArea, options);
});
}
#reloadThenFocusZone(serviceArea, zone, options = {}) {
this.#reloadServiceAreas()
.then((serviceAreas) => {
const canonicalServiceArea = this.#findResource(serviceAreas, serviceArea) ?? serviceArea;
const canonicalZone = this.#findResource(canonicalServiceArea?.zones, zone) ?? zone;
this.#focusZoneAfterCreate(canonicalZone, options);
})
.catch(() => {
this.#includeZone(serviceArea, zone);
this.#focusZoneAfterCreate(zone, options);
});
}
#reloadServiceAreas() {
return Promise.resolve(this.serviceAreaActions.loadAll.perform()).then(() => this.serviceAreaActions.serviceAreas ?? []);
}
#findResource(resources, resource) {
if (!resource || !resources) return null;
const resourceId = resource.id ?? resource.public_id;
const publicId = resource.public_id ?? resource.id;
return (
Array.from(resources).find((candidate) => {
return candidate?.id === resourceId || candidate?.id === publicId || candidate?.public_id === publicId || candidate?.public_id === resourceId;
}) ?? null
);
}
#focusServiceAreaAfterCreate(serviceArea, options = {}) {
this.#focusWhenReady([serviceArea], () => this.focusServiceArea(serviceArea), options);
}
#focusZoneAfterCreate(zone, options = {}) {
this.#focusWhenReady([zone], () => this.focusZone(zone), options);
}
#focusWhenReady(models, focus, { attempts = 60, delay = 100, draftLayer = null } = {}) {
const overlays = models
.map((model) => {
const overlay = this.mapManager.getOverlay(model?.id);
if (overlay) {
return overlay;
}
return model?.leafletLayer === draftLayer ? null : model?.leafletLayer;
})
.filter(Boolean);
const allRegistered = overlays.length === models.length;
if (allRegistered || attempts <= 0) {
focus();
if (draftLayer && !draftLayer.__removedAfterCreate && !overlays.includes(draftLayer)) {
draftLayer.__removedAfterCreate = true;
this.mapManager.removeLayer(draftLayer);
}
return;
}
later(this, () => this.#focusWhenReady(models, focus, { attempts: attempts - 1, delay, draftLayer }), delay);
}
#showServiceArea(serviceArea, { pin = false } = {}) {
[serviceArea, ...(serviceArea.zones ?? [])].forEach((model) => {
const overlay = this.mapManager.getOverlay(model.id);
const layer = overlay ?? model?.leafletLayer ?? null;
this.#setLayerPinned(layer, pin);
if (overlay) {
this.mapManager.showPolygon(model.id);
} else if (layer) {
this.mapManager.showLayer?.(layer);
}
});
}
#hideServiceArea(serviceArea) {
[serviceArea, ...(serviceArea.zones ?? [])].forEach((model) => {
const overlay = this.mapManager.getOverlay(model.id);
const layer = model?.leafletLayer;
this.#setLayerPinned(layer ?? overlay, false);
if (overlay) {
this.mapManager.hidePolygon(model.id);
}
if (layer && layer !== overlay) {
this.mapManager.hideLayer?.(layer);
}
});
}
#setLayerPinned(layer, pinned = false) {
if (!layer) return;
layer.__pinnedByFocus = pinned;
}
}