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 8b0191c652 More dynamic and more efficient comparisons of temporal 
values depending on their type. This is required by the change of period 
beginning/ending times to `Temporal`. This work duplicates work already done in 
`ComparisonFilter`, but the latter will need to be refactored in order to 
delegate to the new temporal filters.
8b0191c652 is described below

commit 8b0191c6527602cd2aa5f99ac937d7ccc70c9d0c
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue May 28 18:29:00 2024 +0200

    More dynamic and more efficient comparisons of temporal values depending on 
their type.
    This is required by the change of period beginning/ending times to 
`Temporal`.
    This work duplicates work already done in `ComparisonFilter`, but the latter
    will need to be refactored in order to delegate to the new temporal filters.
---
 .../org/apache/sis/filter/ComparisonFilter.java    |   4 +
 .../apache/sis/filter/DefaultFilterFactory.java    |  68 +-
 .../main/org/apache/sis/filter/TemporalFilter.java | 811 ++++-----------------
 .../org/apache/sis/filter/TemporalOperation.java   | 762 +++++++++++++++++++
 .../main/org/apache/sis/filter/TimeMethods.java    | 396 ++++++++++
 .../org/apache/sis/filter/TemporalFilterTest.java  |  23 +-
 .../metadata/iso/extent/DefaultTemporalExtent.java |   2 +-
 .../apache/sis/pending/temporal/DefaultPeriod.java |  14 +-
 .../sis/pending/temporal/TemporalUtilities.java    |  26 +-
 .../apache/sis/xml/bind/gml/TimePeriodBound.java   |  12 +-
 .../referencing/datum/DefaultTemporalDatum.java    |   4 +-
 .../org/apache/sis/referencing/internal/Epoch.java |   6 +-
 .../main/org/apache/sis/util/Classes.java          |  20 +-
 .../main/org/apache/sis/util/resources/Errors.java |   5 +
 .../apache/sis/util/resources/Errors.properties    |   1 +
 .../apache/sis/util/resources/Errors_fr.properties |   1 +
 geoapi/snapshot                                    |   2 +-
 17 files changed, 1425 insertions(+), 732 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
index 0529727c64..04b5b9d050 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
@@ -66,6 +66,8 @@ import org.opengis.filter.BetweenComparisonOperator;
  * returning 0 if {@code false} or 1 if {@code true}. Comparisons of other 
types is done by overriding
  * the {@code compare(…)} methods.</p>
  *
+ * @todo Delegate all comparisons of temporal objects to {@link 
TemporalFilter}.
+ *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  *
@@ -232,6 +234,8 @@ abstract class ComparisonFilter<R> extends 
BinaryFunction<R,Object,Object>
      *
      * @param  left   the first object to compare. Must be non-null.
      * @param  right  the second object to compare. Must be non-null.
+     *
+     * @todo Delegate all comparisons of temporal objects to {@link 
TemporalFilter}.
      */
     private boolean evaluate(Object left, Object right) {
         /*
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index ceb827936b..c020754996 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@ -32,6 +32,7 @@ import org.apache.sis.filter.sqlmm.Registry;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.iso.AbstractFactory;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.privy.Strings;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import java.util.Iterator;
@@ -66,6 +67,11 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
      */
     private final Geometries<G> library;
 
+    /**
+     * The base class of temporal objects.
+     */
+    private final Class<T> temporal;
+
     /**
      * The strategy to use for representing a region crossing the 
anti-meridian.
      */
@@ -93,14 +99,14 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
      *   <tr><td>Default</td> <td>{@code java.lang.Object}</td></tr>
      * </table>
      *
-     * <table class="sis">
-     *   <caption>Authorized temporal class argument values</caption>
-     *   <tr><th>Library</th> <th>Temporal class</th></tr>
-     *   <tr><td>Default</td> <td>{@code java.lang.Object}</td></tr>
-     * </table>
+     * The {@code temporal} argument should be one of the {@link 
java.time.temporal.Temporal}
+     * implementation classes. The {@code Temporal.class} or {@code 
Object.class} arguments
+     * are also accepted if the temporal class is not known at compile-time, 
in which case
+     * it will be determined on a case-by-case basis at runtime. Note the 
latter is lightly
+     * more expensive that specifying an implementation class in advance.
      *
      * @param  spatial     type of spatial objects,  or {@code Object.class} 
for default.
-     * @param  temporal    type of temporal objects, or {@code Object.class} 
for default.
+     * @param  temporal    type of temporal objects, or {@code Object.class} 
for any supported type.
      * @param  wraparound  the strategy to use for representing a region 
crossing the anti-meridian.
      */
     @SuppressWarnings("unchecked")
@@ -116,9 +122,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
                 throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, 
"spatial", spatial));
             }
         }
-        if (temporal != Object.class) {
-            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, 
"temporal", temporal));
-        }
+        this.temporal = temporal;
         this.wraparound = wraparound;
         availableFunctions = new HashMap<>();
     }
@@ -126,10 +130,9 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     /**
      * Returns a factory operating on {@link Feature} instances.
      * The {@linkplain GeometryLibrary geometry library} will be the system 
default.
+     * The temporal objects can be {@link java.util.Date} or implementations 
of {@link java.time.temporal.Temporal}.
      *
      * @return factory operating on {@link Feature} instances.
-     *
-     * @todo The type of temporal objects is not yet determined.
      */
     public static FilterFactory<Feature, Object, Object> forFeatures() {
         return Features.DEFAULT;
@@ -170,7 +173,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
          * super-class constructor} for a list of valid class arguments.
          *
          * @param  spatial     type of spatial objects,  or {@code 
Object.class} for default.
-         * @param  temporal    type of temporal objects, or {@code 
Object.class} for default.
+         * @param  temporal    type of temporal objects, or {@code 
Object.class} for any supported type.
          * @param  wraparound  the strategy to use for representing a region 
crossing the anti-meridian.
          *
          * @see DefaultFilterFactory#forFeatures()
@@ -729,7 +732,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> after(final Expression<R, ? extends T> time1,
                                      final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.After<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.After::new, 
time1, time2);
     }
 
     /**
@@ -745,7 +748,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> before(final Expression<R, ? extends T> time1,
                                       final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.Before<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.Before::new, 
time1, time2);
     }
 
     /**
@@ -761,7 +764,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> begins(final Expression<R, ? extends T> time1,
                                       final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.Begins<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.Begins::new, 
time1, time2);
     }
 
     /**
@@ -777,7 +780,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> begunBy(final Expression<R, ? extends T> time1,
                                        final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.BegunBy<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.BegunBy::new, 
time1, time2);
     }
 
     /**
@@ -793,7 +796,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> tcontains(final Expression<R, ? extends T> 
time1,
                                          final Expression<R, ? extends T> 
time2)
     {
-        return new TemporalFilter.Contains<>(time1, time2);
+        return TemporalFilter.create(temporal, 
TemporalOperation.Contains::new, time1, time2);
     }
 
     /**
@@ -809,7 +812,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> during(final Expression<R, ? extends T> time1,
                                       final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.During<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.During::new, 
time1, time2);
     }
 
     /**
@@ -825,7 +828,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> tequals(final Expression<R, ? extends T> time1,
                                        final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.Equals<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.Equals::new, 
time1, time2);
     }
 
     /**
@@ -841,7 +844,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> toverlaps(final Expression<R, ? extends T> 
time1,
                                          final Expression<R, ? extends T> 
time2)
     {
-        return new TemporalFilter.Overlaps<>(time1, time2);
+        return TemporalFilter.create(temporal, 
TemporalOperation.Overlaps::new, time1, time2);
     }
 
     /**
@@ -857,7 +860,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> meets(final Expression<R, ? extends T> time1,
                                      final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.Meets<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.Meets::new, 
time1, time2);
     }
 
     /**
@@ -873,7 +876,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> ends(final Expression<R, ? extends T> time1,
                                     final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.Ends<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.Ends::new, 
time1, time2);
     }
 
     /**
@@ -889,7 +892,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> overlappedBy(final Expression<R, ? extends T> 
time1,
                                             final Expression<R, ? extends T> 
time2)
     {
-        return new TemporalFilter.OverlappedBy<>(time1, time2);
+        return TemporalFilter.create(temporal, 
TemporalOperation.OverlappedBy::new, time1, time2);
     }
 
     /**
@@ -905,7 +908,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> metBy(final Expression<R, ? extends T> time1,
                                      final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.MetBy<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.MetBy::new, 
time1, time2);
     }
 
     /**
@@ -921,7 +924,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> endedBy(final Expression<R, ? extends T> time1,
                                        final Expression<R, ? extends T> time2)
     {
-        return new TemporalFilter.EndedBy<>(time1, time2);
+        return TemporalFilter.create(temporal, TemporalOperation.EndedBy::new, 
time1, time2);
     }
 
     /**
@@ -938,7 +941,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
     public TemporalOperator<R> anyInteracts(final Expression<R, ? extends T> 
time1,
                                             final Expression<R, ? extends T> 
time2)
     {
-        return new TemporalFilter.AnyInteracts<>(time1, time2);
+        return TemporalFilter.create(temporal, 
TemporalOperation.AnyInteracts::new, time1, time2);
     }
 
     /**
@@ -1152,4 +1155,17 @@ public abstract class DefaultFilterFactory<R,G,T> 
extends AbstractFactory implem
     public SortProperty<R> sort(final ValueReference<R,?> property, final 
SortOrder order) {
         return new DefaultSortProperty<>(property, order);
     }
+
+    /**
+     * Returns a string representation of this factory for debugging purposes.
+     * The string returned by this method may change in any future version.
+     *
+     * @return a string representation for debugging purposes.
+     *
+     * @since 1.5
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), "spatial", library.library, 
"temporal", temporal.getSimpleName());
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
index f6e567be64..88ebd77415 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
@@ -16,7 +16,8 @@
  */
 package org.apache.sis.filter;
 
-import java.time.Instant;
+import org.apache.sis.util.Classes;
+import org.apache.sis.feature.privy.FeatureExpression;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.temporal.Period;
@@ -27,743 +28,247 @@ import org.opengis.filter.TemporalOperatorName;
 
 
 /**
- * Temporal operations between a period and an instant.
- * The nature of the operation depends on the subclass.
- * Subclasses shall override at least one of following methods:
- *
- * <ul>
- *   <li>{@link #evaluate(Instant, Instant)}</li>
- *   <li>{@link #evaluate(Period, Instant)}</li>
- *   <li>{@link #evaluate(Period, Period)}</li>
- * </ul>
+ * Temporal operations between a period and an instant or between two periods.
+ * The base class represents the general case when don't know if the the 
argument are periods or not.
+ * The subclasses represent specializations when at least one of the arguments 
is known to be a period.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  *
- * @param  <T>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
+ * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
+ * @param  <T>  the base type of temporal objects, or {@code Object.class} for 
any type.
  */
-abstract class TemporalFilter<T> extends BinaryFunction<T,Object,Object>
-        implements TemporalOperator<T>, Optimization.OnFilter<T>
+class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
+        implements TemporalOperator<R>, Optimization.OnFilter<R>
 {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = 5392780837658687513L;
+    private static final long serialVersionUID = 8248634286785309435L;
+
+    /**
+     * The operation to apply on instants or periods.
+     */
+    protected final TemporalOperation<T> operation;
 
     /**
      * Creates a new temporal function.
      *
+     * @param  operation    the operation to apply on instants or periods.
      * @param  expression1  the first of the two expressions to be used by 
this function.
      * @param  expression2  the second of the two expressions to be used by 
this function.
      */
-    TemporalFilter(final Expression<T,?> expression1,
-                   final Expression<T,?> expression2)
+    private TemporalFilter(final TemporalOperation<T> operation,
+                           final Expression<R, ? extends T> expression1,
+                           final Expression<R, ? extends T> expression2)
     {
         super(expression1, expression2);
+        this.operation = operation;
     }
 
     /**
-     * Returns {@code true} if {@code self} is non null and before {@code 
other}.
-     * This is an helper function for {@code evaluate(…)} methods 
implementations.
-     */
-    private static boolean isBefore(final Instant self, final Instant other) {
-        return (self != null) && (other != null) && self.isBefore(other);
-    }
-
-    /**
-     * Returns {@code true} if {@code self} is non null and after {@code 
other}.
-     * This is an helper function for {@code evaluate(…)} methods 
implementations.
-     */
-    private static boolean isAfter(final Instant self, final Instant other) {
-        return (self != null) && (other != null) && self.isAfter(other);
-    }
-
-    /**
-     * Returns {@code true} if {@code self} is non null and equal to {@code 
other}.
-     * This is an helper function for {@code evaluate(…)} methods 
implementations.
-     */
-    private static boolean isEqual(final Instant self, final Instant other) {
-        return (self != null) && self.equals(other);
-    }
-
-    /**
-     * Determines if the test(s) represented by this filter passes with the 
given operands.
-     * Values of {@link #expression1} and {@link #expression2} shall be two 
single values.
+     * Creates a new temporal function.
+     *
+     * @param  expression1  the first of the two expressions to be used by 
this function.
+     * @param  expression2  the second of the two expressions to be used by 
this function.
+     * @param  operation    the operation to apply on instants or periods.
      */
-    @Override
-    public final boolean test(final T candidate) {
-        final Object left = expression1.apply(candidate);
-        if (left instanceof Period) {
-            final Object right = expression2.apply(candidate);
-            if (right instanceof Period) {
-                return evaluate((Period) left, (Period) right);
-            }
-            final Instant t = ComparisonFilter.toInstant(right);
-            if (t != null) {
-                return evaluate((Period) left, t);
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public static <R,V> TemporalFilter<R,?> create(
+            final Class<V> type,
+            final TemporalOperation.Factory factory,
+            final Expression<R, ? extends V> expression1,
+            final Expression<R, ? extends V> expression2)
+    {
+        final Class<? extends V> c1 = getValueClass(expression1, type);
+        final Class<? extends V> c2 = getValueClass(expression2, type);
+        Class<? extends V> commonType = type;
+        if (type.isInterface()) {
+            for (final Class<?> c : Classes.findCommonInterfaces(c1, c2)) {
+                if (commonType.isAssignableFrom(c)) {
+                    commonType = (Class<? extends V>) c;        // Safe 
because verified by `isAssignableFrom(c)`.
+                }
             }
         } else {
-            final Instant t = ComparisonFilter.toInstant(left);
-            if (t != null) {
-                final Instant t2 = 
ComparisonFilter.toInstant(expression2.apply(candidate));
-                if (t2 != null) {
-                    return evaluate(t, t2);
-                }
+            final Class<?> c = Classes.findCommonClass(c1, c2);
+            if (commonType.isAssignableFrom(c)) {
+                commonType = (Class<? extends V>) c;            // Safe 
because verified by `isAssignableFrom(c)`.
             }
         }
-        return false;
-    }
-
-    /**
-     * Evaluates the filter between two instants.
-     * Both arguments given to this method are non-null.
-     * The {@code self} and {@code other} argument names are chosen to match 
ISO 19108 tables.
-     */
-    protected boolean evaluate(Instant self, Instant other) {
-        return false;
-    }
-
-    /**
-     * Evaluates the filter between a period and an instant.
-     * Both arguments given to this method are non-null, but period begin or 
end instant may be null.
-     * The {@code self} and {@code other} argument names are chosen to match 
ISO 19108 tables.
-     */
-    protected boolean evaluate(Period self, Instant other) {
-        return false;
-    }
-
-    /**
-     * Evaluates the filter between two periods.
-     * Both arguments given to this method are non-null, but period begin or 
end instant may be null.
-     * The {@code self} and {@code other} argument names are chosen to match 
ISO 19108 tables.
-     */
-    protected boolean evaluate(Period self, Period other) {
-        return false;
-    }
-
-    /*
-     * No operation on numbers for now. We could revisit this policy in a 
future version if we
-     * allow the temporal function to have a CRS and to operate on temporal 
coordinate values.
-     */
-
-
-    /**
-     * The {@code "TEquals"} (=) filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self = other}</li>
-     *   <li>{@literal self.begin = other.begin  AND  self.end = 
other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
-     */
-    static final class Equals<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -6060822291802339424L;
-
-        /** Creates a new filter. */
-        Equals(Expression<T,?> expression1,
-               Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Equals<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.EQUALS;
-        }
-
-        /** Symbol of this operation. */
-        @Override protected char symbol() {
-            return '=';
-        }
-
-        /** Condition defined by ISO 19108:2002 §5.2.3.5. */
-        @Override protected boolean evaluate(final Instant self, final Instant 
other) {
-            return self.equals(other);
-        }
-
-        /** Extension to ISO 19108: handle instant as a tiny period. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isEqual(self.getBeginning(), other) &&
-                   isEqual(self.getEnding(),    other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual(self.getBeginning(), other.getBeginning()) &&
-                   isEqual(self.getEnding(),    other.getEnding());
-        }
-    }
-
-
-    /**
-     * The {@code "Before"} {@literal (<)} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self     < other}</li>
-     *   <li>{@literal self.end < other}</li>
-     *   <li>{@literal self.end < other.begin}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
-     */
-    static final class Before<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -3422629447456003982L;
-
-        /** Creates a new filter. */
-        Before(Expression<T,?> expression1,
-               Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Before<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.BEFORE;
-        }
-
-        /** Symbol of this operation. */
-        @Override protected char symbol() {
-            return '<';
-        }
-
-        /** Condition defined by ISO 19108:2002 §5.2.3.5. */
-        @Override protected boolean evaluate(final Instant self, final Instant 
other) {
-            return self.isBefore(other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isBefore(self.getEnding(), other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isBefore(self.getEnding(), other.getBeginning());
-        }
-    }
-
-
-    /**
-     * The {@code "After"} {@literal (>)} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self       > other}</li>
-     *   <li>{@literal self.begin > other}</li>
-     *   <li>{@literal self.begin > other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
-     */
-    static final class After<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 5410476260417497682L;
-
-        /** Creates a new filter. */
-        After(Expression<T,?> expression1,
-              Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new After<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.AFTER;
-        }
-
-        /** Symbol of this operation. */
-        @Override protected char symbol() {
-            return '>';
-        }
-
-        /** Condition defined by ISO 19108:2002 §5.2.3.5. */
-        @Override protected boolean evaluate(final Instant self, final Instant 
other) {
-            return self.isAfter(other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isAfter(self.getBeginning(), other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isAfter(self.getBeginning(), other.getEnding());
-        }
-    }
-
-
-    /**
-     * The {@code "Begins"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin = other.begin  AND  self.end < 
other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
-     */
-    static final class Begins<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -7880699329127762233L;
-
-        /** Creates a new filter. */
-        Begins(Expression<T,?> expression1,
-               Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Begins<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.BEGINS;
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual (self.getBeginning(), other.getBeginning()) &&
-                   isBefore(self.getEnding(),    other.getEnding());
-        }
+        /*
+         * We cannot use a more specific type here because the `find(…)` 
method argument is parameterized
+         * with `<? extends T>` while its return value is parameterized with 
`<? super T>`. Java language
+         * has no parameterized type that can express those conflicting 
covariance and contra-variance,
+         * therefore we must use `<?>`.
+         *
+         * Creations of `TemporalFilter` instances below are safe because 
`TimeMethods.type` is a parent
+         * of both `expression1` and `expression2` value types (verified by 
assertions). Therefore, with
+         * `commonType` of type `Class<T>` no matter if <T> is a super-type or 
a sub-type of <V>, we can
+         * assert that the parmeterized type of the two expressions is `<? 
extends T>`.
+         */
+        final TemporalOperation<?> operation = 
factory.create(TimeMethods.find(commonType)).unique();
+        assert operation.comparators.type.isAssignableFrom(commonType) : 
commonType;
+        assert commonType.isAssignableFrom(c1) : c1;
+        assert commonType.isAssignableFrom(c2) : c2;
+        if (Period.class.isAssignableFrom(commonType)) {
+            // Safe because `commonType` extends both Period and T.
+            return new Periods(operation, expression1, expression2);
+        }
+        if (operation.comparators.isDynamic()) {
+            return new TemporalFilter(operation, expression1, expression2);
+        }
+        return new Instants(operation, expression1, expression2);
     }
 
-
     /**
-     * The {@code "Ends"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin > other.begin  AND  self.end = 
other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
+     * Returns the class of values computed by the given expression, or {@code 
type} if unknown.
      */
-    static final class Ends<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -5508229966320563437L;
-
-        /** Creates a new filter. */
-        Ends(Expression<T,?> expression1,
-             Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Ends<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.ENDS;
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual(self.getEnding(),    other.getEnding()) &&
-                   isAfter(self.getBeginning(), other.getBeginning());
-        }
-    }
-
-
-    /**
-     * The {@code "BegunBy"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin = other}</li>
-     *   <li>{@literal self.begin = other.begin  AND  self.end > 
other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
-     */
-    static final class BegunBy<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -7212413827394364384L;
-
-        /** Creates a new filter. */
-        BegunBy(Expression<T,?> expression1,
-                Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new BegunBy<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.BEGUN_BY;
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isEqual(self.getBeginning(), other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual(self.getBeginning(), other.getBeginning()) &&
-                   isAfter(self.getEnding(),    other.getEnding());
-        }
-    }
-
-
-    /**
-     * The {@code "EndedBy"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.end = other}</li>
-     *   <li>{@literal self.begin < other.begin  AND  self.end = 
other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
-     */
-    static final class EndedBy<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 8586566103462153666L;
-
-        /** Creates a new filter. */
-        EndedBy(Expression<T,?> expression1,
-                Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new EndedBy<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.ENDED_BY;
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isEqual(self.getEnding(), other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual (self.getEnding(),    other.getEnding()) &&
-                   isBefore(self.getBeginning(), other.getBeginning());
+    @SuppressWarnings("unchecked")
+    private static <T> Class<? extends T> getValueClass(final Expression<?,? 
extends T> e, final Class<T> type) {
+        if (e instanceof FeatureExpression<?,?>) {
+            final Class<?> c = ((FeatureExpression<?, ? extends T>) 
e).getValueClass();
+            if (type.isAssignableFrom(c)) {
+                return (Class<? extends T>) c;
+            }
         }
+        return type;
     }
 
-
     /**
-     * The {@code "Meets"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.end = other.begin}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
+     * Returns an identification of this operation.
      */
-    static final class Meets<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -3534843269384858443L;
-
-        /** Creates a new filter. */
-        Meets(Expression<T,?> expression1,
-              Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Meets<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.MEETS;
-        }
-
-        /** Extension to ISO 19108: handle instant as a tiny period. */
-        @Override public boolean evaluate(final Instant self, final Instant 
other) {
-            return self.equals(other);
-        }
-
-        /** Extension to ISO 19108: handle instant as a tiny period. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isEqual(self.getEnding(), other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual(self.getEnding(), other.getBeginning());
-        }
+    @Override
+    public final TemporalOperatorName getOperatorType() {
+        return operation.getOperatorType();
     }
 
-
     /**
-     * The {@code "MetBy"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin = other.end}</li>
-     * </ul>
+     * Returns the mathematical symbol for this temporal operation.
      *
-     * @param  <T>  the type of resources used as inputs.
+     * @return the mathematical symbol, or 0 if none.
      */
-    static final class MetBy<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 5358059498707330482L;
-
-        /** Creates a new filter. */
-        MetBy(Expression<T,?> expression1,
-              Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new MetBy<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.MET_BY;
-        }
-
-        /** Extension to ISO 19108: handle instant as a tiny period. */
-        @Override public boolean evaluate(final Instant self, final Instant 
other) {
-            return self.equals(other);
-        }
-
-        /** Extension to ISO 19108: handle instant as a tiny period. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isEqual(self.getBeginning(), other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isEqual(self.getBeginning(), other.getEnding());
-        }
+    @Override
+    protected final char symbol() {
+        return operation.symbol();
     }
 
-
     /**
-     * The {@code "During"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin > other.begin  AND  self.end < 
other.end}</li>
-     * </ul>
+     * Casts an expression returning values of unknown type.
+     * This is an helper function for {@code recreate(…)} method 
implementations.
      *
-     * @param  <T>  the type of resources used as inputs.
+     * @param  effective  the expression to cast.
+     * @return an expression that can be used with this temporal filter.
+     * @throws ClassCastException if the expression cannot be casted.
      */
-    static final class During<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = -4674319635076886196L;
-
-        /** Creates a new filter. */
-        During(Expression<T,?> expression1,
-               Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new During<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.DURING;
-        }
-
-        /** Symbol of this operation. */
-        @Override protected char symbol() {
-            return '⊊';         // `self` is a proper (or strict) subset of 
`other`.
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isAfter (self.getBeginning(), other.getBeginning()) &&
-                   isBefore(self.getEnding(),    other.getEnding());
-        }
+    protected final Expression<R, ? extends T> cast(final Expression<R,?> 
effective) {
+        return effective.toValueType(operation.comparators.type);
     }
 
-
     /**
-     * The {@code "TContains"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin < other AND self.end > other}</li>
-     *   <li>{@literal self.begin < other.begin  AND  self.end > 
other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
+     * Creates a new filter of the same type but different parameters.
      */
-    static final class Contains<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 9107531246948034411L;
-
-        /** Creates a new filter. */
-        Contains(Expression<T,?> expression1,
-                 Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Contains<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.CONTAINS;
-        }
-
-        /** Symbol of this operation. */
-        @Override protected char symbol() {
-            return '⊋';         // `self` is a proper (or strict) superset of 
`other`.
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Instant 
other) {
-            return isBefore(self.getBeginning(), other) &&
-                   isAfter (self.getEnding(),    other);
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            return isBefore(self.getBeginning(), other.getBeginning()) &&
-                   isAfter (self.getEnding(),    other.getEnding());
-        }
+    @Override
+    public Filter<R> recreate(final Expression<R,?>[] effective) {
+        return new TemporalFilter<>(operation, cast(effective[0]), 
cast(effective[1]));
     }
 
-
     /**
-     * The {@code "TOverlaps"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin < other.begin  AND  self.end > other.begin  
AND  self.end < other.end}</li>
-     * </ul>
-     *
-     * @param  <T>  the type of resources used as inputs.
+     * Determines if the test(s) represented by this filter passes with the 
given operands.
+     * Values of {@link #expression1} and {@link #expression2} shall be two 
single values.
      */
-    static final class Overlaps<T> extends TemporalFilter<T> {
-        /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 1517443045593389773L;
-
-        /** Creates a new filter. */
-        Overlaps(Expression<T,?> expression1,
-                 Expression<T,?> expression2)
-        {
-            super(expression1, expression2);
-        }
-
-        /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new Overlaps<>(effective[0], effective[1]);
-        }
-
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.OVERLAPS;
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            final Instant selfBegin, selfEnd, otherBegin, otherEnd;
-            return ((otherBegin = other.getBeginning()) != null) &&
-                   ((selfBegin  = self .getBeginning()) != null) && 
selfBegin.isBefore(otherBegin) &&
-                   ((selfEnd    = self .getEnding())    != null) && selfEnd  
.isAfter (otherBegin) &&
-                   ((otherEnd   = other.getEnding())    != null) && otherEnd 
.isAfter (selfEnd);
+    @Override
+    public boolean test(final R candidate) {
+        final T left = expression1.apply(candidate);
+        if (left != null) {
+            final T right = expression2.apply(candidate);
+            if (right != null) {
+                if (left instanceof Period) {
+                    if (right instanceof Period) {
+                        return operation.evaluate((Period) left, (Period) 
right);
+                    } else {
+                        return operation.evaluate((Period) left, right);
+                    }
+                } else if (right instanceof Period) {
+                    return operation.evaluate(left, (Period) right);
+                } else {
+                    return operation.evaluate(left, right);
+                }
+            }
         }
+        return false;
     }
 
 
     /**
-     * The {@code "OverlappedBy"} filter. Defined by ISO 19108 as:
-     * <ul>
-     *   <li>{@literal self.begin > other.begin  AND  self.begin < other.end  
AND  self.end > other.end}</li>
-     * </ul>
+     * A temporal filters where both operands are ISO 19108 instants.
      *
-     * @param  <T>  the type of resources used as inputs.
+     * @param  <R>  the type of resources used as inputs.
+     * @param  <T>  the base type of temporal objects.
      */
-    static final class OverlappedBy<T> extends TemporalFilter<T> {
+    private static final class Instants<R,T> extends TemporalFilter<R,T> {
         /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 2228673820507226463L;
+        private static final long serialVersionUID = -3176521794130878518L;
 
         /** Creates a new filter. */
-        OverlappedBy(Expression<T,?> expression1,
-                     Expression<T,?> expression2)
+        Instants(TemporalOperation<T> operation,
+                 Expression<R, ? extends T> expression1,
+                 Expression<R, ? extends T> expression2)
         {
-            super(expression1, expression2);
+            super(operation, expression1, expression2);
         }
 
         /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new OverlappedBy<>(effective[0], effective[1]);
+        @Override public Filter<R> recreate(final Expression<R,?>[] effective) 
{
+            return new Instants<>(operation, cast(effective[0]), 
cast(effective[1]));
         }
 
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.OVERLAPPED_BY;
-        }
-
-        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            final Instant selfBegin, selfEnd, otherBegin, otherEnd;
-            return ((selfBegin  = self .getBeginning()) != null) &&
-                   ((otherBegin = other.getBeginning()) != null) && 
otherBegin.isBefore(selfBegin) &&
-                   ((otherEnd   = other.getEnding())    != null) && selfBegin 
.isBefore(otherEnd)  &&
-                   ((selfEnd    = self .getEnding())    != null) && selfEnd   
.isAfter (otherEnd);
+        /** Tests if this filter passes on the given resource. */
+        @Override public boolean test(final R candidate) {
+            final T left = expression1.apply(candidate);
+            if (left != null) {
+                final T right = expression2.apply(candidate);
+                if (right != null) {
+                    return operation.evaluate(left, right);
+                }
+            }
+            return false;
         }
     }
 
 
     /**
-     * The {@code "AnyInteracts"} filter.
-     * This is a shortcut for NOT (Before OR Meets OR MetBy OR After).
+     * A temporal filters where both operands are ISO 19108 periods.
      *
-     * @param  <T>  the type of resources used as inputs.
+     * @param  <R>  the type of resources used as inputs.
+     * @param  <T>  the base type of temporal objects.
      */
-    static final class AnyInteracts<T> extends TemporalFilter<T> {
+    private static final class Periods<R, T extends Period> extends 
TemporalFilter<R,T> {
         /** For cross-version compatibility during (de)serialization. */
-        private static final long serialVersionUID = 5972351564286442392L;
+        private static final long serialVersionUID = 7570449007668484459L;
 
         /** Creates a new filter. */
-        AnyInteracts(Expression<T,?> expression1,
-                     Expression<T,?> expression2)
+        Periods(TemporalOperation<T> operation,
+                Expression<R, ? extends T> expression1,
+                Expression<R, ? extends T> expression2)
         {
-            super(expression1, expression2);
+            super(operation, expression1, expression2);
         }
 
         /** Creates a new filter of the same type but different parameters. */
-        @Override public Filter<T> recreate(final Expression<T,?>[] effective) 
{
-            return new AnyInteracts<>(effective[0], effective[1]);
+        @Override public Filter<R> recreate(final Expression<R,?>[] effective) 
{
+            return new Periods<>(operation, cast(effective[0]), 
cast(effective[1]));
         }
 
-        /** Identification of this operation. */
-        @Override public TemporalOperatorName getOperatorType() {
-            return TemporalOperatorName.ANY_INTERACTS;
-        }
-
-        /** Condition defined by OGC filter specification. */
-        @Override public boolean evaluate(final Period self, final Period 
other) {
-            final Instant selfBegin, selfEnd, otherBegin, otherEnd;
-            return ((selfBegin  = self .getBeginning()) != null) &&
-                   ((otherEnd   = other.getEnding())    != null) && 
selfBegin.isBefore(otherEnd) &&
-                   ((selfEnd    = self .getEnding())    != null) &&
-                   ((otherBegin = other.getBeginning()) != null) && 
selfEnd.isAfter(otherBegin);
+        /** Tests if this filter passes on the given resource. */
+        @Override public boolean test(final R candidate) {
+            final Period left = expression1.apply(candidate);
+            if (left != null) {
+                final Period right = expression2.apply(candidate);
+                if (right != null) {
+                    return operation.evaluate(left, right);
+                }
+            }
+            return false;
         }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
new file mode 100644
index 0000000000..d826f84485
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
@@ -0,0 +1,762 @@
+/*
+ * 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.filter;
+
+import java.io.Serializable;
+import java.time.temporal.Temporal;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.privy.Strings;
+import org.apache.sis.util.collection.WeakHashSet;
+import static org.apache.sis.filter.TimeMethods.BEFORE;
+import static org.apache.sis.filter.TimeMethods.AFTER;
+import static org.apache.sis.filter.TimeMethods.EQUAL;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.temporal.Period;
+import org.opengis.filter.TemporalOperatorName;
+
+
+/**
+ * Temporal operations between periods and/or instants.
+ * The nature of the operation depends on the subclass.
+ * Subclasses shall override at least one of the following methods:
+ *
+ * <ul>
+ *   <li>{@link #evaluate(T, T)}</li>
+ *   <li>{@link #evaluate(T, Period)}</li>
+ *   <li>{@link #evaluate(Period, T)}</li>
+ *   <li>{@link #evaluate(Period, Period)}</li>
+ * </ul>
+ *
+ * Instances of this classes are immutable and thread-safe.
+ * The same instances are shared by many filters.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @param  <T>  the base type of temporal objects, or {@code Object.class} for 
any type.
+ */
+abstract class TemporalOperation<T> implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -5304814915639460679L;
+
+    /**
+     * Temporal operations created in the current JVM.
+     */
+    @SuppressWarnings("unchecked")
+    private static final WeakHashSet<TemporalOperation<?>> POOL = new 
WeakHashSet<>((Class) TemporalOperation.class);
+
+    /**
+     * The set of methods to invoke for performing "is before", "is after" or 
"is equal" comparisons.
+     */
+    protected final TimeMethods<T> comparators;
+
+    /**
+     * Creates a new temporal operation.
+     *
+     * @param  comparators  the set of methods to invoke for performing 
comparisons.
+     */
+    protected TemporalOperation(final TimeMethods<T> comparators) {
+        this.comparators = comparators;
+    }
+
+    /**
+     * Returns a unique, shared instance of this operation.
+     *
+     * @return a unique instance equal to {@code this}.
+     */
+    public final TemporalOperation<T> unique() {
+        return POOL.unique(this);
+    }
+
+    /**
+     * Returns a hash code value for this operation.
+     * Used for {@link #unique()} implementation.
+     */
+    @Override
+    public final int hashCode() {
+        return getClass().hashCode() + 31 * comparators.type.hashCode();
+    }
+
+    /**
+     * Compares the given object with this operation for equality.
+     * Used for {@link #unique()} implementation.
+     */
+    @Override
+    public final boolean equals(final Object other) {
+        return (other != null) && (other.getClass() == getClass()) &&
+                ((TemporalOperation) other).comparators.type == 
comparators.type;
+    }
+
+    /**
+     * Returns a string representation for debugging purposes.
+     */
+    @Override
+    public final String toString() {
+        return Strings.toString(getClass(),
+                "operator", getOperatorType().identifier(),
+                "type", comparators.type.getSimpleName());
+    }
+
+    /**
+     * Returns an identification of this operation.
+     */
+    public abstract TemporalOperatorName getOperatorType();
+
+    /**
+     * Returns the mathematical symbol for this temporal operation.
+     *
+     * @return the mathematical symbol, or 0 if none.
+     */
+    protected char symbol() {
+        return (char) 0;
+    }
+
+    /**
+     * Evaluates the filter between two temporal objects.
+     * Both arguments given to this method shall be non-null.
+     */
+    protected boolean evaluate(T self, T other) {
+        return false;
+    }
+
+    /**
+     * Evaluates the filter between a temporal object and a period.
+     * Both arguments given to this method shall be non-null, but period begin 
or end instant may be null.
+     *
+     * <p><b>Note:</b> this relationship is not defined by ISO 19108. This 
method should be overridden
+     * only when an ISO 19108 extension can be easily defined, for example for 
the "equal" operation.</p>
+     */
+    protected boolean evaluate(T self, Period other) {
+        return false;
+    }
+
+    /**
+     * Evaluates the filter between a period and a temporal object.
+     * Both arguments given to this method shall be non-null, but period begin 
or end instant may be null.
+     * Note: the {@code self} and {@code other} argument names are chosen to 
match ISO 19108 tables.
+     */
+    protected boolean evaluate(Period self, T other) {
+        return false;
+    }
+
+    /**
+     * Evaluates the filter between two periods.
+     * Both arguments given to this method shall be non-null, but period begin 
or end instant may be null.
+     * Note: the {@code self} and {@code other} argument names are chosen to 
match ISO 19108 tables.
+     */
+    protected abstract boolean evaluate(Period self, Period other);
+
+    /**
+     * Returns {@code true} if {@code other} is non-null and the specified 
comparison evaluates to {@code true}.
+     * This is a helper function for {@code evaluate(…)} methods 
implementations.
+     *
+     * @param  test   enumeration value such as {@link TimeMethods#BEFORE} or 
{@link TimeMethods#AFTER}.
+     * @param  self   the object on which to invoke the method identified by 
{@code test}.
+     * @param  other  the argument to give to the test method call, or {@code 
null} if none.
+     * @return the result of performing the comparison identified by {@code 
test}.
+     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     */
+    final boolean compare(final int test, final T self, final Temporal other) {
+        return (other != null) && comparators.compare(test, self, other);
+    }
+
+    /**
+     * Returns {@code true} if both arguments are non-null and the specified 
comparison evaluates to {@code true}.
+     * This is a helper function for {@code evaluate(…)} methods 
implementations.
+     *
+     * @param  test   enumeration value such as {@link TimeMethods#BEFORE} or 
{@link TimeMethods#AFTER}.
+     * @param  self   the object on which to invoke the method identified by 
{@code test}, or {@code null} if none.
+     * @param  other  the argument to give to the test method call, or {@code 
null} if none.
+     * @return the result of performing the comparison identified by {@code 
test}.
+     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     */
+    @SuppressWarnings("unchecked")
+    static boolean compare(final int test, final Temporal self, final Temporal 
other) {
+        return (self != null) && (other != null) && TimeMethods.compare(test,
+                (Class) Classes.findCommonClass(self.getClass(), 
other.getClass()), self, other);
+    }
+
+
+    /**
+     * Reference to a sub-class constructor.
+     *
+     * @param <T> type of temporal object.
+     */
+    @FunctionalInterface
+    static interface Factory {
+        /**
+         * Creates a new temporal operation.
+         *
+         * @param  <T>          type of temporal objects that the operation 
will accept.
+         * @param  comparators  the set of methods to invoke for performing 
comparisons.
+         * @return the temporal operation using the given "is before", "is 
after" and "is equal" methods.
+         */
+        <T> TemporalOperation<T> create(TimeMethods<T> comparators);
+    }
+
+
+    /**
+     * The {@code "TEquals"} (=) operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self = other}</li>
+     *   <li>{@literal self.begin = other.begin  AND  self.end = 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Equals<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -6060822291802339424L;
+
+        /** Creates a new operation. */
+        Equals(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.EQUALS;
+        }
+
+        /** Symbol of this operation. */
+        @Override protected char symbol() {
+            return '=';
+        }
+
+        /** Condition defined by ISO 19108:2002 §5.2.3.5. */
+        @Override protected boolean evaluate(T self, T other) {
+            return comparators.isEqual.test(self, other);
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(T self, Period other) {
+            return compare(EQUAL, self, other.getBeginning()) &&
+                   compare(EQUAL, self, other.getEnding());
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(Period self, T other) {
+            return compare(EQUAL, other, self.getBeginning()) &&
+                   compare(EQUAL, other, self.getEnding());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(Period self, Period other) {
+            return compare(EQUAL, self.getBeginning(), other.getBeginning()) &&
+                   compare(EQUAL, self.getEnding(),    other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "Before"} {@literal (<)} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self     < other}</li>
+     *   <li>{@literal self.end < other}</li>
+     *   <li>{@literal self.end < other.begin}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Before<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -3422629447456003982L;
+
+        /** Creates a new operation. */
+        Before(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.BEFORE;
+        }
+
+        /** Symbol of this operation. */
+        @Override protected char symbol() {
+            return '<';
+        }
+
+        /** Condition defined by ISO 19108:2002 §5.2.3.5. */
+        @Override protected boolean evaluate(T self, T other) {
+            return comparators.isBefore.test(self, other);
+        }
+
+        /** Relationship not defined by ISO 19108:2006. */
+        @Override public boolean evaluate(T self, Period other) {
+            return compare(BEFORE, self, other.getBeginning());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(Period self, T other) {
+            return compare(AFTER, other, self.getEnding());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(Period self, Period other) {
+            return compare(BEFORE, self.getEnding(), other.getBeginning());
+        }
+    }
+
+
+    /**
+     * The {@code "After"} {@literal (>)} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self       > other}</li>
+     *   <li>{@literal self.begin > other}</li>
+     *   <li>{@literal self.begin > other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class After<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 5410476260417497682L;
+
+        /** Creates a new operation. */
+        After(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.AFTER;
+        }
+
+        /** Symbol of this operation. */
+        @Override protected char symbol() {
+            return '>';
+        }
+
+        /** Condition defined by ISO 19108:2002 §5.2.3.5. */
+        @Override protected boolean evaluate(T self, T other) {
+            return comparators.isAfter.test(self, other);
+        }
+
+        /** Relationship not defined by ISO 19108:2006. */
+        @Override public boolean evaluate(T self, Period other) {
+            return compare(AFTER, self, other.getEnding());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(Period self, T other) {
+            return compare(BEFORE, other, self.getBeginning());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(Period self, Period other) {
+            return compare(AFTER, self.getBeginning(), other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "Begins"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin = other.begin  AND  self.end < 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Begins<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -7880699329127762233L;
+
+        /** Creates a new operation. */
+        Begins(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.BEGINS;
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(EQUAL,  self.getBeginning(), other.getBeginning()) 
&&
+                   compare(BEFORE, self.getEnding(),    other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "Ends"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin > other.begin  AND  self.end = 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Ends<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -5508229966320563437L;
+
+        /** Creates a new operation. */
+        Ends(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.ENDS;
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(EQUAL, self.getEnding(),    other.getEnding()) &&
+                   compare(AFTER, self.getBeginning(), other.getBeginning());
+        }
+    }
+
+
+    /**
+     * The {@code "BegunBy"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin = other}</li>
+     *   <li>{@literal self.begin = other.begin  AND  self.end > 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class BegunBy<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -7212413827394364384L;
+
+        /** Creates a new operation. */
+        BegunBy(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.BEGUN_BY;
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(Period self, T other) {
+            return compare(EQUAL, other, self.getBeginning());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(EQUAL, self.getBeginning(), other.getBeginning()) &&
+                   compare(AFTER, self.getEnding(),    other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "EndedBy"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.end = other}</li>
+     *   <li>{@literal self.begin < other.begin  AND  self.end = 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class EndedBy<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 8586566103462153666L;
+
+        /** Creates a new operation. */
+        EndedBy(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.ENDED_BY;
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final T other) {
+            return compare(EQUAL, other, self.getEnding());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(EQUAL,  self.getEnding(),    other.getEnding()) &&
+                   compare(BEFORE, self.getBeginning(), other.getBeginning());
+        }
+    }
+
+
+    /**
+     * The {@code "Meets"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.end = other.begin}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Meets<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -3534843269384858443L;
+
+        /** Creates a new operation. */
+        Meets(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.MEETS;
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(final T self, final T other) {
+            return comparators.isEqual.test(self, other);
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(final T self, final Period other) {
+            return compare(EQUAL, self, other.getBeginning());
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(final Period self, final T other) {
+            return compare(EQUAL, other, self.getEnding());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(EQUAL, self.getEnding(), other.getBeginning());
+        }
+    }
+
+
+    /**
+     * The {@code "MetBy"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin = other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class MetBy<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 5358059498707330482L;
+
+        /** Creates a new operation. */
+        MetBy(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.MET_BY;
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(final T self, final T other) {
+            return comparators.isEqual.test(self, other);
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(final T self, final Period other) {
+            return compare(EQUAL, self, other.getEnding());
+        }
+
+        /** Extension to ISO 19108: handle instant as a tiny period. */
+        @Override public boolean evaluate(final Period self, final T other) {
+            return compare(EQUAL, other, self.getBeginning());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(EQUAL, self.getBeginning(), other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "During"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin > other.begin  AND  self.end < 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class During<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = -4674319635076886196L;
+
+        /** Creates a new operation. */
+        During(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.DURING;
+        }
+
+        /** Symbol of this operation. */
+        @Override protected char symbol() {
+            return '⊊';         // `self` is a proper (or strict) subset of 
`other`.
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(AFTER,  self.getBeginning(), other.getBeginning()) 
&&
+                   compare(BEFORE, self.getEnding(),    other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "TContains"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin < other AND self.end > other}</li>
+     *   <li>{@literal self.begin < other.begin  AND  self.end > 
other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Contains<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 9107531246948034411L;
+
+        /** Creates a new operation. */
+        Contains(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.CONTAINS;
+        }
+
+        /** Symbol of this operation. */
+        @Override protected char symbol() {
+            return '⊋';         // `self` is a proper (or strict) superset of 
`other`.
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final T other) {
+            return compare(AFTER,  other, self.getBeginning()) &&
+                   compare(BEFORE, other, self.getEnding());
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            return compare(BEFORE, self.getBeginning(), other.getBeginning()) 
&&
+                   compare(AFTER,  self.getEnding(),    other.getEnding());
+        }
+    }
+
+
+    /**
+     * The {@code "TOverlaps"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin < other.begin  AND  self.end > other.begin  
AND  self.end < other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class Overlaps<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 1517443045593389773L;
+
+        /** Creates a new operation. */
+        Overlaps(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.OVERLAPS;
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            final Temporal selfBegin, selfEnd, otherBegin, otherEnd;
+            return ((otherBegin = other.getBeginning()) != null) &&
+                   ((selfBegin  = self .getBeginning()) != null) && 
compare(BEFORE, selfBegin, otherBegin) &&
+                   ((selfEnd    = self .getEnding())    != null) && 
compare(AFTER,  selfEnd,   otherBegin) &&
+                   ((otherEnd   = other.getEnding())    != null) && 
compare(BEFORE, selfEnd,   otherEnd);
+        }
+    }
+
+
+    /**
+     * The {@code "OverlappedBy"} operation. Defined by ISO 19108 as:
+     * <ul>
+     *   <li>{@literal self.begin > other.begin  AND  self.begin < other.end  
AND  self.end > other.end}</li>
+     * </ul>
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class OverlappedBy<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 2228673820507226463L;
+
+        /** Creates a new operation. */
+        OverlappedBy(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.OVERLAPPED_BY;
+        }
+
+        /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            final Temporal selfBegin, selfEnd, otherBegin, otherEnd;
+            return ((selfBegin  = self .getBeginning()) != null) &&
+                   ((otherBegin = other.getBeginning()) != null) && 
compare(AFTER,  selfBegin,  otherBegin) &&
+                   ((otherEnd   = other.getEnding())    != null) && 
compare(BEFORE, selfBegin,  otherEnd)   &&
+                   ((selfEnd    = self .getEnding())    != null) && 
compare(AFTER,  selfEnd,    otherEnd);
+        }
+    }
+
+
+    /**
+     * The {@code "AnyInteracts"} filter.
+     * This is a shortcut for NOT (Before OR Meets OR MetBy OR After).
+     *
+     * @param  <T>  the base type of temporal objects.
+     */
+    static final class AnyInteracts<T> extends TemporalOperation<T> {
+        /** For cross-version compatibility during (de)serialization. */
+        private static final long serialVersionUID = 5972351564286442392L;
+
+        /** Creates a new operation. */
+        AnyInteracts(TimeMethods<T> comparators) {
+            super(comparators);
+        }
+
+        /** Identification of this operation. */
+        @Override public TemporalOperatorName getOperatorType() {
+            return TemporalOperatorName.ANY_INTERACTS;
+        }
+
+        /** Condition defined by OGC filter specification. */
+        @Override public boolean evaluate(final Period self, final Period 
other) {
+            final Temporal selfBegin, selfEnd, otherBegin, otherEnd;
+            return ((selfBegin  = self .getBeginning()) != null) &&
+                   ((otherEnd   = other.getEnding())    != null) && 
compare(BEFORE, selfBegin, otherEnd) &&
+                   ((selfEnd    = self .getEnding())    != null) &&
+                   ((otherBegin = other.getBeginning()) != null) && 
compare(AFTER, selfEnd, otherBegin);
+        }
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TimeMethods.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TimeMethods.java
new file mode 100644
index 0000000000..11cc0ca6b7
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TimeMethods.java
@@ -0,0 +1,396 @@
+/*
+ * 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.filter;
+
+import java.util.Map;
+import java.util.Date;
+import java.time.Instant;
+import java.time.Year;
+import java.time.YearMonth;
+import java.time.MonthDay;
+import java.time.LocalTime;
+import java.time.OffsetTime;
+import java.time.OffsetDateTime;
+import java.time.DateTimeException;
+import java.time.chrono.ChronoLocalDate;
+import java.time.chrono.ChronoLocalDateTime;
+import java.time.chrono.ChronoZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAccessor;
+import java.util.function.BiPredicate;
+import java.lang.reflect.Modifier;
+import java.io.Serializable;
+import java.io.ObjectStreamException;
+import org.apache.sis.util.privy.Strings;
+import org.apache.sis.util.resources.Errors;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.InvalidFilterValueException;
+
+
+/**
+ * Provides the <i>is before</i> and <i>is after</i> operations for various 
{@code java.time} objects.
+ * This class delegates to the {@code isBefore(T)} or {@code isAfter(T)} 
methods of each supported classes.
+ *
+ * Instances of this classes are immutable and thread-safe.
+ * The same instance can be shared by many {@link TemporalOperation} instances.
+ *
+ * <h2>Design note about alternative approaches</h2>
+ * We do not delegate to {@link Comparable#compareTo(Object)} because the 
latter method compares not only
+ * positions on the timeline, but also other properties not relevant to an "is 
before" or "is after" test.
+ * We could use {@link ChronoLocalDate#timeLineOrder()} comparators instead, 
but those comparators are not
+ * defined for every classes where the "is before" and "is after" methods 
differ from "compare to" method.
+ * Furthermore, some temporal classes override {@code isBefore(T)} or {@code 
isAfter(T)} for performance.
+ *
+ * @param  <T>  the base type of temporal objects, or {@code Object.class} for 
any type.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+class TimeMethods<T> implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 1075289362575825939L;
+
+    /**
+     * The type of temporal objects accepted by this set of operations.
+     */
+    public final Class<T> type;
+
+    /**
+     * Enumeration values for a test to apply.
+     *
+     * @see #compare(int, T, TemporalAccessor)
+     */
+    static final int BEFORE=1, AFTER=2, EQUAL=0;
+
+    /**
+     * Predicate to execute for testing the ordering between temporal objects.
+     * This comparison operator differs from the {@code compareTo(…)} method 
in that it compares only the
+     * positions on the timeline, ignoring metadata such as the calendar used 
for representing positions.
+     */
+    public final transient BiPredicate<T,T> isBefore, isAfter, isEqual;
+
+    /**
+     * Creates a new set of operators. This method is for subclasses only.
+     * For getting a {@code TimeMethods} instance, see {@link #find(Class)}.
+     */
+    private TimeMethods(final Class<T> type,
+            final BiPredicate<T,T> isBefore,
+            final BiPredicate<T,T> isAfter,
+            final BiPredicate<T,T> isEqual)
+    {
+        this.type     = type;
+        this.isBefore = isBefore;
+        this.isAfter  = isAfter;
+        this.isEqual  = isEqual;
+    }
+
+    /**
+     * Returns whether the end point will be determined dynamically every time 
that a method is invoked.
+     *
+     * @return whether the methods are determined dynamically on an 
instance-by-instance basis.
+     */
+    public boolean isDynamic() {
+        return false;
+    }
+
+    /**
+     * Delegates the comparison to the method identified by the {@code test} 
argument.
+     * This method is overridden in subclasses where the delegation can be 
more direct.
+     *
+     * @param  test   {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}.
+     * @param  self   the object on which to invoke the method identified by 
{@code test}.
+     * @param  other  the argument to give to the test method call.
+     * @return the result of performing the comparison identified by {@code 
test}.
+     */
+    boolean delegate(final int test, final T self, final T other) {
+        final BiPredicate<T,T> p;
+        switch (test) {
+            case BEFORE: p = isBefore; break;
+            case AFTER:  p = isAfter;  break;
+            case EQUAL:  p = isEqual;  break;
+            default: throw new AssertionError(test);
+        }
+        return p.test(self, other);
+    }
+
+    /**
+     * Compares an object of class {@code <T>} with a temporal object of 
unknown class.
+     * The other object is typically the beginning or ending of a period.
+     *
+     * @param  test   {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}.
+     * @param  self   the object on which to invoke the method identified by 
{@code test}.
+     * @param  other  the argument to give to the test method call.
+     * @return the result of performing the comparison identified by {@code 
test}.
+     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     */
+    @SuppressWarnings("unchecked")
+    final boolean compare(final int test, final T self, final TemporalAccessor 
other) {
+        if (type.isInstance(other)) {
+            return delegate(test, self, (T) other);         // Safe because of 
above `isInstance(…)` check.
+        }
+        return compareAsInstants(test, accessor(self), other);
+    }
+
+    /**
+     * Compares two temporal objects of unknown class. This method needs to 
check for specialized implementations
+     * before to delegate to {@link Comparable#compareTo(Object)}, because the 
comparison methods on the timeline
+     * are not always the same as {@code compareTo(…)}.
+     *
+     * @param  <T>    base class of the objects to compare.
+     * @param  test   {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}.
+     * @param  type   base class of the {@code self} and {@code other} 
arguments.
+     * @param  self   the object on which to invoke the method identified by 
{@code test}.
+     * @param  other  the argument to give to the test method call.
+     * @return the result of performing the comparison identified by {@code 
test}.
+     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     */
+    static <T> boolean compare(final int test, final Class<T> type, final T 
self, final T other) {
+        /*
+         * The following cast is not strictly true, it should be `<? extends 
T>`.
+         * However, because of the `isInstance(…)` check and because <T> is 
used
+         * only as parameter type (no collection), it is okay to use it that 
way.
+         */
+        final TimeMethods<? super T> tc = findSpecialized(type);
+        if (tc != null) {
+            /*
+             * Found one of the special cases listed in `INTERFACES` or 
`FINAL_TYPE`.
+             * If the other type is compatible, the comparison is executed 
directly.
+             * Note: the `switch` statement is equivalent to `tc.compare(test, 
…)`,
+             * but is inlined because that method is never overridden in this 
context.
+             */
+            if (tc.type.isInstance(other)) {
+                assert tc.type.isAssignableFrom(type) : tc;     // Those types 
are not necessarily equal.
+                final BiPredicate<? super T, ? super T> p;
+                switch (test) {
+                    case BEFORE: p = tc.isBefore; break;
+                    case AFTER:  p = tc.isAfter;  break;
+                    case EQUAL:  p = tc.isEqual;  break;
+                    default: throw new AssertionError(test);
+                }
+                return p.test(self, other);
+            }
+        } else if (self instanceof Comparable<?> && type.isInstance(other)) {
+            /*
+             * The type of the first operand is not a special case, but the 
second operand is compatible
+             * for a call to the generic `compareTo(…)` method. This case does 
not happen often, because
+             * not many `java.time` classes have no "is before" or "is after" 
operations.
+             * Some examples are `Month` and `DayOfWeek`.
+             */
+            @SuppressWarnings("unchecked")          // Safe because 
verification done by `isInstance(…)`.
+            final int c = ((Comparable) self).compareTo(other);
+            switch (test) {
+                case BEFORE: return c <  0;
+                case AFTER:  return c >  0;
+                case EQUAL:  return c == 0;
+                default: throw new AssertionError(test);
+            }
+        }
+        /*
+         * If we reach this point, the two operands are of different classes 
and we cannot compare them directly.
+         * Try to compare the two operands as instants on the timeline.
+         */
+        return compareAsInstants(test, accessor(self), accessor(other));
+    }
+
+    /**
+     * Returns the given object as a temporal accessor.
+     */
+    private static TemporalAccessor accessor(final Object value) {
+        if (value instanceof TemporalAccessor) {
+            return (TemporalAccessor) value;
+        } else if (value instanceof Date) {
+            return ((Date) value).toInstant();      // Overridden in `Date` 
subclasses.
+        } else {
+            throw new InvalidFilterValueException(Errors.format(
+                    Errors.Keys.CannotCompareInstanceOf_2, value.getClass(), 
TemporalAccessor.class));
+        }
+    }
+
+    /**
+     * Compares two temporal objects as instants.
+     * This is a last-resort fallback, when objects cannot be compared by 
their own methods.
+     *
+     * @param  test   {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}.
+     * @param  self   the object on which to invoke the method identified by 
{@code test}.
+     * @param  other  the argument to give to the test method call.
+     * @return the result of performing the comparison identified by {@code 
test}.
+     * @throws InvalidFilterValueException if the two objects cannot be 
compared.
+     */
+    private static boolean compareAsInstants(final int test, final 
TemporalAccessor self, final TemporalAccessor other) {
+        try {
+            long t1 =  self.getLong(ChronoField.INSTANT_SECONDS);
+            long t2 = other.getLong(ChronoField.INSTANT_SECONDS);
+            if (t1 == t2) {
+                t1 =  self.getLong(ChronoField.NANO_OF_SECOND);     // Should 
be present according Javadoc.
+                t2 = other.getLong(ChronoField.NANO_OF_SECOND);
+                if (t1 == t2) {
+                    return test == EQUAL;
+                }
+            }
+            return test == ((t1 < t2) ? BEFORE : AFTER);
+        } catch (DateTimeException | ArithmeticException e) {
+            throw new InvalidFilterValueException(Errors.format(
+                    Errors.Keys.CannotCompareInstanceOf_2, self.getClass(), 
other.getClass()), e);
+        }
+    }
+
+    /**
+     * Returns the set of methods that can be invoked on instances of the 
given type, or {@code null} if none.
+     * This method returns only one of the methods defined in {@link 
#FINAL_TYPES} or {@link #INTERFACES}.
+     * It shall not try to create fallbacks.
+     *
+     * @param  <T>   compile-time value of the {@code type} argument.
+     * @param  type  the type of temporal object for which to get specialized 
methods.
+     * @return set of specialized methods for the given object type, or {@code 
null} if none.
+     */
+    @SuppressWarnings("unchecked")
+    private static <T> TimeMethods<? super T> findSpecialized(final Class<T> 
type) {
+        {   // Block for keeping `tc` in local scope.
+            TimeMethods<?> tc = FINAL_TYPES.get(type);
+            if (tc != null) {
+                assert tc.type == type : tc;
+                return (TimeMethods<T>) tc;             // Safe because of 
`==` checks.
+            }
+        }
+        for (TimeMethods<?> tc : INTERFACES) {
+            if (tc.type.isAssignableFrom(type)) {
+                return (TimeMethods<? super T>) tc;     // Safe because of 
`isAssignableFrom(…)` checks.
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the set of methods that can be invoked on instances of the 
given type.
+     *
+     * @param  <T>   compile-time value of the {@code type} argument.
+     * @param  type  the type of temporal object for which to get specialized 
methods.
+     * @return set of comparison methods for the given object type.
+     */
+    @SuppressWarnings("unchecked")          // For (Comparable) casts.
+    public static <T> TimeMethods<? super T> find(final Class<T> type) {
+        final TimeMethods<? super T> tc = findSpecialized(type);
+        if (tc != null) {
+            return tc;
+        }
+        if (Modifier.isFinal(type.getModifiers())) {
+            if (Comparable.class.isAssignableFrom(type)) {
+                return new TimeMethods<>(type,
+                        (self, other) -> ((Comparable) self).compareTo(other) 
< 0,
+                        (self, other) -> ((Comparable) self).compareTo(other) 
> 0,
+                        (self, other) -> ((Comparable) self).compareTo(other) 
== 0);
+            } else {
+                throw new 
InvalidFilterValueException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2,
 type, type));
+            }
+        } else {
+            return fallback(type);
+        }
+    }
+
+    /**
+     * Returns the last-resort fallback when the type of temporal objects 
cannot be determined in advance.
+     *
+     * @param  <T>   compile-time value of the {@code type} argument.
+     * @param  type  the type of temporal object for which to get the 
last-resource fallback methods.
+     * @return set of last-resort comparison methods for the given object type.
+     */
+    private static <T> TimeMethods<? super T> fallback(final Class<T> type) {
+        return new TimeMethods<>(type,
+                (self, other) -> compare(BEFORE, type, self, other),
+                (self, other) -> compare(AFTER,  type, self, other),
+                (self, other) -> compare(EQUAL,  type, self, other))
+        {
+            @Override public boolean isDynamic() {
+                return true;
+            }
+            @Override boolean delegate(final int test, final T self, final T 
other) {
+                return compare(test, type, self, other);
+            }
+        };
+    }
+
+    /**
+     * Returns the unique instance for the type after deserialization.
+     * This is needed for avoiding to serialize the lambda functions.
+     *
+     * @return the object to use after deserialization.
+     * @throws ObjectStreamException if the serialized object contains invalid 
data.
+     */
+    private Object readResolve() throws ObjectStreamException {
+        return find(type);
+    }
+
+    /**
+     * Operators for all supported temporal types that are interfaces or 
non-final classes.
+     * Those types need to be checked with {@link 
Class#isAssignableFrom(Class)} in iteration order.
+     */
+    @SuppressWarnings({"rawtypes", "unchecked"})            // For `Chrono*` 
interfaces, because they are parameterized.
+    private static final TimeMethods<?>[] INTERFACES = {
+        new TimeMethods<>(ChronoZonedDateTime.class, 
ChronoZonedDateTime::isBefore, ChronoZonedDateTime::isAfter, 
ChronoZonedDateTime::isEqual),
+        new TimeMethods<>(ChronoLocalDateTime.class, 
ChronoLocalDateTime::isBefore, ChronoLocalDateTime::isAfter, 
ChronoLocalDateTime::isEqual),
+        new TimeMethods<>(    ChronoLocalDate.class,     
ChronoLocalDate::isBefore,     ChronoLocalDate::isAfter,     
ChronoLocalDate::isEqual),
+        new TimeMethods<>(               Date.class,                Date::  
before,                Date::  after,                Date::equals)
+    };
+
+    /*
+     * No operation on numbers for now. We could revisit this policy in a 
future version if we
+     * allow the temporal function to have a CRS and to operate on temporal 
coordinate values.
+     */
+
+    /**
+     * Operators for all supported temporal types for which there is no need 
to check for subclasses.
+     * Those classes are usually final, except when wanting to intentionally 
ignore all subclasses.
+     * Those types should be tested before {@link #INTERFACES} because this 
check is quick.
+     *
+     * <h4>Implementation note</h4>
+     * {@link Year}, {@link YearMonth}, {@link MonthDay}, {@link LocalTime} 
and {@link Instant}
+     * could be replaced by {@link Comparable}. We nevertheless keep the 
specialized classes in
+     * case the implementations change in the future, and also for performance 
reason, because
+     * the code working on generic {@link Comparable} needs to check for 
special cases again.
+     */
+    private static final Map<Class<?>, TimeMethods<?>> FINAL_TYPES = 
Map.ofEntries(
+        entry(new TimeMethods<>(OffsetDateTime.class, 
OffsetDateTime::isBefore, OffsetDateTime::isAfter, OffsetDateTime::isEqual)),
+        entry(new TimeMethods<>(    OffsetTime.class,     
OffsetTime::isBefore,     OffsetTime::isAfter,     OffsetTime::isEqual)),
+        entry(new TimeMethods<>(     LocalTime.class,      
LocalTime::isBefore,      LocalTime::isAfter,      LocalTime::equals)),
+        entry(new TimeMethods<>(          Year.class,           
Year::isBefore,           Year::isAfter,           Year::equals)),
+        entry(new TimeMethods<>(     YearMonth.class,      
YearMonth::isBefore,      YearMonth::isAfter,      YearMonth::equals)),
+        entry(new TimeMethods<>(      MonthDay.class,       
MonthDay::isBefore,       MonthDay::isAfter,       MonthDay::equals)),
+        entry(new TimeMethods<>(       Instant.class,        
Instant::isBefore,        Instant::isAfter,        Instant::equals)),
+        entry(fallback(Temporal.class)),    // Frequently declared type. 
Intentionally no "instance of" checks.
+        entry(fallback(Object.class)));     // Not a final class, but to be 
used when the declared type is Object.
+
+    /**
+     * Helper method for adding entries to the {@link #FINAL_TYPES} map.
+     * Shall be used only for final classes.
+     */
+    private static Map.Entry<Class<?>, TimeMethods<?>> entry(final 
TimeMethods<?> op) {
+        return Map.entry(op.type, op);
+    }
+
+    /**
+     * Returns a string representation of this set of operations for debugging 
purposes.
+     *
+     * @return a string representation for debugging purposes.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(TimeMethods.class, "type", 
type.getSimpleName());
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
index 249639181b..b349e62672 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/TemporalFilterTest.java
@@ -16,16 +16,18 @@
  */
 package org.apache.sis.filter;
 
+import java.time.Instant;
+import org.apache.sis.geometry.WraparoundMethod;
 import static org.apache.sis.util.privy.Constants.MILLISECONDS_PER_DAY;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
-import org.apache.sis.test.TestUtilities;
 import static org.apache.sis.test.Assertions.assertSerializedEquals;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.temporal.Period;
 import org.opengis.feature.Feature;
 import org.opengis.filter.FilterFactory;
 import org.opengis.filter.TemporalOperator;
@@ -42,7 +44,7 @@ public final class TemporalFilterTest extends TestCase {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,Object> factory;
+    private FilterFactory<Feature, Object, ? super Period> factory;
 
     /**
      * The filter to test. This field shall be assigned by each {@code 
testFoo()} method by invoking
@@ -63,8 +65,8 @@ public final class TemporalFilterTest extends TestCase {
         factory = DefaultFilterFactory.forFeatures();
         expression1 = new PeriodLiteral();
         expression2 = new PeriodLiteral();
-        expression1.begin = expression2.begin = TestUtilities.date("2000-01-01 
09:00:00").getTime();
-        expression1.end   = expression2.end   = TestUtilities.date("2000-01-05 
10:00:00").getTime();
+        expression1.begin = expression2.begin = 
Instant.parse("2000-01-01T09:00:00Z").toEpochMilli();
+        expression1.end   = expression2.end   = 
Instant.parse("2000-01-05T10:00:00Z").toEpochMilli();
     }
 
     /**
@@ -126,6 +128,8 @@ public final class TemporalFilterTest extends TestCase {
 
     /**
      * Tests "After" (construction, evaluation, serialization, equality).
+     *
+     * @see #testOnPeriods()
      */
     @Test
     public void testAfter() {
@@ -344,4 +348,15 @@ public final class TemporalFilterTest extends TestCase {
         expression1.end += 2;
         assertTrue(evaluate());
     }
+
+    /**
+     * Re-test an arbitrary operation, but using a different factory.
+     * This test uses a factory specialized for the {@link Period} type
+     * in order to test a different code path.
+     */
+    @Test
+    public void testOnPeriods() {
+        factory = new DefaultFilterFactory.Features<>(Object.class, 
Period.class, WraparoundMethod.NONE);
+        testAfter();
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
index 4dcfbc663c..a2690ff1a3 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultTemporalExtent.java
@@ -169,7 +169,7 @@ public class DefaultTemporalExtent extends ISOMetadata 
implements TemporalExtent
     static Instant getBound(final TemporalPrimitive extent, final boolean 
begin) {
         if (extent instanceof Period) {
             var p = (Period) extent;
-            return begin ? p.getBeginning() : p.getEnding();
+            return TemporalDate.toInstant(begin ? p.getBeginning() : 
p.getEnding(), null);
         }
         return null;
     }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/DefaultPeriod.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/DefaultPeriod.java
index d79b3a65e6..ee08276f1e 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/DefaultPeriod.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/DefaultPeriod.java
@@ -17,12 +17,12 @@
 package org.apache.sis.pending.temporal;
 
 import java.util.Objects;
-import java.time.Instant;
+import java.time.temporal.Temporal;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import java.time.Duration;
 import java.time.temporal.TemporalAmount;
 import org.opengis.temporal.Period;
-import org.apache.sis.pending.jdk.JDK23;
 
 
 /**
@@ -33,27 +33,27 @@ import org.apache.sis.pending.jdk.JDK23;
  */
 final class DefaultPeriod extends Primitive implements Period {
     /** Bounds making this period. */
-    private final Instant beginning, ending;
+    private final Temporal beginning, ending;
 
     /** Creates a new period with the given bounds. */
-    DefaultPeriod(final Instant beginning, final Instant ending) {
+    DefaultPeriod(final Temporal beginning, final Temporal ending) {
         this.beginning = beginning;
         this.ending    = ending;
     }
 
     /** The beginning instant at which this period starts. */
-    @Override public Instant getBeginning() {
+    @Override public Temporal getBeginning() {
         return beginning;
     }
 
     /** The ending instant at which this period ends. */
-    @Override public Instant getEnding() {
+    @Override public Temporal getEnding() {
         return ending;
     }
 
     /** Duration of this temporal geometric primitive. */
     @Override public TemporalAmount length() {
-        return (beginning != null && ending != null) ? JDK23.until(beginning, 
ending) : null;
+        return (beginning != null && ending != null) ? 
Duration.between(beginning, ending) : null;
     }
 
     /** String representation. */
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
index baedbaaaed..eeced6a22a 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/pending/temporal/TemporalUtilities.java
@@ -46,21 +46,10 @@ public final class TemporalUtilities {
      * @param  time  the date for which to create instant, or {@code null}.
      * @return the instant, or {@code null} if the given time was null.
      */
-    public static TemporalPrimitive createInstant(final Instant time) {
+    public static TemporalPrimitive createInstant(final Temporal time) {
         return (time == null) ? null : new DefaultPeriod(time, time);
     }
 
-    /**
-     * Creates a period for the given begin and end instant.
-     *
-     * @param  begin  the begin instant (inclusive), or {@code null}.
-     * @param  end    the end instant (inclusive), or {@code null}.
-     * @return the period, or {@code null} if both arguments are null.
-     */
-    public static TemporalPrimitive createPeriod(final Instant begin, final 
Instant end) {
-        return (begin == null && end == null) ? null : new 
DefaultPeriod(begin, end);
-    }
-
     /**
      * Creates a period for the given begin and end instant.
      *
@@ -71,8 +60,7 @@ public final class TemporalUtilities {
      * @todo Needs to avoid assuming UTC timezone.
      */
     public static TemporalPrimitive createPeriod(final Temporal begin, final 
Temporal end) {
-        return createPeriod(TemporalDate.toInstant(begin, ZoneOffset.UTC),
-                            TemporalDate.toInstant(end,   ZoneOffset.UTC));
+        return (begin == null && end == null) ? null : new 
DefaultPeriod(begin, end);
     }
 
     /**
@@ -86,9 +74,9 @@ public final class TemporalUtilities {
     public static Period getPeriod(final TemporalPrimitive time) {
         if (time instanceof Period) {
             var p = (Period) time;
-            final Instant begin = p.getBeginning();
+            final Temporal begin = p.getBeginning();
             if (begin != null) {
-                final Instant end = p.getEnding();
+                final Temporal end = p.getEnding();
                 if (end != null && !begin.equals(end)) {
                     return p;
                 }
@@ -108,8 +96,8 @@ public final class TemporalUtilities {
     public static Instant getInstant(final TemporalPrimitive time) {
         if (time instanceof Period) {
             var p = (Period) time;
-            final Instant begin = p.getBeginning();
-            final Instant end = p.getEnding();
+            final Instant begin = TemporalDate.toInstant(p.getBeginning(), 
ZoneOffset.UTC);
+            final Instant end = TemporalDate.toInstant(p.getEnding(), 
ZoneOffset.UTC);
             if (end == null) {
                 return begin;
             }
@@ -130,7 +118,7 @@ public final class TemporalUtilities {
     public static Date getAnyDate(final TemporalPrimitive time) {
         if (time instanceof Period) {
             var p = (Period) time;
-            Instant instant;
+            Temporal instant;
             if ((instant = p.getEnding()) != null || (instant = 
p.getBeginning()) != null) {
                 return TemporalDate.toDate(instant);
             }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
index da5c230f3f..edfe70d45d 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gml/TimePeriodBound.java
@@ -16,12 +16,14 @@
  */
 package org.apache.sis.xml.bind.gml;
 
-import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.Temporal;
 import javax.xml.datatype.XMLGregorianCalendar;
 import jakarta.xml.bind.annotation.XmlValue;
 import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlAttribute;
 import jakarta.xml.bind.annotation.XmlTransient;
+import org.apache.sis.util.privy.TemporalDate;
 
 
 /**
@@ -98,8 +100,8 @@ public abstract class TimePeriodBound {
          * @param instant        the instant of the new bound, or {@code null}.
          * @param indeterminate  the value to give to {@link 
#indeterminatePosition} if the date is null.
          */
-        GML3(final Instant instant, final String indeterminate) {
-            value = TimeInstant.toXML(instant);
+        GML3(final Temporal instant, final String indeterminate) {
+            value = TimeInstant.toXML(TemporalDate.toInstant(instant, 
ZoneOffset.UTC));
             if (value == null) {
                 indeterminatePosition = indeterminate;
             }
@@ -154,8 +156,8 @@ public abstract class TimePeriodBound {
          *
          * @param instant The instant of the new bound, or {@code null}.
          */
-        GML2(final Instant instant) {
-            timeInstant = new TimeInstant(instant);
+        GML2(final Temporal instant) {
+            timeInstant = new TimeInstant(TemporalDate.toInstant(instant, 
ZoneOffset.UTC));
         }
 
         /**
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
index 51ebb99c78..99648a7ba6 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultTemporalDatum.java
@@ -146,7 +146,9 @@ public class DefaultTemporalDatum extends AbstractDatum 
implements TemporalDatum
      * @param  properties  the properties to be given to the identified object.
      * @param  origin      the date and time origin of this temporal datum.
      *
-     * @see 
org.apache.sis.referencing.factory.GeodeticObjectFactory#createTemporalDatum(Map,
 Date)
+     * @see 
org.apache.sis.referencing.factory.GeodeticObjectFactory#createTemporalDatum(Map,
 Temporal)
+     *
+     * @since 1.5
      */
     public DefaultTemporalDatum(final Map<String,?> properties, final Temporal 
origin) {
         super(properties);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
index 558fff3fef..5be71863b9 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Epoch.java
@@ -69,9 +69,9 @@ public final class Epoch extends FormattableObject {
                 epoch = ((YearMonth) epoch).atDay(1);
                 fractionDigits = 2;
             } else if (epoch.isSupported(ChronoField.NANO_OF_DAY)) {
-                day = epoch.getLong(ChronoField.NANO_OF_DAY);
-                day /= (double) Constants.NANOSECONDS_PER_DAY;
-                fractionDigits = (epoch.get(ChronoField.NANO_OF_SECOND) != 0) 
? 16 : 8;
+                final long nano = epoch.getLong(ChronoField.NANO_OF_DAY);
+                day = nano / (double) Constants.NANOSECONDS_PER_DAY;
+                fractionDigits = ((nano % Constants.NANOS_PER_SECOND) != 0) ? 
16 : 8;
             }
             day += epoch.get(ChronoField.DAY_OF_YEAR) - 1;
             value = year + day / Year.of(year).length();
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
index c14ee6774a..03b1ea2bd4 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
@@ -539,9 +539,9 @@ next:       for (final Class<?> candidate : candidates) {
     }
 
     /**
-     * Returns the most specific class implemented by the objects in the given 
collection.
-     * If there is more than one specialized class, returns their {@linkplain 
#findCommonClass
-     * most specific common super class}.
+     * Returns the most specific class of the objects in the given collection.
+     * If there is more than one specialized class,
+     * returns their {@linkplain #findCommonClass most specific common super 
class}.
      *
      * <p>This method searches for classes only, not interfaces.</p>
      *
@@ -653,7 +653,7 @@ next:       for (final Class<?> candidate : candidates) {
             return Collections.emptySet();
         }
         interfaces.retainAll(buffer);
-        for (Iterator<Class<?>> it=interfaces.iterator(); it.hasNext();) {
+        for (Iterator<Class<?>> it = interfaces.iterator(); it.hasNext();) {
             final Class<?> candidate = it.next();
             buffer.clear();     // Safe because the buffer cannot be 
Collections.EMPTY_SET at this point.
             getInterfaceSet(candidate, buffer);
@@ -729,29 +729,25 @@ cmp:    for (final Class<?> c : c1) {
      *     <th>{@code getSimpleName()}</th>
      *     <th>{@code getCanonicalName()}</th>
      *     <th>{@code getShortName()}</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@link String}</td>
      *     <td>{@code "java.lang.String"}</td>
      *     <td>{@code "String"}</td>
      *     <td>{@code "java.lang.String"}</td>
      *     <td>{@code "String"}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@code double[]}</td>
      *     <td>{@code "[D"}</td>
      *     <td>{@code "double[]"}</td>
      *     <td>{@code "double[]"}</td>
      *     <td>{@code "double[]"}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@link java.awt.geom.Point2D.Double}</td>
      *     <td>{@code "java.awt.geom.Point2D$Double"}</td>
      *     <td>{@code "Double"}</td>
      *     <td>{@code "java.awt.geom.Point2D.Double"}</td>
      *     <td>{@code "Point2D.Double"}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>Anonymous {@link Comparable}</td>
      *     <td>{@code "com.mycompany.myclass$1"}</td>
      *     <td>{@code ""}</td>
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
index 77f10593fa..3ebaf7cd64 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
@@ -193,6 +193,11 @@ public class Errors extends IndexedResourceBundle {
          */
         public static final short CanNotWriteFile_2 = 27;
 
+        /**
+         * Cannot compare instance of ‘{0}’ with ‘{1}’.
+         */
+        public static final short CannotCompareInstanceOf_2 = 205;
+
         /**
          * Circular reference.
          */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
index cf1ed4b216..955faafe3e 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
@@ -32,6 +32,7 @@ CanNotAddToExclusiveSet_2         = No element can be added 
to this set because
 CanNotAssign_2                    = Cannot assign \u201c{1}\u201d to 
\u201c{0}\u201d.
 CanNotAssignUnitToDimension_2     = Cannot assign units \u201c{1}\u201d to 
dimension \u201c{0}\u201d.
 CanNotAssignUnitToVariable_2      = Cannot assign units \u201c{1}\u201d to 
variable \u201c{0}\u201d.
+CannotCompareInstanceOf_2         = Cannot compare instance of \u2018{0}\u2019 
with \u2018{1}\u2019.
 CanNotConnectTo_1                 = Cannot connect to \u201c{0}\u201d.
 CanNotConvertFromType_2           = Cannot convert from type \u2018{0}\u2019 
to type \u2018{1}\u2019.
 CanNotConvertValue_2              = Cannot convert value \u201c{0}\u201d to 
type \u2018{1}\u2019.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
index 46b4e9cdfb..cfc7abb465 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
@@ -29,6 +29,7 @@ CanNotAddToExclusiveSet_2         = Aucun \u00e9l\u00e9ment 
ne peut \u00eatre aj
 CanNotAssign_2                    = Ne peut pas assigner 
\u00ab\u202f{1}\u202f\u00bb \u00e0 \u00ab\u202f{0}\u202f\u00bb.
 CanNotAssignUnitToDimension_2     = Ne peut pas assigner les unit\u00e9s 
\u00ab\u202f{1}\u202f\u00bb \u00e0 la dimension \u00ab\u202f{0}\u202f\u00bb.
 CanNotAssignUnitToVariable_2      = Ne peut pas assigner les unit\u00e9s 
\u00ab\u202f{1}\u202f\u00bb \u00e0 la variable \u00ab\u202f{0}\u202f\u00bb.
+CannotCompareInstanceOf_2         = Ne peut pas comparer des instances de 
\u2018{0}\u2019 avec \u2018{1}\u2019.
 CanNotConnectTo_1                 = Ne peut pas se connecter \u00e0 
\u00ab\u202f{0}\u202f\u00bb.
 CanNotConvertFromType_2           = Ne peut pas convertir du type 
\u2018{0}\u2019 vers le type \u2018{1}\u2019.
 CanNotConvertValue_2              = La valeur \u00ab\u202f{0}\u202f\u00bb ne 
peut pas \u00eatre convertie vers le type \u2018{1}\u2019.
diff --git a/geoapi/snapshot b/geoapi/snapshot
index ccc41ee613..dd12a26da8 160000
--- a/geoapi/snapshot
+++ b/geoapi/snapshot
@@ -1 +1 @@
-Subproject commit ccc41ee613b7451366a796fada91c1f74f8f7168
+Subproject commit dd12a26da8121f0af6d324e33195912f21e16969

Reply via email to