# catalog/operations/raster/mask.yaml

id: raster_mask
name: Mask Raster
description: >
  Apply a mask to a raster. POLARITY: 0 DROPS, NONZERO KEEPS. A pixel whose mask value is 0
  is masked out (set to nodata); every other value, including negatives, fractions, and NaN,
  keeps the pixel unchanged. (NaN is not zero: a NaN mask value KEEPS the pixel — verified
  identical across every binding.)

  This is INVERTED from openEO's .mask(), where the mask is TRUE where pixels should be
  REMOVED. Porting an openEO example means FLIPPING the condition: openEO's
  ~((SCL == 4) | (SCL == 5)) becomes (SCL == 4) | (SCL == 5) here. Nothing can catch a flip
  for you — an inverted mask produces a perfectly plausible raster of exactly the wrong
  pixels — so check the polarity before you check the values.

  Masking is BAND-WISE when the mask has the same band count as the raster (a per-scene mask
  over a per-scene stack, which is the cloud-masking shape), and BROADCASTS when the mask is
  single-band. Band count is preserved: masking does not reduce.
version: 1.0.0
category: raster
type: raster-to-raster
default_implementation: native

inputs:
  - name: raster
    type: raster
    format: geotiff
    description: Input raster to mask
    required: true
  - name: mask_raster
    type: raster
    format: geotiff
    description: >
      Mask raster. 0 = DROP the pixel, any NONZERO value = KEEP it. Inverted from openEO's
      .mask() (which drops where TRUE), so a ported openEO mask must be negated. Either
      band-for-band with the raster, or single-band to broadcast over every band.
    required: true

requires: {}  # structural — operates on any raster

outputs:
  - name: masked
    type: raster
    format: cog
    description: Masked raster with nodata where mask is 0

params: {}
# `nodata: <number>` was declared here and READ BY NOTHING — not the engine arm, not the WASM
# handle (`h_raster_mask(data, mask)` takes no third argument), not the Python side. Dropped
# 2026-08-07 rather than left advertised, for the same reason `formula:` was: a param the
# schema promises and no backend reads is worse than an absent one, because it looks handled.
# Masked pixels are NaN. That is deliberate and load-bearing: every downstream reducer here is
# NaN-aware and SKIPS them, so a masked pixel does not join the population. A sentinel number
# would be counted as data. Re-add the param only with a kernel that carries it AND an answer
# for what the reducers should then do.

cache_policy:
  ttl_days: 30
  invalidate_on: [source_update, mask_change]

# backends: audited 2026-08-14 (defect 50). A key means a runtime that DISPATCHES this op —
# folia-engine `dispatch_op` (products/sdk/folia-engine/src/lib.rs), a `registerOp`/OP_TABLE
# entry in packages/compute, `_BUILTIN_OP_MAP` in folia/compute.py, or a backend manifest
# (folia/backends/*/backend.yaml).
backends:
  rust-engine:
    op: raster_mask
    function: raster_mask_op
    dispatch: products/sdk/folia-engine/src/lib.rs:1023
  rust-wasm:
    function: core_ops::raster_mask
    handle_function: h_raster_mask
  js:
    function: raster_mask
  python:
    function: geo.raster.mask
    dispatch: folia/compute.py _BUILTIN_OP_MAP

display_hints:
  map:
    renderer: maplibre

ui:
  icon: eye-off
  color: "#696969"
