App startup, part 2 — extension seams & the fuller FimMap API — usage reference
Continues APP_STARTUP.md (mounting + file upload). This covers the three
extension seams a host reaches for less often — registering a custom map backend, registering a
custom file-format decoder or reprojector, and the parts of FimMap's own API beyond
addDataset/addLayer.
Contents
Map provider seam · Built-in provider options · Neutral vector style vocabulary · Materialize / decode extension seam · FimMap — the fuller instance API
Map provider seam
Two built-in backends cover the documented use — 'leaflet' (no credentials) and 'google'. Pick
one with the required provider option and read
Built-in provider options below; that is all most hosts ever need from
this seam. The registry hangs off FimViz, which is what boots a map:
FimViz.mapProviders() // → string[] — the backends available
FimViz.providerAcceptsCRS(name, crs) // → boolean — can it render content in this CRS?
createMap(el, options) // what mount() calls internally; you rarely call it directly
Adding a third backend is possible but deliberately under-documented.
FimViz.registerMapProvider(name, impl) takes an implementation of ~12 methods (create, addVector/
removeVector, fitBounds, the addRasterImage family, onMapMouseMove/onMapEvent, plus the
requiresApiKey and acceptsCRS properties) — src/package/mapProvider.js carries the full JSDoc
contract and two complete implementations to copy from, which is a better spec than a table here
could be. Four tiers stay provider-specific and outside the seam regardless: velocity's animated
canvas, the comparison draw-mask tool, HAZUS AdvancedMarkerElement markers, and FloodDepthLayer's
ArcGIS MapServer tiles — so a third provider gets a map, vectors and static rasters, not those.
One contract detail that bites even when using the built-ins: setRasterImageUrl returns the
handle to use next. Google can't swap a GroundOverlay's image in place, so it removes and
recreates; always reassign from the return value rather than reusing the handle you passed in.
onMapEvent's type is one of click/hover/dblclick/mousedown/mouseup/rightclick
(hover maps to the provider's mousemove; rightclick and contextmenu are synonyms that both work
on either built-in). The callback gets { type, lat, lng, originalEvent }, where originalEvent is
the DOM MouseEvent — for a UI that positions itself at the pointer.
Built-in provider options
// 'google' (requiresApiKey: true)
await mount('#el', {
provider: 'google', apiKey, version?, libraries?, center?, zoom?, mapId?, mapOptions?,
});
// version default 'weekly'; center defaults to the continental US; zoom defaults 5;
// mapOptions is raw google.maps.MapOptions, merged LAST (wins over every default/derived option)
// 'leaflet' (requiresApiKey: false)
await mount('#el', { provider: 'leaflet', center?, zoom?, tileUrl?, tileOptions?, mapOptions? });
// tileUrl defaults to OpenStreetMap; pass tileUrl: null to opt out of the default tile layer
// (e.g. to add your own via L.tileLayer yourself); mapOptions is raw L.Map options
Both accept the WGS84 family only (EPSG:4326/EPSG:4269) for acceptsCRS — a non-WGS84 raster
must be reprojected first (see DATASET_OPERATIONS.md). Neither is loaded
until it is used: create() dynamic-imports the SDK, so a Leaflet-only page downloads no Maps loader
and vice versa. An unregistered provider name throws config-invalid, listing the registered ones.
Leaflet needs leaflet.css. The provider imports it itself (leaflet/dist/leaflet.css, alongside
the SDK), which the shipped dist/fimviz.js resolves — style-loader injects it on first create(),
no CDN request. Bundling from fimviz/src instead makes that import your bundler's problem: give it
a CSS rule, or drop the import and add a <link> of your own. Without the stylesheet, tiles and
markers lay out wrongly rather than failing loudly.
Neutral vector style vocabulary
What makes VectorLayer.render({style})/setStyle(patch) provider-agnostic — describe a style ONCE,
each provider translates it to its own SDK names:
{ fillColor?, fillOpacity?, strokeColor?, strokeWidth?, strokeOpacity? }
Any other key passes through untouched to the provider's native option (e.g. Google's icon,
Leaflet's dashArray) — portable only via the neutral names, but a single-provider caller can still
use provider-native options directly.
style may also be a function ({feature, index}) => style | colorString | null, called once per
GeoJSON feature (same original feature + 0-based index on every provider) — a bare color string
shorthand expands to {fillColor, strokeColor}; null = that feature's provider-default styling.
The translation itself (styleToGoogle/styleToLeaflet) and the per-feature resolution
(featuresOf/resolveFeatureStyle) are internal to mapProvider.js — a VectorLayer applies
them for you. Read them there if you are implementing a provider.
Materialize / decode extension seam
The registries behind Dataset's lazy force — plug in a custom file format decoder, or swap the
reprojection implementation. They are statics on Dataset, the type they serve:
Dataset.registerMaterializer(format, fn) // fn: async (root, ds) => RasterGrid | VectorFeatures
// root: {kind:'inline', data} | {kind:'url', url}
// root.select: present ONLY when the Dataset came from an
// in-file selector ref — {variable, t, …}, the slice of a
// multi-dim source to decode. Absent for ordinary sources,
// so ignoring it is the correct default. See
// DATASET_OPERATIONS.md → "Axis entries".
Dataset.formats() // → string[] — every format decodable right now
// (built-ins + your own). Outside a custom decoder this
// is the one to reach for: build a file picker's
// `accept` list, or check an upload BEFORE parsing it.
Dataset.registerReprojector(fn) // fn: async (grid, toCrs) => RasterGrid — the ONE warp slot
Dataset.registerDefaultReprojectorLoader(fn)
// fn: async () => void — a JIT fallback invoked at most
// once, on the first force that finds nothing
// registered; it is expected to call
// Dataset.registerReprojector() before resolving. This
// is how the GDAL warp auto-loads with no setup call.
Dataset.registerResampler(fn) // methods beyond nearest/bilinear/average (cubic/lanczos/…)
registerBuiltinMaterializers() // a plain barrel function, not a Dataset static: it lives
// in io/materializers.js, which imports geotiff, and
// Dataset must never reach that. Idempotent, and it
// auto-runs at module scope there — which the barrel
// re-exports from, so importing anything from 'fimviz'
// has already registered the built-ins. Call it by hand
// only from a raw-browser page driving dist directly.
The registries' other readers (getMaterializer, getReprojector, resolveReprojector) are
internal: they hand back the implementation, and the seam already dispatches to it — a caller
who wants a decode calls ds.load(), and one who wants a warp calls ds.reproject(crs).
// Register a custom format decoder:
Dataset.registerMaterializer('my-format', async (root, ds) => {
const bytes = root.kind === 'url' ? await (await fetch(root.url)).arrayBuffer() : root.data;
// ... decode bytes ...
return new RasterGrid({ pixels, width, height, bounds, crs, noData, meta });
});
The two value types a decoder must return:
new RasterGrid({ pixels, width, height, bounds?, crs?, noData?, bands?, meta? })
new VectorFeatures({ features, bounds?, crs?, meta? }) // features: a GeoJSON FeatureCollection
FimMap — the fuller instance API
Beyond addDataset/addLayer/getLayer (see LAYERS.md):
// Map-event dispatch (hover/click routed to layers by hitTest + z-order)
fim.enableMapEvents(types?, { simultaneous? }) // types default ['click','hover']
fim.disableMapEvents()
fim.simultaneousLayerEvents = true|false // or read it back as a getter
fim.captureInteraction(handler) // MODAL: ALL map events go to handler until released;
// returns a release function (region-draw uses this)
fim.releaseInteraction()
fim.capturing // getter — is a modal interaction active?
// data-action wiring (declarative markup: <button data-action="foo">)
fim.registerAction(name, fn) // fn: function(this: Element, ...args)
fim.registerActions({ foo, bar }) // batch
fim.getAction(name) // this instance's registry, falling back to window[name]
fim.actionNames // string[] — registered on THIS instance only
fim.bindActions() // attach the one delegated listener (idempotent)
// scoped DOM query (resolves within this instance's root, not the whole document)
fim.$(sel) // Element|null
fim.$$(sel) // Element[]
fim.storage // getter — throws unless `storage` was passed to mount()/create()
fim.layerPanel // getter — this instance's per-instance panel (null if no runtime registered one)
fim.config // getter — this instance's app's config (the engine reads config only per-instance)
fim.destroy() // detaches the map, clears injected markup, releases from its FimViz