This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 869a6d4541d78390e8083f65d82eba8b68666185 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Nov 30 14:13:41 2021 +0100 Allow to specify default color palettes for netCDF variables. This is an internal API for now, with colors depending on conventions. --- .../apache/sis/coverage/grid/ImageRenderer.java | 69 +++++++++++++++++++++- .../sis/internal/coverage/j2d/Colorizer.java | 3 +- .../apache/sis/internal/earth/netcdf/GCOM_C.java | 31 ++++++++++ .../org/apache/sis/internal/netcdf/Convention.java | 26 +++++++- .../org/apache/sis/internal/netcdf/Raster.java | 28 ++++++++- .../apache/sis/internal/netcdf/RasterResource.java | 31 +++++++--- 6 files changed, 173 insertions(+), 15 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java index e269dc3..bf2f424 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java @@ -16,9 +16,11 @@ */ package org.apache.sis.coverage.grid; -import java.util.Hashtable; import java.util.Arrays; +import java.util.Hashtable; +import java.util.function.Function; import java.nio.Buffer; +import java.awt.Color; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.ColorModel; @@ -37,6 +39,7 @@ import org.apache.sis.image.DataType; import org.apache.sis.coverage.SubspaceNotSpecifiedException; import org.apache.sis.coverage.MismatchedCoverageRangeException; import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.coverage.Category; import org.apache.sis.internal.coverage.j2d.Colorizer; import org.apache.sis.internal.coverage.j2d.DeferredProperty; import org.apache.sis.internal.coverage.j2d.RasterFactory; @@ -247,7 +250,10 @@ public class ImageRenderer { private int[] bankIndices; /** - * The band to be made visible (usually 0). All other bands, if any will be ignored. + * The band to use for defining pixel colors when the image is displayed on screen. + * All other bands, if any, will exist in the raster but be ignored at display time. + * + * @see #setVisibleBand(int) */ private int visibleBand; @@ -258,6 +264,15 @@ public class ImageRenderer { private DataBuffer buffer; /** + * The colors to use for each category. Never {@code null}. + * The function may return {@code null}, which means transparent. + * The default value is {@link Colorizer#GRAYSCALE}. + * + * @see #setCategoryColors(Function) + */ + private Function<Category,Color[]> colors; + + /** * The properties to give to the image, or {@code null} if none. * * @see #addProperty(String, Object) @@ -347,6 +362,7 @@ public class ImageRenderer { this.pixelStride = toIntExact(pixelStride); this.scanlineStride = toIntExact(scanlineStride); this.offsetZ = offsetZ; + this.colors = Colorizer.GRAYSCALE; } /** @@ -603,6 +619,53 @@ public class ImageRenderer { } /** + * Specifies the band to use for defining pixel colors when the image is displayed on screen. + * All other bands, if any, will exist in the raster but be ignored at display time. + * The default value is 0, the first (and often only) band. + * + * <div class="note"><b>Implementation note:</b> + * an {@link java.awt.image.IndexColorModel} will be used for displaying the image.</div> + * + * @param band the band to use for display purpose. + * @throws IllegalArgumentException if the given band is not between 0 (inclusive) + * and {@link #getNumBands()} (exclusive). + * + * @since 1.2 + */ + public void setVisibleBand(final int band) { + ArgumentChecks.ensureBetween("band", 0, getNumBands() - 1, band); + visibleBand = band; + } + + /** + * Specifies the colors to apply for each category in a sample dimension. + * The given function can return {@code null}, which means transparent. + * If this method is never invoked, then the default is a grayscale for + * {@linkplain Category#isQuantitative() quantitative categories} and + * transparent for qualitative categories (typically "no data" values). + * + * <h4>Example</h4> + * the following code specifies a color palette from blue to red with white in the middle. + * This is useful for data with a clear 0 (white) in the middle of the range, + * with a minimal value equals to the negative of the maximal value. + * + * {@preformat java + * setCategoryColors((category) -> category.isQuantitative() ? new Color[] { + * Color.BLUE, Color.CYAN, Color.WHITE, Color.YELLOW, Color.RED + * } : null); + * } + * + * @param colors the colors to use for each category. The {@code colors} argument can not be null, + * but {@code colors.apply(Category)} can return null. + * + * @since 1.2 + */ + public void setCategoryColors(final Function<Category,Color[]> colors) { + ArgumentChecks.ensureNonNull("colors", colors); + this.colors = colors; + } + + /** * Creates a raster with the data specified by the last call to a {@code setData(…)} method. * The raster upper-left corner is located at the position given by {@link #getBounds()}. * The returned raster is often an instance of {@link WritableRaster}, but read-only rasters are also allowed. @@ -666,7 +729,7 @@ public class ImageRenderer { @SuppressWarnings("UseOfObsoleteCollectionType") public RenderedImage createImage() { final Raster raster = createRaster(); - final Colorizer colorizer = new Colorizer(Colorizer.GRAYSCALE); + final Colorizer colorizer = new Colorizer(colors); final ColorModel colors; if (colorizer.initialize(bands[visibleBand]) || colorizer.initialize(raster.getSampleModel(), visibleBand)) { colors = colorizer.createColorModel(buffer.getDataType(), bands.length, visibleBand); diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java index 1b3a83f..26e1c45 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/Colorizer.java @@ -98,7 +98,8 @@ public final class Colorizer { /** * Blue to red color palette with white in the middle. Useful for data with a clear 0 (white) - * in the range center and negative and positive values (to appear blue and red respectively). + * in the middle of the range and with minimal value equals to the negative of maximal value + * (to appear blue and red respectively). * Used for debugging purposes; production code should use a {@code PaletteFactory} instead. */ @Debug diff --git a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java index 9df14da..3e23d77 100644 --- a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java +++ b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java @@ -16,12 +16,15 @@ */ package org.apache.sis.internal.earth.netcdf; +import java.util.Objects; import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.Collections; import java.util.LinkedHashSet; import java.util.regex.Pattern; +import java.util.function.Function; +import java.awt.Color; import javax.measure.Unit; import javax.measure.format.ParserException; import org.opengis.referencing.crs.ProjectedCRS; @@ -41,6 +44,7 @@ import org.apache.sis.referencing.operation.transform.TransferFunction; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.referencing.operation.matrix.Matrix3; import org.apache.sis.referencing.CommonCRS; +import org.apache.sis.coverage.Category; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.Units; import ucar.nc2.constants.CF; @@ -536,4 +540,31 @@ public final class GCOM_C extends Convention { } return super.getUnitFallback(data); } + + /** + * Returns the colors to use for each category, or {@code null} for the default colors. + * + * @param data the variable for which to get the colors. + * @return colors to use for each category, or {@code null} for the default. + */ + @Override + public Function<Category,Color[]> getColors(final Variable data) { + if (QA_FLAG.equals(data.getName())) { + return (category) -> { + final NumberRange<?> range = category.getSampleRange(); + if (Objects.equals(range.getMinValue(), range.getMaxValue())) { + return null; // A "no data" value. + } + /* + * Following colors are not really appropriate for "QA_flag" because that variable is a bitmask + * rather than a continuous coverage. Following code may be replaced by a better color palette + * in a future version. + */ + return new Color[] { + Color.BLUE, Color.CYAN, Color.YELLOW, Color.RED + }; + }; + } + return super.getColors(data); + } } diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java index 19f26c8..d73cf32 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java @@ -23,6 +23,8 @@ import java.util.Iterator; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Locale; +import java.util.function.Function; +import java.awt.Color; import javax.measure.Unit; import javax.measure.format.ParserException; import org.opengis.referencing.crs.ProjectedCRS; @@ -35,6 +37,7 @@ import org.apache.sis.referencing.CommonCRS; import org.apache.sis.internal.referencing.LazySet; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.measure.NumberRange; +import org.apache.sis.coverage.Category; import org.apache.sis.math.Vector; import org.apache.sis.util.Numbers; import org.apache.sis.util.resources.Errors; @@ -64,7 +67,7 @@ import ucar.nc2.constants.CF; * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.1 + * @version 1.2 * * @see <a href="https://issues.apache.org/jira/browse/SIS-315">SIS-315</a> * @@ -766,4 +769,25 @@ public class Convention { public Unit<?> getUnitFallback(final Variable data) throws ParserException { return null; } + + /** + * Returns the band to use for defining pixel colors when the image is displayed on screen. + * All other bands, if any, will exist in the raster but be ignored at display time. + * The default value is 0, the first (and often only) band. + * + * @return the band on which {@link #getColors(Variable)} will apply. + */ + public int getVisibleBand() { + return 0; + } + + /** + * Returns the colors to use for each category, or {@code null} for the default colors. + * + * @param data the variable for which to get the colors. + * @return colors to use for each category, or {@code null} for the default. + */ + public Function<Category,Color[]> getColors(final Variable data) { + return null; + } } diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java index c17ea2e..44196cd 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java @@ -17,10 +17,13 @@ package org.apache.sis.internal.netcdf; import java.util.List; +import java.util.function.Function; +import java.awt.Color; import java.awt.image.DataBuffer; import java.awt.image.RenderedImage; import java.awt.image.RasterFormatException; import org.opengis.coverage.CannotEvaluateException; +import org.apache.sis.coverage.Category; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridExtent; @@ -40,7 +43,7 @@ import org.apache.sis.coverage.grid.BufferedGridCoverage; * but it is {@link ImageRenderer} responsibility to perform this substitution as an optimization.</p> * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.0 * @module */ @@ -57,20 +60,37 @@ final class Raster extends BufferedGridCoverage { private final int[] bandOffsets; /** + * The band to use for defining pixel colors when the image is displayed on screen. + * All other bands, if any, will exist in the raster but be ignored at display time. + * + * @see Convention#getVisibleBand() + */ + private final int visibleBand; + + /** * Name to display in error messages. Not to be used for processing. */ private final String label; /** + * The colors to use for each category, or {@code null} for default. + * The function may return {@code null}, which means transparent. + */ + private final Function<Category,Color[]> colors; + + /** * Creates a new raster from the given resource. */ Raster(final GridGeometry domain, final List<SampleDimension> range, final DataBuffer data, - final int pixelStride, final int[] bandOffsets, final String label) + final String label, final int pixelStride, final int[] bandOffsets, final int visibleBand, + final Function<Category,Color[]> colors) { super(domain, range, data); this.label = label; + this.colors = colors; this.pixelStride = pixelStride; this.bandOffsets = bandOffsets; + this.visibleBand = visibleBand; } /** @@ -85,6 +105,10 @@ final class Raster extends BufferedGridCoverage { if (bandOffsets != null) { renderer.setInterleavedPixelOffsets(pixelStride, bandOffsets); } + if (colors != null) { + renderer.setCategoryColors(colors); + } + renderer.setVisibleBand(visibleBand); return renderer.createImage(); } catch (IllegalArgumentException | ArithmeticException | RasterFormatException e) { throw new CannotEvaluateException(Resources.format(Resources.Keys.CanNotRender_2, label, e), e); diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java index 7001f2e..4fa11f7 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java @@ -152,6 +152,14 @@ public final class RasterResource extends AbstractGridResource implements Resour private final int bandDimension; /** + * The band to use for defining pixel colors when the image is displayed on screen. + * All other bands, if any, will exist in the raster but be ignored at display time. + * + * @see Convention#getVisibleBand() + */ + private final int visibleBand; + + /** * Path to the netCDF file for information purpose, or {@code null} if unknown. * * @see #getComponentFiles() @@ -182,13 +190,14 @@ public final class RasterResource extends AbstractGridResource implements Resour final int numBands, final int bandDim, final Object lock) { super(decoder.listeners); - data = bands.toArray(new Variable[bands.size()]); - ranges = new SampleDimension[numBands]; - identifier = decoder.nameFactory.createLocalName(decoder.namespace, name); - location = decoder.location; + this.lock = lock; gridGeometry = grid; bandDimension = bandDim; - this.lock = lock; + location = decoder.location; + identifier = decoder.nameFactory.createLocalName(decoder.namespace, name); + visibleBand = decoder.convention().getVisibleBand(); + ranges = new SampleDimension[numBands]; + data = bands.toArray(new Variable[bands.size()]); assert data.length == (bandDimension >= 0 ? 1 : ranges.length); } @@ -523,7 +532,7 @@ public final class RasterResource extends AbstractGridResource implements Resour * Adds the "missing value" or "fill value" as qualitative categories. If a value has both roles, use "missing value" * as category name. If the sample values are already real values, then the "no data" values have been replaced by NaN * values by Variable.replaceNaN(Object). The qualitative categories constructed below must be consistent with the NaN - * values created by 'replaceNaN'. + * values created by `replaceNaN`. */ boolean setBackground = true; int ordinal = band.hasRealValues() ? 0 : -1; @@ -659,7 +668,7 @@ public final class RasterResource extends AbstractGridResource implements Resour } /* * Iterate over netCDF variables in the order they appear in the file, not in the order requested - * by the 'range' argument. The intent is to perform sequential I/O as much as possible, without + * by the `range` argument. The intent is to perform sequential I/O as much as possible, without * seeking backward. In the (uncommon) case where bands are one of the variable dimension instead * than different variables, the reading of the whole variable occurs during the first iteration. */ @@ -721,11 +730,17 @@ public final class RasterResource extends AbstractGridResource implements Resour throw new DataStoreContentException(canNotReadFile(), e); } } + /* + * At this point the I/O operation is completed and sample values have been stored in a NIO buffer. + * Provide to `Raster` all information needed for building a `RenderedImage` when requested. + */ if (imageBuffer == null) { throw new DataStoreContentException(Errors.getResources(getLocale()).getString(Errors.Keys.UnsupportedType_1, dataType.name())); } + final Variable main = data[visibleBand]; final Raster raster = new Raster(domain, UnmodifiableArrayList.wrap(bands), imageBuffer, - rangeIndices.getPixelStride(), bandOffsets, String.valueOf(identifier)); + String.valueOf(identifier), rangeIndices.getPixelStride(), bandOffsets, visibleBand, + main.decoder.convention().getColors(main)); logReadOperation(location, domain, startTime); return raster; }