Repository navigation
Fix COG tile layers with s3:// URLs being treated as mission-relative - #147
devin-ai-integration[bot] wants to merge 12 commits into
Conversation
…ility toggles (NASA-AMMOS#1094) * Fix attachment op lookup broken by the module key rename `withOp` still read `mods[id].plugin`, the pre-rename key, so it matched nothing: an attachment's per-feature ops were never found and clearing a clicked image overlay or model silently did nothing. Guard both sides of the generated boundary in a test, since a mismatch here fails as a no-op rather than an error. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make the plugin families authorable: docs, scaffolds, capability validation The code grew config/filter/time surfaces, classification capabilities and a whole attachment family; the authoring materials still described the render contract. Closes that gap, and moves the CLI out of /plugins so the directory is containers only. - plugin-cli/ at the root: cli.js, lib/, and the six scaffolds as real files under scaffolds/<type>/, token-substituted into paths and contents. They are valid JSON/JS as they sit, so the validator checks them (a broken scaffold fails CI, not someone's first plugin). registries.json is CLI config and moves with it; plugin-state.json stays in /plugins, where the server reads it. - create layerattachment, and a layerattachments README with the contract and a worked example — it was the one family with neither. - Validate classification capabilities against a schema: a wrong type or an out-of-enum value is an error, an unknown key warns, and an omission core acts on (map.stacking, host.order) warns. These previously failed silently as a mis-ordered or un-pickable layer. - Document the config/filter/time surfaces, drop the capture surface and the onHostToggle op (neither has a runtime implementation). - Destroy a layer's attachments when its host is removed for good. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Document the six plugin families and their scaffolds in the docs site Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Layertype file layout: one path segment per surface The leaf name repeated what the plugin directory already said (Vector/map/vector.js), and globe/config.js was not the config surface at all but a helper shared by the two engine modules, one directory away from the real config/vector.js. - Vector/map.js, config.js, filter.js, time.js; globe/cesium.js, globe/lithosphere.js. - globe/config.js → globe/layerConfig.js: it builds the engine-neutral globe layer descriptor, which is not the `config` surface. - Non-surface helpers go in lib/ (VectorTile/lib/SimplifiedVectorGrid.js). - Drop the export default from the layerConfig helpers; every importer uses the named exports. - Vector/map.js declares its ops as named functions like the other 29 modules instead of inline methods. Paths are convention only — the manifest key is what names a surface — so this is manifests, imports and regenerated registries. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Show the op phase form in the layertype scaffold The scaffold's bare `make` is shorthand for `{ main }`, which made Vector's phase object look like a different contract. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drop the undispatched `load` op; derive the CLI's plugin types from one map `load` was in LAYER_OPS, the validator, the README table and the scaffold, but core never dispatched it and no type implemented it: data acquisition (including the dynamic-extent refetch) is core's, in LayerCapturer. The op table also omitted `render` and `onToggle`, which core does dispatch. `create`'s valid types and the discovery directories were two hand-kept lists, so the help text still advertised five families after layerattachment landed. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Serialize the specs that regenerate the shared plugin registries src/pre/*.js and configure/public/*Configs.json are single copies in the repo, so a worker running the CLI could regenerate away a fixture another worker had just written and was asserting on (~1 in 3 runs locally; CI uses one worker). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Declare the lock helper's Atomics globals for ESLint Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix the authoring papercuts eight fresh plugin authors hit Eight sessions each wrote a plugin against this branch with no help beyond the docs. Every one of them tripped over the same three things: - `create layertype` printed "implement make in map/<name>.js", a path that stopped existing when the layout collapsed to one segment per surface. - A scaffolded plugin is untracked, since a container is normally its own repo. Said so where `create` is documented, rather than leaving it to git status. - `plugin-registries.json" is `plugin-cli/registries.json`. Also: a type that `extends` another was warned for capabilities its parent declares, and the two layer families' templates weren't linked from the README's Creating Plugins section that lists the other four. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Document extends where the docs already claimed it was The plugins README points at this file for extends; this file never mentioned it. The one child of the eight who was told extends existed found out how it works by reading LayerTypeRegistry and pluginValidation; the others would not have found it at all, and a new data source is exactly the case it exists for. Also states what an expanded sublayer must carry, since that was copied from Vector's STAC path by guess. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Correct the manifest-reference facts authors checked against reality Every one of these was found by an author who followed the doc and then found the code disagreed: icons are MDI, not Ionicons (the scaffold itself emits `mdi mdi-`); the toolbar priority bands are 1-4/101-102/1001-1002, not '~1001-1020'; `create` takes six types, not four; and the documented way to run a plugin's own tests fails in global setup without PLAYWRIGHT_TEST_UNIT_ONLY, which only appeared in a package.json script. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Enforce interaction applicability; document backend; runnable scaffold tests `applicableLayerTypes` was descriptive: a mission config, a kind preset or a type's `defaultInteractions` could name an interaction the interaction says it doesn't handle. updateInteractions now emits APPLICABLE_LAYER_TYPES and the runner filters the whole pipeline — preamble and postamble included — against the layer's type chain, so a type that `extends` vector keeps vector's interactions. The scaffolded tests were CommonJS assertions about plugin.json sitting beside ESM modules, and no npm script ran them. They are now ESM, import the module where the module can load outside the bundler (attachment, interaction, backend), check that every declared module path resolves — the usual cause of a plugin that validates but never appears — and run under `test:plugins:unit`. Backend was the one family with no contract document: adds plugins/core/backend/README.md (the `s` object, lifecycle ordering, ensureAdmin's whitelist, envs, models) and gives the scaffold a deliberate auth gate. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Validate the manifest parts that fail quietly The Configure form a manifest declares was validated only as "is an object": Maker skips a component type it cannot render and writes wherever `field` points, so a typo'd type or an attachment field outside its own `configPath` yields a form that looks right and configures nothing. validateMetaconfig now checks component types against what Maker renders, field presence, dropdown options, objectarray shape, grid widths, and that an attachment only writes inside its configPath. `validate` also now reports what it could not see before: an enabled plugin absent from src/pre (valid, but the app never loads it) and a registry import whose plugin is gone — both meaning activate has not been run. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add the source, legend, onConfigChange and configPath surfaces Four surfaces the eight authoring experiments asked for, each replacing something core was deciding on their behalf: - layertype `source.fetch(layerObj, ctx) -> GeoJSON`, dispatched from LayerCapturer for every acquisition. Core keeps the whole dynamic-extent policy (extent, debounce/settling, zoom gate, move threshold, request staleness, clear/update, reload subscribers) — extracted out of the two duplicated branches into capture/dynamicExtent.js so it is testable — so `fetch` is a pure "given this, give me GeoJSON" for data that doesn't come out of a url core can fetch (POST bodies, headers, pagination, an SDK). - layertype `legend.derive(layerObj)`, replacing the `['image','tile'].includes(type) && cogTransform || type === 'velocity' || (type === 'data' && shader === 'colorize')` list that both the LayersTool and the LegendTool carried. Tile/Image/Velocity/Data now declare the surface. - attachment `onConfigChange(ctx)`, with `mmgisAPI.setLayerAttachmentConfig()` as its dispatcher: core writes the new settings into the host's live config at the declared configPath, then lets the attachment retune in place rather than paying for the default host rebuild. - interaction `configPath`, so an interaction with per-layer settings is handed its own subtree as `ctx.config` instead of digging through layerData. Also: `raw` (the engine namespace) on both globe contexts, matching `mctx.raw`; `test:plugins:unit` no longer fails when no plugin ships a @unit spec. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix the doc and scaffold gaps a second authoring round found Eight plugins were written against this branch by separate agents; these are the things they had to guess at or work around. - The layertypes README still said data acquisition is not in the vocabulary and belongs in `make`, 75 lines above the `source` surface that replaced that advice. - `gctx.raw` was absent from the gctx table, which still sent globe authors to `gctx.renderer` for engine-specific work. Documented, including that the two engines' namespaces are symmetric in name only. - Globe operations are dispatched by layer *name* (only `make` gets a layer object), and `setVisibility`/`setOpacity` take the new value positionally. None of that was written down; two authors guessed. - Nothing said how a layer opts into dynamic extent, so `source.fetch` saw a null `ctx.view` with no explanation. - `legend.derive` is handed config, not data; say so, and where render state actually lives, instead of leaving authors to stash on the layer. - `onConfigChange` is passed the built attachment, which the ctx table omitted. - The layertype scaffold's own test asserted `modules.globe[key]` over `Object.keys(renderers.globe)`, so it failed for every globe layertype, and it assumed `modules` where a single-module `extends` plugin has `module`. - The scaffold and the attachment README both captured `window.L` at import time, which is undefined in the unit test the same scaffold ships. Read the global per call instead, and drop `F_` from the example: importing an MMGIS singleton pulls jQuery and makes a module un-importable in Node, which browser-globals now states rather than implying it protects against that. - `create` printed `npm run build` for interactions instead of `activate`, said nothing when a name collided with an existing plugin in another family (a `Curtain` layertype silently shadowed the `Curtain` tool), and left the author to discover that a non-core container is untracked here. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add a textarea component, document the metaconfig, isolate ctx.config A plugin whose main setting is written in another language — the Overpass QL query one of the authoring experiments built its layertype around — got a single-line text field, because every free-text component Maker renders is one. `textarea` is multiline and monospace, sized by an optional `rows`. The component vocabulary itself was only discoverable by declaring something invalid and reading the validator's error, so plugins/README.md now has the metaconfig section that error was standing in for: what a row is, what each component field does, and what all 23 component types render. Also: the interaction runner assigned `ctx.config` only for interactions that declare a `configPath`, so an interaction without one was handed whichever config the interaction before it in the pipeline had. It now gets null, which is also what a layer that was never configured yields — the previous test asserted the leak, so it changes with it. For the globe, `gctx.raw` on LithoSphere is the globe class and nothing else (the package exports one thing), so the README now says where the work actually happens: `renderer` for the live globe and `window.THREE` for geometry, which MMGIS vendors and puts on the window. An author looked for THREE on `raw`, didn't find it, and dropped LithoSphere support. `create layertype` scaffolds a standalone renderer, which is also now called out as the wrong starting point for a type that draws like one MMGIS already has: extend it instead. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give interactions a settings form on their own card An interaction could be handed `ctx.config` but had no way to declare what an admin should be able to put there, so its settings existed only as prose in a README. It now declares `config.rows` in the same metaconfig vocabulary an attachment uses, and Maker renders those rows on the interaction's card in a layer's Interactions tab, behind a gear — beside where the interaction was chosen and dragged, rather than in a tab of its own. Validation follows from where the form lives: rows need a `configPath` (the runner has nowhere else to read them back from), every `field` must sit inside it, and `tab`/`tabs` are rejected. A row's `default` remains a form default — nothing is written until an admin touches the field — so plugins still default their own values, which the scaffold and README now spell out. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep an interaction's settings when the layer modal trims a layer Closing a layer modal rebuilds the layer from the fields its layer type's tabs declare, which is why attachment config paths were already exempted. An interaction's settings come from its own manifest, so they were written, rendered, and then silently dropped on close. Interaction manifests now load into the store alongside the attachment ones, and their config paths are preserved by the same pass. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Cover an interaction's settings form end to end No core interaction has per-layer settings, so the fixture interaction is served in place of the generated registry — a static JSON file, so no plugin has to be installed to stand in for one. Fails without the layer modal preserving interaction-owned config paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix what a third-party author trips on: names, tabs, core's own tests Eight fresh authors built a plugin each against this branch's docs. The contracts held; the tooling around them did not. - `create FOVWedges` emitted `f_ovwedges` and `fOVWedges` — an acronym is one word now, and so is a word with a digit in it (`HTML5Parser`). The printed next-steps filenames derive from the same tokens rather than re-deriving them wrongly. - The layerattachment scaffold defaulted to a Configure tab of its own, the exact thing its README warns against; it now joins an existing tab, `validate` warns when a non-core attachment's `config.tab` matches no core tab, and the next steps say so. - `create`'s activate diff buried what you just made under every plugin there is on a first run; it summarises past 8 lines, `--verbose` to list them. Two of core's own unit assertions failed for a *valid* third-party attachment: an exact tab list, and a field walk that descended into `objectarray.object`, whose item fields are relative and so are not paths under `configPath`. Both keep their invariant (core's tabs present and in order, no duplicate tabs, every real field owned by the attachment that reads it) while allowing an installed plugin, with the third-party case asserted rather than assumed. `enableWhenField` is `{ field, value }`; given a bare path Configure greys the control out forever, so validation rejects it now. Docs, all from things an author had to read core source to learn: an interactions family README (the `ctx` table, phase/order, settings, and what can't be unit tested); a `Time` section (the TimeControl surface, and that a feature's timestamps live at the properties `time.startProp`/`endProp` name); what the inherited vector renderer does with the features a `source.fetch` returns, including `prop-<name>` styling; `objectarray`'s relative item fields, `default` vs `defaultChecked`, and that a cleared number arrives as `""`; what an attachment's returned object may carry and that `ctx.config` is never null in `make`; and importing `L_` by alias in a globe module. Also drops two overwritten no-op `TimeControl.subscribe`/`unsubscribe` stubs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix the cross-family seams a multi-plugin feature runs into A feature that needs a layer type *and* an attachment *and* an interaction was buildable but had three ways to fail quietly, all found by building eight of them: - `pluginDependencies` resolved against tools/backend/components/interactions only, so an interaction depending on the layer type it was written for was reported enabled, validated clean, and then left out of the generated registry. Dependency discovery now covers every family, and the warning says the plugin never loads instead of leaving that to be inferred. - `validate --json` was unparseable whenever the manifest validator logged, since it logs JSON lines to stdout. Captured and returned as warnings. - The module validator accepted `export default { … }` but not the named form the scaffolds and docs use, and matched examples inside comments. Also: `syncData` is handed the attachment's `config` and `layerObj`, as `make` already was, so a redraw need not stash them; stale-registry validation resolves the imported module rather than its directory, which is what a type switching to `extends` deletes; and `applicableLayerTypes`/`defaultInteractions` ids that no enabled plugin provides are reported. `create layertype --extends <typeId>` scaffolds what the README recommends and the best plugins are — a manifest with `extends` and one module whose keys are surfaces, starting at `source.fetch` — instead of a standalone renderer. The parent is checked as you type it. Local `install` no longer copies `.git`, and `--container` pins the container name, which plugin ids embed. The interaction scaffold's spec tested the handler, which cannot be imported in Node once it imports a singleton; the decisions now live in `logic.js` and that is what it tests. Both other scaffolds emit a named default export, so a plugin following them no longer starts with a lint warning. Docs: the `_legend` entry shape, one container holding a whole feature and how its plugins reach each other, attachment time (no op needed — `syncData`), `syncData`'s context, calling your own backend under a subpath, `kindAlias` being an array, `NODE_ENV` for eslint, and install/container identity. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Let a layer type declare the attachments it comes with A feature is often a layer type plus an attachment plus an interaction, and the type is the plugin that knows what the attachment should be. It could already declare capabilities.defaultInteractions but had no way to say the same about an attachment, so three separate plugin authors worked around it the same wrong way: the type's own config rows wrote a string-literal copy of the attachment's configPath, coupling two plugins through a path either could rename. capabilities.defaultAttachments maps attachmentId to that attachment's settings ({} for none), and core hands it to the attachment under the attachment's own configPath as if an admin had filled the form in. A layer's own settings sit on top field by field, so changing one thing doesn't lose the rest and enabled: false opts out. It resolves through configFor/isEnabledOn, which everything already went through, so no call site changes. validate warns for a declared attachmentId no enabled plugin provides, and for one whose applicableLayerTypes excludes the declaring type - a well-formed default that would silently never apply. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix what round five's authors hit: extends drops inherited operations, empty form fields beat a type's defaults extends merged parent and child one level per *surface*, so a type adding a config.normalize silently lost its parent's config.expand — the opposite of 'declare only what differs'. Merging is now per operation (and per engine under globe), in a pure typeInheritance module. A layer's attachment settings also overrode a type's defaultAttachments with '' — which is what Configure writes into a row nobody filled in — leaving the attachment with no property name at all. Empty (nullish or '') no longer overrides; false and 0 still do. Plus the smaller traps: 'create tool XTool' produced XToolTool; activate reported 'No changes' after a manifest edit that rewrote a registry; ToolController_.getTool returned a silent no-op stub for a tool that isn't loaded; the backend scaffold had no models/ despite its README documenting one; and the docs said nothing about interaction-to-tool calls, stopGuests being mount-wide and status-blind, failure responses arriving as HTTP 200, layer types resolving defaultIcon against MUI rather than MDI, or how L_.layers.attachments is keyed. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A layer type can configure the interactions it ships, not just name them capabilities.defaultInteractions took interaction ids only, so a type that shipped an interaction had no way to tell it the property names it had just fetched — the gap defaultAttachments closed for the other family, and four of round five's authors worked around it by duplicating those names into a third subtree or stashing them on the layer under a private key. An event may now be an object instead of a list: "click": { "wind:report": { "speedProp": "windSpeed" } } Key order is the pipeline order, as the array's is; the settings resolve into the interaction's own configPath on the way to ctx.config, with the layer's own settings over them field by field. An interaction is written once and cannot tell whether a type configured it or an admin did. The merge rules are now shared with attachments (declaredConfig), validate cross-checks both forms — including a declared interaction whose applicableLayerTypes excludes the declaring type — and VectorTile's click, which had been passing no layerTypeChain, now enforces applicability like every other click path. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A mission with a layer of a plugin-provided type can now be saved Round six's authors could author, validate, activate and register a new layer type and still not configure a layer with it: both config validators switched on the layer's type and called anything they did not recognise unknown. They ask the generated layer-type registry instead, and hold a plugin type only to the checks it declares for itself (a `source`-backed type may have no url) — while a type extending a built-in still gets its parent's field defaults, since it is drawn by the parent's renderer. Also from that round: a LithoSphere type that registers itself in gctx.layers now has its destroy/setVisibility/setOpacity dispatched rather than declared, validated and never called; a type that declares a map renderer and resolves none says so instead of loading and drawing nothing; and window.location exists in the unit-test browser globals, so a source can resolve a relative url under test. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A plugin can ask for its layer back, and a type can say when it has data Two surfaces round six asked for, both of which existed as core internals a plugin had to reach into. `ctx.refreshLayer()`, on every interaction and attachment: an interaction that writes a feature to its own backend had to call `L_.Map_.refreshLayer(layerObj, cb, skipOrderedBringToFront, stopLoops, resolvedUrl)` to see its own write. The re-acquisition goes through the same path every other trigger does, and a `source`-backed type now sees it as `ctx.trigger === 'refresh'` rather than as another `'make'`. `time.availability(layerObj, ctx)`: `capabilities.time.histogram` validated, reported true, and meant nothing — TimeUI found histogram sources by matching `stac-collection:` and `{t}` against the layer's url, so any other endpoint was silently excluded. The type answers now, with times and counts; core keeps the window, the binning and the drawing. Tile's two url schemes move into its time module, and declaring the capability without a `modules.time` to implement it in is a validation error. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A refreshed layer's checkbox no longer shows it turned off Turning the Reference Mission's geodataset time-series layer on and then moving the time bar left the Layers tool showing it off, while it was on and drawn. The refresh swap hides the old layer and shows the rebuilt one with `ignoreToggleStateChange`, so `L_.layers.on` deliberately never changes \u2014 and both halves then compute the state to broadcast from that unchanged value and notify subscribers 'off' twice. Subscribers are what track on-ness (the checkbox; a dynamic-extent layer's re-query), so a toggle that preserves state now tells them nothing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Plugin system: inherited ops, source cancellation/paging, dynamic dropdown options The four remaining findings from the round-six plugin authoring sessions. extends: a child's operation is handed the inherited implementation as one extra last argument, so overriding vector's `config.normalize` to add a field no longer silently drops the `kind`/`radius` it sets. Phases match by name, a bare function stays a bare function, and `inherited()` is a safe no-op when the parent has no such operation. source.fetch: `ctx.signal` (a layer's next acquisition aborts its last, and an AbortError is not logged as a failure), `ctx.emit(geojson)` for a paged source to draw what it has so far, and `ctx.resolveUrl` so a plugin with an endpoint in its own config doesn't hand-roll mission/ROOT_PATH resolution. Core keeps the extent, staleness, move threshold and rendering; an emit after the request is stale is dropped. Maker: a dropdown may name an `optionsFrom` provider instead of listing options — `layerProperties` (the layer's own feature property names, from the geodataset schema or by sampling the file), `layers`, `layerTypes` — so "pick a property of this layer's data" stops being an unchecked text field. Validated against the provider names Configure actually has. Docs: a components family README (when init runs and what exists by then, the z-index bands, the subscriptions, and why there is no core interaction → component channel), and why composing across *layers* is unsupported — with the supportable shape being one layer whose source.fetch acquires both inputs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Plugin system: a third-party layer type is selectable, and duplicate ids fail validate The layer modal's Layer Type control is a row in the *selected* type's own manifest, and every core manifest listed the eleven built-in ids literally — so a plugin type could be authored, validated, activated and registered, and then never be chosen by an admin. The row asks the registry instead (`optionsFrom: "layerTypes"`), which is also the only list that stays correct. Two owners of one `typeId`/`attachmentId` are refused by registry generation but were reported by `validate` as clean, and a failed generation leaves the previous registries in place — a green check followed by a build failure, or an app silently running the last generation. `validate` now reports them, as it already did for `interactionId`. Docs: the inherited-op example counted `inherited` from the wrong position (`config.normalize` is dispatched with one argument, not two), and the per-family `config` shape (a layer type declares `tabs`, everyone else `rows`) was stated for interactions only. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Plugin system: acquire another layer's data, reference a layer's fields from a type's declarations, and drop only the offender on activate - `ctx.acquire(layerName)` on source/interaction/attachment contexts: a configured layer's data, acquired through its own layer type, headlessly — not turned on, not drawn, no rendered state exposed, and the layer's own live acquisition untouched. - A type's `defaultAttachments`/`defaultInteractions` values may be `"$variables.…"` references read off the host layer, so the property an admin picks on the type's form isn't typed again on the other plugins'. An unanswerable path drops its key, leaving the plugin its own default; `$$` escapes a literal `$`. - `activate` leaves a duplicate-id or bad-`extends` plugin out of the registry and regenerates everything else, rather than aborting and silently leaving the previous generation of every registry in place. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Do not collect plugin-cli scaffold templates as tests `npx playwright test` (no path argument) matched plugin-cli/scaffolds/*/tests/*.spec.js, which are templates a plugin is created from: their `__name__` placeholders and relative helper paths only resolve once copied into a plugin directory, so collection failed with six 'Cannot find module' errors before any test ran. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add geodataset statistics: per-group stats= and dataset-wide field_stats Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.5-20260806 [version bump] * Restore CRLF line endings in mmgis-openapi.json Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Assert geodataset statistics end to end against a purpose-built geodataset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: reject digit-prefixed text as numeric, don't half-summarize on append Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Compute field_stats when an append creates the geodataset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: keep out-of-range numbers out of the cast, lock field_stats merges Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add nullCount and stddev to dataset-wide field_stats Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add dynamicStyle resolver: map a feature property to a style attribute Pure core of dynamic vector styling. A layer's variables.dynamicStyle declares a property, an attribute, and how to get from one to the other (colour ramp, numeric range, or value table); this compiles that into a function of a feature's properties, once per layer rather than per feature. Knows nothing about Leaflet, the globe, or geodatasets — a geodataset's stored field_stats is only one of the places a numeric domain can come from, alongside the loaded features and an avg +/- sigma window. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Style 2D vector features from variables.dynamicStyle; drop styleMatching The legend no longer assigns styles. The ~155-line styleMatching engine in the per-feature style function is removed in favour of the compiled resolver, which also means a layer's ramp and domain are worked out once instead of being regrouped and rebuilt for every feature. Precedence is unchanged in shape: feature.properties.style still wins, and a dynamic style now sits between it and the *Prop fields it generalizes. Layers that request only some of their properties ask for the ones their rules style by. Configs carrying styleMatching entries keep loading; those entries now only describe the legend. Commit 6 adds the one-click conversion. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reach 3D: resolve the dynamic style onto the globe's GeoJSON clone A styleMatching layer looked right in 2D and default-coloured on the globe, because those colours were only ever Leaflet options. The globe styles from feature.properties.style, so toGlobeConfig writes the resolved values there — onto its own clone, leaving the map's features (what Draw and export read) alone. It reuses the resolver 2D compiled, so a domain measured over the loaded features is the same domain in both views rather than being re-measured from whatever the clone happens to hold. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Draw the dynamic style in the Legend, from the same compiled rules The legend reads variables.dynamicStyle rather than a materialized _legend, and every swatch is coloured by asking the layer's own resolver what a value looks like — so the legend can't drift from the map, and it shows the domain actually in use (avg +/- 2 sigma, a geodataset's whole-dataset extent) instead of a second reading of the config. Each scale is titled with the property it describes; a rule that drives weight or radius has nothing a swatch can show and is left out. A layer that also has configured entries keeps them, with its scale after. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Author the dynamic style in the vector Style tab, and convert legacy legend styling into it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Let a viewer restyle a dynamic layer, and stretch its scale over the view Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Pick the ramp by its colours, and let the bins be dragged Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add the Reference Mission's dynamic style layer data Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: escape property names, keep the legend read-only, let a failed stats fetch retry Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: the whole-dataset toggle keeps a rule's configured domain source Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Layers Tool: attribute picker, aligned ramp rows, user-settable gate, visual ramp picker in Configure Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Dynamic style: reset to the configured style, rule-backed properties only, and review fixes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Dynamic style: legend follows automatic restyles, and its heading is no longer the composite sentinel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hide the Dynamic Style fields a rule has made irrelevant Maker gains `showIf`, a condition (or array of them) naming a sibling field and what it must read, and `subheading`, a section header. The Dynamic Style rule uses both: Property/Values Are, then Defaults, Scale, No Data and Categorical Mappings - with Sigma only under a mean-sigma domain, the ramp only for a colour attribute, bins only when discrete, and the style range only for weight/radius/opacity. Two review findings, both real: - In 2D, a feature carrying any `style` of its own skipped dynamic styling entirely, so `{radius: 12}` also dropped its fill colour. It now merges key by key under the feature's style, as the globe does. - Converting a legend spaced its colours evenly, so entries at 0, 10 and 100 rendered as 0%, 50% and 100%. Stops now carry their position, and the domain comes from the entries that contributed a colour. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Let a fieldless component sit inside an objectarray An objectarray read every declared component's field to seed a new item and to fill the control, so a subheading - which edits nothing and has no field - took the whole Configure page down with it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Offer only the styles a rule can still produce A value table of colours compiles to nothing when aimed at weight or radius, so the layer quietly lost its colouring and its legend. The runtime picker now offers a categorical rule only the family its mappings hold. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a single feature's stats about its group The window aggregates run after the WHERE clause, so asking for one feature by id left it alone in its own partition, reporting its value as the group's min, max and average. The id restriction now waits outside the query the group is aggregated in. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Sort out three Dynamic Style and statistics snags A deduplicated request for one feature with statistics matched nothing: the id was filtered outside the query, and DISTINCT ON had already kept only its group's highest id. It now narrows the query to the feature's group instead, which also saves scanning the table. An objectarray item's field is relative to its item, so an unset one was reading a like-named key of the layer - a new rule's Values Are showed the layer's own 'vector'. Values Are now sits below No Data, so choosing categorical reveals the mappings directly beneath it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Number objectarray items, and mind the groupless feature A feature with no group value was matched with = against a null, so asking for one with statistics returned nothing at all; null groups are one group to PARTITION BY, so the comparison is now null-safe too. The searchdropdown was the one case left reading the enclosing config for an unset objectarray field. Items of an objectarray look alike, so each now wears its number. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.6-20260807 [version bump] * Keep the ramp list with its picker, and unmount a ramp that has gone A portalled list was placed against the viewport once, at opening, so scrolling the settings pane it was opened in left it behind. It now measures again while it is open. A rule that stops wanting a ramp leaves no mount for one, and the stale root was only let go of when a new mount was found. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Style by a group's statistics, and by as many rules at runtime as configured A rule aimed at _.stats.depth_m.avg now asks the geodataset for depth_m's per-group statistics itself, so a feature can be styled by its group rather than only by itself; an admin can name further fields to summarise. The LayersTool panel exposes every rule rather than the first, with rules addable and removable for the session, reports what the layer knows about the numbers it styles by, and resets from an icon beside its heading - as the layer's own settings reset now does too. A rule that widens lines or fades them is drawn in the legend as lines of that weight or swatches at that opacity, rather than being left out. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a rule's number span when it is re-aimed at another style Patching range with undefined erased it, so switching a weight rule to radius left it mapping through nothing and it stopped styling at all. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Style by a group statistic without typing its path A dynamic style rule now says what kind of thing it reads - a feature's own property, or a statistic of its geodataset group - so an admin picks depth_m and avg rather than writing _.stats.depth_m.avg, and a viewer can switch that statistic from the Layers Tool. Paths already written keep working. Also stops a colour picker inside an objectarray item from falling back to a like-named colour on the layer being edited. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep the stats heading from standing in for the composite one Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Three review fixes to the runtime style panel Ramp editors are now cached per layer rather than under a key another layer's name can prefix, a ramp carrying its own colours is drawn and kept rather than shown as viridis, and the Stat dropdown leads with the average it actually defaults to. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Key a rule's fields by what they edit, not where they land Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Key a row's components by where they are declared too Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Measure a group statistic over the groups, not the features Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a view-measured layer coloured when it's off screen Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Read a group statistic filed under a dotted field name Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Ask for a nested statistics field by its whole name Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Show the bin split the map is actually drawing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Colour a group-statistic layer that has no groups measured yet Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.7-20260810 [version bump] * Stop a switched-off dynamic style from asking for statistics Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Report a group's sum and deviation, and style by them Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Name the new statistics, and let a viewer aim a numeric attribute Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Recompute an old geodataset's statistics from Configure A geodataset written before field_stats was kept has no dataset-wide numbers for a dynamic style to stretch over, and an append cannot repair that: it merges only the features it appends. A button per row in Configure's Geodatasets tab now rescans the table in one Postgres pass, flattening nested properties to the same dotted paths and over the same numeric guard ingest used. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Halve a rescan's cost, and show a list field inside a repeatable group The rescan walked every property recursively and evaluated its numeric guard twice; only nesting needs the walk, and jsonb-to-text extraction happens once now. 200k features x 8 properties: 9.6s -> 5.0s, temp spill 174MB -> 63MB. A comma-separated list field belonging to a repeatable item read from the top level of what was being configured, so it always rendered blank and typing overwrote what was stored. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a legend label's exponent, and say what a Stat is measured over A legend shows units once in its title and takes them off the labels, but what counted as units was everything after the digits - so 1.8e10 read as 1.8, smaller than the 1.0 below it. An exponent now belongs to its number. The Stat row gains an info icon: a group statistic describes the group, which is easy to read as describing the feature. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Re-read the geodataset entries after a rescan The entries carry each geodataset's statistics, so the copy in hand described the dataset before the rescan until the page was reloaded. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Colour a view-following layer over the view it is built in The first compile ran before there was a drawn layer to measure what is inside the map bounds, so a layer stretched over the current view was painted with its whole file's spread until the map was nudged. Verified on a framed pair of features: 48.2-76.4 where it used to be 5.1-103.7. A restyle also no longer compiles over an empty feature set, which would drop the styling a layer already has. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Dress the Layers Tool's dynamic style panel The Stat explanation is a real tooltip rather than a native title, as are the stats' 'measured over' notes. Section headings sit at the settings' left edge in sentence case, with the dynamic style's reset beside its name and a labelled Add opposite; rules and stat groups are told apart by the indent of what belongs to them, and a rule between the Stats section spans the panel. Dropdowns, the ramp picker and the bin box all share one width. Also centres the Filters help icon, which its own margin was pushing 6px below the heading it sits in. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Shorten Configure dynamic style rules and keep a restyled globe layer Property fills the rest of its row (3-7-2 with Stat) and Domain widens to half, with three descriptions trimmed, so each rule is shorter. A Cesium restyle no longer removes the globe layer before rebuilding it - the rebuild reloads it in place, so a failure cannot leave the globe without a layer that is still on. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drop legend style matching, and keep a domain to what its source says Legend entries no longer declare styling at all: the deprecated 'Style Matching' field, the conversion button and its module are gone, since Dynamic Style covers what they did. Also from review: - a feature property named __proto__ accumulated field statistics onto Object.prototype instead of into the statistics - Data Min/Max, which Configure only shows for a literal domain, kept pinning the scale after the domain source was changed - 'Nulls' was measured against a stored feature count that a rescan can now correct Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep dynamic style honest about its domain, and draw draped polygon edges Stats now measure what the chosen domain measures, and switching it rebuilds the readout. A refreshed layer announces its restyle so the legend and the Layers Tool follow a time change. Vector layers can list the properties a viewer may aim a rule at. Cesium refuses outlines on terrain, so a draped polygon's stroke is drawn as a clamped polyline - an outline-only polygon was invisible on the globe. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Refresh only the stats a restyle changed Rebuilding the whole dynamic style block on every restyle destroyed the input being typed into, so a bin count of more than one digit could not be entered. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Only rebuild the settings block when there are stats to put in it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * List selectable properties and stats fields separately Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.8-20260811 [version bump] * Report the domain of every rule, and stats for what each one colours by Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make Whole dataset / Current view one setting the rules follow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Author dynamic style rules in Configure and switch them on and off at runtime A viewer no longer aims a layer at a whitelisted property: the rules an admin wrote are the vocabulary, each with an On switch they may flip for their session. A rule aimed at a text field with no mappings written for it takes them from the values the layer holds, so a categorical field is styled rather than silently painting nothing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Draw a repeatable item's checkbox from what was saved, not its default A stored false is falsy, so a dynamic style rule left switched off came back switched on when the layer was reopened. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Stop a feature's style becoming the layer's, and repaint when the last rule goes off A vector layer's style function used layerObj.style as its scratch and left the last feature's values there, so the next feature and every rebuild started from them - a dynamicExtent layer refetching after a zoom drew features the rule doesn't resolve in the last one's colour and opacity. Switching every rule off now repaints instead of returning early, the Configure On field is a switch, and a geodataset without stored statistics says so beside its Domain choice rather than silently scaling over what is loaded. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a bin's range whole in the legend A label like "60 - 80" was split into the number 60 and the units "- 80", so every intermediate bin was labelled with its lower edge alone. A remainder with a digit in it is a second number, not a unit. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Read a marker's shape and animation from the feature's own style The style function no longer leaves the feature's style on the layer, so pointToLayer must take shapeIcon, shapeProp and animation from what it returns or a point carrying its own shape would be drawn with the layer's. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Say a scale is missing dataset-wide numbers only where they could exist A group statistic is of the query by design and a categorical rule spans no range, so neither is short of anything stored - naming them said a recompute would help when it wouldn't. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Recognise a geodataset url however its scheme is written Loading a layer lowercases the scheme, so 'GeoDatasets:x' loads - but its dataset-wide statistics were never fetched and the note that says so was suppressed for exactly those layers. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bound a group statistic by its field's whole-dataset numbers A group average lies inside the field's own extent and a group spread is at most half of it, so Whole dataset now gives those rules a scale that doesn't move as you pan. A sum has no such bound and is still measured over the groups in hand, which the note now says. Adds a Field Statistics modal to Configure's Geodatasets tab, and pins a domain typed with no source named. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Register the field statistics modal so it opens and closes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Query the globe's own extent at every zoom, and let a fill-less footprint be clicked Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Label a bin boundary with the value it falls at, and keep a rule's style range positional Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Quieter stat rows, a flush rule switch, and a filter property list that catches up with the layer Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make the bin values legible Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give the stop bar room above it and a reset that stays put Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Butt the stop reset against the bar and stop bins at 20 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Requery over whichever engine moved last, not always the map Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Square the rule switch and name the stats' domain in place of a tooltip Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Query the globe's real frustum extent instead of a Leaflet zoom estimate Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Store geodataset properties as jsonb, and gather row actions into a menu New geodataset tables store their properties as jsonb rather than json, so a property read is a lookup instead of reparsing the document - measured at 11.2s -> 4.2s for a 200k-feature /get with stats= over the whole dataset. Existing tables keep json until converted from Configure, since the same operators read either type. Configure's Geodatasets rows end in a menu holding Field Statistics, Recompute Statistics and Convert Properties to jsonb, the last disabled for a geodataset already storing them that way. /entries reports each geodataset's properties_type. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.9-20260812 [version bump] * Make a globe click find its feature when a dynamic style is on A resolved style is not part of a feature's identity, a rule on one style key alone now repaints the globe's points and lines, and a reload no longer leaves a data source detached or hidden. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make a globe click's highlight show on the polygon it picked Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Highlight the feature that was clicked, and spin while a requery is out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Pick the footprint the click is on, and keep ids of different kinds apart Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * List what a style a viewer can't change is styling by Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fit a listed rule to its row, and let an admin hide the stats Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Match on a kind of id both features carry Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give a globe reload's entities ids of their own Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Record nesting only as deep as a rescan reads it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep the old features up until the new ones are drawn Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Match the retire call as it is written Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Measure a line as the open path it is Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Refuse a pinned end that crosses the discovered one Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Open the globe onto what the map is showing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Leave the rest of the file as it was Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Free a globe data source the reload replaced Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hold the row a closing menu is still drawing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Clear a highlight the reload's entities left behind Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Draw a configured image legend beside a scale Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give the dynamic style tests room for the ramp controls Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.10-20260813 [version bump] * Validate Missions path on getprofile/getbands/getminmax before GDAL Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.5-20260813 [version bump] * Drop what nothing calls and share what was written twice Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Put the escaper and the legend builder where they belong Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Send the mission-relative raster path to getminmax for image layers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * File a template geodataset's statistics under the name it was stored as Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a kebab menu opened again during its own close Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Allow operator-allowlisted remote GDAL datasets via GDAL_ALLOWED_REMOTE_PREFIXES Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Pass remote GDAL urls through unaltered and require allowlist entries to name a host Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Trim comments in validateGdalDatasetPath Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve percent signs in Missions paths and match remote prefixes on a boundary Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Clean the empty values inside an objectarray item too Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make Leaflet a managed dependency at 1.9.4 and document vendored plugins - add leaflet@^1.9.4, leaflet-geometryutil, leaflet-polylinedecorator to root deps - import leaflet + leaflet/dist/leaflet.css in src/index.js; window.L unchanged - delete vendored leaflet 1.5.1/1.9.2 engine builds, vendored proj4, geometryutil, polylineDecorator - restore L.DomEvent.fakeStop (removed in 1.9) for leaflet.vectorGrid - keep pre-1.9 popup content font metrics - README.md recording upstream version + local divergence per vendored plugin Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove unused vendored Leaflet.ImageOverlay.Rotated Not imported anywhere; drop its attributions entry and README row too. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make the fakeStop compatibility shim a no-op so map drag still starts on vector tile features Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update Leaflet and GeometryUtil versions in attributions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Record the local patches that were in the deleted vendored Leaflet copies Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Sync the lockfile version with the merge Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix geodataset temporal bugs: legacy tables, start-only time filters, recreate with new time mappings Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.11-20260820 [version bump] * Fix globe layers and time histogram handling Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.12-20260824 [version bump] * fix(deps): pin svgo override to locked version Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Harden security boundaries and content handling Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve Markdown breaks and restore API rate limit Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Allow development WebSocket origin ports Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Run Playwright tests on supported Node version Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve request size rejection status Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Allow large dataset recreation payloads Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix browser security regressions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve layer control element IDs Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * ci: correct checkout pin comments, bump CI Node to 22, harden head_ref use Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.14-20260825 [version bump] * deps: pin stac-fastapi-api/types/extensions to 6.2.1 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.15-20260825 [version bump] * Add Broadcast plugin: push-based layer refresh via POST /api/broadcast/layerUpdate - New core backend plugin plugins/core/backend/Broadcast mounted at /api/broadcast (ensureAdmin + checkMissionPermission) - Broadcasts notify-only envelope { info: { type: 'refreshLayer', layerName }, body: { mission } } - Client handles refreshLayer by reloading the layer(s) via TimeControl_.reloadLayer without refetching the config - New time.refreshViaBroadcast layer flag skips the setInterval polling for push-managed layers - OpenAPI + docs Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.19-20260902 [version bump] * Drop time.refreshViaBroadcast switch; push refresh applies to any time-enabled layer, polling unchanged Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Broadcast plugin tests (unit helpers + /api/broadcast/layerUpdate API spec) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Broadcast: resolve display names to UUIDs, refresh non-time layers via Map_.refreshLayer, report broadcasted:false when wss unavailable Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Re-query layers on turn-on when a refreshLayer broadcast arrived while they were off Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.20-20260903 [version bump] * Make Broadcast plugin non-overridable Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Move broadcast re-query logic from essence.js into L_.requeryLayers (Layers_/lifecycle/refresh.js) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Broadcast: validate body before checkMissionPermission Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Extract websocket connect/retry/routing into WebSocketService; restore refreshLayer default export Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Document which layer types layerUpdate refreshes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.21-20260904 [version bump] * Security fix for geodatasets filter handling * chore: bump version to 5.3.22-20260909 [version bump] * Bound geodatasets in-filter value count/length; extend tests * Adjust probe values in in-filter test * Neutralize test naming for geodatasets in-filter spec Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * DrawTool: scope files by mission with showNullMissionFiles/showMasterFiles toggles Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.23-20260910 [version bump] * Add missions_viewing: restrict mission visibility and config loading per user under AUTH=local Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.23-20260910 [version bump] * Landing page: show a message when the user has no viewable missions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * missions_viewing: SuperAdmin-only updates, follow mission renames, MAIN_MISSION guard, and e2e tests Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * User model: only re-hash password when it actually changed Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Enforce missions_viewing on /Missions/<mission>/ static files under AUTH=local Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Static mission file check: authorize msv.missionFolderName folders, cache per-user lookup, document cross-mission note in Users UI Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Viewable-folder cache: generation guard and post-response invalidation on mission mutations Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Return 403 instead of login page for unauthenticated /Missions file requests Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Styled 403 page (with ROOT_PATH home link) for unauthorized file routes; plain 403 for non-browser clients Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Revert "Merge pull request #130 from JPL-Devin/devin/1788999714-missions-viewing" This reverts commit 79a09e41c3eb9e3cec062a78c2bc70677d6c30db, reversing changes made to c666775f1188a1ab09e610f85373a72022f57ea2. * DrawTool: hide Lead Maps header when no master files are listed Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * DrawTool config: label toggle 'Show Lead Files' Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * DrawTool: await mission migration before makeMasterFiles; follow mission renames in user_files Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * DrawTool: read mission visibility toggles server-side from configs; validate mission on make Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.24-20260915 [version bump] * DrawTool: show file mission in info modal; leads can reassign it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * DrawTool: leads can reassign file mission even when other file info is read-only Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * DrawTool: interpolate mission markup directly into file in…
…ASA-AMMOS#1088) * Name the layertype/attachment Configure form 'config', matching tools * Declare renderer modules by surface, and make the two new families ordinary plugins `paths` is the tools/interactions concept — a key there is the export name a mission config names in `"js"`. Layer types were borrowing the field for render surfaces and attachments for a constant `"plugin"` wrapper, so the same key meant three things. Layer types now declare `modules` (nested per globe engine rather than dotted string keys) and attachments a single `module`. Also aligns the two families with the older four: they carry `uuid`/`id`, they version as `"core"` like every other plugin shipped here (rejecting anything that is neither `"core"` nor semver), and they are discovered by `plugin-cli`, so they list, show `info`, validate and destroy like any plugin — core's stay undisableable through `overridable: false` rather than through being invisible. `activate` regenerates their registries too, so installing one no longer needs a full build. Removes `settings`: declared in the contract, validated, resolved by the generator and exposed as `getSettings()`, but no plugin ever used it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix two Devin Review findings: leaked view listener, doubled gradient build Velocity's fade-on-move subscription lived in `make`, which re-runs on every show and time change, so listeners accumulated for the life of the page. Subscribe once per layer; the handler already resolves the current layer. A path gradient built its Cesium geometry twice on a host's first toggle-on: core now makes the map layer before the attachment loop, so the loop shows the attachment, and the deferred globe branch (keyed on hadToMake) showed it again. Defer only when that loop didn't run. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix attachment op lookup broken by the module key rename `withOp` still read `mods[id].plugin`, the pre-rename key, so it matched nothing: an attachment's per-feature ops were never found and clearing a clicked image overlay or model silently did nothing. Guard both sides of the generated boundary in a test, since a mismatch here fails as a no-op rather than an error. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make the plugin families authorable: docs, scaffolds, capability validation The code grew config/filter/time surfaces, classification capabilities and a whole attachment family; the authoring materials still described the render contract. Closes that gap, and moves the CLI out of /plugins so the directory is containers only. - plugin-cli/ at the root: cli.js, lib/, and the six scaffolds as real files under scaffolds/<type>/, token-substituted into paths and contents. They are valid JSON/JS as they sit, so the validator checks them (a broken scaffold fails CI, not someone's first plugin). registries.json is CLI config and moves with it; plugin-state.json stays in /plugins, where the server reads it. - create layerattachment, and a layerattachments README with the contract and a worked example — it was the one family with neither. - Validate classification capabilities against a schema: a wrong type or an out-of-enum value is an error, an unknown key warns, and an omission core acts on (map.stacking, host.order) warns. These previously failed silently as a mis-ordered or un-pickable layer. - Document the config/filter/time surfaces, drop the capture surface and the onHostToggle op (neither has a runtime implementation). - Destroy a layer's attachments when its host is removed for good. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Document the six plugin families and their scaffolds in the docs site Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Layertype file layout: one path segment per surface The leaf name repeated what the plugin directory already said (Vector/map/vector.js), and globe/config.js was not the config surface at all but a helper shared by the two engine modules, one directory away from the real config/vector.js. - Vector/map.js, config.js, filter.js, time.js; globe/cesium.js, globe/lithosphere.js. - globe/config.js → globe/layerConfig.js: it builds the engine-neutral globe layer descriptor, which is not the `config` surface. - Non-surface helpers go in lib/ (VectorTile/lib/SimplifiedVectorGrid.js). - Drop the export default from the layerConfig helpers; every importer uses the named exports. - Vector/map.js declares its ops as named functions like the other 29 modules instead of inline methods. Paths are convention only — the manifest key is what names a surface — so this is manifests, imports and regenerated registries. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Show the op phase form in the layertype scaffold The scaffold's bare `make` is shorthand for `{ main }`, which made Vector's phase object look like a different contract. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drop the undispatched `load` op; derive the CLI's plugin types from one map `load` was in LAYER_OPS, the validator, the README table and the scaffold, but core never dispatched it and no type implemented it: data acquisition (including the dynamic-extent refetch) is core's, in LayerCapturer. The op table also omitted `render` and `onToggle`, which core does dispatch. `create`'s valid types and the discovery directories were two hand-kept lists, so the help text still advertised five families after layerattachment landed. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Serialize the specs that regenerate the shared plugin registries src/pre/*.js and configure/public/*Configs.json are single copies in the repo, so a worker running the CLI could regenerate away a fixture another worker had just written and was asserting on (~1 in 3 runs locally; CI uses one worker). Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Declare the lock helper's Atomics globals for ESLint Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix the authoring papercuts eight fresh plugin authors hit Eight sessions each wrote a plugin against this branch with no help beyond the docs. Every one of them tripped over the same three things: - `create layertype` printed "implement make in map/<name>.js", a path that stopped existing when the layout collapsed to one segment per surface. - A scaffolded plugin is untracked, since a container is normally its own repo. Said so where `create` is documented, rather than leaving it to git status. - `plugin-registries.json" is `plugin-cli/registries.json`. Also: a type that `extends` another was warned for capabilities its parent declares, and the two layer families' templates weren't linked from the README's Creating Plugins section that lists the other four. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Document extends where the docs already claimed it was The plugins README points at this file for extends; this file never mentioned it. The one child of the eight who was told extends existed found out how it works by reading LayerTypeRegistry and pluginValidation; the others would not have found it at all, and a new data source is exactly the case it exists for. Also states what an expanded sublayer must carry, since that was copied from Vector's STAC path by guess. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Correct the manifest-reference facts authors checked against reality Every one of these was found by an author who followed the doc and then found the code disagreed: icons are MDI, not Ionicons (the scaffold itself emits `mdi mdi-`); the toolbar priority bands are 1-4/101-102/1001-1002, not '~1001-1020'; `create` takes six types, not four; and the documented way to run a plugin's own tests fails in global setup without PLAYWRIGHT_TEST_UNIT_ONLY, which only appeared in a package.json script. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Enforce interaction applicability; document backend; runnable scaffold tests `applicableLayerTypes` was descriptive: a mission config, a kind preset or a type's `defaultInteractions` could name an interaction the interaction says it doesn't handle. updateInteractions now emits APPLICABLE_LAYER_TYPES and the runner filters the whole pipeline — preamble and postamble included — against the layer's type chain, so a type that `extends` vector keeps vector's interactions. The scaffolded tests were CommonJS assertions about plugin.json sitting beside ESM modules, and no npm script ran them. They are now ESM, import the module where the module can load outside the bundler (attachment, interaction, backend), check that every declared module path resolves — the usual cause of a plugin that validates but never appears — and run under `test:plugins:unit`. Backend was the one family with no contract document: adds plugins/core/backend/README.md (the `s` object, lifecycle ordering, ensureAdmin's whitelist, envs, models) and gives the scaffold a deliberate auth gate. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Validate the manifest parts that fail quietly The Configure form a manifest declares was validated only as "is an object": Maker skips a component type it cannot render and writes wherever `field` points, so a typo'd type or an attachment field outside its own `configPath` yields a form that looks right and configures nothing. validateMetaconfig now checks component types against what Maker renders, field presence, dropdown options, objectarray shape, grid widths, and that an attachment only writes inside its configPath. `validate` also now reports what it could not see before: an enabled plugin absent from src/pre (valid, but the app never loads it) and a registry import whose plugin is gone — both meaning activate has not been run. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add the source, legend, onConfigChange and configPath surfaces Four surfaces the eight authoring experiments asked for, each replacing something core was deciding on their behalf: - layertype `source.fetch(layerObj, ctx) -> GeoJSON`, dispatched from LayerCapturer for every acquisition. Core keeps the whole dynamic-extent policy (extent, debounce/settling, zoom gate, move threshold, request staleness, clear/update, reload subscribers) — extracted out of the two duplicated branches into capture/dynamicExtent.js so it is testable — so `fetch` is a pure "given this, give me GeoJSON" for data that doesn't come out of a url core can fetch (POST bodies, headers, pagination, an SDK). - layertype `legend.derive(layerObj)`, replacing the `['image','tile'].includes(type) && cogTransform || type === 'velocity' || (type === 'data' && shader === 'colorize')` list that both the LayersTool and the LegendTool carried. Tile/Image/Velocity/Data now declare the surface. - attachment `onConfigChange(ctx)`, with `mmgisAPI.setLayerAttachmentConfig()` as its dispatcher: core writes the new settings into the host's live config at the declared configPath, then lets the attachment retune in place rather than paying for the default host rebuild. - interaction `configPath`, so an interaction with per-layer settings is handed its own subtree as `ctx.config` instead of digging through layerData. Also: `raw` (the engine namespace) on both globe contexts, matching `mctx.raw`; `test:plugins:unit` no longer fails when no plugin ships a @unit spec. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix the doc and scaffold gaps a second authoring round found Eight plugins were written against this branch by separate agents; these are the things they had to guess at or work around. - The layertypes README still said data acquisition is not in the vocabulary and belongs in `make`, 75 lines above the `source` surface that replaced that advice. - `gctx.raw` was absent from the gctx table, which still sent globe authors to `gctx.renderer` for engine-specific work. Documented, including that the two engines' namespaces are symmetric in name only. - Globe operations are dispatched by layer *name* (only `make` gets a layer object), and `setVisibility`/`setOpacity` take the new value positionally. None of that was written down; two authors guessed. - Nothing said how a layer opts into dynamic extent, so `source.fetch` saw a null `ctx.view` with no explanation. - `legend.derive` is handed config, not data; say so, and where render state actually lives, instead of leaving authors to stash on the layer. - `onConfigChange` is passed the built attachment, which the ctx table omitted. - The layertype scaffold's own test asserted `modules.globe[key]` over `Object.keys(renderers.globe)`, so it failed for every globe layertype, and it assumed `modules` where a single-module `extends` plugin has `module`. - The scaffold and the attachment README both captured `window.L` at import time, which is undefined in the unit test the same scaffold ships. Read the global per call instead, and drop `F_` from the example: importing an MMGIS singleton pulls jQuery and makes a module un-importable in Node, which browser-globals now states rather than implying it protects against that. - `create` printed `npm run build` for interactions instead of `activate`, said nothing when a name collided with an existing plugin in another family (a `Curtain` layertype silently shadowed the `Curtain` tool), and left the author to discover that a non-core container is untracked here. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add a textarea component, document the metaconfig, isolate ctx.config A plugin whose main setting is written in another language — the Overpass QL query one of the authoring experiments built its layertype around — got a single-line text field, because every free-text component Maker renders is one. `textarea` is multiline and monospace, sized by an optional `rows`. The component vocabulary itself was only discoverable by declaring something invalid and reading the validator's error, so plugins/README.md now has the metaconfig section that error was standing in for: what a row is, what each component field does, and what all 23 component types render. Also: the interaction runner assigned `ctx.config` only for interactions that declare a `configPath`, so an interaction without one was handed whichever config the interaction before it in the pipeline had. It now gets null, which is also what a layer that was never configured yields — the previous test asserted the leak, so it changes with it. For the globe, `gctx.raw` on LithoSphere is the globe class and nothing else (the package exports one thing), so the README now says where the work actually happens: `renderer` for the live globe and `window.THREE` for geometry, which MMGIS vendors and puts on the window. An author looked for THREE on `raw`, didn't find it, and dropped LithoSphere support. `create layertype` scaffolds a standalone renderer, which is also now called out as the wrong starting point for a type that draws like one MMGIS already has: extend it instead. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give interactions a settings form on their own card An interaction could be handed `ctx.config` but had no way to declare what an admin should be able to put there, so its settings existed only as prose in a README. It now declares `config.rows` in the same metaconfig vocabulary an attachment uses, and Maker renders those rows on the interaction's card in a layer's Interactions tab, behind a gear — beside where the interaction was chosen and dragged, rather than in a tab of its own. Validation follows from where the form lives: rows need a `configPath` (the runner has nowhere else to read them back from), every `field` must sit inside it, and `tab`/`tabs` are rejected. A row's `default` remains a form default — nothing is written until an admin touches the field — so plugins still default their own values, which the scaffold and README now spell out. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep an interaction's settings when the layer modal trims a layer Closing a layer modal rebuilds the layer from the fields its layer type's tabs declare, which is why attachment config paths were already exempted. An interaction's settings come from its own manifest, so they were written, rendered, and then silently dropped on close. Interaction manifests now load into the store alongside the attachment ones, and their config paths are preserved by the same pass. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Cover an interaction's settings form end to end No core interaction has per-layer settings, so the fixture interaction is served in place of the generated registry — a static JSON file, so no plugin has to be installed to stand in for one. Fails without the layer modal preserving interaction-owned config paths. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix what a third-party author trips on: names, tabs, core's own tests Eight fresh authors built a plugin each against this branch's docs. The contracts held; the tooling around them did not. - `create FOVWedges` emitted `f_ovwedges` and `fOVWedges` — an acronym is one word now, and so is a word with a digit in it (`HTML5Parser`). The printed next-steps filenames derive from the same tokens rather than re-deriving them wrongly. - The layerattachment scaffold defaulted to a Configure tab of its own, the exact thing its README warns against; it now joins an existing tab, `validate` warns when a non-core attachment's `config.tab` matches no core tab, and the next steps say so. - `create`'s activate diff buried what you just made under every plugin there is on a first run; it summarises past 8 lines, `--verbose` to list them. Two of core's own unit assertions failed for a *valid* third-party attachment: an exact tab list, and a field walk that descended into `objectarray.object`, whose item fields are relative and so are not paths under `configPath`. Both keep their invariant (core's tabs present and in order, no duplicate tabs, every real field owned by the attachment that reads it) while allowing an installed plugin, with the third-party case asserted rather than assumed. `enableWhenField` is `{ field, value }`; given a bare path Configure greys the control out forever, so validation rejects it now. Docs, all from things an author had to read core source to learn: an interactions family README (the `ctx` table, phase/order, settings, and what can't be unit tested); a `Time` section (the TimeControl surface, and that a feature's timestamps live at the properties `time.startProp`/`endProp` name); what the inherited vector renderer does with the features a `source.fetch` returns, including `prop-<name>` styling; `objectarray`'s relative item fields, `default` vs `defaultChecked`, and that a cleared number arrives as `""`; what an attachment's returned object may carry and that `ctx.config` is never null in `make`; and importing `L_` by alias in a globe module. Also drops two overwritten no-op `TimeControl.subscribe`/`unsubscribe` stubs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix the cross-family seams a multi-plugin feature runs into A feature that needs a layer type *and* an attachment *and* an interaction was buildable but had three ways to fail quietly, all found by building eight of them: - `pluginDependencies` resolved against tools/backend/components/interactions only, so an interaction depending on the layer type it was written for was reported enabled, validated clean, and then left out of the generated registry. Dependency discovery now covers every family, and the warning says the plugin never loads instead of leaving that to be inferred. - `validate --json` was unparseable whenever the manifest validator logged, since it logs JSON lines to stdout. Captured and returned as warnings. - The module validator accepted `export default { … }` but not the named form the scaffolds and docs use, and matched examples inside comments. Also: `syncData` is handed the attachment's `config` and `layerObj`, as `make` already was, so a redraw need not stash them; stale-registry validation resolves the imported module rather than its directory, which is what a type switching to `extends` deletes; and `applicableLayerTypes`/`defaultInteractions` ids that no enabled plugin provides are reported. `create layertype --extends <typeId>` scaffolds what the README recommends and the best plugins are — a manifest with `extends` and one module whose keys are surfaces, starting at `source.fetch` — instead of a standalone renderer. The parent is checked as you type it. Local `install` no longer copies `.git`, and `--container` pins the container name, which plugin ids embed. The interaction scaffold's spec tested the handler, which cannot be imported in Node once it imports a singleton; the decisions now live in `logic.js` and that is what it tests. Both other scaffolds emit a named default export, so a plugin following them no longer starts with a lint warning. Docs: the `_legend` entry shape, one container holding a whole feature and how its plugins reach each other, attachment time (no op needed — `syncData`), `syncData`'s context, calling your own backend under a subpath, `kindAlias` being an array, `NODE_ENV` for eslint, and install/container identity. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Let a layer type declare the attachments it comes with A feature is often a layer type plus an attachment plus an interaction, and the type is the plugin that knows what the attachment should be. It could already declare capabilities.defaultInteractions but had no way to say the same about an attachment, so three separate plugin authors worked around it the same wrong way: the type's own config rows wrote a string-literal copy of the attachment's configPath, coupling two plugins through a path either could rename. capabilities.defaultAttachments maps attachmentId to that attachment's settings ({} for none), and core hands it to the attachment under the attachment's own configPath as if an admin had filled the form in. A layer's own settings sit on top field by field, so changing one thing doesn't lose the rest and enabled: false opts out. It resolves through configFor/isEnabledOn, which everything already went through, so no call site changes. validate warns for a declared attachmentId no enabled plugin provides, and for one whose applicableLayerTypes excludes the declaring type - a well-formed default that would silently never apply. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix what round five's authors hit: extends drops inherited operations, empty form fields beat a type's defaults extends merged parent and child one level per *surface*, so a type adding a config.normalize silently lost its parent's config.expand — the opposite of 'declare only what differs'. Merging is now per operation (and per engine under globe), in a pure typeInheritance module. A layer's attachment settings also overrode a type's defaultAttachments with '' — which is what Configure writes into a row nobody filled in — leaving the attachment with no property name at all. Empty (nullish or '') no longer overrides; false and 0 still do. Plus the smaller traps: 'create tool XTool' produced XToolTool; activate reported 'No changes' after a manifest edit that rewrote a registry; ToolController_.getTool returned a silent no-op stub for a tool that isn't loaded; the backend scaffold had no models/ despite its README documenting one; and the docs said nothing about interaction-to-tool calls, stopGuests being mount-wide and status-blind, failure responses arriving as HTTP 200, layer types resolving defaultIcon against MUI rather than MDI, or how L_.layers.attachments is keyed. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A layer type can configure the interactions it ships, not just name them capabilities.defaultInteractions took interaction ids only, so a type that shipped an interaction had no way to tell it the property names it had just fetched — the gap defaultAttachments closed for the other family, and four of round five's authors worked around it by duplicating those names into a third subtree or stashing them on the layer under a private key. An event may now be an object instead of a list: "click": { "wind:report": { "speedProp": "windSpeed" } } Key order is the pipeline order, as the array's is; the settings resolve into the interaction's own configPath on the way to ctx.config, with the layer's own settings over them field by field. An interaction is written once and cannot tell whether a type configured it or an admin did. The merge rules are now shared with attachments (declaredConfig), validate cross-checks both forms — including a declared interaction whose applicableLayerTypes excludes the declaring type — and VectorTile's click, which had been passing no layerTypeChain, now enforces applicability like every other click path. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A mission with a layer of a plugin-provided type can now be saved Round six's authors could author, validate, activate and register a new layer type and still not configure a layer with it: both config validators switched on the layer's type and called anything they did not recognise unknown. They ask the generated layer-type registry instead, and hold a plugin type only to the checks it declares for itself (a `source`-backed type may have no url) — while a type extending a built-in still gets its parent's field defaults, since it is drawn by the parent's renderer. Also from that round: a LithoSphere type that registers itself in gctx.layers now has its destroy/setVisibility/setOpacity dispatched rather than declared, validated and never called; a type that declares a map renderer and resolves none says so instead of loading and drawing nothing; and window.location exists in the unit-test browser globals, so a source can resolve a relative url under test. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A plugin can ask for its layer back, and a type can say when it has data Two surfaces round six asked for, both of which existed as core internals a plugin had to reach into. `ctx.refreshLayer()`, on every interaction and attachment: an interaction that writes a feature to its own backend had to call `L_.Map_.refreshLayer(layerObj, cb, skipOrderedBringToFront, stopLoops, resolvedUrl)` to see its own write. The re-acquisition goes through the same path every other trigger does, and a `source`-backed type now sees it as `ctx.trigger === 'refresh'` rather than as another `'make'`. `time.availability(layerObj, ctx)`: `capabilities.time.histogram` validated, reported true, and meant nothing — TimeUI found histogram sources by matching `stac-collection:` and `{t}` against the layer's url, so any other endpoint was silently excluded. The type answers now, with times and counts; core keeps the window, the binning and the drawing. Tile's two url schemes move into its time module, and declaring the capability without a `modules.time` to implement it in is a validation error. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * A refreshed layer's checkbox no longer shows it turned off Turning the Reference Mission's geodataset time-series layer on and then moving the time bar left the Layers tool showing it off, while it was on and drawn. The refresh swap hides the old layer and shows the rebuilt one with `ignoreToggleStateChange`, so `L_.layers.on` deliberately never changes \u2014 and both halves then compute the state to broadcast from that unchanged value and notify subscribers 'off' twice. Subscribers are what track on-ness (the checkbox; a dynamic-extent layer's re-query), so a toggle that preserves state now tells them nothing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Plugin system: inherited ops, source cancellation/paging, dynamic dropdown options The four remaining findings from the round-six plugin authoring sessions. extends: a child's operation is handed the inherited implementation as one extra last argument, so overriding vector's `config.normalize` to add a field no longer silently drops the `kind`/`radius` it sets. Phases match by name, a bare function stays a bare function, and `inherited()` is a safe no-op when the parent has no such operation. source.fetch: `ctx.signal` (a layer's next acquisition aborts its last, and an AbortError is not logged as a failure), `ctx.emit(geojson)` for a paged source to draw what it has so far, and `ctx.resolveUrl` so a plugin with an endpoint in its own config doesn't hand-roll mission/ROOT_PATH resolution. Core keeps the extent, staleness, move threshold and rendering; an emit after the request is stale is dropped. Maker: a dropdown may name an `optionsFrom` provider instead of listing options — `layerProperties` (the layer's own feature property names, from the geodataset schema or by sampling the file), `layers`, `layerTypes` — so "pick a property of this layer's data" stops being an unchecked text field. Validated against the provider names Configure actually has. Docs: a components family README (when init runs and what exists by then, the z-index bands, the subscriptions, and why there is no core interaction → component channel), and why composing across *layers* is unsupported — with the supportable shape being one layer whose source.fetch acquires both inputs. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Plugin system: a third-party layer type is selectable, and duplicate ids fail validate The layer modal's Layer Type control is a row in the *selected* type's own manifest, and every core manifest listed the eleven built-in ids literally — so a plugin type could be authored, validated, activated and registered, and then never be chosen by an admin. The row asks the registry instead (`optionsFrom: "layerTypes"`), which is also the only list that stays correct. Two owners of one `typeId`/`attachmentId` are refused by registry generation but were reported by `validate` as clean, and a failed generation leaves the previous registries in place — a green check followed by a build failure, or an app silently running the last generation. `validate` now reports them, as it already did for `interactionId`. Docs: the inherited-op example counted `inherited` from the wrong position (`config.normalize` is dispatched with one argument, not two), and the per-family `config` shape (a layer type declares `tabs`, everyone else `rows`) was stated for interactions only. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Plugin system: acquire another layer's data, reference a layer's fields from a type's declarations, and drop only the offender on activate - `ctx.acquire(layerName)` on source/interaction/attachment contexts: a configured layer's data, acquired through its own layer type, headlessly — not turned on, not drawn, no rendered state exposed, and the layer's own live acquisition untouched. - A type's `defaultAttachments`/`defaultInteractions` values may be `"$variables.…"` references read off the host layer, so the property an admin picks on the type's form isn't typed again on the other plugins'. An unanswerable path drops its key, leaving the plugin its own default; `$$` escapes a literal `$`. - `activate` leaves a duplicate-id or bad-`extends` plugin out of the registry and regenerates everything else, rather than aborting and silently leaving the previous generation of every registry in place. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Do not collect plugin-cli scaffold templates as tests `npx playwright test` (no path argument) matched plugin-cli/scaffolds/*/tests/*.spec.js, which are templates a plugin is created from: their `__name__` placeholders and relative helper paths only resolve once copied into a plugin directory, so collection failed with six 'Cannot find module' errors before any test ran. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add geodataset statistics: per-group stats= and dataset-wide field_stats Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.5-20260806 [version bump] * Restore CRLF line endings in mmgis-openapi.json Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Assert geodataset statistics end to end against a purpose-built geodataset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: reject digit-prefixed text as numeric, don't half-summarize on append Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Compute field_stats when an append creates the geodataset Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: keep out-of-range numbers out of the cast, lock field_stats merges Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add nullCount and stddev to dataset-wide field_stats Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add dynamicStyle resolver: map a feature property to a style attribute Pure core of dynamic vector styling. A layer's variables.dynamicStyle declares a property, an attribute, and how to get from one to the other (colour ramp, numeric range, or value table); this compiles that into a function of a feature's properties, once per layer rather than per feature. Knows nothing about Leaflet, the globe, or geodatasets — a geodataset's stored field_stats is only one of the places a numeric domain can come from, alongside the loaded features and an avg +/- sigma window. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Style 2D vector features from variables.dynamicStyle; drop styleMatching The legend no longer assigns styles. The ~155-line styleMatching engine in the per-feature style function is removed in favour of the compiled resolver, which also means a layer's ramp and domain are worked out once instead of being regrouped and rebuilt for every feature. Precedence is unchanged in shape: feature.properties.style still wins, and a dynamic style now sits between it and the *Prop fields it generalizes. Layers that request only some of their properties ask for the ones their rules style by. Configs carrying styleMatching entries keep loading; those entries now only describe the legend. Commit 6 adds the one-click conversion. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Reach 3D: resolve the dynamic style onto the globe's GeoJSON clone A styleMatching layer looked right in 2D and default-coloured on the globe, because those colours were only ever Leaflet options. The globe styles from feature.properties.style, so toGlobeConfig writes the resolved values there — onto its own clone, leaving the map's features (what Draw and export read) alone. It reuses the resolver 2D compiled, so a domain measured over the loaded features is the same domain in both views rather than being re-measured from whatever the clone happens to hold. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Draw the dynamic style in the Legend, from the same compiled rules The legend reads variables.dynamicStyle rather than a materialized _legend, and every swatch is coloured by asking the layer's own resolver what a value looks like — so the legend can't drift from the map, and it shows the domain actually in use (avg +/- 2 sigma, a geodataset's whole-dataset extent) instead of a second reading of the config. Each scale is titled with the property it describes; a rule that drives weight or radius has nothing a swatch can show and is left out. A layer that also has configured entries keeps them, with its scale after. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Author the dynamic style in the vector Style tab, and convert legacy legend styling into it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Let a viewer restyle a dynamic layer, and stretch its scale over the view Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Pick the ramp by its colours, and let the bins be dragged Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add the Reference Mission's dynamic style layer data Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: escape property names, keep the legend read-only, let a failed stats fetch retry Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Address review: the whole-dataset toggle keeps a rule's configured domain source Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Layers Tool: attribute picker, aligned ramp rows, user-settable gate, visual ramp picker in Configure Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Dynamic style: reset to the configured style, rule-backed properties only, and review fixes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Dynamic style: legend follows automatic restyles, and its heading is no longer the composite sentinel Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hide the Dynamic Style fields a rule has made irrelevant Maker gains `showIf`, a condition (or array of them) naming a sibling field and what it must read, and `subheading`, a section header. The Dynamic Style rule uses both: Property/Values Are, then Defaults, Scale, No Data and Categorical Mappings - with Sigma only under a mean-sigma domain, the ramp only for a colour attribute, bins only when discrete, and the style range only for weight/radius/opacity. Two review findings, both real: - In 2D, a feature carrying any `style` of its own skipped dynamic styling entirely, so `{radius: 12}` also dropped its fill colour. It now merges key by key under the feature's style, as the globe does. - Converting a legend spaced its colours evenly, so entries at 0, 10 and 100 rendered as 0%, 50% and 100%. Stops now carry their position, and the domain comes from the entries that contributed a colour. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Let a fieldless component sit inside an objectarray An objectarray read every declared component's field to seed a new item and to fill the control, so a subheading - which edits nothing and has no field - took the whole Configure page down with it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Offer only the styles a rule can still produce A value table of colours compiles to nothing when aimed at weight or radius, so the layer quietly lost its colouring and its legend. The runtime picker now offers a categorical rule only the family its mappings hold. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a single feature's stats about its group The window aggregates run after the WHERE clause, so asking for one feature by id left it alone in its own partition, reporting its value as the group's min, max and average. The id restriction now waits outside the query the group is aggregated in. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Sort out three Dynamic Style and statistics snags A deduplicated request for one feature with statistics matched nothing: the id was filtered outside the query, and DISTINCT ON had already kept only its group's highest id. It now narrows the query to the feature's group instead, which also saves scanning the table. An objectarray item's field is relative to its item, so an unset one was reading a like-named key of the layer - a new rule's Values Are showed the layer's own 'vector'. Values Are now sits below No Data, so choosing categorical reveals the mappings directly beneath it. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Number objectarray items, and mind the groupless feature A feature with no group value was matched with = against a null, so asking for one with statistics returned nothing at all; null groups are one group to PARTITION BY, so the comparison is now null-safe too. The searchdropdown was the one case left reading the enclosing config for an unset objectarray field. Items of an objectarray look alike, so each now wears its number. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.6-20260807 [version bump] * Keep the ramp list with its picker, and unmount a ramp that has gone A portalled list was placed against the viewport once, at opening, so scrolling the settings pane it was opened in left it behind. It now measures again while it is open. A rule that stops wanting a ramp leaves no mount for one, and the stale root was only let go of when a new mount was found. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Style by a group's statistics, and by as many rules at runtime as configured A rule aimed at _.stats.depth_m.avg now asks the geodataset for depth_m's per-group statistics itself, so a feature can be styled by its group rather than only by itself; an admin can name further fields to summarise. The LayersTool panel exposes every rule rather than the first, with rules addable and removable for the session, reports what the layer knows about the numbers it styles by, and resets from an icon beside its heading - as the layer's own settings reset now does too. A rule that widens lines or fades them is drawn in the legend as lines of that weight or swatches at that opacity, rather than being left out. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a rule's number span when it is re-aimed at another style Patching range with undefined erased it, so switching a weight rule to radius left it mapping through nothing and it stopped styling at all. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Style by a group statistic without typing its path A dynamic style rule now says what kind of thing it reads - a feature's own property, or a statistic of its geodataset group - so an admin picks depth_m and avg rather than writing _.stats.depth_m.avg, and a viewer can switch that statistic from the Layers Tool. Paths already written keep working. Also stops a colour picker inside an objectarray item from falling back to a like-named colour on the layer being edited. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep the stats heading from standing in for the composite one Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Three review fixes to the runtime style panel Ramp editors are now cached per layer rather than under a key another layer's name can prefix, a ramp carrying its own colours is drawn and kept rather than shown as viridis, and the Stat dropdown leads with the average it actually defaults to. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Key a rule's fields by what they edit, not where they land Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Key a row's components by where they are declared too Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Measure a group statistic over the groups, not the features Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a view-measured layer coloured when it's off screen Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Read a group statistic filed under a dotted field name Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Ask for a nested statistics field by its whole name Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Show the bin split the map is actually drawing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Colour a group-statistic layer that has no groups measured yet Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.7-20260810 [version bump] * Stop a switched-off dynamic style from asking for statistics Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Report a group's sum and deviation, and style by them Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Name the new statistics, and let a viewer aim a numeric attribute Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Recompute an old geodataset's statistics from Configure A geodataset written before field_stats was kept has no dataset-wide numbers for a dynamic style to stretch over, and an append cannot repair that: it merges only the features it appends. A button per row in Configure's Geodatasets tab now rescans the table in one Postgres pass, flattening nested properties to the same dotted paths and over the same numeric guard ingest used. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Halve a rescan's cost, and show a list field inside a repeatable group The rescan walked every property recursively and evaluated its numeric guard twice; only nesting needs the walk, and jsonb-to-text extraction happens once now. 200k features x 8 properties: 9.6s -> 5.0s, temp spill 174MB -> 63MB. A comma-separated list field belonging to a repeatable item read from the top level of what was being configured, so it always rendered blank and typing overwrote what was stored. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a legend label's exponent, and say what a Stat is measured over A legend shows units once in its title and takes them off the labels, but what counted as units was everything after the digits - so 1.8e10 read as 1.8, smaller than the 1.0 below it. An exponent now belongs to its number. The Stat row gains an info icon: a group statistic describes the group, which is easy to read as describing the feature. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Re-read the geodataset entries after a rescan The entries carry each geodataset's statistics, so the copy in hand described the dataset before the rescan until the page was reloaded. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Colour a view-following layer over the view it is built in The first compile ran before there was a drawn layer to measure what is inside the map bounds, so a layer stretched over the current view was painted with its whole file's spread until the map was nudged. Verified on a framed pair of features: 48.2-76.4 where it used to be 5.1-103.7. A restyle also no longer compiles over an empty feature set, which would drop the styling a layer already has. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Dress the Layers Tool's dynamic style panel The Stat explanation is a real tooltip rather than a native title, as are the stats' 'measured over' notes. Section headings sit at the settings' left edge in sentence case, with the dynamic style's reset beside its name and a labelled Add opposite; rules and stat groups are told apart by the indent of what belongs to them, and a rule between the Stats section spans the panel. Dropdowns, the ramp picker and the bin box all share one width. Also centres the Filters help icon, which its own margin was pushing 6px below the heading it sits in. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Shorten Configure dynamic style rules and keep a restyled globe layer Property fills the rest of its row (3-7-2 with Stat) and Domain widens to half, with three descriptions trimmed, so each rule is shorter. A Cesium restyle no longer removes the globe layer before rebuilding it - the rebuild reloads it in place, so a failure cannot leave the globe without a layer that is still on. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Drop legend style matching, and keep a domain to what its source says Legend entries no longer declare styling at all: the deprecated 'Style Matching' field, the conversion button and its module are gone, since Dynamic Style covers what they did. Also from review: - a feature property named __proto__ accumulated field statistics onto Object.prototype instead of into the statistics - Data Min/Max, which Configure only shows for a literal domain, kept pinning the scale after the domain source was changed - 'Nulls' was measured against a stored feature count that a rescan can now correct Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep dynamic style honest about its domain, and draw draped polygon edges Stats now measure what the chosen domain measures, and switching it rebuilds the readout. A refreshed layer announces its restyle so the legend and the Layers Tool follow a time change. Vector layers can list the properties a viewer may aim a rule at. Cesium refuses outlines on terrain, so a draped polygon's stroke is drawn as a clamped polyline - an outline-only polygon was invisible on the globe. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Refresh only the stats a restyle changed Rebuilding the whole dynamic style block on every restyle destroyed the input being typed into, so a bin count of more than one digit could not be entered. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Only rebuild the settings block when there are stats to put in it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * List selectable properties and stats fields separately Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.8-20260811 [version bump] * Report the domain of every rule, and stats for what each one colours by Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make Whole dataset / Current view one setting the rules follow Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Author dynamic style rules in Configure and switch them on and off at runtime A viewer no longer aims a layer at a whitelisted property: the rules an admin wrote are the vocabulary, each with an On switch they may flip for their session. A rule aimed at a text field with no mappings written for it takes them from the values the layer holds, so a categorical field is styled rather than silently painting nothing. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Draw a repeatable item's checkbox from what was saved, not its default A stored false is falsy, so a dynamic style rule left switched off came back switched on when the layer was reopened. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Stop a feature's style becoming the layer's, and repaint when the last rule goes off A vector layer's style function used layerObj.style as its scratch and left the last feature's values there, so the next feature and every rebuild started from them - a dynamicExtent layer refetching after a zoom drew features the rule doesn't resolve in the last one's colour and opacity. Switching every rule off now repaints instead of returning early, the Configure On field is a switch, and a geodataset without stored statistics says so beside its Domain choice rather than silently scaling over what is loaded. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a bin's range whole in the legend A label like "60 - 80" was split into the number 60 and the units "- 80", so every intermediate bin was labelled with its lower edge alone. A remainder with a digit in it is a second number, not a unit. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Read a marker's shape and animation from the feature's own style The style function no longer leaves the feature's style on the layer, so pointToLayer must take shapeIcon, shapeProp and animation from what it returns or a point carrying its own shape would be drawn with the layer's. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Say a scale is missing dataset-wide numbers only where they could exist A group statistic is of the query by design and a categorical rule spans no range, so neither is short of anything stored - naming them said a recompute would help when it wouldn't. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Recognise a geodataset url however its scheme is written Loading a layer lowercases the scheme, so 'GeoDatasets:x' loads - but its dataset-wide statistics were never fetched and the note that says so was suppressed for exactly those layers. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Bound a group statistic by its field's whole-dataset numbers A group average lies inside the field's own extent and a group spread is at most half of it, so Whole dataset now gives those rules a scale that doesn't move as you pan. A sum has no such bound and is still measured over the groups in hand, which the note now says. Adds a Field Statistics modal to Configure's Geodatasets tab, and pins a domain typed with no source named. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Register the field statistics modal so it opens and closes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Query the globe's own extent at every zoom, and let a fill-less footprint be clicked Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Label a bin boundary with the value it falls at, and keep a rule's style range positional Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Quieter stat rows, a flush rule switch, and a filter property list that catches up with the layer Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make the bin values legible Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give the stop bar room above it and a reset that stays put Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Butt the stop reset against the bar and stop bins at 20 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Requery over whichever engine moved last, not always the map Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Square the rule switch and name the stats' domain in place of a tooltip Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Query the globe's real frustum extent instead of a Leaflet zoom estimate Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Store geodataset properties as jsonb, and gather row actions into a menu New geodataset tables store their properties as jsonb rather than json, so a property read is a lookup instead of reparsing the document - measured at 11.2s -> 4.2s for a 200k-feature /get with stats= over the whole dataset. Existing tables keep json until converted from Configure, since the same operators read either type. Configure's Geodatasets rows end in a menu holding Field Statistics, Recompute Statistics and Convert Properties to jsonb, the last disabled for a geodataset already storing them that way. /entries reports each geodataset's properties_type. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.9-20260812 [version bump] * Make a globe click find its feature when a dynamic style is on A resolved style is not part of a feature's identity, a rule on one style key alone now repaints the globe's points and lines, and a reload no longer leaves a data source detached or hidden. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make a globe click's highlight show on the polygon it picked Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Highlight the feature that was clicked, and spin while a requery is out Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Pick the footprint the click is on, and keep ids of different kinds apart Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * List what a style a viewer can't change is styling by Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fit a listed rule to its row, and let an admin hide the stats Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Match on a kind of id both features carry Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give a globe reload's entities ids of their own Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Record nesting only as deep as a rescan reads it Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep the old features up until the new ones are drawn Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Match the retire call as it is written Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Measure a line as the open path it is Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Refuse a pinned end that crosses the discovered one Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Open the globe onto what the map is showing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Leave the rest of the file as it was Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Free a globe data source the reload replaced Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Hold the row a closing menu is still drawing Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Clear a highlight the reload's entities left behind Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Draw a configured image legend beside a scale Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Give the dynamic style tests room for the ramp controls Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.10-20260813 [version bump] * Validate Missions path on getprofile/getbands/getminmax before GDAL Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.5-20260813 [version bump] * Drop what nothing calls and share what was written twice Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Put the escaper and the legend builder where they belong Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Send the mission-relative raster path to getminmax for image layers Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * File a template geodataset's statistics under the name it was stored as Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Keep a kebab menu opened again during its own close Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Allow operator-allowlisted remote GDAL datasets via GDAL_ALLOWED_REMOTE_PREFIXES Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Pass remote GDAL urls through unaltered and require allowlist entries to name a host Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Trim comments in validateGdalDatasetPath Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve percent signs in Missions paths and match remote prefixes on a boundary Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Clean the empty values inside an objectarray item too Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make Leaflet a managed dependency at 1.9.4 and document vendored plugins - add leaflet@^1.9.4, leaflet-geometryutil, leaflet-polylinedecorator to root deps - import leaflet + leaflet/dist/leaflet.css in src/index.js; window.L unchanged - delete vendored leaflet 1.5.1/1.9.2 engine builds, vendored proj4, geometryutil, polylineDecorator - restore L.DomEvent.fakeStop (removed in 1.9) for leaflet.vectorGrid - keep pre-1.9 popup content font metrics - README.md recording upstream version + local divergence per vendored plugin Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Remove unused vendored Leaflet.ImageOverlay.Rotated Not imported anywhere; drop its attributions entry and README row too. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Make the fakeStop compatibility shim a no-op so map drag still starts on vector tile features Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Update Leaflet and GeometryUtil versions in attributions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Record the local patches that were in the deleted vendored Leaflet copies Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Sync the lockfile version with the merge Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix geodataset temporal bugs: legacy tables, start-only time filters, recreate with new time mappings Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.11-20260820 [version bump] * Fix globe layers and time histogram handling Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.12-20260824 [version bump] * fix(deps): pin svgo override to locked version Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Harden security boundaries and content handling Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve Markdown breaks and restore API rate limit Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Allow development WebSocket origin ports Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Run Playwright tests on supported Node version Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve request size rejection status Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Allow large dataset recreation payloads Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Fix browser security regressions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Preserve layer control element IDs Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * ci: correct checkout pin comments, bump CI Node to 22, harden head_ref use Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.14-20260825 [version bump] * deps: pin stac-fastapi-api/types/extensions to 6.2.1 Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.15-20260825 [version bump] * Add Broadcast plugin: push-based layer refresh via POST /api/broadcast/layerUpdate - New core backend plugin plugins/core/backend/Broadcast mounted at /api/broadcast (ensureAdmin + checkMissionPermission) - Broadcasts notify-only envelope { info: { type: 'refreshLayer', layerName }, body: { mission } } - Client handles refreshLayer by reloading the layer(s) via TimeControl_.reloadLayer without refetching the config - New time.refreshViaBroadcast layer flag skips the setInterval polling for push-managed layers - OpenAPI + docs Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.19-20260902 [version bump] * Drop time.refreshViaBroadcast switch; push refresh applies to any time-enabled layer, polling unchanged Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add Broadcast plugin tests (unit helpers + /api/broadcast/layerUpdate API spec) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Broadcast: resolve display names to UUIDs, refresh non-time layers via Map_.refreshLayer, report broadcasted:false when wss unavailable Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add on-map attribution dropdown gated by look.showAttributionsOnMap Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.19-20260902 [version bump] * Re-query layers on turn-on when a refreshLayer broadcast arrived while they were off Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.20-20260903 [version bump] * Make Broadcast plugin non-overridable Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Move broadcast re-query logic from essence.js into L_.requeryLayers (Layers_/lifecycle/refresh.js) Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Broadcast: validate body before checkMissionPermission Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Extract websocket connect/retry/routing into WebSocketService; restore refreshLayer default export Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Document which layer types layerUpdate refreshes Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.21-20260904 [version bump] * Security fix for geodatasets filter handling * chore: bump version to 5.3.22-20260909 [version bump] * Bound geodatasets in-filter value count/length; extend tests * Adjust probe values in in-filter test * Neutralize test naming for geodatasets in-filter spec Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Add missions_viewing: restrict mission visibility and config loading per user under AUTH=local Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * chore: bump version to 5.3.23-20260910 [version bump] * Landing page: show a message when the user has no viewable missions Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * missions_viewing: SuperAdmin-only updates, follow mission renames, MAIN_MISSION guard, and e2e tests Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * User model: only re-hash password when it actually changed Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> * Enforce missions_viewing on /Missions/<mission>/ static files under AUTH=local Co-Authored-By: tariq.k…
…AMMOS#1017) * Add custom GDAL color table support for COG layers via TiTiler Adds a cogColormapJson layer config field ("Custom Color Table (GDAL)", in its own section of the COG tab) that is passed through to TiTiler's `colormap` query parameter. Accepts either a discrete value-to-RGBA map (e.g. {"1":[10,40,85,255]}) or a list of [[min,max],RGBA] intervals, letting classified rasters (land cover, sea-ice type, etc.) be colored by raw pixel value. Behavior: - The color table supersedes the named Colormap (colormap_name), and the Colormap dropdown disables itself with an explanatory tooltip when both a color table and the COG transform are set (via a new generic `disableWhen` hook in the configure app's Maker) - rescale is suppressed when a color table is set: TiTiler applies rescale before the colormap lookup, which would remap raw values away from the table's keys - A discrete legend (one labeled color chip per class or interval, suffixed with cogUnits) is generated automatically through the LegendTool/populateCogScale path, with or without the COG transform enabled; fully transparent entries (e.g. nodata) are skipped - The layer settings panel renders the color table as a swatch strip in place of the named-colormap preview image * Add Custom Color Table (GDAL) demo layer to Reference-Mission blueprint Adds a "USGS SF DEM — Custom Color Table (GDAL)" tile layer under Raster Layers → TiTiler Tile Layers that showcases the cogColormapJson feature: the SF DEM served as a direct COG via TiTiler and colored by an 8-band interval color table (blue sea level → green → tan → white peaks), with rescale suppressed and a discrete legend generated automatically. Off by default; needs WITH_TITILER only (no STAC). * chore: bump version to 5.4.1-20260916 [version bump] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Tariq Soliman <Tariq.K.Soliman@jpl.nasa.gov>
…lAbsolute Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
|
I'll fix CI failures and address comments from users with write access that start with 'DevinAI' or '@devin'.
Original prompt from tariq.k.soliman
|
✅ Version Already UpdatedThis PR includes a manual version update to No automatic version bump needed. |
…source Globe layer config passed the raw COG source (s3://..., ../../Missions/...) to Cesium/LithoSphere instead of the /titiler/cog/tiles endpoint the 2D map builds. Extract buildCogTileUrl into Tile/cog.js and use it from both, and encode the source as a single url= value (also in the Configure cog/info and Identifier cog/point requests) so sources with their own query strings (?versionId=...&...) survive. Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
This reverts commit 0161c70.
…the COG source" This reverts commit 0c56dee.
🤖 Version Auto-BumpedThe version has been automatically incremented to This commit was added to your PR branch. When you merge this PR, the new version will be included. If you want a different version, update |
…4801-cog-s3-scheme Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com> # Conflicts: # configure/package.json # package.json
There was a problem hiding this comment.
Devin Review found 8 new potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| Userfiles.update( | ||
| { mission: null }, | ||
| { where: { mission: missionName }, transaction: t } | ||
| ), |
There was a problem hiding this comment.
🔴 Deleted mission drawings reappear everywhere
Deleting a mission sets its drawings' mission to null. getfiles includes null-mission drawings by default, so they appear in every remaining mission.
Learn more
Draw files are retained after mission deletion, but clearing their mission changes their meaning. getfiles treats null as a legacy, unscoped file and includes it whenever the mission has not explicitly disabled showNullMissionFiles. A normal deletion therefore broadens the file's display scope to all remaining missions.
Example: A drawing made in mission A has mission: 'A'. Delete A while missions B and C use default Draw settings; that drawing now appears in both B and C.
Recommended fix: Preserve a non-null tombstone mission association for retained files or explicitly archive/delete associated drawings when destroying a mission. Keep null reserved for genuinely legacy unscoped drawings.
Was this helpful? React with 👍 or 👎 to provide feedback.
| onceSynced: async (s) => { | ||
| if (typeof file_histories.up === "function") { | ||
| file_histories.up(); | ||
| } | ||
| if (typeof ufiles.up === "function") { | ||
| ufiles.up(); | ||
| await ufiles.up(); |
There was a problem hiding this comment.
🔴 Draw requests race the mission migration
On upgrade, onceSynced starts the mission-column migration without blocking requests. setups.synced discards its promise, so early Draw requests fail before the column exists.
Learn more
The Draw model now queries mission on normal file reads and writes. setups.synced invokes lifecycle hooks without awaiting their return values, while server startup opens the HTTP listener independently of the Sequelize sync/migration chain. Thus await ufiles.up() only orders statements inside the Draw hook; it does not prevent HTTP requests from seeing the old schema.
Example: Restart an upgraded installation with an existing user_files table lacking mission. A user opens Draw immediately; getfiles queries the new model before ALTER TABLE finishes and receives a database error.
Recommended fix: Await all onceSynced promises and complete migrations before accepting file requests. Ensure migration failures block readiness rather than being silently logged and ignored.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (DrawTool.filesOn.indexOf(parseInt(fileId)) !== -1) | ||
| DrawTool.toggleFile(parseInt(fileId), 'off', true) | ||
| DrawTool.getFiles(function () { | ||
| DrawTool.populateFiles() | ||
| }) |
There was a problem hiding this comment.
🔴 Moved drawing stays selected for edits
When a lead moves an inactive-on-map drawing, currentFileId remains selected after getFiles removes it. New drawings can still target the moved file from the old mission.
Learn more
The move handler clears the on-map file only if its ID occurs in filesOn. The active drawing selection instead lives in currentFileId, and getFiles replaces the visible list without clearing that selection. After a moved file disappears from the list, drawing actions can continue using its ID.
Example: A lead selects file 7 in mission A, toggles its map visibility off, then moves it to mission B. File 7 vanishes from A's list but stays selected as the target of new drawings.
Recommended fix: If the moved file equals currentFileId, clear the active drawing selection regardless of map visibility. Apply this to both save handlers and reload the list after the selection is reset.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (isLead && req.body.mission != null && req.body.mission !== "") { | ||
| let vars = null; | ||
| try { | ||
| vars = await getDrawToolVars(req.body.mission); | ||
| } catch (err) { | ||
| logger("error", "Failed to edit file.", req.originalUrl, req, err); | ||
| } | ||
| if (vars == null) { | ||
| res.send({ | ||
| status: "failure", | ||
| message: "Failed to edit file. Unknown mission.", | ||
| body: {}, | ||
| }); | ||
| return; | ||
| } | ||
| toUpdateTo.mission = req.body.mission; |
There was a problem hiding this comment.
| Userfiles.update( | ||
| { mission: null }, | ||
| { where: { mission: missionName }, transaction: t } | ||
| ), |
There was a problem hiding this comment.
| if (req.body.mission != null) { | ||
| const missionOr = [{ mission: req.body.mission }]; | ||
| if (showNullMissionFiles) missionOr.push({ mission: null }); | ||
| if (showMasterFiles) missionOr.push({ is_master: true }); | ||
| andWhere.push({ [Sequelize.Op.or]: missionOr }); | ||
| } | ||
| if (!showMasterFiles) andWhere.push({ is_master: { [Sequelize.Op.ne]: true } }); |
There was a problem hiding this comment.
| if (req.body.mission != null) { | ||
| let vars = null; | ||
| try { | ||
| vars = await getDrawToolVars(req.body.mission); | ||
| } catch (err) { | ||
| logger("error", "Failed to make a new file.", req.originalUrl, req, err); | ||
| } | ||
| if (vars == null) { | ||
| res.send({ | ||
| status: "failure", | ||
| message: "Failed to make a new file. Unknown mission.", | ||
| body: {}, | ||
| }); | ||
| return; | ||
| } | ||
| } |
| if (this.options.cogColormapJson != null) { | ||
| let colormap = this.options.cogColormapJson | ||
| if (typeof colormap !== 'string') { | ||
| try { | ||
| colormap = JSON.stringify(colormap) | ||
| } catch (e) { | ||
| colormap = null | ||
| } | ||
| } | ||
| if (colormap != null && colormap !== '') { | ||
| url += `${ | ||
| url.indexOf('?') === -1 ? '?' : '&' | ||
| }colormap=${encodeURIComponent(colormap)}` | ||
| } |
There was a problem hiding this comment.
Purpose
A Tile layer with
sourceType: COGand ans3://bucket/key.tifURL did not work; users had to hand-write a fullhttps://.../titiler/cog/tiles/...?url=s3://...URL instead.Root cause:
F_.isUrlAbsolute(and its copy inconfigure/src/core/utils.js) used^(?:[a-z]+:)?//, so any scheme containing a digit (s3:) was not recognized as absolute.L_.getUrlthen prepended the mission path, producing tile requests like:Proposed Changes
isUrlAbsolutenow accepts any RFC 3986 scheme (^(?:[a-z][a-z0-9+.-]*:)?//) in bothFormulae_.jsandconfigure/src/core/utils.js, sos3://,gs://,az://,svn+ssh://etc. count as absolute.tests/unit/formulae.spec.js.(The Cesium TiTiler-wrapping and
url=encoding changes from review were reverted as out of scope for this PR; they are pre-existing behaviours affecting https:// COGs too and can be a follow-up.)Issues
Testing
sourceType: COG,url: s3://my-bucket/path/file.tiftile layer: before the fix tile requests carriedurl=../../Missions/CogTest/s3://...; after they are.../titiler/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.webp?url=s3://my-bucket/path/file.tif.PLAYWRIGHT_TEST_UNIT_ONLY=true npx playwright test tests/unit/formulae.spec.js tests/unit/layerSource.spec.js— all pass.Note for deployers:
TITILER_ALLOWED_URL_PATTERNS(default only allows^https://and^/Missions/) must also permit^s3://...for the proxy to forward s3 URLs to TiTiler.Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/e0febe17a9a44e899720ac13eebc34d2
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/e0febe17a9a44e899720ac13eebc34d2?variant=devin
Requested by: @tariqksoliman