Outdoor Style
A comprehensive topographic map style built for hiking, cycling, skiing, and general outdoor navigation. Based on OpenStreetMap data, rendered with MapLibre GL.
| Style URL | https://api.mapriot.com/styles/outdoor.json?apiKey=<key> |
Usage with MapLibre GL JS
const MAPRIOT_APIKEY = 'YOUR_API_KEY';
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.mapriot.com/styles/outdoor.json',
center: [14.42, 50.08],
zoom: 13,
transformRequest: (url) => {
if (url.startsWith('https://api.mapriot.com') && !url.includes('apiKey')) {
return {
url: url + (url.includes('?') ? '&' : '?') + 'apiKey=' + MAPRIOT_APIKEY,
};
}
return { url };
},
});The transformRequest callback is required to append your API key to all tile, font, and sprite requests that the style triggers. See the MapLibre GL JS example for a complete setup including 3D terrain.
Label language
By default, labels show the local name from OpenStreetMap (name:latin). The mapriot vector tiles also carry:
| Attribute | What it holds |
|---|---|
name:cs, name:en, name:sk, name:pl, name:de, name:es | The translation, only when it differs from the local name |
name_translit | For a name in a non-Latin script, its romanization: Athína for Αθήνα |
name_nonlatin | true on a non-Latin name that has no romanization |
name_local | For a name that mixes a Latin form with another script (Casablanca ⵜⴰⴷⴷⴰⵔⵜ الدار البيضاء), the non-Latin part (الدار البيضاء); name_translit then holds the Latin part |
A simple translated label falls back to the local name:
['coalesce', ['get', 'name:en'], ['get', 'name:latin']]The style already labels this way: every name in a non-Latin script is followed by its romanization, smaller — beneath it for places, beside it for streets. You get that without any code.
To switch the map to a language, rewrite those labels: a name in a non-Latin script keeps its own script first, with the translation (else the romanization) after it; a Latin name and a country name show the translation. The snippet below handles both the style’s own labels and a plain {name:latin} one:
const LOCAL = new Map(); // original text-fields, so 'default' restores local names
function setLabelLanguage(map, lang) {
const local = ['coalesce', ['get', 'name_local'], ['get', 'name:latin']];
const translit = ['get', 'name_translit'];
const translation = ['get', 'name:' + lang];
const translated = ['has', 'name:' + lang];
const nonLatin = ['any', ['has', 'name_translit'], ['has', 'name_nonlatin']];
// [first name, second name]; the second starts with sep, or is ''
const parts = (sep, country) => country
? [['coalesce', translation, local],
['case', ['all', ['!', translated], ['has', 'name_translit']], ['concat', sep, translit], '']]
: [['case', nonLatin, local, ['coalesce', translation, local]],
['case', ['!', nonLatin], '', translated, ['concat', sep, translation],
['has', 'name_translit'], ['concat', sep, translit], '']];
// the style's own name sections: [local, {}, romanization, { 'font-scale': 0.8 }]
const localJson = JSON.stringify(local);
// format can't sit inside concat: there, use the same two names as one string
const localize = (tf, sep, country, inString = false) => {
const [first, second] = parts(sep, country);
const sections = [first, {}, second, { 'font-scale': 0.8 }];
if (Array.isArray(tf) && tf[0] === 'format') {
const out = [...tf];
let swapped = false;
for (let i = 1; i + 3 < out.length; i += 2) {
if (JSON.stringify(out[i]) !== localJson) continue;
out[i] = first; out[i + 2] = second; out[i + 3] = { 'font-scale': 0.8 };
swapped = true;
}
if (swapped) return out;
}
if (typeof tf === 'string') {
if (!tf.includes('{name:latin}')) return tf;
// '{name:latin}' templates -> format expression
return ['format', ...tf.split(/(\{[^{}]+\})/).filter(Boolean).flatMap((p) =>
p === '{name:latin}' ? sections : [p.startsWith('{') ? ['coalesce', ['get', p.slice(1, -1)], ''] : p, {}])];
}
if (Array.isArray(tf)) {
if (tf[0] === 'get' && tf[1] === 'name:latin') return inString ? ['concat', first, second] : ['format', ...sections];
const str = inString || !['case', 'step', 'match', 'coalesce'].includes(tf[0]);
return tf.map((x) => (Array.isArray(x) ? localize(x, sep, country, str) : x));
}
if (tf && tf.stops) { // legacy zoom function -> step expression
return tf.stops.reduce((e, [z, v], i) =>
(i ? [...e, z, localize(v, sep, country)] : [...e, localize(v, sep, country)]), ['step', ['zoom']]);
}
return tf;
};
for (const layer of map.getStyle().layers) {
const tf = LOCAL.get(layer.id) ?? layer.layout?.['text-field'];
if (tf === undefined || !JSON.stringify(tf).includes('name:latin')) continue;
LOCAL.set(layer.id, tf);
const placement = layer.layout?.['symbol-placement'];
const sep = placement === 'line' || placement === 'line-center' ? ' ' : '\n';
const country = /"class","(country|continent)"/.test(JSON.stringify(layer.filter ?? ''));
map.setLayoutProperty(layer.id, 'text-field', lang === 'default' ? tf : localize(tf, sep, country));
}
}
map.on('load', () => setLabelLanguage(map, 'en'));Call it again after map.setStyle() (on style.load), because that resets the layers. With MapLibre Native (iOS/Android), set the same expressions as each symbol layer’s text-field.
Data sources
The style references four tile sources. Each source tile request costs 1 unit.
| Source ID | Type | Zoom range | Description | |
|---|---|---|---|---|
mapriot | vector | 0–14+ | Worldwide OSM map data | → |
relief | raster | 0–4 | Shaded relief (Natural Earth Data) | → |
hillshading | raster-dem | 5–12 (overzoomed beyond) | DEM for hillshading and 3D terrain | → |
contours | vector | 11+ | Elevation contour lines | → |
The terrain layers transition smoothly: relief provides broad terrain shape at low zoom (z0–4), then hillshading takes over from z5 for higher-resolution slope shading. Contour lines appear from z11.
What’s included
Terrain — contour lines (10 m or 5 m intervals depending on region), hillshading, 3D terrain support, low-zoom shaded relief
Trails and routes — color-coded hiking trails parsed from OSM osmc:symbol tags with proper stacking when routes share a path, cycling routes, hiking difficulty grading (SAC scale)
Winter sports — ski lifts, chairlifts, gondolas
Transportation — full road hierarchy from motorways to footpaths, with surface type distinction (paved/unpaved), bridges, tunnels
Places — city, town, village, hamlet, and neighbourhood labels with population-based ranking
Water — rivers, streams, lakes, coastlines, intermittent waterways, springs, waterfalls
Land cover and use — forests, parks, residential areas, farmland, glaciers, bare rock, scree, cliffs, military areas
Points of interest — mountain peaks (with prominence ranking), shelters, campsites, viewpoints, restaurants, public transport stops, parking areas, and more
Sprites and fonts
The style references sprite (icon) and font (glyph) assets hosted on the same API. These require your API key and are authenticated automatically via transformRequest. They are free — no billing.
Custom styles
If you build your own style using the mapriot vector source, you will need to supply your own sprite and font (glyph) assets. The ones served by our API are designed specifically for the outdoor style.
We can create custom map styles tailored to your project — contact us to discuss.
Style version
https://api.mapriot.com/styles/style-versions.json?apiKey=<key>Returns the current version of each style. Poll this to check for updates without fetching the full style JSON.