10-minute walk to a park map with MapLibre GL JS
A 10-minute walk, about half a mile, is the yardstick park planners use for whether a park is close to home. This map shows everywhere in Portland within a 10-minute walk of one of its parks and natural areas. The parks are dark green and the walks to them light green, on Mapricot's default map.
The biggest parks are named, and the rest show their names as you zoom in or point at them. Click anywhere for the 10-minute walk from there, and the parks it reaches.
Most of the rest is industrial land along the Columbia and the Willamette and around the airport, where few people live. The largest gaps in neighborhoods are in East Portland, east of I-205.
The parks are the open parks and natural areas in Metro's RLIS that people can walk in, in Portland and just outside it. Trails, greenways, parkways, and corridors, which are long strips beside a path or a road, are left out. The walks are measured from the edge of each park, not its middle, since a big park like Forest Park is miles across.
Mapricot's isochrone API returns the area you can reach from one starting point, so the edges of the parks have a point about every 250 m, 2,425 in all. The light green is the 10-minute walks from all of them, merged into one.
Build it yourself
The page below asks Metro for the parks in inner Southeast Portland, puts a point every 250 m around each park's edge, asks for the 10-minute walk from each point, and merges the walks into one area with Turf. It's the map above for a few neighborhoods, without the colors or the clicks. It was tested with MapLibre GL JS 6.15 and Turf 7.3 in Chromium.
What you need
A web server to serve the page, and a browser key with the Routing and Map tiles products, made as API keys shows. Add http://localhost:* to its allowed origins, so it works from your computer while you build. Signing up is free, with no credit card, and includes 250,000 requests every 30 days. Map tiles don't count toward them. The page sends about 120 isochrone requests each time it loads.
Make the page
Make a folder, and in it index.html, with your browser key in place of both mapricot_pk_…:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A 10-minute walk to a park</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
<script type="importmap">
{
"imports": {
"maplibre-gl": "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm",
"mapricot": "https://cdn.jsdelivr.net/npm/mapricot@0.17.0/+esm",
"@turf/turf": "https://cdn.jsdelivr.net/npm/@turf/turf@7.3.5/+esm"
}
}
</script>
</head>
<body style="margin: 0">
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "maplibre-gl";
import { Mapricot } from "mapricot";
import { featureCollection, flatten, lineChunk, polygonToLine, union } from "@turf/turf";
const client = new Mapricot({ apiKey: "mapricot_pk_…" });
const box = [-122.665, 45.5, -122.6, 45.53];
const map = new maplibregl.Map({
container: "map",
style: "https://api.mapricot.com/tiles/v1/styles/default.json?key=mapricot_pk_…",
bounds: [box.slice(0, 2), box.slice(2)],
});
await map.once("load");
const query = new URLSearchParams({
where: "UNITTYPE IN ('Park', 'Natural Area') AND RECREATION IN ('Yes', 'Some') AND STATUS = 'Open'",
geometry: box.join(","),
geometryType: "esriGeometryEnvelope",
inSR: 4326,
outSR: 4326,
f: "geojson",
});
const response = await fetch(`https://services2.arcgis.com/McQ0OlIABe29rJJy/ArcGIS/rest/services/Outdoor_Recreation_and_Conservation_Areas_ORCA/FeatureServer/5/query?${query}`);
const parks = await response.json();
const starts = parks.features.flatMap((park) =>
flatten(polygonToLine(park)).features.flatMap((edge) => lineChunk(edge, 0.25).features.map((chunk) => chunk.geometry.coordinates[0])),
);
const walks = [];
for (let i = 0; i < starts.length; i += 4) {
const batch = starts.slice(i, i + 4).map(([lon, lat]) => client.routing.isochrone({ location: { lat, lon }, minutes: [10], mode: "walk" }));
for (const { features } of await Promise.all(batch)) walks.push(...features);
}
const walk = union(featureCollection(walks));
map.addSource("walk", { type: "geojson", data: walk });
map.addLayer({ id: "walk", type: "fill", source: "walk", paint: { "fill-color": "#79d8a2" } }, "water");
map.addSource("parks", { type: "geojson", data: parks });
map.addLayer({ id: "parks", type: "fill", source: "parks", paint: { "fill-color": "#248f52" } }, "water");
</script>
</body>
</html>
- Metro's parks layer is an ArcGIS feature service, which answers requests from any website.
queryreturns the parks that touchboxas GeoJSON, in longitude and latitude withoutSR=4326andf=geojson. Thewhereclause keeps the open parks and natural areas people can walk in, as the map above does. polygonToLineturns each park into the line around its edge,flattensplits a park in several pieces into one line each, andlineChunkcuts each line into stretches of 250 m. The first point of each stretch is a starting point.client.routing.isochronecomes from Mapricot's JavaScript client, loaded from jsDelivr like MapLibre. It takes one starting point, so the page asks for each point's walk, four at a time, and keeps each walk's area. Each one counts as a request.unionmerges the walks into one area, which covers everywhere at least one park's edge reaches in 10 minutes.- The walk and the parks go in under the style's
water, so the river and the street names stay on top.
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 parks of inner Southeast Portland in dark green, and after about 15 seconds the area within a 10-minute walk of them in a lighter green. Everywhere left uncolored is further than that.
To map another city, change box and swap Metro's parks for your own, such as OpenStreetMap's leisure=park areas. A bigger area takes more requests, and the map above took 2,425.
Data
- Parks and city limits: Outdoor Recreation and Conservation Areas and City limits, Metro's Regional Land Information System
- Regional Land Information System data are a product of Metro and made publicly available according to the license agreement found in https://rlisdiscovery.oregonmetro.gov/pages/open-database-license.
- Walks: Mapricot's routing, on OpenStreetMap's streets and paths (ODbL)
- Map: OpenStreetMap contributors, in Mapricot's map tiles