Connect
Two things connect to Carpathian Maps: the map, which any browser or app draws from the public map files, and directions, which your backend asks for with the key.
The map
The map files are public and need no key. Point any MapLibre client at the map base URL:
https://maps.carpathian.ai/map
| File | Holds |
|---|---|
planet.pmtiles | The vector tiles, one PMTiles archive read with HTTP Range requests |
fonts/{fontstack}/{range}.pbf | The label fonts |
sprites/v4/light, sprites/v4/dark | The map icons |
MapLibre GL JS
Install the three libraries the map page on this server uses:
npm install maplibre-gl pmtiles @protomaps/basemaps
Then draw the map:
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { Protocol } from "pmtiles";
import { layers, namedFlavor } from "@protomaps/basemaps";
const base = "https://maps.carpathian.ai/map";
maplibregl.addProtocol("pmtiles", new Protocol().tile);
const map = new maplibregl.Map({
container: "map",
center: [-93.625, 41.5868],
zoom: 12,
style: {
version: 8,
glyphs: base + "/fonts/{fontstack}/{range}.pbf",
sprite: base + "/sprites/v4/light",
sources: {
protomaps: {
type: "vector",
url: "pmtiles://" + base + "/planet.pmtiles",
attribution:
'<a href="https://protomaps.com">Protomaps</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
},
},
layers: layers("protomaps", namedFlavor("light"), { lang: "en" }),
},
});
For a dark map, use sprites/v4/dark and namedFlavor("dark"). The map page here runs MapLibre GL JS 5.24, PMTiles 4.5 and Protomaps Basemaps 5.7, so those versions are known to work with these files.
Credits
Keep the OpenStreetMap credit visible on every map. The ODbL requires it, and the attribution above shows it.
Other sites
A browser on another site reads the map files only if the server allows that site's origin. The operator lists the allowed origins in MAPS_CORS_ORIGINS, and * allows any. If tiles fail with a CORS error in the browser console, ask the operator to add your origin, for example https://app.example.com.
To check that the map file answers:
curl -I https://maps.carpathian.ai/map/planet.pmtiles
Directions
Routes and isochrones need the key, sent as a bearer token. The key is one shared secret for the whole server, so keep it on your backend and never ship it to a browser or app. The operator gives it to you (in the console, key show).
curl https://maps.carpathian.ai/v1/route \
-H "Authorization: Bearer $MAPS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"origin":{"lat":41.5868,"lng":-93.625},"destination":{"lat":41.6005,"lng":-93.6091},"mode":"walk"}'
mode is walk, bike, drive or transit. Directions work only inside the areas listed under Coverage. The API reference has every field of the answer.
From a backend
Your users' browsers call your backend, and your backend calls this server with the key and passes the answer on. Your backend decides who may ask for directions and how often; this server only knows the key.
Node 18 or later:
async function route(origin, destination, mode) {
const res = await fetch("https://maps.carpathian.ai/v1/route", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.MAPS_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ origin, destination, mode }),
});
const body = await res.json();
if (!res.ok) throw new Error(body.error);
return body.route;
}
Python, standard library only. urlopen raises HTTPError on any status other than 2xx:
import json, os, urllib.request
def route(origin, destination, mode):
req = urllib.request.Request(
"https://maps.carpathian.ai/v1/route",
data=json.dumps({"origin": origin, "destination": destination, "mode": mode}).encode(),
headers={"Authorization": "Bearer " + os.environ["MAPS_API_KEY"], "Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=10) as r:
return json.load(r)["route"]
Draw a route
path is [[lat, lng], ...], and GeoJSON wants [lng, lat], so swap each pair. Add the layer once the map has loaded:
map.addSource("route", {
type: "geojson",
data: {
type: "Feature",
geometry: { type: "LineString", coordinates: route.path.map(([lat, lng]) => [lng, lat]) },
},
});
map.addLayer({ id: "route", type: "line", source: "route", paint: { "line-color": "#2563eb", "line-width": 5 } });
Isochrone geometries are already GeoJSON in [lng, lat] order and go straight into a fill layer.
Check the connection
curl https://maps.carpathian.ai/healthz
{"valhalla": true, "otp": true} means both route engines answer: Valhalla for walk, bike and drive, OpenTripPlanner for transit. A 503 from /v1/route means the engine for that mode is down or rebuilding, so retry later. Every status is listed under Errors.