From 1f07c2ed97e8afd80a1de0bc34f779f9c1a19c47 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 3 Feb 2026 10:34:26 -0800 Subject: [PATCH] Fix LineSegment.project to handle segments projecting onto endpoint --- .../function/LineSegmentFunctions.java | 21 +++++++ .../locationtech/jts/geom/LineSegment.java | 30 ++++++--- .../jts/geom/LineSegmentTest.java | 61 ++++++++++++++++++- .../test/java/test/jts/GeometryTestCase.java | 22 ++++++- 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/LineSegmentFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/LineSegmentFunctions.java index 09ae32e003..a87680121e 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/LineSegmentFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/LineSegmentFunctions.java @@ -116,4 +116,25 @@ public static Geometry reflectPoint(Geometry g1, Geometry g2) return g1.getFactory().createPoint(reflectPt); } + public static Geometry project(Geometry g1, Geometry g2) + { + LineSegment seg1 = toLineSegment(g1); + Coordinate[] line2 = g2.getCoordinates(); + if (line2.length == 1) { + Coordinate pt = line2[0]; + Coordinate result = seg1.project(pt); + return g1.getFactory().createPoint(result); + } + LineSegment seg2 = new LineSegment(line2[0], line2[1]); + LineSegment result = seg1.project(seg2); + if (result == null) + return g1.getFactory().createLineString(); + return result.toGeometry(g1.getFactory()); + } + + private static LineSegment toLineSegment(Geometry g) { + Coordinate[] line = g.getCoordinates(); + return new LineSegment(line[0], line[1]); + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java b/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java index 75d6bc3146..e9391da112 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java @@ -468,16 +468,30 @@ public LineSegment project(LineSegment seg) double pf0 = projectionFactor(seg.p0); double pf1 = projectionFactor(seg.p1); // check if segment projects at all - if (pf0 >= 1.0 && pf1 >= 1.0) return null; - if (pf0 <= 0.0 && pf1 <= 0.0) return null; + if (pf0 > 1.0 && pf1 > 1.0) return null; + if (pf0 < 0.0 && pf1 < 0.0) return null; - Coordinate newp0 = project(seg.p0, pf0); - if (pf0 < 0.0) newp0 = p0; - if (pf0 > 1.0) newp0 = p1; + Coordinate newp0; + if (pf0 < 0.0) { + newp0 = p0; + } + else if (pf0 > 1.0) { + newp0 = p1; + } + else { + newp0 = project(seg.p0, pf0); + } - Coordinate newp1 = project(seg.p1, pf1); - if (pf1 < 0.0) newp1 = p0; - if (pf1 > 1.0) newp1 = p1; + Coordinate newp1; + if (pf1 < 0.0) { + newp1 = p0; + } + else if (pf1 > 1.0) { + newp1 = p1; + } + else { + newp1 = project(seg.p1, pf1); + } return new LineSegment(newp0, newp1); } diff --git a/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java b/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java index cfc6115ca8..9adf4430cb 100644 --- a/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java @@ -11,14 +11,17 @@ */ package org.locationtech.jts.geom; -import junit.framework.TestCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import junit.textui.TestRunner; +import test.jts.GeometryTestCase; /** * Test LineSegment methods */ -public class LineSegmentTest extends TestCase { +public class LineSegmentTest extends GeometryTestCase { public static void main(String args[]) { TestRunner.run(LineSegmentTest.class); @@ -53,6 +56,60 @@ public void testProjectionFactor() assertTrue(seg2.projectionFactor(new Coordinate(11, 0)) == 0.1); } + public void testProjectPoint() { + //-- interior point + checkProjectPoint("LINESTRING (4 0, 8 0)", "POINT (5 2)", 5, 0); + //-- endpoint + checkProjectPoint("LINESTRING (4 0, 8 0)", "POINT (8 2)", 8, 0); + //-- beyond end + checkProjectPoint("LINESTRING (4 0, 8 0)", "POINT (9 2)", 9, 0); + //-- before end + checkProjectPoint("LINESTRING (4 0, 8 0)", "POINT (3 2)", 3, 0); + //-- collinear + checkProjectPoint("LINESTRING (4 0, 8 0)", "POINT (2 0)", 2, 0); + } + + private void checkProjectPoint(String wkt1, String wkt2, double x, double y) { + LineSegment seg1 = readLineSegment(wkt1); + Point pt = (Point) read(wkt2); + Coordinate p = pt.getCoordinate(); + Coordinate actual = seg1.project(p); + + checkEqualXY(new Coordinate(x, y), actual, 0.0001); + } + + public void testProjectSegment() { + //-- project onto interior segment + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (1 2, 2 3)", "LINESTRING(1 0, 2 0)"); + //-- project onto interior point + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (1 2, 1 4)", "LINESTRING(1 0, 1 0)"); + //-- projection includes endpoint + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (0 2, 1 4)", "LINESTRING(0 0, 1 0)"); + //- projection onto endpoint + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (8 2, 8 4)", "LINESTRING(8 0, 8 0)"); + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (0 2, 0 4)", "LINESTRING(0 0, 0 0)"); + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (0 2, -1 4)", "LINESTRING(0 0, 0 0)"); + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (9 1, 8 0)", "LINESTRING(8 0, 8 0)"); + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (9 1, 8 1)", "LINESTRING(8 0, 8 0)"); + //-- no projection + checkProjectSegment("LINESTRING (0 0, 8 0)", "LINESTRING (9 1, 9 2)", null); + } + + private void checkProjectSegment(String wkt1, String wkt2, String wktExpected) { + LineSegment seg1 = readLineSegment(wkt1); + LineSegment seg2 = readLineSegment(wkt2); + LineSegment actual = seg1.project(seg2); + + LineSegment expected = wktExpected == null ? null : readLineSegment(wktExpected); + checkEqual(expected, actual, 0.0001); + } + + private LineSegment readLineSegment(String wkt) { + Geometry g = read(wkt); + LineString line = (LineString) g; + return new LineSegment(line.getCoordinateN(0), line.getCoordinateN(1)); + } + public void testLineIntersection() { // simple case checkLineIntersection( diff --git a/modules/core/src/test/java/test/jts/GeometryTestCase.java b/modules/core/src/test/java/test/jts/GeometryTestCase.java index e1562a9d28..5c2db3990a 100644 --- a/modules/core/src/test/java/test/jts/GeometryTestCase.java +++ b/modules/core/src/test/java/test/jts/GeometryTestCase.java @@ -233,7 +233,27 @@ protected void checkEqualXY(String message, Coordinate expected, Coordinate actu assertEquals(message + " X", expected.getX(), actual.getX(), tolerance); assertEquals(message + " Y", expected.getY(), actual.getY(), tolerance); } - + + protected void checkEqual(LineSegment expected, LineSegment actual, double tolerance) { + boolean equal; + if (actual == null || expected == null) { + equal = actual == null && expected == null; + } + else { + equal = isEqual(actual, expected, tolerance); + } + if (! equal) { + System.out.format(CHECK_EQUAL_FAIL_MSG, expected, actual ); + } + assertTrue(equal); + } + + private boolean isEqual(LineSegment actual, LineSegment expected, double tolerance) { + return expected.getCoordinate(0).equals2D(actual.getCoordinate(0), tolerance) + && expected.getCoordinate(1).equals2D(actual.getCoordinate(1), tolerance); + + } + protected void checkNoAlias(Geometry geom, Geometry geom2) { Geometry geom2Copy = geom2.copy(); geom.apply(new CoordinateFilter() {