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";