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);
- }
- }
- }
- }
}