BeNomad Tiles
Styles & BeMaputnik#
A BeNomad Tiles map paints a tiny bundled fallback instantly, then swaps in the live cartographic charte published on the worker, so the map you author in BeMaputnik reaches every application without a single redeploy.
The bundled style is not the source of truth. bemap.fallbackStyle exists only to fill the canvas during the first few hundred milliseconds and to survive an offline start. The real style is fetched from the worker after login (/api/default-style → /styles/<name>). Change the charte server-side and every app picks it up on its next load, no library rebuild, no client release.
At a glance#
- Four shapes of the
styleoption: omitted /'default', a bare name, a full URL, an inline object. - All four flow through
bemap.TilesStyle.fetch()→resolvePlaceholders()→hardenSymbolCollisions()before MapLibre sees them. bemap.fallbackStyle: "BeMap Fallback (charte 2026, no fonts)": fill/line only, no symbol layers, noglyphs, so it never requests a font.- Discovery:
map.fetchAvailableStyles()→{ styles, defaultStyle }; runtime swap:map.setStyle('<name>'). - Two placeholders:
TILES_SOURCE(rewritten topmtiles://<tilesHost>/<tilesFile>) and__BILINGUAL_PLACE__(a MapLibreformatexpression). - Localized names are
name_fr/name_en: underscore, not thename:frOpenMapTiles convention. - The library ships no fonts. A root-relative
glyphsis absolutised against the tiles host; override withctx.glyphsUrl. setStyle()is overlay-preserving:bemap.TilesOverlayCatalogreplays your shapes, including on the401→ refresh → reload path.- Styles are authored in BeMaputnik, the BeNomad style editor at
https://bemaputnik.benomad.net.
Usage#
The default path: pin nothing, and the map tracks whatever the worker publishes:
var ctx = new bemap.Context({
login: 'YOUR_LOGIN',
password: 'YOUR_PASSWORD',
secure: true,
host: 'bemap.benomad.com',
tilesHost: 'mptiles-api.benomad.net'
});
// No `style` option → tiny fallback now, live server charte right after login.
var map = new bemap.MapLibreMap(ctx, 'map').move(2.35, 48.85, 12);
// Style-load failures are reported in the browser console - 2.0.2 has no
// public error subscription. See "Map errors" on the Events page.List what the worker allows for your account, then swap at runtime:
map.fetchAvailableStyles().then(function (cfg) {
// cfg.styles → ['openfreemap_graylevel', 'charte_2026', …]
// cfg.defaultStyle → the name applied when you pin none
cfg.styles.forEach(function (name) { console.log(name, name === cfg.defaultStyle); });
});
map.setStyle('openfreemap_graylevel'); // bare name → <tilesHost>/styles/openfreemap_graylevelPin a style explicitly when you do not want the server default: a name, a URL on your tiles host, or an inline object:
new bemap.MapLibreMap(ctx, 'map', { style: 'openfreemap_graylevel' });
new bemap.MapLibreMap(ctx, 'map', { style: 'https://mptiles-api.benomad.net/styles/charte_2026.json' });
new bemap.MapLibreMap(ctx, 'map', {
style: {
version: 8,
metadata: { source_placeholder: 'TILES_SOURCE' },
sources: { 'TILES_SOURCE': { type: 'vector', url: '' } },
layers: [ { id: 'bg', type: 'background', paint: { 'background-color': '#f4f1ea' } } ]
}
});Point the font server somewhere other than the tiles host: this overrides every style's glyphs:
var ctx = new bemap.Context({
tilesHost: 'mptiles-api.benomad.net',
glyphsUrl: 'https://my-host.example.com/fonts/{fontstack}/{range}.pbf'
});Reference#
Style resolution precedence#
style option | Resolution |
|---|---|
omitted or 'default' | bemap.fallbackStyle paints immediately (no network, no fonts), then (once login completes) GET /api/default-style names the live style and GET /styles/<name> loads it through an overlay-preserving setStyle(). A failure is swallowed: the fallback simply stays, never a blank canvas. |
'<name>' | Fallback first, then <tilesHost>/styles/<name>. A value already carrying a path ('styles/foo.json', as returned by /api/styles) resolves against the host root instead of being re-nested. |
'<url>' | Fetched as-is. Auth is injected only when the URL contains ctx.tilesHost: session cookie by default, X-Session-Token header or ?token= per tilesAuth. Other origins get a plain fetch, so nothing leaks cross-origin. |
{ …json… } | Deep-cloned and used as-is. Placeholders are still resolved, so an inline style can carry TILES_SOURCE. |
Map methods#
| Member | Returns | Behaviour |
|---|---|---|
fetchAvailableStyles() | Promise<Object> | GET /api/styles → { styles, defaultStyle }. Waits for login. Rejects with MAPLIBRE_ONLY on Leaflet / OpenLayers |
fetchDefaultStyle() | Promise<Object> | GET /api/default-style → the name of the style applied when you pin none |
setStyle(urlOrObject, [options]) | this | Name, URL or inline object. Returns synchronously for chaining; the URL path resolves asynchronously and reports failures as STYLE_LOAD_FAILED on the map error channel. Marks the map as explicitly styled |
getStyle() | Object | The resolved spec the library applied: placeholders substituted, glyphs absolutised |
setPaintProperty / setLayoutProperty / setFilter / setLayerZoomRange | this | Surgical passthroughs for one-off tweaks; no-ops (warn once) on Leaflet and OpenLayers |
Context URL helpers#
| Member | Result |
|---|---|
ctx.getTilesBaseUrl() | https://<tilesHost><tilesPath> |
ctx.getTilesStylesUrl() | <base>/api/styles: the allow-listed style names for this environment |
ctx.getTilesDefaultStyleUrl() | <base>/api/default-style: the default-style pointer |
ctx.getTilesStyleUrl(name) | <base>/styles/<name>, or <base>/<path> when name already contains a /. null without a tilesHost |
bemap.TilesStyle#
Pure functions, unit-testable without instantiating MapLibre. You rarely call them yourself: setStyle() and the constructor already do.
| Member | Signature | Purpose |
|---|---|---|
SOURCE_PLACEHOLDER | String | 'TILES_SOURCE': override per style via metadata.source_placeholder |
PLACE_LABEL_PLACEHOLDER | String | '__BILINGUAL_PLACE__': override via metadata.place_label_placeholder |
fetch(ctx, urlOrObject, [options]) | Promise<Object> | Coerces input to a deep-cloned style object. Objects resolve immediately; strings are fetched with the tiles auth applied when the URL matches ctx.tilesHost. options.getToken supplies the JWT for header / query modes |
resolvePlaceholders(rawStyle, ctx, [tilesFile]) | Object | Absolutises glyphs, rewrites the source to pmtiles://<tilesHost>/<tilesFile> under the key tiles, repoints every layer, and substitutes the bilingual text-field. Mutates and returns the input |
buildBilingualExpr() | Array | Emits the MapLibre format expression used for place labels |
hardenSymbolCollisions(styleObject) | Object | Sets symbol-avoid-edges on every symbol layer, plus text-padding: 200 and text-optional: true on layers whose id starts with road_shield, so the OSM tileset stops fighting itself at low zoom |
Placeholders#
| Placeholder | Where | Replaced with |
|---|---|---|
TILES_SOURCE | A source key, and source: on each layer | A vector source named tiles with url: 'pmtiles://<tilesHost>/<tilesFile>'. The style's own url / tiles template is deliberately discarded, so every BeNomad style routes through the worker for auth, gating and edge cache. Only the placeholder key or tiles is rewritten: a source keyed anything else is left untouched |
__BILINGUAL_PLACE__ | layout['text-field'] | A format expression: coalesce(name_<browserLang>, name_en, name) in Noto Sans Bold, and (only when it differs) the local name on a second line at font-scale 0.75 in Noto Sans Regular |
BeMaputnik: the style editor#
| Item | Value |
|---|---|
| URL | https://bemaputnik.benomad.net |
| What it is | A MapLibre/Maputnik-based visual style editor, hosted by BeNomad, previewing live against BeNomad Tiles |
| Auth | The same tiles session: it logs in and carries the JWT on the style, glyph and PMTiles requests |
| Style markers | BeNomad styles carry "maputnik:renderer": "mlgljs" and metadata.source_placeholder (plus place_label_placeholder on label-bearing styles) |
| No service worker | Every reload re-fetches, so an edit is visible immediately |
Workflow. Open a style in BeMaputnik and edit it against a live preview → the reviewed style is published server-side into the worker's allow-list → applications pick it up automatically through /api/default-style, or explicitly with map.setStyle('<name>'). Keep the placeholders intact when saving: strip TILES_SOURCE and the style stops routing through the worker; strip __BILINGUAL_PLACE__ and labels fall back to the raw local name.
Notes#
Why the fallback-then-live dance matters. It is the single most important idea on this page. Shipping the charte inside the bundle would mean a library rebuild and a customer redeploy for every cartographic change. Instead the bundle carries only a ~10-layer fill/line style for the first frame, and the authoritative charte lives on the worker: versioned, cached at the edge, and swapped in a few hundred milliseconds later. The swap is skipped if you call setStyle() yourself during that async gap, so an explicit choice always wins.
bemap.fallbackStyle. Frozen object, name "BeMap Fallback (charte 2026, no fonts)": background, earth, landcover, water, river lines and major roads / highways, with colours lifted from the charte so the transition is barely visible. No symbol layers and no glyphs key at all: it can never trigger a font request or a font 404. bemap.defaultStyle is a deprecated back-compat alias of the same object.
Glyphs. The library ships no fonts and there is no dist/fonts/ to deploy. Server styles declare their own glyphs; a root-relative one such as /fonts/{fontstack}/{range}.pbf is absolutised against the tiles host, because MapLibre resolves a relative glyphs in an object style against the page origin, which 404s and blanks every label. An absolute (https://…) or protocol-relative (//host/…) value is left alone. ctx.glyphsUrl overrides all of them.
Overlay preservation. Native setStyle() wipes user-added sources and layers. bemap.TilesOverlayCatalog stores a factory per overlay and replays them once MapLibre fires 'idle': polylines, polygons, circles, raster layers, heatmaps and 3D buildings. This covers the internal 401 → refresh → setStyle renewal path, so a session timeout cannot destroy a customer's overlays. Markers, multimarkers and popups are DOM overlays that live outside the style: setStyle() never touches them, and they are deliberately excluded from the replay set (replaying would duplicate them).
Projection. The SDK owns the root projection: your setProjection('globe') choice is remembered and re-asserted on every subsequent style swap, and a server style shipping "projection": {"type": "globe"} is overridden back to the SDK value with a one-time console.info. Opt in explicitly if you want a globe.
Gotchas#
- Localized names use an underscore:
name_fr,name_en, notname:fr. BeNomad OSM PMTiles do not follow the OpenMapTiles colon convention. A['get', 'name:fr']in a hand-written expression silently misses on every feature and collapses the label to the local name, or to nothing. Verified inbemap-tiles-style.js,buildBilingualExpr(). - An unreachable glyph server makes labels vanish silently. No exception, no error event, just an empty-looking map. If a label-heavy style renders as bare geometry, check the
fonts/{fontstack}/{range}.pbfrequests in the network panel before anything else. - Custom MapLibre layers added straight to
map.nativeare wiped by every style swap. They are not in the catalogue. Re-add them from amap.native.on('style.load', …)handler so they survive the server-default swap and any token-renewal reload. - Cluster layers created with
addClusterPoints()are not replayed either: unlike polylines and polygons, they are not registered in the overlay catalogue. Re-add them after a style change. - The style's own tile source is ignored, by design. Some third-party styles ship a hardcoded archive URL that would 404; the SDK always repoints the conventional source at your configured tileset on the worker.
- Calling
setStyle()during startup cancels the server-default swap. That is intentional, but it means a style you set in aloadhandler can look like "the default never arrived". resolvePlaceholders()mutates its argument. Pass a clone if you keep a reference to the raw style.fetchAvailableStyles()is MapLibre-only. On Leaflet and OpenLayers it rejects withMAPLIBRE_ONLY: those engines render WMS raster, wheresetStyle('<name>')re-applies a WMSSTYLESvalue instead.
See also#
- Overview & access: the BeNomad Tiles chapter, environments and integration paths
- Web: PMTiles: how the
pmtiles://source the placeholder produces is actually served - Sessions & tokens: the auth injected into style, glyph and tile requests
- Cache & slices: why a style swap is cheap on a warm cache
- Troubleshooting: blank map, missing labels,
401/403symptoms - MapLibre maps: the engine,
setStyle()in context, projection and camera