# Vector basemap | Rijwind docs

# Vector basemap

The Rijwind basemap is delivered as PMTiles — a single addressable file
served from a global CDN. MapLibre GL JS fetches byte ranges from it as
the user pans and zooms; you don't run a tile server.

## How it works

The quickest path is the `@rijwind/sdk` `Map` — it renders the basemap,
handles tile delivery, and keeps everything fresh from a single API key.
Under the hood it's [MapLibre GL JS](https://maplibre.org/), so every
MapLibre method and option you already know works unchanged.

MapLibre fetches byte ranges from the PMTiles file as the user pans and
zooms; you don't run a tile server.

## How tile billing works

Tiles are billed per **map session**, not per tile — roughly one unit
per map view. Pan, zoom, and switch themes as much as you like within a
session; it stays one unit. A session covers a map for about an hour, so
a typical visitor loading your map costs a single unit.

The SDK takes care of this automatically — one map is one session, kept
alive for as long as it's on screen. Integrating without the SDK? See the
[API reference](/api) for the `session` parameter so your usage is counted
as sessions too.

## Ready-made styles

The Streets basemap ships in five colour variants, plus an **Outdoor** style
(hillshade + contour lines — see [Outdoor & contour lines](#outdoor--contour-lines)).
The full catalog is on [Styles & tilesets](/styles).

| Style              | URL                                         |
| ------------------ | ------------------------------------------- |
| `light` (default)  | `https://rijwind.com/styles/light.json`     |
| `dark`             | `https://rijwind.com/styles/dark.json`      |
| `grayscale`        | `https://rijwind.com/styles/grayscale.json` |
| `white`            | `https://rijwind.com/styles/white.json`     |
| `black`            | `https://rijwind.com/styles/black.json`     |
| `outdoor`          | `https://rijwind.com/styles/outdoor.json`   |

The font glyphs and sprite atlases the styles reference are hosted at
`cdn.rijwind.com/basemap/...` — same edge cache as the tile bytes, so
the second visitor to a page using the same theme pays no origin
latency for the assets.

You can pass a theme by name to the SDK (below), or fetch the JSON and
tweak it yourself — see [Custom styling](#custom-styling). The styles
reference `pmtiles://{{TILE_URL}}` as the tile source, a placeholder the
SDK substitutes for you.

## Quickstart

```sh
npm install @rijwind/sdk
```

```ts
import { Map, MapStyle, config } from "@rijwind/sdk";
import "@rijwind/sdk/style.css";

config.apiKey = "rw_live_•••YOUR_KEY•••";

const map = new Map({
  container: "map",
  style: MapStyle.LIGHT, // LIGHT · DARK · GRAYSCALE · WHITE · BLACK
  center: [4.9041, 52.3676],
  zoom: 10,
});

// Tear-down (the tile session is cleaned up with the map):
// map.remove();
```

That's the full integration. `Map` extends MapLibre GL JS, so markers,
popups, layers, and events all work as usual. No tile server to operate,
no signed URLs to manage, no font or sprite hosting on your side — and the
map keeps working for as long as it's open.

To colour the basemap, pass your own style URL or object instead of a
`MapStyle` (see [Custom styling](#custom-styling)).

### Label language

Labels show **local/native** names by default. Set `language` to render them
in a specific language (names without it fall back to local), and switch at
runtime with `setLanguage` — no flicker:

```ts
const map = new Map({ container: "map", language: "en" });

// later — a language toggle, say:
map.setLanguage("nl"); // or "de", "fr", … or "local" for native names
```

Supported codes: `ar, bg, cs, da, de, el, en, es, et, fa, fi, fr, ga, he, hi,
hr, hu, id, it, ja, ko, lt, lv, mr, mt, ne, nl, no, pl, pt, ro, ru, sk, sl, sv,
tr, uk, ur, vi, zh-Hans, zh-Hant`. Set a default for every map with
`config.language = "en"`.

### Without the SDK

Prefer raw MapLibre, or already have a map configured? Call
`GET /tiles/v1/token` yourself and feed the returned URL to MapLibre's
`pmtiles://` protocol. You then keep the URL fresh as it rotates, and pass
a `session` id so your usage groups into sessions — both are documented in
the [API reference](/api).

## Custom styling

If you need to colour the basemap to match your product palette, fetch
one of the ready-made styles, mutate the `layers` array, and pass the
result to MapLibre. The layer ids are stable across releases — see the
JSON itself for the exact set. Background colour, water fill, road
casings, and label colours are the usual things to tune.

## Terrain and hillshade

The basemap can render **shaded relief** (hillshade) and optional tilted **3D
terrain** from elevation data. Terrain is part of the same map session as the
basemap, so it adds **no extra quota cost**.

```ts
import { Map } from "@rijwind/sdk";
import "@rijwind/sdk/style.css";

const map = new Map({
  container: "map",
  center: [6.87, 45.92], // somewhere with relief
  zoom: 12,
  terrain: true, // shaded relief
});

// 3D terrain — wire this to your own button
map.on("load", () => map.setTerrain3D(true));
```

`terrain: true` adds hillshade; `setTerrain3D(true)` tilts the map and raises the
relief (`setTerrain3D(false)` flattens it again).

### Terrain without the SDK

The `GET /tiles/v1/token` response includes a `terrainUrl` next to the basemap
`url`. Register it as a `raster-dem` source with the `terrarium` encoding and add
a `hillshade` layer (or call `map.setTerrain`):

```js
map.addSource("terrain", {
  type: "raster-dem",
  url: "pmtiles://<terrainUrl from /tiles/v1/token>",
  tileSize: 512,
  encoding: "terrarium",
  maxzoom: 12,
});
map.addLayer({ id: "hillshade", type: "hillshade", source: "terrain" });
// 3D:
map.setTerrain({ source: "terrain", exaggeration: 1.2 });
```

When you render terrain, keep the terrain data credit shown on the map (the SDK
adds it for you via the attribution control). To read elevation **values** —
points or a route profile — use the [Elevation API](/elevation) instead.

## Outdoor & contour lines

The **Outdoor** style is Streets plus shaded relief and elevation **contour
lines** — built for cycling, hiking, and the outdoors. With the SDK it's one
line:

```ts
import { Map, MapStyle } from "@rijwind/sdk";
import "@rijwind/sdk/style.css";

const map = new Map({
  container: "map",
  style: MapStyle.OUTDOOR, // hillshade + contours, on automatically
  center: [6.87, 45.92],
  zoom: 12,
});
```

The contour lines are generated **in the browser** from the elevation data, so
there's no contour tileset to load and they stay crisp at every zoom — and
because they ride the same map session as the basemap, they add **no extra
quota cost**.

### Outdoor without the SDK

Generate the contours yourself with
[maplibre-contour](https://github.com/onthegomap/maplibre-contour), pointed at
the Rijwind terrain tiles. The contour generator runs in a web worker and can't
send an `Authorization` header, so the key rides the query string — use an
[origin-restricted key](/authentication). The terrain tiles are Terrarium-encoded
and free (part of the map session).

```js
import mlcontour from "maplibre-contour";

const demSource = new mlcontour.DemSource({
  url: "https://api.rijwind.com/tiles/v1/terrain/{z}/{x}/{y}?key=YOUR_KEY",
  encoding: "terrarium",
  maxzoom: 12,
  worker: true,
});
demSource.setupMaplibre(maplibregl);

map.addSource("contours", {
  type: "vector",
  tiles: [
    demSource.contourProtocolUrl({
      // [minor, major] line spacing in metres, per zoom (coarse on purpose so
      // flat areas near sea level stay uncluttered)
      thresholds: { 11: [100, 500], 13: [50, 250] },
      elevationKey: "ele",
      levelKey: "level",
      contourLayer: "contours",
    }),
  ],
  maxzoom: 15,
});

map.addLayer({
  id: "contour-lines",
  type: "line",
  source: "contours",
  "source-layer": "contours",
  paint: {
    "line-color": "rgba(150,116,74,0.5)",
    "line-width": ["match", ["get", "level"], 1, 1.1, 0.5],
  },
});

map.addLayer({
  id: "contour-labels",
  type: "symbol",
  source: "contours",
  "source-layer": "contours",
  filter: [">", ["get", "level"], 0],
  layout: {
    "symbol-placement": "line",
    "text-size": 11,
    "text-field": ["concat", ["number-format", ["get", "ele"], {}], " m"],
    "text-font": ["Noto Sans Regular"],
  },
  paint: {
    "text-color": "#6f5535",
    "text-halo-color": "rgba(255,255,255,0.85)",
    "text-halo-width": 1.2,
  },
});
```

Pair this with the hillshade snippet above for the full Outdoor look.

## What about raster tiles?

We don't serve raster tiles in v1. If you need a raster basemap for a
print pipeline or a legacy GIS client, render the vector basemap to a
PNG on your side. Get in touch if this becomes a pain point.
