Console snippets — usage reference
A companion to the docs in this folder (USAGE.md, APP_STARTUP.md, APP_STARTUP_ADVANCED.md, LAYERS.md, LAYER_SUBTYPES.md, STORAGE.md, COLOR_SCALE.md, LEGEND.md, UI.md, DATASET_OPERATIONS.md) — one runnable snippet per documented method, in the same order as the doc that names it, so you can walk a doc top-to-bottom and paste the matching block into the console to check it still behaves as written. Not a test suite (no assertions) — read the printed value yourself.
Contents
Setup · 1. USAGE.md · 2. APP_STARTUP.md · 3. APP_STARTUP_ADVANCED.md · 4. LAYERS.md · 5. LAYER_SUBTYPES.md · 6. STORAGE.md · 7. COLOR_SCALE.md · 8. LEGEND.md · 9. UI.md · 10. DATASET_OPERATIONS.md
Setup
These snippets assume a page that has booted a FimMap and exposed the package on window — a real,
booted FimMap as window.fim plus every export these snippets use callable directly. Any host page
that does import * as FIM from 'fimviz'; import * as UI from 'fimviz/ui'; Object.assign(window, FIM, UI) and mounts a map will do — the UI module is a separate entry, so both
imports are needed. No page in examples/ does that any more; the quickest bench is to open
examples/01-quickstart.html, which already has a booted map,
and paste the two imports plus the Object.assign into its console.
Registries are reached through the type that owns them (Layer.registerType,
ColorScale.registerPalette, Dataset.registerMaterializer, FimViz.registerMapProvider, …), so
those calls appear qualified below rather than as bare globals.
Wait for window.fim to exist (mount() resolves asynchronously — the app's own .then sets it):
fim // FimMap — should already be your booted map
FimViz // the namespace
Dataset, Storage, Layer, RasterLayer, VectorLayer, ColorScale, Legend, Stats, Filter, SpatialFilter
A small fetch helper + a bounds→Stats-shaped-meta helper, used throughout below:
async function urlToFile(url, name, type = 'application/octet-stream') {
const res = await fetch(url);
if (!res.ok) throw new Error(`${url}: ${res.status}`);
return new File([await res.blob()], name, { type });
}
// RasterGrid.bounds is {north,south,east,west}; several functions (Stats.raster, resampleGrid,
// alignRasters, resolveTargetGrid) instead want the flat {bw,bs,be,bn,width,height} shape.
function gridMeta(grid, extra = {}) {
const b = grid.bounds || {};
return { bw: b.west, bs: b.south, be: b.east, bn: b.north, width: grid.width, height: grid.height,
noData: grid.noData, ...extra };
}
Sample files this doc reuses (served by the dev app under /assets/SampleFiles/):
const RASTER_URL = '/assets/SampleFiles/4326.tif'; // WGS84 already, per filename
const DEPTH_URL = '/assets/SampleFiles/Brazos_RP100_depth.tif'; // a second, differently-shaped raster
const CMP_A_URL = '/assets/SampleFiles/Compare_0-0-DEP-12840.tif'; // two aligned extent rasters —
const CMP_B_URL = '/assets/SampleFiles/Compare_0-0-DEP-17780.tif'; // good for Comparison/Ensemble tests
// A real FeatureCollection (99 county polygons). NOT Iowa_city.json — that one is a site-config
// blob with no `type`/`features`, so parseFile rejects it as vector.
const GEOJSON_URL = '/assets/SampleFiles/Iowa_County_Boundaries.json';
const KMZ_URL = '/assets/SampleFiles/ames.kmz';
One raster + one vector Dataset, reused across sections (re-run this any time you reload the page):
const rasterFile = await urlToFile(RASTER_URL, '4326.tif', 'image/tiff');
let ds = await fim.addDataset(rasterFile); // → Dataset, kind:'raster'
console.log(ds.kind, ds.format, ds.crs, ds.bounds);
const vectorFile = await urlToFile(GEOJSON_URL, 'Iowa_County_Boundaries.json', 'application/geo+json');
const dsVector = await fim.addDataset(vectorFile); // → Dataset, kind:'vector'
console.log(dsVector.kind, dsVector.format, dsVector.crs, dsVector.bounds);
const grid = await ds.grid(); // decode once, reuse below
console.log(grid.width, grid.height, grid.bounds, grid.noData);
1. USAGE.md
FimViz namespace
FimViz.current(); // → this page's FimVizInstance (or null before boot)
FimViz.current().config; // this app's locked config
FimViz.current().maps; // → [fim] (the one mounted map)
FimViz.current().isDefault; // true (the dev site never passes {isolated:true})
FimViz.current().hasMaps; // true
const dsHead = await FimViz.parseFile(rasterFile); // pure — no instance, no side effect
dsHead.toJSON();
FimMap instance
fim.root; fim.app; fim.map; fim.map === fim.map;
fim.config;
fim.storage; // ⚠️ THROWS on the dev site — SITE_CONFIG passes no `storage` option
fim.layerPanel; // the app's real Layer Panel (this map has a registered runtime)
fim.datasets; // Dataset[] — includes ds/dsVector from Setup
fim.layers; // Layer[]
fim.namedLayers; // Layer[] — the subset registered under a filename
fim.$('#layer-panel'); // scoped querySelector (root-relative, not document-relative)
fim.$$('.layer-toggle-window');
Delegated actions (data-action markup) — see 4. LAYERS.md below for
registerAction/registerActions/bindActions/actionNames; the widget's own widget.html already
uses this mechanism, so fim.actionNames.length > 0 should already be true on the dev site.
Configuration
fim.config.provider; // 'google' on the dev site
fim.config.apiKey; // present, truthy
fim.config.dataSource; // SITE_CONFIG.dataSource
fim.config.corsProxy;
fim.config.resolveUrl; // siteResolveUrl — a function
fim.config.gdalPath;
fim.config.theme;
fim.app.configure({ apiKey: 'ignored' }); // no-op + console warning — locked after boot
isLoopbackHost is an internal import (fimviz/src/package/config.js), not on the public
barrel, so it isn't reachable from this console hook — see it exercised directly in
test/*.test.mjs instead.
Events
fim.on('ready', () => console.log('(already fired before you could attach this, but wiring works)'));
fim.on('error', (e) => console.log('error event:', e.code, e.message));
// One event, two subscriptions — build any RasterLayer, then:
const evtLayer = await fim.addLayer('raster', { source: ds });
evtLayer.on('rendered', (p) => console.log('layer-local rendered', p));
fim.on(`${evtLayer.type}:rendered`, (p) => console.log('map-bus rendered', p.layer === evtLayer));
await evtLayer.render({ render: 'recreate' }); // fires both listeners above
evtLayer.remove();
Dataset (core, not the full ops catalogue — see §10)
ds.id; ds.name; ds.kind; ds.format; ds.crs; ds.bounds; ds.meta;
const rec = ds.toRecord(); // small — source + recipe, no decoded payload
const rehydrated = Dataset.fromRecord(rec);
rehydrated.toJSON(); // {id,name,kind,format,crs,bounds,meta,axes}
Dataset.formats(); // every format decodable right now
// fromGrid: the way back INTO the op chain from an already-decoded grid
const wrapped = Dataset.fromGrid(grid, { name: 'from-grid' });
wrapped.isMaterialized; // true — nothing to decode
(await wrapped.clip(ds.bounds).grid()).width;
// features() is iterable — no .features.features
const vf = await dsVector.features();
vf.count; vf.toArray().length; [...vf][0]?.type;
ds.download() triggers a real file save — run it manually if you want to see the browser's save
dialog; skipped here since it can't be observed from a returned value.
reproject (the standalone function)
// reproject() returns the SAME Dataset (no copy) when the request is a same-CRS no-op, and a NEW
// one only when a warp actually ran:
const warped = await warp(ds, 'EPSG:4326');
console.log(warped.crs, warped === ds);
Storage (headless — see the full guide at §6 for everything else)
const db = new Storage({ name: 'test-md-store', version: 1, tables: ['userFiles'] });
const userFiles = db.table('userFiles');
await userFiles.put('x.tif', ds.toRecord());
Dataset.fromRecord(await userFiles.get('x.tif')).toJSON();
await db.destroy(); // clean up — don't leave a stray IndexedDB database behind
ColorScale / Palettes (see the full guide at §7)
const cs = new ColorScale({ palette: 'viridis', min: 0, max: 10, continuous: false, unit: 'm' });
cs.getStops();
ColorScale.palettes(); // built-in + registered
ColorScale.palettes().includes('viridis'); // true
ColorScale.palettes().includes('not-a-palette'); // false
ColorScale.registerPalette('test-md-ramp', ['#000000', '#ffffff']);
ColorScale.palettes().includes('test-md-ramp'); // true
cs.set({ missingColor: '#cccccc' }); // what an absent value (null/NaN/'') is painted
cs.missingColor; // survives a mode switch — it is orthogonal to the 3 modes
cs.set({ missingColor: null }); // back to "the consumer decides"
Legend / Stats / Filters (full guides at §8 and here)
const legend = Legend.fromColorScale(cs);
legend.toJSON();
const stats = Stats.raster(grid.pixels, gridMeta(grid));
stats.describe();
stats.toJSON();
new SpatialFilter([{ lat: grid.bounds.south, lng: grid.bounds.west },
{ lat: grid.bounds.north, lng: grid.bounds.west },
{ lat: grid.bounds.north, lng: grid.bounds.east }]).isEmpty(); // false (3 points)
new PredicateFilter((v) => v > 0).test({ value: 5 }); // true
Filter.from((v) => v > 0).test({ value: -1 }); // false
Filter.all([new PredicateFilter((v) => v > 0), new PredicateFilter((v) => v < 100)]).test({ value: 5 });
Layer (base — see §4 for the full pipeline)
const vlayer = await fim.addLayer('vector', { source: dsVector,
style: { fillColor: '#58a6ff', fillOpacity: 0.3, strokeColor: '#1f6feb', strokeWidth: 2 } });
vlayer.id; vlayer.type; vlayer.visible;
vlayer.fit();
vlayer.remove();
// Chainable ops — the Dataset ops applied to the layer's sources IMMEDIATELY, drawn once at render()
const rlayer = await fim.addLayer('raster', { source: ds });
rlayer.clip({ north: 90, south: -90, east: 180, west: -180 });
rlayer.dirty; // true — applied but not yet drawn
await rlayer.render();
rlayer.dirty; // false
await rlayer.reset(); // back to the pristine source
rlayer.remove();
### From file to rendered layer
```js
// registerNamedLayer / getLayerByName / namedLayers (the file-viewer registry) —
const l = await fim.addLayer(ds); // type inferred from ds.kind ('raster')
fim.registerNamedLayer('4326.tif', l);
fim.getLayerByName('4326.tif') === l; // true
fim.namedLayers.includes(l); // true
fim.getLayerByName('4326.tif')?.remove(); // teardown + drops from both registries
Vendored primitives
const tiff = await fromArrayBuffer(await rasterFile.arrayBuffer());
(await tiff.getImage()).getWidth();
// parseFile's own kmz support (unzip + kml() internally) — a real sample file round-trip:
const kmzFile = await urlToFile(KMZ_URL, 'ames.kmz', 'application/vnd.google-earth.kmz');
const dsKmz = await fim.addDataset(kmzFile);
dsKmz.kind, dsKmz.format; // 'vector', 'kmz'
// kml() itself takes an XML Document, not raw .kmz bytes — exercise the standalone primitive
// directly on a real .kml doc instead:
const kmlDoc = new DOMParser().parseFromString(
'<kml><Document><Placemark><name>p</name><Point><coordinates>-90.07,29.95</coordinates></Point></Placemark></Document></kml>',
'text/xml');
kml(kmlDoc); // → GeoJSON FeatureCollection
const shpBuf = await (await fetch('/assets/SampleFiles/FIMS_Sample_Map_Layers.zip')).arrayBuffer();
await shp(shpBuf).catch((e) => console.log('shp() needs a real shapefile zip inside:', e.message));
typeof Loader; // 'function' — the @googlemaps/js-api-loader class, re-exported
GDAL (see callGdal fully covered in §10)
fim.config.gdalPath; // where gdal3.js loads its wasm/data from — the version-pinned CDN by default
2. APP_STARTUP.md
Mounting + options
The dev site already mounted fim once (the default app allows exactly one). Exercise a second,
independent map instead of touching fim:
const div = document.createElement('div');
div.id = 'test-md-leaflet'; div.style.cssText = 'position:fixed;bottom:8px;right:8px;width:300px;height:200px;z-index:9999;border:2px solid #58a6ff';
document.body.appendChild(div);
const fim2 = await mount(div, { provider: 'leaflet', isolated: true, center: { lat: 30, lng: -90 }, zoom: 6 });
fim2.map; // a real L.Map
fim2.config.provider; // 'leaflet'
Two boot paths
fim2 above took the no-runtime bare-map path (nothing registered a runtime for a second,
isolated app) — confirm it has no widget chrome:
fim2.layerPanel; // null — no createPanel factory for this isolated app
fim2.root.querySelector('#layer-panel'); // null — no widget markup was injected either
Multiple maps on one page
fim.app.hasMaps; // true (the default app)
fim.app === fim2.app; // false — fim2 is isolated, its own FimVizInstance
Listening before resolution
const target = document.createElement('div');
target.style.cssText = 'position:fixed;bottom:8px;left:8px;width:300px;height:200px;z-index:9999;border:2px solid #3fb950';
document.body.appendChild(target);
let sawReady = false;
mount(target, { provider: 'leaflet', isolated: true })
.on('ready', () => { sawReady = true; console.log('ready fired'); })
.on('error', (e) => console.log('error', e.code));
Namespace-level parse (no map needed)
const dsHeadless = await FimViz.parseFile(rasterFile); // no FimMap, no fim.datasets registration
fim.datasets.includes(dsHeadless); // false — confirms no side effect
Teardown
fim2.destroy();
div.remove();
Uploading files (drag-and-drop)
The doc's snippet is literally the app's own io/fileUploadSetup.js wiring — already live on the
page. Simulate the same addDataset call the drop handler makes, without a real drag gesture:
const dropSimFile = await urlToFile(DEPTH_URL, 'Brazos_RP100_depth.tif', 'image/tiff');
const f1 = await fim.addDataset(dropSimFile); // async — same call fileUploadSetup.js awaits on 'drop'
f1.kind, f1.crs;
Getting it on the map
const l1 = await fim.addLayer(f1); // infers 'raster' from f1.kind — no type arg needed
l1.type; // 'raster'
l1.remove();
The CRS check
f1.crs; // whatever Brazos_RP100_depth.tif declares
if (f1.crs && !FimViz.providerAcceptsCRS(fim.config.provider, f1.crs)) {
const l2 = await fim.addLayer(f1.reproject('EPSG:4326')); // lazy op; warp forced inside render()
l2.remove();
} else {
console.log('already renderable as-is:', f1.crs);
}
3. APP_STARTUP_ADVANCED.md
Map provider seam
FimViz.mapProviders(); // → ['google', 'leaflet'] on a stock build
FimViz.providerAcceptsCRS('google', 'EPSG:4326'); // true
FimViz.providerAcceptsCRS('google', 'EPSG:26915');// false — needs reprojecting first
FimViz.providerAcceptsCRS('leaflet', null); // true — unknown CRS treated permissively
providerRequiresApiKey(name) is not re-exported from the barrel — only mount.js uses it
internally, and mount() already throws config-invalid naming the missing key, so nothing was left
for a caller to do with it. Exercise the same behaviour black-box instead, via the error it produces:
try { await mount(document.createElement('div'), { provider: 'google', isolated: true, apiKey: '' }); }
catch (e) { console.log(e.code); } // 'config-invalid' — proves google requires a key
await mount(document.createElement('div'), { provider: 'leaflet', isolated: true }); // no apiKey, succeeds
Register a trivial third provider to exercise the whole MapProviderImpl contract yourself:
FimViz.registerMapProvider('test-md-noop', {
requiresApiKey: false,
acceptsCRS: (crs) => crs == null || crs === 'EPSG:4326',
async create(el) { const d = document.createElement('div'); d.textContent = 'fake map'; el.appendChild(d); return d; },
addVector(map, geojson, { style } = {}) { return { geojson, style }; },
removeVector() {},
fitBounds() {},
addRasterImage(map, dataUrl, bounds, opts) { return { dataUrl, bounds, opts }; },
removeRasterImage() {},
setRasterImageOpacity() {},
setRasterImageUrl(map, handle, dataUrl, bounds, opts) { return { dataUrl, bounds, opts }; },
onMapMouseMove() { return () => {}; },
onMapEvent() { return () => {}; },
});
FimViz.mapProviders().includes('test-md-noop'); // true
const fim3 = await mount(document.createElement('div'), { provider: 'test-md-noop', isolated: true });
fim3.map; // the fake <div>"fake map"</div>
fim3.destroy();
Built-in provider options
const fimLeaf = await mount(document.createElement('div'),
{ provider: 'leaflet', isolated: true, center: { lat: 41.66, lng: -91.53 }, zoom: 10, tileUrl: null });
fimLeaf.map; // an L.Map with NO default OSM tile layer (tileUrl: null)
fimLeaf.destroy();
Neutral vector style vocabulary
// styleToGoogle/styleToLeaflet/featuresOf/resolveFeatureStyle are internal to mapProvider.js — a
// VectorLayer applies them for you. Exercise the vocabulary through the layer instead:
const vl = await fim.addLayer('vector', { source: dsVector, style: '#ff8800' }); // bare color string shorthand
vl._style;
vl.setStyle({ strokeWidth: 4 });
// Colour features BY A PROPERTY through the same ColorScale a raster uses:
vl.set({ colorScale: new ColorScale({ palette: 'viridis', min: 0, max: 10, continuous: true }),
colorBy: 'SOME_NUMERIC_PROPERTY' });
vl.getLegend(); // a Legend, exactly as on a raster
(await vl.getStats()).byClass; // bucketed by that same scale
vl.set({ missingColor: '#cccccc' }); // features with no usable value, painted explicitly
vl.remove();
const vlFn = await fim.addLayer('vector', { source: dsVector,
style: ({ feature, index }) => (index % 2 === 0 ? '#58a6ff' : null) }); // per-feature function style
vlFn.remove();
Materialize / decode extension seam
Dataset.registerMaterializer('test-md-format', async (root, dsForRoot) => {
// root: {kind:'inline', data} | {kind:'url', url} — return a RasterGrid or VectorFeatures
return new RasterGrid({ pixels: new Float32Array([1, 2, 3, 4]), width: 2, height: 2,
bounds: { north: 1, south: 0, east: 1, west: 0 }, crs: 'EPSG:4326' });
});
const dsCustom = new Dataset({ kind: 'raster', format: 'test-md-format', data: {} });
const customGrid = await dsCustom.grid();
customGrid.width, customGrid.pixels;
Dataset.registerReprojector(async (grid, targetCrs) => { console.log('custom reprojector called for', targetCrs); return grid; });
// Careful: this REPLACES the GDAL reprojector for the whole page — only run this in a throwaway tab.
FimMap — the fuller instance API
fim.enableMapEvents(); // default ['click','hover']
fim.simultaneousLayerEvents = true;
fim.simultaneousLayerEvents; // true
fim.disableMapEvents();
const release = fim.captureInteraction((evt) => console.log('captured', evt.type));
fim.capturing; // true
release();
fim.capturing; // false
fim.registerAction('testMdAction', function (...args) { console.log('action fired on', this, args); });
fim.actionNames.includes('testMdAction'); // true
fim.getAction('testMdAction');
fim.registerActions({ testMdAction2: () => console.log('two') });
fim.bindActions(); // idempotent — safe to call again
4. LAYERS.md
Construction / properties
const rawLayer = new Layer({ map: fim, type: 'test-md-raw', sources: [ds] });
rawLayer.id; rawLayer.type; rawLayer.sources; rawLayer.map === fim; rawLayer.dataset === ds;
rawLayer.visible; rawLayer.exclusive; rawLayer.result; // result is null until compute()
Events
rawLayer.on('rendered', (p) => console.log('rendered', p));
fim.on('test-md-raw:rendered', (p) => console.log('forwarded', p.layer === rawLayer));
await rawLayer.render(); // base _draw() is a no-op → console.warn, but still fires 'rendered'
rawLayer.once('removed', () => console.log('removed once'));
Lifecycle
rawLayer.show(); rawLayer.visible; // true — base show()/hide() only flip the flag
rawLayer.hide(); rawLayer.visible; // false
rawLayer.fit(); // base no-op
rawLayer.remove(); // teardown + synchronous 'removed' + unregister
Render pipeline (compute/render, the CRS precondition, _draw warning)
const rl = new RasterLayer({ map: fim, sources: [ds] });
await rl.compute(); // forces ds.grid(), memoizes onto rl.result
rl.result != null; // true
await rl.render(); // compute is skipped this time (result already set) → CRS check → _draw
rl.overlay != null; // true — RasterLayer DOES override _draw, unlike the bare Layer above
// CRS precondition — force a Dataset with an unrenderable fake CRS:
const dsBadCrs = new Dataset({ kind: 'raster', format: 'test-md-format', data: {}, crs: 'EPSG:9999' });
try { await new RasterLayer({ map: fim, sources: [dsBadCrs] }).render(); }
catch (e) { console.log(e.message); } // "...cannot render CRS 'EPSG:9999'... Reproject first..."
Swapping sources
await rl.setSources([ds]); // cold swap — same Dataset here, just exercises the call
await rl.deriveSources((cur) => cur[0].clip(ds.bounds)); // hot-modify — reuses memoized ancestors
Settings
rl.settings.get(); // whole state object (copy)
rl.settings.get('opacity');
await rl.set({ opacity: 0.5 }); // sugar for rl.settings.set({opacity:0.5})
rl.get(); // sugar for rl.settings.get()
await rl.settings.reset();
Read-models / hit-testing
rl.getLegend(); // Legend, once a ColorScale has been resolved (after render())
await rl.getStats();
rl.hitTest(grid.bounds.north - 0.001, grid.bounds.west + 0.001); // near a corner — pixel-level test
The type registry
Layer.registerType('test-md-type', (fimArg, opts) => new RasterLayer({ ...opts, map: fimArg, type: 'test-md-type' }));
const tLayer = await fim.addLayer('test-md-type', { source: ds });
tLayer.type; // 'test-md-type'
tLayer.remove();
Layer.types(); // every type addLayer can construct, incl. the one just registered
// hasLayerType/createLayer/dispatchMapEventToLayers are internal — Layer.types() is the public read.
// An unresolvable type demonstrates the registry indirectly too (it lists what IS registered):
try { await fim.addLayer('not-a-real-type', {}); } catch (e) { console.log(e.message); }
FimMap-side layer registry
const named = await fim.addLayer('raster', { source: ds });
fim.registerNamedLayer('test-md-named.tif', named);
fim.getLayer(named.id) === named; // true
fim.getLayerByName('test-md-named.tif') === named; // true
fim.namedLayers.includes(named); // true
fim.removeLayer(named.id); // → named.remove()
Map-event dispatch
dispatchMapEventToLayers is an internal pure function (fimMap.js calls it, not barrel-exported) —
exercise it through the real map instead:
fim.enableMapEvents(['click', 'hover'], { simultaneous: false });
// now click/hover the actual map in the browser and watch layers with hitTest()==true react.
fim.disableMapEvents();
5. LAYER_SUBTYPES.md
RasterLayer
const rl2 = new RasterLayer({ map: fim, sources: [ds], noData: -99999, opacity: 1 });
await rl2.render();
rl2.overlay; rl2.rasterData; rl2.meta; rl2.noData; rl2.opacity; rl2.colorScale;
rl2.setNoData(-9999);
rl2.setOpacity(0.6);
rl2.hide(); // overlay opacity -> 0 via the provider, NOT torn down
rl2.show(); // restored to .opacity
rl2.set({ colorScale: new ColorScale({ palette: 'plasma', min: 0, max: 10, continuous: true }) });
rl2.getLegend();
await rl2.getStats();
rl2.fit();
rl2.hitTest(grid.bounds.north - 0.001, grid.bounds.west + 0.001);
rl2.valueAt(grid.bounds.north - 0.001, grid.bounds.west + 0.001);
await rl2.settings.set({ palette: 'viridis', continuous: false, noData: -9999, opacity: 0.8, hover: true });
rl2.remove();
VectorLayer
const vl2 = new VectorLayer({ map: fim, sources: [dsVector] });
vl2.render({ style: { fillColor: '#ff0000' } }); // SYNCHRONOUS — no await needed, but still awaitable
vl2.dataLayer; vl2._style;
vl2.setStyle({ strokeWidth: 3 });
vl2.hide(); // removes dataLayer via the provider (no vector "invisible" primitive)
vl2.show(); // re-adds at the last style
vl2.fit();
vl2.hitTest(dsVector.bounds.north - 0.001, dsVector.bounds.west + 0.001);
vl2.featureAt(dsVector.bounds.north - 0.001, dsVector.bounds.west + 0.001);
vl2.remove();
CSV / XYZ → VectorLayer:
const csvText = 'name,lat,lng\nA,29.95,-90.07\nB,29.96,-90.08\n';
const csvFile = new File([csvText], 'points.csv', { type: 'text/csv' });
csvHeaders(csvText); // → ['name','lat','lng'] — read BEFORE parseFile
const dsCsv = await fim.addDataset(csvFile); // auto-detects lat/lng by common column names
dsCsv.kind; // 'vector'
const xyzText = '-90.07 29.95 1.2\n-90.08 29.96 1.4\n';
const xyzFile = new File([xyzText], 'points.xyz', { type: 'text/plain' });
const dsXyz = await fim.addDataset(xyzFile, { swapXY: false });
dsXyz.kind;
wktToGeometry('POINT(-90.07 29.95)'); // standalone — what the csv geometryField path uses
ComparisonLayer
const cmpFileA = await urlToFile(CMP_A_URL, 'a.tif', 'image/tiff');
const cmpFileB = await urlToFile(CMP_B_URL, 'b.tif', 'image/tiff');
const dsA = await fim.addDataset(cmpFileA);
const dsB = await fim.addDataset(cmpFileB);
const gridA = await dsA.grid(), gridB = await dsB.grid();
const cmp = new ComparisonLayer({ map: fim,
sources: [{ pixels: gridA.pixels, meta: gridMeta(gridA) }, { pixels: gridB.pixels, meta: gridMeta(gridB) }] });
const cmpResult = cmp.compute({ policy: 'high', method: 'nearest' });
cmpResult.grid, cmpResult.rgba, cmpResult.counts, cmpResult.nLayers, cmpResult.metrics, cmpResult.warnings;
cmp.getAligned();
cmp.metricsForMask(null); // whole-extent metrics again (no mask)
// registered-type construction:
const cmpLayer = await fim.addLayer('comparison',
{ sources: [{ pixels: gridA.pixels, meta: gridMeta(gridA) }, { pixels: gridB.pixels, meta: gridMeta(gridB) }] });
cmpLayer.remove();
await layer.prepare() only matters when sources are RasterLayer[]/Dataset-backed rather than
already-decoded {pixels,meta} pairs — since the snippet above already hands compute() decoded
pixels directly, prepare() is a no-op here; it's the step that forces Dataset sources first when
you construct from RasterLayer[] instead.
EnsembleAggregationLayer
Reusing the same two aligned rasters purely to exercise the shape (this method wants N member rasters, not specifically an ensemble product):
const ens = new EnsembleAggregationLayer({ map: fim,
sources: [{ pixels: gridA.pixels, meta: gridMeta(gridA) }, { pixels: gridB.pixels, meta: gridMeta(gridB) }] });
const ensResult = ens.compute({ policy: 'high', method: 'nearest' });
ensResult.grid, ensResult.rgba, ensResult.perPixel, ensResult.histogram, ensResult.nLayers, ensResult.warnings;
ens.getAligned();
App-tier renderFile subclasses
EnsembleLayer/DepthLayer/DamageLayer aren't barrel-exported (they're app-registered layer
TYPES, not classes on the engine barrel) — exercise them through their registered type strings
instead, which is how the app itself uses them:
fim.addLayer; // 'ensemble' / 'depth' / 'damage' types are registered by a host, not the engine —
// see layers/ensemble.js, layers/depthMap.js for their own FIM-specific option
// shapes; not documented in the generic usage docs.
6. STORAGE.md
Getting an instance
const sdb = new Storage({ name: 'test-md-storage', version: 1, tables: ['userFiles', 'scratch'] });
sdb.name; // 'test-md-storage'
Rows (via table(name))
const uf = sdb.table('userFiles');
uf.name; // 'userFiles'
await uf.put('a.tif', { hello: 'world' }); // → 'a.tif'
await uf.get('a.tif'); // → {hello:'world'}
await uf.get('missing-key'); // → undefined
await uf.has('a.tif'); // true
await uf.has('missing-key'); // false
await uf.list(); // [{key:'a.tif', value:{...}}]
await uf.list({ keys: true }); // ['a.tif']
await uf.delete('a.tif'); // true
await uf.clear(); // true (no-op-safe on an empty table)
// interchangeable with the db-level calls:
await sdb.put('userFiles', 'b.tif', 42);
await uf.get('b.tif'); // 42 — same row, either form
Tables (structural)
await sdb.tables(); // ['userFiles', 'scratch']
await sdb.createTable('userFiles'); // false — already exists
await sdb.createTable('extra'); // true — created, version bumped
await sdb.dropTable('extra'); // true
await sdb.dropTable('nope'); // false — never existed
Version-management primitives
await uf.put('x', 1); await uf.put('y', 2); await uf.put('z', 3);
const changed = await uf.map((key, value) => (key === 'y' ? Storage.DELETE : value * 10));
changed; // 3 — x and z updated, y deleted (1 delete + 2 updates)
await uf.list();
await sdb.clearAll(); // wipes every table, keeps schema + version
await sdb.tables(); // still ['userFiles', 'scratch']
versionMigrate (constructor option) only runs during an actual version-bumping open — demonstrate
it by re-opening at a higher version:
const sdb2 = new Storage({ name: 'test-md-storage', version: 2, tables: ['userFiles', 'scratch', 'migrated'],
versionMigrate: ({ db, transaction, oldVersion, newVersion }) => {
console.log('migrating', oldVersion, '->', newVersion, [...db.objectStoreNames]);
} });
await sdb2.tables(); // ['userFiles','scratch','migrated'] — versionMigrate ran, plus the new table
Connection lifecycle
await sdb2.close(); // drop the connection; data persists
await sdb2.tables(); // reopens transparently
await sdb.destroy(); // delete test-md-storage entirely
await sdb2.destroy(); // (same underlying DB name — second destroy is a clean no-op-ish delete)
7. COLOR_SCALE.md
Using rl2-style layer isn't required — ColorScale is fully standalone:
const csScale = new ColorScale({ palette: 'blues', min: 0, max: 10, continuous: true, unit: 'm' });
1. Palette mode
csScale.set({ palette: 'viridis' });
csScale.set({ palette: ['#000033', '#3366ff', '#ffffff'] });
csScale.set({ continuous: true });
csScale.set({ continuous: false });
csScale.set({ min: 0, max: 5 });
csScale.set({ palette: 'plasma', continuous: true, min: 0, max: 100 }); // one batch, one onChange
2. Classed / discrete stops
csScale.setStops([
{ range: [0, 2], color: '#08306b', label: 'low' },
{ range: [2, 5], color: '#4292c6', label: 'mid' },
{ range: [5, Infinity], color: '#deebf7', label: 'high' },
]);
csScale.setRange(0, { min: 0, max: 3 });
csScale.setColor(0, '#ff0000');
csScale.setLabel(0, 'Shallow');
csScale.getStops(); csScale.getValues(); csScale.getRange(0); csScale.getColor(1); csScale.getRgb(1);
csScale.kind; csScale.isExplicit; csScale.discrete; csScale.source;
ColorScale.fromGdalLegend(
[{ value: 0, color: '#ffffff', label: 'none' }, { value: 1, color: '#0000ff', label: 'flooded' }], 'm');
3. Continuous color stops
const csCont = new ColorScale({ palette: 'blues', min: -1, max: 1 });
csCont.setColorStops([-1, 0, 1], ['#015498', '#ffffff', '#21bf90']);
csCont.set({ continuous: true });
csCont.getRgb(-0.5); csCont.getRgb(-5); csCont.getRgb(5);
csCont.set({ continuous: false });
csCont.getRgb(-0.9);
The override callback
csCont.colorFor = (v) => (v < 0 ? '#000000' : null);
csCont.getColor(-1); // '#000000' — override wins
csCont.getColor(1); // falls through to the normal scale (colorFor returned null)
csCont.colorFor = null;
Reacting to changes
const onEdit = (c) => console.log('scale changed, palette now:', c.palette);
csScale.onChange(onEdit);
csScale.set({ palette: 'reds' }); // triggers onEdit
csScale.offChange(onEdit);
csScale.set({ palette: 'heat' }); // silent now
Attaching to a layer / precedence
const rl3 = new RasterLayer({ map: fim, sources: [ds] });
await rl3.render(); // no explicit scale → auto-resolved (GDAL legend, else default "blues")
rl3.colorScale.source; // 'gdal' | 'default'
rl3.set({ colorScale: new ColorScale({ palette: 'plasma', min: 0, max: 10, continuous: true }) });
rl3.colorScale.source; // now the explicit one you attached
try { rl3.set({ colorScale: 'not-a-colorscale' }); } catch (e) { console.log(e.message); } // validated, throws cleanly
await rl3.settings.set({ palette: 'viridis', continuous: false, opacity: 0.7 });
rl3.remove();
8. LEGEND.md
const legendA = new Legend({ unit: 'm', kind: 'classed', source: 'custom',
stops: [{ value: 1, color: '#000', label: 'one' }] });
legendA.unit; legendA.kind; legendA.source; legendA.stops;
legendA.formatValue(3.14159); // '3.14'
legendA.formatLabel = (s) => `${s.label}!`;
legendA.toHtml();
const legendB = Legend.fromColorScale(csScale); // derives unit/kind/stops from the ColorScale above
legendB.toHtml(); // classed → one row per stop
const legendC = Legend.fromColorScale(csCont);
legendC.toHtml(); // continuous → a gradient bar
const rl4 = new RasterLayer({ map: fim, sources: [ds] });
await rl4.render();
rl4.getLegend().toJSON(); // non-null — _draw() always attaches SOME ColorScale first
rl4.remove();
9. UI.md
Toast
const toast = createToast(document.body, { corner: 'br' });
toast.show('Hello from test.md', { level: 'success', timeout: 3000 });
// toast.clear(); toast.destroy();
connectToast(fim, toast); // wires fim's 'notify' host event to this toast
fim.emit('notify', { message: 'wired via connectToast', level: 'info' });
Tooltip (raster hover)
fim.enableMapEvents();
const rl5 = new RasterLayer({ map: fim, sources: [ds] });
await rl5.render();
const hoverBinding = bindHoverValue(rl5, { format: (v) => v.toFixed(2) });
// hover the raster's footprint on the real map to see the tooltip; then:
hoverBinding.off();
rl5.remove();
Info window (vector click)
const vl3 = new VectorLayer({ map: fim, sources: [dsVector] });
vl3.render();
const infoBinding = bindFeatureInfo(vl3, { render: (f) => propsTable(f.properties) });
// click a feature on the real map to see the info window; then:
infoBinding.off();
vl3.remove();
Tools panel
const rl6 = new RasterLayer({ map: fim, sources: [ds] });
await rl6.render();
rasterControls(rl6); // pure control spec — no DOM
const panelHost = document.createElement('div');
panelHost.style.cssText = 'position:fixed;top:8px;right:8px;z-index:9999;background:#111';
document.body.appendChild(panelHost);
const toolsPanel = createToolsPanel(panelHost, { layer: rl6, pretty: true });
toolsPanel.update();
// toolsPanel.destroy(); panelHost.remove(); rl6.remove();
const vl4 = new VectorLayer({ map: fim, sources: [dsVector] });
vl4.render();
vectorControls(vl4);
Legend/Stats renderers
renderLegend(rl6.getLegend()); // → legend.toJSON()
renderLegend(rl6.getLegend(), { html: true }); // → legend.toHtml()
const statsForRender = await rl6.getStats();
renderStats(statsForRender);
renderStats(statsForRender, { html: true });
Region draw
const draw = createRegionDraw(fim, {
onPoint: (points) => console.log('vertex added, total:', points.length),
onComplete: (filter, points) => console.log('done', filter?.isEmpty(), points.length),
onCancel: () => console.log('cancelled'),
});
draw.start(); // now click a few points on the real map, then:
draw.active; // true while drawing
// draw.finish(); // or draw.cancel();
Operations panel
const rl7 = new RasterLayer({ map: fim, sources: [ds] });
await rl7.render();
let lastRegion = null;
const draw2 = createRegionDraw(fim, { onComplete: (f) => { lastRegion = f; } });
const opsHost = document.createElement('div');
opsHost.style.cssText = 'position:fixed;top:8px;left:8px;z-index:9999;background:#111';
document.body.appendChild(opsHost);
const ops = createOperationsPanel(opsHost, { layer: rl7, region: () => lastRegion, pretty: true,
onApply: (layer, err) => console.log('applied', err?.message || 'ok') });
// click "Threshold" / "Mask to region" / "Reset" in the panel, or drive it manually:
// ops.destroy(); opsHost.remove(); rl7.remove();
10. DATASET_OPERATIONS.md
Construction
Dataset.fromURL(RASTER_URL, { format: 'geotiff' }); // lazy URL root — not fetched yet
const dsFromRecord = Dataset.fromRecord(ds.toRecord());
Kind-agnostic
ds.toRecord({ storeMaterialized: false }); // small — recipe only
await ds.load(); // force it once so storeMaterialized:true is legal below
ds.toRecord({ storeMaterialized: true }); // now embeds the decoded grid too
ds.toJSON();
// ds.download(); // real file-save dialog — run manually to observe
// select/selectAxisEntry/reduce need a Dataset with .axes (a selection-axis series) — a plain
// parsed file (like `ds`) has none, so these correctly return null/throw here; that IS the
// documented behavior for a non-axis Dataset:
ds.selectAxisEntry(0); // null — no axes on a plain parsed file
ds.select(0); // null
try { ds.reduce('mean'); } catch (e) { console.log(e.message); } // throws — no axis to reduce
Terminals
await ds.load(); // RasterGrid, memoized
await ds.grid(); // load() + assert raster
try { await dsVector.grid(); } catch (e) { console.log(e.message); } // throws — dsVector is vector
await dsVector.features(); // VectorFeatures
ds.isMaterialized; // true, already forced above
ds.warnings; // [] unless something (e.g. a warp) logged one
ds.release(); // evicts the memoized grid
ds.isMaterialized; // false again
Raster — unary
const dsReproj = ds.reproject(ds.crs || 'EPSG:4326'); // lazy — same-CRS is a same-ref no-op
const dsClip = ds.clip({ north: grid.bounds.north, south: (grid.bounds.north + grid.bounds.south) / 2,
east: grid.bounds.east, west: grid.bounds.west });
(await dsClip.grid()).height <= grid.height; // true — cropped
const ring = [{ lat: grid.bounds.south, lng: grid.bounds.west },
{ lat: grid.bounds.north, lng: grid.bounds.west },
{ lat: grid.bounds.north, lng: grid.bounds.east },
{ lat: grid.bounds.south, lng: grid.bounds.east }];
const dsMask = ds.mask(ring, { invert: false });
await dsMask.grid();
const dsReclass = ds.reclassify([{ min: 0, max: 100, value: 1 }], { unmatched: 'nodata' });
await dsReclass.grid();
const dsResample = ds.resampleTo({ width: 50, height: 50, bw: grid.bounds.west, bs: grid.bounds.south,
be: grid.bounds.east, bn: grid.bounds.north }, { method: 'nearest' });
(await dsResample.grid()).width; // 50
const dsSlope = ds.slope({ unit: 'degrees' }); // meaningful cellsizeX/Y matter for real terrain —
await dsSlope.grid(); // see COLOR_SCALE-adjacent slope notes; this just
// exercises the call
const dsAspect = ds.aspect();
await dsAspect.grid();
const dsHillshade = ds.hillshade({ altitude: 45, azimuth: 315 });
await dsHillshade.grid();
const zones = await ds.zonalStats([{ id: 'whole-extent', polygon: ring }]); // terminal — returns data
zones;
Raster — binary / N-ary
Two ALIGNED same-region rasters (not ds/DEPTH_URL, which cover unrelated areas — combine will
run either way, but a meaningless overlap makes the result all-noData, which defeats the point of
watching it work):
const dsCmpA = await fim.addDataset(await urlToFile(CMP_A_URL, 'a.tif', 'image/tiff'));
const dsCmpB = await fim.addDataset(await urlToFile(CMP_B_URL, 'b.tif', 'image/tiff'));
const dsCombined = dsCmpA.combine(dsCmpB, { op: 'difference' });
await dsCombined.grid();
const dsDiff = dsCmpA.difference(dsCmpB); // sugar for combine(..., {op:'difference'})
await dsDiff.grid();
const dsSum = dsCmpA.combine([dsCmpB], { op: 'sum' }); // N-ary form, one other Dataset here
await dsSum.grid();
Vector — the one kind-changing op
const dsRasterized = dsVector.rasterize({ width: 100, height: 100, burnValue: 1 });
dsRasterized.kind; // 'raster'
await dsRasterized.grid();
try { ds.rasterize({ width: 10, height: 10 }); } catch (e) { console.log(e.message); } // "vector-only op"
Standalone grid functions (no Dataset needed)
maskGrid(grid, ring, { invert: false });
clipGrid(grid, { north: grid.bounds.north, south: (grid.bounds.north + grid.bounds.south) / 2,
east: grid.bounds.east, west: grid.bounds.west });
reclassifyGrid(grid, [{ min: 0, max: 100, value: 1 }], { unmatched: 'nodata' });
const gridB = await dsCmpB.grid(); // dsCmpB from the "binary/N-ary" snippet just above
combineGrids([grid, gridB], { op: 'difference' });
zonalStats(grid, [{ id: 'z1', polygon: ring }]);
slopeGrid(grid, { unit: 'degrees' });
aspectGrid(grid);
hillshadeGrid(grid, { altitude: 45, azimuth: 315 });
rasterizeFeatures((await dsVector.features()).features, dsVector.bounds, { width: 50, height: 50 });
Resample/align:
GRID_POLICY; // { LOW, HIGH, AVERAGE }
RESAMPLE_METHODS; // the full gdalwarp -r vocabulary
resolveTargetGrid([gridMeta(grid), gridMeta(gridB)], GRID_POLICY.HIGH);
resampleGrid(grid.pixels, gridMeta(grid), gridMeta(gridB), { method: 'nearest' });
alignRasters([{ pixels: grid.pixels, meta: gridMeta(grid) }, { pixels: gridB.pixels, meta: gridMeta(gridB) }],
{ policy: 'high', method: 'nearest' });
Dataset.registerResampler((pixels, srcMeta, dstMeta, opts) => { console.log('custom resampler called', opts); return pixels; });
resampleGrid(grid.pixels, gridMeta(grid), { ...gridMeta(gridB), width: gridB.width + 1 }, { method: 'cubic' });
Dataset.registerResampler(null); // un-register when done poking at it
Colorize:
const rgba = colorizeGrid(grid, { colorScale: csScale, alpha: 255 });
rgba instanceof Uint8ClampedArray; // true
const dataUrl = gridToDataURL(grid, { colorScale: csScale });
dataUrl.startsWith('data:image/png;base64,'); // true
Standalone reproject()
const eagerlyWarped = await warp(ds, 'EPSG:4326'); // eager — warps NOW, distinct from ds.reproject()
eagerlyWarped === ds; // true if ds was already WGS84/equivalent; a new Dataset otherwise
Vendored primitives
Already exercised in §1 (fromArrayBuffer, shp) — kml/Loader too.
GDAL escape hatch (callGdal)
const gdalInFile = new File([await rasterFile.arrayBuffer()], 'in.tif', { type: 'image/tiff' });
const { datasets } = await callGdal('open', gdalInFile);
const outPath = await callGdal('gdal_translate', datasets[0],
['-of', 'GTiff', '-outsize', '50%', '50%'], 'out.tif');
await callGdal('close', datasets[0]);
const bytes = await callGdal('getFileBytes', outPath); // Uint8Array
bytes.byteLength > 0;
try { await callGdal('not_a_real_gdal_method'); }
catch (e) { console.log(e.message); } // lists every real method name available