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 0a68f08237 feat(Geometry): add adapter from AWT Shape to SIS Geometry
0a68f08237 is described below
commit 0a68f08237c2a00ae77b3b7a9e6e0b9c1154088b
Author: jsorel <[email protected]>
AuthorDate: Wed Aug 26 16:56:48 2026 +0200
feat(Geometry): add adapter from AWT Shape to SIS Geometry
---
.../main/org/apache/sis/geometries/Empty.java | 8 +
.../main/org/apache/sis/geometries/Geometries.java | 20 +-
.../org/apache/sis/geometries/GeometryFactory.java | 17 ++
.../apache/sis/geometries/PolyhedralSurface.java | 11 +
.../org/apache/sis/geometries/PreparedTIN.java | 2 +
.../main/org/apache/sis/geometries/Sphere.java | 5 +
.../main/org/apache/sis/geometries/Surface.java | 4 +-
.../apache/sis/geometries/adapter/JTSAdapter.java | 6 +-
.../sis/geometries/adapter/ShapeConverter.java | 331 +++++++++++++++++++++
.../geometries/internal/shared/DefaultEmpty.java | 59 ++++
.../geometries/internal/shared/DefaultPolygon.java | 7 +
.../internal/shared/DefaultRawMultiPoint.java | 58 ++++
.../geometries/operation/GeometryProcessor.java | 60 +++-
.../sis/geometries/adapter/ShapeConverterTest.java | 231 ++++++++++++++
14 files changed, 797 insertions(+), 22 deletions(-)
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Empty.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Empty.java
index 99f92b0be2..997fdbf91d 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Empty.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Empty.java
@@ -27,8 +27,16 @@ import org.opengis.annotation.UML;
@UML(identifier="Empty", specification=ISO_19107) // section 6.4.10
public interface Empty extends Geometry{
+ public static final String TYPE = "EMPTY";
+
@Override
default boolean isEmpty() {
return true;
}
+
+ @Override
+ public default String getGeometryType() {
+ return TYPE;
+ }
+
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometries.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometries.java
index 30fc748a1b..14b3b34c7f 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometries.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometries.java
@@ -28,11 +28,13 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Objects;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import javax.measure.Unit;
import org.apache.sis.geometries.adapter.JTSAdapter;
import org.apache.sis.geometries.adapter.ShapeAdapter;
+import org.apache.sis.geometries.adapter.ShapeConverter;
import org.opengis.geometry.Envelope;
import org.opengis.referencing.IdentifiedObject;
import static org.opengis.referencing.IdentifiedObject.ALIAS_KEY;
@@ -76,6 +78,8 @@ import org.apache.sis.util.SimpleInternationalString;
*/
public final class Geometries {
+ private static final org.locationtech.jts.geom.GeometryFactory JTS_FACTORY
= new org.locationtech.jts.geom.GeometryFactory();
+
private static final CoordinateReferenceSystem UNDEFINED_CRS_1D =
createUndefined(1);
private static final CoordinateReferenceSystem UNDEFINED_CRS_2D =
createUndefined(2);
private static final CoordinateReferenceSystem UNDEFINED_CRS_3D =
createUndefined(3);
@@ -722,10 +726,11 @@ public final class Geometries {
* View a geometry as a JTS geometry.
*
* @param copy if true create a copy of the point sequence, otherwise
create a view
+ * @param gf JTS factory or null for default
* @return JTS equivalent
*/
public static org.locationtech.jts.geom.Geometry asJTS(Geometry geom,
boolean copy, org.locationtech.jts.geom.GeometryFactory gf) {
- return JTSAdapter.asJTS(geom, copy, gf);
+ return JTSAdapter.asJTS(geom, copy, gf == null ? JTS_FACTORY : gf);
}
/**
@@ -738,4 +743,17 @@ public final class Geometries {
// Null value check in the invoked constructor.
return new ShapeAdapter(geometry);
}
+
+ /**
+ * Converts a Java2D shape to a SIS geometry. If the given shape is a view
created by {@link #asShape(Geometry)},
+ * then the original geometry is returned. Otherwise a new geometry is
created with a copy (not a view) of the
+ * shape coordinates.
+ *
+ * @param shape the Java2D shape to convert. Cannot be {@code null}.
+ * @param flatness the maximum distance that line segments are allowed
to deviate from curves.
+ * @return SIS geometry with shape coordinates. Never null but can be
empty.
+ */
+ public static Geometry fromAWT(final Shape shape, final double flatness) {
+ return ShapeConverter.create(Objects.requireNonNull(shape), flatness);
+ }
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
index 5bdc74bfcf..73743486aa 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/GeometryFactory.java
@@ -28,6 +28,7 @@ import org.apache.sis.geometries.math.SampleSystem;
import org.apache.sis.geometries.math.NDArrays;
import org.apache.sis.geometries.math.Array;
import org.apache.sis.geometries.internal.shared.ArraySequence;
+import org.apache.sis.geometries.internal.shared.DefaultEmpty;
import org.apache.sis.geometries.internal.shared.DefaultGeometryCollection;
import org.apache.sis.geometries.internal.shared.DefaultLineString;
import org.apache.sis.geometries.internal.shared.DefaultLinearRing;
@@ -37,7 +38,9 @@ import
org.apache.sis.geometries.internal.shared.DefaultMultiPolygon;
import org.apache.sis.geometries.internal.shared.DefaultMultiSurface;
import org.apache.sis.geometries.internal.shared.DefaultPoint;
import org.apache.sis.geometries.internal.shared.DefaultPolygon;
+import org.apache.sis.geometries.internal.shared.DefaultRawMultiPoint;
import org.apache.sis.geometries.internal.shared.DefaultTriangle;
+import org.apache.sis.geometries.math.DataType;
import org.apache.sis.geometries.spirals.Clothoid;
import org.apache.sis.geometry.wrapper.Capability;
import org.apache.sis.geometry.wrapper.Dimensions;
@@ -58,6 +61,16 @@ public final class GeometryFactory extends
org.apache.sis.geometry.wrapper.Geome
super(GeometryLibrary.SIS, Geometry.class, Point.class);
}
+ public static Empty createEmpty(CoordinateReferenceSystem crs) {
+ final AttributesType.Template attType = new AttributesType.Template();
+ attType.addOrReplaceAttribute(AttributesType.ATT_POSITION,
SampleSystem.of(crs), DataType.DOUBLE);
+ return new DefaultEmpty(attType);
+ }
+
+ public static Empty createEmpty(AttributesType attType) {
+ return new DefaultEmpty(attType);
+ }
+
public static Point createPoint(CoordinateReferenceSystem crs) {
return new DefaultPoint(crs);
}
@@ -94,6 +107,10 @@ public final class GeometryFactory extends
org.apache.sis.geometry.wrapper.Geome
return new DefaultMultiPoint(sequence);
}
+ public static MultiPoint createMultiPoint(Point ... geometries) {
+ return new DefaultRawMultiPoint(geometries);
+ }
+
public static MultiLineString createMultiLineString(LineString ...
geometries) {
return new DefaultMultiLineString(geometries);
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PolyhedralSurface.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PolyhedralSurface.java
index 170acdedc3..4c3b9adc0f 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PolyhedralSurface.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PolyhedralSurface.java
@@ -16,8 +16,12 @@
*/
package org.apache.sis.geometries;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
import static org.opengis.annotation.Specification.ISO_19107;
import org.opengis.annotation.UML;
+import org.opengis.referencing.operation.TransformException;
/**
@@ -73,6 +77,13 @@ public interface PolyhedralSurface<T extends Polygon>
extends /*GeometryCollecti
*/
T getPatchN(int n);
+ @Override
+ public default double getArea() {
+ try (Stream<Surface> stream = IntStream.range(0,
getNumPatches()).mapToObj(this::getPatchN)) {
+ return stream.collect(Collectors.summingDouble(Surface::getArea));
+ }
+ }
+
/**
* Returns the collection of polygons in this surface that bounds the
given polygon “p” for any polygon “p” in the surface.
*
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PreparedTIN.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PreparedTIN.java
index 2ed3948a97..f213314df9 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PreparedTIN.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/PreparedTIN.java
@@ -21,6 +21,8 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
+import java.util.function.ToDoubleFunction;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.locationtech.jts.index.quadtree.Quadtree;
import org.opengis.coverage.CannotEvaluateException;
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Sphere.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Sphere.java
index e480e8998e..14f08876f7 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Sphere.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Sphere.java
@@ -87,6 +87,11 @@ public final class Sphere extends AbstractGeometry
implements ParametricCurveSur
return false;
}
+ @Override
+ public double getArea() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
@Override
public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs)
throws IllegalArgumentException {
if (cs.getCoordinateSystem().getDimension() !=
getCoordinateReferenceSystem().getCoordinateSystem().getDimension()) {
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
index 11162926a9..aec63bdf93 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Surface.java
@@ -60,9 +60,7 @@ public interface Surface extends Orientable {
* @return area of the surface.
*/
@UML(identifier="area", specification=ISO_19107) // section 6.4.25.7
- default double getArea() {
- throw new UnsupportedOperationException();
- }
+ double getArea();
/**
* The mathematical centroid for this Surface as a Point.
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/JTSAdapter.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/JTSAdapter.java
index 78e2462072..bed13ebfc0 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/JTSAdapter.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/JTSAdapter.java
@@ -88,11 +88,11 @@ public final class JTSAdapter {
return GeometryFactory.createPolygon(exterior, interiors);
} else if (jts instanceof org.locationtech.jts.geom.MultiPolygon cdt) {
- final Surface[] geoms = new Surface[cdt.getNumGeometries()];
+ final Polygon[] geoms = new Polygon[cdt.getNumGeometries()];
for (int i = 0; i < geoms.length; i++) {
- geoms[i] = (Surface) fromJTS(cdt.getGeometryN(i), crs, copy);
+ geoms[i] = (Polygon) fromJTS(cdt.getGeometryN(i), crs, copy);
}
- return GeometryFactory.createMultiSurface(geoms);
+ return GeometryFactory.createMultiPolygon(geoms);
} else if (jts instanceof org.locationtech.jts.geom.GeometryCollection
cdt) {
final Geometry[] geoms = new Geometry[cdt.getNumGeometries()];
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/ShapeConverter.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/ShapeConverter.java
new file mode 100644
index 0000000000..7546d296a1
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/ShapeConverter.java
@@ -0,0 +1,331 @@
+/*
+ * 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.adapter;
+
+import java.awt.Shape;
+import java.util.List;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.awt.geom.PathIterator;
+import java.awt.geom.IllegalPathStateException;
+import org.apache.sis.geometries.Geometries;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.GeometryFactory;
+import org.apache.sis.geometries.LineString;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.PointSequence;
+import org.apache.sis.geometries.Surface;
+import org.apache.sis.geometries.internal.shared.ArraySequence;
+import org.apache.sis.geometries.math.Array;
+import org.apache.sis.geometries.math.NDArrays;
+import org.apache.sis.geometries.math.SampleSystem;
+import org.apache.sis.referencing.internal.shared.AbstractShape;
+
+
+/**
+ * Converts a Java2D {@link Shape} to a SIS {@link Geometry}.
+ * Two subclasses exist depending on whether the geometries will store
+ * coordinates as {@code float} or {@code double} floating point numbers.
+ *
+ * @author Johann Sorel (Puzzle-GIS, Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public abstract class ShapeConverter {
+
+ private static final int DIMENSION = 2;
+
+ /**
+ * Initial number of coordinate values that the buffer can hold.
+ * The buffer capacity will be expanded as needed.
+ */
+ private static final int INITIAL_CAPACITY = 64;
+
+ /**
+ * Bit mask of the kind of geometric objects created.
+ * Used for detecting if all objects are of the same type.
+ *
+ * @see #geometryType
+ */
+ private static final int POINT = 1, LINESTRING = 2, POLYGON = 4;
+
+ /**
+ * All geometries that are component of a multi-geometries.
+ * The above masks tell if the geometry can be built as a multi-line
strings or multi-points.
+ */
+ private final List<Geometry> geometries;
+
+ /**
+ * Iterator over the coordinates of the Java2D shape to convert to a SIS
geometry.
+ */
+ protected final PathIterator iterator;
+
+ /**
+ * Number of values in the {@code float[]} or {@code double[]} array
stored by sub-class.
+ */
+ protected int length;
+
+ /**
+ * Bitmask combination of the type of all geometries built.
+ * This is a combination of {@link #POINT}, {@link #LINESTRING} and/or
{@link #POLYGON}.
+ */
+ private int geometryType;
+
+ /**
+ * Creates a new converter from Java2D shape to SIS geometry.
+ *
+ * @param iterator iterator over the coordinates of the Java2D shape to
convert to a SIS geometry.
+ */
+ ShapeConverter(final PathIterator iterator) {
+ this.iterator = iterator;
+ this.geometries = new ArrayList<>();
+ }
+
+ /**
+ * Converts a Java2D Shape to a SIS geometry.
+ * Coordinates are copies; this is not a view.
+ *
+ * @param shape the Java2D shape to convert. Cannot be {@code null}.
+ * @param flatness the maximum distance that line segments are allowed
to deviate from curves.
+ * @return SIS geometry with shape coordinates. Never null but can be
empty.
+ */
+ public static Geometry create(final Shape shape, final double flatness) {
+ if (shape instanceof ShapeAdapter) {
+ return ((ShapeAdapter) shape).geometry;
+ }
+ final PathIterator iterator = shape.getPathIterator(null, flatness);
+ final ShapeConverter converter;
+ if (AbstractShape.isFloat(shape)) {
+ converter = new ShapeConverter.Float(iterator);
+ } else {
+ converter = new ShapeConverter.Double(iterator);
+ }
+ return converter.build();
+ }
+
+ /**
+ * A converter of Java2D {@link Shape} to a SIS {@link Geometry}
+ * storing coordinates as {@code double} values.
+ */
+ private static final class Double extends ShapeConverter {
+ /** A temporary array for the transfer of coordinate values. */
+ private final double[] vertex;
+
+ /** Coordinate of current geometry. The number of valid values is
{@link #length}. */
+ private double[] buffer;
+
+ /** Creates a new converter for the given path iterator. */
+ Double(final PathIterator iterator) {
+ super(iterator);
+ vertex = new double[6];
+ buffer = new double[INITIAL_CAPACITY];
+ }
+
+ /** Delegates to {@link PathIterator#currentSegment(double[])}. */
+ @Override int currentSegment() {
+ return iterator.currentSegment(vertex);
+ }
+
+ /** Stores the single point obtained by the last call to {@link
#currentSegment()}. */
+ @Override void addPoint() {
+ addPoint(vertex);
+ }
+
+ /** Implementation of {@link #addPoint()} shared with {@link
#toSequence(boolean)}. */
+ private void addPoint(final double[] source) {
+ if (length >= buffer.length) {
+ buffer = Arrays.copyOf(buffer, length * 2);
+ }
+ System.arraycopy(source, 0, buffer, length, DIMENSION);
+ length += DIMENSION;
+ }
+
+ /** Returns a copy of current coordinate values as a SIS coordinate
sequence. */
+ @Override PointSequence toSequence(final boolean close) {
+ if (close && !Arrays.equals(buffer, 0, 2, buffer, length - 2,
length)) {
+ addPoint(buffer);
+ }
+
+ final Array array = NDArrays.of(SampleSystem.cartesian(2),
Arrays.copyOf(buffer, length));
+ return new ArraySequence(array);
+ }
+ }
+
+ /**
+ * A converter of Java2D {@link Shape} to a SIS {@link Geometry}
+ * storing coordinates as {@code float} values.
+ */
+ private static final class Float extends ShapeConverter {
+ /** A temporary array for the transfer of coordinate values. */
+ private final float[] vertex;
+
+ /** Coordinate of current geometry. The number of valid values is
{@link #length}. */
+ private float[] buffer;
+
+ /** Creates a new converter for the given path iterator. */
+ Float(final PathIterator iterator) {
+ super(iterator);
+ vertex = new float[6];
+ buffer = new float[INITIAL_CAPACITY];
+ }
+
+ /** Delegates to {@link PathIterator#currentSegment(float[])}. */
+ @Override int currentSegment() {
+ return iterator.currentSegment(vertex);
+ }
+
+ /** Stores the single point obtained by the last call to {@link
#currentSegment()}. */
+ @Override void addPoint() {
+ addPoint(vertex);
+ }
+
+ /** Implementation of {@link #addPoint()} shared with {@link
#toSequence(boolean)}. */
+ private void addPoint(final float[] source) {
+ if (length >= buffer.length) {
+ buffer = Arrays.copyOf(buffer, length * 2);
+ }
+ System.arraycopy(source, 0, buffer, length, DIMENSION);
+ length += DIMENSION;
+ }
+
+ /** Returns a copy of current coordinate values as a SIS coordinate
sequence. */
+ @Override PointSequence toSequence(final boolean close) {
+ if (close && !Arrays.equals(buffer, 0, 2, buffer, length - 2,
length)) {
+ addPoint(buffer);
+ }
+ final Array array = NDArrays.of(SampleSystem.cartesian(2),
Arrays.copyOf(buffer, length));
+ return new ArraySequence(array);
+ }
+ }
+
+ /**
+ * Returns the coordinates and type of the current path segment in the
iteration.
+ * This method delegate to one of the two {@code
PathIterator.currentSegment(…)}
+ * methods, depending on the precision of floating-point values.
+ */
+ abstract int currentSegment();
+
+ /**
+ * Stores the single point obtained by the last call to {@link
#currentSegment()}.
+ * As a consequence, {@link #length} is increased by {@value #DIMENSION}.
+ */
+ abstract void addPoint();
+
+ /**
+ * Returns a copy of current coordinate values as a SIS coordinate
sequence.
+ * The number of values to copy in a new array is {@link #length}.
+ *
+ * @param close whether to ensure that the first point is repeated as
the last point.
+ * @return a SIS coordinate sequence containing a copy of current
coordinate values.
+ */
+ abstract PointSequence toSequence(boolean close);
+
+ /**
+ * Iterates over all coordinates given by the {@link #iterator} and stores
them in a SIS geometry.
+ * The path shall contain only straight lines. Curves are not supported
yet.
+ * The geometry will be constrained to two-dimensional coordinate tuples.
+ */
+ private Geometry build() {
+ while (!iterator.isDone()) {
+ switch (currentSegment()) {
+ case PathIterator.SEG_MOVETO: {
+ flush(false);
+ addPoint();
+ break;
+ }
+ case PathIterator.SEG_LINETO: {
+ if (length == 0) {
+ throw new IllegalPathStateException("LINETO without
previous MOVETO.");
+ }
+ addPoint();
+ break;
+ }
+ case PathIterator.SEG_CLOSE: {
+ flush(true);
+ break;
+ }
+ default: {
+ throw new IllegalPathStateException("Must contain only
flat segments.");
+ }
+ }
+ iterator.next();
+ }
+ flush(false);
+ final int count = geometries.size();
+ if (count == 1) {
+ return geometries.get(0);
+ }
+
+ switch (geometryType) {
+ case 0: return
GeometryFactory.INSTANCE.createEmpty(Geometries.getUndefinedCRS(DIMENSION));
+ default: return
GeometryFactory.INSTANCE.createGeometryCollection(geometries.toArray(Geometry[]::new));
+ case POINT: return
GeometryFactory.INSTANCE.createMultiPoint(geometries.toArray(Point[]::new));
+ case LINESTRING: return
GeometryFactory.INSTANCE.createMultiLineString(geometries.toArray(LineString[]::new));
+ case POLYGON: break;
+ }
+ /*
+ * Java2D shapes and SIS geometries differ in their way to fill
interior.
+ * Java2D fills the resulting contour based on visual winding rules.
+ * SIS has a system where outer shell and holes are clearly separated.
+ * We would need to draw contours as Java2D for computing SIS
equivalent,
+ * but it would require a lot of work. In the meantime, the
SymDifference
+ * operation is what behave the most like EVEN_ODD or NON_ZERO winding
rules.
+ */
+ // Sort by area, bigger geometries are the outter rings.
+ geometries.sort((Geometry o1, Geometry o2) -> {
+ double area1 = (o1 instanceof Surface s) ? s.getArea() : 0.0;
+ double area2 = (o2 instanceof Surface s) ? s.getArea() : 0.0;
+ return java.lang.Double.compare(area2, area1);
+ });
+
+ Geometry result = geometries.get(0);
+ for (int i=1; i<count; i++) {
+ Geometry other = geometries.get(i);
+ if (result.intersects(other)) {
+ result = result.symDifference(other); // Ring is a hole.
+ } else {
+ result = result.union(other); // Ring is a separate
polygon.
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Copies current coordinates in a new SIS geometry,
+ * then resets {@link #length} to 0 in preparation for the next geometry.
+ *
+ * @param isRing whether the geometry should be a closed polygon.
+ */
+ private void flush(final boolean isRing) {
+ if (length != 0) {
+ Geometry geometry;
+ if (length == DIMENSION) {
+ geometry =
GeometryFactory.INSTANCE.createPoint(toSequence(false));
+ geometryType |= POINT;
+ } else {
+ if (isRing) {
+ geometry =
GeometryFactory.INSTANCE.createPolygon(GeometryFactory.INSTANCE.createLinearRing(toSequence(true)),
null);
+ geometryType |= POLYGON;
+ } else {
+ geometry =
GeometryFactory.INSTANCE.createLineString(toSequence(false));
+ geometryType |= LINESTRING;
+ }
+ }
+ geometries.add(geometry);
+ length = 0;
+ }
+ }
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEmpty.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEmpty.java
new file mode 100644
index 0000000000..5308308ba3
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultEmpty.java
@@ -0,0 +1,59 @@
+/*
+ * 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.internal.shared;
+
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Empty;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultEmpty extends AbstractGeometry implements Empty {
+
+ private final AttributesType attType;
+
+ public DefaultEmpty(AttributesType attType) {
+ this.attType = attType;
+ }
+
+ @Override
+ public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return
attType.getAttributeSystem(AttributesType.ATT_POSITION).getCoordinateReferenceSystem();
+ }
+
+ @Override
+ public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs)
throws IllegalArgumentException {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public AttributesType getAttributesType() {
+ return attType;
+ }
+
+ @Override
+ public Envelope getEnvelope() {
+ final GeneralEnvelope env = new
GeneralEnvelope(getCoordinateReferenceSystem());
+ env.setToNaN();
+ return env;
+ }
+
+}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java
index b2cad90d70..9e7826fc0a 100644
---
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultPolygon.java
@@ -24,6 +24,7 @@ import org.apache.sis.geometries.Curve;
import org.apache.sis.geometries.Geometries;
import org.apache.sis.geometries.LinearRing;
import org.apache.sis.geometries.Polygon;
+import org.locationtech.jts.geom.GeometryFactory;
/**
@@ -82,4 +83,10 @@ public class DefaultPolygon extends AbstractGeometry
implements Polygon {
return getExteriorRing().getEnvelope();
}
+ @Override
+ public double getArea() {
+ //TODO : fallback on JTS until implemented
+ return Geometries.asJTS(this, false, new GeometryFactory()).getArea();
+ }
+
}
diff --git
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
new file mode 100644
index 0000000000..ad9d28f720
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultRawMultiPoint.java
@@ -0,0 +1,58 @@
+/*
+ * 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.internal.shared;
+
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.MultiPoint;
+import org.apache.sis.geometries.Point;
+
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultRawMultiPoint extends AbstractGeometry implements
MultiPoint<Point> {
+
+ private final Point[] geometries;
+
+ public DefaultRawMultiPoint(Point[] geometries) {
+ this.geometries = geometries;
+ }
+
+ @Override
+ public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return geometries[0].getCoordinateReferenceSystem();
+ }
+
+ @Override
+ public void setCoordinateReferenceSystem(CoordinateReferenceSystem cs)
throws IllegalArgumentException {
+ for (Point c : geometries) {
+ c.setCoordinateReferenceSystem(cs);
+ }
+ }
+
+ @Override
+ public int getNumGeometries() {
+ return geometries.length;
+ }
+
+ @Override
+ public Point getGeometryN(int n) {
+ return geometries[n];
+ }
+
+}
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 360becd4f0..721d5c97c9 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.Length;
import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Geometries;
import org.apache.sis.geometries.Geometry;
import org.apache.sis.geometries.GeometryCollection;
import org.apache.sis.geometries.LineString;
@@ -75,7 +76,9 @@ public final class GeometryProcessor {
* but it should be near the resolution of the coordinates used.
*/
public Geometry buffer(Geometry geom, double distance) throws
OperationException {
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented, this loss the attributes !
+ return Geometries.fromJTS(jts(geom).buffer(distance), true);
}
@UML(identifier="buffer", specification=ISO_19107) // section 6.4.4.24 and
6.4.8.3
@@ -91,7 +94,9 @@ public final class GeometryProcessor {
*/
//@UML(identifier="3DconvexHull", specification=ISO_19107) // section 6.4.9
public Geometry convexHull(Geometry geom) throws OperationException {
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented, this loss the attributes !
+ return Geometries.fromJTS(jts(geom).convexHull(), true);
}
/**
@@ -100,7 +105,9 @@ public final class GeometryProcessor {
@UML(identifier="difference", specification=ISO_19107) // section 6.4.4.30
and 6.4.8.5
//@UML(identifier="3Ddifference", specification=ISO_19107) // section 6.4.9
public Geometry difference(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented, this loss the attributes !
+ return Geometries.fromJTS(jts(geom1).difference(jts(geom2)), true);
}
/**
@@ -140,7 +147,8 @@ public final class GeometryProcessor {
}
}
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented, this loss the attributes !
+ return Geometries.fromJTS(jts(geom1).intersection(jts(geom2)), true);
}
/**
@@ -150,7 +158,9 @@ public final class GeometryProcessor {
@UML(identifier="symDifference", specification=ISO_19107) // section
6.4.4.30 and 6.4.8.6
//@UML(identifier="3DsymDifference", specification=ISO_19107) // section
6.4.9
public Geometry symDifference(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented, this loss the attributes !
+ return Geometries.fromJTS(jts(geom1).symDifference(jts(geom2)), true);
}
/**
@@ -159,7 +169,9 @@ public final class GeometryProcessor {
@UML(identifier="union", specification=ISO_19107) // section 6.4.4.30 and
6.4.8.7
//@UML(identifier="3Dunion", specification=ISO_19107) // section 6.4.9
public Geometry union(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented, this loss the attributes !
+ return Geometries.fromJTS(jts(geom1).union(jts(geom2)), true);
}
@UML(identifier="contains", specification=ISO_19107) // section 6.4.4.30 ?
@@ -178,7 +190,9 @@ public final class GeometryProcessor {
return Contains.contains(polygon, pt);
}
}
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).contains(jts(geom2));
}
/**
@@ -187,7 +201,8 @@ public final class GeometryProcessor {
@UML(identifier="crosses", specification=ISO_19107) // section 6.4.8.8
//@UML(identifier="3Dcrosses", specification=ISO_19107) // section 6.4.9
public boolean crosses(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).crosses(jts(geom2));
}
/**
@@ -196,7 +211,8 @@ public final class GeometryProcessor {
@UML(identifier="disjoint", specification=ISO_19107) // section 6.4.8.8
//@UML(identifier="3Ddisjoint", specification=ISO_19107) // section 6.4.9
public boolean disjoint(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).disjoint(jts(geom2));
}
/**
@@ -205,7 +221,8 @@ public final class GeometryProcessor {
@UML(identifier="equals", specification=ISO_19107) // section 6.4.8.8,
6.4.4.30
//@UML(identifier="3Dequals", specification=ISO_19107) // section 6.4.9
public boolean equal(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).equals(jts(geom2));
}
/**
@@ -214,7 +231,8 @@ public final class GeometryProcessor {
@UML(identifier="intersects", specification=ISO_19107) // section 6.4.8.8,
6.4.4.30
//@UML(identifier="3Dintersects", specification=ISO_19107) // section 6.4.9
public boolean intersects(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).intersects(jts(geom2));
}
/**
@@ -243,7 +261,8 @@ public final class GeometryProcessor {
@UML(identifier="overlaps", specification=ISO_19107) // section 6.4.8.8
//@UML(identifier="3Doverlaps", specification=ISO_19107) // section 6.4.9
public boolean overlaps(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).overlaps(jts(geom2));
}
/**
@@ -261,7 +280,8 @@ public final class GeometryProcessor {
@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 {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).relate(jts(geom2), matrix);
}
/**
@@ -270,7 +290,8 @@ public final class GeometryProcessor {
@UML(identifier="touches", specification=ISO_19107) // section 6.4.8.8
//@UML(identifier="3Dtouches", specification=ISO_19107) // section 6.4.9
public boolean touches(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).touches(jts(geom2));
}
/**
@@ -279,7 +300,9 @@ public final class GeometryProcessor {
@UML(identifier="within", specification=ISO_19107) // section 6.4.8.8
//@UML(identifier="3Dwithin", specification=ISO_19107) // section 6.4.9
public boolean within(Geometry geom1, Geometry geom2) throws
OperationException {
- throw new UnsupportedOperationException();
+
+ //TODO : fallback on JTS until implemented
+ return jts(geom1).within(jts(geom2));
}
@UML(identifier="withinDistance", specification=ISO_19107) // section
6.4.8.8
@@ -458,4 +481,11 @@ public final class GeometryProcessor {
}
return sep;
}
+
+ /**
+ * TODO fallback on JTS until we implemetend all methods.
+ */
+ private static org.locationtech.jts.geom.Geometry jts(Geometry geom) {
+ return Geometries.asJTS(geom, false, null);
+ }
}
diff --git
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/ShapeConverterTest.java
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/ShapeConverterTest.java
new file mode 100644
index 0000000000..d495285fb0
--- /dev/null
+++
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/ShapeConverterTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.adapter;
+
+import java.util.Arrays;
+import java.awt.Shape;
+import java.awt.Graphics2D;
+import java.awt.Font;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.awt.geom.Area;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Line2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.sis.geometries.Empty;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.GeometryCollection;
+import org.apache.sis.geometries.LineString;
+import org.apache.sis.geometries.LinearRing;
+import org.apache.sis.geometries.MultiPolygon;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.PointSequence;
+import org.apache.sis.geometries.Polygon;
+import org.apache.sis.geometries.math.Tuple;
+import org.apache.sis.geometries.math.Vector2D;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.opengis.geometry.Envelope;
+
+
+/**
+ * Tests {@link ShapeConverter}.
+ *
+ * @author Johann Sorel (Puzzle-GIS, Geomatys)
+ */
+public final class ShapeConverterTest {
+
+ /**
+ * Creates a new test case.
+ */
+ public ShapeConverterTest() {
+ }
+
+ /**
+ * Verifies that the given geometry is an instance of the expected class
+ * and contains the expected coordinate values.
+ *
+ * @param shape the Java2D shape to convert with {@link
ShapeConverter}.
+ * @param type expected class of the actual geometry.
+ * @param expected expected coordinates of the actual geometry.
+ */
+ private static void assertCoordinatesEqual(final Shape shape, final
Class<?> type, final Tuple<?>... expected) {
+ assertCoordinatesEqual(ShapeConverter.create(shape, 0.0001), type,
expected);
+ }
+
+ /**
+ * Verifies that the given geometry is an instance of the expected class
+ * and contains the expected coordinate values.
+ *
+ * @param geometry the SIS geometry to test.
+ * @param type expected class of the actual geometry.
+ * @param expected expected coordinates of the actual geometry.
+ */
+ private static void assertCoordinatesEqual(final Geometry geometry, final
Class<?> type, final Tuple<?>... expected) {
+ assertInstanceOf(type, geometry, "Geometry class");
+ assertArrayEquals(expected, getCoordinates(geometry), "Coordinates");
+ }
+
+ private static Tuple[] getCoordinates(Geometry geom) {
+ final List<Tuple> lst = new ArrayList();
+ getCoordinates(geom, lst);
+ return lst.toArray(Tuple[]::new);
+ }
+
+ private static void getCoordinates(Geometry geom, List<Tuple> lst) {
+ if (geom instanceof Empty) {
+ //nothing
+ } else if (geom instanceof Point pt) {
+ lst.add(new Vector2D.Double(pt.getPosition().toArrayDouble()));
+ } else if (geom instanceof LineString ls) {
+ PointSequence ps = ls.getPoints();
+ for (int i = 0, n = ps.size(); i < n; i++) {
+ lst.add(new
Vector2D.Double(ps.getPosition(i).toArrayDouble()));
+ }
+ } else if (geom instanceof Polygon pl) {
+ getCoordinates(pl.getExteriorRing(), lst);
+ for (int i = 0, n = pl.getNumInteriorRing(); i < n; i++) {
+ getCoordinates(pl.getInteriorRingN(i), lst);
+ }
+ } else if (geom instanceof GeometryCollection col) {
+ for (int i = 0, n = col.getNumGeometries(); i < n; i++) {
+ getCoordinates(col.getGeometryN(i), lst);
+ }
+ } else {
+ throw new UnsupportedOperationException("Unsuported geometry type
" + geom);
+ }
+ }
+
+ /**
+ * Tests {@link ShapeConverter} with a point.
+ */
+ @Test
+ public void testPoint() {
+ final var shape = new GeneralPath();
+ shape.moveTo(10, 20);
+ assertCoordinatesEqual(shape, Point.class,
+ new Vector2D.Double(10, 20));
+ }
+
+ /**
+ * Tests {@link ShapeConverter} with a line.
+ */
+ @Test
+ public void testLine() {
+ final var shape = new Line2D.Double(1, 2, 3, 4);
+ assertCoordinatesEqual(shape, LineString.class,
+ new Vector2D.Double(1, 2),
+ new Vector2D.Double(3, 4));
+ }
+
+ /**
+ * Tests {@link ShapeConverter} with a rectangle.
+ */
+ @Test
+ public void testRectangle() {
+ final var shape = new Rectangle2D.Double(1, 2, 10, 20);
+ assertCoordinatesEqual(shape, Polygon.class,
+ new Vector2D.Double( 1, 2),
+ new Vector2D.Double(11, 2),
+ new Vector2D.Double(11, 22),
+ new Vector2D.Double( 1, 22),
+ new Vector2D.Double( 1, 2));
+ }
+
+ /**
+ * Tests {@link ShapeConverter} with a rectangle with a hole shape.
+ */
+ @Test
+ public void testRectangleWithHole() {
+ final var contour = new Rectangle2D.Double(1, 2, 10, 20);
+ final var hole = new Rectangle2D.Double(5, 6, 2, 3);
+ final var shape = new Area(contour);
+ shape.subtract(new Area(hole));
+
+ final Geometry geometry = ShapeConverter.create(shape, 0.0001);
+ final Polygon polygon = assertInstanceOf(Polygon.class, geometry);
+ assertEquals(1, polygon.getNumInteriorRing());
+
+ assertCoordinatesEqual(polygon.getExteriorRing(), LinearRing.class,
+ new Vector2D.Double(1, 2),
+ new Vector2D.Double(1, 22),
+ new Vector2D.Double(11, 22),
+ new Vector2D.Double(11, 2),
+ new Vector2D.Double(1, 2));
+
+ assertCoordinatesEqual(polygon.getInteriorRingN(0), LinearRing.class,
+ new Vector2D.Double(7, 6),
+ new Vector2D.Double(7, 9),
+ new Vector2D.Double(5, 9),
+ new Vector2D.Double(5, 6),
+ new Vector2D.Double(7, 6));
+ }
+
+ /**
+ * Tests {@link ShapeConverter} with the shape of an arbitrary text.
+ * We use that as an easy way to create relatively complex shapes.
+ * The arbitrary text is "Labi": 4 letters, 5 polygons (because "i" is made
+ * of 2 detached polygons), with 2 polygons ("a" and "b") having a hole.
+ */
+ @Test
+ public void testText() {
+ final Shape shape;
+ final Graphics2D handler = new BufferedImage(1, 1,
BufferedImage.TYPE_INT_ARGB).createGraphics();
+ try {
+ final FontRenderContext fontRenderContext =
handler.getFontRenderContext();
+ final Font font = new Font("Monospaced", Font.PLAIN, 12);
+ final GlyphVector glyphs =
font.createGlyphVector(fontRenderContext, "Labi");
+ shape = glyphs.getOutline();
+ } finally {
+ handler.dispose();
+ }
+ final Geometry geometry = ShapeConverter.create(shape, 0.1);
+ final MultiPolygon mp = assertInstanceOf(MultiPolygon.class, geometry);
+ /*
+ * The "Labi" text contains 4 characters but `i` is split in two
ploygons,
+ * for a total of 5 polygons. Two letters ("a" and "b") are polyogns
whith
+ * a hole inside them.
+ */
+ assertEquals(5, mp.getNumGeometries());
+ final var parts = new Geometry[mp.getNumGeometries()];
+ Arrays.setAll(parts, mp::getGeometryN);
+ Arrays.sort(parts, (Geometry o1, Geometry o2) -> //
Sort on X
+ Double.compare(o1.getEnvelope().getMinimum(0),
+ o2.getEnvelope().getMinimum(0)));
+
+ for (int i=0; i < parts.length; i++) {
+ final String message = "Glyph #" + i;
+ final Geometry glyph = parts[i];
+ final Polygon polygon = assertInstanceOf(Polygon.class, glyph,
message);
+ assertEquals((i == 1 || i == 2) ? 1 : 0,
polygon.getNumInteriorRing(), message); // Expect a hole in `a` and `b`.
+ }
+ /*
+ * Compare the bounding boxes.
+ */
+ final Rectangle2D bounds2D = shape.getBounds2D();
+ final Envelope env = geometry.getEnvelope();
+ assertEquals(bounds2D.getMinX(), env.getMinimum(0));
+ assertEquals(bounds2D.getMaxX(), env.getMaximum(0));
+ assertEquals(bounds2D.getMinY(), env.getMinimum(1));
+ assertEquals(bounds2D.getMaxY(), env.getMaximum(1));
+ }
+}