@openwaters/seamap
v0.1.0
Published
MapLibre GL style for Open Waters Seamap – base map, bathymetry, chart symbology
Downloads
79
Readme
@openwaters/seamap
The Open Waters nautical chart as a MapLibre GL style: a VersaTiles base map, Seascape bathymetry, and chart symbology (buoys, beacons, lights, topmarks, landmarks, restricted areas) with the sprite sheet that draws it.
Whole style
style() assembles everything; setup() registers the runtime images the style depends on (the generic-icon fallback and the "unsurveyed water" stipple):
import { style, setup, attribution } from "@openwaters/seamap";
const map = new maplibregl.Map({
container: "map",
style: await style({ spriteBase: new URL("sprites", document.baseURI).href }),
attributionControl: { customAttribution: attribution },
});
setup(map);style() is async: land hillshading comes from the VersaTiles elevation tiles, and the builder fetches their TileJSON. Options: tiles (seamark TileJSON URL), seascape, versatiles, language, spriteBase, hillshade (on by default; false to skip, or an object to tune the shading), and the seascape passthroughs — flavor (overrides merged over its day), unit, safety, shading, and the dem/vector/coverage source id overrides.
Composed
sources() + layers() hand over just the chart symbology for a style you assemble yourself, following the same split as seascape:
import { sources, layers, sprite, handleMissingImages, attribution } from "@openwaters/seamap";
const { areas, symbols } = layers();
const style = {
version: 8,
sources: { ...mySources, ...sources() },
sprite: [mySprite, sprite(new URL("sprites", document.baseURI).href)],
// areas go below land fills and labels; symbols on top of everything
layers: [...myBaseLayers, ...areas, ...myLandLayers, ...symbols],
};
const map = new maplibregl.Map({ style /* ... */ });
handleMissingImages(map);sources({ url? })— theseamapvector source (defaults tohttps://tiles.openwaters.io/seamap/tiles.json).layers({ font? })— the chart layers, split intoareasandsymbolsto preserve draw order around your land layers.fontrenames glyph fontstacks ("Noto Sans Regular") to match your glyph server.sprite(base)— thestyle.spriteentry pointing at wherever you serve the sheet.handleMissingImages(map)— falls back to each shape'sgenericicon when tags compose a colour combination the sheet doesn't carry. Without it, unusual marks render as nothing.attribution— the sprite artwork credit; sprites aren't a MapLibre source, so pass it ascustomAttribution.
Sprites
The built sheet ships in the package at sprites/dist/ (freenauticalchart.{json,png}, @2x variants, LICENSE, PROVENANCE.md). MapLibre loads sprites from a URL prefix — it appends .json/.png/@2x itself — so the files must be served together under a stable path rather than imported through a bundler's asset pipeline. With Vite:
import { viteStaticCopy } from "vite-plugin-static-copy";
viteStaticCopy({
targets: [{ src: "node_modules/@openwaters/seamap/sprites/dist/*", dest: "sprites" }],
});Icon names are composed from tag values at render time, so the style layers and the sprite sheet must always move together — always serve the sheet from this same package version.
In this repo the sheet is generated, not committed: bin/sprites (here in style/) expands the vendored SVG sources in sprites/ into sprites/dist/. Needs spreet (pinned in the repo's mise.toml) and Python 3. npm publish runs it via prepublishOnly; consumers of the published package never need the toolchain.
The same run composes the poi-* badges via bin/poi-badges, pulling glyphs from the CC0 Maki and Temaki sets (the @iconify-json/* dev dependencies) and wrapping each in a halo and disc. Those packages have to be installed first, so npm install comes before bin/sprites, not after. To add an amenity symbol, map a sprite name to a glyph in sprites/poi-icons.json — there is nothing to draw. Both scripts write into sprites/icons/gen/, which is gitignored, so no generated artwork is ever committed.
Chart layers
The symbology lives in layers/, grouped by what it draws. Each module exports one function returning its layers in draw order; layers/index.ts concatenates them and makes the areas/symbols split.
| module | draws | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | areas.ts | rocks, wrecks and obstructions, seabed quality, restricted and allowed areas | | routes.ts | traffic separation schemes, ferry routes, navigation lines and tracks, submarine cables and pipelines | | structures.ts | piers and breakwaters, piles and dolphins, platforms, cranes, shore stations, harbours, small craft facilities | | lights.ts | lit-mark flares, sector arcs and rays, major and minor lights, fog signals | | marks.ts | buoys and beacons, topmarks, radar reflectors | | labels.ts | landmarks and all name text, including the light characteristic |
The order of that concatenation is load-bearing twice. Paint order is the obvious half. The other is symbol collision: MapLibre places symbols in reverse draw order, so a layer listed later wins the anchor in a crowded harbour — which is why labels come last. index.test.ts asserts the full id order, so a reshuffle can't happen by accident.
Layer geometry comes from the seamark and light layers of the seamap tiles; the symbols come from the sprite sheet, and because icon names are composed from tag values the two must move together.
The symbology derives from styles/freenauticalchart.json in signalk-seamap-plugin (CC0), which in turn draws the sprite set from quantenschaum/mapping. Positioning conventions — tight-cropped icons anchored and offset in the style rather than padded in the sheet — follow that project's own vector/styles/s57.json.
Versioning
Semver tracks the consumer-facing contract: renaming or reordering layer ids, changing the icon naming scheme, or removing icons is major; new layers or icons are minor; visual tweaks are patch.
License
GPL-3.0. The sprite artwork is GPL-3.0 from quantenschaum/mapping (see sprites/PROVENANCE.md), which carries the package as a whole; the symbology it derives from is CC0.
