Gallery

All roads lead to Rome

The fastest drive to the Colosseum from 388 towns, spread evenly across Europe. Where routes share a road, its line gets thicker, until the last miles into Rome carry all of them.

Click anywhere in Europe to drive from there to Rome.

Build it yourself

A Node.js script routes from about 390 towns to the Colosseum and saves the roads they share as GeoJSON, and a page draws them on a dark style of its own. It's a simpler version of the map above, without the starting towns or the route on click. It was tested with MapLibre GL JS 6.15 in Chromium and Node.js 22.

What you need

Collect the routes

Make a folder for the map, and in it a file called routes.mjs:

import { writeFile } from "node:fs/promises";

const colosseum = { lat: 41.8902, lon: 12.4922 };

// Routes come as polylines with six decimal places; this gives [lon, lat] pairs.
function decode(polyline) {
  const points = [];
  const position = [0, 0];
  let index = 0;
  while (index < polyline.length) {
    for (const axis of [0, 1]) {
      let shift = 0;
      let value = 0;
      let byte;
      do {
        byte = polyline.charCodeAt(index++) - 63;
        value |= (byte & 0x1f) << shift;
        shift += 5;
      } while (byte >= 0x20);
      position[axis] += value & 1 ? ~(value >> 1) : value >> 1;
    }
    points.push([position[1] / 1e6, position[0] / 1e6]);
  }
  return points;
}

// The most populous town in each cell of a grid over Europe, one degree of latitude by one and a half of longitude.
const response = await fetch(
  "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_10m_populated_places_simple.geojson",
);
const places = await response.json();
const outside = new Set(["DZA", "LBY", "MAR", "SYR", "TUN", "TUR", "CYP"]);
const towns = new Map();
for (const { properties: town } of places.features) {
  const { latitude: lat, longitude: lon } = town;
  if (lon < -10.5 || lon > 32 || lat < 36 || lat > 60.5 || outside.has(town.adm0_a3)) continue;
  const cell = `${Math.floor(lat)},${Math.floor(lon / 1.5)}`;
  if (!towns.has(cell) || town.pop_max > towns.get(cell).pop_max) towns.set(cell, town);
}

// The drive from each town, rounded to two decimal places (about half a mile) so routes on the same road line up.
const routes = [];
for (const town of towns.values()) {
  const response = await fetch("https://api.mapricot.com/v1/route", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.MAPRICOT_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ waypoints: [{ lat: town.latitude, lon: town.longitude }, colosseum] }),
  });
  if (!response.ok) {
    console.warn(`No route from ${town.name}: ${response.status}`);
    continue;
  }
  const { routes: [route] } = await response.json();
  const points = decode(route.polyline).map(([lon, lat]) => [Math.round(lon * 100) / 100, Math.round(lat * 100) / 100]);
  routes.push(points.filter((point, i) => i === 0 || point.join() !== points[i - 1].join()));
}

// How many routes use each stretch between two rounded points.
const segment = (a, b) => `${a}|${b}`;
const counts = new Map();
for (const points of routes) {
  for (let i = 1; i < points.length; i++) {
    const key = segment(points[i - 1], points[i]);
    counts.set(key, (counts.get(key) ?? 0) + 1);
  }
}

// Each stretch drawn once, joined into lines while the count stays the same.
const drawn = new Set();
const features = [];
for (const points of routes) {
  let line = null;
  for (let i = 1; i < points.length; i++) {
    const key = segment(points[i - 1], points[i]);
    if (drawn.has(key) || counts.get(key) !== line?.properties.routes) line = null;
    if (drawn.has(key)) continue;
    drawn.add(key);
    if (!line) {
      line = { type: "Feature", properties: { routes: counts.get(key) }, geometry: { type: "LineString", coordinates: [points[i - 1]] } };
      features.push(line);
    }
    line.geometry.coordinates.push(points[i]);
  }
}

await writeFile("rome.geojson", JSON.stringify({ type: "FeatureCollection", features }));
console.log(`Saved ${routes.length} routes as ${features.length} lines`);

Run it with your server key:

MAPRICOT_KEY=mapricot_sk_… node routes.mjs

It sends one request at a time and takes a few minutes, then prints Saved 388 routes as 1296 lines, or close to it, and writes rome.geojson.

Make the page

Next to rome.geojson, make index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>All roads lead to Rome</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
  </head>
  <body style="margin: 0">
    <div id="map" style="height: 100vh"></div>
    <script type="module">
      import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";

      const width = ["interpolate", ["linear"], ["sqrt", ["get", "routes"]], 1, 0.8, 20, 10];

      const map = new maplibregl.Map({
        container: "map",
        style: {
          version: 8,
          sources: {
            protomaps: { type: "vector", url: "https://tiles.mapricot.com/planet.json" },
            routes: { type: "geojson", data: "rome.geojson", attribution: "Towns: Natural Earth" },
          },
          layers: [
            { id: "background", type: "background", paint: { "background-color": "#0d1117" } },
            { id: "earth", type: "fill", source: "protomaps", "source-layer": "earth", paint: { "fill-color": "#1c2128" } },
            {
              id: "water",
              type: "fill",
              source: "protomaps",
              "source-layer": "water",
              filter: ["==", ["geometry-type"], "Polygon"],
              paint: { "fill-color": "#0d1117" },
            },
            {
              id: "borders",
              type: "line",
              source: "protomaps",
              "source-layer": "boundaries",
              filter: ["<=", ["get", "kind_detail"], 2],
              paint: { "line-color": "#373e47", "line-width": 0.6 },
            },
            {
              id: "routes",
              type: "line",
              source: "routes",
              layout: { "line-cap": "round", "line-join": "round", "line-sort-key": ["get", "routes"] },
              paint: {
                "line-color": ["interpolate", ["linear"], ["get", "routes"], 1, "#9a4a14", 20, "#fd7e14", 150, "#ffc078"],
                "line-width": ["interpolate", ["linear"], ["zoom"], 3, ["*", 0.5, width], 8, ["*", 1.5, width]],
              },
            },
          ],
        },
        bounds: [-10, 36, 32, 60.5],
      });
    </script>
  </body>
</html>

Open it

Serve the folder with npx serve or python3 -m http.server, and open the address it prints. Opened straight from the file, the map stays blank, as Open the page explains. You should see Europe in the dark, with orange roads thickening as they near Rome.

The idea comes from moovel lab's Roads to Rome, which routed almost 500,000 starting points across Europe. To draw a single route, see Draw a route with MapLibre GL JS.

Data