Gallery

A drive down Highway 1

From the Golden Gate Bridge to Big Sur, by way of Half Moon Bay, Santa Cruz, Monterey, and Carmel: 150 miles and three hours of coastal driving in a minute and a half. Press play and the camera rides along. Pause, and the map is yours to look around.

Build it yourself

The page asks Mapricot's routing to drive from the Golden Gate Bridge to Big Sur, with a stop in each town along the coast so the route keeps to Highway 1 instead of the faster inland freeways. The play button flies the camera along it. It was tested with MapLibre GL JS 6.15 in Chromium.

What you need

Make the page

Make a folder, and in it index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>A drive down Highway 1</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
  </head>
  <body style="margin: 0">
    <button id="play" style="position: absolute; z-index: 1; top: 10px; left: 10px">Play</button>
    <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 key = "mapricot_pk_…";
      const seconds = 90;
      const stops = [
        { lat: 37.8078, lon: -122.4752 }, // Golden Gate Bridge
        { lat: 37.4636, lon: -122.4286 }, // Half Moon Bay
        { lat: 36.9741, lon: -122.0308 }, // Santa Cruz
        { lat: 36.6002, lon: -121.8947 }, // Monterey
        { lat: 36.5552, lon: -121.9233 }, // Carmel
        { lat: 36.3715, lon: -121.9017 }, // Bixby Bridge
        { lat: 36.2704, lon: -121.8081 }, // Big Sur
      ];

      // Routes come as polylines with six decimal places; MapLibre wants [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;
      }

      const response = await fetch("https://api.mapricot.com/v1/route", {
        method: "POST",
        headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
        body: JSON.stringify({ waypoints: stops }),
      });
      if (!response.ok) throw new Error(`Mapricot returned ${response.status}`);
      const { routes } = await response.json();
      const coordinates = decode(routes[0].polyline);

      // How far along the route each point is, in meters.
      const along = [0];
      for (let i = 1; i < coordinates.length; i++) {
        const [[lon1, lat1], [lon2, lat2]] = [coordinates[i - 1], coordinates[i]];
        const x = (lon2 - lon1) * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
        along.push(along[i - 1] + Math.hypot(x, lat2 - lat1) * 111320);
      }
      const total = along.at(-1);

      function at(distance) {
        const i = Math.max(1, along.findIndex((value) => value >= distance));
        const t = (distance - along[i - 1]) / (along[i] - along[i - 1] || 1);
        const [[lon1, lat1], [lon2, lat2]] = [coordinates[i - 1], coordinates[i]];
        return [lon1 + (lon2 - lon1) * t, lat1 + (lat2 - lat1) * t];
      }

      function bearing([lon1, lat1], [lon2, lat2]) {
        return (Math.atan2((lon2 - lon1) * Math.cos((lat1 * Math.PI) / 180), lat2 - lat1) * 180) / Math.PI;
      }

      const map = new maplibregl.Map({
        container: "map",
        style: "https://tiles.mapricot.com/styles/light.json",
        bounds: [
          [-122.6, 36.2],
          [-121.7, 37.85],
        ],
      });
      await map.once("load");

      map.addSource("route", {
        type: "geojson",
        lineMetrics: true,
        data: { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates } },
      });
      map.addLayer({
        id: "route",
        type: "line",
        source: "route",
        layout: { "line-cap": "round", "line-join": "round" },
        paint: { "line-width": 5, "line-color": "#868e96" },
      });

      let progress = 0;
      let playing = false;
      let last;
      let heading;
      function frame(now) {
        if (!playing) return;
        progress = Math.min(1, progress + (now - (last ?? now)) / 1000 / seconds);
        last = now;
        const here = at(progress * total);
        const ahead = bearing(here, at(Math.min(total, progress * total + 1500)));
        heading = heading === undefined ? ahead : heading + (((ahead - heading + 540) % 360) - 180) * 0.05;
        map.jumpTo({ center: here, bearing: heading, pitch: 60, zoom: 13.5 });
        map.setPaintProperty("route", "line-gradient", [
          "step",
          ["line-progress"],
          "#fd7e14",
          Math.max(progress, 0.0001),
          "#868e96",
        ]);
        if (progress < 1) requestAnimationFrame(frame);
        else playing = false;
      }

      document.querySelector("#play").addEventListener("click", () => {
        playing = !playing;
        if (!playing) return;
        if (progress >= 1) progress = 0;
        last = undefined;
        heading = undefined;
        requestAnimationFrame(frame);
      });
    </script>
  </body>
</html>

Put your browser key in place of mapricot_pk_….

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 and the API refuses the key, as Open the page explains. You should see the coast from San Francisco to Big Sur. Press Play for the drive, in 90 seconds.

MapLibre's examples animate a point along a route the same way. To draw a route without the animation, see Draw a route with MapLibre GL JS.

Data