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 e45d94515849b665770ac89d82e8bca2d3f01493 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat Sep 16 14:51:04 2023 +0200 Add `HyperRectangleWriter` as an helper class for multi-dimensional grid coverage writers. --- .../apache/sis/io/stream/HyperRectangleWriter.java | 301 +++++++++++++++++++++ .../sis/io/stream/HyperRectangleWriterTest.java | 206 ++++++++++++++ 2 files changed, 507 insertions(+) diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java new file mode 100644 index 0000000000..961007a995 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/HyperRectangleWriter.java @@ -0,0 +1,301 @@ +/* + * 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.io.stream; + +import java.io.IOException; +import java.awt.Rectangle; +import java.awt.image.DataBuffer; +import java.awt.image.SampleModel; +import java.awt.image.ComponentSampleModel; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import org.apache.sis.util.ArraysExt; + + +/** + * Helper methods for writing a rectangular area, a cube or a hyper-cube in a channel. + * A rectangular area is usually a tile, and consequently should be relatively small. + * The same instance can be reused when the shape of source arrays and the shape of + * the region to write do not change, which is typically the case when writing tiles. + * This class is thread-safe if writing in different output channels. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class HyperRectangleWriter { + /** + * Index of the first value to use in the array given to write methods. + */ + private final int startAt; + + /** + * Number of elements that can be written in a single I/O operation. + */ + private final int contiguousDataLength; + + /** + * Number of elements to write in each dimension after the contiguous dimensions, in reverse order. + * For an image, it may be an array of length 2 with the height and width of the destination region, + * in that order. However it may be an array of length 1 with only the height, or an empty array + * if some chunks of data can be written in a single I/O operation. + * + * <p>Values in this array are decremented by 1.</p> + */ + private final int[] remaining; + + /** + * Value by which to increment the flat array index for moving to the next value. + * The array length and the element order is the same as in {@link #remaining}. + */ + private final int[] strides; + + /** + * Creates a new writer for data of a shape specified by the given region. + * The region also specifies the subset to write. + * + * @param output where to write data. + * @param region size of the source hyper-rectangle and region to write. + * @throws ArithmeticException if the region is too large. + */ + public HyperRectangleWriter(final Region region) { + startAt = Math.toIntExact(region.startAt); + int cdd = region.contiguousDataDimension(); + contiguousDataLength = region.targetLength(cdd); + final int d = region.getDimension() - cdd; + remaining = new int[d]; + strides = new int[d]; + for (int i=d; --i>=0; cdd++) { + if ((remaining[i] = region.getTargetSize(cdd) - 1) < 0 || + (strides[i] = Math.toIntExact(region.getSkip(cdd) + contiguousDataLength)) == 0) + { + /* + * Should have been verified as of Region constructor contract. + * Check again as a safety against never-ending loops. + */ + throw new AssertionError(region); + } + } + } + + /** + * Creates a new writer for raster data described by the given sample model and strides. + * If the given {@code region} is non-null, it specifies a subset of the data to write. + */ + private static HyperRectangleWriter of(final SampleModel sm, final Rectangle region, + final int subX, final int pixelStride, final int scanlineStride) + { + final int[] subsampling = {subX, 1}; + final long[] sourceSize = {scanlineStride, sm.getHeight()}; + final long[] regionLower = new long[2]; + final long[] regionUpper = new long[2]; + if (region != null) { + regionUpper[0] = (regionLower[0] = region.x) + region.width; + regionUpper[1] = (regionLower[1] = region.y) + region.height; + } else { + regionUpper[0] = sm.getWidth(); + regionUpper[1] = sm.getHeight(); + } + regionLower[0] = Math.multiplyExact(regionLower[0], pixelStride); + regionUpper[0] = Math.multiplyExact(regionUpper[0], pixelStride); + return new HyperRectangleWriter(new Region(sourceSize, regionLower, regionUpper, subsampling)); + } + + /** + * Creates a new writer for raster data described by the given sample model. + * This method supports only the writing of either a single band, or all bands + * in the order they appear in the array. + * + * @param sm the sample model of the rasters to write. + * @param region subset to write, or {@code null} if none. + * @return writer, or {@code null} if the given sample model is not supported. + */ + public static HyperRectangleWriter of(final ComponentSampleModel sm, final Rectangle region) { + final int pixelStride = sm.getPixelStride(); + final int[] d = sm.getBandOffsets(); + final int subX; + if (d.length == pixelStride && ArraysExt.isRange(0, d)) { + subX = 1; + } else if (d.length == 1) { + subX = pixelStride; + } else { + return null; + } + return of(sm, region, subX, pixelStride, sm.getScanlineStride()); + } + + /** + * Creates a new writer for raster data described by the given sample model. + * This method supports only the writing of a single band using all bits. + * + * @param sm the sample model of the rasters to write. + * @param region subset to write, or {@code null} if none. + * @return writer, or {@code null} if the given sample model is not supported. + */ + public static HyperRectangleWriter of(final SinglePixelPackedSampleModel sm, final Rectangle region) { + final int[] d = sm.getBitMasks(); + if (d.length == 1) { + final long mask = (1L << DataBuffer.getDataTypeSize(sm.getDataType())) - 1; + if ((d[0] & mask) == mask) { + return of(sm, region, 1, 1, sm.getScanlineStride()); + } + } + return null; + } + + /** + * Creates a new writer for raster data described by the given sample model. + * This method supports only the writing of a single band using all bits. + * + * @param sm the sample model of the rasters to write. + * @param region subset to write, or {@code null} if none. + * @return writer, or {@code null} if the given sample model is not supported. + */ + public static HyperRectangleWriter of(final MultiPixelPackedSampleModel sm, final Rectangle region) { + final int[] d = sm.getSampleSize(); + if (d.length == 1) { + final int size = DataBuffer.getDataTypeSize(sm.getDataType()); + if (d[0] == size && sm.getPixelBitStride() == size) { + return of(sm, region, 1, 1, sm.getScanlineStride()); + } + } + return null; + } + + /** + * Returns the offset of the first element to use in user supplied array. + * + * @param offset offset supplied by the useR. + * @return offset to use. + */ + private int startAt(final int offset) { + return Math.addExact(startAt, offset); + } + + /** + * Returns an array which will be decremented for counting the number of contiguous write operations to apply. + * + * @return number of I/O operations to apply. + */ + private int[] count() { + return remaining.clone(); + } + + /** + * Updates the array index to the next value to use after a contiguous write operation. + * + * @param count array of counters to update in-place. + * @return next offset, or -1 if the iteration is finished. + */ + private int next(int offset, final int[] count) { + for (int i = count.length; --i >= 0;) { + if (--count[i] >= 0) { + return offset + strides[i]; + } + count[i] = remaining[i]; + } + return -1; + } + + /** + * Writes an hyper-rectangle with the shape described at construction time. + * + * @param output where to write data. + * @param data data of the hyper-rectangle. + * @param offset offset to add to array index. + * @throws IOException if an error occurred while writing the data. + */ + public void write(final ChannelDataOutput output, final byte[] data, int offset) throws IOException { + offset = startAt(offset); + final int[] count = count(); + do output.write(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } + + /** + * Writes an hyper-rectangle with the shape described at construction time. + * + * @param output where to write data. + * @param data data of the hyper-rectangle. + * @param offset offset to add to array index. + * @throws IOException if an error occurred while writing the data. + */ + public void write(final ChannelDataOutput output, final short[] data, int offset) throws IOException { + offset = startAt(offset); + final int[] count = count(); + do output.writeShorts(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } + + /** + * Writes an hyper-rectangle with the shape described at construction time. + * + * @param output where to write data. + * @param data data of the hyper-rectangle. + * @param offset offset to add to array index. + * @throws IOException if an error occurred while writing the data. + */ + public void write(final ChannelDataOutput output, final int[] data, int offset) throws IOException { + offset = startAt(offset); + final int[] count = count(); + do output.writeInts(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } + + /** + * Writes an hyper-rectangle with the shape described at construction time. + * + * @param output where to write data. + * @param data data of the hyper-rectangle. + * @param offset offset to add to array index. + * @throws IOException if an error occurred while writing the data. + */ + public void write(final ChannelDataOutput output, final long[] data, int offset) throws IOException { + offset = startAt(offset); + final int[] count = count(); + do output.writeLongs(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } + + /** + * Writes an hyper-rectangle with the shape described at construction time. + * + * @param output where to write data. + * @param data data of the hyper-rectangle. + * @param offset offset to add to array index. + * @throws IOException if an error occurred while writing the data. + */ + public void write(final ChannelDataOutput output, final float[] data, int offset) throws IOException { + offset = startAt(offset); + final int[] count = count(); + do output.writeFloats(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } + + /** + * Writes an hyper-rectangle with the shape described at construction time. + * + * @param output where to write data. + * @param data data of the hyper-rectangle. + * @param offset offset to add to array index. + * @throws IOException if an error occurred while writing the data. + */ + public void write(final ChannelDataOutput output, final double[] data, int offset) throws IOException { + offset = startAt(offset); + final int[] count = count(); + do output.writeDoubles(data, offset, contiguousDataLength); + while ((offset = next(offset, count)) >= 0); + } +} diff --git a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/HyperRectangleWriterTest.java b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/HyperRectangleWriterTest.java new file mode 100644 index 0000000000..67ff0abd68 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/io/stream/HyperRectangleWriterTest.java @@ -0,0 +1,206 @@ +/* + * 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.io.stream; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.lang.reflect.Array; +import java.util.Random; +import java.util.function.IntFunction; +import java.util.function.ToDoubleFunction; +import org.apache.sis.test.TestUtilities; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.junit.jupiter.api.Assertions.*; + + +/** + * Tests {@link HyperRectangleWriter}. + * + * @author Martin Desruisseaux (Geomatys) + */ +public final class HyperRectangleWriterTest extends TestCase { + /** + * The writer to test. + */ + private HyperRectangleWriter writer; + + /** + * The channel where the {@linkplain #writer} will write. + */ + private ChannelDataOutput output; + + /** + * The data actually written by the {@linkplain #writer}. + */ + private ByteBuffer actual; + + /** + * Indices of the first value written along each dimension (inclusive). + */ + private int lowerX, lowerY, lowerZ; + + /** + * Indices after the last value written along each dimension (exclusive). + */ + private int upperX, upperY, upperZ; + + /** + * Subsampling along each dimension. Shall be greater than zero. + * A value of 1 means no subsampling. + */ + private int subsamplingX, subsamplingY, subsamplingZ; + + /** + * An arbitrary offset for the first valid element in the arrays to write. + */ + private int offset; + + /** + * Creates a new test case. + */ + public HyperRectangleWriterTest() { + } + + /** + * Allocates resources for a test of a primitive type. + * + * @param <A> type of the array of primitive type. + * @param creator function to invoke for creating an array of specified length. + * @param dataSize size in bytes of the primitive type. + * @return array of data which will be given to a {@code write(…)} method to test. + * @throws IOException should never happen since we are writing in memory. + */ + private <A> A allocate(final IntFunction<A> creator, final int dataSize) throws IOException { + final Random random = TestUtilities.createRandomNumberGenerator(); + final int sourceSizeX, sourceSizeY, sourceSizeZ, sourceLength; + subsamplingX = random.nextInt(2) + 1; + subsamplingY = random.nextInt(2) + 1; + subsamplingZ = random.nextInt(2) + 1; + offset = random.nextInt(4); + lowerX = random.nextInt(5); + lowerY = random.nextInt(5); + lowerZ = random.nextInt(5); + upperX = random.nextInt(5) + lowerX + subsamplingX * 3; + upperY = random.nextInt(5) + lowerY + subsamplingY * 3; + upperZ = random.nextInt(5) + lowerZ + subsamplingZ * 3; + sourceSizeX = random.nextInt(5) + upperX; // Number of columns in source array. + sourceSizeY = random.nextInt(5) + upperY; // Number of rows in source array. + sourceSizeZ = random.nextInt(5) + upperZ; + sourceLength = sourceSizeX * sourceSizeY * sourceSizeZ; + final A source = creator.apply(sourceLength + offset); + for (int i=0; i<sourceLength; i++) { + final int x = i % sourceSizeX; + final int y = (i / sourceSizeX) % sourceSizeY; + final int z = i / (sourceSizeX * sourceSizeY); + Array.setShort(source, offset + i, (short) (1000 + (z*10 + y) * 10 + x)); + } + final var region = new Region( + new long[] {sourceSizeX, sourceSizeY, sourceSizeZ}, + new long[] {lowerX, lowerY, lowerZ}, + new long[] {upperX, upperY, upperZ}, + new int[] {subsamplingX, subsamplingY, subsamplingZ}); + + final byte[] target = new byte[dataSize * region.targetLength(3)]; + final var buffer = ByteBuffer.allocate(random.nextInt(10) + Double.BYTES); + actual = ByteBuffer.wrap(target); + output = new ChannelDataOutput("Test", new ByteArrayChannel(target, false), buffer); + writer = new HyperRectangleWriter(region); + return source; + } + + /** + * Verifies that the bytes written by {@linkplain #writer} are equal to the expected value. + * + * @param getter the {@link ByteBuffer} getter method corresponding to the tested type. + * @param dataSize size in bytes of the primitive type. + * @throws IOException should never happen since we are writing in memory. + */ + private void verifyWrittenBytes(final ToDoubleFunction<ByteBuffer> getter, final int dataSize) throws IOException { + output.flush(); + for (int z = lowerZ; z < upperZ; z += subsamplingZ) { + for (int y = lowerY; y < upperY; y += subsamplingY) { + for (int x = lowerX; x < upperX; x += subsamplingX) { + assertEquals(1000 + (z*10 + y) * 10 + x, getter.applyAsDouble(actual), + () -> "At index " + (actual.position() / dataSize - 1)); + } + } + } + assertEquals(0, actual.remaining()); + } + + /** + * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, short[], int)} method. + * + * @throws IOException should never happen since we are writing in memory. + */ + @Test + public void testWriteShorts() throws IOException { + final short[] source = allocate(short[]::new, Short.BYTES); + writer.write(output, source, offset); + verifyWrittenBytes(ByteBuffer::getShort, Short.BYTES); + } + + /** + * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, int[], int)} method. + * + * @throws IOException should never happen since we are writing in memory. + */ + @Test + public void testWriteInts() throws IOException { + final int[] source = allocate(int[]::new, Integer.BYTES); + writer.write(output, source, offset); + verifyWrittenBytes(ByteBuffer::getInt, Integer.BYTES); + } + + /** + * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, long[], int)} method. + * + * @throws IOException should never happen since we are writing in memory. + */ + @Test + public void testWriteLongs() throws IOException { + final long[] source = allocate(long[]::new, Long.BYTES); + writer.write(output, source, offset); + verifyWrittenBytes(ByteBuffer::getLong, Long.BYTES); + } + + /** + * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, float[], int)} method. + * + * @throws IOException should never happen since we are writing in memory. + */ + @Test + public void testWriteFloats() throws IOException { + final float[] source = allocate(float[]::new, Float.BYTES); + writer.write(output, source, offset); + verifyWrittenBytes(ByteBuffer::getFloat, Float.BYTES); + } + + /** + * Tests the {@link HyperRectangleWriter#write(ChannelDataOutput, double[], int)} method. + * + * @throws IOException should never happen since we are writing in memory. + */ + @Test + public void testWriteDoubles() throws IOException { + final double[] source = allocate(double[]::new, Double.BYTES); + writer.write(output, source, offset); + verifyWrittenBytes(ByteBuffer::getDouble, Double.BYTES); + } +}