Skip to main content

Drag and Drop interactions

In this guide we will

  • display an already optimized route on a map
  • remove an activity from a route by drag and drop
  • add a new operation to a route by drag and drop
  • move an activity to another route by drag and drop
  • delete a route by drag and drop
  • merge routes by drag and drop

Follow common setup steps

If not done already, follow common setup and requirements

Display a route on a map

src/index.js
//...
/**
* Display 3 routes already optimized on a map
* Calling State.clean to ensure the state is valid
*/
let state = State.clean({
sites: [
{
id: "_LE_MANS",
coordinates: {
longitude: 0.190882,
latitude: 47.993661,
},
isDepot: false,
},
{
id: "_NANTES",
coordinates: {
longitude: -1.556986,
latitude: 47.238414,
},
isDepot: false,
},
{
id: "AXIOROUTE",
coordinates: {
longitude: -0.561642,
latitude: 47.462544,
},
isDepot: false,
},
{
id: "CJM",
coordinates: {
longitude: -0.345602,
latitude: 48.597481,
},
isDepot: false,
},
{
id: "SINARI",
coordinates: {
longitude: -1.604869,
latitude: 48.113197,
},
isDepot: true,
},
],
trucks: [
{
id: "VEHICLE_01",
availablePeriods: [
{
id: "PERIOD_01",
timeWindow: {
start: "2022-11-01T06:00:00.000+01:00",
end: "2022-11-01T22:00:00.000+01:00",
},
departureSite: "SINARI",
arrivalSite: "SINARI",
},
],
},
{
id: "VEHICLE_02",
availablePeriods: [
{
id: "PERIOD_01",
timeWindow: {
start: "2022-11-01T06:00:00.000+01:00",
end: "2022-11-01T22:00:00.000+01:00",
},
departureSite: "SINARI",
arrivalSite: "SINARI",
},
],
},
],
operations: [
{
id: "OPERATION_01",
type: "DELIVERY",
idSiteDestination: "AXIOROUTE",
},
{
id: "OPERATION_03",
type: "DELIVERY",
idSiteDestination: "CJM",
},
{
id: "OPERATION_08",
type: "DELIVERY",
idSiteDestination: "_LE_MANS",
},
{
id: "OPERATION_09",
type: "DELIVERY",
idSiteDestination: "_NANTES",
},
],
referencePlanning: {
vehicleRoutes: [
{
truck: "VEHICLE_01",
period: "PERIOD_01",
routes: [
{
id: "000-0361H01",
activities: [
{
activity: "LOADING",
site: "SINARI",
operations: [
{
id: "OPERATION_01",
},
],
},
{
activity: "DELIVERY",
site: "AXIOROUTE",
operations: [
{
id: "OPERATION_01",
},
],
},
],
},
{
id: "000-0364G01",
activities: [
{
activity: "LOADING",
site: "SINARI",
operations: [
{
id: "OPERATION_08",
},
{
id: "OPERATION_03",
},
],
},
{
activity: "DELIVERY",
site: "CJM",
operations: [
{
id: "OPERATION_03",
},
],
},
{
activity: "DELIVERY",
site: "_LE_MANS",
operations: [
{
id: "OPERATION_08",
},
],
},
],
},
],
},
{
truck: "VEHICLE_02",
period: "PERIOD_01",
routes: [
{
id: "000-0368Z01",
activities: [
{
activity: "LOADING",
site: "SINARI",
operations: [
{
id: "OPERATION_09",
},
],
},
{
activity: "DELIVERY",
site: "_NANTES",
operations: [
{
id: "OPERATION_09",
},
],
},
],
},
],
},
],
},
});

ARMessage.on("MAP_READY", (event) => {
ARMessage.send({
type: "SET_WEBVIEW_ENTITIES",
payload: state,
});
});

Remove an activity from a route

Activity entities are natively draggable, but what happen upon drag end is up to you.
The map will send an event to the app with all the information about:

  • what features have been dragged
  • what are the features dragged on
  • position of drag end
  • the initial dragged type OPERATION, ACTIVITY, ITINERARY
  • contextual tags, like "TRASHBIN" when dragged on the trashbin
Note

both routes and MCI itineraries will have dragged type ITINERARY

First let's create a listener that will be called each time an MPT solution has been received.
This will update the state and render the new state on the Map.

src/index.js
apiClient.subscribe("RECEIVED", (request) => {
//We update our state (we add the solution to the route)
state = MPTJobTools.updateState(state, request.job.solution);

ARMessage.send({
type: "SET_WEBVIEW_ENTITIES",
payload: state,
});
});

We also need to listen ARMapUIEventDragEnd event.

src/index.js
ARMessage.on("DRAG_END", (event) => {
const { draggedFeatures, draggedOnFeatures, tags, draggedType } =
event.payload;

/**
* we check if tags include "TRASHBIN" (did the drag end on the trashbin)
*/
if (tags.includes("TRASHBIN")) {
/**
* we filter draggedFeatures to keep only activities
*/
const activityFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "ACTIVITY")
);

/**
* We get Activity entities from Activity Features & current state
*/
const activities = activityFeatures.map((activityFeature) =>
VActivities.getFromFeature(activityFeature, state)
);

/**
* MPT Problem await a list of operations to remove (not activity)
* Hence, we get operations from activities
*/

const operationsToRemove = activities
.flatMap((activity) => Activity.getOperations(activity, state))
.map(Operation.toReferenceToOperation);

/**
* let's create an Action to remove operations
*/
const action = {
type: "UNSCHEDULE_OPERATIONS_FROM_EXISTING_ROUTES",
operations: operationsToRemove,
};

/**
* We have everything to get our MPT problem definition
*/
const myMPTProblem = MPTJobTools.getProblem(state, action);

/**
* We can now send our MPT problem to the API
*/
apiClient.sendMPTProblem(myMPTProblem);
}
});

Now each time we drag and drop an Activity on the trashbin we will remove all of its attached operations from the route.
It will work both for single activity and multiple activities.

  • TODO: add gif/video
Note

it's just an example of implementation, remember that the map is just a view, you can implement your own logic.

Add an operation to a route

Now we can also handle the case where we want to add an operation to a route.
Hence, when an operation is dragged on a route, we will add it to the route.

src/index.js
//...
ARMessage.on("DRAG_END", (event) => {
const { draggedFeatures, draggedOnFeatures, tags, draggedType } =
event.payload;
//...
if (tags.includes("TRASHBIN")) {
//...
} else {
/**
* when not dragged on trashbin, we check drag ends on a route
*/
const [targetItinerary] = draggedOnFeatures.filter((f) =>
Features.hasEntityType(f, "ITINERARY")
);

/**
*we get all dragged operations
*/
const operationFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "OPERATION")
);

/**
* we get operations from operation features
*/
const operationsToAdd = VOperations.getFromFeatures(
operationFeatures,
state
).map(Operation.toReferenceToOperation);

/**
* if targetItinerary is defined and operationsToAdd is not empty
* we can create an action to add operations to the route
*/
if (targetItinerary && operationsToAdd.length > 0) {
/**
* we get the route id from the itinerary feature
*/
const routeId = VItineraries.getRouteId(targetItinerary);

/**
* let's create an Action to add operations on a given route
*/
const action = {
type: "SCHEDULE_OPERATIONS_ON_EXISTING_ROUTES",
operations: operationsToAdd,
to: {
idRoute: routeId,
},
};

/**
* We have everything to get our MPT problem definition
*/
const myMPTProblem = MPTJobTools.getProblem(state, action);
apiClient.sendMPTProblem(myMPTProblem);

console.log("action", action);
}
}
//...
});

Now each time we drag and drop an Operation on a route we will affect it to this route. It will work both for single operation and multiple operations.

Move an activity to a different route

We can also handle the case where we want to move an activity to a different route.
This is very straightforward from what we've already done.

src/index.js
//...

/**
* we also collect all dragged activity features
*/
const activityFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "ACTIVITY")
);

/**
* we get operations from both operation and activity features (since Activity hold operations)
*/
const operationsToAdd = VOperations.getFromFeatures(
[...operationFeatures, ...activityFeatures],
state
).map(Operation.toReferenceToOperation);
//...

Now each time we drag and drop an Operation and/or Activity on a route we will affect all related operations to this route.
It will work both for single operation/activity and multiple operations/activities.

Delete a route

If we want to delete a route, we can drag and drop it on the trashbin.

src/index.js
//...
if (tags.includes("TRASHBIN")) {
/**
* since draggedFeatures is a list of features, we will check which was the initial dragged type,
* this is the feature type on which the drag started
*/

/**
* if initial dragged type is ITINERARY, we will delete the route
*/
if (draggedType === "ITINERARY") {
/**
* we filter draggedFeatures to keep only itineraries
*/
const itineraryFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "ITINERARY")
);

/**
* we get route ids from itinerary features
*/
const routeIds = VItineraries.getRouteIds(itineraryFeatures);

/**
* let's create an Action to remove routes
*/
const action = {
type: "UNSCHEDULE_EXISTING_ROUTES",
routes: routeIds,
};

/**
* We have everything to get our MPT problem definition
*/
const myMPTProblem = MPTJobTools.getProblem(state, action);

/**
* We can now send our MPT problem to the API
*/
apiClient.sendMPTProblem(myMPTProblem);
}

/**
* if initial dragged type is OPERATION or ACTIVITY, we will remove related operations from the route
*/
if (["OPERATION", "ACTIVITY"].includes(draggedType)) {
/**
* we filter draggedFeatures to keep only activities
*/
const activityFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "ACTIVITY")
);

/**
* We get Activity entities from Activity Features
*/

const activities = activityFeatures.map((activityFeature) =>
VActivities.getFromFeature(activityFeature, state)
);

/**
* MPT Problem await a list of operations to remove (not activity)
* Hence, we get operations from activities
*/

const operationsToRemove = activities
.flatMap((activity) => Activity.getOperations(activity, state))
.map(Operation.toReferenceToOperation);

/**
* let's create an Action to remove operations
*/
const action = {
type: "UNSCHEDULE_OPERATIONS_FROM_EXISTING_ROUTES",
operations: operationsToRemove,
};

/**
* We have everything to get our MPT problem definition
*/
const myMPTProblem = MPTJobTools.getProblem(state, action);

/**
* We can now send our MPT problem to the API
*/
apiClient.sendMPTProblem(myMPTProblem);
}
}
//...

Now each time we drag and drop a Route on the trashbin we will delete the route.
If the selection is a mix of routes, operations and/or activities the action will depends on the initial dragged type.

Merge routes

If we want to merge routes, we can drag and drop them on each other.

src/index.js
//...
ARMessage.on("DRAG_END", (event) => {
const { draggedFeatures, draggedOnFeatures, tags, draggedType } =
event.payload;
//...
if (tags.includes("TRASHBIN")) {
//...
} else {
/**
* when not dragged on trashbin, we check drag ends on a route
*/
const [targetItinerary] = draggedOnFeatures.filter((f) =>
Features.hasEntityType(f, "ITINERARY")
);

/**
* if initial dragged type is ITINERARY, we will merge the routes
*/
if (draggedType === "ITINERARY") {
/**
* we filter draggedFeatures to keep only itineraries
*/
const itineraryFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "ITINERARY")
);

/**
* we get route ids from itinerary features
*/
const sourceRouteIds = VItineraries.getRouteIds(itineraryFeatures);

if (sourceRouteIds.length > 0 && !!targetItinerary) {
/**
* we get route id from target itinerary feature
*/
const targetRouteId = VItineraries.getRouteId(targetItinerary);

/**
* let's create an Action to merge source routes into target route
*/
const action = {
type: "MERGE_EXISTING_ROUTES",
routes: sourceRouteIds,
to: {
idRoute: targetRouteId,
},
};

/**
* We have everything to get our MPT problem definition
*/
const myMPTProblem = MPTJobTools.getProblem(state, action);
apiClient.sendMPTProblem(myMPTProblem);
}
}

/**
* if initial dragged type is OPERATION or ACTIVITY, we will add related operations to the route
*/
if (["OPERATION", "ACTIVITY"].includes(draggedType)) {
/**
*we get all dragged operations
*/
const operationFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "OPERATION")
);

/**
* we also collect all dragged activity features
*/
const activityFeatures = draggedFeatures.filter((f) =>
Features.hasEntityType(f, "ACTIVITY")
);

/**
* we get operations from both operation and activity features (since Activity hold operations)
*/
const operationsToAdd = VOperations.getFromFeatures(
[...operationFeatures, ...activityFeatures],
state
).map(Operation.toReferenceToOperation);

/**
* if targetItinerary is defined and operationsToAdd is not empty
* we can create an action to add operations to the route
*/
if (targetItinerary && operationsToAdd.length > 0) {
/**
* we get the route id from the itinerary feature
*/
const routeId = VItineraries.getRouteId(targetItinerary);

/**
* let's create an Action to add operations on a given route
*/
const action = {
type: "SCHEDULE_OPERATIONS_ON_EXISTING_ROUTES",
operations: operationsToAdd,
to: {
idRoute: routeId,
},
};

/**
* We have everything to get our MPT problem definition
*/
const myMPTProblem = MPTJobTools.getProblem(state, action);
apiClient.sendMPTProblem(myMPTProblem);
}
}
}
});
//...

Now each time we drag and drop a route (or selection of routes) on another route, it will merge them.
If the selection is a mix of routes, operations and/or activities the action will depends on the initial dragged type.