Overview
OnlyMap JS (@nika-js/onlymap) builds an interactive WebGL map from a small set of HTML custom elements. You write the layers, widgets, popups, interactions, and guided tours as markup. The library does the rendering, the data loading, the live updates, the picking, and the validation. You do not write deck.gl setup code, shader code, or a build step. One HTML file is a complete map application.
OnlyMap JS is also made for AI agents to write. HTML is a reliable output format for a model. The package includes an agent skill and an llms.txt file. OmMap.validate() returns structured errors, and each error has a fix. Together these give an agent a real authoring loop.
Install
Install the package from npm:npx @nika-js/onlymap init— adds VS Code IntelliSense for everyom-*element and attribute. It also adds!-prefix snippets (!starter,!map, …).npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs— installs the OnlyMap authoring skill into coding agents (Claude Code, Cursor, Codex). The agents then write correct manifests on the first try.
Key Features
Declarative Everything
- 37 layer types by name: all of deck.gl’s core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, and more). It adds a built-in
PopupLayerfor WebGL labels at scale, a nativeCOGLayerfor GeoTIFF rasters, aZarrLayerfor chunked Zarr/GeoZarr rasters, anImageOverlayfor georeferenced drone photos, and aBIMLayerfor in-browser IFC models. - Live manifest: the markup is the state. When you change an attribute — from a widget, a behavior, a script, or devtools — the map updates automatically.
- Built-in undo and redo: you can undo a layer toggle, a filter change, a basemap switch, an element edit, or a drawn sketch. Use
<om-widget type="undo-redo">or Cmd/Ctrl-Z.
A Full 3D Scene, Still Just Attributes new in 0.3
- Terrain:
terrain="terrarium"adds real elevation with no configuration (keyless AWS tiles). Geographic layers drape onto the surface automatically. You can also give your own DEM tile URL or register presets. - Scene lighting: set
lighting="daylight|studio|flat|custom", withlighting-*attributes for fine control.lighting-sun-datecomputes the sun position from a date and the map location. Alightingwidget shows these as presets and sliders. - Snapshots:
map.snapshot()captures the basemap and the WebGL canvas together, as a PNG data URL or a Blob. Use it for print and export pipelines. - BIM / IFC models new in 0.6.1:
<om-layer type="BIMLayer" src="./model.ifc">(or theifc-loaderdrop-zone widget, with multi-model federation) parses IFC in the browser via CDN-fetched web-ifc and renders it as 3D Tiles.pick-featuresgives per-element picking on any 3D Tiles layer; declarative isolate/hide/ghost, colour-by-property, anifc-browsermodel tree, afeature-inspector, and anifc-clashoverlay come as attributes and widgets. - Cut, export, and pick inside a 3D scene new in 0.6.2: three tools that work on whatever 3D content is already loaded — 3D Tiles, BIM models, extruded vector geometry alike. A clip box (
<om-map clip-box-min="[lng,lat,elev]" clip-box-max="…">, or theclip-boxwidget with draggable face handles) cuts the scene open;clip-box-invertshows what is outside instead,clip-box-highlightdims the cut-away geometry instead of removing it, andclip="off"opts a layer out. Region export (<om-widget type="draw" export-3d>) downloads the 3D content inside a drawn footprint as a portable GLB, orexport-3d="b3dm"for Cesium and 3D Tiles pipelines.pickable="3d"opts a layer into depth picking, so a click on a building face resolves on the face and carries a real elevation, instead of the ground point behind it. - Snapping to real geometry new in 0.6.2:
<om-map snap="vertex edge midpoint" snap-tolerance="12">refines a click or hover to the nearest vertex, edge, or edge midpoint of whichever feature is under the cursor — including a BIM model’s own wall corners. Vertex wins over midpoint over edge; hold Space to place a point without snapping, the usual CAD convention.snap="off"opts a layer out. - 3D city models new in 0.5: point
dataat a CityJSON file (.city.json, or a streamed.city.jsonl) — national building datasets such as 3DBAG and PLATEAU. The library decodes it to extruded footprints by default, or to real per-face roof geometry with?om-surfaces=1, and reprojects national coordinate grids (NL, CH, DE, JP, AT, SG) automatically.
Data That Stays Fresh
- Formats: JSON/GeoJSON, CSV/TSV, Apache Arrow and GeoArrow (columnar fast path, zstd IPC), Shapefile with
.dbfattributes, KML, GPX, the cloud-native FlatGeobuf (.fgb) and GeoParquet (.parquet) vector formats, and CityJSON / CityJSONSeq (semantic 3D city models). Large parsers load on first use. - GeoTIFF and Cloud-Optimized GeoTIFF new in 0.3.1:
<om-layer type="COGLayer" src="./dem.tif">streams raster tiles by HTTP Range request. The min/max restretch and the colormap are GPU uniforms, so the tiles are not fetched again. Nodata pixels are transparent. - Zarr / GeoZarr rasters new in 0.5.11:
<om-layer type="ZarrLayer" src="./x.zarr" variable="temp" select="time=0">renders chunked N-dimensional array data (climate/weather grids, datacubes) on the GPU. Pin the non-spatial dimensions withselect; GeoZarr stores georeference themselves, a plain Zarr takes manualbounds/crs/spatial-dims.srccan point at any CORS-enabled public remote store — no server setup needed. - Georeferenced drone photos new in 0.5.7:
<om-layer type="ImageOverlay" src="./DJI_0123.jpg" georeference="exif">reads a JPEG’s EXIF/DJI-XMP, computes its WGS84 footprint, and overlays the image — no manual corner-pinning.OmMap.resolveImageOverlay(file)preprocesses once for saved maps; explicitboundsreconstructs without re-reading EXIF. - Live sources: WebSocket streams with upsert-by-key and burst coalescing. Polled REST snapshots with
refresh="5s". - Private endpoints:
OmMap.configureData({ headers, credentials, fetch })keeps credentials out of the markup.
Application UI Without an App Framework
- Widgets: legend, layer switcher, basemap switcher, lighting controls, zoom controls, undo/redo, scale bar, attribution, GPU filter sliders (including readable date labels for epoch-millisecond fields), live Vega-Lite charts, a drawing toolbar, a measure tool, a clip-box control, and an event-driven
dynamic-chart. You can also write custom widgets with a typed context (ctx.data,ctx.stats,ctx.viewport,ctx.emit). - Measurement, including cut/fill volume new in 0.6.2:
<om-widget type="measure" modes="distance area volume">measures geodesic distance and area, and — involumemode — real earthworks cut and fill. Outline a footprint and the library integrates the terrain DEM cell by cell against a base surface, so one shape can report cut and fill on sloping ground, each with a published ± error quoted against the DEM’s own ground-sample distance. Choose the base surface withbase-surface(customdrag-a-plane,triangulatedboundary TIN,plane,lowest,highest,average).density,swell, andshrinkadd Bank/Loose/Compacted volumes and tonnage;profilesamples an elevation profile around the footprint for adynamic-chartto plot. Volume mode needsterrainon the map. - Safe date formatting:
formatDate($time, 'datetime', 'UTC')turns epoch-millisecond or ISO fields into readableget-textlabels without enabling arbitrary JavaScript. The same date style and time-zone contract is available on the built-in filter widget. - Managed, adaptive layout new in 0.4:
positionputs a widget in one of 8 logical slots (top-start…bottom-end), and the library sets the pixel position. Widgets in the same slot stack with a shared gap, and small controls next to each other join into one group. The map attribution and the license badge sit in the bottom slots as normal members, so they do not cover a widget. On narrow maps (640px or less by default; change it with--om-widget-fold-breakpoint) the widgets fold into drawers, and a slot dims when an open popup covers it.<om-map widgets-hidden>hides all your widgets but keeps them in the page, andposition="manual"turns off management for one widget. - Symbology-aware legends new in 0.3: the legend uses each layer’s real color styling. A continuous
scale()ramp becomes a gradient bar with domain labels. A threshold scale becomes discrete class ranges. A categorical expression becomes a swatch palette. A raster colormap becomes its ramp. You do not configure the legend. - Themeable from plain CSS: each built-in widget reads
--om-widget-*custom properties (om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }). You can set a dark theme with one CSS rule and no JavaScript. - Overlays and behaviors: HTML popups anchor to map coordinates or to a picked feature. Declarative
on="click|hover|drag|load"interactions need no event-handler code. - Map stories: an
<om-story>has steps that fire the same actions as behaviors, including basemap, lighting, and terrain changes. A player widget plays the steps, and you can scrub to any step.
Built for Verification
OmMap.validate(html)returns structured errors, and each error has a fix. Add thevalidateattribute for a live error panel on the page.- Headless test harness:
mountForTestruns full behavioral tests (picks, widgets, overlays, real projection math) in jsdom or happy-dom, with no WebGL. - Layout audit new in 0.4:
npx @nika-js/onlymap check-layout <manifest.html>checks the real widget layout in headless Chromium at several viewport widths. It confirms that no widgets overlap, that each widget is hit-testable and in its slot, and that same-slot edges align. It prints structured{severity, element, message, fix}diagnostics and exits 0 or 1. You can also importauditLayout(page)into your own Playwright suite. - React adapter:
@nika-js/onlymap/reactgives<OmMap>,<OmLayer>,<OmWidget>,<OmOverlay>, and a typeduseOmMap()hook over the same core. React keeps the state, and the adapter does not conflict with the virtual DOM. - Custom layer classes:
@nika-js/onlymap/deckre-exports the bundled deck.gl classes.OmMap.registerLayerextensions use the renderer’s own class hierarchy. A desktop app can register domain layers (local-file rasters, catalog sources) as first-class manifest types.
Framework & Programmatic Integration new in 0.4
MapController: a programmatic front-end for frameworks. TypedLayerDescriptors drive the same reconcile core, with no DOM manifest. It adds camera methods,emit,injectPick,snapshot, andonViewChange/onMapPoint/onTilesetLoadcallbacks.- External-store contract:
getStore(token)gives a{subscribe, getSnapshot}store for each token (viewport,selection,layers,data:<id>). The snapshots are cached, immutable, and tagged with anorigin. You can use them directly with React’suseSyncExternalStore, MobX, Redux, or Zustand. - Consumer events:
om-view-changedfires when the camera settles.om-map-pointfires on each click or hover with the map coordinate — use it for custom capture tools that the draw widget does not cover.om-tileset-loadgives aTile3DLayer’s liveTileset3D, for tools such as region export.om-measurecarries the measure widget’s full readout — distance, area, cut/fill volumes with their error bounds, material-adjusted figures, and the elevation profile series — so you can render your own readout instead of the built-in panel.
Roadmap
These features are designed but not shipped yet: Mapbox GL basemaps, depth-interleaved 3D compositing, globe projection, SSE transport, multi-field filters,dblclick behaviors, the transform data pipeline, the typed fluent builder, and stories and draw as React components. The README lists the current state in its “Not implemented yet” section at each release.
Resources
- npm:
@nika-js/onlymap - GitHub (docs, examples, llms.txt): NikaGeospatial/onlymapjs
- Product page: nikaplanet.com/onlymap