diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java index 9bf6df14fd..8f3bec81e6 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java @@ -18,7 +18,7 @@ public class SelectionNGFunctions { - public static Geometry intersectsNG(Geometry a, final Geometry mask) + public static Geometry intersects(Geometry a, final Geometry mask) { return SelectionFunctions.select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { @@ -27,17 +27,17 @@ public boolean isTrue(Geometry g) { }); } - public static Geometry intersectsNGPrep(Geometry a, final Geometry mask) + public static Geometry intersectsPrep(Geometry a, final Geometry mask) { - RelateNG relateNG = new RelateNG(mask, true); + RelateNG relateNG = new RelateNG(mask); return SelectionFunctions.select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { - return relateNG.evaluate(TopologyPredicateFactory.intersects(), g); + return relateNG.compute(TopologyPredicateFactory.intersects(), g); } }); } - public static Geometry coversNG(Geometry a, final Geometry mask) + public static Geometry covers(Geometry a, final Geometry mask) { return SelectionFunctions.select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { @@ -46,12 +46,31 @@ public boolean isTrue(Geometry g) { }); } - public static Geometry coversNGPrep(Geometry a, final Geometry mask) + public static Geometry coversPrep(Geometry a, final Geometry mask) { - RelateNG relateNG = new RelateNG(mask, true); + RelateNG relateNG = new RelateNG(mask); return SelectionFunctions.select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { - return relateNG.evaluate(TopologyPredicateFactory.covers(), g); + return relateNG.compute(TopologyPredicateFactory.covers(), g); + } + }); + } + + public static Geometry adjacentTo(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.evaluate(TopologyPredicateFactory.relate("****1****"), mask, g); + } + }); + } + + public static Geometry adjacentToPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = new RelateNG(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.compute(TopologyPredicateFactory.relate("****1****"), g); } }); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java index 1d250a4a2a..94c9f672b5 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java @@ -47,6 +47,11 @@ public static boolean touches(Geometry a, Geometry b) { public static boolean within(Geometry a, Geometry b) { return RelateNG.evaluate(TopologyPredicateFactory.within(), a, b); } + + public static boolean adjacentTo(Geometry a, Geometry b) { + return RelateNG.evaluate(TopologyPredicateFactory.relate("****1****"), a, b); + } + public static boolean relate(Geometry a, Geometry b, String mask) { return RelateNG.relate(a, b, mask); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java index 632fc89876..c3cd3d60e5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Martin Davis. + * Copyright (c) 2024 Martin Davis. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -12,6 +12,7 @@ package org.locationtech.jts.operation.relateng; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -19,10 +20,9 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.util.LinearComponentExtracter; class LinearBoundary { - - //TODO: handle BoundaryNodeRule private Map vertexDegree = new HashMap(); private boolean hasBoundary; @@ -54,8 +54,8 @@ public boolean isBoundary(Coordinate pt) { private static Map computeBoundaryPoints(Geometry geom) { Map vertexDegree = new HashMap(); - for (int i = 0; i < geom.getNumGeometries(); i++) { - LineString line = (LineString) geom.getGeometryN(i); + List lines = LinearComponentExtracter.getLines(geom); + for (LineString line : lines) { if (line.isEmpty()) continue; addEndpoint(line.getCoordinateN(0), vertexDegree); diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java index 8f69624d62..2d95e2854f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -36,6 +36,7 @@ import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.util.ComponentCoordinateExtracter; import org.locationtech.jts.geom.util.LinearComponentExtracter; +import org.locationtech.jts.geom.util.PointExtracter; import org.locationtech.jts.noding.BasicSegmentString; import org.locationtech.jts.noding.SegmentString; @@ -76,7 +77,10 @@ public RelateGeometry(Geometry input, boolean isPrepared, BoundaryNodeRule bnRul lineBoundary = new LinearBoundary(input, boundaryNodeRule); } } - + + public boolean isPrepared() { + return isPrepared; + } public Envelope getEnvelope() { return geom.getEnvelopeInternal(); @@ -137,6 +141,26 @@ public int locateNode(Coordinate pt) { } public int locate(Coordinate pt) { + //TODO: provide an indexed option + RelatePointLocator locator = new RelatePointLocator(geom); + int loc = locator.locate(pt); + /* + int locOLD = OLDlocate(pt); + if (loc != locOLD) { + throw new IllegalStateException("locations do not match"); + } + */ + return RelatePointLocator.getLocation(loc); + } + + public int locateWithDim(Coordinate pt) { + //TODO: provide an indexed option + RelatePointLocator locator = new RelatePointLocator(geom); + int loc = locator.locateWithDim(pt); + return loc; + } + + public int OLDlocate(Coordinate pt) { //TODO: to support mixed GCs all dimensions will have to be tested switch (dim) { case Dimension.A: @@ -200,6 +224,10 @@ public List getCoordinates() { return pts; } + public List getPoints() { + return PointExtracter.getPoints(geom); + } + public List extractSegmentStringsOLD(Envelope env) { List lines = LinearComponentExtracter.getLines(geom); List segStrings = new ArrayList(); @@ -290,4 +318,8 @@ public boolean hasBoundary() { return lineBoundary.hasBoundary(); } + public String toString() { + return geom.toString(); + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java index f45ad31e93..687379db44 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -24,6 +24,8 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.IntersectionMatrix; import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.prep.PreparedGeometry; import org.locationtech.jts.noding.SegmentString; @@ -55,17 +57,18 @@ public class RelateNG public static boolean evaluate(TopologyPredicate pred, Geometry a, Geometry b) { RelateNG rng = new RelateNG(a); - return rng.evaluate(pred, b); + return rng.compute(pred, b); } - private static boolean evaluate(RelatePredicate pred, Geometry a, Geometry b, BoundaryNodeRule bnRule) { - RelateNG rng = new RelateNG(a, bnRule); - return rng.evaluate(pred, b); + public static boolean evaluate(RelatePredicate pred, Geometry a, Geometry b, BoundaryNodeRule bnRule) { + RelateNG rng = new RelateNG(bnRule); + return rng.compute(pred, a, b); } public static boolean relate(Geometry a, Geometry b, String mask) { RelatePredicate rel = new RelatePredicate(mask); - return RelateNG.evaluate(rel, a, b); + RelateNG rng = new RelateNG(); + return rng.compute(rel, a, b); } public static IntersectionMatrix relate(Geometry a, Geometry b) { @@ -81,31 +84,34 @@ public static IntersectionMatrix relate(Geometry a, Geometry b, BoundaryNodeRule } + private BoundaryNodeRule boundaryNodeRule; private RelateGeometry geomA; - private boolean isPrepared = false; private EdgeSetMutualIntersector edgeMutualInt; - private BoundaryNodeRule boundaryNodeRule; - public RelateNG(Geometry inputA) { - this(inputA, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + public RelateNG() { + this(BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); } - public RelateNG(Geometry inputA, BoundaryNodeRule bnRule) { - this(inputA, false, bnRule); + public RelateNG(BoundaryNodeRule bnRule) { + this.boundaryNodeRule = bnRule; } - public RelateNG(Geometry inputA, boolean isPrepared) { - this(inputA, isPrepared, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + public RelateNG(Geometry inputA) { + this(inputA, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); } - public RelateNG(Geometry inputA, boolean isPrepared, BoundaryNodeRule bnRule) { - this.boundaryNodeRule = bnRule; - geomA = new RelateGeometry(inputA, isPrepared, boundaryNodeRule); - this.boundaryNodeRule = bnRule; + public RelateNG(Geometry inputA, BoundaryNodeRule bnRule) { + this(bnRule); + geomA = new RelateGeometry(inputA, true, boundaryNodeRule); + } + + public boolean compute(TopologyPredicate predicate, Geometry inputA, Geometry inputB) { + geomA = new RelateGeometry(inputA, false, boundaryNodeRule); + return compute(predicate, inputB); } - public boolean evaluate(TopologyPredicate predicate, Geometry inputB) { + public boolean compute(TopologyPredicate predicate, Geometry inputB) { RelateGeometry geomB = new RelateGeometry(inputB, boundaryNodeRule); int dimA = geomA.getDimension(); @@ -198,15 +204,19 @@ private void computeAtPoints(RelateGeometry geomSrc, boolean isA, private boolean computePoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { - //TODO: handle mixed GCs - if (geom.getDimension() != Dimension.P) - return false; - //TODO: get Points only (i.e. from mixed GCs) - List coords = geom.getCoordinates(); - for (Coordinate p : coords) { + List points = geom.getPoints(); + for (Point pt : points) { + if (pt.isEmpty()) + continue; + + Coordinate p = pt.getCoordinate(); //TODO: break when all possible topo locations have been found - int locOnLine = geomTarget.locate(p); - topoBuilder.addPointOnGeometry(isA, locOnLine, p); + int locDimTarget = geomTarget.locateWithDim(p); + int locTarget = RelatePointLocator.getLocation(locDimTarget); + int dimTarget = locTarget == Location.EXTERIOR + ? topoBuilder.dim(! isA) + : RelatePointLocator.dimension(locDimTarget); + topoBuilder.addPointOnGeometry(isA, locTarget, dimTarget, p); if (topoBuilder.isResultKnown()) { return true; } @@ -224,8 +234,12 @@ private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry for (Coordinate p : coords) { //TODO: break when all possible locations have been found? int locLineEnd = geom.locateLineEnd(p); - int locTarget = geomTarget.locate(p); - topoBuilder.addLineEndOnGeometry(isA, locLineEnd, locTarget, p); + int locDimTarget = geomTarget.locateWithDim(p); + int locTarget = RelatePointLocator.getLocation(locDimTarget); + int dimTarget = locTarget == Location.EXTERIOR + ? topoBuilder.dim(! isA) + : RelatePointLocator.dimension(locDimTarget); + topoBuilder.addLineEndOnGeometry(isA, locLineEnd, locTarget, dimTarget, p); if (topoBuilder.isResultKnown()) { return true; } @@ -233,7 +247,7 @@ private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry return false; } - private boolean computeAreaNodes(RelateGeometry geom, boolean isAreaA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { + private boolean computeAreaNodes(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { //-- evaluate against line and area elements only //TODO: handle mixed GCs if (geomTarget.getDimension() < Dimension.L) @@ -247,12 +261,12 @@ private boolean computeAreaNodes(RelateGeometry geom, boolean isAreaA, RelateGeo if (elem instanceof Polygon) { Polygon poly = (Polygon) elem; - computeAreaNode(poly.getExteriorRing(), isAreaA, geomTarget, topoBuilder); + computeAreaNode(poly.getExteriorRing(), isA, geomTarget, topoBuilder); if (topoBuilder.isResultKnown()) { return true; } for (int j = 0; j < poly.getNumInteriorRing(); j++) { - computeAreaNode(poly.getInteriorRingN(j), isAreaA, geomTarget, topoBuilder); + computeAreaNode(poly.getInteriorRingN(j), isA, geomTarget, topoBuilder); if (topoBuilder.isResultKnown()) { return true; } @@ -262,11 +276,15 @@ private boolean computeAreaNodes(RelateGeometry geom, boolean isAreaA, RelateGeo return false; } - private void computeAreaNode(LinearRing ring, boolean isAreaA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { + private void computeAreaNode(LinearRing ring, boolean isA, RelateGeometry geomTarget, TopologyBuilder topoBuilder) { Coordinate pt = ring.getCoordinate(); - int locOnTarget = geomTarget.locate(pt); + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = RelatePointLocator.getLocation(locDimTarget); + int dimTarget = locTarget == Location.EXTERIOR + ? topoBuilder.dim(! isA) + : RelatePointLocator.dimension(locDimTarget); //-- don't need to test against points, since they have already been tested against the area - topoBuilder.addAreaVertexOnLineArea(isAreaA, locOnTarget, pt); + topoBuilder.addAreaVertex(isA, locTarget, dimTarget, pt); } private void computeEdges(RelateGeometry geomB, TopologyBuilder topoBuilder) { @@ -299,7 +317,7 @@ private void computeEdgesAll(List edgesB, Envelope envInt, EdgeSe private void computeEdgesMutual(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { //-- for prepared evaluation the A edge index is reused if (edgeMutualInt == null) { - Envelope envTarget = isPrepared ? null : envInt; + Envelope envTarget = geomA.isPrepared() ? null : envInt; List edgesA = geomA.extractSegmentStrings(envTarget); edgeMutualInt = new EdgeSetMutualIntersector(edgesA, envTarget); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java new file mode 100644 index 0000000000..4065f1c4ce --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2024 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.relateng; + +import java.util.Iterator; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.algorithm.PointLocation; +import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.GeometryCollectionIterator; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +/** + * Locates a point on a geometry, including mixed-type collections. + * GeometryCollections are handled with union semantics; + * i.e. the location of a point is that location of that point + * on the union of the elements of the collection. + * + * @author Martin Davis + * + */ +class RelatePointLocator { + + public static final int POINT_INTERIOR = 103; + public static final int LINE_INTERIOR = 110; + public static final int LINE_BOUNDARY = 111; + public static final int AREA_INTERIOR = 120; + public static final int AREA_BOUNDARY = 121; + + public static int getLocation(int relateLoc) { + switch (relateLoc) { + case POINT_INTERIOR: + case LINE_INTERIOR: + case AREA_INTERIOR: + return Location.INTERIOR; + case LINE_BOUNDARY: + case AREA_BOUNDARY: + return Location.BOUNDARY; + } + return Location.EXTERIOR; + } + + public static int dimension(int relateLoc) { + switch (relateLoc) { + case POINT_INTERIOR: + return Dimension.P; + case LINE_INTERIOR: + case LINE_BOUNDARY: + return Dimension.L; + case AREA_INTERIOR: + case AREA_BOUNDARY: + return Dimension.A; + } + return Dimension.FALSE; + } + + private Geometry geom; + private BoundaryNodeRule boundaryRule; + + private int elementDim = Dimension.FALSE; + private int numBoundaries = 0; // the number of sub-elements whose boundaries the point lies in + private int elementLocation = Location.EXTERIOR; + + public RelatePointLocator(Geometry geom) { + this(geom, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelatePointLocator(Geometry geom, BoundaryNodeRule bnRule) { + this.geom = geom; + this.boundaryRule = bnRule; + } + + /** + * Convenience method to test a point for intersection with + * a Geometry + * @param p the coordinate to test + * @param geom the Geometry to test + * @return true if the point is in the interior or boundary of the Geometry + */ + public boolean intersects(Coordinate p) + { + return locate(p) != Location.EXTERIOR; + } + + public int locate(Coordinate p) { + return getLocation(locateWithDim(p)); + } + + /** + * Computes the topological relationship ({@link Location}) of a single point + * to a Geometry. + * It handles both single-element + * and multi-element Geometries. + * The algorithm for multi-part Geometries + * takes into account the SFS Boundary Determination Rule. + * + * @return the {@link Location} of the point relative to the input Geometry + */ + public int locateWithDim(Coordinate p) + { + if (geom.isEmpty()) return Location.EXTERIOR; + + /* + if (geom instanceof LineString) { + return locateOnLineString(p, (LineString) geom); + } + else if (geom instanceof Polygon) { + return locateInPolygon(p, (Polygon) geom); + } +*/ + + computeLocation(p, geom); + + if (elementLocation == Location.EXTERIOR) + return Location.EXTERIOR; + + if (elementDim == Dimension.P) { + return POINT_INTERIOR; + } + if (elementDim == Dimension.L) { + if (boundaryRule.isInBoundary(numBoundaries)) + return LINE_BOUNDARY; + return LINE_INTERIOR; + + } + if (elementDim == Dimension.A) { + if (elementLocation == Location.BOUNDARY) + return AREA_BOUNDARY; + if (elementLocation == Location.INTERIOR) + return AREA_INTERIOR; + } + + return Location.EXTERIOR; + } + + private void computeLocation(Coordinate p, Geometry geom) + { + if (geom.isEmpty()) + return; + + if (geom instanceof Point) { + updateLocationInfo(locateOnPoint(p, (Point) geom), Dimension.P); + } + if (geom instanceof LineString) { + updateLocationInfo(locateOnLineString(p, (LineString) geom), Dimension.L); + } + else if (geom instanceof Polygon) { + updateLocationInfo(locateInPolygon(p, (Polygon) geom), Dimension.A); + } + else if (geom instanceof MultiLineString) { + MultiLineString ml = (MultiLineString) geom; + for (int i = 0; i < ml.getNumGeometries(); i++) { + LineString l = (LineString) ml.getGeometryN(i); + updateLocationInfo(locateOnLineString(p, l), Dimension.L); + } + } + else if (geom instanceof MultiPolygon) { + MultiPolygon mpoly = (MultiPolygon) geom; + for (int i = 0; i < mpoly.getNumGeometries(); i++) { + Polygon poly = (Polygon) mpoly.getGeometryN(i); + updateLocationInfo(locateInPolygon(p, poly), Dimension.A); + } + } + else if (geom instanceof GeometryCollection) { + Iterator geomi = new GeometryCollectionIterator((GeometryCollection) geom); + while (geomi.hasNext()) { + Geometry g2 = (Geometry) geomi.next(); + if (g2 != geom) + computeLocation(p, g2); + } + } + } + + private void updateLocationInfo(int loc, int dimension) + { + if (dimension < elementDim) + return; + + elementDim = dimension; + + if (loc == Location.EXTERIOR) { + return; + } + + if (dimension == Dimension.A && elementLocation == Location.INTERIOR) { + /** + * Area interior takes precedence in overlaps, so don't change it + */ + return; + } + + if (dimension == Dimension.L && loc == Location.BOUNDARY) { + numBoundaries++; + } + elementLocation = loc; + } + + private int locateOnPoint(Coordinate p, Point pt) + { + // no point in doing envelope test, since equality test is just as fast + + Coordinate ptCoord = pt.getCoordinate(); + if (ptCoord.equals2D(p)) + return Location.INTERIOR; + return Location.EXTERIOR; + } + + private int locateOnLineString(Coordinate p, LineString l) + { + // bounding-box check + if (! l.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR; + + CoordinateSequence seq = l.getCoordinateSequence(); + if (p.equals(seq.getCoordinate(0)) + || p.equals(seq.getCoordinate(seq.size() - 1)) ) { + int boundaryCount = l.isClosed() ? 2 : 1; + int loc = boundaryRule.isInBoundary(boundaryCount) ? Location.BOUNDARY : Location.INTERIOR; + return loc; + } + if (PointLocation.isOnLine(p, seq)) { + return Location.INTERIOR; + } + return Location.EXTERIOR; + } + + private int locateInPolygonRing(Coordinate p, LinearRing ring) + { + // bounding-box check + if (! ring.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR; + + return PointLocation.locateInRing(p, ring.getCoordinates()); + } + + private int locateInPolygon(Coordinate p, Polygon poly) + { + if (poly.isEmpty()) return Location.EXTERIOR; + + LinearRing shell = poly.getExteriorRing(); + + int shellLoc = locateInPolygonRing(p, shell); + if (shellLoc == Location.EXTERIOR) return Location.EXTERIOR; + if (shellLoc == Location.BOUNDARY) return Location.BOUNDARY; + // now test if the point lies in or on the holes + for (int i = 0; i < poly.getNumInteriorRing(); i++) { + LinearRing hole = poly.getInteriorRingN(i); + int holeLoc = locateInPolygonRing(p, hole); + if (holeLoc == Location.INTERIOR) return Location.EXTERIOR; + if (holeLoc == Location.BOUNDARY) return Location.BOUNDARY; + } + return Location.INTERIOR; + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java index 41bbdf3425..7ca06bc315 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java @@ -36,11 +36,11 @@ public boolean valueIM() { return false; boolean val = intMatrix.matches(mask); - /* + //* if (! val) { System.out.println("DEBUG: " + intMatrix + " does not equal mask " + mask); } - */ + //*/ return val; } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java index 47b02fb1af..4f8b9d1fd1 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyBuilder.java @@ -103,7 +103,7 @@ private void initExteriorEmpty(boolean geomNonEmpty) { } - private int dim(boolean isA) { + public int dim(boolean isA) { return isA ? dimA : dimB; } @@ -301,39 +301,45 @@ public void addPointOnPointExterior(boolean isGeomA, Coordinate pt) { updateDim(isGeomA, Location.INTERIOR, Location.EXTERIOR, Dimension.P); } - public void addPointOnGeometry(boolean isGeomA, int loc, Coordinate pt) { - updateDim(isGeomA, Location.INTERIOR, loc, Dimension.P); + public void addPointOnGeometry(boolean isA, int locTarget, int dimTarget, Coordinate pt) { + updateDim(isA, Location.INTERIOR, locTarget, Dimension.P); + switch (dimTarget) { + case Dimension.P: + return; + case Dimension.L: + //updateDim(isGeomA, Location.EXTERIOR, locTarget, Dimension.P); + return; + case Dimension.A: + /** + * If a point intersects an area, then the area interior and boundary + * must extend beyond the point. + */ + updateDim(isA, Location.EXTERIOR, Location.INTERIOR, Dimension.A); + updateDim(isA, Location.EXTERIOR, Location.BOUNDARY, Dimension.L); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); } - - public void addLineEndOnGeometry(boolean isLineA, int locLineEnd, int locTarget, Coordinate pt) { - int dimOther = dim(! isLineA); - if (dimOther == Dimension.P) { + + public void addLineEndOnGeometry(boolean isLineA, int locLineEnd, int locTarget, int dimTarget, Coordinate pt) { + switch (dimTarget) { + case Dimension.P: addLineEndOnPoint(isLineA, locLineEnd, locTarget, pt); - } - else if (dimOther == Dimension.L) { + return; + case Dimension.L: addLineEndOnLine(isLineA, locLineEnd, locTarget, pt); - } - else if (dimOther == Dimension.A) { + return; + case Dimension.A: addLineEndOnArea(isLineA, locLineEnd, locTarget, pt); + return; } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); } private void addLineEndOnPoint(boolean isLineA, int locLineEnd, int locPoint, Coordinate pt) { updateDim(isLineA, locLineEnd, locPoint, Dimension.P); } - private void addLineEndOnArea(boolean isLineA, int locLineEnd, int locArea, Coordinate pt) { - if (locArea == Location.BOUNDARY) { - updateDim(isLineA, locLineEnd, locArea, Dimension.P); - } - else { - //TODO: handle zero-length lines? - updateDim(isLineA, Location.INTERIOR, locArea, Dimension.L); - updateDim(isLineA, locLineEnd, locArea, Dimension.P); - updateDim(isLineA, Location.EXTERIOR, locArea, Dimension.A); - } - } - private void addLineEndOnLine(boolean isGeomA, int locLineEnd, int locLine, Coordinate pt) { updateDim(isGeomA, locLineEnd, locLine, Dimension.P); /** @@ -346,14 +352,29 @@ private void addLineEndOnLine(boolean isGeomA, int locLineEnd, int locLine, Coor updateDim(isGeomA, Location.INTERIOR, Location.EXTERIOR, Dimension.L); } } - - public void addAreaVertexOnLineArea(boolean isGeomA, int loc, Coordinate pt) { - if (dim(! isGeomA) == Dimension.L) { - addAreaVertexOnLine(isGeomA, loc, pt); + + private void addLineEndOnArea(boolean isLineA, int locLineEnd, int locArea, Coordinate pt) { + if (locArea == Location.BOUNDARY) { + updateDim(isLineA, locLineEnd, locArea, Dimension.P); } else { - addAreaVertexOnArea(isGeomA, loc, pt); + //TODO: handle zero-length lines? + updateDim(isLineA, Location.INTERIOR, locArea, Dimension.L); + updateDim(isLineA, locLineEnd, locArea, Dimension.P); + updateDim(isLineA, Location.EXTERIOR, locArea, Dimension.A); + } + } + + public void addAreaVertex(boolean isGeomA, int locTarget, int dimTarget, Coordinate pt) { + switch (dimTarget) { + case Dimension.L: + addAreaVertexOnLine(isGeomA, locTarget, pt); + return; + case Dimension.A: + addAreaVertexOnArea(isGeomA, locTarget, pt); + return; } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); } private void addAreaVertexOnLine(boolean isGeomA, int loc, Coordinate pt) { @@ -363,6 +384,11 @@ private void addAreaVertexOnLine(boolean isGeomA, int loc, Coordinate pt) { updateDim(isGeomA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A); } else { + /** + * If an area vertex intersects a line, all we know is the + * intersection at that point. + * e.g. the line may or may not be collinear with some or all of the area boundary + */ updateDim(isGeomA, Location.BOUNDARY, loc, Dimension.P); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java index 8eb0845e98..9d04776817 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateFactory.java @@ -309,6 +309,10 @@ public boolean valueIM() { }; } + public static TopologyPredicate relate(String mask) { + return new RelatePredicate(mask); + } + static boolean isDimsCompatibleWithCovers(int dim0, int dim1) { //- allow Points coveredBy zero-length Lines if (dim0 == Dimension.P && dim1 == Dimension.L) diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java new file mode 100644 index 0000000000..b003715861 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java @@ -0,0 +1,49 @@ +package org.locationtech.jts.operation.relateng; + +import junit.textui.TestRunner; + +public class RelateNGGCTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGGCTest.class); + } + + public RelateNGGCTest(String name) { + super(name); + } + + // see https://github.com/libgeos/geos/issues/1027 + public void testMP_GLP_GEOS1027() { + String a = "MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0)))"; + String b = "GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0))"; + checkRelate(a, b, "1020F1FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCrosses(a, b, false); + checkEquals(a, b, false); + } + + // see https://github.com/libgeos/geos/issues/1022 + public void testGPL_A() { + String a = "GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4))"; + String b = "POLYGON ((7 1, 1 3, 3 9, 7 1))"; + checkRelate(a, b, "F01FF0212"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCrosses(a, b, false); + checkTouches(a, b, true); + checkEquals(a, b, false); + } + + // see https://github.com/libgeos/geos/issues/982 + public void testP_GPL() { + String a = "POINT(0 0)"; + String b = "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0))"; + checkRelate(a, b, "F0FFFF102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCrosses(a, b, false); + checkTouches(a, b, true); + checkEquals(a, b, false); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java index 499233824d..764e1bd397 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java @@ -59,9 +59,15 @@ protected void checkEquals(String wkta, String wktb, boolean expectedValue) { checkPredicate(TopologyPredicateFactory.equalsTopo(), wktb, wkta, expectedValue); } - protected void checkRelate(String wktb, String wkta, String mask) { - checkPredicate(new RelatePredicate(mask), wktb, wkta, true); - } + protected void checkRelate(String wkta, String wktb, String expectedValue) { + Geometry a = read(wkta); + Geometry b = read(wktb); + RelatePredicate rel = new RelatePredicate(); + TopologyPredicate predTrace = new TopologyPredicateTracer(rel); + RelateNG.evaluate(predTrace, a, b); + String actualVal = rel.getIM().toString(); + assertEquals(expectedValue, actualVal); + } protected void checkPredicate(TopologyPredicate pred, String wkta, String wktb, boolean expectedValue) { Geometry a = read(wkta); diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java index 66030abe0c..d01d26e427 100644 --- a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java @@ -109,9 +109,9 @@ public void runIntersectsNG() public void runIntersectsNGPrep() { - RelateNG rng = new RelateNG(geomA, true); + RelateNG rng = new RelateNG(geomA); for (Geometry b : geomB) { - rng.evaluate(TopologyPredicateFactory.intersects(), b); + rng.compute(TopologyPredicateFactory.intersects(), b); } } @@ -139,17 +139,17 @@ public void runContainsNG() public void runContainsNGPrep() { - RelateNG rng = new RelateNG(geomA, true); + RelateNG rng = new RelateNG(geomA); for (Geometry b : geomB) { - rng.evaluate(TopologyPredicateFactory.contains(), b); + rng.compute(TopologyPredicateFactory.contains(), b); } } public void xrunContainsNGPrepValidate() { - RelateNG rng = new RelateNG(geomA, true); + RelateNG rng = new RelateNG(geomA); for (Geometry b : geomB) { - boolean resultNG = rng.evaluate(TopologyPredicateFactory.contains(), b); + boolean resultNG = rng.compute(TopologyPredicateFactory.contains(), b); boolean resultOld = geomA.contains(b); assertEquals(resultNG, resultOld); } diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java index 317715059d..042ba556f1 100644 --- a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java @@ -110,9 +110,9 @@ public void runIntersectsNG() public void runIntersectsNGPrep() { for (Geometry a : polygons) { - RelateNG rng = new RelateNG(a, true); + RelateNG rng = new RelateNG(a); for (Geometry b : polygons) { - rng.evaluate(TopologyPredicateFactory.intersects(), b); + rng.compute(TopologyPredicateFactory.intersects(), b); } } } @@ -138,9 +138,9 @@ public void runTouchesNG() public void runTouchesNGPrep() { for (Geometry a : polygons) { - RelateNG rng = new RelateNG(a, true); + RelateNG rng = new RelateNG(a); for (Geometry b : polygons) { - rng.evaluate(TopologyPredicateFactory.touches(), b); + rng.compute(TopologyPredicateFactory.touches(), b); } } } diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java index a08f44bbe1..e454fe3eaa 100644 --- a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java @@ -117,9 +117,9 @@ public void runIntersectsNG() public void runIntersectsNGPrep() { - RelateNG rng = new RelateNG(geomA, true); + RelateNG rng = new RelateNG(geomA); for (Geometry b : geomB) { - rng.evaluate(TopologyPredicateFactory.intersects(), b); + rng.compute(TopologyPredicateFactory.intersects(), b); } } @@ -147,17 +147,17 @@ public void runContainsNG() public void runContainsNGPrep() { - RelateNG rng = new RelateNG(geomA, true); + RelateNG rng = new RelateNG(geomA); for (Geometry b : geomB) { - rng.evaluate(TopologyPredicateFactory.contains(), b); + rng.compute(TopologyPredicateFactory.contains(), b); } } public void xrunContainsNGPrepValidate() { - RelateNG rng = new RelateNG(geomA, true); + RelateNG rng = new RelateNG(geomA); for (Geometry b : geomB) { - boolean resultNG = rng.evaluate(TopologyPredicateFactory.contains(), b); + boolean resultNG = rng.compute(TopologyPredicateFactory.contains(), b); boolean resultOld = geomA.contains(b); assertEquals(resultNG, resultOld); } diff --git a/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml b/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml new file mode 100644 index 0000000000..9b74b5ff4c --- /dev/null +++ b/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml @@ -0,0 +1,87 @@ + + + + A/GC-mP + + POLYGON((-60 -50,-70 -50,-60 -40,-60 -50)) + + + GEOMETRYCOLLECTION(MULTIPOINT((-60 -50),(-63 -49))) + + true + true + false + true + false + false + false + true + false + false + false + + + + mA/GC-LP + + MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0))) + + + GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0)) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC-PL/mA + + GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4)) + + + POLYGON ((7 1, 1 3, 3 9, 7 1)) + + true + false + false + false + false + false + false + true + false + true + false + + + + P/GC-PL + + POINT(0 0) + + + GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0)) + + true + false + true + false + false + false + false + true + false + true + true + + +