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 022a1f3fffaf04cc3b2bdf75829287a678889a3a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Aug 11 15:46:36 2021 +0200 Consolidation: - Safer rounding of subsampling values. - Add test and clarifications in Javadoc. --- .../apache/sis/coverage/grid/GridDerivation.java | 40 ++++++++++++++++------ .../sis/coverage/grid/GridDerivationTest.java | 24 +++++++++++++ .../org/apache/sis/storage/geotiff/GeoTIFF.java | 2 +- .../apache/sis/storage/geotiff/GeoTiffStore.java | 2 +- .../sis/internal/storage/AbstractResource.java | 7 ++++ .../apache/sis/storage/event/StoreListeners.java | 6 ++-- 6 files changed, 66 insertions(+), 15 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java index 445e2ff..0c9c26a 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java @@ -1277,7 +1277,7 @@ public class GridDerivation { /** * Rounds a subsampling value according the current {@link RoundingMode}. * If a {@link #chunkSize} has been specified, then the subsampling will be a divisor of that size. - * This is necessary for avoiding a drift in subsampled pixel coordinates computed from tile coordinates. + * This is necessary for avoiding a drift of subsampled pixel coordinates computed from tile coordinates. * * <div class="note"><b>Drift example:</b> * if the tile size is 16 pixels and the subsampling is 3, then the subsampled tile size is ⌊16/3⌋ = 5 pixels. @@ -1306,8 +1306,8 @@ public class GridDerivation { switch (rounding) { default: throw new AssertionError(rounding); case NEAREST: subsampling = (int) Math.min(Math.round(scale), Integer.MAX_VALUE); break; - case CONTAINED: // Assume user wants more data in source (ENCLOSING) or target (CONTAINED) grid. - case ENCLOSING: subsampling = (int) Math.nextUp(scale); break; + case CONTAINED: subsampling = (int) Math.ceil(scale - tolerance(dimension)); break; + case ENCLOSING: subsampling = (int) (scale + tolerance(dimension)); break; } if (subsampling <= 1) { return 1; @@ -1321,24 +1321,42 @@ public class GridDerivation { /* * `binarySearch(…)` should never find an exact match, otherwise (size % r) would have been zero. * Furthermore `i` should never be 0 because divisors[0] = 1, which can not be selected if r > 1. - * We nevertheless check for (i > 0) as a paranoiac safety. + * We do not check `if (i > 0)` "as a safety" because client code such as `TiledGridCoverage` + * will behave erratically if this method does not fulfill its contract (i.e. find a divisor). + * It is better to know now if there is any problem here. */ - if (i > 0) { - int s = divisors[i-1]; - if (rounding == GridRoundingMode.NEAREST && i < divisors.length) { - final int above = divisors[i]; - if (above - r < r - s) { - s = above; + int s = divisors[i-1]; + if (i < divisors.length) { + switch (rounding) { + case CONTAINED: { + s = divisors[i]; + break; + } + case NEAREST: { + final int above = divisors[i]; + if (above - r < r - s) { + s = above; + } + break; } } - return s + (subsampling - r); } + return s + (subsampling - r); } } return subsampling; } /** + * Returns a tolerance factor for comparing scale factors in the given dimension. + * The tolerance is such that the errors of pixel coordinates computed using the + * scale factor should not be greater than 0.5 pixel. + */ + private double tolerance(final int dimension) { + return (base.extent != null) ? 0.5 / base.extent.getSize(dimension, false) : 0; + } + + /** * Returns the offsets to be subtracted from pixel coordinates before subsampling. * In a conversion from <em>derived</em> grid to {@linkplain #base} grid coordinates * (the opposite direction of subsampling), the offset is the value to add after diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java index 3573c30..28887b0 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java @@ -374,6 +374,30 @@ public final strictfp class GridDerivationTest extends TestCase { } /** + * Tests {@link GridDerivation#subgrid(GridExtent)} with a null "grid to CRS" transform. + */ + @Test + public void testSubgridWithoutTransform() { + GridExtent base = new GridExtent(null, new long[] {100, 200}, new long[] {300, 350}, true); + GridExtent query = new GridExtent(null, new long[] {120, 180}, new long[] {280, 360}, true); + GridGeometry result = new GridGeometry(base, null, null, null).derive().subgrid(query).build(); + assertExtentEquals(new long[] {120, 200}, new long[] {280, 350}, result.getExtent()); + assertFalse(result.isDefined(GridGeometry.GRID_TO_CRS)); + assertFalse(result.isDefined(GridGeometry.CRS)); + assertFalse(result.isDefined(GridGeometry.RESOLUTION)); + /* + * Try again with a subsampling. We can get the subsampling information back as the resolution. + * Note that there is no way with current API to get the subsampling offsets. + */ + result = new GridGeometry(base, null, null, null).derive().subgrid(query, 3, 5).build(); + assertExtentEquals(new long[] {40, 40}, new long[] {93, 70}, result.getExtent()); + assertFalse(result.isDefined(GridGeometry.GRID_TO_CRS)); + assertFalse(result.isDefined(GridGeometry.CRS)); + assertTrue (result.isDefined(GridGeometry.RESOLUTION)); + assertArrayEquals(new double[] {3, 5}, result.getResolution(false), STRICT); + } + + /** * Tests {@link GridDerivation#slice(DirectPosition)}. */ @Test diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java index 3f9d7da..baa5a9e 100644 --- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java +++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java @@ -49,7 +49,7 @@ abstract class GeoTIFF implements Closeable { * The locale to use for parsers or formatter. This is <strong>not</strong> the locale * for warnings or other messages emitted to the users. */ - static final Locale LOCALE = Locale.US; + private static final Locale LOCALE = Locale.US; /** * The magic number for big-endian TIFF files or little-endian TIFF files. diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java index 4a234d1..98dd0ed 100644 --- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java +++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java @@ -228,7 +228,7 @@ public class GeoTiffStore extends DataStore implements Aggregate { /* * Add the filename as an identifier only if the input was something convertible to URI (URL, File or Path), * otherwise reader.input.filename may not be useful; it may be just the InputStream classname. If the TIFF - * file did not specified any ImageDescription tag, then we will had the filename as a title instead than an + * file did not specified any ImageDescription tag, then we will add the filename as a title instead than an * identifier because the title is mandatory in ISO 19115 metadata. */ getIdentifier().ifPresent((id) -> builder.addTitleOrIdentifier(id.toString(), MetadataBuilder.Scope.ALL)); diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java index a1c8eba..c2def88 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java @@ -76,6 +76,12 @@ public class AbstractResource extends StoreListeners implements Resource { * Returns the resource persistent identifier if available. * The default implementation returns an empty value. * Subclasses are strongly encouraged to override if they can provide a value. + * + * <p>Note that the default implementation of {@link #createMetadata(MetadataBuilder)} uses this identifier + * for initializing the {@code metadata/identificationInfo/citation/title} property. So it is generally not + * useful to fallback on metadata if the identifier is empty.</p> + * + * @see org.apache.sis.internal.storage.StoreUtilities#getLabel(Resource) */ @Override public Optional<GenericName> getIdentifier() throws DataStoreException { @@ -120,6 +126,7 @@ public class AbstractResource extends StoreListeners implements Resource { * @throws DataStoreException if an error occurred while reading metadata from the data store. */ protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException { + // Note: title is mandatory in ISO metadata, contrarily to the identifier. getIdentifier().ifPresent((name) -> metadata.addTitle(new Sentence(name))); getEnvelope().ifPresent((envelope) -> { try { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java index 8c0819a..55d3d69 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java @@ -31,6 +31,7 @@ import org.apache.sis.util.logging.Logging; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.internal.storage.StoreResource; import org.apache.sis.internal.storage.StoreUtilities; +import org.apache.sis.internal.storage.AbstractResource; import org.apache.sis.storage.DataStoreProvider; import org.apache.sis.storage.DataStore; import org.apache.sis.storage.Resource; @@ -84,6 +85,7 @@ public class StoreListeners implements Localized { /** * The declared source of events. This is not necessarily the real source, * but this is the source that the implementer wants to declare as public API. + * Never {@code null} but may be {@code this}. */ private final Resource source; @@ -214,7 +216,7 @@ public class StoreListeners implements Localized { * This is used as a convenience by AbstractResource internal class. We need this hack * because subclasses can not reference `this` before super-class constructor completed. */ - if (source == null && this instanceof Resource) { + if (source == null && this instanceof AbstractResource) { source = (Resource) this; } else { ArgumentChecks.ensureNonNull("source", source); @@ -226,7 +228,7 @@ public class StoreListeners implements Localized { /** * Returns the source of events. This value is specified at construction time. * - * @return the source of events. + * @return the source of events. Never {@code null} but may be {@code this}. */ public Resource getSource() { return source;