Add marine vector charts to your map
Connect an entitled PMTiles archive, then use the NautaCharts plugin to install chart layers, sprites, and themes without replacing your base map or operational overlays.
New to marine vector charts? Read the architecture guide or check coverage and sources.
Integration overview
NautaCharts adds chart portrayal to an existing MapLibre or Mapbox map. The host application remains responsible for the map, base style, camera, PMTiles transport, authenticated vector source, and operational overlays.
Prerequisites
- A browser API key and an organization entitled to Europe, USA, or both.
- An existing MapLibre GL JS or current Mapbox GL JS map with a loaded style.
- The
pmtilespackage and one-time protocol registration when using MapLibre. - An optional host layer ID, such as the first AIS or vessel layer, to keep operational overlays above the chart.
Implementation sequence
- Create the account, select the correct organization, generate a browser key, and confirm archive access.
- Test an authenticated byte-range request before introducing the map renderer.
- Load the NautaCharts plugin once and wait for the host map style.
- Register PMTiles for MapLibre, then add the entitled archive as a host-owned vector source.
- Call
addTowith the source ID, archive name (pmtilesName), theme, and optional insertion layer. - Confirm that
layerIdsis non-empty and inspectfailedLayers. WithblockOnSprites: false, layers are ready first and symbols continue warming in the background. - After replacing the host style, restore the source and reinstall the plugin layers.
<script src="https://maps.scalarmaps.com/maritime-vectors-plugin.js"></script>Place this blocking script before your application bundle, or use the promise-based loader in Plugin API. Do not call addTo until the plugin global exists.
Account and archives
Keys identify the organization at the tile edge. Archive entitlements and quota state determine which PMTiles files that key can read.
- Create an account or sign in and verify the selected organization.
- Open Chart access and confirm Europe or USA is enabled for that organization.
- Open API keys, generate an
sm_pk_live_...browser key, and store it in your public runtime configuration. - Revoke keys that are no longer used. Create a replacement before removing a key used by a deployed application.
| Archive | Authenticated source URL | Baked zooms |
|---|---|---|
| Europe | https://tiles.scalarmaps.com/pmtiles/europe.pmtiles?token=<browser-api-key> | z5-z15 |
| USA | https://tiles.scalarmaps.com/pmtiles/usa.pmtiles?token=<browser-api-key> | z5-z14 |
The camera may zoom beyond the baked maximum by overzooming. Keep the vector source at 15 for Europe and 14 for USA so the PMTiles client does not request nonexistent higher-zoom tiles.
headers=$(mktemp)
status=$(curl --silent --show-error \
--dump-header "$headers" \
--header "Range: bytes=0-16383" \
--output /dev/null \
--write-out "%{http_code}" \
"https://tiles.scalarmaps.com/pmtiles/europe.pmtiles?token=<browser-api-key>")
cat "$headers"
rm "$headers"
test "$status" = "206" || { echo "Expected HTTP 206, received $status"; exit 1; }MapLibre GL JS
Install PMTiles, register its protocol once in the application lifecycle, and prefix the authenticated HTTPS archive with pmtiles://.
npm install maplibre-gl pmtilesimport * as maplibregl from "maplibre-gl"
import "maplibre-gl/dist/maplibre-gl.css"
import { Protocol } from "pmtiles"
const region = "europe" // Change to "usa" when entitled.
const maxZoom = region === "europe" ? 15 : 14
const archiveUrl =
"https://tiles.scalarmaps.com/pmtiles/" + region + ".pmtiles?token=<browser-api-key>"
// Register this once in the application lifecycle.
const protocol = new Protocol()
maplibregl.addProtocol("pmtiles", protocol.tile)
async function waitForStyle(map) {
if (map.isStyleLoaded()) return
await new Promise((resolve) => {
let settled = false
const ready = () => {
if (settled) return
settled = true
map.off("load", ready)
map.off("style.load", ready)
resolve()
}
map.on("load", ready)
map.on("style.load", ready)
if (map.isStyleLoaded()) ready()
})
}
await waitForStyle(map)
map.addSource("scalarmaps-inland-vectors", {
type: "vector",
url: "pmtiles://" + archiveUrl,
minzoom: 7,
maxzoom: maxZoom,
})
// Optional for pitched MapLibre maps only. This is host-side, not a plugin requirement.
if (typeof map.setSourceTileLodParams === "function") {
map.setSourceTileLodParams(4, 2, "scalarmaps-inland-vectors")
}
const enc = await window.InlandVectorsMapLibre.addTo(map, {
theme: "day",
pmtilesName: region,
sourceId: "scalarmaps-inland-vectors",
replaceSource: false,
beforeId: "<first-host-overlay-layer>",
blockOnSprites: false, // Return while sprite warmup continues.
bearingCorrection: true, // Default; set false only if the host owns that policy.
// inspectOnHold: { delayMs: 3000, maxFeatures: 8 }, // Opt-in mouse inspector.
})Load the NautaCharts plugin before calling InlandVectorsMapLibre.addTo. In React, register the PMTiles protocol at the application root and remove it only when that root unmounts.
Mapbox GL JS
Mapbox GL JS 3.21 and later detects PMTiles from the archive URL and can use the authenticated HTTPS URL directly.
const region = "europe" // Change to "usa" when entitled.
const maxZoom = region === "europe" ? 15 : 14
const archiveUrl =
"https://tiles.scalarmaps.com/pmtiles/" + region + ".pmtiles?token=<browser-api-key>"
async function waitForStyle(map) {
if (map.isStyleLoaded()) return
await new Promise((resolve) => {
let settled = false
const ready = () => {
if (settled) return
settled = true
map.off("load", ready)
map.off("style.load", ready)
resolve()
}
map.on("load", ready)
map.on("style.load", ready)
if (map.isStyleLoaded()) ready()
})
}
await waitForStyle(map)
map.addSource("scalarmaps-inland-vectors", {
type: "vector",
url: archiveUrl,
minzoom: 7,
maxzoom: maxZoom,
})
const enc = await window.InlandVectorsMapbox.addTo(map, {
theme: "day",
pmtilesName: region,
sourceId: "scalarmaps-inland-vectors",
replaceSource: false,
beforeId: "<first-host-overlay-layer>",
blockOnSprites: false, // Return while sprite warmup continues.
bearingCorrection: true, // Default; set false only if the host owns that policy.
// inspectOnHold: { delayMs: 3000, maxFeatures: 8 }, // Opt-in mouse inspector.
})Insert chart layers below your first ship, AIS, track, route, or other operational overlay. Wait for that layer before calling addTo.
Plugin API
The standalone plugin is a style and sprite installer. It exposes one install function and returns a small lifecycle handle.
Load it once
A normal script tag is sufficient for a static page. Applications that mount maps on demand can use one shared promise so concurrent map views never insert the script twice.
let scalarMapsPluginPromise
export function loadScalarMapsPlugin() {
if (window.InlandVectorsMapLibre) {
return Promise.resolve(window.InlandVectorsMapLibre)
}
scalarMapsPluginPromise ??= new Promise((resolve, reject) => {
const script = document.createElement("script")
const fail = (error) => {
script.remove()
scalarMapsPluginPromise = undefined
reject(error)
}
script.src = "https://maps.scalarmaps.com/maritime-vectors-plugin.js"
script.async = true
script.onload = () => {
const plugin = window.InlandVectorsMapLibre
plugin ? resolve(plugin) : fail(new Error("NautaCharts plugin did not initialize"))
}
script.onerror = () => fail(new Error("NautaCharts plugin failed to load"))
document.head.append(script)
})
return scalarMapsPluginPromise
}The script creates both globals. InlandVectorsMapLibre and InlandVectorsMapbox reference the same API, so use the name that matches the host map library.
Install options
Call addTo(map, options) after the host style is loaded and the vector source exists. The plugin waits briefly for map readiness, but it does not create the map, register PMTiles, or add the authenticated source.
| Option | Default | Purpose |
|---|---|---|
| theme | day | day, dusk, or dark. A theme string may also be passed directly as the second argument. |
| pmtilesName | europe | Archive used when requesting the NautaCharts style. Pass europe or usa to match the host source. |
| sourceId | inland-vectors | ID of the existing host vector source containing NautaCharts source layers. |
| replaceSource | false | Compatibility marker used in examples. It is currently a no-op: the plugin always requires a host source and never replaces or removes it. |
| beforeId | none | Existing host layer ID before which chart layers are inserted. If it does not exist, layers use the configured slot or normal top insertion. |
| slot | auto | Mapbox style slot for chart layers. Without beforeId, slot-based styles default to top. |
| layerPrefix | ienc | Prefix applied to installed chart layer IDs. Use a unique prefix for separate installs. |
| hiddenLayerIds | built-in set | Additional unprefixed NautaCharts layer IDs to hide alongside the plugin's built-in hidden layers. |
| baseUrl | script origin | Override the style and sprite service origin. Most integrations should leave this unset. |
| mapLoadTimeoutMs | 5 seconds | Maximum wait for the host map style to become ready, clamped to 250 ms through 30 seconds. |
| requestTimeoutMs | 8 seconds | Style, metadata, and sprite request timeout, clamped to 250 ms through 30 seconds. |
| bearingCorrection | true | Corrects the two line-centred absolute-orientation symbols once after rotation settles, zoom ends, or a full style reload. Set false only when the host owns that orientation policy. |
| inspectOnHold | off | Set true or { delayMs: 3000, maxFeatures: 8 } for a mouse-only inspector of rendered NautaCharts layers and key properties at a stationary point. It closes on pointer or map movement. |
Sprite options
The defaults prioritize complete symbols. Change these only when initial map responsiveness matters more than immediate sprite availability.
| Option | Default | Purpose |
|---|---|---|
| blockOnSprites | true | Set false to return after layers install while sprite warmup continues. |
| warmSprites | true | Set false to skip eager sprite registration and load missing images on demand. |
| pixelRatio | 2 | Packed sprite atlas scale. Use 1 only when a lower-density atlas is preferred. |
| spriteWarmupBatching | false | Yield between sprite batches during eager warmup. |
| spriteWarmupBatchSize | 50 | Enables batching and sets 1-200 images per batch. |
| spriteWarmupDelayMs | 0 | Delay between batches, clamped to 0-250 ms. |
enc.setVisibility(false)
enc.setVisibility(true)
await enc.setTheme("dusk") // "day" | "dusk" | "dark"
console.log(enc.sourceId, enc.layerIds, enc.failedLayers)
// Remove NautaCharts layers and generated-image handlers.
enc.remove()// The install handle lists every NautaCharts layer it owns.
for (const layerId of enc.layerIds) {
if (layerId.includes("soundings")) {
map.setLayoutProperty(layerId, "visibility", "none")
}
}window.InlandVectorsMapbox and window.InlandVectorsMapLibre expose version and addTo.layerIds, failedLayers, sourceId, setVisibility, setTheme, and remove. Theme changes update the same handle.failedLayers lists the layer ID, source layer, and error for any layer the host style rejected.window.__inlandVectorsLastInstall.map.setSourceTileLodParams(4, 2, sourceId) after adding the source to reduce simultaneous source tiles.inspectOnHold for a three-second mouse hold inspector. It is intentionally disabled by default and does not run for touch gestures.Operations and troubleshooting
Start with the authenticated archive range request, then verify renderer setup, plugin loading, and layer installation in that order.
| Status | Meaning | Fix |
|---|---|---|
| 401 | Missing or unknown browser key. | Add the token query parameter or generate a replacement key. |
| 403 | Inactive key or missing archive entitlement. | Check key status and confirm access to the requested archive. |
| 404 | PMTiles archive was not found. | Check the archive name and deployment. |
| 416 | Missing or invalid Range header. | Let the PMTiles client issue a valid byte range. |
| 429 | Organization quota is blocked. | Review account usage and quota state. |
| 5xx | Edge, storage, or service configuration failure. | Retry once with backoff, then record the URL, status, and time and contact NautaCharts. Persistent failures require operator action. |
| Symptom | Likely cause | Check |
|---|---|---|
| Plugin global missing | Plugin script was blocked, failed, or has not finished loading. | Allow maps.scalarmaps.com in script-src and load the script before the application, or await the shared loader promise. |
| Protocol error | The PMTiles protocol was not registered in MapLibre. | Register Protocol.tile once and use the pmtiles:// prefix around the authenticated HTTPS URL. |
| Source missing | The style or host-owned source does not exist yet. | Wait for the host style, add the vector source, and pass its exact ID to addTo. |
| Partial install | The host style rejected one or more chart layers. | Inspect failedLayers and window.__inlandVectorsLastInstall for the rejected layer and message. |
maxzoom: 15 and USA at maxzoom: 14. A higher source maximum makes the protocol request tiles that do not exist.beforeId, and install only after that layer exists.maps.scalarmaps.com in script-src, connect-src, and img-src. Allow tiles.scalarmaps.com in connect-src.