This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new de04f27 Add limited options to `ExtentSelector` for configuring the selection criteria. de04f27 is described below commit de04f27807d5be3bdb80fdf196496628c20cbc3b Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Dec 1 15:17:20 2021 +0100 Add limited options to `ExtentSelector` for configuring the selection criteria. --- .../sis/internal/referencing/ExtentSelector.java | 99 ++++++++++++++++++++-- .../internal/referencing/ExtentSelectorTest.java | 87 ++++++++++++++++--- 2 files changed, 165 insertions(+), 21 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java index 11da980..c95f9fd 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ExtentSelector.java @@ -24,6 +24,7 @@ import org.opengis.metadata.extent.GeographicBoundingBox; import org.apache.sis.metadata.iso.extent.Extents; import org.apache.sis.math.MathFunctions; import org.apache.sis.measure.Range; +import org.apache.sis.util.resources.Errors; /** @@ -58,8 +59,15 @@ import org.apache.sis.measure.Range; * then the first of those candidates is selected.</li> * </ol> * + * <h2>Change of rule order</h2> + * The following configuration flags change the order in which above rules are applied. + * + * <ul> + * <li>{@link #alternateOrdering} — whether the time center criterion is tested last.</li> + * </ul> + * * <h2>Usage</h2> - * Example! + * Example: * * {@preformat java * ExtentSelector<Foo> selector = new ExtentSelector<>(areaOfInterest); @@ -70,7 +78,7 @@ import org.apache.sis.measure.Range; * } * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * * @param <T> the type of object to be selected. * @@ -95,6 +103,27 @@ public final class ExtentSelector<T> { private Instant minTOI, maxTOI; /** + * Granularity of the time of interest in seconds, or 0 if none. + * If non-zero, this this is always positive. + * + * @see #setTimeGranularity(Duration) + * @see #round(Duration) + */ + private long granularity; + + /** + * Whether to use an alternate conditions order where the time center criterion is tested last. + * This flag can be set to {@code true} if the Time Of Interest (TOI) is expected to be larger + * than the temporal extent of candidate objects, in which case many objects may fit in the TOI. + * Those candidates may be considered practically equally good regarding temporal aspect, + * in which case the caller may want to give precedence to geographic area. + * + * <p>This flag is often used together with {@link #setTimeGranularity(Duration)} method + * for reducing the preponderance of temporal criteria.</p> + */ + public boolean alternateOrdering; + + /** * The best object found so far. */ private T best; @@ -108,6 +137,7 @@ public final class ExtentSelector<T> { /** * Duration of the {@linkplain #best} object, or {@code null} if none. * This is equivalent to {@link #largestArea} in the temporal domain. + * Value is rounded by {@link #round(Duration)}. */ private Duration longestTime; @@ -222,6 +252,48 @@ public final class ExtentSelector<T> { } /** + * Sets the temporal granularity of the Time of Interest (TOI). If non-null, intersections with TOI + * will be rounded to an integer amount of this granularity. This is useful if data are expected at + * an approximately regular interval (for example one remote sensing image per day) and we want to + * ignore slight variations in the temporal extent declared for each image. + * + * <p>This method is often used together with {@link #alternateOrdering} flag + * for reducing the preponderance of temporal criteria.</p> + * + * @param resolution granularity of the time of interest, or {@code null} if none. + * @throws IllegalArgumentException if the given resolution is zero or negative. + */ + public final void setTimeGranularity(final Duration resolution) { + if (resolution == null) { + granularity = 0; + } else if (resolution.isZero() || resolution.isNegative()) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "resolution", resolution)); + } else { + granularity = resolution.getSeconds(); + } + } + + /** + * Returns the given duration rounded to the nearest integer amount of temporal granularity. + * If no granularity has been specified, then this method returns the given duration unmodified. + */ + private Duration round(Duration duration) { + if (duration != null && !duration.isZero() && granularity != 0) { + long t = duration.getSeconds(); + long n = t / granularity; + long r = t % granularity; + if (r != 0) { + t -= r; + if (t == 0 || r >= (granularity >> 1)) { + t += granularity; + }; + duration = Duration.ofSeconds(t); + } + } + return duration; + } + + /** * Computes a pseudo-distance between the center of given area and of {@link #areaOfInterest}. * This is <strong>not</strong> a real distance, neither great circle distance or rhumb line. * May be {@link Double#NaN} if information is unknown. @@ -274,10 +346,16 @@ public final class ExtentSelector<T> { /** * Computes the amount of time outside the time of interest (TOI). The returned value is always positive * because {@code intersection} should always be less than {@code endTime} − {@code startTime} duration. + * Value is rounded by {@link #round(Duration)}. + * + * @param startTime start time of of the candidate object, or {@code null} if none (unbounded). + * @param endTime end time of the candidate object, or {@code null} if none (unbounded). + * @param intersection duration of the intersection of [{@code startTime} … {@code endTime}] + * with [{@link #minTOI} … {@link #maxTOI}]. */ - private static Duration overtime(final Instant startTime, final Instant endTime, final Duration intersection) { + private Duration overtime(final Instant startTime, final Instant endTime, final Duration intersection) { return (startTime != null && endTime != null && intersection != null) - ? Duration.between(startTime, endTime).minus(intersection) : null; + ? round(Duration.between(startTime, endTime).minus(intersection)) : null; } /** @@ -349,8 +427,9 @@ public final class ExtentSelector<T> { * // Compute and test criteria. * } */ + final Duration durationRounded = round(duration); int comparison, remainingFieldsToCompute = OVERTIME; - if (best != null && (comparison = compare(duration, longestTime, -1)) <= 0) { + if (best != null && (comparison = compare(durationRounded, longestTime, -1)) <= 0) { if (comparison != 0) return; /* * Criterion #2: select the object having smallest amount of time outside Time Of Interest (TOI). @@ -362,10 +441,11 @@ public final class ExtentSelector<T> { if (comparison != 0) return; /* * Criterion #3: select the object having median time closest to TOI median time. + * This condition is skipped in the "alternate condition ordering" mode. */ remainingFieldsToCompute = OUTSIDE_AREA; final double td = temporalDistance(startTime, endTime); - if ((comparison = compare(td, temporalDistance, +1)) >= 0) { + if (alternateOrdering || (comparison = compare(td, temporalDistance, +1)) >= 0) { if (comparison != 0) return; /* * Criterion #4: select the object covering largest geographic area. @@ -383,22 +463,27 @@ public final class ExtentSelector<T> { /* * Criterion #5: select the object having center closest to AOI center. * Distances are computed with inexact formulas (not a real distance). + * TOI is also compared here in "alternate condition ordering" mode. */ remainingFieldsToCompute = NONE; final double pd = pseudoDistance(bbox); if (compare(pd, pseudoDistance, +1) >= 0) { return; } + if (alternateOrdering && compare(td, temporalDistance, +1) >= 0) { + return; + } pseudoDistance = pd; } outsideArea = out; } + // largestArea = area; assigned below because was computed early. } temporalDistance = td; } overtime = et; } - longestTime = duration; + longestTime = durationRounded; largestArea = area; switch (remainingFieldsToCompute) { // Intentional fallthrough in every cases. case OVERTIME: overtime = overtime(startTime, endTime, duration); diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ExtentSelectorTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ExtentSelectorTest.java index 219094f..6d54eaa 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ExtentSelectorTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ExtentSelectorTest.java @@ -17,6 +17,7 @@ package org.apache.sis.internal.referencing; import java.util.Date; +import java.time.Duration; import org.opengis.metadata.extent.Extent; import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent; @@ -32,12 +33,24 @@ import static org.junit.Assert.*; * Tests {@link ExtentSelector}. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ public final strictfp class ExtentSelectorTest extends TestCase { /** + * Whether to test an alternate ordering where distance to TOI is tested last. + * + * @see ExtentSelector#alternateOrdering + */ + private boolean alternateOrdering; + + /** + * The temporal granularity of the Time Of Interest (TOI), or {@code null} if none. + */ + private Duration granularity; + + /** * Tests the selector when intersection with AOI is a sufficient criterion. */ @Test @@ -76,10 +89,37 @@ public final strictfp class ExtentSelectorTest extends TestCase { @Test @Ignore("Require temporal module, not yet available in SIS.") public void testTemporal() { - assertBestEquals(time(1000, 2000), 2, - time(1500, 3000), - time(1300, 1800), // Same duration than above, but better centered. - time(1400, 1600)); // Well centered but intersection is small. + assertBestEquals(time(1000, 2000, true), 2, + time(1500, 2000, true), + time(1300, 1800, false), // Same duration than above, but better centered. + time(1400, 1600, true)); // Well centered but intersection is small. + } + + /** + * Tests using temporal ranges with {@link ExtentSelector#alternateOrdering} set to {@code true}. + * The criterion should give precedence to larger geographic area instead of better centered. + */ + @Test + @Ignore("Require temporal module, not yet available in SIS.") + public void testAlternateOrdering() { + alternateOrdering = true; + assertBestEquals(time(1000, 2000, true), 1, + time(1500, 2000, true), // Not well centered by has larger geographic area. + time(1300, 1800, false), // Better centered but smaller geographic area. + time(1400, 1600, true)); + } + + /** + * Tests using temporal ranges with {@link ExtentSelector#setTimeGranularity(Duration)} defined. + */ + @Test + @Ignore("Require temporal module, not yet available in SIS.") + public void testTimeGranularity() { + granularity = Duration.ofSeconds(20); + assertBestEquals(time(10000, 70000, true), 3, + time(14000, 47000, false), // 2 units of temporal resolution. + time(15000, 46000, true), // Same size if counted in units of temporal resolution. + time(25000, 55000, true)); // Same size in units of resolution, but better centered. } /** @@ -98,11 +138,30 @@ public final strictfp class ExtentSelectorTest extends TestCase { /** * Creates an extent for a temporal range having the given boundaries. + * A geographic extent is also added, but may be ignored because temporal extent has precedence. + * + * @param startTime arbitrary start time in milliseconds. + * @param endTime arbitrary end time in milliseconds. + * @param largeArea {@code false} for associating a small geographic area, + * {@code true} for associating a larger geographic area. */ - private static Extent time(final long startTime, final long endTime) { + private static Extent time(final long startTime, final long endTime, final boolean largeArea) { + final DefaultGeographicBoundingBox bbox = new DefaultGeographicBoundingBox( + largeArea ? -20 : -10, 10, + largeArea ? 10 : 20, 30); final DefaultTemporalExtent range = new DefaultTemporalExtent(); range.setBounds(new Date(startTime), new Date(endTime)); - return new DefaultExtent(null, null, null, range); + return new DefaultExtent(null, bbox, null, range); + } + + /** + * Creates the selector to use for testing purpose. + */ + private ExtentSelector<Integer> create(final Extent aoi) { + ExtentSelector<Integer> selector = new ExtentSelector<>(aoi); + selector.alternateOrdering = alternateOrdering; + selector.setTimeGranularity(granularity); + return selector; } /** @@ -111,34 +170,34 @@ public final strictfp class ExtentSelectorTest extends TestCase { * @param aoi area of interest to give to {@link ExtentSelector} constructor. * @param expected expected best result: 1 for <var>a</var>, 2 for <var>b</var> or 3 for <var>c</var>. */ - private static void assertBestEquals(final Extent aoi, final Integer expected, - final Extent a, final Extent b, final Extent c) + private void assertBestEquals(final Extent aoi, final Integer expected, + final Extent a, final Extent b, final Extent c) { - ExtentSelector<Integer> selector = new ExtentSelector<>(aoi); + ExtentSelector<Integer> selector = create(aoi); selector.evaluate(a, 1); selector.evaluate(b, 2); selector.evaluate(c, 3); assertEquals(expected, selector.best()); - selector = new ExtentSelector<>(aoi); + selector = create(aoi); selector.evaluate(b, 2); selector.evaluate(c, 3); selector.evaluate(a, 1); assertEquals(expected, selector.best()); - selector = new ExtentSelector<>(aoi); + selector = create(aoi); selector.evaluate(c, 3); selector.evaluate(a, 1); selector.evaluate(b, 2); assertEquals(expected, selector.best()); - selector = new ExtentSelector<>(aoi); + selector = create(aoi); selector.evaluate(a, 1); selector.evaluate(c, 3); selector.evaluate(b, 2); assertEquals(expected, selector.best()); - selector = new ExtentSelector<>(aoi); + selector = create(aoi); selector.evaluate(b, 2); selector.evaluate(a, 1); selector.evaluate(c, 3);