Skip to content

Fix COG tile layers with s3:// URLs being treated as mission-relative - #1099

Merged
tariqksoliman merged 1850 commits into
NASA-AMMOS:developmentfrom
JPL-Devin:devin/1790274801-cog-s3-scheme
Oct 1, 2026
Merged

tariqksoliman merged 1850 commits into
NASA-AMMOS:developmentfrom
JPL-Devin:devin/1790274801-cog-s3-scheme

Conversation

@tariqksoliman

@tariqksoliman tariqksoliman commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

With Devin: JPL-Devin#147

Purpose

A Tile layer with sourceType: COG and an s3://bucket/key.tif URL did not work; users had to hand-write a full https://.../titiler/cog/tiles/...?url=s3://... URL instead.

Root cause: F_.isUrlAbsolute (and its copy in configure/src/core/utils.js) used ^(?:[a-z]+:)?//, so any scheme containing a digit (s3:) was not recognized as absolute. L_.getUrl then prepended the mission path, producing tile requests like:

/titiler/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.webp?url=../../Missions/CogTest/s3://my-bucket/path/file.tif

Proposed Changes

  • [FIX] isUrlAbsolute now accepts any RFC 3986 scheme (^(?:[a-z][a-z0-9+.-]*:)?//) in both Formulae_.js and configure/src/core/utils.js, so s3://, gs://, az://, svn+ssh:// etc. count as absolute.
  • [ADD] Unit tests for the new schemes in 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

  • Reported via chat (COG + s3 URL requiring a manual titiler URL).

Testing

  • Reproduced locally with a sourceType: COG, url: s3://my-bucket/path/file.tif tile layer: before the fix tile requests carried url=../../Missions/CogTest/s3://...; after they are .../titiler/cog/tiles/WebMercatorQuad/{z}/{x}/{y}.webp?url=s3://my-bucket/path/file.tif.
  • Recorded browser verification (see PR comment): S3 / relative / https COG URL generation and regular tile layers.
  • 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.


Devin Review

devin-ai-integration Bot and others added 30 commits August 3, 2026 23:30
Each of the seven built-in attachments now owns its own make() plus the
lifecycle ops its behavior needs; core builds attachments by iterating
LayerAttachmentRegistry in manifest-declared order, deferring the ones
that need their siblings. Pairings owns the peer-feature/az-el
calculation Photosphere used to reach in for, and PathGradient owns its
globe primitive's add/remove.

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…entity

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…dinary 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>
… 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>
`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>
…dation

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>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
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>
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>
…ne 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>
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>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
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>
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>
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>
…d 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>
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>
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>
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>
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>
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>
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>
devin-ai-integration Bot and others added 23 commits September 10, 2026 00:29
…per user under AUTH=local

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…IN_MISSION guard, and e2e tests

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…UTH=local

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…ache per-user lookup, document cross-mission note in Users UI

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…n on mission mutations

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…equests

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…es; plain 403 for non-browser clients

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
Per-user mission viewing permissions (missions_viewing) under AUTH=local
…ons-viewing"

This reverts commit 79a09e4, reversing
changes made to c666775.
Revert "Per-user mission viewing permissions (missions_viewing) under AUTH=local" (#130)
…3049-sync-upstream-development

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

# Conflicts:
#	configure/package.json
#	package.json
…-development

Sync upstream NASA-AMMOS/MMGIS development into development
…lAbsolute

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
…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>
@tariqksoliman tariqksoliman self-assigned this Sep 25, 2026
@tariqksoliman tariqksoliman added the bug Something isn't working label Sep 25, 2026
…4801-cog-s3-scheme

Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>

# Conflicts:
#	configure/package.json
#	package.json

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Devin Review: 1 flag

Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

@tariqksoliman
tariqksoliman merged commit bb19bb8 into NASA-AMMOS:development Oct 1, 2026
6 of 9 checks passed
@github-project-automation github-project-automation Bot moved this to Done in MMGIS Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant