GMPRO docs: Complete all deliveries before pickups in cargo bike logistics
Easy to park and inexpensive to operate, cargo bikes are an increasingly popular option for logistics companies looking for a carbon free alternative to traditional delivery vans. In this blog post, I'll explain the basics of cargo bike logistics, and highlight how the pickupToDeliveryTimeLimit feature of the Google Maps Platform Route Optimization API (GMPRO) can address a common issue: ensuring all deliveries are completed before the rider returns to the depot to pick up new packages.
Part 1: GMPRO: Google Maps Platform route optimization API
Part 2: GMPRO TSP solver: Google Maps with more than 25 waypoints
Part 3: Google Maps route optimization: multi vehicle
Part 4: GMPRO fleet routing app - free route planner for multiple stops
Part 5: GMPRO docs: Fixed vehicle costs
Part 6: GMPRO docs: Territory optimization and route planning
Part 7: GMPRO docs: Solving the VRP with route clustering and soft constraints
Part 8: GMPRO docs: Driver load balancing with soft constraints
Part 9: GMPRO docs: Driver breaks
Part 10: GMPRO docs: Complete all deliveries before pickups in cargo bike logistics (this article)
What is cargo bike logistics?
Cargo bike logistics refers to the use of cargo bikes for transporting goods, typically in urban environments, as part of a sustainable and efficient delivery system. It focuses on the operational processes required to ensure timely and effective deliveries, such as managing loads, optimizing routes, and coordinating pickups and dropoffs.
In a typical cargo bike delivery business, the customer is a local retailer that sells small items such as groceries, medication or coffee beans. These retailers either send items purchased by consumers online directly to a central depot (usually a warehouse or a dark store) or for an extra fee, can have these items picked up by the delivery company.
At the depot, the operations staff assign outbound deliveries based on zones, with each zone handled by a single rider. Due to the limited capacity of the cargo bike, it is not possible to deliver all the packages in one trip. Therefore, the rider may need to return to the depot several times throughout the day to deliver more packages if time permits. Any undelivered packages are kept in the depot overnight for delivery the next day.
How do you model cargo bike logistics as a route optimization problem?
We can model a cargo bike logistics operation in a route optimization system such as GMPRO by framing it as a capacitated pickup and delivery routing problem, where pickups from a central depot are linked to deliveries at the recipient's home.
On the shipment
object, we include:
loadDemands
, which refers to the size of the package the rider is delivering and is measured relative to the loadLimits
of his cargo bike.
/* shipment object */
{
"loadDemands": {
"weight": {
"amount": 1
}
}
}
While on the vehicle
object, we add the corresponding loadLimits
, ensuring that the units match those used for the shipment
. We also configure startTimeWindows.startTime
and endTimeWindows.endTime
appropriately to provide the rider enough time to return to the depot and complete multiple trips.
/* vehicle object */
{
"loadLimits": {
"weight": {
"maxLoad": 3
}
},
"startTimeWindows": [
{
"startTime": "2024-07-08T16:00:00Z"
}
],
"endTimeWindows": [
{
"endTime": "2024-07-08T20:00:00Z"
}
]
}
In the example above, the weight.maxLoad
of 3 means that each cargo bike can load three shipments of weight.amount
1. This means the cargo bike has a capacity of 3 kg, allowing it to carry up to 3 packages weighing 1 kg each. If there are five packages, each weighing 1 kg, the rider would load two or three packages for the first delivery run, then return to the depot for the remaining packages on the second.
Cargo bike logistics worked example
Let's run through a worked example with 1 vehicle
and 5 shipments
. Due to capacity constraints, the vehicle
can hold at most 3 shipments
so we'd expect at least one return to depot trip.
Input
{
"model": {
"shipments": [
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "B",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2688067,
"longitude": -123.1345749
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "A",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2656424,
"longitude": -123.1227316
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "C",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2812657,
"longitude": -123.0306518
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "D",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2808222,
"longitude": -123.0124126
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "E",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2801047,
"longitude": -122.9857553
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
}
],
"vehicles": [
{
"travelMode": "DRIVING",
"startLocation": {
"latitude": 49.2847643,
"longitude": -123.1146621
},
"startTimeWindows": [
{
"startTime": "2024-07-08T16:00:00Z"
}
],
"endTimeWindows": [
{
"endTime": "2024-07-08T20:00:00Z"
}
],
"loadLimits": {
"weight": {
"maxLoad": 3
}
},
"label": "mark-yvr",
"costPerKilometer": 1
}
],
"globalStartTime": "2024-07-08T16:00:00.000Z",
"globalEndTime": "2024-07-08T20:00:00.000Z"
},
"populatePolylines": true
}
Output
{
"routes": [
{
"vehicleLabel": "mark-yvr",
"vehicleStartTime": "2024-07-08T16:00:00Z",
"vehicleEndTime": "2024-07-08T16:52:42Z",
"visits": [
{
"isPickup": true,
"startTime": "2024-07-08T16:08:38Z",
"detour": "0s",
"shipmentLabel": "B",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 1,
"isPickup": true,
"startTime": "2024-07-08T16:08:38Z",
"detour": "0s",
"shipmentLabel": "A",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 2,
"isPickup": true,
"startTime": "2024-07-08T16:08:38Z",
"detour": "0s",
"shipmentLabel": "C",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"startTime": "2024-07-08T16:15:39Z",
"detour": "0s",
"shipmentLabel": "B",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 1,
"startTime": "2024-07-08T16:19:53Z",
"detour": "352s",
"shipmentLabel": "A",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 4,
"isPickup": true,
"startTime": "2024-07-08T16:25:17Z",
"detour": "999s",
"shipmentLabel": "E",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 3,
"isPickup": true,
"startTime": "2024-07-08T16:25:17Z",
"detour": "999s",
"shipmentLabel": "D",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 2,
"startTime": "2024-07-08T16:43:41Z",
"detour": "999s",
"shipmentLabel": "C",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 3,
"startTime": "2024-07-08T16:48:16Z",
"detour": "259s",
"shipmentLabel": "D",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 4,
"startTime": "2024-07-08T16:52:42Z",
"detour": "340s",
"shipmentLabel": "E",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
}
],
"transitions": [
{
"travelDuration": "518s",
"travelDistanceMeters": 2474,
"waitDuration": "0s",
"totalDuration": "518s",
"startTime": "2024-07-08T16:00:00Z",
"vehicleLoads": {
"weight": {}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:08:38Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:08:38Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "421s",
"travelDistanceMeters": 2802,
"waitDuration": "0s",
"totalDuration": "421s",
"startTime": "2024-07-08T16:08:38Z",
"vehicleLoads": {
"weight": {
"amount": "3"
}
}
},
{
"travelDuration": "254s",
"travelDistanceMeters": 1555,
"waitDuration": "0s",
"totalDuration": "254s",
"startTime": "2024-07-08T16:15:39Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "324s",
"travelDistanceMeters": 1670,
"waitDuration": "0s",
"totalDuration": "324s",
"startTime": "2024-07-08T16:19:53Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:25:17Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "1104s",
"travelDistanceMeters": 8339,
"waitDuration": "0s",
"totalDuration": "1104s",
"startTime": "2024-07-08T16:25:17Z",
"vehicleLoads": {
"weight": {
"amount": "3"
}
}
},
{
"travelDuration": "275s",
"travelDistanceMeters": 1485,
"waitDuration": "0s",
"totalDuration": "275s",
"startTime": "2024-07-08T16:43:41Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "266s",
"travelDistanceMeters": 1975,
"waitDuration": "0s",
"totalDuration": "266s",
"startTime": "2024-07-08T16:48:16Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:52:42Z",
"vehicleLoads": {
"weight": {}
}
}
],
"routePolyline": {
"points": "a|xkHtzlnVzAmC`@s@\\k@|AwC~B_EzBaEHOz@wANYHQFO@CHUFUBU@EBQFg@P}A@KDg@@GFi@PiB@CJcAbAaJ@W@OH_ALgABMDWDc@De@Bk@FqJzA?b@@R@B?J?NAv@WREDABAH?LAJ?JAh@?rC@V?\\?Z@@?T?n@@@?@?d@@J?N@HBH@RH\\p@L\\HR`AcAHGNKTMVGD?HAn@Mz@ONCpB]PCRCPARAN?lADlABJ?\\C`AAL?LBZDv@J@@dAb@tAl@b@RLFTFPBTFFBB?D?H?f@BL@PBA~@ArC?^IhBAd@P?rA@p@@j@??ZA|@Fl@CzFCnEAx@Af@AbB?h@@XBf@@RBNDTJh@BHX|@b@fAh@pAVp@P^Rh@?`@LVx@pBl@`BRj@HXJVHN~@~B^`ATh@Nb@Rf@BF@BJVPb@p@jBDJNf@J`@Hf@Jn@@P@\\@\\?rA?|B?Z@r@?fGE`D?d@A|D@vA?b@AL@H?HDl@?d@AzE?v@EpLCtC?t@ErGGfMGVGdDy@AW?QAKj@Kd@AHQbAG\\ABKh@Mr@Kp@Mt@Mp@Mp@Mp@EVUtACJGXCTAFAFEPENEL?@MZIPKTQ\\GJGJEHY`@KNEFGHOJEBC@CBKJGDMJEBEDIDEBE@EBIBCw@E_AAMCKIOIMEIEu@Ew@Gm@Fl@Dv@Dt@DHHLHNBJ@LD~@Bv@@RHANIHEPOPSLMDE@C?C@GNKFIDGJOXa@DIFKFKP]JUHQL[?ADMDODQ@G@GBUFYBKTuADWLq@Lq@Lq@Lu@Jq@Ls@Ji@@CF]PcA@IJe@Jk@P@V?x@@P@@iDCUFgMDsG?u@BuCDqL?w@@{E?e@D]@E?G?m@@iBPED?R?V@J?Ad@?dA?@?r@A`@@a@?s@?A?eA@e@K?WAS?E?QD?e@BgE@}A@aA?uG?E?EAG?Q@yC@q@Co@?UC_@C_@CQEYSiAIWUk@i@wAEIQe@M]KUKe@KUu@eBq@eBOa@c@gASm@i@wACG_A}BQKSi@Q_@Wq@i@qAc@gAY}@CIKi@EUCOASCg@AY?i@@cB@g@@y@BoEeBEoAV@cBAyB@{DrA@p@@q@AsAAEoC?_@@sC@_ALGNEF?N?@?R?R?r@@f@BNJBaE?I?Q?M@i@@U@IBUDYBUJ_@_@AqBEw@EM?QAQ?s@Ca@?WCEAOAOEKAGCKE[Oo@YOG[OQGMCKCKAWCM?eBEBSHy@Fc@XwCPkBNaBZkD^iEFk@Hu@TcCHs@\\uDJ{@BSh@_GZmDFi@Z_DD[F]De@@IDi@LsAFw@D_@BSNaBFk@p@wH@Kf@iGX_Df@mF?g@@KB[Dm@@W@W?c@@e@?eB@uB?eB?wB@i@?A?k@@}B?s@?e@?]?_@?W?oA@eD?u@?c@BaI?G?m@AgDBiC?c@Fm@?w@?eC@cA?i@?sA@eC?mD@{AEo@?kA?}B@w@?mA@q@?mB@mB?E?q@?A?u@@kD?{E@qA?sA?Q@aB?aB@{A?}A@c@?w@?gADg@@uB@]@}@@aA?W@q@@mA?O?g@?O?e@?O?e@?O?w@?qF?sG@mO?sN?sD?aBAaB?{C?a@?gB?a@@cD@cF?yL?u@?G?wB?A?s@@iB?E?O?uA?G?wA?U?W?yA?Y?}A?O@oD?kB?]?o@?aB?_B?kB?qDAoB@}GAM?Q?]?A?_@?mA?u@@W?AA]?[?A@W?w@AwA?e@?AAW@{B?gF?qC@_AM?u@?c@?S?G?u@As@?S?gBA_A?yBCeBBkA@e@@y@?{@@[?eBAuAAaB?m@Aa@?}BA{@?kAAw@A[?Q?K?{@?_A?o@Ac@?qA?u@?O?a@?A?a@?e@@e@@I?O?c@Ak@?c@Aa@AKAAbA?b@?l@Az@?t@?d@@lA?^?v@?t@?r@?pB?b@?R?l@?|@?n@?b@?V?\\?pAApAIl@?x@A~A?`BGLCBKNEBEBA?A?[?S?BK?C@C?S?U?_@?WA_@?U?w@?w@?u@?w@?mBfA?X??]?W?c@?o@?}@?m@?S?c@?qB?s@?u@?w@?_@AmA?e@?u@@{@?m@?c@@cA?aA?q@?gA?I?[?s@AeA@aA?u@@}A?s@@}@?q@?{@?eB?u@@eAAk@?q@?iA?cB?sA?u@@w@?g@@uF?gA@m@?cB?mB@s@?u@?]?Yl@@m@A?gA@cA?oM?uA?cE?yA@}C?qB?c@?oA?oA?_@?k@@gA?aA?}@?qA?iA?i@@a@?}@?cA?S?kC?qA?u@?cA@kA?s@?_A?_E?w@@k@?gB?g@?cA?YAo@?a@?y@?m@?a@?W?_A@aA@W@UB]BYDU?CBMBKFW?CFSJWHSDKBIHOHQFIPWBIVc@BGLg@DUBOBS?C@K@Q@K?M?Y?k@A_@U}CEi@Eg@C]AWAY?CGUCICkA?QAc@?m@?[?EAk@?Y?[?mC?W?uA?mA@c@?q@"
},
"metrics": {
"performedShipmentCount": 5,
"travelDuration": "3162s",
"waitDuration": "0s",
"delayDuration": "0s",
"breakDuration": "0s",
"visitDuration": "0s",
"totalDuration": "3162s",
"travelDistanceMeters": 20300,
"maxLoads": {
"weight": {
"amount": "3"
}
}
},
"routeCosts": {
"model.vehicles.cost_per_kilometer": 20.3
},
"routeTotalCost": 20.3
}
],
"metrics": {
"aggregatedRouteMetrics": {
"performedShipmentCount": 5,
"travelDuration": "3162s",
"waitDuration": "0s",
"delayDuration": "0s",
"breakDuration": "0s",
"visitDuration": "0s",
"totalDuration": "3162s",
"travelDistanceMeters": 20300,
"maxLoads": {
"weight": {
"amount": "3"
}
}
},
"usedVehicleCount": 1,
"earliestVehicleStartTime": "2024-07-08T16:00:00Z",
"latestVehicleEndTime": "2024-07-08T16:52:42Z",
"totalCost": 20.3,
"costs": {
"model.vehicles.cost_per_kilometer": 20.3
}
}
}
Cargo bike logistics problems
In the example above, we have a single rider mark-yvr
assigned to do 5 deliveries, A
, B
, C
, D
and E
. He starts his route in downtown Vancouver and heads to the central depot, where he picks up three packages, A
, B
and C
. He makes his way west to deliver A
and B
(but not C
, which is east of the depot) and returns with C
still on his bike.
This is a frustrating problem because the rider had to return to the depot with package C
, meaning the entire delivery run was made with C
unnecessarily.
Why did this happen? A typical route optimization algorithm focuses solely on minimizing total cost or travel time, without accounting for factors such as "this package is heavy and I don't want to have to carry it any longer than necessary". If we want GMPRO to force all deliveries to be completed before returning to depot (while maintaining route optimality), we need to build this into the optimization.
How to correctly use pickupToDeliveryTimeLimit
The easiest way to force all deliveries to be done before returning to the depot is to use the pickupToDeliveryTimeLimit
field on the shipment
object (docs), which specifies the maximum duration from start of pickup to start of delivery of a shipment. For example, "pickupToDeliveryTimeLimit": "600s"
specifies that the rider has a maximum of 10 minutes (600 seconds) to go from picking up the shipment to dropping it off. If this time limit cannot be met - perhaps because the dropoff location is too far from the pickup point - the shipment will remain unserved. The main challenge is determining an appropriate value for pickupToDeliveryTimeLimit
that ensures shipments are not picked up too early while still allowing sufficient time for them to be delivered in a single delivery run.
Since cargo bike logistics usually involves round trip delivery runs to and from a central depot, we can check to see how long a typical delivery run is.
If we look at package C
, it was picked up at 09:08 and delivered at 09:42, for a total trip time of 34 minutes. This represents an upper bound for our pickupToDeliveryTimeLimit
value. If we made it larger than 34 minutes, the route solution won't change because the pickupToDeliveryTimeLimit
value isn't binding.
Package E
, the last and furthest delivery from the depot, was picked up at 09:25 and delivered at 09:52, resulting in a total trip time of 27 minutes. This represents the lower limit for pickupToDeliveryTimeLimit
. Setting the limit below 27 minutes would make it impossible to deliver package E
, leaving it unserved due to insufficient time to reach the delivery location.
Since we know that 27 min < pickupToDeliveryTimeLimit
< 34 min, let's try setting "pickupToDeliveryTimeLimit": "1800"
, or 30 minutes, on each shipment
object. This is the functional equivalent of adding maximum in-vehicle time as a hard constraint to the routing algorithm.
Input
{
"model": {
"shipments": [
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "B",
"pickupToDeliveryTimeLimit": "1800s",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2688067,
"longitude": -123.1345749
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "A",
"pickupToDeliveryTimeLimit": "1800s",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2656424,
"longitude": -123.1227316
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "C",
"pickupToDeliveryTimeLimit": "1800s",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2812657,
"longitude": -123.0306518
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "D",
"pickupToDeliveryTimeLimit": "1800s",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2808222,
"longitude": -123.0124126
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
},
{
"loadDemands": {
"weight": {
"amount": 1
}
},
"label": "E",
"pickupToDeliveryTimeLimit": "1800s",
"pickups": [
{
"arrivalLocation": {
"latitude": 49.269498,
"longitude": -123.1044087
},
"timeWindows": [
{}
],
"duration": "0s"
}
],
"deliveries": [
{
"arrivalLocation": {
"latitude": 49.2801047,
"longitude": -122.9857553
},
"timeWindows": [
{
"startTime": "2024-07-08T16:00:00Z",
"endTime": "2024-07-08T20:00:00Z"
}
],
"duration": "0s"
}
]
}
],
"vehicles": [
{
"travelMode": "DRIVING",
"startLocation": {
"latitude": 49.2847643,
"longitude": -123.1146621
},
"startTimeWindows": [
{
"startTime": "2024-07-08T16:00:00Z"
}
],
"endTimeWindows": [
{
"endTime": "2024-07-08T20:00:00Z"
}
],
"loadLimits": {
"weight": {
"maxLoad": 3
}
},
"label": "mark-yvr",
"costPerKilometer": 1
}
],
"globalStartTime": "2024-07-08T16:00:00.000Z",
"globalEndTime": "2024-07-08T20:00:00.000Z"
},
"populatePolylines": true
}
Output
{
"routes": [
{
"vehicleLabel": "mark-yvr",
"vehicleStartTime": "2024-07-08T16:00:00Z",
"vehicleEndTime": "2024-07-08T16:53:20Z",
"visits": [
{
"isPickup": true,
"startTime": "2024-07-08T16:08:40Z",
"detour": "0s",
"shipmentLabel": "B",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 1,
"isPickup": true,
"startTime": "2024-07-08T16:08:40Z",
"detour": "0s",
"shipmentLabel": "A",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"startTime": "2024-07-08T16:15:40Z",
"detour": "0s",
"shipmentLabel": "B",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 1,
"startTime": "2024-07-08T16:19:55Z",
"detour": "347s",
"shipmentLabel": "A",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 4,
"isPickup": true,
"startTime": "2024-07-08T16:25:17Z",
"detour": "997s",
"shipmentLabel": "E",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 3,
"isPickup": true,
"startTime": "2024-07-08T16:25:17Z",
"detour": "997s",
"shipmentLabel": "D",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 2,
"isPickup": true,
"startTime": "2024-07-08T16:25:17Z",
"detour": "997s",
"shipmentLabel": "C",
"loadDemands": {
"weight": {
"amount": "1"
}
}
},
{
"shipmentIndex": 2,
"startTime": "2024-07-08T16:44:02Z",
"detour": "0s",
"shipmentLabel": "C",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 3,
"startTime": "2024-07-08T16:48:37Z",
"detour": "258s",
"shipmentLabel": "D",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
},
{
"shipmentIndex": 4,
"startTime": "2024-07-08T16:53:20Z",
"detour": "381s",
"shipmentLabel": "E",
"loadDemands": {
"weight": {
"amount": "-1"
}
}
}
],
"transitions": [
{
"travelDuration": "520s",
"travelDistanceMeters": 2474,
"waitDuration": "0s",
"totalDuration": "520s",
"startTime": "2024-07-08T16:00:00Z",
"vehicleLoads": {
"weight": {}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:08:40Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "420s",
"travelDistanceMeters": 2802,
"waitDuration": "0s",
"totalDuration": "420s",
"startTime": "2024-07-08T16:08:40Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "255s",
"travelDistanceMeters": 1555,
"waitDuration": "0s",
"totalDuration": "255s",
"startTime": "2024-07-08T16:15:40Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "322s",
"travelDistanceMeters": 1670,
"waitDuration": "0s",
"totalDuration": "322s",
"startTime": "2024-07-08T16:19:55Z",
"vehicleLoads": {
"weight": {}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:25:17Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:25:17Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "1125s",
"travelDistanceMeters": 8521,
"waitDuration": "0s",
"totalDuration": "1125s",
"startTime": "2024-07-08T16:25:17Z",
"vehicleLoads": {
"weight": {
"amount": "3"
}
}
},
{
"travelDuration": "275s",
"travelDistanceMeters": 1485,
"waitDuration": "0s",
"totalDuration": "275s",
"startTime": "2024-07-08T16:44:02Z",
"vehicleLoads": {
"weight": {
"amount": "2"
}
}
},
{
"travelDuration": "283s",
"travelDistanceMeters": 2022,
"waitDuration": "0s",
"totalDuration": "283s",
"startTime": "2024-07-08T16:48:37Z",
"vehicleLoads": {
"weight": {
"amount": "1"
}
}
},
{
"travelDuration": "0s",
"waitDuration": "0s",
"totalDuration": "0s",
"startTime": "2024-07-08T16:53:20Z",
"vehicleLoads": {
"weight": {}
}
}
],
"routePolyline": {
"points": "a|xkHtzlnVzAmC`@s@\\k@|AwC~B_EzBaEHOz@wANYHQFO@CHUFUBU@EBQFg@P}A@KDg@@GFi@PiB@CJcAbAaJ@W@OH_ALgABMDWDc@De@Bk@FqJzA?b@@R@B?J?NAv@WREDABAH?LAJ?JAh@?rC@V?\\?Z@@?T?n@@@?@?d@@J?N@HBH@RH\\p@L\\HR`AcAHGNKTMVGD?HAn@Mz@ONCpB]PCRCPARAN?lADlABJ?\\C`AAL?LBZDv@J@@dAb@tAl@b@RLFTFPBTFFBB?D?H?f@BL@PBA~@ArC?^IhBAd@P?rA@p@@j@??ZA|@Fl@CzFCnEAx@Af@AbB?h@@XBf@@RBNDTJh@BHX|@b@fAh@pAVp@P^Rh@?`@LVx@pBl@`BRj@HXJVHN~@~B^`ATh@Nb@Rf@BF@BJVPb@p@jBDJNf@J`@Hf@Jn@@P@\\@\\?rA?|B?Z@r@?fGE`D?d@A|D@vA?b@AL@H?HDl@?d@AzE?v@EpLCtC?t@ErGGfMGVGdDy@AW?QAKj@Kd@AHQbAG\\ABKh@Mr@Kp@Mt@Mp@Mp@Mp@EVUtACJGXCTAFAFEPENEL?@MZIPKTQ\\GJGJEHY`@KNEFGHOJEBC@CBKJGDMJEBEDIDEBE@EBIBCw@E_AAMCKIOIMEIEu@Ew@Gm@Fl@Dv@Dt@DHHLHNBJ@LD~@Bv@@RHANIHELKBCPSLMDE@C?C@GNKFIDGJOXa@DIFKFKP]JUHQL[?ADMDODQ@G@GBUFYBKTuADWLq@Lq@Lq@Lu@Jq@Ls@Ji@@CF]PcA@IJe@Jk@P@V?x@@P@@iDCUFgMDsG?u@BuCDqL?w@@{E?e@D]@E?G?m@@iBPED?R?V@J?Ad@?dA?@?r@A`@@a@?s@?A?eA@e@K?WAS?E?QD?e@BgE@}A@aA?uG?E?EAG?Q@yC@q@Co@?UC_@C_@CQEYSiAIWUk@i@wAEIQe@M]KUKe@KUu@eBq@eBOa@c@gASm@i@wACG_A}BQKSi@Q_@Wq@i@qAc@gAY}@CIKi@EUCOASCg@AY?i@@cB@g@@y@BoEeBEoAV@cBAyB@{DrA@p@@q@AsAAEoC?_@@sC@_ALGNEF?N?@?R?R?r@@f@BNJBaE?I?Q?M@i@@U@IBUDYBUJ_@No@J]BK@ADKh@mAz@oB\\s@DW~@wBxA}CJSBCJYJ[J[H]DW@EH_@@IBQBODa@@Q@W@a@?g@?u@?yC@]?oA?{@?aB@c@?mA?K?g@?A@]?c@?cB@u@?w@?uC?O?U?A@W@U@]@a@BYDYJ_A@C?CPuARiBBO?EHu@DYDYPcBHw@NqAJ}@\\_DPaBBO?ABONqA@MNwAPuANyATgBBWBY@W?U@[@U@mD?E@}C?iA?E?o@?E@q@?qA@aA?w@o@AW?O?KA[?U?S?_@A[?g@As@Aa@AyDAk@?a@?QEo@Cc@?k@Ak@?aAAWB?_@?O?G?o@?_@@eD?u@?c@BaI?G?m@AgDBiC?c@Fm@?w@?eC@cA?i@?sA@eC?mD@{AEo@?kA?}B@w@?mA@q@?mB@mB?E?q@?A?u@@kD?{E@qA?sA?Q@aB?aB@{A?}A@c@?w@?gADg@@uB@]@}@@aA?W@q@@mA?O?g@?O?e@?O?e@?O?w@?qF?sG@mO?sN?sD?aBAaB?{C?a@?gB?a@@cD@cF?yL?u@?G?wB?A?s@?A@gB?E?O?uA?G?wA?U?W?yA?Y?}A?O@oD?kB?]?o@?aB?_B?kB?qDAoB@}GAG?E?Q?]?A?_@?mA?u@@W?AA]?[?A@W?w@AwA?e@?AAW@{B?gF?qC@_AM?u@?c@?S?G?u@As@?S?gBA_A?yBCeBBkA@e@@y@?{@@[?eBAuAAaB?m@Aa@?}BA{@?kAAw@A[?Q?K?{@?_A?o@Ac@?qA?u@?O?a@?A?a@?e@@e@@I?O?c@Ak@?c@Aa@AKAAbA?b@?l@Az@?t@?d@@lA?^?v@?t@?r@?pB?b@?R?l@?|@?n@?b@?V?\\?pAApAIl@?x@A~A?`BGLCBKNEBEBA?A?[?S?BK?C@C?S?U?_@?WA_@?U?w@?w@?u@?w@?mBfA?X??]?W?c@?o@?}@?m@?S?c@?qB?s@?u@?w@?_@AmA?e@?u@@{@?m@?c@@cA?aA?q@?gA?I?[?s@?QAs@@aA?u@@}A?s@@}@?q@?{@?eB?u@@eAAk@?q@?iA?cB?sA?u@@w@?g@@uF?gA@m@?cB?mB@s@?u@?]?Yl@@h@@?mA@_AgA?OA?oM?uA?cE?yA@}C?qB?c@?oA?oA?_@?k@@gA?aA?}@?qA?iA?i@@a@?}@?cA?S?kC?qA?u@?cA@kA?s@?_A?_E?w@@k@?gB?g@?cA?YAo@?a@?y@?m@?a@?W?_A@aA@W@UB]BYDU?CBMBKFW?CFSJWHSDKBIHOHQFIPWBIVc@BGLg@DUBOBS?C@K@Q@K?M?Y?k@A_@U}CEi@Eg@C]AWAY?CGUCICkA?QAc@?m@?[?EAk@?Y?[?mC?W?uA?mA@c@?q@"
},
"metrics": {
"performedShipmentCount": 5,
"travelDuration": "3200s",
"waitDuration": "0s",
"delayDuration": "0s",
"breakDuration": "0s",
"visitDuration": "0s",
"totalDuration": "3200s",
"travelDistanceMeters": 20529,
"maxLoads": {
"weight": {
"amount": "3"
}
}
},
"routeCosts": {
"model.vehicles.cost_per_kilometer": 20.529
},
"routeTotalCost": 20.529
}
],
"metrics": {
"aggregatedRouteMetrics": {
"performedShipmentCount": 5,
"travelDuration": "3200s",
"waitDuration": "0s",
"delayDuration": "0s",
"breakDuration": "0s",
"visitDuration": "0s",
"totalDuration": "3200s",
"travelDistanceMeters": 20529,
"maxLoads": {
"weight": {
"amount": "3"
}
}
},
"usedVehicleCount": 1,
"earliestVehicleStartTime": "2024-07-08T16:00:00Z",
"latestVehicleEndTime": "2024-07-08T16:53:20Z",
"totalCost": 20.529,
"costs": {
"model.vehicles.cost_per_kilometer": 20.529
}
}
}
A quick review of the response confirms that the route sequence is now much better. The rider first picks up packages A
and B
, delivers them, then returns to the depot to pick up packages C
, D
, and E
.
There are more robust methods to ensure all deliveries are completed before starting the next round of pickups. For instance, you could use transitionAttributes
to assign a high cost to transitions from any delivery back to the depot for a pickup if the vehicle still has an active load. Alternatively, you could impose direct penalties when a vehicle picks up a new shipment while carrying undelivered items. However, in my experience, leveraging the pickupToDeliveryTimeLimit
feature is the simplest and most effective way to achieve this goal.
Beyond cargo bike logistics
So as you can see, the pickupToDeliveryTimeLimit
feature of GMPRO is tremendously useful, but its applications go way beyond cargo bike routing. You can use it in school bus routing to enforce reasonable travel time limits to improve student well being. You could also use it in DoorDash-style food delivery to ensure that hot foods stay warm and cold items remain chilled upon reaching the customer.
Using a route optimization algorithm to ensure all deliveries are completed before new pickups is undoubtedly a niche topic, but it’s one that arises frequently enough to warrant this blog post. Hope you liked it!
👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.