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
commit 9680bcdb1aff63af9e26613db2006eaff5efae59 Author: jsorel <[email protected]> AuthorDate: Tue Oct 13 11:57:48 2026 +0200 feat(Geometry): add Curve.subCurve(dist), start implementing getRepresentativePoint --- .../main/org/apache/sis/geometries/Curve.java | 37 +++++ .../apache/sis/geometries/curve/CompoundCurve.java | 33 +++++ .../main/org/apache/sis/geometries/curve/Line.java | 14 ++ .../apache/sis/geometries/curve/LineString.java | 14 ++ .../sis/geometries/internal/shared/Curves.java | 160 +++++++++++++++++++++ .../geometries/internal/shared/DefaultLine.java | 10 -- .../internal/shared/DefaultLineString.java | 10 -- .../internal/shared/DefaultReversedCurve.java | 16 +++ .../sis/geometries/curve/CompoundCurveTest.java | 26 +++- .../sis/geometries/curve/LineStringTest.java | 108 +++++++++++++- .../internal/shared/DefaultReversedCurveTest.java | 22 ++- 11 files changed, 421 insertions(+), 29 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java index 9d731eb4c8..fc97b02f23 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Curve.java @@ -159,6 +159,22 @@ public sealed interface Curve extends Orientable return points.getPoint(points.size()-1); } + /** + * Returns the point at the middle of this curve, i.e. at 50% of its {@linkplain #getLength() length}. + * + * @return the point at middle distance along this curve, or {@code null} if this curve is empty. + * + * @see #subCurve(Quantity) + * @see ISO 19107:2019 - 6.4.4.19 + */ + @Override + default Point getRepresentativePoint() { + if (isEmpty()) { + return null; + } + return subCurve(Quantities.create(50, Units.PERCENT)); + } + /** * Returns whether this curve is closed, i.e. whether its start point equals its end point. * For a simple curve, this is equivalent to {@link Geometry#isCycle()}. @@ -667,6 +683,27 @@ public sealed interface Curve extends Orientable throw new UnsupportedOperationException(); } + /** + * Extracts the portion of this curve at given length. + * + * <p>Difference with ISO 19107: declared as a copy constructor in the standard, + * with a start and end parameter, this version gets only one point.</p> + * + * <p>Difference with ISO-19107, the Length type has been changed to Quantity to + * handle temporal geometries and crs-less geometries.</p> + * + * <p>To get the start or end point use the Percent unit, 0 or 100</p> + * + * @param dist length parameter where is the returned point. + * @return the ppoint at given length. + * + * @see derived from ISO 19107:2019 - 6.4.18.22 + */ + default Point subCurve(Quantity<?> dist) { + //TODO + throw new UnsupportedOperationException(); + } + /** * Extracts the portion of this curve between two arc length parameters. * diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/CompoundCurve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/CompoundCurve.java index 1a0ec466fa..ade5c4dad8 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/CompoundCurve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/CompoundCurve.java @@ -19,6 +19,7 @@ package org.apache.sis.geometries.curve; import java.util.ArrayList; import java.util.List; import javax.measure.Quantity; +import javax.measure.Unit; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.CurveInterpolation; import org.apache.sis.geometries.DataPoints; @@ -27,6 +28,7 @@ import org.apache.sis.geometries.Geometry; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Point; import org.apache.sis.geometries.internal.shared.ConcatenatedDataPoints; +import org.apache.sis.geometries.internal.shared.Curves; import org.apache.sis.geometries.internal.shared.DefaultCompoundCurve; import org.apache.sis.measure.Quantities; import org.apache.sis.measure.Units; @@ -124,6 +126,37 @@ public sealed interface CompoundCurve extends Curve return length; } + /** + * Returns the point at the given distance along this compound curve, found on the component + * which covers that distance. The distance is in the unit of the {@linkplain #getLength() length}, + * or in percent of that length. + * + * @return the point at the given distance, or {@code null} if this compound curve has no component. + * @throws IllegalArgumentException if the distance is outside this compound curve. + */ + @Override + default Point subCurve(Quantity<?> dist) { + final int n = getNumCurves(); + if (n == 0) { + return null; + } + final Unit<?> unit = getCurveN(0).getLength().getUnit(); + final double[] lengths = new double[n]; + double total = 0; + for (int i = 0; i < n; i++) { + lengths[i] = Curves.convert(getCurveN(i).getLength(), unit); + total += lengths[i]; + } + double s = Curves.toArcLength(dist, total, unit); + for (int i = 0; i < n; i++) { + if (s <= lengths[i] || i == n - 1) { + return getCurveN(i).subCurve(Quantities.create(Math.min(s, lengths[i]), unit)); + } + s -= lengths[i]; + } + throw new AssertionError(); // Unreachable: the last component always returns. + } + /** * Returns whether the last component ends where the first one starts. * An empty compound curve is not closed. diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Line.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Line.java index 7694226626..1298ae7230 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Line.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/Line.java @@ -28,6 +28,8 @@ import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Point; import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Geometries; +import org.apache.sis.geometries.internal.shared.Curves; import org.apache.sis.geometries.internal.shared.DefaultLine; import org.apache.sis.geometries.internal.shared.DefaultLineString; import org.apache.sis.maths.Array; @@ -95,6 +97,18 @@ public sealed interface Line extends Curve return new DefaultLineString(getDataPoints()); } + /** + * Returns the point at the given distance along the segments of this line. + * The distance is in the unit of the coordinate system axes, or in percent of the length. + * + * @return the point at the given distance, or {@code null} if this line is empty. + * @throws IllegalArgumentException if the distance is outside this line. + */ + @Override + default Point subCurve(Quantity<?> dist) { + return Curves.alongPolyline(getDataPoints(), dist, Geometries.getLinearUnit(this)); + } + /** * The number of Points in this LineString. * diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java index 3c0cf0400e..cffc5b9109 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/curve/LineString.java @@ -27,6 +27,8 @@ import org.apache.sis.geometries.DataPoints; import org.apache.sis.geometries.GeometryType; import org.apache.sis.geometries.Point; import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.Geometries; +import org.apache.sis.geometries.internal.shared.Curves; import org.apache.sis.geometries.internal.shared.DefaultLineString; import org.apache.sis.geometries.mesh.MeshPrimitive; import org.apache.sis.geometries.operation.triangulate.delaunay.OrientedEdge; @@ -99,6 +101,18 @@ public sealed interface LineString extends Curve return this; } + /** + * Returns the point at the given distance along the segments of this line string. + * The distance is in the unit of the coordinate system axes, or in percent of the length. + * + * @return the point at the given distance, or {@code null} if this line string is empty. + * @throws IllegalArgumentException if the distance is outside this line string. + */ + @Override + default Point subCurve(Quantity<?> dist) { + return Curves.alongPolyline(getDataPoints(), dist, Geometries.getLinearUnit(this)); + } + /** * The number of Points in this LineString. * diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java new file mode 100644 index 0000000000..45462b3f3e --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/Curves.java @@ -0,0 +1,160 @@ +/* + * 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 java.util.LinkedHashMap; +import java.util.Map; +import javax.measure.IncommensurableException; +import javax.measure.Quantity; +import javax.measure.Unit; +import org.apache.sis.geometries.AttributeInterpolation; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.Point; +import org.apache.sis.maths.Array; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.Vector; +import org.apache.sis.measure.Units; + + +/** + * Helper methods for locating positions at a distance along a curve. + * + * @author Johann Sorel (Geomatys) + */ +public final class Curves { + + private Curves() { + } + + /** + * Returns whether the given distance is expressed in percent of the curve length. + * + * @param dist the distance along a curve. + * @return whether the distance is a percentage. + */ + public static boolean isPercent(final Quantity<?> dist) { + return Units.PERCENT.equals(dist.getUnit()); + } + + /** + * Converts the given quantity to the given unit. + * + * @param q the quantity to convert. + * @param unit the target unit. + * @return the value of the quantity in the target unit. + * @throws IllegalArgumentException if the quantity cannot be expressed in the target unit. + */ + public static double convert(final Quantity<?> q, final Unit<?> unit) { + try { + return q.getUnit().getConverterToAny(unit).convert(q.getValue().doubleValue()); + } catch (IncommensurableException e) { + throw new IllegalArgumentException("Cannot express " + q + " in " + unit + '.', e); + } + } + + /** + * Converts a distance along a curve to an arc length in the unit of the curve length. + * A distance in {@linkplain Units#PERCENT percent} is relative to the given length. + * + * @param dist the distance along the curve. + * @param length the length of the curve, in {@code unit}. + * @param unit the unit of the curve length. + * @return the arc length, between 0 and {@code length} inclusive. + * @throws IllegalArgumentException if the distance cannot be expressed in the length unit, + * or is outside the curve. + */ + public static double toArcLength(final Quantity<?> dist, final double length, final Unit<?> unit) { + double s = isPercent(dist) ? dist.getValue().doubleValue() / 100 * length : convert(dist, unit); + // Rounding errors of unit conversions and of length sums are not a distance outside the curve. + final double tolerance = length * 1E-12; + if (s < 0 && s >= -tolerance) { + s = 0; + } else if (s > length && s <= length + tolerance) { + s = length; + } + if (!(s >= 0 && s <= length)) { + throw new IllegalArgumentException("The distance " + dist + " is outside the curve, whose length is " + + length + ' ' + unit + '.'); + } + return s; + } + + /** + * Returns the point at the given distance along the line segments joining consecutive points. + * + * @param points the points of the polyline. + * @param dist the distance along the polyline, in {@code unit} or in percent. + * @param unit the unit of the coordinates. + * @return the point at the given distance, or {@code null} if there are no points. + * @throws IllegalArgumentException if the distance is outside the polyline. + */ + public static Point alongPolyline(final DataPoints points, final Quantity<?> dist, final Unit<?> unit) { + final int n = points.size(); + if (n == 0) { + return null; + } + final double[] segments = new double[n - 1]; + double total = 0; + for (int i=0; i<segments.length; i++) { + segments[i] = points.getPosition(i).subtract(points.getPosition(i+1)).length(); + total += segments[i]; + } + double s = toArcLength(dist, total, unit); + for (int i=0; i<segments.length; i++) { + final double length = segments[i]; + if (s <= length || i == segments.length - 1) { + final double ratio = (length > 0) ? Math.min(s / length, 1) : 0; + return interpolate(points, i, i+1, ratio); + } + s -= length; + } + return interpolate(points, 0, 0, 0); // A single position, at distance zero. + } + + /** + * Returns a new point on the segment joining two points of the given sequence. + * The position is linearly interpolated, and each other attribute is computed as declared by + * its {@linkplain DataPointsType#getAttributeInterpolation(String) attribute interpolation}. + * + * @param points the sequence holding the two points. + * @param i0 index of the point at ratio 0, the start of the segment. + * @param i1 index of the point at ratio 1, the end of the segment. + * @param ratio position between the two points, from 0 to 1. + * @return a new point holding the computed attributes, independent of the given sequence. + */ + public static Point interpolate(final DataPoints points, final int i0, final int i1, final double ratio) { + final DataPointsType type = points.getType(); + final Map<String,Array> attributes = new LinkedHashMap<>(); + for (final String name : type.getAttributeNames()) { + final AttributeInterpolation method = DataPointsType.ATT_POSITION.equals(name) + ? AttributeInterpolation.LINEAR : type.getAttributeInterpolation(name); + final Vector<?> value; + switch ((method != null) ? method : AttributeInterpolation.NEAREST) { + case LINEAR: + case TRANSLATE: value = points.getAttribute(i0, name).lerp(points.getAttribute(i1, name), ratio); break; + case LEADING: value = points.getAttribute(ratio < 1 ? i0 : i1, name); break; + default: value = points.getAttribute(ratio <= 0.5 ? i0 : i1, name); break; + } + final Array array = NDArrays.of(type.getAttributeSystem(name), type.getAttributeType(name), 1); + array.set(0, value); + attributes.put(name, array); + } + return GeometryFactory.DEFAULT.createPoint(GeometryFactory.DEFAULT.createDataPoints(attributes)); + } +} diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLine.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLine.java index 61172ef87e..b2ec3d8312 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLine.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLine.java @@ -62,16 +62,6 @@ public non-sealed class DefaultLine extends AbstractGeometry implements Line { return (Point) fromJTS(asJTS().getCentroid()); } - @Override - public Point getRepresentativePoint() { - if (isEmpty()) { - // The empty set has no interior position. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getInteriorPoint()); - } - @Override public boolean isSimple() { //TODO : fallback on JTS until implemented diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java index 762200d7e1..53a183b0d0 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultLineString.java @@ -62,16 +62,6 @@ public non-sealed class DefaultLineString extends AbstractGeometry implements Li return (Point) fromJTS(asJTS().getCentroid()); } - @Override - public Point getRepresentativePoint() { - if (isEmpty()) { - // The empty set has no interior position. - return null; - } - //TODO : fallback on JTS until implemented - return (Point) fromJTS(asJTS().getInteriorPoint()); - } - @Override public boolean isSimple() { //TODO : fallback on JTS until implemented diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java index 08760adab1..1dd945c9ce 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/internal/shared/DefaultReversedCurve.java @@ -29,6 +29,8 @@ import org.apache.sis.geometries.Point; import org.apache.sis.geometries.Primitive; import org.apache.sis.geometries.DataPointsType; import org.apache.sis.maths.Array; +import org.apache.sis.measure.Quantities; +import org.apache.sis.measure.Units; /** @@ -99,6 +101,20 @@ public non-sealed class DefaultReversedCurve extends AbstractGeometry implements return base.getLength(); } + /** + * Returns the point of the base curve at the given distance from its end, which is where this + * curve starts. + */ + @Override + public Point subCurve(Quantity<?> dist) { + if (Curves.isPercent(dist)) { + return base.subCurve(Quantities.create(100 - dist.getValue().doubleValue(), Units.PERCENT)); + } + final Quantity<?> length = base.getLength(); + final double s = Curves.toArcLength(dist, length.getValue().doubleValue(), length.getUnit()); + return base.subCurve(Quantities.create(length.getValue().doubleValue() - s, length.getUnit())); + } + @Override public boolean isClosed() { return base.isClosed(); diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/CompoundCurveTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/CompoundCurveTest.java index b3ee4f38bf..37344bdb84 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/CompoundCurveTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/CompoundCurveTest.java @@ -17,9 +17,12 @@ package org.apache.sis.geometries.curve; import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.measure.Quantities; +import org.apache.sis.measure.Units; // Test dependencies import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.apache.sis.geometries.GeometryTest; @@ -204,6 +207,24 @@ public class CompoundCurveTest extends GeometryTest { public void testSubCurve_Length_Length() { } + /** + * The point at a distance along two components of length 10 is found on the component + * which covers that distance. + */ + @Test + public void testSubCurve_Length() { + final CompoundCurve curve = GeometryFactory.DEFAULT.createCompoundCurve( + GeometryFactory.DEFAULT.createLineString(GeometryFactory.DEFAULT.createDataPoints(CRS_2D, 0,0, 10,0)), + GeometryFactory.DEFAULT.createLineString(GeometryFactory.DEFAULT.createDataPoints(CRS_2D, 10,0, 10,10))); + assertPositionEquals( 5, 0, curve.subCurve(Quantities.create( 25, Units.PERCENT))); + assertPositionEquals(10, 0, curve.subCurve(Quantities.create( 50, Units.PERCENT))); + assertPositionEquals(10, 5, curve.subCurve(Quantities.create( 75, Units.PERCENT))); + assertPositionEquals(10, 10, curve.subCurve(Quantities.create(100, Units.PERCENT))); + assertPositionEquals(10, 8, curve.subCurve(Quantities.create( 18, Units.DEGREE))); + assertPositionEquals(10, 0, curve.getRepresentativePoint()); + assertThrows(IllegalArgumentException.class, () -> curve.subCurve(Quantities.create(21, Units.DEGREE))); + } + @Test @Disabled("Not implemented yet.") public void testGetOrientationSign() { @@ -285,8 +306,11 @@ public class CompoundCurveTest extends GeometryTest { } @Test - @Disabled("Not implemented yet.") public void testGetRepresentativePoint() { + final LineString segment = GeometryFactory.DEFAULT.createLineString( + GeometryFactory.DEFAULT.createDataPoints(CRS_2D, 0,0, 10,0)); + final CompoundCurve curve = GeometryFactory.DEFAULT.createCompoundCurve(segment); + assertPositionEquals(5, 0, curve.getRepresentativePoint()); } @Test diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java index a5f127d55f..7146cab4d7 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/curve/LineStringTest.java @@ -16,10 +16,26 @@ */ package org.apache.sis.geometries.curve; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import javax.measure.Quantity; import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.geometries.AttributeInterpolation; +import org.apache.sis.geometries.DataPoints; +import org.apache.sis.geometries.DataPointsType; import org.apache.sis.geometries.Geometry; import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.Point; +import org.apache.sis.maths.Array; +import org.apache.sis.maths.DataType; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.ReadOnly; +import org.apache.sis.maths.SampleSystem; +import org.apache.sis.maths.Vector; +import org.apache.sis.measure.Quantities; import org.apache.sis.measure.Units; // Test dependencies @@ -267,6 +283,88 @@ public abstract class LineStringTest extends GeometryTest { public void testSubCurve_Length_Length() { } + /** + * The point at a distance along a curve of two segments of length 10, the distance being given + * either in the unit of the axes or in percent of the length. + */ + @Test + public void testSubCurve_Length() { + final LineString curve = createLineString(CRS_2D, 0,0, 10,0, 10,10); + assertPositionEquals( 0, 0, curve.subCurve(Quantities.create( 0, Units.PERCENT))); + assertPositionEquals(10, 5, curve.subCurve(Quantities.create( 75, Units.PERCENT))); + assertPositionEquals(10, 10, curve.subCurve(Quantities.create(100, Units.PERCENT))); + assertPositionEquals( 5, 0, curve.subCurve(Quantities.create( 5, Units.DEGREE))); + assertPositionEquals(10, 0, curve.subCurve(Quantities.create( 10, Units.DEGREE))); + assertPositionEquals(10, 8, curve.subCurve(Quantities.create( 18, Units.DEGREE))); + } + + /** + * The attributes other than the position are kept, each computed as its attribute interpolation + * declares. The point at 80% lies at 60% of the second segment, between the values 10 and 30. + */ + @Test + public void testSubCurve_LengthWithAttributes() { + final Map<String,Array> attributes = new LinkedHashMap<>(); + attributes.put(DataPointsType.ATT_POSITION, NDArrays.of(CRS_2D, new double[] {0,0, 10,0, 10,10})); + attributes.put("nearest", NDArrays.of(1, new double[] {0, 10, 30})); + attributes.put("linear", NDArrays.of(1, new double[] {0, 10, 30})); + attributes.put("leading", NDArrays.of(1, new double[] {0, 10, 30})); + final LineString curve = GeometryFactory.DEFAULT.createLineString(withInterpolations( + GeometryFactory.DEFAULT.createDataPoints(attributes), + Map.of("linear", AttributeInterpolation.LINEAR, + "leading", AttributeInterpolation.LEADING))); + + Point point = curve.subCurve(Quantities.create(80, Units.PERCENT)); + assertPositionEquals(10, 6, point); + assertEquals(Set.of(DataPointsType.ATT_POSITION, "nearest", "linear", "leading"), + Set.copyOf(point.getDataPointsType().getAttributeNames())); + assertEquals(30, point.getAttribute("nearest").get(0), TOLERANCE, "Value of the nearest point."); + assertEquals(22, point.getAttribute("linear" ).get(0), TOLERANCE, "Interpolated along the segment."); + assertEquals(10, point.getAttribute("leading").get(0), TOLERANCE, "Value of the segment start."); + + point = curve.subCurve(Quantities.create(100, Units.PERCENT)); + assertEquals(30, point.getAttribute("leading").get(0), TOLERANCE, "The end point has its own value."); + } + + /** + * Returns a view of the given sequence declaring the given attribute interpolations. + * Attributes absent from the map keep the interpolation declared by the sequence. + */ + private static DataPoints withInterpolations(final DataPoints points, final Map<String,AttributeInterpolation> methods) { + final DataPointsType base = points.getType(); + final DataPointsType type = new DataPointsType() { + @Override public List<String> getAttributeNames() {return base.getAttributeNames();} + @Override public SampleSystem getAttributeSystem(String name) {return base.getAttributeSystem(name);} + @Override public DataType getAttributeType(String name) {return base.getAttributeType(name);} + @Override public AttributeInterpolation getAttributeInterpolation(String name) { + return methods.getOrDefault(name, base.getAttributeInterpolation(name)); + } + }; + return new DataPoints() { + @Override public CoordinateReferenceSystem getCoordinateReferenceSystem() {return points.getCoordinateReferenceSystem();} + @Override public void setCoordinateReferenceSystem(CoordinateReferenceSystem crs) {points.setCoordinateReferenceSystem(crs);} + @Override public DataPointsType getType() {return type;} + @Override public int size() {return points.size();} + @Override public Point getPoint(int index) {return points.getPoint(index);} + @Override public Vector<?> getPosition(int index) {return points.getPosition(index);} + @Override public void setPosition(int index, ReadOnly.Vector<?> value) {points.setPosition(index, value);} + @Override public Vector<?> getAttribute(int index, String name) {return points.getAttribute(index, name);} + @Override public void setAttribute(int index, String name, ReadOnly.Vector<?> value) {points.setAttribute(index, name, value);} + }; + } + + /** + * A distance outside the curve, or which cannot be expressed in the unit of the axes, is rejected. + */ + @Test + public void testSubCurve_LengthOutside() { + final LineString curve = createLineString(CRS_2D, SEGMENT); + assertThrows(IllegalArgumentException.class, () -> curve.subCurve(Quantities.create(101, Units.PERCENT))); + assertThrows(IllegalArgumentException.class, () -> curve.subCurve(Quantities.create( -1, Units.DEGREE))); + assertThrows(IllegalArgumentException.class, () -> curve.subCurve(Quantities.create( 11, Units.DEGREE))); + assertThrows(IllegalArgumentException.class, () -> curve.subCurve(Quantities.create( 5, Units.METRE))); + } + @Test @Disabled("Not implemented yet.") public void testGetOrientationSign() { @@ -332,16 +430,12 @@ public abstract class LineStringTest extends GeometryTest { } /** - * Contrarily to the centroid, the returned position shall lie on the curve, but which position - * is returned is left to the implementation. The segment under test being horizontal, lying on - * it means having a null second ordinate and a first ordinate within the range of the segment. + * The point at middle distance along a curve made of a single segment is that segment's midpoint. */ @Test public void testGetRepresentativePoint() { - final double[] position = createLineString(CRS_2D, SEGMENT).getRepresentativePoint() - .getPosition().toArrayDouble(); - assertEquals(0, position[1], TOLERANCE, "The position shall lie on the segment."); - assertTrue(position[0] >= 0 && position[0] <= 10, "The position shall lie on the segment."); + assertPositionEquals(5, 0, createLineString(CRS_2D, SEGMENT).getRepresentativePoint()); + assertPositionEquals(5, 5, createLineString(CRS_2D, DIAGONAL).getRepresentativePoint()); } @Test diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/internal/shared/DefaultReversedCurveTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/internal/shared/DefaultReversedCurveTest.java index 3cda2af249..6260a35a83 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/internal/shared/DefaultReversedCurveTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/internal/shared/DefaultReversedCurveTest.java @@ -19,6 +19,8 @@ package org.apache.sis.geometries.internal.shared; import org.apache.sis.geometries.Curve; import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.measure.Quantities; +import org.apache.sis.measure.Units; // Test dependencies import static org.junit.jupiter.api.Assertions.assertEquals; @@ -186,6 +188,21 @@ public class DefaultReversedCurveTest extends GeometryTest { public void testSubCurve_Length_Length() { } + /** + * Distances along the reversed curve are measured from the end of the base curve. + */ + @Test + public void testSubCurve_Length() { + final LineString base = GeometryFactory.DEFAULT.createLineString( + GeometryFactory.DEFAULT.createDataPoints(CRS_2D, 0,0, 10,0, 10,10)); + final Curve reversed = GeometryFactory.DEFAULT.createReversed(base); + assertPositionEquals(10, 10, reversed.subCurve(Quantities.create( 0, Units.PERCENT))); + assertPositionEquals(10, 5, reversed.subCurve(Quantities.create( 25, Units.PERCENT))); + assertPositionEquals( 0, 0, reversed.subCurve(Quantities.create(100, Units.PERCENT))); + assertPositionEquals(10, 5, reversed.subCurve(Quantities.create( 5, Units.DEGREE))); + assertPositionEquals( 5, 0, reversed.subCurve(Quantities.create( 15, Units.DEGREE))); + } + @Test @Disabled("Not implemented yet.") public void testGetOrientationSign() { @@ -272,8 +289,11 @@ public class DefaultReversedCurveTest extends GeometryTest { } @Test - @Disabled("Not implemented yet.") public void testGetRepresentativePoint() { + final LineString base = GeometryFactory.DEFAULT.createLineString( + GeometryFactory.DEFAULT.createDataPoints(CRS_2D, 0,0, 10,0)); + final Curve reversed = GeometryFactory.DEFAULT.createReversed(base); + assertPositionEquals(5, 0, reversed.getRepresentativePoint()); } @Test
