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 <[email protected]>
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";

Reply via email to