FIMViz.js

Layer subtypes — usage reference

Shared base mechanics (render/compute/setSources/the chainable ops/settings/events/...): see LAYERS.md. This doc covers every subtype's own methods/properties — not just its "ops" in the Dataset sense, but everything you can call on it.

Contents

RasterLayer · VectorLayer · ComparisonLayer · EnsembleAggregationLayer · App-tier renderFile subclasses

RasterLayer

One class for extent/userRaster/depth/ensemble/... — subtypes differ only by .type string, not by subclass. Registered as "raster".

new RasterLayer({ map?, type?, sources=[], id?, exclusive?, noData?, opacity=1 })

Properties

Property Type Notes
.overlay * The provider's positioned-image handle (GroundOverlay/ImageOverlay).
.rasterData TypedArray|null Pixel array, for hover lookups (valueAt).
.meta {bw,bs,be,bn,width,height,noData,unit}|null Populated by _draw().
.noData number|null Overrides the decoded grid's own noData when set.
.opacity number (0–1) Placement setting, applied live via the provider.
.colorScale ColorScale|null See COLOR_SCALE.md for everything about this.

Methods

layer.setNoData(v)                // chainable
layer.setOpacity(v)                // chainable — live via provider, no redraw needed
layer.hide()                       // chainable — overlay opacity -> 0 via the provider (overlay is
                                    // NOT torn down, so show() is instant); OVERRIDES the base's
                                    // flip-.visible-only no-op
layer.show()                       // chainable — restores the overlay to its configured .opacity
layer.set({ colorScale: cs })      // cs: ColorScale|null — VALIDATED before mutating (throws on a
                                    // non-ColorScale value cleanly, doesn't corrupt .colorScale)
layer.getLegend()                  // Legend derived from .colorScale, or null until one is attached
await layer.getStats(opts?)        // Stats classified by .colorScale; null until pixels are loaded
                                   // (VectorLayer: feature counts by type + area/length/bbox,
                                   //  computed from dataset.data — no map or render required)
layer.fit()                        // fit the map to this raster's bounds
layer.hitTest(lat, lng)            // PIXEL-LEVEL — true only over a real (non-noData) pixel, so a
                                    // click over a transparent part falls through to layers below
layer.valueAt(lat, lng, opts?)     // the pixel value at a point (nearest cell), or null.
                                    // opts.noDataTolerance (default 0 = exact match) widens the
                                    // noData check to |v-noData|<=tolerance — for a raster whose
                                    // sentinel can drift after resampling. hitTest() always uses the
                                    // default; widening only affects a direct valueAt() call.
layer.enableHover(opts?)           // wires a live hover readout: onMapMouseMove + valueAt() on every
                                    // move, emits 'hover' {lat,lng,value,text}. Idempotent (replaces,
                                    // doesn't stack); torn down automatically on remove(). Opts:
                                    // noDataTolerance (→ valueAt), isEmpty(v)=>bool (an in-range value
                                    // to ALSO treat as absent — e.g. "zero means nothing" for a depth
                                    // layer), formatValue(v)=>string (defaults to String(v)).
layer.disableHover()               // stop a hover readout wired by enableHover() — a no-op if none active

ColorScale resolution on render

When _draw() runs and no ColorScale is attached yet, one is resolved automatically, in order: (1) already explicit → untouched, (2) a GDAL-embedded legend auto-detected from the file → built via ColorScale.fromGdalLegend, (3) a default continuous scale (ColorScale's own default palette, "blues") ranged to the grid's own min/max. Whichever is chosen is attached, so .colorScale/ getLegend()/getStats() always reflect what's actually drawn. Full detail: COLOR_SCALE.md.

Settings knobs (layer.settings.set({...}))

Key Effect
palette routes to the attached colorScale — no-op if none attached yet.
continuous routes to the attached colorScale.
noData setNoData(v)'recomputed' (redraw).
opacity setOpacity(v)'restyle'.
hover Interaction-only; a tooltip reads settings.get('hover').

Registered-type construction

await fim.addLayer('raster', { source: ds, sources?, noData?, opacity?, colorScale?, render? })

VectorLayer

Renders a vector Dataset's GeoJSON via the provider's addVector/removeVector. Registered as "vector". Note: render() here is synchronous (returns VectorLayer, not a Promise) — unlike the base/RasterLayer async pipeline, since parsing nothing is needed to draw already-parsed GeoJSON.

new VectorLayer({ map?, type?, sources=[], id?, exclusive?, colorScale?, colorBy? })

Properties

Property Notes
.dataLayer The provider's vector handle.
._style The current neutral style patch (fillColor/strokeColor/fillOpacity/...).
.colorScale ColorScale|null — grades features by colorBy (below).
.colorBy string|null — the feature property whose value the scale reads.

Methods

layer.render({ style? })          // SYNC. style: provider-native style object; throws if unmounted
                                    // or the Dataset has no GeoJSON, or the provider lacks addVector
layer.setStyle(patch)              // merge + re-add (no in-place setVectorStyle in the provider
                                    // contract — this removes + re-adds)
layer.hide()                       // chainable — removes the dataLayer via the provider (no vector
                                    // "invisible" primitive short of removing it); OVERRIDES the
                                    // base's flip-.visible-only no-op
layer.show()                       // chainable — re-adds via addVector, at the last style, if hidden
layer.fit()                        // fit the map to the Dataset's bounds
layer.hitTest(lat, lng)             // true if the point falls inside any feature's geometry
layer.featureAt(lat, lng)          // the first matching GeoJSON Feature, or null (drives an info window)

Building from CSV/XYZ (both produce a vector Dataset → VectorLayer)

fim.addDataset(source, options)'s options for these two formats:

// CSV — a row becomes a Point (lat/lng column pair) or whatever geometry a WKT/GeoJSON column
// carries. Common column names auto-detect when no mapping is given (lat/latitude/y,
// lng/lon/long/longitude/x, geometry/geom/wkt/the_geom); every other column → feature properties.
await fim.addDataset(csvFile, { latField?, lngField?, geometryField?, delimiter? })

// Read the header row FIRST (before parsing) to build a column-mapping picker:
import { csvHeaders } from 'fimviz';
const headers = csvHeaders(await csvFile.text(), { delimiter? });

// A failed auto-detect throws with `.columns` attached (the detected headers), for a reactive picker:
try { await fim.addDataset(csvFile); }
catch (e) { console.log(e.columns); }   // present only on this specific failure

// WKT parsing standalone (what geometryField uses internally), if you need it directly:
import { wktToGeometry } from 'fimviz';
wktToGeometry('POINT(-90.07 29.95)');   // → GeoJSON geometry; POINT/MULTIPOINT/LINESTRING/
                                          // MULTILINESTRING/POLYGON/MULTIPOLYGON (2-D only)

// XYZ — headerless x,y,z point files (LiDAR/survey dumps). Extra columns beyond x,y,z → z2, z3, ...
await fim.addDataset(xyzFile, { swapXY? })   // swapXY:true if a file orders northing/easting first

Colouring features by a property

A VectorLayer owns a ColorScale exactly as a RasterLayer does — it's the same class, because nothing in it is pixel-specific: it maps a value to a colour, and a feature property is as good a source of that value as a pixel. colorBy names the property to read:

layer.set({
  colorScale: new ColorScale({ palette: 'viridis', min: 0, max: 12, continuous: true }),
  colorBy: 'depth_ft',
});

layer.getLegend();          // → a Legend, same as a raster's
await layer.getStats();     // → byClass buckets, keyed by the same scale
layer.set({ palette: 'plasma' });   // routes to the attached scale, restyles live

Both knobs are needed — a scale with no colorBy (or the reverse) leaves the flat color/style in charge.

Features with no usable value (property absent, null, '', or non-numeric) are never given a colour from the ramp — "no value" must not be able to look like a real one. What they do get is yours to choose, via the scale's missingColor:

layer.set({ missingColor: '#cccccc' });   // paint them explicitly as "no data"
layer.set({ missingColor: null });         // (default) leave them at the base style

Either way they still count in featureCount and land in no byClass bucket. missingColor sits outside the three colouring modes — it answers a question none of them can — so switching palette/stops/colorStops leaves it untouched.

Under the hood this hands the provider a per-feature style function (the seam already supports one) merged over whatever flat style render({style})/setStyle() set — so an explicit strokeWidth still applies while the colour comes from the scale.

Settings knobs

Key Effect
color setStyle({fillColor: v, strokeColor: v})'restyle'.
opacity setStyle({fillOpacity: v})'restyle'.
colorScale Attach/detach the scale (validated — a palette-name string throws) → 'restyle'.
colorBy The feature property the scale reads → 'restyle'.
palette / continuous / missingColor Route to the attached colorScale; no-op if none is attached — the same keys a raster takes, so one UI control fits either kind.
useFileColors 'restyle' (the UI reads this back; no direct style call here).
hover Interaction-only.

Registered-type construction

await fim.addLayer('vector', { source: ds, sources?, style? })   // resolves synchronously (still awaitable)

ComparisonLayer

Aligns N (2–8) extent rasters onto one grid, classifies which subset is wet (2ⁿ−1 colours), and — for exactly 2 sources — scores agreement (confusion-matrix metrics). Registered as "comparison". Headless: names no map SDK; emits 'computed' for a UI binder.

new ComparisonLayer({ map?, sources=[] })   // sources: {pixels,meta}[] | RasterLayer[]

Methods

layer.compute({ policy?, method?, colors?, dryValue?, mask? })
// policy: 'low'|'high'|'average' (default 'high' — a warning is recorded, not thrown, if omitted)
// method: a resample method name (default 'nearest'; unknown → warning + falls back)
// colors: 2^n-1 colour override array · dryValue: the "dry" sentinel (default -99999)
// mask: draw polygon scoping the 2-ary metrics
// → returns { grid, rgba, counts, nLayers, metrics, policy, method, warnings, aligned, dryValue }
//   and emits 'computed' (→ map bus 'comparison:computed') with the same payload

await layer.prepare()          // materializes any Dataset sources into RasterGrids first — needed
                                // because compute() itself is SYNC and can't force a Dataset (async)
layer.getAligned()              // the last compute()'s aligned per-layer pixel arrays, or null
layer.metricsForMask(mask)      // recompute ONLY the 2-ary metrics under a new mask, reusing the
                                // already-aligned pixels — null if nLayers !== 2

Registered-type construction

await fim.addLayer('comparison', { sources: [{pixels,meta}, ...], policy?, method?, colors?, compute? })
// compute defaults true (computes at construction); pass { compute: false } to build without computing

EnsembleAggregationLayer

The ensemble twin of ComparisonLayer — same align-N-onto-one-grid shape, different reduction: asks how many members are wet (a per-pixel agreement count 0..N over an N-colour ramp), not which subset. Registered as "ensembleAgreement". Distinct from layers/ensemble.js's single-file pre-baked-GeoTIFF RasterLayer (type 'ensemble') — this one aggregates multiple member rasters.

new EnsembleAggregationLayer({ map?, sources=[] })   // sources: {pixels,meta}[] | RasterLayer[]

Methods

layer.compute({ policy?, method?, colors?, dryValue? })
// same policy/method/dryValue as ComparisonLayer; colors: N-colour agreement ramp (omitted → a
// viridis ramp + a warning, from the reducer)
// → returns { grid, rgba, perPixel, histogram, nLayers, policy, method, warnings }
//   and emits 'computed' (→ map bus 'ensembleAgreement:computed')

await layer.prepare()    // same as ComparisonLayer — materialize Dataset sources first
layer.getAligned()        // the last compute()'s aligned per-member pixel arrays, or null

Registered-type construction

await fim.addLayer('ensembleAgreement', { sources: [{pixels,meta}, ...], policy?, method?, colors?, compute? })

App-tier renderFile-pattern subclasses

Three real Layer subclasses drive the "one uploaded user file → one persistent rendering singleton" pattern (EnsembleLayer/DepthLayer in the engine's layers/, DamageLayer in the app): a thin subclass whose constructor just sets type, and one method:

await layer.renderFile(fileData, map)   // delegates to the subsystem singleton, takes ownership of
                                          // teardown, sets .visible=true, emits 'rendered'

layer._name (the filename) must be set first — fim.registerNamedLayer(name, layer) does that. These exist so every user-file type is a proper Layer (participates in fim.layers/removeLayer/ 'removed'), even though the actual pixel-level rendering lives in a module-level subsystem singleton, not on the instance.