This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 9627d2e9cca92fee58ebb298f1f0a2b52fd6b843 Author: Martin Desruisseaux <[email protected]> AuthorDate: Mon Mar 27 16:14:38 2023 +0200 Initial version of an `Colorizer` interface for building the `ColorModel` of a computed image. Replacement is not yet done everywhere. https://issues.apache.org/jira/browse/SIS-577 --- .../coverage/grid/BandAggregateGridCoverage.java | 18 +- .../sis/coverage/grid/GridCoverageProcessor.java | 12 +- .../org/apache/sis/image/BandAggregateImage.java | 32 ++- .../apache/sis/image/BandedSampleConverter.java | 13 +- .../main/java/org/apache/sis/image/Colorizer.java | 265 +++++++++++++++++++++ .../org/apache/sis/image/CombinedImageLayout.java | 43 ++-- .../java/org/apache/sis/image/ComputedImage.java | 22 +- .../java/org/apache/sis/image/ImageProcessor.java | 113 +++++++-- .../internal/coverage/j2d/ColorModelFactory.java | 240 ++++++++++++------- .../sis/internal/coverage/j2d/ColorsForRange.java | 6 +- .../sis/internal/coverage/j2d/ImageUtilities.java | 2 +- .../apache/sis/image/BandAggregateImageTest.java | 2 +- .../sis/util/collection/WeakValueHashMap.java | 89 +++++-- .../org/apache/sis/internal/netcdf/Convention.java | 3 +- .../aggregate/BandAggregateGridResource.java | 19 +- .../aggregate/BandAggregateGridResourceTest.java | 2 +- 16 files changed, 663 insertions(+), 218 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java index 303f4d7b14..6b95c83080 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java @@ -18,7 +18,6 @@ package org.apache.sis.coverage.grid; import java.util.Map; import java.util.TreeMap; -import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.operation.TransformException; @@ -75,15 +74,10 @@ final class BandAggregateGridCoverage extends GridCoverage { */ private final DataType dataType; - /** - * The color model to apply on aggregated image, or {@code null} for default. - * If {@code null}, the color model will be inferred from the aggregated number - * of bands and the sample data type. - */ - private final ColorModel colors; - /** * The processor to use for creating images. + * The processor {@linkplain ImageProcessor#getColorizer() colorizer} + * will determine the color model applied on the aggregated images. */ private final ImageProcessor processor; @@ -91,20 +85,16 @@ final class BandAggregateGridCoverage extends GridCoverage { * Creates a new band aggregated coverage from the given sources. * * @param aggregate the source grid coverages together with bands to select. - * @param colors the color model to apply on aggregated image, or {@code null} for default. * @param processor the processor to use for creating images. * @throws IllegalArgumentException if there is an incompatibility between some source coverages * or if some band indices are duplicated or outside their range of validity. */ - BandAggregateGridCoverage(final MultiSourcesArgument<GridCoverage> aggregate, final ColorModel colors, - final ImageProcessor processor) - { + BandAggregateGridCoverage(final MultiSourcesArgument<GridCoverage> aggregate, final ImageProcessor processor) { super(aggregate.domain(GridCoverage::getGridGeometry), aggregate.ranges()); this.sources = aggregate.sources(); this.bandsPerSource = aggregate.bandsPerSource(); this.numBands = aggregate.numBands(); this.sourceOfGridToCRS = aggregate.sourceOfGridToCRS(); - this.colors = colors; this.processor = processor; this.dataType = sources[0].getBandType(); for (int i=1; i < sources.length; i++) { @@ -150,7 +140,7 @@ final class BandAggregateGridCoverage extends GridCoverage { for (int i=0; i<images.length; i++) { images[i] = sources[i].render(sliceExtent); } - return processor.aggregateBands(images, bandsPerSource, colors); + return processor.aggregateBands(images, bandsPerSource); } /** 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 ea557d7a04..31465949a6 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 @@ -652,13 +652,13 @@ public class GridCoverageProcessor implements Cloneable { * @return the aggregated coverage, or {@code sources[0]} returned directly if only one coverage was supplied. * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others. * - * @see #aggregateRanges(GridCoverage[], int[][], ColorModel) + * @see #aggregateRanges(GridCoverage[], int[][]) * @see ImageProcessor#aggregateBands(RenderedImage...) * * @since 1.4 */ public GridCoverage aggregateRanges(final GridCoverage... sources) { - return aggregateRanges(sources, null, null); + return aggregateRanges(sources, (int[][]) null); } /** @@ -674,24 +674,22 @@ public class GridCoverageProcessor implements Cloneable { * @param sources coverages whose bands shall be aggregated, in order. At least one coverage must be provided. * @param bandsPerSource bands to use for each source coverage, in order. * May be {@code null} or may contain {@code null} elements. - * @param colors the color model to apply on aggregated image, or {@code null} for inferring - * a default color model using aggregated number of bands and sample data type. * @return the aggregated coverage, or one of the sources if it can be used directly. * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others. * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity. * - * @see ImageProcessor#aggregateBands(RenderedImage[], int[][], ColorModel) + * @see ImageProcessor#aggregateBands(RenderedImage[], int[][]) * * @since 1.4 */ - public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] bandsPerSource, ColorModel colors) { + public GridCoverage aggregateRanges(GridCoverage[] sources, int[][] bandsPerSource) { final var aggregate = new MultiSourcesArgument<>(sources, bandsPerSource); aggregate.identityAsNull(); aggregate.validate(GridCoverage::getSampleDimensions); if (aggregate.isIdentity()) { return aggregate.sources()[0]; } - return new BandAggregateGridCoverage(aggregate, colors, imageProcessor); + return new BandAggregateGridCoverage(aggregate, imageProcessor); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java index d0abc2f628..fd7aaa5758 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/BandAggregateImage.java @@ -77,14 +77,14 @@ final class BandAggregateImage extends ComputedImage { * * @param sources images to combine, in order. * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements. - * @param colors the color model to use for this image, or {@code null} for automatic. + * @param colorizer provider of color model to use for this image, or {@code null} for automatic. * @throws IllegalArgumentException if there is an incompatibility between some source images * or if some band indices are duplicated or outside their range of validity. * @return the band aggregate image. */ @Workaround(library="JDK", version="1.8") - static RenderedImage create(RenderedImage[] sources, int[][] bandsPerSource, ColorModel colors) { - var image = new BandAggregateImage(CombinedImageLayout.create(sources, bandsPerSource), colors); + static RenderedImage create(RenderedImage[] sources, int[][] bandsPerSource, Colorizer colorizer) { + var image = new BandAggregateImage(CombinedImageLayout.create(sources, bandsPerSource), colorizer); if (image.filteredSources.length == 1) { final RenderedImage c = image.filteredSources[0]; if (image.colorModel == null) { @@ -101,25 +101,21 @@ final class BandAggregateImage extends ComputedImage { /** * Creates a new aggregation of bands. * - * @param layout pixel and tile coordinate spaces of this image, together with sample model. - * @param colors the color model to use for this image, or {@code null} for automatic. + * @param layout pixel and tile coordinate spaces of this image, together with sample model. + * @param colorizer provider of color model to use for this image, or {@code null} for automatic. */ - private BandAggregateImage(final CombinedImageLayout layout, ColorModel colors) { + private BandAggregateImage(final CombinedImageLayout layout, final Colorizer colorizer) { super(layout.sampleModel, layout.sources); final Rectangle r = layout.domain; - minX = r.x; - minY = r.y; - width = r.width; - height = r.height; - minTileX = layout.minTileX; - minTileY = layout.minTileY; - if (colors == null) { - colors = layout.createColorModel(); - } else { - layout.ensureCompatible("colors", colors); - } - colorModel = colors; + minX = r.x; + minY = r.y; + width = r.width; + height = r.height; + minTileX = layout.minTileX; + minTileY = layout.minTileY; filteredSources = layout.getFilteredSources(); + colorModel = layout.createColorModel(colorizer); + ensureCompatible(colorModel); } /** Returns the information inferred at construction time. */ 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 2c55e46362..ab8ee294a0 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,6 +32,7 @@ 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.ImageLayout; import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.internal.coverage.j2d.TileOpExecutor; @@ -185,14 +186,14 @@ class BandedSampleConverter extends ComputedImage { * @param converters the transfer functions to apply on each band of the source image. * @param targetType the type of this image resulting from conversion of given image. * Shall be one of {@link DataBuffer} constants. - * @param colorModel the color model for the expected range of values, or {@code null}. + * @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) */ static BandedSampleConverter create(RenderedImage source, final ImageLayout layout, final NumberRange<?>[] sourceRanges, final MathTransform1D[] converters, - final int targetType, final ColorModel colorModel) + final int targetType, final Colorizer colorizer) { /* * Since this operation applies its own ColorModel anyway, skip operation that was doing nothing else @@ -203,6 +204,14 @@ class BandedSampleConverter extends ComputedImage { } final int numBands = converters.length; final BandedSampleModel sampleModel = layout.createBandedSampleModel(targetType, numBands, source, null); + final int visibleBand = ImageUtilities.getVisibleBand(source); + ColorModel colorModel = null; + if (colorizer != null) { + colorModel = colorizer.apply(new Colorizer.Target(sampleModel, null, visibleBand)).orElse(null); + } + if (colorModel == null) { + colorModel = ColorModelFactory.createGrayScale(sampleModel, visibleBand, null); + } /* * If the source image is writable, then changes 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 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 new file mode 100644 index 0000000000..50aecab8b0 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/image/Colorizer.java @@ -0,0 +1,265 @@ +/* + * 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.Map; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.function.Function; +import java.awt.Color; +import java.awt.image.ColorModel; +import java.awt.image.SampleModel; +import java.awt.image.IndexColorModel; +import org.apache.sis.coverage.Category; +import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.internal.coverage.j2d.ColorModelFactory; +import org.apache.sis.measure.NumberRange; +import org.apache.sis.util.ArgumentChecks; + + +/** + * Colorization algorithm to apply for colorizing a computed image. + * The {@link #apply(Target)} method is invoked when {@link ImageProcessor} needs a new color model for + * the computation result. The {@link Target} argument contains information about the image to colorize, + * in particular the {@link SampleModel} of the computed image. The colorization result is optional, + * i.e. the {@code apply(Target)} method may return an empty value if it does not support the target. + * In the latter case the caller will fallback on a default color model, typically a grayscale. + * + * <p>Constants or static methods in this interface provide colorizers for common cases. + * For example {@link #ARGB} interprets image bands as Red, Green, Blue and optionally Alpha channels. + * Colorizers can be chained with {@link #orElse(Colorizer)} for trying different strategies until one succeeds.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ +public interface Colorizer extends Function<Colorizer.Target, Optional<ColorModel>> { + /** + * Information about the computed image to colorize. + * The most important information is the {@link SampleModel}, as the inferred color model must be + * {@linkplain ColorModel#isCompatibleSampleModel(SampleModel) compatible with the sample model}. + * A {@code Target} instance may also contain contextual information + * such as the {@link SampleDimension}s of the target coverage. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 1.4 + */ + class Target { + /** + * Sample model of the computed image to colorize. + */ + private final SampleModel model; + + /** + * Description of the bands of the computed image to colorize, or {@code null} if none. + */ + private final List<SampleDimension> ranges; + + /** + * The band to colorize if the colorization algorithm uses only one band. + * Ignored if the colorization uses many bands (e.g. {@link #ARGB}). + * A negative value means that no visible band has been specified. + */ + private final int visibleBand; + + /** + * Creates a new record with the sample model of the image to colorize. + * + * @param model sample model of the computed image to colorize (mandatory). + * @param ranges description of the bands of the computed image to colorize, or {@code null} if none. + * @param visibleBand the band to colorize if the colorization algorithm uses only one band, or -1 if none. + */ + public Target(final SampleModel model, final List<SampleDimension> ranges, final int visibleBand) { + this.model = Objects.requireNonNull(model); + this.ranges = (ranges != null) ? List.copyOf(ranges) : null; + final int numBands = model.getNumBands(); + if (visibleBand < 0) { + if (numBands == 1) { + this.visibleBand = ColorModelFactory.DEFAULT_VISIBLE_BAND; + return; + } + } else if (visibleBand < numBands) { + this.visibleBand = visibleBand; + return; + } + this.visibleBand = -1; + } + + /** + * Returns the sample model of the computed image to colorize. + * The color model created by {@link #apply(Target)} + * must be compatible with this sample model. + * + * @return computed image sample model (never null). + * @see ColorModel#isCompatibleSampleModel(SampleModel) + */ + public SampleModel getSampleModel() { + return model; + } + + /** + * Returns a description of the bands of the image to colorize. + * This is typically obtained by {@link org.apache.sis.coverage.grid.GridCoverage#getSampleDimensions()}. + * + * @return description of the bands of the image to colorize. + */ + public Optional<List<SampleDimension>> getRanges() { + return Optional.ofNullable(ranges); + } + + /** + * Returns the band to colorize if the colorization algorithm uses only one band. + * The value is always positive and less than the number of bands of the sample model. + * 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. + */ + public OptionalInt getVisibleBand() { + return (visibleBand >= 0) ? OptionalInt.of(visibleBand) : OptionalInt.empty(); + } + } + + /** + * RGB(A) color model for images storing 8 bits integer on 3 or 4 bands. + * The color model is RGB for image having 3 bands, or ARGB for images having 4 bands. + */ + Colorizer ARGB = (target) -> Optional.ofNullable(ColorModelFactory.createRGB(target.getSampleModel())); + + /** + * Creates a colorizer which will interpolate the given colors in the given range of values. + * When the image data type is 8 or 16 bits integer, this colorizer creates {@link IndexColorModel} instances. + * For other kinds of data type such as floating points, + * this colorizer creates a non-standard (and potentially slow) color model. + * + * <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. + * + * @param lower the minimum sample value, inclusive. + * @param upper the maximum sample value, exclusive. + * @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. + */ + public static Colorizer forRange(final double lower, final double upper, final Color... colors) { + ArgumentChecks.ensureNonEmpty("colors", colors); + return forRanges(Map.of(new NumberRange<>(Double.class, lower, true, upper, false), colors)); + } + + /** + * Creates a colorizer which will interpolate colors in multiple ranges of values. + * When the image data type is 8 or 16 bits integer, this colorizer creates {@link IndexColorModel} instances. + * For other kinds of data type such as floating points, + * this colorizer creates a non-standard (and potentially slow) color model. + * + * <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. + * + * @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. + */ + public static Colorizer forRanges(final Map<NumberRange<?>,Color[]> colors) { + ArgumentChecks.ensureNonEmpty("colors", colors.entrySet()); + final var factory = ColorModelFactory.piecewise(colors); + return (target) -> { + final OptionalInt visibleBand = target.getVisibleBand(); + if (visibleBand.isEmpty()) { + return Optional.empty(); + } + final SampleModel model = target.getSampleModel(); + final int numBands = model.getNumBands(); + return Optional.ofNullable(factory.createColorModel(model.getDataType(), numBands, visibleBand.getAsInt())); + }; + } + + /** + * Creates a colorizer which will associate colors to coverage categories. + * 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>. + * + * <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> + * + * @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. + */ + public static Colorizer forCategories(final Function<Category,Color[]> colors) { + ArgumentChecks.ensureNonNull("colors", colors); + return (target) -> { + final int visibleBand = target.getVisibleBand().orElse(-1); + if (visibleBand >= 0) { + final List<SampleDimension> ranges = target.getRanges().orElse(null); + if (visibleBand < ranges.size()) { + final SampleModel model = target.getSampleModel(); + final var c = new org.apache.sis.internal.coverage.j2d.Colorizer(colors); + c.initialize(model, ranges.get(visibleBand)); + return Optional.ofNullable(c.createColorModel(model.getDataType(), model.getNumBands(), visibleBand)); + } + } + return Optional.empty(); + }; + } + + /** + * Creates a colorizer which will use the specified color model instance if compatible with the target. + * + * @param colors the color model instance to use. + * @return a colorizer which will try to apply the specified color model <i>as-is</i>. + */ + public static Colorizer forInstance(final ColorModel colors) { + ArgumentChecks.ensureNonNull("colors", colors); + return (target) -> colors.isCompatibleSampleModel(target.getSampleModel()) ? Optional.of(colors) : Optional.empty(); + } + + /** + * Returns the color model to use for an image having the given sample model. + * If this function does not support the creation of a color model for the given sample model, + * then an empty value is returned. In the latter case, caller will typically fallback on grayscale. + * Otherwise if a non-empty value is returned, then that color model shall be + * {@linkplain ColorModel#isCompatibleSampleModel(SampleModel) compatible} + * with the {@linkplain Target#getSampleModel() target sample model}. + * + * @param model the sample model of the image for which to create a color model. + * @return the color model to use for the specified sample model. + */ + @Override + Optional<ColorModel> apply(Target model); + + /** + * Returns a new colorizer which will apply the specified alternative + * if this colorizer can not infer a color model. + * + * @param alternative the alternative strategy for creating a color model. + * @return a new colorizer which will attempt to apply {@code this} first, + * then fallback on the specified alternative this colorizer did not produced a result. + */ + default Colorizer orElse(final Colorizer alternative) { + ArgumentChecks.ensureNonNull("alternative", alternative); + return (model) -> { + var result = apply(model); + if (result.isEmpty()) { + result = alternative.apply(model); + } + return result; + }; + } +} diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java index 8cacace734..2ab7c0ef27 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/CombinedImageLayout.java @@ -16,6 +16,7 @@ */ package org.apache.sis.image; +import java.util.Optional; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; @@ -24,7 +25,6 @@ import java.awt.image.DataBuffer; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import org.apache.sis.util.Workaround; -import org.apache.sis.util.resources.Errors; import org.apache.sis.util.collection.FrequencySortedSet; import org.apache.sis.internal.feature.Resources; import org.apache.sis.internal.coverage.j2d.ImageLayout; @@ -310,18 +310,15 @@ final class CombinedImageLayout extends ImageLayout { } /** - * Builds a default color model with RGB(A) colors or the colors of the first visible band. - * If the combined image has 3 or 4 bands and the data type is 8 bits integer (bytes), - * then this method returns a RGB or RGBA color model depending if there is 3 or 4 bands. - * Otherwise if {@link ImageUtilities#getVisibleBand(RenderedImage)} finds that a source image - * declares a visible band, then the returned color model will reuse the colors of that band. + * Builds a default color model with the colors of the first visible band found in source images. + * If a band is declared visible according {@link ImageUtilities#getVisibleBand(RenderedImage)}, + * then the returned color model will reuse the colors of that visible band. * Otherwise a grayscale color model is built with a value range inferred from the data-type. + * + * @param colorizer user-supplied provider of color model, or {@code null} if none. */ - final ColorModel createColorModel() { - ColorModel colors = ColorModelFactory.createRGB(sampleModel); - if (colors != null) { - return colors; - } + final ColorModel createColorModel(final Colorizer colorizer) { + ColorModel colors = null; int visibleBand = ColorModelFactory.DEFAULT_VISIBLE_BAND; int base = 0; search: for (int i=0; i < sources.length; i++) { @@ -344,28 +341,16 @@ search: for (int i=0; i < sources.length; i++) { } base += (bands != null) ? bands.length : ImageUtilities.getNumBands(source); } + if (colorizer != null) { + Optional<ColorModel> candidate = colorizer.apply(new Colorizer.Target(sampleModel, null, visibleBand)); + if (candidate.isPresent()) { + return candidate.get(); + } + } colors = ColorModelFactory.derive(colors, sampleModel.getNumBands(), visibleBand); if (colors != null) { return colors; } return ColorModelFactory.createGrayScale(sampleModel, visibleBand, null); } - - /** - * Ensures that a user-supplied color model is compatible. - * - * @param name parameter name of the user-supplied color model. - * @param cm the color model to validate. Can be {@code null}. - * @throws IllegalArgumentException if the color model is incompatible. - */ - void ensureCompatible(final String name, final ColorModel cm) { - final String reason = PlanarImage.verifyCompatibility(sampleModel, cm); - if (reason != null) { - String message = Resources.format(Resources.Keys.IncompatibleColorModel); - if (!reason.isEmpty()) { - message = message + ' ' + Errors.format(Errors.Keys.IllegalValueForProperty_2, reason, name); - } - throw new IllegalArgumentException(message); - } - } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java index a8f78f1a35..859cb95883 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java +++ b/core/sis-feature/src/main/java/org/apache/sis/image/ComputedImage.java @@ -29,6 +29,7 @@ import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.awt.image.WritableRenderedImage; import java.awt.image.RenderedImage; +import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.awt.image.TileObserver; import java.awt.image.ImagingOpException; @@ -36,6 +37,7 @@ import org.apache.sis.internal.util.Numerics; import org.apache.sis.util.collection.Cache; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.Classes; import org.apache.sis.util.Disposable; import org.apache.sis.util.Exceptions; import org.apache.sis.util.resources.Errors; @@ -116,7 +118,7 @@ import org.apache.sis.internal.feature.Resources; * if the change to dirty state happened after the call to {@link #getTile(int, int) getTile(…)}.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.4 * @since 1.1 */ public abstract class ComputedImage extends PlanarImage implements Disposable { @@ -257,6 +259,24 @@ public abstract class ComputedImage extends PlanarImage implements Disposable { reference = new ComputedTiles(this, ws); // Create cleaner last after all arguments have been validated. } + /** + * Ensures that a user-supplied color model is compatible. + * This is a helper method for argument validation in sub-classes constructors. + * + * @param colors the color model to validate. Can be {@code null}. + * @throws IllegalArgumentException if the color model is incompatible. + */ + final void ensureCompatible(final ColorModel colors) { + final String reason = verifyCompatibility(sampleModel, colors); + if (reason != null) { + String message = Resources.format(Resources.Keys.IncompatibleColorModel); + if (!reason.isEmpty()) { + message = message + ' ' + Errors.format(Errors.Keys.IllegalValueForProperty_2, Classes.getShortClassName(colors), reason); + } + throw new IllegalArgumentException(message); + } + } + /** * Returns a weak reference to this image. Using weak reference instead of strong reference may help to * reduce memory usage when recomputing the image is cheap. This method should not be public because the 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 b01eede431..78a52cb585 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 @@ -73,8 +73,7 @@ import org.apache.sis.coverage.grid.GridCoverageProcessor; * </li><li> * {@linkplain #setFillValues(Number...) Fill values} to use for pixels that cannot be computed. * </li><li> - * {@linkplain #setCategoryColors(Function) Category colors} for mapping sample values - * (identified by their range, name or unit of measurement) to colors. + * {@linkplain #setColorizer(Colorizer) Colorization algorithm} to apply for colorizing a computed image. * </li><li> * {@linkplain #setImageResizingPolicy(Resizing) Image resizing policy} to apply * if a requested image size prevent the image to be tiled. @@ -216,13 +215,25 @@ public class ImageProcessor implements Cloneable { */ private Number[] fillValues; + /** + * Colorization algorithm to apply on computed image. + * A null value means to use implementation-specific default. + * + * @see #getColorizer() + * @see #setColorizer(Colorizer) + */ + private Colorizer colorizer; + /** * Colors to use for arbitrary categories of sample values. This function can return {@code null} * or empty arrays for some categories, which are interpreted as fully transparent pixels. * * @see #getCategoryColors() * @see #setCategoryColors(Function) + * + * @deprecated Replaced by {@link #colorizer}. */ + @Deprecated(since="1.4", forRemoval=true) private Function<Category,Color[]> colors; /** @@ -360,12 +371,50 @@ public class ImageProcessor implements Cloneable { fillValues = (values != null) ? values.clone() : null; } + /** + * Returns the colorization algorithm to apply on computed images, or {@code null} for default. + * This method returns the value set by the last call to {@link #setColorizer(Colorizer)}. + * + * @return colorization algorithm to apply on computed image, or {@code null} for default. + * + * @since 1.4 + */ + public synchronized Colorizer getColorizer() { + return colorizer; + } + + /** + * Sets the colorization algorithm to apply on computed images. + * The colorizer is invoked when the rendered image produced by an {@code ImageProcessor} operation + * needs a {@link ColorModel} which is not straightforward. + * + * <h4>Examples</h4> + * <p>The color model of a {@link #resample(RenderedImage, Rectangle, MathTransform) resample(…)} + * operation is straightforward: it is the same {@link ColorModel} than the source image. + * Consequently the colorizer is not invoked for that operation.</p> + * + * <p>But by contrast, the color model of an {@link #aggregateBands(RenderedImage...) aggregateBands(…)} + * operation can not be determined in such straightforward way. + * If three or four bands are aggregated, should they be interpreted as an (A)RGB image? + * The {@link Colorizer} allows to specify the desired behavior.</p> + * + * @param colorizer colorization algorithm to apply on computed image, or {@code null} for default. + * + * @since 1.4 + */ + public synchronized void setColorizer(final Colorizer colorizer) { + this.colorizer = colorizer; + } + /** * Returns the colors to use for given categories of sample values, or {@code null} is unspecified. * This method returns the function set by the last call to {@link #setCategoryColors(Function)}. * * @return colors to use for arbitrary categories of sample values, or {@code null} for default. + * + * @deprecated Replaced by {@link #getColorizer()}. */ + @Deprecated(since="1.4", forRemoval=true) public synchronized Function<Category,Color[]> getCategoryColors() { return colors; } @@ -383,8 +432,12 @@ public class ImageProcessor implements Cloneable { * empty arrays for some categories, which are interpreted as fully transparent pixels.</p> * * @param colors colors to use for arbitrary categories of sample values, or {@code null} for default. + * + * @deprecated Replaced by {@link #setColorizer(Colorizer)}. */ + @Deprecated(since="1.4", forRemoval=true) public synchronized void setCategoryColors(final Function<Category,Color[]> colors) { + setColorizer(colors != null ? Colorizer.forCategories(colors) : null); this.colors = colors; } @@ -837,19 +890,19 @@ public class ImageProcessor implements Cloneable { * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: * <ul> - * <li>(none)</li> + * <li>{@linkplain #getColorizer() Colorizer}.</li> * </ul> * * @param sources images whose bands shall be aggregated, in order. At least one image must be provided. * @return the aggregated image, or {@code sources[0]} returned directly if only one image was supplied. * @throws IllegalArgumentException if there is an incompatibility between some source images. * - * @see #aggregateBands(RenderedImage[], int[][], ColorModel) + * @see #aggregateBands(RenderedImage[], int[][]) * * @since 1.4 */ public RenderedImage aggregateBands(final RenderedImage... sources) { - return aggregateBands(sources, null, null); + return aggregateBands(sources, (int[][]) null); } /** @@ -872,21 +925,24 @@ public class ImageProcessor implements Cloneable { * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: * <ul> - * <li>(none)</li> + * <li>{@linkplain #getColorizer() Colorizer}.</li> * </ul> * * @param sources images whose bands shall be aggregated, in order. At least one image must be provided. * @param bandsPerSource bands to use for each source image, in order. May contain {@code null} elements. - * @param colors the color model to apply on aggregated image, or {@code null} for inferring a default color model - * using aggregated number of bands and sample data type. * @return the aggregated image, or one of the sources if it can be used directly. * @throws IllegalArgumentException if there is an incompatibility between some source images * or if some band indices are duplicated or outside their range of validity. * * @since 1.4 */ - public RenderedImage aggregateBands(RenderedImage[] sources, int[][] bandsPerSource, ColorModel colors) { - return BandAggregateImage.create(sources, bandsPerSource, colors); + public RenderedImage aggregateBands(RenderedImage[] sources, int[][] bandsPerSource) { + ArgumentChecks.ensureNonEmpty("sources", sources); + final Colorizer colorizer; + synchronized (this) { + colorizer = this.colorizer; + } + return BandAggregateImage.create(sources, bandsPerSource, colorizer); } /** @@ -939,7 +995,7 @@ public class ImageProcessor implements Cloneable { * <h4>Properties used</h4> * This operation uses the following properties in addition to method parameters: * <ul> - * <li>(none)</li> + * <li>{@linkplain #getColorizer() Colorizer}.</li> * </ul> * * <h4>Result relationship with source</h4> @@ -950,13 +1006,14 @@ public class ImageProcessor implements Cloneable { * @param sourceRanges approximate ranges of values for each band in source image, or {@code null} if unknown. * @param converters the transfer functions to apply on each band of the source image. * @param targetType the type of data in the image resulting from conversions. - * @param colorModel color model of resulting image, or {@code null}. * @return the image which computes converted values from the given source. * * @see GridCoverageProcessor#convert(GridCoverage, MathTransform1D[], Function) + * + * @since 1.4 */ public RenderedImage convert(final RenderedImage source, final NumberRange<?>[] sourceRanges, - MathTransform1D[] converters, final DataType targetType, final ColorModel colorModel) + MathTransform1D[] converters, final DataType targetType) { ArgumentChecks.ensureNonNull("source", source); ArgumentChecks.ensureNonNull("converters", converters); @@ -967,12 +1024,33 @@ public class ImageProcessor implements Cloneable { ArgumentChecks.ensureNonNullElement("converters", i, converters[i]); } final ImageLayout layout; + final Colorizer colorizer; synchronized (this) { layout = this.layout; + colorizer = this.colorizer; } // No need to clone `sourceRanges` because it is not stored by `BandedSampleConverter`. - return unique(BandedSampleConverter.create(source, layout, - sourceRanges, converters, targetType.toDataBufferType(), colorModel)); + return unique(BandedSampleConverter.create(source, layout, sourceRanges, converters, + targetType.toDataBufferType(), colorizer)); + } + + /** + * @deprecated Replaced by {@link #convert(RenderedImage, NumberRange<?>[], MathTransform1D[], DataType)} + * with a color model inferred from the {@link Colorizer}. + * + * @param colorModel color model of resulting image, or {@code null}. + */ + @Deprecated(since="1.4", forRemoval=true) + public synchronized RenderedImage convert(final RenderedImage source, final NumberRange<?>[] sourceRanges, + MathTransform1D[] converters, final DataType targetType, final ColorModel colorModel) + { + final Colorizer old = colorizer; + try { + colorizer = Colorizer.forInstance(colorModel); + return convert(source, sourceRanges, converters, targetType); + } finally { + colorizer = old; + } } /** @@ -1301,6 +1379,7 @@ public class ImageProcessor implements Cloneable { final Interpolation interpolation; final Number[] fillValues; final ImageLayout layout; + final Colorizer colorizer; final Function<Category,Color[]> colors; final Quantity<?>[] positionalAccuracyHints; synchronized (this) { @@ -1309,6 +1388,7 @@ public class ImageProcessor implements Cloneable { interpolation = this.interpolation; fillValues = this.fillValues; layout = this.layout; + colorizer = this.colorizer; colors = this.colors; positionalAccuracyHints = this.positionalAccuracyHints; } @@ -1317,6 +1397,7 @@ public class ImageProcessor implements Cloneable { errorHandler.equals(other.errorHandler) && executionMode.equals(other.executionMode) && interpolation.equals(other.interpolation) && + Objects.equals(colorizer, other.colorizer) && Objects.equals(colors, other.colors) && Arrays.equals(fillValues, other.fillValues) && Arrays.equals(positionalAccuracyHints, other.positionalAccuracyHints); @@ -1332,7 +1413,7 @@ public class ImageProcessor implements Cloneable { */ @Override public synchronized int hashCode() { - return Objects.hash(getClass(), errorHandler, executionMode, colors, interpolation, layout) + return Objects.hash(getClass(), errorHandler, executionMode, colorizer, interpolation, layout) + 37 * Arrays.hashCode(fillValues) + 39 * Arrays.hashCode(positionalAccuracyHints); } 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 0fc2398eed..1b1aad4e5b 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 @@ -29,6 +29,7 @@ import java.awt.image.DirectColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.SampleModel; import java.awt.image.DataBuffer; +import java.awt.image.RenderedImage; import org.apache.sis.image.DataType; import org.apache.sis.measure.NumberRange; import org.apache.sis.internal.util.Numerics; @@ -43,6 +44,7 @@ import org.apache.sis.util.Debug; /** * A factory for {@link ColorModel} objects built from a sequence of colors. + * Instances of {@code ColorModelFactory} are immutable and thread-safe. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Johann Sorel (Geomatys) @@ -52,8 +54,10 @@ import org.apache.sis.util.Debug; */ public final class ColorModelFactory { /** - * Band to make visible if an image contains many bands - * but a color map is defined for only one band. + * Band to make visible if an image contains many bands but a color map is defined for only one band. + * Should always be zero, because this is the only value guaranteed to be always present. + * This constant is used mostly for making easier to identify locations in Java code + * where this default value is hard-coded. */ public static final int DEFAULT_VISIBLE_BAND = 0; @@ -74,9 +78,11 @@ public final class ColorModelFactory { /** * A pool of color models previously created by {@link #createColorModel()}. + * This is stored in a static map instead of in a {@link ColorModelFactory} field for allowing + * the {@code ColorModelFactory} reference to be cleared when the color model is no longer used. * - * <div class="note"><b>Note:</b> - * we use {@linkplain java.lang.ref.WeakReference weak references} instead of {@linkplain java.lang.ref.SoftReference + * <h4>Implementation note</h4> + * We use {@linkplain java.lang.ref.WeakReference weak references} instead of {@linkplain java.lang.ref.SoftReference * soft references} because the intent is not to cache the values. The intent is to share existing instances in order * to reduce memory usage. Rational: * @@ -88,9 +94,8 @@ public final class ColorModelFactory { * for saving the few milliseconds requiring for building a new color model. Client code should retains their own * reference to a {@link ColorModel} if they plan to reuse it often in a short period of time.</li> * </ul> - * </div> */ - private static final Map<ColorModelFactory,ColorModel> PIECEWISES = new WeakValueHashMap<>(ColorModelFactory.class); + private static final WeakValueHashMap<ColorModelFactory,ColorModel> PIECEWISES = new WeakValueHashMap<>(ColorModelFactory.class); /** * Comparator for sorting ranges by their minimal value. @@ -135,9 +140,9 @@ public final class ColorModelFactory { * The number of pieces (segments) is {@code pieceStarts.length}. The last element of this array is the index after the * end of the last piece. The indices are integers. Never {@code null} but may be empty. * - * <div class="note"><b>Note:</b> - * indices as unsigned short are not sufficient because in the worst case the last next index will be 65536, - * which would be converted to 0 as a short, causing several exceptions afterward.</div> + * <h4>Implementation note</h4> + * Unsigned short type is not sufficient because in the worst case the last next index will be 65536, + * which would be converted to 0 as a short, causing several exceptions afterward. */ private final int[] pieceStarts; @@ -150,7 +155,12 @@ public final class ColorModelFactory { /** * Constructs a new {@code ColorModelFactory}. This object will be used as a key in a {@link Map}, * so this is not really a {@code ColorModelFactory} but a kind of "{@code ColorModelKey}" instead. - * However, since this constructor is private, user does not need to know that. + * However, since this constructor is private, users do not need to know that implementation details. + * + * @param dataType one of the {@link DataBuffer} constants. + * @param numBands the number of bands (usually 1). + * @param visibleBand the visible band (usually 0). + * @param colors colors associated to their range of values. * * @see #createPiecewise(int, int, int, ColorsForRange[]) */ @@ -224,9 +234,115 @@ public final class ColorModelFactory { } /** - * Constructs the color model from the {@code codes} and {@link #ARGB} data. - * This method is invoked the first time the color model is created, or when - * the value in the cache has been discarded. + * Creates a new instance with the same colors than the specified one but different type and number of bands. + * The color arrays are shared, not cloned. + * + * @param dataType one of the {@link DataBuffer} constants. + * @param numBands the number of bands (usually 1). + * @param visibleBand the visible band (usually 0). + * @param colors colors associated to their range of values. + */ + private ColorModelFactory(final int dataType, final int numBands, final int visibleBand, final ColorModelFactory colors) { + this.dataType = dataType; + this.numBands = numBands; + this.visibleBand = visibleBand; + this.minimum = colors.minimum; + this.maximum = colors.maximum; + this.pieceStarts = colors.pieceStarts; + this.ARGB = colors.ARGB; + } + + /** + * Compares this object with the specified object for equality. + * Defined for using {@code ColorModelFactory} as key in a hash map. + * This method is public as an implementation side-effect. + * + * @param other the other object to compare for equality. + * @return whether the two objects are equal. + * @hidden + */ + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; + } + if (other instanceof ColorModelFactory) { + final ColorModelFactory that = (ColorModelFactory) other; + return this.dataType == that.dataType + && this.numBands == that.numBands + && this.visibleBand == that.visibleBand + && this.minimum == that.minimum // Should never be NaN. + && this.maximum == that.maximum + && Arrays.equals(pieceStarts, that.pieceStarts) + && Arrays.deepEquals(ARGB, that.ARGB); + } + return false; + } + + /** + * Returns a hash-code value for use as key in a hash map. + * This method is public as an implementation side-effect. + * + * @return a hash code for using this factory as a key. + * @hidden + */ + @Override + public int hashCode() { + final int categoryCount = pieceStarts.length - 1; + int code = 962745549 + (numBands*31 + visibleBand)*31 + categoryCount; + for (int i=0; i<categoryCount; i++) { + code += Arrays.hashCode(ARGB[i]); + } + return code; + } + + /** + * Prepares a factory of color models interpolated for the ranges in the given map entries. + * The {@link ColorModel} instances will be shared among all callers in the running virtual machine. + * + * @param colors the colors to use for each range of sample values. + * The map may contain {@code null} values, which means transparent. + * @return a factory of color model suitable for {@link RenderedImage} objects with values in the given ranges. + */ + public static ColorModelFactory piecewise(final Map<NumberRange<?>, Color[]> colors) { + return PIECEWISES.intern(new ColorModelFactory(DataBuffer.TYPE_BYTE, 0, DEFAULT_VISIBLE_BAND, + ColorsForRange.list(colors.entrySet()))); + } + + /** + * Gets or creates a color model for the specified type and number of bands. + * This method returns a shared color model if a previous instance exists. + * + * @param dataType the color model type. One of {@link DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT}, + * {@link DataBuffer#TYPE_SHORT}, {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT} + * or {@link DataBuffer#TYPE_DOUBLE}. + * @param numBands the number of bands for the color model (usually 1). The returned color model will render only + * the {@code visibleBand} and ignore the others, but the existence of all {@code numBands} will + * be at least tolerated. Supplemental bands, even invisible, are useful for processing. + * @param visibleBand the band to be made visible (usually 0). All other bands (if any) will be ignored. + * @return the color model for the specified type and number of bands. + */ + public ColorModel createColorModel(final int dataType, final int numBands, final int visibleBand) { + return new ColorModelFactory(dataType, numBands, visibleBand, this).getColorModel(); + } + + /** + * Returns the color model associated to this {@code ColorModelFactory} instance. + * This method always returns a unique color model instance, + * even if this {@code ColorModelFactory} instance is new. + * + * @return the color model associated to this instance. + */ + private ColorModel getColorModel() { + synchronized (PIECEWISES) { + return PIECEWISES.computeIfAbsent(this, ColorModelFactory::createColorModel); + } + } + + /** + * Constructs the color model from the {@link #ARGB} data. + * This method is invoked the first time the color model is created, + * or when the value in the cache has been discarded. */ private ColorModel createColorModel() { /* @@ -248,7 +364,7 @@ public final class ColorModelFactory { final int[] nBits = { DataBuffer.getDataTypeSize(dataType) }; - return CACHE.unique(new ComponentColorModel(cs, nBits, false, true, Transparency.OPAQUE, dataType)); + return unique(new ComponentColorModel(cs, nBits, false, true, Transparency.OPAQUE, dataType)); } /* * Interpolates the colors in the color palette. Colors that do not fall @@ -273,68 +389,6 @@ public final class ColorModelFactory { return createIndexColorModel(numBands, visibleBand, colorMap, true, transparent); } - /** - * Public as an implementation side-effect. - * - * @return a hash code. - */ - @Override - public int hashCode() { - final int categoryCount = pieceStarts.length - 1; - int code = 962745549 + (numBands*31 + visibleBand)*31 + categoryCount; - for (int i=0; i<categoryCount; i++) { - code += Arrays.hashCode(ARGB[i]); - } - return code; - } - - /** - * Public as an implementation side-effect. - * - * @param other the other object to compare for equality. - * @return whether the two objects are equal. - */ - @Override - public boolean equals(final Object other) { - if (other == this) { - return true; - } - if (other instanceof ColorModelFactory) { - final ColorModelFactory that = (ColorModelFactory) other; - return this.dataType == that.dataType - && this.numBands == that.numBands - && this.visibleBand == that.visibleBand - && this.minimum == that.minimum // Should never be NaN. - && this.maximum == that.maximum - && Arrays.equals(pieceStarts, that.pieceStarts) - && Arrays.deepEquals(ARGB, that.ARGB); - } - return false; - } - - /** - * Returns a color model interpolated for the ranges in the given map entries. - * Returned instances of {@link ColorModel} are shared among all callers in the running virtual machine. - * - * @param dataType the color model type. One of {@link DataBuffer#TYPE_BYTE}, {@link DataBuffer#TYPE_USHORT}, - * {@link DataBuffer#TYPE_SHORT}, {@link DataBuffer#TYPE_INT}, {@link DataBuffer#TYPE_FLOAT} - * or {@link DataBuffer#TYPE_DOUBLE}. - * @param numBands the number of bands for the color model (usually 1). The returned color model will render only - * the {@code visibleBand} and ignore the others, but the existence of all {@code numBands} will - * be at least tolerated. Supplemental bands, even invisible, are useful for processing. - * @param visibleBand the band to be made visible (usually 0). All other bands (if any) will be ignored. - * @param colors the colors to use for each range of sample values. - * The map may contain {@code null} values, which means transparent. - * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges. - * - * @see Colorizer - */ - public static ColorModel createPiecewise(final int dataType, final int numBands, final int visibleBand, - final Map<NumberRange<?>, Color[]> colors) - { - return createPiecewise(dataType, numBands, visibleBand, ColorsForRange.list(colors.entrySet())); - } - /** * Returns a color model interpolated for the given ranges and colors. * This method builds up the color model from each set of colors associated to ranges in the given entries. @@ -352,15 +406,12 @@ public final class ColorModelFactory { * be at least tolerated. Supplemental bands, even invisible, are useful for processing. * @param visibleBand the band to be made visible (usually 0). All other bands, if any, will be ignored. * @param colors the colors associated to ranges of sample values. - * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges. + * @return a color model suitable for {@link RenderedImage} objects with values in the given ranges. */ static ColorModel createPiecewise(final int dataType, final int numBands, final int visibleBand, final ColorsForRange[] colors) { - final ColorModelFactory key = new ColorModelFactory(dataType, numBands, visibleBand, colors); - synchronized (PIECEWISES) { - return PIECEWISES.computeIfAbsent(key, ColorModelFactory::createColorModel); - } + return new ColorModelFactory(dataType, numBands, visibleBand, colors).getColorModel(); } /** @@ -375,7 +426,7 @@ public final class ColorModelFactory { * @param ARGB an array of ARGB values. * @param hasAlpha indicates whether alpha values are contained in the {@code ARGB} array. * @param transparent the transparent pixel, or -1 for auto-detection. - * @return An index color model for the specified array. + * @return an index color model for the specified array of ARGB values. */ public static IndexColorModel createIndexColorModel(final int numBands, final int visibleBand, final int[] ARGB, final boolean hasAlpha, final int transparent) @@ -398,8 +449,19 @@ public final class ColorModelFactory { } /** - * Returns a color model interpolated for the given range of values. This is a convenience method for - * {@link #createPiecewise(int, int, int, Map)} when the map contains only one element. + * Returns a unique instance of the given color model. + * This method is a shortcut used when the return type does not need to be a specialized type. + * + * @param cm the color model. + * @return a unique instance of the given color model. + */ + private static ColorModel unique(final ColorModel cm) { + return CACHE.unique(cm); + } + + /** + * Returns a color model interpolated for the given range of values. + * This is a convenience method for {@link #piecewise(Map)} when the map contains only one element. * * @param dataType the color model type. * @param numBands the number of bands for the color model (usually 1). @@ -407,7 +469,7 @@ public final class ColorModelFactory { * @param lower the minimum value, inclusive. * @param upper the maximum value, exclusive. * @param colors the colors to use for the range of sample values. - * @return a color model suitable for {@link java.awt.image.RenderedImage} objects with values in the given ranges. + * @return a color model suitable for {@link RenderedImage} objects with values in the given ranges. */ public static ColorModel createColorScale(final int dataType, final int numBands, final int visibleBand, final double lower, final double upper, final Color... colors) @@ -421,7 +483,7 @@ public final class ColorModelFactory { * Creates a color model for opaque images storing pixels as real numbers. * The color model can have an arbitrary number of bands, but in current implementation only one band is used. * - * <p><b>Warning:</b> the use of this color model is very slow. + * <p><b>Warning:</b> the use of this color model may be very slow. * It should be used only when no standard color model can be used.</p> * * @param dataType the color model type as one of {@code DataBuffer.TYPE_*} constants. @@ -445,7 +507,7 @@ public final class ColorModelFactory { final ScaledColorSpace cs = new ScaledColorSpace(numComponents, visibleBand, minimum, maximum); cm = new ScaledColorModel(cs, dataType); } - return CACHE.unique(cm); + return unique(cm); } /** @@ -578,7 +640,7 @@ public final class ColorModelFactory { cm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), numBits, hasAlpha, false, hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, DataBuffer.TYPE_BYTE); } - return CACHE.unique(cm); + return unique(cm); } /** @@ -660,7 +722,7 @@ public final class ColorModelFactory { // TODO: handle other color models. return null; } - return CACHE.unique(subset); + return unique(subset); } /** 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 58cceeb1d6..a284b51ec9 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 @@ -29,13 +29,13 @@ import org.apache.sis.util.ArraysExt; /** - * Colors to apply on a range of sample values. Instances of {@code ColorsForRange} are temporary, used only - * the time needed for {@link ColorModelFactory#createColorModel(int, int, int, ColorsForRange[])}. + * Colors to apply on a range of sample values. Instances of {@code ColorsForRange} are usually temporary, + * used only the time needed for {@link ColorModelFactory#createPiecewise(int, int, int, ColorsForRange[])}. * * @author Martin Desruisseaux (Geomatys) * @version 1.3 * - * @see ColorModelFactory#createColorModel(int, int, int, ColorsForRange[]) + * @see ColorModelFactory#createPiecewise(int, int, int, ColorsForRange[]) * * @since 1.1 */ diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java index 397eec973e..8b6bdbb63c 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ImageUtilities.java @@ -156,7 +156,7 @@ public final class ImageUtilities extends Static { } final SampleModel sm = image.getSampleModel(); if (sm != null && sm.getNumBands() == 1) { // Should never be null, but we are paranoiac. - return 0; + return ColorModelFactory.DEFAULT_VISIBLE_BAND; } } return -1; diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java index 5af127340f..2de3cd9135 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/image/BandAggregateImageTest.java @@ -156,7 +156,7 @@ public final class BandAggregateImageTest extends TestCase { new int[] {1}, // Take second band of image 1. null, // Take all bands of image 2. new int[] {0} // Take first band of image 1. - }, null); + }); assertNotNull(result); assertEquals(minX, result.getMinX()); assertEquals(minY, result.getMinY()); diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java index c31b6aa324..466a5d2c0b 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java @@ -23,6 +23,7 @@ import java.util.AbstractSet; import java.util.Iterator; import java.util.Objects; import java.util.Arrays; +import java.util.function.Function; import java.lang.ref.WeakReference; import org.apache.sis.util.Debug; import org.apache.sis.util.ArraysExt; @@ -72,7 +73,7 @@ import static org.apache.sis.util.collection.WeakEntry.*; * then the caller can synchronize on {@code this}. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.2 + * @version 1.4 * * @param <K> the class of key elements. * @param <V> the class of value elements. @@ -290,7 +291,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { * @return whether {@link #count} matches the expected value. */ @Debug - final boolean isValid() { + private boolean isValid() { if (!Thread.holdsLock(this)) { throw new AssertionError(); } @@ -316,7 +317,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { * * @param key the key (cannot be null). */ - final int keyHashCode(final Object key) { + private int keyHashCode(final Object key) { switch (comparisonMode) { case IDENTITY: return System.identityHashCode(key); case EQUALS: return key.hashCode(); @@ -331,7 +332,7 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { * @param k1 the first key (cannot be null). * @paral k2 the second key. */ - final boolean keyEquals(final Object k1, final Object k2) { + private boolean keyEquals(final Object k1, final Object k2) { switch (comparisonMode) { case IDENTITY: return k1 == k2; case EQUALS: return k1.equals(k2); @@ -340,6 +341,49 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { } } + /** + * Locates the entry for the given key and, if present, invokes the given getter method. + * + * @param <R> type of value returned by the getter method. + * @param key key of the entry to search in this map. + * @param getter getter method to invoke on the entry. + * @param defaultValue value to return if there is no entry for the given key. + * @return result of the getter function invoked on the entry, or the default value if there is no entry. + */ + @SuppressWarnings("unchecked") + private synchronized <R> R get(final Object key, final Function<Entry,R> getter, final R defaultValue) { + assert isValid(); + if (key != null) { + final Entry[] table = this.table; + final int index = (keyHashCode(key) & HASH_MASK) % table.length; + for (Entry e = table[index]; e != null; e = (Entry) e.next) { + if (keyEquals(key, e.key)) { + return getter.apply(e); + } + } + } + return defaultValue; + } + + /** + * If this map contains the specified key, returns the instance contained in this map. + * Otherwise returns the given {@code key} instance. + * + * <p>This method can be useful when the keys are potentially large objects. + * It allows to opportunistically share existing instances, a little bit like + * when using {@link WeakHashSet} except that this method does not add the given + * key to this map if not present.</p> + * + * @param key key to look for in this map. + * @return the key instance in this map which is equal to the specified key, or {@code key} if none. + * + * @since 1.4 + */ + @SuppressWarnings("unchecked") + public K intern(final K key) { + return get(key, Entry::getKey, key); + } + /** * Returns {@code true} if this map contains a mapping for the specified key. * Null keys are considered never present. @@ -349,15 +393,15 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { */ @Override public boolean containsKey(final Object key) { - return get(key) != null; + return get(key, Function.identity(), null) != null; } /** - * Returns {@code true} if this map maps one or more keys to this value. + * Returns {@code true} if this map maps one or more keys to the specified value. * Null values are considered never present. * * @param value value whose presence in this map is to be tested. - * @return {@code true} if this map maps one or more keys to this value. + * @return {@code true} if this map maps one or more keys to the specified value. */ @Override public synchronized boolean containsValue(final Object value) { @@ -373,19 +417,24 @@ public class WeakValueHashMap<K,V> extends AbstractMap<K,V> { * @return the value to which this map maps the specified key. */ @Override - @SuppressWarnings("unchecked") - public synchronized V get(final Object key) { - assert isValid(); - if (key != null) { - final Entry[] table = this.table; - final int index = (keyHashCode(key) & HASH_MASK) % table.length; - for (Entry e = table[index]; e != null; e = (Entry) e.next) { - if (keyEquals(key, e.key)) { - return e.get(); - } - } - } - return null; + public V get(final Object key) { + return get(key, Entry::get, null); + } + + /** + * Returns the value to which this map maps the specified key. + * Returns {@code defaultValue} if the map contains no mapping for this key. + * Null keys are considered never present. + * + * @param key key whose associated value is to be returned. + * @param defaultValue the default mapping of the key. + * @return the value to which this map maps the specified key. + * + * @since 1.4 + */ + @Override + public V getOrDefault(final Object key, final V defaultValue) { + return get(key, Entry::get, defaultValue); } /** diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java index 6f81080122..e3cf89b6e3 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java @@ -34,6 +34,7 @@ import org.apache.sis.referencing.operation.transform.TransferFunction; import org.apache.sis.referencing.datum.BursaWolfParameters; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.internal.referencing.LazySet; +import org.apache.sis.internal.coverage.j2d.ColorModelFactory; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.measure.NumberRange; import org.apache.sis.coverage.Category; @@ -780,7 +781,7 @@ public class Convention { * @return the band on which {@link #getColors(Variable)} will apply. */ public int getVisibleBand() { - return 0; + return ColorModelFactory.DEFAULT_VISIBLE_BAND; } /** diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java index 9720025fb9..adb90fecb4 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/BandAggregateGridResource.java @@ -19,7 +19,6 @@ package org.apache.sis.storage.aggregate; import java.util.List; import java.util.Arrays; import java.util.Optional; -import java.awt.image.ColorModel; import org.opengis.util.GenericName; import org.opengis.metadata.Metadata; import org.apache.sis.coverage.SampleDimension; @@ -60,7 +59,7 @@ import org.apache.sis.util.collection.BackingStoreException; * @author Martin Desruisseaux (Geomatys) * @version 1.4 * - * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][], ColorModel) + * @see GridCoverageProcessor#aggregateRanges(GridCoverage[], int[][]) * * @since 1.4 */ @@ -109,13 +108,6 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource { */ private final int[][] bandsPerSource; - /** - * The color model to apply on aggregated image, or {@code null} for default. - * If {@code null}, the color model will be inferred from the aggregated number - * of bands and the sample data type. - */ - private final ColorModel colors; - /** * The processor to use for creating grid coverages. */ @@ -130,7 +122,7 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource { * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others. */ public BandAggregateGridResource(final GridCoverageResource... sources) throws DataStoreException { - this(null, null, sources, null, null, null); + this(null, null, sources, null, null); } /** @@ -161,15 +153,13 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource { * @param sources resources whose bands shall be aggregated, in order. At least one resource must be provided. * @param bandsPerSource sample dimensions for each source. May be {@code null} or may contain {@code null} elements. * @param processor the processor to use for creating grid coverages, or {@code null} for a default processor. - * @param colors the color model to apply on aggregated image, or {@code null} for inferring - * a default color model using aggregated number of bands and sample data type. * @throws DataStoreException if an error occurred while fetching the grid geometry or sample dimensions from a resource. * @throws IllegalGridGeometryException if a grid geometry is not compatible with the others. * @throws IllegalArgumentException if some band indices are duplicated or outside their range of validity. */ public BandAggregateGridResource(final Resource parent, final GenericName name, final GridCoverageResource[] sources, final int[][] bandsPerSource, - final GridCoverageProcessor processor, final ColorModel colors) throws DataStoreException + final GridCoverageProcessor processor) throws DataStoreException { super(parent); try { @@ -181,7 +171,6 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource { this.sampleDimensions = List.copyOf(aggregate.ranges()); this.bandsPerSource = aggregate.bandsPerSource(); this.processor = (processor != null) ? processor : new GridCoverageProcessor(); - this.colors = colors; } catch (BackingStoreException e) { throw e.unwrapOrRethrow(DataStoreException.class); } @@ -374,6 +363,6 @@ public class BandAggregateGridResource extends AbstractGridCoverageResource { cursorIndex = source; bandsToLoad[numBandsToLoad++] = bandsForCurrentSource[cursorIndex - cursorBase]; } - return processor.aggregateRanges(coverages, coverageBands, colors); + return processor.aggregateRanges(coverages, coverageBands); } } diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java index e4a1b8e434..c447fa6aca 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/aggregate/BandAggregateGridResourceTest.java @@ -108,7 +108,7 @@ public final class BandAggregateGridResourceTest extends TestCase { final LocalName testName = Names.createLocalName(null, null, "test-name"); aggregation = new BandAggregateGridResource(null, testName, new GridCoverageResource[] {firstAndSecondBands, thirdAndFourthBands, fifthAndSixthBands}, - new int[][] {null, new int[] {1, 0}, new int[] {1}}, null, null); + new int[][] {null, new int[] {1, 0}, new int[] {1}}, null); assertEquals(testName, aggregation.getIdentifier().orElse(null)); assertAllPixelsEqual(aggregation.read(null), 101, 102, 104, 103, 106);
