Coffee within a walk
The ten cafés nearest a corner in Portland's Buckman neighborhood, and the walk to each, darker for the longer walks. Click anywhere to see the cafés around there instead: in most cities, the map fills with routes in a second or two.
Build it yourself
A click sends one nearby search for food:coffee_shop within half a mile, then asks routing for a walking route to each café, one at a time. It's the map above without the list of cafés. It was tested with MapLibre GL JS 6.15 in Chromium.
What you need
- A browser key with the Places and Routing products, 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>Coffee within a walk</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 key = "mapricot_pk_…";
const empty = { type: "FeatureCollection", features: [] };
async function mapricot(path, body) {
const response = await fetch(`https://api.mapricot.com${path}`, {
method: body ? "POST" : "GET",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: body && JSON.stringify(body),
});
if (!response.ok) throw new Error(`Mapricot returned ${response.status}`);
return response.json();
}
// 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 map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/light.json",
center: [-122.6375, 45.5235],
zoom: 15,
});
await map.once("load");
map.addSource("walks", { type: "geojson", data: empty });
map.addLayer({
id: "walks",
type: "line",
source: "walks",
layout: { "line-cap": "round", "line-join": "round" },
paint: {
"line-width": 4,
"line-color": ["interpolate", ["linear"], ["get", "minutes"], 0, "#ffc078", 5, "#fd7e14", 15, "#9a3412"],
},
});
map.addSource("cafes", { type: "geojson", data: empty });
map.addLayer({
id: "cafes",
type: "circle",
source: "cafes",
paint: { "circle-radius": 5, "circle-color": "#fd7e14", "circle-stroke-color": "#ffffff", "circle-stroke-width": 2 },
});
async function walkFrom(lat, lon) {
const start = { lat: Number(lat.toFixed(6)), lon: Number(lon.toFixed(6)) };
const { results } = await mapricot(
`/v1/nearby?lat=${start.lat}&lon=${start.lon}&category=food:coffee_shop&radius=800&limit=10`,
);
const walks = [];
for (const place of results) {
const { routes } = await mapricot("/v1/route", { waypoints: [start, { lat: place.lat, lon: place.lon }], mode: "walk" });
walks.push({
type: "Feature",
properties: { minutes: routes[0].durationSeconds / 60 },
geometry: { type: "LineString", coordinates: decode(routes[0].polyline) },
});
}
map.getSource("walks").setData({ type: "FeatureCollection", features: walks });
map.getSource("cafes").setData({
type: "FeatureCollection",
features: results.map((place) => ({
type: "Feature",
properties: { name: place.name },
geometry: { type: "Point", coordinates: [place.lon, place.lat] },
})),
});
}
map.on("click", ({ lngLat }) => walkFrom(lngLat.lat, lngLat.wrap().lng));
map.on("click", "cafes", (event) => {
const [cafe] = event.features;
new maplibregl.Popup({ offset: 10 }).setLngLat(cafe.geometry.coordinates).setText(cafe.properties.name).addTo(map);
});
await walkFrom(45.5235, -122.6375);
</script>
</body>
</html>
Put your browser key in place of mapricot_pk_….
mapricotsends a GET, or a POST when it has a body, with your key, and throws if the API answers with an error.- Routes come back as encoded polylines, which
decodeturns into the[lon, lat]pairs a GeoJSON line needs. - The API takes coordinates with up to six decimal places, so
walkFromrounds the clicked point.wrapkeeps its longitude between -180 and 180 on a map that's been panned around the world. - Each route's line is colored by its walking time, from light orange for a minute or two to brown for a quarter of an hour.
- That's eleven requests a click, which is why the map above loads its first view from requests made when the site was built.
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 ten walks from a corner in Buckman, in Portland. Click anywhere for the cafés around it.
The cafés come back nearest first as the crow flies, but the walk can put one across a river or a highway further away. For the same nearby search with markers and a list, see Add a search box to a MapLibre map and Add markers to MapLibre GL JS.
Data
- Cafés: Mapricot's places, from OpenStreetMap and Overture Maps
- Walking routes: Mapricot's routing, from OpenStreetMap contributors (ODbL)
- Map: OpenStreetMap contributors, in Mapricot's map tiles