https://map-generator.toolforge.org/api/v1/ serves maps, their metadata and data joins
over HTTP, for tools such as Maphue that
shouldn't have to bundle the engine or hundreds of MB of boundary data.
- Same maps everywhere. The options are the WebAssembly build's
RenderSpec, and a map from the API is byte-identical to the CLI's and the WASM build's for the same data and options. - Cacheable. Output is deterministic. The
ETagis the SHA-1 of the body, the same hash Wikimedia Commons stores for each file version. - Read-only and anonymous. No accounts or cookies, and nothing users send is stored. Requests are logged without IP addresses or user agents. CORS is open to every origin.
- Described.
/api/v1/openapi.json(OpenAPI 3.1) lists every endpoint, parameter and error.
# France's départements, labelled, 900 px wide
curl -o france.svg 'https://map-generator.toolforge.org/api/v1/maps/ne-admin1/FRA.svg?labels=true&width=900'
# Its metadata: size, insets, empty areas for a legend, SHA-1, credit, licence, boundary version
curl 'https://map-generator.toolforge.org/api/v1/maps/ne-admin1/FRA.json?labels=true&width=900'// From a web page on any origin, with no build step:
import { MapgenClient } from "https://map-generator.toolforge.org/api/v1/client.js";
const api = new MapgenClient();
const svg = await api.map("ne-admin1", "FRA", { labels: true, languages: ["fr"], colors: { water: "#c6ecff" } });
const regions = await api.features("ne-admin1", "FRA"); // codes, names, parents: no SVG parsingGET /api/v1/ |
Links to everything below |
GET /api/v1/openapi.json |
OpenAPI description |
GET /api/v1/client.js |
Dependency-free ES module client (MapgenClient) |
GET /api/v1/health |
200 when the data is loaded |
GET /api/v1/version |
mapgen version, commit, SVG contract version, dataset releases |
GET /api/v1/themes, /bbox-presets |
Colour themes and slots; frame presets |
GET /api/v1/render-options |
Every map setting, described for settings forms (below) |
GET /api/v1/datasets |
Hosted datasets: level, licence, share-alike, release, boundary year, points of view, languages |
GET /api/v1/datasets/{dataset}/regions |
What a map can be made for: countries, continents, world… with each one's licence |
GET /api/v1/datasets/{dataset}/regions/{region}/features |
The map's regions without geometry: code, name, names (?languages=fr,zh-Hant), parent, parentName, country, wikidata |
GET /api/v1/maps/{dataset}/{region}.svg |
The map. {region} can list several regions, by code or name: /maps/ne-admin0/BEL,LUX,NLD.svg |
GET /api/v1/maps/{dataset}/{region}.json |
Its metadata (below) |
GET /api/v1/maps/{dataset}/{region}.html |
An interactive page with colour pickers |
POST /api/v1/render |
The same, with a JSON body {"dataset", "region", "worldview"?, "spec": RenderSpec}, or a map recipe (Content-Type: text/csv) |
POST /api/v1/match |
Compare a data table's codes with a map |
GET /api/v1/crosswalks, /crosswalks/{id}.csv |
Hosted crosswalks between boundary versions |
POST /api/v1/reshape |
Move a data table to new codes through a crosswalk |
GET /api/v1/contract/fixture.svg |
A small map following the SVG contract, for your tests |
| id | Regions |
|---|---|
ne-admin0 |
countries (FRA), continents (Europe, South America), world; points of view with worldview= |
ne-admin1 |
first-level subdivisions of a country (FRA gives départements, with régions as parents) |
mixed |
countries and single subdivisions together: DEU,FR-75,Bavaria (codes, names in any language, Wikidata items; a name shared by both means the country) |
us-counties |
USA: counties with FIPS codes (US-31109), states as parents |
geoboundaries-adm1 |
one country per file (FRA), each with its own licence |
/api/v1/datasets is authoritative.
Query parameters are the RenderSpec fields in kebab-case:
width,height(fixed; the frame widens to fill it),theme,labels,languages=fr,zh-Hant,target=commons|web,css-vars;capitals=countries|all(national capitals, or also regional capitals inside the map) andcontext-labels(name the neighbouring countries);disputed=falseto leave out disputed areas (hatched) and disputed boundaries (dashed);title,show-title(draw it above the map),caption(under the map),alt(for screen readers);frame,bbox,projection,insets,border-mode,credit;- colour slots as
color-water=%23c6ecff,color-context-land=tan.
Also:
worldview=INDfor a Natural Earth point of view (applied to the neighbouring countries, and to the regions themselves forne-admin0);release=<dataset release>, which pins the URL: the response is then cached as immutable, and the URL returns 404 once that release is no longer hosted.
Unknown or misspelled parameters are errors, never silently ignored.
The .json metadata:
width,height,projection,center;insets,outsideFrame(regions shown nowhere);legendSlots: empty areas for a legend or title, largest first:position(top-left…bottom-right),x,y,width,heightin the SVG's own coordinates (the map's top-left is0,0; a title band sits above it, at negativey), andlandShare, how much of the third of the map at that position is regions or insets. An area of width 0 has no room;sha1,credit,licence,licenceUrl,shareAlike;boundaryYear,sourceRelease,release;contract, andurl(the canonical URL).
GET /api/v1/render-options describes every map setting, so a client can build its settings
form from the API rather than keep its own list. The same table types the query parameters
and generates openapi.json, and tests check it against what the API accepts, so it can't
fall behind. (renderOptions() in the WebAssembly build returns the same, without the host's
width limit.)
{
"version": 1,
"groups": [{ "id": "map", "label": "Map", "order": 10 }, …],
"options": [
{
"name": "projection", "key": "projection", "inRenderSpec": true,
"type": "string", "widget": "select", "label": "Projection",
"description": "Automatic picks one suited to the area.", "default": "auto",
"choices": [{ "value": "auto", "label": "Automatic" }, …],
"group": "frame", "order": 30, "advanced": false
},
{
"name": "parallels", "type": "pair", "widget": "pair", "advanced": true,
"visibleWhen": { "option": "projection", "in": ["albers", "lcc"] }, …
}
],
"colorSlots": [{ "slot": "water", "name": "color-water", "key": "water", "label": "Water", "description": "Seas and oceans", "default": "#c6ecff" }, …],
"themesSource": "/api/v1/themes",
"excluded": [{ "name": "region", "reason": "part of the path: …" }, …]
}nameis the query parameter and recipe key;keytheRenderSpecmember (forPOST /renderand WebAssembly).worldviewhasinRenderSpec: false: it is a query parameter and recipe key only.type:boolean,integer,number,string,list(comma-separated) orpair(two comma-separated numbers).widget:checkbox,select,number,text,textarea,tokens,bboxorpair.choicesare the only values accepted;choicesSourcenames the endpoint that lists them (/themes,/bbox-presets);suggestionsare common values of an open list (languages).worldview's choices are the points of view this host has, named after their country.minimum,exclusiveMinimum,maximum,stepandmaxLengthare the limits the API enforces (stepis a hint).width's maximum is this host's.visibleWhensays when a setting applies:{option, equals},{option, in},{option, notEmpty}or{anyOf: [...]}. Options markedadvancedbelong in a collapsed section.- Colours are
colorSlots, with the default theme's colours; each theme's are at/api/v1/themes. excludedlists the map endpoint's other parameters and why they aren't settings.
The shape can gain fields within version 1; ignore what you don't recognise. A form in a
few lines:
const { options, groups } = await api.renderOptions();
for (const group of groups) {
for (const o of options.filter((o) => o.group === group.id && !o.advanced)) {
const input = o.widget === "select"
? select(o.choices.map((c) => [c.value, c.label]), o.default)
: o.widget === "checkbox" ? checkbox(o.default) : textInput(o);
field(o.label, o.description, input); // render labels as text, never HTML
}
}
// Values go back as query parameters, `name=value`, or as `{ [o.key]: value }`.# Which codes of my table aren't on the map? (e.g. data on 2020 counties, map on older ones)
curl -X POST https://map-generator.toolforge.org/api/v1/match -H 'Content-Type: application/json' -d '{
"dataset": "us-counties", "region": "USA",
"table": "fips,pop\n02063,7000\n02066,2600\n", "codeColumn": "fips", "codePrefix": "US-"}'
# → dataNotOnMap, mapWithoutData, missingShare, and hints naming a hosted crosswalk
# Move a table to 2020 county codes
curl -X POST https://map-generator.toolforge.org/api/v1/reshape -H 'Content-Type: application/json' -d '{
"table": "fips,pop\n02261,9600\n", "codeColumn": "fips", "crosswalk": "us-counties-2010-2020"}'reshape adds values up and shares them out, so use it on counts, not rates. Values without
a rule, such as a split without weights, give 422 with a conflicts list, unless you pass
"allowConflicts": true.
- Content types:
image/svg+xml,application/json,text/htmlandtext/csv, all in UTF-8. - Caching:
ETaggives304onIf-None-Match. Maps arepublic, max-age=2592000(30 days, since data changes only with a release), orimmutablefor a year withrelease=. Listings (datasets, regions, features, crosswalks) are cached for a day, andhealth/versionnot at all. - Provenance headers:
Content-Location(the canonical URL),Link: <licence>; rel="license",X-Mapgen-Version,X-Mapgen-ContractandX-Dataset-Release. All are exposed to cross-origin scripts. - Errors: RFC 9457
application/problem+json, as{"type", "title", "status", "detail", "param"?}:400: invalid parameter;404: unknown dataset, region, format, release or point of view;422: reshape conflicts;503: busy or too slow; retry afterRetry-After.
widthat most 4000 px.- Request bodies up to 5 MB.
- 20 s per render.
- A few renders at a time; beyond that, requests wait up to 10 s, then get
503.
Common maps are pre-rendered after every data update, so most requests are served from the cache. There are no user-supplied geometries: for one-off maps from your own data, use the WebAssembly build in the browser.
/api/v1is stable. Additions (endpoints, parameters, response members) don't change the version, so clients should ignore members they don't know.- Removing or changing something goes to
/api/v2./api/v1then keeps working for at least six months, announced withDeprecationandSunsetheaders and in the changelog. - The SVG contract (
data-mapgen-contract) has its own version: see contract.md.
The API runs on Wikimedia Toolforge
under the Wikimedia Cloud Services Terms of Use.
The code is MIT-licensed. Each map carries its data's credit and licence (credit,
licence, shareAlike, and the Link header). Share-alike data (for example some
geoBoundaries countries) requires sharing derived maps under the same licence.
scripts/prepare-server-data.sh target/server-data --worldviews IND,PAK # add --geoboundaries ADM1 for all countries
MAPGEN_DATA_DIR=target/server-data MAPGEN_CACHE_DIR=target/server-cache PORT=8000 \
cargo run --release -p mapgen-serverSettings (environment):
| Variable | Default | Meaning |
|---|---|---|
MAPGEN_DATA_DIR |
data |
the prepared data directory |
MAPGEN_CACHE_DIR |
none (no cache) | disk cache of rendered maps |
MAPGEN_CACHE_MAX_BYTES |
2 GB | cache size limit |
MAPGEN_MAX_CONCURRENT |
2 | renders at a time |
MAPGEN_RENDER_TIMEOUT |
20 | seconds per render |
MAPGEN_WWW_DIR |
none | a static site served at / (the WebAssembly playground) |
MAPGEN_MAX_WIDTH |
4000 | largest width |
PORT |
8000 | port to listen on |
datasets.toml in the data directory lists what is served. prepare-server-data.sh writes
it, and crates/mapgen-server/src/registry.rs documents its fields. Deployment is described
in toolforge.md.