Skip to content

[GH-3324] Keep other bands' no-data value when clearing one band - #3327

Open
Eliaaazzz wants to merge 5 commits into
apache:masterfrom
Eliaaazzz:fix/GH-3324-per-band-nodata-clear
Open

[GH-3324] Keep other bands' no-data value when clearing one band#3327
Eliaaazzz wants to merge 5 commits into
apache:masterfrom
Eliaaazzz:fix/GH-3324-per-band-nodata-clear

Conversation

@Eliaaazzz

Copy link
Copy Markdown
Contributor

Did you read the Contributor Guide?

Is this PR related to a ticket?

Stacked on #3312. #3324 is a follow-up to that PR and only reproduces with it applied, so this branch starts from 2b06236 (its current head) and adds two commits on top. The first three commits in the diff belong to #3312. Please merge #3312 first and I will rebase this onto master, or fold this into #3312 if that is easier to review.

What changes were proposed in this PR?

Two independent commits, so the second can be dropped without losing the first.

1. Keep other bands' no-data value when clearing one band (309b16a)

No-data values are tracked per band on the sample dimensions. GC_NODATA is a single sentinel for the whole coverage, carried both by the coverage properties and by the rendered image. The clear path dropped that sentinel unconditionally, so clearing one band also stopped GeoTools operations that read the sentinel from honouring the bands that still declared a no-data value. Jiffle reads it, which is how clearing band 2 changed band 1's out = rast[0] + 1; from NaN to 1.0.

The fix drops the sentinel only once no band declares a no-data value at all. Reproduced before the change and verified after, on the exact steps in the issue:

  • Loaded from GeoTIFF: band 1 0.0, band 2 0.0, rast[0] + 1 gives NaN.
  • After clearing band 2, before this PR: band 1 0.0, band 2 null, rast[0] + 1 gives 1.0. This is the bug.
  • After clearing band 2, with this PR: band 1 0.0, band 2 null, rast[0] + 1 gives NaN.
  • After clearing band 1 as well: band 1 null, band 2 null, rast[0] + 1 gives 1.0.

2. Reject mixed per-band no-data values when writing GeoTiff (9e5525c)

This is the second half of the issue: a (0, null) raster came back from a GeoTIFF round trip as (0, 0). GDAL_NODATA is one value for the whole file, so that state cannot be written faithfully. RS_AsGeoTiff now throws IllegalArgumentException on it, rather than resurrecting a no-data value on a band that had it cleared.

This commit needs a maintainer decision, because its blast radius is wider than the clear path. RasterUtils.copyRasterAndAppendBand sets the appended band's no-data value and leaves the existing bands' sample dimensions alone, so RS_AddBand and RS_Union routinely produce rasters whose bands disagree. Writing one of those to GeoTIFF silently rewrites the other bands' no-data today; with this commit it fails loudly. That follows the issue's preference for rejecting unsupported mixed states, but if it reads as too strong for RS_AsGeoTiff, dropping 9e5525c leaves commit 1 intact and I will open a separate issue for the writer.

How was this patch tested?

New tests in common:

  • RasterBandEditorsTest.testSetBandNoDataValueWithNullKeepsNoDataOnOtherBands covers the issue's reproducer. It checks both bands and map algebra after clearing either one, before serialization, and that the input raster is not mutated.
  • RasterBandEditorsTest.testSetBandNoDataValueWithNullOnEveryBandClearsNoDataProperty covers the case where every band is cleared: the sentinel is dropped from the coverage and the image, map algebra reads the zeros as ordinary data, and the state survives a GeoTIFF round trip.
  • RasterOutputTest.testAsGeoTiffRejectsBandsWithDifferentNoDataValues covers "only one band set" and "both set but different". Rasters whose bands agree, and rasters with no no-data value at all, still round-trip.

mvn -pl common test was run before and after the change on Windows with JDK 17. The same 13 tests fail both times: RasterBandEditorsTest.testClip, 5 RasterEditorsTest.testResample*, and 7 RasterOutputTest.testAsMatrix*. They fail identically on unmodified 2b06236, so they are pre-existing on this platform and unrelated to this change. Everything else passes, including the three new tests.

I could not run spark/common locally. It fails to compile on the generated OSM PBF protobuf sources (package proto4 does not exist) before reaching any raster code, so those suites are left to CI.

One note for whoever reviews the tests. MapAlgebra caches a compiled Jiffle runtime per script text in a ThreadLocal, so I checked whether a cached runtime could mask this regression by carrying a previous raster's no-data handling into a later call with the same script. It does not: setSourceImage re-reads the property. The tests therefore use a plain shared script.

Did this PR include necessary documentation updates?

  • Yes, I have updated the documentation. RS_SetBandNoDataValue now states that no-data is tracked per band, and RS_AsGeoTiff documents the single-value-per-file constraint and the new rejection.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P6hpqAnXfeaWRLkL1VQDMd

james-willis and others added 5 commits September 2, 2026 10:26
…e in Spark SQL

The docs and the common Java implementation say a null noDataValue removes
the band's no-data value, but the catalyst expression null-propagated on any
null argument, so RS_SetBandNoDataValue(raster, 1, NULL) returned a null
raster and the documented clear path was unreachable from SQL. Route the
2- and 3-arg overloads through the allowRightNull pattern (adding an arity-3
variant) so a null noDataValue reaches the Java implementation. The 4-arg
replace overload still null-propagates since replacing pixels with a null
no-data value is meaningless.
…hrough GeoTIFF round trips

The null path consulted band 1's no-data value instead of the target
band's, so clearing band N was a no-op whenever band 1 had none.

Clearing now also drops the GC_NODATA sentinel from the coverage
properties and copies the pixels into a property-free image, since the
sentinel rides on the rendered image itself through serialization.

RS_AsGeoTiff only writes GDAL_NODATA when the raster actually has a
no-data value: GeoTiffWriter otherwise writes a default of 0, which
resurrected cleared (and never-set) no-data values on re-read.
…the GeoTIFF nodata opt-out

Clearing no longer copies pixels. RenderedImageAdapter cannot mask an
inherited property (it declares getProperty final and answers from the
source), so PropertyMaskedRenderedImage wraps the image and hides
GC_NODATA while delegating everything else. That keeps a lazily decoded
raster undecoded and preserves a non-zero image origin, which the
previous copyData path lost -- BufferedImage rejects a raster whose
minX/minY are not zero.

GridCoverage2D.getProperty falls through to the rendered image, so the
sentinel is dropped from the coverage property map as well, on a
defensive copy so the input raster is left untouched. GeoTools' own
CoverageUtilities.getNoDataProperty now reports null for a cleared
raster.

RS_AsGeoTiff only forces WRITE_NODATA off when no band has a no-data
value; forcing it on otherwise would override an explicit
-Dgeotiff.writenodata=false.
Clearing a band's no-data value dropped the GC_NODATA sentinel from the
coverage properties and the rendered image unconditionally. That sentinel
is a single value for the whole coverage, while no-data values are tracked
per band on the sample dimensions, so clearing one band also stopped
GeoTools operations that read the sentinel from honouring the bands that
still declared a no-data value. Jiffle map algebra reads it, so on a
two-band raster with no-data 0 on both bands, clearing band 2 changed
band 1's result for `out = rast[0] + 1;` from NaN to 1.0.

Drop the sentinel only once no band declares a no-data value at all.
GeoTiff stores one GDAL_NODATA value for the whole file, so a raster whose
bands disagree about their no-data value cannot be written faithfully:
every band reads back with whichever value was written. A two-band raster
at (0, null) came back as (0, 0), silently resurrecting a no-data value on
a band that had it cleared.

Reject the write. Rasters whose bands agree, including rasters with no
no-data value at all, are unaffected.
@Eliaaazzz
Eliaaazzz requested a review from jiayuasu as a code owner September 6, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clearing NoData on one raster band changes another band's MapAlgebra results

2 participants