# 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:

```text
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:

```sh
npm install maplibre-gl pmtiles @protomaps/basemaps
```

Then draw the map:

```js
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> &copy; <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:

```sh
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`).

```sh
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](index.md#coverage). The [API reference](api.md) 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:

```js
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:

```python
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:

```js
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

```sh
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](api.md#errors).
