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
- A browser key with the Routing product, made as Getting started shows. Add
http://localhost:*to its allowed origins, so it works from your computer while you build. - The map tiles need no key.
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_….
alongholds how far along the route each point is. It treats each short stretch as flat, which is close enough at this scale.atfinds the point a given distance along the route, between the two points on either side of it.- Each frame moves the camera to the point reached so far, tilted and turned to face a point 1,500 meters ahead. The turn eases in a little at a time, so bends in the road don't jerk the view.
lineMetricson the source lets the line useline-progress, so aline-gradientcolors the route orange up to the point reached and gray after it.
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
- Route: Mapricot's routing, from OpenStreetMap contributors (ODbL)
- Map: OpenStreetMap contributors, in Mapricot's map tiles