name: dubai-coastline
version: "0.2"
category: change-detection
themes: [hydrology]
family: surface-water-extent-change
description: >
  Two-epoch shoreline and water extent at Dubai from Landsat NDWI. Between the
  early 1990s and today, Palm Jumeirah, Palm Jebel Ali and the World islands
  turned open Gulf water into land — one of the largest deliberate coastline
  changes on Earth.

# WHAT THIS MEASURES, AND WHAT IT DOES NOT. READ BEFORE CITING A NUMBER.
#   NDWI > threshold is an OPEN-WATER PROXY, not a bathymetric or volumetric
#   measurement. Wet playa, salt crust, shallow turbid water and wet vegetation
#   sit near the threshold and cross it between epochs. The answer below is a
#   count of proxy-water pixels at 30 m (0.09 ha each), not a gauged surface
#   area, and it carries no uncertainty estimate.
#
#   No per-year axis. Each epoch is ONE median composite over its window, so the
#   answer is a two-epoch comparison, not an annual series. A genuine annual
#   series needs a per-year grouping reduction, and no shipped op provides one
#   (recipe-library tracker, defect 63).
#
#   UPSTREAM BUG (recipe-library tracker, defects 61/62). Some Landsat 7 (LE07)
#   assets on the Planetary Computer answer an HTML error page where a COG
#   should be, and the read dies as "Unknown byte order marker: [3C, 21]" (`<!`).
#   Confirmed 2026-08-17 on LE07_L2SP_036035_20221027_02_T1. There is no
#   platform filter to exclude L7 with: the source params carry none, and the
#   `query.platform` block the previous version of this recipe declared was
#   silently ignored. The only lever an author has is the calendar. Windows that
#   overlap 1999-2023 can pull L7 and can therefore hit this.
#
#   No month filter. The engine's source params carry no month selector, so the
#   seasonal windows below are calendar spans and off-season scenes do enter the
#   median. A median over many scenes is robust to that; a mean would not be.

settings:
  ndwi_threshold: 0.0        # NDWI > this → open water; the complement is the shoreline
  cloud_cover_max_pct: 20    # eo:cloud_cover upper bound — Gulf scenes are clear
  pixel_area_m2: 900         # 30 m Landsat pixel — multiply `count` by this for area


inputs:
  aoi:
    bbox: [54.9, 24.9, 55.4, 25.4]
    crs_target: EPSG:32640
    notes: >
      Dubai's Gulf frontage from Jebel Ali to the creek, covering both palms and
      the World islands. Reclamation is the signal; suspended sediment plumes
      during construction are the main confounder.


  baseline_epoch:
    start: "1990-01-01"
    end: "1994-12-31"
    notes: Landsat 5 TM, the natural shoreline before reclamation.

  recent_epoch:
    start: "2021-01-01"
    end: "2025-12-31"
    notes: Landsat 8/9 OLI, the built coastline.

  collection:
    uri: "@usgs/landsat/c2-l2"

layers:

  # ── SOURCE ──────────────────────────────────────────────────────────────────
  #
  # Bands are CANONICAL concept codes from registry/datasets/usgs/landsat/c2-l2/
  # _dataset.yaml (G = green, N = nir, QA = qa-pixel). Each distribution maps them onto its own
  # producer asset keys, so ONE band list is correct across TM, ETM+ and OLI —
  # which the previous SR_Bx spelling was not (SR_B5 is SWIR-1 on TM/ETM+ and
  # NIR on OLI, so the old files asked for a different physical band per sensor).
  #
  # The FUSED per-scene form (`scenes: true` + `expression:` + `mask_band:`) is
  # what makes the index correct: the distribution's declared scale/offset is
  # applied per band before the expression, fill DNs become NaN, and QA_PIXEL is
  # the platform mask band, excluded from the b1..bN binding. `temporal_reduce`
  # then takes the median across the surviving scenes.
  #
  # Do NOT collapse this to a bare `bands: [A, B, QA]` source and difference the
  # result. A source read emits the DECLARED BANDS (measured 2026-08-17: three
  # bands out for a three-band request), so `b2 - b1` there is raw band math,
  # not a normalized-difference change. It plans green and answers plausibly.

  source/ndwi-baseline-scenes:
    type: raster
    description: Per-scene NDWI, QA-masked, baseline epoch.
    uri: "@usgs/landsat/c2-l2"
    params:
      scenes: true
      bands: [G, N, QA]
      mask_band: QA
      expression: "(b1 - b2) / (b1 + b2)"
      bbox: [54.9, 24.9, 55.4, 25.4]
      temporal: ["1990-01-01", "1994-12-31"]
      cloud_lt: 20
      resolution: 30
      limit: 60

  source/ndwi-recent-scenes:
    type: raster
    description: Per-scene NDWI, QA-masked, recent epoch.
    uri: "@usgs/landsat/c2-l2"
    params:
      scenes: true
      bands: [G, N, QA]
      mask_band: QA
      expression: "(b1 - b2) / (b1 + b2)"
      bbox: [54.9, 24.9, 55.4, 25.4]
      temporal: ["2021-01-01", "2025-12-31"]
      cloud_lt: 20
      resolution: 30
      limit: 60

  # ── COMPUTE ─────────────────────────────────────────────────────────────────

  compute/ndwi-baseline:
    type: raster
    description: Median NDWI over the baseline epoch's scenes.
    compute:
      op: temporal_reduce
      inputs:
        time_series: { layer: source/ndwi-baseline-scenes }
      params:
        method: median

  compute/ndwi-recent:
    type: raster
    description: Median NDWI over the recent epoch's scenes.
    compute:
      op: temporal_reduce
      inputs:
        time_series: { layer: source/ndwi-recent-scenes }
      params:
        method: median

  compute/ndwi-pair:
    type: raster
    description: The two NDWI composites stacked into one 2-band raster (b1 = baseline, b2 = recent).
    compute:
      op: stack_concat
      inputs:
        layers:
          - { layer: compute/ndwi-baseline }
          - { layer: compute/ndwi-recent }

  compute/ndwi-change:
    type: raster
    description: >
      dNDWI = recent - baseline. Strongly negative over every reclaimed
      footprint: open water that became land.
    compute:
      op: raster_calc
      inputs:
        raster: { layer: compute/ndwi-pair }
      params:
        expression: "b2 - b1"
        output_band: dndwi

  compute/water-baseline:
    type: raster
    description: >
      Proxy-water footprint. Water keeps the value 1; land becomes NaN
      (`b1 / 0.0`), which the stats kernel skips — so the answer layer's
      `count` is the water-pixel count.
    compute:
      op: raster_calc
      inputs:
        raster: { layer: compute/ndwi-baseline }
      params:
        expression: "where(b1 > ${settings.ndwi_threshold}, 1.0, b1 / 0.0)"
        output_band: water

  compute/water-recent:
    type: raster
    description: >
      Proxy-water footprint. Water keeps the value 1; land becomes NaN
      (`b1 / 0.0`), which the stats kernel skips — so the answer layer's
      `count` is the water-pixel count.
    compute:
      op: raster_calc
      inputs:
        raster: { layer: compute/ndwi-recent }
      params:
        expression: "where(b1 > ${settings.ndwi_threshold}, 1.0, b1 / 0.0)"
        output_band: water

  # ── ANSWERS ─────────────────────────────────────────────────────────────────

  answer/water-baseline:
    type: answer
    description: >
      Proxy-water pixel count, baseline epoch. Area = count x 900 m²; the drop
      between epochs is the reclaimed area.
    compute:
      op: summary
      inputs:
        raster: { layer: compute/water-baseline }
      params:
        stats: [count]

  answer/water-recent:
    type: answer
    description: >
      Proxy-water pixel count, recent epoch. Area = count x 900 m²; the drop
      between epochs is the reclaimed area.
    compute:
      op: summary
      inputs:
        raster: { layer: compute/water-recent }
      params:
        stats: [count]

outputs:
  water_baseline:
    layer: answer/water-baseline
    value_kind: answer
    description: Pre-reclamation proxy-water pixel count.
  water_recent:
    layer: answer/water-recent
    value_kind: answer
    description: Present proxy-water pixel count. THE answer — the drop is reclaimed land.
  ndwi_change:
    layer: compute/ndwi-change
    value_kind: raster
    description: dNDWI raster, recent minus baseline (show-your-work).


license:
  recipe: Apache-2.0
  output_dataset: CC-BY-4.0
  upstream_data:
    - { source: "@usgs/landsat/c2-l2", license: usgs-public-domain }
