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 df7a5102a51c591f9ac7a85be55f1b1bd375760b Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Apr 25 16:05:34 2022 +0200 Add partial support for color map file (*.clr). In current implementation it can apply only to data type byte and unsigned short. In particular, color map on floating point values is not yet supported. --- .../internal/coverage/j2d/ColorModelFactory.java | 7 +- ide-project/NetBeans/build.xml | 2 + .../sis/internal/storage/esri/RasterStore.java | 125 +++++++++++++++++++-- .../sis/internal/storage/esri/package-info.java | 13 +++ .../org/apache/sis/internal/storage/esri/BIP.hdr | 2 + .../org/apache/sis/internal/storage/esri/BIP.stx | 7 ++ .../org/apache/sis/internal/storage/esri/grid.clr | 14 +++ 7 files changed, 156 insertions(+), 14 deletions(-) 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 99192128ee..f3a12df41c 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 @@ -18,7 +18,6 @@ package org.apache.sis.internal.coverage.j2d; import java.util.Map; import java.util.Arrays; -import java.util.Collection; import java.util.Comparator; import java.util.Optional; import java.awt.Transparency; @@ -317,9 +316,9 @@ public final class ColorModelFactory { * @see Colorizer */ public static ColorModel createPiecewise(final int dataType, final int numBands, final int visibleBand, - final Collection<Map.Entry<NumberRange<?>,Color[]>> colors) + final Map<NumberRange<?>, Color[]> colors) { - return createPiecewise(dataType, numBands, visibleBand, ColorsForRange.list(colors)); + return createPiecewise(dataType, numBands, visibleBand, ColorsForRange.list(colors.entrySet())); } /** @@ -386,7 +385,7 @@ 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, Collection)} when the collection contains only one element. + * {@link #createPiecewise(int, int, int, 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). diff --git a/ide-project/NetBeans/build.xml b/ide-project/NetBeans/build.xml index 557ef9f261..bbb1786f11 100644 --- a/ide-project/NetBeans/build.xml +++ b/ide-project/NetBeans/build.xml @@ -304,6 +304,8 @@ <include name="**/*.txt"/> <include name="**/*.asc"/> <include name="**/*.hdr"/> + <include name="**/*.stx"/> + <include name="**/*.clr"/> <include name="**/*.raw"/> <include name="**/*.png"/> <include name="**/*.pgw"/> 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 6955c061b0..0e588da647 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 @@ -26,6 +26,7 @@ import java.io.FileNotFoundException; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; import java.awt.image.SampleModel; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; @@ -53,6 +54,7 @@ import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.internal.util.Numerics; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.util.CharSequences; +import org.apache.sis.util.ArraysExt; import org.apache.sis.math.Statistics; @@ -197,6 +199,89 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource metadata = builder.buildAndFreeze(); } + /** + * Reads the {@code "*.clr"} auxiliary file. Syntax is as below, with one line per color: + * + * <pre>value red green blue</pre> + * + * The specification said that lines that do not start with a number shall be ignored as comment. + * Any characters after the fourth number shall also be ignored and can be used as comment. + * + * <h4>Limitations</h4> + * Current implementation requires the data type to be {@link DataBuffer#TYPE_BYTE} or + * {@link DataBuffer#TYPE_SHORT}. A future version could create scaled color model for + * floating point values as well. + * + * @param mapSize minimal size of index color model map. The actual size may be larger. + * @param numBands number of bands in the sample model. Only one of them will be visible. + * @return the color model, or {@code null} if the file does not contain enough entries. + * @throws NoSuchFileException if the auxiliary file has not been found (when opened from path). + * @throws FileNotFoundException if the auxiliary file has not been found (when opened from URL). + * @throws IOException if another error occurred while opening the stream. + * @throws NumberFormatException if a number can not be parsed. + */ + private ColorModel readColorMap(final int dataType, final int mapSize, final int numBands) + throws DataStoreException, IOException + { + final int maxSize; + switch (dataType) { + case DataBuffer.TYPE_BYTE: maxSize = 0xFF; break; + case DataBuffer.TYPE_USHORT: maxSize = 0xFFFF; break; + default: return null; // Limitation documented in above javadoc. + } + int count = 0; + long[] indexAndColors = ArraysExt.EMPTY_LONG; // Index in highest 32 bits, ARGB in lowest 32 bits. + for (final CharSequence line : CharSequences.splitOnEOL(readAuxiliaryFile(CLR))) { + final int end = CharSequences.skipTrailingWhitespaces(line, 0, line.length()); + final int start = CharSequences.skipLeadingWhitespaces(line, 0, end); + if (start < end && Character.isDigit(Character.codePointAt(line, start))) { + int column = 0; + long code = 0; + for (final CharSequence item : CharSequences.split(line.subSequence(start, end), ' ')) { + if (item.length() != 0) { + int value = Integer.parseInt(item.toString()); + if (column == 0) { + code = ((long) value) << Integer.SIZE; + } else { + value = Math.max(0, Math.min(255, value)); + code |= value << ((3 - column) * Byte.SIZE); + } + if (++column >= 4) break; + } + } + if (count >= indexAndColors.length) { + indexAndColors = Arrays.copyOf(indexAndColors, Math.max(count*2, 64)); + } + indexAndColors[count++] = code | 0xFF000000L; + } + } + if (count <= 1) { + return null; + } + /* + * Sort the color entries in increasing index order. Because we put the value in the highest bits, + * we can sort the `long` entries directly. If the file contains more entries than what the color + * map can contains, the last entries are discarded. + */ + Arrays.sort(indexAndColors, 0, count); + int[] ARGB = new int[Math.max(mapSize, Math.toIntExact((indexAndColors[count-1] >>> Integer.SIZE) + 1))]; + final int[] colors = new int[2]; + for (int i=1; i<count; i++) { + final int lower = (int) (indexAndColors[i-1] >>> Integer.SIZE); + final int upper = (int) (indexAndColors[i ] >>> Integer.SIZE); + if (upper >= lower) { + colors[0] = (int) indexAndColors[i-1]; + colors[1] = (int) indexAndColors[i ]; + ColorModelFactory.expand(colors, ARGB, lower, upper + 1); + } + if (upper > maxSize) { + ARGB = Arrays.copyOf(ARGB, maxSize + 1); + break; + } + } + return ColorModelFactory.createIndexColorModel(numBands, VISIBLE_BAND, ARGB, true, -1); + } + /** * Reads the {@code "*.stx"} auxiliary file. Syntax is as below, with one line per band. * Value between {…} are optional and can be skipped with a # sign in place of the number. @@ -207,17 +292,16 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource * * @todo Stretch values are not yet stored. * - * @param numBands length of the array to return. * @return statistics for each band. Some elements may be null if not specified in the file. * @throws NoSuchFileException if the auxiliary file has not been found (when opened from path). * @throws FileNotFoundException if the auxiliary file has not been found (when opened from URL). * @throws IOException if another error occurred while opening the stream. * @throws NumberFormatException if a number can not be parsed. */ - private Statistics[] readStatistics(final String name, final SampleModel sm, final int numBands) + private Statistics[] readStatistics(final String name, final SampleModel sm) throws DataStoreException, IOException { - final Statistics[] stats = new Statistics[numBands]; + final Statistics[] stats = new Statistics[sm.getNumBands()]; for (final CharSequence line : CharSequences.splitOnEOL(readAuxiliaryFile(STX))) { final int end = CharSequences.skipTrailingWhitespaces(line, 0, line.length()); final int start = CharSequences.skipLeadingWhitespaces(line, 0, end); @@ -246,7 +330,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource } if (band >= 1 && band <= stats.length) { final int count = Math.multiplyExact(sm.getWidth(), sm.getHeight()); - stats[band - 1] = new Statistics(name, 0, count, minimum, maximum, mean, stdev, true); + stats[band - 1] = new Statistics(name, 0, count, minimum, maximum, mean, stdev, false); } } } @@ -270,11 +354,9 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource * overwrite them because we need the minimum/maximum values for building the sample dimensions. */ try { - stats = readStatistics(name, sm, bands.length); - } catch (NoSuchFileException | FileNotFoundException e) { - listeners.warning(Level.FINE, Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, STX), e); + stats = readStatistics(name, sm); } catch (IOException | NumberFormatException e) { - throw new DataStoreReferencingException(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, STX), e); + canNotReadAuxiliaryFile(STX, e); } /* * Build the sample dimensions and the color model. @@ -334,18 +416,41 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource /* * Create the color model using the statistics of the band that we choose to make visible, * or using a RGB color model if the number of bands or the data type is compatible. + * The color file is optional and will be used if present. */ if (band == VISIBLE_BAND) { if (isRGB) { colorModel = ColorModelFactory.createRGB(sm); } else { - colorModel = ColorModelFactory.createGrayScale(dataType, bands.length, band, minimum, maximum); + try { + colorModel = readColorMap(dataType, (int) (maximum + 1), bands.length); + } catch (IOException | NumberFormatException e) { + canNotReadAuxiliaryFile(CLR, e); + } + if (colorModel == null) { + colorModel = ColorModelFactory.createGrayScale(dataType, bands.length, band, minimum, maximum); + } } } } sampleDimensions = UnmodifiableArrayList.wrap(bands); } + /** + * Sends a warning about a failure to read an optional auxiliary file. + * This is used for errors that affect only the rendering, not the georeferencing. + * + * @param suffix suffix of the auxiliary file. + * @param exception error that occurred while reading the auxiliary file. + */ + private void canNotReadAuxiliaryFile(final String suffix, final Exception exception) { + Level level = Level.WARNING; + if (exception instanceof NoSuchFileException || exception instanceof FileNotFoundException) { + level = Level.FINE; + } + listeners.warning(level, Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1, suffix), exception); + } + /** * Default names of bands when the color model is RGB or RGBA. */ @@ -380,7 +485,7 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource ColorModel cm = colorModel; if (!range.isIdentity()) { bands = Arrays.asList(range.select(sampleDimensions)); - cm = range.select(colorModel).orElse(null); + cm = range.select(cm).orElse(null); if (cm == null) { final SampleDimension band = bands.get(VISIBLE_BAND); cm = ColorModelFactory.createGrayScale(data.getSampleModel(), VISIBLE_BAND, band.getSampleRange().orElse(null)); diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java index e03933fae7..1746e7c191 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java @@ -32,6 +32,19 @@ * ({@code XDIM}, {@code YDIM}, color file, statistics file, <i>etc.</i>).</li> * </ul> * + * <h2>Limitations</h2> + * Statistics file ({@code *.stx}) contains {@code band}, {@code minimum}, {@code maximum}, {@code mean}, + * {@code std_deviation}, {@code linear_stretch_min} and {@code linear_stretch_max} values. + * But in current Apache SIS implementation, the last two values ({@code linear_stretch_*}) are ignored. + * + * <p>Color map file ({@code *.clr}) is read only when the raster does not have 3 or 4 bands + * (in which case the raster is considered RGB) and when the data type is byte or unsigned short. + * In all other cases, notably in the case of floating point values, the color map is ignored.</p> + * + * <p>Current implementation of ASCII Grid store loads, caches and returns the full image + * no matter the subregion or subsampling specified to the {@code read(…)} method. + * Sub-setting parameters are ignored.</p> + * * @author Martin Desruisseaux (Geomatys) * @version 1.2 * diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.hdr b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.hdr index 197bf27453..550b24582e 100644 --- a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.hdr +++ b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.hdr @@ -1,4 +1,6 @@ Header file for the `BIP.raw` test raster. +The BIP test also contains a `BIP.stx` file +for testing the loading of statistics data. NROWS 9 NCOLS 9 diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.stx b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.stx new file mode 100644 index 0000000000..8585d48ce5 --- /dev/null +++ b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/BIP.stx @@ -0,0 +1,7 @@ +Pseudo-statistics about the `BIP.raw` file content. +Format is (where <…> are mandatory and {…} optional): + + <band> <minimum> <maximum> {mean} {std_deviation} {linear_stretch_min} {linear_stretch_max} + +1 2 250 100 10 +Above values are not real statistics. diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/grid.clr b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/grid.clr new file mode 100644 index 0000000000..8626f5ff73 --- /dev/null +++ b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/esri/grid.clr @@ -0,0 +1,14 @@ +Color map for `grid.asc` file. This auxiliary file is +actually is actually for BIP/BIL/BSQ raster formats, +but Apache SIS applies it to ASCII Grid file as well. + +-10 0 0 255 (blue) + 0 0 255 255 (cyan) + 10 0 255 0 (green) + 20 255 255 0 (yellow) + 30 255 165 0 (orange) + 40 160 32 240 (purple) + +Note: in current Apache SIS implementation this file is ignored +because ASCII grid data are floating points and color maps are +restricted to integer types.