The basemap and relief¶
Land, water, roads, towns and names under the wind come from OpenStreetMap, as vector tiles from Protomaps, drawn in Windfenster's own style (light and dark, names in the viewer's language). The hill shading comes from Mapterhorn. The server cuts both itself and serves every tile; there is no map service and no API key.
Three layers of detail¶
flowchart TB
W["World to WF_BASEMAP_WORLD_ZOOM (7)<br>coasts, countries, towns"]
R["Region to WF_BASEMAP_ZOOM (8)<br>Europe by default: main roads"]
S["Spots to zoom 14, 20 km around<br>stations, opened places, tapped spots"]
W --> R --> S
A higher zoom is more detailed: zoom 0 is the whole world on one tile, 7–8 shows countries and main roads, 10–12 streets, forests and small lakes, 14 every path, building and slipway. Every level splits each tile into four, so each costs three to four times the one before.
| Setting | Default | Covers | Size |
|---|---|---|---|
WF_BASEMAP_WORLD_ZOOM |
7 | the whole world | 6: 45 MB · 7: about 150 MB |
WF_BASEMAP_REGION |
-25,27,45,72 (Europe) |
the deeper region, west,south,east,north |
– |
WF_BASEMAP_ZOOM |
8 | the region | Europe 8: about 190 MB · 12: about 7 GB · the Upper Rhine to 15: about 210 MB |
WF_BASEMAP_SPOTS |
60 | detail areas kept | 2 to 17 MB each |
WF_TERRAIN_ZOOM |
7 | relief, the whole world | 6: 278 MB · 7: 948 MB |
How it is made¶
Towns, roads and lakes from close up come from Protomaps, vector tiles of OpenStreetMap, drawn in Windfenster's own style, light and dark, with names in the viewer's language. Without them the map shows the world from Natural Earth (Europe finer).
By default (WF_BASEMAP=auto) the server makes the basemap itself, a minute after the start and
then checked daily: from Protomaps' newest daily build it takes, by byte range, the whole world
up to zoom 7 (coasts, countries, towns) and a region deeper, Europe up to zoom 8 by default
(main roads), into one file (together 323 MB, 24,752 tiles, about 13 minutes on 11 October),
{WF_DATA_DIR}/map/basemap.pmtiles. A note next to it (basemap.json) says what it was made
from; it is made again after 120 days, or when the settings change.
Detail (streets, lakes, beaches, up to zoom 14) comes where people look, anywhere in the world:
around each tracked station (WF_TRACKED_STATIONS), each place opened in the last week and each
spot tapped on the map, the server cuts 20 km from the newest build into a file of its own,
{WF_DATA_DIR}/map/basemap-spots/{lat}_{lon}.pmtiles (2 to 17 MB; spots are snapped to a 0.2°
grid, so places close together share one). It checks every minute and makes two at a time, so a
tapped spot has its detail within a minute or two, and the open map shows it without reloading.
At most 60 spots are kept (about 600 MB), a hard limit: when more are wanted, tracked stations come
first, then the places opened most recently, then the latest taps (the server remembers 200
taps); spots nobody wants any more go first.
The map gets every tile from the server, at /api/map/basemap/{z}/{x}/{y}: from the spot that
holds it, else from the main file, and where neither goes that deep, cut out of the deepest tile
above it (scaled and clipped, as MapLibre does past a source's last zoom). So the map is one
source with no seams and never blank; it is only coarser where nobody looked yet. With room to
spare, set WF_BASEMAP_WORLD_ZOOM and WF_BASEMAP_ZOOM higher (the region with
WF_BASEMAP_REGION), and WF_TERRAIN_ZOOM for finer relief.
Relief comes from Mapterhorn (elevation from national surveys and
other open data, sources): the server cuts the whole world
to zoom 7 from its planet archive into {WF_DATA_DIR}/map/terrain.pmtiles (948 MB, 13,522 tiles, about 7 minutes on 11 October; renewed
yearly, served at /api/map/terrain.pmtiles), and the map draws it as hillshade over the colour
field and under water, roads and labels, softer from town zoom on.
A file you put there yourself (no note next to it) is never replaced. To cover more, cut one
with the pmtiles CLI (or its Docker image
protomaps/go-pmtiles):
pmtiles extract https://build.protomaps.com/20261010.pmtiles data/map/basemap.pmtiles \
--bbox=-25,27,45,72 --maxzoom=12
Europe to zoom 12 is about 7 GB; the Upper Rhine to zoom 15 about 210 MB. WF_BASEMAP=off
leaves the file to you; the server cuts its tiles the same way where it stops.
With WF_BASEMAP=ask (or "Ask" on the admin page) the server does not
renew by itself: when the basemap or relief is due, the admin page shows it with the newest
build, and an admin says when.
OpenStreetMap is credited on the map.
Changing the depth¶
Change the variable and restart. A minute after the start the server compares the settings with
the note next to the file (basemap.json, terrain.json); when they differ it makes the file
anew from the newest build:
- it downloads the whole file again (no tiles are reused from the old one);
- it writes to
basemap.pmtiles.partand swaps it in only when complete, so the old map stays in use meanwhile, and you need room for both; - each range that fails for a moment (a 5xx, 429 or cut connection, which Protomaps and Mapterhorn answer now and then over hours of requests) is asked again up to nine times, the wait doubling from 1 s to a minute (about five minutes in all); only when it keeps failing does the cut stop. A restart in the middle starts over; after an error it tries again in an hour;
- the tiles are walked one at a time in the archive's order, only the source's directories in use are kept (64 MB at most), and the new archive is written as it goes: tile data straight into the file, a leaf directory every few thousand entries into a side file, the root and header at the end. Memory stays flat however large the cut (50 million tiles in under 30 MB); only time grows, and the log says every five minutes how many tiles are done and how long is left;
- before a cut it checks that the disk has room for it (the source's size times the share of its tiles asked for) and does not begin without;
- the detail spots stay as they are; from
WF_BASEMAP_ZOOM=14none are made inside the region, and spot files the main file now holds as deep are removed.
Reload the map page in the browser afterwards.
The whole world at full depth¶
When the cut asked for is all of the source, the server does not cut it tile by tile but downloads the source file as it is:
| Setting | Source file | Size | At 35 to 60 MB/s |
|---|---|---|---|
WF_BASEMAP_REGION=-180,-85.05,180,85.05 with WF_BASEMAP_ZOOM=15 |
Protomaps' daily build | about 139 GB | about 40 min |
WF_TERRAIN_ZOOM=12 |
Mapterhorn's planet.pmtiles |
about 356 GB | about 2.5 h |
Cut tile by tile, the same world would take days (one request per tile, a few hundred tiles a second). The download:
- fetches pieces of 512 MiB one after the other, each its own range request on a connection
of its own, written at its place in
basemap.pmtiles.part. Both sources sit behind Cloudflare, where one long connection was seen to stop sending after some 12 GB while a new one went on at full speed: a request that brings less than 1 MB/s for 20 s is dropped and its piece asked again from where it got. A piece counts only when it is complete; - retries a moment's failure (5xx, 429, a cut or hung connection) without end, waiting 5 to 60 s; only 30 minutes without a single piece done end the round, and the next round an hour later goes on from the pieces there;
- goes on after a restart with the pieces still missing: the source, its ETag, its length and
the pieces done are kept in
basemap.pmtiles.part.json, and the last MiB before them is first fetched again and compared with the part. If the source changed (another ETag, a 200 instead of 206, bytes that differ), it starts over. The build is chosen once and kept for the whole download; only if Protomaps deletes it meanwhile does it switch to the newest; - checks the room on the disk first and does not begin without it (the log says how much is needed and free), and checks the file's length and PMTiles header before it swaps it in;
- says every five minutes how far it is, how fast and how long it will take.
With the world at zoom 15 in the main file no detail spots are made any more.
Full detail where you ride
A small region very deep costs little: WF_BASEMAP_REGION=7.5,47.5,9.0,49.5 with
WF_BASEMAP_ZOOM=15 covers the Upper Rhine with every street and building in about 210 MB.