路线优化API: 用50行代码将配送成本降低30%
Tutorials

路线优化API: 用50行代码将配送成本降低30%

使用MapAtlas Routing API优化多站点配送路线、执行时间窗口、避开欧盟低排放区,并在50行代码中将结果显示在地图上。

MapAtlas Team11 min read
#route optimization#delivery route#routing api#last mile delivery#multi-stop route#logistics api

Last-mile delivery is the most expensive part of any supply chain. Industry benchmarks consistently put last-mile costs at 53% of total shipping cost. Within that, the biggest controllable variable is route efficiency. A driver completing 15 stops in the wrong order may drive 40% more kilometres than necessary, burning fuel, wearing out the vehicle, and missing delivery time windows that trigger redelivery fees.

Route optimisation is not a hard problem to solve in code anymore. What used to require expensive specialist logistics software is now an API call. This tutorial builds a complete multi-stop route optimiser using the MapAtlas Routing API: a Python script that sends a list of delivery stops, gets back an optimised sequence with total distance and time, applies time window constraints, and handles EU Low Emission Zone restrictions for urban deliveries. A JavaScript snippet then draws the result on a map.

The Python implementation is under 55 lines. The JavaScript map display is an additional 30.

The Last-Mile Cost Problem

To understand what optimisation actually saves, run the numbers for a realistic delivery scenario:

  • Fleet: 10 vans
  • Stops per van per day: 18
  • Current average distance: 210 km/van/day
  • Fuel cost: €0.38/km (diesel, EU average)
  • Driver cost: €22/hour
  • Average current route time: 7.5 hours/day

Current daily cost per van: (210 × €0.38) + (7.5 × €22) = €79.80 + €165 = €244.80/van/day

A 30% distance reduction (achievable with good optimisation on a dense urban network) and a 20% time saving produces:

  • Optimised distance: 147 km → fuel cost: €55.86
  • Optimised time: 6 hours → driver cost: €132
  • Optimised daily cost per van: €187.86/van/day

Saving per van per day: €56.94. For 10 vans over 250 working days: €142,350/year, from one API integration.

The benchmarks above reflect real published figures from last-mile logistics studies. Your specific numbers will vary by geography, vehicle type, and stop density. Dense urban areas see the largest gains because naive sequential routes waste the most distance on unnecessary backtracking.

Naive vs Optimised Routes: A Visual Comparison

The difference between a naive (sequential) route and an optimised one is stark on a map.

Side-by-side comparison of naive and optimised delivery routes

[Image: Two side-by-side map views of Amsterdam. Left map labelled "Sequential (naive)" shows a delivery route that zigzags across the city with visible backtracking, lines crossing over each other. Right map labelled "Optimised" shows the same 15 stops connected in a clean loop with no line crossings and a visibly shorter total path.]

Naive routing happens when you feed stops in the order they were entered, first customer who placed an order is first on the route, regardless of geography. In a city like Amsterdam or Berlin, this creates the "spaghetti route" problem: your driver constantly crossing their own path.

Optimisation solves the Travelling Salesman Problem (TSP) for your stop set. For 15–20 stops this is computationally tractable in milliseconds. For larger fleets with hundreds of stops, vehicle routing problem (VRP) solvers handle the additional constraints of multiple vehicles and capacity limits.

Step 1: Structure Your Delivery Data

Each stop needs a location and, for time-windowed deliveries, a time_window specifying when the delivery is acceptable.

import requests
import json

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.mapatlas.eu/v1"

# Depot (start and end point)
depot = {
    "lat": 52.3402,
    "lng": 4.8952,
    "name": "Warehouse - Sloterdijk"
}

# Delivery stops with optional time windows
stops = [
    { "lat": 52.3726, "lng": 4.8971, "name": "Albert Heijn Jordaan",
      "time_window": { "start": "09:00", "end": "12:00" } },
    { "lat": 52.3601, "lng": 4.9123, "name": "Café De Jaren",
      "time_window": { "start": "08:00", "end": "11:00" } },
    { "lat": 52.3780, "lng": 4.8801, "name": "Westergasfabriek Events",
      "time_window": { "start": "10:00", "end": "14:00" } },
    { "lat": 52.3545, "lng": 4.9041, "name": "Hotel V Nesplein",
      "time_window": None },
    { "lat": 52.3620, "lng": 4.8820, "name": "Vondelpark Paviljoen",
      "time_window": { "start": "07:00", "end": "10:00" } }
]

Step 2: Call the Route Optimisation Endpoint

POST the depot and stop list to the optimised routing endpoint. The API returns the stops in the most efficient visit order along with the total route distance and duration.

def optimise_route(depot, stops, vehicle_profile="van-euro6"):
    """
    Request an optimised multi-stop route from the MapAtlas Routing API.
    vehicle_profile options: van-euro6, van-diesel-euro5, electric-van, bike
    """
    waypoints = [
        {
            "lat": s["lat"],
            "lng": s["lng"],
            "name": s["name"],
            **({"time_window": s["time_window"]} if s.get("time_window") else {})
        }
        for s in stops
    ]

    payload = {
        "origin": { "lat": depot["lat"], "lng": depot["lng"] },
        "destination": { "lat": depot["lat"], "lng": depot["lng"] },  # return to depot
        "waypoints": waypoints,
        "optimise": True,
        "vehicle_profile": vehicle_profile,
        "avoid_low_emission_zones": True  # auto-avoids LEZs for non-compliant profiles
    }

    response = requests.post(
        f"{BASE_URL}/routing/optimise",
        json=payload,
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json"
        }
    )
    response.raise_for_status()
    return response.json()

result = optimise_route(depot, stops)

Step 3: Parse and Display the Optimised Route

The API response includes the stops in optimised order, cumulative ETAs for each stop, total distance, and total duration.

def display_route_summary(result):
    route = result["route"]
    print(f"\n--- Optimised Route Summary ---")
    print(f"Total distance : {route['total_distance_km']:.1f} km")
    print(f"Total duration : {route['total_duration_min']:.0f} min")
    print(f"Stops          : {len(route['waypoints'])}\n")

    print(f"  START  {depot['name']}")
    for i, stop in enumerate(route["waypoints"], 1):
        eta     = stop["eta"]
        tw      = stop.get("time_window")
        on_time = "(on time)" if tw and tw["start"] <= eta <= tw["end"] else ""
        print(f"  {i:>2}.   {stop['name']:<35} ETA {eta}  {on_time}")
    print(f"  END    {depot['name']}")

    print(f"\nEstimated fuel saving vs sequential: "
          f"{result.get('saving_vs_naive_km', 0):.1f} km "
          f"({result.get('saving_pct', 0):.0f}%)")

display_route_summary(result)

Sample output for the five stops above:

--- Optimised Route Summary ---
Total distance : 38.4 km
Total duration : 94 min
Stops          : 5

  START  Warehouse - Sloterdijk
   1.   Vondelpark Paviljoen               ETA 07:48  (on time)
   2.   Café De Jaren                      ETA 08:31  (on time)
   3.   Albert Heijn Jordaan               ETA 09:15  (on time)
   4.   Hotel V Nesplein                   ETA 10:02
   5.   Westergasfabriek Events            ETA 10:44  (on time)
  END    Warehouse - Sloterdijk

Estimated fuel saving vs sequential: 14.2 km (27%)

Step 4: EU Low Emission Zone Handling

Amsterdam's ZTL zone, Paris's Crit'Air system, and Berlin's Umweltzone restrict certain vehicle types from central areas at specified times. A route that looks efficient on distance alone may be invalid for your vehicle.

The avoid_low_emission_zones: true parameter combined with the vehicle_profile automatically routes around restricted zones for non-compliant vehicles. For electric and Euro 6 vehicles, LEZs are passable and the parameter has no effect.

# Example: diesel Euro 5 van, will be re-routed around Amsterdam ZTL
result_euro5 = optimise_route(depot, stops, vehicle_profile="van-diesel-euro5")

# Example: electric van, LEZ restrictions do not apply
result_electric = optimise_route(depot, stops, vehicle_profile="electric-van")

print(f"Euro 5 route distance  : {result_euro5['route']['total_distance_km']:.1f} km")
print(f"Electric route distance: {result_electric['route']['total_distance_km']:.1f} km")
# Electric route will typically be shorter as it can use LEZ-restricted roads

For logistics operations planning a transition from diesel to electric, comparing these two outputs per route provides a direct quantification of the range improvement available from electrification.

Step 5: Display the Optimised Route on a Map

Take the route geometry from the API response and render it as a line layer in JavaScript.

import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

// routeResult is the parsed API JSON response passed to the frontend
function renderOptimisedRoute(map, routeResult) {
  const { waypoints, geometry, total_distance_km, total_duration_min } = routeResult.route;

  map.on('load', () => {
    // Route line
    map.addSource('optimised-route', { type: 'geojson', data: { type: 'Feature', geometry } });
    map.addLayer({
      id: 'route-line',
      type: 'line',
      source: 'optimised-route',
      layout: { 'line-join': 'round', 'line-cap': 'round' },
      paint: { 'line-color': '#2563EB', 'line-width': 4 }
    });

    // Stop markers with sequence numbers
    waypoints.forEach((stop, i) => {
      const el = document.createElement('div');
      el.textContent = i + 1;
      el.style.cssText = `
        width:28px;height:28px;border-radius:50%;background:#2563EB;color:#fff;
        display:flex;align-items:center;justify-content:center;font-weight:700;
        font-size:13px;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3)
      `;

      new mapmetricsgl.Marker({ element: el })
        .setLngLat([stop.lng, stop.lat])
        .setPopup(
          new mapmetricsgl.Popup().setHTML(`
            <strong>${i + 1}. ${stop.name}</strong>
            <p>ETA: ${stop.eta}</p>
          `)
        )
        .addTo(map);
    });

    // Fit map to route bounds
    const coords = geometry.coordinates;
    const bounds = coords.reduce(
      (b, c) => b.extend(c),
      new mapmetricsgl.LngLatBounds(coords[0], coords[0])
    );
    map.fitBounds(bounds, { padding: 48 });

    // Summary panel
    document.getElementById('route-summary').innerHTML = `
      <strong>${total_distance_km.toFixed(1)} km</strong> ·
      <strong>${total_duration_min.toFixed(0)} min</strong> ·
      ${waypoints.length} stops
    `;
  });
}

const map = new mapmetricsgl.Map({
  container: 'route-map',
  style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
  center: [4.9041, 52.3676],
  zoom: 12
});

renderOptimisedRoute(map, routeResult);

Optimised multi-stop delivery route rendered on a map with numbered stop markers

[Image: A map showing Amsterdam with a blue route line connecting five numbered circular markers in sequence. A summary bar above the map reads "38.4 km · 94 min · 5 stops". The route makes a clean loop with no visible backtracking.]

Calculating Your Real Savings

Once you have the API response in hand, the saving calculation is straightforward. The saving_vs_naive_km field in the response gives you distance saved directly. From that, derive cost savings:

def calculate_savings(result, fuel_cost_per_km=0.38, driver_cost_per_hour=22.0,
                       days_per_year=250, fleet_size=10):
    saving_km    = result.get("saving_vs_naive_km", 0)
    saving_hours = saving_km / 50  # assume 50 km/h average

    daily_fuel_saving   = saving_km * fuel_cost_per_km
    daily_driver_saving = saving_hours * driver_cost_per_hour
    daily_total         = daily_fuel_saving + daily_driver_saving

    annual_fleet_saving = daily_total * days_per_year * fleet_size

    print(f"Distance saved per route : {saving_km:.1f} km")
    print(f"Time saved per route     : {saving_hours * 60:.0f} min")
    print(f"Daily saving (1 vehicle) : €{daily_total:.2f}")
    print(f"Annual saving ({fleet_size} vehicles): €{annual_fleet_saving:,.0f}")

calculate_savings(result)

Time Window Optimisation

Delivering to a bakery at 06:00 and a restaurant at 14:00 while minimising total route distance is a constrained optimisation problem. The API handles this automatically, you only need to provide the windows:

# Time-sensitive stops, the API will schedule these within their windows
stops_with_windows = [
    { "lat": 52.3726, "lng": 4.8971, "name": "Bakery",
      "time_window": { "start": "05:30", "end": "07:00" } },
    { "lat": 52.3620, "lng": 4.8820, "name": "Café",
      "time_window": { "start": "07:00", "end": "09:00" } },
    { "lat": 52.3545, "lng": 4.9041, "name": "Restaurant",
      "time_window": { "start": "13:00", "end": "15:00" } }
]

If any time window constraint cannot be satisfied given the depot departure time and current traffic model, the API returns a constraint_violations array listing which stops could not be reached on time. Your dispatch software can then alert the driver or suggest an earlier departure.

What to Build on Top of This

Route optimisation is the foundation. Once it is running, the natural extensions are:

The Logistics and Delivery industry page and the Fleet Management industry page cover additional MapAtlas features relevant to dispatch software, including multi-vehicle VRP and return-to-depot optimisation.

Getting Started

Frequently Asked Questions

How does route optimisation reduce delivery costs?

Route optimisation reorders multi-stop delivery sequences to minimise total distance and drive time. Studies consistently show 20–35% reductions in distance driven versus a naive sequential route. For a vehicle driving 200 km/day at €0.35/km fuel cost, a 30% reduction saves around €21 per vehicle per day, roughly €5,000 per year per vehicle.

What are time windows in route optimisation?

Time windows are delivery constraints that require a stop to be visited within a specified time range, for example, a business that accepts deliveries only between 09:00 and 12:00. The optimiser must respect all time windows while still minimising total route distance, which is a significantly harder computational problem than unconstrained optimisation.

Does the MapAtlas Routing API handle EU Low Emission Zones?

Yes. The MapAtlas Routing API includes road restriction data for EU Low Emission Zones including Amsterdam's ZTL, the Paris Crit'Air zone, and Berlin's Umweltzone. Pass the vehicle profile (diesel Euro 5, petrol, electric) as a parameter and the router will automatically avoid restricted zones for non-compliant vehicles.

觉得有用?分享给他人吧。

Back to Blog