@fleetbase/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
334 lines (282 loc) • 11.8 kB
JavaScript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action, get } from '@ember/object';
import { task } from 'ember-concurrency';
import { colorForId, routeColorForStatus, routeStyleForStatus } from '../../../utils/route-colors';
import { buildRoutePointMarkerPresentation, buildRoutePointsFromPayload, describeRoutePoint } from '../../../utils/route-visualization';
const ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT = [420, 0];
const ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS = 13;
const ORDER_ROUTE_PREVIEW_MAX_ZOOM_MULTI_POINTS = 12;
const ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY = [10, 0];
export default class OrderFormRouteComponent extends Component {
mapManager;
routeEngine;
routeOptimization;
location;
store;
currentUser;
notifications;
placeActions;
orderCreation;
multipleWaypoints = false;
routingControl;
route;
focusPlace(place, zoom = 18) {
if (place?.hasValidCoordinates) {
this.mapManager.positionWaypoints([[place.latitude, place.longitude]], {
singlePointZoom: zoom,
panBy: ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY,
});
}
}
get coordinates() {
return this.routePoints.map(({ place }) => [place.latitude, place.longitude]);
}
get places() {
return this.routePoints.map(({ place }) => place);
}
get routePoints() {
return buildRoutePointsFromPayload(this.args.resource.payload);
}
get routeColor() {
const order = this.args.resource;
return colorForId(order?.public_id ?? order?.id ?? 'new-order');
}
get waypointRouteStops() {
const waypoints = this.args.resource.payload?.waypoints ?? [];
return waypoints.map((_waypoint, index) => {
const routePoint = {
role: 'waypoint',
stopNumber: index + 1,
};
return {
...describeRoutePoint(routePoint, this.routeColor),
badgeStyle: this.badgeStyleForWaypoint(index),
required: this.isRequiredWaypoint(index),
};
});
}
badgeStyleForWaypoint(index) {
const { markerColor } = describeRoutePoint({ role: 'waypoint', stopNumber: index + 1 }, this.routeColor);
const normalizedColor = markerColor?.toLowerCase?.();
const isYellow = normalizedColor === '#facc15' || normalizedColor === '#ca8a04';
const textColor = isYellow ? '#111827' : '#ffffff';
return `background-color: ${markerColor}; color: ${textColor};`;
}
isRequiredWaypoint(index) {
return index < 2;
}
willDestroy() {
super.willDestroy(...arguments);
if (this.routingControl) {
this.mapManager.removeRoutingControl(this.routingControl);
}
}
toggleWaypoints(multipleWaypoints) {
this.multipleWaypoints = multipleWaypoints;
const { pickup, dropoff } = this.args.resource.payload;
if (multipleWaypoints) {
if (pickup) {
this.addWaypoint({ place: pickup, customer: this.args.resource.customer });
if (dropoff) {
this.addWaypoint({ place: dropoff, customer: this.args.resource.customer });
}
// clear pickup and dropoff
this.args.resource.payload.setProperties({ pickup: null, dropoff: null });
} else {
this.addWaypoint({ customer: this.args.resource.customer });
}
} else {
const pickup = get(this.args.resource.payload.waypoints, '0.place');
const dropoff = get(this.args.resource.payload.waypoints, '1.place');
if (pickup) {
this.setPayloadPlace('pickup', pickup);
}
if (dropoff) {
this.setPayloadPlace('dropoff', dropoff);
}
this.clearWaypoints();
}
this.requestServiceQuoteRefresh('route.waypoints.toggled');
}
sortWaypoints({ sourceList, sourceIndex, targetList, targetIndex }) {
if (sourceList === targetList && sourceIndex === targetIndex) {
return;
}
const item = sourceList.objectAt(sourceIndex);
sourceList.removeAt(sourceIndex);
targetList.insertAt(targetIndex, item);
this.previewRoute();
this.requestServiceQuoteRefresh('route.waypoints.reordered');
}
addWaypoint(properties = {}) {
if (this.args.resource.customer) {
properties.customer = this.args.resource.customer;
}
const waypoint = this.store.createRecord('waypoint', { ...properties, type: 'dropoff' });
this.args.resource.payload.waypoints.pushObject(waypoint);
this.previewRoute();
this.requestServiceQuoteRefresh('route.waypoint.added');
}
setWaypointPlace(index, place) {
if (!this.args.resource.payload.waypoints[index]) return;
this.args.resource.payload.waypoints[index].place = place;
this.args.resource.payload.waypoints[index]?.setProperties({
street1: place.street1,
street2: place.street2,
city: place.city,
province: place.province,
postal_code: place.postal_code,
country: place.country,
location: place.location,
});
this.previewRoute();
this.requestServiceQuoteRefresh('route.waypoint.place.changed');
}
setWaypointCustomer(waypoint, model) {
waypoint.set('customer', model);
waypoint.set('customer_type', `fleet-ops:${model.customer_type}`);
}
removeWaypoint(waypoint) {
if (this.multipleWaypoints && this.args.resource.payload.waypoints.length === 1) return;
this.args.resource.payload.waypoints.removeObject(waypoint);
this.previewRoute();
this.requestServiceQuoteRefresh('route.waypoint.removed');
}
clearWaypoints() {
this.args.resource.payload.waypoints.clear();
this.previewRoute();
this.requestServiceQuoteRefresh('route.waypoints.cleared');
}
setPayloadPlace(prop, place) {
this.args.resource.payload[prop] = place;
this.previewRoute();
this.requestServiceQuoteRefresh(`route.${prop}.changed`);
}
editPlace(place) {
this.placeActions.modal.edit(place);
}
async previewRoute() {
if (!this.coordinates.length) {
if (this.routingControl) {
this.mapManager.removeRoutingControl(this.routingControl);
this.routingControl = null;
}
return;
}
const order = this.args.resource;
const orderId = order.public_id ?? order.id ?? 'new-order';
const routeColor = colorForId(orderId);
const routeStatus = order.status ?? 'created';
const statusColor = routeColorForStatus(routeStatus);
const routeStyles = routeStyleForStatus(routeStatus, statusColor);
const isSinglePointPreview = this.coordinates.length === 1;
const fitOptions = isSinglePointPreview
? {
paddingBottomRight: [0, 0],
panBy: ORDER_ROUTE_PREVIEW_SINGLE_POINT_PANBY,
}
: {
paddingBottomRight: ORDER_ROUTE_PREVIEW_PADDING_BOTTOM_RIGHT,
panBy: [0, 0],
maxZoom: this.coordinates.length === 2 ? ORDER_ROUTE_PREVIEW_MAX_ZOOM_TWO_POINTS : ORDER_ROUTE_PREVIEW_MAX_ZOOM_MULTI_POINTS,
};
const routingControl = await this.mapManager.replaceRoutingControl(this.coordinates, this.routingControl, {
engine: this.routeEngine.getDisplayEngine('osrm'),
color: statusColor,
orderId,
status: routeStatus,
fitOptions,
places: this.places,
markerWaypoints: this.coordinates,
polylineOptions: {
color: statusColor,
weight: routeStyles.at(-1)?.weight ?? 4,
opacity: routeStyles.at(-1)?.opacity ?? 0.85,
styles: routeStyles,
},
createMarker: (_waypoint, index) => {
const routePoint = this.routePoints[index];
return buildRoutePointMarkerPresentation(routePoint, routeColor);
},
onRouteFound: (route) => this.setRoute(route),
removeOptions: {
filter: (handle) => handle?.tag === this.args.resource.driver_assigned?.id,
},
tag: this.args.resource.driver_assigned?.id,
});
this.routingControl = routingControl;
}
*optimizeRouteWithService(service) {
const order = this.args.resource;
const payload = order.payload;
const waypoints = this.args.resource.payload.waypoints;
const coordinates = this.coordinates.map((coord) => coord.reverse());
try {
const result = yield this.routeOptimization.optimize(service, {
context: 'create_order',
order,
payload,
waypoints,
coordinates,
});
this.handleRouteOptimization(result);
} catch (err) {
this.notifications.error(err.message ?? this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
}
}
*optimizeRoute() {
const order = this.args.resource;
const payload = order.payload;
const waypoints = this.args.resource.payload.waypoints;
const coordinates = this.coordinates.map((coord) => coord.reverse());
const service = this.routeEngine.getOptimizationEngine('osrm');
try {
const result = yield this.routeOptimization.optimize(service, {
context: 'create_order',
order,
payload,
waypoints,
coordinates,
});
this.handleRouteOptimization(result);
} catch (err) {
this.notifications.error(this.intl.t('fleet-ops.operations.orders.index.new.route-error'));
}
}
handleRouteOptimization({ sortedWaypoints, route, trip, result, engine = 'osrm' }) {
// Update controller state
this.args.resource.payload.waypoints = sortedWaypoints;
if (route) {
this.setOptimizedRoute(route, trip, result.waypoints, engine);
}
this.previewRoute();
this.args.resource.set('optimized', true);
this.requestServiceQuoteRefresh('route.optimized');
}
setOptimizedRoute(route, trip, waypoints, engine = 'osrm') {
let summary = {
totalDistance: trip?.distance ?? trip?.totalDistance ?? 0,
totalTime: trip?.duration ?? trip?.totalTime ?? 0,
};
let payload = {
optimized: true,
coordinates: route,
waypoints,
trip,
summary,
engine,
};
this.setRoute(payload);
}
setRoute(payload) {
const routeModel = this.store.createRecord('route', payload);
this.args.resource.route = routeModel;
this.route = payload;
this.requestServiceQuoteRefresh('route.changed');
}
requestServiceQuoteRefresh(reason) {
this.orderCreation.requestServiceQuoteRefresh(reason, this.args.resource);
}
}