You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/04/01 16:38:01 UTC
[sis] branch geoapi-4.0 updated: When no color is specified for a category or a range of sample values, and provided that `Colorizer` is used for styling an existing image, preserve the existing colors.
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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new d0147d6a96 When no color is specified for a category or a range of sample values, and provided that `Colorizer` is used for styling an existing image, preserve the existing colors.
d0147d6a96 is described below
commit d0147d6a9634b5ee11881fcf693dcd1d179caff4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Apr 1 18:35:42 2023 +0200
When no color is specified for a category or a range of sample values,
and provided that `Colorizer` is used for styling an existing image,
preserve the existing colors.
---
.../apache/sis/gui/coverage/CoverageCanvas.java | 58 +++++++-----
.../apache/sis/gui/coverage/CoverageControls.java | 2 +-
.../apache/sis/gui/coverage/CoverageStyling.java | 18 ++--
.../apache/sis/internal/gui/control/ColorCell.java | 2 +-
.../java/org/apache/sis/coverage/Category.java | 4 +-
.../sis/coverage/grid/GridCoverageBuilder.java | 2 +-
.../apache/sis/coverage/grid/ImageRenderer.java | 7 +-
.../apache/sis/image/BandedSampleConverter.java | 2 +-
.../main/java/org/apache/sis/image/Colorizer.java | 22 ++---
.../java/org/apache/sis/image/RecoloredImage.java | 3 +-
.../java/org/apache/sis/image/Visualization.java | 13 ++-
.../internal/coverage/j2d/ColorModelBuilder.java | 56 ++++++++---
.../internal/coverage/j2d/ColorModelFactory.java | 13 ++-
.../sis/internal/coverage/j2d/ColorsForRange.java | 105 ++++++++++++++++-----
.../coverage/j2d/ColorModelBuilderTest.java | 4 +-
.../sis/internal/map/coverage/RenderingData.java | 2 +-
.../java/org/apache/sis/measure/NumberRange.java | 7 +-
.../main/java/org/apache/sis/measure/Range.java | 23 ++++-
.../java/org/apache/sis/measure/RangeTest.java | 17 +++-
19 files changed, 248 insertions(+), 112 deletions(-)
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index 735b28b475..5cfe75de75 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -21,7 +21,6 @@ import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Future;
-import java.util.function.Function;
import java.util.logging.LogRecord;
import java.io.IOException;
import java.awt.Graphics2D;
@@ -54,7 +53,6 @@ import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
-import org.apache.sis.coverage.Category;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.coverage.grid.GridCoverage;
@@ -62,6 +60,7 @@ import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.geometry.Envelope2D;
import org.apache.sis.geometry.Shapes2D;
+import org.apache.sis.image.Colorizer;
import org.apache.sis.image.PlanarImage;
import org.apache.sis.image.Interpolation;
import org.apache.sis.storage.GridCoverageResource;
@@ -407,21 +406,44 @@ public class CoverageCanvas extends MapCanvasAWT {
* @see #interpolationProperty
*/
public final void setInterpolation(final Interpolation interpolation) {
- assert Platform.isFxApplicationThread();
interpolationProperty.set(interpolation);
}
/**
- * Sets the colors to use for given categories in image. Invoking this method causes a repaint event,
- * so it should be invoked only if at least one color is known to have changed.
+ * Sets the colorization algorithm to apply on rendered images.
+ * Should be an algorithm based on coverage categories.
*
- * @param colors colors to use for arbitrary categories of sample values, or {@code null} for default.
+ * <p>{@code CoverageCanvas} can not detect when the given colorizer changes its internal state.
+ * The {@link #stylingChanged()} method should be invoked explicitly when such change occurs.</p>
+ *
+ * @param colorizer colorization algorithm to apply on computed image, or {@code null} for default.
*/
- final void setCategoryColors(final Function<Category, java.awt.Color[]> colors) {
+ final void setColorizer(final Colorizer colors) {
+ data.processor.setColorizer(colors);
+ stylingChanged();
+ }
+
+ /**
+ * Invoked by {@link CoverageControls} when the user selected a new color stretching mode.
+ * The sample values are assumed the same, only the image appearance is modified.
+ */
+ final void setStretching(final Stretching selection) {
if (TRACE) {
- trace("setCategoryColors(Function): causes repaint.");
+ trace("setStretching(%s)", selection);
}
- data.processor.setCategoryColors(colors);
+ if (data.selectedDerivative != selection) {
+ data.selectedDerivative = selection;
+ stylingChanged();
+ }
+ }
+
+ /**
+ * Invoked when image colors changed. Derived features such are isolines are assumed unchanged.
+ * This method should be invoked explicitly when the {@link Colorizer} changes its internal state.
+ *
+ * @see #clearRenderedImage()
+ */
+ final void stylingChanged() {
resampledImage = null;
requestRepaint();
}
@@ -701,8 +723,7 @@ public class CoverageCanvas extends MapCanvasAWT {
trace("onInterpolationSpecified(%s)", newValue);
}
data.processor.setInterpolation(newValue);
- resampledImage = null;
- requestRepaint();
+ stylingChanged();
}
/**
@@ -1114,21 +1135,6 @@ public class CoverageCanvas extends MapCanvasAWT {
return Shapes2D.transform(MathTransforms.bidimensional(getObjectiveToDisplay().inverse()), displayBounds, null);
}
- /**
- * Invoked by {@link CoverageControls} when the user selected a new color stretching mode.
- * The sample values are assumed the same; only the image appearance is modified.
- */
- final void setStyling(final Stretching selection) {
- if (TRACE) {
- trace("setStyling(%s)", selection);
- }
- if (data.selectedDerivative != selection) {
- data.selectedDerivative = selection;
- resampledImage = null;
- requestRepaint();
- }
- }
-
/**
* Invoked when an exception occurred while computing a transform but the painting process can continue.
*/
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index 48e6437384..719edb2fb2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -122,7 +122,7 @@ final class CoverageControls extends ViewAndControls {
* - Color stretching
*/
interpolation = InterpolationConverter.button(view);
- stretching = Stretching.createButton((p,o,n) -> view.setStyling(n));
+ stretching = Stretching.createButton((p,o,n) -> view.setStretching(n));
final GridPane valuesControl = Styles.createControlGrid(0,
label(vocabulary, Vocabulary.Keys.Interpolation, interpolation),
label(vocabulary, Vocabulary.Keys.Stretching, stretching));
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
index 987f588f7a..07fa93f7e1 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageStyling.java
@@ -32,6 +32,7 @@ import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContextMenu;
import org.opengis.util.InternationalString;
import org.apache.sis.coverage.Category;
+import org.apache.sis.image.Colorizer;
import org.apache.sis.internal.gui.Resources;
import org.apache.sis.internal.gui.ImmutableObjectProperty;
import org.apache.sis.internal.gui.control.ColorRamp;
@@ -53,7 +54,7 @@ final class CoverageStyling extends ColorColumnHandler<Category> implements Func
/**
* Customized colors selected by user. Keys are English names of categories.
*
- * @see #key(Category)
+ * @see #apply(Category)
*/
private final Map<String,ColorRamp> customizedColors;
@@ -65,9 +66,13 @@ final class CoverageStyling extends ColorColumnHandler<Category> implements Func
/**
* Creates a new styling instance.
*/
+ @SuppressWarnings("ThisEscapedInObjectConstruction")
CoverageStyling(final CoverageCanvas canvas) {
customizedColors = new HashMap<>();
this.canvas = canvas;
+ if (canvas != null) {
+ canvas.setColorizer(Colorizer.forCategories(this));
+ }
}
/**
@@ -77,7 +82,7 @@ final class CoverageStyling extends ColorColumnHandler<Category> implements Func
final void copyStyling(final CoverageStyling source) {
customizedColors.putAll(source.customizedColors);
if (canvas != null) {
- canvas.setCategoryColors(customizedColors.isEmpty() ? null : this);
+ canvas.stylingChanged();
}
}
@@ -92,7 +97,7 @@ final class CoverageStyling extends ColorColumnHandler<Category> implements Func
customizedColors.clear();
items.setAll(content);
if (canvas != null) {
- canvas.setCategoryColors(null);
+ canvas.stylingChanged();
}
}
@@ -146,8 +151,8 @@ final class CoverageStyling extends ColorColumnHandler<Category> implements Func
}
/**
- * Associates colors to the given category.
- * This is invoked when users confirmed that (s)he wants to use the selected colors.
+ * Associates colors to the given category. This method is invoked when new categories are shown
+ * in table column managed by this {@code CoverageStyling}, and when user selects new colors.
*
* @param category the category for which to assign new color(s).
* @param colors the new color for the given category, or {@code null} for resetting default value.
@@ -163,8 +168,7 @@ final class CoverageStyling extends ColorColumnHandler<Category> implements Func
old = customizedColors.remove(key);
}
if (canvas != null && !Objects.equals(colors, old)) {
- canvas.setCategoryColors(customizedColors.isEmpty() ? null : this);
- // Above method call causes a repaint event even if value is the same.
+ canvas.stylingChanged();
}
return category.isQuantitative() ? ColorRamp.Type.GRADIENT : ColorRamp.Type.SOLID;
}
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
index 87e3df3fe1..c4a206d230 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/control/ColorCell.java
@@ -292,7 +292,7 @@ final class ColorCell<S> extends TableCell<S,ColorRamp> implements EventHandler<
if (row != null) {
final S item = row.getItem();
if (item != null) {
- type = handler.applyColors(item, colors);
+ type = handler.applyColors(item, (colors != ColorRamp.DEFAULT) ? colors : null);
}
}
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
index 59efc35e42..df3901ef6c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/Category.java
@@ -179,8 +179,8 @@ public class Category implements Serializable {
* Creates a copy of the given category except for the {@link #converse} and {@link #toConverse} fields.
* This constructor serves two purposes:
* <ul>
- * <li>If {@code caller} is null, then {@link #toConverse} is is set to identity.
- * This is used only if a user specify a {@code ConvertedCategory} to {@link SampleDimension} constructor.
+ * <li>If {@code caller} is null, then {@link #toConverse} is set to identity.
+ * This is used only if a user specifies a {@code ConvertedCategory} to {@link SampleDimension} constructor.
* Such converted category can only come from another {@code SampleDimension} and may have inconsistent
* information for the new sample dimension that the user is creating.</li>
* <li>If {@code caller} is non-null, then {@link #toConverse} is set to the same transform than {@code copy} and
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
index 15a52e41ad..5e46bc3419 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageBuilder.java
@@ -470,7 +470,7 @@ public class GridCoverageBuilder {
*/
bands = GridCoverage2D.defaultIfAbsent(bands, null, raster.getNumBands());
final SampleModel sm = raster.getSampleModel();
- final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+ final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
final ColorModel colors;
if (colorizer.initialize(sm, bands.get(visibleBand)) || colorizer.initialize(sm, visibleBand)) {
colors = colorizer.createColorModel(ImageUtilities.getBandType(sm), bands.size(), visibleBand);
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 31545550ea..1d5bd9e17e 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
@@ -661,8 +661,9 @@ public class ImageRenderer {
/**
* 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
+ * The given function can return {@code null} for unrecognized categories.
+ * If this method is never invoked, or if a category is unrecognized,
+ * then the default is a grayscale for
* {@linkplain Category#isQuantitative() quantitative categories} and
* transparent for qualitative categories (typically "no data" values).
*
@@ -752,7 +753,7 @@ public class ImageRenderer {
@SuppressWarnings("UseOfObsoleteCollectionType")
public RenderedImage createImage() {
final Raster raster = createRaster();
- final ColorModelBuilder colorizer = new ColorModelBuilder(colors);
+ final ColorModelBuilder colorizer = new ColorModelBuilder(colors, null);
final ColorModel colors;
final SampleModel sm = raster.getSampleModel();
if (colorizer.initialize(sm, bands[visibleBand]) || colorizer.initialize(sm, visibleBand)) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
index 8cc2527aa5..3ba4de609c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandedSampleConverter.java
@@ -238,7 +238,7 @@ class BandedSampleConverter extends ComputedImage {
if (sampleDimensions != null && visibleBand >= 0 && visibleBand < sampleDimensions.length) {
sd = sampleDimensions[visibleBand];
}
- final var builder = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+ final var builder = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
if (builder.initialize(source.getSampleModel(), sd) ||
builder.initialize(source.getColorModel()))
{
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
index 8888913868..f926f3e84e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java
@@ -191,18 +191,19 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode
* this colorizer creates a non-standard (and potentially slow) color model.</p>
*
* <h4>Default colors</h4>
- * The {@code colors} map shall not be null or empty but may contain {@code null} values.
- * Those null values are translated to default sets of colors in an implementation dependent way.
+ * The given {@code colors} map can associate to some keys a null or an empty color arrays.
+ * An empty array (i.e. no color) is interpreted as an explicit request for transparency.
+ * But null values are interpreted as unspecified colors,
+ * in which case the defaults are implementation dependent.
* In current implementation, the defaults are:
*
* <ul>
- * <li>If the range minimum and maximum values are not equal, default to grayscale colors.</li>
+ * <li>If this colorizer is used for {@linkplain ImageProcessor#visualize(RenderedImage) visualization},
+ * try to keep the existing colors of the image to visualize.</li>
+ * <li>Otherwise if the range minimum and maximum values are not equal, default to grayscale colors.</li>
* <li>Otherwise default to a fully transparent color.</li>
* </ul>
*
- * Those defaults may change in any future Apache SIS version.
- * For example a future version may first tries to preserve the existing colors of an image.
- *
* <h4>Limitations</h4>
* In current implementation, the non-standard color model ignores the specified colors.
* If the image data type is not 8 or 16 bits integer, the colors are always grayscale.
@@ -247,14 +248,13 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode
* In current implementation, the defaults are:
*
* <ul>
- * <li>If all categories are unrecognized, then the colorizer returns an empty value.</li>
+ * <li>If this colorizer is used for {@linkplain ImageProcessor#visualize(RenderedImage) visualization},
+ * try to keep the existing colors of the image to visualize.</li>
+ * <li>Otherwise if all categories are unrecognized, then the colorizer returns an empty value.</li>
* <li>Otherwise, {@linkplain Category#isQuantitative() quantitative} categories default to grayscale colors.</li>
* <li>Otherwise qualitative categories default to a fully transparent color.</li>
* </ul>
*
- * Those defaults may change in any future Apache SIS version.
- * For example a future version may first tries to preserve the existing colors of an image.
- *
* <h4>Conditions</h4>
* This colorizer is used when {@link Target#getRanges()} provides a non-empty value.
* That value is typically fetched from the {@value PlanarImage#SAMPLE_DIMENSIONS_KEY} image property,
@@ -280,7 +280,7 @@ public interface Colorizer extends Function<Colorizer.Target, Optional<ColorMode
final List<SampleDimension> ranges = target.getRanges().orElse(null);
if (visibleBand < ranges.size()) {
final SampleModel model = target.getSampleModel();
- final var c = new ColorModelBuilder(colors);
+ final var c = new ColorModelBuilder(colors, null);
if (c.initialize(model, ranges.get(visibleBand))) {
return Optional.ofNullable(c.createColorModel(model.getDataType(), model.getNumBands(), visibleBand));
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
index ce24f13234..564d45b88f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/RecoloredImage.java
@@ -304,8 +304,7 @@ final class RecoloredImage extends ImageAdapter {
Arrays.fill(ARGB, end+1, validMax+1, icm.getRGB(validMax));
final float scale = (float) ((validMax - validMin) / (maximum - minimum));
for (int i = start; i <= end; i++) {
- final float s = (i - start) * scale + validMin;
- ARGB[i] = icm.getRGB(Math.round(s));
+ ARGB[i] = icm.getRGB(Math.round((i - start) * scale) + validMin);
}
final SampleModel sm = source.getSampleModel();
cm = ColorModelFactory.createIndexColorModel(sm.getNumBands(), visibleBand, ARGB, icm.hasAlpha(), icm.getTransparentPixel());
diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
index d83417ecba..ed6313022e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/Visualization.java
@@ -287,6 +287,7 @@ final class Visualization extends ResampledImage {
if (colorizer != null) {
colorModel = colorizer.apply(target).orElse(null);
}
+ final ColorModel sourceCM = coloredSource.getColorModel();
/*
* Get a `ColorModelBuilder` which will compute the `ColorModel` of destination image.
* There is different ways to setup the builder, depending on which `Colorizer` is used.
@@ -300,15 +301,14 @@ final class Visualization extends ResampledImage {
final ColorModelBuilder builder;
final var rangeColors = target.rangeColors;
if (rangeColors != null && !rangeColors.isEmpty()) {
- builder = new ColorModelBuilder(rangeColors.entrySet());
+ builder = new ColorModelBuilder(rangeColors.entrySet(), sourceCM);
initialized = true;
} else {
/*
* Ranges of sample values were not specified explicitly. Instead, we will try to infer them
* in various ways: sample dimensions, scaled color model, or image statistics in last resort.
*/
- builder = new ColorModelBuilder(target.categoryColors);
- final ColorModel colorModel = coloredSource.getColorModel();
+ builder = new ColorModelBuilder(target.categoryColors, sourceCM);
initialized = builder.initialize(coloredSource.getSampleModel(), visibleSD);
if (initialized) {
/*
@@ -317,7 +317,7 @@ final class Visualization extends ResampledImage {
* determined by the SampleModel, then user enhanced contrast by a call to `stretchColorRamp(…)`.
* We want to preserve that contrast enhancement.
*/
- builder.rescaleMainRange(colorModel);
+ builder.rescaleMainRange(sourceCM);
} else {
/*
* At this point there is no more user-supplied colors (through `Colorizer`) that we can use.
@@ -325,7 +325,7 @@ final class Visualization extends ResampledImage {
* There is no call to `rescaleMainRange(…)` because the following code already uses the range
* specified by the ColorModel, if available.
*/
- initialized = builder.initialize(colorModel);
+ initialized = builder.initialize(sourceCM);
if (!initialized) {
if (coloredSource instanceof RecoloredImage) {
final RecoloredImage colored = (RecoloredImage) coloredSource;
@@ -353,6 +353,9 @@ final class Visualization extends ResampledImage {
builder.getSampleToIndexValues() // Must be after `compactColorModel(…)`.
};
if (shortcut) {
+ if (converters[0].isIdentity() && colorModel.equals(sourceCM)) {
+ return coloredSource;
+ }
interpolation = Interpolation.NEAREST;
} else {
interpolation = combine(interpolation.toCompatible(source), converters);
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
index 455899fa16..2243187865 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilder.java
@@ -51,8 +51,9 @@ import org.apache.sis.util.resources.Vocabulary;
* <ol>
* <li>Create a new {@link ColorModelBuilder} instance.</li>
* <li>Invoke one of {@code initialize(…)} methods.</li>
- * <li>Invoke {@link #createColorModel(int, int, int)}.</li>
- * <li>Discards {@code ColorModelBuilder}; each instance should be used only once.</li>
+ * <li>Invoke {@link #createColorModel(int, int, int)} or {@link #compactColorModel(int, int)}.</li>
+ * <li>Invoke {@link #getSampleToIndexValues()} if this auxiliary information is useful.</li>
+ * <li>Discards {@code ColorModelBuilder}. Each instance shall be used only once.</li>
* </ol>
*
* There is no {@code initialize(Raster)} or {@code initialize(RenderedImage)} method because if those methods
@@ -126,8 +127,8 @@ public final class ColorModelBuilder {
/**
* The colors to use for each range of values in the source image.
- * Entries will be sorted and modified in place.
- * The array may be null if unspecified, but shall not contain null element.
+ * This array is initially null and created by an {@code initialize(…)} method.
+ * After initialization, this array shall not contain null element.
*/
private ColorsForRange[] entries;
@@ -146,14 +147,26 @@ public final class ColorModelBuilder {
* <p>This sample dimension should not be returned to the user because it may not contain meaningful values.
* For example, it may contain an "artificial" transfer function for computing a {@link MathTransform1D} from
* source range to the [0 … 255] value range.</p>
+ *
+ * @see #getSampleToIndexValues()
*/
private SampleDimension target;
/**
- * Default range of values to use if no explicitly specified by a {@link Category}.
+ * Default range of values to use if not explicitly specified by a {@link Category}.
*/
private NumberRange<?> defaultRange;
+ /**
+ * Colors to inherit if a range of values is undefined, or {@code null} if none.
+ * This field should be non-null only when this builder is used for styling an image before visualization.
+ * This field should be null when this builder is created for creating a new image because the meaning of
+ * pixel values may be completely different (i.e. meaning of {@linkplain #source} may not be applicable).
+ *
+ * @see ColorsForRange#isUndefined()
+ */
+ private final ColorModel inheritedColors;
+
/**
* Creates a new colorizer which will apply colors on the given range of values in source image.
* The {@code ColorModelBuilder} is considered initialized after this constructor;
@@ -164,11 +177,14 @@ public final class ColorModelBuilder {
* and to grayscale colors otherwise.
* Empty arrays of colors are interpreted as explicitly transparent.</p>
*
- * @param colors the colors to use for each range of values in source image.
+ * @param colors the colors to use for each range of values in source image.
+ * @param inherited the colors to use as fallback if some ranges have undefined colors, or {@code null}.
+ * Should be non-null only for styling an exiting image before visualization.
*/
- public ColorModelBuilder(final Collection<Map.Entry<NumberRange<?>,Color[]>> colors) {
+ public ColorModelBuilder(final Collection<Map.Entry<NumberRange<?>,Color[]>> colors, final ColorModel inherited) {
ArgumentChecks.ensureNonEmpty("colors", colors);
- entries = ColorsForRange.list(colors);
+ entries = ColorsForRange.list(colors, inherited);
+ inheritedColors = inherited;
this.colors = GRAYSCALE;
}
@@ -176,11 +192,19 @@ public final class ColorModelBuilder {
* Creates a new colorizer which will use the given function for determining the colors to apply.
* Callers need to invoke an {@code initialize(…)} method after this constructor.
*
- * @param colors the colors to use for each category, or {@code null} for default.
- * The function may return {@code null} for unrecognized categories.
+ * <p>The {@code inherited} parameter is non-null when this builder is created for styling
+ * an existing image before visualization. This parameter should be null when this builder
+ * is created for creating a new image, even when that new image is derived from a source,
+ * because the meaning of pixel values may be completely different.</p>
+ *
+ * @param colors the colors to use for each category, or {@code null} for default.
+ * The function may return {@code null} for unrecognized categories.
+ * @param inherited the colors to use as fallback for unrecognized categories, or {@code null}.
+ * Should be non-null only for styling an exiting image before visualization.
*/
- public ColorModelBuilder(final Function<Category,Color[]> colors) {
+ public ColorModelBuilder(final Function<Category,Color[]> colors, final ColorModel inherited) {
this.colors = (colors != null) ? colors : GRAYSCALE;
+ inheritedColors = inherited;
}
/**
@@ -222,7 +246,7 @@ public final class ColorModelBuilder {
boolean missingNodata = true;
ColorsForRange[] entries = new ColorsForRange[categories.size()];
for (int i=0; i<entries.length; i++) {
- final var range = new ColorsForRange(categories.get(i), colors);
+ final var range = new ColorsForRange(categories.get(i), colors, inheritedColors);
isUndefined &= range.isUndefined();
missingNodata &= range.isData;
entries[i] = range;
@@ -236,7 +260,7 @@ public final class ColorModelBuilder {
final int count = entries.length;
entries = Arrays.copyOf(entries, count + 1);
entries[count] = new ColorsForRange(TRANSPARENT,
- NumberRange.create(Float.class, Float.NaN), null, false);
+ NumberRange.create(Float.class, Float.NaN), null, false, inheritedColors);
}
// Leave `target` to null. It will be computed by `compact()` if needed.
this.entries = entries;
@@ -351,7 +375,9 @@ public final class ColorModelBuilder {
final ColorsForRange[] entries = new ColorsForRange[categories.size()];
for (int i=0; i<entries.length; i++) {
final Category category = categories.get(i);
- entries[i] = new ColorsForRange(category, colors);
+ final var range = new ColorsForRange(category.forConvertedValues(true), colors, inheritedColors);
+ range.sampleRange = category.getSampleRange();
+ entries[i] = range;
}
this.entries = entries;
}
@@ -530,7 +556,7 @@ reuse: if (source != null) {
span += sourceRange.getSpan();
final ColorsForRange[] tmp = Arrays.copyOf(entries, ++count);
System.arraycopy(entries, deferred, tmp, ++deferred, count - deferred);
- tmp[deferred-1] = new ColorsForRange(null, sourceRange, null, true);
+ tmp[deferred-1] = new ColorsForRange(null, sourceRange, null, true, null);
entries = tmp;
}
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
index 510fa6cd61..69410a46a4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java
@@ -209,7 +209,7 @@ public final class ColorModelFactory {
}
}
}
- codes [ count] = entry.toARGB();
+ codes [ count] = entry.toARGB(upper - lower);
starts[ count] = lower;
starts[++count] = upper;
}
@@ -309,7 +309,8 @@ public final class ColorModelFactory {
public static ColorModelFactory piecewise(final Map<NumberRange<?>, Color[]> colors) {
final var entries = colors.entrySet();
ArgumentChecks.ensureNonEmpty("colors", entries);
- return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 0, DEFAULT_VISIBLE_BAND, ColorsForRange.list(entries)));
+ final var ranges = ColorsForRange.list(entries, null);
+ return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 0, DEFAULT_VISIBLE_BAND, ranges));
}
/**
@@ -479,7 +480,7 @@ public final class ColorModelFactory {
{
ArgumentChecks.ensureNonEmpty("colors", colors);
return createPiecewise(dataType, numBands, visibleBand, new ColorsForRange[] {
- new ColorsForRange(null, new NumberRange<>(Double.class, lower, true, upper, false), colors, true)
+ new ColorsForRange(null, new NumberRange<>(Double.class, lower, true, upper, false), colors, true, null)
});
}
@@ -779,7 +780,7 @@ public final class ColorModelFactory {
/**
* Copies {@code colors} into {@code ARGB} array from index {@code lower} inclusive to index {@code upper} exclusive.
* If {@code upper-lower} is not equal to the length of {@code colors} array, then colors will be interpolated.
- * The given {@code colors} array must be initialized with zero values in the {@code lower} … {@code upper} range.
+ * The given {@code ARGB} array must be initialized with zero values in the {@code lower} … {@code upper} range.
*
* @param colors colors to copy into the {@code ARGB} array.
* @param ARGB array of integer to write ARGB values into.
@@ -796,6 +797,10 @@ public final class ColorModelFactory {
case 1: ARGB[lower] = colors[0]; // fall through
case 0: return;
}
+ if (upper - lower == colors.length) {
+ System.arraycopy(colors, 0, ARGB, 0, colors.length);
+ return;
+ }
/*
* Prepares the coefficients for the iteration.
* The non-final ones will be updated inside the loop.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
index 31894df212..76dc35f90a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorsForRange.java
@@ -21,6 +21,7 @@ import java.util.Collection;
import java.util.Objects;
import java.util.function.Function;
import java.awt.Color;
+import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import org.apache.sis.coverage.Category;
import org.apache.sis.measure.NumberRange;
@@ -53,6 +54,13 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
*/
NumberRange<?> sampleRange;
+ /**
+ * The range of sample values as originally specified.
+ * Contrarily to {@link #sampleRange}, this range will not be modified by {@code compact()}.
+ * This is used for fetching colors from {@link #inheritedColors} if {@link #colors} is null.
+ */
+ private final NumberRange<?> originalSampleRange;
+
/**
* The colors to apply on the range of sample values.
* An empty array means that the category is explicitly specified as transparent.
@@ -60,10 +68,22 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
* is grayscale for quantitative category and transparent for qualitative category.
*
* @see #isUndefined()
- * @see #toARGB()
+ * @see #toARGB(int)
*/
private final Color[] colors;
+ /**
+ * The original colors, or {@code null} if unspecified.
+ * This is used as a fallback if {@link #colors} is null.
+ * This field should be non-null only when this {@code ColorsForRange} is created for
+ * styling an image before visualization. It should be null when creating a new image,
+ * because the meaning of pixel values (i.e. the sample dimensions) may be different.
+ *
+ * @see #originalSampleRange
+ * @see ColorModelBuilder#inheritedColors
+ */
+ private final ColorModel inheritedColors;
+
/**
* {@code true} if this entry should be taken as data, or {@code false} if it should be ignored.
* Entry to ignore are entries associated to NaN values.
@@ -73,57 +93,71 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
/**
* Creates a new instance for the given category.
*
- * @param category the category for which this {@code ColorsForRange} is created, or {@code null}.
- * @param colors colors to apply on the category.
+ * @param category the category for which this {@code ColorsForRange} is created.
+ * @param colors colors to apply on the category.
+ * @param inherited the original colors to use as fallback, or {@code null} if none.
+ * Should be non-null only for styling an exiting image before visualization.
*/
- ColorsForRange(final Category category, final Function<Category,Color[]> colors) {
- final CharSequence name = category.getName();
- this.name = (name != null) ? name : sampleRange.toString();
+ ColorsForRange(final Category category, final Function<Category,Color[]> colors, final ColorModel inherited) {
+ this.name = category.getName();
this.sampleRange = category.getSampleRange();
- this.colors = colors.apply(category);
this.isData = category.isQuantitative();
+ this.colors = colors.apply(category);
+ inheritedColors = inherited;
+ originalSampleRange = sampleRange;
}
/**
* Creates a new instance for the given range of values.
*
- * @param name a name identifying the range of values, or {@code null} for automatic.
- * @param sampleRange range of sample values on which the colors will be applied.
- * @param colors colors to apply on the range of sample values, or {@code null} for default.
- * @param isData whether this entry should be taken as main data (not fill values).
+ * @param name a name identifying the range of values, or {@code null} for automatic.
+ * @param sampleRange range of sample values on which the colors will be applied.
+ * @param colors colors to apply on the range of sample values, or {@code null} for default.
+ * @param isData whether this entry should be taken as main data (not fill values).
+ * @param inherited the original colors to use as fallback, or {@code null} if none.
+ * Should be non-null only for styling an exiting image before visualization.
*/
- ColorsForRange(final CharSequence name, final NumberRange<?> sampleRange, final Color[] colors, final boolean isData) {
+ ColorsForRange(final CharSequence name, final NumberRange<?> sampleRange, final Color[] colors,
+ final boolean isData, final ColorModel inherited)
+ {
ArgumentChecks.ensureNonNull("sampleRange", sampleRange);
this.name = (name != null) ? name : sampleRange.toString();
- this.sampleRange = sampleRange;
- this.colors = colors;
+ this.sampleRange = originalSampleRange = sampleRange;
this.isData = isData;
+ this.colors = colors;
+ inheritedColors = inherited;
}
/**
* Returns {@code true} if no color has been specified for this range.
* Note that "undefined" is not the same as fully transparent color.
+ *
+ * <p>If no colors were explicitly defined but a fallback exists, then this method considers
+ * this range as defined for allowing {@link ColorModelBuilder} to inherit those colors with
+ * the range of values specified by {@link #originalSampleRange}. We conceptually accept any
+ * {@link #inheritedColors} even if {@link #toARGB(int)} can not handle all of them.</p>
*/
final boolean isUndefined() {
- return colors == null;
+ return colors == null && inheritedColors == null;
}
/**
* Converts {@linkplain Map#entrySet() map entries} to an array of {@code ColorsForRange} entries.
* The {@link #category} of each entry is left to null.
*
- * @param colors the colors to use for each range of sample values.
- * A {@code null} entry value means transparent.
+ * @param colors the colors to use for each range of sample values.
+ * A {@code null} entry value means transparent.
+ * @param inherited the original color model from which to inherit undefined colors, or {@code null} if none.
* @return colors to use for each range of values in the source image.
* Never null and does not contain null elements.
*/
- static ColorsForRange[] list(final Collection<Map.Entry<NumberRange<?>,Color[]>> colors) {
+ static ColorsForRange[] list(final Collection<Map.Entry<NumberRange<?>,Color[]>> colors, final ColorModel inherited) {
final ColorsForRange[] entries = new ColorsForRange[colors.size()];
int n = 0;
for (final Map.Entry<NumberRange<?>,Color[]> entry : colors) {
final NumberRange<?> range = entry.getKey();
boolean singleton = Objects.equals(range.getMinValue(), range.getMaxValue());
- entries[n++] = new ColorsForRange(null, range, entry.getValue(), !singleton);
+ entries[n++] = new ColorsForRange(null, range, entry.getValue(), !singleton, inherited);
}
return ArraysExt.resize(entries, n); // `resize` should not be needed, but we are paranoiac.
}
@@ -134,7 +168,7 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder(name).append(": ").append(sampleRange);
- appendColorRange(buffer, toARGB());
+ appendColorRange(buffer, toARGB(2));
return buffer.toString();
}
@@ -197,9 +231,10 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
* Returns the ARGB codes for the colors.
* If all colors are transparent, returns an empty array.
*
- * @return ARGB codes for the given colors. Never {@code null} but may be empty.
+ * @param length desired array length. This is only a hint and may be ignored.
+ * @return ARGB codes for this color ramp. Never {@code null} but may be empty.
*/
- final int[] toARGB() {
+ final int[] toARGB(final int length) {
if (colors != null) {
int combined = 0;
final int[] ARGB = new int[colors.length];
@@ -214,6 +249,32 @@ final class ColorsForRange implements Comparable<ColorsForRange> {
if ((combined & 0xFF000000) != 0) {
return ARGB;
}
+ } else if (!originalSampleRange.isEmpty() && inheritedColors instanceof IndexColorModel) {
+ /*
+ * If colors are undefined, try to inherit them from the original colors.
+ * If the number of available colors is larger than the desired number,
+ * this block returns a subset of the inherited colors.
+ */
+ final IndexColorModel icm = (IndexColorModel) inheritedColors;
+ int offset = Math.round((float) originalSampleRange.getMinDouble(true));
+ int numSrc = Math.round((float) originalSampleRange.getMaxDouble()) - offset;
+ if (originalSampleRange.isMinIncluded()) numSrc++;
+ final int[] ARGB;
+ if (numSrc <= length) {
+ ARGB = new int[numSrc];
+ if (offset == 0 && numSrc == icm.getMapSize()) {
+ icm.getRGBs(ARGB);
+ } else for (int i=0; i<numSrc; i++) {
+ ARGB[i] = icm.getRGB(i + offset);
+ }
+ } else {
+ ARGB = new int[length];
+ final float scale = ((float) (numSrc-1)) / (length-1);
+ for (int i=0; i<length; i++) {
+ ARGB[i] = icm.getRGB(Math.round(i * scale) + offset);
+ }
+ }
+ return ARGB;
} else if (isData) {
return new int[] {0xFF000000, 0xFFFFFFFF};
}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
index 66bb12b931..012e1dcae6 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/j2d/ColorModelBuilderTest.java
@@ -52,7 +52,7 @@ public final class ColorModelBuilderTest extends TestCase {
final ColorModelBuilder colorizer = new ColorModelBuilder(List.of(
new SimpleEntry<>(NumberRange.create(0, true, 0, true), new Color[] {Color.GRAY}),
new SimpleEntry<>(NumberRange.create(1, true, 1, true), new Color[] {ColorModelFactory.TRANSPARENT}),
- new SimpleEntry<>(NumberRange.create(2, true, 15, true), new Color[] {Color.BLUE, Color.WHITE, Color.RED})));
+ new SimpleEntry<>(NumberRange.create(2, true, 15, true), new Color[] {Color.BLUE, Color.WHITE, Color.RED})), null);
/*
* No conversion of sample values should be necessary because the
* above-given ranges already fit in a 4-bits IndexColormodel.
@@ -99,7 +99,7 @@ public final class ColorModelBuilderTest extends TestCase {
.addQualitative ("Error", MathFunctions.toNanFloat(3))
.setName("Temperature").build();
- final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+ final ColorModelBuilder colorizer = new ColorModelBuilder(ColorModelBuilder.GRAYSCALE, null);
assertTrue("initialize", colorizer.initialize(null, sd));
final IndexColorModel cm = (IndexColorModel) colorizer.compactColorModel(1, 0); // Must be first.
/*
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 6c51dd0946..0b5f9efe5b 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -662,7 +662,7 @@ public class RenderingData implements Cloneable {
*/
if (CREATE_INDEX_COLOR_MODEL) {
final ColorModelType ct = ColorModelType.find(recoloredImage.getColorModel());
- if (ct.isSlow || (ct.useColorRamp && processor.getColorizer() != null)) try {
+ if (ct.isSlow || ct.useColorRamp) try {
SampleDimensions.IMAGE_PROCESSOR_ARGUMENT.set(dataRanges);
return processor.visualize(recoloredImage, bounds, displayToCenter);
} finally {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
index 80f72e2717..5be9072aa1 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/NumberRange.java
@@ -108,11 +108,10 @@ public class NumberRange<E extends Number & Comparable<? super E>> extends Range
/**
* Returns a unique instance of the given range, except if the range is empty.
*
- * <div class="note"><b>Rational:</b>
- * we exclude empty ranges because the {@link Range#equals(Object)} consider them as equal.
+ * <h4>Rational</h4>
+ * We exclude empty ranges because the {@link Range#equals(Object)} consider them as equal.
* Consequently, if empty ranges were included in the pool, this method would return in some
* occasions an empty range with different values than the given {@code range} argument.
- * </div>
*
* We use this method only for caching range of wrapper of primitive types ({@link Byte},
* {@link Short}, <i>etc.</i>) because those types are known to be immutable.
@@ -645,7 +644,7 @@ public class NumberRange<E extends Number & Comparable<? super E>> extends Range
/**
* Computes the difference between minimum and maximum values. If numbers are integers, the difference is computed
- * using inclusive values (e.g. equivalent to <code>{@linkplain #getMinDouble(boolean) getMinDouble}(true)</code>).
+ * using inclusive values (e.g. using <code>{@linkplain #getMinDouble(boolean) getMinDouble}(true)</code>).
* Otherwise the minimum and maximum values are used as-is
* (because making them inclusive is considered an infinitely small change).
*
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
index 6e63f3f36b..ffe769cf1b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java
@@ -81,7 +81,7 @@ import org.apache.sis.util.Numbers;
* @author Joe White
* @author Martin Desruisseaux (Geomatys)
* @author Jody Garnett (for parameterized type inspiration)
- * @version 1.0
+ * @version 1.4
*
* @param <E> the type of range elements, typically a {@link Number} subclass or {@link java.util.Date}.
*
@@ -269,7 +269,7 @@ public class Range<E extends Comparable<? super E>> implements CheckedContainer<
* Returns {@code true} if this range is empty. A range is empty if the
* {@linkplain #getMinValue() minimum value} is greater than the
* {@linkplain #getMaxValue() maximum value}, or if they are equal while
- * at least one of them is exclusive.
+ * at least one of them is exclusive, or if both bounds are NaN.
*
* <h4>API note</h4>
* This method is final because often used by the internal implementation.
@@ -286,8 +286,25 @@ public class Range<E extends Comparable<? super E>> implements CheckedContainer<
if (c < 0) {
return false; // Minimum is smaller than maximum.
}
+ if (c != 0) { // Minimum is NaN or greater than maximum.
+ return !isNaN(minValue);
+ }
// If min and max are equal, then the range is empty if at least one of them is exclusive.
- return (c != 0) || !isMinIncluded || !isMaxIncluded;
+ if (!isMinIncluded || !isMaxIncluded) {
+ return true;
+ }
+ return isNaN(minValue); // At this point if min is NaN, max is also NaN.
+ }
+
+ /**
+ * Returns {@code true} if the given value is NaN. This method tests only the primitive wrappers
+ * because the behavior of their {@code compareTo(…)} method is clearly documented. Calls to this
+ * method assume that NaNs are considered by {@code compareTo(…)} as greater than all other values.
+ */
+ private static boolean isNaN(final Object value) {
+ if (value instanceof Double) return ((Double) value).isNaN();
+ if (value instanceof Float) return ((Float) value).isNaN();
+ return false;
}
/**
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java
index f2b2750853..d266990144 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/RangeTest.java
@@ -30,7 +30,7 @@ import static org.apache.sis.test.Assert.*;
*
* @author Joe White
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
* @since 0.3
*/
public final class RangeTest extends TestCase {
@@ -103,6 +103,21 @@ public final class RangeTest extends TestCase {
new Range(String.class, 123.233, true, 8740.09, true);
}
+ /**
+ * Tests {@link Range#isEmpty()}.
+ */
+ @Test
+ public void testIsEmpty() {
+ assertFalse(new Range<>(Float.class, 3f, true, 5f, true).isEmpty());
+ assertFalse(new Range<>(Float.class, 3f, true, 3f, true).isEmpty());
+ assertTrue (new Range<>(Float.class, 3f, true, 3f, false).isEmpty());
+ assertTrue (new Range<>(Float.class, 3f, false, 3f, true).isEmpty());
+ assertTrue (new Range<>(Float.class, 3f, false, 3f, false).isEmpty());
+ assertFalse(new Range<>(Float.class, Float.NaN, true, 5f, true).isEmpty());
+ assertFalse(new Range<>(Float.class, 3f, true, Float.NaN, true).isEmpty());
+ assertTrue (new Range<>(Float.class, Float.NaN, true, Float.NaN, true).isEmpty());
+ }
+
/**
* Tests the {@link Range#contains(Comparable)} method.
*/