You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2026/09/04 08:10:26 UTC
(sis) branch geoapi-4.0 updated: feat(Geometry): add ArcByBulge and ArcByCenterPoint
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 5fa9f0d37c feat(Geometry): add ArcByBulge and ArcByCenterPoint
5fa9f0d37c is described below
commit 5fa9f0d37c1c761f409d640c28569e80cf9d876d
Author: jsorel <jo...@geomatys.com>
AuthorDate: Fri Sep 4 10:10:08 2026 +0200
feat(Geometry): add ArcByBulge and ArcByCenterPoint
---
.../org/apache/sis/geometries/GeometryFactory.java | 34 +++
.../apache/sis/geometries/curve/ArcByBulge.java | 121 +++++++++++
.../sis/geometries/curve/ArcByCenterPoint.java | 127 +++++++++++
.../internal/shared/DefaultArcByBulge.java | 90 ++++++++
.../internal/shared/DefaultArcByCenterPoint.java | 111 ++++++++++
.../main/org/apache/sis/gml/GML3Reader.java | 240 ++++++++++++++++++++-
.../main/org/apache/sis/gml/GML3Tags.java | 22 ++
.../main/org/apache/sis/gml/GML3Writer.java | 75 ++++++-
.../test/org/apache/sis/gml/3/curve_arcbybulge.gml | 10 +
.../apache/sis/gml/3/curve_arcbycenterpoint.gml | 11 +
.../test/org/apache/sis/gml/GML3ReaderTest.java | 149 +++++++++++++
.../test/org/apache/sis/gml/GML3WriterTest.java | 54 +++++
.../test/org/apache/sis/gml/TestData.java | 2 +
13 files changed, 1027 insertions(+), 19 deletions(-)
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 b8748a9692..a973bc6b06 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
@@ -21,13 +21,19 @@ import java.nio.DoubleBuffer;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import javax.measure.Unit;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.curve.ArcByBulge;
+import org.apache.sis.geometries.curve.ArcByCenterPoint;
import org.apache.sis.geometries.conics.Circle;
import org.apache.sis.geometries.conics.CircularString;
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.math.Vector;
import org.apache.sis.geometries.internal.shared.ArraySequence;
+import org.apache.sis.geometries.internal.shared.DefaultArcByBulge;
+import org.apache.sis.geometries.internal.shared.DefaultArcByCenterPoint;
import org.apache.sis.geometries.internal.shared.DefaultCircularString;
import org.apache.sis.geometries.internal.shared.DefaultCompoundCurve;
import org.apache.sis.geometries.internal.shared.DefaultCurvePolygon;
@@ -190,6 +196,34 @@ public final class GeometryFactory extends org.apache.sis.geometry.wrapper.Geome
return new DefaultCircularString(sequence);
}
+ /**
+ * Creates a circular arc from the centre of its circle, that circle's radius expressed in the
+ * given unit, and the bearings at which the arc starts and ends.
+ *
+ * @param center centre of the circle the arc is a part of.
+ * @param radius radius of that circle, expressed in {@code radiusUnit}. Must be greater than zero.
+ * @param radiusUnit unit of {@code radius}, or {@code null} for the units of the coordinate system axes.
+ * @param startAngle bearing at which the arc starts, in decimal degrees.
+ * @param endAngle bearing at which the arc ends, in decimal degrees.
+ */
+ public static ArcByCenterPoint createArcByCenterPoint(Point center, double radius, Unit<?> radiusUnit,
+ double startAngle, double endAngle)
+ {
+ return new DefaultArcByCenterPoint(center, radius, radiusUnit, startAngle, endAngle);
+ }
+
+ /**
+ * Creates a circular arc from its two end points, the distance by which it bulges away from the
+ * chord joining them, and the direction of that bulge.
+ *
+ * @param points the start point followed by the end point. Its size must be exactly 2.
+ * @param bulge distance from the midpoint of the chord to the arc, along {@code normal}.
+ * @param normal direction the arc bulges towards, perpendicular to the chord.
+ */
+ public static ArcByBulge createArcByBulge(PointSequence points, double bulge, Vector<?> normal) {
+ return new DefaultArcByBulge(points, bulge, normal);
+ }
+
public static CurvePolygon createCurvePolygon(Curve exterior, List<Curve> interiors) {
return new DefaultCurvePolygon(exterior, interiors);
}
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ArcByBulge.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ArcByBulge.java
new file mode 100644
index 0000000000..ce907576ff
--- /dev/null
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ArcByBulge.java
@@ -0,0 +1,121 @@
+/*
+ * 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.curve;
+
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Curve;
+import org.apache.sis.geometries.CurveInterpolation;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.PointSequence;
+import org.apache.sis.geometries.math.Vector;
+
+
+/**
+ * A single circular arc described by its two end points, how far it bulges away from the chord
+ * joining them, and which side of the chord it bulges towards.
+ *
+ * <p>This is the parameterisation that GML calls {@code gml:ArcByBulge}. Compared with
+ * {@link CircularString}, which needs a third point <em>on</em> the arc, this one replaces that
+ * point by a scalar {@linkplain #getBulge() bulge} — the distance from the midpoint of the chord to
+ * the arc, measured along the {@linkplain #getNormal() normal}. The two carry the same information
+ * and neither is an approximation of the other, but converting between them means solving for the
+ * circle, so both are kept as they were written.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ * @see GML ArcByBulge
+ */
+public interface ArcByBulge extends Curve {
+
+ public static final String TYPE = "ARCBYBULGE";
+
+ @Override
+ public default String getGeometryType() {
+ return TYPE;
+ }
+
+ /**
+ * Returns the two end points of this arc: its start point followed by its end point.
+ *
+ * @return the start and end points, never null and always of size 2.
+ */
+ PointSequence getPoints();
+
+ /**
+ * Returns the distance from the midpoint of the chord joining the two end points to the arc,
+ * measured along the {@linkplain #getNormal() normal}. It is expressed in the units of the
+ * coordinate system axes.
+ *
+ * @return the bulge of this arc.
+ */
+ double getBulge();
+
+ /**
+ * Returns the direction the arc bulges towards, as a vector perpendicular to the chord joining
+ * the two end points.
+ *
+ * @return the normal to the chord, never null.
+ */
+ Vector<?> getNormal();
+
+ /**
+ * Returns {@link CurveInterpolation#CIRCULAR}.
+ */
+ @Override
+ default CurveInterpolation getInterpolation() {
+ return CurveInterpolation.CIRCULAR;
+ }
+
+ @Override
+ default CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return getPoints().getCoordinateReferenceSystem();
+ }
+
+ @Override
+ default void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) throws IllegalArgumentException {
+ getPoints().setCoordinateReferenceSystem(cs);
+ }
+
+ @Override
+ default AttributesType getAttributesType() {
+ return getPoints().getAttributesType();
+ }
+
+ @Override
+ default boolean isEmpty() {
+ return getPoints().isEmpty();
+ }
+
+ @Override
+ default Point getStartPoint() {
+ return getPoints().getPoint(0);
+ }
+
+ @Override
+ default Point getEndPoint() {
+ return getPoints().getPoint(getPoints().size() - 1);
+ }
+
+ /**
+ * Returns {@code false}: an arc joining two distinct end points cannot close on itself, and two
+ * identical end points would give a chord of zero length, for which no circle is determined.
+ */
+ @Override
+ default boolean isClosed() {
+ return false;
+ }
+}
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ArcByCenterPoint.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ArcByCenterPoint.java
new file mode 100644
index 0000000000..f05e413aed
--- /dev/null
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/ArcByCenterPoint.java
@@ -0,0 +1,127 @@
+/*
+ * 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.curve;
+
+import javax.measure.Unit;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Curve;
+import org.apache.sis.geometries.CurveInterpolation;
+import org.apache.sis.geometries.Point;
+
+
+/**
+ * A single circular arc described by the centre of its circle, the radius of that circle and the
+ * angles at which the arc starts and ends.
+ *
+ * <p>This is the parameterisation that GML calls {@code gml:ArcByCenterPoint}, and it is a
+ * genuinely different one from {@link CircularString}'s: a circular string carries points
+ * <em>on</em> the curve and computes the circle from them, whereas here the circle is given and the
+ * points on the curve have to be computed from it. Neither can be converted to the other without
+ * either solving for a circle or evaluating trigonometric functions, which is why the two coexist
+ * rather than one being expressed in terms of the other.</p>
+ *
+ * <p>Because the arc is defined by a bearing sweep around a centre, this parameterisation is
+ * two-dimensional by nature; GML says as much.</p>
+ *
+ * @author Johann Sorel (Geomatys)
+ * @see GML ArcByCenterPoint
+ */
+public interface ArcByCenterPoint extends Curve {
+
+ public static final String TYPE = "ARCBYCENTERPOINT";
+
+ @Override
+ public default String getGeometryType() {
+ return TYPE;
+ }
+
+ /**
+ * Returns the centre of the circle this arc is a part of.
+ *
+ * @return the centre point, never null.
+ */
+ Point getCenter();
+
+ /**
+ * Returns the radius of the circle this arc is a part of,
+ * expressed in {@linkplain #getRadiusUnit() its unit}.
+ *
+ * @return the radius, always greater than zero.
+ */
+ double getRadius();
+
+ /**
+ * Returns the unit the {@linkplain #getRadius() radius} is expressed in, or {@code null} if
+ * unspecified. A {@code null} unit means that the radius is expressed in the units of the
+ * coordinate system axes, which is what a document declaring no unit leaves implied.
+ *
+ * <p>Unlike the angles, the radius is <em>not</em> normalised to a canonical unit: there is
+ * none. A radius is a length in the coordinate system this arc lives in, and that system may
+ * measure its axes in metres, in feet or in degrees of arc; converting to any one of those
+ * would be meaningless for the others.</p>
+ *
+ * @return the unit of the radius, or {@code null} if the radius is in coordinate system units.
+ */
+ Unit<?> getRadiusUnit();
+
+ /**
+ * Returns the bearing at which the arc starts, in decimal degrees.
+ *
+ * @return the start angle, in decimal degrees.
+ */
+ double getStartAngle();
+
+ /**
+ * Returns the bearing at which the arc ends, in decimal degrees.
+ *
+ * @return the end angle, in decimal degrees.
+ */
+ double getEndAngle();
+
+ /**
+ * Returns {@link CurveInterpolation#CIRCULAR}.
+ */
+ @Override
+ default CurveInterpolation getInterpolation() {
+ return CurveInterpolation.CIRCULAR;
+ }
+
+ @Override
+ default CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return getCenter().getCoordinateReferenceSystem();
+ }
+
+ @Override
+ default void setCoordinateReferenceSystem(CoordinateReferenceSystem cs) throws IllegalArgumentException {
+ getCenter().setCoordinateReferenceSystem(cs);
+ }
+
+ @Override
+ default AttributesType getAttributesType() {
+ return getCenter().getAttributesType();
+ }
+
+ /**
+ * Returns {@code false}: an arc by centre point always has a centre and a radius,
+ * so it is never the empty point set.
+ */
+ @Override
+ default boolean isEmpty() {
+ return false;
+ }
+}
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java
new file mode 100644
index 0000000000..b60451267d
--- /dev/null
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByBulge.java
@@ -0,0 +1,90 @@
+/*
+ * 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.geometry.Envelope;
+import org.apache.sis.geometries.PointSequence;
+import org.apache.sis.geometries.curve.ArcByBulge;
+import org.apache.sis.geometries.math.Vector;
+
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultArcByBulge extends AbstractGeometry implements ArcByBulge {
+
+ private final PointSequence points;
+ private final double bulge;
+ private final Vector<?> normal;
+
+ /**
+ * Creates an arc between the two given points.
+ *
+ * @param points the start point followed by the end point. Its size must be exactly 2.
+ * @param bulge distance from the midpoint of the chord to the arc, along {@code normal}.
+ * @param normal direction the arc bulges towards, perpendicular to the chord.
+ * @throws IllegalArgumentException if the number of points is not 2, if the bulge is not a
+ * real number, or if no normal is given.
+ */
+ public DefaultArcByBulge(final PointSequence points, final double bulge, final Vector<?> normal) {
+ if (points == null || points.size() != 2) {
+ throw new IllegalArgumentException("An arc by bulge is defined by exactly 2 points"
+ + " (its start and its end), but got " + ((points != null) ? points.size() : 0) + '.');
+ }
+ if (!Double.isFinite(bulge)) {
+ throw new IllegalArgumentException("The bulge of an arc must be a real number, but got " + bulge + '.');
+ }
+ if (normal == null) {
+ throw new IllegalArgumentException("An arc by bulge needs a normal: without it, the two"
+ + " arcs joining the end points cannot be told apart.");
+ }
+ this.points = points;
+ this.bulge = bulge;
+ this.normal = normal;
+ }
+
+ @Override
+ public PointSequence getPoints() {
+ return points;
+ }
+
+ @Override
+ public double getBulge() {
+ return bulge;
+ }
+
+ @Override
+ public Vector<?> getNormal() {
+ return normal;
+ }
+
+ @Override
+ public Envelope getEnvelope() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public String asText() {
+ final StringBuilder sb = new StringBuilder(TYPE).append(" (");
+ toText(sb, points);
+ sb.append(", BULGE ").append(bulge);
+ sb.append(", NORMAL ");
+ toText(sb, normal);
+ return sb.append(')').toString();
+ }
+}
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java
new file mode 100644
index 0000000000..e67a2a451c
--- /dev/null
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultArcByCenterPoint.java
@@ -0,0 +1,111 @@
+/*
+ * 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 javax.measure.Unit;
+import org.opengis.geometry.Envelope;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.curve.ArcByCenterPoint;
+
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DefaultArcByCenterPoint extends AbstractGeometry implements ArcByCenterPoint {
+
+ private final Point center;
+ private final double radius;
+ private final Unit<?> radiusUnit;
+ private final double startAngle;
+ private final double endAngle;
+
+ /**
+ * Creates an arc around the given centre.
+ *
+ * @param center centre of the circle the arc is a part of.
+ * @param radius radius of that circle, expressed in {@code radiusUnit}. Must be greater than zero.
+ * @param radiusUnit unit of {@code radius}, or {@code null} if the radius is expressed in the
+ * units of the coordinate system axes.
+ * @param startAngle bearing at which the arc starts, in decimal degrees.
+ * @param endAngle bearing at which the arc ends, in decimal degrees.
+ * @throws IllegalArgumentException if the radius is not a strictly positive real number,
+ * or if either angle is not a real number.
+ */
+ public DefaultArcByCenterPoint(final Point center, final double radius, final Unit<?> radiusUnit,
+ final double startAngle, final double endAngle)
+ {
+ if (center == null) {
+ throw new IllegalArgumentException("An arc by centre point needs a centre point.");
+ }
+ if (!(radius > 0) || Double.isInfinite(radius)) { // Rejects NaN as well.
+ throw new IllegalArgumentException("The radius of an arc must be a strictly positive"
+ + " real number, but got " + radius + '.');
+ }
+ if (!Double.isFinite(startAngle) || !Double.isFinite(endAngle)) {
+ throw new IllegalArgumentException("The start and end angles of an arc must be real"
+ + " numbers, but got " + startAngle + " and " + endAngle + '.');
+ }
+ this.center = center;
+ this.radius = radius;
+ this.radiusUnit = radiusUnit;
+ this.startAngle = startAngle;
+ this.endAngle = endAngle;
+ }
+
+ @Override
+ public Point getCenter() {
+ return center;
+ }
+
+ @Override
+ public double getRadius() {
+ return radius;
+ }
+
+ @Override
+ public Unit<?> getRadiusUnit() {
+ return radiusUnit;
+ }
+
+ @Override
+ public double getStartAngle() {
+ return startAngle;
+ }
+
+ @Override
+ public double getEndAngle() {
+ return endAngle;
+ }
+
+ @Override
+ public Envelope getEnvelope() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public String asText() {
+ final StringBuilder sb = new StringBuilder(TYPE).append(" (");
+ toText(sb, center.getPosition());
+ sb.append(", RADIUS ").append(radius);
+ if (radiusUnit != null) {
+ sb.append(' ').append(radiusUnit);
+ }
+ sb.append(", ANGLES ").append(startAngle).append(' ').append(endAngle);
+ return sb.append(')').toString();
+ }
+}
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Reader.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Reader.java
index e7d5d8894b..79ba348f7a 100644
--- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Reader.java
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Reader.java
@@ -19,6 +19,8 @@ package org.apache.sis.gml;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
+import javax.measure.Unit;
+import javax.measure.format.MeasurementParseException;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
@@ -40,6 +42,11 @@ import org.apache.sis.geometries.Polyhedron;
import org.apache.sis.geometries.Surface;
import org.apache.sis.geometries.TIN;
import org.apache.sis.geometries.Triangle;
+import org.apache.sis.geometries.curve.ArcByBulge;
+import org.apache.sis.geometries.curve.ArcByCenterPoint;
+import org.apache.sis.geometries.math.Vector;
+import org.apache.sis.geometries.math.Vectors;
+import org.apache.sis.measure.Units;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreReferencingException;
@@ -149,7 +156,7 @@ public final class GML3Reader extends AbstractGMLReader {
/**
* Returns the error message for a curve or surface kind whose Apache SIS interface exists but
* has no implementation, and whose parameterisation is a design question in its own right
- * (splines, clothoids, centre-point arcs, offset curves, gridded surfaces).
+ * (splines, clothoids, whole circles, offset curves, gridded surfaces).
*
* <p>The wording says <em>deferred</em>, not impossible: nothing here is beyond the model, it
* simply has not been built. What is never done is silently substituting an approximation —
@@ -717,18 +724,16 @@ public final class GML3Reader extends AbstractGMLReader {
addTo.add(GeometryFactory.createCircularString(parseCoordinateSequence(segment, crs).build(crs)));
break;
}
- /*
- * gml:Circle is deliberately NOT read as a circular string. Its three
- * control points describe a whole closed circle, whereas the same three
- * points in an arc string describe only the arc that passes through them --
- * at most half the circle. Reading it as an arc string would therefore
- * return a different geometry, not an approximation of the right one, and
- * `conics.Circle` has no implementation to return instead.
- */
+ case GML3Tags.ARC_BY_CENTER_POINT: {
+ addTo.add(parseArcByCenterPoint(crs));
+ break;
+ }
+ case GML3Tags.ARC_BY_BULGE: {
+ addTo.add(parseArcByBulge(crs));
+ break;
+ }
case GML3Tags.CIRCLE:
case GML3Tags.CIRCLE_BY_CENTER_POINT:
- case GML3Tags.ARC_BY_CENTER_POINT:
- case GML3Tags.ARC_BY_BULGE:
case GML3Tags.CUBIC_SPLINE:
case GML3Tags.BSPLINE:
case GML3Tags.BEZIER:
@@ -751,6 +756,219 @@ public final class GML3Reader extends AbstractGMLReader {
}
}
+ /**
+ * Parses a {@code <gml:ArcByCenterPoint>} curve segment: the centre of a circle, that circle's
+ * radius, and the bearings at which the arc starts and ends. The cursor must be on the
+ * element's {@link #START_ELEMENT} event, and is left on its matching {@link #END_ELEMENT}.
+ *
+ * <p>The centre may be given either as a coordinate-carrying child ({@code gml:pos} and,
+ * tolerantly, the GML 2.0 encodings) or as a {@code gml:pointProperty} holding a
+ * {@code gml:Point}. The {@code numArc} and {@code interpolation} attributes are ignored: the
+ * schema fixes both, so they carry no information.</p>
+ *
+ * <p>Both angles are required here even though the schema makes them optional, because an arc
+ * with no angular extent is not an arc. The element that means <q>the whole circle</q> is
+ * {@code gml:CircleByCenterPoint}, which is a different element and is reported as deferred.</p>
+ */
+ private ArcByCenterPoint parseArcByCenterPoint(final CoordinateReferenceSystem crs)
+ throws XMLStreamException, DataStoreContentException, DataStoreReferencingException
+ {
+ final PositionListBuilder coordinates = new PositionListBuilder();
+ Point center = null;
+ double radius = Double.NaN;
+ Unit<?> unit = null;
+ double startAngle = Double.NaN;
+ double endAngle = Double.NaN;
+ while (true) {
+ switch (reader.next()) {
+ case START_ELEMENT: {
+ switch (reader.getLocalName()) {
+ case GML3Tags.POINT_PROPERTY: {
+ center = parseMember(Point.class, GML3Tags.POINT_PROPERTY, crs);
+ break;
+ }
+ case GML3Tags.RADIUS: {
+ unit = unitOfMeasure(); // Before `measure(…)`, which consumes the element.
+ radius = measure(GML3Tags.RADIUS);
+ break;
+ }
+ case GML3Tags.START_ANGLE: startAngle = angle(GML3Tags.START_ANGLE); break;
+ case GML3Tags.END_ANGLE: endAngle = angle(GML3Tags.END_ANGLE); break;
+ default: {
+ if (!parseCoordinateElement(coordinates, crs)) {
+ skipUntilEnd();
+ }
+ break;
+ }
+ }
+ break;
+ }
+ case END_ELEMENT: {
+ if (GML3Tags.ARC_BY_CENTER_POINT.equals(reader.getLocalName())) {
+ if (center == null) {
+ if (coordinates.size() != 1) {
+ throw new DataStoreContentException("A GML 3 ArcByCenterPoint must give its"
+ + " centre as either a pointProperty or exactly one coordinate tuple,"
+ + " but " + coordinates.size() + " tuples were found.");
+ }
+ center = GeometryFactory.createPoint(coordinates.build(crs));
+ }
+ if (Double.isNaN(radius)) {
+ throw new DataStoreContentException("A GML 3 ArcByCenterPoint must contain a radius element.");
+ }
+ if (Double.isNaN(startAngle) || Double.isNaN(endAngle)) {
+ throw new DataStoreContentException("A GML 3 ArcByCenterPoint must contain both a"
+ + " startAngle and an endAngle element.");
+ }
+ try {
+ return GeometryFactory.createArcByCenterPoint(center, radius, unit, startAngle, endAngle);
+ } catch (IllegalArgumentException e) {
+ throw new DataStoreContentException(e.getMessage(), e);
+ }
+ }
+ break;
+ }
+ case END_DOCUMENT: throw new DataStoreContentException(endOfDocument());
+ }
+ }
+ }
+
+ /**
+ * Parses a {@code <gml:ArcByBulge>} curve segment: the two end points of an arc, the distance
+ * by which it bulges away from the chord joining them, and the direction of that bulge. The
+ * cursor must be on the element's {@link #START_ELEMENT} event, and is left on its matching
+ * {@link #END_ELEMENT}.
+ *
+ * <p>The {@code gml:normal} is required, as the schema requires it: without it the two arcs
+ * joining the end points cannot be told apart, and picking one would be a coin toss dressed up
+ * as a geometry. The {@code numArc} and {@code interpolation} attributes are ignored, both
+ * being fixed by the schema.</p>
+ */
+ private ArcByBulge parseArcByBulge(final CoordinateReferenceSystem crs)
+ throws XMLStreamException, DataStoreContentException, DataStoreReferencingException
+ {
+ final PositionListBuilder coordinates = new PositionListBuilder();
+ double bulge = Double.NaN;
+ double[] normal = null;
+ while (true) {
+ switch (reader.next()) {
+ case START_ELEMENT: {
+ switch (reader.getLocalName()) {
+ case GML3Tags.BULGE: bulge = measure(GML3Tags.BULGE); break;
+ case GML3Tags.NORMAL: normal = ordinates(GML3Tags.NORMAL); break;
+ default: {
+ if (!parseCoordinateElement(coordinates, crs)) {
+ skipUntilEnd();
+ }
+ break;
+ }
+ }
+ break;
+ }
+ case END_ELEMENT: {
+ if (GML3Tags.ARC_BY_BULGE.equals(reader.getLocalName())) {
+ if (coordinates.size() != 2) {
+ throw new DataStoreContentException("A GML 3 ArcByBulge must contain exactly two"
+ + " coordinate tuples, its start and its end, but " + coordinates.size()
+ + " were found.");
+ }
+ if (Double.isNaN(bulge)) {
+ throw new DataStoreContentException("A GML 3 ArcByBulge must contain a bulge element.");
+ }
+ if (normal == null) {
+ throw new DataStoreContentException("A GML 3 ArcByBulge must contain a normal element.");
+ }
+ final Vector<?> direction = Vectors.createDouble(normal.length);
+ direction.set(normal);
+ try {
+ return GeometryFactory.createArcByBulge(coordinates.build(crs), bulge, direction);
+ } catch (IllegalArgumentException e) {
+ throw new DataStoreContentException(e.getMessage(), e);
+ }
+ }
+ break;
+ }
+ case END_DOCUMENT: throw new DataStoreContentException(endOfDocument());
+ }
+ }
+ }
+
+ /**
+ * Returns the unit declared by the {@code uom} attribute of the element the cursor is on, or
+ * {@code null} if the attribute is absent or empty. An absent unit is not an error: GML
+ * documents in the wild routinely omit it, and it then means the units of the coordinate
+ * system axes.
+ */
+ private Unit<?> unitOfMeasure() throws DataStoreContentException {
+ final String uom = reader.getAttributeValue(null, GML3Tags.UOM);
+ if (uom == null || uom.isBlank()) {
+ return null;
+ }
+ try {
+ return Units.valueOf(uom.trim());
+ } catch (MeasurementParseException e) {
+ throw new DataStoreContentException("Cannot interpret \"" + uom + "\" as the unit of measurement"
+ + " of a GML 3 <" + reader.getLocalName() + "> element.", e);
+ }
+ }
+
+ /**
+ * Reads the text content of the element the cursor is on as a single number. The cursor must be
+ * on the element's {@link #START_ELEMENT} event, and is left on its matching
+ * {@link #END_ELEMENT}.
+ */
+ private double measure(final String tagName) throws XMLStreamException, DataStoreContentException {
+ final String text = reader.getElementText().trim();
+ try {
+ return Double.parseDouble(text);
+ } catch (NumberFormatException e) {
+ throw new DataStoreContentException("A GML 3 <" + tagName + "> element must contain a number,"
+ + " but contains \"" + text + "\".", e);
+ }
+ }
+
+ /**
+ * Reads an angle-valued element, converted to the decimal degrees that
+ * {@link ArcByCenterPoint} reports. The cursor must be on the element's
+ * {@link #START_ELEMENT} event, and is left on its matching {@link #END_ELEMENT}.
+ */
+ private double angle(final String tagName) throws XMLStreamException, DataStoreContentException {
+ final Unit<?> unit = unitOfMeasure(); // Before `measure(…)`, which consumes the element.
+ final double value = measure(tagName);
+ if (unit == null || Units.DEGREE.equals(unit)) {
+ return value;
+ }
+ try {
+ return Units.ensureAngular(unit).getConverterTo(Units.DEGREE).convert(value);
+ } catch (IllegalArgumentException e) {
+ throw new DataStoreContentException("The unit \"" + unit + "\" declared by a GML 3 <" + tagName
+ + "> element is not an angular unit.", e);
+ }
+ }
+
+ /**
+ * Reads the text content of the element the cursor is on as a whitespace-separated list of
+ * numbers, such as the {@code gml:normal} of an arc by bulge. The cursor must be on the
+ * element's {@link #START_ELEMENT} event, and is left on its matching {@link #END_ELEMENT}.
+ */
+ private double[] ordinates(final String tagName) throws XMLStreamException, DataStoreContentException {
+ final String text = reader.getElementText().trim();
+ if (text.isEmpty()) {
+ throw new DataStoreContentException("A GML 3 <" + tagName + "> element must contain at least one number.");
+ }
+ final String[] tokens = text.split("\\s+");
+ final double[] values = new double[tokens.length];
+ for (int i = 0; i < tokens.length; i++) {
+ try {
+ values[i] = Double.parseDouble(tokens[i]);
+ } catch (NumberFormatException e) {
+ throw new DataStoreContentException("A GML 3 <" + tagName + "> element must contain only"
+ + " numbers, but contains \"" + tokens[i] + "\".", e);
+ }
+ }
+ return values;
+ }
+
/**
* Parses a {@code <gml:CompositeCurve>} element, whose {@code curveMember} children are joined
* end to end into a single curve. Unlike {@code gml:MultiCurve}, the members of a composite
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Tags.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Tags.java
index 3aaf20516c..8bbbc724c1 100644
--- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Tags.java
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Tags.java
@@ -90,6 +90,28 @@ final class GML3Tags {
public static final String CIRCLE_BY_CENTER_POINT = "CircleByCenterPoint";
public static final String ARC_BY_CENTER_POINT = "ArcByCenterPoint";
public static final String ARC_BY_BULGE = "ArcByBulge";
+
+ // parameters of the ArcByCenterPoint and ArcByBulge curve segments
+ public static final String POINT_PROPERTY = "pointProperty";
+ public static final String RADIUS = "radius";
+ public static final String START_ANGLE = "startAngle";
+ public static final String END_ANGLE = "endAngle";
+ public static final String BULGE = "bulge";
+ public static final String NORMAL = "normal";
+
+ /**
+ * Attribute naming the unit of measurement of a {@code gml:radius}, {@code gml:startAngle}
+ * or {@code gml:endAngle} value.
+ */
+ public static final String UOM = "uom";
+
+ /**
+ * Value of the {@code uom} attribute written for the angles of a {@code gml:ArcByCenterPoint}.
+ * Those are always kept in decimal degrees by
+ * {@link org.apache.sis.geometries.conics.ArcByCenterPoint}.
+ */
+ public static final String UOM_DEGREE = "deg";
+
public static final String CUBIC_SPLINE = "CubicSpline";
public static final String BSPLINE = "BSpline";
public static final String BEZIER = "Bezier";
diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Writer.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Writer.java
index eedd156f51..2040ea0092 100644
--- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Writer.java
+++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/gml/GML3Writer.java
@@ -18,6 +18,7 @@ package org.apache.sis.gml;
import java.io.OutputStream;
import java.util.function.IntFunction;
+import javax.measure.Unit;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
@@ -43,6 +44,8 @@ import org.apache.sis.geometries.PolyhedralSurface;
import org.apache.sis.geometries.Polyhedron;
import org.apache.sis.geometries.Surface;
import org.apache.sis.geometries.TIN;
+import org.apache.sis.geometries.curve.ArcByBulge;
+import org.apache.sis.geometries.curve.ArcByCenterPoint;
import org.apache.sis.geometries.conics.CircularString;
import org.apache.sis.geometries.math.Tuple;
import org.apache.sis.storage.DataStoreContentException;
@@ -275,14 +278,6 @@ public final class GML3Writer extends AbstractGMLWriter {
/**
* Writes the curve, surface and solid kinds that GML 3 can express and GML 2.0 cannot.
- *
- * <p>The order of the tests matters for the same reason it does in
- * {@link AbstractGMLWriter#writeGeometryElement writeGeometryElement(…)}: a reversed
- * orientation is checked first because it may wrap any curve or surface, then each type before
- * its supertypes ({@code CircularString} and {@code CompoundCurve} before {@code Curve};
- * {@code TIN} before {@code PolyhedralSurface} before {@code Surface}). {@code MultiPolyhedron}
- * has to be claimed here rather than left to the collection catch-all, because it extends
- * {@code GeometryCollection}.</p>
*/
@Override
protected boolean writeExtendedGeometry(final Geometry geometry, final String srsName, final boolean declareNamespace)
@@ -294,6 +289,10 @@ public final class GML3Writer extends AbstractGMLWriter {
writeOriented(GML3Tags.ORIENTABLE_SURFACE, GML3Tags.BASE_SURFACE, s.getPrimitive(), srsName, declareNamespace);
} else if (geometry instanceof CircularString g) { // Before Curve.
writeCircularString(g, srsName, declareNamespace);
+ } else if (geometry instanceof ArcByCenterPoint g) { // Before Curve.
+ writeArcByCenterPoint(g, srsName, declareNamespace);
+ } else if (geometry instanceof ArcByBulge g) { // Before Curve.
+ writeArcByBulge(g, srsName, declareNamespace);
} else if (geometry instanceof CompoundCurve g) { // Before Curve.
writeCompoundCurve(g, srsName, declareNamespace);
} else if (geometry instanceof CurvePolygon g) { // Before Surface.
@@ -352,6 +351,66 @@ public final class GML3Writer extends AbstractGMLWriter {
writer.writeEndElement();
}
+ /**
+ * Writes a centre-point arc as a {@code <gml:Curve>} whose single segment is a
+ * {@code <gml:ArcByCenterPoint>}, which is the element it was read from and the only GML
+ * spelling of this parameterisation.
+ */
+ private void writeArcByCenterPoint(final ArcByCenterPoint g, final String srsName, final boolean declareNamespace)
+ throws XMLStreamException
+ {
+ writeStart(GML3Tags.CURVE, srsName, declareNamespace);
+ writer.writeStartElement(GML3Tags.SEGMENTS);
+ writer.writeStartElement(GML3Tags.ARC_BY_CENTER_POINT);
+ writePos(GML3Tags.POS, g.getCenter().getPosition());
+ final Unit<?> unit = g.getRadiusUnit();
+ writeMeasure(GML3Tags.RADIUS, g.getRadius(), (unit != null) ? unit.toString() : null);
+ writeMeasure(GML3Tags.START_ANGLE, g.getStartAngle(), GML3Tags.UOM_DEGREE);
+ writeMeasure(GML3Tags.END_ANGLE, g.getEndAngle(), GML3Tags.UOM_DEGREE);
+ writer.writeEndElement();
+ writer.writeEndElement();
+ writer.writeEndElement();
+ }
+
+ /**
+ * Writes a bulge arc as a {@code <gml:Curve>} whose single segment is a
+ * {@code <gml:ArcByBulge>}.
+ */
+ private void writeArcByBulge(final ArcByBulge g, final String srsName, final boolean declareNamespace)
+ throws XMLStreamException
+ {
+ writeStart(GML3Tags.CURVE, srsName, declareNamespace);
+ writer.writeStartElement(GML3Tags.SEGMENTS);
+ writer.writeStartElement(GML3Tags.ARC_BY_BULGE);
+ writePosList(g.getPoints());
+ writeMeasure(GML3Tags.BULGE, g.getBulge(), null);
+ writePos(GML3Tags.NORMAL, g.getNormal());
+ writer.writeEndElement();
+ writer.writeEndElement();
+ writer.writeEndElement();
+ }
+
+ /**
+ * Writes an element holding a single number, with a {@code uom} attribute when a unit is known.
+ * The attribute is omitted for an unknown unit rather than filled in with a guess: GML then
+ * means the units of the coordinate system axes, which is exactly what a value with no unit of
+ * its own is.
+ *
+ * <p>The attribute is derived from the unit itself, so a document that spelled it as an
+ * authority code — {@code uom="urn:ogc:def:uom:EPSG::9001"} — comes back with the symbol
+ * ({@code uom="m"}). The unit is the same one; only the spelling is normalised.</p>
+ *
+ * @param uom the unit of measurement to declare, or {@code null} to omit the attribute.
+ */
+ private void writeMeasure(final String tagName, final double value, final String uom) throws XMLStreamException {
+ writer.writeStartElement(tagName);
+ if (uom != null) {
+ writer.writeAttribute(GML3Tags.UOM, uom);
+ }
+ writer.writeCharacters(String.valueOf(value));
+ writer.writeEndElement();
+ }
+
/**
* Writes a compound curve as a {@code <gml:CompositeCurve>} with one {@code curveMember} per
* component.
diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/3/curve_arcbybulge.gml b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/3/curve_arcbybulge.gml
new file mode 100644
index 0000000000..c2efa55a98
--- /dev/null
+++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/3/curve_arcbybulge.gml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gml:Curve xmlns:gml="http://www.opengis.net/gml/3.2" srsName="EPSG:4326">
+ <gml:segments>
+ <gml:ArcByBulge>
+ <gml:posList srsDimension="2">0.0 0.0 10.0 0.0</gml:posList>
+ <gml:bulge>2.0</gml:bulge>
+ <gml:normal>0.0 1.0</gml:normal>
+ </gml:ArcByBulge>
+ </gml:segments>
+</gml:Curve>
diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/3/curve_arcbycenterpoint.gml b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/3/curve_arcbycenterpoint.gml
new file mode 100644
index 0000000000..f51e890684
--- /dev/null
+++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/3/curve_arcbycenterpoint.gml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gml:Curve xmlns:gml="http://www.opengis.net/gml/3.2" srsName="EPSG:4326">
+ <gml:segments>
+ <gml:ArcByCenterPoint>
+ <gml:pos>10.0 20.0</gml:pos>
+ <gml:radius uom="m">5.0</gml:radius>
+ <gml:startAngle uom="deg">0.0</gml:startAngle>
+ <gml:endAngle uom="deg">90.0</gml:endAngle>
+ </gml:ArcByCenterPoint>
+ </gml:segments>
+</gml:Curve>
diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3ReaderTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3ReaderTest.java
index 4aa49017b5..a126b9094b 100644
--- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3ReaderTest.java
+++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3ReaderTest.java
@@ -43,9 +43,12 @@ import org.apache.sis.geometries.PolyhedralSurface;
import org.apache.sis.geometries.Polyhedron;
import org.apache.sis.geometries.Surface;
import org.apache.sis.geometries.TIN;
+import org.apache.sis.geometries.curve.ArcByBulge;
+import org.apache.sis.geometries.curve.ArcByCenterPoint;
import org.apache.sis.geometries.conics.CircularString;
import org.apache.sis.geometries.math.NDArrays;
import org.apache.sis.geometries.math.SampleSystem;
+import org.apache.sis.measure.Units;
import org.apache.sis.referencing.CRS;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreReferencingException;
@@ -426,6 +429,152 @@ public final class GML3ReaderTest {
assertCRS(wgs84, g);
}
+ /**
+ * Tests that a {@code <gml:ArcByCenterPoint>} segment becomes an {@link ArcByCenterPoint},
+ * keeping the centre, radius and bearings the document actually stated rather than being
+ * evaluated into points on the arc.
+ */
+ @Test
+ public void testArcByCenterPoint() throws Exception {
+ final Geometry g = read(TestData.V3, TestData.CURVE_ARC_BY_CENTER);
+ final ArcByCenterPoint arc = assertInstanceOf(ArcByCenterPoint.class, g);
+ assertEquals(10.0, arc.getCenter().getPosition().get(0), GeometryAssert.TOLERANCE, "centre x");
+ assertEquals(20.0, arc.getCenter().getPosition().get(1), GeometryAssert.TOLERANCE, "centre y");
+ assertEquals( 5.0, arc.getRadius(), GeometryAssert.TOLERANCE, "radius");
+ assertEquals( 0.0, arc.getStartAngle(), GeometryAssert.TOLERANCE, "start angle");
+ assertEquals(90.0, arc.getEndAngle(), GeometryAssert.TOLERANCE, "end angle");
+ assertEquals(Units.METRE, arc.getRadiusUnit(), "radius unit");
+ assertEquals(CurveInterpolation.CIRCULAR, arc.getInterpolation());
+ assertCRS(wgs84, g);
+ }
+
+ /**
+ * Tests that the centre of an arc may be given as a {@code gml:pointProperty} instead of a
+ * {@code gml:pos}, and that a radius declaring no {@code uom} is reported with no unit —
+ * meaning the units of the coordinate system axes — rather than with an invented one.
+ */
+ @Test
+ public void testArcByCenterPointProperty() throws Exception {
+ final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<gml:Curve xmlns:gml=\"http://www.opengis.net/gml/3.2\" srsName=\"EPSG:4326\"><gml:segments>"
+ + "<gml:ArcByCenterPoint numArc=\"1\">"
+ + "<gml:pointProperty><gml:Point><gml:pos>10.0 20.0</gml:pos></gml:Point></gml:pointProperty>"
+ + "<gml:radius>5.0</gml:radius>"
+ + "<gml:startAngle>0.0</gml:startAngle>"
+ + "<gml:endAngle>90.0</gml:endAngle>"
+ + "</gml:ArcByCenterPoint>"
+ + "</gml:segments></gml:Curve>";
+ final ArcByCenterPoint arc = assertInstanceOf(ArcByCenterPoint.class, readInline(xml));
+ assertEquals(10.0, arc.getCenter().getPosition().get(0), GeometryAssert.TOLERANCE, "centre x");
+ assertEquals( 5.0, arc.getRadius(), GeometryAssert.TOLERANCE, "radius");
+ assertNull(arc.getRadiusUnit(), "A radius with no uom must not be given an invented unit.");
+ }
+
+ /**
+ * Tests that angles declared in a unit other than degrees are converted, since
+ * {@link ArcByCenterPoint} reports them in decimal degrees.
+ */
+ @Test
+ public void testArcByCenterPointAngleUnits() throws Exception {
+ final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<gml:Curve xmlns:gml=\"http://www.opengis.net/gml/3.2\"><gml:segments>"
+ + "<gml:ArcByCenterPoint>"
+ + "<gml:pos>0 0</gml:pos>"
+ + "<gml:radius uom=\"m\">5</gml:radius>"
+ + "<gml:startAngle uom=\"rad\">0</gml:startAngle>"
+ + "<gml:endAngle uom=\"rad\">" + (Math.PI / 2) + "</gml:endAngle>"
+ + "</gml:ArcByCenterPoint>"
+ + "</gml:segments></gml:Curve>";
+ final ArcByCenterPoint arc = assertInstanceOf(ArcByCenterPoint.class, readInline(xml));
+ assertEquals( 0.0, arc.getStartAngle(), GeometryAssert.TOLERANCE, "start angle in degrees");
+ assertEquals(90.0, arc.getEndAngle(), GeometryAssert.TOLERANCE, "end angle in degrees");
+ }
+
+ /**
+ * Tests that an arc missing its radius is reported, rather than read as an arc of some
+ * default radius.
+ */
+ @Test
+ public void testArcByCenterPointWithoutRadiusRejected() {
+ final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<gml:Curve xmlns:gml=\"http://www.opengis.net/gml/3.2\"><gml:segments>"
+ + "<gml:ArcByCenterPoint>"
+ + "<gml:pos>0 0</gml:pos>"
+ + "<gml:startAngle>0</gml:startAngle>"
+ + "<gml:endAngle>90</gml:endAngle>"
+ + "</gml:ArcByCenterPoint>"
+ + "</gml:segments></gml:Curve>";
+ assertThrows(DataStoreContentException.class, () -> readInline(xml));
+ }
+
+ /**
+ * Tests that an arc missing its angles is reported. The element meaning <q>the whole
+ * circle</q> is {@code gml:CircleByCenterPoint}, not an angle-less {@code gml:ArcByCenterPoint}.
+ */
+ @Test
+ public void testArcByCenterPointWithoutAnglesRejected() {
+ final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<gml:Curve xmlns:gml=\"http://www.opengis.net/gml/3.2\"><gml:segments>"
+ + "<gml:ArcByCenterPoint>"
+ + "<gml:pos>0 0</gml:pos>"
+ + "<gml:radius uom=\"m\">5</gml:radius>"
+ + "</gml:ArcByCenterPoint>"
+ + "</gml:segments></gml:Curve>";
+ assertThrows(DataStoreContentException.class, () -> readInline(xml));
+ }
+
+ /**
+ * Tests that a {@code <gml:ArcByBulge>} segment becomes an {@link ArcByBulge}, keeping its two
+ * end points, its bulge and its normal.
+ */
+ @Test
+ public void testArcByBulge() throws Exception {
+ final Geometry g = read(TestData.V3, TestData.CURVE_ARC_BY_BULGE);
+ final ArcByBulge arc = assertInstanceOf(ArcByBulge.class, g);
+ assertEquals(2, arc.getPoints().size(), "number of points");
+ assertEquals( 0.0, arc.getPoints().getPosition(0).get(0), GeometryAssert.TOLERANCE, "start x");
+ assertEquals(10.0, arc.getPoints().getPosition(1).get(0), GeometryAssert.TOLERANCE, "end x");
+ assertEquals( 2.0, arc.getBulge(), GeometryAssert.TOLERANCE, "bulge");
+ assertEquals(2, arc.getNormal().getDimension(), "normal dimension");
+ assertEquals(0.0, arc.getNormal().get(0), GeometryAssert.TOLERANCE, "normal x");
+ assertEquals(1.0, arc.getNormal().get(1), GeometryAssert.TOLERANCE, "normal y");
+ assertEquals(CurveInterpolation.CIRCULAR, arc.getInterpolation());
+ assertCRS(wgs84, g);
+ }
+
+ /**
+ * Tests that an arc by bulge with no {@code gml:normal} is reported. Without the normal, the
+ * two arcs joining the end points cannot be told apart, and picking one would be a guess.
+ */
+ @Test
+ public void testArcByBulgeWithoutNormalRejected() {
+ final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<gml:Curve xmlns:gml=\"http://www.opengis.net/gml/3.2\"><gml:segments>"
+ + "<gml:ArcByBulge>"
+ + "<gml:posList srsDimension=\"2\">0 0 10 0</gml:posList>"
+ + "<gml:bulge>2.0</gml:bulge>"
+ + "</gml:ArcByBulge>"
+ + "</gml:segments></gml:Curve>";
+ assertThrows(DataStoreContentException.class, () -> readInline(xml));
+ }
+
+ /**
+ * Tests that an arc by bulge with more than two coordinate tuples is reported. Three or more
+ * points with one bulge each is {@code gml:ArcStringByBulge}, a different element.
+ */
+ @Test
+ public void testArcByBulgeWithTooManyPointsRejected() {
+ final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<gml:Curve xmlns:gml=\"http://www.opengis.net/gml/3.2\"><gml:segments>"
+ + "<gml:ArcByBulge>"
+ + "<gml:posList srsDimension=\"2\">0 0 10 0 20 0</gml:posList>"
+ + "<gml:bulge>2.0</gml:bulge>"
+ + "<gml:normal>0 1</gml:normal>"
+ + "</gml:ArcByBulge>"
+ + "</gml:segments></gml:Curve>";
+ assertThrows(DataStoreContentException.class, () -> readInline(xml));
+ }
+
/**
* Tests that a {@code <gml:Surface>} with a single {@code gml:PolygonPatch} collapses to a
* plain {@link Polygon}.
diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3WriterTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3WriterTest.java
index 9d5cdaa327..d1851e7954 100644
--- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3WriterTest.java
+++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/GML3WriterTest.java
@@ -29,8 +29,11 @@ import org.apache.sis.geometries.GeometryFactory;
import org.apache.sis.geometries.LinearRing;
import org.apache.sis.geometries.Point;
import org.apache.sis.geometries.PointSequence;
+import org.apache.sis.geometries.curve.ArcByCenterPoint;
import org.apache.sis.geometries.math.NDArrays;
import org.apache.sis.geometries.math.SampleSystem;
+import org.apache.sis.geometries.math.Vector;
+import org.apache.sis.geometries.math.Vectors;
import org.apache.sis.referencing.CRS;
// Test dependencies
@@ -259,6 +262,57 @@ public final class GML3WriterTest extends TestCase {
assertRoundTrip(TestData.CURVE_ARC);
}
+ /**
+ * Tests that a {@code gml:ArcByCenterPoint} survives a full read/write cycle, centre, radius,
+ * bearings and units included.
+ */
+ @Test
+ public void testArcByCenterPointRoundTrip() throws Exception {
+ assertRoundTrip(TestData.CURVE_ARC_BY_CENTER);
+ }
+
+ /**
+ * Tests that a {@code gml:ArcByBulge} survives a full read/write cycle, bulge and normal
+ * included.
+ */
+ @Test
+ public void testArcByBulgeRoundTrip() throws Exception {
+ assertRoundTrip(TestData.CURVE_ARC_BY_BULGE);
+ }
+
+ /**
+ * Tests that an arc built with no radius unit is written with no {@code uom} attribute on its
+ * {@code gml:radius}, rather than with a unit the caller never supplied. The angles always
+ * carry {@code uom="deg"}, since {@link ArcByCenterPoint} keeps them in degrees.
+ */
+ @Test
+ public void testArcByCenterPointWithoutRadiusUnit() throws Exception {
+ final Geometry g = GeometryFactory.createArcByCenterPoint(
+ GeometryFactory.createPoint(sequence(10.0, 20.0)), 5.0, null,0.0, 90.0);
+ final String xml = write(g);
+ assertTrue(xml.contains("ArcByCenterPoint"), () -> "Expected a gml:ArcByCenterPoint in: " + xml);
+ assertTrue(xml.contains("<gml:radius>5.0</gml:radius>")
+ || xml.contains("<radius>5.0</radius>"),
+ () -> "Expected a radius with no uom attribute in: " + xml);
+ assertEquals(2, countOccurrences(xml, "uom=\"deg\""), () -> "Expected both angles in degrees in: " + xml);
+ }
+
+ /**
+ * Tests that an arc by bulge is written as a {@code gml:Curve} holding a
+ * {@code gml:ArcByBulge}, with its two end points, its bulge and its normal.
+ */
+ @Test
+ public void testArcByBulge() throws Exception {
+ final Vector<?> normal = Vectors.createDouble(2);
+ normal.set(new double[] {0, 1});
+ final Geometry g = GeometryFactory.createArcByBulge(sequence(0.0, 0.0, 10.0, 0.0), 2.0, normal);
+ final String xml = write(g);
+ assertTrue(xml.contains("ArcByBulge"), () -> "Expected a gml:ArcByBulge in: " + xml);
+ assertTrue(xml.contains("0.0 0.0 10.0 0.0"), () -> "Expected the two end points in: " + xml);
+ assertTrue(xml.contains("2.0"), () -> "Expected the bulge in: " + xml);
+ assertTrue(xml.contains("0.0 1.0"), () -> "Expected the normal in: " + xml);
+ }
+
/**
* Tests that {@code gml:CompositeCurve} survives a full read/write cycle.
*/
diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/TestData.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/TestData.java
index a21ca152b8..a35664fe3c 100644
--- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/TestData.java
+++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/gml/TestData.java
@@ -65,6 +65,8 @@ public enum TestData {
static final String TOLERANCE_LEGACY_STYLE_NEW_NAMESPACE = "tolerance_legacy_style_new_namespace.gml";
static final String CURVE = "curve.gml";
static final String CURVE_ARC = "curve_arc.gml";
+ static final String CURVE_ARC_BY_CENTER = "curve_arcbycenterpoint.gml";
+ static final String CURVE_ARC_BY_BULGE = "curve_arcbybulge.gml";
static final String SURFACE = "surface.gml";
static final String RING = "ring.gml";
static final String COMPOSITE_CURVE = "compositecurve.gml";