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 f691d87e35 Store sample dimensions in a `RenderedImage` property. Use
that property instead of argument value in `ImageProcessor`.
f691d87e35 is described below
commit f691d87e35689303ff1dbcd9995f85f42673eb6d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Mar 29 20:31:06 2023 +0200
Store sample dimensions in a `RenderedImage` property.
Use that property instead of argument value in `ImageProcessor`.
https://issues.apache.org/jira/browse/SIS-577
---
.../org/apache/sis/coverage/grid/GridCoverage.java | 37 +++---
.../apache/sis/coverage/grid/GridCoverage2D.java | 2 +
.../sis/coverage/grid/GridCoverageBuilder.java | 13 ++-
.../sis/coverage/grid/GridCoverageProcessor.java | 118 +++++++++++++------
.../apache/sis/coverage/grid/ImageRenderer.java | 77 ++++++++-----
.../sis/coverage/grid/ResampledGridCoverage.java | 3 +-
.../java/org/apache/sis/image/AnnotatedImage.java | 12 +-
.../java/org/apache/sis/image/BandSelectImage.java | 14 ++-
.../apache/sis/image/BandedSampleConverter.java | 89 +++++++++++----
.../main/java/org/apache/sis/image/Colorizer.java | 28 +++--
.../java/org/apache/sis/image/ImageAdapter.java | 6 +-
.../java/org/apache/sis/image/ImageProcessor.java | 126 +++++++++++++++++----
.../java/org/apache/sis/image/PlanarImage.java | 17 ++-
.../java/org/apache/sis/image/RecoloredImage.java | 44 ++++---
.../java/org/apache/sis/image/ResampledImage.java | 10 +-
.../java/org/apache/sis/image/UserProperties.java | 124 ++++++++++++++++++++
.../java/org/apache/sis/image/Visualization.java | 56 +++++----
.../sis/internal/coverage/SampleDimensions.java | 37 ++++--
.../coverage/grid/ConvertedGridCoverageTest.java | 24 +++-
.../org/apache/sis/image/ImageProcessorTest.java | 31 ++++-
.../apache/sis/image/StatisticsCalculatorTest.java | 2 +-
.../sis/internal/map/coverage/RenderingData.java | 34 ++++--
.../sis/internal/storage/esri/RasterStore.java | 8 +-
23 files changed, 696 insertions(+), 216 deletions(-)
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index d1f6a544a8..fbea3ecdb7 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -20,7 +20,6 @@ import java.util.Map;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
-import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
@@ -36,8 +35,7 @@ import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.image.DataType;
import org.apache.sis.image.ImageProcessor;
-import org.apache.sis.internal.coverage.j2d.ImageUtilities;
-import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
+import org.apache.sis.internal.coverage.SampleDimensions;
import org.apache.sis.util.collection.DefaultTreeTable;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable;
@@ -58,7 +56,7 @@ import org.opengis.coverage.CannotEvaluateException;
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 1.0
*/
public abstract class GridCoverage extends BandedCoverage {
@@ -206,6 +204,19 @@ public abstract class GridCoverage extends BandedCoverage {
return ranges;
}
+ /**
+ * Returns the background value of each sample dimension.
+ * The array length is the number of sample dimensions (bands).
+ * Some array element may be {@code null} if the corresponding band has no
background value.
+ *
+ * @return background value of each sample dimension.
+ *
+ * @see SampleDimension#getBackground()
+ */
+ final Number[] getBackground() {
+ return SampleDimensions.backgrounds(sampleDimensions);
+ }
+
/**
* Returns the data type identifying the primitive type used for storing
sample values in each band.
* We assume no packed sample model (e.g. no packing of 4 byte ARGB values
in a single 32-bits integer).
@@ -289,6 +300,9 @@ public abstract class GridCoverage extends BandedCoverage {
/**
* Creates a new image of the given data type which will compute values
using the given converters.
+ * The {@link #sampleDimensions} declared in this {@code GridCoverage}
instances shall be applicable
+ * to the returned image, as it will be assigned to the image property
+ * {@value org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY}.
*
* @param source the image for which to convert sample values.
* @param bandType the type of data in the bands resulting from
conversion of given image.
@@ -299,17 +313,12 @@ public abstract class GridCoverage extends BandedCoverage
{
final RenderedImage convert(final RenderedImage source, final DataType
bandType,
final MathTransform1D[] converters, final ImageProcessor processor)
{
- final int visibleBand = Math.max(0,
ImageUtilities.getVisibleBand(source));
- final ColorModelBuilder colorizer = new
ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
- final ColorModel colors;
- if (colorizer.initialize(source.getSampleModel(),
sampleDimensions[visibleBand]) ||
- colorizer.initialize(source.getColorModel()))
- {
- colors = colorizer.createColorModel(bandType.toDataBufferType(),
sampleDimensions.length, visibleBand);
- } else {
- colors = ColorModelBuilder.NULL_COLOR_MODEL;
+ try {
+ SampleDimensions.CONVERTED_BANDS.set(sampleDimensions);
+ return processor.convert(source, getRanges(), converters,
bandType);
+ } finally {
+ SampleDimensions.CONVERTED_BANDS.remove();
}
- return processor.convert(source, getRanges(), converters, bandType,
colors);
}
/**
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
index d48f05c83e..7bbcd934b1 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage2D.java
@@ -471,6 +471,8 @@ public class GridCoverage2D extends GridCoverage {
/**
* Creates a grid coverage that contains real values or sample values,
* depending if {@code converted} is {@code true} or {@code false}
respectively.
+ * This method is invoked by the default implementation of {@link
#forConvertedValues(boolean)}
+ * when first needed.
*
* @param converted {@code true} for a coverage containing converted
values,
* or {@code false} for a coverage containing packed
values.
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 c437c4ea81..ca04e9a09e 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
@@ -33,6 +33,7 @@ import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.image.PlanarImage;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
@@ -87,7 +88,7 @@ import org.apache.sis.util.resources.Errors;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
*
* @see GridCoverage2D
* @see SampleDimension.Builder
@@ -169,12 +170,13 @@ public class GridCoverageBuilder {
* @see #addImageProperty(String, Object)
*/
@SuppressWarnings("UseOfObsoleteCollectionType")
- private Hashtable<String,Object> properties;
+ private final Hashtable<String,Object> properties;
/**
* Creates an initially empty builder.
*/
public GridCoverageBuilder() {
+ properties = new Hashtable<>();
}
/**
@@ -419,9 +421,6 @@ public class GridCoverageBuilder {
public GridCoverageBuilder addImageProperty(final String key, final Object
value) {
ArgumentChecks.ensureNonNull("key", key);
ArgumentChecks.ensureNonNull("value", value);
- if (properties == null) {
- properties = new Hashtable<>();
- }
if (properties.putIfAbsent(key, value) != null) {
throw new
IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1,
key));
}
@@ -482,6 +481,9 @@ public class GridCoverageBuilder {
* Create an image from the raster. We favor BufferedImage
instance when possible,
* and fallback on TiledImage only if the BufferedImage cannot
be created.
*/
+ if (bands != null) {
+ properties.put(PlanarImage.SAMPLE_DIMENSIONS_KEY,
bands.toArray(SampleDimension[]::new));
+ }
if (raster instanceof WritableRaster) {
final WritableRaster wr = (WritableRaster) raster;
if (colors != null && (wr.getMinX() | wr.getMinY()) == 0) {
@@ -492,6 +494,7 @@ public class GridCoverageBuilder {
} else {
image = new TiledImage(properties, colors,
raster.getWidth(), raster.getHeight(), 0, 0, raster);
}
+ properties.remove(PlanarImage.SAMPLE_DIMENSIONS_KEY);
}
/*
* At this point `image` shall be non-null but `bands` may still
be null (it is okay).
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index 31465949a6..b915d36752 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -36,6 +36,7 @@ import org.apache.sis.coverage.RegionOfInterest;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.image.DataType;
+import org.apache.sis.image.Colorizer;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.image.Interpolation;
import org.apache.sis.internal.coverage.MultiSourcesArgument;
@@ -57,6 +58,8 @@ import org.apache.sis.measure.NumberRange;
* </li><li>
* {@linkplain #setFillValues(Number...) Fill values} to use for cells
that cannot be computed.
* </li><li>
+ * {@linkplain #setColorizer(Colorizer) Colorization algorithm} to apply
for colorizing a computed image.
+ * </li><li>
* {@linkplain #setPositionalAccuracyHints(Quantity...) Positional
accuracy hints}
* for enabling the use of faster algorithm when a lower accuracy is
acceptable.
* </li><li>
@@ -86,11 +89,27 @@ public class GridCoverageProcessor implements Cloneable {
private static final WeakHashSet<ImageProcessor> PROCESSORS = new
WeakHashSet<>(ImageProcessor.class);
/**
- * Returns an unique instance of the given processor. Both the given and
the returned processors
- * shall be unmodified, because they may be shared by many {@link
GridCoverage} instances.
+ * Returns a unique instance of the given processor. Both the given and
the returned processors shall not
+ * be modified after this method call, because they may be shared by many
{@link GridCoverage} instances.
+ * It implies that the given processor shall <em>not</em> be {@link
#imageProcessor}. It must be a clone.
+ *
+ * @param clone a clone of {@link #imageProcessor} for which to return a
unique instance.
+ * @return a unique instance of the given clone. Shall not be modified by
the caller.
*/
- static ImageProcessor unique(final ImageProcessor image) {
- return PROCESSORS.unique(image);
+ static ImageProcessor unique(final ImageProcessor clone) {
+ return PROCESSORS.unique(clone);
+ }
+
+ /**
+ * Returns a unique instance of the current state of {@link
#imageProcessor}.
+ * Callers shall not modify the returned object because it may be shared
by many {@link GridCoverage} instances.
+ */
+ private ImageProcessor snapshot() {
+ ImageProcessor shared = PROCESSORS.get(imageProcessor);
+ if (shared == null) {
+ shared = unique(imageProcessor.clone());
+ }
+ return shared;
}
/**
@@ -150,6 +169,64 @@ public class GridCoverageProcessor implements Cloneable {
imageProcessor.setInterpolation(method);
}
+ /**
+ * Returns the values to use for pixels that cannot be computed.
+ * The default implementation delegates to the image processor.
+ *
+ * @return fill values to use for pixels that cannot be computed, or
{@code null} for the defaults.
+ *
+ * @see ImageProcessor#getFillValues()
+ *
+ * @since 1.2
+ */
+ public Number[] getFillValues() {
+ return imageProcessor.getFillValues();
+ }
+
+ /**
+ * Sets the values to use for pixels that cannot be computed.
+ * The default implementation delegates to the image processor.
+ *
+ * @param values fill values to use for pixels that cannot be computed,
or {@code null} for the defaults.
+ *
+ * @see ImageProcessor#setFillValues(Number...)
+ *
+ * @since 1.2
+ */
+ public void setFillValues(final Number... values) {
+ imageProcessor.setFillValues(values);
+ }
+
+ /**
+ * Returns the colorization algorithm to apply on computed images.
+ * The default implementation delegates to the image processor.
+ *
+ * @return colorization algorithm to apply on computed image, or {@code
null} for default.
+ *
+ * @see ImageProcessor#getColorizer()
+ *
+ * @since 1.4
+ */
+ public Colorizer getColorizer() {
+ return imageProcessor.getColorizer();
+ }
+
+ /**
+ * Sets the colorization algorithm to apply on computed images.
+ * The colorizer is used by {@link #convert(GridCoverage,
MathTransform1D[], Function) convert(…)}
+ * and {@link #aggregateRanges(GridCoverage...) aggregateRanges(…)}
operations among others.
+ * The default implementation delegates to the image processor.
+ *
+ * @param colorizer colorization algorithm to apply on computed image, or
{@code null} for default.
+ *
+ * @see ImageProcessor#setColorizer(Colorizer)
+ *
+ * @since 1.4
+ */
+ public void setColorizer(final Colorizer colorizer) {
+ imageProcessor.setColorizer(colorizer);
+ }
+
/**
* Returns hints about the desired positional accuracy, in "real world"
units or in pixel units.
* The default implementation delegates to the image processor.
@@ -245,34 +322,6 @@ public class GridCoverageProcessor implements Cloneable {
optimizations.addAll(enabled);
}
- /**
- * Returns the values to use for pixels that cannot be computed.
- * The default implementation delegates to the image processor.
- *
- * @return fill values to use for pixels that cannot be computed, or
{@code null} for the defaults.
- *
- * @see ImageProcessor#getFillValues()
- *
- * @since 1.2
- */
- public Number[] getFillValues() {
- return imageProcessor.getFillValues();
- }
-
- /**
- * Sets the values to use for pixels that cannot be computed.
- * The default implementation delegates to the image processor.
- *
- * @param values fill values to use for pixels that cannot be computed,
or {@code null} for the defaults.
- *
- * @see ImageProcessor#setFillValues(Number...)
- *
- * @since 1.2
- */
- public void setFillValues(final Number... values) {
- imageProcessor.setFillValues(values);
- }
-
/**
* Applies a mask defined by a region of interest (ROI). If {@code
maskInside} is {@code true},
* then all pixels inside the given ROI are set to the {@linkplain
#getFillValues() fill values}.
@@ -359,7 +408,7 @@ public class GridCoverageProcessor implements Cloneable {
builder.clear();
}
return new ConvertedGridCoverage(source,
UnmodifiableArrayList.wrap(targetBands),
- converters, true,
unique(imageProcessor), true);
+ converters, true, snapshot(), true);
}
/**
@@ -484,6 +533,7 @@ public class GridCoverageProcessor implements Cloneable {
}
final GridCoverage resampled;
try {
+ // `ResampledGridCoverage` will create itself a clone of
`imageProcessor`.
resampled = ResampledGridCoverage.create(source, target,
imageProcessor, allowOperationReplacement);
} catch (IllegalGridGeometryException e) {
final Throwable cause = e.getCause();
@@ -689,7 +739,7 @@ public class GridCoverageProcessor implements Cloneable {
if (aggregate.isIdentity()) {
return aggregate.sources()[0];
}
- return new BandAggregateGridCoverage(aggregate, imageProcessor);
+ return new BandAggregateGridCoverage(aggregate, snapshot());
}
/**
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 79eddf520c..9fde21c79b 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
@@ -59,6 +59,7 @@ import static java.lang.Math.multiplyExact;
import static java.lang.Math.incrementExact;
import static java.lang.Math.toIntExact;
import static org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY;
+import static org.apache.sis.image.PlanarImage.SAMPLE_DIMENSIONS_KEY;
/**
@@ -98,7 +99,7 @@ import static
org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY;
* Support for tiled images will be added in a future version.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
*
* @see GridCoverage#render(GridExtent)
*
@@ -146,9 +147,10 @@ public class ImageRenderer {
* Location of the first image pixel relative to the grid coverage extent.
The (0,0) offset means that the first pixel
* in the {@code sliceExtent} (specified at construction time) is the
first pixel in the whole {@link GridCoverage}.
*
- * <div class="note"><b>Note:</b> if those offsets exceed 32 bits integer
capacity, then it may not be possible to build
- * an image for given {@code sliceExtent} from a single {@link
DataBuffer}, because accessing sample values would exceed
- * the capacity of index in Java arrays. In those cases the image needs to
be tiled.</div>
+ * <h4>Implementation note</h4>
+ * If those offsets exceed 32 bits integer capacity, then it may not be
possible to build an image
+ * for given {@code sliceExtent} from a single {@link DataBuffer}, because
accessing sample values
+ * would exceed the capacity of index in Java arrays. In those cases the
image needs to be tiled.
*/
private final long offsetX, offsetY;
@@ -461,9 +463,14 @@ public class ImageRenderer {
}
/**
- * Returns the value associated to the given property. By default the only
property is
- * {@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}, but more
properties can
- * be added by calls to {@link #addProperty(String, Object)}.
+ * Returns the value associated to the given property.
+ * The properties recognized by current implementation are:
+ *
+ * <ul>
+ * <li>{@value org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY}.</li>
+ * <li>{@value
org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY}.</li>
+ * <li>Any property added by calls to {@link #addProperty(String,
Object)}.</li>
+ * </ul>
*
* @param key the property for which to get a value.
* @return value associated to the given property, or {@code null} if none.
@@ -471,8 +478,9 @@ public class ImageRenderer {
* @since 1.1
*/
public Object getProperty(final String key) {
- if (GRID_GEOMETRY_KEY.equals(key)) {
- return getImageGeometry(GridCoverage2D.BIDIMENSIONAL);
+ switch (key) {
+ case GRID_GEOMETRY_KEY: return
getImageGeometry(GridCoverage2D.BIDIMENSIONAL);
+ case SAMPLE_DIMENSIONS_KEY: return bands.clone();
}
return (properties != null) ? properties.get(key) : null;
}
@@ -491,7 +499,7 @@ public class ImageRenderer {
public void addProperty(final String key, final Object value) {
ArgumentChecks.ensureNonNull("key", key);
ArgumentChecks.ensureNonNull("value", value);
- if (!GRID_GEOMETRY_KEY.equals(key)) {
+ if (!(GRID_GEOMETRY_KEY.equals(key) ||
SAMPLE_DIMENSIONS_KEY.equals(key))) {
if (properties == null) {
properties = new Hashtable<>();
}
@@ -635,13 +643,15 @@ public class ImageRenderer {
* 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>
+ * <h4>Implementation note</h4>
+ * An {@link java.awt.image.IndexColorModel} will be used for displaying
the image.
*
* @param band the band to use for display purpose.
* @throws IllegalArgumentException if the given band is not between 0
(inclusive)
* and {@link #getNumBands()} (exclusive).
*
+ * @see org.apache.sis.image.Colorizer.Target#getVisibleBand()
+ *
* @since 1.2
*/
public void setVisibleBand(final int band) {
@@ -725,10 +735,12 @@ public class ImageRenderer {
* The image upper-left corner is located at the position given by {@link
#getBounds()}.
* The two-dimensional {@linkplain #getImageGeometry(int) image geometry}
is stored as
* a property associated to the {@value
org.apache.sis.image.PlanarImage#GRID_GEOMETRY_KEY} key.
+ * The sample dimensions are stored as a property associated to the
+ * {@value org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY} key.
*
* <p>The default implementation returns an instance of {@link
java.awt.image.WritableRenderedImage}
- * if the {@link #createRaster()} return value is an instance of {@link
WritableRaster}, or a read-only
- * {@link RenderedImage} otherwise.</p>
+ * if the {@link #createRaster()} return value is an instance of {@link
WritableRaster},
+ * or a read-only {@link RenderedImage} otherwise.</p>
*
* @return the image.
* @throws IllegalStateException if no {@code setData(…)} method has been
invoked before this method call.
@@ -758,12 +770,13 @@ public class ImageRenderer {
}
final WritableRaster wr = (raster instanceof WritableRaster) ?
(WritableRaster) raster : null;
if (wr != null && colors != null && (imageX | imageY) == 0) {
- return new Untiled(colors, wr, properties, imageGeometry,
supplier);
+ return new Untiled(colors, wr, properties, imageGeometry,
supplier, bands);
}
if (properties == null) {
properties = new Hashtable<>();
}
properties.putIfAbsent(GRID_GEOMETRY_KEY, (supplier != null) ? new
DeferredProperty(supplier) : imageGeometry);
+ properties.putIfAbsent(SAMPLE_DIMENSIONS_KEY, bands);
if (wr != null) {
return new WritableTiledImage(properties, colors, width, height,
0, 0, wr);
} else {
@@ -791,16 +804,22 @@ public class ImageRenderer {
*/
private SliceGeometry supplier;
+ /**
+ * The value associated to the {@value
org.apache.sis.image.PlanarImage#SAMPLE_DIMENSIONS_KEY} key.
+ */
+ private final SampleDimension[] bands;
+
/**
* Creates a new buffered image wrapping the given raster.
*/
@SuppressWarnings("UseOfObsoleteCollectionType")
Untiled(final ColorModel colors, final WritableRaster raster, final
Hashtable<?,?> properties,
- final GridGeometry geometry, final SliceGeometry supplier)
+ final GridGeometry geometry, final SliceGeometry supplier,
final SampleDimension[] bands)
{
super(colors, raster, false, properties);
this.geometry = geometry;
this.supplier = supplier;
+ this.bands = bands;
}
/**
@@ -808,7 +827,8 @@ public class ImageRenderer {
*/
@Override
public String[] getPropertyNames() {
- return ArraysExt.concatenate(super.getPropertyNames(), new
String[] {GRID_GEOMETRY_KEY});
+ return ArraysExt.concatenate(super.getPropertyNames(),
+ new String[] {GRID_GEOMETRY_KEY, SAMPLE_DIMENSIONS_KEY});
}
/**
@@ -820,19 +840,22 @@ public class ImageRenderer {
*/
@Override
public Object getProperty(final String key) {
- if (!GRID_GEOMETRY_KEY.equals(key)) {
- return super.getProperty(key);
- }
- synchronized (this) {
- if (geometry == null) {
- final SliceGeometry s = supplier;
- if (s != null) {
- supplier = null; // Let GC do its work.
- geometry = s.apply(this);
+ switch (key) {
+ default: return super.getProperty(key);
+ case SAMPLE_DIMENSIONS_KEY: return bands.clone();
+ case GRID_GEOMETRY_KEY: {
+ synchronized (this) {
+ if (geometry == null) {
+ final SliceGeometry s = supplier;
+ if (s != null) {
+ supplier = null; // Let GC do
its work.
+ geometry = s.apply(this);
+ }
+ }
+ return geometry;
}
}
}
- return geometry;
}
}
}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
index 00d9b52850..997b357e3b 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ResampledGridCoverage.java
@@ -32,7 +32,6 @@ import org.apache.sis.image.ImageProcessor;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.internal.feature.Resources;
import org.apache.sis.internal.util.DoubleDouble;
-import org.apache.sis.internal.coverage.SampleDimensions;
import org.apache.sis.internal.referencing.DirectPositionView;
import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
import org.apache.sis.referencing.operation.transform.LinearTransform;
@@ -113,7 +112,7 @@ final class ResampledGridCoverage extends
DerivedGridCoverage {
* NaN for floating point values.
*/
processor = processor.clone();
-
processor.setFillValues(SampleDimensions.backgrounds(getSampleDimensions()));
+ processor.setFillValues(getBackground());
changeOfCRS.setAccuracyOf(processor);
imageProcessor = GridCoverageProcessor.unique(processor);
final Dimension s = imageProcessor.getInterpolation().getSupportSize();
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
index 4a661a3e9a..577ca19dfc 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/AnnotatedImage.java
@@ -52,9 +52,9 @@ import org.apache.sis.internal.util.Strings;
* <p>The computation results are cached by this class. The cache strategy
assumes that the
* property value depend only on sample values, not on properties of the
source image.</p>
*
- * <div class="note"><b>Design note:</b>
- * most non-abstract methods are final because {@link PixelIterator} (among
others) relies
- * on the fact that it can unwrap this image and still get the same pixel
values.</div>
+ * <h2>Design note</h2>
+ * Most non-abstract methods are final because {@link PixelIterator} (among
others) relies
+ * on the fact that it can unwrap this image and still get the same pixel
values.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.2
@@ -258,9 +258,9 @@ abstract class AnnotatedImage extends ImageAdapter {
* i.e. for distinguishing between two {@code AnnotatedImage} instances
that are identical
* except for subclass-defined parameters.
*
- * <div class="note"><b>API note:</b>
- * the return value is an array because there is typically one parameter
value per band.
- * This method will not modify the returned array.</div>
+ * <h4>API note</h4>
+ * The return value is an array because there is typically one parameter
value per band.
+ * This method will not modify the returned array.
*
* @return subclass specific extra parameter, or {@code null} if none.
*/
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
index 712c910420..43ade8f563 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandSelectImage.java
@@ -50,8 +50,16 @@ final class BandSelectImage extends SourceAlignedImage {
* @see #getProperty(String)
*/
private static final Set<String> INHERITED_PROPERTIES = Set.of(
- GRID_GEOMETRY_KEY, POSITIONAL_ACCURACY_KEY, // Properties
to forward as-is.
- SAMPLE_RESOLUTIONS_KEY, STATISTICS_KEY); // Properties
to forward after band reduction.
+ GRID_GEOMETRY_KEY, POSITIONAL_ACCURACY_KEY,
// Properties to forward as-is.
+ SAMPLE_DIMENSIONS_KEY, SAMPLE_RESOLUTIONS_KEY, STATISTICS_KEY);
// Properties to forward after band reduction.
+
+ /**
+ * Inherited properties that require band reduction.
+ * Shall be a subset of {@link #INHERITED_PROPERTIES}.
+ * All values must be arrays.
+ */
+ private static final Set<String> REDUCED_PROPERTIES = Set.of(
+ SAMPLE_DIMENSIONS_KEY, SAMPLE_RESOLUTIONS_KEY, STATISTICS_KEY);
/**
* The selected bands.
@@ -144,7 +152,7 @@ final class BandSelectImage extends SourceAlignedImage {
*/
private static Object getProperty(final RenderedImage source, final String
key, final int[] bands) {
final Object value = source.getProperty(key);
- if (value != null && (key.equals(SAMPLE_RESOLUTIONS_KEY) ||
key.equals(STATISTICS_KEY))) {
+ if (value != null && REDUCED_PROPERTIES.contains(key)) {
final Class<?> componentType = value.getClass().getComponentType();
if (componentType != null) {
final Object reduced = Array.newInstance(componentType,
bands.length);
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 ab8ee294a0..baab75fe89 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
@@ -32,16 +32,19 @@ import java.lang.reflect.Array;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
+import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
import org.apache.sis.internal.coverage.j2d.ImageLayout;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
import org.apache.sis.internal.coverage.j2d.WriteSupport;
+import org.apache.sis.internal.coverage.SampleDimensions;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.measure.NumberRange;
+import org.apache.sis.coverage.SampleDimension;
import static org.apache.sis.internal.coverage.j2d.ImageUtilities.LOGGER;
@@ -81,7 +84,7 @@ class BandedSampleConverter extends ComputedImage {
*
* @see #getPropertyNames()
*/
- private static final String[] ADDED_PROPERTIES = {SAMPLE_RESOLUTIONS_KEY};
+ private static final String[] ADDED_PROPERTIES = {SAMPLE_DIMENSIONS_KEY,
SAMPLE_RESOLUTIONS_KEY};
/**
* The transfer functions to apply on each band of the source image.
@@ -93,6 +96,16 @@ class BandedSampleConverter extends ComputedImage {
*/
private final ColorModel colorModel;
+ /**
+ * Description of bands, or {@code null} if unknown.
+ * Not used by this class, but provided as a {@value
#SAMPLE_DIMENSIONS_KEY} property.
+ * The value is fetched from {@link SampleDimensions#CONVERTED_BANDS} for
avoiding to
+ * expose a {@code SampleDimension[]} argument in public {@link
ImageProcessor} API.
+ *
+ * @see #getProperty(String)
+ */
+ private final SampleDimension[] sampleDimensions;
+
/**
* The sample resolutions, or {@code null} if unknown.
*/
@@ -107,14 +120,17 @@ class BandedSampleConverter extends ComputedImage {
* @param ranges the expected range of values for each band, or
{@code null} if unknown.
* @param converters the transfer functions to apply on each band of
the source image.
* If this array was a user-provided parameter,
should be cloned by caller.
+ * @param sampleDimensions description of conversion result, or {@code
null} if unknown.
*/
private BandedSampleConverter(final RenderedImage source, final
BandedSampleModel sampleModel,
final ColorModel colorModel, final
NumberRange<?>[] ranges,
- final MathTransform1D[] converters)
+ final MathTransform1D[] converters,
+ final SampleDimension[] sampleDimensions)
{
super(sampleModel, source);
this.colorModel = colorModel;
this.converters = converters;
+ this.sampleDimensions = sampleDimensions;
/*
* Get an estimation of the resolution, arbitrarily looking in the
middle of the range of values.
* If the converters are linear (which is the most common case), the
middle value does not matter
@@ -189,7 +205,7 @@ class BandedSampleConverter extends ComputedImage {
* @param colorizer provider of color model for the expected range of
values, or {@code null}.
* @return the image which compute converted values from the given source.
*
- * @see ImageProcessor#convert(RenderedImage, NumberRange[],
MathTransform1D[], DataType, ColorModel)
+ * @see ImageProcessor#convert(RenderedImage, NumberRange[],
MathTransform1D[], DataType)
*/
static BandedSampleConverter create(RenderedImage source, final
ImageLayout layout,
final NumberRange<?>[] sourceRanges, final MathTransform1D[]
converters,
@@ -197,23 +213,40 @@ class BandedSampleConverter extends ComputedImage {
{
/*
* Since this operation applies its own ColorModel anyway, skip
operation that was doing nothing else
- * than changing the color model.
+ * than changing the color model. The new color model may be specified
by the user if (s)he provided
+ * a `Colorizer` instance. Otherwise a default color model will be
inferred.
*/
if (source instanceof RecoloredImage) {
source = ((RecoloredImage) source).source;
}
final int numBands = converters.length;
final BandedSampleModel sampleModel =
layout.createBandedSampleModel(targetType, numBands, source, null);
+ final SampleDimension[] sampleDimensions =
SampleDimensions.CONVERTED_BANDS.get();
final int visibleBand = ImageUtilities.getVisibleBand(source);
- ColorModel colorModel = null;
+ ColorModel colorModel = ColorModelBuilder.NULL_COLOR_MODEL;
if (colorizer != null) {
- colorModel = colorizer.apply(new Colorizer.Target(sampleModel,
null, visibleBand)).orElse(null);
+ var target = new Colorizer.Target(sampleModel,
UnmodifiableArrayList.wrap(sampleDimensions), visibleBand);
+ colorModel = colorizer.apply(target).orElse(null);
}
if (colorModel == null) {
- colorModel = ColorModelFactory.createGrayScale(sampleModel,
visibleBand, null);
+ /*
+ * If no color model was specified or inferred from a colorizer,
+ * default to grayscale for a range inferred from the sample
dimension.
+ * If no sample dimension is specified, infer value range from
data type.
+ */
+ SampleDimension sd = null;
+ if (sampleDimensions != null && visibleBand >= 0 && visibleBand <
sampleDimensions.length) {
+ sd = sampleDimensions[visibleBand];
+ }
+ final var builder = new
ColorModelBuilder(ColorModelBuilder.GRAYSCALE);
+ if (builder.initialize(source.getSampleModel(), sd) ||
+ builder.initialize(source.getColorModel()))
+ {
+ colorModel = builder.createColorModel(targetType, numBands,
Math.max(visibleBand, 0));
+ }
}
/*
- * If the source image is writable, then changes in the converted
image may be retro-propagated
+ * If the source image is writable, then change in the converted image
may be retro-propagated
* to that source image. If we fail to compute the required inverse
transforms, log a notice at
* a low level because this is not a serious problem; writable
BandedSampleConverter is a plus
* but not a requirement.
@@ -223,25 +256,42 @@ class BandedSampleConverter extends ComputedImage {
for (int i=0; i<numBands; i++) {
inverses[i] = converters[i].inverse();
}
- return new Writable((WritableRenderedImage) source, sampleModel,
colorModel, sourceRanges, converters, inverses);
+ return new Writable((WritableRenderedImage) source, sampleModel,
colorModel, sourceRanges, converters, inverses, sampleDimensions);
} catch (NoninvertibleTransformException e) {
Logging.recoverableException(LOGGER, ImageProcessor.class,
"convert", e);
}
- return new BandedSampleConverter(source, sampleModel, colorModel,
sourceRanges, converters);
+ return new BandedSampleConverter(source, sampleModel, colorModel,
sourceRanges, converters, sampleDimensions);
}
/**
* Gets a property from this image. Current implementation recognizes:
- * {@value #SAMPLE_RESOLUTIONS_KEY}.
+ * <ul>
+ * <li>{@value #SAMPLE_RESOLUTIONS_KEY}, computed by this class.</li>
+ * <li>{@value #SAMPLE_DIMENSIONS_KEY}, provided to the constructor.</li>
+ * <li>All positional properties, forwarded to source image.</li>
+ * </ul>
*/
@Override
public Object getProperty(final String key) {
- if (SAMPLE_RESOLUTIONS_KEY.equals(key)) {
- if (sampleResolutions != null) {
- return sampleResolutions.clone();
+ switch (key) {
+ case SAMPLE_DIMENSIONS_KEY: {
+ if (sampleDimensions != null) {
+ return sampleDimensions.clone();
+ }
+ break;
+ }
+ case SAMPLE_RESOLUTIONS_KEY: {
+ if (sampleResolutions != null) {
+ return sampleResolutions.clone();
+ }
+ break;
+ }
+ default: {
+ if (SourceAlignedImage.POSITIONAL_PROPERTIES.contains(key)) {
+ return getSource().getProperty(key);
+ }
+ break;
}
- } else if (SourceAlignedImage.POSITIONAL_PROPERTIES.contains(key)) {
- return getSource().getProperty(key);
}
return super.getProperty(key);
}
@@ -394,9 +444,10 @@ class BandedSampleConverter extends ComputedImage {
*/
Writable(final WritableRenderedImage source, final BandedSampleModel
sampleModel,
final ColorModel colorModel, final NumberRange<?>[] ranges,
- final MathTransform1D[] converters, final MathTransform1D[]
inverses)
+ final MathTransform1D[] converters, final MathTransform1D[]
inverses,
+ final SampleDimension[] sampleDimensions)
{
- super(source, sampleModel, colorModel, ranges, converters);
+ super(source, sampleModel, colorModel, ranges, converters,
sampleDimensions);
this.inverses = inverses;
}
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 330e7170e4..d21f3d2895 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
@@ -118,9 +118,12 @@ public interface Colorizer extends
Function<Colorizer.Target, Optional<ColorMode
/**
* Returns a description of the bands of the image to colorize.
- * This is typically obtained by {@link
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
+ * This information may be present if the image operation is invoked
by a
+ * {@link org.apache.sis.coverage.grid.GridCoverageProcessor}
operation,
+ * or if the source image contains the {@value
PlanarImage#SAMPLE_DIMENSIONS_KEY} property
*
* @return description of the bands of the image to colorize.
+ * @see org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()
*/
public Optional<List<SampleDimension>> getRanges() {
return Optional.ofNullable(ranges);
@@ -132,6 +135,7 @@ public interface Colorizer extends
Function<Colorizer.Target, Optional<ColorMode
* This information is ignored if the colorization uses many bands
(e.g. {@link #ARGB}).
*
* @return the band to colorize if the colorization algorithm uses
only one band.
+ * @see org.apache.sis.coverage.grid.ImageRenderer#setVisibleBand(int)
*/
public OptionalInt getVisibleBand() {
return (visibleBand >= 0) ? OptionalInt.of(visibleBand) :
OptionalInt.empty();
@@ -188,7 +192,7 @@ public interface Colorizer extends
Function<Colorizer.Target, Optional<ColorMode
* @param colors the colors to use for the specified range of sample
values.
* @return a colorizer which will interpolate the given colors in the
given range of values.
*
- * @see ImageProcessor#visualize(RenderedImage, List)
+ * @see ImageProcessor#visualize(RenderedImage)
*/
public static Colorizer forRanges(final Map<NumberRange<?>,Color[]>
colors) {
ArgumentChecks.ensureNonEmpty("colors", colors.entrySet());
@@ -209,19 +213,26 @@ public interface Colorizer extends
Function<Colorizer.Target, Optional<ColorMode
}
/**
- * Creates a colorizer which will associate colors to coverage categories.
+ * Creates a colorizer which will interpolate colors in ranges identified
by categories.
+ * This colorizer is similar to {@link #forRanges(Map)} (with the same
limitations) except that instead of mapping
+ * colors to predefined ranges of pixel values, it maps colors to
{@linkplain Category#getName() category names},
+ * {@linkplain org.apache.sis.measure.MeasurementRange#unit() units of
measurement} or other properties.
* The given function provides a way to colorize images without knowing in
advance the numerical values of pixels.
* For example, instead of specifying <cite>"pixel value 0 is blue, 1 is
green, 2 is yellow"</cite>,
* the given function allows to specify <cite>"Lakes are blue, Forests are
green, Sand is yellow"</cite>.
+ * The function can return {@code null} or empty color arrays for some
categories,
+ * which are interpreted as fully transparent pixels.
*
* <p>This colorizer is used when {@link Target#getRanges()} provides a
non-empty value.
- * The given function can return {@code null} or empty arrays for some
categories,
- * which are interpreted as fully transparent pixels.</p>
+ * That value is typically fetched from the {@value
PlanarImage#SAMPLE_DIMENSIONS_KEY} image property,
+ * which is itself typically fetched from {@link
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
+ * If no sample dimension information is available, then this colorizer do
not build a color model.
+ * A fallback can be specified with {@link #orElse(Colorizer)}.</p>
*
* @param colors colors to use for arbitrary categories of sample values.
* @return a colorizer which will apply colors determined by the {@link
Category} of sample values.
*
- * @see ImageProcessor#visualize(RenderedImage, List)
+ * @see ImageProcessor#visualize(RenderedImage)
*/
public static Colorizer forCategories(final Function<Category,Color[]>
colors) {
ArgumentChecks.ensureNonNull("colors", colors);
@@ -235,8 +246,9 @@ public interface Colorizer extends
Function<Colorizer.Target, Optional<ColorMode
if (visibleBand < ranges.size()) {
final SampleModel model = target.getSampleModel();
final var c = new ColorModelBuilder(colors);
- c.initialize(model, ranges.get(visibleBand));
- return
Optional.ofNullable(c.createColorModel(model.getDataType(),
model.getNumBands(), visibleBand));
+ 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/ImageAdapter.java
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
index 932afd66a6..0d0dd534bb 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageAdapter.java
@@ -34,12 +34,12 @@ import org.apache.sis.util.Disposable;
* indices), and all methods fetching tiles, delegate to the wrapped image.
*
* <h2>Design note</h2>
- * most non-abstract methods are final because {@link PixelIterator} (among
others) relies
+ * Most non-abstract methods are final because {@link PixelIterator} (among
others) relies
* on the fact that it can unwrap this image and still get the same pixel
values.
*
* <h2>Relationship with other classes</h2>
* This class is similar to {@link SourceAlignedImage} except that it does not
extend {@link ComputedImage}
- * and forward {@link #getTile(int, int)}, {@link #getData()} and other data
methods to the source image.
+ * and forwards {@link #getTile(int, int)}, {@link #getData()} and other data
methods to the source image.
*
* <h2>Requirements for subclasses</h2>
* All subclasses shall override {@link #equals(Object)} and {@link
#hashCode()}.
@@ -80,7 +80,7 @@ abstract class ImageAdapter extends PlanarImage {
/**
* Returns the names of properties of wrapped image.
*
- * @return all recognized property names.
+ * @return all recognized property names, or {@code null} if none.
*/
@Override
public String[] getPropertyNames() {
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
index f1f30fd2d4..e0ca82b6cc 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageProcessor.java
@@ -618,7 +618,7 @@ public class ImageProcessor implements Cloneable {
*
* @since 1.2
*/
- public DoubleUnaryOperator filterNodataValues(final Number... values) {
+ public static DoubleUnaryOperator filterNodataValues(final Number...
values) {
return (values != null) ?
StatisticsCalculator.filterNodataValues(values) : null;
}
@@ -820,10 +820,13 @@ public class ImageProcessor implements Cloneable {
* </tr><tr>
* <td>{@code "sampleDimensions"}</td>
* <td>Meaning of pixel values.</td>
- * <td>{@link SampleDimension}</td>
+ * <td>{@link SampleDimension} or {@code SampleDimension[]}</td>
* </tr>
* </table>
*
+ * <b>Note:</b> if no value is associated to the {@code
"sampleDimensions"} key, then the default
+ * value will be the {@value PlanarImage#SAMPLE_DIMENSIONS_KEY} image
property value if defined.
+ *
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method
parameters:
* <ul>
@@ -846,6 +849,44 @@ public class ImageProcessor implements Cloneable {
return RecoloredImage.stretchColorRamp(this, source, modifiers);
}
+ /**
+ * Returns an image augmented with user-defined property values.
+ * The specified properties overwrite any property that may be defined by
the source image.
+ * When an {@linkplain RenderedImage#getProperty(String) image property
value is requested}, the steps are:
+ *
+ * <ol>
+ * <li>If the {@code properties} map has an entry for the property name,
returns the associated value.
+ * It may be {@code null}.</li>
+ * <li>Otherwise if the property is defined by the source image, returns
its value.
+ * It may be {@code null}.</li>
+ * <li>Otherwise returns {@link java.awt.Image#UndefinedProperty}.</li>
+ * </ol>
+ *
+ * The given {@code properties} map is retained by reference in the
returned image.
+ * The {@code Map} is <em>not</em> copied in order to allow
+ * the use of custom implementations doing deferred calculations.
+ * If the caller intends to modify the map content after this method call,
+ * (s)he should use a {@link java.util.concurrent.ConcurrentMap}.
+ *
+ * <p>The returned image is "live": changes in {@code source} image
properties or in
+ * {@code properties} map entries are immediately reflected in the
returned image.</p>
+ *
+ * <p>Null are valid image property values. An entry associated with the
{@code null}
+ * value in the {@code properties} map is not the same as an absence of
entry.</p>
+ *
+ * @param source the source image to augment with user-specified
property values.
+ * @param properties properties overwriting or completing {@code
source} properties.
+ * @return an image augmented with the specified properties.
+ *
+ * @see RenderedImage#getPropertyNames()
+ * @see RenderedImage#getProperty(String)
+ *
+ * @since 1.4
+ */
+ public RenderedImage addUserProperties(final RenderedImage source, final
Map<String,Object> properties) {
+ return unique(new UserProperties(source, properties));
+ }
+
/**
* Selects a subset of bands in the given image. This method can also be
used for changing band order
* or repeating the same band from the source image. If the specified
{@code bands} are the same than
@@ -937,7 +978,7 @@ public class ImageProcessor implements Cloneable {
*
* @since 1.4
*/
- public RenderedImage aggregateBands(RenderedImage[] sources, int[][]
bandsPerSource) {
+ public RenderedImage aggregateBands(final RenderedImage[] sources, final
int[][] bandsPerSource) {
ArgumentChecks.ensureNonEmpty("sources", sources);
final Colorizer colorizer;
synchronized (this) {
@@ -1214,8 +1255,7 @@ public class ImageProcessor implements Cloneable {
*
* @param source the image to recolor for visualization purposes.
* @param colors colors to use for each range of values in the source
image.
- * @deprecated Replaced by {@link #visualize(RenderedImage, List)} with
{@code null} list argument
- * and colors map inferred from the {@link Colorizer}.
+ * @deprecated Replaced by {@link #visualize(RenderedImage)} with colors
map inferred from the {@link Colorizer}.
*/
@Deprecated(since="1.4", forRemoval=true)
public synchronized RenderedImage visualize(final RenderedImage source,
final Map<NumberRange<?>,Color[]> colors) {
@@ -1234,6 +1274,19 @@ public class ImageProcessor implements Cloneable {
}
}
+ /**
+ * @deprecated Replaced by {@link #visualize(RenderedImage)} with sample
dimensions
+ * read from the {@value PlanarImage#SAMPLE_DIMENSIONS_KEY}
property.
+ *
+ * @param ranges description of {@code source} bands, or {@code null} if
none. This is typically
+ * obtained by {@link
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
+ */
+ @Deprecated(since="1.4", forRemoval=true)
+ public RenderedImage visualize(final RenderedImage source, final
List<SampleDimension> ranges) {
+ ArgumentChecks.ensureNonNull("source", source);
+ return visualize(new Visualization.Builder(null, source, null,
ranges));
+ }
+
/**
* Returns an image where all sample values are indices of colors in an
{@link IndexColorModel}.
* If the given image stores sample values as unsigned bytes or short
integers, then those values
@@ -1244,7 +1297,26 @@ public class ImageProcessor implements Cloneable {
* There is no guarantee about the number of bands in returned image or
about which formula is used for converting
* floating point values to integer values.</p>
*
- * <h4>Specifying colors for ranges of pixel values</h4>
+ * <h4>How to specify colors</h4>
+ * The image colors can be controlled by the {@link Colorizer} set on this
image processor.
+ * It is possible to {@linkplain Colorizer#forInstance(ColorModel) specify
explicitely} the
+ * {@link ColorModel} to use, but this approach is unsafe because it
depends on the pixel values
+ * <em>after</em> their conversion to the visualization image, which is
implementation dependent.
+ * A safer approach is to define colors relative to pixel values
<em>before</em> their conversions.
+ * It can be done in two ways, depending on whether the {@value
PlanarImage#SAMPLE_DIMENSIONS_KEY}
+ * image property is defined or not.
+ * Those two ways are described in next sections and can be combined in a
chain of fallbacks.
+ * For example the following colorizer will choose colors based on sample
dimensions if available,
+ * or fallback on predefined ranges of pixel values otherwise:
+ *
+ * {@snippet lang="java" :
+ * Function<Category,Color[]> flexible = ...;
+ * Map<NumberRange<?>,Color[]> predefined = ...;
+ * processor.setColorizer(Colorizer.forCategories(flexible) //
Preferred way.
+ * .orElse(Colorizer.forRanges(predefined))); //
Fallback.
+ * }
+ *
+ * <h5>Specifying colors for ranges of pixel values</h5>
* When no {@link SampleDimension} information is available, the
recommended way to specify colors is like below.
* In this example, <var>min</var> and <var>max</var> are minimum and
maximum values
* (inclusive in this example, but they could be exclusive as well) in the
<em>source</em> image.
@@ -1263,7 +1335,7 @@ public class ImageProcessor implements Cloneable {
* The {@link Color} arrays may have any length; colors will be
interpolated as needed for fitting
* the ranges of values in the destination image.
*
- * <h4>Specifying colors for sample dimension categories</h4>
+ * <h5>Specifying colors for sample dimension categories</h5>
* If {@link SampleDimension} information is available, a more flexible
way to specify colors
* is to associate colors to category names instead of predetermined
ranges of pixel values.
* The ranges will be inferred indirectly, {@linkplain
Category#getSampleRange() from the categories}
@@ -1287,15 +1359,6 @@ public class ImageProcessor implements Cloneable {
* The {@link Color} arrays may have any length; colors will be
interpolated as needed for fitting
* the ranges of values in the destination image.
*
- * <p>The two approaches can be combined. For example the following
colorizer will choose colors based
- * on sample dimensions if available, or fallback on predefined ranges of
pixel values otherwise:</p>
- *
- * {@snippet lang="java" :
- * Function<Category,Color[]> flexible = ...;
- * Map<NumberRange<?>,Color[]> predefined = ...;
- *
processor.setColorizer(Colorizer.forCategories(flexible).orElse(Colorizer.forRanges(predefined)));
- * }
- *
* <h4>Properties used</h4>
* This operation uses the following properties in addition to method
parameters:
* <ul>
@@ -1303,16 +1366,17 @@ public class ImageProcessor implements Cloneable {
* </ul>
*
* @param source the image to recolor for visualization purposes.
- * @param ranges description of {@code source} bands, or {@code null} if
none. This is typically
- * obtained by {@link
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
* @return recolored image for visualization purposes only.
*
* @see Colorizer#forRanges(Map)
* @see Colorizer#forCategories(Function)
+ * @see PlanarImage#SAMPLE_DIMENSIONS_KEY
+ *
+ * @since 1.4
*/
- public RenderedImage visualize(final RenderedImage source, final
List<SampleDimension> ranges) {
+ public RenderedImage visualize(final RenderedImage source) {
ArgumentChecks.ensureNonNull("source", source);
- return visualize(new Visualization.Builder(null, source, null,
ranges));
+ return visualize(new Visualization.Builder(null, source, null));
}
/**
@@ -1322,7 +1386,7 @@ public class ImageProcessor implements Cloneable {
*
* <ol>
* <li><code>{@linkplain #resample(RenderedImage, Rectangle,
MathTransform) resample}(source, bounds, toSource)</code></li>
- * <li><code>{@linkplain #visualize(RenderedImage, List)
visualize}(resampled, ranges)</code></li>
+ * <li><code>{@linkplain #visualize(RenderedImage)
visualize}(resampled)</code></li>
* </ol>
*
* Combining above steps may be advantageous when the {@code resample(…)}
result is not needed for anything
@@ -1349,10 +1413,26 @@ public class ImageProcessor implements Cloneable {
* @param bounds domain of pixel coordinates of resampled image to
create.
* Updated by this method if {@link Resizing#EXPAND}
policy is applied.
* @param toSource conversion of pixel coordinates from resampled image
to {@code source} image.
- * @param ranges description of {@code source} bands, or {@code null}
if none. This is typically
- * obtained by {@link
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
* @return resampled and recolored image for visualization purposes only.
+ *
+ * @since 1.4
+ */
+ public RenderedImage visualize(final RenderedImage source, final Rectangle
bounds, final MathTransform toSource) {
+ ArgumentChecks.ensureNonNull("source", source);
+ ArgumentChecks.ensureNonNull("bounds", bounds);
+ ArgumentChecks.ensureNonNull("toSource", toSource);
+ ensureNonEmpty(bounds);
+ return visualize(new Visualization.Builder(bounds, source, toSource));
+ }
+
+ /**
+ * @deprecated Replaced by {@link #visualize(RenderedImage, Rectangle,
MathTransform)} with
+ * sample dimensions read from the {@value
PlanarImage#SAMPLE_DIMENSIONS_KEY} property.
+ *
+ * @param ranges description of {@code source} bands, or {@code null} if
none. This is typically
+ * obtained by {@link
org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}.
*/
+ @Deprecated(since="1.4", forRemoval=true)
public RenderedImage visualize(final RenderedImage source, final Rectangle
bounds, final MathTransform toSource,
final List<SampleDimension> ranges)
{
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
index 5fb2e07651..de86487842 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PlanarImage.java
@@ -37,6 +37,7 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.coverage.j2d.TileOpExecutor;
import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
import org.apache.sis.coverage.grid.GridGeometry; // For javadoc
+import org.apache.sis.coverage.SampleDimension;
import static java.lang.Math.multiplyFull;
@@ -143,6 +144,17 @@ public abstract class PlanarImage implements RenderedImage
{
*/
public static final String POSITIONAL_ACCURACY_KEY =
"org.apache.sis.PositionalAccuracy";
+ /**
+ * Key for a property defining a conversion from pixel values to the units
of measurement.
+ * The value should be an array of {@link SampleDimension} instances.
+ * The array length should be the number of bands.
+ *
+ * @see org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()
+ *
+ * @since 1.4
+ */
+ public static final String SAMPLE_DIMENSIONS_KEY =
"org.apache.sis.SampleDimensions";
+
/**
* Key of a property defining the resolutions of sample values in each
band. This property is recommended
* for images having sample values as floating point numbers. For example
if sample values were computed by
@@ -155,7 +167,7 @@ public abstract class PlanarImage implements RenderedImage {
* {@linkplain
org.apache.sis.coverage.grid.GridCoverage#forConvertedValues(boolean)
conversions from
* integer values to floating point values}.</p>
*/
- public static final String SAMPLE_RESOLUTIONS_KEY =
"org.apache.sis.SampleResolution";
+ public static final String SAMPLE_RESOLUTIONS_KEY =
"org.apache.sis.SampleResolutions";
/**
* Key of property providing statistics on sample values in each band.
Providing a value for this key
@@ -231,6 +243,9 @@ public abstract class PlanarImage implements RenderedImage {
* <td>{@value #POSITIONAL_ACCURACY_KEY}</td>
* <td>Estimation of positional accuracy, typically in metres or pixel
units.</td>
* </tr><tr>
+ * <td>{@value #SAMPLE_DIMENSIONS_KEY}</td>
+ * <td>Conversions from pixel values to the units of measurement for
each band.</td>
+ * </tr><tr>
* <td>{@value #SAMPLE_RESOLUTIONS_KEY}</td>
* <td>Resolutions of sample values in each band.</td>
* </tr><tr>
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 42d535f1d2..ce24f13234 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
@@ -44,7 +44,7 @@ import org.apache.sis.measure.NumberRange;
* for {@link ImageProcessor}, defined here for reducing {@link
ImageProcessor} size.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 1.1
*/
final class RecoloredImage extends ImageAdapter {
@@ -226,18 +226,9 @@ final class RecoloredImage extends ImageAdapter {
}
value = modifiers.get("sampleDimensions");
if (value != null) {
- if (value instanceof List<?>) {
- final List<?> ranges = (List<?>) value;
- if (visibleBand < ranges.size()) {
- value = ranges.get(visibleBand);
- }
- }
- if (value != null) {
- if (value instanceof SampleDimension) {
- range = (SampleDimension) value;
- } else {
- throw illegalPropertyType(modifiers,
"sampleDimensions", value);
- }
+ range = getSampleDimension(value, visibleBand);
+ if (range == null) {
+ throw illegalPropertyType(modifiers, "sampleDimensions",
value);
}
}
}
@@ -249,7 +240,7 @@ final class RecoloredImage extends ImageAdapter {
if (statistics == null) {
if (statsAllBands == null) {
final DoubleUnaryOperator[] sampleFilters = new
DoubleUnaryOperator[visibleBand + 1];
- sampleFilters[visibleBand] =
processor.filterNodataValues(nodataValues);
+ sampleFilters[visibleBand] =
ImageProcessor.filterNodataValues(nodataValues);
statsAllBands = processor.valueOfStatistics(statsSource,
areaOfInterest, sampleFilters);
}
if (statsAllBands != null && visibleBand <
statsAllBands.length) {
@@ -282,6 +273,9 @@ final class RecoloredImage extends ImageAdapter {
final int size = icm.getMapSize();
int validMin = 0;
int validMax = size - 1; // Inclusive.
+ if (range == null) {
+ range =
getSampleDimension(source.getProperty(PlanarImage.SAMPLE_DIMENSIONS_KEY),
visibleBand);
+ }
if (range != null) {
double span = 0;
for (final Category category : range.getCategories()) {
@@ -345,6 +339,28 @@ final class RecoloredImage extends ImageAdapter {
return ImageProcessor.unique(new RecoloredImage(source, cm, minimum,
maximum));
}
+ /**
+ * Gets the sample dimension from the given property value.
+ *
+ * @param value the property value.
+ * @param visibleBand index of the element to fetch if the property is a
list or an array.
+ * @return the sample dimension at the given visible band index, or {@code
null} if none.
+ */
+ private static SampleDimension getSampleDimension(Object value, final int
visibleBand) {
+ if (value instanceof SampleDimension[]) {
+ final var ranges = (SampleDimension[]) value;
+ if (visibleBand < ranges.length) {
+ return ranges[visibleBand];
+ }
+ } else if (value instanceof List<?>) {
+ final var ranges = (List<?>) value;
+ if (visibleBand < ranges.size()) {
+ value = ranges.get(visibleBand);
+ }
+ }
+ return (value instanceof SampleDimension) ? (SampleDimension) value :
null;
+ }
+
/**
* Returns the exception to be thrown when a property is of illegal type.
*/
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
index f9cc28d46f..0aa72e1f55 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ResampledImage.java
@@ -479,24 +479,25 @@ public class ResampledImage extends ComputedImage {
/**
* Gets a property from this image. Current default implementation
supports the following keys
- * (more properties may be added to this list in future Apache SIS
versions):
+ * (more properties may be added to this list in any future Apache SIS
versions):
*
* <ul>
* <li>{@value #POSITIONAL_ACCURACY_KEY}</li>
* <li>{@value #POSITIONAL_CONSISTENCY_KEY}</li>
+ * <li>{@value #SAMPLE_DIMENSIONS_KEY} (forwarded to the source
image)</li>
* <li>{@value #SAMPLE_RESOLUTIONS_KEY} (forwarded to the source
image)</li>
* <li>{@value #MASK_KEY} if the image uses floating point numbers.</li>
* </ul>
*
- * <div class="note"><b>Note:</b>
- * the sample resolutions are retained because they should have
approximately the same values before and after
+ * <h4>Note on sample values</h4>
+ * The sample resolutions are retained because they should have
approximately the same values before and after
* resampling. {@linkplain #STATISTICS_KEY Statistics} are not in this
list because, while minimum and maximum
* values should stay approximately the same, the average value and
standard deviation may be quite different.
- * </div>
*/
@Override
public Object getProperty(final String key) {
switch (key) {
+ case SAMPLE_DIMENSIONS_KEY:
case SAMPLE_RESOLUTIONS_KEY: {
return getSource().getProperty(key);
}
@@ -527,6 +528,7 @@ public class ResampledImage extends ComputedImage {
public String[] getPropertyNames() {
final String[] inherited = getSource().getPropertyNames();
final String[] names = {
+ SAMPLE_DIMENSIONS_KEY,
SAMPLE_RESOLUTIONS_KEY,
POSITIONAL_ACCURACY_KEY,
POSITIONAL_CONSISTENCY_KEY,
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/UserProperties.java
b/core/sis-feature/src/main/java/org/apache/sis/image/UserProperties.java
new file mode 100644
index 0000000000..a26f9718de
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/UserProperties.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.image;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
+import java.awt.Image;
+import java.awt.image.RenderedImage;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * An image with some properties overwritten by user-specified properties.
+ * The property calculations may be deferred, and {@code null} is a valid
property value.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since 1.4
+ */
+final class UserProperties extends ImageAdapter {
+ /**
+ * The user-specified properties which may overwrite source image
properties.
+ * This is a reference to the map specified at construction time, not a
copy.
+ * No copy is done for allowing the use of instances doing deferred
computation.
+ * It is legal to have {@code null} value associated to keys: the meaning
is not
+ * the same as "undefined properties".
+ *
+ * <p>This {@code UserProperties} class shall not modify the content of
this map.</p>
+ */
+ private final Map<String,Object> properties;
+
+ /**
+ * Creates a new wrapper for the given image.
+ *
+ * @param source the image to wrap. The map is retained directly (not
cloned).
+ */
+ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
+ UserProperties(final RenderedImage source, final Map<String,Object>
properties) {
+ super(source);
+ ArgumentChecks.ensureNonNull("properties", properties);
+ this.properties = properties;
+ }
+
+ /**
+ * Returns the names of all supported properties, including in wrapped
image.
+ * The property names are computed on each invocation for allowing dynamic
changes
+ * in source image properties or in {@link #properties} map.
+ *
+ * @return all recognized property names, or {@code null} if none.
+ */
+ @Override
+ public String[] getPropertyNames() {
+ String[] names = super.getPropertyNames();
+ if (!properties.isEmpty()) {
+ final Set<String> union;
+ if (names != null) {
+ union = new HashSet<>(Arrays.asList(names));
+ union.addAll(properties.keySet());
+ } else {
+ union = properties.keySet();
+ }
+ names = union.toArray(String[]::new);
+ }
+ return (names.length != 0) ? names : null;
+ }
+
+ /**
+ * Gets a property from this image or from its source.
+ *
+ * @param name name of the property to get.
+ * @return the property for the given name ({@code null} is a valid
result),
+ * or {@link Image#UndefinedProperty} if the given name is not a
recognized property name.
+ */
+ @Override
+ public Object getProperty(final String name) {
+ Object value = properties.getOrDefault(name, Image.UndefinedProperty);
+ if (value == Image.UndefinedProperty) {
+ value = super.getProperty(name);
+ }
+ return value;
+ }
+
+ /**
+ * Compares the given object with this image for equality. This method
should be quick and compare
+ * how images compute their values from their sources; it should not
compare the actual pixel values.
+ */
+ @Override
+ public boolean equals(final Object object) {
+ return super.equals(object) && properties.equals(((UserProperties)
object).properties);
+ }
+
+ /**
+ * Returns a hash code value for this image. This method should be quick.
+ */
+ @Override
+ public int hashCode() {
+ return super.hashCode() + 71 * properties.hashCode();
+ }
+
+ /**
+ * Appends a content to show in the {@link #toString()} representation.
+ */
+ @Override
+ Class<? extends ImageAdapter> appendStringContent(final StringBuilder
buffer) {
+ buffer.append(properties.keySet());
+ return UserProperties.class;
+ }
+}
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 4b8f38804f..35229898d1 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
@@ -41,7 +41,6 @@ import
org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.internal.coverage.SampleDimensions;
import org.apache.sis.internal.coverage.CompoundTransform;
import org.apache.sis.internal.coverage.j2d.ColorModelBuilder;
-import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
import org.apache.sis.internal.coverage.j2d.ImageLayout;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
import org.apache.sis.internal.feature.Resources;
@@ -118,7 +117,7 @@ final class Visualization extends ResampledImage {
*
* <p>This builder accepts two kinds of input:</p>
* <ul>
- * <li>Non-null {@link #sourceBands} and {@link
Target#categoryColors}.</li>
+ * <li>Non-null {@link #sampleDimensions} and {@link
Target#categoryColors}.</li>
* <li>Non-null {@link Target#rangeColors}.</li>
* </ul>
*
@@ -136,8 +135,8 @@ final class Visualization extends ResampledImage {
/** Number of bands of the image to create. */
private static final int NUM_BANDS = 1;
- /** Band to make visible. */
- private static final int VISIBLE_BAND =
ColorModelFactory.DEFAULT_VISIBLE_BAND;
+ /** Band to make visible among the remaining {@value #NUM_BANDS}
bands. */
+ private static final int VISIBLE_BAND = 0;
//// ┌─────────────────────────────────────┐
//// │ Arguments given by user │
@@ -153,7 +152,7 @@ final class Visualization extends ResampledImage {
private MathTransform toSource;
/** Description of {@link #source} bands, or {@code null} if none. */
- private List<SampleDimension> sourceBands;
+ private SampleDimension[] sampleDimensions;
//// ┌─────────────────────────────────────┐
//// │ Given by ImageProcesor.configure(…) │
@@ -190,18 +189,30 @@ final class Visualization extends ResampledImage {
/**
* Creates a builder for a visualization image with colors inferred
from sample dimensions.
*
- * @param bounds desired domain of pixel coordinates, or {@code
null} if same as {@code source} image.
- * @param source the image for which to replace the color model.
- * @param toSource pixel coordinates conversion to {@code source}
image, or {@code null} if none.
- * @param sourceBands description of {@code source} bands.
+ * @param bounds desired domain of pixel coordinates, or {@code
null} if same as {@code source} image.
+ * @param source the image for which to replace the color model.
+ * @param toSource pixel coordinates conversion to {@code source}
image, or {@code null} if none.
*/
+ Builder(final Rectangle bounds, final RenderedImage source, final
MathTransform toSource) {
+ this.bounds = bounds;
+ this.source = source;
+ this.toSource = toSource;
+ Object ranges = source.getProperty(SAMPLE_DIMENSIONS_KEY);
+ if (ranges instanceof SampleDimension[]) {
+ sampleDimensions = (SampleDimension[]) ranges;
+ }
+ }
+
+ @Deprecated(since="1.4", forRemoval=true)
Builder(final Rectangle bounds, final RenderedImage source, final
MathTransform toSource,
- final List<SampleDimension> sourceBands)
+ final List<SampleDimension> sampleDimensions)
{
- this.bounds = bounds;
- this.source = source;
- this.toSource = toSource;
- this.sourceBands = sourceBands;
+ this.bounds = bounds;
+ this.source = source;
+ this.toSource = toSource;
+ if (sampleDimensions != null) {
+ this.sampleDimensions =
sampleDimensions.toArray(SampleDimension[]::new);
+ }
}
/**
@@ -232,13 +243,16 @@ final class Visualization extends ResampledImage {
}
/*
* Skip any previous `RecoloredImage` since we will replace the
`ColorModel` by a new one.
+ * Discards image properties such as statistics because this image
is not for computation.
* Keep only the band to make visible in order to reduce the
amount of calculation during
* resampling and for saving memory.
*/
- while (source instanceof RecoloredImage) {
- source = ((RecoloredImage) source).source;
+ while (source instanceof ImageAdapter) {
+ source = ((ImageAdapter) source).source;
}
source = BandSelectImage.create(source, new int[] {visibleBand});
+ final SampleDimension visibleSD = (sampleDimensions != null &&
visibleBand < sampleDimensions.length)
+ ? sampleDimensions[visibleBand] :
null;
/*
* If there is no conversion of pixel coordinates, there is no
need for interpolations.
* In such case the `Visualization.computeTile(…)` implementation
takes a shortcut which
@@ -258,7 +272,7 @@ final class Visualization extends ResampledImage {
* which must be done before to build the color model.
*/
sampleModel =
layout.createBandedSampleModel(ColorModelBuilder.TYPE_COMPACT, NUM_BANDS,
source, bounds);
- final Target target = new Target(sampleModel, visibleBand,
sourceBands != null);
+ final Target target = new Target(sampleModel, VISIBLE_BAND,
visibleSD != null);
if (colorizer != null) {
colorModel = colorizer.apply(target).orElse(null);
}
@@ -267,8 +281,8 @@ final class Visualization extends ResampledImage {
* There is different ways to setup the builder, depending on
which `Colorizer` is used.
* In precedence order:
*
- * - rangeColors : Map<NumberRange<?>,Color[]>
- * - sourceBands : List<SampleDimension>
+ * - rangeColors : Map<NumberRange<?>,Color[]>
+ * - sampleDimensions : SampleDimension[]
* - statistics
*/
boolean initialized;
@@ -282,7 +296,7 @@ final class Visualization extends ResampledImage {
* in various ways: sample dimensions, scaled color model, or
image statistics in last resort.
*/
builder = new ColorModelBuilder(target.categoryColors);
- initialized = (sourceBands != null) &&
builder.initialize(coloredSource.getSampleModel(),
sourceBands.get(visibleBand));
+ initialized =
builder.initialize(coloredSource.getSampleModel(), visibleSD);
if (initialized) {
/*
* If we have been able to configure ColorModelBuilder
using SampleDimension, apply an adjustment
@@ -314,7 +328,7 @@ final class Visualization extends ResampledImage {
* If none of above `ColorModelBuilder` configurations worked,
use statistics in last resort.
* We do that after we reduced the image to a single band in
order to reduce the amount of calculations.
*/
- final DoubleUnaryOperator[] sampleFilters =
SampleDimensions.toSampleFilters(processor, sourceBands);
+ final DoubleUnaryOperator[] sampleFilters =
SampleDimensions.toSampleFilters(visibleSD);
final Statistics statistics =
processor.valueOfStatistics(source, null, sampleFilters)[VISIBLE_BAND];
builder.initialize(statistics.minimum(), statistics.maximum());
}
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
index 349c4e6d6d..d896643025 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/SampleDimensions.java
@@ -19,6 +19,8 @@ package org.apache.sis.internal.coverage;
import java.util.List;
import java.util.Optional;
import java.util.function.DoubleUnaryOperator;
+import java.awt.Shape;
+import java.awt.image.RenderedImage;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.Category;
import org.apache.sis.image.ImageProcessor;
@@ -30,10 +32,20 @@ import org.apache.sis.util.Static;
* Utility methods working on {@link SampleDimension} instances.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
* @since 1.2
*/
public final class SampleDimensions extends Static {
+ /**
+ * The sample dimensions of a {@link
org.apache.sis.image.BandedSampleConverter} image.
+ * We use this thread-local variable as an internal workaround for an
parameter that we
+ * do not expose in the public API of {@link ImageProcessor}.
+ *
+ * <p>The content of the array in this thread-local variable shall not be
modified,
+ * because it may be a direct reference to an internal array (not a
clone).</p>
+ */
+ public static final ThreadLocal<SampleDimension[]> CONVERTED_BANDS = new
ThreadLocal<>();
+
/**
* Do not allow instantiation of this class.
*/
@@ -49,13 +61,13 @@ public final class SampleDimensions extends Static {
* @return the background values, or {@code null} if the given argument
was null.
* Otherwise the returned array is never null but may contain null
elements.
*/
- public static Number[] backgrounds(final List<SampleDimension> bands) {
+ public static Number[] backgrounds(final SampleDimension... bands) {
if (bands == null) {
return null;
}
- final Number[] fillValues = new Number[bands.size()];
+ final Number[] fillValues = new Number[bands.length];
for (int i=fillValues.length; --i >= 0;) {
- final SampleDimension band = bands.get(i);
+ final SampleDimension band = bands[i];
final Optional<Number> bg = band.getBackground();
if (bg.isPresent()) {
fillValues[i] = bg.get();
@@ -66,25 +78,26 @@ public final class SampleDimensions extends Static {
/**
* Returns the {@code sampleFilters} arguments to use in a call to
- * {@link ImageProcessor#statistics ImageProcessor.statistics(…)} for
excluding no-data values.
+ * {@code ImageProcessor.statistics(…)} for excluding no-data values.
* If the given sample dimensions are {@linkplain
SampleDimension#converted() converted to units of measurement},
* then all "no data" values are already NaN values and this method
returns an array of {@code null} operators.
- * Otherwise this method returns an array of operators that covert "no
data" values to {@link Double#NaN}.
+ * Otherwise this method returns an array of operators that convert "no
data" values to {@link Double#NaN}.
*
* <p>This method is not in public API because it partially duplicates the
work
* of {@linkplain SampleDimension#getTransferFunction() transfer
function}.</p>
*
- * @param processor the processor to use for creating {@link
DoubleUnaryOperator}.
- * @param bands the sample dimensions for which to create {@code
sampleFilters}, or {@code null}.
+ * @param bands the sample dimensions for which to create {@code
sampleFilters}, or {@code null}.
* @return the filters, or {@code null} if {@code bands} was null. The
array may contain null elements.
+ *
+ * @see ImageProcessor#statistics(RenderedImage, Shape,
DoubleUnaryOperator...)
*/
- public static DoubleUnaryOperator[] toSampleFilters(final ImageProcessor
processor, final List<SampleDimension> bands) {
+ public static DoubleUnaryOperator[] toSampleFilters(final
SampleDimension... bands) {
if (bands == null) {
return null;
}
- final DoubleUnaryOperator[] sampleFilters = new
DoubleUnaryOperator[bands.size()];
+ final DoubleUnaryOperator[] sampleFilters = new
DoubleUnaryOperator[bands.length];
for (int i = 0; i < sampleFilters.length; i++) {
- final SampleDimension band = bands.get(i);
+ final SampleDimension band = bands[i];
if (band != null) {
final List<Category> categories = band.getCategories();
final Number[] nodataValues = new Number[categories.size()];
@@ -103,7 +116,7 @@ public final class SampleDimensions extends Static {
nodataValues[j] = value;
}
}
- sampleFilters[i] = processor.filterNodataValues(nodataValues);
+ sampleFilters[i] =
ImageProcessor.filterNodataValues(nodataValues);
}
}
return sampleFilters;
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
index 7c98795851..962168a6c1 100644
---
a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
+++
b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/ConvertedGridCoverageTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.coverage.grid;
import java.util.List;
import java.awt.image.DataBuffer;
+import java.awt.image.RenderedImage;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform1D;
import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -32,6 +33,7 @@ import org.junit.Test;
import static org.apache.sis.test.FeatureAssert.*;
import static org.apache.sis.test.TestUtilities.getSingleton;
+import static org.apache.sis.image.PlanarImage.SAMPLE_DIMENSIONS_KEY;
/**
@@ -39,7 +41,7 @@ import static org.apache.sis.test.TestUtilities.getSingleton;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
* @since 1.1
*/
public final class ConvertedGridCoverageTest extends TestCase {
@@ -69,6 +71,20 @@ public final class ConvertedGridCoverageTest extends
TestCase {
return coverage;
}
+ /**
+ * Creates a rendering of the given coverage and verifies that it contains
+ * a property for the sample dimensions.
+ */
+ private static RenderedImage render(final GridCoverage coverage) {
+ final RenderedImage image = coverage.render(null);
+ final Object bands = image.getProperty(SAMPLE_DIMENSIONS_KEY);
+ assertInstanceOf(SAMPLE_DIMENSIONS_KEY, SampleDimension[].class,
bands);
+ assertArrayEquals(SAMPLE_DIMENSIONS_KEY,
+ coverage.getSampleDimensions().toArray(SampleDimension[]::new),
+ (SampleDimension[]) bands);
+ return image;
+ }
+
/**
* Tests forward conversion from packed values to "geophysics" values.
* Test includes a conversion of an integer value to {@link Float#NaN}.
@@ -79,12 +95,12 @@ public final class ConvertedGridCoverageTest extends
TestCase {
/*
* Verify values before and after conversion.
*/
- assertValuesEqual(coverage.forConvertedValues(false).render(null), 0,
new double[][] {
+ assertValuesEqual(render(coverage.forConvertedValues(false)), 0, new
double[][] {
{-1, 3}
});
final float nan = MathFunctions.toNanFloat(-1);
assertTrue(Float.isNaN(nan));
- assertValuesEqual(coverage.forConvertedValues(true).render(null), 0,
new double[][] {
+ assertValuesEqual(render(coverage.forConvertedValues(true)), 0, new
double[][] {
{nan, 3}
});
}
@@ -101,7 +117,7 @@ public final class ConvertedGridCoverageTest extends
TestCase {
}, null);
assertSame(target, target.forConvertedValues(true));
assertSame(source, target.forConvertedValues(false));
- assertValuesEqual(target.render(null), 0, new double[][] {
+ assertValuesEqual(render(target), 0, new double[][] {
{90, 130} // {-1, 3} × 10 + 100
});
final SampleDimension band =
getSingleton(target.getSampleDimensions());
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
index 4ad4b9f097..cf5c1a23bb 100644
---
a/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
+++
b/core/sis-feature/src/test/java/org/apache/sis/image/ImageProcessorTest.java
@@ -34,11 +34,38 @@ import static
org.apache.sis.test.TestUtilities.getSingleton;
* Tests {@link ImageProcessor}.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
* @since 1.1
*/
@DependsOn(org.apache.sis.internal.processing.isoline.IsolinesTest.class)
public final class ImageProcessorTest extends TestCase {
+ /**
+ * The processor to test.
+ */
+ private final ImageProcessor processor;
+
+ /**
+ * Creates a new test case.
+ */
+ public ImageProcessorTest() {
+ processor = new ImageProcessor();
+ }
+
+ /**
+ * Tests {@link ImageProcessor#addUserProperties(RenderedImage, Map)}.
+ */
+ @Test
+ public void testAddUserProperties() {
+ final String key = "my-property";
+ final String value = "my-value";
+ final RenderedImage source = new BufferedImage(2, 2,
BufferedImage.TYPE_BYTE_BINARY);
+ final RenderedImage image = processor.addUserProperties(source,
Map.of(key, value));
+ assertSame(BufferedImage.UndefinedProperty, source.getProperty(key));
+ assertSame(BufferedImage.UndefinedProperty,
image.getProperty("another-property"));
+ assertSame(value, image.getProperty(key));
+ assertArrayEquals(new String[] {key}, image.getPropertyNames());
+ }
+
/**
* Tests {@link ImageProcessor#isolines(RenderedImage, double[][],
MathTransform)}.
*/
@@ -46,8 +73,6 @@ public final class ImageProcessorTest extends TestCase {
public void testIsolines() {
final BufferedImage image = new BufferedImage(3, 3,
BufferedImage.TYPE_BYTE_BINARY);
image.getRaster().setSample(1, 1, 0, 1);
-
- final ImageProcessor processor = new ImageProcessor();
boolean parallel = false;
do {
processor.setExecutionMode(parallel ?
ImageProcessor.Mode.SEQUENTIAL : ImageProcessor.Mode.PARALLEL);
diff --git
a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
index ad822fa29d..5fa7d1c10a 100644
---
a/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
+++
b/core/sis-feature/src/test/java/org/apache/sis/image/StatisticsCalculatorTest.java
@@ -161,7 +161,7 @@ public final class StatisticsCalculatorTest extends
TestCase {
public void testWithSampleFilters() {
final ImageProcessor operations = new ImageProcessor();
sampleFilters = new DoubleUnaryOperator[] {
- operations.filterNodataValues(100, 51324, 51323, 201, 310)
+ ImageProcessor.filterNodataValues(100, 51324, 51323, 201, 310)
};
compareParallelWithSequential(operations, 101, 51322);
}
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 2ba1782b1f..bea3a65a64 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
@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.Objects;
+import java.util.logging.Logger;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.awt.Graphics2D;
@@ -29,7 +30,6 @@ import java.awt.image.RenderedImage;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
-import java.util.logging.Logger;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
import org.opengis.metadata.extent.GeographicBoundingBox;
@@ -194,7 +194,7 @@ public class RenderingData implements Cloneable {
* @see #setImageSpace(GridGeometry, List, int[])
* @see #statistics()
*/
- private List<SampleDimension> dataRanges;
+ private SampleDimension[] dataRanges;
/**
* Conversion or transformation from {@linkplain #data} CRS to {@linkplain
PlanarCanvas#getObjectiveCRS()
@@ -306,10 +306,10 @@ public class RenderingData implements Cloneable {
*/
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
public final void setImageSpace(final GridGeometry domain, final
List<SampleDimension> ranges, final int[] xyDims) {
- processor.setFillValues(SampleDimensions.backgrounds(ranges));
- dataRanges = ranges; // Not cloned because already an
unmodifiable list.
+ dataRanges = (ranges != null) ?
ranges.toArray(SampleDimension[]::new) : null;
dataGeometry = domain;
xyDimensions = xyDims;
+ processor.setFillValues(SampleDimensions.backgrounds(dataRanges));
/*
* If the grid geometry does not define a "grid to CRS" transform, set
it to an identity transform.
* We do that because this class needs a complete `GridGeometry` as
much as possible.
@@ -536,7 +536,7 @@ public class RenderingData implements Cloneable {
image = coarse.render(sliceExtent);
}
}
- statistics = processor.valueOfStatistics(image, null,
SampleDimensions.toSampleFilters(processor, dataRanges));
+ statistics = processor.valueOfStatistics(image, null,
SampleDimensions.toSampleFilters(dataRanges));
}
final Map<String,Object> modifiers = new HashMap<>(8);
modifiers.put("statistics", statistics);
@@ -652,7 +652,8 @@ public class RenderingData implements Cloneable {
}
/*
* Apply a map projection on the image, then convert the floating
point results to integer values
- * that we can use with IndexColorModel.
+ * that we can use with `IndexColorModel`. The two operations
(resampling and conversions) are
+ * combined in a single "visualization" operation of efficiency.
*
* TODO: if `colors` is null, instead of defaulting to
`ColorModelBuilder.GRAYSCALE` we should get the colors
* from the current ColorModel. This work should be done in
`ColorModelBuilder` by converting the ranges
@@ -661,13 +662,30 @@ public class RenderingData implements Cloneable {
*/
if (CREATE_INDEX_COLOR_MODEL) {
final ColorModelType ct =
ColorModelType.find(recoloredImage.getColorModel());
- if (ct.isSlow || (ct.useColorRamp && processor.getCategoryColors()
!= null)) {
- return processor.visualize(recoloredImage, bounds,
displayToCenter, dataRanges);
+ if (ct.isSlow || (ct.useColorRamp && processor.getColorizer() !=
null)) {
+ return
processor.visualize(withSampleDimensions(recoloredImage), bounds,
displayToCenter);
}
}
return processor.resample(recoloredImage, bounds, displayToCenter);
}
+ /**
+ * Returns an image augmented with the sample dimensions if not already
present.
+ * If the property is present but with a different value, the {@link
#dataRanges}
+ * will overwrite the image property value.
+ *
+ * @param image the image for which to add a property if not already
present.
+ * @return image augmented with the given property.
+ */
+ private RenderedImage withSampleDimensions(RenderedImage image) {
+ final String key = PlanarImage.SAMPLE_DIMENSIONS_KEY;
+ final SampleDimension[] value = dataRanges;
+ if (!Objects.deepEquals(image.getProperty(key), value)) {
+ image = processor.addUserProperties(image, Map.of(key, value));
+ }
+ return image;
+ }
+
/**
* Conversion or transformation from {@linkplain
PlanarCanvas#getObjectiveCRS() objective CRS} to
* {@linkplain #data} CRS. This transform will include {@code
WraparoundTransform} steps if needed.
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
index 497a3b8b0e..fc83cb467c 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
@@ -473,24 +473,24 @@ abstract class RasterStore extends PRJDataStore
implements GridCoverageResource
final GridCoverage2D createCoverage(final GridGeometry domain, final
RangeArgument range,
final WritableRaster data, final
Statistics stats)
{
+ final SampleDimension[] bands = range.select(sampleDimensions);
Hashtable<String,Object> properties = null;
if (stats != null) {
final Statistics[] as = new Statistics[range.getNumBands()];
Arrays.fill(as, stats);
properties = new Hashtable<>();
properties.put(PlanarImage.STATISTICS_KEY, as);
+ properties.put(PlanarImage.SAMPLE_DIMENSIONS_KEY, bands);
}
- List<SampleDimension> bands = sampleDimensions;
ColorModel cm = colorModel;
if (!range.isIdentity()) {
- bands = Arrays.asList(range.select(sampleDimensions));
cm = range.select(cm);
if (cm == null) {
- final SampleDimension band = bands.get(VISIBLE_BAND);
+ final SampleDimension band = bands[VISIBLE_BAND];
cm = ColorModelFactory.createGrayScale(data.getSampleModel(),
VISIBLE_BAND, band.getSampleRange().orElse(null));
}
}
- return new GridCoverage2D(domain, bands, new BufferedImage(cm, data,
false, properties));
+ return new GridCoverage2D(domain, Arrays.asList(bands), new
BufferedImage(cm, data, false, properties));
}
/**