@walkthru-earth/objex-utils 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -32,6 +32,12 @@ declare const VIEWER_DIR_EXTENSIONS: Set<string>;
32
32
  declare const LAYER_HUE_MULTIPLIER = 137;
33
33
  /** Duration (ms) to show "Copied!" feedback before resetting. */
34
34
  declare const COPY_FEEDBACK_MS = 2000;
35
+ /** Region assumed when a connection or bucket name yields none. AWS's global default. */
36
+ declare const DEFAULT_AWS_REGION = "us-east-1";
37
+ /** deck.gl tile-layer debounce (ms) before fetching after a viewport change. */
38
+ declare const TILE_DEBOUNCE_MS = 200;
39
+ /** Zoom level used when flying to the first feature of a vector dataset. */
40
+ declare const FIRST_FEATURE_FLY_ZOOM = 14;
35
41
 
36
42
  /**
37
43
  * Centralized file type detection module.
@@ -39,7 +45,7 @@ declare const COPY_FEEDBACK_MS = 2000;
39
45
  * Single source of truth for extension → icon, color, label, category,
40
46
  * viewer, DuckDB read function, MIME type, and queryability.
41
47
  *
42
- * Used by: FileRow, FileTreeSidebar, ViewerRouter, TableViewer, WasmQueryEngine, etc.
48
+ * Used by: FileTreeSidebar, ViewerRouter, TableViewer, WasmQueryEngine, etc.
43
49
  */
44
50
  type FileCategory = 'data' | 'geo' | 'code' | 'document' | 'config' | 'image' | 'video' | 'audio' | 'archive' | 'database' | '3d' | 'other';
45
51
  type ViewerKind = 'table' | 'image' | 'video' | 'audio' | 'markdown' | 'code' | 'cog' | 'pmtiles' | 'flatgeobuf' | 'pdf' | '3d' | 'archive' | 'database' | 'zarr' | 'copc' | 'notebook' | 'raw';
@@ -149,6 +155,16 @@ interface QueryEngine {
149
155
  crs: string | null;
150
156
  }>;
151
157
  queryCancellable?(connId: string, sql: string): QueryHandle;
158
+ /**
159
+ * Streaming variant of `queryCancellable`. Yields one `QueryResult`-shaped
160
+ * chunk per Arrow RecordBatch instead of materializing the full row set
161
+ * before returning. Peak memory tracks one batch (~64 KiB rows) rather
162
+ * than the full result, which avoids DuckDB-WASM OOMs on large parquet
163
+ * scans. The first chunk carries the schema (`columns` / `types`); later
164
+ * chunks repeat them so consumers don't need to track first-batch state.
165
+ * Cancellation aborts the inner DuckDB cursor.
166
+ */
167
+ queryStream?(connId: string, sql: string, signal?: AbortSignal): AsyncIterable<QueryResult>;
152
168
  queryForMapCancellable?(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): MapQueryHandle;
153
169
  forceCancel?(): Promise<void>;
154
170
  /** Register a file buffer in DuckDB-WASM's virtual filesystem for ATTACH. */
@@ -344,6 +360,431 @@ declare class UrlAdapter implements StorageAdapter {
344
360
  copy(): Promise<WriteResult>;
345
361
  }
346
362
 
363
+ /** A basemap option a host can offer. Consumed in Phase 2. */
364
+ interface BasemapConfig {
365
+ id: string;
366
+ label: string;
367
+ type: 'vector' | 'raster';
368
+ url: string;
369
+ variant?: 'light' | 'dark';
370
+ }
371
+ /** A preloaded connection definition. Never carries secrets. Consumed in Phase 2. */
372
+ interface ConnectionSeed {
373
+ name: string;
374
+ provider: string;
375
+ bucket: string;
376
+ region?: string;
377
+ endpoint?: string;
378
+ anonymous?: boolean;
379
+ authMethod?: 'sigv4' | 'sas-token';
380
+ rootPrefix?: string;
381
+ }
382
+ interface AppConfigDefaults {
383
+ theme: Theme;
384
+ locale: string;
385
+ featureLimit: number;
386
+ mosaicItemLimit: number;
387
+ }
388
+ interface AppConfigUi {
389
+ showConnectionRail: boolean;
390
+ showFileTree: boolean;
391
+ showSettings: boolean;
392
+ }
393
+ interface AppConfig {
394
+ defaults: AppConfigDefaults;
395
+ ui: AppConfigUi;
396
+ basemaps: BasemapConfig[];
397
+ defaultBasemap: {
398
+ light?: string;
399
+ dark?: string;
400
+ };
401
+ connections: ConnectionSeed[];
402
+ }
403
+ /** Hardcoded fallback. Matches current app behaviour when no config is present. */
404
+ declare const DEFAULT_APP_CONFIG: AppConfig;
405
+ declare function coerceTheme(v: unknown): Theme | undefined;
406
+ declare function coerceString(v: unknown): string | undefined;
407
+ declare function coercePositiveInt(v: unknown): number | undefined;
408
+ declare function coerceBool(v: unknown): boolean | undefined;
409
+ /** First defined (non-null, non-undefined) candidate wins. Encodes the precedence chain. */
410
+ declare function resolveSetting<T>(...candidates: (T | null | undefined)[]): T | undefined;
411
+ /** Maps the ?sidebar / ?tree visibility param to a boolean, or undefined when absent or invalid. */
412
+ declare function parseVisibilityParam(value: string | null): boolean | undefined;
413
+ /**
414
+ * Pick the basemap a map should render. Precedence: explicit user pick (when it
415
+ * still exists in the configured list) > the configured default for the theme
416
+ * variant > the first basemap matching the variant > the first basemap of any
417
+ * variant. Returns undefined when no basemaps are configured, signalling the
418
+ * caller to fall back to its hardcoded default.
419
+ */
420
+ declare function resolveBasemap(config: AppConfig, variant: 'light' | 'dark', userId: string | undefined): BasemapConfig | undefined;
421
+ /**
422
+ * Merge an untrusted JSON value over a base config, field by field.
423
+ * Unknown fields are ignored. Malformed values fall back to the base.
424
+ * Never reads secrets.
425
+ */
426
+ declare function mergeAppConfig(base: AppConfig, override: unknown): AppConfig;
427
+
428
+ /**
429
+ * STAC (SpatioTemporal Asset Catalog) detection and parsing.
430
+ *
431
+ * Pure TypeScript helpers shared by ViewerRouter, StacMosaicViewer, and
432
+ * MultiCogViewer. No Svelte dependency, publishable via objex-utils.
433
+ */
434
+ /** STAC Link (shared by Catalog/Collection/Item). */
435
+ interface StacLink {
436
+ rel: string;
437
+ href: string;
438
+ type?: string;
439
+ title?: string;
440
+ }
441
+ /** STAC Item (GeoJSON Feature shape with stac_version). */
442
+ interface StacItem {
443
+ type: 'Feature';
444
+ stac_version: string;
445
+ id: string;
446
+ bbox?: [number, number, number, number];
447
+ geometry?: unknown;
448
+ properties?: Record<string, unknown>;
449
+ assets?: Record<string, StacAsset>;
450
+ collection?: string;
451
+ links?: StacLink[];
452
+ }
453
+ /** STAC FeatureCollection (collection of Items). Also used for STAC API responses. */
454
+ interface StacFeatureCollection {
455
+ type: 'FeatureCollection';
456
+ stac_version?: string;
457
+ features: StacItem[];
458
+ links?: StacLink[];
459
+ }
460
+ /** STAC Collection (a grouping of Items with its own metadata and links). */
461
+ interface StacCollection {
462
+ type: 'Collection';
463
+ stac_version: string;
464
+ id: string;
465
+ description?: string;
466
+ extent?: {
467
+ spatial?: {
468
+ bbox?: number[][];
469
+ };
470
+ temporal?: unknown;
471
+ };
472
+ links: StacLink[];
473
+ }
474
+ /** STAC Catalog (a directory-like grouping of Catalogs/Collections/Items via links). */
475
+ interface StacCatalog {
476
+ type: 'Catalog';
477
+ stac_version: string;
478
+ id: string;
479
+ description?: string;
480
+ links: StacLink[];
481
+ }
482
+ /** Single asset entry within a STAC item. */
483
+ interface StacAsset {
484
+ href: string;
485
+ type?: string;
486
+ title?: string;
487
+ roles?: string[];
488
+ /** Set when the asset carries `eo:bands`. */
489
+ 'eo:bands'?: {
490
+ name?: string;
491
+ common_name?: string;
492
+ }[];
493
+ }
494
+ /** Sentinel-2 band slot identifier, shared with utils/cog.ts composites. */
495
+ type BandSlot = 'red' | 'green' | 'blue' | 'nir' | 'swir1' | 'swir2' | 'rededge';
496
+ /** Parsed band map: slot name → HTTPS asset URL. */
497
+ type BandMap = Partial<Record<BandSlot, string>>;
498
+ /** Asset keys providers use for the single "display COG" asset, in priority order. */
499
+ declare const STAC_COG_ASSET_KEYS: readonly ["visual", "image", "data", "rendered_preview"];
500
+ /** Shape-check a parsed JSON object as a STAC Item. */
501
+ declare function isStacItem(json: unknown): json is StacItem;
502
+ /** Shape-check a parsed JSON object as a STAC FeatureCollection. */
503
+ declare function isStacFeatureCollection(json: unknown): json is StacFeatureCollection;
504
+ /** STAC Collection detection: `type === 'Collection'` + stac_version + links array. */
505
+ declare function isStacCollection(json: unknown): json is StacCollection;
506
+ /** STAC Catalog detection: `type === 'Catalog'` + stac_version + links array. */
507
+ declare function isStacCatalog(json: unknown): json is StacCatalog;
508
+ /** Routing verdict for any STAC-shaped JSON payload. */
509
+ type StacRoutableKind = {
510
+ kind: 'item';
511
+ item: StacItem;
512
+ } | {
513
+ kind: 'item-collection';
514
+ fc: StacFeatureCollection;
515
+ } | {
516
+ kind: 'collection';
517
+ payload: StacCollection;
518
+ } | {
519
+ kind: 'catalog';
520
+ payload: StacCatalog;
521
+ } | {
522
+ kind: 'none';
523
+ };
524
+ /** Classify an arbitrary parsed JSON into one of the STAC routing buckets. */
525
+ declare function classifyStac(json: unknown): StacRoutableKind;
526
+ /**
527
+ * Pick the COG-ish asset href from a STAC Item. Returns the href of the named
528
+ * asset when `preferred` is given and present, else scans STAC_COG_ASSET_KEYS,
529
+ * else falls back to any asset whose `type` contains "tiff". Returns null when
530
+ * nothing matches.
531
+ */
532
+ declare function pickCogAssetHref(item: StacItem, preferred?: string): string | null;
533
+ /** True when a single STAC Item exposes a COG-ish asset and a bbox. */
534
+ declare function detectMosaicCapable(item: StacItem): boolean;
535
+ /** True when a single STAC Item exposes ≥3 single-band raster COG assets,
536
+ * whether tagged with S2-style common names or not. The MultiCogViewer's
537
+ * band picker fills in the gap when presets don't auto-resolve. */
538
+ declare function detectMultiCogCapable(item: StacItem): boolean;
539
+ /** WGS84 bbox helper. Returns `null` if no bbox can be derived. */
540
+ declare function stacItemBbox(item: StacItem): [number, number, number, number] | null;
541
+ /** Normalized mosaic source entry consumed by MosaicLayer. */
542
+ interface MosaicSourceMeta {
543
+ id: string;
544
+ bbox: [number, number, number, number];
545
+ href: string;
546
+ }
547
+ /**
548
+ * Normalize a STAC Item or a plain `{id?, bbox, href}` record into a
549
+ * MosaicSourceMeta. Returns null when essentials (bbox / href) are missing.
550
+ */
551
+ declare function buildMosaicSourceMeta(input: StacItem | {
552
+ id?: string;
553
+ bbox: [number, number, number, number] | number[];
554
+ href: string;
555
+ }, assetKey?: string): MosaicSourceMeta | null;
556
+ /**
557
+ * Stable spatial cell key for an item, used to dedupe revisits when the
558
+ * caller wants only the freshest scene per footprint. STAC providers
559
+ * default to descending-datetime sort, so the first item per key is the
560
+ * newest. Prefers STAC `grid:code`, then Sentinel-2 MGRS triplet, then
561
+ * `s2:mgrs_tile`, then a rounded bbox so non-S2 providers still dedupe.
562
+ */
563
+ declare function spatialCellKey(item: StacItem, bbox: [number, number, number, number]): string;
564
+ /**
565
+ * Map Sentinel-2 STAC item assets to a BandMap. Recognizes:
566
+ * - `eo:bands[0].common_name` (preferred, stable across providers)
567
+ * - asset key heuristics for Microsoft PC / Element 84 / AWS S2 L2A buckets
568
+ * Returns an empty map when no bands are identifiable so callers can fall
569
+ * back to a different viewer.
570
+ */
571
+ declare function extractSentinelBandAssets(item: StacItem): BandMap;
572
+ /** True when the band map has enough channels for a True Color composite. */
573
+ declare function hasRgbBands(map: BandMap): boolean;
574
+ /**
575
+ * Generic raster band asset description, vendor-neutral. Carries the bits a
576
+ * MultiCOG band picker needs to populate dropdowns and resolve presets.
577
+ *
578
+ * `key` is the STAC asset key as it appears in `item.assets` (`red`, `B04`,
579
+ * `image`, `analytic`, ...). It is the natural identifier and lines up 1:1
580
+ * with `MultiCOGLayer.sources`'s record key.
581
+ */
582
+ interface RasterBandAsset {
583
+ key: string;
584
+ href: string;
585
+ commonName?: string;
586
+ bandCount?: number;
587
+ roles?: string[];
588
+ mediaType?: string;
589
+ title?: string;
590
+ }
591
+ /**
592
+ * Enumerate every asset on a STAC Item that looks like a single-band raster
593
+ * COG suitable for compositing in MultiCOGLayer. Used by the band picker UI.
594
+ *
595
+ * Inclusion: `media_type` matches `image/tiff*` (drops JP2 mirrors, PNGs,
596
+ * thumbnails) AND it is not a thumbnail / overview / metadata role. Pre-baked
597
+ * multi-band visuals (`raster:bands.length > 1` or `eo:bands.length > 1`) are
598
+ * dropped because compositing needs single-band sources, not a 3-band visual
599
+ * COG; that asset belongs in `CogViewer`.
600
+ *
601
+ * `bandCount` is reported as `1` when neither tag is present and the asset
602
+ * media type is COG-like, so vendor catalogs that omit `eo:bands` are still
603
+ * pickable. Callers wanting to be strict can filter on `bandCount === 1`.
604
+ */
605
+ declare function extractRasterBandAssets(item: StacItem): RasterBandAsset[];
606
+ /**
607
+ * Resolve a semantic band slot to an asset key on this Item.
608
+ *
609
+ * Priority:
610
+ * 1. First asset whose `eo:bands[0].common_name` (lowercased) equals the slot.
611
+ * 2. First asset whose key appears in `BAND_KEY_FALLBACKS[slot]` (vendor key
612
+ * conventions: `B04`, `red-jp2`, etc.).
613
+ *
614
+ * Returns the asset key (NOT the href) so callers can plumb it into
615
+ * `composite: { r, g, b }` and look it up in `extractRasterBandAssets()`.
616
+ */
617
+ declare function resolveBandSlotAssetKey(assets: RasterBandAsset[], slot: BandSlot): string | undefined;
618
+ /**
619
+ * Resolve a preset's R/G/B BandSlot triple into asset keys for this Item.
620
+ * Returns null when any of the three required slots cannot be resolved, so
621
+ * the caller can disable the preset rather than half-apply it.
622
+ */
623
+ declare function resolvePresetComposite(assets: RasterBandAsset[], composite: {
624
+ r: BandSlot;
625
+ g: BandSlot;
626
+ b: BandSlot;
627
+ }): {
628
+ r: string;
629
+ g: string;
630
+ b: string;
631
+ } | null;
632
+ /** True when ≥3 single-band raster COG assets exist, so manual RGB pick is viable. */
633
+ declare function hasCompositableBands(assets: RasterBandAsset[]): boolean;
634
+ /**
635
+ * Same shape as `extractRasterBandAssets`, but does NOT drop multi-band assets.
636
+ * Used by the mosaic asset picker, where a 3-band pre-baked `visual` TCI is a
637
+ * legitimate choice alongside the per-band single-band COGs.
638
+ *
639
+ * `bandCount` is reported when known (`raster:bands.length` or `eo:bands.length`),
640
+ * else left undefined so the consuming UI can fall back to probing the COG.
641
+ */
642
+ declare function extractMosaicAssets(item: StacItem): RasterBandAsset[];
643
+
644
+ /**
645
+ * Generic per-channel asset descriptor for the unified RGB picker.
646
+ *
647
+ * Pure TypeScript. No Svelte dependency. Publishable via objex-utils.
648
+ *
649
+ * `CogAsset` is the canonical shape every viewer (CogViewer, MultiCogViewer,
650
+ * StacMosaicViewer) hands to the shared ChannelPicker UI. Each entry records
651
+ * the STAC asset key (`red`, `B04`, `image`, `visual`, ... or `self` when the
652
+ * viewer is a single bare COG file with no STAC context), the href, the band
653
+ * count (from `raster:bands.length` when STAC populates it, lazily probed from
654
+ * the COG header otherwise), and the optional `eo:bands` common name.
655
+ */
656
+
657
+ interface CogAsset {
658
+ /** STAC asset key, or `self` for a single bare COG without STAC context. */
659
+ key: string;
660
+ /** Absolute or relative href as it appears in the STAC item / URL. */
661
+ href: string;
662
+ /** Number of bands in the asset. 1 by default until probed. */
663
+ bandCount: number;
664
+ /** True when bandCount came from STAC metadata or a probe; false → trust default. */
665
+ bandCountKnown: boolean;
666
+ /** `raster:bands[0].data_type` if known. */
667
+ dtype?: string;
668
+ /** `eo:bands[].common_name` lowercased, aligned to bandIndex order. */
669
+ eoCommon: string[];
670
+ /** STAC asset roles (`data`, `visual`, `reflectance`, ...). */
671
+ roles: string[];
672
+ /** Optional human title. */
673
+ title?: string;
674
+ /** Asset media_type as advertised by STAC. */
675
+ mediaType?: string;
676
+ }
677
+ /** Per-channel pixel coordinate inside a STAC item. */
678
+ interface ChannelRef {
679
+ assetKey: string;
680
+ bandIndex: number;
681
+ }
682
+ /** RGB(A) composite expressed as `(asset, bandIndex)` per channel. */
683
+ interface ChannelComposite {
684
+ r: ChannelRef;
685
+ g: ChannelRef;
686
+ b: ChannelRef;
687
+ a?: ChannelRef;
688
+ }
689
+ /**
690
+ * Enumerate every TIFF/COG asset on a STAC Item, keeping multi-band assets
691
+ * (NAIP `image`, S2 `visual` TCI) alongside single-band per-band assets.
692
+ *
693
+ * Reads band metadata from (in priority order):
694
+ * 1. `asset.bands` (STAC 1.1 unified bands array)
695
+ * 2. `asset['raster:bands']` (STAC 1.0 raster extension)
696
+ * 3. `asset['eo:bands']` (STAC 1.0 eo extension)
697
+ * 4. `item.properties.bands` (STAC 1.1 item-level bands, applies to all
698
+ * assets that do not override) — covers catalogs like the Hamilton
699
+ * NAIP-style 3-inch where each item has 4 bands but the single `data`
700
+ * asset carries no band metadata of its own.
701
+ *
702
+ * `bandCount` is set when any of the above provides one; otherwise defaults
703
+ * to 1 with `bandCountKnown: false` so callers can lazily probe on first pick.
704
+ */
705
+ declare function extractCogAssets(item: StacItem): CogAsset[];
706
+ /**
707
+ * For `CogViewer` (single bare COG file, no STAC context). Returns one synthetic
708
+ * asset with key `self` so the same ChannelPicker UI works without special-casing.
709
+ * `bandCount` defaults to 1, set to the probed `geotiff.count` once known.
710
+ */
711
+ declare function syntheticSelfAsset(href: string, probedBandCount?: number): CogAsset;
712
+ /**
713
+ * Pick the most natural and most performant default composite for an item.
714
+ *
715
+ * Priority (first match wins):
716
+ * 1. A 3-band uint8 pre-baked visual asset (`visual` / `image` / `tci` etc):
717
+ * all three channels bind to it, bands 0/1/2. Single COGLayer path,
718
+ * one decoder, fastest.
719
+ * 2. Common-name red/green/blue resolvable across separate single-band assets.
720
+ * MultiCOGLayer path.
721
+ * 3. Fallback: first three raster assets, band 0 each.
722
+ *
723
+ * Returns null when no raster assets exist.
724
+ */
725
+ declare function pickNaturalColorComposite(assets: CogAsset[]): {
726
+ composite: ChannelComposite;
727
+ source: 'visual-asset' | 'rgb-bands' | 'fallback';
728
+ } | null;
729
+ /** True when all three RGB channels target the same asset key. */
730
+ declare function isSingleAssetComposite(c: ChannelComposite): boolean;
731
+ /** True when all three channels are at band index 0 (the MultiCOGLayer-compatible case). */
732
+ declare function allChannelsBand0(c: ChannelComposite): boolean;
733
+
734
+ /**
735
+ * Preset definitions, URL round-trip, and preset application for the unified
736
+ * RGB picker. Pure TypeScript, publishable via objex-utils.
737
+ *
738
+ * Presets describe a SEMANTIC band slot triple (`red`/`green`/`blue` for
739
+ * Natural Color, `nir`/`red`/`green` for False-Color IR, ...). Resolving a
740
+ * preset against a specific item walks `BAND_KEY_FALLBACKS` in `utils/stac.ts`
741
+ * to map slots to actual asset keys on that item. NDVI and other single-band
742
+ * derived presets are intentionally NOT in this list for this slice.
743
+ */
744
+
745
+ interface PresetDef {
746
+ id: string;
747
+ labelKey: string;
748
+ slots: {
749
+ r: BandSlot;
750
+ g: BandSlot;
751
+ b: BandSlot;
752
+ };
753
+ }
754
+ declare const PRESETS: PresetDef[];
755
+ /** Subset of PRESETS whose slot triple resolves on this item. */
756
+ declare function availablePresets(assets: CogAsset[]): PresetDef[];
757
+ /** Resolve a preset to a ChannelComposite for this item. Returns null when not applicable. */
758
+ declare function applyPreset(assets: CogAsset[], preset: PresetDef): ChannelComposite | null;
759
+ /** True when the preset's resolved composite still matches the user's current picks. */
760
+ declare function presetMatchesComposite(preset: PresetDef, c: ChannelComposite, assets: CogAsset[]): boolean;
761
+ /**
762
+ * Decode a `URLSearchParams` chunk into a ChannelComposite.
763
+ *
764
+ * Format: `r=<asset>&g=<asset>&b=<asset>&band_r=<n>&band_g=<n>&band_b=<n>` plus
765
+ * optional `a=<asset>&band_a=<n>`. `band_*` defaults to 0 when absent so
766
+ * legacy MultiCog URLs (`?r=red&g=green&b=blue&preset=true-color`) keep
767
+ * round-tripping. Returns null when any required asset key is missing from
768
+ * the current item's asset list.
769
+ */
770
+ declare function compositeFromUrl(params: URLSearchParams, assets: CogAsset[]): ChannelComposite | null;
771
+ /** Encode a composite + active preset id into URLSearchParams for the hash. */
772
+ declare function compositeToUrl(c: ChannelComposite, presetId: string | null): URLSearchParams;
773
+
774
+ /**
775
+ * Copy text to clipboard and run a feedback callback for COPY_FEEDBACK_MS.
776
+ * Silently catches clipboard errors (e.g. insecure context).
777
+ *
778
+ * @returns true if copy succeeded, false otherwise.
779
+ */
780
+ declare function copyToClipboard(text: string, onFeedback?: (copied: boolean) => void): Promise<boolean>;
781
+ /**
782
+ * Wire click-to-copy on all elements matching `selector` inside `root`.
783
+ * Each element must have `data-code` (URI-encoded) with the text to copy.
784
+ * Adds/removes a `copied` CSS class for visual feedback.
785
+ */
786
+ declare function wireCodeCopyButtons(root: Element, selector: string): void;
787
+
347
788
  /**
348
789
  * Cloud storage protocol URL utilities — pure TS, no Svelte dependency.
349
790
  *
@@ -404,9 +845,75 @@ declare function typeColor(category: TypeCategory): string;
404
845
  declare function typeBadgeClass(category: TypeCategory): string;
405
846
  declare function typeLabel(category: TypeCategory): string;
406
847
 
848
+ /**
849
+ * Canonical connection identity.
850
+ *
851
+ * Single source of truth for deciding when two connection configs point at
852
+ * the same bucket. Used by the connections store to deduplicate auto-detect,
853
+ * manual add, and edit flows, so one physical bucket never ends up with
854
+ * multiple competing local records.
855
+ *
856
+ * Identity rules, per provider:
857
+ *
858
+ * azure → (provider, endpoint, bucket) endpoint carries the account
859
+ * gcs → (provider, bucket) GCS bucket names are global
860
+ * s3 → (provider, bucket, region) AWS native: same bucket name
861
+ * can exist in exactly one region,
862
+ * but the region is load-bearing
863
+ * for signing, so a paste with a
864
+ * different region is a distinct
865
+ * connection until the user merges
866
+ * other → (provider, endpoint, bucket) r2, b2, minio, wasabi, storj,
867
+ * digitalocean, contabo, hetzner,
868
+ * linode, ovhcloud, custom
869
+ *
870
+ * Endpoint normalization is aggressive: scheme + host + non-default port +
871
+ * pathname, with trailing slashes and default ports stripped, host lowercased.
872
+ * That collapses the common trip hazards — http vs https, :443 vs empty,
873
+ * trailing slash drift, mixed case host.
874
+ */
875
+
876
+ interface ConnectionIdentityInput {
877
+ provider: string;
878
+ endpoint: string;
879
+ bucket: string;
880
+ region: string;
881
+ }
882
+ /**
883
+ * Normalize an endpoint URL to a canonical form suitable for equality checks.
884
+ * Empty / whitespace-only input returns `''` (the "no endpoint" sentinel).
885
+ * Non-URL strings are lowercased and stripped of trailing slashes as a best
886
+ * effort so the comparison is still deterministic.
887
+ */
888
+ declare function normalizeEndpoint(raw: string | undefined | null): string;
889
+ /** Collapse unknown / empty providers to `'s3'`; otherwise lowercase. */
890
+ declare function normalizeProvider(provider: string | undefined | null): ProviderId;
891
+ /**
892
+ * Produce a canonical key for a connection's identity. Two connection
893
+ * configs with the same identity key point at the same physical bucket.
894
+ * Returns `''` when the config is too incomplete to identify a bucket.
895
+ */
896
+ declare function connectionIdentityKey(input: ConnectionIdentityInput): string;
897
+ /** Convenience: true when both inputs share the same non-empty identity. */
898
+ declare function isSameConnectionIdentity(a: ConnectionIdentityInput, b: ConnectionIdentityInput): boolean;
899
+
900
+ /**
901
+ * True when the given CRS is WGS84 lon/lat (no ST_Transform needed).
902
+ * Accepts a numeric EPSG code or a string like "EPSG:4326" / "OGC:CRS84".
903
+ */
904
+ declare function isWgs84(crs: number | string | null | undefined): boolean;
905
+
407
906
  /**
408
907
  * Shared error handling for async viewer load operations.
409
908
  */
909
+ /**
910
+ * True for any abort cascade. Recognizes raw `DOMException(AbortError)`,
911
+ * objects whose `.name` is `AbortError`, deck.gl's `_SourceError("Failed
912
+ * to fetch")` wrapper whose `cause` is an AbortError, and free-text errors
913
+ * from `@developmentseed/geotiff` that mention "aborted". Used to silence
914
+ * cancellation noise without swallowing real failures.
915
+ */
916
+ declare function isAbortError(err: unknown): boolean;
410
917
  /**
411
918
  * Extract an error message from an unknown caught value.
412
919
  * Returns null for AbortError (caller should silently return).
@@ -515,6 +1022,59 @@ declare function buildGeoArrowTables(wkbArrays: Uint8Array[], attributes: Map<st
515
1022
  type: string;
516
1023
  }>, knownGeomType?: GeoArrowGeomType): GeoArrowResult[];
517
1024
 
1025
+ /**
1026
+ * Helpers for parsing DuckDB v1.5 parameterized GEOMETRY type strings.
1027
+ *
1028
+ * DuckDB v1.5 made GEOMETRY a core type with an optional CRS parameter:
1029
+ * GEOMETRY — no CRS attached
1030
+ * GEOMETRY('EPSG:4326') — EPSG form
1031
+ * GEOMETRY('OGC:CRS84') — OGC form (canonical for GeoParquet 1.1+)
1032
+ * GEOMETRY('EPSG:27700') — projected CRS
1033
+ *
1034
+ * Type strings may come from `DESCRIBE`, from the Arrow schema, or from a
1035
+ * legacy code path that still reports `BLOB`. Use these helpers everywhere
1036
+ * instead of ad-hoc regex so behaviour stays consistent.
1037
+ */
1038
+ interface GeometryTypeInfo {
1039
+ /** True if the type is some form of GEOMETRY (with or without CRS). */
1040
+ isGeometry: boolean;
1041
+ /** True if the type carries a CRS parameter, e.g. GEOMETRY('EPSG:4326'). */
1042
+ hasCrs: boolean;
1043
+ /** The CRS string if present, otherwise null. Raw value, including WGS84. */
1044
+ rawCrs: string | null;
1045
+ /**
1046
+ * The CRS string if present and NOT a WGS84 variant (EPSG:4326, EPSG:4979,
1047
+ * OGC:CRS84). Returns null for WGS84 so callers can skip ST_Transform.
1048
+ */
1049
+ nonWgs84Crs: string | null;
1050
+ }
1051
+ /**
1052
+ * Parse a DuckDB type string and report whether it is a GEOMETRY type, and
1053
+ * whether a CRS parameter is attached.
1054
+ */
1055
+ declare function parseGeometryTypeCrs(typeStr: string | null | undefined): GeometryTypeInfo;
1056
+ /** True for EPSG:4326, EPSG:4979, OGC:CRS84 and equivalent strings. */
1057
+ declare function isWgs84Crs(crs: string | null | undefined): boolean;
1058
+ /**
1059
+ * Build a `ST_Transform(...)` SQL expression choosing the 2-arg form when the
1060
+ * input already carries its CRS in the GEOMETRY type (DuckDB v1.5), and the
1061
+ * 3-arg form otherwise.
1062
+ *
1063
+ * `geometry_always_xy` is set globally at DB init, so no per-call `always_xy`
1064
+ * argument is needed.
1065
+ */
1066
+ declare function buildTransformExpr(innerExpr: string, sourceType: string, sourceCrs: string, targetCrs: string): string;
1067
+ /**
1068
+ * Wrap a bare WKB expression with `ST_SetCRS(ST_GeomFromWKB(...))` so that the
1069
+ * resulting GEOMETRY value carries a CRS through the rest of the pipeline.
1070
+ * Used in the legacy GeoParquet fallback where we read the geometry column as
1071
+ * BLOB but still know the source CRS from hyparquet metadata or the GeoParquet
1072
+ * footer.
1073
+ *
1074
+ * If `sourceCrs` is null/empty, returns a plain `ST_GeomFromWKB(...)`.
1075
+ */
1076
+ declare function wrapWkbWithCrs(wkbExpr: string, sourceCrs: string | null | undefined): string;
1077
+
518
1078
  interface HexRow {
519
1079
  offset: string;
520
1080
  hex: string[];
@@ -526,6 +1086,142 @@ interface HexRow {
526
1086
  */
527
1087
  declare function generateHexDump(data: Uint8Array, bytesPerRow?: number): HexRow[];
528
1088
 
1089
+ /**
1090
+ * Universal cloud storage URL / bucket parser.
1091
+ *
1092
+ * Accepts the many URI/URL formats that users commonly paste and extracts
1093
+ * the correct bucket, region, endpoint, and provider.
1094
+ *
1095
+ * Supported URI schemes:
1096
+ * s3:// s3a:// s3n:// aws:// — Amazon S3 / S3-compatible
1097
+ * r2:// — Cloudflare R2
1098
+ * gs:// gcs:// — Google Cloud Storage
1099
+ * azure:// az:// — Azure Blob Storage
1100
+ * abfs:// abfss:// — Azure Data Lake (ADLS Gen2)
1101
+ * wasbs:// — Azure Blob (Hadoop WASB driver)
1102
+ * swift:// — OpenStack Swift
1103
+ * file:// filesystem:// — Local filesystem
1104
+ *
1105
+ * Supported HTTPS URL patterns:
1106
+ * https://<bucket>.s3.<region>.amazonaws.com[/prefix] — AWS virtual-hosted
1107
+ * https://s3.<region>.amazonaws.com/<bucket>[/prefix] — AWS path-style
1108
+ * https://s3.amazonaws.com/<bucket> — AWS global
1109
+ * https://<account>.r2.cloudflarestorage.com/<bucket> — Cloudflare R2
1110
+ * https://storage.googleapis.com/<bucket> — Google Cloud Storage
1111
+ * https://<bucket>.storage.googleapis.com[/prefix] — GCS virtual-hosted
1112
+ * https://<bucket>.<region>.digitaloceanspaces.com — DigitalOcean Spaces
1113
+ * https://<region>.digitaloceanspaces.com/<bucket> — DO Spaces path-style
1114
+ * https://s3.<region>.wasabisys.com/<bucket> — Wasabi
1115
+ * https://f<id>.backblazeb2.com/file/<bucket> — Backblaze B2
1116
+ * https://<bucket>.s3.<region>.backblazeb2.com — B2 S3-compatible
1117
+ * https://<bucket>.oss-<region>.aliyuncs.com — Alibaba Cloud OSS
1118
+ * https://<bucket>.cos.<region>.myqcloud.com — Tencent COS
1119
+ * https://storage.yandexcloud.net/<bucket> — Yandex Cloud
1120
+ * https://gateway.storjshare.io/<bucket> — Storj S3 gateway
1121
+ * https://link.storjshare.io/raw/<access>/<bucket> — Storj linksharing
1122
+ * https://<custom-endpoint>/<bucket> — Generic S3-compatible
1123
+ *
1124
+ * Also handles plain bucket names (no protocol).
1125
+ */
1126
+ type StorageProvider = string;
1127
+ interface ParsedStorageUrl {
1128
+ bucket: string;
1129
+ region: string;
1130
+ endpoint: string;
1131
+ provider: StorageProvider;
1132
+ /** Original prefix/path after bucket, if any */
1133
+ prefix: string;
1134
+ }
1135
+ /** STAC API path test, one source of truth. Tests pathname only. */
1136
+ declare const STAC_API_PATH_RE: RegExp;
1137
+ /**
1138
+ * Returns true when the host matches any of the provider host patterns
1139
+ * that `parseStorageUrl` recognizes on the HTTPS branch.
1140
+ */
1141
+ declare function isKnownBucketHost(host: string): boolean;
1142
+ interface Defaults {
1143
+ region?: string;
1144
+ endpoint?: string;
1145
+ provider?: StorageProvider;
1146
+ }
1147
+ /**
1148
+ * Parse a user-provided bucket/URL string into structured storage connection parts.
1149
+ */
1150
+ declare function parseStorageUrl(input: string, defaults?: Defaults): ParsedStorageUrl;
1151
+ /**
1152
+ * Returns true if the input looks like a URL/URI rather than a plain bucket name.
1153
+ * Covers all recognized cloud storage URI schemes.
1154
+ */
1155
+ declare function looksLikeUrl(input: string): boolean;
1156
+ /**
1157
+ * Given a parsed URL result, build a human-readable summary of what was detected.
1158
+ */
1159
+ declare function describeParseResult(parsed: ParsedStorageUrl): string;
1160
+ type UrlClassification = {
1161
+ kind: 'scheme';
1162
+ parsed: ParsedStorageUrl;
1163
+ } | {
1164
+ kind: 'object-storage';
1165
+ parsed: ParsedStorageUrl;
1166
+ } | {
1167
+ kind: 'stac-api';
1168
+ url: URL;
1169
+ } | {
1170
+ kind: 'remote-file';
1171
+ url: URL;
1172
+ };
1173
+ /**
1174
+ * Classify a user-supplied URL/URI into one of four buckets. Unparseable or
1175
+ * plain inputs fall through to `remote-file` with a best-effort URL parse,
1176
+ * returning a synthetic `https://` URL when `new URL()` would throw.
1177
+ */
1178
+ declare function classifyUrl(input: string): UrlClassification;
1179
+
1180
+ /**
1181
+ * Auto-detect hosting bucket from URL search params and window.location.
1182
+ *
1183
+ * Detection priority:
1184
+ * 1. `?url=<storage-url>` query parameter (highest priority)
1185
+ * 2. `window.location.hostname` pattern matching (fallback)
1186
+ *
1187
+ * Also extracts `rootPrefix` when the app is hosted inside a subfolder.
1188
+ */
1189
+
1190
+ interface DetectedHost {
1191
+ provider: StorageProvider;
1192
+ bucket: string;
1193
+ region: string;
1194
+ endpoint: string;
1195
+ rootPrefix: string;
1196
+ bucketUrl: string;
1197
+ }
1198
+ /**
1199
+ * Detect hosting bucket from current URL.
1200
+ * Returns null when no hosting bucket can be determined.
1201
+ */
1202
+ declare function detectHostBucket(): DetectedHost | null;
1203
+ /**
1204
+ * Enrich an in-progress connection-config draft with hints from a STAC Item
1205
+ * that declares the Storage Extension (`storage:region`,
1206
+ * `storage:requester_pays`, `storage:platform`, v2 `storage:schemes`).
1207
+ *
1208
+ * Modular by design, callers opt in. The existing `detectHostBucket` flow
1209
+ * does NOT know about STAC, so call sites that already hold a representative
1210
+ * `StacItem` (e.g. classification just after fetching a Catalog / Collection /
1211
+ * ItemCollection) should funnel through this helper before handing the draft
1212
+ * to `connectionStore.saveHostConnection` / `connectionStore.save`. Existing
1213
+ * non-empty fields on the draft are preserved (this never clobbers a
1214
+ * user-set value).
1215
+ *
1216
+ * Returns a shallow copy of `input` with hint fields filled in. Safe to call
1217
+ * with an unrelated item, returns `input` untouched when the extension is
1218
+ * absent or unparseable.
1219
+ */
1220
+ declare function applyStacItemStorageHints<T extends {
1221
+ region?: string;
1222
+ endpoint?: string;
1223
+ }>(input: T, item: StacItem): T;
1224
+
529
1225
  /**
530
1226
  * Generic localStorage helpers with SSR safety.
531
1227
  *
@@ -543,6 +1239,93 @@ declare function loadFromStorage<T>(key: string, defaultValue: T): T;
543
1239
  */
544
1240
  declare function persistToStorage(key: string, value: unknown): void;
545
1241
 
1242
+ /**
1243
+ * Tiny insertion-order LRU built on top of `Map`. `get` / `has` move the entry
1244
+ * to the most-recent slot, `set` evicts the oldest (and runs `onEvict`) once
1245
+ * the cap is exceeded.
1246
+ *
1247
+ * Keeps the implementation deliberately small. Used by viewer modules that
1248
+ * cache per-source resources (GeoTIFF headers, presigned URLs) which would
1249
+ * otherwise leak across long pan / viewport-reload sessions.
1250
+ */
1251
+ interface LruCacheOptions<K, V> {
1252
+ /** Maximum number of entries. Must be > 0. */
1253
+ max: number;
1254
+ /** Called when an entry is evicted (LRU overflow or `delete()`). */
1255
+ onEvict?: (key: K, value: V) => void;
1256
+ }
1257
+ declare class LruCache<K, V> {
1258
+ private map;
1259
+ readonly max: number;
1260
+ private onEvict?;
1261
+ constructor(opts: LruCacheOptions<K, V>);
1262
+ get size(): number;
1263
+ has(key: K): boolean;
1264
+ get(key: K): V | undefined;
1265
+ set(key: K, value: V): void;
1266
+ delete(key: K): boolean;
1267
+ clear(): void;
1268
+ }
1269
+
1270
+ /**
1271
+ * Framework-agnostic helper that wires a "click on map, run a probe, surface the
1272
+ * result" flow used by every COG-style viewer (`CogViewer`, `StacMosaicViewer`,
1273
+ * `MultiCogViewer`).
1274
+ *
1275
+ * The viewers differ in what the probe returns (a single pixel sample, a per-
1276
+ * channel fan-out, or a topmost-source bbox hit), but they share the same
1277
+ * boilerplate: subscribe to `click`, mark inspecting, await the probe, surface
1278
+ * the payload, abort the previous probe if a new click arrives mid-flight, and
1279
+ * tear the listener down on cleanup.
1280
+ *
1281
+ * No dependency on Svelte, MapLibre, or deck.gl. The `MapLike` shape captures
1282
+ * only what the helper needs from the underlying map so this can be unit-tested
1283
+ * with a tiny stub if needed.
1284
+ */
1285
+ interface PixelInspectClickEvent {
1286
+ lngLat: {
1287
+ lng: number;
1288
+ lat: number;
1289
+ };
1290
+ }
1291
+ type PixelInspectClickHandler = (event: PixelInspectClickEvent) => void;
1292
+ /**
1293
+ * Minimal subset of MapLibre's map API used by the inspector. Anything that
1294
+ * dispatches a `click` event with `{lngLat}` and supports symmetric on/off
1295
+ * registration plugs in.
1296
+ */
1297
+ interface MapLike {
1298
+ on(type: 'click', handler: PixelInspectClickHandler): unknown;
1299
+ off(type: 'click', handler: PixelInspectClickHandler): unknown;
1300
+ }
1301
+ interface PixelInspectProbeRequest {
1302
+ lng: number;
1303
+ lat: number;
1304
+ signal: AbortSignal;
1305
+ }
1306
+ type PixelInspectProbe<T> = (req: PixelInspectProbeRequest) => Promise<T | null>;
1307
+ interface PixelInspectCallbacks<T> {
1308
+ /** Called synchronously when a click is accepted, before the probe is awaited. */
1309
+ onStart(): void;
1310
+ /**
1311
+ * Called once per click after the probe settles. Receives `null` when the
1312
+ * probe returned `null` or threw a non-helper-driven error (including an
1313
+ * `AbortError` that did not originate from this helper's own controller).
1314
+ */
1315
+ onResult(result: T | null): void;
1316
+ }
1317
+ interface AttachPixelInspectorOptions<T> {
1318
+ probe: PixelInspectProbe<T>;
1319
+ onStart: PixelInspectCallbacks<T>['onStart'];
1320
+ onResult: PixelInspectCallbacks<T>['onResult'];
1321
+ }
1322
+ /**
1323
+ * Wire a click-to-inspect probe onto `map`. Returns a `detach()` function that
1324
+ * removes the listener AND aborts any in-flight probe. Subsequent clicks abort
1325
+ * the previous probe so a fast double-click never leaves a stale result behind.
1326
+ */
1327
+ declare function attachPixelInspector<T>(map: MapLike, { probe, onStart, onResult }: AttachPixelInspectorOptions<T>): () => void;
1328
+
546
1329
  interface SqlBlock {
547
1330
  name: string;
548
1331
  sql: string;
@@ -574,6 +1357,92 @@ declare function interpolateTemplates(text: string, queryResults: Map<string, Re
574
1357
  */
575
1358
  declare function markSqlBlocks(content: string): string;
576
1359
 
1360
+ /**
1361
+ * Executes the SQL blocks parsed out of a markdown document (Evidence.dev style)
1362
+ * against an injected query engine, and caches the results by block name. Pairs
1363
+ * with `markdown-sql.ts` (the parser). Pure TypeScript, the engine is supplied by
1364
+ * the host so this module never imports DuckDB or any other heavy dependency.
1365
+ */
1366
+ declare class MarkdownSqlContext {
1367
+ private engine;
1368
+ private connId;
1369
+ private prefix;
1370
+ private results;
1371
+ constructor(engine: QueryEngine, connId: string, prefix?: string);
1372
+ /** Execute a SQL query and store the result under the given name. */
1373
+ executeSql(sql: string, queryName: string): Promise<Record<string, any>[]>;
1374
+ /**
1375
+ * Transform relative file paths in SQL to full S3 URLs.
1376
+ * e.g. read_parquet('data.parquet') becomes read_parquet('s3://bucket/prefix/data.parquet').
1377
+ */
1378
+ private transformPaths;
1379
+ getResult(queryName: string): {
1380
+ result: QueryResult;
1381
+ rows: Record<string, any>[];
1382
+ } | undefined;
1383
+ getAllResults(): Map<string, Record<string, any>[]>;
1384
+ getColumns(queryName: string): string[];
1385
+ }
1386
+
1387
+ /**
1388
+ * Lightweight browser-native Jupyter notebook renderer.
1389
+ * Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
1390
+ * Handles nbformat 2, 3, 4, and 5.
1391
+ */
1392
+ interface NotebookConfig {
1393
+ markdown: (md: string) => string;
1394
+ ansi: (text: string) => string;
1395
+ highlighter: (code: string, lang: string) => string;
1396
+ }
1397
+ interface RawNotebook {
1398
+ nbformat: number;
1399
+ nbformat_minor?: number;
1400
+ metadata?: Record<string, any>;
1401
+ cells?: RawCell[];
1402
+ worksheets?: {
1403
+ cells: RawCell[];
1404
+ }[];
1405
+ }
1406
+ interface RawCell {
1407
+ cell_type: string;
1408
+ source?: string | string[];
1409
+ input?: string | string[];
1410
+ outputs?: RawOutput[];
1411
+ prompt_number?: number;
1412
+ execution_count?: number | null;
1413
+ level?: number;
1414
+ language?: string;
1415
+ }
1416
+ interface RawOutput {
1417
+ output_type: string;
1418
+ data?: Record<string, string | string[]>;
1419
+ text?: string | string[];
1420
+ stream?: string;
1421
+ name?: string;
1422
+ png?: string;
1423
+ jpeg?: string;
1424
+ svg?: string;
1425
+ html?: string;
1426
+ latex?: string;
1427
+ traceback?: string[];
1428
+ ename?: string;
1429
+ evalue?: string;
1430
+ [key: string]: any;
1431
+ }
1432
+ interface NotebookMeta {
1433
+ kernelName: string;
1434
+ language: string;
1435
+ cellCount: number;
1436
+ }
1437
+ /**
1438
+ * Parse and render a Jupyter notebook JSON to a DOM element.
1439
+ * Returns the rendered element and metadata.
1440
+ */
1441
+ declare function renderNotebook(raw: RawNotebook, config: NotebookConfig): {
1442
+ element: HTMLElement;
1443
+ meta: NotebookMeta;
1444
+ };
1445
+
577
1446
  /**
578
1447
  * Lightweight Parquet metadata reader using hyparquet.
579
1448
  *
@@ -640,42 +1509,185 @@ declare function extractGeometryTypes(geo: GeoParquetMeta): GeoArrowGeomType[];
640
1509
  declare function extractBounds(geo: GeoParquetMeta): [number, number, number, number] | null;
641
1510
 
642
1511
  /**
643
- * STAC (SpatioTemporal Asset Catalog) detection and parsing.
1512
+ * STAC item facets, filtering, and sorting. Pure TS, framework-agnostic.
644
1513
  *
645
- * Pure TypeScript helpers shared by ViewerRouter, StacMosaicViewer, and
646
- * MultiCogViewer. No Svelte dependency, publishable via objex-utils.
1514
+ * Inputs are STAC Items (or any subset compatible with `StacItem`). Outputs
1515
+ * are slim views, auto-detected facet descriptors, and filtered/sorted view
1516
+ * arrays. No DOM, no Svelte, no fetch, no maplibre, safe to publish via
1517
+ * `@walkthru-earth/objex-utils`.
1518
+ *
1519
+ * The flow is intentionally one-directional:
1520
+ * StacItem[] --extractItemView--> StacItemView[]
1521
+ * StacItemView[] --buildFacets--> FacetSet
1522
+ * StacItemView[] + FacetState --applyFacets--> StacItemView[]
1523
+ * StacItemView[] + FacetSort --sortViews--> StacItemView[]
1524
+ *
1525
+ * Callers (any framework) can hold the views array, derive a FacetSet on
1526
+ * change, and reactively filter / sort it without re-touching the original
1527
+ * StacItems.
647
1528
  */
648
- /** STAC Link (shared by Catalog/Collection/Item). */
649
- interface StacLink {
650
- rel: string;
651
- href: string;
652
- type?: string;
653
- title?: string;
654
- }
655
- /** STAC Item (GeoJSON Feature shape with stac_version). */
656
- interface StacItem {
657
- type: 'Feature';
658
- stac_version: string;
1529
+
1530
+ /**
1531
+ * Compact, render-ready projection of a STAC Item. Keeps only the fields
1532
+ * needed for facet UI, sorting, footprint rendering, and the inspector
1533
+ * panel. The full original item is retained on `raw` for callers that need
1534
+ * to inspect arbitrary properties without a re-extract pass.
1535
+ */
1536
+ interface StacItemView {
659
1537
  id: string;
660
- bbox?: [number, number, number, number];
661
- geometry?: unknown;
662
- properties?: Record<string, unknown>;
663
- assets?: Record<string, StacAsset>;
664
- collection?: string;
665
- links?: StacLink[];
1538
+ collection: string | null;
1539
+ bbox: [number, number, number, number] | null;
1540
+ /** ISO 8601 datetime, or `start_datetime` when only an interval is given. */
1541
+ datetime: string | null;
1542
+ /** End of `start_datetime` / `end_datetime` interval, when present. */
1543
+ endDatetime: string | null;
1544
+ /** `eo:cloud_cover` percent (0-100), null when absent. */
1545
+ cloudCover: number | null;
1546
+ /** Ground sample distance in meters, null when absent. */
1547
+ gsd: number | null;
1548
+ platform: string | null;
1549
+ constellation: string | null;
1550
+ instruments: string[];
1551
+ /** EPSG code from `proj:epsg`, null when absent or non-numeric. */
1552
+ epsg: number | null;
1553
+ /** Best-effort thumbnail / overview href, null when no preview asset. */
1554
+ thumbnailHref: string | null;
1555
+ /** Asset role set across all assets on the item. */
1556
+ assetRoles: string[];
1557
+ /** Original item, retained so the inspector can show the raw JSON. */
1558
+ raw: StacItem;
666
1559
  }
667
- /** Single asset entry within a STAC item. */
668
- interface StacAsset {
669
- href: string;
670
- type?: string;
671
- title?: string;
672
- roles?: string[];
673
- /** Set when the asset carries `eo:bands`. */
674
- 'eo:bands'?: {
675
- name?: string;
676
- common_name?: string;
1560
+ /** Project a STAC Item into a `StacItemView`. Always succeeds. */
1561
+ declare function extractItemView(item: StacItem): StacItemView;
1562
+ /**
1563
+ * Numeric facet, e.g. cloud cover. `min`/`max` are derived from the loaded
1564
+ * views so the UI can use them as slider bounds. `count` is how many of the
1565
+ * input views had this field at all.
1566
+ */
1567
+ interface NumericFacet {
1568
+ kind: 'numeric';
1569
+ field: NumericFacetField;
1570
+ min: number;
1571
+ max: number;
1572
+ count: number;
1573
+ }
1574
+ /**
1575
+ * Enum facet, e.g. platform. `values` is sorted by descending count so the
1576
+ * most common values surface first in chip lists.
1577
+ */
1578
+ interface EnumFacet {
1579
+ kind: 'enum';
1580
+ field: EnumFacetField;
1581
+ values: {
1582
+ value: string;
1583
+ count: number;
677
1584
  }[];
678
1585
  }
1586
+ /**
1587
+ * Calendar-aligned bin granularity for the datetime histogram. Picked
1588
+ * automatically from the loaded items' time span so callers never see a
1589
+ * span-vs-resolution mismatch (e.g. month bins on a 30-day window or day bins
1590
+ * on a 20-year archive). See `pickGranularity` for the breakpoints.
1591
+ */
1592
+ type DatetimeGranularity = 'day' | 'week' | 'month' | 'year';
1593
+ /**
1594
+ * Datetime facet, with min/max for slider bounds and a calendar-aligned
1595
+ * histogram the UI can render under a range slider. Each bin spans one
1596
+ * `granularity` unit (UTC day, ISO week starting Monday, calendar month, or
1597
+ * calendar year). `bins[i]` is the count of items whose `datetime` falls
1598
+ * inside the bin starting at `binEdges[i]` (epoch ms, UTC). `bins.length ===
1599
+ * binEdges.length`, capped at `DATETIME_HISTOGRAM_BINS_MAX`.
1600
+ */
1601
+ interface DatetimeFacet {
1602
+ kind: 'datetime';
1603
+ field: 'datetime';
1604
+ /** Earliest datetime, ISO 8601. */
1605
+ min: string;
1606
+ /** Latest datetime, ISO 8601. */
1607
+ max: string;
1608
+ count: number;
1609
+ /** Per-bin counts. Length matches `binEdges.length`. */
1610
+ bins: number[];
1611
+ /** Auto-picked calendar granularity for each histogram bucket. */
1612
+ granularity: DatetimeGranularity;
1613
+ /** Epoch ms (UTC) of each bin's start boundary. Same length as `bins`. */
1614
+ binEdges: number[];
1615
+ }
1616
+ type Facet = NumericFacet | EnumFacet | DatetimeFacet;
1617
+ type NumericFacetField = 'cloudCover' | 'gsd';
1618
+ type EnumFacetField = 'collection' | 'platform' | 'constellation' | 'instruments' | 'assetRoles';
1619
+ /**
1620
+ * Soft cap on histogram bin count. The actual count is derived from the span
1621
+ * + granularity (e.g. a 5-year span at month granularity emits 60 bins, a
1622
+ * 90-day span at day granularity emits 90 bins). Spans that would exceed the
1623
+ * cap are clamped here, the next coarser granularity should already have been
1624
+ * picked by `pickGranularity` so this only protects against pathological
1625
+ * inputs.
1626
+ */
1627
+ declare const DATETIME_HISTOGRAM_BINS_MAX = 64;
1628
+ /** @deprecated Retained for backward compatibility. Use `DATETIME_HISTOGRAM_BINS_MAX`. */
1629
+ declare const DATETIME_HISTOGRAM_BINS = 32;
1630
+ /** Result of `buildFacets`: every facet that has variance in the input set. */
1631
+ interface FacetSet {
1632
+ datetime: DatetimeFacet | null;
1633
+ numeric: NumericFacet[];
1634
+ enums: EnumFacet[];
1635
+ /** Total number of views the facet set was built from. */
1636
+ total: number;
1637
+ }
1638
+ /**
1639
+ * Pick a calendar granularity from the time span. Inspired by lazycogs's
1640
+ * `_TemporalGrouper` family: short windows surface daily / weekly cadence,
1641
+ * long archives roll up to month / year so each bin still represents a
1642
+ * meaningful slice of data.
1643
+ */
1644
+ declare function pickGranularity(spanMs: number): DatetimeGranularity;
1645
+ /**
1646
+ * Scan a list of views and emit only those facets that have meaningful
1647
+ * variance. A facet is omitted when:
1648
+ * - numeric: fewer than two distinct finite values
1649
+ * - enum: fewer than two distinct values
1650
+ * - datetime: fewer than two parseable timestamps with distinct values
1651
+ *
1652
+ * The intent is "render only the controls that will narrow this dataset",
1653
+ * so callers can map each returned facet to a UI component without further
1654
+ * checks.
1655
+ */
1656
+ declare function buildFacets(views: StacItemView[]): FacetSet;
1657
+ /**
1658
+ * Mutable filter state, intended to be held by the UI layer. Each entry is
1659
+ * optional, omitting an entry means "no filter on this field". Numeric
1660
+ * ranges are inclusive on both ends. Enum sets are union-match (any value
1661
+ * in the set passes).
1662
+ */
1663
+ interface FacetState {
1664
+ datetime?: {
1665
+ min?: string;
1666
+ max?: string;
1667
+ };
1668
+ numeric?: Partial<Record<NumericFacetField, {
1669
+ min?: number;
1670
+ max?: number;
1671
+ }>>;
1672
+ enums?: Partial<Record<EnumFacetField, string[]>>;
1673
+ }
1674
+ /**
1675
+ * Filter views by `state`. Empty / missing entries are no-ops. Returns a new
1676
+ * array, the input is never mutated. Order is preserved, run `sortViews`
1677
+ * afterwards if a different order is needed.
1678
+ */
1679
+ declare function applyFacets(views: StacItemView[], state: FacetState | null | undefined): StacItemView[];
1680
+ type FacetSort = 'datetime-desc' | 'datetime-asc' | 'cloud-asc' | 'cloud-desc' | 'gsd-asc' | 'gsd-desc' | 'id-asc';
1681
+ /**
1682
+ * Sort views by one of a fixed set of strategies. Items missing the sort
1683
+ * field always sink to the bottom (regardless of asc/desc) so a `cloud-asc`
1684
+ * sort never surfaces "items with no cloud cover" above the cleanest scenes.
1685
+ */
1686
+ declare function sortViews(views: StacItemView[], sort: FacetSort): StacItemView[];
1687
+ /** True when any filter in `state` would actually narrow the input. */
1688
+ declare function hasActiveFilters(state: FacetState | null | undefined): boolean;
1689
+ /** Return `state` with every filter cleared. Useful for reset buttons. */
1690
+ declare function emptyFacetState(): FacetState;
679
1691
 
680
1692
  /**
681
1693
  * stac-geoparquet helpers.
@@ -769,69 +1781,449 @@ declare function pickStacPrimaryAsset(assets: Record<string, StacAsset> | null |
769
1781
  declare function stacRowToItem(row: StacGeoparquetRow, baseUrl: string, opts?: StacRowToItemOptions): StacItem;
770
1782
 
771
1783
  /**
772
- * Universal cloud storage URL / bucket parser.
1784
+ * STAC link-following hydrator. Walks `links[rel=item]` (Collection),
1785
+ * `links[rel=child]` → `links[rel=item]` (Catalog), and `links[rel=next]`
1786
+ * (paginated FeatureCollection / STAC API) into a flat list of StacItems.
1787
+ */
1788
+
1789
+ interface HydrateOptions {
1790
+ signal: AbortSignal;
1791
+ /** Max parallel fetches. Default 12. */
1792
+ concurrency?: number;
1793
+ /** Hard cap on items; catalogs larger than this are truncated. Default 2000. */
1794
+ limit?: number;
1795
+ /** Follow `links[rel=next]` pagination in FeatureCollections. Default true. */
1796
+ followPagination?: boolean;
1797
+ /** Emit fetched items in batches for progressive rendering. */
1798
+ onBatch?: (items: StacItem[]) => void;
1799
+ /** Emit progress totals for UI. */
1800
+ onProgress?: (fetched: number, totalHinted: number | undefined) => void;
1801
+ /**
1802
+ * Map an absolute HTTPS URL to a bucket-relative key when it belongs to the
1803
+ * caller's connection. When provided and it returns a non-null string,
1804
+ * `fetchJson` routes through the storage adapter (which handles SigV4) instead
1805
+ * of a raw cross-origin `fetch`, so private-bucket catalogs can be walked.
1806
+ */
1807
+ urlToKey?: (absoluteUrl: string) => string | null;
1808
+ /**
1809
+ * Optional native STAC API filters appended to the `rel="items"` endpoint
1810
+ * (and applied to `links[rel=next]` pages). Lets callers narrow a
1811
+ * collection by spatial / temporal extent before hydration.
1812
+ */
1813
+ itemsQuery?: StacItemsQuery;
1814
+ }
1815
+ /** Native filters supported by OGC API Features / STAC API on `/items`. */
1816
+ interface StacItemsQuery {
1817
+ /** WGS84 bbox `[west, south, east, north]`. */
1818
+ bbox?: [number, number, number, number];
1819
+ /** RFC 3339 instant or interval `start/end` (use `..` for open ends). */
1820
+ datetime?: string;
1821
+ /** Per-page item count hint, the server may cap this. */
1822
+ limit?: number;
1823
+ /**
1824
+ * CQL2-JSON filter expression (STAC API Filter extension). When set, gets
1825
+ * appended as `?filter=<json>&filter-lang=cql2-json` and re-stamped onto
1826
+ * every `rel="next"` page so cursor URLs cannot strip it.
1827
+ */
1828
+ filter?: unknown;
1829
+ }
1830
+ interface HydrateResult {
1831
+ items: StacItem[];
1832
+ truncated: boolean;
1833
+ rootBaseHref: string;
1834
+ }
1835
+ declare function hydrateStacItems(root: StacRoutableKind, baseHref: string, adapter: StorageAdapter, opts: HydrateOptions): Promise<HydrateResult>;
1836
+ /**
1837
+ * True when the payload exposes a `rel="items"` link (OGC API Features /
1838
+ * STAC API convention). Lets callers switch to viewport-scoped fetching
1839
+ * instead of walking every page.
1840
+ */
1841
+ declare function hasStacItemsEndpoint(payload: StacCollection | StacCatalog): boolean;
1842
+ /**
1843
+ * Resolve a possibly-relative href against a base. STAC catalogs commonly use
1844
+ * `./child/foo.json` or `../foo.json`. `new URL(relative, base)` handles both.
1845
+ */
1846
+ declare function absolutizeHref(href: string, baseHref: string): string;
1847
+
1848
+ /**
1849
+ * Translate a `FacetState` into native STAC API parameters and CQL2 filters,
1850
+ * gated by what the endpoint advertises in `conformsTo`. Pure TS, framework
1851
+ * and transport agnostic.
773
1852
  *
774
- * Accepts the many URI/URL formats that users commonly paste and extracts
775
- * the correct bucket, region, endpoint, and provider.
1853
+ * The split between this module and `stac-facets.ts` is intentional. Facets
1854
+ * own discovery and client-side filtering (works for any source). This
1855
+ * module owns *server-side push-down*, which only makes sense when the
1856
+ * source is a STAC API that supports OGC API Features query params or the
1857
+ * STAC API filter extension.
776
1858
  *
777
- * Supported URI schemes:
778
- * s3:// s3a:// s3n:// aws:// — Amazon S3 / S3-compatible
779
- * r2:// — Cloudflare R2
780
- * gs:// gcs:// — Google Cloud Storage
781
- * azure:// az:// — Azure Blob Storage
782
- * abfs:// abfss:// — Azure Data Lake (ADLS Gen2)
783
- * wasbs:// — Azure Blob (Hadoop WASB driver)
784
- * swift:// — OpenStack Swift
785
- * file:// filesystem:// — Local filesystem
1859
+ * Callers that want push-down:
1860
+ * 1. Fetch the API root, read `conformsTo`.
1861
+ * 2. `caps = sniffApiCapabilities(conformsTo)` once per session.
1862
+ * 3. On each pan / filter change:
1863
+ * const native = toNativeQuery(state, caps);
1864
+ * const filter = caps.cql2 ? toCql2Filter(state, caps) : null;
1865
+ * Pass `native` to the items endpoint and apply `filter` via
1866
+ * `?filter=<json-encoded>` when present. Anything that could not be
1867
+ * pushed down stays in `state` and is filtered client-side via
1868
+ * `applyFacets`.
1869
+ */
1870
+
1871
+ /**
1872
+ * Subset of STAC API capabilities relevant to filter push-down. Read once
1873
+ * per session from the API root's `conformsTo` array.
1874
+ */
1875
+ interface StacApiCapabilities {
1876
+ /** Supports `bbox=` query param (OGC API Features core). */
1877
+ bbox: boolean;
1878
+ /** Supports `datetime=` query param (OGC API Features core). */
1879
+ datetime: boolean;
1880
+ /** Supports `collections=` filter (STAC API Item Search). */
1881
+ collections: boolean;
1882
+ /** Supports the STAC API Filter extension via `filter=` + `filter-lang=cql2-json`. */
1883
+ cql2: boolean;
1884
+ /** Queryables endpoint advertised, lets clients sniff filterable property names. */
1885
+ queryables: boolean;
1886
+ }
1887
+ /**
1888
+ * Parse a STAC API `conformsTo` array into a capability flag set. Tolerant
1889
+ * of unknown URIs, missing entries, and casing differences. Defaults to all
1890
+ * `false` when given an empty / non-array input, so a caller that hasn't
1891
+ * fetched the root yet never accidentally pushes down something the API
1892
+ * cannot honor.
1893
+ */
1894
+ declare function sniffApiCapabilities(conformsTo: unknown): StacApiCapabilities;
1895
+ /**
1896
+ * Generic STAC items query, compatible with both OGC API Features
1897
+ * (`/collections/{id}/items`) and STAC API Item Search (`/search`). Mirrors
1898
+ * the shape `stac-hydrate.ts::StacItemsQuery` expects, plus optional
1899
+ * `collections` and `filter` for the search endpoint.
1900
+ */
1901
+ interface StacNativeQuery {
1902
+ bbox?: [number, number, number, number];
1903
+ datetime?: string;
1904
+ collections?: string[];
1905
+ limit?: number;
1906
+ /** CQL2-JSON object, encode with `JSON.stringify` when serializing. */
1907
+ filter?: unknown;
1908
+ 'filter-lang'?: 'cql2-json';
1909
+ }
1910
+ interface ToNativeQueryOptions {
1911
+ bbox?: [number, number, number, number];
1912
+ limit?: number;
1913
+ }
1914
+ /**
1915
+ * Translate `state` into the subset of native query params the API supports.
1916
+ * Anything that can't be pushed down is silently dropped here, the caller is
1917
+ * expected to keep applying it client-side via `applyFacets`. This is safe
1918
+ * because client filtering is always a superset, never a contradiction.
786
1919
  *
787
- * Supported HTTPS URL patterns:
788
- * https://<bucket>.s3.<region>.amazonaws.com[/prefix] — AWS virtual-hosted
789
- * https://s3.<region>.amazonaws.com/<bucket>[/prefix] — AWS path-style
790
- * https://s3.amazonaws.com/<bucket> — AWS global
791
- * https://<account>.r2.cloudflarestorage.com/<bucket> — Cloudflare R2
792
- * https://storage.googleapis.com/<bucket> — Google Cloud Storage
793
- * https://<bucket>.storage.googleapis.com[/prefix] — GCS virtual-hosted
794
- * https://<bucket>.<region>.digitaloceanspaces.com — DigitalOcean Spaces
795
- * https://<region>.digitaloceanspaces.com/<bucket> — DO Spaces path-style
796
- * https://s3.<region>.wasabisys.com/<bucket> — Wasabi
797
- * https://f<id>.backblazeb2.com/file/<bucket> — Backblaze B2
798
- * https://<bucket>.s3.<region>.backblazeb2.com — B2 S3-compatible
799
- * https://<bucket>.oss-<region>.aliyuncs.com — Alibaba Cloud OSS
800
- * https://<bucket>.cos.<region>.myqcloud.com — Tencent COS
801
- * https://storage.yandexcloud.net/<bucket> — Yandex Cloud
802
- * https://gateway.storjshare.io/<bucket> — Storj S3 gateway
803
- * https://link.storjshare.io/raw/<access>/<bucket> — Storj linksharing
804
- * https://<custom-endpoint>/<bucket> — Generic S3-compatible
1920
+ * `bbox` / `limit` are accepted as overrides because they typically come
1921
+ * from the viewer's viewport + user setting, not from `state`.
1922
+ */
1923
+ declare function toNativeQuery(state: FacetState | null | undefined, caps: StacApiCapabilities, opts?: ToNativeQueryOptions): StacNativeQuery;
1924
+ /**
1925
+ * CQL2-JSON expression node (very loose typing because the spec allows
1926
+ * arbitrary nesting and we only emit a small subset). Use `unknown` at the
1927
+ * boundary, cast inside this module.
1928
+ */
1929
+ type Cql2Node = unknown;
1930
+ /**
1931
+ * Build a CQL2-JSON `and` expression from a `FacetState`, covering the
1932
+ * filters that aren't already handled by native params. Returns `null` when
1933
+ * nothing in `state` requires CQL2 (so the caller can omit `filter=`).
805
1934
  *
806
- * Also handles plain bucket names (no protocol).
1935
+ * Currently emits:
1936
+ * - eo:cloud_cover (between)
1937
+ * - gsd (between)
1938
+ * - proj:epsg (=)
1939
+ * - platform (in)
1940
+ * - constellation (in)
1941
+ * - instruments (a_overlaps)
1942
+ *
1943
+ * `collection` and `datetime` are skipped here when the corresponding native
1944
+ * cap is set, since those are cheaper to push as plain query params. They
1945
+ * fall through to CQL2 only when the API advertises CQL2 but not the
1946
+ * matching native capability (rare but legal).
807
1947
  */
808
- type StorageProvider = string;
809
- interface ParsedStorageUrl {
810
- bucket: string;
811
- region: string;
812
- endpoint: string;
813
- provider: StorageProvider;
814
- /** Original prefix/path after bucket, if any */
815
- prefix: string;
1948
+ declare function toCql2Filter(state: FacetState | null | undefined, caps: StacApiCapabilities): Cql2Node | null;
1949
+ /**
1950
+ * Subtract everything that was pushed down from `state`, returning the
1951
+ * remaining state that the caller still has to apply client-side. Lets the
1952
+ * UI avoid double-filtering (which would just be a no-op but wastes work).
1953
+ *
1954
+ * This is a structural diff, not a deep clone, the input is not mutated.
1955
+ */
1956
+ declare function residualState(state: FacetState | null | undefined, caps: StacApiCapabilities): FacetState;
1957
+
1958
+ /**
1959
+ * StacSource contract. Unified interface for the three STAC ingestion paths
1960
+ * (STAC API, stac-geoparquet, self-contained static catalog) so the viewer
1961
+ * has a single orchestration loop and the UI can branch on capability flags
1962
+ * instead of hard-coded discovery modes.
1963
+ *
1964
+ * Pure TypeScript. No Svelte / maplibre / deck.gl / DuckDB on this import
1965
+ * graph. The DuckDB-bound parquet implementation lives under `query/` so the
1966
+ * `utils/` side stays publishable via `@walkthru-earth/objex-utils` (slice 6).
1967
+ *
1968
+ * Per-batch `pushedDown` / `residual` reporting lets the caller skip
1969
+ * client-side filtering for dimensions the engine already narrowed, and lets
1970
+ * the UI render capability badges. A parquet file with a STRUCT
1971
+ * `properties` column can push `eo:cloud_cover` while a sibling file with
1972
+ * an opaque `properties` cannot, so the report is per batch, not per source.
1973
+ */
1974
+
1975
+ /** Which underlying engine drives this source. Used by the viewer to pick
1976
+ * atomic-swap-vs-append, by the UI to choose copy / badges, and by tests. */
1977
+ type StacSourceKind = 'api' | 'parquet' | 'static';
1978
+ /**
1979
+ * Per-source capability surface. Read by the viewer at construction (no await
1980
+ * — sources are synchronous to construct so the orchestrator can branch on
1981
+ * `kind` before any I/O) and by the filter UI to decide which controls to
1982
+ * disable / badge as "client-side only".
1983
+ *
1984
+ * The `pushdown` map is exhaustive: every facet field listed in
1985
+ * `FacetState` has a flag here, so adding a new facet is a compile-time
1986
+ * error in every consumer until they handle it.
1987
+ */
1988
+ interface StacSourceCapabilities {
1989
+ kind: StacSourceKind;
1990
+ /** Human-readable label for HUD copy. e.g. "STAC API", "stac-geoparquet". */
1991
+ label: string;
1992
+ /** True when count(filter, bbox) is cheap. UI surfaces "Y of X". */
1993
+ countAvailable: boolean;
1994
+ /** True when query() yields multiple batches before completing. */
1995
+ streaming: boolean;
1996
+ /**
1997
+ * True when the underlying source is a hive-partitioned parquet directory
1998
+ * (e.g. `s3://bucket/prefix/year=2023/month=01/...`). Set by the parquet
1999
+ * source when the factory detects a directory layout (or the SDK passes
2000
+ * `hivePartitioned: true`). Lets the viewer surface a HUD hint without
2001
+ * inspecting `kind === 'parquet'` alone, since the same `kind` covers
2002
+ * single-file stac-geoparquet.
2003
+ */
2004
+ hivePartitioned?: boolean;
2005
+ pushdown: {
2006
+ bbox: boolean;
2007
+ datetime: boolean;
2008
+ collection: boolean;
2009
+ cloudCover: boolean;
2010
+ gsd: boolean;
2011
+ epsg: boolean;
2012
+ platform: boolean;
2013
+ constellation: boolean;
2014
+ instruments: boolean;
2015
+ assetRoles: boolean;
2016
+ };
816
2017
  }
817
- interface Defaults {
818
- region?: string;
819
- endpoint?: string;
820
- provider?: StorageProvider;
2018
+ /** Per-query inputs. The signal is required, sources MUST throw
2019
+ * `DOMException("Aborted", "AbortError")` on abort, never silently complete. */
2020
+ interface StacSourceRequest {
2021
+ /** WGS84 viewport bbox `[west, south, east, north]`. Required. Sources that
2022
+ * cannot push down bbox still receive it so they can stream the whole set
2023
+ * and rely on the caller's residual filter. */
2024
+ bbox: [number, number, number, number];
2025
+ filter: FacetState;
2026
+ limit: number;
2027
+ /** Per-page hint for sources that paginate. Server may ignore. */
2028
+ pageSize?: number;
2029
+ signal: AbortSignal;
821
2030
  }
2031
+ /** One yielded batch of items. */
2032
+ interface StacSourceBatch {
2033
+ items: StacItem[];
2034
+ /** Subset of filter the source / engine applied. UI reports as "pushed". */
2035
+ pushedDown: FacetState;
2036
+ /** Subset of filter the caller still has to apply via applyFacets(). */
2037
+ residual: FacetState;
2038
+ /** True when no more batches will arrive for this request. The async
2039
+ * iterator's own end-of-iteration also signals done; this flag lets a
2040
+ * caller break the loop at the moment a single-yield source completes. */
2041
+ done: boolean;
2042
+ /** Best-effort hint of total matching items, when the source knows. */
2043
+ totalHinted?: number;
2044
+ }
2045
+ interface StacSource {
2046
+ capabilities: StacSourceCapabilities;
2047
+ query(req: StacSourceRequest): AsyncIterable<StacSourceBatch>;
2048
+ /** Optional cheap count(filter, bbox). Surfaced as "Y of X" when set. */
2049
+ count?(filter: FacetState, bbox: StacSourceRequest['bbox'], signal: AbortSignal): Promise<number>;
2050
+ }
2051
+ /** All-false push-down flags. Helper to keep capability declarations terse. */
2052
+ declare function emptyPushdown(): StacSourceCapabilities['pushdown'];
2053
+
822
2054
  /**
823
- * Parse a user-provided bucket/URL string into structured storage connection parts.
2055
+ * STAC API implementation of the StacSource contract. Wraps `hydrateStacItems`
2056
+ * link-walking with `itemsQuery: {bbox, datetime, limit, filter}` push-down and
2057
+ * yields each `onBatch` as a `StacSourceBatch`.
2058
+ *
2059
+ * Slice 2 sniffs the catalog/collection's `conformsTo` array once per source
2060
+ * instance, builds a CQL2-JSON filter (cloud cover / gsd / platform /
2061
+ * constellation / instruments / collection) via `toCql2Filter`, and reports
2062
+ * the actually-pushed subset of `FacetState` plus the residual the caller still
2063
+ * has to apply via `applyFacets`. When the sniff fails or `conformsTo` lacks
2064
+ * the Filter extension, behavior degrades gracefully to slice-1 (bbox+datetime
2065
+ * only).
2066
+ *
2067
+ * Pure TypeScript. No DuckDB / Svelte / maplibre / deck.gl import. The
2068
+ * `StorageAdapter` import is structural (an interface), and the actual
2069
+ * adapter is injected via `deps`.
824
2070
  */
825
- declare function parseStorageUrl(input: string, defaults?: Defaults): ParsedStorageUrl;
2071
+
2072
+ interface StacApiSourceDeps {
2073
+ adapter: StorageAdapter;
2074
+ baseHref: string;
2075
+ urlToKey?: (absoluteUrl: string) => string | null;
2076
+ concurrency?: number;
2077
+ }
826
2078
  /**
827
- * Returns true if the input looks like a URL/URI rather than a plain bucket name.
828
- * Covers all recognized cloud storage URI schemes.
2079
+ * Construct a STAC API source. `kind` is the classified payload from
2080
+ * `classifyStac` (Collection / Catalog with `rel="items"`, or a STAC API
2081
+ * `item-collection` page). The factory checks before dispatching here, this
2082
+ * function does not re-validate.
2083
+ *
2084
+ * The advertised `capabilities.pushdown` flags reflect the *ceiling* of what a
2085
+ * STAC API can push (everything CQL2 covers). The actual push-down per request
2086
+ * depends on the `conformsTo` sniff and is reported per-batch in
2087
+ * `pushedDown` / `residual` so the caller can re-filter only what's left.
829
2088
  */
830
- declare function looksLikeUrl(input: string): boolean;
2089
+ declare function createApiSource(kind: StacRoutableKind, deps: StacApiSourceDeps): StacSource;
2090
+
831
2091
  /**
832
- * Given a parsed URL result, build a human-readable summary of what was detected.
2092
+ * Self-contained static catalog implementation of the StacSource contract.
2093
+ * Wraps `hydrateStacItems` link-walking with no `itemsQuery`, so the entire
2094
+ * advertised tree is fetched and the caller filters client-side.
2095
+ *
2096
+ * Slice 1 reports zero push-down. Slice 4 adds extent-pruning (skip child
2097
+ * links whose `extent.spatial` / `extent.temporal` does not intersect the
2098
+ * request bbox / datetime), which lifts `bbox` and `datetime` to true.
2099
+ *
2100
+ * Pure TypeScript. No DuckDB / Svelte / maplibre / deck.gl import.
833
2101
  */
834
- declare function describeParseResult(parsed: ParsedStorageUrl): string;
2102
+
2103
+ interface StacStaticSourceDeps {
2104
+ adapter: StorageAdapter;
2105
+ baseHref: string;
2106
+ urlToKey?: (absoluteUrl: string) => string | null;
2107
+ concurrency?: number;
2108
+ }
2109
+ declare function createStaticSource(kind: StacRoutableKind, deps: StacStaticSourceDeps): StacSource;
2110
+
2111
+ /**
2112
+ * STAC Storage Extension parser.
2113
+ *
2114
+ * Detects the Storage Extension version on a STAC Item and extracts
2115
+ * connection-relevant hints (region, requester-pays, custom-S3 endpoint).
2116
+ *
2117
+ * Inspired by lazycogs's `_storage_ext.py`. Pure TypeScript, no fetch, no
2118
+ * Svelte dependency. Suitable for `@walkthru-earth/objex-utils` re-export.
2119
+ *
2120
+ * Supported schema URLs:
2121
+ * - https://stac-extensions.github.io/storage/v1.0.0/schema.json
2122
+ * - https://stac-extensions.github.io/storage/v2.0.0/schema.json
2123
+ *
2124
+ * v1 (item / asset properties)
2125
+ * storage:platform e.g. "AWS", "GCP", "AZURE"
2126
+ * storage:region
2127
+ * storage:requester_pays boolean
2128
+ * storage:tier (ignored — no obstore equivalent)
2129
+ *
2130
+ * v2 (item-level scheme map + asset-level refs)
2131
+ * properties.storage:schemes = {
2132
+ * <ref>: { type, platform, region?, requester_pays?, endpoint? }
2133
+ * }
2134
+ * asset.storage:refs = ["primary", ...] (first matching ref wins)
2135
+ *
2136
+ * Asset-level fields take precedence over item-level fields in v1.
2137
+ */
2138
+
2139
+ /** Recognized Storage Extension schema versions. */
2140
+ type StorageExtensionVersion = '1.0.0' | '2.0.0';
2141
+ /**
2142
+ * Connection-relevant hints extracted from the Storage Extension. All fields
2143
+ * are nullable so callers can merge selectively into existing config without
2144
+ * clobbering user-set values.
2145
+ */
2146
+ interface StorageHints {
2147
+ /** e.g. "AWS", "GCP", "AZURE", "MINIO". Uppercased. Null when absent. */
2148
+ platform: string | null;
2149
+ /** Region code, e.g. "us-west-2". Null when absent. */
2150
+ region: string | null;
2151
+ /** True when requester-pays must be set. False when absent or false. */
2152
+ requesterPays: boolean;
2153
+ /** Concrete S3-compatible endpoint URL. Null unless v2 `custom-s3` with
2154
+ * a non-templated `platform` value. */
2155
+ endpoint: string | null;
2156
+ }
2157
+ /** Empty hints record. Returned when extension absent or unparseable. */
2158
+ declare function emptyStorageHints(): StorageHints;
2159
+ /**
2160
+ * Scan `item.stac_extensions[]` for the Storage Extension schema URL and
2161
+ * return its parsed version. Returns null when the extension is absent or
2162
+ * the version is not one we recognize.
2163
+ */
2164
+ declare function detectStorageExtensionVersion(item: StacItem): StorageExtensionVersion | null;
2165
+ /**
2166
+ * Extract connection hints from a STAC Item. Dispatches on the detected
2167
+ * Storage Extension version. Returns `emptyStorageHints()` when the
2168
+ * extension is absent or fails to parse.
2169
+ *
2170
+ * `assetKey` is optional. When given:
2171
+ * - v1: that asset's overrides take precedence over item-level fields.
2172
+ * - v2: that asset's `storage:refs[0]` resolves the item scheme.
2173
+ * When omitted in v2, the first scheme found in any asset's refs wins.
2174
+ */
2175
+ declare function extractStorageHints(item: StacItem, assetKey?: string): StorageHints;
2176
+ /**
2177
+ * TODO(host-detection): a future PR should call this from the connection
2178
+ * auto-fill path so a STAC Item carrying Storage Extension metadata can
2179
+ * pre-populate `region` and (for `custom-s3`) `endpoint` on a new
2180
+ * connection.
2181
+ *
2182
+ * Today this lives next to the parser so consumers can opt-in without
2183
+ * touching `host-detection.ts` or the connection store. The function is
2184
+ * intentionally generic — it takes any object with `region` / `endpoint`
2185
+ * keys and returns a shallow copy with hint fields filled only when the
2186
+ * existing value is empty.
2187
+ */
2188
+ declare function applyStorageHintsToConnection<T extends {
2189
+ region?: string;
2190
+ endpoint?: string;
2191
+ }>(conn: T, hints: StorageHints): T;
2192
+
2193
+ /**
2194
+ * Open-time storage probe. Issues a single ranged GET against a presigned
2195
+ * asset URL to surface auth, CORS, and bucket-misconfiguration errors at
2196
+ * viewer load time, before any tile read kicks off.
2197
+ *
2198
+ * Inspired by lazycogs `_smoketest_store` (developmentseed/lazycogs). The
2199
+ * Python library calls `store.head()` on a representative asset during
2200
+ * `open()` so credential or region misconfiguration fails in <1s instead of
2201
+ * mid-mosaic when the first COG range fetch errors out.
2202
+ *
2203
+ * We use ranged GET instead of HEAD because:
2204
+ * - Many private buckets allow GET but block HEAD via CORS.
2205
+ * - Range `bytes=0-0` is one byte, cheaper than a full body fetch.
2206
+ * - Successful 206 / 200 confirms BOTH auth and CORS headers are correct,
2207
+ * which a HEAD response sometimes lies about under bucket policies that
2208
+ * return mismatched `Access-Control-Expose-Headers`.
2209
+ *
2210
+ * Pure TS, no Svelte / framework deps. Safe to publish via objex-utils.
2211
+ */
2212
+ type SmokeTestResult = {
2213
+ ok: true;
2214
+ } | {
2215
+ ok: false;
2216
+ status: number | null;
2217
+ reason: string;
2218
+ };
2219
+ /**
2220
+ * Probe a presigned URL with a one-byte Range GET. Resolves to `{ ok: true }`
2221
+ * on 200 / 206, otherwise returns a structured failure with the HTTP status
2222
+ * (or null when the request never reached a server, e.g. CORS preflight
2223
+ * failure or DNS error). AbortError is re-thrown so callers can distinguish
2224
+ * intentional cancellation from real failures.
2225
+ */
2226
+ declare function smokeTestHref(href: string, signal?: AbortSignal): Promise<SmokeTestResult>;
835
2227
 
836
2228
  /**
837
2229
  * Lightweight WKB (Well-Known Binary) parser for extracting coordinates.
@@ -877,4 +2269,4 @@ declare function findGeoColumnFromRows(rows: Record<string, unknown>[], schema:
877
2269
  type: string;
878
2270
  }[]): string | null;
879
2271
 
880
- export { type AccessMode, type AccessModeInput, COPY_FEEDBACK_MS, type CogInfo, type Connection, type ConnectionConfig, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, type Defaults, type DuckDbReadFn, type FileCategory, type FileEntry, type FileTypeInfo, type GeoArrowGeomType, type GeoArrowResult, type GeoBounds, type GeoColumnMeta, type GeoParquetMeta, type GeoType, type HexRow, LAYER_HUE_MULTIPLIER, type ListPage, MAX_QUERY_HISTORY_ENTRIES, type MapQueryHandle, type MapQueryResult, PROVIDERS, PROVIDER_IDS, type ParquetFileMetadata, type ParsedGeometry, type ParsedMarkdownDocument, type ParsedStorageUrl, type ProviderDef, type ProviderId, type ProviderRegion, QueryCancelledError, type QueryEngine, type QueryHandle, type QueryResult, type QuerySource, SF_LABELS, SQL_PREVIEW_LENGTH, STAC_GEOPARQUET_REQUIRED_COLUMNS, STORAGE_KEYS, type SchemaField, type SortConfig, type SortDirection, type SortField, type SqlBlock, type StacBboxStruct, type StacGeoparquetRow, type StacGeoparquetSchemaColumn, type StacRowToItemOptions, type StorageAdapter, type StorageProvider, type Tab, type Theme, type TypeCategory, UrlAdapter, VIEWER_DIR_EXTENSIONS, type ViewerKind, WGS84_CODES, type WriteResult, buildDataTypeLabel, buildDuckDbSource, buildEndpointFromTemplate, buildGeoArrowTables, buildProviderBaseUrl, clampBounds, classifyType, describeParseResult, escapeCsvField, exportToCsv, exportToJson, extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, findGeoColumn, findGeoColumnFromRows, flattenStacBbox, formatDate, formatFileSize, formatValue, generateHexDump, getAccessMode, getDuckDbReadFn, getFileExtension, getFileTypeInfo, getMimeType, getNativeScheme, getProvider, getViewerKind, handleLoadError, interpolateTemplates, isCloudNativeFormat, isGcsProvider, isPubliclyStreamable, isQueryable, isStacGeoparquetSchema, jsonReplacerBigInt, loadFromStorage, looksLikeUrl, markSqlBlocks, normalizeGeomType, parseMarkdownDocument, parseStorageUrl, parseWKB, persistToStorage, pickStacPrimaryAsset, readParquetMetadata, resolveCloudUrl, resolveProviderEndpoint, resolveStacAssetHref, safeClamp, safeDecodeURIComponent, serializeToCsv, serializeToJson, sortFileEntries, stacRowToItem, toBinary, toggleSortField, typeBadgeClass, typeColor, typeLabel };
2272
+ export { type AccessMode, type AccessModeInput, type AppConfig, type AppConfigDefaults, type AppConfigUi, type AttachPixelInspectorOptions, type BandMap, type BandSlot, type BasemapConfig, COPY_FEEDBACK_MS, type ChannelComposite, type ChannelRef, type CogAsset, type CogInfo, type Connection, type ConnectionConfig, type ConnectionIdentityInput, type ConnectionSeed, DATETIME_HISTOGRAM_BINS, DATETIME_HISTOGRAM_BINS_MAX, DEFAULT_APP_CONFIG, DEFAULT_AWS_REGION, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, type DatetimeFacet, type DatetimeGranularity, type Defaults, type DetectedHost, type DuckDbReadFn, type EnumFacet, type EnumFacetField, FIRST_FEATURE_FLY_ZOOM, type Facet, type FacetSet, type FacetSort, type FacetState, type FileCategory, type FileEntry, type FileTypeInfo, type GeoArrowGeomType, type GeoArrowResult, type GeoBounds, type GeoColumnMeta, type GeoParquetMeta, type GeoType, type GeometryTypeInfo, type HexRow, type HydrateOptions, type HydrateResult, LAYER_HUE_MULTIPLIER, type ListPage, LruCache, type LruCacheOptions, MAX_QUERY_HISTORY_ENTRIES, type MapLike, type MapQueryHandle, type MapQueryResult, MarkdownSqlContext, type MosaicSourceMeta, type NotebookConfig, type NotebookMeta, type NumericFacet, type NumericFacetField, PRESETS, PROVIDERS, PROVIDER_IDS, type ParquetFileMetadata, type ParsedGeometry, type ParsedMarkdownDocument, type ParsedStorageUrl, type PixelInspectCallbacks, type PixelInspectClickEvent, type PixelInspectClickHandler, type PixelInspectProbe, type PixelInspectProbeRequest, type PresetDef, type ProviderDef, type ProviderId, type ProviderRegion, QueryCancelledError, type QueryEngine, type QueryHandle, type QueryResult, type QuerySource, type RasterBandAsset, SF_LABELS, SQL_PREVIEW_LENGTH, STAC_API_PATH_RE, STAC_COG_ASSET_KEYS, STAC_GEOPARQUET_REQUIRED_COLUMNS, STORAGE_KEYS, type SchemaField, type SmokeTestResult, type SortConfig, type SortDirection, type SortField, type SqlBlock, type StacApiCapabilities, type StacApiSourceDeps, type StacAsset, type StacBboxStruct, type StacCatalog, type StacCollection, type StacFeatureCollection, type StacGeoparquetRow, type StacGeoparquetSchemaColumn, type StacItem, type StacItemView, type StacItemsQuery, type StacLink, type StacNativeQuery, type StacRoutableKind, type StacRowToItemOptions, type StacSource, type StacSourceBatch, type StacSourceCapabilities, type StacSourceKind, type StacSourceRequest, type StacStaticSourceDeps, type StorageAdapter, type StorageExtensionVersion, type StorageHints, type StorageProvider, TILE_DEBOUNCE_MS, type Tab, type Theme, type ToNativeQueryOptions, type TypeCategory, UrlAdapter, type UrlClassification, VIEWER_DIR_EXTENSIONS, type ViewerKind, WGS84_CODES, type WriteResult, absolutizeHref, allChannelsBand0, applyFacets, applyPreset, applyStacItemStorageHints, applyStorageHintsToConnection, attachPixelInspector, availablePresets, buildDataTypeLabel, buildDuckDbSource, buildEndpointFromTemplate, buildFacets, buildGeoArrowTables, buildMosaicSourceMeta, buildProviderBaseUrl, buildTransformExpr, clampBounds, classifyStac, classifyType, classifyUrl, coerceBool, coercePositiveInt, coerceString, coerceTheme, compositeFromUrl, compositeToUrl, connectionIdentityKey, copyToClipboard, createApiSource, createStaticSource, describeParseResult, detectHostBucket, detectMosaicCapable, detectMultiCogCapable, detectStorageExtensionVersion, emptyFacetState, emptyPushdown, emptyStorageHints, escapeCsvField, exportToCsv, exportToJson, extractBounds, extractCogAssets, extractEpsgFromGeoMeta, extractGeometryTypes, extractItemView, extractMosaicAssets, extractRasterBandAssets, extractSentinelBandAssets, extractStorageHints, findGeoColumn, findGeoColumnFromRows, flattenStacBbox, formatDate, formatFileSize, formatValue, generateHexDump, getAccessMode, getDuckDbReadFn, getFileExtension, getFileTypeInfo, getMimeType, getNativeScheme, getProvider, getViewerKind, handleLoadError, hasActiveFilters, hasCompositableBands, hasRgbBands, hasStacItemsEndpoint, hydrateStacItems, interpolateTemplates, isAbortError, isCloudNativeFormat, isGcsProvider, isKnownBucketHost, isPubliclyStreamable, isQueryable, isSameConnectionIdentity, isSingleAssetComposite, isStacCatalog, isStacCollection, isStacFeatureCollection, isStacGeoparquetSchema, isStacItem, isWgs84, isWgs84Crs, jsonReplacerBigInt, loadFromStorage, looksLikeUrl, markSqlBlocks, mergeAppConfig, normalizeEndpoint, normalizeGeomType, normalizeProvider, parseGeometryTypeCrs, parseMarkdownDocument, parseStorageUrl, parseVisibilityParam, parseWKB, persistToStorage, pickCogAssetHref, pickGranularity, pickNaturalColorComposite, pickStacPrimaryAsset, presetMatchesComposite, readParquetMetadata, renderNotebook, residualState, resolveBandSlotAssetKey, resolveBasemap, resolveCloudUrl, resolvePresetComposite, resolveProviderEndpoint, resolveSetting, resolveStacAssetHref, safeClamp, safeDecodeURIComponent, serializeToCsv, serializeToJson, smokeTestHref, sniffApiCapabilities, sortFileEntries, sortViews, spatialCellKey, stacItemBbox, stacRowToItem, syntheticSelfAsset, toBinary, toCql2Filter, toNativeQuery, toggleSortField, typeBadgeClass, typeColor, typeLabel, wireCodeCopyButtons, wrapWkbWithCrs };