This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits 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 2c02d91895 feat(Geometry): add a D9IM matrix class, replacing int and 
string patterns
2c02d91895 is described below

commit 2c02d91895434598ec0aa4fc230437108a4887fd
Author: jsorel <[email protected]>
AuthorDate: Fri Sep 18 10:42:19 2026 +0200

    feat(Geometry): add a D9IM matrix class, replacing int and string patterns
---
 .../main/org/apache/sis/geometries/DE9IM.java      | 512 +++++++++++++++++++++
 .../main/org/apache/sis/geometries/Geometry.java   |  65 ++-
 .../geometries/operation/GeometryProcessor.java    |  22 +-
 .../test/org/apache/sis/geometries/DE9IMTest.java  | 181 ++++++++
 .../sis/geometries/operation/RelateTest.java       |  52 +--
 5 files changed, 743 insertions(+), 89 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DE9IM.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DE9IM.java
new file mode 100644
index 0000000000..5524f7f955
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/DE9IM.java
@@ -0,0 +1,512 @@
+/*
+ * 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.geometries;
+
+import java.util.Arrays;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * A dimensionally extended nine-intersection matrix, the pattern against which
+ * {@link Geometry#relate(Geometry, DE9IM)} tests a pair of geometries.
+ *
+ * <p>Each cell constrains the intersection between a part of the first 
geometry (the row)
+ * and a part of the second geometry (the column). Two orders are defined:</p>
+ * <ul>
+ *   <li>An <dfn>order 4</dfn> matrix has the {@linkplain Element#CLOSURE 
closure} and the
+ *       {@linkplain Element#EXTERIOR exterior} as rows and columns.</li>
+ *   <li>An <dfn>order 9</dfn> matrix has the {@linkplain Element#INTERIOR 
interior},
+ *       the {@linkplain Element#BOUNDARY boundary} and the {@linkplain 
Element#EXTERIOR exterior}
+ *       as rows and columns.</li>
+ * </ul>
+ *
+ * <p>The matrix can be read from and written to a string of 4 or 9 characters 
in row-major order,
+ * such as {@code "TNNNNNFFN"}, or to an integer mask for compact storage. It 
can also be built
+ * cell by cell:</p>
+ *
+ * {@snippet lang="java" :
+ *     DE9IM contains = DE9IM.order9().set(Element.INTERIOR, Element.INTERIOR, 
Constraint.NON_EMPTY)
+ *                                    .set(Element.EXTERIOR, Element.INTERIOR, 
Constraint.EMPTY)
+ *                                    .set(Element.EXTERIOR, Element.BOUNDARY, 
Constraint.EMPTY);
+ *     }
+ *
+ * <p>Instances of this class are mutable and therefore not thread-safe.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see Geometry#relate(Geometry, DE9IM)
+ * @see ISO 19107:2019 - relate & 3Drelate - 6.4.8.8, 6.4.9, 10.8.4, 10.8.5, 
10.8.6
+ */
+public final class DE9IM implements Cloneable {
+    /**
+     * The part of a geometry designated by a row or a column of the matrix.
+     * {@link #INTERIOR}, {@link #BOUNDARY} and {@link #EXTERIOR} apply to 
matrices of order 9,
+     * while {@link #CLOSURE} and {@link #EXTERIOR} apply to matrices of order 
4.
+     */
+    public enum Element {
+        /**
+         * The geometry without its boundary.
+         * Only in matrices of {@linkplain DE9IM#getOrder() order} 9.
+         */
+        INTERIOR,
+
+        /**
+         * The boundary of the geometry.
+         * Only in matrices of {@linkplain DE9IM#getOrder() order} 9.
+         */
+        BOUNDARY,
+
+        /**
+         * All the positions which do not belong to the geometry.
+         * In matrices of both orders.
+         */
+        EXTERIOR,
+
+        /**
+         * The geometry with its boundary, that is the union of the interior 
and the boundary.
+         * Only in matrices of {@linkplain DE9IM#getOrder() order} 4.
+         */
+        CLOSURE
+    }
+
+    /**
+     * The condition that the intersection of a row element with a column 
element shall meet.
+     *
+     * <p>Difference with ISO 19107, where a digit requires the intersection 
to be of that topological
+     * dimension <em>at most</em>: the digits are interpreted as requiring 
<em>exactly</em> that dimension,
+     * which is the interpretation of OGC Simple Feature Access and of the 
usual implementations such as
+     * JTS. Consequently {@link #CURVE} for example is not met by an 
intersection made of points.</p>
+     */
+    public enum Constraint {
+        /**
+         * The cell is not tested. Written {@code 'N'}, also accepted as 
{@code '*'}.
+         */
+        ANY('N'),
+
+        /**
+         * The intersection shall be empty. Written {@code 'F'}.
+         */
+        EMPTY('F'),
+
+        /**
+         * The intersection shall not be empty, whatever its dimension. 
Written {@code 'T'}.
+         */
+        NON_EMPTY('T'),
+
+        /**
+         * The intersection shall be a set of points, of topological dimension 
0.
+         * Written {@code '0'}.
+         */
+        POINT('0'),
+
+        /**
+         * The intersection shall be a set of curves, of topological dimension 
1.
+         * Written {@code '1'}.
+         */
+        CURVE('1'),
+
+        /**
+         * The intersection shall be a set of surfaces, of topological 
dimension 2.
+         * Written {@code '2'}.
+         */
+        SURFACE('2'),
+
+        /**
+         * The intersection shall be a set of solids, of topological dimension 
3.
+         * Written {@code '3'}.
+         */
+        SOLID('3');
+
+        /**
+         * The character representing this constraint in the string form of a 
matrix.
+         */
+        private final char symbol;
+
+        /**
+         * Creates a new constraint represented by the given character.
+         */
+        private Constraint(final char symbol) {
+            this.symbol = symbol;
+        }
+
+        /**
+         * Returns the character representing this constraint in the string 
form of a matrix.
+         *
+         * @return the symbol of this constraint.
+         */
+        public char symbol() {
+            return symbol;
+        }
+
+        /**
+         * Returns the constraint represented by the given character.
+         * Lower cases are accepted, and {@code '*'} is synonymous of {@code 
'N'}.
+         *
+         * @param  symbol  the character to decode.
+         * @return the constraint represented by the given character.
+         * @throws IllegalArgumentException if the given character is not a 
valid symbol.
+         */
+        public static Constraint forSymbol(final char symbol) {
+            if (symbol == '*') {
+                return ANY;
+            }
+            final char c = Character.toUpperCase(symbol);
+            for (final Constraint candidate : VALUES) {
+                if (candidate.symbol == c) {
+                    return candidate;
+                }
+            }
+            throw new IllegalArgumentException("Unknown DE-9IM symbol: " + 
symbol);
+        }
+
+        /**
+         * Returns whether an intersection of the given topological dimension 
meets this constraint.
+         *
+         * @param  dimension  topological dimension of the intersection, or -1 
if the intersection is empty.
+         * @return whether the given dimension meets this constraint.
+         */
+        public boolean accept(final int dimension) {
+            switch (this) {
+                case ANY:       return true;
+                case EMPTY:     return dimension < 0;
+                case NON_EMPTY: return dimension >= 0;
+                default:        return dimension == (symbol - '0');
+            }
+        }
+
+        /**
+         * All constraints, fetched only once because this array is not cloned.
+         */
+        private static final Constraint[] VALUES = values();
+    }
+
+    /**
+     * Number of bits used by the encoding of a single cell in the {@linkplain 
#toInt() integer form}.
+     * The 7 {@linkplain Constraint constraints} are encoded by their ordinal 
value.
+     */
+    private static final int BITS_PER_CELL = 3;
+
+    /**
+     * Mask of the bits used by the encoding of a single cell in the 
{@linkplain #toInt() integer form}.
+     */
+    private static final int CELL_MASK = (1 << BITS_PER_CELL) - 1;
+
+    /**
+     * Bit set in the {@linkplain #toInt() integer form} of a matrix of order 
9.
+     * This is the bit after the 9 cells of {@value #BITS_PER_CELL} bits each.
+     */
+    private static final int ORDER_9_FLAG = 1 << (9 * BITS_PER_CELL);
+
+    /**
+     * Number of rows and columns: 2 for a matrix of order 4, or 3 for a 
matrix of order 9.
+     */
+    private final int side;
+
+    /**
+     * Constraint on each cell, in row-major order. The length of this array 
is {@link #side} squared.
+     * Never {@code null} and never contains null elements.
+     */
+    private final Constraint[] cells;
+
+    /**
+     * Creates a new matrix with the given number of rows and columns, with 
all cells untested.
+     */
+    private DE9IM(final int side) {
+        this.side  = side;
+        this.cells = new Constraint[side * side];
+        Arrays.fill(cells, Constraint.ANY);
+    }
+
+    /**
+     * Creates a copy of the given matrix.
+     */
+    private DE9IM(final DE9IM other) {
+        side  = other.side;
+        cells = other.cells.clone();
+    }
+
+    /**
+     * Creates a matrix of order 4 with all cells untested.
+     * The rows and columns are the {@linkplain Element#CLOSURE closure}
+     * and the {@linkplain Element#EXTERIOR exterior} of the geometries.
+     *
+     * @return a new matrix of order 4 accepting all geometry pairs.
+     */
+    public static DE9IM order4() {
+        return new DE9IM(2);
+    }
+
+    /**
+     * Creates a matrix of order 9 with all cells untested.
+     * The rows and columns are the {@linkplain Element#INTERIOR interior},
+     * the {@linkplain Element#BOUNDARY boundary} and the {@linkplain 
Element#EXTERIOR exterior}
+     * of the geometries.
+     *
+     * @return a new matrix of order 9 accepting all geometry pairs.
+     */
+    public static DE9IM order9() {
+        return new DE9IM(3);
+    }
+
+    /**
+     * Creates a matrix from its string form: 4 or 9 characters in row-major 
order.
+     * Each character is a {@linkplain Constraint#symbol() constraint symbol}.
+     *
+     * @param  pattern  the pattern of 4 or 9 characters to decode.
+     * @return the matrix represented by the given pattern.
+     * @throws IllegalArgumentException if the given pattern has an invalid 
length or contains an invalid symbol.
+     *
+     * @see #toString()
+     */
+    public static DE9IM valueOf(final String pattern) {
+        ArgumentChecks.ensureNonNull("pattern", pattern);
+        final int side;
+        switch (pattern.length()) {
+            case 4:  side = 2; break;
+            case 9:  side = 3; break;
+            default: throw new IllegalArgumentException("A DE-9IM pattern 
shall have 4 or 9 characters, "
+                            + "but the given pattern has " + pattern.length() 
+ " of them.");
+        }
+        final DE9IM matrix = new DE9IM(side);
+        for (int i=0; i<matrix.cells.length; i++) {
+            matrix.cells[i] = Constraint.forSymbol(pattern.charAt(i));
+        }
+        return matrix;
+    }
+
+    /**
+     * Creates a matrix from its integer form.
+     *
+     * @param  code  the integer mask to decode.
+     * @return the matrix represented by the given mask.
+     * @throws IllegalArgumentException if the given mask is not a valid 
encoding.
+     *
+     * @see #toInt()
+     */
+    public static DE9IM valueOf(final int code) {
+        final DE9IM matrix = new DE9IM((code & ORDER_9_FLAG) != 0 ? 3 : 2);
+        final int n = matrix.cells.length;
+        int remaining = code & ~ORDER_9_FLAG;
+        for (int i=0; i<n; i++) {
+            final int ordinal = remaining & CELL_MASK;
+            if (ordinal >= Constraint.VALUES.length) {
+                throw new IllegalArgumentException("Invalid constraint code " 
+ ordinal + " at cell " + i + '.');
+            }
+            matrix.cells[i] = Constraint.VALUES[ordinal];
+            remaining >>>= BITS_PER_CELL;
+        }
+        if (remaining != 0) {
+            throw new IllegalArgumentException("The given code has bits set 
outside the "
+                    + n + " cells of the matrix: " + code);
+        }
+        return matrix;
+    }
+
+    /**
+     * Returns the number of cells in this matrix.
+     *
+     * @return the order of this matrix, either 4 or 9.
+     */
+    public int getOrder() {
+        return cells.length;
+    }
+
+    /**
+     * Returns the index of the given element in a row or a column of this 
matrix.
+     *
+     * @param  element  the element for which to get the index.
+     * @param  name     the argument name, for the error message if the 
element is invalid.
+     * @return index of the given element, from 0 inclusive to {@link #side} 
exclusive.
+     * @throws IllegalArgumentException if the given element does not apply to 
the order of this matrix.
+     */
+    private int indexOf(final Element element, final String name) {
+        ArgumentChecks.ensureNonNull(name, element);
+        if (side == 3) {
+            switch (element) {
+                case INTERIOR: return 0;
+                case BOUNDARY: return 1;
+                case EXTERIOR: return 2;
+            }
+        } else {
+            switch (element) {
+                case CLOSURE:  return 0;
+                case EXTERIOR: return 1;
+            }
+        }
+        throw new IllegalArgumentException("Element " + element + " given as 
\"" + name
+                + "\" does not apply to a matrix of order " + cells.length + 
'.');
+    }
+
+    /**
+     * Returns the constraint applied on the intersection of the given parts 
of the two geometries.
+     *
+     * @param  row     the part of the first geometry.
+     * @param  column  the part of the second geometry.
+     * @return the constraint applied on that intersection.
+     * @throws IllegalArgumentException if an element does not apply to the 
order of this matrix.
+     */
+    public Constraint get(final Element row, final Element column) {
+        return cells[indexOf(row, "row") * side + indexOf(column, "column")];
+    }
+
+    /**
+     * Sets the constraint applied on the intersection of the given parts of 
the two geometries.
+     *
+     * @param  row         the part of the first geometry.
+     * @param  column      the part of the second geometry.
+     * @param  constraint  the constraint to apply on that intersection.
+     * @return {@code this} for method call chaining.
+     * @throws IllegalArgumentException if an element does not apply to the 
order of this matrix.
+     */
+    public DE9IM set(final Element row, final Element column, final Constraint 
constraint) {
+        ArgumentChecks.ensureNonNull("constraint", constraint);
+        cells[indexOf(row, "row") * side + indexOf(column, "column")] = 
constraint;
+        return this;
+    }
+
+    /**
+     * Sets all cells of this matrix to the given constraint.
+     *
+     * @param  constraint  the constraint to apply on all intersections.
+     * @return {@code this} for method call chaining.
+     */
+    public DE9IM setAll(final Constraint constraint) {
+        ArgumentChecks.ensureNonNull("constraint", constraint);
+        Arrays.fill(cells, constraint);
+        return this;
+    }
+
+    /**
+     * Returns whether the given intersection dimensions meet all the 
constraints of this matrix.
+     * The given array holds the topological dimension of the intersection of 
each part of the first
+     * geometry (the rows) with each part of the second geometry (the 
columns), in the interior,
+     * boundary and exterior order used by matrices of order 9. A negative 
value means that the
+     * intersection is empty.
+     *
+     * <p>If this matrix is of order 4, the dimensions of the closures are 
derived from the given
+     * dimensions: the closure is the union of the interior and the boundary, 
therefore the dimension
+     * of its intersection with another part is the greatest dimension of the 
parts of that union.</p>
+     *
+     * @param  dimensions  topological dimension of the 9 intersections in 
row-major order,
+     *         with a negative value for an empty intersection.
+     * @return whether the given dimensions meet all the constraints of this 
matrix.
+     * @throws IllegalArgumentException if the given array does not have a 
length of 9.
+     */
+    public boolean matches(final int... dimensions) {
+        ArgumentChecks.ensureNonNull("dimensions", dimensions);
+        if (dimensions.length != 9) {
+            throw new IllegalArgumentException("Expected the dimensions of 9 
intersections, but got "
+                    + dimensions.length + " of them.");
+        }
+        if (side == 3) {
+            for (int i=0; i<cells.length; i++) {
+                if (!cells[i].accept(dimensions[i])) {
+                    return false;
+                }
+            }
+        } else {
+            /*
+             * A row or a column of index 0 is the closure, which merges the 
interior (index 0) and
+             * the boundary (index 1) of the order 9 matrix. A row or a column 
of index 1 is the
+             * exterior, which is at index 2 in the order 9 matrix. Since the 
closure is the union
+             * of the interior and the boundary, the dimension of an 
intersection with the closure
+             * is the greatest dimension of the intersections with those two 
parts.
+             */
+            for (int i=0; i<cells.length; i++) {
+                final boolean rowIsClosure    = (i / side) == 0;
+                final boolean columnIsClosure = (i % side) == 0;
+                int dimension = -1;
+                for (int r = rowIsClosure ? 0 : 2; r <= (rowIsClosure ? 1 : 
2); r++) {
+                    for (int c = columnIsClosure ? 0 : 2; c <= 
(columnIsClosure ? 1 : 2); c++) {
+                        dimension = Math.max(dimension, dimensions[r*3 + c]);
+                    }
+                }
+                if (!cells[i].accept(dimension)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the integer form of this matrix. Each cell is encoded on 
{@value #BITS_PER_CELL} bits
+     * in row-major order, the first cell being in the lowest bits, and an 
additional bit tells whether
+     * the matrix is of order 9. This form is more compact than the string 
form but is specific to
+     * Apache SIS; it is not defined by any standard.
+     *
+     * @return the integer form of this matrix.
+     *
+     * @see #valueOf(int)
+     */
+    public int toInt() {
+        int code = (side == 3) ? ORDER_9_FLAG : 0;
+        for (int i=0; i<cells.length; i++) {
+            code |= cells[i].ordinal() << (i * BITS_PER_CELL);
+        }
+        return code;
+    }
+
+    /**
+     * Returns the string form of this matrix: the {@linkplain 
Constraint#symbol() symbol} of each cell
+     * in row-major order. Untested cells are written {@code 'N'}, which is 
the symbol used by ISO 19107,
+     * but {@code '*'} is also accepted when {@linkplain #valueOf(String) 
reading} a pattern.
+     *
+     * @return the string form of this matrix, of 4 or 9 characters.
+     *
+     * @see #valueOf(String)
+     */
+    @Override
+    public String toString() {
+        final char[] symbols = new char[cells.length];
+        for (int i=0; i<symbols.length; i++) {
+            symbols[i] = cells[i].symbol;
+        }
+        return new String(symbols);
+    }
+
+    /**
+     * Returns a copy of this matrix which can be modified without impacting 
this instance.
+     *
+     * @return a copy of this matrix.
+     */
+    @Override
+    public DE9IM clone() {
+        return new DE9IM(this);
+    }
+
+    /**
+     * Compares this matrix with the given object for equality.
+     *
+     * @param  other  the object to compare with this matrix, or {@code null}.
+     * @return whether the given object is a matrix of the same order with the 
same constraints.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        return (other instanceof DE9IM) && Arrays.equals(cells, ((DE9IM) 
other).cells);
+    }
+
+    /**
+     * Returns a hash code value for this matrix.
+     *
+     * @return a hash code value.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(cells) ^ side;
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
index 0d73c65069..267a183d25 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
@@ -812,7 +812,7 @@ public sealed interface Geometry
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>Equivalent to {@code relate(other, "TNNNNNFFN")}.</li>
+     *   <li>Equivalent to the {@code "TNNNNNFFN"} {@linkplain DE9IM 
intersection pattern}.</li>
      *   <li>{@code a.contains(b)} is equivalent to {@code b.within(a)}.</li>
      * </ul>
      *
@@ -855,7 +855,8 @@ public sealed interface Geometry
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>Equivalent to {@code relate(other, "FFNFFNNNN")}, or {@code 
"FNNN"} on the order 4 matrix.</li>
+     *   <li>Equivalent to the {@code "FFNFFNNNN"} {@linkplain DE9IM 
intersection pattern},
+     *       or {@code "FNNN"} on the order 4 matrix.</li>
      *   <li>The negation of {@link #intersects(Geometry)}.</li>
      * </ul>
      *
@@ -876,7 +877,7 @@ public sealed interface Geometry
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>Equivalent to {@code relate(other, "NFFFNFNNF")}.</li>
+     *   <li>Equivalent to the {@code "NFFFNFNNF"} {@linkplain DE9IM 
intersection pattern}.</li>
      *   <li>Only the spatial coordinates are compared, so a spatio-temporal 
geometry is tested
      *       for spatial equality only.</li>
      *   <li>The given geometry is converted to the coordinate reference 
system of this geometry
@@ -901,7 +902,7 @@ public sealed interface Geometry
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>Equivalent to {@code relate(other, "TNNN")} on the order 4 
matrix.</li>
+     *   <li>Equivalent to the {@code "TNNN"} {@linkplain DE9IM intersection 
pattern} on the order 4 matrix.</li>
      *   <li>The negation of {@link #disjoint(Geometry)}.</li>
      * </ul>
      *
@@ -967,7 +968,7 @@ public sealed interface Geometry
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>Equivalent to {@code relate(other, "TNTNNNTNN")}.</li>
+     *   <li>Equivalent to the {@code "TNTNNNTNN"} {@linkplain DE9IM 
intersection pattern}.</li>
      *   <li>Symmetric: {@code a.overlaps(b)} is equivalent to {@code 
b.overlaps(a)}.</li>
      * </ul>
      *
@@ -984,49 +985,41 @@ public sealed interface Geometry
     }
 
     /**
-     * Returns whether the two geometries are related according to the given 
intersection pattern,
-     * encoded as an integer mask.
-     *
-     * <p>Difference with ISO 19107, which specifies the pattern as a string:
-     * see {@link #relate(Geometry, String)} for the standard operation.</p>
-     *
-     * @param  other   the geometry to test against.
-     * @param  matrix  intersection pattern, encoded as an integer mask.
-     * @return {@code true} if the two geometries match the given pattern.
-     * @throws OperationException if the test cannot be performed.
-     *
-     * @see GeometryProcessor#relate(org.apache.sis.geometries.Geometry, 
org.apache.sis.geometries.Geometry, int)
-     */
-    default boolean relate(Geometry other, int matrix) throws 
OperationException {
-        return new GeometryProcessor().relate(this, other, matrix);
-    }
-
-    /**
-     * Returns whether the two geometries are related according to the given 
intersection pattern.
+     * Returns whether the two geometries are related according to the given 
intersection matrix.
      * This is the reference operation from which all the named topological 
predicates are derived.
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>A pattern of 4 characters tests the intersections between the 
closures and the exteriors
+     *   <li>A matrix of order 4 tests the intersections between the closures 
and the exteriors
      *       of the two geometries, in row-major order.</li>
-     *   <li>A pattern of 9 characters tests the intersections between the 
interiors, the boundaries
+     *   <li>A matrix of order 9 tests the intersections between the 
interiors, the boundaries
      *       and the exteriors of the two geometries, in row-major order.</li>
-     *   <li>{@code T} requires a non-empty intersection, {@code F} an empty 
one, and {@code N}
-     *       (also written {@code *}) leaves that cell untested.</li>
-     *   <li>The digits {@code 0} to {@code 3} additionally require the 
intersection to be of that
-     *       topological dimension at most.</li>
+     *   <li>{@link DE9IM.Constraint#NON_EMPTY} requires a non-empty 
intersection,
+     *       {@link DE9IM.Constraint#EMPTY} an empty one, and {@link 
DE9IM.Constraint#ANY}
+     *       leaves that cell untested.</li>
+     *   <li>{@link DE9IM.Constraint#POINT} to {@link DE9IM.Constraint#SOLID} 
require the intersection
+     *       to be of exactly that topological dimension.</li>
      * </ul>
      *
+     * <p>Difference with ISO 19107, which specifies the pattern as a string: 
the pattern is given as
+     * a {@link DE9IM} matrix, which can be {@linkplain DE9IM#valueOf(String) 
read from a string} of
+     * 4 or 9 characters, from an {@linkplain DE9IM#valueOf(int) integer 
mask}, or built cell by cell.</p>
+     *
+     * <p>Difference with ISO 19107, where a digit requires the intersection 
to be of that topological
+     * dimension <em>at most</em>: the digits are interpreted as requiring 
<em>exactly</em> that dimension,
+     * which is the interpretation of OGC Simple Feature Access and of the 
usual implementations such as
+     * JTS.</p>
+     *
      * @param  other   the geometry to test against.
-     * @param  matrix  intersection pattern of 4 or 9 characters.
+     * @param  matrix  intersection pattern of order 4 or 9.
      * @return {@code true} if the two geometries match the given pattern.
      * @throws OperationException if the test cannot be performed.
      *
-     * @see GeometryProcessor#relate(org.apache.sis.geometries.Geometry, 
org.apache.sis.geometries.Geometry, java.lang.String)
+     * @see GeometryProcessor#relate(org.apache.sis.geometries.Geometry, 
org.apache.sis.geometries.Geometry, org.apache.sis.geometries.DE9IM)
      * @see ISO 19107:2019 - relate & 3Drelate - 6.4.8.8, 6.4.9, 10.8.4, 
10.8.5, 10.8.6
      */
     @UML(identifier="relate", specification=ISO_19107)
-    default boolean relate(Geometry other, String matrix) throws 
OperationException {
+    default boolean relate(Geometry other, DE9IM matrix) throws 
OperationException {
         return new GeometryProcessor().relate(this, other, matrix);
     }
 
@@ -1036,8 +1029,8 @@ public sealed interface Geometry
      * <p>Constraints:</p>
      * <ul>
      *   <li>The closures intersect but the interiors are disjoint.</li>
-     *   <li>Equivalent to {@code relate(other, "FT*******")}, {@code 
"F**T*****"}
-     *       or {@code "F***T****"}.</li>
+     *   <li>Equivalent to the {@code "FT*******"}, {@code "F**T*****"} or 
{@code "F***T****"}
+     *       {@linkplain DE9IM intersection patterns}.</li>
      * </ul>
      *
      * @param  other  the geometry to test against.
@@ -1058,7 +1051,7 @@ public sealed interface Geometry
      *
      * <p>Constraints:</p>
      * <ul>
-     *   <li>Equivalent to {@code relate(other, "TNFNNFNNN")}.</li>
+     *   <li>Equivalent to the {@code "TNFNNFNNN"} {@linkplain DE9IM 
intersection pattern}.</li>
      *   <li>{@code a.within(b)} is equivalent to {@code b.contains(a)}.</li>
      * </ul>
      *
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
index 002c3c0fce..5ec4d75fe8 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/GeometryProcessor.java
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import javax.measure.Quantity;
 import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.DE9IM;
 import org.apache.sis.geometries.Geometries;
 import org.apache.sis.geometries.Geometry;
 import org.apache.sis.geometries.GeometryCollection;
@@ -45,6 +46,7 @@ import org.apache.sis.maths.SampleSystem;
 import org.apache.sis.maths.Tuple;
 import org.apache.sis.measure.Quantities;
 import org.apache.sis.measure.Units;
+import org.apache.sis.util.ArgumentChecks;
 import static org.opengis.annotation.Specification.ISO_19107;
 import org.opengis.annotation.UML;
 import org.opengis.geometry.DirectPosition;
@@ -262,17 +264,23 @@ public final class GeometryProcessor {
      * intersectionPatternMatrix.
      * This returns FALSE if all the tested intersections are empty except 
exterior (this) intersect exterior (another).
      *
-     * @todo merge with ISO Relate beneath
+     * @param  geom1   the geometry on which the operation is invoked.
+     * @param  geom2   the geometry to test against.
+     * @param  matrix  the intersection pattern, of order 4 or 9.
+     * @return whether the two geometries match the given pattern.
+     * @throws OperationException if the test cannot be performed.
      */
-    public boolean relate(Geometry geom1, Geometry geom2, int matrix) throws 
OperationException {
-        throw new UnsupportedOperationException();
-    }
-
     @UML(identifier="relate", specification=ISO_19107) // section 6.4.8.8
     //@UML(identifier="3Drelate", specification=ISO_19107) // section 6.4.9
-    public boolean relate(Geometry geom1, Geometry geom2, String matrix) 
throws OperationException {
+    public boolean relate(Geometry geom1, Geometry geom2, DE9IM matrix) throws 
OperationException {
+        ArgumentChecks.ensureNonNull("matrix", matrix);
         //TODO : fallback on JTS until implemented
-        return jts(geom1).relate(jts(geom2), matrix);
+        final org.locationtech.jts.geom.IntersectionMatrix computed = 
jts(geom1).relate(jts(geom2));
+        final int[] dimensions = new int[9];
+        for (int i=0; i<dimensions.length; i++) {
+            dimensions[i] = computed.get(i / 3, i % 3);
+        }
+        return matrix.matches(dimensions);
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DE9IMTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DE9IMTest.java
new file mode 100644
index 0000000000..05d620bc10
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/DE9IMTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.geometries;
+
+import org.apache.sis.geometries.DE9IM.Constraint;
+import org.apache.sis.geometries.DE9IM.Element;
+
+// Test dependencies
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * Tests {@link DE9IM}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DE9IMTest {
+    /**
+     * Creates a new test case.
+     */
+    public DE9IMTest() {
+    }
+
+    /**
+     * Tests the creation of matrices with all cells untested.
+     */
+    @Test
+    public void testDefault() {
+        assertEquals(4, DE9IM.order4().getOrder());
+        assertEquals(9, DE9IM.order9().getOrder());
+        assertEquals("NNNN",      DE9IM.order4().toString());
+        assertEquals("NNNNNNNNN", DE9IM.order9().toString());
+        assertEquals(Constraint.ANY, DE9IM.order9().get(Element.BOUNDARY, 
Element.EXTERIOR));
+    }
+
+    /**
+     * Tests the configuration of individual cells.
+     */
+    @Test
+    public void testSet() {
+        final DE9IM matrix = DE9IM.order9().set(Element.INTERIOR, 
Element.INTERIOR, Constraint.NON_EMPTY)
+                                           .set(Element.EXTERIOR, 
Element.INTERIOR, Constraint.EMPTY)
+                                           .set(Element.EXTERIOR, 
Element.BOUNDARY, Constraint.EMPTY);
+        assertEquals("TNNNNNFFN", matrix.toString());
+        assertEquals(Constraint.NON_EMPTY, matrix.get(Element.INTERIOR, 
Element.INTERIOR));
+        assertEquals(Constraint.EMPTY,     matrix.get(Element.EXTERIOR, 
Element.BOUNDARY));
+        assertEquals(Constraint.ANY,       matrix.get(Element.EXTERIOR, 
Element.EXTERIOR));
+
+        final DE9IM order4 = DE9IM.order4().set(Element.CLOSURE, 
Element.CLOSURE, Constraint.NON_EMPTY);
+        assertEquals("TNNN", order4.toString());
+        /*
+         * The interior and the boundary exist only in a matrix of order 9,
+         * and the closure only in a matrix of order 4.
+         */
+        assertThrows(IllegalArgumentException.class, () -> 
order4.get(Element.INTERIOR, Element.EXTERIOR));
+        assertThrows(IllegalArgumentException.class, () -> 
matrix.get(Element.CLOSURE,  Element.EXTERIOR));
+    }
+
+    /**
+     * Tests the conversions between a matrix and its string form.
+     */
+    @Test
+    public void testString() {
+        assertEquals("FFNFFNNNN", DE9IM.valueOf("FFNFFNNNN").toString());
+        assertEquals("FNNN",      DE9IM.valueOf("FNNN")     .toString());
+        assertEquals("0123TFNNN", DE9IM.valueOf("0123TFNNN").toString());
+        /*
+         * The `*` character is synonymous of `N` and lower cases are accepted,
+         * but the canonical form uses `N` and upper cases.
+         */
+        assertEquals("FTNNNNNNN", DE9IM.valueOf("FT*******").toString());
+        assertEquals("FTNNNNNNN", DE9IM.valueOf("ft*******").toString());
+        assertEquals(DE9IM.valueOf("FT*******"), DE9IM.valueOf("FTNNNNNNN"));
+
+        assertThrows(IllegalArgumentException.class, () -> 
DE9IM.valueOf("TNN"));       // Invalid length.
+        assertThrows(IllegalArgumentException.class, () -> 
DE9IM.valueOf("TNNNNNNNNN"));
+        assertThrows(IllegalArgumentException.class, () -> 
DE9IM.valueOf("TNN4"));      // Invalid symbol.
+    }
+
+    /**
+     * Tests the conversions between a matrix and its integer form.
+     */
+    @Test
+    public void testInt() {
+        for (final String pattern : new String[] {"NNNN", "FNNN", "TNNNNNFFN", 
"0123TFNNN", "NNNNNNNNN"}) {
+            final DE9IM matrix = DE9IM.valueOf(pattern);
+            assertEquals(matrix, DE9IM.valueOf(matrix.toInt()), pattern);
+        }
+        /*
+         * A matrix of order 4 and a matrix of order 9 with the same cells in 
their
+         * first 4 positions shall nevertheless have different integer forms.
+         */
+        assertNotEquals(DE9IM.valueOf("FNNN").toInt(), 
DE9IM.valueOf("FNNNNNNNN").toInt());
+        assertEquals(0, DE9IM.order4().toInt());
+
+        assertThrows(IllegalArgumentException.class, () -> DE9IM.valueOf(7));  
          // No such constraint.
+        assertThrows(IllegalArgumentException.class, () -> DE9IM.valueOf(1 << 
12));      // Bit outside the cells.
+    }
+
+    /**
+     * Tests the evaluation of a matrix of order 9 against computed 
intersection dimensions.
+     */
+    @Test
+    public void testMatchesOrder9() {
+        // A polygon containing a point. The point has no boundary, hence the 
empty column in the middle.
+        final int[] dimensions = {
+             0, -1,  2,
+            -1, -1,  1,
+            -1, -1,  2
+        };
+        assertTrue (DE9IM.valueOf("TNNNNNFFN").matches(dimensions));     // 
contains
+        assertTrue (DE9IM.valueOf("T********").matches(dimensions));
+        assertTrue (DE9IM.valueOf("0********").matches(dimensions));     // 
Exactly dimension 0.
+        assertFalse(DE9IM.valueOf("1********").matches(dimensions));
+        assertFalse(DE9IM.valueOf("F********").matches(dimensions));
+        assertFalse(DE9IM.valueOf("NFFFNFNNF").matches(dimensions));     // 
equals
+        assertTrue (DE9IM.valueOf("NNNNNNNNN").matches(dimensions));     // 
Everything untested.
+        /*
+         * A digit requires the intersection to be of exactly that dimension,
+         * as in OGC Simple Feature Access rather than in ISO 19107.
+         */
+        assertTrue (DE9IM.valueOf("NNNNNNNN2").matches(dimensions));
+        assertFalse(DE9IM.valueOf("NNNNNNNN3").matches(dimensions));
+        assertFalse(DE9IM.valueOf("NNNNNNNN1").matches(dimensions));
+        assertFalse(DE9IM.valueOf("NNNNNN0NN").matches(dimensions));     // 
Empty intersection.
+
+        assertThrows(IllegalArgumentException.class, () -> 
DE9IM.valueOf("TNNNNNFFN").matches(0, 1, 2, 3));
+    }
+
+    /**
+     * Tests the evaluation of a matrix of order 4, where the closure merges
+     * the interior and the boundary of the order 9 matrix.
+     */
+    @Test
+    public void testMatchesOrder4() {
+        // Two disjoint polygons: only the intersections with the exteriors 
are non-empty.
+        final int[] disjoint = {
+            -1, -1,  2,
+            -1, -1,  1,
+             2,  1,  2
+        };
+        assertTrue (DE9IM.valueOf("FNNN").matches(disjoint));            // 
disjoint
+        assertFalse(DE9IM.valueOf("TNNN").matches(disjoint));            // 
intersects
+        /*
+         * The dimension of the intersection of a closure with the exterior is 
the greatest
+         * dimension among the interior and the boundary intersections with 
that exterior.
+         */
+        assertTrue (DE9IM.valueOf("F2NN").matches(disjoint));
+        assertFalse(DE9IM.valueOf("F1NN").matches(disjoint));
+
+        // Two polygons sharing a boundary line: only the boundaries intersect.
+        final int[] touches = {
+            -1, -1,  2,
+            -1,  1,  1,
+             2,  1,  2
+        };
+        assertTrue (DE9IM.valueOf("TNNN").matches(touches));             // 
intersects
+        assertFalse(DE9IM.valueOf("FNNN").matches(touches));             // 
disjoint
+        assertTrue (DE9IM.valueOf("1NNN").matches(touches));             // 
Exactly dimension 1.
+        assertFalse(DE9IM.valueOf("0NNN").matches(touches));
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/RelateTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/RelateTest.java
index 741b87cb35..bd60e8df0c 100644
--- 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/RelateTest.java
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/RelateTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.geometries.operation;
 
+import org.apache.sis.geometries.DE9IM;
 import org.apache.sis.geometries.Geometry;
 
 // Test dependencies
@@ -25,13 +26,13 @@ import org.junit.jupiter.api.Test;
 
 
 /**
- * Tests the {@code relate} operations of {@link GeometryProcessor}.
+ * Tests the {@code relate} operation of {@link GeometryProcessor}.
  *
  * @author Johann Sorel (Geomatys)
  */
 public class RelateTest {
     /**
-     * The inputs and expected result of a single test of {@code 
relate(Geometry, Geometry, int)}.
+     * The inputs and expected result of a single test of {@code 
relate(Geometry, Geometry, DE9IM)}.
      *
      * @param input    the geometry on which the operation is invoked.
      * @param other    the other operand.
@@ -41,20 +42,20 @@ public class RelateTest {
      */
     private record Entry(Geometry input,
                          Geometry other,
-                         int matrix,
+                         DE9IM matrix,
                          Boolean expected,
                          Class<? extends Exception> error)
     {
     }
 
     /**
-     * All test cases of {@code relate(Geometry, Geometry, int)}.
+     * All test cases of {@code relate(Geometry, Geometry, DE9IM)}.
      */
     private static final Entry[] ENTRIES = {
     };
 
     /**
-     * Tests {@code relate(Geometry, Geometry, int)} on all declared test 
cases.
+     * Tests {@code relate(Geometry, Geometry, DE9IM)} on all declared test 
cases.
      */
     @Test
     public void testRelate() {
@@ -70,45 +71,4 @@ public class RelateTest {
             }
         }
     }
-
-    /**
-     * The inputs and expected result of a single test of {@code 
relate(Geometry, Geometry, String)}.
-     *
-     * @param input    the geometry on which the operation is invoked.
-     * @param other    the other operand.
-     * @param matrix   the intersection matrix pattern.
-     * @param expected the expected result, or {@code null} if an exception is 
expected.
-     * @param error    the type of the expected exception, or {@code null} if 
the operation should succeed.
-     */
-    private record PatternEntry(Geometry input,
-                                Geometry other,
-                                String matrix,
-                                Boolean expected,
-                                Class<? extends Exception> error)
-    {
-    }
-
-    /**
-     * All test cases of {@code relate(Geometry, Geometry, String)}.
-     */
-    private static final PatternEntry[] PATTERN_ENTRIES = {
-    };
-
-    /**
-     * Tests {@code relate(Geometry, Geometry, String)} on all declared test 
cases.
-     */
-    @Test
-    public void testRelateByPattern() {
-        for (final PatternEntry entry : PATTERN_ENTRIES) {
-            try {
-                final boolean result = new 
GeometryProcessor().relate(entry.input(), entry.other(), entry.matrix());
-                assertNull(entry.error(), "An exception was expected.");
-                assertEquals(entry.expected(), result);
-            } catch (Exception ex) {
-                if (entry.error() == null || !entry.error().isInstance(ex)) {
-                    throw new AssertionError("Unexpected exception for " + 
entry, ex);
-                }
-            }
-        }
-    }
 }

Reply via email to