diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6d037e3607..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: java -jdk: - - oraclejdk8 -sudo: false -cache: - directories: - - $HOME/.m2 -install: mvn install -B -V -dist: trusty - diff --git a/README.md b/README.md index 6bb4bd7eb4..4cc97937dd 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,17 @@ The JTS Topology Suite is a Java library for creating and manipulating vector ge ![JTS logo](jts_logo.png) -[![Travis Build Status](https://api.travis-ci.org/locationtech/jts.svg)](http://travis-ci.org/locationtech/jts) [![GitHub Action Status](https://github.com/locationtech/jts/workflows/GitHub%20CI/badge.svg)](https://github.com/locationtech/jts/actions) +[![GitHub Action Status](https://github.com/locationtech/jts/workflows/GitHub%20CI/badge.svg)](https://github.com/locationtech/jts/actions) [![Join the chat at https://gitter.im/locationtech/jts](https://badges.gitter.im/locationtech/jts.svg)](https://gitter.im/locationtech/jts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -JTS is a project in the [LocationTech](http://www.locationtech.org) working group of the Eclipse Foundation. +JTS is a project in the [LocationTech](https://www.locationtech.org) working group of the Eclipse Foundation. ![LocationTech](locationtech_mark.png) ## Requirements -Currently JTS targets Java 1.8 and above. +Currently JTS targets Java 8 and above. ## Resources @@ -41,7 +40,7 @@ Currently JTS targets Java 1.8 and above. JTS is open source software. It is dual-licensed under: * [Eclipse Public License 2.0](https://www.eclipse.org/legal/epl-v20.html) -* [Eclipse Distribution License 1.0](http://www.eclipse.org/org/documents/edl-v10.php) (a BSD Style License) +* [Eclipse Distribution License 1.0](https://www.eclipse.org/org/documents/edl-v10.php) (a BSD Style License) See also: @@ -74,7 +73,7 @@ If you are interested in contributing to JTS please read the [**Contributing Gui * [**GEOS**](https://trac.osgeo.org/geos) - C++ * [**NetTopologySuite**](https://github.com/NetTopologySuite/NetTopologySuite) - .NET * [**JSTS**](https://github.com/bjornharrtell/jsts) - JavaScript -* [**dart_jts]([https://pub.dev/packages/dart_jts](https://github.com/moovida/dart_jts)) - Dart +* [**dart_jts**](https://github.com/moovida/dart_jts) - Dart ### Via GEOS * [**Shapely**](https://github.com/Toblerity/Shapely) - Python wrapper of GEOS diff --git a/USING.md b/USING.md index 0ce1b50cef..6e2dbdf677 100644 --- a/USING.md +++ b/USING.md @@ -44,7 +44,7 @@ Our [build server](https://ci.eclipse.org/jts/) publishes to the LocationTech Ma locationtech-releases - https://repo.eclipse.org/content/groups/releases + https://repo.eclipse.org/content/repositories/jts false @@ -66,7 +66,7 @@ The latest snapshot builds are now avaialble: ```xml - 1.17.0-SNAPSHOT + 1.20.0-SNAPSHOT org.locationtech.jts @@ -123,6 +123,7 @@ module org.foo.baz { ## JTS System Properties * `-Djts.overlay=ng` enables the use of OverlayNG in `Geometry` overlay methods. (*Note: in a future release this will become the default behaviour*) +* `-Djts.relate=ng` enables the use of RelateNG in `Geometry` topological predicate methods. (*Note: in a future release this will become the default behaviour*) ## JTS Tools diff --git a/build-tools/pom.xml b/build-tools/pom.xml index 2dbf23033a..ba82456142 100644 --- a/build-tools/pom.xml +++ b/build-tools/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.locationtech.jts build-tools - 1.20.0-SNAPSHOT + 1.20.0 JTS Topology Suite Build Configuration diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 794cf66ad6..2276f09a3b 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -17,9 +17,23 @@ Distributions for older JTS versions can be obtained at the +Version 1.x + +Release Date: TBD + # Version 1.x -*Release Date: TBD* +### New Features + +### Functionality Improvements + +### Bug Fixes + +### Performance Improvements + +# Version 1.20.0 + +*Release Date: 09/18/2024* ### New Features * Add `CoverageValidator` `CoveragePolygonValidator` (#900) @@ -30,14 +44,22 @@ Distributions for older JTS versions can be obtained at the * Add `Geometry.hasDimension(int dim)` method {#944} * Add `ConcaveHull.alphaShape` function (#952) * Add `OffsetCurve` Joined mode (#956) +* Add `PointLocation.isOnSegment` function (#1048) +* Add `RelateNG` API for improved topological relationship functionality and performance (#1052, #1055) +* Add system property `jts.relate=ng` to enable use of RelateNG in `Geometry` methods (#1073) ### Functionality Improvements * Improve `TopologyPreservingSimplifier` to prevent edge-disjoint line collapse (#925) * Improve `OffsetCurve` to return more linework for some input situations (#956) * Reduce buffer curve short fillet segments (#960) * Added ability to specify boundary for `LargestEmptyCircle` (#973) +* Improve `DouglaPeuckerSimplifier` and `TopologyPreservingSimplifier` to handle ring endpoints (#1013) +* Add `Angle` functions `sinSnap` and `cosSnap` to avoid small errors, e.g. with buffer operations (#1016) +* Improve Buffer input simplification for rings (#1022) +* Improve CoverageSimplifier with ring removal, smoothing, inner/outer and per-feature tolerances (#1060) ### Bug Fixes +* Fix `WKBReader` and `WKBWriter` handling of M measures when writing to WKB and reading from WKB (#734) * Fix `PreparedGeometry` handling of EMPTY elements (#904) * Fix `WKBReader` parsing of WKB containing multiple empty elements (#905) * Fix `LineSegment.orientationIndex(LineSegment)` to correct orientation for non-collinear segments on right (#914) @@ -57,11 +79,28 @@ Distributions for older JTS versions can be obtained at the * Fix `Geometry.getCoordinate` to return non-null coordinate for collections with empty first element (#987) * Fix `LargestEmptyCircle` to handle polygonal obstacles (#988) * Make intersection computation more robust (#989) +* Fix `VariableBuffer` to handle zero vertex buffer distances correctly (#997) +* Fix `IncrementalDelaunayTriangulator` to ensure triangulation boundary is convex (#1004) +* Fix OverlayNG Area Check heuristic for difference (#1005) +* Fix `InteriorPointPoint` to handle empty elements +* Fix `DistanceOp` for empty elements (#1010) +* Fix predicates for MultiPoint with EMPTY (#1015) +* Fix `InteriorPoint` for MultiLineString with EMPTY (#1023) +* Fix TopologyPreservingSimplifier to prevent incorrect topology from jumping components (#1024) +* Fix OffsetCurve to ensure end segments are included (#1029) +* Fix `PointLocator` to respect `BoundaryNodeRule` for single lines (#1031) +* Fix `BufferOp` Inverted Ring Removal check (#1038) +* Improve `VariableBuffer` segment buffer cap generation (#1041) +* Fix `TopologyPreservingSimplifier` ring endpoint removal indexing (#1059) ### Performance Improvements * Improve `Polygonizer` performance in some cases with many islands (#906) * Improve Convex Hull performance by avoiding duplicate uniquing (#985) +* Improve `HPRtree` performance (#1012) +* Improve performance of noding and overlay via `HPRtree` (#1012) +* Improve `DistanceOp` performance for Point-Point (#1049) +* Improve `CoveragePolygonValidator` via section performance optimization (#1053) # Version 1.19 diff --git a/modules/app/pom.xml b/modules/app/pom.xml index bea8faf0a9..e25145a9e9 100644 --- a/modules/app/pom.xml +++ b/modules/app/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-app ${project.groupId}:${project.artifactId} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java index 3c4c400675..887c1004f5 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/CommandOptions.java @@ -30,19 +30,23 @@ public class CommandOptions { public static final String EACHA = "eacha"; public static final String EACHB = "eachb"; public static final String ARGS = "args"; - public static final String VALIDATE = "validate"; public static final String INDEX = "index"; + public static final String TIME = "time"; + public static final String LIMIT = "limit"; + public static final String OFFSET = "offset"; + public static final String QUIET = "q"; + public static final String VALIDATE = "validate"; + public static final String WHERE = "where"; + + public static final String SOURCE_STDIN = "stdin"; - public static final String STDIN = "stdin"; public static final String FORMAT_GML = "gml"; public static final String FORMAT_WKB = "wkb"; public static final String FORMAT_TXT = "txt"; public static final String FORMAT_WKT = "wkt"; public static final String FORMAT_GEOJSON = "geojson"; public static final String FORMAT_SVG = "svg"; - public static final String TIME = "time"; - public static final String LIMIT = "limit"; - public static final String OFFSET = "offset"; - public static final String WHERE = "where"; + + } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java index 29df546c81..6f09f88476 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpCmd.java @@ -38,31 +38,35 @@ * *
  * --- Compute the area of a WKT geometry, output it
- * jtsop -a some-file-with-geom.wkt -f txt area 
+ * jtsop -a some-file-with-geom.wkt area 
  * 
  * --- Validate geometries from a WKT file using limit and offset
- * jtsop -a some-file-with-geom.wkt -limit 100 -offset 40 -f txt isValid 
+ * jtsop -a some-file-with-geom.wkt -limit 100 -offset 40 isValid 
  * 
  * --- Compute the unary union of a WKT geometry, output as WKB
  * jtsop -a some-file-with-geom.wkt -f wkb Overlay.unaryUnion 
  * 
  * --- Compute the union of two geometries in WKT and WKB, output as WKT
- * jtsop -a some-file-with-geom.wkt -b some-other-geom.wkb -f wkt Overlay.Union
+ * jtsop -a some-file-with-geom.wkt -b some-other-geom.wkb Overlay.Union
  * 
  * --- Compute the buffer of distance 10 of a WKT geometry, output as GeoJSON
  * jtsop -a some-file-with-geom.wkt -f geojson Buffer.buffer 10
  * 
  * --- Compute the buffer of a literal geometry, output as WKT
- * jtsop -a "POINT (10 10)" -f wkt Buffer.buffer 10
+ * jtsop -a "POINT (10 10)" Buffer.buffer 10
  * 
  * --- Compute buffers of multiple sizes
- * jtsop -a "POINT (10 10)" -f wkt Buffer.buffer 1,10,100
+ * jtsop -a "POINT (10 10)" Buffer.buffer 1,10,100
  * 
  * --- Run op for each A 
- * jtsop -a "MULTIPOINT ((10 10), (20 20))" -eacha -f wkt Buffer.buffer
+ * jtsop -a "MULTIPOINT ((10 10), (20 20))" -eacha Buffer.buffer
  * 
  * --- Output a literal geometry as GeoJSON
  * jtsop -a "POINT (10 10)" -f geojson
+ * 
+ * --- Run op but don't output result (quiet mode) 
+ * jtsop -a "MULTIPOINT ((10 10), (20 20))" -q Buffer.buffer
+
  * 
* * @author Martin Davis @@ -120,6 +124,7 @@ private static CommandLine createCmdLine() { .addOptionSpec(new OptionSpec(CommandOptions.LIMIT, 1)) .addOptionSpec(new OptionSpec(CommandOptions.OFFSET, 1)) .addOptionSpec(new OptionSpec(CommandOptions.REPEAT, 1)) + .addOptionSpec(new OptionSpec(CommandOptions.QUIET, 0)) .addOptionSpec(new OptionSpec(CommandOptions.SRID, 1)) .addOptionSpec(new OptionSpec(CommandOptions.WHERE, 2)) .addOptionSpec(new OptionSpec(CommandOptions.VALIDATE, 0)) @@ -145,6 +150,7 @@ private static CommandLine createCmdLine() { " [ -explode", " [ -srid SRID ]", " [ -f ( txt | wkt | wkb | geojson | gml | svg ) ]", + " [ -q", " [ -time ]", " [ -v, -verbose ]", " [ -help ]", @@ -166,15 +172,16 @@ private static CommandLine createCmdLine() { " -eachb execute op on each element of B", " -index index the B geometries", " -repeat repeat the operation N times", - " -where op v output geometry where operation result matches predicate op and value.", - " Predicates ops are: eq,ne,ge,gt,le,lt", + " -where cond v output geometry where operation result matches condition and value.", + " Conditions are: eq, ne, ge, gt, le, lt", " -validate validate the result of each operation", " -geomfunc specifies class providing geometry operations", " -op separator to delineate operation arguments", "===== Output options:", - " -srid Sets the SRID on output geometries", + " -srid sets the SRID on output geometries", " -explode output atomic geometries", - " -f output format to use. If omitted output is silent", + " -f output format to use. Default is txt/wkt", + " -q quiet mode - result is not output", "===== Logging options:", " -time display execution time", " -v, -verbose display information about execution", @@ -331,7 +338,9 @@ JTSOpRunner.OpParams parseArgs(String[] args) throws ParseException, ClassNotFou ? commandLine.getOptionArgAsInt(CommandOptions.OFFSET, 0) : 0; - cmdArgs.format = commandLine.getOptionArg(CommandOptions.FORMAT, 0); + cmdArgs.format = commandLine.hasOption(CommandOptions.FORMAT) + ? commandLine.getOptionArg(CommandOptions.FORMAT, 0) + : CommandOptions.FORMAT_TXT; cmdArgs.srid = commandLine.hasOption(CommandOptions.SRID) ? commandLine.getOptionArgAsInt(CommandOptions.SRID, 0) @@ -339,6 +348,8 @@ JTSOpRunner.OpParams parseArgs(String[] args) throws ParseException, ClassNotFou cmdArgs.isIndexed = commandLine.hasOption(CommandOptions.INDEX); + cmdArgs.isQuiet = commandLine.hasOption(CommandOptions.QUIET); + cmdArgs.repeat = commandLine.hasOption(CommandOptions.REPEAT) ? commandLine.getOptionArgAsInt(CommandOptions.REPEAT, 0) : 1; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java index 76723c6c3e..cf82f216a7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/cmd/JTSOpRunner.java @@ -95,7 +95,8 @@ static class OpParams { public boolean isGeomAB = false; public boolean isCollect = false; - String format = null; + public boolean isQuiet = false; + public String format = null; public Integer repeat; public boolean eachA = false; public boolean eachB = false; @@ -361,7 +362,9 @@ private Object executeFunctionOnce(Geometry geomA, GeometryFunction func, Object if (param.validate) { validate(result); } - outputResult(result, param.isExplode, param.format); + if (! param.isQuiet) { + outputResult(result, param.isExplode, param.format); + } return result; } @@ -430,7 +433,7 @@ private List readGeometry(String geomLabel, String filename, String ge if (filename == null) return null; // must be a filename - if (filename.equalsIgnoreCase(CommandOptions.STDIN)){ + if (filename.equalsIgnoreCase(CommandOptions.SOURCE_STDIN)){ return readStdin(limit, offset); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java index 61915b749a..3986d0bf94 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConversionFunctions.java @@ -73,12 +73,12 @@ public static Geometry toMultiPolygon(Geometry g1, Geometry g2) .createMultiPolygon( GeometryFactory.toPolygonArray(polys)); } - public static Geometry toGeometryCollection(Geometry g, Geometry g2) + public static Geometry toGeometryCollection(Geometry g1, Geometry g2) { List atomicGeoms = new ArrayList(); - if (g != null) addComponents(g, atomicGeoms); + if (g1 != null) addComponents(g1, atomicGeoms); if (g2 != null) addComponents(g2, atomicGeoms); - return g.getFactory().createGeometryCollection( + return FunctionsUtil.getFactoryOrDefault(g1, g2).createGeometryCollection( GeometryFactory.toGeometryArray(atomicGeoms)); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java index 74f52f6a1b..0e847718f2 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java @@ -11,6 +11,7 @@ */ package org.locationtech.jtstest.function; +import java.util.Arrays; import java.util.List; import org.locationtech.jts.coverage.CoverageGapFinder; @@ -62,20 +63,82 @@ public static Geometry union(Geometry coverage) { public static Geometry simplify(Geometry coverage, double tolerance) { Geometry[] cov = toGeometryArray(coverage); Geometry[] result = CoverageSimplifier.simplify(cov, tolerance); - return FunctionsUtil.buildGeometry(result); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify a coverage with a smoothness weight") + public static Geometry simplifySharp(Geometry coverage, + @Metadata(title="Distance tol") + double tolerance, + @Metadata(title="Weight") + double weight) { + Geometry[] cov = toGeometryArray(coverage); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + simplifier.setSmoothWeight(weight); + Geometry[] result = simplifier.simplify(tolerance); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify a coverage with a ring removal size factor") + public static Geometry simplifyRemoveRings(Geometry coverage, + @Metadata(title="Distance tol") + double tolerance, + @Metadata(title="Removal Size Factor") + double factor) { + Geometry[] cov = toGeometryArray(coverage); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + simplifier.setRemovableRingSizeFactor(factor); + Geometry[] result = simplifier.simplify(tolerance); + return coverage.getFactory().createGeometryCollection(result); } @Metadata(description="Simplify inner edges of a coverage") - public static Geometry simplifyinner(Geometry coverage, double tolerance) { + public static Geometry simplifyInner(Geometry coverage, double tolerance) { Geometry[] cov = toGeometryArray(coverage); Geometry[] result = CoverageSimplifier.simplifyInner(cov, tolerance); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify outer edges of a coverage") + public static Geometry simplifyOuter(Geometry coverage, double tolerance) { + Geometry[] cov = toGeometryArray(coverage); + Geometry[] result = CoverageSimplifier.simplifyOuter(cov, tolerance); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify inner and outer edges of a coverage differently") + public static Geometry simplifyInOut(Geometry coverage, + @Metadata(title="Inner Distance tol") + double toleranceInner, + @Metadata(title="Outer Distance tol") + double toleranceOuter) { + Geometry[] cov = toGeometryArray(coverage); + CoverageSimplifier simplifier = new CoverageSimplifier(cov); + Geometry[] result = simplifier.simplify(toleranceInner, toleranceOuter); + return coverage.getFactory().createGeometryCollection(result); + } + + @Metadata(description="Simplify a coverage with per-geometry tolerances") + public static Geometry simplifyTolerances(Geometry coverage, + @Metadata(title="Tolerances (comma-sep)") + String tolerancesCSV) { + Geometry[] cov = toGeometryArray(coverage); + double[] tolerances = tolerances(tolerancesCSV, cov.length); + Geometry[] result = CoverageSimplifier.simplify(cov, tolerances); return FunctionsUtil.buildGeometry(result); } - static Geometry extractPolygons(Geometry geom) { - List components = PolygonExtracter.getPolygons(geom); - Geometry result = geom.getFactory().buildGeometry(components); - return result; + private static double[] tolerances(String csvList, int len) { + Double[] tolsDouble = toDoubleArray(csvList); + double[] tols = new double[len]; + for (int i = 0; i < tolsDouble.length; i++) { + tols[i] = tolsDouble[i]; + } + return tols; + } + + private static Double[] toDoubleArray(String csvList) { + return Arrays.stream(csvList.split(",")).map(Double::parseDouble).toArray(Double[]::new); } private static Geometry[] toGeometryArray(Geometry geom) { diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java index 367bf706a5..e211bbeff3 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/DistanceFunctions.java @@ -82,6 +82,10 @@ public static double distanceIndexed(Geometry a, Geometry b) { return IndexedFacetDistance.distance(a, b); } + public static boolean isWithinDistanceIndexed(Geometry a, Geometry b, double distance) { + return IndexedFacetDistance.isWithinDistance(a, b, distance); + } + public static Geometry nearestPointsIndexed(Geometry a, Geometry b) { Coordinate[] pts = IndexedFacetDistance.nearestPoints(a, b); return a.getFactory().createLineString(pts); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java index 008784e6cc..1302a5e6dc 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/OffsetCurveFunctions.java @@ -16,6 +16,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.util.GeometryCombiner; +import org.locationtech.jts.operation.buffer.BufferParameters; import org.locationtech.jts.operation.buffer.OffsetCurve; import org.locationtech.jtstest.geomfunction.Metadata; @@ -75,4 +76,25 @@ public static Geometry rawCurve(Geometry geom, double distance) return curve; } + public static Geometry rawCurveWithParams(Geometry geom, + Double distance, + @Metadata(title="Quadrant Segs") + Integer quadrantSegments, + @Metadata(title="NOT USED") + Integer capStyle, + @Metadata(title="Join style") + Integer joinStyle, + @Metadata(title="Mitre limit") + Double mitreLimit) + { + BufferParameters bufferParams = new BufferParameters(); + if (quadrantSegments >= 0) bufferParams.setQuadrantSegments(quadrantSegments); + if (joinStyle >= 0) bufferParams.setJoinStyle(joinStyle); + if (mitreLimit >= 0) bufferParams.setMitreLimit(mitreLimit); + Coordinate[] pts = OffsetCurve.rawOffset((LineString) geom, distance, bufferParams); + Geometry curve = geom.getFactory().createLineString(pts); + return curve; + } + + } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java new file mode 100644 index 0000000000..a0c8ed77b5 --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/OrientationFPFunctions.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 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.jtstest.function; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +public class OrientationFPFunctions { + + public static int orientationIndex(Geometry segment, Geometry ptGeom) { + if (segment.getNumPoints() != 2 || ptGeom.getNumPoints() != 1) { + throw new IllegalArgumentException("A must have two points and B must have one"); + } + Coordinate[] segPt = segment.getCoordinates(); + + Coordinate p = ptGeom.getCoordinate(); + int index = orientationIndex(segPt[0], segPt[1], p); + return index; + } + + private static int orientationIndex(Coordinate p1, Coordinate p2, Coordinate q) + { + double dx1 = p2.x - p1.x; + double dy1 = p2.y - p1.y; + double dx2 = q.x - p2.x; + double dy2 = q.y - p2.y; + double det = dx1*dy2 - dx2*dy1; + if (det > 0.0) return 1; + if (det < 0.0) return -1; + return 0; + } +} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java index c58f9aea6e..047c5e48e7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionFunctions.java @@ -16,12 +16,23 @@ import java.util.List; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; import org.locationtech.jts.operation.distance.IndexedFacetDistance; - - public class SelectionFunctions { + + public static Geometry intersectsPrep(Geometry a, final Geometry mask) + { + PreparedGeometry prep = PreparedGeometryFactory.prepare(mask); + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return prep.intersects(g); + } + }); + } + public static Geometry intersects(Geometry a, final Geometry mask) { return select(a, new GeometryPredicate() { @@ -35,7 +46,17 @@ public static Geometry covers(Geometry a, final Geometry mask) { return select(a, new GeometryPredicate() { public boolean isTrue(Geometry g) { - return g.covers(mask); + return mask.covers(g); + } + }); + } + + public static Geometry coversPrep(Geometry a, final Geometry mask) + { + PreparedGeometry prep = PreparedGeometryFactory.prepare(mask); + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return prep.covers(g); } }); } @@ -49,6 +70,15 @@ public boolean isTrue(Geometry g) { }); } + public static Geometry touches(Geometry a, final Geometry mask) + { + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return mask.touches(g); + } + }); + } + public static Geometry disjoint(Geometry a, Geometry mask) { List selected = new ArrayList(); @@ -60,6 +90,16 @@ public static Geometry disjoint(Geometry a, Geometry mask) } return a.getFactory().buildGeometry(selected); } + + public static Geometry relatePattern(Geometry a, final Geometry mask, String pattern) + { + return select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return mask.relate(g, pattern); + } + }); + } + public static Geometry valid(Geometry a) { List selected = new ArrayList(); @@ -168,7 +208,7 @@ public boolean isTrue(Geometry g) { }); } - private static Geometry select(Geometry geom, GeometryPredicate pred) + public static Geometry select(Geometry geom, GeometryPredicate pred) { List selected = new ArrayList(); for (int i = 0; i < geom.getNumGeometries(); i++ ) { 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 new file mode 100644 index 0000000000..c571e0b3f5 --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SelectionNGFunctions.java @@ -0,0 +1,138 @@ +/* + * 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.jtstest.function; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +public class SelectionNGFunctions +{ + public static Geometry intersects(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.intersects()); + } + }); + } + + public static Geometry intersectsPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + Envelope maskEnv = mask.getEnvelopeInternal(); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + if (maskEnv.disjoint(g.getEnvelopeInternal())) + return false; + return relateNG.evaluate(g, RelatePredicate.intersects()); + } + }); + } + + public static Geometry contains(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.contains()); + } + }); + } + + public static Geometry covers(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.covers()); + } + }); + } + + public static Geometry coversPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + Envelope maskEnv = mask.getEnvelopeInternal(); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + if (maskEnv.disjoint(g.getEnvelopeInternal())) + return false; + return relateNG.evaluate(g, RelatePredicate.covers()); + } + }); + } + + public static Geometry touches(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.touches()); + } + }); + } + + public static Geometry touchesPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + Envelope maskEnv = mask.getEnvelopeInternal(); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + if (maskEnv.disjoint(g.getEnvelopeInternal())) + return false; + return relateNG.evaluate(g, RelatePredicate.touches()); + } + }); + } + + public static Geometry adjacent(Geometry a, final Geometry mask) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + }); + } + + public static Geometry adjacentPrep(Geometry a, final Geometry mask) + { + RelateNG relateNG = RelateNG.prepare(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(g, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + }); + } + + public static Geometry relatePattern(Geometry a, final Geometry mask, String pattern) + { + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return RelateNG.relate(mask, g, RelatePredicate.matches(pattern)); + } + }); + } + + public static Geometry relatePatternPrep(Geometry a, final Geometry mask, String pattern) + { + RelateNG relateNG = RelateNG.prepare(mask); + return SelectionFunctions.select(a, new GeometryPredicate() { + public boolean isTrue(Geometry g) { + return relateNG.evaluate(g, RelatePredicate.matches(pattern)); + } + }); + } + +} + + diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java index 7e81aaa71a..f4e8b5f0a7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateFunctions.java @@ -14,6 +14,8 @@ import org.locationtech.jts.algorithm.BoundaryNodeRule; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.operation.relate.RelateOp; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; /** * Implementations for spatial predicate functions. @@ -34,12 +36,24 @@ public class SpatialPredicateFunctions { public static boolean overlaps(Geometry a, Geometry b) { return a.overlaps(b); } public static boolean touches(Geometry a, Geometry b) { return a.touches(b); } - public static boolean interiorIntersects(Geometry a, Geometry b) { return a.relate(b, "T********"); } - public static boolean adjacentTo(Geometry a, Geometry b) { return a.relate(b, "F***T****"); } - - public static String relate(Geometry a, Geometry b) { + public static boolean interiorIntersects(Geometry a, Geometry b) { + return a.relate(b, IntersectionMatrixPattern.INTERIOR_INTERSECTS); + } + + public static boolean adjacent(Geometry a, Geometry b) { + return a.relate(b, IntersectionMatrixPattern.ADJACENT); + } + + public static boolean containsProperly(Geometry a, Geometry b) { + return a.relate(b, IntersectionMatrixPattern.CONTAINS_PROPERLY); + } + + public static String relateMatrix(Geometry a, Geometry b) { return a.relate(b).toString(); } + public static boolean relate(Geometry a, Geometry b, String mask) { + return a.relate(b, mask); + } public static String relateEndpoint(Geometry a, Geometry b) { return RelateOp.relate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE).toString(); } 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 new file mode 100644 index 0000000000..17f01c2404 --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/SpatialPredicateNGFunctions.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 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.jtstest.function; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +public class SpatialPredicateNGFunctions { + public static boolean contains(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.contains()); + } + public static boolean covers(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.covers()); + } + public static boolean coveredBy(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.coveredBy()); + } + public static boolean disjoint(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.disjoint()); + } + public static boolean equals(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.equalsTopo()); + } + public static boolean equalsTopo(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.equalsTopo()); + } + public static boolean intersects(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.intersects()); + } + public static boolean crosses(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.crosses()); + } + public static boolean overlaps(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.overlaps()); + } + public static boolean touches(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.touches()); + } + public static boolean within(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.within()); + } + + public static boolean adjacent(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + + public static boolean containsProperly(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.CONTAINS_PROPERLY)); + } + + public static boolean interiorIntersects(Geometry a, Geometry b) { + return RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.INTERIOR_INTERSECTS)); + } + + public static boolean relate(Geometry a, Geometry b, String mask) { + return RelateNG.relate(a, b, mask); + } + public static String relateMatrix(Geometry a, Geometry b) { + return RelateNG.relate(a, b).toString(); + } + public static String relateEndpoint(Geometry a, Geometry b) { + return RelateNG.relate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE).toString(); + } + public static String relateMultiValent(Geometry a, Geometry b) { + return RelateNG.relate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE).toString(); + } + public static String relateMonoValent(Geometry a, Geometry b) { + return RelateNG.relate(a, b, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE).toString(); + } +} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java index 0e64d6e115..f99b1d74f7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/TriangleFunctions.java @@ -52,6 +52,15 @@ public static double circumradius(Geometry g) return Triangle.circumradius(pts[0], pts[1], pts[2]); } + public static Geometry circumcircle(Geometry g, int quadSegs) + { + Coordinate[] pts = trianglePts(g); + Coordinate cc = Triangle.circumcentreDD(pts[0], pts[1], pts[2]); + Geometry ccPt = g.getFactory().createPoint(cc); + double cr = Triangle.circumradius(pts[0], pts[1], pts[2]); + return ccPt.buffer(cr, quadSegs); + } + public static Geometry circumcentreDD(Geometry g) { return GeometryMapper.map(g, diff --git a/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java b/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java index 568e0f419d..c7f02cc2a2 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/geomfunction/GeometryFunctionRegistry.java @@ -45,6 +45,7 @@ import org.locationtech.jtstest.function.LinearReferencingFunctions; import org.locationtech.jtstest.function.NodingFunctions; import org.locationtech.jtstest.function.OffsetCurveFunctions; +import org.locationtech.jtstest.function.OrientationFPFunctions; import org.locationtech.jtstest.function.OrientationFunctions; import org.locationtech.jtstest.function.OverlayFunctions; import org.locationtech.jtstest.function.OverlayNGFunctions; @@ -61,11 +62,13 @@ import org.locationtech.jtstest.function.PrecisionFunctions; import org.locationtech.jtstest.function.PreparedGeometryFunctions; import org.locationtech.jtstest.function.SelectionFunctions; +import org.locationtech.jtstest.function.SelectionNGFunctions; import org.locationtech.jtstest.function.SimplificationFunctions; import org.locationtech.jtstest.function.SnappingFunctions; import org.locationtech.jtstest.function.SortingFunctions; import org.locationtech.jtstest.function.SpatialIndexFunctions; import org.locationtech.jtstest.function.SpatialPredicateFunctions; +import org.locationtech.jtstest.function.SpatialPredicateNGFunctions; import org.locationtech.jtstest.function.TriangleFunctions; import org.locationtech.jtstest.function.TriangulatePolyFunctions; import org.locationtech.jtstest.function.TriangulationFunctions; @@ -102,6 +105,7 @@ public static GeometryFunctionRegistry createTestBuilderRegistry() funcRegistry.add(PrecisionFunctions.class); funcRegistry.add(PreparedGeometryFunctions.class); funcRegistry.add(SelectionFunctions.class); + funcRegistry.add(SelectionNGFunctions.class); funcRegistry.add(SimplificationFunctions.class); funcRegistry.add(AffineTransformationFunctions.class); funcRegistry.add(DiffFunctions.class); @@ -112,10 +116,12 @@ public static GeometryFunctionRegistry createTestBuilderRegistry() funcRegistry.add(CreateRandomShapeFunctions.class); funcRegistry.add(SpatialIndexFunctions.class); funcRegistry.add(SpatialPredicateFunctions.class); + funcRegistry.add(SpatialPredicateNGFunctions.class); funcRegistry.add(JTSFunctions.class); //funcRegistry.add(MemoryFunctions.class); funcRegistry.add(OffsetCurveFunctions.class); funcRegistry.add(OrientationFunctions.class); + funcRegistry.add(OrientationFPFunctions.class); funcRegistry.add(LineSegmentFunctions.class); funcRegistry.add(OverlayFunctions.class); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java index 812984c6d2..f974683738 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppColors.java @@ -16,9 +16,31 @@ public class AppColors { + public static final Color GEOM_A = Color.BLUE; + public static final Color GEOM_B = Color.RED; + public static final Color GEOM_RESULT = new Color(100, 150, 0); // YellowGreen + + public static final Color GEOM_A_HIGHLIGHT_CLR = new Color(0, 0, 255); + public static final Color GEOM_A_LINE_CLR = new Color(0, 0, 255, 150); + public static final Color GEOM_A_FILL_CLR = new Color(200, 200, 255, 150); + public static final Color GEOM_A_BAND = Color.cyan; + + public static final Color GEOM_B_HIGHLIGHT_CLR = new Color(255, 0, 0); + public static final Color GEOM_B_LINE_CLR = new Color(150, 0, 0, 150); + public static final Color GEOM_B_FILL_CLR = new Color(255, 200, 200, 150); + public static final Color GEOM_B_BAND = Color.pink; + // YellowGreen + public static final Color GEOM_RESULT_LINE_CLR = new Color(120, 180, 0, 200); + // Yellow + public static final Color GEOM_RESULT_FILL_CLR = new Color(255, 255, 100, 100); + + public static final Color GEOM_VIEW_BACKGROUND = Color.white; + + public static final Color BACKGROUND_FOCUS = Color.white; public static final Color BACKGROUND = UIManager.getColor ( "Panel.background" ); public static final Color BACKGROUND_ERROR = Color.PINK; public static final Color TAB_FOCUS = UIManager.getColor ("TabbedPane.highlight" ); - public static final Color GEOM_VIEW_BACKGROUND = Color.white; + + } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java index b927e7c223..8276105023 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java @@ -23,12 +23,12 @@ public class AppConstants { public static final int POINT_SIZE = 5; public static final int VERTEX_SIZE = 4; - public static double HIGHLIGHT_SIZE = 50.0; - public static double VERTEX_SHADOW_SIZE = 100; + public static final double HIGHLIGHT_SIZE = 50.0; + public static final double VERTEX_SHADOW_SIZE = 100; - public static double TOPO_STRETCH_VIEW_DIST = 5; + public static final double TOPO_STRETCH_VIEW_DIST = 5; - public static double MASK_WIDTH_FRAC = 0.3333; + public static final double MASK_WIDTH_FRAC = 0.3333; // a very light gray public static final Color MASK_CLR = new Color(230, 230, 230); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java index 9c8ff00154..13b62e0473 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java @@ -18,6 +18,7 @@ public class AppStrings { public static final String GEOM_LABEL_A = "A"; public static final String GEOM_LABEL_B = "B"; + public static final String GEOM_LABEL_RESULT = "Result"; public static final String TAB_LABEL_LOG = "Log"; public static final String TAB_LABEL_VALUE = "Value"; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java deleted file mode 100644 index e98a35915d..0000000000 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryDepiction.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2016 Vivid Solutions. - * - * 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.jtstest.testbuilder; - -import java.awt.Color; - - -/** - * @version 1.7 - */ -public class GeometryDepiction -{ - - public static final Color GEOM_A_HIGHLIGHT_CLR = new Color(0, 0, 255); - public static final Color GEOM_A_LINE_CLR = new Color(0, 0, 255, 150); - public static final Color GEOM_A_FILL_CLR = new Color(200, 200, 255, 150); - - public static final Color GEOM_B_HIGHLIGHT_CLR = new Color(255, 0, 0); - public static final Color GEOM_B_LINE_CLR = new Color(150, 0, 0, 150); - public static final Color GEOM_B_FILL_CLR = new Color(255, 200, 200, 150); - - // YellowGreen - public static final Color GEOM_RESULT_LINE_CLR = new Color(120, 180, 0, 200); - // Yellow - public static final Color GEOM_RESULT_FILL_CLR = new Color(255, 255, 100, 100); - - public static final GeometryDepiction RESULT = new GeometryDepiction( - new Color(154, 205, 0, 150), - new Color(255, 255, 100, 100), - // Yellow - null); - - public static final GeometryDepiction GEOM_A = new GeometryDepiction( - new Color(0, 0, 255, 150), - new Color(200, 200, 255, 150), - Color.cyan); - - public static final GeometryDepiction GEOM_B = new GeometryDepiction( - new Color(255, 0, 0, 150), - new Color(255, 200, 200, 150), - Color.pink); - - - private Color color; - private Color fillColor; - private Color bandColor; - - public Color getColor() { - return color; - } - - public Color getFillColor() { - return fillColor; - } - - public Color getBandColor() { - return bandColor; - } - - public GeometryDepiction(Color color, Color fillColor, Color bandColor) { - this.color = color; - this.fillColor = fillColor; - this.bandColor = bandColor; - } -} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java index 3ca39506c5..4877583f14 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java @@ -733,8 +733,8 @@ public void renderMagnifiedVertices(Graphics2D g) for (int j = 0; j < stretchedVerts.size(); j++) { Coordinate p = (Coordinate) stretchedVerts.get(j); drawHighlightedVertex(g, p, - i == 0 ? GeometryDepiction.GEOM_A_HIGHLIGHT_CLR : - GeometryDepiction.GEOM_B_HIGHLIGHT_CLR); + i == 0 ? AppColors.GEOM_A_HIGHLIGHT_CLR : + AppColors.GEOM_B_HIGHLIGHT_CLR); } } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java index 2b4d5b8048..1a403a67e1 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryInputDialog.java @@ -80,9 +80,9 @@ void jbInit() throws Exception { border1 = BorderFactory.createLineBorder(Color.gray, 2); panel1.setLayout(borderLayout1); jLabel1.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel1.setForeground(Color.blue); + jLabel1.setForeground(AppColors.GEOM_A); jLabel1.setToolTipText(""); - jLabel1.setText("A"); + jLabel1.setText(AppStrings.GEOM_LABEL_A); jPanel1.setLayout(gridBagLayout2); btnLoad.setToolTipText(""); btnLoad.setText("Load"); @@ -100,8 +100,8 @@ public void actionPerformed(ActionEvent e) { } }); jLabel2.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel2.setForeground(Color.red); - jLabel2.setText("B"); + jLabel2.setForeground(AppColors.GEOM_B); + jLabel2.setText(AppStrings.GEOM_LABEL_B); lblError.setToolTipText(""); txtError.setLineWrap(true); txtError.setBorder(BorderFactory.createEtchedBorder()); @@ -211,9 +211,9 @@ void btnCancel_actionPerformed(ActionEvent e) { void btnLoad_actionPerformed(ActionEvent e) { parseError = false; - geom[0] = parseGeometry(txtA, Color.blue); + geom[0] = parseGeometry(txtA, AppColors.GEOM_A); if (!parseError) - geom[1] = parseGeometry(txtB, Color.red); + geom[1] = parseGeometry(txtB, AppColors.GEOM_B); if (!parseError) setVisible(false); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java index cfcb246a1e..3da5216c11 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java @@ -41,6 +41,8 @@ public class GeometryTreeModel implements TreeModel public static Comparator SORT_AREA_DESC = new AreaComparator(true); public static Comparator SORT_LEN_ASC = new LengthComparator(false); public static Comparator SORT_LEN_DESC = new LengthComparator(true); + public static Comparator SORT_NUMPTS_ASC = new NumPointsComparator(false); + public static Comparator SORT_NUMPTS_DESC = new NumPointsComparator(true); private Vector treeModelListeners = new Vector(); @@ -153,6 +155,21 @@ public int compare(GeometricObjectNode o1, GeometricObjectNode o2) { return dirFactor * Double.compare(area1, area2); } } + public static class NumPointsComparator implements Comparator { + + private int dirFactor; + + public NumPointsComparator(boolean direction) { + this.dirFactor = direction ? 1 : -1; + } + + @Override + public int compare(GeometricObjectNode o1, GeometricObjectNode o2) { + int num1 = o1.getGeometry().getNumPoints(); + int num2 = o2.getGeometry().getNumPoints(); + return dirFactor * Integer.compare(num1, num2); + } + } } abstract class GeometricObjectNode diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java index 62fd8f2162..c80849590a 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java @@ -47,8 +47,8 @@ public class InspectorPanel extends TestBuilderPanel { private Geometry geometry; private Comparator sorterArea; - private Comparator sorterLen; + private Comparator sorterNumPoints; public InspectorPanel() { this(true); @@ -128,6 +128,11 @@ public void actionPerformed(ActionEvent e) { sortByLen(); } }); + JButton btnSortByNumPts = SwingUtil.createButton(AppIcons.ICON_POINT, "Sort by Num Points (Asc/Desc)", new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + sortByNumPoints(); + } + }); JPanel btn2Panel = new JPanel(); btn2Panel.setLayout(new BoxLayout(btn2Panel, BoxLayout.PAGE_AXIS)); @@ -148,6 +153,7 @@ public void actionPerformed(ActionEvent e) btn2Panel.add(Box.createRigidArea(new Dimension(0, 10))); btn2Panel.add(new JLabel("Sort")); + btn2Panel.add(btnSortByNumPts); btn2Panel.add(btnSortByLen); btn2Panel.add(btnSortByArea); btn2Panel.add(btnSortNone); @@ -199,7 +205,7 @@ public void setGeometry(String tag, Geometry geom, int source, boolean isEditabl btnDelete.setEnabled(isEditable); lblGeom.setText(tag); lblGeom.setToolTipText(tag); - lblGeom.setForeground(source == 0 ? Color.BLUE : Color.RED); + lblGeom.setForeground(source == 0 ? AppColors.GEOM_A : AppColors.GEOM_B); sortNone(); } @@ -214,12 +220,14 @@ public void sortNone() { sorterLen = null; sorterArea = null; + sorterNumPoints = null; geomTreePanel.populate(geometry, source); } public void sortByArea() { sorterLen = null; + sorterNumPoints = null; if (sorterArea == GeometryTreeModel.SORT_AREA_ASC) { sorterArea = GeometryTreeModel.SORT_AREA_DESC; @@ -233,6 +241,8 @@ public void sortByArea() public void sortByLen() { sorterArea = null; + sorterNumPoints = null; + if (sorterLen == GeometryTreeModel.SORT_LEN_ASC) { sorterLen = GeometryTreeModel.SORT_LEN_DESC; } @@ -242,6 +252,18 @@ public void sortByLen() geomTreePanel.populate(geometry, source, sorterLen); } - + public void sortByNumPoints() + { + sorterArea = null; + sorterLen = null; + + if (sorterNumPoints == GeometryTreeModel.SORT_NUMPTS_ASC) { + sorterNumPoints = GeometryTreeModel.SORT_NUMPTS_DESC; + } + else { + sorterNumPoints = GeometryTreeModel.SORT_NUMPTS_ASC; + } + geomTreePanel.populate(geometry, source, sorterNumPoints); + } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java index 7e4a44e9c3..750ec09fcb 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java @@ -466,10 +466,6 @@ public void stateChanged(ChangeEvent e) jSplitPane1.add(panelTop, JSplitPane.TOP); jSplitPane1.add(panelBottom, JSplitPane.BOTTOM); - /* - border4 = BorderFactory.createBevelBorder(BevelBorder.LOWERED, Color.white, - Color.white, new Color(93, 93, 93), new Color(134, 134, 134)); - */ contentPane = (JPanel) this.getContentPane(); contentPane.setLayout(contentLayout); contentPane.setPreferredSize(new Dimension(601, 690)); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java index 7b2dee2a89..bf94202278 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/RelatePanel.java @@ -263,20 +263,20 @@ void jbInit() throws Exception { relateIB.setText("F"); jPanel1.setLayout(gridBagLayout2); jLabel14.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel14.setForeground(Color.blue); - jLabel14.setText("A"); + jLabel14.setForeground(AppColors.GEOM_A); + jLabel14.setText(AppStrings.GEOM_LABEL_A); jLabel13.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel13.setForeground(Color.blue); + jLabel13.setForeground(AppColors.GEOM_A); jLabel13.setText("Ext"); jLabel12.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel12.setForeground(Color.blue); + jLabel12.setForeground(AppColors.GEOM_A); jLabel12.setText("Bdy"); jLabel11.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel11.setForeground(Color.blue); + jLabel11.setForeground(AppColors.GEOM_A); jLabel11.setToolTipText(""); jLabel11.setText("Int"); jLabel10.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel10.setForeground(Color.red); + jLabel10.setForeground(AppColors.GEOM_B); jLabel10.setToolTipText(""); jLabel10.setText("Ext"); txtAB.setBackground(AppColors.BACKGROUND); @@ -288,25 +288,25 @@ void jbInit() throws Exception { txtAB.setEditable(false); txtAB.setHorizontalAlignment(SwingConstants.LEFT); jLabel23.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel23.setForeground(Color.red); - jLabel23.setText("B"); + jLabel23.setForeground(AppColors.GEOM_B); + jLabel23.setText(AppStrings.GEOM_LABEL_B); relateBI.setFont(new java.awt.Font("Dialog", 1, 12)); relateBI.setText("F"); jLabel22.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel22.setForeground(Color.blue); + jLabel22.setForeground(AppColors.GEOM_A); jLabel22.setToolTipText(""); - jLabel22.setText("A"); + jLabel22.setText(AppStrings.GEOM_LABEL_A); relateEI.setFont(new java.awt.Font("Dialog", 1, 12)); relateEI.setText("F"); jLabel21.setToolTipText(""); jLabel21.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel21.setForeground(Color.blue); + jLabel21.setForeground(AppColors.GEOM_A); jLabel21.setToolTipText(""); - jLabel21.setText("A"); + jLabel21.setText(AppStrings.GEOM_LABEL_A); jLabel20.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel20.setForeground(Color.red); + jLabel20.setForeground(AppColors.GEOM_B); jLabel20.setToolTipText(""); - jLabel20.setText("B"); + jLabel20.setText(AppStrings.GEOM_LABEL_B); relateBE.setFont(new java.awt.Font("Dialog", 1, 12)); relateBE.setText("F"); relateEE.setFont(new java.awt.Font("Dialog", 1, 12)); @@ -321,18 +321,18 @@ void jbInit() throws Exception { relateBB.setFont(new java.awt.Font("Dialog", 1, 12)); relateBB.setText("F"); jLabel9.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel9.setForeground(Color.red); + jLabel9.setForeground(AppColors.GEOM_B); jLabel9.setToolTipText(""); jLabel9.setText("Bdy"); relateEB.setFont(new java.awt.Font("Dialog", 1, 12)); relateEB.setText("F"); jLabel8.setFont(new java.awt.Font("Dialog", 2, 12)); - jLabel8.setForeground(Color.red); + jLabel8.setForeground(AppColors.GEOM_B); jLabel8.setToolTipText(""); jLabel8.setText("Int"); jLabel7.setFont(new java.awt.Font("Dialog", 1, 12)); - jLabel7.setForeground(Color.red); - jLabel7.setText("B"); + jLabel7.setForeground(AppColors.GEOM_B); + jLabel7.setText(AppStrings.GEOM_LABEL_B); relateII.setBackground(Color.white); relateII.setFont(new java.awt.Font("Dialog", 1, 12)); relateII.setText("F"); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java index dca7dc145f..b47ae1c91c 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java @@ -356,9 +356,9 @@ public void actionPerformed(ActionEvent e) { tabFunctions.add(spatialFunctionPanel, "Geometry"); tabFunctions.add(scalarFunctionPanel, "Scalar"); - jTabbedPane1.add(tabFunctions, "Functions"); - jTabbedPane1.add(relateTabPanel, "Predicates"); - jTabbedPane1.add(validPanel, "Valid / Mark"); + jTabbedPane1.add(tabFunctions, "Function"); + jTabbedPane1.add(relateTabPanel, "Predicate"); + jTabbedPane1.add(validPanel, "Valid/Mark"); relateTabPanel.add(relatePanel, BorderLayout.CENTER); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java index a0f3737231..fa6355db13 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCaseTextDialog.java @@ -263,9 +263,9 @@ void rbGML_actionPerformed(ActionEvent e) { private void writeView(String a, String b, String result) { txtGeomView.setText(""); - writeViewGeometry("A", a); - writeViewGeometry("B", b); - writeViewGeometry("Result", result); + writeViewGeometry(AppStrings.GEOM_LABEL_A, a); + writeViewGeometry(AppStrings.GEOM_LABEL_B, b); + writeViewGeometry(AppStrings.GEOM_LABEL_RESULT, result); } private void writeViewGeometry(String tag, String str) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java index e9bf9a801e..c245d06698 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ValidPanel.java @@ -14,22 +14,29 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; -import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; -import java.awt.Insets; import java.awt.event.ActionEvent; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.util.Vector; + import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JRadioButton; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.io.WKTWriter; import org.locationtech.jts.operation.valid.IsSimpleOp; import org.locationtech.jts.operation.valid.IsValidOp; @@ -54,7 +61,7 @@ public class ValidPanel extends JPanel { JTextField txtIsSimple = new JTextField(); JTextArea taInvalidMsg = new JTextArea(); JLabel lblValidSimple = new JLabel(); - JPanel jPanel1 = new JPanel(); + JPanel panelValidSimple = new JPanel(); private transient Vector validPanelListeners; GridLayout gridLayout1 = new GridLayout(); JPanel markPanel = new JPanel(); @@ -66,6 +73,9 @@ public class ValidPanel extends JPanel { JButton btnClearMark = new JButton(); JButton btnSetMark = new JButton(); private JCheckBox cbInvertedRingAllowed; + JRadioButton rbA = new JRadioButton(); + JRadioButton rbB = new JRadioButton(); + JRadioButton rbResult = new JRadioButton(); public ValidPanel() { try { @@ -98,7 +108,48 @@ public void actionPerformed(ActionEvent e) { clearAll(); } }); + + rbA.setSelected(true); + rbA.setText(AppStrings.GEOM_LABEL_A); + rbA.setForeground(AppColors.GEOM_A); + rbB.setText(AppStrings.GEOM_LABEL_B); + rbB.setForeground(AppColors.GEOM_B); + rbResult.setText(AppStrings.GEOM_LABEL_RESULT); + rbResult.setForeground(AppColors.GEOM_RESULT); + rbA.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + clearAll(); + } + }}); + rbB.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + clearAll(); + } + }}); + rbResult.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + clearAll(); + } + }}); + + + ButtonGroup btnGrpStdInFormat = new ButtonGroup(); + btnGrpStdInFormat.add(rbA); + btnGrpStdInFormat.add(rbB); + btnGrpStdInFormat.add(rbResult); + + JPanel panelABR = new JPanel(); + panelABR.setLayout(new BoxLayout(panelABR, BoxLayout.X_AXIS)); + panelABR.add(rbA); + panelABR.add(rbB); + panelABR.add(rbResult); cbInvertedRingAllowed = new JCheckBox(); cbInvertedRingAllowed.setToolTipText(AppStrings.TIP_ALLOW_INVERTED_RINGS); @@ -152,9 +203,9 @@ public void actionPerformed(ActionEvent e) { } }); - JPanel panelValidSimple = new JPanel(); - panelValidSimple.add(btnValidate); - panelValidSimple.add(txtIsValid); + JPanel panelValid = new JPanel(); + panelValid.add(btnValidate); + panelValid.add(txtIsValid); JPanel panelSimple = new JPanel(); panelSimple.add(btnSimple); @@ -164,28 +215,18 @@ public void actionPerformed(ActionEvent e) { panelMsg.add(taInvalidMsg); JPanel panelClear = new JPanel(); - panelClear.add(btnClear); + panelClear.setLayout(new BorderLayout()); + panelClear.add(cbInvertedRingAllowed, BorderLayout.WEST); + panelClear.add(btnClear, BorderLayout.EAST); - jPanel1.setLayout(new GridBagLayout()); - jPanel1.add(panelSimple, new GridBagConstraints(0, 1, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(10, 5, 10, 5), 0, 0)); - jPanel1.add(panelValidSimple, new GridBagConstraints(0, 2, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(10, 5, 10, 5), 0, 0)); - jPanel1.add(cbInvertedRingAllowed, new GridBagConstraints(0, 3, 2, 1, 1.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(4, 0, 4, 0), 10, 0)); + panelValidSimple.setLayout(new BoxLayout(panelValidSimple, BoxLayout.Y_AXIS)); + panelValidSimple.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panelValidSimple.add(panelABR); + panelValidSimple.add(panelSimple); + panelValidSimple.add(panelValid); + panelValidSimple.add(panelClear); + panelValidSimple.add(panelMsg); - /*jPanel1.add(lblValidSimple, new GridBagConstraints(0, 4, 1, 1, 1.0, 0.0 - ,GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 4, 0, 4), 0, 0)); -*/ - /* - jPanel1.add(txtIsValid, new GridBagConstraints(1, 4, 1, 1, 1.0, 0.0 - ,GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(4, 0, 4, 0), 10, 0)); - */ - jPanel1.add(panelMsg, new GridBagConstraints(0, 4, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 4, 0, 4), 0, 0)); - jPanel1.add(panelClear, new GridBagConstraints(0, 5, 2, 1, 0.0, 0.0 - ,GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 4, 0, 4), 0, 0)); - //---------------------------------------------- txtMarkLocation.setBorder(BorderFactory.createLoweredBevelBorder()); txtMarkLocation.setToolTipText(""); @@ -209,7 +250,7 @@ public void actionPerformed(ActionEvent e) { //---------------------------------------------- this.setLayout(new BorderLayout()); - this.add(jPanel1, BorderLayout.CENTER); + this.add(panelValidSimple, BorderLayout.NORTH); this.add(markPanel, BorderLayout.SOUTH); } @@ -228,14 +269,12 @@ void clearAll() { void btnValidate_actionPerformed(ActionEvent e) { - TopologyValidationError err = null; - if (testCase.getGeometry(0) != null) { - IsValidOp validOp = new IsValidOp(testCase.getGeometry(0)); - if (cbInvertedRingAllowed.isSelected()) { - validOp.setSelfTouchingRingFormingHoleValid(true); - } - err = validOp.getValidationError(); - } + clearFlag(txtIsValid); + Geometry geom = getGeometry(); + if (geom == null) + return; + + TopologyValidationError err = checkValid(geom, cbInvertedRingAllowed.isSelected()); String msg = ""; boolean isValid = true; Coordinate invalidPoint = null; @@ -248,12 +287,34 @@ void btnValidate_actionPerformed(ActionEvent e) setFlagText(txtIsValid, isValid); setMarkPoint(invalidPoint); } + + private TopologyValidationError checkValid(Geometry geom, boolean isAllowInverted) { + TopologyValidationError err = null; + if (geom != null) { + IsValidOp validOp = new IsValidOp(geom); + if (isAllowInverted) { + validOp.setSelfTouchingRingFormingHoleValid(true); + } + err = validOp.getValidationError(); + } + return err; + } + + private Geometry getGeometry() { + if (rbA.isSelected()) + return testCase.getGeometry(0); + if (rbB.isSelected()) + return testCase.getGeometry(1); + return testCase.getResult(); + } + void btnSimple_actionPerformed(ActionEvent e) { boolean isSimple = true; Coordinate nonSimpleLoc = null; - if (testCase.getGeometry(0) != null) { - IsSimpleOp simpleOp = new IsSimpleOp(testCase.getGeometry(0)); + Geometry geom = getGeometry(); + if (geom != null) { + IsSimpleOp simpleOp = new IsSimpleOp(geom); isSimple = simpleOp.isSimple(); nonSimpleLoc = simpleOp.getNonSimpleLocation(); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java index 7b339ecbc2..6f86ea9f4d 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/WKTPanel.java @@ -150,14 +150,14 @@ public void actionPerformed(ActionEvent e) { panelAB.setLayout(gridBagLayout2); aLabel.setFont(new java.awt.Font("Dialog", 1, 16)); - aLabel.setForeground(Color.blue); - aLabel.setText("A"); + aLabel.setForeground(AppColors.GEOM_A); + aLabel.setText(AppStrings.GEOM_LABEL_A); aLabel.setPreferredSize(new Dimension(20, 20)); aLabel.setHorizontalTextPosition(SwingConstants.LEFT); bLabel.setFont(new java.awt.Font("Dialog", 1, 16)); - bLabel.setForeground(Color.red); - bLabel.setText("B"); + bLabel.setForeground(AppColors.GEOM_B); + bLabel.setText(AppStrings.GEOM_LABEL_B); bLabel.setPreferredSize(new Dimension(20, 20)); aScrollPane.setBorder(BorderFactory.createLoweredBevelBorder()); @@ -501,7 +501,7 @@ public void filesDropped(java.io.File[] files) { //Border otherBorder = BorderFactory.createEmptyBorder(); Border otherBorder = BorderFactory.createMatteBorder(0, 2, 0, 0, Color.white); - private static Color focusBackgroundColor = Color.white; //new Color(240,255,250); + private static Color focusBackgroundColor = AppColors.BACKGROUND_FOCUS; private static Color otherBackgroundColor = AppColors.BACKGROUND; private void setFocusGeometry(int index) { diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java index ecf525e61e..5de2ccf584 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java @@ -19,6 +19,7 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jtstest.testbuilder.AppStrings; import org.locationtech.jtstest.testbuilder.geom.ComponentLocater; import org.locationtech.jtstest.testbuilder.geom.GeometryLocation; import org.locationtech.jtstest.testbuilder.geom.SegmentExtracter; @@ -51,9 +52,9 @@ public LayerList() } void initFixed() { - layers.add(new Layer("A")); - layers.add(new Layer("B")); - layers.add(new Layer("Result")); + layers.add(new Layer(AppStrings.GEOM_LABEL_A)); + layers.add(new Layer(AppStrings.GEOM_LABEL_B)); + layers.add(new Layer(AppStrings.GEOM_LABEL_RESULT)); } public int size() { return layers.size(); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java index 5d09c5ca04..c084d2eb36 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java @@ -30,9 +30,9 @@ import org.locationtech.jts.util.Assert; import org.locationtech.jtstest.test.TestCaseList; import org.locationtech.jtstest.test.Testable; +import org.locationtech.jtstest.testbuilder.AppColors; import org.locationtech.jtstest.testbuilder.AppConstants; import org.locationtech.jtstest.testbuilder.AppStrings; -import org.locationtech.jtstest.testbuilder.GeometryDepiction; import org.locationtech.jtstest.testbuilder.ui.SwingUtil; import org.locationtech.jtstest.testbuilder.ui.style.BasicStyle; import org.locationtech.jtstest.testrunner.TestReader; @@ -165,16 +165,16 @@ private void initLayers() new ResultGeometryContainer(geomEditModel)); Layer lyrA = layerList.getLayer(LayerList.LYR_A); - lyrA.setGeometryStyle(new BasicStyle(GeometryDepiction.GEOM_A_LINE_CLR, - GeometryDepiction.GEOM_A_FILL_CLR)); + lyrA.setGeometryStyle(new BasicStyle(AppColors.GEOM_A_LINE_CLR, + AppColors.GEOM_A_FILL_CLR)); Layer lyrB = layerList.getLayer(LayerList.LYR_B); - lyrB.setGeometryStyle(new BasicStyle(GeometryDepiction.GEOM_B_LINE_CLR, - GeometryDepiction.GEOM_B_FILL_CLR)); + lyrB.setGeometryStyle(new BasicStyle(AppColors.GEOM_B_LINE_CLR, + AppColors.GEOM_B_FILL_CLR)); Layer lyrR = layerList.getLayer(LayerList.LYR_RESULT); - lyrR.setGeometryStyle(new BasicStyle(GeometryDepiction.GEOM_RESULT_LINE_CLR, - GeometryDepiction.GEOM_RESULT_FILL_CLR)); + lyrR.setGeometryStyle(new BasicStyle(AppColors.GEOM_RESULT_LINE_CLR, + AppColors.GEOM_RESULT_FILL_CLR)); } public void pasteGeometry(int geomIndex) throws Exception { diff --git a/modules/core/pom.xml b/modules/core/pom.xml index 76570703b5..b9737403be 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-core ${project.groupId}:${project.artifactId} diff --git a/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java b/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java index 170b15d03e..45da12823f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java +++ b/modules/core/src/main/java/org/locationtech/jts/JTSVersion.java @@ -46,7 +46,7 @@ public class JTSVersion { /** * An optional string providing further release info (such as "alpha 1"); */ - private static final String RELEASE_INFO = "SNAPSHOT"; + private static final String RELEASE_INFO = ""; /** * Prints the current JTS version to stdout. diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java index 1da839b395..58dd05b34e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Angle.java @@ -313,12 +313,36 @@ public static double diff(double ang1, double ang2) { } if (delAngle > Math.PI) { - delAngle = (2 * Math.PI) - delAngle; + delAngle = PI_TIMES_2 - delAngle; } return delAngle; } - + + /** + * Computes sin of an angle, snapping near-zero values to zero. + * + * @param ang the input angle (in radians) + * @return the result of the trigonometric function + */ + public static double sinSnap(double ang) { + double res = Math.sin(ang); + if (Math.abs(res) < 5e-16) return 0.0; + return res; + } + + /** + * Computes cos of an angle, snapping near-zero values to zero. + * + * @param ang the input angle (in radians) + * @return the result of the trigonometric function + */ + public static double cosSnap(double ang) { + double res = Math.cos(ang); + if (Math.abs(res) < 5e-16) return 0.0; + return res; + } + /** * Projects a point by a given angle and distance. * @@ -328,8 +352,8 @@ public static double diff(double ang1, double ang2) { * @return the projected point */ public static Coordinate project(Coordinate p, double angle, double dist) { - double x = p.getX() + dist * Math.cos(angle); - double y = p.getY() + dist * Math.sin(angle); + double x = p.getX() + dist * Angle.cosSnap(angle); + double y = p.getY() + dist * Angle.sinSnap(angle); return new Coordinate(x, y); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java index 7770f6273a..8b20bef1b3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/BoundaryNodeRule.java @@ -121,6 +121,10 @@ public boolean isInBoundary(int boundaryCount) // the "Mod-2 Rule" return boundaryCount % 2 == 1; } + + public String toString() { + return "Mod2 Boundary Node Rule"; + } } /** @@ -152,6 +156,10 @@ public boolean isInBoundary(int boundaryCount) { return boundaryCount > 0; } + + public String toString() { + return "EndPoint Boundary Node Rule"; + } } /** @@ -171,6 +179,10 @@ public boolean isInBoundary(int boundaryCount) { return boundaryCount > 1; } + + public String toString() { + return "MultiValent EndPoint Boundary Node Rule"; + } } /** @@ -189,6 +201,10 @@ public boolean isInBoundary(int boundaryCount) { return boundaryCount == 1; } + + public String toString() { + return "MonoValent EndPoint Boundary Node Rule"; + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java index 5697bd2c5a..dad9032d41 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/CGAlgorithms.java @@ -179,12 +179,10 @@ public static int locatePointInRing(Coordinate p, Coordinate[] ring) */ public static boolean isOnLine(Coordinate p, Coordinate[] pt) { - LineIntersector lineIntersector = new RobustLineIntersector(); for (int i = 1; i < pt.length; i++) { Coordinate p0 = pt[i - 1]; Coordinate p1 = pt[i]; - lineIntersector.computeIntersection(p, p0, p1); - if (lineIntersector.hasIntersection()) { + if (PointLocation.isOnSegment(p, p0, p1)) { return true; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java index 47b411c26b..75282a838b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPoint.java @@ -59,8 +59,7 @@ public static Coordinate getInteriorPoint(Geometry geom) { return null; Coordinate interiorPt = null; - //int dim = geom.getDimension(); - int dim = effectiveDimension(geom); + int dim = dimensionNonEmpty(geom); // this should not happen, but just in case... if (dim < 0) { return null; @@ -77,13 +76,13 @@ else if (dim == 1) { return interiorPt; } - private static int effectiveDimension(Geometry geom) { - EffectiveDimensionFilter dimFilter = new EffectiveDimensionFilter(); + private static int dimensionNonEmpty(Geometry geom) { + DimensionNonEmptyFilter dimFilter = new DimensionNonEmptyFilter(); geom.apply(dimFilter); return dimFilter.getDimension(); } - private static class EffectiveDimensionFilter implements GeometryFilter + private static class DimensionNonEmptyFilter implements GeometryFilter { private int dim = -1; diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java index ca5a9832d4..0594a4817f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointLine.java @@ -69,6 +69,9 @@ public Coordinate getInteriorPoint() */ private void addInterior(Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof LineString) { addInterior(geom.getCoordinates()); } @@ -93,6 +96,9 @@ private void addInterior(Coordinate[] pts) */ private void addEndpoints(Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof LineString) { addEndpoints(geom.getCoordinates()); } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java index 23a0243062..2c6169b3c8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/InteriorPointPoint.java @@ -56,6 +56,9 @@ public InteriorPointPoint(Geometry g) */ private void add(Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof Point) { add(geom.getCoordinate()); } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java index 54fa6c4351..2b694836b7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocation.java @@ -13,17 +13,37 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Location; /** * Functions for locating points within basic geometric - * structures such as lines and rings. + * structures such as line segments, lines and rings. * * @author Martin Davis * */ public class PointLocation { + /** + * Tests whether a point lies on a line segment. + * + * @param p the point to test + * @param p0 a point of the line segment + * @param p1 a point of the line segment + * @return true if the point lies on the line segment + */ + public static boolean isOnSegment(Coordinate p, Coordinate p0, Coordinate p1) { + //-- test envelope first since it's faster + if (! Envelope.intersects(p0, p1, p)) + return false; + //-- handle zero-length segments + if (p.equals2D(p0)) + return true; + boolean isOnLine = Orientation.COLLINEAR == Orientation.index(p0, p1, p); + return isOnLine; + } + /** * Tests whether a point lies on the line defined by a list of * coordinates. @@ -35,12 +55,10 @@ public class PointLocation { */ public static boolean isOnLine(Coordinate p, Coordinate[] line) { - LineIntersector lineIntersector = new RobustLineIntersector(); for (int i = 1; i < line.length; i++) { Coordinate p0 = line[i - 1]; Coordinate p1 = line[i]; - lineIntersector.computeIntersection(p, p0, p1); - if (lineIntersector.hasIntersection()) { + if (isOnSegment(p, p0, p1)) { return true; } } @@ -58,15 +76,13 @@ public static boolean isOnLine(Coordinate p, Coordinate[] line) */ public static boolean isOnLine(Coordinate p, CoordinateSequence line) { - LineIntersector lineIntersector = new RobustLineIntersector(); Coordinate p0 = new Coordinate(); Coordinate p1 = new Coordinate(); int n = line.size(); for (int i = 1; i < n; i++) { line.getCoordinate(i-1, p0); line.getCoordinate(i, p1); - lineIntersector.computeIntersection(p, p0, p1); - if (lineIntersector.hasIntersection()) { + if (isOnSegment(p, p0, p1)) { return true; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java index 3e2f469463..31da848f5e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PointLocator.java @@ -107,6 +107,9 @@ else if (geom instanceof Polygon) { private void computeLocation(Coordinate p, Geometry geom) { + if (geom.isEmpty()) + return; + if (geom instanceof Point) { updateLocationInfo(locateOnPoint(p, (Point) geom)); } @@ -162,11 +165,11 @@ private int locateOnLineString(Coordinate p, LineString l) if (! l.getEnvelopeInternal().intersects(p)) return Location.EXTERIOR; CoordinateSequence seq = l.getCoordinateSequence(); - if (! l.isClosed()) { - if (p.equals(seq.getCoordinate(0)) + if (p.equals(seq.getCoordinate(0)) || p.equals(seq.getCoordinate(seq.size() - 1)) ) { - return Location.BOUNDARY; - } + 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; diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java index a898d036cf..de47f251e5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/PolygonNodeTopology.java @@ -24,9 +24,10 @@ public class PolygonNodeTopology { /** - * Check if the segments at a node between two rings (or one ring) cross. + * Check if four segments at a node cross. + * Typically the segments lie in two different rings, or different sections of one ring. * The node is topologically valid if the rings do not cross. - * This function assumes that the segments are not collinear. + * If any segments are collinear, the test returns false. * * @param nodePt the node location * @param a0 the previous segment endpoint in a ring @@ -42,14 +43,24 @@ public static boolean isCrossing(Coordinate nodePt, Coordinate a0, Coordinate a1 aLo = a1; aHi = a0; } - /** - * Find positions of b0 and b1. - * If they are the same they do not cross the other edge - */ + /* boolean isBetween0 = isBetween(nodePt, b0, aLo, aHi); boolean isBetween1 = isBetween(nodePt, b1, aLo, aHi); return isBetween0 != isBetween1; + */ + + /** + * Find positions of b0 and b1. + * The edges cross if the positions are different. + * If any edge is collinear they are reported as not crossing + */ + int compBetween0 = compareBetween(nodePt, b0, aLo, aHi); + if (compBetween0 == 0) return false; + int compBetween1 = compareBetween(nodePt, b1, aLo, aHi); + if (compBetween1 == 0) return false; + + return compBetween0 != compBetween1; } /** @@ -99,6 +110,28 @@ private static boolean isBetween(Coordinate origin, Coordinate p, Coordinate e0, return ! isGreater1; } + /** + * Compares whether an edge p is between or outside the edges e0 and e1, + * where the edges all originate at a common origin. + * The "inside" of e0 and e1 is the arc which does not include + * the positive X-axis at the origin. + * If p is collinear with an edge 0 is returned. + * + * @param origin the origin + * @param p the destination point of edge p + * @param e0 the destination point of edge e0 + * @param e1 the destination point of edge e1 + * @return a negative integer, zero or positive integer as the vector P lies outside, collinear with, or inside the vectors E0 and E1 + */ + private static int compareBetween(Coordinate origin, Coordinate p, Coordinate e0, Coordinate e1) { + int comp0 = compareAngle(origin, p, e0); + if (comp0 == 0) return 0; + int comp1 = compareAngle(origin, p, e1); + if (comp1 == 0) return 0; + if (comp0 > 0 && comp1 < 0) return 1; + return -1; + } + /** * Tests if the angle with the origin of a vector P is greater than that of the * vector Q. @@ -126,6 +159,38 @@ private static boolean isAngleGreater(Coordinate origin, Coordinate p, Coordinat return orient == Orientation.COUNTERCLOCKWISE; } + /** + * Compares the angles of two vectors + * relative to the positive X-axis at their origin. + * Angles increase CCW from the X-axis. + * + * @param origin the origin of the vectors + * @param p the endpoint of the vector P + * @param q the endpoint of the vector Q + * @return a negative integer, zero, or a positive integer as this vector P has angle less than, equal to, or greater than vector Q + */ + public static int compareAngle(Coordinate origin, Coordinate p, Coordinate q) { + int quadrantP = quadrant(origin, p); + int quadrantQ = quadrant(origin, q); + + /** + * If the vectors are in different quadrants, + * that determines the ordering + */ + if (quadrantP > quadrantQ) return 1; + if (quadrantP < quadrantQ) return -1; + + //--- vectors are in the same quadrant + // Check relative orientation of vectors + // P > Q if it is CCW of Q + int orient = Orientation.index(origin, q, p); + switch (orient) { + case Orientation.COUNTERCLOCKWISE: return 1; + case Orientation.CLOCKWISE: return -1; + default: return 0; + } + } + private static int quadrant(Coordinate origin, Coordinate p) { double dx = p.getX() - origin.getX(); double dy = p.getY() - origin.getY(); diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java index 3e22a7331d..6f1de5689f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/RayCrossingCounter.java @@ -174,15 +174,24 @@ public void countSegment(Coordinate p1, Coordinate p2) { } } -/** - * Reports whether the point lies exactly on one of the supplied segments. - * This method may be called at any time as segments are processed. - * If the result of this method is true, - * no further segments need be supplied, since the result - * will never change again. - * - * @return true if the point lies exactly on a segment - */ + /** + * Gets the count of crossings. + * + * @return the crossing count + */ + public int getCount() { + return crossingCount; + } + + /** + * Reports whether the point lies exactly on one of the supplied segments. + * This method may be called at any time as segments are processed. + * If the result of this method is true, + * no further segments need be supplied, since the result + * will never change again. + * + * @return true if the point lies exactly on a segment + */ public boolean isOnSegment() { return isPointOnSegment; } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java index 21a5891773..92ed60715f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java @@ -15,6 +15,10 @@ *@version 1.7 */ import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateXY; +import org.locationtech.jts.geom.CoordinateXYM; +import org.locationtech.jts.geom.CoordinateXYZM; +import org.locationtech.jts.geom.Coordinates; import org.locationtech.jts.geom.Envelope; /** @@ -211,8 +215,8 @@ private static Coordinate copyWithZInterpolate(Coordinate p, Coordinate p1, Coor private static Coordinate copyWithZ(Coordinate p, double z) { Coordinate pCopy = copy(p); - if (! Double.isNaN(z)) { - pCopy.setZ( z ); + if (! Double.isNaN(z) && Coordinates.hasZ(pCopy)) { + pCopy.setZ(z); } return pCopy; } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java index b1512d4449..a45cbcbae9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/distance/DiscreteHausdorffDistance.java @@ -140,7 +140,6 @@ public static class MaxPointDistanceFilter { private PointPairDistance maxPtDist = new PointPairDistance(); private PointPairDistance minPtDist = new PointPairDistance(); - private DistanceToPoint euclideanDist = new DistanceToPoint(); private Geometry geom; public MaxPointDistanceFilter(Geometry geom) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java b/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java index 1f57fa10d5..e46889e862 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java @@ -25,12 +25,12 @@ class Corner implements Comparable { private int next; private double area; - public Corner(LinkedLine edge, int i) { + public Corner(LinkedLine edge, int i, double area) { this.edge = edge; this.index = i; this.prev = edge.prev(i); this.next = edge.next(i); - this.area = area(edge, i); + this.area = area; } public boolean isVertex(int index) { @@ -58,13 +58,6 @@ public Coordinate prev() { public Coordinate next() { return edge.getCoordinate(next); } - - private static double area(LinkedLine edge, int index) { - Coordinate pp = edge.prevCoordinate(index); - Coordinate p = edge.getCoordinate(index); - Coordinate pn = edge.nextCoordinate(index); - return Triangle.area(pp, p, pn); - } /** * Orders corners by increasing area. diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java new file mode 100644 index 0000000000..8338625727 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CornerArea.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 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.coverage; + +import org.locationtech.jts.algorithm.Angle; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Triangle; +import org.locationtech.jts.math.MathUtil; + +/** + * Computes the effective area of corners, + * taking into account the smoothing weight. + * + *

FUTURE WORK

+ * + * Support computing geodetic area + * + * @author Martin Davis + * + */ +class CornerArea { + public static final double DEFAULT_SMOOTH_WEIGHT = 0.0; + + private double smoothWeight = DEFAULT_SMOOTH_WEIGHT; + + public CornerArea() { + } + + /** + * Creates a new corner area computer. + * + * @param smoothWeight the weight for smoothing corners. In range [0..1]. + */ + public CornerArea(double smoothWeight) { + this.smoothWeight = smoothWeight; + } + + public double area(Coordinate pp, Coordinate p, Coordinate pn) { + + double area = Triangle.area(pp, p, pn); + double ang = angleNorm(pp, p, pn); + //-- rescale to [-1 .. 1], with 1 being narrow and -1 being flat + double angBias = 1.0 - 2.0 * ang; + //-- reduce area for narrower corners, to make them more likely to be removed + double areaWeighted = (1 - smoothWeight * angBias) * area; + return areaWeighted; + } + + private static double angleNorm(Coordinate pp, Coordinate p, Coordinate pn) { + double angNorm = Angle.angleBetween(pp, p, pn) / 2 / Math.PI; + return MathUtil.clamp(angNorm, 0, 1); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index 2208200b29..74bd2993b1 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -11,47 +11,38 @@ */ package org.locationtech.jts.coverage; -import java.util.List; - import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.io.WKTWriter; /** * An edge of a polygonal coverage formed from all or a section of a polygon ring. - * An edge may be a free ring, which is a ring which has not node points - * (i.e. does not touch any other rings in the parent coverage). + * An edge may be a free ring, which is a ring which has no node points + * (i.e. does not share a vertex with any other rings in the parent coverage). * * @author mdavis * */ class CoverageEdge { - public static CoverageEdge createEdge(Coordinate[] ring) { + public static final int RING_COUNT_INNER = 2; + public static final int RING_COUNT_OUTER = 1; + + public static CoverageEdge createEdge(Coordinate[] ring, boolean isPrimary) { Coordinate[] pts = extractEdgePoints(ring, 0, ring.length - 1); - CoverageEdge edge = new CoverageEdge(pts, true); + CoverageEdge edge = new CoverageEdge(pts, isPrimary, true); return edge; } - public static CoverageEdge createEdge(Coordinate[] ring, int start, int end) { + public static CoverageEdge createEdge(Coordinate[] ring, int start, int end, boolean isPrimary) { Coordinate[] pts = extractEdgePoints(ring, start, end); - CoverageEdge edge = new CoverageEdge(pts, false); + CoverageEdge edge = new CoverageEdge(pts, isPrimary, false); return edge; } - static MultiLineString createLines(List edges, GeometryFactory geomFactory) { - LineString lines[] = new LineString[edges.size()]; - for (int i = 0; i < edges.size(); i++) { - CoverageEdge edge = edges.get(i); - lines[i] = edge.toLineString(geomFactory); - } - MultiLineString mls = geomFactory.createMultiLineString(lines); - return mls; - } - private static Coordinate[] extractEdgePoints(Coordinate[] ring, int start, int end) { int size = start < end ? end - start + 1 @@ -136,12 +127,16 @@ else if (i > pts.length - 1) { private Coordinate[] pts; private int ringCount = 0; private boolean isFreeRing = true; + private boolean isPrimary = true; + private int adjacentIndex0 = -1; + private int adjacentIndex1 = -1; - public CoverageEdge(Coordinate[] pts, boolean isFreeRing) { + public CoverageEdge(Coordinate[] pts, boolean isPrimary, boolean isFreeRing) { this.pts = pts; + this.isPrimary = isPrimary; this.isFreeRing = isFreeRing; } - + public void incRingCount() { ringCount++; } @@ -150,9 +145,30 @@ public int getRingCount() { return ringCount; } + public boolean isInner() { + return ringCount == RING_COUNT_INNER; + } + + public boolean isOuter() { + return ringCount == RING_COUNT_OUTER; + } + + public void setPrimary(boolean isPrimary) { + //-- preserve primary status if set + if (this.isPrimary) + return; + this.isPrimary = isPrimary; + } + + public boolean isRemovableRing() { + boolean isRing = CoordinateArrays.isRing(pts); + return isRing && ! isPrimary; + } + /** * Returns whether this edge is a free ring; - * i.e. one with no constrained nodes. + * i.e. one that does not have nodes + * which are anchored because they occur in another ring. * * @return true if this is a free ring */ @@ -184,5 +200,28 @@ public String toString() { return WKTWriter.toLineString(pts); } + public void addIndex(int index) { + //TODO: keep information about which element is L and R? + + // assert: at least one elementIndex is unset (< 0) + if (adjacentIndex0 < 0) { + adjacentIndex0 = index; + } + else { + adjacentIndex1 = index; + } + } + + public int getAdjacentIndex(int index) { + if (index == 0) + return adjacentIndex0; + return adjacentIndex1; + } + + public boolean hasAdjacentIndex(int index) { + if (index == 0) + return adjacentIndex0 >= 0; + return adjacentIndex1 >= 0; + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java new file mode 100644 index 0000000000..9c3fe95440 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygon.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 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.coverage; + +import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; +import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Polygon; + +class CoveragePolygon { + + private Polygon polygon; + private Envelope polyEnv; + IndexedPointInAreaLocator locator; + + public CoveragePolygon(Polygon poly) { + this.polygon = poly; + polyEnv = polygon.getEnvelopeInternal(); + } + + public boolean intersectsEnv(Envelope env) { + //-- test intersection explicitly to avoid expensive null check + return ! (env.getMinX() > polyEnv.getMaxX() + || env.getMaxX() < polyEnv.getMinX() + || env.getMinY() > polyEnv.getMaxY() + || env.getMaxY() < polyEnv.getMinY()); + } + + private boolean intersectsEnv(Coordinate p) { + //-- test intersection explicitly to avoid expensive null check + return ! (p.x > polyEnv.getMaxX() || + p.x < polyEnv.getMinX() || + p.y > polyEnv.getMaxY() || + p.y < polyEnv.getMinY()); + } + + public boolean contains(Coordinate p) { + if (! intersectsEnv(p)) + return false; + PointOnGeometryLocator pia = getLocator(); + return Location.INTERIOR == pia.locate(p); + } + + private PointOnGeometryLocator getLocator() { + if (locator == null) { + locator = new IndexedPointInAreaLocator(polygon); + } + return locator; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java index a84c69e33a..6e00488116 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java @@ -16,15 +16,12 @@ import java.util.List; import java.util.Map; -import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; -import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.Location; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.util.PolygonExtracter; import org.locationtech.jts.noding.MCIndexSegmentSetMutualIntersector; @@ -85,7 +82,7 @@ * */ public class CoveragePolygonValidator { - + /** * Validates that a polygon is coverage-valid against the * surrounding polygons in a polygonal coverage. @@ -122,8 +119,7 @@ public static Geometry validate(Geometry targetPolygon, Geometry[] adjPolygons, private double gapWidth = 0.0; private GeometryFactory geomFactory; private Geometry[] adjGeoms; - private List adjPolygons; - private IndexedPointInAreaLocator[] adjPolygonLocators; + private List adjCovPolygons; /** * Create a new validator. @@ -158,8 +154,8 @@ public void setGapWidth(double gapWidth) { * @return a linear geometry containing the segments causing invalidity (if any) */ public Geometry validate() { - adjPolygons = extractPolygons(adjGeoms); - adjPolygonLocators = new IndexedPointInAreaLocator[adjPolygons.size()]; + List adjPolygons = extractPolygons(adjGeoms); + adjCovPolygons = toCoveragePolygons(adjPolygons); List targetRings = CoverageRing.createRings(targetGeom); List adjRings = CoverageRing.createRings(adjPolygons); @@ -177,6 +173,14 @@ public Geometry validate() { return createInvalidLines(targetRings); } + private static List toCoveragePolygons(List polygons) { + List covPolys = new ArrayList(); + for (Polygon poly : polygons) { + covPolys.add(new CoveragePolygon(poly)); + } + return covPolys; + } + private void checkTargetRings(List targetRings, List adjRings, Envelope targetEnv) { markMatchedSegments(targetRings, adjRings, targetEnv); @@ -197,7 +201,8 @@ private void checkTargetRings(List targetRings, List * Do further checks to see if any of them are are invalid. */ markInvalidInteractingSegments(targetRings, adjRings, gapWidth); - markInvalidInteriorSegments(targetRings, adjPolygons); + markInvalidInteriorSegments(targetRings, adjCovPolygons); + //OLDmarkInvalidInteriorSegments(targetRings, adjPolygons); } private static List extractPolygons(Geometry[] geoms) { @@ -368,77 +373,79 @@ private void markInvalidInteractingSegments(List targetRings, List segSetMutInt.process(adjRings, detector); } + /** + * Stride is chosen experimentally to provide good performance + */ + private static final int RING_SECTION_STRIDE = 1000; + /** * Marks invalid target segments which are fully interior * to an adjacent polygon. * * @param targetRings the rings with segments to test - * @param adjPolygons the adjacent polygons + * @param adjCovPolygons the adjacent polygons */ - private void markInvalidInteriorSegments(List targetRings, List adjPolygons) { + private void markInvalidInteriorSegments(List targetRings, List adjCovPolygons) { for (CoverageRing ring : targetRings) { - for (int i = 0; i < ring.size() - 1; i++) { - //-- skip check for segments with known state. - if (ring.isKnown(i)) - continue; + int stride = RING_SECTION_STRIDE; + for (int i = 0; i < ring.size() - 1; i += stride) { + int iEnd = i + stride; + if (iEnd >= ring.size()) + iEnd = ring.size() - 1; - /** - * Check if vertex is in interior of an adjacent polygon. - * If so, the segments on either side are in the interior. - * Mark them invalid, unless they are already matched. - */ - Coordinate p = ring.getCoordinate(i); - if (isInteriorVertex(p, adjPolygons)) { - ring.markInvalid(i); - //-- previous segment may be interior (but may also be matched) - int iPrev = i == 0 ? ring.size() - 2 : i-1; - if (! ring.isKnown(iPrev)) - ring.markInvalid(iPrev); - } + markInvalidInteriorSection(ring, i, iEnd, adjCovPolygons); } } } - + /** - * Tests if a coordinate is in the interior of some adjacent polygon. - * Uses the cached Point-In-Polygon indexed locators, for performance. + * Marks invalid target segments in a section which are interior + * to an adjacent polygon. + * Processing a section at a time dramatically improves efficiency. + * Due to the coherent organization of polygon rings, + * sections usually have a high spatial locality. + * This means that sections typically intersect only a few or often no adjacent polygons. + * The section envelope can be computed and tested against adjacent polygon envelopes quickly. + * The section can be skipped entirely if it does not interact with any polygons. * - * @param p the coordinate to test - * @param adjPolygons the list of polygons - * @return true if the point is in the interior + * @param ring + * @param iStart + * @param iEnd + * @param adjPolygons */ - private boolean isInteriorVertex(Coordinate p, List adjPolygons) { - /** - * There should not be too many adjacent polygons, - * and hopefully not too many segments with unknown status - * so a linear scan should not be too inefficient - */ - //TODO: try a spatial index? - for (int i = 0; i < adjPolygons.size(); i++) { - Polygon adjPoly = adjPolygons.get(i); - - if (polygonContainsPoint(i, adjPoly, p)) - return true; + private void markInvalidInteriorSection(CoverageRing ring, int iStart, int iEnd, List adjPolygons) { + Envelope sectionEnv = ring.getEnvelope(iStart, iEnd); + //TODO: is it worth indexing polygons? + for (CoveragePolygon adjPoly : adjPolygons) { + if (adjPoly.intersectsEnv(sectionEnv)) { + //-- test vertices in section + for (int i = iStart; i < iEnd; i++) { + markInvalidInteriorSegment(ring, i, adjPoly); + } + } } - return false; - } - - private boolean polygonContainsPoint(int index, Polygon poly, Coordinate pt) { - if (! poly.getEnvelopeInternal().intersects(pt)) - return false; - PointOnGeometryLocator pia = getLocator(index, poly); - return Location.INTERIOR == pia.locate(pt); } - private PointOnGeometryLocator getLocator(int index, Polygon poly) { - IndexedPointInAreaLocator loc = adjPolygonLocators[index]; - if (loc == null) { - loc = new IndexedPointInAreaLocator(poly); - adjPolygonLocators[index] = loc; + private void markInvalidInteriorSegment(CoverageRing ring, int i, CoveragePolygon adjPoly) { + //-- skip check for segments with known state. + if (ring.isKnown(i)) + return; + + /** + * Check if vertex is in interior of an adjacent polygon. + * If so, the segments on either side are in the interior. + * Mark them invalid, unless they are already matched. + */ + Coordinate p = ring.getCoordinate(i); + if (adjPoly.contains(p)) { + ring.markInvalid(i); + //-- previous segment may be interior (but may also be matched) + int iPrev = i == 0 ? ring.size() - 2 : i-1; + if (! ring.isKnown(iPrev)) + ring.markInvalid(iPrev); } - return loc; } - + private Geometry createInvalidLines(List rings) { List lines = new ArrayList(); for (CoverageRing ring : rings) { diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java index 82ce9dfc3a..479d9d3df0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java @@ -17,6 +17,7 @@ import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; @@ -92,6 +93,14 @@ private CoverageRing(Coordinate[] pts, boolean isInteriorOnRight) { isMatched = new boolean[size() - 1]; } + public Envelope getEnvelope(int start, int end) { + Envelope env = new Envelope(); + for (int i = start; i < end; i++) { + env.expandToInclude(getCoordinate(i)); + } + return env; + } + /** * Reports if the ring has canonical orientation, * with the polygon interior on the right (shell is CW). diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java index ba5f852219..a2b83ddf55 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiPolygon; @@ -73,28 +74,15 @@ public List getEdges() { return edges; } - /** - * Selects the edges with a given ring count (which can be 1 or 2). - * - * @param ringCount the edge ring count to select (1 or 2) - * @return the selected edges - */ - public List selectEdges(int ringCount) { - List result = new ArrayList(); - for (CoverageEdge edge : edges) { - if (edge.getRingCount() == ringCount) { - result.add(edge); - } - } - return result; - } - private void build() { Set nodes = findMultiRingNodes(coverage); Set boundarySegs = CoverageBoundarySegmentFinder.findBoundarySegments(coverage); nodes.addAll(findBoundaryNodes(boundarySegs)); HashMap uniqueEdgeMap = new HashMap(); - for (Geometry geom : coverage) { + for (int i = 0; i < coverage.length; i++) { + //-- geom is a Polygon or MultiPolygon + Geometry geom = coverage[i]; + int indexLargest = findLargestPolygonIndex(geom); for (int ipoly = 0; ipoly < geom.getNumGeometries(); ipoly++) { Polygon poly = (Polygon) geom.getGeometryN(ipoly); @@ -102,25 +90,45 @@ private void build() { if (poly.isEmpty()) continue; + //-- largest polygon is the primary one, which is never removed + boolean isPrimary = ipoly == indexLargest; + //-- extract shell LinearRing shell = poly.getExteriorRing(); - addRingEdges(shell, nodes, boundarySegs, uniqueEdgeMap); + addRingEdges(i, shell, isPrimary, nodes, boundarySegs, uniqueEdgeMap); //-- extract holes for (int ihole = 0; ihole < poly.getNumInteriorRing(); ihole++) { LinearRing hole = poly.getInteriorRingN(ihole); - //-- skip empty rings. Missing rings are copied in result + //-- skip empty holes. Missing rings are copied in result if (hole.isEmpty()) continue; - addRingEdges(hole, nodes, boundarySegs, uniqueEdgeMap); + //-- holes are never primary + addRingEdges(i, hole, false, nodes, boundarySegs, uniqueEdgeMap); } } } } - private void addRingEdges(LinearRing ring, Set nodes, Set boundarySegs, + private int findLargestPolygonIndex(Geometry geom) { + if (geom instanceof Polygon) + return 0; + int indexLargest = -1; + double areaLargest = -1; + for (int ipoly = 0; ipoly < geom.getNumGeometries(); ipoly++) { + Polygon poly = (Polygon) geom.getGeometryN(ipoly); + double area = poly.getArea(); + if (area > areaLargest) { + areaLargest = area; + indexLargest = ipoly; + } + } + return indexLargest; + } + + private void addRingEdges(int index, LinearRing ring, boolean isPrimary, Set nodes, Set boundarySegs, HashMap uniqueEdgeMap) { addBoundaryInnerNodes(ring, boundarySegs, nodes); - List ringEdges = extractRingEdges(ring, uniqueEdgeMap, nodes); + List ringEdges = extractRingEdges(index, ring, isPrimary, uniqueEdgeMap, nodes); if (ringEdges != null) ringEdgesMap.put(ring, ringEdges); } @@ -149,8 +157,18 @@ private void addBoundaryInnerNodes(LinearRing ring, Set boundarySeg } } - private List extractRingEdges(LinearRing ring, - HashMap uniqueEdgeMap, + /** + * Extracts the {@link CoverageEdge}s for a ring. + * @param index + * + * @param ring + * @param isRetained true if the ring is retained (must not be removed) + * @param uniqueEdgeMap + * @param nodes + * @return null if the ring has too few distinct vertices + */ + private List extractRingEdges(int index, LinearRing ring, + boolean isPrimary, HashMap uniqueEdgeMap, Set nodes) { // System.out.println(ring); List ringEdges = new ArrayList(); @@ -164,15 +182,21 @@ private List extractRingEdges(LinearRing ring, int first = findNextNodeIndex(pts, -1, nodes); if (first < 0) { //-- ring does not contain a node, so edge is entire ring - CoverageEdge edge = createEdge(pts, uniqueEdgeMap); + CoverageEdge edge = createEdge(pts, -1, -1, index, isPrimary, uniqueEdgeMap); ringEdges.add(edge); } else { int start = first; int end = start; + //-- two-node edges are always primary + boolean isEdgePrimary = true; do { end = findNextNodeIndex(pts, start, nodes); - CoverageEdge edge = createEdge(pts, start, end, uniqueEdgeMap); + //-- a single-node ring is only retained if specified + if (end == start) { + isEdgePrimary = isPrimary; + } + CoverageEdge edge = createEdge(pts, start, end, index, isEdgePrimary, uniqueEdgeMap); // System.out.println(ringEdges.size() + " : " + edge); ringEdges.add(edge); start = end; @@ -180,33 +204,37 @@ private List extractRingEdges(LinearRing ring, } return ringEdges; } - - private CoverageEdge createEdge(Coordinate[] ring, HashMap uniqueEdgeMap) { - CoverageEdge edge; - LineSegment edgeKey = CoverageEdge.key(ring); - if (uniqueEdgeMap.containsKey(edgeKey)) { - edge = uniqueEdgeMap.get(edgeKey); - } - else { - edge = CoverageEdge.createEdge(ring); - uniqueEdgeMap.put(edgeKey, edge); - edges.add(edge); - } - edge.incRingCount(); - return edge; - } - private CoverageEdge createEdge(Coordinate[] ring, int start, int end, HashMap uniqueEdgeMap) { + /** + * Creates or updates an edge for the given ring or ring section. + * + * @param ring ring to create edge for + * @param start start index of ring section; -1 indicates edge is entire ring + * @param end end index of ring section + * @param index + * @param isPrimary whether this ring is a primary ring + * @param uniqueEdgeMap map of edges + * @return the CoverageEdge for the ring or portion of ring + */ + private CoverageEdge createEdge(Coordinate[] ring, int start, int end, int index, boolean isPrimary, HashMap uniqueEdgeMap) { CoverageEdge edge; LineSegment edgeKey = (end == start) ? CoverageEdge.key(ring) : CoverageEdge.key(ring, start, end); if (uniqueEdgeMap.containsKey(edgeKey)) { edge = uniqueEdgeMap.get(edgeKey); + //-- update shared attributes + edge.setPrimary(isPrimary); } else { - edge = CoverageEdge.createEdge(ring, start, end); + if (start < 0) { + edge = CoverageEdge.createEdge(ring, isPrimary); + } + else { + edge = CoverageEdge.createEdge(ring, start, end, isPrimary); + } uniqueEdgeMap.put(edgeKey, edge); edges.add(edge); } + edge.addIndex(index); edge.incRingCount(); return edge; } @@ -299,24 +327,43 @@ private Geometry buildPolygonal(Geometry geom) { } private Geometry buildMultiPolygon(MultiPolygon geom) { - Polygon[] polys = new Polygon[geom.getNumGeometries()]; - for (int i = 0; i < polys.length; i++) { - polys[i] = buildPolygon((Polygon) geom.getGeometryN(i)); + List polyList = new ArrayList(); + for (int i = 0; i < geom.getNumGeometries(); i++) { + Polygon poly = buildPolygon((Polygon) geom.getGeometryN(i)); + if (poly != null) { + polyList.add(poly); + } + } + if (polyList.size() == 1) { + return polyList.get(0); } + Polygon[] polys = GeometryFactory.toPolygonArray(polyList); return geom.getFactory().createMultiPolygon(polys); } + /** + * + * @param polygon + * @return null if the polygon has been removed + */ private Polygon buildPolygon(Polygon polygon) { LinearRing shell = buildRing(polygon.getExteriorRing()); - + if (shell == null) { + return null; + } if (polygon.getNumInteriorRing() == 0) { return polygon.getFactory().createPolygon(shell); } - LinearRing holes[] = new LinearRing[polygon.getNumInteriorRing()]; - for (int i = 0; i < holes.length; i++) { + List holeList = new ArrayList(); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { LinearRing hole = polygon.getInteriorRingN(i); - holes[i] = buildRing(hole); + LinearRing newHole = buildRing(hole); + if (newHole != null) { + holeList.add(newHole); + } } + //LinearRing holes[] = new LinearRing[polygon.getNumInteriorRing()]; + LinearRing holes[] = GeometryFactory.toLinearRingArray(holeList); return polygon.getFactory().createPolygon(shell, holes); } @@ -326,6 +373,11 @@ private LinearRing buildRing(LinearRing ring) { if (ringEdges == null) return (LinearRing) ring.copy(); + boolean isRemoved = ringEdges.size() == 1 + && ringEdges.get(0).getCoordinates().length == 0; + if (isRemoved) + return null; + CoordinateList ptsList = new CoordinateList(); for (int i = 0; i < ringEdges.size(); i++) { Coordinate lastPt = ptsList.size() > 0 diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java index 6a0134e814..3ad9b16381 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java @@ -11,12 +11,10 @@ */ package org.locationtech.jts.coverage; -import java.util.BitSet; import java.util.List; +import org.locationtech.jts.coverage.TPVWSimplifier.Edge; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.MultiLineString; /** * Simplifies the boundaries of the polygons in a polygonal coverage @@ -35,18 +33,34 @@ *

* The simplified result coverage has the following characteristics: *

    - *
  • It has the same number and types of polygonal geometries as the input + *
  • It has the same number of polygonal geometries as the input + *
  • If the input is a valid coverage, then so is the result *
  • Node points (inner vertices shared by three or more polygons, * or boundary vertices shared by two or more) are not changed - *
  • If the input is a valid coverage, then so is the result + *
  • Polygons maintain their line-adjacency (edges are never removed) + *
  • Rings are simplified to a minimum of 4 vertices, to better preserve their shape + *
  • Rings smaller than the area tolerance are removed where possible. + * This applies to both holes and "islands" (multipolygon elements + * which are disjoint or touch another polygon at a single vertex). + * At least one polygon is retained for each input geometry + * (the one with largest area). *
- * This class also supports inner simplification, which simplifies + * This class supports simplification using different distance tolerances + * for inner and outer edges of the coverage (including no simplfication + * using a tolerance of 0.0). + * This allows, for example, inner simplification, which simplifies * only edges of the coverage which are adjacent to two polygons. * This allows partial simplification of a coverage, since a simplified * subset of a coverage still matches the remainder of the coverage. *

+ * The class allows specifying a separate tolerance for each element of the input coverage. + *

* The input coverage should be valid according to {@link CoverageValidator}. - * Invalid coverages may still be simplified, but the result will still be invalid. + * Invalid coverages may be simplified, but the result will likely still be invalid. + * + *

FUTURE WORK

+ * + * Support geodetic data by computing true geodetic area, and accepting tolerances in metres. * * @author Martin Davis */ @@ -58,13 +72,29 @@ public class CoverageSimplifier { * * @param coverage a set of polygonal geometries forming a coverage * @param tolerance the simplification tolerance - * @return the simplified polygons + * @return the simplified coverage polygons */ public static Geometry[] simplify(Geometry[] coverage, double tolerance) { CoverageSimplifier simplifier = new CoverageSimplifier(coverage); return simplifier.simplify(tolerance); } + /** + * Simplifies the boundaries of a set of polygonal geometries forming a coverage, + * preserving the coverage topology, using a separate tolerance + * for each element of the coverage. + * Coverage edges are simplified using the lowest tolerance of each adjacent + * element. + * + * @param coverage a set of polygonal geometries forming a coverage + * @param tolerance the simplification tolerances (one per input element) + * @return the simplified coverage polygons + */ + public static Geometry[] simplify(Geometry[] coverage, double[] tolerances) { + CoverageSimplifier simplifier = new CoverageSimplifier(coverage); + return simplifier.simplify(tolerances); + } + /** * Simplifies the inner boundaries of a set of polygonal geometries forming a coverage, * preserving the coverage topology. @@ -72,15 +102,30 @@ public static Geometry[] simplify(Geometry[] coverage, double tolerance) { * * @param coverage a set of polygonal geometries forming a coverage * @param tolerance the simplification tolerance - * @return the simplified polygons + * @return the simplified coverage polygons */ public static Geometry[] simplifyInner(Geometry[] coverage, double tolerance) { CoverageSimplifier simplifier = new CoverageSimplifier(coverage); - return simplifier.simplifyInner(tolerance); + return simplifier.simplify(tolerance, 0); } - private Geometry[] input; - private GeometryFactory geomFactory; + /** + * Simplifies the outer boundaries of a set of polygonal geometries forming a coverage, + * preserving the coverage topology. + * Edges in the interior of the coverage are left unchanged. + * + * @param coverage a set of polygonal geometries forming a coverage + * @param tolerance the simplification tolerance + * @return the simplified polygons + */ + public static Geometry[] simplifyOuter(Geometry[] coverage, double tolerance) { + CoverageSimplifier simplifier = new CoverageSimplifier(coverage); + return simplifier.simplify(0, tolerance); + } + + private Geometry[] coverage; + private double smoothWeight = CornerArea.DEFAULT_SMOOTH_WEIGHT; + private double removableSizeFactor = 1.0; /** * Create a new coverage simplifier instance. @@ -88,63 +133,148 @@ public static Geometry[] simplifyInner(Geometry[] coverage, double tolerance) { * @param coverage a set of polygonal geometries forming a coverage */ public CoverageSimplifier(Geometry[] coverage) { - input = coverage; - geomFactory = coverage[0].getFactory(); + this.coverage = coverage; } /** - * Computes the simplified coverage, preserving the coverage topology. + * Sets the factor applied to the area tolerance to determine + * if small rings should be removed. + * Larger values cause more rings to be removed. + * A value of 0 prevents rings from being removed. * - * @param tolerance the simplification tolerance - * @return the simplified polygons + * @param removableSizeFactor the factor to determine ring size to remove */ - public Geometry[] simplify(double tolerance) { - CoverageRingEdges cov = CoverageRingEdges.create(input); - simplifyEdges(cov.getEdges(), null, tolerance); - Geometry[] result = cov.buildCoverage(); - return result; + public void setRemovableRingSizeFactor(double removableSizeFactor) { + double factor = removableSizeFactor; + if (factor < 0.0) + factor = 0.0; + this.removableSizeFactor = factor; } /** - * Computes the inner-boundary simplified coverage, - * preserving the coverage topology, - * and leaving outer boundary edges unchanged. + * Sets the weight influencing how smooth the simplification should be. + * The weight must be between 0 and 1. + * Larger values increase the smoothness of the simplified edges. * - * @param tolerance the simplification tolerance - * @return the simplified polygons + * @param smoothWeight a value between 0 and 1 */ - public Geometry[] simplifyInner(double tolerance) { - CoverageRingEdges cov = CoverageRingEdges.create(input); - List innerEdges = cov.selectEdges(2); - List outerEdges = cov.selectEdges(1); - MultiLineString constraintEdges = CoverageEdge.createLines(outerEdges, geomFactory); + public void setSmoothWeight(double smoothWeight) { + if (smoothWeight < 0.0 || smoothWeight > 1.0) + throw new IllegalArgumentException("smoothWeight must be in range [0 - 1]"); + this.smoothWeight = smoothWeight; + } + + /** + * Computes the simplified coverage using a single distance tolerance, + * preserving the coverage topology. + * + * @param tolerance the simplification distance tolerance + * @return the simplified coverage polygons + */ + public Geometry[] simplify(double tolerance) { + return simplifyEdges(tolerance, tolerance); + } - simplifyEdges(innerEdges, constraintEdges, tolerance); - Geometry[] result = cov.buildCoverage(); - return result; + /** + * Computes the simplified coverage using separate distance tolerances + * for inner and outer edges, + * preserving the coverage topology. + * + * @param toleranceInner the distance tolerance for inner edges + * @param toleranceOuter the distance tolerance for outer edges + * @return the simplified coverage polygons + */ + public Geometry[] simplify(double toleranceInner, double toleranceOuter) { + return simplifyEdges(toleranceInner, toleranceOuter); + } + + /** + * Computes the simplified coverage using separate distance tolerances + * for each coverage element, + * preserving the coverage topology. + * + * @param tolerances the distance tolerances for the coverage elements + * @return the simplified coverage polygons + */ + public Geometry[] simplify(double[] tolerances) { + if (tolerances.length != coverage.length) + throw new IllegalArgumentException("number of tolerances does not match number of coverage elements"); + return simplifyEdges(tolerances); + } + + private Geometry[] simplifyEdges(double[] tolerances) { + CoverageRingEdges covRings = CoverageRingEdges.create(coverage); + List covEdges = covRings.getEdges(); + TPVWSimplifier.Edge[] edges = createEdges(covEdges, tolerances); + return simplify(covRings, covEdges, edges); + } + + private Edge[] createEdges(List covEdges, double[] tolerances) { + TPVWSimplifier.Edge[] edges = new TPVWSimplifier.Edge[covEdges.size()]; + for (int i = 0; i < covEdges.size(); i++) { + CoverageEdge covEdge = covEdges.get(i); + double tol = computeTolerance(covEdge, tolerances); + edges[i] = createEdge(covEdge, tol); + } + return edges; } - private void simplifyEdges(List edges, MultiLineString constraints, double tolerance) { - MultiLineString lines = CoverageEdge.createLines(edges, geomFactory); - BitSet freeRings = getFreeRings(edges); - MultiLineString linesSimp = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); - //Assert: mlsSimp.getNumGeometries = edges.length + private double computeTolerance(CoverageEdge covEdge, double[] tolerances) { + int index0 = covEdge.getAdjacentIndex(0); + // assert: index0 >= 0 + double tolerance = tolerances[index0]; - setCoordinates(edges, linesSimp); + if (covEdge.hasAdjacentIndex(1)) { + int index1 = covEdge.getAdjacentIndex(1); + double tol1 = tolerances[index1]; + //-- use lowest tolerance for edge + if (tol1 < tolerance) + tolerance = tol1; + } + return tolerance; + } + + private Geometry[] simplifyEdges(double toleranceInner, double toleranceOuter) { + CoverageRingEdges covRings = CoverageRingEdges.create(coverage); + List covEdges = covRings.getEdges(); + TPVWSimplifier.Edge[] edges = createEdges(covEdges, toleranceInner, toleranceOuter); + return simplify(covRings, covEdges, edges); + } + + private Geometry[] simplify(CoverageRingEdges covRings, List covEdges, TPVWSimplifier.Edge[] edges) { + CornerArea cornerArea = new CornerArea(smoothWeight); + TPVWSimplifier.simplify(edges, cornerArea, removableSizeFactor); + setCoordinates(covEdges, edges); + Geometry[] result = covRings.buildCoverage(); + return result; } - private void setCoordinates(List edges, MultiLineString lines) { - for (int i = 0; i < edges.size(); i++) { - edges.get(i).setCoordinates(lines.getGeometryN(i).getCoordinates()); + private static TPVWSimplifier.Edge[] createEdges(List covEdges, double toleranceInner, double toleranceOuter) { + TPVWSimplifier.Edge[] edges = new TPVWSimplifier.Edge[covEdges.size()]; + for (int i = 0; i < covEdges.size(); i++) { + CoverageEdge covEdge = covEdges.get(i); + double tol = computeTolerance(covEdge, toleranceInner, toleranceOuter); + edges[i] = createEdge(covEdge, tol); } + return edges; } - private BitSet getFreeRings(List edges) { - BitSet freeRings = new BitSet(edges.size()); - for (int i = 0 ; i < edges.size() ; i++) { - freeRings.set(i, edges.get(i).isFreeRing()); + private static Edge createEdge(CoverageEdge covEdge, double tol) { + return new TPVWSimplifier.Edge(covEdge.getCoordinates(), tol, + covEdge.isFreeRing(), covEdge.isRemovableRing()); + } + + private static double computeTolerance(CoverageEdge covEdge, double toleranceInner, double toleranceOuter) { + return covEdge.isInner() ? toleranceInner : toleranceOuter; + } + + private void setCoordinates(List covEdges, Edge[] edges) { + for (int i = 0; i < covEdges.size(); i++) { + Edge edge = edges[i]; + if (edge.getTolerance() > 0) { + covEdges.get(i).setCoordinates(edges[i].getCoordinates()); + } } - return freeRings; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index 64f37913d6..dc73247f28 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -11,17 +11,13 @@ */ package org.locationtech.jts.coverage; -import java.util.ArrayList; -import java.util.BitSet; import java.util.List; import java.util.PriorityQueue; +import org.locationtech.jts.algorithm.Area; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.index.VertexSequencePackedRtree; import org.locationtech.jts.index.strtree.STRtree; import org.locationtech.jts.simplify.LinkedLine; @@ -33,6 +29,8 @@ * in the original input. * Line and ring endpoints are preserved, except for rings * which are flagged as "free". + * Rings which are smaller than the tolerance area + * may be removed entirely, as long as they are flagged as removable. *

* The amount of simplification is determined by a tolerance value, * which is a non-zero quantity. @@ -45,19 +43,6 @@ * */ class TPVWSimplifier { - - /** - * Simplifies a set of lines, preserving the topology of the lines. - * - * @param lines the lines to simplify - * @param distanceTolerance the simplification tolerance - * @return the simplified lines - */ - public static MultiLineString simplify(MultiLineString lines, double distanceTolerance) { - TPVWSimplifier simp = new TPVWSimplifier(lines, distanceTolerance); - MultiLineString result = (MultiLineString) simp.simplify(); - return result; - } /** * Simplifies a set of lines, preserving the topology of the lines between @@ -72,104 +57,118 @@ public static MultiLineString simplify(MultiLineString lines, double distanceTol * @param distanceTolerance the simplification tolerance * @return the simplified lines */ - public static MultiLineString simplify(MultiLineString lines, BitSet freeRings, - MultiLineString constraintLines, double distanceTolerance) { - TPVWSimplifier simp = new TPVWSimplifier(lines, distanceTolerance); - simp.setFreeRingIndices(freeRings); - simp.setConstraints(constraintLines); - MultiLineString result = (MultiLineString) simp.simplify(); - return result; + public static void simplify(Edge[] edges, + CornerArea cornerArea, + double removableSizeFactor) { + TPVWSimplifier simp = new TPVWSimplifier(edges); + simp.setCornerArea(cornerArea); + simp.setRemovableRingSizeFactor(removableSizeFactor); + simp.simplify(); } - private MultiLineString inputLines; - private BitSet isFreeRing; - private double areaTolerance; - private GeometryFactory geomFactory; - private MultiLineString constraintLines = null; - - private TPVWSimplifier(MultiLineString lines, double distanceTolerance) { - this.inputLines = lines; - this.areaTolerance = distanceTolerance * distanceTolerance; - geomFactory = inputLines.getFactory(); + private CornerArea cornerArea; + private double removableSizeFactor = 1.0; + private Edge[] edges; + + public TPVWSimplifier(Edge[] edges) { + this.edges = edges; } - private void setConstraints(MultiLineString constraints) { - this.constraintLines = constraints; + public void setRemovableRingSizeFactor(double removableSizeFactor) { + this.removableSizeFactor = removableSizeFactor; } - - public void setFreeRingIndices(BitSet isFreeRing) { - //Assert: bit set has same size as number of lines. - this.isFreeRing = isFreeRing; + + public void setCornerArea(CornerArea cornerArea) { + this.cornerArea = cornerArea; } - - private Geometry simplify() { - List edges = createEdges(inputLines, this.isFreeRing); - List constraintEdges = createEdges(constraintLines, null); - + + private void simplify() { EdgeIndex edgeIndex = new EdgeIndex(); - edgeIndex.add(edges); - edgeIndex.add(constraintEdges); + add(edges, edgeIndex); - LineString[] result = new LineString[edges.size()]; - for (int i = 0 ; i < edges.size(); i++) { - Edge edge = edges.get(i); - Coordinate[] ptsSimp = edge.simplify(edgeIndex); - result[i] = geomFactory.createLineString(ptsSimp); + for (int i = 0 ; i < edges.length; i++) { + Edge edge = edges[i]; + edge.simplify(cornerArea, edgeIndex); } - return geomFactory.createMultiLineString(result); } - private List createEdges(MultiLineString lines, BitSet isFreeRing) { - List edges = new ArrayList(); - if (lines == null) - return edges; - for (int i = 0 ; i < lines.getNumGeometries(); i++) { - LineString line = (LineString) lines.getGeometryN(i); - boolean isFree = isFreeRing == null ? false : isFreeRing.get(i); - edges.add(new Edge(line, isFree, areaTolerance)); + private void add(Edge[] edges, EdgeIndex edgeIndex) { + for (Edge edge : edges) { + //-- don't include removed edges in index + edge.updateRemoved(removableSizeFactor); + if (! edge.isRemoved()) { + //-- avoid fluffing up removed edges + edge.init(); + edgeIndex.add(edge); + } } - return edges; } - private static class Edge { - private double areaTolerance; + public static class Edge { + private static final int MIN_EDGE_SIZE = 2; + private static final int MIN_RING_SIZE = 4; + private LinkedLine linkedLine; - private int minEdgeSize; private boolean isFreeRing; - private int nbPts; - + private int nPts; + private Coordinate[] pts; private VertexSequencePackedRtree vertexIndex; private Envelope envelope; + private boolean isRemoved = false; + private boolean isRemovable; + private double distanceTolerance = 0.0; /** * Creates a new edge. * The endpoints of the edge are preserved during simplification, * unless it is a ring and the {@Link #isFreeRing} flag is set. * - * @param inputLine the line or ring + * @param pts the line or ring + * @param distanceTolerance * @param isFreeRing whether a ring endpoint can be removed - * @param areaTolerance the simplification tolerance + * @param isFreeRing + * @param isRemovable */ - Edge(LineString inputLine, boolean isFreeRing, double areaTolerance) { - this.areaTolerance = areaTolerance; + Edge(Coordinate[] pts, double distanceTolerance, boolean isFreeRing, boolean isRemovable) { + this.envelope = CoordinateArrays.envelope(pts); + this.pts = pts; + this.nPts = pts.length; this.isFreeRing = isFreeRing; - this.envelope = inputLine.getEnvelopeInternal(); - Coordinate[] pts = inputLine.getCoordinates(); - this.nbPts = pts.length; - linkedLine = new LinkedLine(pts); - minEdgeSize = linkedLine.isRing() ? 3 : 2; - - vertexIndex = new VertexSequencePackedRtree(pts); - //-- remove ring duplicate final vertex - if (linkedLine.isRing()) { - vertexIndex.remove(pts.length-1); - } + this.isRemovable = isRemovable; + this.distanceTolerance = distanceTolerance; } + public void updateRemoved(double removableSizeFactor) { + if (! isRemovable) + return; + double areaTolerance = distanceTolerance * distanceTolerance; + isRemoved = CoordinateArrays.isRing(pts) + && Area.ofRing(pts) < removableSizeFactor * areaTolerance; + } + + public void init() { + linkedLine = new LinkedLine(pts); + } + + public double getTolerance() { + return distanceTolerance; + } + + public boolean isRemoved() { + return isRemoved; + } + private Coordinate getCoordinate(int index) { - return linkedLine.getCoordinate(index); + return pts[index]; } + public Coordinate[] getCoordinates() { + if (isRemoved) { + return new Coordinate[0]; + } + return linkedLine.getCoordinates(); + } + public Envelope getEnvelope() { return envelope; } @@ -178,8 +177,18 @@ public int size() { return linkedLine.size(); } - private Coordinate[] simplify(EdgeIndex edgeIndex) { - PriorityQueue cornerQueue = createQueue(); + public void simplify(CornerArea cornerArea, EdgeIndex edgeIndex) { + if (isRemoved) { + return; + } + //-- don't simplify + if (distanceTolerance <= 0.0) + return; + + double areaTolerance = distanceTolerance * distanceTolerance; + int minEdgeSize = linkedLine.isRing() ? MIN_RING_SIZE : MIN_EDGE_SIZE; + + PriorityQueue cornerQueue = createQueue(areaTolerance, cornerArea); while (! cornerQueue.isEmpty() && size() > minEdgeSize) { Corner corner = cornerQueue.poll(); @@ -191,31 +200,39 @@ && size() > minEdgeSize) { if (corner.getArea() > areaTolerance) break; if (isRemovable(corner, edgeIndex) ) { - removeCorner(corner, cornerQueue); + removeCorner(corner, areaTolerance, cornerArea, cornerQueue); } } - return linkedLine.getCoordinates(); } - private PriorityQueue createQueue() { + private PriorityQueue createQueue(double areaTolerance, CornerArea cornerArea) { PriorityQueue cornerQueue = new PriorityQueue(); int minIndex = (linkedLine.isRing() && isFreeRing) ? 0 : 1; - int maxIndex = nbPts - 1; + int maxIndex = nPts - 1; for (int i = minIndex; i < maxIndex; i++) { - addCorner(i, cornerQueue); + addCorner(i, areaTolerance, cornerArea, cornerQueue); } return cornerQueue; } - private void addCorner(int i, PriorityQueue cornerQueue) { - if (isFreeRing || (i != 0 && i != nbPts-1)) { - Corner corner = new Corner(linkedLine, i); - if (corner.getArea() <= areaTolerance) { + private void addCorner(int i, double areaTolerance, CornerArea cornerArea, PriorityQueue cornerQueue) { + //-- add if this vertex can be a corner + if (isFreeRing || (i != 0 && i != nPts - 1)) { + double area = area(i, cornerArea); + if (area <= areaTolerance) { + Corner corner = new Corner(linkedLine, i, area); cornerQueue.add(corner); } } } + private double area(int index, CornerArea cornerArea) { + Coordinate pp = linkedLine.prevCoordinate(index); + Coordinate p = linkedLine.getCoordinate(index); + Coordinate pn = linkedLine.nextCoordinate(index); + return cornerArea.area(pp, p, pn); + } + private boolean isRemovable(Corner corner, EdgeIndex edgeIndex) { Envelope cornerEnv = corner.envelope(); //-- check nearby lines for violating intersections @@ -260,7 +277,18 @@ private boolean hasIntersectingVertex(Corner corner, Envelope cornerEnv, return false; } + private void initIndex() { + vertexIndex = new VertexSequencePackedRtree(pts); + //-- remove ring duplicate final vertex + if (CoordinateArrays.isRing(pts)) { + vertexIndex.remove(pts.length-1); + } + } + private int[] query(Envelope cornerEnv) { + if (vertexIndex == null) { + initIndex(); + } return vertexIndex.query(cornerEnv); } @@ -271,9 +299,11 @@ private int[] query(Envelope cornerEnv) { * (if they are non-convex and thus removable). * * @param corner the corner to remove + * @param cornerArea + * @param areaTolerance * @param cornerQueue the corner queue */ - private void removeCorner(Corner corner, PriorityQueue cornerQueue) { + private void removeCorner(Corner corner, double areaTolerance, CornerArea cornerArea, PriorityQueue cornerQueue) { int index = corner.getIndex(); int prev = linkedLine.prev(index); int next = linkedLine.next(index); @@ -281,8 +311,8 @@ private void removeCorner(Corner corner, PriorityQueue cornerQueue) { vertexIndex.remove(index); //-- potentially add the new corners created - addCorner(prev, cornerQueue); - addCorner(next, cornerQueue); + addCorner(prev, areaTolerance, cornerArea, cornerQueue); + addCorner(next, areaTolerance, cornerArea, cornerQueue); } public String toString() { @@ -294,12 +324,6 @@ private static class EdgeIndex { STRtree index = new STRtree(); - public void add(List edges) { - for (Edge edge : edges) { - add(edge); - } - } - public void add(Edge edge) { index.insert(edge.getEnvelope(), edge); } diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Coordinates.java b/modules/core/src/main/java/org/locationtech/jts/geom/Coordinates.java index 71f5b186f9..a7e79628f0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Coordinates.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Coordinates.java @@ -67,6 +67,26 @@ public static int dimension(Coordinate coordinate) return 3; } + /** + * Check if coordinate can store Z valye, based on subclass of {@link Coordinate}. + * + * @param coordinate supplied coordinate + * @return true if setZ is available + */ + public static boolean hasZ(Coordinate coordinate) + { + if (coordinate instanceof CoordinateXY) { + return false; + } else if (coordinate instanceof CoordinateXYM) { + return false; + } else if (coordinate instanceof CoordinateXYZM) { + return true; + } else if (coordinate instanceof Coordinate) { + return true; + } + return true; + } + /** * Determine number of measures based on subclass of {@link Coordinate}. * diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java index d9e6abd92a..eaefb80b54 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java @@ -713,10 +713,7 @@ public boolean disjoint(Geometry g) { * Returns false if both Geometrys are points */ public boolean touches(Geometry g) { - // short-circuit test - if (! getEnvelopeInternal().intersects(g.getEnvelopeInternal())) - return false; - return relate(g).isTouches(getDimension(), g.getDimension()); + return GeometryRelate.touches(this, g); } /** @@ -771,18 +768,8 @@ public boolean intersects(Geometry g) { if (g.isRectangle()) { return RectangleIntersects.intersects((Polygon) g, this); } - if (isGeometryCollection() || g.isGeometryCollection()) { - for (int i = 0 ; i < getNumGeometries() ; i++) { - for (int j = 0 ; j < g.getNumGeometries() ; j++) { - if (getGeometryN(i).intersects(g.getGeometryN(j))) { - return true; - } - } - } - return false; - } - // general case - return relate(g).isIntersects(); + + return GeometryRelate.intersects(this, g); } /** @@ -845,7 +832,7 @@ public boolean crosses(Geometry g) { * @see Geometry#coveredBy */ public boolean within(Geometry g) { - return g.contains(this); + return GeometryRelate.within(this, g); } /** @@ -876,25 +863,13 @@ public boolean within(Geometry g) { * @see Geometry#covers */ public boolean contains(Geometry g) { - // optimization - lower dimension cannot contain areas - if (g.getDimension() == 2 && getDimension() < 2) { - return false; - } - // optimization - P cannot contain a non-zero-length L - // Note that a point can contain a zero-length lineal geometry, - // since the line has no boundary due to Mod-2 Boundary Rule - if (g.getDimension() == 1 && getDimension() < 1 && g.getLength() > 0.0) { - return false; - } - // optimization - envelope test - if (! getEnvelopeInternal().contains(g.getEnvelopeInternal())) - return false; + // optimization for rectangle arguments if (isRectangle()) { return RectangleContains.contains((Polygon) this, g); } // general case - return relate(g).isContains(); + return GeometryRelate.contains(this, g); } /** @@ -919,10 +894,7 @@ public boolean contains(Geometry g) { *@return true if the two Geometrys overlap. */ public boolean overlaps(Geometry g) { - // short-circuit test - if (! getEnvelopeInternal().intersects(g.getEnvelopeInternal())) - return false; - return relate(g).isOverlaps(getDimension(), g.getDimension()); + return GeometryRelate.overlaps(this, g); } /** @@ -960,24 +932,7 @@ public boolean overlaps(Geometry g) { * @see Geometry#coveredBy */ public boolean covers(Geometry g) { - // optimization - lower dimension cannot cover areas - if (g.getDimension() == 2 && getDimension() < 2) { - return false; - } - // optimization - P cannot cover a non-zero-length L - // Note that a point can cover a zero-length lineal geometry - if (g.getDimension() == 1 && getDimension() < 1 && g.getLength() > 0.0) { - return false; - } - // optimization - envelope test - if (! getEnvelopeInternal().covers(g.getEnvelopeInternal())) - return false; - // optimization for rectangle arguments - if (isRectangle()) { - // since we have already tested that the test envelope is covered - return true; - } - return relate(g).isCovers(); + return GeometryRelate.covers(this, g); } /** @@ -1010,7 +965,7 @@ public boolean covers(Geometry g) { * @see Geometry#covers */ public boolean coveredBy(Geometry g) { - return g.covers(this); + return GeometryRelate.coveredBy(this, g); } /** @@ -1037,7 +992,7 @@ public boolean coveredBy(Geometry g) { * @see IntersectionMatrix */ public boolean relate(Geometry g, String intersectionPattern) { - return relate(g).matches(intersectionPattern); + return GeometryRelate.relate(this, g, intersectionPattern); } /** @@ -1048,9 +1003,7 @@ public boolean relate(Geometry g, String intersectionPattern) { * boundaries and exteriors of the two Geometrys */ public IntersectionMatrix relate(Geometry g) { - checkNotGeometryCollection(this); - checkNotGeometryCollection(g); - return RelateOp.relate(this, g); + return GeometryRelate.relate(this, g); } /** @@ -1101,10 +1054,7 @@ public boolean equals(Geometry g) { */ public boolean equalsTopo(Geometry g) { - // short-circuit test - if (! getEnvelopeInternal().equals(g.getEnvelopeInternal())) - return false; - return relate(g).isEquals(getDimension(), g.getDimension()); + return GeometryRelate.equalsTopo(this, g); } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java new file mode 100644 index 0000000000..615e7d1ab8 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryRelate.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2020 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.geom; + +import org.locationtech.jts.operation.relate.RelateOp; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +/** + * Internal class which encapsulates the runtime switch to use RelateNG. + *

+ * This class allows the {@link Geometry} predicate methods to be + * switched between the original {@link RelateOp} algorithm + * and the modern {@link RelateNG} codebase + * via a system property jts.relate. + *

    + *
  • jts.relate=old - (default) use original RelateOp algorithm + *
  • jts.relate=ng - use RelateNG + *
+ * + * @author mdavis + * + */ +class GeometryRelate +{ + public static String RELATE_PROPERTY_NAME = "jts.relate"; + + public static String RELATE_PROPERTY_VALUE_NG = "ng"; + public static String RELATE_PROPERTY_VALUE_OLD = "old"; + + /** + * Currently the old relate implementation is the default + */ + public static boolean RELATE_NG_DEFAULT = false; + + private static boolean isRelateNG = RELATE_NG_DEFAULT; + + static { + setRelateImpl(System.getProperty(RELATE_PROPERTY_NAME)); + } + + /** + * This function is provided primarily for unit testing. + * It is not recommended to use it dynamically, since + * that may result in inconsistent overlay behaviour. + * + * @param relateImplCode the code for the overlay method (may be null) + */ + static void setRelateImpl(String relateImplCode) { + if (relateImplCode == null) + return; + // set flag explicitly since current value may not be default + isRelateNG = RELATE_NG_DEFAULT; + + if (RELATE_PROPERTY_VALUE_NG.equalsIgnoreCase(relateImplCode) ) + isRelateNG = true; + } + + static boolean intersects(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.intersects()); + } + if (a.isGeometryCollection() || b.isGeometryCollection()) { + for (int i = 0 ; i < a.getNumGeometries() ; i++) { + for (int j = 0 ; j < b.getNumGeometries() ; j++) { + if (a.getGeometryN(i).intersects(b.getGeometryN(j))) { + return true; + } + } + } + return false; + } + return RelateOp.relate(a, b).isIntersects(); + } + + static boolean contains(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.contains()); + } + // optimization - lower dimension cannot contain areas + if (b.getDimension() == 2 && a.getDimension() < 2) { + return false; + } + // optimization - P cannot contain a non-zero-length L + // Note that a point can contain a zero-length lineal geometry, + // since the line has no boundary due to Mod-2 Boundary Rule + if (b.getDimension() == 1 && a.getDimension() < 1 && b.getLength() > 0.0) { + return false; + } + // optimization - envelope test + if (! a.getEnvelopeInternal().contains(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isContains(); + } + + static boolean covers(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.covers()); + } + // optimization - lower dimension cannot cover areas + if (b.getDimension() == 2 && a.getDimension() < 2) { + return false; + } + // optimization - P cannot cover a non-zero-length L + // Note that a point can cover a zero-length lineal geometry + if (b.getDimension() == 1 && a.getDimension() < 1 && b.getLength() > 0.0) { + return false; + } + // optimization - envelope test + if (! a.getEnvelopeInternal().covers(b.getEnvelopeInternal())) + return false; + // optimization for rectangle arguments + if (a.isRectangle()) { + // since we have already tested that the test envelope is covered + return true; + } + return RelateOp.relate(a, b).isCovers(); + } + + static boolean coveredBy(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.coveredBy()); + } + return covers(b, a); + } + + static boolean crosses(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.crosses()); + } + // short-circuit test + if (! a.getEnvelopeInternal().intersects(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isCrosses(a.getDimension(), b.getDimension()); + } + + static boolean disjoint(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.disjoint()); + } + return ! intersects(a, b); + } + + static boolean equalsTopo(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.equalsTopo()); + } + if (! a.getEnvelopeInternal().equals(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isEquals(a.getDimension(), b.getDimension()); + } + + static boolean overlaps(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.overlaps()); + } + if (! a.getEnvelopeInternal().intersects(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isOverlaps(a.getDimension(), b.getDimension()); + } + + static boolean touches(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.touches()); + } + if (! a.getEnvelopeInternal().intersects(b.getEnvelopeInternal())) + return false; + return RelateOp.relate(a, b).isTouches(a.getDimension(), b.getDimension()); + } + + static boolean within(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b, RelatePredicate.within()); + } + return contains(b, a); + } + + static IntersectionMatrix relate(Geometry a, Geometry b) + { + if (isRelateNG) { + return RelateNG.relate(a, b); + } + Geometry.checkNotGeometryCollection(a); + Geometry.checkNotGeometryCollection(b); + return RelateOp.relate(a, b); + } + + static boolean relate(Geometry a, Geometry b, String intersectionPattern) + { + if (isRelateNG) { + return RelateNG.relate(a, b, intersectionPattern); + } + Geometry.checkNotGeometryCollection(a); + Geometry.checkNotGeometryCollection(b); + return RelateOp.relate(a, b).matches(intersectionPattern); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java b/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java index 7e1c2c0d9d..c8305a21c7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java @@ -50,7 +50,7 @@ *
  • Point: keep valid coordinate, or EMPTY
  • *
  • LineString: coordinates are fixed
  • *
  • LinearRing: coordinates are fixed. Keep valid ring, or else convert into LineString
  • - *
  • Polygon: transform into a valid polygon, + *
  • Polygon: transform into a valid polygon or multipolygon, * preserving as much of the extent and vertices as possible. *
      *
    • Rings are fixed to ensure they are valid
    • diff --git a/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java b/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java index 6e3f824ef9..0d6809a286 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java +++ b/modules/core/src/main/java/org/locationtech/jts/geomgraph/GeometryGraph.java @@ -221,10 +221,10 @@ private void add(Geometry g) private void addCollection(GeometryCollection gc) { for (int i = 0; i < gc.getNumGeometries(); i++) { - Geometry g = gc.getGeometryN(i); - add(g); + add(gc.getGeometryN(i)); } } + /** * Add a Point to the graph. */ diff --git a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java index f596673a8b..f6f166712a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java +++ b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HPRtree.java @@ -12,16 +12,14 @@ package org.locationtech.jts.index.hprtree; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.index.ArrayListVisitor; import org.locationtech.jts.index.ItemVisitor; import org.locationtech.jts.index.SpatialIndex; import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.util.IntArrayList; /** * A Hilbert-Packed R-tree. This is a static R-tree @@ -59,28 +57,32 @@ * @author Martin Davis * */ -public class HPRtree +public class HPRtree implements SpatialIndex { private static final int ENV_SIZE = 4; private static final int HILBERT_LEVEL = 12; - private static int DEFAULT_NODE_CAPACITY = 16; + private static final int DEFAULT_NODE_CAPACITY = 16; - private List items = new ArrayList(); - - private int nodeCapacity = DEFAULT_NODE_CAPACITY; + private List itemsToLoad = new ArrayList<>(); + + private final int nodeCapacity; + + private int numItems = 0; - private Envelope totalExtent = new Envelope(); + private final Envelope totalExtent = new Envelope(); private int[] layerStartIndex; private double[] nodeBounds; - private boolean isBuilt = false; + private double[] itemBounds; - //public int nodeIntersectsCount; + private Object[] itemValues; + + private volatile boolean isBuilt = false; /** * Creates a new index with the default node capacity. @@ -104,7 +106,7 @@ public HPRtree(int nodeCapacity) { * @return the number of items */ public int size() { - return items.size(); + return numItems; } @Override @@ -112,7 +114,8 @@ public void insert(Envelope itemEnv, Object item) { if (isBuilt) { throw new IllegalStateException("Cannot insert items after tree is built."); } - items.add( new Item(itemEnv, item) ); + numItems++; + itemsToLoad.add( new Item(itemEnv, item) ); totalExtent.expandToInclude(itemEnv); } @@ -153,7 +156,7 @@ private void queryTopLayer(Envelope searchEnv, ItemVisitor visitor) { private void queryNode(int layerIndex, int nodeOffset, Envelope searchEnv, ItemVisitor visitor) { int layerStart = layerStartIndex[layerIndex]; int nodeIndex = layerStart + nodeOffset; - if (! intersects(nodeIndex, searchEnv)) return; + if (! intersects(nodeBounds, nodeIndex, searchEnv)) return; if (layerIndex == 0) { int childNodesOffset = nodeOffset / ENV_SIZE * nodeCapacity; queryItems(childNodesOffset, searchEnv, visitor); @@ -164,12 +167,11 @@ private void queryNode(int layerIndex, int nodeOffset, Envelope searchEnv, ItemV } } - private boolean intersects(int nodeIndex, Envelope env) { - //nodeIntersectsCount++; - boolean isBeyond = (env.getMaxX() < nodeBounds[nodeIndex]) - || (env.getMaxY() < nodeBounds[nodeIndex+1]) - || (env.getMinX() > nodeBounds[nodeIndex+2]) - || (env.getMinY() > nodeBounds[nodeIndex+3]); + private static boolean intersects(double[] bounds, int nodeIndex, Envelope env) { + boolean isBeyond = (env.getMaxX() < bounds[nodeIndex]) + || (env.getMaxY() < bounds[nodeIndex+1]) + || (env.getMinX() > bounds[nodeIndex+2]) + || (env.getMinY() > bounds[nodeIndex+3]); return ! isBeyond; } @@ -187,34 +189,14 @@ private void queryNodeChildren(int layerIndex, int blockOffset, Envelope searchE private void queryItems(int blockStart, Envelope searchEnv, ItemVisitor visitor) { for (int i = 0; i < nodeCapacity; i++) { - int itemIndex = blockStart + i; + int itemIndex = blockStart + i; // don't query past end of items - if (itemIndex >= items.size()) break; - - // visit the item if its envelope intersects search env - Item item = items.get(itemIndex); - //nodeIntersectsCount++; - if (intersects( item.getEnvelope(), searchEnv) ) { - //if (item.getEnvelope().intersects(searchEnv)) { - visitor.visitItem(item.getItem()); + if (itemIndex >= numItems) break; + if (intersects(itemBounds, itemIndex * ENV_SIZE, searchEnv)) { + visitor.visitItem(itemValues[itemIndex]); } } } - - /** - * Tests whether two envelopes intersect. - * Avoids the null check in {@link Envelope#intersects(Envelope)}. - * - * @param env1 an envelope - * @param env2 an envelope - * @return true if the envelopes intersect - */ - private static boolean intersects(Envelope env1, Envelope env2) { - return !(env2.getMinX() > env1.getMaxX() || - env2.getMaxX() < env1.getMinX() || - env2.getMinY() > env1.getMaxY() || - env2.getMaxY() < env1.getMinY()); - } private int layerSize(int layerIndex) { int layerStart = layerStartIndex[layerIndex]; @@ -231,47 +213,55 @@ public boolean remove(Envelope itemEnv, Object item) { /** * Builds the index, if not already built. */ - public synchronized void build() { + public void build() { // skip if already built - if (isBuilt) return; - isBuilt = true; + if (!isBuilt) { + synchronized (this) { + if (!isBuilt) { + prepareIndex(); + prepareItems(); + this.isBuilt = true; + } + } + } + } + + private void prepareIndex() { // don't need to build an empty or very small tree - if (items.size() <= nodeCapacity) return; + if (itemsToLoad.size() <= nodeCapacity) return; sortItems(); - //dumpItems(items); - - layerStartIndex = computeLayerIndices(items.size(), nodeCapacity); + + layerStartIndex = computeLayerIndices(numItems, nodeCapacity); // allocate storage int nodeCount = layerStartIndex[ layerStartIndex.length - 1 ] / 4; nodeBounds = createBoundsArray(nodeCount); - + // compute tree nodes computeLeafNodes(layerStartIndex[1]); for (int i = 1; i < layerStartIndex.length - 1; i++) { computeLayerNodes(i); } - //dumpNodes(); } - /* - private void dumpNodes() { - GeometryFactory fact = new GeometryFactory(); - for (int i = 0; i < nodeMinX.length; i++) { - Envelope env = new Envelope(nodeMinX[i], nodeMaxX[i], nodeMinY[i], nodeMaxY[i]);; - System.out.println(fact.toGeometry(env)); + private void prepareItems() { + // copy item contents out to arrays for querying + int boundsIndex = 0; + int valueIndex = 0; + itemBounds = new double[itemsToLoad.size() * 4]; + itemValues = new Object[itemsToLoad.size()]; + for (Item item : itemsToLoad) { + Envelope envelope = item.getEnvelope(); + itemBounds[boundsIndex++] = envelope.getMinX(); + itemBounds[boundsIndex++] = envelope.getMinY(); + itemBounds[boundsIndex++] = envelope.getMaxX(); + itemBounds[boundsIndex++] = envelope.getMaxY(); + itemValues[valueIndex++] = item.getItem(); } + // and let GC free the original list + itemsToLoad = null; } - private static void dumpItems(List items) { - GeometryFactory fact = new GeometryFactory(); - for (Item item : items) { - Envelope env = item.getEnvelope(); - System.out.println(fact.toGeometry(env)); - } - } - */ - private static double[] createBoundsArray(int size) { double[] a = new double[4*size]; for (int i = 0; i < size; i++) { @@ -292,7 +282,6 @@ private void computeLayerNodes(int layerIndex) { for (int i = 0; i < layerSize; i += ENV_SIZE) { int childStart = childLayerStart + nodeCapacity * i; computeNodeBounds(layerStart + i, childStart, childLayerEnd); - //System.out.println("Layer: " + layerIndex + " node: " + i + " - " + getNodeEnvelope(layerStart + i)); } } @@ -313,8 +302,8 @@ private void computeLeafNodes(int layerSize) { private void computeLeafNodeBounds(int nodeIndex, int blockStart) { for (int i = 0; i <= nodeCapacity; i++ ) { int itemIndex = blockStart + i; - if (itemIndex >= items.size()) break; - Envelope env = items.get(itemIndex).getEnvelope(); + if (itemIndex >= itemsToLoad.size()) break; + Envelope env = itemsToLoad.get(itemIndex).getEnvelope(); updateNodeBounds(nodeIndex, env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY()); } } @@ -325,13 +314,9 @@ private void updateNodeBounds(int nodeIndex, double minX, double minY, double ma if (maxX > nodeBounds[nodeIndex+2]) nodeBounds[nodeIndex+2] = maxX; if (maxY > nodeBounds[nodeIndex+3]) nodeBounds[nodeIndex+3] = maxY; } - - private Envelope getNodeEnvelope(int i) { - return new Envelope(nodeBounds[i], nodeBounds[i+1], nodeBounds[i+2], nodeBounds[i+3]); - } private static int[] computeLayerIndices(int itemSize, int nodeCapacity) { - List layerIndexList = new ArrayList(); + IntArrayList layerIndexList = new IntArrayList(); int layerSize = itemSize; int index = 0; do { @@ -339,7 +324,7 @@ private static int[] computeLayerIndices(int itemSize, int nodeCapacity) { layerSize = numNodesToCover(layerSize, nodeCapacity); index += ENV_SIZE * layerSize; } while (layerSize > 1); - return toIntArray(layerIndexList); + return layerIndexList.toArray(); } /** @@ -356,14 +341,6 @@ private static int numNodesToCover(int nChild, int nodeCapacity) { if (total == nChild) return mult; return mult + 1; } - - private static int[] toIntArray(List list) { - int[] array = new int[list.size()]; - for (int i = 0; i < array.length; i++) { - array[i] = list.get(i); - } - return array; - } /** * Gets the extents of the internal index nodes @@ -383,24 +360,46 @@ public Envelope[] getBounds() { } private void sortItems() { - ItemComparator comp = new ItemComparator(new HilbertEncoder(HILBERT_LEVEL, totalExtent)); - Collections.sort(items, comp); + HilbertEncoder encoder = new HilbertEncoder(HILBERT_LEVEL, totalExtent); + int[] hilbertValues = new int[itemsToLoad.size()]; + int pos = 0; + for (Item item : itemsToLoad) { + hilbertValues[pos++] = encoder.encode(item.getEnvelope()); + } + quickSortItemsIntoNodes(hilbertValues, 0, itemsToLoad.size() - 1); } - - static class ItemComparator implements Comparator { - private HilbertEncoder encoder; - - public ItemComparator(HilbertEncoder encoder) { - this.encoder = encoder; + private void quickSortItemsIntoNodes(int[] values, int lo, int hi) { + // stop sorting when left/right pointers are within the same node + // because queryItems just searches through them all sequentially + if (lo / nodeCapacity < hi / nodeCapacity) { + int pivot = hoarePartition(values, lo, hi); + quickSortItemsIntoNodes(values, lo, pivot); + quickSortItemsIntoNodes(values, pivot + 1, hi); } + } - @Override - public int compare(Item item1, Item item2) { - int hcode1 = encoder.encode(item1.getEnvelope()); - int hcode2 = encoder.encode(item2.getEnvelope()); - return Integer.compare(hcode1, hcode2); + private int hoarePartition(int[] values, int lo, int hi) { + int pivot = values[(lo + hi) >> 1]; + int i = lo - 1; + int j = hi + 1; + + while (true) { + do i++; while (values[i] < pivot); + do j--; while (values[j] > pivot); + if (i >= j) return j; + swapItems(values, i, j); } } + private void swapItems(int[] values, int i, int j) { + Item tmpItemp = itemsToLoad.get(i); + itemsToLoad.set(i, itemsToLoad.get(j)); + itemsToLoad.set(j, tmpItemp); + + int tmpValue = values[i]; + values[i] = values[j]; + values[j] = tmpValue; + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java b/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java index ff8f6a09c0..c4bb54647d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/WKBReader.java @@ -12,6 +12,7 @@ package org.locationtech.jts.io; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.CoordinateSequenceFactory; @@ -244,6 +245,14 @@ else if(isStrict) boolean hasM = ((typeInt & 0x40000000) != 0 || (typeInt & 0xffff)/1000 == 2 || (typeInt & 0xffff)/1000 == 3); //System.out.println(typeInt + " - " + geometryType + " - hasZ:" + hasZ); inputDimension = 2 + (hasZ ? 1 : 0) + (hasM ? 1 : 0); + + EnumSet ordinateFlags = EnumSet.of(Ordinate.X, Ordinate.Y); + if (hasZ) { + ordinateFlags.add(Ordinate.Z); + } + if (hasM) { + ordinateFlags.add(Ordinate.M); + } // determine if SRIDs are present (EWKB only) boolean hasSRID = (typeInt & 0x20000000) != 0; @@ -258,13 +267,13 @@ else if(isStrict) Geometry geom = null; switch (geometryType) { case WKBConstants.wkbPoint : - geom = readPoint(); + geom = readPoint(ordinateFlags); break; case WKBConstants.wkbLineString : - geom = readLineString(); + geom = readLineString(ordinateFlags); break; case WKBConstants.wkbPolygon : - geom = readPolygon(); + geom = readPolygon(ordinateFlags); break; case WKBConstants.wkbMultiPoint : geom = readMultiPoint(SRID); @@ -298,9 +307,9 @@ private Geometry setSRID(Geometry g, int SRID) return g; } - private Point readPoint() throws IOException, ParseException + private Point readPoint(EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence pts = readCoordinateSequence(1); + CoordinateSequence pts = readCoordinateSequence(1, ordinateFlags); // If X and Y are NaN create a empty point if (Double.isNaN(pts.getX(0)) || Double.isNaN(pts.getY(0))) { return factory.createPoint(); @@ -308,21 +317,21 @@ private Point readPoint() throws IOException, ParseException return factory.createPoint(pts); } - private LineString readLineString() throws IOException, ParseException + private LineString readLineString(EnumSet ordinateFlags) throws IOException, ParseException { int size = readNumField(FIELD_NUMCOORDS); - CoordinateSequence pts = readCoordinateSequenceLineString(size); + CoordinateSequence pts = readCoordinateSequenceLineString(size, ordinateFlags); return factory.createLineString(pts); } - private LinearRing readLinearRing() throws IOException, ParseException + private LinearRing readLinearRing(EnumSet ordinateFlags) throws IOException, ParseException { int size = readNumField(FIELD_NUMCOORDS); - CoordinateSequence pts = readCoordinateSequenceRing(size); + CoordinateSequence pts = readCoordinateSequenceRing(size, ordinateFlags); return factory.createLinearRing(pts); } - private Polygon readPolygon() throws IOException, ParseException + private Polygon readPolygon(EnumSet ordinateFlags) throws IOException, ParseException { int numRings = readNumField(FIELD_NUMRINGS); LinearRing[] holes = null; @@ -333,9 +342,9 @@ private Polygon readPolygon() throws IOException, ParseException if (numRings <= 0) return factory.createPolygon(); - LinearRing shell = readLinearRing(); + LinearRing shell = readLinearRing(ordinateFlags); for (int i = 0; i < numRings - 1; i++) { - holes[i] = readLinearRing(); + holes[i] = readLinearRing(ordinateFlags); } return factory.createPolygon(shell, holes); } @@ -390,9 +399,9 @@ private GeometryCollection readGeometryCollection(int SRID) throws IOException, return factory.createGeometryCollection(geoms); } - private CoordinateSequence readCoordinateSequence(int size) throws IOException, ParseException + private CoordinateSequence readCoordinateSequence(int size, EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence seq = csFactory.create(size, inputDimension); + CoordinateSequence seq = csFactory.create(size, inputDimension, ordinateFlags.contains(Ordinate.M) ? 1 : 0); int targetDim = seq.getDimension(); if (targetDim > inputDimension) targetDim = inputDimension; @@ -405,17 +414,17 @@ private CoordinateSequence readCoordinateSequence(int size) throws IOException, return seq; } - private CoordinateSequence readCoordinateSequenceLineString(int size) throws IOException, ParseException + private CoordinateSequence readCoordinateSequenceLineString(int size, EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence seq = readCoordinateSequence(size); + CoordinateSequence seq = readCoordinateSequence(size, ordinateFlags); if (isStrict) return seq; if (seq.size() == 0 || seq.size() >= 2) return seq; return CoordinateSequences.extend(csFactory, seq, 2); } - private CoordinateSequence readCoordinateSequenceRing(int size) throws IOException, ParseException + private CoordinateSequence readCoordinateSequenceRing(int size, EnumSet ordinateFlags) throws IOException, ParseException { - CoordinateSequence seq = readCoordinateSequence(size); + CoordinateSequence seq = readCoordinateSequence(size, ordinateFlags); if (isStrict) return seq; if (CoordinateSequences.isRing(seq)) return seq; return CoordinateSequences.ensureValidRing(csFactory, seq); diff --git a/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java b/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java index 5fe9e840f6..38fd9b1c5e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/WKBWriter.java @@ -13,6 +13,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; @@ -211,6 +212,7 @@ private static char toHexDigit(int n) return (char) ('A' + (n - 10)); } + private EnumSet outputOrdinates; private int outputDimension = 2; private int byteOrder; private boolean includeSRID = false; @@ -272,13 +274,27 @@ public WKBWriter(int outputDimension, int byteOrder) { /** * Creates a writer that writes {@link Geometry}s with - * the given dimension (2 or 3) for output coordinates + * the given dimension (2 to 4) for output coordinates * and byte order. This constructor also takes a flag to * control whether srid information will be written. * If the input geometry has a small coordinate dimension, * coordinates will be padded with {@link Coordinate#NULL_ORDINATE}. + * The output follows the following rules: + *
        + *
      • If the specified output dimension is 3 and the z is measure flag + * is set to true, the Z value of coordinates will be written if it is present + * (i.e. if it is not Double.NaN)
      • + *
      • If the specified output dimension is 3 and the z is measure flag + * is set to false, the Measure value of coordinates will be written if it is present + * (i.e. if it is not Double.NaN)
      • + *
      • If the specified output dimension is 4, the Z value of coordinates will + * be written even if it is not present when the Measure value is present. The Measure + * value of coordinates will be written if it is present + * (i.e. if it is not Double.NaN)
      • + *
      + * See also {@link #setOutputOrdinates(EnumSet)} * - * @param outputDimension the coordinate dimension to output (2 or 3) + * @param outputDimension the coordinate dimension to output (2 to 4) * @param byteOrder the byte ordering to use * @param includeSRID indicates whether SRID should be written */ @@ -287,10 +303,57 @@ public WKBWriter(int outputDimension, int byteOrder, boolean includeSRID) { this.byteOrder = byteOrder; this.includeSRID = includeSRID; - if (outputDimension < 2 || outputDimension > 3) - throw new IllegalArgumentException("Output dimension must be 2 or 3"); + if (outputDimension < 2 || outputDimension > 4) + throw new IllegalArgumentException("Output dimension must be 2 to 4"); + + this.outputOrdinates = EnumSet.of(Ordinate.X, Ordinate.Y); + if (outputDimension > 2) + outputOrdinates.add(Ordinate.Z); + if (outputDimension > 3) + outputOrdinates.add(Ordinate.M); } - + + /** + * Sets the {@link Ordinate} that are to be written. Possible members are: + *
        + *
      • {@link Ordinate#X}
      • + *
      • {@link Ordinate#Y}
      • + *
      • {@link Ordinate#Z}
      • + *
      • {@link Ordinate#M}
      • + *
      + * Values of {@link Ordinate#X} and {@link Ordinate#Y} are always assumed and not + * particularly checked for. + * + * @param outputOrdinates A set of {@link Ordinate} values + */ + public void setOutputOrdinates(EnumSet outputOrdinates) { + + this.outputOrdinates.remove(Ordinate.Z); + this.outputOrdinates.remove(Ordinate.M); + + if (this.outputDimension == 3) { + if (outputOrdinates.contains(Ordinate.Z)) + this.outputOrdinates.add(Ordinate.Z); + else if (outputOrdinates.contains(Ordinate.M)) + this.outputOrdinates.add(Ordinate.M); + } + if (this.outputDimension == 4) { + if (outputOrdinates.contains(Ordinate.Z)) + this.outputOrdinates.add(Ordinate.Z); + if (outputOrdinates.contains(Ordinate.M)) + this.outputOrdinates.add(Ordinate.M); + } + } + + /** + * Gets a bit-pattern defining which ordinates should be + * @return an ordinate bit-pattern + * @see #setOutputOrdinates(EnumSet) + */ + public EnumSet getOutputOrdinates() { + return this.outputOrdinates; + } + /** * Writes a {@link Geometry} into a byte array. * @@ -405,7 +468,16 @@ private void writeByteOrder(OutStream os) throws IOException private void writeGeometryType(int geometryType, Geometry g, OutStream os) throws IOException { - int flag3D = (outputDimension == 3) ? 0x80000000 : 0; + int ordinals = 0; + if (outputOrdinates.contains(Ordinate.Z)) { + ordinals = ordinals | 0x80000000; + } + + if (outputOrdinates.contains(Ordinate.M)) { + ordinals = ordinals | 0x40000000; + } + + int flag3D = (outputDimension > 2) ? ordinals : 0; int typeInt = geometryType | flag3D; typeInt |= includeSRID ? 0x20000000 : 0; writeInt(typeInt, os); @@ -442,10 +514,14 @@ private void writeCoordinate(CoordinateSequence seq, int index, OutStream os) // only write 3rd dim if caller has requested it for this writer if (outputDimension >= 3) { // if 3rd dim is requested, only write it if the CoordinateSequence provides it - double ordVal = Coordinate.NULL_ORDINATE; - if (seq.getDimension() >= 3) { - ordVal = seq.getOrdinate(index, 2); - } + double ordVal = seq.getOrdinate(index, 2); + ByteOrderValues.putDouble(ordVal, buf, byteOrder); + os.write(buf, 8); + } + // only write 4th dim if caller has requested it for this writer + if (outputDimension == 4) { + // if 4th dim is requested, only write it if the CoordinateSequence provides it + double ordVal = seq.getOrdinate(index, 3); ByteOrderValues.putDouble(ordVal, buf, byteOrder); os.write(buf, 8); } diff --git a/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java b/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java index 530a5686ed..4cb2b28790 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/WKTWriter.java @@ -257,10 +257,11 @@ public WKTWriter() * is set to false, the Measure value of coordinates will be written if it is present * (i.e. if it is not Double.NaN) *
    • If the specified output dimension is 4, the Z value of coordinates will - * be written even if it is not present when the Measure value is present.The Measrue + * be written even if it is not present when the Measure value is present. The Measure * value of coordinates will be written if it is present * (i.e. if it is not Double.NaN)
    • *
    + * See also {@link #setOutputOrdinates(EnumSet)} * * @param outputDimension the coordinate dimension to output (2 to 4) */ diff --git a/modules/core/src/main/java/org/locationtech/jts/io/kml/KMLReader.java b/modules/core/src/main/java/org/locationtech/jts/io/kml/KMLReader.java index f1aabdc15f..bf00a0e203 100644 --- a/modules/core/src/main/java/org/locationtech/jts/io/kml/KMLReader.java +++ b/modules/core/src/main/java/org/locationtech/jts/io/kml/KMLReader.java @@ -34,6 +34,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Constructs a {@link Geometry} object from the OGC KML representation. @@ -44,6 +46,8 @@ public class KMLReader { private final GeometryFactory geometryFactory; private final Set attributeNames; + private final Pattern whitespaceRegex = Pattern.compile("\\s+"); + private static final String POINT = "Point"; private static final String LINESTRING = "LineString"; private static final String POLYGON = "Polygon"; @@ -119,6 +123,8 @@ private Coordinate[] parseKMLCoordinates(XMLStreamReader xmlStreamReader) throws if (coordinates.isEmpty()) { raiseParseError("Empty coordinates"); } + Matcher matcher= whitespaceRegex.matcher(coordinates.trim()); + coordinates = matcher.replaceAll(" "); double[] parsedOrdinates = {Double.NaN, Double.NaN, Double.NaN}; List coordinateList = new ArrayList(); diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java index 9536abc3d5..399efda0d9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java @@ -21,14 +21,14 @@ import org.locationtech.jts.index.chain.MonotoneChain; import org.locationtech.jts.index.chain.MonotoneChainBuilder; import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; -import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.index.hprtree.HPRtree; /** * Nodes a set of {@link SegmentString}s using a index based * on {@link MonotoneChain}s and a {@link SpatialIndex}. * The {@link SpatialIndex} used should be something that supports * envelope (range) queries efficiently (such as a Quadtree} - * or {@link STRtree} (which is the default index provided). + * or {@link HPRtree} (which is the default index provided). *

    * The noder supports using an overlap tolerance distance . * This allows determining segment intersection using a buffer for uses @@ -40,7 +40,7 @@ public class MCIndexNoder extends SinglePassNoder { private List monoChains = new ArrayList(); - private SpatialIndex index= new STRtree(); + private SpatialIndex index= new HPRtree(); private int idCounter = 0; private Collection nodedSegStrings; // statistics diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java index c5038b2dfc..f1ee7af509 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexSegmentSetMutualIntersector.java @@ -42,6 +42,7 @@ public class MCIndexSegmentSetMutualIntersector implements SegmentSetMutualInter */ private STRtree index = new STRtree(); private double overlapTolerance = 0.0; + private Envelope envelope = null; /** * Constructs a new intersector for a given set of {@link SegmentString}s. @@ -53,6 +54,12 @@ public MCIndexSegmentSetMutualIntersector(Collection baseSegStrings) initBaseSegments(baseSegStrings); } + public MCIndexSegmentSetMutualIntersector(Collection baseSegStrings, Envelope env) + { + this.envelope = env; + initBaseSegments(baseSegStrings); + } + public MCIndexSegmentSetMutualIntersector(Collection baseSegStrings, double overlapTolerance) { initBaseSegments(baseSegStrings); @@ -84,7 +91,9 @@ private void addToIndex(SegmentString segStr) List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); for (Iterator i = segChains.iterator(); i.hasNext(); ) { MonotoneChain mc = (MonotoneChain) i.next(); - index.insert(mc.getEnvelope(overlapTolerance), mc); + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + index.insert(mc.getEnvelope(overlapTolerance), mc); + } } } @@ -114,7 +123,9 @@ private void addToMonoChains(SegmentString segStr, List monoChains) List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); for (Iterator i = segChains.iterator(); i.hasNext(); ) { MonotoneChain mc = (MonotoneChain) i.next(); - monoChains.add(mc); + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + monoChains.add(mc); + } } } diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java b/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java index 8860ab59b7..88d866dad0 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/SegmentString.java @@ -36,8 +36,66 @@ public interface SegmentString */ public void setData(Object data); + /** + * Gets the number of coordinates in this segment string. + * + * @return the number of coordinates + */ public int size(); + + /** + * Gets the segment string coordinate at a given index. + * + * @param i the coordinate index + * @return the coordinate at the index + */ public Coordinate getCoordinate(int i); + + /** + * Gets the coordinates in this segment string. + * + * @return the coordinates as an array + */ public Coordinate[] getCoordinates(); + + /** + * Tests if a segment string is a closed ring. + * + * @return true if the segment string is closed + */ public boolean isClosed(); + + /** + * Gets the previous vertex in a ring from a vertex index. + * + * @param ringSS a segment string forming a ring + * @param index the vertex index + * @return the previous vertex in the ring + * + * @see #isClosed + */ + public default Coordinate prevInRing(int index) { + int prevIndex = index - 1; + if (prevIndex < 0) { + prevIndex = size() - 2; + } + return getCoordinate( prevIndex ); + } + + /** + * Gets the next vertex in a ring from a vertex index. + * + * @param ringSS a segment string forming a ring + * @param index the vertex index + * @return the next vertex in the ring + * + * @see #isClosed + */ + public default Coordinate nextInRing(int index) { + int nextIndex = index + 1; + if (nextIndex > size() - 1) { + nextIndex = 1; + } + return getCoordinate( nextIndex ); + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java b/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java index 043ac92640..e4c6f95cdb 100644 --- a/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java +++ b/modules/core/src/main/java/org/locationtech/jts/noding/snapround/MCIndexPointSnapper.java @@ -18,7 +18,6 @@ import org.locationtech.jts.index.SpatialIndex; import org.locationtech.jts.index.chain.MonotoneChain; import org.locationtech.jts.index.chain.MonotoneChainSelectAction; -import org.locationtech.jts.index.strtree.STRtree; import org.locationtech.jts.noding.NodedSegmentString; import org.locationtech.jts.noding.SegmentString; @@ -32,10 +31,10 @@ public class MCIndexPointSnapper { //public static final int nSnaps = 0; - private STRtree index; + private SpatialIndex index; public MCIndexPointSnapper(SpatialIndex index) { - this.index = (STRtree) index; + this.index = index; } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java index ff175f36a8..11f68f17db 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferCurveSetBuilder.java @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Location; @@ -327,57 +328,75 @@ private void addRingSide(Coordinate[] coord, double offsetDistance, int side, in *

    * See https://github.com/locationtech/jts/issues/472 * - * @param inputPts the input ring + * @param inputRing the input ring * @param distance the buffer distance - * @param curvePts the generated offset curve + * @param curveRing the generated offset curve ring * @return true if the offset curve is inverted */ - private static boolean isRingCurveInverted(Coordinate[] inputPts, double distance, Coordinate[] curvePts) { + private static boolean isRingCurveInverted(Coordinate[] inputRing, double distance, Coordinate[] curveRing) { if (distance == 0.0) return false; /** * Only proper rings can invert. */ - if (inputPts.length <= 3) return false; + if (inputRing.length <= 3) return false; /** * Heuristic based on low chance that a ring with many vertices will invert. * This low limit ensures this test is fairly efficient. */ - if (inputPts.length >= MAX_INVERTED_RING_SIZE) return false; + if (inputRing.length >= MAX_INVERTED_RING_SIZE) return false; /** * Don't check curves which are much larger than the input. * This improves performance by avoiding checking some concave inputs * (which can produce fillet arcs with many more vertices) */ - if (curvePts.length > INVERTED_CURVE_VERTEX_FACTOR * inputPts.length) return false; + if (curveRing.length > INVERTED_CURVE_VERTEX_FACTOR * inputRing.length) return false; /** - * Check if the curve vertices are all closer to the input ring - * than the buffer distance. - * If so, the curve is NOT a valid buffer curve. + * If curve contains points which are on the buffer, + * it is not inverted and can be included in the raw curves. */ - double distTol = NEARNESS_FACTOR * Math.abs(distance); - double maxDist = maxDistance(curvePts, inputPts); - boolean isCurveTooClose = maxDist < distTol; - return isCurveTooClose; + if (hasPointOnBuffer(inputRing, distance, curveRing)) + return false; + + //-- curve is inverted, so discard it + return true; } /** - * Computes the maximum distance out of a set of points to a linestring. + * Tests if there are points on the raw offset curve which may + * lie on the final buffer curve + * (i.e. they are (approximately) at the buffer distance from the input ring). + * For efficiency this only tests a limited set of points on the curve. * - * @param pts the points - * @param line the linestring vertices - * @return the maximum distance + * @param inputRing + * @param distance + * @param curveRing + * @return true if the curve contains points lying at the required buffer distance */ - private static double maxDistance(Coordinate[] pts, Coordinate[] line) { - double maxDistance = 0; - for (Coordinate p : pts) { - double dist = Distance.pointToSegmentString(p, line); - if (dist > maxDistance) { - maxDistance = dist; + private static boolean hasPointOnBuffer(Coordinate[] inputRing, double distance, Coordinate[] curveRing) { + double distTol = NEARNESS_FACTOR * Math.abs(distance); + + for (int i = 0; i < curveRing.length - 1; i++) { + Coordinate v = curveRing[i]; + + //-- check curve vertices + double dist = Distance.pointToSegmentString(v, inputRing); + if (dist > distTol) { + return true; + } + + //-- check curve segment midpoints + int iNext = (i < curveRing.length - 1) ? i + 1 : 0; + Coordinate vnext = curveRing[iNext]; + Coordinate midPt = LineSegment.midPoint(v, vnext); + + double distMid = Distance.pointToSegmentString(midPt, inputRing); + if (distMid > distTol) { + return true; } } - return maxDistance; + return false; } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java index 036f818e02..6dc899028b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferInputLineSimplifier.java @@ -14,18 +14,20 @@ import org.locationtech.jts.algorithm.Distance; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.CoordinateList; /** * Simplifies a buffer input line to * remove concavities with shallow depth. *

    - * The most important benefit of doing this + * The major benefit of doing this * is to reduce the number of points and the complexity of * shape which will be buffered. + * This improve performance and robustness. * It also reduces the risk of gores created by * the quantized fillet arcs (although this issue - * should be eliminated in any case by the + * should be eliminated by the * offset curve generation logic). *

    * A key aspect of the simplification is that it @@ -35,8 +37,9 @@ * lies at the correct distance from the input geometry. *

    * Another important heuristic used is that the end segments - * of the input are never simplified. This ensures that + * of linear inputs are never simplified. This ensures that * the client buffer code is able to generate end caps faithfully. + * Ring inputs can have end segments removed by simplification. *

    * No attempt is made to avoid self-intersections in the output. * This is acceptable for use for generating a buffer offset curve, @@ -67,18 +70,18 @@ public static Coordinate[] simplify(Coordinate[] inputLine, double distanceTol) return simp.simplify(distanceTol); } - private static final int INIT = 0; private static final int DELETE = 1; - private static final int KEEP = 1; - private Coordinate[] inputLine; private double distanceTol; - private byte[] isDeleted; + private boolean isRing; + private boolean[] isDeleted; private int angleOrientation = Orientation.COUNTERCLOCKWISE; + public BufferInputLineSimplifier(Coordinate[] inputLine) { this.inputLine = inputLine; + isRing = CoordinateArrays.isRing(inputLine); } /** @@ -94,11 +97,12 @@ public BufferInputLineSimplifier(Coordinate[] inputLine) { public Coordinate[] simplify(double distanceTol) { this.distanceTol = Math.abs(distanceTol); + angleOrientation = Orientation.COUNTERCLOCKWISE; if (distanceTol < 0) angleOrientation = Orientation.CLOCKWISE; - // rely on fact that boolean array is filled with false value - isDeleted = new byte[inputLine.length]; + // rely on fact that boolean array is filled with false values + isDeleted = new boolean[inputLine.length]; boolean isChanged = false; do { @@ -112,18 +116,19 @@ public Coordinate[] simplify(double distanceTol) * Uses a sliding window containing 3 vertices to detect shallow angles * in which the middle vertex can be deleted, since it does not * affect the shape of the resulting buffer in a significant way. - * @return + * + * @return true if any vertices were deleted */ private boolean deleteShallowConcavities() { /** - * Do not simplify end line segments of the line string. + * Do not simplify end line segments of lines. * This ensures that end caps are generated consistently. */ - int index = 1; + int index = isRing ? 0 : 1; - int midIndex = findNextNonDeletedIndex(index); - int lastIndex = findNextNonDeletedIndex(midIndex); + int midIndex = nextIndex(index); + int lastIndex = nextIndex(midIndex); boolean isChanged = false; while (lastIndex < inputLine.length) { @@ -131,7 +136,7 @@ private boolean deleteShallowConcavities() boolean isMiddleVertexDeleted = false; if (isDeletable(index, midIndex, lastIndex, distanceTol)) { - isDeleted[midIndex] = DELETE; + isDeleted[midIndex] = true; isMiddleVertexDeleted = true; isChanged = true; } @@ -141,8 +146,8 @@ private boolean deleteShallowConcavities() else index = midIndex; - midIndex = findNextNonDeletedIndex(index); - lastIndex = findNextNonDeletedIndex(midIndex); + midIndex = nextIndex(index); + lastIndex = nextIndex(midIndex); } return isChanged; } @@ -153,10 +158,10 @@ private boolean deleteShallowConcavities() * @return the next non-deleted index, if any * or inputLine.length if there are no more non-deleted indices */ - private int findNextNonDeletedIndex(int index) + private int nextIndex(int index) { int next = index + 1; - while (next < inputLine.length && isDeleted[next] == DELETE) + while (next < inputLine.length && isDeleted[next]) next++; return next; } @@ -165,10 +170,9 @@ private Coordinate[] collapseLine() { CoordinateList coordList = new CoordinateList(); for (int i = 0; i < inputLine.length; i++) { - if (isDeleted[i] != DELETE) + if (! isDeleted[i]) coordList.add(inputLine[i]); } -// if (coordList.size() < inputLine.length) System.out.println("Simplified " + (inputLine.length - coordList.size()) + " pts"); return coordList.toCoordinateArray(); } @@ -181,22 +185,8 @@ private boolean isDeletable(int i0, int i1, int i2, double distanceTol) if (! isConcave(p0, p1, p2)) return false; if (! isShallow(p0, p1, p2, distanceTol)) return false; - // MD - don't use this heuristic - it's too restricting -// if (p0.distance(p2) > distanceTol) return false; - return isShallowSampled(p0, p1, i0, i2, distanceTol); } - - private boolean isShallowConcavity(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol) - { - int orientation = Orientation.index(p0, p1, p2); - boolean isAngleToSimplify = (orientation == angleOrientation); - if (! isAngleToSimplify) - return false; - - double dist = Distance.pointToSegment(p1, p0, p2); - return dist < distanceTol; - } private static final int NUM_PTS_TO_CHECK = 10; @@ -219,18 +209,17 @@ private boolean isShallowSampled(Coordinate p0, Coordinate p2, int i0, int i2, d if (inc <= 0) inc = 1; for (int i = i0; i < i2; i += inc) { - if (! isShallow(p0, p2, inputLine[i], distanceTol)) return false; + if (! isShallow(p0, inputLine[i], p2, distanceTol)) return false; } return true; } - private boolean isShallow(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol) + private static boolean isShallow(Coordinate p0, Coordinate p1, Coordinate p2, double distanceTol) { double dist = Distance.pointToSegment(p1, p0, p2); return dist < distanceTol; } - private boolean isConcave(Coordinate p0, Coordinate p1, Coordinate p2) { int orientation = Orientation.index(p0, p1, p2); diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java index e9cc6f8799..f4799fc26b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/BufferParameters.java @@ -11,6 +11,8 @@ */ package org.locationtech.jts.operation.buffer; +import org.locationtech.jts.algorithm.Angle; + /** * A value class containing the parameters which * specify how a buffer should be constructed. @@ -176,7 +178,7 @@ public void setQuadrantSegments(int quadSegs) */ public static double bufferDistanceError(int quadSegs) { - double alpha = Math.PI / 2.0 / quadSegs; + double alpha = Angle.PI_OVER_2 / quadSegs; return 1 - Math.cos(alpha / 2.0); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java index 79c598af06..25d044cccd 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java @@ -24,6 +24,7 @@ import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.util.GeometryMapper; @@ -421,8 +422,9 @@ private int matchSegments(Coordinate raw0, Coordinate raw1, int rawCurveIndex, private static class MatchCurveSegmentAction extends MonotoneChainSelectAction { - private Coordinate p0; - private Coordinate p1; + private Coordinate raw0; + private Coordinate raw1; + private double rawLen; private int rawCurveIndex; private Coordinate[] bufferRingPts; private double matchDistance; @@ -430,11 +432,12 @@ private static class MatchCurveSegmentAction private double minRawLocation = -1; private int bufferRingMinIndex = -1; - public MatchCurveSegmentAction(Coordinate p0, Coordinate p1, + public MatchCurveSegmentAction(Coordinate raw0, Coordinate raw1, int rawCurveIndex, double matchDistance, Coordinate[] bufferRingPts, double[] rawCurveLoc) { - this.p0 = p0; - this.p1 = p1; + this.raw0 = raw0; + this.raw1 = raw1; + rawLen = raw0.distance(raw1); this.rawCurveIndex = rawCurveIndex; this.bufferRingPts = bufferRingPts; this.matchDistance = matchDistance; @@ -448,34 +451,61 @@ public int getBufferMinIndex() { public void select(MonotoneChain mc, int segIndex) { /** - * A curveRingPt segment may match all or only a portion of a single raw segment. - * There may be multiple curve ring segs that match along the raw segment. + * Generally buffer segments are no longer than raw curve segments, + * since the final buffer line likely has node points added. + * So a buffer segment may match all or only a portion of a single raw segment. + * There may be multiple buffer ring segs that match along the raw segment. + * + * HOWEVER, in some cases the buffer construction may contain + * a matching buffer segment which is slightly longer than a raw curve segment. + * Specifically, at the endpoint of a closed line with nearly parallel end segments + * - the closing fillet line is very short so is heuristically removed in the buffer. + * In this case, the buffer segment must still be matched. + * This produces closed offset curves, which is technically + * an anomaly, but only happens in rare cases. */ double frac = segmentMatchFrac(bufferRingPts[segIndex], bufferRingPts[segIndex+1], - p0, p1, matchDistance); + raw0, raw1, matchDistance); //-- no match if (frac < 0) return; //-- location is used to sort segments along raw curve double location = rawCurveIndex + frac; rawCurveLoc[segIndex] = location; - //-- record lowest index + //-- buffer seg index at lowest raw location is the curve start if (minRawLocation < 0 || location < minRawLocation) { minRawLocation = location; bufferRingMinIndex = segIndex; } } - } - private static double segmentMatchFrac(Coordinate p0, Coordinate p1, - Coordinate seg0, Coordinate seg1, double matchDistance) { - if (matchDistance < Distance.pointToSegment(p0, seg0, seg1)) + private double segmentMatchFrac(Coordinate buf0, Coordinate buf1, + Coordinate raw0, Coordinate raw1, double matchDistance) { + if (! isMatch(buf0, buf1, raw0, raw1, matchDistance)) return -1; - if (matchDistance < Distance.pointToSegment(p1, seg0, seg1)) - return -1; - //-- matched - determine position as fraction along segment - LineSegment seg = new LineSegment(seg0, seg1); - return seg.segmentFraction(p0); + + //-- matched - determine location as fraction along raw segment + LineSegment seg = new LineSegment(raw0, raw1); + return seg.segmentFraction(buf0); + } + + private boolean isMatch(Coordinate buf0, Coordinate buf1, Coordinate raw0, Coordinate raw1, double matchDistance) { + double bufSegLen = buf0.distance(buf1); + if (rawLen <= bufSegLen) { + if (matchDistance < Distance.pointToSegment(raw0, buf0, buf1)) + return false; + if (matchDistance < Distance.pointToSegment(raw1, buf0, buf1)) + return false; + } + else { + //TODO: only match longer buf segs at raw curve end segs? + if (matchDistance < Distance.pointToSegment(buf0, raw0, raw1)) + return false; + if (matchDistance < Distance.pointToSegment(buf1, raw0, raw1)) + return false; + } + return true; + } } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java index f1591e4451..611d5e0c9b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetSegmentGenerator.java @@ -114,7 +114,7 @@ public OffsetSegmentGenerator(PrecisionModel precisionModel, int quadSegs = bufParams.getQuadrantSegments(); if (quadSegs < 1) quadSegs = 1; - filletAngleQuantum = Math.PI / 2.0 / quadSegs; + filletAngleQuantum = Angle.PI_OVER_2 / quadSegs; /** * Non-round joins cause issues with short closing segments, so don't use @@ -428,7 +428,7 @@ public void addLineEndCap(Coordinate p0, Coordinate p1) case BufferParameters.CAP_ROUND: // add offset seg points with a fillet between them segList.addPt(offsetL.p1); - addDirectedFillet(p1, angle + Math.PI / 2, angle - Math.PI / 2, Orientation.CLOCKWISE, distance); + addDirectedFillet(p1, angle + Angle.PI_OVER_2, angle - Angle.PI_OVER_2, Orientation.CLOCKWISE, distance); segList.addPt(offsetR.p1); break; case BufferParameters.CAP_FLAT: @@ -439,8 +439,8 @@ public void addLineEndCap(Coordinate p0, Coordinate p1) case BufferParameters.CAP_SQUARE: // add a square defined by extensions of the offset segment endpoints Coordinate squareCapSideOffset = new Coordinate(); - squareCapSideOffset.x = Math.abs(distance) * Math.cos(angle); - squareCapSideOffset.y = Math.abs(distance) * Math.sin(angle); + squareCapSideOffset.x = Math.abs(distance) * Angle.cosSnap(angle); + squareCapSideOffset.y = Math.abs(distance) * Angle.sinSnap(angle); Coordinate squareCapLOffset = new Coordinate( offsetL.p1.x + squareCapSideOffset.x, @@ -534,7 +534,7 @@ private void addLimitedMitreJoin( Coordinate bevelMidPt = project(cornerPt, -mitreLimitDistance, dirBisector); // direction of bevel segment (at right angle to corner bisector) - double dirBevel = Angle.normalize(dirBisector + Math.PI/2.0); + double dirBevel = Angle.normalize(dirBisector + Angle.PI_OVER_2); // compute the candidate bevel segment by projecting both sides of the midpoint Coordinate bevel0 = project(bevelMidPt, distance, dirBevel); @@ -567,8 +567,8 @@ private void addLimitedMitreJoin( * @return the projected point */ private static Coordinate project(Coordinate pt, double d, double dir) { - double x = pt.x + d * Math.cos(dir); - double y = pt.y + d * Math.sin(dir); + double x = pt.x + d * Angle.cosSnap(dir); + double y = pt.y + d * Angle.sinSnap(dir); return new Coordinate(x, y); } @@ -607,10 +607,10 @@ private void addCornerFillet(Coordinate p, Coordinate p0, Coordinate p1, int dir double endAngle = Math.atan2(dy1, dx1); if (direction == Orientation.CLOCKWISE) { - if (startAngle <= endAngle) startAngle += 2.0 * Math.PI; + if (startAngle <= endAngle) startAngle += Angle.PI_TIMES_2; } else { // direction == COUNTERCLOCKWISE - if (startAngle >= endAngle) startAngle -= 2.0 * Math.PI; + if (startAngle >= endAngle) startAngle -= Angle.PI_TIMES_2; } segList.addPt(p0); addDirectedFillet(p, startAngle, endAngle, direction, radius); @@ -641,8 +641,8 @@ private void addDirectedFillet(Coordinate p, double startAngle, double endAngle, Coordinate pt = new Coordinate(); for (int i = 0; i < nSegs; i++) { double angle = startAngle + directionFactor * i * angleInc; - pt.x = p.x + radius * Math.cos(angle); - pt.y = p.y + radius * Math.sin(angle); + pt.x = p.x + radius * Angle.cosSnap(angle); + pt.y = p.y + radius * Angle.sinSnap(angle); segList.addPt(pt); } } @@ -655,7 +655,7 @@ public void createCircle(Coordinate p) // add start point Coordinate pt = new Coordinate(p.x + distance, p.y); segList.addPt(pt); - addDirectedFillet(p, 0.0, 2.0 * Math.PI, -1, distance); + addDirectedFillet(p, 0.0, Angle.PI_TIMES_2, -1, distance); segList.closeRing(); } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java index c15c3394ed..6874f1d2c8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/VariableBuffer.java @@ -15,6 +15,7 @@ import java.util.List; import org.locationtech.jts.algorithm.Angle; +import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.Geometry; @@ -28,8 +29,9 @@ /** * Creates a buffer polygon with a varying buffer distance * at each vertex along a line. + * Vertex distances may be zero. *

    - * Only single lines are supported as input, since buffer widths + * Only single linestrings are supported as input, since buffer widths * are typically specified individually for each line. * * @author Martin Davis @@ -37,6 +39,8 @@ */ public class VariableBuffer { + private static final int MIN_CAP_SEG_LEN_FACTOR = 4; + /** * Creates a buffer polygon along a line with the buffer distance interpolated * between a start distance and an end distance. @@ -221,7 +225,7 @@ public VariableBuffer(Geometry line, double[] distance) { } /** - * Computes the buffer polygon. + * Computes the variable buffer polygon. * * @return a buffer polygon */ @@ -244,7 +248,7 @@ public Geometry getResult() { .createGeometryCollection(GeometryFactory.toGeometryArray(parts)); Geometry buffer = partsGeom.union(); - // ensure an empty polygon is returned if needed + //-- ensure an empty polygon is returned if needed if (buffer.isEmpty()) { return geomFactory.createPolygon(); } @@ -256,26 +260,41 @@ public Geometry getResult() { * with the given endpoints and buffer distances. * The individual segment buffers are unioned * to form the final buffer. + * If one distance is zero, the end cap at that + * segment end is the endpoint of the segment. + * If both distances are zero, no polygon is returned. * * @param p0 the segment start point * @param p1 the segment end point * @param dist0 the buffer distance at the start point * @param dist1 the buffer distance at the end point - * @return the segment buffer. + * @return the segment buffer, or null if void */ private Polygon segmentBuffer(Coordinate p0, Coordinate p1, double dist0, double dist1) { /** - * Compute for increasing distance only, so flip if needed + * Skip buffer polygon if both distances are zero + */ + if (dist0 <= 0 && dist1 <= 0) + return null; + + /** + * Generation algorithm requires increasing distance, so flip if needed */ if (dist0 > dist1) { - return segmentBuffer(p1, p0, dist1, dist0); + return segmentBufferOriented(p1, p0, dist1, dist0); } - - // forward tangent line + return segmentBufferOriented(p0, p1, dist0, dist1); + } + + private Polygon segmentBufferOriented(Coordinate p0, Coordinate p1, + double dist0, double dist1) { + //-- Assert: dist0 <= dist1 + + //-- forward tangent line LineSegment tangent = outerTangent(p0, dist0, p1, dist1); - // if tangent is null then compute a buffer for largest circle + //-- if tangent is null then compute a buffer for largest circle if (tangent == null) { Coordinate center = p0; double dist = dist0; @@ -286,34 +305,32 @@ private Polygon segmentBuffer(Coordinate p0, Coordinate p1, return circle(center, dist); } - Coordinate t0 = tangent.getCoordinate(0); - Coordinate t1 = tangent.getCoordinate(1); - - // reverse tangent line on other side of segment - LineSegment seg = new LineSegment(p0, p1); - Coordinate tr0 = seg.reflect(t0); - Coordinate tr1 = seg.reflect(t1); + //-- reverse tangent line on other side of segment + LineSegment tangentReflect = reflect(tangent, p0, p1, dist0); CoordinateList coords = new CoordinateList(); - coords.add(t0); - coords.add(t1); - - // end cap - addCap(p1, dist1, t1, tr1, coords); - - coords.add(tr1); - coords.add(tr0); - - // start cap - addCap(p0, dist0, tr0, t0, coords); + //-- end cap + addCap(p1, dist1, tangent.p1, tangentReflect.p1, coords); + //-- start cap + addCap(p0, dist0, tangentReflect.p0, tangent.p0, coords); - // close - coords.add(t0); + coords.closeRing(); Coordinate[] pts = coords.toCoordinateArray(); Polygon polygon = geomFactory.createPolygon(pts); +//System.out.println(polygon); return polygon; } + + private LineSegment reflect(LineSegment seg, Coordinate p0, Coordinate p1, double dist0) { + LineSegment line = new LineSegment(p0, p1); + Coordinate r0 = line.reflect(seg.p0); + Coordinate r1 = line.reflect(seg.p1); + //-- avoid numeric jitter if first distance is zero (second dist must be > 0) + if (dist0 == 0) + r0 = p0.copy(); + return new LineSegment(r0, r1); + } /** * Returns a circular polygon. @@ -337,6 +354,10 @@ private Polygon circle(Coordinate center, double radius) { /** * Adds a semi-circular cap CCW around the point p. + * <>p> + * The vertices in caps are generated at fixed angles around a point. + * This allows caps at the same point to share vertices, + * which reduces artifacts when the segment buffers are merged. * * @param p the centre point of the cap * @param r the cap radius @@ -345,6 +366,13 @@ private Polygon circle(Coordinate center, double radius) { * @param coords the coordinate list to add to */ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, CoordinateList coords) { + //-- if radius is zero just copy the vertex + if (r == 0) { + coords.add(p.copy(), false); + return; + } + + coords.add(t1, false); double angStart = Angle.angle(p, t1); double angEnd = Angle.angle(p, t2); @@ -354,18 +382,55 @@ private void addCap(Coordinate p, double r, Coordinate t1, Coordinate t2, Coordi int indexStart = capAngleIndex(angStart); int indexEnd = capAngleIndex(angEnd); - for (int i = indexStart; i > indexEnd; i--) { - // use negative increment to create points CW + double capSegLen = r * 2 * Math.sin(Math.PI / 4 / quadrantSegs); + double minSegLen = capSegLen / MIN_CAP_SEG_LEN_FACTOR; + + for (int i = indexStart; i >= indexEnd; i--) { + //-- use negative increment to create points CW double ang = capAngle(i); - coords.add( projectPolar(p, r, ang) ); + Coordinate capPt = projectPolar(p, r, ang); + + boolean isCapPointHighQuality = true; + /** + * Due to the fixed locations of the cap points, + * a start or end cap point might create + * a "reversed" segment to the next tangent point. + * This causes an unwanted narrow spike in the buffer curve, + * which can cause holes in the final buffer polygon. + * These checks remove these points. + */ + if (i == indexStart + && Orientation.CLOCKWISE != Orientation.index(p, t1, capPt)) { + isCapPointHighQuality = false; + } + else if (i == indexEnd + && Orientation.COUNTERCLOCKWISE != Orientation.index(p, t2, capPt)) { + isCapPointHighQuality = false; + } + + /** + * Remove short segments between the cap and the tangent segments. + */ + if (capPt.distance(t1) < minSegLen) { + isCapPointHighQuality = false; + } + else if (capPt.distance(t2) < minSegLen) { + isCapPointHighQuality = false; + } + + if (isCapPointHighQuality) { + coords.add(capPt, false ); + } } + + coords.add(t2, false); } /** - * Computes the angle for the given cap point index. + * Computes the actual angle for a cap angle index. * - * @param index the fillet angle index - * @return + * @param index the cap angle index + * @return the angle */ private double capAngle(int index) { double capSegAng = Math.PI / 2 / quadrantSegs; @@ -374,15 +439,13 @@ private double capAngle(int index) { /** * Computes the canonical cap point index for a given angle. - * The angle is rounded down to the next lower - * index. + * The angle is rounded down to the next lower index. *

    * In order to reduce the number of points created by overlapping end caps, * cap points are generated at the same locations around a circle. * The index is the index of the points around the circle, * with 0 being the point at (1,0). - * The total number of points around the circle is - * 4 * quadrantSegs. + * The total number of points around the circle is 4 * quadrantSegs. * * @param ang the angle * @return the index for the angle. @@ -396,6 +459,7 @@ private int capAngleIndex(double ang) { /** * Computes the two circumference points defining the outer tangent line * between two circles. + * The tangent line may be null if one circle mostly overlaps the other. *

    * For the algorithm see Wikipedia. * diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java b/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java index 57f1e88c1f..f5af68e3fe 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/distance/DistanceOp.java @@ -114,6 +114,7 @@ public static Coordinate[] closestPoints(Geometry g0, Geometry g1) private Geometry[] geom; private double terminateDistance = 0.0; // working + private PointLocator ptLocator = new PointLocator(); private GeometryLocation[] minDistanceLocation; private double minDistance = Double.MAX_VALUE; private final DistanceSupport external; @@ -164,6 +165,11 @@ public double distance() if (geom[0].isEmpty() || geom[1].isEmpty()) return 0.0; + //-- optimization for Point/Point case + if (geom[0] instanceof Point && geom[1] instanceof Point) { + return geom[0].getCoordinate().distance(geom[1].getCoordinate()); + } + computeMinDistance(); return minDistance; } @@ -355,8 +361,12 @@ private void computeMinDistancePoints(List points0, List points1, { for (int i = 0; i < points0.size(); i++) { Point pt0 = points0.get(i); + if (pt0.isEmpty()) + continue; for (int j = 0; j < points1.size(); j++) { Point pt1 = points1.get(j); + if (pt1.isEmpty()) + continue; double dist = external.distance(pt0.getCoordinate(), pt1.getCoordinate()); if (dist < minDistance) { minDistance = dist; @@ -375,6 +385,8 @@ private void computeMinDistanceLinesPoints(List lines, List p LineString line = lines.get(i); for (int j = 0; j < points.size(); j++) { Point pt = points.get(j); + if (pt.isEmpty()) + continue; computeMinDistance(line, pt, locGeom); if (minDistance <= terminateDistance) return; } diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java index 6265bd0e38..dd82a8de2d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java @@ -392,6 +392,8 @@ public static boolean isResultAreaConsistent(Geometry geom0, Geometry geom1, int if (geom0 == null || geom1 == null) return true; + if (result.getDimension() < 2) return true; + double areaResult = result.getArea(); double areaA = geom0.getArea(); double areaB = geom1.getArea(); @@ -403,8 +405,7 @@ public static boolean isResultAreaConsistent(Geometry geom0, Geometry geom1, int && isLess(areaResult, areaB, AREA_HEURISTIC_TOLERANCE); break; case OverlayNG.DIFFERENCE: - isConsistent = isLess(areaResult, areaA, AREA_HEURISTIC_TOLERANCE) - && isGreater(areaResult, areaA - areaB, AREA_HEURISTIC_TOLERANCE); + isConsistent = isDifferenceAreaConsistent(areaA, areaB, areaResult, AREA_HEURISTIC_TOLERANCE); break; case OverlayNG.SYMDIFFERENCE: isConsistent = isLess(areaResult, areaA + areaB, AREA_HEURISTIC_TOLERANCE); @@ -417,6 +418,23 @@ && isLess(areaB, areaResult, AREA_HEURISTIC_TOLERANCE) } return isConsistent; } + + /** + * Tests if the area of a difference is greater than the minimum possible difference area. + * This is a heuristic which will only detect gross overlay errors. + * @param areaA the area of A + * @param areaB the area of B + * @param areaResult the result area + * @param tolFrac the area tolerance fraction + * + * @return true if the difference area is consistent. + */ + private static boolean isDifferenceAreaConsistent(double areaA, double areaB, double areaResult, double tolFrac) { + if (! isLess(areaResult, areaA, tolFrac)) + return false; + double areaDiffMin = areaA - areaB - tolFrac * areaA; + return areaResult > areaDiffMin; + } private static boolean isLess(double v1, double v2, double tol) { return v1 <= v2 * (1 + tol); diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java new file mode 100644 index 0000000000..e66a68cf5e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocator.java @@ -0,0 +1,117 @@ +/* + * 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.ArrayList; +import java.util.List; + +import org.locationtech.jts.algorithm.PointLocation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Polygon; + +/** + * Determines the location for a point which is known to lie + * on at least one edge of a set of polygons. + * This provides the union-semantics for determining + * point location in a GeometryCollection, which may + * have polygons with adjacent edges which are effectively + * in the interior of the geometry. + * Note that it is also possible to have adjacent edges which + * lie on the boundary of the geometry + * (e.g. a polygon contained within another polygon with adjacent edges). + * + * @author mdavis + * + */ +class AdjacentEdgeLocator { + + private List ringList;; + + public AdjacentEdgeLocator(Geometry geom) { + init(geom); + } + + public int locate(Coordinate p) { + NodeSections sections = new NodeSections(p); + for (Coordinate[] ring : ringList) { + addSections(p, ring, sections); + } + RelateNode node = sections.createNode(); + //node.finish(false, false); + return node.hasExteriorEdge(true) ? Location.BOUNDARY : Location.INTERIOR; + } + + private void addSections(Coordinate p, Coordinate[] ring, NodeSections sections) { + for (int i = 0; i < ring.length - 1; i++) { + Coordinate p0 = ring[i]; + Coordinate pnext = ring[i + 1]; + + if (p.equals2D(pnext)) { + //-- segment final point is assigned to next segment + continue; + } + else if (p.equals2D(p0)) { + int iprev = i > 0 ? i - 1 : ring.length - 2; + Coordinate pprev = ring[iprev]; + sections.addNodeSection(createSection(p, pprev, pnext)); + } + else if (PointLocation.isOnSegment(p, p0, pnext)) { + sections.addNodeSection(createSection(p, p0, pnext)); + } + } + } + + private NodeSection createSection(Coordinate p, Coordinate prev, Coordinate next) { + if (prev.distance(p) == 0 || next.distance(p) == 0) { + System.out.println("Found zero-length section segment"); + }; + NodeSection ns = new NodeSection(true, Dimension.A, 1, 0, null, false, prev, p, next); + return ns; + } + + private void init(Geometry geom) { + if (geom.isEmpty()) + return; + ringList = new ArrayList(); + addRings(geom, ringList); + } + + private void addRings(Geometry geom, List ringList2) { + if (geom instanceof Polygon) { + Polygon poly = (Polygon) geom; + LinearRing shell = poly.getExteriorRing(); + addRing(shell, true); + for (int i = 0; i < poly.getNumInteriorRing(); i++) { + LinearRing hole = poly.getInteriorRingN(i); + addRing(hole, false); + } + } + else if (geom instanceof GeometryCollection) { + //-- recurse through collections + for (int i = 0; i < geom.getNumGeometries(); i++) { + addRings(geom.getGeometryN(i), ringList); + } + } + } + + private void addRing(LinearRing ring, boolean requireCW) { + //TODO: remove repeated points? + Coordinate[] pts = RelateGeometry.orient(ring.getCoordinates(), requireCW); + ringList.add(pts); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java new file mode 100644 index 0000000000..cc0260c1ed --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/BasicPredicate.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +/** + * The base class for relate topological predicates + * with a boolean value. + * Implements tri-state logic for the predicate value, + * to detect when the final value has been determined. + * + * @author Martin Davis + * + */ +abstract class BasicPredicate implements TopologyPredicate { + + private static final int UNKNOWN = -1; + private static final int FALSE = 0; + private static final int TRUE = 1; + + private static boolean isKnown(int value) { + return value > UNKNOWN; + } + + private static boolean toBoolean(int value) { + return value == TRUE; + } + + private static int toValue(boolean val) { + return val ? TRUE : FALSE; + } + + /** + * Tests if two geometries intersect + * based on an interaction at given locations. + * + * @param locA the location on geometry A + * @param locB the location on geometry B + * @return true if the geometries intersect + */ + public static boolean isIntersection(int locA, int locB) { + //-- i.e. some location on both geometries intersects + return locA != Location.EXTERIOR && locB != Location.EXTERIOR; + } + + private int value = UNKNOWN; + + /* + public boolean isSelfNodingRequired() { + return false; + } + */ + + @Override + public boolean isKnown() { + return isKnown(value); + } + + @Override + public boolean value() { + return toBoolean(value); + } + + /** + * Updates the predicate value to the given state + * if it is currently unknown. + * + * @param val the predicate value to update + */ + protected void setValue(boolean val) { + //-- don't change already-known value + if (isKnown()) + return; + value = toValue(val); + } + + protected void setValue(int val) { + //-- don't change already-known value + if (isKnown()) + return; + value = val; + } + + protected void setValueIf(boolean value, boolean cond) { + if (cond) + setValue(value); + } + + protected void require(boolean cond) { + if (! cond) + setValue(false); + } + + protected void requireCovers(Envelope a, Envelope b) { + require(a.covers(b)); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java new file mode 100644 index 0000000000..bcdc421ac2 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/DimensionLocation.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; + +/** + * Codes which combine a geometry dimension and a location + * on the geometry. + * + * @author mdavis + * + */ +class DimensionLocation { + + public static final int EXTERIOR = Location.EXTERIOR; + 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 locationArea(int loc) { + switch (loc) { + case Location.INTERIOR: return AREA_INTERIOR; + case Location.BOUNDARY: return AREA_BOUNDARY; + } + return EXTERIOR; + } + + public static int locationLine(int loc) { + switch (loc) { + case Location.INTERIOR: return LINE_INTERIOR; + case Location.BOUNDARY: return LINE_BOUNDARY; + } + return EXTERIOR; + } + + public static int locationPoint(int loc) { + switch (loc) { + case Location.INTERIOR: return POINT_INTERIOR; + } + return EXTERIOR; + } + + public static int location(int dimLoc) { + switch (dimLoc) { + 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 dimLoc) { + switch (dimLoc) { + 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; + } + + public static int dimension(int dimLoc, int exteriorDim) { + if (dimLoc == EXTERIOR) + return exteriorDim; + return dimension(dimLoc); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java new file mode 100644 index 0000000000..f60e0f6452 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentIntersector.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022 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 org.locationtech.jts.algorithm.RobustLineIntersector; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.noding.SegmentIntersector; +import org.locationtech.jts.noding.SegmentString; + +/** + * Tests segments of {@link RelateSegmentString}s + * and if they intersect adds the intersection(s) + * to the {@link TopologyComputer}. + * + * @author Martin Davis + * + */ +class EdgeSegmentIntersector implements SegmentIntersector +{ + private RobustLineIntersector li = new RobustLineIntersector(); + private TopologyComputer topoComputer; + + public EdgeSegmentIntersector(TopologyComputer topoBuilder) { + this.topoComputer = topoBuilder; + } + + @Override + public boolean isDone() { + return topoComputer.isResultKnown(); + } + + public void processIntersections(SegmentString ss0, int segIndex0, + SegmentString ss1, int segIndex1) { + // don't intersect a segment with itself + if (ss0 == ss1 && segIndex0 == segIndex1) return; + + RelateSegmentString rss0 = (RelateSegmentString) ss0; + RelateSegmentString rss1 = (RelateSegmentString) ss1; + //TODO: move this ordering logic to TopologyBuilder + if (rss0.isA()) { + addIntersections(rss0, segIndex0, rss1, segIndex1); + } + else { + addIntersections(rss1, segIndex1, rss0, segIndex0); + } + } + + private void addIntersections(RelateSegmentString ssA, int segIndexA, + RelateSegmentString ssB, int segIndexB) { + + Coordinate a0 = ssA.getCoordinate(segIndexA); + Coordinate a1 = ssA.getCoordinate(segIndexA + 1); + Coordinate b0 = ssB.getCoordinate(segIndexB); + Coordinate b1 = ssB.getCoordinate(segIndexB + 1); + + li.computeIntersection(a0, a1, b0, b1); + + if (! li.hasIntersection()) + return; + + for (int i = 0; i < li.getIntersectionNum(); i++) { + Coordinate intPt = li.getIntersection(i); + /** + * Ensure endpoint intersections are added once only, for their canonical segments. + * Proper intersections lie on a unique segment so do not need to be checked. + * And it is important that the Containing Segment check not be used, + * since due to intersection computation roundoff, + * it is not reliable in that situation. + */ + if (li.isProper() + || (ssA.isContainingSegment(segIndexA, intPt) + && ssB.isContainingSegment(segIndexB, intPt))) { + NodeSection nsa = ssA.createNodeSection(segIndexA, intPt); + NodeSection nsb = ssB.createNodeSection(segIndexB, intPt); + topoComputer.addIntersection(nsa, nsb); + } + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java new file mode 100644 index 0000000000..7a44fd979a --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSegmentOverlapAction.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.index.chain.MonotoneChain; +import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; +import org.locationtech.jts.noding.SegmentIntersector; +import org.locationtech.jts.noding.SegmentString; + +class EdgeSegmentOverlapAction + extends MonotoneChainOverlapAction +{ + private SegmentIntersector si = null; + + public EdgeSegmentOverlapAction(SegmentIntersector si) + { + this.si = si; + } + + public void overlap(MonotoneChain mc1, int start1, MonotoneChain mc2, int start2) + { + SegmentString ss1 = (SegmentString) mc1.getContext(); + SegmentString ss2 = (SegmentString) mc2.getContext(); + si.processIntersections(ss1, start1, ss2, start2); + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java new file mode 100644 index 0000000000..5e4cc40b15 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/EdgeSetIntersector.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 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.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.chain.MonotoneChain; +import org.locationtech.jts.index.chain.MonotoneChainBuilder; +import org.locationtech.jts.index.chain.MonotoneChainOverlapAction; +import org.locationtech.jts.index.hprtree.HPRtree; +import org.locationtech.jts.noding.SegmentString; + +class EdgeSetIntersector { + + private HPRtree index = new HPRtree(); + private Envelope envelope; + private List monoChains = new ArrayList(); + private int idCounter = 0; + + public EdgeSetIntersector(List edgesA, List edgesB, Envelope env) { + this.envelope = env; + addEdges(edgesA); + addEdges(edgesB); + // build index to ensure thread-safety + index.build(); + } + + private void addEdges(Collection segStrings) + { + for (SegmentString ss : segStrings) { + addToIndex(ss); + } + } + + private void addToIndex(SegmentString segStr) + { + List segChains = MonotoneChainBuilder.getChains(segStr.getCoordinates(), segStr); + for (MonotoneChain mc : segChains ) { + if (envelope == null || envelope.intersects(mc.getEnvelope())) { + mc.setId(idCounter ++); + index.insert(mc.getEnvelope(), mc); + monoChains.add(mc); + } + } + } + + public void process(EdgeSegmentIntersector intersector) { + MonotoneChainOverlapAction overlapAction = new EdgeSegmentOverlapAction(intersector); + + for (MonotoneChain queryChain : monoChains) { + List overlapChains = index.query(queryChain.getEnvelope()); + for (MonotoneChain testChain : overlapChains) { + /** + * following test makes sure we only compare each pair of chains once + * and that we don't compare a chain to itself + */ + if (testChain.getId() <= queryChain.getId()) + continue; + + testChain.computeOverlaps(queryChain, overlapAction); + if (intersector.isDone()) + return; + } + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java new file mode 100644 index 0000000000..ea93e3ac4d --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPatternMatcher.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.IntersectionMatrix; +import org.locationtech.jts.geom.Location; + +/** + * A predicate that matches a DE-9IM pattern. + * + *

    FUTURE WORK

    + * Extend the expressiveness of the DE-9IM pattern language to allow: + *
      + *
    • Combining patterns via disjunction using "|". + *
    • Limiting patterns via geometry dimension. + * A dimension limit specifies the allowable dimensions + * for both or individual geometries as [d] or [ab] or [ab;cd] + *
    + * + * @author Martin Davis + * + */ +class IMPatternMatcher extends IMPredicate +{ + private String imPattern = null; + private IntersectionMatrix patternMatrix; + + public IMPatternMatcher(String imPattern) { + this.imPattern = imPattern; + this.patternMatrix = new IntersectionMatrix(imPattern); + } + + public String name() { return "IMPattern"; } + + //TODO: implement requiresExteriorCheck by inspecting matrix entries for E + + public void init(Envelope envA, Envelope envB) { + super.init(dimA, dimB); + //-- if pattern specifies any non-E/non-E interaction, envelopes must not be disjoint + boolean requiresInteraction = requireInteraction(patternMatrix); + boolean isDisjoint = envA.disjoint(envB); + setValueIf(false, requiresInteraction && isDisjoint); + } + + @Override + public boolean requireInteraction() { + return requireInteraction(patternMatrix); + } + + private static boolean requireInteraction(IntersectionMatrix im) { + boolean requiresInteraction = + isInteraction(im.get(Location.INTERIOR, Location.INTERIOR)) + || isInteraction(im.get(Location.INTERIOR, Location.BOUNDARY)) + || isInteraction(im.get(Location.BOUNDARY, Location.INTERIOR)) + || isInteraction(im.get(Location.BOUNDARY, Location.BOUNDARY)); + return requiresInteraction; + } + + private static boolean isInteraction(int imDim) { + return imDim == Dimension.TRUE || imDim >= Dimension.P; + } + + @Override + public boolean isDetermined() { + /** + * Matrix entries only increase in dimension as topology is computed. + * The predicate can be short-circuited (as false) if + * any computed entry is greater than the mask value. + */ + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + int patternEntry = patternMatrix.get(i, j); + + if (patternEntry == Dimension.DONTCARE) + continue; + + int matrixVal = getDimension(i, j); + + //-- mask entry TRUE requires a known matrix entry + if (patternEntry == Dimension.TRUE) { + if (matrixVal < 0) + return false; + } + //-- result is known (false) if matrix entry has exceeded mask + else if (matrixVal > patternEntry) + return true; + } + } + return false; + } + + @Override + public boolean valueIM() { + boolean val = intMatrix.matches(imPattern); + return val; + } + + public String toString() { + return name() + "(" + imPattern + ")"; + } +} \ No newline at end of file diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java new file mode 100644 index 0000000000..1905e305b5 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IMPredicate.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.IntersectionMatrix; +import org.locationtech.jts.geom.Location; + +/** + * A base class for predicates which are + * determined using entries in a {@link IntersectionMatrix}. + * + * @author Martin Davis + * + */ +abstract class IMPredicate extends BasicPredicate { + + public static boolean isDimsCompatibleWithCovers(int dim0, int dim1) { + //- allow Points coveredBy zero-length Lines + if (dim0 == Dimension.P && dim1 == Dimension.L) + return true; + return dim0 >= dim1; + } + + static final int DIM_UNKNOWN = Dimension.DONTCARE; + + protected int dimA; + protected int dimB; + protected IntersectionMatrix intMatrix; + + public IMPredicate() { + intMatrix = new IntersectionMatrix(); + //-- E/E is always dim = 2 + intMatrix.set(Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + + @Override + public void init(int dimA, int dimB) { + this.dimA = dimA; + this.dimB = dimB; + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + //-- only record an increased dimension value + if (isDimChanged(locA, locB, dimension)) { + intMatrix.set(locA, locB, dimension); + //-- set value if predicate value can be known + if (isDetermined()) { + setValue( valueIM()); + } + } + } + + public boolean isDimChanged(int locA, int locB, int dimension) { + return dimension > intMatrix.get(locA, locB); + } + + /** + * Tests whether predicate evaluation can be short-circuited + * due to the current state of the matrix providing + * enough information to determine the predicate value. + *

    + * If this value is true then {@link valueIM()} + * must provide the correct result of the predicate. + * + * @return true if the predicate value is determined + */ + protected abstract boolean isDetermined(); + + /** + * Tests whether the exterior of the specified input geometry + * is intersected by any part of the other input. + * + * @param isA the input geometry + * @return true if the input geometry exterior is intersected + */ + protected boolean intersectsExteriorOf(boolean isA) { + if (isA) { + return isIntersects(Location.EXTERIOR, Location.INTERIOR) + || isIntersects(Location.EXTERIOR, Location.BOUNDARY); + } + else { + return isIntersects(Location.INTERIOR, Location.EXTERIOR) + || isIntersects(Location.BOUNDARY, Location.EXTERIOR); + } + } + + protected boolean isIntersects(int locA, int locB) { + return intMatrix.get(locA, locB) >= Dimension.P; + } + + public boolean isKnown(int locA, int locB) { + return intMatrix.get(locA, locB) != DIM_UNKNOWN; + } + + public boolean isDimension(int locA, int locB, int dimension) { + return intMatrix.get(locA, locB) == dimension; + } + + public int getDimension(int locA, int locB) { + return intMatrix.get(locA, locB); + } + + /** + * Sets the final value based on the state of the IM. + */ + @Override + public void finish() { + setValue(valueIM()); + } + + /** + * Gets the value of the predicate according to the current + * intersection matrix state. + * + * @return the current predicate value + */ + protected abstract boolean valueIM(); + + public String toString() { + return name() + ": " + intMatrix; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java new file mode 100644 index 0000000000..3e7e6dffb3 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/IntersectionMatrixPattern.java @@ -0,0 +1,63 @@ +/* + * 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; + +/** + * String constants for DE-9IM matrix patterns for topological relationships. + * These can be used with {@link RelateNG#evaluate(org.locationtech.jts.geom.Geometry, String)} + * and {@link RelateNG#relate(org.locationtech.jts.geom.Geometry, org.locationtech.jts.geom.Geometry, String)}. + * + *

    DE-9IM Pattern Matching

    + * Matrix patterns are specified as a 9-character string + * containing the pattern symbols for the DE-9IM 3x3 matrix entries, + * listed row-wise. + * The pattern symbols are: + *
      + *
    • 0 - topological interaction has dimension 0 + *
    • 1 - topological interaction has dimension 1 + *
    • 2 - topological interaction has dimension 2 + *
    • F - no topological interaction + *
    • T - topological interaction of any dimension + *
    • * - any topological interaction is allowed, including none + *
    + * + * @author Martin Davis + * + */ +public class IntersectionMatrixPattern { + + /** + * A DE-9IM pattern to detect whether two polygonal geometries are adjacent along + * an edge, but do not overlap. + */ + public static final String ADJACENT = "F***1****"; + + /** + * A DE-9IM pattern to detect a geometry which properly contains another + * geometry (i.e. which lies entirely in the interior of the first geometry). + */ + public static final String CONTAINS_PROPERLY = "T**FF*FF*"; + + /** + * A DE-9IM pattern to detect if two geometries intersect in their interiors. + * This can be used to determine if a polygonal coverage contains any overlaps + * (although not whether they are correctly noded). + */ + public static final String INTERIOR_INTERSECTS = "T********"; + + /** + * Cannot be instantiated. + */ + private IntersectionMatrixPattern() { + + } +} 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 new file mode 100644 index 0000000000..1172a95a30 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/LinearBoundary.java @@ -0,0 +1,83 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.LineString; + +/** + * Determines the boundary points of a linear geometry, + * using a {@link BoundaryNodeRule}. + * + * @author mdavis + * + */ +class LinearBoundary { + + private Map vertexDegree = new HashMap(); + private boolean hasBoundary; + private BoundaryNodeRule boundaryNodeRule; + + public LinearBoundary(List lines, BoundaryNodeRule bnRule) { + //assert: dim(geom) == 1 + this.boundaryNodeRule = bnRule; + vertexDegree = computeBoundaryPoints(lines); + hasBoundary = checkBoundary(vertexDegree); + } + + private boolean checkBoundary(Map vertexDegree) { + for (int degree : vertexDegree.values()) { + if (boundaryNodeRule.isInBoundary(degree)) { + return true; + } + } + return false; + } + + public boolean hasBoundary() { + return hasBoundary; + } + + public boolean isBoundary(Coordinate pt) { + if (! vertexDegree.containsKey(pt)) + return false; + int degree = vertexDegree.get(pt); + return boundaryNodeRule.isInBoundary(degree); + } + + private static Map computeBoundaryPoints(List lines) { + Map vertexDegree = new HashMap(); + for (LineString line : lines) { + if (line.isEmpty()) + continue; + addEndpoint(line.getCoordinateN(0), vertexDegree); + addEndpoint(line.getCoordinateN(line.getNumPoints() - 1), vertexDegree); + } + return vertexDegree; + } + + private static void addEndpoint(Coordinate p, Map degree) { + int dim = 0; + if (degree.containsKey(p)) { + dim = degree.get(p); + } + dim++; + degree.put(p, dim); + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java new file mode 100644 index 0000000000..f707df0109 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSection.java @@ -0,0 +1,201 @@ +/* + * 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.Comparator; + +import org.locationtech.jts.algorithm.PolygonNodeTopology; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.io.WKTWriter; + +/** + * Represents a computed node along with the incident edges on either side of + * it (if they exist). + * This captures the information about a node in a geometry component + * required to determine the component's contribution to the node topology. + * A node in an area geometry always has edges on both sides of the node. + * A node in a linear geometry may have one or other incident edge missing, if + * the node occurs at an endpoint of the line. + * The edges of an area node are assumed to be provided + * with CW-shell orientation (as per JTS norm). + * This must be enforced by the caller. + * + * @author Martin Davis + * + */ +class NodeSection implements Comparable +{ + /** + * Compares sections by the angle the entering edge makes with the positive X axis. + */ + public static class EdgeAngleComparator implements Comparator { + + @Override + public int compare(NodeSection ns1, NodeSection ns2) { + return PolygonNodeTopology.compareAngle(ns1.nodePt, ns1.getVertex(0), ns2.getVertex(0)); + } + } + + public static boolean isAreaArea(NodeSection a, NodeSection b) { + return a.dimension() == Dimension.A && b.dimension() == Dimension.A; + } + + private boolean isA; + private int dim; + private int id; + private int ringId; + private boolean isNodeAtVertex; + private Coordinate nodePt; + private Coordinate v0; + private Coordinate v1; + private Geometry poly; + + public NodeSection(boolean isA, + int dimension, int id, int ringId, + Geometry poly, boolean isNodeAtVertex, Coordinate v0, Coordinate nodePt, Coordinate v1) { + this.isA = isA; + this.dim = dimension; + this.id = id; + this.ringId = ringId; + this.poly = poly; + this.isNodeAtVertex = isNodeAtVertex; + this.nodePt = nodePt; + this.v0 = v0; + this.v1 = v1; + } + + public Coordinate getVertex(int i) { + return i == 0 ? v0 : v1; + } + + public Coordinate nodePt() { + return nodePt; + } + + public int dimension() { + return dim; + } + + public int id() { + return id; + } + + public int ringId() { + return ringId; + } + + /** + * Gets the polygon this section is part of. + * Will be null if section is not on a polygon boundary. + * + * @return the associated polygon, or null + */ + public Geometry getPolygonal() { + return poly; + } + + public boolean isShell() { + return ringId == 0; + } + + public boolean isArea() { + return dim == Dimension.A; + } + + public boolean isA() { + return isA; + } + + public boolean isSameGeometry(NodeSection ns) { + return isA() == ns.isA(); + } + + public boolean isSamePolygon(NodeSection ns) { + return isA() == ns.isA() && id() == ns.id(); + } + + public boolean isNodeAtVertex() { + return isNodeAtVertex; + } + + public boolean isProper() { + return ! isNodeAtVertex; + } + + public static boolean isProper(NodeSection a, NodeSection b) { + return a.isProper() && b.isProper(); + } + + public String toString() { + String geomName = RelateGeometry.name(isA); + String atVertexInd = isNodeAtVertex ? "-V-" : "---"; + String polyId = id >= 0 ? "[" + id + ":" + ringId + "]" : ""; + return String.format("%s%d%s: %s %s %s", + geomName, dim, polyId, edgeRep(v0, nodePt), atVertexInd, edgeRep(nodePt, v1)); + } + + private String edgeRep(Coordinate p0, Coordinate p1) { + if (p0 == null || p1 == null) + return "null"; + return WKTWriter.toLineString(p0, p1); + } + + /** + * Compare node sections by parent geometry, dimension, element id and ring id, + * and edge vertices. + * Sections are assumed to be at the same node point. + */ + @Override + public int compareTo(NodeSection o) { + // Assert: nodePt.equals2D(o.nodePt()) + + // sort A before B + if (isA != o.isA) { + if (isA) return -1; + return 1; + } + //-- sort on dimensions + int compDim = Integer.compare(dim, o.dim); + if (compDim != 0) return compDim; + + //-- sort on id and ring id + int compId = Integer.compare(id, o.id); + if (compId != 0) return compId; + + int compRingId = Integer.compare(ringId, o.ringId); + if (compRingId != 0) return compRingId; + + //-- sort on edge coordinates + int compV0 = compareWithNull(v0, o.v0); + if (compV0 != 0) return compV0; + + return compareWithNull(v1, o.v1); + } + + private static int compareWithNull(Coordinate v0, Coordinate v1) { + if (v0 == null) { + if (v1 == null) + return 0; + //-- null is lower than non-null + return -1; + } + // v0 is non-null + if (v1 == null) + return 1; + return v0.compareTo(v1); + } + + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java new file mode 100644 index 0000000000..c1a7ea96f0 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/NodeSections.java @@ -0,0 +1,122 @@ +/* + * 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.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +class NodeSections { + + private Coordinate nodePt; + + private List sections = new ArrayList();; + + public NodeSections(Coordinate pt) { + this.nodePt = pt; + } + + public Coordinate getCoordinate() { + return nodePt; + } + + public void addNodeSection(NodeSection e) { +//System.out.println(e); + sections.add(e); + } + + public boolean hasInteractionAB() { + boolean isA = false; + boolean isB = false; + for (NodeSection ns : sections) { + if (ns.isA()) + isA = true; + else + isB = true; + if (isA && isB) + return true; + } + return false; + } + + + public Geometry getPolygonal(boolean isA) { + for (NodeSection ns : sections) { + if (ns.isA() == isA) { + Geometry poly = ns.getPolygonal(); + if (poly != null) + return poly; + } + } + return null; + } + + public RelateNode createNode() { + prepareSections(); + + RelateNode node = new RelateNode(nodePt); + int i = 0; + while (i < sections.size()) { + NodeSection ns = sections.get(i); + //-- if there multiple polygon sections incident at node convert them to maximal-ring structure + if (ns.isArea() && hasMultiplePolygonSections(sections, i)) { + List polySections = collectPolygonSections(sections, i); + List nsConvert = PolygonNodeConverter.convert(polySections); + node.addEdges(nsConvert); + i += polySections.size(); + } + else { + //-- the most common case is a line or a single polygon ring section + node.addEdges(ns); + i += 1; + } + } + return node; + } + + /** + * Sorts the sections so that: + *
      + *
    • lines are before areas + *
    • edges from the same polygon are contiguous + *
    + */ + private void prepareSections() { + sections.sort(null); + //TODO: remove duplicate sections + } + + private static boolean hasMultiplePolygonSections(List sections, int i) { + //-- if last section can only be one + if (i >= sections.size() - 1) + return false; + //-- check if there are at least two sections for same polygon + NodeSection ns = sections.get(i); + NodeSection nsNext = sections.get(i + 1); + return ns.isSamePolygon(nsNext); + } + + private static List collectPolygonSections(List sections, int i) { + List polySections = new ArrayList(); + //-- note ids are only unique to a geometry + NodeSection polySection = sections.get(i); + while (i < sections.size() && + polySection.isSamePolygon(sections.get(i))) { + polySections.add(sections.get(i)); + i++; + } + return polySections; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java new file mode 100644 index 0000000000..079e30023e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/PolygonNodeConverter.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 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.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; + +/** + * Converts the node sections at a polygon node where + * a shell and one or more holes touch, or two or more holes touch. + * This converts the node topological structure from + * the OGC "touching-rings" (AKA "minimal-ring") model to the equivalent "self-touch" + * (AKA "inverted/exverted ring" or "maximal ring") model. + * In the "self-touch" model the converted NodeSection corners enclose areas + * which all lies inside the polygon + * (i.e. they does not enclose hole edges). + * This allows {@link RelateNode} to use simple area-additive semantics + * for adding edges and propagating edge locations. + *

    + * The input node sections are assumed to have canonical orientation + * (CW shells and CCW holes). + * The arrangement of shells and holes must be topologically valid. + * Specifically, the node sections must not cross or be collinear. + *

    + * This supports multiple shell-shell touches + * (including ones containing holes), and hole-hole touches, + * This generalizes the relate algorithm to support + * both the OGC model and the self-touch model. + * + * @author Martin Davis + * @see RelateNode + */ +class PolygonNodeConverter { + + /** + * Converts a list of sections of valid polygon rings + * to have "self-touching" structure. + * There are the same number of output sections as input ones. + * + * @param polySections the original sections + * @return the converted sections + */ + public static List convert(List polySections) { + polySections.sort(new NodeSection.EdgeAngleComparator()); + + //TODO: move uniquing up to caller + List sections = extractUnique(polySections); + if (sections.size() == 1) + return sections; + + //-- find shell section index + int shellIndex = findShell(sections); + if (shellIndex < 0) { + return convertHoles(sections); + } + //-- at least one shell is present. Handle multiple ones if present + List convertedSections = new ArrayList(); + int nextShellIndex = shellIndex; + do { + nextShellIndex = convertShellAndHoles(sections, nextShellIndex, convertedSections); + } while (nextShellIndex != shellIndex); + + return convertedSections; + } + + private static int convertShellAndHoles(List sections, int shellIndex, + List convertedSections) { + NodeSection shellSection = sections.get(shellIndex); + Coordinate inVertex = shellSection.getVertex(0); + int i = next(sections, shellIndex); + NodeSection holeSection = null; + while (! sections.get(i).isShell()) { + holeSection = sections.get(i); + // Assert: holeSection.isShell() = false + Coordinate outVertex = holeSection.getVertex(1); + NodeSection ns = createSection(shellSection, inVertex, outVertex); + convertedSections.add(ns); + + inVertex = holeSection.getVertex(0); + i = next(sections, i); + } + //-- create final section for corner from last hole to shell + Coordinate outVertex = shellSection.getVertex(1); + NodeSection ns = createSection(shellSection, inVertex, outVertex); + convertedSections.add(ns); + return i; + } + + private static List convertHoles(List sections) { + List convertedSections = new ArrayList(); + NodeSection copySection = sections.get(0); + for (int i = 0; i < sections.size(); i++) { + int inext = next(sections, i); + Coordinate inVertex = sections.get(i).getVertex(0); + Coordinate outVertex = sections.get(inext).getVertex(1); + NodeSection ns = createSection(copySection, inVertex, outVertex); + convertedSections.add(ns); + } + return convertedSections; + } + + private static NodeSection createSection(NodeSection ns, Coordinate v0, Coordinate v1) { + return new NodeSection(ns.isA(), + Dimension.A, ns.id(), 0, ns.getPolygonal(), + ns.isNodeAtVertex(), + v0, ns.nodePt(), v1); + } + + private static List extractUnique(List sections) { + List uniqueSections = new ArrayList(); + NodeSection lastUnique = sections.get(0); + uniqueSections.add(lastUnique); + for (NodeSection ns : sections) { + if (0 != lastUnique.compareTo(ns)) { + uniqueSections.add(ns); + lastUnique = ns; + } + } + return uniqueSections; + } + + private static int next(List ns, int i) { + int next = i + 1; + if (next >= ns.size()) + next = 0; + return next; + } + + private static int findShell(List polySections) { + for (int i = 0; i < polySections.size(); i++) { + if (polySections.get(i).isShell()) + return i; + } + return -1; + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java new file mode 100644 index 0000000000..0a2e0bf5fb --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateEdge.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2023 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.List; + +import org.locationtech.jts.algorithm.PolygonNodeTopology; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Position; +import org.locationtech.jts.io.WKTWriter; +import org.locationtech.jts.util.Assert; + +class RelateEdge { + + public static final boolean IS_FORWARD = true; + public static final boolean IS_REVERSE = false; + + public static RelateEdge create(RelateNode node, Coordinate dirPt, boolean isA, int dim, boolean isForward) { + if (dim == Dimension.A) + //-- create an area edge + return new RelateEdge(node, dirPt, isA, isForward); + //-- create line edge + return new RelateEdge(node, dirPt, isA); + } + + public static int findKnownEdgeIndex(List edges, boolean isA) { + for (int i = 0; i < edges.size(); i++) { + RelateEdge e = edges.get(i); + if (e.isKnown(isA)) + return i; + } + return -1; + } + + public static void setAreaInterior(List edges, boolean isA) { + for (RelateEdge e : edges) { + e.setAreaInterior(isA); + } + } + + /** + * The dimension of an input geometry which is not known + */ + public static final int DIM_UNKNOWN = -1; + + /** + * Indicates that the location is currently unknown + */ + private static int LOC_UNKNOWN = Location.NONE; + + private RelateNode node; + private Coordinate dirPt; + + private int aDim = DIM_UNKNOWN; + private int aLocLeft = LOC_UNKNOWN; + private int aLocRight = LOC_UNKNOWN; + private int aLocLine = LOC_UNKNOWN; + + private int bDim = DIM_UNKNOWN; + private int bLocLeft = LOC_UNKNOWN; + private int bLocRight = LOC_UNKNOWN; + private int bLocLine = LOC_UNKNOWN; + + /* + private int aDim = DIM_UNKNOWN; + private int aLocLeft = Location.EXTERIOR; + private int aLocRight = Location.EXTERIOR; + private int aLocLine = Location.EXTERIOR; + + private int bDim = DIM_UNKNOWN; + private int bLocLeft = Location.EXTERIOR; + private int bLocRight = Location.EXTERIOR; + private int bLocLine = Location.EXTERIOR; + */ + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA, boolean isForward) { + this.node = node; + this.dirPt = pt; + setLocationsArea(isA, isForward); + } + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA) { + this.node = node; + this.dirPt = pt; + setLocationsLine(isA); + } + + public RelateEdge(RelateNode node, Coordinate pt, boolean isA, int locLeft, int locRight, int locLine) { + this.node = node; + this.dirPt = pt; + setLocations(isA, locLeft, locRight, locLine); + } + + private void setLocations(boolean isA, int locLeft, int locRight, int locLine) { + if (isA) { + aDim = 2; + aLocLeft = locLeft; + aLocRight = locRight; + aLocLine = locLine; + } + else { + bDim = 2; + bLocLeft = locLeft; + bLocRight = locRight; + bLocLine = locLine; + } + } + + private void setLocationsLine(boolean isA) { + if (isA) { + aDim = 1; + aLocLeft = Location.EXTERIOR; + aLocRight = Location.EXTERIOR; + aLocLine = Location.INTERIOR; + } + else { + bDim = 1; + bLocLeft = Location.EXTERIOR; + bLocRight = Location.EXTERIOR; + bLocLine = Location.INTERIOR; + } + } + + private void setLocationsArea(boolean isA, boolean isForward) { + int locLeft = isForward ? Location.EXTERIOR : Location.INTERIOR; + int locRight = isForward ? Location.INTERIOR : Location.EXTERIOR; + if (isA) { + aDim = 2; + aLocLeft = locLeft; + aLocRight = locRight; + aLocLine = Location.BOUNDARY; + } + else { + bDim = 2; + bLocLeft = locLeft; + bLocRight = locRight; + bLocLine = Location.BOUNDARY; + } + } + + public int compareToEdge(Coordinate edgeDirPt) { + return PolygonNodeTopology.compareAngle(node.getCoordinate(), this.dirPt, edgeDirPt); + } + + public void merge(boolean isA, Coordinate dirPt, int dim, boolean isForward) { + int locEdge = Location.INTERIOR; + int locLeft = Location.EXTERIOR; + int locRight = Location.EXTERIOR; + if (dim == Dimension.A) { + locEdge = Location.BOUNDARY; + locLeft = isForward ? Location.EXTERIOR : Location.INTERIOR; + locRight = isForward ? Location.INTERIOR : Location.EXTERIOR; + } + + if (! isKnown(isA)) { + setDimension(isA, dim); + setOn(isA, locEdge); + setLeft(isA, locLeft); + setRight(isA, locRight); + return; + } + + // Assert: node-dirpt is collinear with node-pt + mergeDimEdgeLoc(isA, locEdge); + mergeSideLocation(isA, Position.LEFT, locLeft); + mergeSideLocation(isA, Position.RIGHT, locRight); + } + + /** + * Area edges override Line edges. + * Merging edges of same dimension is a no-op for + * the dimension and on location. + * But merging an area edge into a line edge + * sets the dimension to A and the location to BOUNDARY. + * + * @param isA + * @param locEdge + */ + private void mergeDimEdgeLoc(boolean isA, int locEdge) { + //TODO: this logic needs work - ie handling A edges marked as Interior + int dim = locEdge == Location.BOUNDARY ? Dimension.A : Dimension.L; + if (dim == Dimension.A && dimension(isA) == Dimension.L) { + setDimension(isA, dim); + setOn(isA, Location.BOUNDARY); + } + } + + private void mergeSideLocation(boolean isA, int pos, int loc) { + int currLoc = location(isA, pos); + //-- INTERIOR takes precedence over EXTERIOR + if (currLoc != Location.INTERIOR) { + setLocation(isA, pos, loc); + } + } + + private void setDimension(boolean isA, int dimension) { + if (isA) { + aDim = dimension; + } + else { + bDim = dimension; + } + } + + public void setLocation(boolean isA, int pos, int loc) { + switch (pos) { + case Position.LEFT: + setLeft(isA, loc); + break; + case Position.RIGHT: + setRight(isA, loc); + break; + case Position.ON: + setOn(isA, loc); + break; + } + } + + public void setAllLocations(boolean isA, int loc) { + setLeft(isA, loc); + setRight(isA, loc); + setOn(isA, loc); + } + + public void setUnknownLocations(boolean isA, int loc) { + if (! isKnown(isA, Position.LEFT)) { + setLocation(isA, Position.LEFT, loc); + } + if (! isKnown(isA, Position.RIGHT)) { + setLocation(isA, Position.RIGHT, loc); + } + if (! isKnown(isA, Position.ON)) { + setLocation(isA, Position.ON, loc); + } + } + + private void setLeft(boolean isA, int loc) { + if (isA) { + aLocLeft = loc; + } + else { + bLocLeft = loc; + } + } + + private void setRight(boolean isA, int loc) { + if (isA) { + aLocRight = loc; + } + else { + bLocRight = loc; + } + } + + private void setOn(boolean isA, int loc) { + if (isA) { + aLocLine = loc; + } + else { + bLocLine = loc; + } + } + + public int location(boolean isA, int position) { + if (isA) { + switch (position) { + case Position.LEFT: return aLocLeft; + case Position.RIGHT: return aLocRight; + case Position.ON: return aLocLine; + } + } + else { + switch (position) { + case Position.LEFT: return bLocLeft; + case Position.RIGHT: return bLocRight; + case Position.ON: return bLocLine; + } + } + Assert.shouldNeverReachHere(); + return LOC_UNKNOWN; + } + + private int dimension(boolean isA) { + return isA ? aDim : bDim; + } + + private boolean isKnown(boolean isA) { + if (isA) + return aDim != DIM_UNKNOWN; + return bDim != DIM_UNKNOWN; + } + + private boolean isKnown(boolean isA, int pos) { + return location(isA, pos) != LOC_UNKNOWN; + } + + public boolean isInterior(boolean isA, int position) { + return location(isA, position) == Location.INTERIOR; + } + + public void setDimLocations(boolean isA, int dim, int loc) { + if (isA) { + aDim = dim; + aLocLeft = loc; + aLocRight = loc; + aLocLine = loc; + } + else { + bDim = dim; + bLocLeft = loc; + bLocRight = loc; + bLocLine = loc; + } + } + + public void setAreaInterior(boolean isA) { + if (isA) { + aLocLeft = Location.INTERIOR; + aLocRight = Location.INTERIOR; + aLocLine = Location.INTERIOR; + } + else { + bLocLeft = Location.INTERIOR; + bLocRight = Location.INTERIOR; + bLocLine = Location.INTERIOR; + } + } + + public String toString() { + return WKTWriter.toLineString(node.getCoordinate(), dirPt) + + " - " + labelString(); + } + + private String labelString() { + StringBuilder buf = new StringBuilder(); + buf.append("A:"); + buf.append(locationString(RelateGeometry.GEOM_A)); + buf.append("/B:"); + buf.append(locationString(RelateGeometry.GEOM_B)); + return buf.toString(); + } + + private String locationString(boolean isA) { + StringBuilder buf = new StringBuilder(); + buf.append(Location.toLocationSymbol(location(isA, Position.LEFT))); + buf.append(Location.toLocationSymbol(location(isA, Position.ON))); + buf.append(Location.toLocationSymbol(location(isA, Position.RIGHT))); + return buf.toString(); + } + +} 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 new file mode 100644 index 0000000000..84d2d57f8e --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateGeometry.java @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2023 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.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +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.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.util.ComponentCoordinateExtracter; +import org.locationtech.jts.geom.util.PointExtracter; + +class RelateGeometry { + + public static final boolean GEOM_A = true; + public static final boolean GEOM_B = false; + + public static String name(boolean isA) { + return isA ? "A" : "B"; + } + + private Geometry geom; + private boolean isPrepared = false; + + private Envelope geomEnv; + private int geomDim = Dimension.FALSE; + private Set uniquePoints; + private BoundaryNodeRule boundaryNodeRule; + private RelatePointLocator locator; + private int elementId = 0; + private boolean hasPoints; + private boolean hasLines; + private boolean hasAreas; + private boolean isLineZeroLen; + private boolean isGeomEmpty; + + public RelateGeometry(Geometry input) { + this(input, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelateGeometry(Geometry input, BoundaryNodeRule bnRule) { + this(input, false, bnRule); + } + + public RelateGeometry(Geometry input, boolean isPrepared, BoundaryNodeRule bnRule) { + this.geom = input; + this.geomEnv = input.getEnvelopeInternal(); + this.isPrepared = isPrepared; + this.boundaryNodeRule = bnRule; + //-- cache geometry metadata + isGeomEmpty = geom.isEmpty(); + geomDim = input.getDimension(); + analyzeDimensions(); + isLineZeroLen = isZeroLengthLine(geom); + } + + private boolean isZeroLengthLine(Geometry geom) { + // avoid expensive zero-length calculation if not linear + if (getDimension() != Dimension.L) + return false; + return isZeroLength(geom); + } + + private void analyzeDimensions() { + if (isGeomEmpty) { + return; + } + if (geom instanceof Point || geom instanceof MultiPoint) { + hasPoints = true; + geomDim = Dimension.P; + return; + } + if (geom instanceof LineString || geom instanceof MultiLineString) { + hasLines = true; + geomDim = Dimension.L; + return; + } + if (geom instanceof Polygon || geom instanceof MultiPolygon) { + hasAreas = true; + geomDim = Dimension.A; + return; + } + //-- analyze a (possibly mixed type) collection + Iterator geomi = new GeometryCollectionIterator(geom); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem.isEmpty()) + continue; + if (elem instanceof Point) { + hasPoints = true; + if (geomDim < Dimension.P) geomDim = Dimension.P; + } + if (elem instanceof LineString) { + hasLines = true; + if (geomDim < Dimension.L) geomDim = Dimension.L; + } + if (elem instanceof Polygon) { + hasAreas = true; + if (geomDim < Dimension.A) geomDim = Dimension.A; + } + } + } + + /** + * Tests if all geometry linear elements are zero-length. + * For efficiency the test avoids computing actual length. + * + * @param geom + * @return + */ + private static boolean isZeroLength(Geometry geom) { + Iterator geomi = new GeometryCollectionIterator(geom); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem instanceof LineString) { + if (! isZeroLength((LineString) elem)) + return false; + } + } + return true; + } + + private static boolean isZeroLength(LineString line) { + if (line.getNumPoints() >= 2) { + Coordinate p0 = line.getCoordinateN(0); + for (int i = 0 ; i < line.getNumPoints(); i++) { + Coordinate pi = line.getCoordinateN(i); + //-- most non-zero-len lines will trigger this right away + if (! p0.equals2D(pi)) + return false; + } + } + return true; + } + + public Geometry getGeometry() { + return geom; + } + + public boolean isPrepared() { + return isPrepared; + } + + public Envelope getEnvelope() { + return geomEnv; + } + + public int getDimension() { + return geomDim; + } + + public boolean hasDimension(int dim) { + switch (dim) { + case Dimension.P: return hasPoints; + case Dimension.L: return hasLines; + case Dimension.A: return hasAreas; + } + return false; + } + + /** + * Gets the actual non-empty dimension of the geometry. + * Zero-length LineStrings are treated as Points. + * + * @return the real (non-empty) dimension + */ + public int getDimensionReal() { + if (isGeomEmpty) return Dimension.FALSE; + if (getDimension() == 1 && isLineZeroLen) + return Dimension.P; + if (hasAreas) return Dimension.A; + if (hasLines) return Dimension.L; + return Dimension.P; + } + + public boolean hasEdges() { + return hasLines || hasAreas; + } + + private RelatePointLocator getLocator() { + if (locator == null) + locator = new RelatePointLocator(geom, isPrepared, boundaryNodeRule); + return locator; + } + + public boolean isNodeInArea(Coordinate nodePt, Geometry parentPolygonal) { + int loc = getLocator().locateNodeWithDim(nodePt, parentPolygonal); + return loc == DimensionLocation.AREA_INTERIOR; + } + + public int locateLineEndWithDim(Coordinate p) { + return getLocator().locateLineEndWithDim(p); + } + + /** + * Locates a vertex of a polygon. + * A vertex of a Polygon or MultiPolygon is on + * the {@link Location#BOUNDARY}. + * But a vertex of an overlapped polygon in a GeometryCollection + * may be in the {@link Location#INTERIOR}. + * + * @param pt the polygon vertex + * @return the location of the vertex + */ + public int locateAreaVertex(Coordinate pt) { + /** + * Can pass a null polygon, because the point is an exact vertex, + * which will be detected as being on the boundary of its polygon + */ + return locateNode(pt, null); + } + + public int locateNode(Coordinate pt, Geometry parentPolygonal) { + return getLocator().locateNode(pt, parentPolygonal); + } + + public int locateWithDim(Coordinate pt) { + int loc = getLocator().locateWithDim(pt); + return loc; + } + + /** + * Indicates whether the geometry requires self-noding + * for correct evaluation of specific spatial predicates. + * Self-noding is required for geometries which may self-cross + * - i.e. lines, and overlapping elements in GeometryCollections. + * Self-noding is not required for polygonal geometries, + * since they can only touch at vertices. + * + * @return true if self-noding is required for this geometry + */ + public boolean isSelfNodingRequired() { + if (geom instanceof Point + || geom instanceof MultiPoint + || geom instanceof Polygon + || geom instanceof MultiPolygon) + return false; + //-- a GC with a single polygon does not need noding + if (hasAreas && geom.getNumGeometries() == 1) + return false; + return true; + } + + /** + * Tests whether the geometry has polygonal topology. + * This is not the case if it is a GeometryCollection + * containing more than one polygon (since they may overlap + * or be adjacent). + * The significance is that polygonal topology allows more assumptions + * about the location of boundary vertices. + * + * @return true if the geometry has polygonal topology + */ + public boolean isPolygonal() { + //TODO: also true for a GC containing one polygonal element (and possibly some lower-dimension elements) + return geom instanceof Polygon + || geom instanceof MultiPolygon; + } + + public boolean isEmpty() { + return isGeomEmpty; + } + + public boolean hasBoundary() { + return getLocator().hasBoundary(); + } + + public Set getUniquePoints() { + //-- will be re-used in prepared mode + if (uniquePoints == null) { + uniquePoints = createUniquePoints(); + } + return uniquePoints; + } + + private Set createUniquePoints() { + //-- only called on P geometries + List pts = ComponentCoordinateExtracter.getCoordinates(geom); + Set set = new HashSet(); + set.addAll(pts); + return set; + } + + public List getEffectivePoints() { + List ptListAll = PointExtracter.getPoints(geom); + + if (getDimensionReal() <= Dimension.P) + return ptListAll; + + //-- only return Points not covered by another element + List ptList = new ArrayList(); + for (Point p : ptListAll) { + if (p.isEmpty()) + continue; + int locDim = locateWithDim(p.getCoordinate()); + if (DimensionLocation.dimension(locDim) == Dimension.P) { + ptList.add(p); + } + } + return ptList; + } + + /** + * Extract RelateSegmentStrings from the geometry which + * intersect a given envelope. + * If the envelope is null all edges are extracted. + * @param geomA + * + * @param env the envelope to extract around (may be null) + * @return a list of RelateSegmentStrings + */ + public List extractSegmentStrings(boolean isA, Envelope env) { + List segStrings = new ArrayList(); + extractSegmentStrings(isA, env, geom, segStrings); + return segStrings; + } + + private void extractSegmentStrings(boolean isA, Envelope env, Geometry geom, List segStrings) { + //-- record if parent is MultiPolygon + MultiPolygon parentPolygonal = null; + if (geom instanceof MultiPolygon) { + parentPolygonal = (MultiPolygon) geom; + } + + for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry g = geom.getGeometryN(i); + if (g instanceof GeometryCollection) { + extractSegmentStrings(isA, env, g, segStrings); + } + else { + extractSegmentStringsFromAtomic(isA, g, parentPolygonal, env, segStrings); + } + } + } + + private void extractSegmentStringsFromAtomic(boolean isA, Geometry geom, MultiPolygon parentPolygonal, Envelope env, + List segStrings) { + if (geom.isEmpty()) + return; + boolean doExtract = env == null || env.intersects(geom.getEnvelopeInternal()); + if (! doExtract) + return; + + elementId++; + if (geom instanceof LineString) { + RelateSegmentString ss = RelateSegmentString.createLine(geom.getCoordinates(), isA, elementId, this); + segStrings.add(ss); + } + else if (geom instanceof Polygon) { + Polygon poly = (Polygon) geom; + Geometry parentPoly = parentPolygonal != null ? parentPolygonal : poly; + extractRingToSegmentString(isA, poly.getExteriorRing(), 0, env, parentPoly, segStrings); + for (int i = 0; i < poly.getNumInteriorRing(); i++) { + extractRingToSegmentString(isA, poly.getInteriorRingN(i), i+1, env, parentPoly, segStrings); + } + } + } + + private void extractRingToSegmentString(boolean isA, LinearRing ring, int ringId, Envelope env, + Geometry parentPoly, List segStrings) { + if (ring.isEmpty()) + return; + if (env != null && ! env.intersects(ring.getEnvelopeInternal())) + return; + + //-- orient the points if required + boolean requireCW = ringId == 0; + Coordinate[] pts = orient(ring.getCoordinates(), requireCW); + RelateSegmentString ss = RelateSegmentString.createRing(pts, isA, elementId, ringId, parentPoly, this); + segStrings.add(ss); + } + + public static Coordinate[] orient(Coordinate[] pts, boolean orientCW) { + boolean isFlipped = orientCW == Orientation.isCCW(pts); + if (isFlipped) { + pts = pts.clone(); + CoordinateArrays.reverse(pts); + } + return pts; + } + + public String toString() { + return geom.toString(); + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java new file mode 100644 index 0000000000..6a456b1b73 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateMatrixPredicate.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.IntersectionMatrix; + +/** + * Evaluates the full relate {@link IntersectionMatrix}. + * @author mdavis + * + */ +class RelateMatrixPredicate extends IMPredicate +{ + public RelateMatrixPredicate() { + } + + public String name() { return "relateMatrix"; } + + @Override + public boolean requireInteraction() { + //-- ensure entire matrix is computed + return false; + } + + @Override + public boolean isDetermined() { + //-- ensure entire matrix is computed + return false; + } + + @Override + public boolean valueIM() { + //-- indicates full matrix is being evaluated + return false; + + } + + /** + * Gets the current state of the IM matrix (which may only be partially complete). + * + * @return the IM matrix + */ + public IntersectionMatrix getIM() { + return intMatrix; + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..97af365712 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNG.java @@ -0,0 +1,553 @@ +/* + * 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 static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_A; +import static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_B; + +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollectionIterator; +import org.locationtech.jts.geom.IntersectionMatrix; +import org.locationtech.jts.geom.LineString; +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.MCIndexSegmentSetMutualIntersector; +import org.locationtech.jts.operation.relate.RelateOp; + +/** + * Computes the value of topological predicates between two geometries based on the + * Dimensionally-Extended 9-Intersection Model (DE-9IM). + * Standard and custom topological predicates are provided by {@link RelatePredicate}. + *

    + * The RelateNG algorithm has the following capabilities: + *

      + *
    1. Efficient short-circuited evaluation of topological predicates + * (including matching custom DE-9IM matrix patterns) + *
    2. Optimized repeated evaluation of predicates against a single geometry + * via cached spatial indexes (AKA "prepared mode") + *
    3. Robust computation (only point-local topology is required, + * so invalid geometry topology does not cause failures) + *
    4. {@link GeometryCollection} inputs containing mixed types and overlapping polygons + * are supported, using union semantics. + *
    5. Zero-length LineStrings are treated as being topologically identical to Points. + *
    6. Support for {@link BoundaryNodeRule}s. + *
    + * + * See {@link IntersectionMatrixPattern} for a description of DE-9IM patterns. + * + * If not specified, the standard {@link BoundaryNodeRule#MOD2_BOUNDARY_RULE} is used. + * + * RelateNG operates in 2D only; it ignores any Z ordinates. + * + * This implementation replaces {@link RelateOp} and {@link PreparedGeometry}. + * + *

    FUTURE WORK

    + *
      + *
    • Support for a distance tolerance to provide "approximate" predicate evaluation + *
    + * + * + * @author Martin Davis + * + * @see RelateOp + * @see PreparedGeometry + */ +public class RelateNG +{ + + /** + * Tests whether the topological relationship between two geometries + * satisfies a topological predicate. + * + * @param a the A input geometry + * @param b the A input geometry + * @param pred the topological predicate + * @return true if the topological relationship is satisfied + */ + public static boolean relate(Geometry a, Geometry b, TopologyPredicate pred) { + RelateNG rng = new RelateNG(a, false); + return rng.evaluate(b, pred); + } + + /** + * Tests whether the topological relationship between two geometries + * satisfies a topological predicate, + * using a given {@link BoundaryNodeRule}. + * + * @param a the A input geometry + * @param b the A input geometry + * @param pred the topological predicate + * @param bnRule the Boundary Node Rule to use + * @return true if the topological relationship is satisfied + */ + public static boolean relate(Geometry a, Geometry b, TopologyPredicate pred, BoundaryNodeRule bnRule) { + RelateNG rng = new RelateNG(a, false, bnRule); + return rng.evaluate(b, pred); + } + + /** + * Tests whether the topological relationship to a geometry + * matches a DE-9IM matrix pattern. + * + * @param a the A input geometry + * @param b the A input geometry + * @param imPattern the DE-9IM pattern to match + * @return true if the geometries relationship matches the DE-9IM pattern + * + * @see IntersectionMatrixPattern + */ + public static boolean relate(Geometry a, Geometry b, String imPattern) { + RelateNG rng = new RelateNG(a, false); + return rng.evaluate(b, imPattern); + } + + /** + * Computes the DE-9IM matrix + * for the topological relationship between two geometries. + * + * @param a the A input geometry + * @param b the A input geometry + * @return the DE-9IM matrix for the topological relationship + */ + public static IntersectionMatrix relate(Geometry a, Geometry b) { + RelateNG rng = new RelateNG(a, false); + return rng.evaluate(b); + } + + /** + * Computes the DE-9IM matrix + * for the topological relationship between two geometries. + * + * @param a the A input geometry + * @param b the A input geometry + * @param bnRule the Boundary Node Rule to use + * @return the DE-9IM matrix for the relationship + */ + public static IntersectionMatrix relate(Geometry a, Geometry b, BoundaryNodeRule bnRule) { + RelateNG rng = new RelateNG(a, false, bnRule); + return rng.evaluate(b); + } + + /** + * Creates a prepared RelateNG instance to optimize the + * evaluation of relationships against a single geometry. + * + * @param a the A input geometry + * @return a prepared instance + */ + public static RelateNG prepare(Geometry a) { + return new RelateNG(a, true); + } + + /** + * Creates a prepared RelateNG instance to optimize the + * computation of predicates against a single geometry, + * using a given {@link BoundaryNodeRule}. + * + * @param a the A input geometry + * @param bnRule the required BoundaryNodeRule + * @return a prepared instance + */ + public static RelateNG prepare(Geometry a, BoundaryNodeRule bnRule) { + return new RelateNG(a, true, bnRule); + } + + private BoundaryNodeRule boundaryNodeRule; + private RelateGeometry geomA; + private MCIndexSegmentSetMutualIntersector edgeMutualInt; + + private RelateNG(Geometry inputA, boolean isPrepared) { + this(inputA, isPrepared, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + private RelateNG(Geometry inputA, boolean isPrepared, BoundaryNodeRule bnRule) { + this.boundaryNodeRule = bnRule; + geomA = new RelateGeometry(inputA, isPrepared, boundaryNodeRule); + } + + /** + * Computes the DE-9IM matrix for the topological relationship to a geometry. + * + * @param b the B geometry to test against + * @return the DE-9IM matrix + */ + public IntersectionMatrix evaluate(Geometry b) { + RelateMatrixPredicate rel = new RelateMatrixPredicate(); + evaluate(b, rel); + return rel.getIM(); + } + + /** + * Tests whether the topological relationship to a geometry + * matches a DE-9IM matrix pattern. + * + * @param b the B geometry to test against + * @param imPattern the DE-9IM pattern to match + * @return true if the geometries' topological relationship matches the DE-9IM pattern + * + * @see IntersectionMatrixPattern + */ + public boolean evaluate(Geometry b, String imPattern) { + return evaluate(b, RelatePredicate.matches(imPattern)); + } + + /** + * Tests whether the topological relationship to a geometry + * satisfies a topology predicate. + * + * @param b the B geometry to test against + * @param predicate the topological predicate + * @return true if the predicate is satisfied + */ + public boolean evaluate(Geometry b, TopologyPredicate predicate) { + //-- fast envelope checks + if (! hasRequiredEnvelopeInteraction(b, predicate)) { + return false; + } + + RelateGeometry geomB = new RelateGeometry(b, boundaryNodeRule); + + if (geomA.isEmpty() && geomB.isEmpty()) { + //TODO: what if predicate is disjoint? Perhaps use result on disjoint envs? + return finishValue(predicate); + } + int dimA = geomA.getDimensionReal(); + int dimB = geomB.getDimensionReal(); + + //-- check if predicate is determined by dimension or envelope + predicate.init(dimA, dimB); + if (predicate.isKnown()) + return finishValue(predicate); + + predicate.init(geomA.getEnvelope(), geomB.getEnvelope()); + if (predicate.isKnown()) + return finishValue(predicate); + + TopologyComputer topoComputer = new TopologyComputer(predicate, geomA, geomB); + + //-- optimized P/P evaluation + if (dimA == Dimension.P && dimB == Dimension.P) { + computePP(geomB, topoComputer); + topoComputer.finish(); + return topoComputer.getResult(); + } + + //-- test points against (potentially) indexed geometry first + computeAtPoints(geomB, GEOM_B, geomA, topoComputer); + if (topoComputer.isResultKnown()) { + return topoComputer.getResult(); + } + computeAtPoints(geomA, GEOM_A, geomB, topoComputer); + if (topoComputer.isResultKnown()) { + return topoComputer.getResult(); + } + + if (geomA.hasEdges() && geomB.hasEdges()) { + computeAtEdges(geomB, topoComputer); + } + + //-- after all processing, set remaining unknown values in IM + topoComputer.finish(); + return topoComputer.getResult(); + } + + private boolean hasRequiredEnvelopeInteraction(Geometry b, TopologyPredicate predicate) { + Envelope envB = b.getEnvelopeInternal(); + boolean isInteracts = false; + if (predicate.requireCovers(GEOM_A)) { + if (! geomA.getEnvelope().covers(envB)) { + return false; + } + isInteracts = true; + } + else if (predicate.requireCovers(GEOM_B)) { + if (! envB.covers(geomA.getEnvelope())) { + return false; + } + isInteracts = true; + } + if (! isInteracts + && predicate.requireInteraction() + && ! geomA.getEnvelope().intersects(envB)) { + return false; + } + return true; + } + + private boolean finishValue(TopologyPredicate predicate) { + predicate.finish(); + return predicate.value(); + } + + /** + * An optimized algorithm for evaluating P/P cases. + * It tests one point set against the other. + * + * @param geomB + * @param topoComputer + */ + private void computePP(RelateGeometry geomB, TopologyComputer topoComputer) { + Set ptsA = geomA.getUniquePoints(); + //TODO: only query points in interaction extent? + Set ptsB = geomB.getUniquePoints(); + + int numBinA = 0; + for (Coordinate ptB : ptsB) { + if (ptsA.contains(ptB)) { + numBinA++; + topoComputer.addPointOnPointInterior(ptB); + } + else { + topoComputer.addPointOnPointExterior(GEOM_B, ptB); + } + if (topoComputer.isResultKnown()) { + return; + } + } + /** + * If number of matched B points is less than size of A, + * there must be at least one A point in the exterior of B + */ + if (numBinA < ptsA.size()) { + //TODO: determine actual exterior point? + topoComputer.addPointOnPointExterior(GEOM_A, null); + } + } + + private void computeAtPoints(RelateGeometry geom, boolean isA, + RelateGeometry geomTarget, TopologyComputer topoComputer) { + + boolean isResultKnown = false; + isResultKnown = computePoints(geom, isA, geomTarget, topoComputer); + if (isResultKnown) + return; + + /** + * Performance optimization: only check points against target + * if it has areas OR if the predicate requires checking for + * exterior interaction. + * In particular, this avoids testing line ends against lines + * for the intersects predicate (since these are checked + * during segment/segment intersection checking anyway). + * Checking points against areas is necessary, since the input + * linework is disjoint if one input lies wholly inside an area, + * so segment intersection checking is not sufficient. + */ + boolean checkDisjointPoints = geomTarget.hasDimension(Dimension.A) + || topoComputer.isExteriorCheckRequired(isA); + if (! checkDisjointPoints) + return; + + isResultKnown = computeLineEnds(geom, isA, geomTarget, topoComputer); + if (isResultKnown) + return; + + computeAreaVertex(geom, isA, geomTarget, topoComputer); + } + + private boolean computePoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, + TopologyComputer topoComputer) { + if (! geom.hasDimension(Dimension.P)) { + return false; + } + + List points = geom.getEffectivePoints(); + for (Point point : points) { + //TODO: exit when all possible target locations (E,I,B) have been found? + if (point.isEmpty()) + continue; + + Coordinate pt = point.getCoordinate(); + computePoint(isA, pt, geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + } + return false; + } + + private void computePoint(boolean isA, Coordinate pt, RelateGeometry geomTarget, TopologyComputer topoComputer) { + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = DimensionLocation.location(locDimTarget); + int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); + topoComputer.addPointOnGeometry(isA, locTarget, dimTarget, pt); + } + + private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, + TopologyComputer topoComputer) { + if (! geom.hasDimension(Dimension.L)) { + return false; + } + + boolean hasExteriorIntersection = false; + Iterator geomi = new GeometryCollectionIterator(geom.getGeometry()); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem.isEmpty()) + continue; + + if (elem instanceof LineString) { + //-- once an intersection with target exterior is recorded, skip further known-exterior points + if (hasExteriorIntersection + && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) + continue; + + LineString line = (LineString) elem; + Coordinate e0 = line.getCoordinateN(0); + hasExteriorIntersection |= computeLineEnd(geom, isA, e0, geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + + if (! line.isClosed()) { + Coordinate e1 = line.getCoordinateN(line.getNumPoints() - 1); + hasExteriorIntersection |= computeLineEnd(geom, isA, e1, geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + } + //TODO: break when all possible locations have been found? + } + } + return false; + } + + /** + * Compute the topology of a line endpoint. + * Also reports if the line end is in the exterior of the target geometry, + * to optimize testing multiple exterior endpoints. + * + * @param geom + * @param isA + * @param pt + * @param geomTarget + * @param topoComputer + * @return true if the line endpoint is in the exterior of the target + */ + private boolean computeLineEnd(RelateGeometry geom, boolean isA, Coordinate pt, + RelateGeometry geomTarget, TopologyComputer topoComputer) { + int locDimLineEnd = geom.locateLineEndWithDim(pt); + int dimLineEnd = DimensionLocation.dimension(locDimLineEnd, topoComputer.getDimension(isA)); + //-- skip line ends which are in a GC area + if (dimLineEnd != Dimension.L) + return false; + int locLineEnd = DimensionLocation.location(locDimLineEnd); + + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = DimensionLocation.location(locDimTarget); + int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); + topoComputer.addLineEndOnGeometry(isA, locLineEnd, locTarget, dimTarget, pt); + return locTarget == Location.EXTERIOR; + } + + private boolean computeAreaVertex(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) { + if (! geom.hasDimension(Dimension.A)) { + return false; + } + //-- evaluate for line and area targets only, since points are handled in the reverse direction + if (geomTarget.getDimension() < Dimension.L) + return false; + + boolean hasExteriorIntersection = false; + Iterator geomi = new GeometryCollectionIterator(geom.getGeometry()); + while (geomi.hasNext()) { + Geometry elem = (Geometry) geomi.next(); + if (elem.isEmpty()) + continue; + + if (elem instanceof Polygon) { + //-- once an intersection with target exterior is recorded, skip further known-exterior points + if (hasExteriorIntersection + && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope())) + continue; + + Polygon poly = (Polygon) elem; + hasExteriorIntersection |= computeAreaVertex(geom, isA, poly.getExteriorRing(), geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + for (int j = 0; j < poly.getNumInteriorRing(); j++) { + hasExteriorIntersection |= computeAreaVertex(geom, isA, poly.getInteriorRingN(j), geomTarget, topoComputer); + if (topoComputer.isResultKnown()) { + return true; + } + } + } + } + return false; + } + + private boolean computeAreaVertex(RelateGeometry geom, boolean isA, LinearRing ring, RelateGeometry geomTarget, TopologyComputer topoComputer) { + //TODO: use extremal (highest) point to ensure one is on boundary of polygon cluster + Coordinate pt = ring.getCoordinate(); + + int locArea = geom.locateAreaVertex(pt); + int locDimTarget = geomTarget.locateWithDim(pt); + int locTarget = DimensionLocation.location(locDimTarget); + int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA)); + topoComputer.addAreaVertex(isA, locArea, locTarget, dimTarget, pt); + return locTarget == Location.EXTERIOR; + } + + private void computeAtEdges(RelateGeometry geomB, TopologyComputer topoComputer) { + Envelope envInt = geomA.getEnvelope().intersection(geomB.getEnvelope()); + if (envInt.isNull()) + return; + + List edgesB = geomB.extractSegmentStrings(GEOM_B, envInt); + EdgeSegmentIntersector intersector = new EdgeSegmentIntersector(topoComputer); + + if (topoComputer.isSelfNodingRequired()) { + computeEdgesAll(edgesB, envInt, intersector); + } + else { + computeEdgesMutual(edgesB, envInt, intersector); + } + if (topoComputer.isResultKnown()) { + return; + } + + topoComputer.evaluateNodes(); + } + + private void computeEdgesAll(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { + //TODO: find a way to reuse prepared index? + List edgesA = geomA.extractSegmentStrings(GEOM_A, envInt); + + EdgeSetIntersector edgeInt = new EdgeSetIntersector(edgesA, edgesB, envInt); + edgeInt.process(intersector); + } + + private void computeEdgesMutual(List edgesB, Envelope envInt, EdgeSegmentIntersector intersector) { + //-- in prepared mode the A edge index is reused + if (edgeMutualInt == null) { + Envelope envExtract = geomA.isPrepared() ? null : envInt; + List edgesA = geomA.extractSegmentStrings(GEOM_A, envExtract); + edgeMutualInt = new MCIndexSegmentSetMutualIntersector(edgesA, envExtract); + } + + edgeMutualInt.process(edgesB, intersector); + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java new file mode 100644 index 0000000000..7c3ff91f8b --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateNode.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2023 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.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Position; +import org.locationtech.jts.io.WKTWriter; + +class RelateNode { + + private Coordinate nodePt; + + /** + * A list of the edges around the node in CCW order, + * ordered by their CCW angle with the positive X-axis. + */ + private ArrayList edges = new ArrayList(); + + public RelateNode(Coordinate pt) { + this.nodePt = pt; + } + + public Coordinate getCoordinate() { + return nodePt; + } + + public List getEdges() { + return edges; + } + + + public void addEdges(List nss) { + for (NodeSection ns : nss) { + addEdges(ns); + } + } + + public void addEdges(NodeSection ns) { + //Debug.println("Adding NS: " + ns); + switch (ns.dimension()) { + case Dimension.L: + addLineEdge(ns.isA(), ns.getVertex(0)); + addLineEdge(ns.isA(), ns.getVertex(1)); + break; + case Dimension.A: + //-- assumes node edges have CW orientation (as per JTS norm) + //-- entering edge - interior on L + RelateEdge e0 = addAreaEdge(ns.isA(), ns.getVertex(0), false); + //-- exiting edge - interior on R + RelateEdge e1 = addAreaEdge(ns.isA(), ns.getVertex(1), true); + + int index0 = edges.indexOf(e0); + int index1 = edges.indexOf(e1); + updateEdgesInArea(ns.isA(), index0, index1); + updateIfAreaPrev(ns.isA(), index0); + updateIfAreaNext(ns.isA(), index1); + } + } + + private void updateEdgesInArea(boolean isA, int indexFrom, int indexTo) { + int index = nextIndex(edges, indexFrom); + while (index != indexTo) { + RelateEdge edge = edges.get(index); + edge.setAreaInterior(isA); + index = nextIndex(edges, index); + } + } + + private void updateIfAreaPrev(boolean isA, int index) { + int indexPrev = prevIndex(edges, index); + RelateEdge edgePrev = edges.get(indexPrev); + if (edgePrev.isInterior(isA, Position.LEFT)) { + RelateEdge edge = edges.get(index); + edge.setAreaInterior(isA); + } + } + + private void updateIfAreaNext(boolean isA, int index) { + int indexNext = nextIndex(edges, index); + RelateEdge edgeNext = edges.get(indexNext); + if (edgeNext.isInterior(isA, Position.RIGHT)) { + RelateEdge edge = edges.get(index); + edge.setAreaInterior(isA); + } + } + + private RelateEdge addLineEdge(boolean isA, Coordinate dirPt) { + return addEdge(isA, dirPt, Dimension.L, false); + } + + private RelateEdge addAreaEdge(boolean isA, Coordinate dirPt, boolean isForward) { + return addEdge(isA, dirPt, Dimension.A, isForward); + } + + /** + * Adds or merges an edge to the node. + * + * @param isA + * @param dirPt + * @param dim dimension of the geometry element containing the edge + * @param isForward the direction of the edge + * + * @return the created or merged edge for this point + */ + private RelateEdge addEdge(boolean isA, Coordinate dirPt, int dim, boolean isForward) { + //-- check for well-formed edge - skip null or zero-len input + if (dirPt == null) + return null; + if (nodePt.equals2D(dirPt)) + return null; + + int insertIndex = -1; + for (int i = 0; i < edges.size(); i++) { + RelateEdge e = edges.get(i); + int comp = e.compareToEdge(dirPt); + if (comp == 0) { + e.merge(isA, dirPt, dim, isForward); + return e; + } + if (comp == 1 ) { + //-- found further edge, so insert a new edge at this position + insertIndex = i; + break; + } + } + //-- add a new edge + RelateEdge e = RelateEdge.create(this, dirPt, isA, dim, isForward); + if (insertIndex < 0) { + //-- add edge at end of list + edges.add(e); + } + else { + //-- add edge before higher edge found + edges.add(insertIndex, e); + } + return e; + } + + /** + * Computes the final topology for the edges around this node. + * Although nodes lie on the boundary of areas or the interior of lines, + * in a mixed GC they may also lie in the interior of an area. + * This changes the locations of the sides and line to Interior. + * + * @param isAreaInteriorA true if the node is in the interior of A + * @param isAreaInteriorB true if the node is in the interior of B + */ + public void finish(boolean isAreaInteriorA, boolean isAreaInteriorB) { + +//Debug.println("finish Node."); +//Debug.println("Before: " + this); + + finishNode(RelateGeometry.GEOM_A, isAreaInteriorA); + finishNode(RelateGeometry.GEOM_B, isAreaInteriorB); +//Debug.println("After: " + this); + } + + private void finishNode(boolean isA, boolean isAreaInterior) { + if (isAreaInterior) { + RelateEdge.setAreaInterior(edges, isA); + } + else { + int startIndex = RelateEdge.findKnownEdgeIndex(edges, isA); + //-- only interacting nodes are finished, so this should never happen + //Assert.isTrue(startIndex >= 0l, "Node at "+ nodePt + "does not have AB interaction"); + propagateSideLocations(isA, startIndex); + } + } + + private void propagateSideLocations(boolean isA, int startIndex) { + int currLoc = edges.get(startIndex).location(isA, Position.LEFT); + //-- edges are stored in CCW order + int index = nextIndex(edges, startIndex); + while (index != startIndex) { + RelateEdge e = edges.get(index); + e.setUnknownLocations(isA, currLoc); + currLoc = e.location(isA, Position.LEFT); + index = nextIndex(edges, index); + } + } + + private static int prevIndex(ArrayList list, int index) { + if (index > 0) + return index - 1; + //-- index == 0 + return list.size() - 1; + } + + private static int nextIndex(List list, int i) { + if (i >= list.size() - 1) { + return 0; + } + return i + 1; + } + + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("Node[" + WKTWriter.toPoint(nodePt) + "]:"); + buf.append("\n"); + for (RelateEdge e : edges) { + buf.append(e.toString()); + buf.append("\n"); + } + return buf.toString(); + } + + public boolean hasExteriorEdge(boolean isA) { + for (RelateEdge e : edges) { + if (Location.EXTERIOR == e.location(isA, Position.LEFT) + || Location.EXTERIOR == e.location(isA, Position.RIGHT)) { + return true; + } + } + return false; + } +} 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..c6f63680f0 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePointLocator.java @@ -0,0 +1,347 @@ +/* + * 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.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.algorithm.PointLocation; +import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; +import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; +import org.locationtech.jts.algorithm.locate.SimplePointInAreaLocator; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Location; +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. + * The dimension of the containing geometry element is also determined. + * 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. + *

    + * Union semantics for GeometryCollections has the following behaviours: + *

      + *
    1. For a mixed-dimension (heterogeneous) collection + * a point may lie on two geometry elements with different dimensions. + * In this case the location on the largest-dimension element is reported. + *
    2. For a collection with overlapping or adjacent polygons, + * points on polygon element boundaries may lie in the effective interior + * of the collection geometry. + *
    + * Prepared mode is supported via cached spatial indexes. + *

    + * Supports specifying the {@link BoundaryNodeRule} to use + * for line endpoints. + * + * @author Martin Davis + * + */ +class RelatePointLocator { + + private Geometry geom; + private boolean isPrepared = false; + private BoundaryNodeRule boundaryRule; + private AdjacentEdgeLocator adjEdgeLocator; + private Set points; + private List lines; + private List polygons; + private PointOnGeometryLocator[] polyLocator; + private LinearBoundary lineBoundary; + private boolean isEmpty; + + public RelatePointLocator(Geometry geom) { + this(geom, false, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE); + } + + public RelatePointLocator(Geometry geom, boolean isPrepared, BoundaryNodeRule bnRule) { + this.geom = geom; + this.isPrepared = isPrepared; + this.boundaryRule = bnRule; + init(geom); + } + + private void init(Geometry geom) { + //-- cache empty status, since may be checked many times + isEmpty = geom.isEmpty(); + extractElements(geom); + + if (lines != null) { + lineBoundary = new LinearBoundary(lines, boundaryRule); + } + + if (polygons != null) { + polyLocator = isPrepared + ? new IndexedPointInAreaLocator[polygons.size()] + : new SimplePointInAreaLocator[polygons.size()]; + } + } + + public boolean hasBoundary() { + return lineBoundary.hasBoundary(); + } + + private void extractElements(Geometry geom) { + if (geom.isEmpty()) + return; + + if (geom instanceof Point) { + addPoint((Point) geom); + } + else if (geom instanceof LineString) { + addLine((LineString) geom); + } + else if (geom instanceof Polygon + || geom instanceof MultiPolygon) { + addPolygonal(geom); + } + else if (geom instanceof GeometryCollection){ + for (int i = 0; i < geom.getNumGeometries(); i++) { + Geometry g = geom.getGeometryN(i); + extractElements(g); + } + } + } + + private void addPoint(Point pt) { + if (points == null) { + points = new HashSet(); + } + points.add(pt.getCoordinate()); + } + + private void addLine(LineString line) { + if (lines == null) { + lines = new ArrayList(); + } + lines.add(line); + } + + private void addPolygonal(Geometry polygonal) { + if (polygons == null) { + polygons = new ArrayList(); + } + polygons.add(polygonal); + } + + public int locate(Coordinate p) { + return DimensionLocation.location(locateWithDim(p)); + } + + /** + * Locates a line endpoint, as a {@link DimensionLocation}. + * In a mixed-dim GC, the line end point may also lie in an area. + * In this case the area location is reported. + * Otherwise, the dimLoc is either LINE_BOUNDARY + * or LINE_INTERIOR, depending on the endpoint valence + * and the BoundaryNodeRule in place. + * + * @param p the line end point to locate + * @return the dimension and location of the line end point + */ + public int locateLineEndWithDim(Coordinate p) { + //-- if a GC with areas, check for point on area + if (polygons != null) { + int locPoly = locateOnPolygons(p, false, null); + if (locPoly != Location.EXTERIOR) + return DimensionLocation.locationArea(locPoly); + } + //-- not in area, so return line end location + return lineBoundary.isBoundary(p) + ? DimensionLocation.LINE_BOUNDARY + : DimensionLocation.LINE_INTERIOR; + } + + /** + * Locates a point which is known to be a node of the geometry + * (i.e. a vertex or on an edge). + * + * @param p the node point to locate + * @param parentPolygonal the polygon the point is a node of + * @return the location of the node point + */ + public int locateNode(Coordinate p, Geometry parentPolygonal) { + return DimensionLocation.location(locateNodeWithDim(p, parentPolygonal)); + } + + /** + * Locates a point which is known to be a node of the geometry, + * as a {@link DimensionLocation}. + * + * @param p the point to locate + * @param parentPolygonal the polygon the point is a node of + * @return the dimension and location of the point + */ + public int locateNodeWithDim(Coordinate p, Geometry parentPolygonal) { + return locateWithDim(p, true, parentPolygonal); + } + + /** + * Computes the topological location ({@link Location}) of a single point + * in a Geometry, as well as the dimension of the geometry element the point + * is located in (if not in the Exterior). + * It handles both single-element and multi-element Geometries. + * The algorithm for multi-part Geometries + * takes into account the SFS Boundary Determination Rule. + * + * @param p the point to locate + * @return the {@link Location} of the point relative to the input Geometry + */ + public int locateWithDim(Coordinate p) { + return locateWithDim(p, false, null); + } + + /** + * Computes the topological location ({@link Location}) of a single point + * in a Geometry, as well as the dimension of the geometry element the point + * is located in (if not in the Exterior). + * It handles both single-element and multi-element Geometries. + * The algorithm for multi-part Geometries + * takes into account the SFS Boundary Determination Rule. + * + * @param p the coordinate to locate + * @param isNode whether the coordinate is a node (on an edge) of the geometry + * @param polygon + * @return the {@link Location} of the point relative to the input Geometry + */ + private int locateWithDim(Coordinate p, boolean isNode, Geometry parentPolygonal) + { + if (isEmpty) return DimensionLocation.EXTERIOR; + + /** + * In a polygonal geometry a node must be on the boundary. + * (This is not the case for a mixed collection, since + * the node may be in the interior of a polygon.) + */ + if (isNode && (geom instanceof Polygon || geom instanceof MultiPolygon)) + return DimensionLocation.AREA_BOUNDARY; + + int dimLoc = computeDimLocation(p, isNode, parentPolygonal); + return dimLoc; + } + + private int computeDimLocation(Coordinate p, boolean isNode, Geometry parentPolygonal) { + //-- check dimensions in order of precedence + if (polygons != null) { + int locPoly = locateOnPolygons(p, isNode, parentPolygonal); + if (locPoly != Location.EXTERIOR) + return DimensionLocation.locationArea(locPoly); + } + if (lines != null) { + int locLine = locateOnLines(p, isNode); + if (locLine != Location.EXTERIOR) + return DimensionLocation.locationLine(locLine); + } + if (points != null) { + int locPt = locateOnPoints(p); + if (locPt != Location.EXTERIOR) + return DimensionLocation.locationPoint(locPt); + } + return DimensionLocation.EXTERIOR; + } + + private int locateOnPoints(Coordinate p) { + if (points.contains(p)) { + return Location.INTERIOR; + } + return Location.EXTERIOR; + } + + private int locateOnLines(Coordinate p, boolean isNode) { + if (lineBoundary != null + && lineBoundary.isBoundary(p)) { + return Location.BOUNDARY; + } + //-- must be on line, in interior + if (isNode) + return Location.INTERIOR; + + //TODO: index the lines + for (LineString line : lines) { + //-- have to check every line, since any/all may contain point + int loc = locateOnLine(p, isNode, line); + if (loc != Location.EXTERIOR) + return loc; + //TODO: minor optimization - some BoundaryNodeRules can short-circuit + } + return Location.EXTERIOR; + } + + private int locateOnLine(Coordinate p, boolean isNode, LineString l) + { + // bounding-box check + if (! l.getEnvelopeInternal().intersects(p)) + return Location.EXTERIOR; + + CoordinateSequence seq = l.getCoordinateSequence(); + if (PointLocation.isOnLine(p, seq)) { + return Location.INTERIOR; + } + return Location.EXTERIOR; + } + + private int locateOnPolygons(Coordinate p, boolean isNode, Geometry parentPolygonal) { + int numBdy = 0; + //TODO: use a spatial index on the polygons + for (int i = 0; i < polygons.size(); i++) { + int loc = locateOnPolygonal(p, isNode, parentPolygonal, i); + if (loc == Location.INTERIOR) { + return Location.INTERIOR; + } + if (loc == Location.BOUNDARY) { + numBdy += 1; + } + } + if (numBdy == 1) { + return Location.BOUNDARY; + } + //-- check for point lying on adjacent boundaries + else if (numBdy > 1) { + if (adjEdgeLocator == null) { + adjEdgeLocator = new AdjacentEdgeLocator(geom); + } + return adjEdgeLocator.locate(p); + } + return Location.EXTERIOR; + } + + private int locateOnPolygonal(Coordinate p, boolean isNode, Geometry parentPolygonal, int index) { + Geometry polygonal = polygons.get(index); + if (isNode && parentPolygonal == polygonal) { + return Location.BOUNDARY; + } + PointOnGeometryLocator locator = getLocator(index); + return locator.locate(p); + } + + private PointOnGeometryLocator getLocator(int index) { + PointOnGeometryLocator locator = polyLocator[index]; + if (locator == null) { + Geometry polygonal = polygons.get(index); + locator = isPrepared + ? new IndexedPointInAreaLocator(polygonal) + : new SimplePointInAreaLocator(polygonal); + polyLocator[index] = locator; + } + return locator; + } + +} 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 new file mode 100644 index 0000000000..87d9f51b53 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelatePredicate.java @@ -0,0 +1,622 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +/** + * Creates predicate instances for evaluating OGC-standard named topological relationships. + * Predicates can be evaluated for geometries using {@link RelateNG}. + * + * @author Martin Davis + * + */ +public interface RelatePredicate { + + /** + * Creates a predicate to determine whether two geometries intersect. + *

    + * The intersects predicate has the following equivalent definitions: + *

      + *
    • The two geometries have at least one point in common + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the patterns + *
        + *
      • [T********] + *
      • [*T*******] + *
      • [***T*****] + *
      • [****T****] + *
      + *
    • disjoint() = false + *
      (intersects is the inverse of disjoint) + *
    + * + *@return the predicate instance + * + * @see #disjoint() + */ + public static TopologyPredicate intersects() { + return new BasicPredicate() { + + public String name() { return "intersects"; } + + @Override + public boolean requireSelfNoding() { + //-- self-noding is not required to check for a simple interaction + return false; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + //-- intersects only requires testing interaction + return false; + } + + @Override + public void init(Envelope envA, Envelope envB) { + require(envA.intersects(envB)); + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + setValueIf(true, isIntersection(locA, locB)); + } + + @Override + public void finish() { + //-- if no intersecting locations were found + setValue(false); + } + + }; + } + + /** + * Creates a predicate to determine whether two geometries are disjoint. + *

    + * The disjoint predicate has the following equivalent definitions: + *

      + *
    • The two geometries have no point in common + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * [FF*FF****] + *
    • intersects() = false + *
      (disjoint is the inverse of intersects) + *
    + * + *@return the predicate instance + * + * @see #intersects() + */ + public static TopologyPredicate disjoint() { + return new BasicPredicate() { + + public String name() { return "disjoint"; } + + @Override + public boolean requireSelfNoding() { + //-- self-noding is not required to check for a simple interaction + return false; + } + + @Override + public boolean requireInteraction() { + //-- ensure entire matrix is computed + return false; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + //-- disjoint only requires testing interaction + return false; + } + + @Override + public void init(Envelope envA, Envelope envB) { + setValueIf(true, envA.disjoint(envB)); + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + setValueIf(false, isIntersection(locA, locB)); + } + + @Override + public void finish() { + //-- if no intersecting locations were found + setValue(true); + } + + }; + } + + /** + * Creates a predicate to determine whether a geometry contains another geometry. + *

    + * The contains predicate has the following equivalent definitions: + *

      + *
    • Every point of the other geometry is a point of this geometry, + * and the interiors of the two geometries have at least one point in common. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * the pattern + * [T*****FF*] + *
    • within(B, A) = true + *
      (contains is the converse of {@link #within} ) + *
    + * An implication of the definition is that "Geometries do not + * contain their boundary". In other words, if a geometry A is a subset of + * the points in the boundary of a geometry B, B.contains(A) = false. + * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.) + * For a predicate with similar behavior but avoiding + * this subtle limitation, see {@link #covers}. + * + *@return the predicate instance + * + * @see #within() + */ + public static TopologyPredicate contains() { + return new IMPredicate() { + + public String name() { return "contains"; } + + @Override + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + //-- only need to check B against Exterior of A + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimA, dimB) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envA, envB); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_A); + } + + @Override + public boolean valueIM() { + return intMatrix.isContains(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry is within another geometry. + *

    + * The within predicate has the following equivalent definitions: + *

      + *
    • Every point of this geometry is a point of the other geometry, + * and the interiors of the two geometries have at least one point in common. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * [T*F**F***] + *
    • contains(B, A) = true + *
      (within is the converse of {@link #contains}) + *
    + * An implication of the definition is that + * "The boundary of a Geometry is not within the Geometry". + * In other words, if a geometry A is a subset of + * the points in the boundary of a geometry B, within(B, A) = false + * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.) + * For a predicate with similar behavior but avoiding + * this subtle limitation, see {@link #coveredBy}. + * + *@return the predicate instance + * + * @see #contains() + */ + public static TopologyPredicate within() { + return new IMPredicate() { + + public String name() { return "within"; } + + @Override + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + //-- only need to check A against Exterior of B + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimB, dimA) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envB, envA); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_B); + } + + public boolean valueIM() { + return intMatrix.isWithin(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry covers another geometry. + *

    + * The covers predicate has the following equivalent definitions: + *

      + *
    • Every point of the other geometry is a point of this geometry. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the following patterns: + *
        + *
      • [T*****FF*] + *
      • [*T****FF*] + *
      • [***T**FF*] + *
      • [****T*FF*] + *
      + *
    • coveredBy(b, a) = true + *
      (covers is the converse of {@link #coveredBy}) + *
    + * If either geometry is empty, the value of this predicate is false. + *

    + * This predicate is similar to {@link #contains()}, + * but is more inclusive (i.e. returns true for more cases). + * In particular, unlike contains it does not distinguish between + * points in the boundary and in the interior of geometries. + * For most cases, covers should be used in preference to contains. + * As an added benefit, covers is more amenable to optimization, + * and hence should be more performant. + * + *@return the predicate instance + * + * @see #coveredBy() + */ + public static TopologyPredicate covers() { + return new IMPredicate() { + + public String name() { return "covers"; } + + @Override + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + //-- only need to check B against Exterior of A + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimA, dimB) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envA, envB); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_A); + } + + @Override + public boolean valueIM() { + return intMatrix.isCovers(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry is covered by another geometry. + *

    + * The coveredBy predicate has the following equivalent definitions: + *

      + *
    • Every point of this geometry is a point of the other geometry. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the following patterns: + *
        + *
      • [T*F**F***] + *
      • [*TF**F***] + *
      • [**FT*F***] + *
      • [**F*TF***] + *
      + *
    • covers(B, A) = true + *
      (coveredBy is the converse of {@link #covers}) + *
    + * If either geometry is empty, the value of this predicate is false. + *

    + * This predicate is similar to {@link #within}, + * but is more inclusive (i.e. returns true for more cases). + * + *@return the predicate instance + * + * @see #covers() + */ + public static TopologyPredicate coveredBy() { + return new IMPredicate() { + public String name() { return "coveredBy"; } + + @Override + public boolean requireCovers(boolean isSourceA) { + return isSourceA == RelateGeometry.GEOM_B; + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + //-- only need to check A against Exterior of B + return isSourceA == RelateGeometry.GEOM_A; + } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require( isDimsCompatibleWithCovers(dimB, dimA) ); + } + + @Override + public void init(Envelope envA, Envelope envB) { + requireCovers(envB, envA); + } + + @Override + public boolean isDetermined() { + return intersectsExteriorOf(RelateGeometry.GEOM_B); + } + + @Override + public boolean valueIM() { + return intMatrix.isCoveredBy(); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry crosses another geometry. + *

    + * The crosses predicate has the following equivalent definitions: + *

      + *
    • The geometries have some but not all interior points in common. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * one of the following patterns: + *
        + *
      • [T*T******] (for P/L, P/A, and L/A cases) + *
      • [T*****T**] (for L/P, A/P, and A/L cases) + *
      • [0********] (for L/L cases) + *
      + *
    + * For the A/A and P/P cases this predicate returns false. + *

    + * The SFS defined this predicate only for P/L, P/A, L/L, and L/A cases. + * To make the relation symmetric + * JTS extends the definition to apply to L/P, A/P and A/L cases as well. + * + * @return the predicate instance + */ + public static TopologyPredicate crosses() { + return new IMPredicate() { + public String name() { return "crosses"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + boolean isBothPointsOrAreas = (dimA == Dimension.P && dimB == Dimension.P) + || (dimA == Dimension.A && dimB == Dimension.A); + require(! isBothPointsOrAreas); + } + + @Override + public boolean isDetermined() { + if (dimA == Dimension.L && dimB == Dimension.L) { + //-- L/L interaction can only be dim = P + if (getDimension(Location.INTERIOR, Location.INTERIOR) > Dimension.P) + return true; + } + else if (dimA < dimB) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.INTERIOR, Location.EXTERIOR)) { + return true; + } + } + else if (dimA > dimB) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) { + return true; + } + } + return false; + } + + @Override + public boolean valueIM() { + return intMatrix.isCrosses(dimA, dimB); + } + }; + } + + /** + * Creates a predicate to determine whether two geometries are topologically equal. + *

    + * The equals predicate has the following equivalent definitions: + *

      + *
    • The two geometries have at least one point in common, + * and no point of either geometry lies in the exterior of the other geometry. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * the pattern T*F**FFF* + *
    + * + * @return the predicate instance + */ + public static TopologyPredicate equalsTopo() { + return new IMPredicate() { + public String name() { return "equals"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require(dimA == dimB); + } + + @Override + public void init(Envelope envA, Envelope envB) { + require(envA.equals(envB)); + } + + @Override + public boolean isDetermined() { + boolean isEitherExteriorIntersects = + isIntersects(Location.INTERIOR, Location.EXTERIOR) + || isIntersects(Location.BOUNDARY, Location.EXTERIOR) + || isIntersects(Location.EXTERIOR, Location.INTERIOR) + || isIntersects(Location.EXTERIOR, Location.BOUNDARY); + + return isEitherExteriorIntersects; + } + + @Override + public boolean valueIM() { + return intMatrix.isEquals(dimA, dimB); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry overlaps another geometry. + *

    + * The overlaps predicate has the following equivalent definitions: + *

      + *
    • The geometries have at least one point each not shared by the other + * (or equivalently neither covers the other), + * they have the same dimension, + * and the intersection of the interiors of the two geometries has + * the same dimension as the geometries themselves. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * [T*T***T**] (for P/P and A/A cases) + * or [1*T***T**] (for L/L cases) + *
    + * If the geometries are of different dimension this predicate returns false. + * This predicate is symmetric. + * + * @return the predicate instance + */ + public static TopologyPredicate overlaps() { + return new IMPredicate() { + public String name() { return "overlaps"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + require(dimA == dimB); + } + + @Override + public boolean isDetermined() { + if (dimA == Dimension.A || dimA == Dimension.P) { + if (isIntersects(Location.INTERIOR, Location.INTERIOR) + && isIntersects(Location.INTERIOR, Location.EXTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) + return true; + } + if (dimA == Dimension.L) { + if (isDimension(Location.INTERIOR, Location.INTERIOR, Dimension.L) + && isIntersects(Location.INTERIOR, Location.EXTERIOR) + && isIntersects(Location.EXTERIOR, Location.INTERIOR)) + return true; + } + return false; + } + + @Override + public boolean valueIM() { + return intMatrix.isOverlaps(dimA, dimB); + } + }; + } + + /** + * Creates a predicate to determine whether a geometry touches another geometry. + *

    + * The touches predicate has the following equivalent definitions: + *

      + *
    • The geometries have at least one point in common, + * but their interiors do not intersect. + *
    • The DE-9IM Intersection Matrix for the two geometries matches + * at least one of the following patterns + *
        + *
      • [FT*******] + *
      • [F**T*****] + *
      • [F***T****] + *
      + *
    + * If both geometries have dimension 0, the predicate returns false, + * since points have only interiors. + * This predicate is symmetric. + * + * @return the predicate instance + */ + public static TopologyPredicate touches() { + return new IMPredicate() { + public String name() { return "touches"; } + + @Override + public void init(int dimA, int dimB) { + super.init(dimA, dimB); + //-- Points have only interiors, so cannot touch + boolean isBothPoints = dimA == 0 && dimB == 0; + require(! isBothPoints); + } + + @Override + public boolean isDetermined() { + //-- for touches interiors cannot intersect + boolean isInteriorsIntersects = isIntersects(Location.INTERIOR, Location.INTERIOR); + return isInteriorsIntersects; + } + + @Override + public boolean valueIM() { + return intMatrix.isTouches(dimA, dimB); + } + }; + } + + /** + * Creates a predicate that matches a DE-9IM matrix pattern. + * + * @param imPattern the pattern to match + * @return a predicate that matches the pattern + * + * @see IntersectionMatrixPattern + */ + public static TopologyPredicate matches(String imPattern) { + return new IMPatternMatcher(imPattern); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java new file mode 100644 index 0000000000..f81485136d --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/RelateSegmentString.java @@ -0,0 +1,158 @@ +/* + * 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 org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.noding.BasicSegmentString; + +/** + * Models a linear edge of a {@link RelateGeometry}. + * + * @author mdavis + * + */ +class RelateSegmentString extends BasicSegmentString { + + public static RelateSegmentString createLine(Coordinate[] pts, boolean isA, int elementId, RelateGeometry parent) { + return createSegmentString(pts, isA, Dimension.L, elementId, -1, null, parent); + } + + public static RelateSegmentString createRing(Coordinate[] pts, boolean isA, int elementId, int ringId, + Geometry poly, RelateGeometry parent) { + return createSegmentString(pts, isA, Dimension.A, elementId, ringId, poly, parent); + } + + private static RelateSegmentString createSegmentString(Coordinate[] pts, boolean isA, int dim, int elementId, int ringId, + Geometry poly, RelateGeometry parent) { + pts = removeRepeatedPoints(pts); + return new RelateSegmentString(pts, isA, dim, elementId, ringId, poly, parent); + } + + private static Coordinate[] removeRepeatedPoints(Coordinate[] pts) { + if (CoordinateArrays.hasRepeatedPoints(pts)) { + pts = CoordinateArrays.removeRepeatedPoints(pts); + } + return pts; + } + + private boolean isA; + private int dimension; + private int id; + private int ringId; + private RelateGeometry inputGeom; + private Geometry parentPolygonal = null; + + private RelateSegmentString(Coordinate[] pts, boolean isA, int dimension, int id, int ringId, Geometry poly, RelateGeometry inputGeom) { + super(pts, null); + this.isA = isA; + this.dimension = dimension; + this.id = id; + this.ringId = ringId; + this.parentPolygonal = poly; + this.inputGeom = inputGeom; + } + + public boolean isA() { + return isA; + } + + public RelateGeometry getGeometry() { + return inputGeom; + } + + public Geometry getPolygonal() { + return parentPolygonal; + } + + public NodeSection createNodeSection(int segIndex, Coordinate intPt) { + boolean isNodeAtVertex = + intPt.equals2D(getCoordinate(segIndex)) + || intPt.equals2D(getCoordinate(segIndex + 1)); + Coordinate prev = prevVertex(segIndex, intPt); + Coordinate next = nextVertex(segIndex, intPt); + NodeSection a = new NodeSection(isA, dimension, id, ringId, parentPolygonal, isNodeAtVertex, prev, intPt, next); + return a; + } + + /** + * + * @param ss + * @param segIndex + * @param pt + * @return the previous vertex, or null if none exists + */ + private Coordinate prevVertex(int segIndex, Coordinate pt) { + Coordinate segStart = getCoordinate(segIndex); + if (! segStart.equals2D(pt)) + return segStart; + //-- pt is at segment start, so get previous vertex + if (segIndex > 0) + return getCoordinate(segIndex - 1); + if (isClosed()) + return prevInRing(segIndex); + return null; + } + + /** + * + * @param ss + * @param segIndex + * @param pt + * @return the next vertex, or null if none exists + */ + private Coordinate nextVertex(int segIndex, Coordinate pt) { + Coordinate segEnd = getCoordinate(segIndex + 1); + if (! segEnd.equals2D(pt)) + return segEnd; + //-- pt is at seg end, so get next vertex + if (segIndex < size() - 2) + return getCoordinate(segIndex + 2); + if (isClosed()) + return nextInRing(segIndex + 1); + //-- segstring is not closed, so there is no next segment + return null; + } + + /** + * Tests if a segment intersection point has that segment as its + * canonical containing segment. + * Segments are half-closed, and contain their start point but not the endpoint, + * except for the final segment in a non-closed segment string, which contains + * its endpoint as well. + * This test ensures that vertices are assigned to a unique segment in a segment string. + * In particular, this avoids double-counting intersections which lie exactly + * at segment endpoints. + * + * @param segIndex the segment the point may lie on + * @param pt the point + * @return true if the segment contains the point + */ + public boolean isContainingSegment(int segIndex, Coordinate pt) { + //-- intersection is at segment start vertex - process it + if (pt.equals2D(getCoordinate(segIndex))) + return true; + if (pt.equals2D(getCoordinate(segIndex+1))) { + boolean isFinalSegment = segIndex == size() - 2; + if (isClosed() || ! isFinalSegment) + return false; + //-- for final segment, process intersections with final endpoint + return true; + } + //-- intersection is interior - process it + return true; + } + + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java new file mode 100644 index 0000000000..313d10abae --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyComputer.java @@ -0,0 +1,494 @@ +/* + * Copyright (c) 2022 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.HashMap; +import java.util.Map; + +import org.locationtech.jts.algorithm.PolygonNodeTopology; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Position; +import org.locationtech.jts.util.Assert; + +class TopologyComputer { + + private static final String MSG_GEOMETRY_DIMENSION_UNEXPECTED = "Unexpected combination of geometry dimensions"; + + private TopologyPredicate predicate; + private RelateGeometry geomA; + private RelateGeometry geomB; + private Map nodeMap = new HashMap(); + + public TopologyComputer(TopologyPredicate predicate, RelateGeometry geomA, RelateGeometry geomB) { + this.predicate = predicate; + this.geomA = geomA; + this.geomB = geomB; + + initExteriorDims(); + } + + /** + * Determine a priori partial EXTERIOR topology based on dimensions. + */ + private void initExteriorDims() { + int dimRealA = geomA.getDimensionReal(); + int dimRealB = geomB.getDimensionReal(); + + /** + * For P/L case, P exterior intersects L interior + */ + if (dimRealA == Dimension.P && dimRealB == Dimension.L) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.L); + } + else if (dimRealA == Dimension.L && dimRealB == Dimension.P) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.L); + } + /** + * For P/A case, the Area Int and Bdy intersect the Point exterior. + */ + else if (dimRealA == Dimension.P && dimRealB == Dimension.A) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); + updateDim(Location.EXTERIOR, Location.BOUNDARY, Dimension.L); + } + else if (dimRealA == Dimension.A && dimRealB == Dimension.P) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); + updateDim(Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + } + else if (dimRealA == Dimension.L && dimRealB == Dimension.A) { + updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A); + } + else if (dimRealA == Dimension.A && dimRealB == Dimension.L) { + updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + //-- cases where one geom is EMPTY + else if (dimRealA == Dimension.FALSE || dimRealB == Dimension.FALSE) { + if (dimRealA != Dimension.FALSE) { + initExteriorEmpty(RelateGeometry.GEOM_A); + } + if (dimRealB != Dimension.FALSE) { + initExteriorEmpty(RelateGeometry.GEOM_B); + } + } + } + + private void initExteriorEmpty(boolean geomNonEmpty) { + int dimNonEmpty = getDimension(geomNonEmpty); + switch (dimNonEmpty) { + case Dimension.P: + updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.P); + break; + case Dimension.L: + if (getGeometry(geomNonEmpty).hasBoundary()) { + updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.P); + } + updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.L); + break; + case Dimension.A: + updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + break; + } + } + + private RelateGeometry getGeometry(boolean isA) { + return isA ? geomA : geomB; + } + + public int getDimension(boolean isA) { + return getGeometry(isA).getDimension(); + } + + public boolean isAreaArea() { + return getDimension(RelateGeometry.GEOM_A) == Dimension.A + && getDimension(RelateGeometry.GEOM_B) == Dimension.A; + } + + /** + * Indicates whether the input geometries require self-noding + * for correct evaluation of specific spatial predicates. + * Self-noding is required for geometries which may + * have self-crossing linework. + * This causes the coordinates of nodes created by + * crossing segments to be computed explicitly. + * This ensures that node locations match in situations + * where a self-crossing and mutual crossing occur at the same logical location. + * The canonical example is a self-crossing line tested against a single segment + * identical to one of the crossed segments. + * + * @return true if self-noding is required + */ + public boolean isSelfNodingRequired() { + if (predicate.requireSelfNoding()) { + if (geomA.isSelfNodingRequired() + || geomB.isSelfNodingRequired()) + return true; + } + return false; + } + + public boolean isExteriorCheckRequired(boolean isA) { + return predicate.requireExteriorCheck(isA); + } + + private void updateDim(int locA, int locB, int dimension) { + //System.out.println(Location.toLocationSymbol(locA) + "/" + Location.toLocationSymbol(locB) + ": " + dimension); + predicate.updateDimension(locA, locB, dimension); + } + + private void updateDim(boolean isAB, int loc1, int loc2, int dimension) { + if (isAB) { + updateDim(loc1, loc2, dimension); + } + else { + // is ordered BA + updateDim(loc2, loc1, dimension); + } + } + + public boolean isResultKnown() { + return predicate.isKnown(); + } + + public boolean getResult() { + return predicate.value(); + } + + /** + * Finalize the evaluation. + */ + public void finish() { + predicate.finish(); + } + + private NodeSections getNodeSections(Coordinate nodePt) { + NodeSections node = nodeMap.get(nodePt); + if (node == null) { + node = new NodeSections(nodePt); + nodeMap.put(nodePt, node); + } + return node; + } + + public void addIntersection(NodeSection a, NodeSection b) { + if (! a.isSameGeometry(b)) { + updateIntersectionAB(a, b); + } + //-- add edges to node to allow full topology evaluation later + addNodeSections(a, b); + } + + /** + * Update topology for an intersection between A and B. + * + * @param a the section for geometry A + * @param b the section for geometry B + */ + private void updateIntersectionAB(NodeSection a, NodeSection b) { + if (NodeSection.isAreaArea(a, b)) { + updateAreaAreaCross(a, b); + } + updateNodeLocation(a, b); + } + + /** + * Updates topology for an AB Area-Area crossing node. + * Sections cross at a node if (a) the intersection is proper + * (i.e. in the interior of two segments) + * or (b) if non-proper then whether the linework crosses + * is determined by the geometry of the segments on either side of the node. + * In these situations the area geometry interiors intersect (in dimension 2). + * + * @param a the section for geometry A + * @param b the section for geometry B + */ + private void updateAreaAreaCross(NodeSection a, NodeSection b) { + boolean isProper = NodeSection.isProper(a, b); + if (isProper || PolygonNodeTopology.isCrossing(a.nodePt(), + a.getVertex(0), a.getVertex(1), + b.getVertex(0), b.getVertex(1))) { + updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.A); + } + } + /** + * Updates topology for a node at an AB edge intersection. + * + * @param a the section for geometry A + * @param b the section for geometry B + */ + private void updateNodeLocation(NodeSection a, NodeSection b) { + Coordinate pt = a.nodePt(); + int locA = geomA.locateNode(pt, a.getPolygonal()); + int locB = geomB.locateNode(pt, b.getPolygonal()); + updateDim(locA, locB, Dimension.P); + } + + private void addNodeSections(NodeSection ns0, NodeSection ns1) { + NodeSections sections = getNodeSections(ns0.nodePt()); + sections.addNodeSection(ns0); + sections.addNodeSection(ns1); + } + + public void addPointOnPointInterior(Coordinate pt) { + updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.P); + } + + public void addPointOnPointExterior(boolean isGeomA, Coordinate pt) { + updateDim(isGeomA, Location.INTERIOR, Location.EXTERIOR, 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: + /** + * Because zero-length lines are handled, + * a point lying in the exterior of the line target + * may imply either P or L for the Exterior interaction + */ + //TODO: determine if effective dimension of linear target is L? + //updateDim(isGeomA, Location.EXTERIOR, locTarget, Dimension.P); + return; + case Dimension.A: + /** + * If a point intersects an area target, then the area interior and boundary + * must extend beyond the point and thus interact with its exterior. + */ + updateDim(isA, Location.EXTERIOR, Location.INTERIOR, Dimension.A); + updateDim(isA, Location.EXTERIOR, Location.BOUNDARY, Dimension.L); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); + } + + /** + * Add topology for a line end. + * The line end point must be "significant"; + * i.e. not contained in an area if the source is a mixed-dimension GC. + * + * @param isLineA the input containing the line end + * @param locLineEnd the location of the line end (Interior or Boundary) + * @param locTarget the location on the target geometry + * @param dimTarget the dimension of the interacting target geometry element, + * (if any), or the dimension of the target + * @param pt the line end coordinate + */ + public void addLineEndOnGeometry(boolean isLineA, int locLineEnd, int locTarget, int dimTarget, Coordinate pt) { + //-- record topology at line end point + updateDim(isLineA, locLineEnd, locTarget, Dimension.P); + + //-- Line and Area targets may have additional topology + switch (dimTarget) { + case Dimension.P: + return; + case Dimension.L: + addLineEndOnLine(isLineA, locLineEnd, locTarget, pt); + return; + case Dimension.A: + addLineEndOnArea(isLineA, locLineEnd, locTarget, pt); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); + } + + private void addLineEndOnLine(boolean isLineA, int locLineEnd, int locLine, Coordinate pt) { + /** + * When a line end is in the EXTERIOR of a Line, + * some length of the source Line INTERIOR + * is also in the target Line EXTERIOR. + * This works for zero-length lines as well. + */ + if (locLine == Location.EXTERIOR) { + updateDim(isLineA, Location.INTERIOR, Location.EXTERIOR, Dimension.L); + } + } + + private void addLineEndOnArea(boolean isLineA, int locLineEnd, int locArea, Coordinate pt) { + if (locArea != Location.BOUNDARY) { + /** + * When a line end is in an Area INTERIOR or EXTERIOR + * some length of the source Line Interior + * AND the Exterior of the line + * is also in that location of the target. + * NOTE: this assumes the line end is NOT also in an Area of a mixed-dim GC + */ + //TODO: handle zero-length lines? + updateDim(isLineA, Location.INTERIOR, locArea, Dimension.L); + updateDim(isLineA, Location.EXTERIOR, locArea, Dimension.A); + } + } + + /** + * Adds topology for an area vertex interaction with a target geometry element. + * Assumes the target geometry element has highest dimension + * (i.e. if the point lies on two elements of different dimension, + * the location on the higher dimension element is provided. + * This is the semantic provided by {@link RelatePointLocator}. + *

    + * Note that in a GeometryCollection containing overlapping or adjacent polygons, + * the area vertex location may be INTERIOR instead of BOUNDARY. + * + * @param isAreaA the input that is the area + * @param locArea the location on the area + * @param locTarget the location on the target geometry element + * @param dimTarget the dimension of the target geometry element + * @param pt the point of interaction + */ + public void addAreaVertex(boolean isAreaA, int locArea, int locTarget, int dimTarget, Coordinate pt) { + if (locTarget == Location.EXTERIOR) { + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + /** + * If area vertex is on Boundary further topology can be deduced + * from the neighbourhood around the boundary vertex. + * This is always the case for polygonal geometries. + * For GCs, the vertex may be either on boundary or in interior + * (i.e. of overlapping or adjacent polygons) + */ + if (locArea == Location.BOUNDARY) { + updateDim(isAreaA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(isAreaA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + return; + } + switch (dimTarget) { + case Dimension.P: + addAreaVertexOnPoint(isAreaA, locArea, pt); + return; + case Dimension.L: + addAreaVertexOnLine(isAreaA, locArea, locTarget, pt); + return; + case Dimension.A: + addAreaVertexOnArea(isAreaA, locArea, locTarget, pt); + return; + } + throw new IllegalStateException("Unknown target dimension: " + dimTarget); + } + + /** + * Updates topology for an area vertex (in Interior or on Boundary) + * intersecting a point. + * Note that because the largest dimension of intersecting target is determined, + * the intersecting point is not part of any other target geometry, + * and hence its neighbourhood is in the Exterior of the target. + * + * @param isAreaA whether the area is the A input + * @param locArea the location of the vertex in the area + * @param pt the point at which topology is being updated + */ + private void addAreaVertexOnPoint(boolean isAreaA, int locArea, Coordinate pt) { + //-- Assert: locArea != EXTERIOR + //-- Assert: locTarget == INTERIOR + /** + * The vertex location intersects the Point. + */ + updateDim(isAreaA, locArea, Location.INTERIOR, Dimension.P); + /** + * The area interior intersects the point's exterior neighbourhood. + */ + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + /** + * If the area vertex is on the boundary, + * the area boundary and exterior intersect the point's exterior neighbourhood + */ + if (locArea == Location.BOUNDARY) { + updateDim(isAreaA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L); + updateDim(isAreaA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A); + } + } + + private void addAreaVertexOnLine(boolean isAreaA, int locArea, int locTarget, Coordinate pt) { + //-- Assert: locArea != EXTERIOR + /** + * 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 the area boundary, + * and the line may or may not intersect the area interior. + * Full topology is determined later by node analysis + */ + updateDim(isAreaA, locArea, locTarget, Dimension.P); + if (locArea == Location.INTERIOR) { + /** + * The area interior intersects the line's exterior neighbourhood. + */ + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + } + + public void addAreaVertexOnArea(boolean isAreaA, int locArea, int locTarget, Coordinate pt) { + if (locTarget == Location.BOUNDARY) { + if (locArea == Location.BOUNDARY) { + //-- B/B topology is fully computed later by node analysis + updateDim(isAreaA, Location.BOUNDARY, Location.BOUNDARY, Dimension.P); + } + else { + // locArea == INTERIOR + updateDim(isAreaA, Location.INTERIOR, Location.INTERIOR, Dimension.A); + updateDim(isAreaA, Location.INTERIOR, Location.BOUNDARY, Dimension.L); + updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A); + } + } + else { + //-- locTarget is INTERIOR or EXTERIOR` + updateDim(isAreaA, Location.INTERIOR, locTarget, Dimension.A); + /** + * If area vertex is on Boundary further topology can be deduced + * from the neighbourhood around the boundary vertex. + * This is always the case for polygonal geometries. + * For GCs, the vertex may be either on boundary or in interior + * (i.e. of overlapping or adjacent polygons) + */ + if (locArea == Location.BOUNDARY) { + updateDim(isAreaA, Location.BOUNDARY, locTarget, Dimension.L); + updateDim(isAreaA, Location.EXTERIOR, locTarget, Dimension.A); + } + } + } + + public void evaluateNodes() { + for (NodeSections nodeSections : nodeMap.values()) { + if (nodeSections.hasInteractionAB()) { + evaluateNode(nodeSections); + if (isResultKnown()) + return; + } + } + } + + private void evaluateNode(NodeSections nodeSections) { + Coordinate p = nodeSections.getCoordinate(); + RelateNode node = nodeSections.createNode(); + //-- Node must have edges for geom, but may also be in interior of a overlapping GC + boolean isAreaInteriorA = geomA.isNodeInArea(p, nodeSections.getPolygonal(RelateGeometry.GEOM_A)); + boolean isAreaInteriorB = geomB.isNodeInArea(p, nodeSections.getPolygonal(RelateGeometry.GEOM_B)); + node.finish(isAreaInteriorA, isAreaInteriorB); + evaluateNodeEdges(node); + } + + private void evaluateNodeEdges(RelateNode node) { + //TODO: collect distinct dim settings by using temporary matrix? + for (RelateEdge e : node.getEdges()) { + //-- An optimization to avoid updates for cases with a linear geometry + if (isAreaArea()) { + updateDim(e.location(RelateGeometry.GEOM_A, Position.LEFT), + e.location(RelateGeometry.GEOM_B, Position.LEFT), Dimension.A); + updateDim(e.location(RelateGeometry.GEOM_A, Position.RIGHT), + e.location(RelateGeometry.GEOM_B, Position.RIGHT), Dimension.A); + } + updateDim(e.location(RelateGeometry.GEOM_A, Position.ON), + e.location(RelateGeometry.GEOM_B, Position.ON), Dimension.L); + } + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java new file mode 100644 index 0000000000..daf183c0fb --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicate.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2022 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 org.locationtech.jts.geom.Envelope; + +/** + * The API for strategy classes implementing + * spatial predicates based on the DE-9IM topology model. + * Predicate values for specific geometry pairs can be evaluated by {@link RelateNG}. + * + * @author Martin Davis + */ +public interface TopologyPredicate { + + /** + * Gets the name of the predicate. + * + * @return the predicate name + */ + String name(); + + /** + * Reports whether this predicate requires self-noding for + * geometries which contain crossing edges + * (for example, {@link LineString}s, or {@line GeometryCollection}s + * containing lines or polygons which may self-intersect). + * Self-noding ensures that intersections are computed consistently + * in cases which contain self-crossings and mutual crossings. + *

    + * Most predicates require this, but it can + * be avoided for simple intersection detection + * (such as in {@link RelatePredicate#intersects()} + * and {@link RelatePredicate#disjoint()}. + * Avoiding self-noding improves performance for polygonal inputs. + * + * @return true if self-noding is required. + */ + default boolean requireSelfNoding() { + return true; + } + + /** + * Reports whether this predicate requires interaction between + * the input geometries. + * This is the case if + *

    +   * IM[I, I] >= 0 or IM[I, B] >= 0 or IM[B, I] >= 0 or IM[B, B] >= 0
    +   * 
    + * This allows a fast result if + * the envelopes of the geometries are disjoint. + * + * @return true if the geometries must interact + */ + default boolean requireInteraction() { + return true; + } + + /** + * Reports whether this predicate requires that the source + * cover the target. + * This is the case if + *
    +   * IM[Ext(Src), Int(Tgt)] = F and IM[Ext(Src), Bdy(Tgt)] = F
    +   * 
    + * If true, this allows a fast result if + * the source envelope does not cover the target envelope. + * + * @param isSourceA indicates the source input geometry + * @return true if the predicate requires checking whether the source covers the target + */ + default boolean requireCovers(boolean isSourceA) { + return false; + } + + /** + * Reports whether this predicate requires checking if the source input intersects + * the Exterior of the target input. + * This is the case if: + *
    +   * IM[Int(Src), Ext(Tgt)] >= 0 or IM[Bdy(Src), Ext(Tgt)] >= 0
    +   * 
    + * If false, this may permit a faster result in some geometric situations. + * + * @param isSourceA indicates the source input geometry + * @return true if the predicate requires checking whether the source intersects the target exterior + */ + default boolean requireExteriorCheck(boolean isSourceA) { + return true; + } + + /** + * Initializes the predicate for a specific geometric case. + * This may allow the predicate result to become known + * if it can be inferred from the dimensions. + * + * @param dimA the dimension of geometry A + * @param dimB the dimension of geometry B + * + * @see Dimension + */ + default void init(int dimA, int dimB) { + //-- default if dimensions provide no information + } + + /** + * Initializes the predicate for a specific geometric case. + * This may allow the predicate result to become known + * if it can be inferred from the envelopes. + * + * @param envA the envelope of geometry A + * @param envB the envelope of geometry B + */ + default void init(Envelope envA, Envelope envB) { + //-- default if envelopes provide no information + } + + /** + * Updates the entry in the DE-9IM intersection matrix + * for given {@link Location}s in the input geometries. + *

    + * If this method is called with a {@link Dimension} value + * which is less than the current value for the matrix entry, + * the implementing class should avoid changing the entry + * if this would cause information loss. + * + * @param locA the location on the A axis of the matrix + * @param locB the location on the B axis of the matrix + * @param dimension the dimension value for the entry + * + * @see Dimension + * @see Location + */ + void updateDimension(int locA, int locB, int dimension); + + /** + * Indicates that the value of the predicate can be finalized + * based on its current state. + */ + void finish(); + + /** + * Tests if the predicate value is known. + * + * @return true if the result is known + */ + boolean isKnown(); + + /** + * Gets the current value of the predicate result. + * The value is only valid if {@link #isKnown()} is true. + * + * @return the predicate result value + */ + boolean value(); + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java new file mode 100644 index 0000000000..46d1c3ac7a --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/TopologyPredicateTracer.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Location; + +/** + * Traces the evaluation of a {@link TopologyPredicate}. + * + * @author mdavis + * + */ +public class TopologyPredicateTracer { + + /** + * Creates a new predicate tracing the evaluation of a given predicate. + * + * @param pred the predicate to trace + * @return the traceable predicate + */ + public static TopologyPredicate trace(TopologyPredicate pred) { + return new PredicateTracer(pred); + } + + private TopologyPredicateTracer() { + + } + + private static class PredicateTracer implements TopologyPredicate + { + private TopologyPredicate pred; + + private PredicateTracer(TopologyPredicate pred) { + this.pred = pred; + } + + public String name() { return pred.name(); } + + @Override + public boolean requireSelfNoding() { + return pred.requireSelfNoding(); + } + + public boolean requireInteraction() { + return pred.requireInteraction(); + } + + @Override + public boolean requireCovers(boolean isSourceA) { + return pred.requireCovers(isSourceA); + } + + @Override + public boolean requireExteriorCheck(boolean isSourceA) { + return pred.requireExteriorCheck(isSourceA); + } + + @Override + public void init(int dimA, int dimB) { + pred.init(dimA, dimB); + checkValue("dimensions"); + } + + @Override + public void init(Envelope envA, Envelope envB) { + pred.init(envA, envB); + checkValue("envelopes"); + } + + @Override + public void updateDimension(int locA, int locB, int dimension) { + String desc = "A:" + Location.toLocationSymbol(locA) + + "/B:" + Location.toLocationSymbol(locB) + + " -> " + dimension; + String ind = ""; + boolean isChanged = isDimChanged(locA, locB, dimension); + if (isChanged) { + ind = " <<< "; + } + System.out.println(desc + ind); + pred.updateDimension(locA, locB, dimension); + if (isChanged) { + checkValue("IM entry"); + } + } + + private boolean isDimChanged(int locA, int locB, int dimension) { + if (pred instanceof IMPredicate) { + return ((IMPredicate) pred).isDimChanged(locA, locB, dimension); + } + return false; + } + + private void checkValue(String source) { + if (pred.isKnown()) { + System.out.println(name() + " = " + pred.value() + + " based on " + source); + } + } + + @Override + public void finish() { + pred.finish(); + } + + @Override + public boolean isKnown() { + return pred.isKnown(); + } + + @Override + public boolean value() { + return pred.value(); + } + + public String toString() { + return pred.toString(); + } + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java new file mode 100644 index 0000000000..63810b2c3c --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/operation/relateng/package-info.java @@ -0,0 +1,99 @@ +/* + * 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. + */ + +/** + * Provides classes to implement the RelateNG algorithm + * computes topological relationships of {@link Geometry}s. + * Topology is evaluated based on the + * Dimensionally-Extended 9-Intersection Model (DE-9IM). + * The {@link RelateNG} class supports computing the value of boolean topological predicates. + * Standard OGC named predicates are provided by the {@link RelatePredicate} functions. + * Custom relationships can be specified via testing against DE-9IM matrix patterns + * (see {@link IntersectionMatrixPattern} for examples). + * The full DE-9IM {@link IntersectionMatrix} can also be computed. + *

    + * The algorithm has the following capabilities: + *

      + *
    1. Efficient short-circuited evaluation of topological predicates + * (including matching custom DE-9IM patterns) + *
    2. Optimized repeated evaluation of predicates against a single geometry + * via cached spatial indexes (AKA "prepared mode") + *
    3. Robust computation (since only point-local topology is required, + * so that invalid geometry topology cannot cause failures) + *
    4. Support for mixed-type and overlapping {@link GeometryCollection} inputs + * (using union semantics) + *
    5. Support for {@link BoundaryNodeRule} + *
    + * + * RelateNG operates in 2D only; it ignores any Z ordinates. + * + *

    Optimized Short-Circuited Evaluation

    + * The RelateNG algorithm uses strategies to optimize the evaluation of + * topological predicates, including matching DE-9IM matrix patterns. + * These include fast tests of dimensions and envelopes, and short-circuited evaluation + * once the predicate value is known + * (either satisfied or failed) based on the value of matrix entries. + * Named predicates used explicit strategy code. + * DE-9IM matrix pattern matching are short-circuited where possible + * based on analysis of the pattern matrix entries. + * Spatial indexes are used to optimize topological computations + * (such as locating points in geometry elements, + * and analyzing the topological relationship between geometry edges). + * + *

    Execution Modes

    + * RelateNG provides two execution modes for evaluating predicates: + *
      + *
    • Single-shot mode evaluates a predicate for a single case of two geometries. + * It is provided by the {@link RelateNG} static functions which take two input geometries. + *
    • Prepared mode optimizes repeated evaluation of predicates + * against a fixed geometry. It is used by creating an instance of {@link RelateNG} + * on the required geometry with the prepare functions, + * and then using the evaluate methods. + * It provides much faster performance for repeated operations against a single geometry. + *
    + * + *

    Robustness

    + * RelateNG provides robust evaluation of topological relationships, + * up to the precision of double-precision computation. + * It computes topological relationships in the locality of discrete points, + * without constructing a full topology graph of the inputs. + * This means that invalid input geometries or numerical round-off do not cause exceptions + * (although they may return incorrect answers). + * However, it is necessary to node some inputs together (in particular, linear elements) + * in order to provide consistent evaluation of the topological structure. + * + *

    GeometryCollection Handling

    + * {@link GeometryCollection}s may contain geometries of different dimensions, nested to any level. + * The element geometries may overlap in any combination. + * The OGC specification did not provide a definition for the topology + * of GeometryCollections, or how they behave under the DE-9IM model. + * RelateNG defines the topology for arbitrary collections of geometries + * using "union semantics". + * This is specified as: + *
      + *
    • GeometryCollections are evaluated as if they were replaced by the topological union + * of their elements. + *
    • The topological location at a point is equal to its location in the geometry of highest + * dimension which contains it. For example, a point located in the interior of a Polygon + * and the boundary of a LineString has location Interior. + *
    + * + *

    Zero-length LineString Handling

    + * Zero-length LineStrings are handled as topologically identical to a Point at the same coordinate. + * + *

    Package Specification

    + * + */ +package org.locationtech.jts.operation.relateng; diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java b/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java index 4e2f8d21a8..2dac531ce5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/valid/PolygonTopologyAnalyzer.java @@ -14,16 +14,15 @@ import java.util.ArrayList; import java.util.List; -import org.locationtech.jts.algorithm.LineIntersector; import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.algorithm.PointLocation; import org.locationtech.jts.algorithm.PolygonNodeTopology; -import org.locationtech.jts.algorithm.RobustLineIntersector; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.noding.BasicSegmentString; import org.locationtech.jts.noding.MCIndexNoder; @@ -185,10 +184,8 @@ private static int ringIndexNext(Coordinate[] ringPts, int index) { * @return the intersection segment index, or -1 if no intersection is found */ private static int intersectingSegIndex(Coordinate[] ringPts, Coordinate pt) { - LineIntersector li = new RobustLineIntersector(); for (int i = 0; i < ringPts.length - 1; i++) { - li.computeIntersection(pt, ringPts[i], ringPts[i + 1]); - if (li.hasIntersection()) { + if (PointLocation.isOnSegment(pt, ringPts[i], ringPts[i + 1])) { //-- check if pt is the start point of the next segment if (pt.equals2D(ringPts[i + 1])) { return i + 1; diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java b/modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java new file mode 100644 index 0000000000..286a27f2d9 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/ComponentJumpChecker.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 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.simplify; + +import java.util.Collection; + +import org.locationtech.jts.algorithm.RayCrossingCounter; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.LineSegment; + +/** + * Checks if simplifying (flattening) line sections or segments + * would cause them to "jump" over other components in the geometry. + * + * @author mdavis + * + */ +class ComponentJumpChecker { + + //TODO: use a spatial index? + private Collection components; + + public ComponentJumpChecker(Collection taggedLines) { + components = taggedLines; + } + + /** + * Checks if a line section jumps a component if flattened. + * + * Assumes start <= end. + * + * @param line the line containing the section being flattened + * @param start start index of the section + * @param end end index of the section + * @param seg the flattening segment + * @return true if the flattened section jumps a component + */ + public boolean hasJump(TaggedLineString line, int start, int end, LineSegment seg) { + Envelope sectionEnv = computeEnvelope(line, start, end); + for (TaggedLineString comp : components) { + //-- don't test component against itself + if (comp == line) + continue; + + Coordinate compPt = comp.getComponentPoint(); + if (sectionEnv.intersects(compPt)) { + if (hasJumpAtComponent(compPt, line, start, end, seg)) { + return true; + } + } + } + return false; + } + + /** + * Checks if two consecutive segments jumps a component if flattened. + * The segments are assumed to be consecutive. + * (so the seg1.p1 = seg2.p0). + * The flattening segment must be the segment between seg1.p0 and seg2.p1. + * + * @param line the line containing the section being flattened + * @param seg1 the first replaced segment + * @param seg2 the next replaced segment + * @param seg the flattening segment + * @return true if the flattened segment jumps a component + */ + public boolean hasJump(TaggedLineString line, LineSegment seg1, LineSegment seg2, LineSegment seg) { + Envelope sectionEnv = computeEnvelope(seg1, seg2); + for (TaggedLineString comp : components) { + //-- don't test component against itself + if (comp == line) + continue; + + Coordinate compPt = comp.getComponentPoint(); + if (sectionEnv.intersects(compPt)) { + if (hasJumpAtComponent(compPt, seg1, seg2, seg)) { + return true; + } + } + } + return false; + } + + private static boolean hasJumpAtComponent(Coordinate compPt, TaggedLineString line, int start, int end, LineSegment seg) { + int sectionCount = crossingCount(compPt, line, start, end); + int segCount = crossingCount(compPt, seg); + boolean hasJump = sectionCount % 2 != segCount % 2; + return hasJump; + } + + private static boolean hasJumpAtComponent(Coordinate compPt, LineSegment seg1, LineSegment seg2, LineSegment seg) { + int sectionCount = crossingCount(compPt, seg1, seg2); + int segCount = crossingCount(compPt, seg); + boolean hasJump = sectionCount % 2 != segCount % 2; + return hasJump; + } + + private static int crossingCount(Coordinate compPt, LineSegment seg) { + RayCrossingCounter rcc = new RayCrossingCounter(compPt); + rcc.countSegment(seg.p0, seg.p1); + return rcc.getCount(); + } + + private static int crossingCount(Coordinate compPt, LineSegment seg1, LineSegment seg2) { + RayCrossingCounter rcc = new RayCrossingCounter(compPt); + rcc.countSegment(seg1.p0, seg1.p1); + rcc.countSegment(seg2.p0, seg2.p1); + return rcc.getCount(); + } + + private static int crossingCount(Coordinate compPt, TaggedLineString line, int start, int end) { + RayCrossingCounter rcc = new RayCrossingCounter(compPt); + for (int i = start; i < end; i++) { + rcc.countSegment(line.getCoordinate(i), line.getCoordinate(i + 1)); + } + return rcc.getCount(); + } + + private static Envelope computeEnvelope(LineSegment seg1, LineSegment seg2) { + Envelope env = new Envelope(); + env.expandToInclude(seg1.p0); + env.expandToInclude(seg1.p1); + env.expandToInclude(seg2.p0); + env.expandToInclude(seg2.p1); + return env; + } + + private static Envelope computeEnvelope(TaggedLineString line, int start, int end) { + Envelope env = new Envelope(); + for (int i = start; i <= end; i++) { + env.expandToInclude(line.getCoordinate(i)); + } + return env; + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java index 05afe0be1b..d9a8ba04c9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerLineSimplifier.java @@ -13,6 +13,7 @@ package org.locationtech.jts.simplify; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.LineSegment; @@ -24,16 +25,18 @@ */ class DouglasPeuckerLineSimplifier { - public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance) + public static Coordinate[] simplify(Coordinate[] pts, double distanceTolerance, boolean isPreserveEndpoint) { DouglasPeuckerLineSimplifier simp = new DouglasPeuckerLineSimplifier(pts); simp.setDistanceTolerance(distanceTolerance); + simp.setPreserveEndpoint(isPreserveEndpoint); return simp.simplify(); } private Coordinate[] pts; private boolean[] usePt; private double distanceTolerance; + private boolean isPreserveEndpoint = false; public DouglasPeuckerLineSimplifier(Coordinate[] pts) { @@ -50,6 +53,10 @@ public void setDistanceTolerance(double distanceTolerance) { this.distanceTolerance = distanceTolerance; } + private void setPreserveEndpoint(boolean isPreserveEndpoint) { + this.isPreserveEndpoint = isPreserveEndpoint; + } + public Coordinate[] simplify() { usePt = new boolean[pts.length]; @@ -57,12 +64,33 @@ public Coordinate[] simplify() usePt[i] = true; } simplifySection(0, pts.length - 1); + CoordinateList coordList = new CoordinateList(); for (int i = 0; i < pts.length; i++) { if (usePt[i]) coordList.add(new Coordinate(pts[i])); } - return coordList.toCoordinateArray(); + + if (! isPreserveEndpoint && CoordinateArrays.isRing(pts)) { + simplifyRingEndpoint(coordList); + } + + return coordList.toCoordinateArray(); + } + + private void simplifyRingEndpoint(CoordinateList pts) { + //-- avoid collapsing triangles + if (pts.size() < 4) + return; + //-- base segment for endpoint + seg.p0 = pts.get(1); + seg.p1 = pts.get(pts.size() - 2); + double distance = seg.distance(pts.get(0)); + if (distance <= distanceTolerance) { + pts.remove(0); + pts.remove(pts.size() - 1); + pts.closeRing(); + } } private LineSegment seg = new LineSegment(); diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java index 6c0e3f358f..e14d0481c3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifier.java @@ -135,13 +135,14 @@ public DPTransformer(boolean isEnsureValidTopology, double distanceTolerance) protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geometry parent) { + boolean isPreserveEndpoint = ! (parent instanceof LinearRing); Coordinate[] inputPts = coords.toCoordinateArray(); Coordinate[] newPts = null; if (inputPts.length == 0) { newPts = new Coordinate[0]; } else { - newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, distanceTolerance); + newPts = DouglasPeuckerLineSimplifier.simplify(inputPts, distanceTolerance, isPreserveEndpoint); } return factory.getCoordinateSequenceFactory().create(newPts); } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java b/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java index 23cc853ec1..219d3afdc2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/LineSegmentIndex.java @@ -52,13 +52,13 @@ public void remove(LineSegment seg) index.remove(new Envelope(seg.p0, seg.p1), seg); } - public List query(LineSegment querySeg) + public List query(LineSegment querySeg) { Envelope env = new Envelope(querySeg.p0, querySeg.p1); LineSegmentVisitor visitor = new LineSegmentVisitor(querySeg); index.query(env, visitor); - List itemsFound = visitor.getItems(); + List itemsFound = visitor.getItems(); // List listQueryItems = index.query(env); // System.out.println("visitor size = " + itemsFound.size() @@ -78,7 +78,7 @@ class LineSegmentVisitor // MD - only seems to make about a 10% difference in overall time. private LineSegment querySeg; - private ArrayList items = new ArrayList(); + private ArrayList items = new ArrayList(); public LineSegmentVisitor(LineSegment querySeg) { this.querySeg = querySeg; @@ -91,5 +91,5 @@ public void visitItem(Object item) items.add(item); } - public ArrayList getItems() { return items; } + public ArrayList getItems() { return items; } } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java index 1b5e67a7b6..d76bfaae0d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineString.java @@ -32,24 +32,38 @@ class TaggedLineString private LineString parentLine; private TaggedLineSegment[] segs; - private List resultSegs = new ArrayList(); + private List resultSegs = new ArrayList(); private int minimumSize; + private boolean isRing = true; - public TaggedLineString(LineString parentLine) { - this(parentLine, 2); - } - - public TaggedLineString(LineString parentLine, int minimumSize) { + public TaggedLineString(LineString parentLine, int minimumSize, boolean isRing) { this.parentLine = parentLine; this.minimumSize = minimumSize; + this.isRing = isRing; init(); } + public boolean isRing() { + return isRing; + } + public int getMinimumSize() { return minimumSize; } public LineString getParent() { return parentLine; } public Coordinate[] getParentCoordinates() { return parentLine.getCoordinates(); } public Coordinate[] getResultCoordinates() { return extractCoordinates(resultSegs); } + public Coordinate getCoordinate(int i) { + return parentLine.getCoordinateN(i); + } + + public int size() { + return parentLine.getNumPoints(); + } + + public Coordinate getComponentPoint() { + return getParentCoordinates()[1]; + } + public int getResultSize() { int resultSegsSize = resultSegs.size(); @@ -58,6 +72,20 @@ public int getResultSize() public TaggedLineSegment getSegment(int i) { return segs[i]; } + /** + * Gets a segment of the result list. + * Negative indexes can be used to retrieve from the end of the list. + * @param i the segment index to retrieve + * @return the result segment + */ + public LineSegment getResultSegment(int i) { + int index = i; + if (i < 0) { + index = resultSegs.size() + i; + } + return (LineSegment) resultSegs.get(index); + } + private void init() { Coordinate[] pts = parentLine.getCoordinates(); @@ -71,6 +99,13 @@ private void init() public TaggedLineSegment[] getSegments() { return segs; } + /** + * Add a simplified segment to the result. + * This assumes simplified segments are computed in the order + * they occur in the line. + * + * @param seg the result segment to add + */ public void addToResult(LineSegment seg) { resultSegs.add(seg); @@ -85,7 +120,7 @@ public LinearRing asLinearRing() { return parentLine.getFactory().createLinearRing(extractCoordinates(resultSegs)); } - private static Coordinate[] extractCoordinates(List segs) + private static Coordinate[] extractCoordinates(List segs) { Coordinate[] pts = new Coordinate[segs.size() + 1]; LineSegment seg = null; @@ -98,5 +133,15 @@ private static Coordinate[] extractCoordinates(List segs) return pts; } + LineSegment removeRingEndpoint() + { + LineSegment firstSeg = (LineSegment) resultSegs.get(0); + LineSegment lastSeg = (LineSegment) resultSegs.get(resultSegs.size() - 1); + + firstSeg.p0 = lastSeg.p0; + resultSegs.remove(resultSegs.size() - 1); + return firstSeg; + } + } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java index f5bc8b15c4..9fe9a9fbc8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLineStringSimplifier.java @@ -16,8 +16,10 @@ import java.util.List; import org.locationtech.jts.algorithm.LineIntersector; +import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.algorithm.RobustLineIntersector; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.LineSegment; /** @@ -31,28 +33,19 @@ public class TaggedLineStringSimplifier { private LineIntersector li = new RobustLineIntersector(); - private LineSegmentIndex inputIndex = new LineSegmentIndex(); - private LineSegmentIndex outputIndex = new LineSegmentIndex(); + private LineSegmentIndex inputIndex; + private LineSegmentIndex outputIndex; + private ComponentJumpChecker jumpChecker; private TaggedLineString line; private Coordinate[] linePts; - private double distanceTolerance = 0.0; public TaggedLineStringSimplifier(LineSegmentIndex inputIndex, - LineSegmentIndex outputIndex) + LineSegmentIndex outputIndex, + ComponentJumpChecker crossChecker) { this.inputIndex = inputIndex; this.outputIndex = outputIndex; - } - - /** - * Sets the distance tolerance for the simplification. - * All vertices in the simplified geometry will be within this - * distance of the original geometry. - * - * @param distanceTolerance the approximation tolerance to use - */ - public void setDistanceTolerance(double distanceTolerance) { - this.distanceTolerance = distanceTolerance; + this.jumpChecker = crossChecker; } /** @@ -60,22 +53,28 @@ public void setDistanceTolerance(double distanceTolerance) { * using the distance tolerance specified. * * @param line the linestring to simplify + * @param distanceTolerance the simplification distance tolerance */ - void simplify(TaggedLineString line) + void simplify(TaggedLineString line, double distanceTolerance) { this.line = line; linePts = line.getParentCoordinates(); - simplifySection(0, linePts.length - 1, 0); + simplifySection(0, linePts.length - 1, 0, distanceTolerance); + + if (line.isRing() && CoordinateArrays.isRing(linePts)) { + simplifyRingEndpoint(distanceTolerance); + } } - private void simplifySection(int i, int j, int depth) + private void simplifySection(int i, int j, int depth, double distanceTolerance) { depth += 1; - int[] sectionIndex = new int[2]; - if((i+1) == j) { + //-- if section has only one segment just keep the segment + if ((i+1) == j) { LineSegment newSeg = line.getSegment(i); line.addToResult(newSeg); - // leave this segment in the input index, for efficiency + //-- do not add segment to output index, since it is unchanged + //-- leave the segment in the input index, for efficiency return; } @@ -95,25 +94,55 @@ private void simplifySection(int i, int j, int depth) double[] distance = new double[1]; int furthestPtIndex = findFurthestPoint(linePts, i, j, distance); + // flattening must be less than distanceTolerance - if (distance[0] > distanceTolerance) isValidToSimplify = false; - // test if flattened section would cause intersection - LineSegment candidateSeg = new LineSegment(); - candidateSeg.p0 = linePts[i]; - candidateSeg.p1 = linePts[j]; - sectionIndex[0] = i; - sectionIndex[1] = j; - if (hasBadIntersection(line, sectionIndex, candidateSeg)) { + if (distance[0] > distanceTolerance) { isValidToSimplify = false; } - + + if (isValidToSimplify) { + // test if flattened section would cause intersection or jump + LineSegment flatSeg = new LineSegment(); + flatSeg.p0 = linePts[i]; + flatSeg.p1 = linePts[j]; + isValidToSimplify = isTopologyValid(line, i, j, flatSeg); + } + if (isValidToSimplify) { LineSegment newSeg = flatten(i, j); line.addToResult(newSeg); return; } - simplifySection(i, furthestPtIndex, depth); - simplifySection(furthestPtIndex, j, depth); + simplifySection(i, furthestPtIndex, depth, distanceTolerance); + simplifySection(furthestPtIndex, j, depth, distanceTolerance); + } + + /** + * Simplifies the result segments on either side of a ring endpoint + * (which was not processed by the initial simplification). + * This ensures that simplification removes flat (collinear) endpoints. + */ + private void simplifyRingEndpoint(double distanceTolerance) + { + if (line.getResultSize() > line.getMinimumSize()) { + LineSegment firstSeg = line.getResultSegment(0); + LineSegment lastSeg = line.getResultSegment(-1); + + LineSegment simpSeg = new LineSegment(lastSeg.p0, firstSeg.p1); + //-- the excluded segments are the ones containing the endpoint + Coordinate endPt = firstSeg.p0; + if (simpSeg.distance(endPt) <= distanceTolerance + && isTopologyValid(line, firstSeg, lastSeg, simpSeg)) { + //-- don't know if segments are original or new, so remove from all indexes + inputIndex.remove(firstSeg); + inputIndex.remove(lastSeg); + outputIndex.remove(firstSeg); + outputIndex.remove(lastSeg); + + LineSegment flatSeg = line.removeRingEndpoint(); + outputIndex.add(flatSeg); + } + } } private int findFurthestPoint(Coordinate[] pts, int i, int j, double[] maxDistance) @@ -152,68 +181,126 @@ private LineSegment flatten(int start, int end) Coordinate p0 = linePts[start]; Coordinate p1 = linePts[end]; LineSegment newSeg = new LineSegment(p0, p1); - // update the indexes - remove(line, start, end); + // update the input and output indexes outputIndex.add(newSeg); + remove(line, start, end); + return newSeg; } - private boolean hasBadIntersection(TaggedLineString parentLine, - int[] sectionIndex, - LineSegment candidateSeg) + /** + * Tests if line topology remains valid after flattening a section of the line. + * The flattened section is being replaced by the flattening segment, + * so there is no need to test it + * (and it may well intersect the segment). + * + * @param line + * @param sectionStart + * @param sectionEnd + * @param flatSeg + * @return true if the flattening leaves valid topology + */ + private boolean isTopologyValid(TaggedLineString line, + int sectionStart, int sectionEnd, + LineSegment flatSeg) { - if (hasBadOutputIntersection(candidateSeg)) return true; - if (hasBadInputIntersection(parentLine, sectionIndex, candidateSeg)) return true; - return false; + if (hasOutputIntersection(flatSeg)) + return false; + if (hasInputIntersection(line, sectionStart, sectionEnd, flatSeg)) + return false; + if (jumpChecker.hasJump(line, sectionStart, sectionEnd, flatSeg)) + return false; + return true; + } + + private boolean isTopologyValid(TaggedLineString line, LineSegment seg1, LineSegment seg2, + LineSegment flatSeg) { + //-- if segments are already flat, topology is unchanged and so is valid + //-- (otherwise, output and/or input intersection test would report false positive) + if (isCollinear(seg1.p0, flatSeg)) + return true; + if (hasOutputIntersection(flatSeg)) + return false; + if (hasInputIntersection(flatSeg)) + return false; + if (jumpChecker.hasJump(line, seg1, seg2, flatSeg)) + return false; + return true; + } + + private boolean isCollinear(Coordinate pt, LineSegment seg) { + return Orientation.COLLINEAR == seg.orientationIndex(pt); } - private boolean hasBadOutputIntersection(LineSegment candidateSeg) + private boolean hasOutputIntersection(LineSegment flatSeg) { - List querySegs = outputIndex.query(candidateSeg); + List querySegs = outputIndex.query(flatSeg); for (Iterator i = querySegs.iterator(); i.hasNext(); ) { LineSegment querySeg = (LineSegment) i.next(); - if (hasInvalidIntersection(querySeg, candidateSeg)) { + if (hasInvalidIntersection(querySeg, flatSeg)) { return true; } } return false; } - private boolean hasBadInputIntersection(TaggedLineString parentLine, - int[] sectionIndex, - LineSegment candidateSeg) + private boolean hasInputIntersection(LineSegment flatSeg) + { + return hasInputIntersection(null, -1, -1, flatSeg); + } + + private boolean hasInputIntersection(TaggedLineString line, + int excludeStart, int excludeEnd, + LineSegment flatSeg) { - List querySegs = inputIndex.query(candidateSeg); + List querySegs = inputIndex.query(flatSeg); for (Iterator i = querySegs.iterator(); i.hasNext(); ) { TaggedLineSegment querySeg = (TaggedLineSegment) i.next(); - if (hasInvalidIntersection(querySeg, candidateSeg)) { - //-- don't fail if the segment is part of parent line - if (isInLineSection(parentLine, sectionIndex, querySeg)) - continue; - return true; + if (hasInvalidIntersection(querySeg, flatSeg)) { + /** + * Ignore the intersection if the intersecting segment is part of the section being collapsed + * to the candidate segment + */ + if (line != null + && isInLineSection(line, excludeStart, excludeEnd, querySeg)) + continue; + return true; } } return false; } /** - * Tests whether a segment is in a section of a TaggedLineString - * @param line - * @param sectionIndex - * @param seg - * @return + * Tests whether a segment is in a section of a TaggedLineString. + * Sections may wrap around the endpoint of the line, + * to support ring endpoint simplification. + * This is indicated by excludedStart > excludedEnd + * + * @param line the TaggedLineString containing the section segments + * @param excludeStart the index of the first segment in the excluded section + * @param excludeEnd the index of the last segment in the excluded section + * @param seg the segment to test + * @return true if the test segment intersects some segment in the line not in the excluded section */ private static boolean isInLineSection( TaggedLineString line, - int[] sectionIndex, + int excludeStart, int excludeEnd, TaggedLineSegment seg) { - // not in this line + //-- test segment is not in this line if (seg.getParent() != line.getParent()) return false; int segIndex = seg.getIndex(); - if (segIndex >= sectionIndex[0] && segIndex < sectionIndex[1]) + if (excludeStart <= excludeEnd) { + //-- section is contiguous + if (segIndex >= excludeStart && segIndex < excludeEnd) + return true; + } + else { + //-- section wraps around the end of a ring + if (segIndex >= excludeStart || segIndex <= excludeEnd) return true; + } return false; } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java index e14aa7404f..d3642ac082 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TaggedLinesSimplifier.java @@ -25,6 +25,7 @@ class TaggedLinesSimplifier { private LineSegmentIndex inputIndex = new LineSegmentIndex(); private LineSegmentIndex outputIndex = new LineSegmentIndex(); + private double distanceTolerance = 0.0; public TaggedLinesSimplifier() @@ -49,14 +50,15 @@ public void setDistanceTolerance(double distanceTolerance) { * @param taggedLines the collection of lines to simplify */ public void simplify(Collection taggedLines) { + ComponentJumpChecker jumpChecker = new ComponentJumpChecker(taggedLines); + for (Iterator i = taggedLines.iterator(); i.hasNext(); ) { inputIndex.add((TaggedLineString) i.next()); } for (Iterator i = taggedLines.iterator(); i.hasNext(); ) { TaggedLineStringSimplifier tlss - = new TaggedLineStringSimplifier(inputIndex, outputIndex); - tlss.setDistanceTolerance(distanceTolerance); - tlss.simplify((TaggedLineString) i.next()); + = new TaggedLineStringSimplifier(inputIndex, outputIndex, jumpChecker); + tlss.simplify((TaggedLineString) i.next(), distanceTolerance); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java index 33ea6e348b..5eb8ab58e2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/TopologyPreservingSimplifier.java @@ -28,8 +28,7 @@ * Simplifies a geometry and ensures that * the result is a valid geometry having the * same dimension and number of components as the input, - * and with the components having the same topological - * relationship. + * and with the components having the same topological relationship. *

    * If the input is a polygonal geometry * ( {@link Polygon} or {@link MultiPolygon} ): @@ -45,6 +44,9 @@ * any intersecting line segments, this property * will be preserved in the output. *

    + * For polygonal geometries and LinearRings the ring endpoint will be simplified. + * For LineStrings the endpoints will be unchanged. + *

    * For all geometry types, the result will contain * enough vertices to ensure validity. For polygons * and closed linear geometries, the result will have at @@ -57,19 +59,6 @@ *

    * The simplification uses a maximum-distance difference algorithm * similar to the Douglas-Peucker algorithm. - * - *

    KNOWN BUGS

    - *
      - *
    • May create invalid topology if there are components which are - * small relative to the tolerance value. - * In particular, if a small hole is very near an edge, it is possible for the edge to be moved by - * a relatively large tolerance value and end up with the hole outside the result shell - * (or inside another hole). - * Similarly, it is possible for a small polygon component to end up inside - * a nearby larger polygon. - * A workaround is to test for this situation in post-processing and remove - * any invalid holes or polygons. - *
    * * @author Martin Davis * @see DouglasPeuckerSimplifier @@ -86,7 +75,7 @@ public static Geometry simplify(Geometry geom, double distanceTolerance) private Geometry inputGeom; private TaggedLinesSimplifier lineSimplifier = new TaggedLinesSimplifier(); - private Map linestringMap; + private Map linestringMap; public TopologyPreservingSimplifier(Geometry inputGeom) { @@ -113,7 +102,7 @@ public Geometry getResultGeometry() // empty input produces an empty result if (inputGeom.isEmpty()) return inputGeom.copy(); - linestringMap = new HashMap(); + linestringMap = new HashMap(); inputGeom.apply(new LineStringMapBuilderFilter(this)); lineSimplifier.simplify(linestringMap.values()); Geometry result = (new LineStringTransformer(linestringMap)).transform(inputGeom); @@ -123,9 +112,9 @@ public Geometry getResultGeometry() static class LineStringTransformer extends GeometryTransformer { - private Map linestringMap; + private Map linestringMap; - public LineStringTransformer(Map linestringMap) { + public LineStringTransformer(Map linestringMap) { this.linestringMap = linestringMap; } @@ -134,7 +123,7 @@ protected CoordinateSequence transformCoordinates(CoordinateSequence coords, Geo if (coords.size() == 0) return null; // for linear components (including rings), simplify the linestring if (parent instanceof LineString) { - TaggedLineString taggedLine = (TaggedLineString) linestringMap.get(parent); + TaggedLineString taggedLine = linestringMap.get(parent); return createCoordinateSequence(taggedLine.getResultCoordinates()); } // for anything else (e.g. points) just copy the coordinates @@ -175,7 +164,8 @@ public void filter(Geometry geom) if (line.isEmpty()) return; int minSize = ((LineString) line).isClosed() ? 4 : 2; - TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize); + boolean isRing = (line instanceof LinearRing) ? true : false; + TaggedLineString taggedLine = new TaggedLineString((LineString) line, minSize, isRing); tps.linestringMap.put(line, taggedLine); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java index 220a3cb00d..8bb4de1af9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/IncrementalDelaunayTriangulator.java @@ -15,6 +15,8 @@ import java.util.Collection; import java.util.Iterator; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.triangulate.quadedge.LocateFailureException; import org.locationtech.jts.triangulate.quadedge.QuadEdge; import org.locationtech.jts.triangulate.quadedge.QuadEdgeSubdivision; @@ -32,6 +34,7 @@ public class IncrementalDelaunayTriangulator { private QuadEdgeSubdivision subdiv; private boolean isUsingTolerance = false; + private boolean isForceConvex = true; /** * Creates a new triangulator using the given {@link QuadEdgeSubdivision}. @@ -46,6 +49,22 @@ public IncrementalDelaunayTriangulator(QuadEdgeSubdivision subdiv) { } + /** + * Sets whether the triangulation is forced to have a convex boundary. Because + * of the use of a finite-size frame, this condition requires special logic to + * enforce. The default is true, since this is a requirement for some uses of + * Delaunay Triangulations (such as Concave Hull generation). However, forcing + * the triangulation boundary to be convex may cause the overall frame + * triangulation to be non-Delaunay. This can cause a problem for Voronoi + * generation, so the logic can be disabled via this method. + * + * @param isForceConvex true if the triangulation boundary is forced to be + * convex + */ + public void forceConvex(boolean isForceConvex) { + this.isForceConvex = isForceConvex; + } + /** * Inserts all sites in a collection. The inserted vertices MUST be * unique up to the provided tolerance value. (i.e. no two vertices should be @@ -105,19 +124,89 @@ else if (subdiv.isOnEdge(e, v.getCoordinate())) { e = base.oPrev(); } while (e.lNext() != startEdge); - // Examine suspect edges to ensure that the Delaunay condition - // is satisfied. + /** + * Examine suspect edges to ensure that the Delaunay condition is satisfied. + * If it is not, flip the edge and continue scanning. + * + * Since the frame is not infinitely far away, + * edges which touch the frame or are adjacent to it require special logic + * to ensure the inner triangulation maintains a convex boundary. + */ do { - QuadEdge t = e.oPrev(); - if (t.dest().rightOf(e) && v.isInCircle(e.orig(), t.dest(), e.dest())) { - QuadEdge.swap(e); - e = e.oPrev(); - } else if (e.oNext() == startEdge) { - return base; // no more suspect edges. - } else { - e = e.oNext().lPrev(); - } - } while (true); + //-- general case - flip if vertex is in circumcircle + QuadEdge t = e.oPrev(); + boolean doFlip = t.dest().rightOf(e) && v.isInCircle(e.orig(), t.dest(), e.dest()); + + if (isForceConvex) { + //-- special cases to ensure triangulation boundary is convex + if (isConcaveBoundary(e)) { + //-- flip if the triangulation boundary is concave + doFlip = true; + } + else if (isBetweenFrameAndInserted(e, v)) { + //-- don't flip if edge lies between the inserted vertex and a frame vertex + doFlip = false; + } + } + + if (doFlip) { + //-- flip the edge within its quadrilateral + QuadEdge.swap(e); + e = e.oPrev(); + continue; + } + + if (e.oNext() == startEdge) { + return base; // no more suspect edges. + } + //-- check next edge + e = e.oNext().lPrev(); + } while (true); } + /** + * Tests if a edge touching a frame vertex + * creates a concavity in the triangulation boundary. + * + * @param e the edge to test + * @return true if the triangulation boundary is concave at the edge + */ + private boolean isConcaveBoundary(QuadEdge e) { + if (subdiv.isFrameVertex(e.dest())) { + return isConcaveAtOrigin(e); + } + if (subdiv.isFrameVertex(e.orig())) { + return isConcaveAtOrigin(e.sym()); + } + return false; + } + + /** + * Tests if the quadrilateral surrounding an edge is concave at the edge origin. + * Used to determine if the triangulation boundary has a concavity. + * @param e + * @return + */ + private static boolean isConcaveAtOrigin(QuadEdge e) { + Coordinate p = e.orig().getCoordinate(); + Coordinate pp = e.oPrev().dest().getCoordinate(); + Coordinate pn = e.oNext().dest().getCoordinate(); + boolean isConcave = Orientation.COUNTERCLOCKWISE == Orientation.index(pp, pn, p); + return isConcave; + } + + /** + * Edges whose adjacent triangles contain + * a frame vertex and the inserted vertex must not be flipped. + * + * @param e the edge to test + * @param vInsert the inserted vertex + * @return true if the edge is between the frame and inserted vertex + */ + private boolean isBetweenFrameAndInserted(QuadEdge e, Vertex vInsert) { + Vertex v1 = e.oNext().dest(); + Vertex v2 = e.oPrev().dest(); + return (v1 == vInsert && subdiv.isFrameVertex(v2)) + || (v2 == vInsert && subdiv.isFrameVertex(v1)); + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java index 37dc9d29e5..5064fbdc84 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/VoronoiDiagramBuilder.java @@ -125,9 +125,14 @@ private void create() List vertices = DelaunayTriangulationBuilder.toVertices(siteCoords); subdiv = new QuadEdgeSubdivision(diagramEnv, tolerance); IncrementalDelaunayTriangulator triangulator = new IncrementalDelaunayTriangulator(subdiv); + /** + * Avoid creating very narrow triangles along triangulation boundary. + * These otherwise can cause malformed Voronoi cells. + */ + triangulator.forceConvex(false); triangulator.insertSites(vertices); } - + /** * Gets the {@link QuadEdgeSubdivision} which models the computed diagram. * @@ -155,11 +160,20 @@ public Geometry getDiagram(GeometryFactory geomFact) create(); Geometry polys = subdiv.getVoronoiDiagram(geomFact); - // clip polys to diagramEnv + /* + System.out.println(polys); + Geometry tris = subdiv.getTriangles(true, geomFact); + System.out.println(tris); + if (! subdiv.isFrameDelaunay()) { + throw new IllegalStateException("Triangulation frame is not Delaunay"); + } + //*/ + + //-- clip polys to diagramEnv return clipGeometryCollection(polys, diagramEnv); } - - private static Geometry clipGeometryCollection(Geometry geom, Envelope clipEnv) + + private static Geometry clipGeometryCollection(Geometry geom, Envelope clipEnv) { Geometry clipPoly = geom.getFactory().toGeometry(clipEnv); List clipped = new ArrayList(); diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java index 2a2e9c57b7..ba98c107b4 100644 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/quadedge/QuadEdgeSubdivision.java @@ -80,7 +80,7 @@ public static void getTriangleEdges(QuadEdge startQE, QuadEdge[] triEdge) { private final static double EDGE_COINCIDENCE_TOL_FACTOR = 1000; - private static final double FRAME_SIZE_FACTOR = 100.0; + private static final double FRAME_SIZE_FACTOR = 10.0; // debugging only - preserve current subdiv statically // private static QuadEdgeSubdivision currentSubdiv; @@ -617,6 +617,24 @@ public List getPrimaryEdges(boolean includeFrame) { return edges; } + /** + * Gets the edges which touch frame vertices. The returned edges are oriented so + * that their origin is a frame vertex. + * + * @return the edges which touch the frame + */ + public List getFrameEdges() { + List edges = getPrimaryEdges(true); + List frameEdges = new ArrayList(); + for (QuadEdge e : edges) { + if (isFrameEdge(e)) { + QuadEdge fe = isFrameVertex(e.orig()) ? e : e.sym(); + frameEdges.add(fe); + } + } + return frameEdges; + } + /** * A TriangleVisitor which computes and sets the * circumcentre as the origin of the dual @@ -867,6 +885,25 @@ public Geometry getTriangles(GeometryFactory geomFact) { return geomFact.createGeometryCollection(tris); } + /** + * Gets the geometry for the triangles in a triangulated subdivision as a {@link GeometryCollection} + * of triangular {@link Polygon}s, optionally including the frame triangles. + * + * @param includeFrame true if the frame triangles should be included + * @param geomFact the GeometryFactory to use + * @return a GeometryCollection of triangular Polygons + */ + public Geometry getTriangles(boolean includeFrame, GeometryFactory geomFact) { + List triPtsList = getTriangleCoordinates(includeFrame); + Polygon[] tris = new Polygon[triPtsList.size()]; + int i = 0; + for (Iterator it = triPtsList.iterator(); it.hasNext();) { + Coordinate[] triPt = (Coordinate[]) it.next(); + tris[i++] = geomFact.createPolygon(geomFact.createLinearRing(triPt)); + } + return geomFact.createGeometryCollection(tris); + } + /** * Gets the cells in the Voronoi diagram for this triangulation. * The cells are returned as a {@link GeometryCollection} of {@link Polygon}s @@ -957,4 +994,61 @@ public Polygon getVoronoiCellPolygon(QuadEdge qe, GeometryFactory geomFact) return cellPoly; } + /** + * Tests whether a subdivision is a valid Delaunay Triangulation. + * This is the case iff every edge is locally Delaunay, meaning that + * the apex of one adjacent triangle is not inside the circumcircle + * of the other adjacent triangle. + * + * @return true if the subdivision is Delaunay + */ + public boolean isDelaunay() { + List edges = getPrimaryEdges(true); + for (QuadEdge e : edges) { + Vertex a0 = e.oPrev().dest(); + Vertex a1 = e.oNext().dest(); + boolean isDelaunay = ! a1.isInCircle(e.orig(), a0, e.dest()); + if (! isDelaunay) { + /* + System.out.println(WKTWriter.toLineString(new Coordinate[] { + e.orig().getCoordinate(), a0.getCoordinate(), e.dest().getCoordinate() + })); + */ + return false; + } + } + return true; + } + + /** + * Tests whether the frame edges are Delaunay + * @return true if the frame edges are Delaunay + */ + /* + public boolean isFrameDelaunay() { + List edges = getFrameEdges(); + for (QuadEdge e : edges) { + Vertex a0 = e.oPrev().dest(); + Vertex a1 = e.oNext().dest(); + boolean isDelaunay = ! a1.isInCircle(e.orig(), a0, e.dest()); + if (! isDelaunay) { + + return false; + } + } + return true; + } + + public void makeFrameDelaunay() { + List edges = getFrameEdges(); + for (QuadEdge e : edges) { + Vertex a0 = e.oPrev().dest(); + Vertex a1 = e.oNext().dest(); + boolean isDelaunay = ! a1.isInCircle(e.orig(), a0, e.dest()); + if (! isDelaunay) { + QuadEdge.swap(e); + } + } + } + */ } diff --git a/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java b/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java index 7ba7dd2ec9..90f5bb26a8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java +++ b/modules/core/src/main/java/org/locationtech/jts/util/GeometricShapeFactory.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.util; +import org.locationtech.jts.algorithm.Angle; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -222,8 +223,8 @@ public Polygon createEllipse() int iPt = 0; for (int i = 0; i < nPts; i++) { double ang = i * (2 * Math.PI / nPts); - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double x = xRadius * Angle.cosSnap(ang) + centreX; + double y = yRadius * Angle.sinSnap(ang) + centreY; pts[iPt++] = coord(x, y); } pts[iPt] = new Coordinate(pts[0]); @@ -319,16 +320,16 @@ public LineString createArc( double centreY = env.getMinY() + yRadius; double angSize = angExtent; - if (angSize <= 0.0 || angSize > 2 * Math.PI) - angSize = 2 * Math.PI; + if (angSize <= 0.0 || angSize > Angle.PI_TIMES_2) + angSize = Angle.PI_TIMES_2; double angInc = angSize / (nPts - 1); Coordinate[] pts = new Coordinate[nPts]; int iPt = 0; for (int i = 0; i < nPts; i++) { double ang = startAng + i * angInc; - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double x = xRadius * Angle.cosSnap(ang) + centreX; + double y = yRadius * Angle.sinSnap(ang) + centreY; pts[iPt++] = coord(x, y); } LineString line = geomFact.createLineString(pts); @@ -353,8 +354,8 @@ public Polygon createArcPolygon(double startAng, double angExtent) { double centreY = env.getMinY() + yRadius; double angSize = angExtent; - if (angSize <= 0.0 || angSize > 2 * Math.PI) - angSize = 2 * Math.PI; + if (angSize <= 0.0 || angSize > Angle.PI_TIMES_2) + angSize = Angle.PI_TIMES_2; double angInc = angSize / (nPts - 1); // double check = angInc * nPts; // double checkEndAng = startAng + check; @@ -366,8 +367,8 @@ public Polygon createArcPolygon(double startAng, double angExtent) { for (int i = 0; i < nPts; i++) { double ang = startAng + angInc * i; - double x = xRadius * Math.cos(ang) + centreX; - double y = yRadius * Math.sin(ang) + centreY; + double x = xRadius * Angle.cosSnap(ang) + centreX; + double y = yRadius * Angle.sinSnap(ang) + centreY; pts[iPt++] = coord(x, y); } pts[iPt++] = coord(centreX, centreY); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java index 417bb1f538..9f005cef06 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/AngleTest.java @@ -158,7 +158,41 @@ public void testAngleBisector() { assertEquals(45, Math.toDegrees(Angle.bisector(p(13,10), p(10,10), p(10,20))), 0.01); } - + + public void testSinCosSnap() { + + // -720 to 720 degrees with 1 degree increments + for (int angdeg = -720; angdeg <= 720; angdeg++) { + double ang = Angle.toRadians(angdeg); + + double rSin = Angle.sinSnap(ang); + double rCos = Angle.cosSnap(ang); + + double cSin = Math.sin(ang); + double cCos = Math.cos(ang); + if ( (angdeg % 90) == 0 ) { + // not always the same for multiples of 90 degrees + assertTrue(Math.abs(rSin - cSin) < 1e-15); + assertTrue(Math.abs(rCos - cCos) < 1e-15); + } else { + assertEquals(rSin, cSin); + assertEquals(rCos, cCos); + } + + } + + // use radian increments that don't snap to exact degrees or zero + for (double angrad = -6.3; angrad < 6.3; angrad += 0.013) { + + double rSin = Angle.sinSnap(angrad); + double rCos = Angle.cosSnap(angrad); + + assertEquals(rSin, Math.sin(angrad)); + assertEquals(rCos, Math.cos(angrad)); + + } + } + private static Coordinate p(double x, double y) { return new Coordinate(x, y); } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java index bed933f7fe..52aee7761d 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/InteriorPointTest.java @@ -50,6 +50,10 @@ public void testPolygonZeroArea() { checkInteriorPoint(read("POLYGON ((10 10, 10 10, 10 10, 10 10))"), new Coordinate(10, 10)); } + public void testMultiLineWithEmpty() { + checkInteriorPoint(read("MULTILINESTRING ((0 0, 1 1), EMPTY)"), new Coordinate(0, 0)); + } + public void testAll() throws Exception { checkInteriorPointFile(TestFiles.getResourceFilePath("world.wkt")); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/IntersectionTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/IntersectionTest.java index c26af8b92f..4bd8aa02c9 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/IntersectionTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/IntersectionTest.java @@ -4,6 +4,14 @@ import junit.framework.TestCase; import junit.textui.TestRunner; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequenceFactory; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.impl.CoordinateArraySequenceFactory; +import org.locationtech.jts.io.WKTReader; public class IntersectionTest extends TestCase { private static final double MAX_ABS_ERROR = 1e-5; @@ -71,6 +79,41 @@ public void testLineSegNone() { checkIntersectionLineSegmentNull( 0, 0, 0, 1, 2, 9, 1, 9 ); } + public void testIntersectionXY() throws Exception { + // intersection with dim 3 x dim3 + WKTReader reader = new WKTReader(); + Geometry poly1 = reader.read("POLYGON((0 0 0, 0 10000 2, 10000 10000 2, 10000 0 0, 0 0 0))"); + Geometry clipArea = reader.read("POLYGON((0 0, 0 2500, 2500 2500, 2500 0, 0 0))"); + Geometry clipped1 = poly1.intersection(clipArea); + + // intersection with dim 3 x dim 2 + GeometryFactory gf = poly1.getFactory(); + CoordinateSequenceFactory csf = gf.getCoordinateSequenceFactory(); + double xmin = 0.0; + double xmax = 2500.0; + double ymin = 0.0; + double ymax = 2500.0; + + CoordinateSequence cs = csf.create(5,2); + cs.setOrdinate(0, 0, xmin); + cs.setOrdinate(0, 1, ymin); + cs.setOrdinate(1, 0, xmin); + cs.setOrdinate(1, 1, ymax); + cs.setOrdinate(2, 0, xmax); + cs.setOrdinate(2, 1, ymax); + cs.setOrdinate(3, 0, xmax); + cs.setOrdinate(3, 1, ymin); + cs.setOrdinate(4, 0, xmin); + cs.setOrdinate(4, 1, ymin); + + LinearRing bounds = gf.createLinearRing(cs); + + Polygon fence = gf.createPolygon(bounds, null); + Geometry clipped2 = poly1.intersection(fence); + + assertTrue(clipped1.equals(clipped2)); + } + //================================================== private void checkIntersection(double p1x, double p1y, double p2x, double p2y, diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationOnLineTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationTest.java similarity index 54% rename from modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationOnLineTest.java rename to modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationTest.java index f7399b80da..216308f2b3 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationOnLineTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocationTest.java @@ -12,34 +12,31 @@ package org.locationtech.jts.algorithm; import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; -import junit.framework.TestCase; import junit.textui.TestRunner; import test.jts.GeometryTestCase; /** - * Tests {@link PointLocation#isOnLine(Coordinate, Coordinate[])}. + * Tests {@link PointLocation}. * * @version 1.15 */ -public class PointLocationOnLineTest extends GeometryTestCase { +public class PointLocationTest extends GeometryTestCase { + public static void main(String args[]) { - TestRunner.run(PointLocationOnLineTest.class); + TestRunner.run(PointLocationTest.class); } - public PointLocationOnLineTest(String name) { + public PointLocationTest(String name) { super(name); } - public void testOnVertex() throws Exception { + public void testOnLineOnVertex() throws Exception { checkOnLine(20, 20, "LINESTRING (0 00, 20 20, 30 30)", true); } - public void testOnSegment() throws Exception { + public void testOnLineInSegment() throws Exception { checkOnLine(10, 10, "LINESTRING (0 0, 20 20, 0 40)", true); checkOnLine(10, 30, "LINESTRING (0 0, 20 20, 0 40)", true); } @@ -48,6 +45,30 @@ public void testNotOnLine() throws Exception { checkOnLine(0, 100, "LINESTRING (10 10, 20 10, 30 10)", false); } + public void testOnSegment() { + checkOnSegment(5, 5, "LINESTRING(0 0, 9 9)", true); + checkOnSegment(0, 0, "LINESTRING(0 0, 9 9)", true); + checkOnSegment(9, 9, "LINESTRING(0 0, 9 9)", true); + } + + public void testNotOnSegment() { + checkOnSegment(5, 6, "LINESTRING(0 0, 9 9)", false); + checkOnSegment(10, 10, "LINESTRING(0 0, 9 9)", false); + checkOnSegment(9, 9.00001, "LINESTRING(0 0, 9 9)", false); + } + + public void testOnZeroLengthSegment() { + checkOnSegment(1, 1, "LINESTRING(1 1, 1 1)", true); + checkOnSegment(1, 2, "LINESTRING(1 1, 1 1)", false); + } + + private void checkOnSegment(double x, double y, String wktLine, boolean expected) { + LineString line = (LineString) read(wktLine); + Coordinate p0 = line.getCoordinateN(0); + Coordinate p1 = line.getCoordinateN(1); + assertTrue(expected == PointLocation.isOnSegment(new Coordinate(x,y), p0, p1)); + } + void checkOnLine(double x, double y, String wktLine, boolean expected) { LineString line = (LineString) read(wktLine); assertTrue(expected == PointLocation.isOnLine(new Coordinate(x,y), line.getCoordinates())); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java index 6cc0a501e8..8b3036473b 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PointLocatorTest.java @@ -67,7 +67,17 @@ public void testPolygon() throws Exception { assertEquals(Location.INTERIOR, pointLocator.locate(new Coordinate(190, 150), polygon)); } - private void runPtLocator(int expected, Coordinate pt, String wkt) + public void testRingBoundaryNodeRule() throws Exception + { + String wkt = "LINEARRING(10 10, 10 20, 20 10, 10 10)"; + Coordinate pt = new Coordinate(10, 10); + runPtLocator(Location.INTERIOR, pt, wkt, BoundaryNodeRule.MOD2_BOUNDARY_RULE); + runPtLocator(Location.BOUNDARY, pt, wkt, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE); + runPtLocator(Location.INTERIOR, pt, wkt, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE); + runPtLocator(Location.BOUNDARY, pt, wkt, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE); + } + + private void runPtLocator(int expected, Coordinate pt, String wkt) throws Exception { Geometry geom = reader.read(wkt); @@ -76,4 +86,14 @@ private void runPtLocator(int expected, Coordinate pt, String wkt) assertEquals(expected, loc); } + private void runPtLocator(int expected, Coordinate pt, String wkt, + BoundaryNodeRule bnr) + throws Exception + { + Geometry geom = reader.read(wkt); + PointLocator pointLocator = new PointLocator(bnr); + int loc = pointLocator.locate(pt, geom); + assertEquals(expected, loc); + } + } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java index 7658007186..5ee5bd6424 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/PolygonNodeTopologyTest.java @@ -16,27 +16,37 @@ public static void main(String args[]) { public void testNonCrossing() { checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", - "LINESTRING (1000 500, 1000 1000, 500 1500)", false); + "LINESTRING (1000 500, 1000 1000, 500 1500)"); } - public void testCrossingQuadrant2() { - checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", + public void testNonCrossingQuadrant2() { + checkNonCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", "LINESTRING (300 1200, 1000 1000, 500 1500)"); } - public void testCrossingQuadrant4() { - checkCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", + public void testNonCrossingQuadrant4() { + checkNonCrossing("LINESTRING (500 1000, 1000 1000, 1000 1500)", "LINESTRING (1000 500, 1000 1000, 1500 1000)"); } + public void testNonCrossingCollinear() { + checkNonCrossing("LINESTRING (3 1, 5 5, 9 9)", + "LINESTRING (2 1, 5 5, 9 9)"); + } + + public void testNonCrossingBothCollinear() { + checkNonCrossing("LINESTRING (3 1, 5 5, 9 9)", + "LINESTRING (3 1, 5 5, 9 9)"); + } + public void testInteriorSegment() { checkInterior("LINESTRING (5 9, 5 5, 9 5)", - "LINESTRING (5 5, 9 9)"); + "LINESTRING (5 5, 0 0)"); } public void testExteriorSegment() { checkExterior("LINESTRING (5 9, 5 5, 9 5)", - "LINESTRING (5 5, 0 0)"); + "LINESTRING (5 5, 9 9)"); } //----------------------------------------------- @@ -44,11 +54,15 @@ private void checkCrossing(String wktA, String wktB) { checkCrossing(wktA, wktB, true); } + private void checkNonCrossing(String wktA, String wktB) { + checkCrossing(wktA, wktB, false); + } + private void checkCrossing(String wktA, String wktB, boolean isExpected) { Coordinate[] a = readPts(wktA); Coordinate[] b = readPts(wktB); // assert: a[1] = b[1] - boolean isCrossing = ! PolygonNodeTopology.isCrossing(a[1], a[0], a[2], b[0], b[2]); + boolean isCrossing = PolygonNodeTopology.isCrossing(a[1], a[0], a[2], b[0], b[2]); assertTrue(isCrossing == isExpected); } @@ -64,7 +78,7 @@ private void checkInteriorSegment(String wktA, String wktB, boolean isExpected) Coordinate[] a = readPts(wktA); Coordinate[] b = readPts(wktB); // assert: a[1] = b[1] - boolean isInterior = ! PolygonNodeTopology.isInteriorSegment(a[1], a[0], a[2], b[1]); + boolean isInterior = PolygonNodeTopology.isInteriorSegment(a[1], a[0], a[2], b[1]); assertTrue(isInterior == isExpected); } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java index dcece33f37..b5bcaed16a 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/hull/ConcaveHullTest.java @@ -128,6 +128,21 @@ public void testAlphaWithHolesCircle() { "POLYGON ((20 90, 40 96, 56 95, 80 90, 90 70, 95 45, 90 20, 80 10, 60 15, 45 5, 20 10, 10 20, 5 40, 11 60, 20 70, 20 90), (40 80, 15 45, 21 30, 40 20, 70 20, 80 40, 80 60, 70 80, 40 80))" ); } + //------------------------------------------------ + + // These tests test that the computed Delaunay triangulation is correct + // See https://github.com/locationtech/jts/pull/1004 + + public void testRobust_GEOS946() { + checkHullByLengthRatio("MULTIPOINT ((113.56577197798602 22.80081530883069),(113.565723279387 22.800815316487014),(113.56571548761124 22.80081531771092),(113.56571548780202 22.800815317674463),(113.56577197817877 22.8008153088047),(113.56577197798602 22.80081530883069))", + 0.75, "POLYGON ((113.56571548761124 22.80081531771092, 113.565723279387 22.800815316487014, 113.56577197798602 22.80081530883069, 113.56577197817877 22.8008153088047, 113.56571548780202 22.800815317674463, 113.56571548761124 22.80081531771092))" ); + } + + public void testRobust_GEOS946_2() { + checkHullByLengthRatio("MULTIPOINT ((584245.72096874 7549593.72686167), (584251.71398371 7549594.01629478), (584242.72446125 7549593.58214511), (584230.73978847 7549592.9760418), (584233.73581213 7549593.13045099), (584236.7318358 7549593.28486019), (584239.72795377 7549593.43742855), (584227.74314188 7549592.83423486))", + 0.75, "POLYGON ((584227.74314188 7549592.83423486, 584239.72795377 7549593.43742855, 584242.72446125 7549593.58214511, 584245.72096874 7549593.72686167, 584251.71398371 7549594.01629478, 584230.73978847 7549592.9760418, 584227.74314188 7549592.83423486))" ); + } + //========================================================================== private void checkHullByLengthRatio(String wkt, double threshold, String wktExpected) { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index 2289e5a5b2..1a70f46c6c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -45,22 +45,6 @@ public void testHolesAndFillWithDifferentEndpoints() { "MULTILINESTRING ((0 10, 0 0, 10 0, 10 10, 0 10), (1 1, 1 9, 4 8, 9 9), (1 1, 9 1, 9 9), (1 1, 9 9))"); } - public void testTouchingSquares() { - String wkt = "MULTIPOLYGON (((2 7, 2 8, 3 8, 3 7, 2 7)), ((1 6, 1 7, 2 7, 2 6, 1 6)), ((0 7, 0 8, 1 8, 1 7, 0 7)), ((0 5, 0 6, 1 6, 1 5, 0 5)), ((2 5, 2 6, 3 6, 3 5, 2 5)))"; - checkEdgesSelected(wkt, 1, - "MULTILINESTRING ((1 6, 0 6, 0 5, 1 5, 1 6), (1 6, 1 7), (1 6, 2 6), (1 7, 0 7, 0 8, 1 8, 1 7), (1 7, 2 7), (2 6, 2 5, 3 5, 3 6, 2 6), (2 6, 2 7), (2 7, 2 8, 3 8, 3 7, 2 7))"); - checkEdgesSelected(wkt, 2, - "MULTILINESTRING EMPTY"); - } - - public void testAdjacentSquares() { - String wkt = "GEOMETRYCOLLECTION (POLYGON ((1 3, 2 3, 2 2, 1 2, 1 3)), POLYGON ((3 3, 3 2, 2 2, 2 3, 3 3)), POLYGON ((3 1, 2 1, 2 2, 3 2, 3 1)), POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)))"; - checkEdgesSelected(wkt, 1, - "MULTILINESTRING ((1 2, 1 1, 2 1), (1 2, 1 3, 2 3), (2 1, 3 1, 3 2), (2 3, 3 3, 3 2))"); - checkEdgesSelected(wkt, 2, - "MULTILINESTRING ((1 2, 2 2), (2 1, 2 2), (2 2, 2 3), (2 2, 3 2))"); - } - public void testMultiPolygons() { checkEdges("GEOMETRYCOLLECTION (MULTIPOLYGON (((5 9, 2.5 7.5, 1 5, 5 5, 5 9)), ((5 5, 9 5, 7.5 2.5, 5 1, 5 5))), MULTIPOLYGON (((5 9, 6.5 6.5, 9 5, 5 5, 5 9)), ((1 5, 5 5, 5 1, 3.5 3.5, 1 5))))", "MULTILINESTRING ((1 5, 2.5 7.5, 5 9), (1 5, 3.5 3.5, 5 1), (1 5, 5 5), (5 1, 5 5), (5 1, 7.5 2.5, 9 5), (5 5, 5 9), (5 5, 9 5), (5 9, 6.5 6.5, 9 5))" @@ -76,16 +60,6 @@ private void checkEdges(String wkt, String wktExpected) { checkEqual(expected, edgeLines); } - private void checkEdgesSelected(String wkt, int ringCount, String wktExpected) { - Geometry geom = read(wkt); - Geometry[] polygons = toArray(geom); - CoverageRingEdges covEdges = CoverageRingEdges.create(polygons); - List edges = covEdges.selectEdges(ringCount); - MultiLineString edgeLines = toArray(edges, geom.getFactory()); - Geometry expected = read(wktExpected); - checkEqual(expected, edgeLines); - } - private MultiLineString toArray(List edges, GeometryFactory geomFactory) { LineString[] lines = new LineString[edges.size()]; for (int i = 0; i < edges.size(); i++) { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index a5822e956d..c14ab56c7c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -58,10 +58,10 @@ public void testNoopMulti() { public void testRepeatedPointRemoved() { checkResult(readArray( - "POLYGON ((5 9, 6.5 6.5, 9 5, 5 5, 5 5, 5 9))" ), + "POLYGON ((2 9, 7 6, 9 1, 2 1, 2 1, 3 6, 2 9))" ), 2, readArray( - "POLYGON ((5 5, 5 9, 9 5, 5 5))" ) + "POLYGON ((2 1, 2 9, 7 6, 9 1, 2 1))" ) ); } @@ -118,10 +118,10 @@ public void testMultiPolygons() { public void testSingleRingNoCollapse() { checkResult(readArray( - "POLYGON ((10 50, 60 90, 70 50, 60 10, 10 50))" ), + "POLYGON ((10 50, 50 90, 60 90, 70 50, 60 10, 10 50))" ), 100000, readArray( - "POLYGON ((10 50, 60 90, 60 10, 10 50))" ) + "POLYGON ((10 50, 60 90, 70 50, 60 10, 10 50))" ) ); } @@ -146,8 +146,8 @@ public void testFilledHole() { "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (50 20, 20 30, 20 80, 60 50, 80 20, 50 20))" ), 28, readArray( - "POLYGON ((20 30, 20 80, 80 20, 20 30))", - "POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (20 30, 80 20, 20 80, 20 30))" ) + "POLYGON ((20 30, 20 80, 60 50, 80 20, 20 30))", + "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 30, 20 80, 60 50, 80 20, 20 30))" ) ); } @@ -158,26 +158,26 @@ public void testTouchingHoles() { "POLYGON (( 12 6, 12 7, 13 7, 13 9, 14 9, 14 6, 12 6 ))"), 1.0, readArray( - "POLYGON ((0 0, 0 11, 19 11, 19 0, 0 0), (4 5, 12 5, 12 6, 10 6, 9 9, 6 8, 6 6, 4 5), (12 6, 14 6, 14 9, 12 6))", + "POLYGON ((0 0, 0 11, 19 11, 19 0, 0 0), (4 5, 12 5, 12 6, 10 6, 9 9, 6 8, 6 6, 4 5), (12 6, 14 6, 14 9, 13 7, 12 6))", "POLYGON ((4 5, 6 6, 6 8, 9 9, 10 6, 12 6, 12 5, 4 5))", - "POLYGON ((12 6, 14 9, 14 6, 12 6))" ) + " POLYGON ((12 6, 14 6, 14 9, 13 7, 12 6))" ) ); } - public void testHoleTouchingShell() { + public void testInnerHoleTouchingShell() { checkResultInner(readArray( - "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (170 220, 170 160, 200 140, 200 250, 170 220), (170 250, 200 250, 200 300, 170 250))", - "POLYGON ((170 220, 200 250, 200 140, 170 160, 170 220))", - "POLYGON ((170 250, 200 300, 200 250, 170 250))"), - 100.0, + "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (170 220, 170 160, 200 140, 210 200, 200 250, 170 220), (170 250, 200 250, 200 300, 180 280, 170 250))", + "POLYGON ((170 220, 200 250, 210 200, 200 140, 170 160, 170 220))", + "POLYGON ((170 250, 180 280, 200 300, 200 250, 170 250))" ), + 70.0, readArray( - "POLYGON ((100 100, 100 300, 200 300, 300 300, 300 100, 100 100), (170 160, 200 140, 200 250, 170 160), (170 250, 200 250, 200 300, 170 250))", - "POLYGON ((170 160, 200 250, 200 140, 170 160))", - "POLYGON ((200 250, 200 300, 170 250, 200 250))" ) + "POLYGON ((100 100, 100 300, 200 300, 300 300, 300 100, 100 100), (170 160, 200 140, 200 250, 170 220, 170 160), (170 250, 200 250, 200 300, 170 250))", + "POLYGON ((170 160, 170 220, 200 250, 200 140, 170 160))", + "POLYGON ((170 250, 200 300, 200 250, 170 250))" ) ); } - public void testHolesTouchingHolesAndShellInner() { + public void testInnerHolesTouchingHolesAndShell() { checkResultInner(readArray( "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))"), 4.0, @@ -191,11 +191,11 @@ public void testHolesTouchingHolesAndShell() { "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))"), 4.0, readArray( - "POLYGON (( 1 2, 1 8, 9 8, 8 5, 9 2, 1 2 ), ( 5 4, 6 3, 6 4, 5 4 ), ( 5 6, 6 6, 6 7, 5 6 ), ( 6 4, 7 4, 8 5, 7 6, 6 6, 6 4 ), ( 7 3, 8 4, 7 4, 7 3 ), ( 7 6, 8 6, 7 7, 7 6 ))") + "POLYGON ((8 5, 9 2, 1 2, 1 8, 9 8, 8 5), (8 5, 7 6, 6 6, 6 4, 7 4, 8 5))") ); } - public void testMultiPolygonWithTouchingShellsInner() { + public void testInnerMultiPolygonWithTouchingShells() { checkResultInner( readArray( "MULTIPOLYGON ((( 2 7, 2 8, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 0 7, 0 8, 1 8, 1 7, 0 7 )), (( 0 5, 0 6, 1 6, 1 5, 0 5 )), (( 2 5, 2 6, 3 6, 3 5, 2 5 )))"), @@ -208,14 +208,14 @@ public void testMultiPolygonWithTouchingShellsInner() { public void testMultiPolygonWithTouchingShells() { checkResult( readArray( - "MULTIPOLYGON ((( 2 7, 2 8, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 0 7, 0 8, 1 8, 1 7, 0 7 )), (( 0 5, 0 6, 1 6, 1 5, 0 5 )), (( 2 5, 2 6, 3 6, 3 5, 2 5 )))"), + "MULTIPOLYGON (((1 6, 1 7, 2 7, 2 6, 1 6)), ((0 7, 0 8, 1 8, 1.2 7.5, 1 7, 0 7)), ((0 5, 0 6, 1 6, 1.2 5.5, 1 5, 0 5)))"), 1.0, readArray( - "MULTIPOLYGON (((0 5, 0 6, 1 6, 0 5)), ((0 8, 1 8, 1 7, 0 8)), ((1 6, 1 7, 2 7, 2 6, 1 6)), ((2 5, 2 6, 3 5, 2 5)), ((2 7, 3 8, 3 7, 2 7)))") + "MULTIPOLYGON (((0 5, 0 6, 1 6, 1 5, 0 5)), ((0 7, 0 8, 1 8, 1 7, 0 7)), ((1 6, 1 7, 2 6, 1 6)))") ); } - public void testTouchingShellsInner() { + public void testInnerTouchingShells() { checkResultInner(readArray( "POLYGON ((0 0, 0 5, 5 6, 10 5, 10 0, 0 0))", "POLYGON ((0 10, 5 6, 10 10, 0 10))"), @@ -235,7 +235,7 @@ public void testShellSimplificationAtStartingNode() { ); } - public void testSimplifyInnerAtStartingNode() { + public void testInnerAtStartingNode() { checkResultInner(readArray( "POLYGON (( 0 5, 0 9, 6 9, 6 2, 1 2, 0 5 ), ( 1 5, 2 3, 5 3, 5 7, 1 7, 1 5 ))", "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), @@ -246,7 +246,7 @@ public void testSimplifyInnerAtStartingNode() { ); } - public void testSimplifyAllAtStartingNode() { + public void testAtStartingNode() { checkResult(readArray( "POLYGON (( 0 5, 0 9, 6 9, 6 2, 1 2, 0 5 ), ( 1 5, 2 3, 5 3, 5 7, 1 7, 1 5 ))", "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), @@ -306,6 +306,81 @@ public void testEmptyHole() { ); } + //============== Test with removed rings ======================= + + // A Polygon with a small hole containing another Polygon - small Polygon is primary so is not removed + public void testPolygonInHoleNotRemoved() { + checkResult(readArray( + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (2 2, 2 3, 3 3, 2 2))", + "POLYGON ((2 2, 2 3, 3 3, 2 2))" + ), + 1, + readArray( + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (2 2, 2 3, 3 3, 2 2))", + "POLYGON ((2 2, 2 3, 3 3, 2 2))" ) + ); + } + + public void testMultiPolygonWithSmallPartRemoved() { + checkResult(readArray( + "MULTIPOLYGON (((11 9, 15 9, 15 5, 11 5, 11 9)), ((11 2, 12 2, 11 1, 11 2)))", + "POLYGON ((15 9, 18 9, 19 4, 14 1, 15 5, 15 9))" + ), + 1, + readArray( + "POLYGON ((11 5, 11 9, 15 9, 15 5, 11 5))", + "POLYGON ((15 9, 18 9, 19 4, 14 1, 15 5, 15 9))" ) + ); + } + + public void testMultiPolygonWithTouchingSmallPartsRemoved() { + checkResult(readArray( + "MULTIPOLYGON (((1 5, 5 5, 5 1, 1 1, 1 5)), ((6 3, 7 2, 6 2, 6 3)), ((8 2, 7 2, 8 3, 8 2)))" + ), + 1, + readArray( + "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5)))" ) + ); + } + + public void testMultiPolygonHolesSmallPartRemoved() { + checkResult(readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (2 5, 2 4, 3 5, 2 5), (4 5, 4 3, 6 3, 6 5, 4 5))", + "MULTIPOLYGON (((2 5, 3 5, 2 4, 2 5)), ((4 5, 6 5, 6 3, 4 3, 4 5)))" + ), + 1, + readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (4 5, 4 3, 6 3, 6 5, 4 5))", + "POLYGON ((4 5, 6 5, 6 3, 4 3, 4 5))" ) + ); + } + + public void testMultiPolygonHolesSmallPart() { + checkResultRemovalSize(readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (2 5, 2 4, 3 5, 2 5), (4 5, 4 3, 6 3, 6 5, 4 5))", + "MULTIPOLYGON (((2 5, 3 5, 2 4, 2 5)), ((4 5, 6 5, 6 3, 4 3, 4 5)))" + ), + 1, 0, + readArray( + "POLYGON ((0 9, 9 9, 9 0, 0 0, 0 9), (2 5, 2 4, 3 5, 2 5), (4 5, 4 3, 6 3, 6 5, 4 5))", + "MULTIPOLYGON (((2 5, 3 5, 2 4, 2 5)), ((4 5, 6 5, 6 3, 4 3, 4 5)))" ) + ); + } + + public void testTolerances() { + checkResult(readArray( + "POLYGON ((1 19, 6 19, 7 11, 6 1, 1 1, 1 19))", + "POLYGON ((6 19, 12 19, 11 15, 12 1, 6 1, 7 11, 6 19))", + "POLYGON ((12 19, 19 19, 22 10, 19 1, 12 1, 11 15, 12 19))" + ), + new double[] { 0, 3, 6 }, + readArray( + "POLYGON ((6 19, 7 11, 6 1, 1 1, 1 19, 6 19))", + "POLYGON ((6 19, 12 19, 12 1, 6 1, 7 11, 6 19))", + "POLYGON ((12 19, 19 19, 19 1, 12 1, 12 19))" ) + ); + } + //================================= @@ -314,11 +389,23 @@ private void checkNoop(Geometry[] input) { checkEqual(input, actual); } + private void checkResult(Geometry[] input, double[] tolerances, Geometry[] expected) { + Geometry[] actual = CoverageSimplifier.simplify(input, tolerances); + checkEqual(expected, actual); + } + private void checkResult(Geometry[] input, double tolerance, Geometry[] expected) { Geometry[] actual = CoverageSimplifier.simplify(input, tolerance); checkEqual(expected, actual); } + private void checkResultRemovalSize(Geometry[] input, double tolerance, double removalFactor, Geometry[] expected) { + CoverageSimplifier simplifier = new CoverageSimplifier(input); + simplifier.setRemovableRingSizeFactor(removalFactor); + Geometry[] actual = simplifier.simplify(tolerance); + checkEqual(expected, actual); + } + private void checkResultInner(Geometry[] input, double tolerance, Geometry[] expected) { Geometry[] actual = CoverageSimplifier.simplifyInner(input, tolerance); checkEqual(expected, actual); diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java index 58bb97f6ec..52d9bccc2e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -11,9 +11,14 @@ */ package org.locationtech.jts.coverage; -import java.util.BitSet; +import java.util.ArrayList; +import java.util.List; +import org.locationtech.jts.coverage.TPVWSimplifier.Edge; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; import junit.textui.TestRunner; @@ -47,10 +52,10 @@ public void testFreeRing() { } public void testNoFreeRing() { - checkSimplify("MULTILINESTRING ((1 9, 9 9, 9 1), (1 9, 1 1, 9 1), (5 5, 4 8, 2 8, 2 2, 4 2, 5 5), (5 5, 6 8, 8 8, 8 2, 6 2, 5 5))", + checkSimplify("MULTILINESTRING ((1 19, 19 19, 19 1), (1 19, 1 1, 19 1), (10 10, 9 18, 2 18, 2 2, 7 6, 10 10), (10 10, 11 18, 18 18, 18 2, 13 6, 10 10))", new int[] { }, 2, - "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 9 1), (5 5, 2 2, 2 8, 5 5), (5 5, 8 2, 8 8, 5 5))"); + "MULTILINESTRING ((1 19, 1 1, 19 1), (1 19, 19 19, 19 1), (10 10, 2 2, 2 18, 9 18, 10 10), (10 10, 11 18, 18 18, 18 2, 10 10))"); } public void testConstraint() { @@ -58,20 +63,15 @@ public void testConstraint() { new int[] { }, "MULTILINESTRING ((1 9, 9 9, 6 5, 9 1), (1 9, 1 1, 9 1))", 1, - "MULTILINESTRING ((6 8, 2 8, 2 2, 6 2, 5.9 5, 6 8))"); + "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 6 5, 9 1), (6 8, 2 8, 2 2, 6 2, 5.9 5, 6 8))"); } private void checkNoop(String wkt, double tolerance) { - MultiLineString geom = (MultiLineString) read(wkt); - Geometry actual = TPVWSimplifier.simplify(geom, tolerance); - checkEqual(geom, actual); + checkSimplify(wkt, null, null, tolerance, wkt); } private void checkSimplify(String wkt, double tolerance, String wktExpected) { - MultiLineString geom = (MultiLineString) read(wkt); - Geometry actual = TPVWSimplifier.simplify(geom, tolerance); - Geometry expected = read(wktExpected); - checkEqual(expected, actual); + checkSimplify(wkt, null, null, tolerance, wktExpected); } private void checkSimplify(String wkt, int[] freeRingIndex, @@ -82,16 +82,50 @@ private void checkSimplify(String wkt, int[] freeRingIndex, private void checkSimplify(String wkt, int[] freeRingIndex, String wktConstraints, double tolerance, String wktExpected) { - MultiLineString lines = (MultiLineString) read(wkt); - BitSet freeRings = new BitSet(); - for (int index : freeRingIndex) { - freeRings.set(index); - } - MultiLineString constraints = wktConstraints == null ? null - : (MultiLineString) read(wktConstraints); - Geometry actual = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); + TPVWSimplifier.Edge[] edges = createEdges(wkt, freeRingIndex, wktConstraints, tolerance); + CornerArea cornerArea = new CornerArea(); + TPVWSimplifier.simplify(edges, cornerArea, 1.0); + Geometry expected = read(wktExpected); + MultiLineString actual = createResult(edges, expected.getFactory()); checkEqual(expected, actual); } + private TPVWSimplifier.Edge[] createEdges(String wkt, int[] freeRingIndex, String wktConstraints, double tolerance) { + List edgeList = new ArrayList(); + addEdges(wkt, freeRingIndex, tolerance, edgeList); + if (wktConstraints != null) { + addEdges(wktConstraints, null, 0.0, edgeList); + } + TPVWSimplifier.Edge[] edges = edgeList.toArray(new TPVWSimplifier.Edge[0]); + return edges; + } + + private void addEdges(String wkt, int[] freeRings, double tolerance, List edges) { + MultiLineString lines = (MultiLineString) read(wkt); + for (int i = 0; i < lines.getNumGeometries(); i++) { + LineString line = (LineString) lines.getGeometryN(i); + boolean isRemovable = false; + boolean isFreeRing = freeRings == null ? false : hasIndex(freeRings, i); + Edge edge = new Edge(line.getCoordinates(), tolerance, isFreeRing, isRemovable); + edges.add(edge); + } + } + + private boolean hasIndex(int[] freeRings, int i) { + for (int fr : freeRings) { + if (fr == i) + return true; + } + return false; + } + + private static MultiLineString createResult(Edge[] edges, GeometryFactory geomFactory) { + LineString[] result = new LineString[edges.length]; + for (int i = 0; i < edges.length; i++) { + Coordinate[] pts = edges[i].getCoordinates(); + result[i] = geomFactory.createLineString(pts); + } + return geomFactory.createMultiLineString(result); + } } diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java index 6928f4184a..95025daca7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKBTest.java @@ -12,6 +12,7 @@ package org.locationtech.jts.io; import java.io.IOException; +import java.util.EnumSet; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateFilter; @@ -152,6 +153,49 @@ public void testGeometryCollectionEmpty() runWKBTest("GEOMETRYCOLLECTION EMPTY"); } + /** + * Tests if a previously written WKB with M-coordinates can be read as expected. + */ + public void testWriteAndReadM() throws ParseException + { + String wkt = "MULTILINESTRING M((1 1 1, 2 2 2))"; + WKTReader wktReader = new WKTReader(); + Geometry geometryBefore = wktReader.read(wkt); + + WKBWriter wkbWriter = new WKBWriter(3); + wkbWriter.setOutputOrdinates(EnumSet.of(Ordinate.X, Ordinate.Y, Ordinate.M)); + byte[] write = wkbWriter.write(geometryBefore); + + WKBReader wkbReader = new WKBReader(); + Geometry geometryAfter = wkbReader.read(write); + + assertEquals(1.0, geometryAfter.getCoordinates()[0].getX()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getY()); + assertEquals(Double.NaN, geometryAfter.getCoordinates()[0].getZ()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getM()); + } + + /** + * Tests if a previously written WKB with Z-coordinates can be read as expected. + */ + public void testWriteAndReadZ() throws ParseException + { + String wkt = "MULTILINESTRING ((1 1 1, 2 2 2))"; + WKTReader wktReader = new WKTReader(); + Geometry geometryBefore = wktReader.read(wkt); + + WKBWriter wkbWriter = new WKBWriter(3); + byte[] write = wkbWriter.write(geometryBefore); + + WKBReader wkbReader = new WKBReader(); + Geometry geometryAfter = wkbReader.read(write); + + assertEquals(1.0, geometryAfter.getCoordinates()[0].getX()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getY()); + assertEquals(1.0, geometryAfter.getCoordinates()[0].getZ()); + assertEquals(Double.NaN, geometryAfter.getCoordinates()[0].getM()); + } + private void runWKBTest(String wkt) throws IOException, ParseException { runWKBTestCoordinateArray(wkt); diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java index 683cb7dace..d70276c5b5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKBWriterTest.java @@ -12,8 +12,10 @@ package org.locationtech.jts.io; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateXYZM; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Point; import junit.textui.TestRunner; @@ -125,6 +127,25 @@ public void testGeometryCollection() { 4326, "0107000020E61000000900000001010000000000000000000000000000000000F03F01010000000000000000000000000000000000F03F01010000000000000000000040000000000000084001020000000200000000000000000000400000000000000840000000000000104000000000000014400102000000020000000000000000000000000000000000F03F000000000000004000000000000008400102000000020000000000000000001040000000000000144000000000000018400000000000001C4001030000000200000005000000000000000000000000000000000000000000000000000000000000000000244000000000000024400000000000002440000000000000244000000000000000000000000000000000000000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000002240000000000000224000000000000022400000000000002240000000000000F03F000000000000F03F000000000000F03F01030000000200000005000000000000000000000000000000000000000000000000000000000000000000244000000000000024400000000000002440000000000000244000000000000000000000000000000000000000000000000005000000000000000000F03F000000000000F03F000000000000F03F0000000000002240000000000000224000000000000022400000000000002240000000000000F03F000000000000F03F000000000000F03F0103000000010000000500000000000000000022C0000000000000000000000000000022C00000000000002440000000000000F0BF0000000000002440000000000000F0BF000000000000000000000000000022C00000000000000000"); } + + public void testWkbLineStringZM() throws ParseException { + LineString lineZM = new GeometryFactory().createLineString(new Coordinate[]{new CoordinateXYZM(1,2,3,4), new CoordinateXYZM(5,6,7,8)}); + byte[] write = new WKBWriter(4).write(lineZM); + + LineString deserialisiert = (LineString) new WKBReader().read(write); + + assertEquals(lineZM, deserialisiert); + + assertEquals(1.0, lineZM.getPointN(0).getCoordinate().getX()); + assertEquals(2.0, lineZM.getPointN(0).getCoordinate().getY()); + assertEquals(3.0, lineZM.getPointN(0).getCoordinate().getZ()); + assertEquals(4.0, lineZM.getPointN(0).getCoordinate().getM()); + + assertEquals(5.0, lineZM.getPointN(1).getCoordinate().getX()); + assertEquals(6.0, lineZM.getPointN(1).getCoordinate().getY()); + assertEquals(7.0, lineZM.getPointN(1).getCoordinate().getZ()); + assertEquals(8.0, lineZM.getPointN(1).getCoordinate().getM()); + } void checkWKB(String wkt, int dimension, String expectedWKBHex) { checkWKB(wkt, dimension, ByteOrderValues.LITTLE_ENDIAN, -1, expectedWKBHex); diff --git a/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java b/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java index 66e573e390..f2d4eb4577 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/WKTWriterTest.java @@ -181,4 +181,22 @@ public void testWrite3D_withNaN() { assertEquals("LINESTRING (1 1, 2 2)", wkt); } + public void testWktLineStringZM() throws ParseException { + LineString lineZM = new GeometryFactory().createLineString(new Coordinate[]{new CoordinateXYZM(1,2,3,4), new CoordinateXYZM(5,6,7,8)}); + String write = new WKTWriter(4).write(lineZM); + + LineString deserialisiert = (LineString) new WKTReader().read(write); + + assertEquals(lineZM, deserialisiert); + + assertEquals(1.0, lineZM.getPointN(0).getCoordinate().getX()); + assertEquals(2.0, lineZM.getPointN(0).getCoordinate().getY()); + assertEquals(3.0, lineZM.getPointN(0).getCoordinate().getZ()); + assertEquals(4.0, lineZM.getPointN(0).getCoordinate().getM()); + + assertEquals(5.0, lineZM.getPointN(1).getCoordinate().getX()); + assertEquals(6.0, lineZM.getPointN(1).getCoordinate().getY()); + assertEquals(7.0, lineZM.getPointN(1).getCoordinate().getZ()); + assertEquals(8.0, lineZM.getPointN(1).getCoordinate().getM()); + } } diff --git a/modules/core/src/test/java/org/locationtech/jts/io/kml/KMLReaderTest.java b/modules/core/src/test/java/org/locationtech/jts/io/kml/KMLReaderTest.java index eda1d31839..742ca4351b 100644 --- a/modules/core/src/test/java/org/locationtech/jts/io/kml/KMLReaderTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/io/kml/KMLReaderTest.java @@ -20,9 +20,7 @@ import org.locationtech.jts.geom.PrecisionModel; import org.locationtech.jts.io.ParseException; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; +import java.util.*; public class KMLReaderTest extends TestCase { public static void main(String args[]) { @@ -131,6 +129,17 @@ public void testMultiGeometryWithAllPolygons() { ); } + public void testCoordinatesWithWhitespace() + { + checkParsingResult( + "" + + " 1.0,2.0" + + " -1.0,-2.0 " + + "", + "LINESTRING (1 2, -1 -2)", + new Map[]{null, null} + ); + } public void testZ() { String kml = "1.0,1.0,50.0"; KMLReader kmlReader = new KMLReader(); @@ -155,6 +164,7 @@ public void testPrecisionAndSRID() { } } + public void testCoordinatesErrors() { checkExceptionThrown("", "No element coordinates found in Point"); checkExceptionThrown("", "Empty coordinates"); diff --git a/modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java b/modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java new file mode 100644 index 0000000000..04df1d98ad --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/noding/SegmentStringTest.java @@ -0,0 +1,52 @@ +/* + * 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.noding; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +import test.jts.GeometryTestCase; + +public class SegmentStringTest extends GeometryTestCase { + + public static void main(String[] args) { + junit.textui.TestRunner.run(SegmentStringTest.class); + } + + public SegmentStringTest(String name) { + super(name); + } + + public void testNextInRing() { + SegmentString ss = create("LINESTRING(0 0, 1 2, 3 1, 0 0)"); + assertTrue(ss.isClosed()); + checkEqualXY(ss.nextInRing(0), new Coordinate(1, 2)); + checkEqualXY(ss.nextInRing(1), new Coordinate(3, 1)); + checkEqualXY(ss.nextInRing(2), new Coordinate(0, 0)); + checkEqualXY(ss.nextInRing(3), new Coordinate(1, 2)); + } + + public void testPrevInRing() { + SegmentString ss = create("LINESTRING(0 0, 1 2, 3 1, 0 0)"); + assertTrue(ss.isClosed()); + checkEqualXY(ss.prevInRing(0), new Coordinate(3, 1)); + checkEqualXY(ss.prevInRing(1), new Coordinate(0, 0)); + checkEqualXY(ss.prevInRing(2), new Coordinate(1, 2)); + checkEqualXY(ss.prevInRing(3), new Coordinate(3, 1)); + } + + private SegmentString create(String wkt) { + Geometry geom = read(wkt); + return new BasicSegmentString(geom.getCoordinates(), null); + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java index 73b5509362..a78c9aea43 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/BufferTest.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.operation.buffer; +import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.GeometryFactory; @@ -574,8 +575,77 @@ public void testLineClosedNoHole() { checkBufferHasHole(wkt, 70, false); } + public void testSmallPolygonNegativeBuffer_1() { + String wkt = "MULTIPOLYGON (((833454.7163917861 6312507.405413097, 833455.3726665961 6312510.208920742, 833456.301153878 6312514.207390314, 833492.2432584754 6312537.770332065, 833493.0901320165 6312536.098774815, 833502.6580673696 6312517.561360772, 833503.9404352929 6312515.0542803425, 833454.7163917861 6312507.405413097)))"; + checkBuffer(wkt, -3.8, + "POLYGON ((833459.9671068499 6312512.066918822, 833490.7876785189 6312532.272283619, 833498.1465258132 6312517.999574621, 833459.9671068499 6312512.066918822))"); + checkBuffer(wkt, -7, + "POLYGON ((833474.0912127121 6312517.50004999, 833489.5713439264 6312527.648521655, 833493.2674441456 6312520.479822435, 833474.0912127121 6312517.50004999))"); + } + + public void testSmallPolygonNegativeBuffer_2() { + String wkt = "POLYGON ((182719.04521570954238996 224897.14115349075291306, 182807.02887436276068911 224880.64421749324537814, 182808.47314301913138479 224877.25002362736267969, 182718.38701137207681313 224740.00115247094072402, 182711.82697281913715415 224742.08599378637154587, 182717.1393717635946814 224895.61432328051887453, 182719.04521570954238996 224897.14115349075291306))"; + checkBuffer(wkt, -5, + "POLYGON ((182717 224746.99999999997, 182722.00000000003 224891.5, 182801.99999999997 224876.49999999997, 182717 224746.99999999997))"); + checkBuffer(wkt, -30, + "POLYGON ((182745.07127364463 224835.32741176756, 182745.97926048582 224861.56823147752, 182760.5070499446 224858.844270954, 182745.07127364463 224835.32741176756))"); + } + + /** + * See GEOS PR https://github.com/libgeos/geos/pull/978 + */ + public void testDefaultBuffer() { + Geometry g = read("POINT (0 0)").buffer(1.0); + Geometry b = g.getBoundary(); + Coordinate[] coords = b.getCoordinates(); + assertEquals(33, coords.length); + assertEquals(coords[0].x, 1.0); + assertEquals(coords[0].y, 0.0); + assertEquals(coords[8].x, 0.0); + assertEquals(coords[8].y, -1.0); + assertEquals(coords[16].x, -1.0); + assertEquals(coords[16].y, 0.0); + assertEquals(coords[24].x, 0.0); + assertEquals(coords[24].y, 1.0); + } + + public void testRingStartSimplified() { + checkBuffer("POLYGON ((200 300, 200 299.9999, 350 100, 30 40, 200 300))", + 20, bufParamRoundMitre(5), + "POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))" + ); + } + + public void testRingEndSimplified() { + checkBuffer("POLYGON ((200 300, 350 100, 30 40, 200 299.9999, 200 300))", + 20, bufParamRoundMitre(5), + "POLYGON ((198.88 334.83, 385.3 86.27, -12.4 11.7, 198.88 334.83))" + ); + } + //=================================================== + private static BufferParameters bufParamRoundMitre(double mitreLimit) { + BufferParameters param = new BufferParameters(); + param.setJoinStyle(BufferParameters.JOIN_MITRE); + param.setMitreLimit(mitreLimit); + return param; + } + + private void checkBuffer(String wkt, double dist, BufferParameters param, String wktExpected) { + Geometry geom = read(wkt); + Geometry result = BufferOp.bufferOp(geom, dist, param); + Geometry expected = read(wktExpected); + checkEqual(expected, result, 0.01); + } + + private void checkBuffer(String wkt, double dist, String wktExpected) { + Geometry geom = read(wkt); + Geometry result = BufferOp.bufferOp(geom, dist); + Geometry expected = read(wktExpected); + checkEqual(expected, result, 0.01); + } + private void checkBufferEmpty(String wkt, double dist, boolean isEmptyExpected) { Geometry a = read(wkt); Geometry result = a.buffer(dist); @@ -602,4 +672,6 @@ private boolean hasHole(Geometry geom) { return false; } + + } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java index f4cc9bb177..366737150e 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java @@ -215,14 +215,14 @@ public void testClosedCurve() { public void testOverlapTriangleInside() { checkOffsetCurve( - "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80))", 10, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", 10, "LINESTRING (70 70, 40 70, 27.23 70, 50 30.15, 72.76 70, 70 70)" ); } public void testOverlapTriangleOutside() { checkOffsetCurve( - "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80))", -10, + "LINESTRING (70 80, 10 80, 50 10, 90 80, 40 80)", -10, "LINESTRING (70 90, 40 90, 10 90, 8.11 89.82, 6.29 89.29, 4.6 88.42, 3.11 87.25, 1.87 85.82, 0.91 84.18, 0.29 82.39, 0.01 80.51, 0.1 78.61, 0.54 76.77, 1.32 75.04, 41.32 5.04, 42.42 3.48, 43.8 2.16, 45.4 1.12, 47.17 0.41, 49.05 0.05, 50.95 0.05, 52.83 0.41, 54.6 1.12, 56.2 2.16, 57.58 3.48, 58.68 5.04, 98.68 75.04, 99.46 76.77, 99.9 78.61, 99.99 80.51, 99.71 82.39, 99.09 84.18, 98.13 85.82, 96.89 87.25, 95.4 88.42, 93.71 89.29, 91.89 89.82, 90 90, 70 90)" ); } @@ -293,6 +293,15 @@ public void testInfiniteLoop() { ); } + // see https://github.com/shapely/shapely/issues/820 + public void testOffsetError() { + checkOffsetCurve( + "LINESTRING (12 20, 60 68, 111 114, 151 159, 210 218)", + 3, + "LINESTRING (9.878679656440358 22.121320343559642, 57.878679656440355 70.12132034355965, 57.99069368916718 70.22770917070595, 108.86775926900314 116.11682714467565, 148.75777204394902 160.99309151648976, 148.87867965644037 161.12132034355963, 207.87867965644037 220.12132034355963)" + ); + } + //--------------------------------------- public void testQuadSegs() { @@ -337,6 +346,33 @@ public void testMinQuadrantSegments_QGIS() { ); } + // See https://trac.osgeo.org/postgis/ticket/4072 + public void testMitreJoinError() { + checkOffsetCurve( + "LINESTRING(362194.505 5649993.044,362197.451 5649994.125,362194.624 5650001.876,362189.684 5650000.114,362192.542 5649992.324,362194.505 5649993.044)", + -0.045, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (362194.52050157124 5649993.001754275, 362197.5086649931 5649994.098225646, 362194.65096611937 5650001.933395073, 362189.626113625 5650000.141129872, 362192.51525161567 5649992.266257602, 362194.5204958858 5649993.001752188)" + ); + } + + // See https://trac.osgeo.org/postgis/ticket/4072 + public void testMitreJoinErrorSimple() { + checkOffsetCurve( + "LINESTRING (4.821 0.72, 7.767 1.801, 4.94 9.552, 0 7.79, 2.858 0, 4.821 0.72)", + -0.045, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (4.83650157122754 0.6777542748970088, 7.824664993161384 1.7742256459460533, 4.966966119329371 9.6093950732796, -0.057886375241824 7.817129871774653, 2.8312516154153906 -0.0577423980712891, 4.836495885800319 0.6777521891305186)" + ); + } + + // See https://trac.osgeo.org/postgis/ticket/3279 + public void testMitreJoinSingleLine() { + checkOffsetCurve( + "LINESTRING (0.39 -0.02, 0.4650008997915482 -0.02, 0.4667128891457749 -0.0202500016082272, 0.4683515425280024 -0.0210000000000019, 0.4699159706879993 -0.0222499999999996, 0.4714061701120011 -0.0240000000000018, 0.4929087886040002 -0.0535958153351002, 0.4968358395870001 -0.0507426457862002, 0.4774061701119963 -0.0239999999999952, 0.476353470688 -0.0222500000000011, 0.4761015425280001 -0.0210000000000007, 0.4766503813740676 -0.0202500058185111, 0.4779990890331232 -0.02, 0.6189999999999996 -0.02, 0.619 -0.0700000000000002, 0.634 -0.0700000000000002, 0.6339999999999998 -0.02, 0.65 -0.02)", + -0.002, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (0.39 -0.022, 0.4648556402268155 -0.022, 0.4661407414895839 -0.0221876631893964, 0.4672953866748729 -0.022716134946407, 0.4685176359449585 -0.0236927292232623, 0.4698334593862525 -0.0252379526243584, 0.4924663251198579 -0.0563894198284619, 0.499629444080312 -0.0511851092703384, 0.479075235654203 -0.022894668402962, 0.4785370545613636 -0.022, 0.6169999999999995 -0.022, 0.617 -0.0720000000000002, 0.636 -0.0720000000000002, 0.6359999999999998 -0.022, 0.65 -0.022)" + ); + } + //======================================= private static final double EQUALS_TOL = 0.05; diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java index dc3953f7be..c0370b2f1a 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/VariableBufferTest.java @@ -17,7 +17,8 @@ public class VariableBufferTest extends GeometryTestCase { - private static final double DEFAULT_TOLERANCE = 1.0e-6; + //-- low tolerance reduces expected geometry literal size + private static final double DEFAULT_TOLERANCE = 1.0e-2; public VariableBufferTest(String name) { super(name); @@ -42,38 +43,76 @@ public void testZeroLength() { public void testSegmentInverseDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 1, - "POLYGON ((200.09 99.00405823463417, 100.9 90.04058234634172, 100 90, 98.04909677983872 90.19214719596769, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98.04909677983872 109.80785280403231, 100 110, 100.9 109.95941765365829, 200.09 100.99594176536583, 200.19509032201614 100.98078528040323, 200.3826834323651 100.9238795325113, 200.5555702330196 100.83146961230254, 200.70710678118655 100.70710678118655, 200.83146961230256 100.55557023301961, 200.92387953251128 100.38268343236508, 200.98078528040324 100.19509032201613, 201 100, 200.98078528040324 99.80490967798387, 200.92387953251128 99.61731656763492, 200.83146961230256 99.44442976698039, 200.70710678118655 99.29289321881345, 200.5555702330196 99.16853038769746, 200.3826834323651 99.0761204674887, 200.09 99.00405823463417))" + "POLYGON ((100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 100.9 109.96, 200.09 101, 200.2 100.98, 200.38 100.92, 200.56 100.83, 200.71 100.71, 200.83 100.56, 200.92 100.38, 200.98 100.2, 201 100, 200.98 99.8, 200.92 99.62, 200.83 99.44, 200.71 99.29, 200.56 99.17, 200.38 99.08, 200.2 99.02, 200.09 99, 100.9 90.04, 100 90))" ); } public void testSegmentSameDist() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 10, - "POLYGON ((90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98.04909677983872 109.80785280403231, 100 110, 200 110, 200 110, 201.95090322016128 109.80785280403231, 203.8268343236509 109.23879532511287, 205.55570233019603 108.31469612302546, 207.07106781186548 107.07106781186548, 208.31469612302544 105.55570233019603, 209.23879532511287 103.8268343236509, 209.8078528040323 101.95090322016128, 210 100, 209.8078528040323 98.04909677983872, 209.23879532511287 96.1731656763491, 208.31469612302544 94.44429766980397, 207.07106781186548 92.92893218813452, 205.55570233019603 91.68530387697454, 203.8268343236509 90.76120467488713, 201.95090322016128 90.19214719596769, 200 90, 100 90, 100 90, 98.04909677983872 90.19214719596769, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100))" + "POLYGON ((201.95 109.81, 203.83 109.24, 205.56 108.31, 207.07 107.07, 208.31 105.56, 209.24 103.83, 209.81 101.95, 210 100, 209.81 98.05, 209.24 96.17, 208.31 94.44, 207.07 92.93, 205.56 91.69, 203.83 90.76, 201.95 90.19, 200 90, 100 90, 98.05 90.19, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98.05 109.81, 100 110, 200 110, 201.95 109.81))" ); } public void testOneSegment() { checkBuffer("LINESTRING (100 100, 200 100)", 10, 30, -"POLYGON ((98 109.79795897113272, 194 129.39387691339815, 194.14729033951616 129.42355841209692, 200 130, 205.85270966048384 129.42355841209692, 211.4805029709527 127.7163859753386, 216.66710699058808 124.94408836907635, 221.21320343559643 121.21320343559643, 224.94408836907635 116.66710699058807, 227.7163859753386 111.4805029709527, 229.42355841209692 105.85270966048385, 230 100, 229.42355841209692 94.14729033951615, 227.7163859753386 88.5194970290473, 224.94408836907635 83.33289300941193, 221.21320343559643 78.78679656440357, 216.66710699058808 75.05591163092365, 211.4805029709527 72.2836140246614, 205.85270966048384 70.57644158790309, 200 70, 194 70.60612308660184, 98 90.20204102886728, 96.1731656763491 90.76120467488714, 94.44429766980397 91.68530387697454, 92.92893218813452 92.92893218813452, 91.68530387697454 94.44429766980397, 90.76120467488713 96.1731656763491, 90.19214719596769 98.04909677983872, 90 100, 90.19214719596769 101.95090322016128, 90.76120467488713 103.8268343236509, 91.68530387697454 105.55570233019603, 92.92893218813452 107.07106781186548, 94.44429766980397 108.31469612302546, 96.1731656763491 109.23879532511287, 98 109.79795897113272))" +"POLYGON ((200 130, 205.85 129.42, 211.48 127.72, 216.67 124.94, 221.21 121.21, 224.94 116.67, 227.72 111.48, 229.42 105.85, 230 100, 229.42 94.15, 227.72 88.52, 224.94 83.33, 221.21 78.79, 216.67 75.06, 211.48 72.28, 205.85 70.58, 200 70, 194 70.61, 98 90.2, 96.17 90.76, 94.44 91.69, 92.93 92.93, 91.69 94.44, 90.76 96.17, 90.19 98.05, 90 100, 90.19 101.95, 90.76 103.83, 91.69 105.56, 92.93 107.07, 94.44 108.31, 96.17 109.24, 98 109.8, 194 129.39, 200 130))" ); } public void testSegments2() { checkBuffer("LINESTRING( 0 0, 40 40, 60 -20)", 10, 20, - "POLYGON ((53.52863576494982 45.80469132164433, 78.37960104024995 -12.113919503248614, 78.47759065022574 -12.346331352698204, 79.61570560806462 -16.098193559677433, 80 -20, 79.61570560806462 -23.901806440322567, 78.47759065022574 -27.653668647301796, 76.62939224605091 -31.111404660392044, 74.14213562373095 -34.14213562373095, 71.11140466039204 -36.629392246050905, 67.6536686473018 -38.477590650225736, 63.90180644032257 -39.61570560806461, 60 -40, 56.09819355967743 -39.61570560806461, 52.34633135269821 -38.477590650225736, 48.88859533960796 -36.629392246050905, 45.85786437626905 -34.14213562373095, 43.370607753949095 -31.111404660392044, 40.56467086974921 -24.718896226748868, 31.314401806419635 13.379424895487343, 6.456226258387812 -7.636566145886759, 5.555702330196018 -8.314696123025454, 3.8268343236509 -9.238795325112866, 1.950903220161283 -9.807852804032304, 0 -10, -1.9509032201612866 -9.807852804032303, -3.8268343236509033 -9.238795325112864, -5.555702330196022 -8.314696123025453, -7.071067811865477 -7.071067811865475, -8.314696123025454 -5.55570233019602, -9.238795325112868 -3.8268343236508966, -9.807852804032304 -1.9509032201612837, -10 0, -9.807852804032304 1.9509032201612861, -9.238795325112868 3.826834323650899, -8.314696123025453 5.555702330196022, -7.636566145886759 6.456226258387811, 28.75793640390754 49.5044428085851, 29.59042683391263 50.40957316608737, 31.821250844443494 52.240363117601376, 34.366379598327015 53.60076277898068, 37.12800522487612 54.4384927541594, 40 54.721359549995796, 42.87199477512389 54.4384927541594, 45.633620401672985 53.60076277898068, 48.17874915555651 52.240363117601376, 50.40957316608737 50.40957316608737, 52.240363117601376 48.17874915555651, 53.52863576494982 45.80469132164433))" + "POLYGON ((79.62 -16.1, 80 -20, 79.62 -23.9, 78.48 -27.65, 76.63 -31.11, 74.14 -34.14, 71.11 -36.63, 67.65 -38.48, 63.9 -39.62, 60 -40, 56.1 -39.62, 52.35 -38.48, 48.89 -36.63, 45.86 -34.14, 43.37 -31.11, 41.52 -27.65, 40.56 -24.72, 31.31 13.38, 6.46 -7.64, 5.56 -8.31, 3.83 -9.24, 1.95 -9.81, 0 -10, -1.95 -9.81, -3.83 -9.24, -5.56 -8.31, -7.07 -7.07, -8.31 -5.56, -9.24 -3.83, -9.81 -1.95, -10 0, -9.81 1.95, -9.24 3.83, -8.31 5.56, -7.64 6.46, 28.76 49.5, 29.59 50.41, 31.82 52.24, 34.37 53.6, 37.13 54.44, 40 54.72, 42.87 54.44, 45.63 53.6, 48.18 52.24, 50.41 50.41, 52.24 48.18, 53.53 45.8, 78.38 -12.11, 79.62 -16.1))" ); } public void testLargeDistance() { checkBuffer("LINESTRING( 0 0, 10 10)", 1, 200, - "POLYGON ((-190 10, -186.1570560806461 49.01806440322572, -174.77590650225736 86.53668647301798, -156.29392246050907 121.11404660392043, -131.42135623730948 151.4213562373095, -101.11404660392039 176.29392246050907, -66.53668647301795 194.77590650225736, -29.018064403225637 206.1570560806461, 10 210, 49.018064403225665 206.1570560806461, 86.53668647301797 194.77590650225736, 121.11404660392046 176.29392246050904, 151.4213562373095 151.42135623730948, 176.29392246050904 121.11404660392043, 194.77590650225736 86.53668647301795, 206.1570560806461 49.01806440322565, 210 10, 206.15705608064607 -29.018064403225743, 194.7759065022573 -66.53668647301808, 176.29392246050904 -101.11404660392043, 151.42135623730948 -131.42135623730954, 121.11404660392037 -156.2939224605091, 86.536686473018 -174.77590650225733, 49.01806440322566 -186.1570560806461, 10 -190, -29.018064403225736 -186.15705608064607, -66.53668647301807 -174.7759065022573, -101.11404660392043 -156.29392246050904, -131.42135623730954 -131.42135623730948, -156.2939224605091 -101.11404660392039, -174.77590650225736 -66.53668647301794, -186.1570560806461 -29.018064403225672, -190 10))" + "POLYGON ((206.16 -29.02, 194.78 -66.54, 176.29 -101.11, 151.42 -131.42, 121.11 -156.29, 86.54 -174.78, 49.02 -186.16, 10 -190, -29.02 -186.16, -66.54 -174.78, -101.11 -156.29, -131.42 -131.42, -156.29 -101.11, -174.78 -66.54, -186.16 -29.02, -190 10, -186.16 49.02, -174.78 86.54, -156.29 121.11, -131.42 151.42, -101.11 176.29, -66.54 194.78, -29.02 206.16, 10 210, 49.02 206.16, 86.54 194.78, 121.11 176.29, 151.42 151.42, 176.29 121.11, 194.78 86.54, 206.16 49.02, 210 10, 206.16 -29.02))" ); } + public void testZeroDistanceAtVertex() { + checkBuffer("LINESTRING( 10 10, 20 20, 30 30)", + new double[] { 5, 0, 5 }, + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((25.44 32.06, 25.84 32.78, 26.46 33.54, 27.22 34.16, 28.09 34.62, 29.02 34.9, 30 35, 30.98 34.9, 31.91 34.62, 32.78 34.16, 33.54 33.54, 34.16 32.78, 34.62 31.91, 34.9 30.98, 35 30, 34.9 29.02, 34.62 28.09, 34.16 27.22, 33.54 26.46, 32.78 25.84, 32.06 25.44, 20 20, 25.44 32.06)))" + ); + } + + public void testZeroDistancesForSegment() { + checkBuffer("LINESTRING( 10 10, 20 20, 30 30, 40 40)", + new double[] { 5, 0, 0, 5 }, + "MULTIPOLYGON (((5.1 10.98, 5.38 11.91, 5.84 12.78, 6.46 13.54, 7.22 14.16, 7.94 14.56, 20 20, 14.56 7.94, 14.16 7.22, 13.54 6.46, 12.78 5.84, 11.91 5.38, 10.98 5.1, 10 5, 9.02 5.1, 8.09 5.38, 7.22 5.84, 6.46 6.46, 5.84 7.22, 5.38 8.09, 5.1 9.02, 5 10, 5.1 10.98)), ((35.44 42.06, 35.84 42.78, 36.46 43.54, 37.22 44.16, 38.09 44.62, 39.02 44.9, 40 45, 40.98 44.9, 41.91 44.62, 42.78 44.16, 43.54 43.54, 44.16 42.78, 44.62 41.91, 44.9 40.98, 45 40, 44.9 39.02, 44.62 38.09, 44.16 37.22, 43.54 36.46, 42.78 35.84, 42.06 35.44, 30 30, 35.44 42.06)))" + ); + } + + // see https://github.com/locationtech/jts/issues/998 + public void testIssue998_Spike() { + checkBuffer("LINESTRING (0.024520295 69.50077743, 0.000508719 74.50086084, 0 76.39546845)", + new double[] { 6.47, 6.9, 7 }, + "POLYGON ((-6.87 77.76, -6.47 79.07, -5.82 80.28, -4.95 81.35, -3.89 82.22, -2.68 82.86, -1.37 83.26, 0 83.4, 1.37 83.26, 2.68 82.86, 3.89 82.22, 4.95 81.35, 5.82 80.28, 6.47 79.07, 6.87 77.76, 7 76.4, 6.99 76.03, 6.89 74.14, 6.88 74.08, 6.88 73.94, 6.47 68.98, 6.37 68.24, 6 67.02, 5.4 65.91, 4.6 64.93, 3.62 64.12, 2.5 63.52, 1.29 63.16, 0.02 63.03, -1.24 63.16, -2.45 63.52, -3.57 64.12, -4.55 64.93, -5.36 65.91, -5.95 67.02, -6.32 68.24, -6.42 68.91, -6.87 73.87, -6.88 74.05, -6.89 74.13, -6.99 76.02, -7 76.4, -6.87 77.76))" + ); + } + + public void testNoReverseSpike() { + checkBuffer("LINESTRING (0 70, 0 80)", + new double[] { 4, 7 }, + "POLYGON ((-6.87 78.63, -7 80, -6.87 81.37, -6.47 82.68, -5.82 83.89, -4.95 84.95, -3.89 85.82, -2.68 86.47, -1.37 86.87, 0 87, 1.37 86.87, 2.68 86.47, 3.89 85.82, 4.95 84.95, 5.82 83.89, 6.47 82.68, 6.87 81.37, 7 80, 6.87 78.63, 6.68 77.9, 3.82 68.8, 3.7 68.47, 3.33 67.78, 2.83 67.17, 2.22 66.67, 1.53 66.3, 0.78 66.08, 0 66, -0.78 66.08, -1.53 66.3, -2.22 66.67, -2.83 67.17, -3.33 67.78, -3.7 68.47, -3.82 68.8, -6.68 77.9, -6.87 78.63))" + ); + } + + public void testNoShortCapSegments() { + checkBuffer("LINESTRING (6.85 78.25, 18 87)", + new double[] { 5, 9 }, + "POLYGON ((11.64 93.36, 13 94.48, 14.56 95.31, 16.24 95.83, 18 96, 19.76 95.83, 21.44 95.31, 23 94.48, 24.36 93.36, 25.48 92, 26.31 90.44, 26.83 88.76, 27 87, 26.83 85.24, 26.31 83.56, 25.48 82, 24.36 80.64, 23 79.52, 21.33 78.64, 8.7 73.61, 7.83 73.35, 6.85 73.25, 5.87 73.35, 4.94 73.63, 4.07 74.09, 3.31 74.71, 2.69 75.47, 2.23 76.34, 1.95 77.27, 1.85 78.25, 1.95 79.23, 2.23 80.16, 2.78 81.15, 10.67 92.22, 11.64 93.36))" + ); + } + + //================================================================ + private void checkBuffer(String wkt, double startDist, double endDist, String wktExpected) { Geometry geom = read(wkt); @@ -82,6 +121,14 @@ private void checkBuffer(String wkt, double startDist, double endDist, checkBuffer(result, wktExpected); } + private void checkBuffer(String wkt, double[] dist, + String wktExpected) { + Geometry geom = read(wkt); + Geometry result = VariableBuffer.buffer(geom, dist); + //System.out.println(result); + checkBuffer(result, wktExpected); + } + private void checkBuffer(Geometry actual, String wktExpected) { Geometry expected = read(wktExpected); checkEqual(expected, actual, DEFAULT_TOLERANCE); diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java index f8e1e2ccf9..06de1b7b14 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGSnappingNoderTest.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.operation.overlayng; +import static org.locationtech.jts.operation.overlayng.OverlayNG.DIFFERENCE; import static org.locationtech.jts.operation.overlayng.OverlayNG.INTERSECTION; import static org.locationtech.jts.operation.overlayng.OverlayNG.UNION; @@ -70,11 +71,27 @@ public void testTrianglesBSegmentsDisplacedUnion() { checkEqual(expected, union(a, b, 0.1)); } + /** + * Failing due to OverlayUtil#isResultAreaConsistent + * See https://github.com/locationtech/jts/issues/951 + */ + public void testRotatedVerticesDifference() { + Geometry a = read("POLYGON ((0.37676311 2.57570853, 7.28652472 0.00028375, 7.60034931 0.81686059, 0.50229292 3.4551325, 0.37676311 2.57570853))"); + Geometry b = read("POLYGON ((0.50229292 3.4551325, 7.60034931 0.81686059, 7.28652472 0.00028375, 0.37676311 2.57570853, 0.50229292 3.4551325))"); + Geometry expected = read("POLYGON EMPTY"); + checkEqual(expected, difference(a, b, 0.00001)); + } + public static Geometry union(Geometry a, Geometry b, double tolerance) { Noder noder = getNoder(tolerance); return OverlayNG.overlay(a, b, UNION, null, noder ); } + public static Geometry difference(Geometry a, Geometry b, double tolerance) { + Noder noder = getNoder(tolerance); + return OverlayNG.overlay(a, b, DIFFERENCE, null, noder ); + } + private static Noder getNoder(double tolerance) { SnappingNoder snapNoder = new SnappingNoder(tolerance); return new ValidatingNoder(snapNoder); diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java index 8b7ee3e872..51975ea0be 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java @@ -101,6 +101,12 @@ public void testIntersectsSnappedEndpoint2() runRelateTest(a, b, "FF10F0102" ); } + public void testMultiPointWithEmpty() + { + String a = "MULTIPOINT(EMPTY,(0 0))"; + String b = "POLYGON ((1 0,0 1,-1 0,0 -1, 1 0))"; + runRelateTest(a, b, "0FFFFF212" ); + } void runRelateTest(String wkt1, String wkt2, String expectedIM) { diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java new file mode 100644 index 0000000000..ff8505d29a --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/AdjacentEdgeLocatorTest.java @@ -0,0 +1,85 @@ +/* + * 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 org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Location; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class AdjacentEdgeLocatorTest extends GeometryTestCase { + + public static void main(String args[]) { + TestRunner.run(AdjacentEdgeLocatorTest.class); + } + + public AdjacentEdgeLocatorTest(String name) { + super(name); + } + + public void testAdjacent2() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 1, 1 1, 1 9)), POLYGON ((9 9, 9 1, 5 1, 5 9, 9 9)))", + 5, 5, Location.INTERIOR + ); + } + + public void testNonAdjacent() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 4 9, 5 1, 1 1, 1 9)), POLYGON ((9 9, 9 1, 5 1, 5 9, 9 9)))", + 5, 5, Location.BOUNDARY + ); + } + + public void testAdjacent6WithFilledHoles() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1)))", + 6, 6, Location.INTERIOR + ); + } + + public void testAdjacent5WithEmptyHole() { + checkLocation( + "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1)))", + 6, 6, Location.BOUNDARY + ); + } + + public void testContainedAndAdjacent() { + String wkt = "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)), POLYGON ((9 2, 2 2, 2 8, 9 8, 9 2)))"; + checkLocation(wkt, + 9, 5, Location.BOUNDARY + ); + checkLocation(wkt, + 9, 8, Location.BOUNDARY + ); + } + + /** + * Tests a bug caused by incorrect point-on-segment logic. + */ + public void testDisjointCollinear() { + checkLocation( + "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 4 4, 4 1, 1 1, 1 4)), ((5 4, 8 4, 8 1, 5 1, 5 4))))", + 2, 4, Location.BOUNDARY + ); + } + + private void checkLocation(String wkt, int x, int y, int expectedLoc) { + Geometry geom = read(wkt); + AdjacentEdgeLocator ael = new AdjacentEdgeLocator(geom); + int loc = ael.locate(new Coordinate(x, y)); + assertEquals("Locations are not equal: ", expectedLoc, loc); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java new file mode 100644 index 0000000000..522f785700 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/LinearBoundaryTest.java @@ -0,0 +1,97 @@ +/* + * 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.HashSet; +import java.util.List; +import java.util.Set; + +import org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.util.LineStringExtracter; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class LinearBoundaryTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(LinearBoundaryTest.class); + } + + public LinearBoundaryTest(String name) { + super(name); + } + + public void testLineMod2() { + checkLinearBoundary("LINESTRING (0 0, 9 9)", + BoundaryNodeRule.MOD2_BOUNDARY_RULE, + "MULTIPOINT((0 0), (9 9))"); + } + + public void testLines2Mod2() { + checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1))", + BoundaryNodeRule.MOD2_BOUNDARY_RULE, + "MULTIPOINT((0 0), (5 1))"); + } + + public void testLines3Mod2() { + checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1), (9 9, 1 5))", + BoundaryNodeRule.MOD2_BOUNDARY_RULE, + "MULTIPOINT((0 0), (5 1), (1 5), (9 9))"); + } + + public void testLines3Monvalent() { + checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1), (9 9, 1 5))", + BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE, + "MULTIPOINT((0 0), (5 1), (1 5))"); + } + + private void checkLinearBoundary(String wkt, BoundaryNodeRule bnr, String wktBdyExpected) { + Geometry geom = read(wkt); + LinearBoundary lb = new LinearBoundary(extractLines(geom), bnr); + boolean hasBoundaryExpected = wktBdyExpected == null ? false : true; + assertEquals("HasBoundary", hasBoundaryExpected, lb.hasBoundary()); + + checkBoundaryPoints(lb, geom, wktBdyExpected); + } + + private void checkBoundaryPoints(LinearBoundary lb, Geometry geom, String wktBdyExpected) { + Set bdySet = extractPoints(wktBdyExpected); + + for (Coordinate p : bdySet) { + assertTrue(lb.isBoundary(p)); + } + + Coordinate[] allPts = geom.getCoordinates(); + for (Coordinate p : allPts) { + if (! bdySet.contains(p)) { + assertFalse(lb.isBoundary(p)); + } + } + } + + private Set extractPoints(String wkt) { + Set ptSet = new HashSet(); + if (wkt == null) return ptSet; + Coordinate[] pts = read(wkt).getCoordinates(); + for (Coordinate p : pts) { + ptSet.add(p); + } + return ptSet; + } + + private List extractLines(Geometry geom) { + return LineStringExtracter.getLines(geom); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java new file mode 100644 index 0000000000..8166e92526 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/PolygonNodeConverterTest.java @@ -0,0 +1,154 @@ +/* + * 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.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Dimension; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class PolygonNodeConverterTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(PolygonNodeConverterTest.class); + } + + public PolygonNodeConverterTest(String name) { + super(name); + } + + public void testShells() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionShell( 8,9, 5,5, 6,9 ), + sectionShell( 4,9, 5,5, 2,9 ) ), + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionShell( 8,9, 5,5, 6,9 ), + sectionShell( 4,9, 5,5, 2,9 ) ) + ); + } + + public void testShellAndHole() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionHole( 6,0, 5,5, 4,0 ) ), + collect( + sectionShell( 1,1, 5,5, 4,0 ), + sectionShell( 6,0, 5,5, 9,9 ) ) + ); + } + + public void testShellsAndHoles() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionHole( 6,0, 5,5, 4,0 ), + + sectionShell( 8,8, 5,5, 1,8 ), + sectionHole( 4,8, 5,5, 6,8 ) + ), + collect( + sectionShell( 1,1, 5,5, 4,0 ), + sectionShell( 6,0, 5,5, 9,9 ), + + sectionShell( 4,8, 5,5, 1,8 ), + sectionShell( 8,8, 5,5, 6,8 ) + ) + ); + } + + public void testShellAnd2Holes() { + checkConversion( + collect( + sectionShell( 1,1, 5,5, 9,9 ), + sectionHole( 7,0, 5,5, 6,0 ), + sectionHole( 4,0, 5,5, 3,0 ) ), + collect( + sectionShell( 1,1, 5,5, 3,0 ), + sectionShell( 4,0, 5,5, 6,0 ), + sectionShell( 7,0, 5,5, 9,9 ) ) + ); + } + + public void testHoles() { + checkConversion( + collect( + sectionHole( 7,0, 5,5, 6,0 ), + sectionHole( 4,0, 5,5, 3,0 ) ), + collect( + sectionShell( 4,0, 5,5, 6,0 ), + sectionShell( 7,0, 5,5, 3,0 ) ) + ); + } + + private void checkConversion(List input, List expected) { + List actual = PolygonNodeConverter.convert(input); + boolean isEqual = checkSectionsEqual(actual, expected); + if (! isEqual) { + System.out.println("Expected:" + formatSections(expected)); + System.out.println("Actual:" + formatSections(actual)); + } + assertTrue(isEqual); + } + + private String formatSections(List sections) { + StringBuilder sb = new StringBuilder(); + for (NodeSection ns : sections) { + sb.append(ns + "\n"); + } + return sb.toString(); + } + + private boolean checkSectionsEqual(List ns1, List ns2) { + if (ns1.size() != ns2.size()) + return false; + sort(ns1); + sort(ns2); + for (int i = 0; i < ns1.size(); i++) { + int comp = ns1.get(i).compareTo(ns2.get(i)); + if (comp != 0) + return false; + } + return true; + } + + private void sort(List ns) { + ns.sort(new NodeSection.EdgeAngleComparator()); + } + + private List collect(NodeSection... sections) { + List sectionList = new ArrayList(); + for (NodeSection s : sections) { + sectionList.add(s); + } + return sectionList; + } + + private NodeSection sectionHole(double v0x, double v0y, double nx, double ny, double v1x, double v1y) { + return section(1, v0x, v0y, nx, ny, v1x, v1y); + } + + private NodeSection section(int ringId, double v0x, double v0y, double nx, double ny, double v1x, double v1y) { + return new NodeSection(true, Dimension.A, 1, ringId, null, false, + new Coordinate(v0x, v0y), new Coordinate(nx, ny), new Coordinate(v1x, v1y)); + } + + private NodeSection sectionShell(double v0x, double v0y, double nx, double ny, double v1x, double v1y) { + return section(0, v0x, v0y, nx, ny, v1x, v1y); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java new file mode 100644 index 0000000000..3c5e116803 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateGeometryTest.java @@ -0,0 +1,72 @@ +/* + * 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.Set; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class RelateGeometryTest extends GeometryTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateGeometryTest.class); + } + + public RelateGeometryTest(String name) { + super(name); + } + + public void testUniquePoints() { + Geometry geom = read("MULTIPOINT ((0 0), (5 5), (5 0), (0 0))"); + RelateGeometry rgeom = new RelateGeometry(geom); + Set pts = rgeom.getUniquePoints(); + assertEquals("Unique pts size", 3, pts.size()); + } + + public void testBoundary() { + Geometry geom = read("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1))"); + RelateGeometry rgeom = new RelateGeometry(geom); + assertTrue("hasBoundary", rgeom.hasBoundary()); + } + + public void testHasDimension() { + Geometry geom = read("GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9)), LINESTRING (1 1, 5 4), POINT (6 5))"); + RelateGeometry rgeom = new RelateGeometry(geom); + assertTrue("hasDimension 0", rgeom.hasDimension(0)); + assertTrue("hasDimension 1", rgeom.hasDimension(1)); + assertTrue("hasDimension 2", rgeom.hasDimension(2)); + } + + public void testDimension() { + checkDimension("POINT (0 0)", 0, 0); + checkDimension("LINESTRING (0 0, 0 0)", 1, 0); + checkDimension("LINESTRING (0 0, 9 9)", 1, 1); + checkDimension("LINESTRING (0 0, 0 0, 9 9)", 1, 1); + checkDimension("POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9))", 2, 2); + checkDimension("GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9)), LINESTRING (1 1, 5 4), POINT (6 5))", 2, 2); + checkDimension("GEOMETRYCOLLECTION (POLYGON EMPTY, LINESTRING (1 1, 5 4), POINT (6 5))", 2, 1); + } + + private void checkDimension(String wkt, int expectedDim, int expectedDimReal) { + Geometry geom = read(wkt); + RelateGeometry rgeom = new RelateGeometry(geom); + assertEquals(expectedDim, rgeom.getDimension()); + assertEquals(expectedDimReal, rgeom.getDimensionReal()); + } + + + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java new file mode 100644 index 0000000000..81896d1555 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGBoundaryNodeRuleTest.java @@ -0,0 +1,174 @@ +/* + * 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 org.locationtech.jts.algorithm.BoundaryNodeRule; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.IntersectionMatrix; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + + +/** + * Tests {@link RelateNG} with {@link BoundaryNodeRule}s. + * + * @author Martin Davis + * @version 1.7 + */ +public class RelateNGBoundaryNodeRuleTest + extends GeometryTestCase +{ + public static void main(String args[]) { + TestRunner.run(RelateNGBoundaryNodeRuleTest.class); + } + + public RelateNGBoundaryNodeRuleTest(String name) + { + super(name); + } + + public void testMultiLineStringSelfIntTouchAtEndpoint() + { + String a = "MULTILINESTRING ((20 20, 100 100, 100 20, 20 100), (60 60, 60 140))"; + String b = "LINESTRING (60 60, 20 60)"; + + // under EndPoint, A has a boundary node - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F00102" ); + } + + public void testLineStringSelfIntTouchAtEndpoint() + { + String a = "LINESTRING (20 20, 100 100, 100 20, 20 100)"; + String b = "LINESTRING (60 60, 20 60)"; + + // results for both rules are the same + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FF0102" ); + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "F01FF0102" ); + } + + public void testMultiLineStringTouchAtEndpoint() + { + String a = "MULTILINESTRING ((0 0, 10 10), (10 10, 20 20))"; + String b = "LINESTRING (10 10, 20 0)"; + + // under Mod2, A touch point is not boundary - A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FF0102" ); + // under EndPoint, A has a boundary node - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F00102" ); + // under MonoValent, A touch point is not boundary - A.bdy / B.bdy = F and A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE, "F01FF0102" ); + // under MultiValent, A has a boundary node but B does not - A.bdy / B.bdy = F and A.bdy / B.int = 0 + runRelate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE, "FF10FF1F2" ); + } + + public void testMultiLineStringClosedTouchAtEndpoint() + { + String a = "MULTILINESTRING ((0 0, 10 10), (10 10, 0 20, 0 0))"; + String b = "LINESTRING (10 10, 20 0)"; + + // under Mod2, A has no boundary - A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FFF102" ); + // under EndPoint, A endpoints are in boundary - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F00102" ); + // under MonoValent, A touch point is not boundary - A.bdy / B.bdy = F and A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE, "F01FFF102" ); + // under MultiValent, A has a boundary node but B does not - A.bdy / B.bdy = F and A.bdy / B.int = 0 + runRelate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE, "FF10F01F2" ); + } + + public void testLineRingTouchAtEndpoints() + { + String a = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + String b = "LINESTRING (20 20, 20 100)"; + + // under Mod2, A has no boundary - A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FFF102" ); + // under EndPoint, A has a boundary node - A.bdy / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FF1F0F102" ); + // under MonoValent, A has no boundary node but B does - A.bdy / B.bdy = F and A.int / B.bdy = 0 + runRelate(a, b, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE, "F01FFF102" ); + // under MultiValent, A has a boundary node but B does not - A.bdy / B.bdy = F and A.bdy / B.int = 0 + runRelate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE, "FF10FF1F2" ); + } + + public void testLineRingTouchAtEndpointAndInterior() + { + String a = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + String b = "LINESTRING (20 20, 40 100)"; + + // this is the same result as for the above test + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "F01FFF102" ); + // this result is different - the A node is now on the boundary, so A.bdy/B.ext = 0 + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "F01FF0102" ); + } + + public void testPolygonEmptyRing() + { + String a = "POLYGON EMPTY"; + String b = "LINESTRING (20 100, 20 220, 120 100, 20 100)"; + + // closed line has no boundary under SFS rule + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + + public void testPolygonEmptyMultiLineStringClosed() + { + String a = "POLYGON EMPTY"; + String b = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))"; + + // closed line has no boundary under SFS rule + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + + public void testPolygonEqualRotated() + { + String a = "POLYGON ((0 0, 140 0, 140 140, 0 140, 0 0))"; + String b = "POLYGON ((140 0, 0 0, 0 140, 140 140, 140 0))"; + + // BNR only considers linear endpoints, so results are equal for all rules + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "2FFF1FFF2" ); + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "2FFF1FFF2" ); + runRelate(a, b, BoundaryNodeRule.MONOVALENT_ENDPOINT_BOUNDARY_RULE, "2FFF1FFF2" ); + runRelate(a, b, BoundaryNodeRule.MULTIVALENT_ENDPOINT_BOUNDARY_RULE, "2FFF1FFF2" ); + } + + public void testLineStringInteriorTouchMultivalent() + { + String a = "POLYGON EMPTY"; + String b = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))"; + + // closed line has no boundary under SFS rule + runRelate(a, b, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE, "FFFFFF1F2" ); + + // closed line has boundary under ENDPOINT rule + runRelate(a, b, BoundaryNodeRule.ENDPOINT_BOUNDARY_RULE, "FFFFFF102" ); + } + + void runRelate(String wkt1, String wkt2, BoundaryNodeRule bnRule, String expectedIM) + { + Geometry g1 = read(wkt1); + Geometry g2 = read(wkt2); + IntersectionMatrix im = RelateNG.relate(g1, g2, bnRule); + String imStr = im.toString(); + //System.out.println(imStr); + assertTrue("Expected " + expectedIM + ", found " + im, im.matches(expectedIM)); + } + +} 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..b17a098cae --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGGCTest.java @@ -0,0 +1,236 @@ +/* + * 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 junit.textui.TestRunner; + +public class RelateNGGCTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGGCTest.class); + } + + public RelateNGGCTest(String name) { + super(name); + } + + public void testDimensionWithEmpty() { + String a = "LINESTRING(0 0, 1 1)"; + String b = "GEOMETRYCOLLECTION(POLYGON EMPTY,LINESTRING(0 0, 1 1))"; + checkCoversCoveredBy(a, b, true); + checkEquals(a, b, true); + } + + // 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); + } + + public void testLineInOverlappingPolygonsTouchingInteriorEdge() { + String a = "LINESTRING (3 7, 7 3)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 9, 7 9, 7 3, 1 3, 1 9)), POLYGON ((9 1, 3 1, 3 7, 9 7, 9 1)))"; + checkRelate(a, b, "1FF0FF212"); + checkContainsWithin(b, a, true); + } + + public void testLineInOverlappingPolygonsCrossingInteriorEdgeAtVertex() { + String a = "LINESTRING (2 2, 8 8)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1)), POLYGON ((9 9, 9 3, 3 3, 3 9, 9 9)))"; + checkRelate(a, b, "1FF0FF212"); + checkContainsWithin(b, a, true); + } + + public void testLineInOverlappingPolygonsCrossingInteriorEdgeProper() { + String a = "LINESTRING (2 4, 6 8)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1)), POLYGON ((9 9, 9 3, 3 3, 3 9, 9 9)))"; + checkRelate(a, b, "1FF0FF212"); + checkContainsWithin(b, a, true); + } + + public void testPolygonInOverlappingPolygonsTouchingBoundaries() { + String a = "GEOMETRYCOLLECTION (POLYGON ((1 9, 6 9, 6 4, 1 4, 1 9)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)) )"; + String b = "POLYGON ((2 6, 6 2, 8 4, 4 8, 2 6))"; + checkRelate(a, b, "212F01FF2"); + checkContainsWithin(a, b, true); + } + + public void testLineInOverlappingPolygonsBoundaries() { + String a = "LINESTRING (1 6, 9 6, 9 1, 1 1, 1 6)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))"; + checkRelate(a, b, "F1FFFF2F2"); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkCoversCoveredBy(b, a, true); + } + + public void testLineCoversOverlappingPolygonsBoundaries() { + String a = "LINESTRING (1 6, 9 6, 9 1, 1 1, 1 6)"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))"; + checkRelate(a, b, "F1FFFF2F2"); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(b, a, true); + } + + public void testAdjacentPolygonsContainedInAdjacentPolygons() { + String a = "GEOMETRYCOLLECTION (POLYGON ((2 2, 2 5, 4 5, 4 2, 2 2)), POLYGON ((8 2, 4 3, 4 4, 8 5, 8 2)))"; + String b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))"; + checkRelate(a, b, "2FF1FF212"); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(b, a, true); + } + + public void testGCMultiPolygonIntersectsPolygon() { + String a = "POLYGON ((2 5, 3 5, 3 3, 2 3, 2 5))"; + String b = "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 4 4, 4 1, 1 1, 1 4)), ((5 4, 8 4, 8 1, 5 1, 5 4))))"; + checkRelate(a, b, "212101212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(b, a, false); + } + + public void testPolygonContainsGCMultiPolygonElement() { + String a = "POLYGON ((0 5, 4 5, 4 1, 0 1, 0 5))"; + String b = "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 3 4, 3 2, 1 2, 1 4)), ((6 4, 8 4, 8 2, 6 2, 6 4))))"; + checkRelate(a, b, "212FF1212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(b, a, false); + } + + /** + * Demonstrates the need for assigning computed nodes to their rings, + * so that subsequent PIP testing can report node as being on ring boundary. + */ + public void testPolygonOverlappingGCPolygon() { + String a = "GEOMETRYCOLLECTION (POLYGON ((18.6 40.8, 16.8825 39.618567, 16.9319 39.5461, 17.10985 39.485133, 16.6143 38.4302, 16.43145 38.313267, 16.2 37.5, 14.8 37.8, 14.96475 40.474933, 18.6 40.8)))"; + String b = "POLYGON ((16.3649953125 38.37219358064516, 16.3649953125 39.545924774193544, 17.949465625000002 39.545924774193544, 17.949465625000002 38.37219358064516, 16.3649953125 38.37219358064516))"; + checkRelate(b, a, "212101212"); + checkRelate(a, b, "212101212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, false); + } + + static final String wktAdjacentPolys = "GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9)))"; + + public void testAdjPolygonsCoverPolygonWithEndpointInside() { + String a = wktAdjacentPolys; + String b = "POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7))"; + checkRelate(b, a, "2FF1FF212"); + checkRelate(a, b, "212FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsCoverPointAtNode() { + String a = wktAdjacentPolys; + String b = "POINT (5 5)"; + checkRelate(b, a, "0FFFFF212"); + checkRelate(a, b, "0F2FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsCoverPointOnEdge() { + String a = wktAdjacentPolys; + String b = "POINT (7 5)"; + checkRelate(b, a, "0FFFFF212"); + checkRelate(a, b, "0F2FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsContainingPolygonTouchingInteriorEndpoint() { + String a = wktAdjacentPolys; + String b = "POLYGON ((5 5, 7 5, 7 3, 5 3, 5 5))"; + checkRelate(a, b, "212FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsOverlappedByPolygonWithHole() { + String a = wktAdjacentPolys; + String b = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (2 8, 8 8, 8 2, 2 2, 2 8))"; + checkRelate(a, b, "2121FF212"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, false); + } + + public void testAdjPolygonsContainingLine() { + String a = wktAdjacentPolys; + String b = "LINESTRING (5 5, 7 7)"; + checkRelate(a, b, "102FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testAdjPolygonsContainingLineAndPoint() { + String a = wktAdjacentPolys; + String b = "GEOMETRYCOLLECTION (POINT (5 5), LINESTRING (5 7, 7 7))"; + checkRelate(a, b, "102FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkCoversCoveredBy(a, b, true); + } + + public void testEmptyMultiPointElements() { + String a = "POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7))"; + String b = "GEOMETRYCOLLECTION (MULTIPOINT (EMPTY, (5 5)), LINESTRING (1 9, 4 9))"; + checkIntersectsDisjoint(a, b, true); + } + + public void testPolygonContainingPointsInBoundary() { + String a = "POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))"; + String b = "GEOMETRYCOLLECTION (POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)), MULTIPOINT ((0 2), (0 5)))"; + checkEquals(a, b, true); + } + + public void testPolygonContainingLineInBoundary() { + String a = "POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))"; + String b = "GEOMETRYCOLLECTION (POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)), LINESTRING (0 2, 0 5))"; + checkEquals(a, b, true); + } + + public void testPolygonContainingLineInBoundaryAndInterior() { + String a = "POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))"; + String b = "GEOMETRYCOLLECTION (POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)), LINESTRING (0 2, 0 5, 5 5))"; + checkEquals(a, b, true); + } + + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java new file mode 100644 index 0000000000..99cf7e4d15 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGRobustnessTest.java @@ -0,0 +1,203 @@ +/* + * 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 junit.textui.TestRunner; + +/** + * Tests from reported cases with robustness issues. + * + * @author mdavis + * + */ +public class RelateNGRobustnessTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGRobustnessTest.class); + } + + public RelateNGRobustnessTest(String name) { + super(name); + } + + //-------------------------------------------------------- + // GeometryCollection semantics + //-------------------------------------------------------- + + // see https://github.com/libgeos/geos/issues/1033 + public void testGEOS_1033() { + checkContainsWithin("POLYGON((1 0,0 4,2 2,1 0))", + "GEOMETRYCOLLECTION(POINT(2 2),POINT(1 0),LINESTRING(1 2,1 1))", + true); + } + + // https://github.com/libgeos/geos/issues/1027 + public void testGEOS_1027() { + checkCoversCoveredBy("MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0)))", + "GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0))", + true); + } + + // https://github.com/libgeos/geos/issues/1022 + public void testGEOS_1022() { + checkCrosses("GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4))", + "POLYGON ((7 1, 1 3, 3 9, 7 1))", + false); + } + + // https://github.com/libgeos/geos/issues/1011 + public void testGEOS_1011() { + String a = "LINESTRING(75 15,55 43)"; + String b = "GEOMETRYCOLLECTION(POLYGON EMPTY,LINESTRING(75 15,55 43))"; + checkCoversCoveredBy(a, b, true); + checkEquals(a, b, true); + } + + // https://github.com/libgeos/geos/issues/983 + public void testGEOS_983() { + String a = "POINT(0 0)"; + String b = "GEOMETRYCOLLECTION(POINT (1 1), LINESTRING (1 1, 2 2))"; + checkIntersectsDisjoint(a, b, false); + } + + // https://github.com/libgeos/geos/issues/982 + public void testGEOS_982() { + String a = "POINT(0 0)"; + String b1 = "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0))"; + checkContainsWithin(b1, a, false); + checkCoversCoveredBy(b1, a, true); + + String b2 = "GEOMETRYCOLLECTION(LINESTRING(0 0, 1 0), POINT(0 0))"; + checkContainsWithin(b2, a, false); + checkCoversCoveredBy(b2, a, true); + } + + // https://github.com/libgeos/geos/issues/981 + public void testGEOS_981() { + String a = "POINT(0 0)"; + String b = "GEOMETRYCOLLECTION(LINESTRING(0 1, 0 0), POINT(0 0))"; + checkRelateMatches(b, a, IntersectionMatrixPattern.CONTAINS_PROPERLY, false); + } + + + //-------------------------------------------------------- + // Noding robustness problems + //-------------------------------------------------------- + + // https://github.com/libgeos/geos/issues/1053 + public void testGEOS_1053() { + String a = "MULTILINESTRING((2 4, 10 10),(15 10,10 5,5 10))"; + String b = "MULTILINESTRING((2 4, 10 10))"; + checkRelate(a, b, "1F1F00FF2"); + } + + // https://github.com/libgeos/geos/issues/968 + public void testGEOS_968() { + String a2 = "LINESTRING(10 0, 0 20)"; + String b2 = "POINT (9 2)"; + checkCoversCoveredBy(a2, b2, true); + } + + public void xtestGEOS_968_2() { + String a = "LINESTRING(1 0, 0 2)"; + String b = "POINT (0.9 0.2)"; + //-- this case doesn't work due to numeric rounding for Orientation test + checkCoversCoveredBy(a, b,true); + } + + // https://github.com/libgeos/geos/issues/933 + public void testGEOS_933() { + String a = "LINESTRING (0 0, 1 1)"; + String b = "LINESTRING (0.2 0.2, 0.5 0.5)"; + checkCoversCoveredBy(a, b, true); + } + + // https://github.com/libgeos/geos/issues/740 + public void testGEOS_740() { + String a = "POLYGON ((1454700 -331500, 1455100 -330700, 1455466.6191038645 -331281.94727476506, 1455467.8182005754 -331293.26796732045, 1454700 -331500))"; + String b = "LINESTRING (1455389.376551584 -331255.3803222172, 1455467.2422460222 -331287.83037053316)"; + checkContainsWithin(a, b, false); + } + + //-------------------------------------------------------- + // Robustness failures (TopologyException in old code) + //-------------------------------------------------------- + + // https://github.com/libgeos/geos/issues/766 + public void testGEOS_766() { + String a = "POLYGON ((26639.240191093646 6039.3615818717535, 26639.240191093646 5889.361620883223, 28000.000095100608 5889.362081553552, 28000.000095100608 6039.361620882992, 28700.00019021402 6039.361620882992, 28700.00019021402 5889.361822800367, 29899.538842431968 5889.362160452064, 32465.59665091549 5889.362882757903, 32969.2837182586 -1313.697771558439, 31715.832811969216 -1489.87008918589, 31681.039836323587 -1242.3030298361555, 32279.3890331618 -1158.210534269224, 32237.63710287376 -861.1301136466199, 32682.89764107368 -802.0828534499739, 32247.445200905553 5439.292852892075, 31797.06861513178 5439.292852892075, 31797.06861513178 5639.36178850523, 29899.538849750803 5639.361268079038, 26167.69458275995 5639.3602445643955, 26379.03654594742 2617.0293071870683, 26778.062167926924 2644.9318977193907, 26792.01346261031 2445.419086759444, 26193.472956813417 2403.5650586598513, 25939.238114175267 6039.361685403233, 26639.240191093646 6039.3615818717535), (32682.89764107368 -802.0828534499738, 32682.89764107378 -802.0828534499669, 32247.445200905655 5439.292852892082, 32247.445200905553 5439.292852892075, 32682.89764107368 -802.0828534499738))"; + String b = "POLYGON ((32450.100392347143 5889.362314133216, 32050.104955691 5891.272957209961, 32100.021071878822 16341.272221116333, 32500.016508656867 16339.361578039587, 32450.100392347143 5889.362314133216))"; + checkIntersectsDisjoint(a, b, true); + } + + // https://github.com/libgeos/geos/issues/1026 + public void testGEOS_1026() { + String a = "POLYGON((335645.7810000004 5677846.65,335648.6579999998 5677845.801999999,335650.8630842535 5677845.143617179,335650.77673334075 5677844.7250704905,335642.90299999993 5677847.498,335645.7810000004 5677846.65))"; + String b = "POLYGON((335642.903 5677847.498,335642.894 5677847.459,335645.92 5677846.69,335647.378 5677852.523,335644.403 5677853.285,335644.374 5677853.293,335642.903 5677847.498))"; + checkTouches(a, b, false); + } + + // https://github.com/libgeos/geos/issues/1069 =- too large to reproduce here + + // https://trac.osgeo.org/postgis/ticket/5583 =- too large to reproduce here + + // https://github.com/locationtech/jts/issues/1051 + public void testJTS_1051() { + String a = "POLYGON ((414188.5999999999 6422867.1, 414193.7 6422866.5, 414205.1 6422859.4, 414223.7 6422846.8, 414229.6 6422843.2, 414235.2 6422835.4, 414224.7 6422837.9, 414219.4 6422842.1, 414210.9 6422849, 414199.2 6422857.6, 414191.1 6422863.4, 414188.5999999999 6422867.1))"; + String b = "LINESTRING (414187.2 6422831.6, 414179 6422836.1, 414182.2 6422841.8, 414176.7 6422844, 414184.5 6422859.5, 414188.6 6422867.1)"; + checkIntersectsDisjoint(a, b, true); + } + + // https://trac.osgeo.org/postgis/ticket/5362 + public void testPostGIS_5362() { + String a = "POLYGON ((-707259.66 -1121493.36, -707205.9 -1121605.808, -707310.5388 -1121540.5446, -707318.8200000001 -1121533.21, -707259.66 -1121493.36))"; + String b = "POLYGON ((-707356.18 -1121550.69, -707332.82 -1121536.63, -707318.82 -1121533.21, -707321.72 -1121535.08, -707327.4 -1121539.21, -707356.18 -1121550.69))"; + checkRelate(a, b, "2F2101212"); + checkIntersectsDisjoint(a, b, true); + } + + //-------------------------------------------------------- + // Topological Inconsistency + //-------------------------------------------------------- + + // https://github.com/libgeos/geos/issues/1064 + public void testGEOS_1064() { + String a = "LINESTRING (16.330791631988802 68.75635661578073, 16.332533372319826 68.75496886016562)"; + String b = "LINESTRING (16.30641253121884 68.75189557630306, 16.33167771310482 68.75565061843871)"; + checkRelate(a, b, "F01FF0102"); + } + + // https://github.com/locationtech/jts/issues/396 + public void testJTS_396() { + String a = "LINESTRING (1 0, 0 2, 0 0, 2 2)"; + String b = "LINESTRING (0 0, 2 2)"; + checkRelate(a, b, "101F00FF2"); + checkCoversCoveredBy(a, b, true); + } + +//https://github.com/locationtech/jts/issues/270 + public void testJTS_270() { + String a = "LINESTRING(0.0 0.0, -10.0 1.2246467991473533E-15)"; + String b = "LINESTRING(-9.999143275740073 -0.13089595571333978, -10.0 1.0535676356486768E-13)"; + checkRelate(a, b, "FF10F0102"); + checkIntersectsDisjoint(a, b, true); + } + +//https://gis.stackexchange.com/questions/484691/topologyexception-side-location-conflict-while-intersects-on-valid-polygons + public void testGISSE_484691() { + String a = "POLYGON ((1.839012980156925 43.169860517728324, 1.838983490127865 43.169860200336274, 1.838898525601717 43.169868281549725, 1.838918565176068 43.1699719478626, 1.838920733577112 43.16998636433192, 1.838978629555589 43.16997979090823, 1.838982586839382 43.169966339940714, 1.838974943184281 43.169918580432174, 1.839020497362873 43.169914572864634, 1.839012980156925 43.169860517728324))"; + String b = "POLYGON ((1.8391355300979277 43.16987802887805, 1.83913336164737 43.16986361241434, 1.8390129801569248 43.169860517728324, 1.8390790978572837 43.16987292371998, 1.8390909520103162 43.16995581178317, 1.8391377530291442 43.16995091801345, 1.8391293863398452 43.16987796276235, 1.8391355300979277 43.16987802887805))"; + checkRelate(a, b, "2F2101212"); + checkIntersectsDisjoint(a, b, true); + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java new file mode 100644 index 0000000000..21f1aac5fe --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTest.java @@ -0,0 +1,633 @@ +/* + * Copyright (c) 2022 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 junit.textui.TestRunner; + +public class RelateNGTest extends RelateNGTestCase { + + public static void main(String args[]) { + TestRunner.run(RelateNGTest.class); + } + + public RelateNGTest(String name) { + super(name); + } + + public void testPointsDisjoint() { + String a = "POINT (0 0)"; + String b = "POINT (1 1)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + checkEquals(a, b, false); + checkRelate(a, b, "FF0FFF0F2"); + } + + //======= P/P ============= + + public void testPointsContained() { + String a = "MULTIPOINT (0 0, 1 1, 2 2)"; + String b = "MULTIPOINT (1 1, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkEquals(a, b, false); + checkRelate(a, b, "0F0FFFFF2"); + } + + public void testPointsEqual() { + String a = "MULTIPOINT (0 0, 1 1, 2 2)"; + String b = "MULTIPOINT (0 0, 1 1, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkEquals(a, b, true); + } + + public void testValidateRelatePP_13() { + String a = "MULTIPOINT ((80 70), (140 120), (20 20), (200 170))"; + String b = "MULTIPOINT ((80 70), (140 120), (80 170), (200 80))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + //======= L/P ============= + + public void testLinePointContains() { + String a = "LINESTRING (0 0, 1 1, 2 2)"; + String b = "MULTIPOINT (0 0, 1 1, 2 2)"; + checkRelate(a, b, "0F10FFFF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, false); + } + + public void testLinePointOverlaps() { + String a = "LINESTRING (0 0, 1 1)"; + String b = "MULTIPOINT (0 0, 1 1, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, false); + checkCoversCoveredBy(b, a, false); + } + + public void testZeroLengthLinePoint() { + String a = "LINESTRING (0 0, 0 0)"; + String b = "POINT (0 0)"; + checkRelate(a, b, "0FFFFFFF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, true); + checkEquals(a, b, true); + } + + public void testZeroLengthLineLine() { + String a = "LINESTRING (10 10, 10 10, 10 10)"; + String b = "LINESTRING (10 10, 10 10)"; + checkRelate(a, b, "0FFFFFFF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, true); + checkEquals(a, b, true); + } + + // tests bug involving checking for non-zero-length lines + public void testNonZeroLengthLinePoint() { + String a = "LINESTRING (0 0, 0 0, 9 9)"; + String b = "POINT (1 1)"; + checkRelate(a, b, "0F1FF0FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, true); + checkCoversCoveredBy(b, a, false); + checkEquals(a, b, false); + } + + public void testLinePointIntAndExt() { + String a = "MULTIPOINT((60 60), (100 100))"; + String b = "LINESTRING(40 40, 80 80)"; + checkRelate(a, b, "0F0FFF102"); + } + + //======= L/L ============= + + public void testLinesCrossProper() { + String a = "LINESTRING (0 0, 9 9)"; + String b = "LINESTRING(0 9, 9 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + } + + public void testLinesOverlap() { + String a = "LINESTRING (0 0, 5 5)"; + String b = "LINESTRING(3 3, 9 9)"; + checkIntersectsDisjoint(a, b, true); + checkTouches(a, b, false); + checkOverlaps(a, b, true); + } + + public void testLinesCrossVertex() { + String a = "LINESTRING (0 0, 8 8)"; + String b = "LINESTRING(0 8, 4 4, 8 0)"; + checkIntersectsDisjoint(a, b, true); + } + + public void testLinesTouchVertex() { + String a = "LINESTRING (0 0, 8 0)"; + String b = "LINESTRING(0 8, 4 0, 8 8)"; + checkIntersectsDisjoint(a, b, true); + } + + public void testLinesDisjointByEnvelope() { + String a = "LINESTRING (0 0, 9 9)"; + String b = "LINESTRING(10 19, 19 10)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testLinesDisjoint() { + String a = "LINESTRING (0 0, 9 9)"; + String b = "LINESTRING (4 2, 8 6)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testLinesClosedEmpty() { + String a = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))"; + String b = "LINESTRING EMPTY"; + checkRelate(a, b, "FF1FFFFF2"); + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testLinesRingTouchAtNode() { + String a = "LINESTRING (5 5, 1 8, 1 1, 5 5)"; + String b = "LINESTRING (5 5, 9 5)"; + checkRelate(a, b, "F01FFF102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkTouches(a, b, true); + } + + public void testLinesTouchAtBdy() { + String a = "LINESTRING (5 5, 1 8)"; + String b = "LINESTRING (5 5, 9 5)"; + checkRelate(a, b, "FF1F00102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkTouches(a, b, true); + } + + public void testLinesOverlapWithDisjointLine() { + String a = "LINESTRING (1 1, 9 9)"; + String b = "MULTILINESTRING ((2 2, 8 8), (6 2, 8 4))"; + checkRelate(a, b, "101FF0102"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkOverlaps(a, b, true); + } + + public void testLinesDisjointOverlappingEnvelopes() { + String a = "LINESTRING (60 0, 20 80, 100 80, 80 120, 40 140)"; + String b = "LINESTRING (60 40, 140 40, 140 160, 0 160)"; + checkRelate(a, b, "FF1FF0102"); + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + checkTouches(a, b, false); + } + + /** + * Case from https://github.com/locationtech/jts/issues/270 + * Strictly, the lines cross, since their interiors intersect + * according to the Orientation predicate. + * However, the computation of the intersection point is + * non-robust, and reports it as being equal to the endpoint + * POINT (-10 0.0000000000000012) + * For consistency the relate algorithm uses the intersection node topology. + */ + public void testLinesCross_JTS270() { + String a = "LINESTRING (0 0, -10 0.0000000000000012)"; + String b = "LINESTRING (-9.999143275740073 -0.1308959557133398, -10 0.0000000000001054)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, true); + } + + public void testLinesContained_JTS396() { + String a = "LINESTRING (1 0, 0 2, 0 0, 2 2)"; + String b = "LINESTRING (0 0, 2 2)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + + /** + * This case shows that lines must be self-noded, + * so that node topology is constructed correctly + * (at least for some predicates). + */ + public void testLinesContainedWithSelfIntersection() { + String a = "LINESTRING (2 0, 0 2, 0 0, 2 2)"; + String b = "LINESTRING (0 0, 2 2)"; + //checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testLineContainedInRing() { + String a = "LINESTRING(60 60, 100 100, 140 60)"; + String b = "LINESTRING(100 100, 180 20, 20 20, 100 100)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(b, a, true); + checkCoversCoveredBy(b, a, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + // see https://github.com/libgeos/geos/issues/933 + public void testLineLineProperIntersection() { + String a = "MULTILINESTRING ((0 0, 1 1), (0.5 0.5, 1 0.1, -1 0.1))"; + String b = "LINESTRING (0 0, 1 1)"; + //checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkCrosses(a, b, false); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testLineSelfIntersectionCollinear() { + String a = "LINESTRING (9 6, 1 6, 1 0, 5 6, 9 6)"; + String b = "LINESTRING (9 9, 3 1)"; + checkRelate(a, b, "0F1FFF102"); + } + + //======= A/P ============= + + public void testPolygonPointInside() { + String a = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10))"; + String b = "POINT (1 1)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + } + + public void testPolygonPointOutside() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "POINT (8 8)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testPolygonPointInBoundary() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "POINT (1 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, true); + } + + public void testAreaPointInExterior() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "POINT (7 7)"; + checkRelate(a, b, "FF2FF10F2"); + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + //======= A/L ============= + + + public void testAreaLineContainedAtLineVertex() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "LINESTRING (2 3, 3 5, 4 3)"; + checkIntersectsDisjoint(a, b, true); + //checkContainsWithin(a, b, true); + //checkCoversCoveredBy(a, b, true); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + public void testAreaLineTouchAtLineVertex() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "LINESTRING (1 8, 3 5, 5 8)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonLineInside() { + String a = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10))"; + String b = "LINESTRING (1 8, 3 5, 5 8)"; + checkRelate(a, b, "102FF1FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + } + + public void testPolygonLineOutside() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "LINESTRING (4 8, 9 3)"; + checkIntersectsDisjoint(a, b, false); + checkContainsWithin(a, b, false); + } + + public void testPolygonLineInBoundary() { + String a = "POLYGON ((10 0, 0 0, 0 10, 10 0))"; + String b = "LINESTRING (1 0, 9 0)"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, true); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonLineCrossingContained() { + String a = "MULTIPOLYGON (((20 80, 180 80, 100 0, 20 80)), ((20 160, 180 160, 100 80, 20 160)))"; + String b = "LINESTRING (100 140, 100 40)"; + checkRelate(a, b, "1020F1FF2"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + public void testValidateRelateLA_220() { + String a = "LINESTRING (90 210, 210 90)"; + String b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + /** + * See RelateLA.xml (line 585) + */ + public void testLineCrossingPolygonAtShellHolePoint() { + String a = "LINESTRING (60 160, 150 70)"; + String b = "POLYGON ((190 190, 360 20, 20 20, 190 190), (110 110, 250 100, 140 30, 110 110))"; + checkRelate(a, b, "F01FF0212"); + checkTouches(a, b, true); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testLineCrossingPolygonAtNonVertex() { + String a = "LINESTRING (20 60, 150 60)"; + String b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkTouches(a, b, false); + checkOverlaps(a, b, false); + } + + public void testPolygonLinesContainedCollinearEdge() { + String a = "POLYGON ((110 110, 200 20, 20 20, 110 110))"; + String b = "MULTILINESTRING ((110 110, 60 40, 70 20, 150 20, 170 40), (180 30, 40 30, 110 80))"; + checkRelate(a, b, "102101FF2"); + } + + //======= A/A ============= + + + public void testPolygonsEdgeAdjacent() { + String a = "POLYGON ((1 3, 3 3, 3 1, 1 1, 1 3))"; + String b = "POLYGON ((5 3, 5 1, 3 1, 3 3, 5 3))"; + //checkIntersectsDisjoint(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonsEdgeAdjacent2() { + String a = "POLYGON ((1 3, 4 3, 3 0, 1 1, 1 3))"; + String b = "POLYGON ((5 3, 5 1, 3 0, 4 3, 5 3))"; + //checkIntersectsDisjoint(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, true); + checkOverlaps(a, b, false); + } + + public void testPolygonsNested() { + String a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + String b = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testPolygonsOverlapProper() { + String a = "POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1))"; + String b = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + public void testPolygonsOverlapAtNodes() { + String a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))"; + String b = "POLYGON ((7 3, 5 1, 3 3, 5 5, 7 3))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + public void testPolygonsContainedAtNodes() { + String a = "POLYGON ((1 5, 5 5, 6 2, 1 1, 1 5))"; + String b = "POLYGON ((1 1, 5 5, 6 2, 1 1))"; + //checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, true); + checkCoversCoveredBy(a, b, true); + checkOverlaps(a, b, false); + checkTouches(a, b, false); + } + + public void testPolygonsNestedWithHole() { + String a = "POLYGON ((40 60, 420 60, 420 320, 40 320, 40 60), (200 140, 160 220, 260 200, 200 140))"; + String b = "POLYGON ((80 100, 360 100, 360 280, 80 280, 80 100))"; + //checkIntersectsDisjoint(true, a, b); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + //checkCoversCoveredBy(false, a, b); + //checkOverlaps(true, a, b); + checkPredicate(RelatePredicate.contains(), a, b, false); + //checkTouches(false, a, b); + } + + public void testPolygonsOverlappingWithBoundaryInside() { + String a = "POLYGON ((100 60, 140 100, 100 140, 60 100, 100 60))"; + String b = "MULTIPOLYGON (((80 40, 120 40, 120 80, 80 80, 80 40)), ((120 80, 160 80, 160 120, 120 120, 120 80)), ((80 120, 120 120, 120 160, 80 160, 80 120)), ((40 80, 80 80, 80 120, 40 120, 40 80)))"; + checkRelate(a, b, "21210F212"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, true); + checkTouches(a, b, false); + } + + public void testPolygonsOverlapVeryNarrow() { + String a = "POLYGON ((120 100, 120 200, 200 200, 200 100, 120 100))"; + String b = "POLYGON ((100 100, 100000 110, 100000 100, 100 100))"; + checkRelate(a, b, "212111212"); + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkContainsWithin(b, a, false); + //checkCoversCoveredBy(false, a, b); + //checkOverlaps(true, a, b); + //checkTouches(false, a, b); + } + + public void testValidateRelateAA_86() { + String a = "POLYGON ((170 120, 300 120, 250 70, 120 70, 170 120))"; + String b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150), (170 120, 330 120, 260 50, 100 50, 170 120))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, false); + checkPredicate(RelatePredicate.within(), a, b, false); + checkTouches(a, b, true); + } + + public void testValidateRelateAA_97() { + String a = "POLYGON ((330 150, 200 110, 150 150, 280 190, 330 150))"; + String b = "MULTIPOLYGON (((140 110, 260 110, 170 20, 50 20, 140 110)), ((300 270, 420 270, 340 190, 220 190, 300 270)))"; + checkIntersectsDisjoint(a, b, true); + checkContainsWithin(a, b, false); + checkCoversCoveredBy(a, b, false); + checkOverlaps(a, b, false); + checkPredicate(RelatePredicate.within(), a, b, false); + checkTouches(a, b, true); + } + + public void testAdjacentPolygons() { + String a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))"; + String b = "POLYGON ((9 9, 9 4, 6 4, 6 9, 9 9))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.ADJACENT, true); + } + + public void testAdjacentPolygonsTouchingAtPoint() { + String a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))"; + String b = "POLYGON ((9 9, 9 4, 6 4, 7 9, 9 9))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.ADJACENT, false); + } + + public void testAdjacentPolygonsOverlappping() { + String a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))"; + String b = "POLYGON ((9 9, 9 4, 6 4, 5 9, 9 9))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.ADJACENT, false); + } + + public void testContainsProperlyPolygonContained() { + String a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + String b = "POLYGON ((2 8, 5 8, 5 5, 2 5, 2 8))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.CONTAINS_PROPERLY, true); + } + + public void testContainsProperlyPolygonTouching() { + String a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + String b = "POLYGON ((9 1, 5 1, 5 5, 9 5, 9 1))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.CONTAINS_PROPERLY, false); + } + + public void testContainsProperlyPolygonsOverlapping() { + String a = "GEOMETRYCOLLECTION (POLYGON ((1 9, 6 9, 6 4, 1 4, 1 9)), POLYGON ((2 4, 6 7, 9 1, 2 4)))"; + String b = "POLYGON ((5 5, 6 5, 6 4, 5 4, 5 5))"; + checkRelateMatches(a, b, IntersectionMatrixPattern.CONTAINS_PROPERLY, true); + } + + //================ Repeated Points ============== + + public void testRepeatedPointLL() { + String a = "LINESTRING(0 0, 5 5, 5 5, 5 5, 9 9)"; + String b = "LINESTRING(0 9, 5 5, 5 5, 5 5, 9 0)"; + checkRelate(a, b, "0F1FF0102"); + checkIntersectsDisjoint(a, b, true); + } + + public void testRepeatedPointAA() { + String a = "POLYGON ((1 9, 9 7, 9 1, 1 3, 1 9))"; + String b = "POLYGON ((1 3, 1 3, 1 3, 3 7, 9 7, 9 7, 1 3))"; + checkRelate(a, b, "212F01FF2"); + } + + //================ Repeated Points ============== + + public void testEmptyEquals() { + String empties[] = { + "POINT EMPTY", + "LINESTRING EMPTY", + "POLYGON EMPTY", + "MULTIPOINT EMPTY", + "MULTILINESTRING EMPTY", + "MULTIPOLYGON EMPTY", + "GEOMETRYCOLLECTION EMPTY" + }; + int nempty = 7; + for (int i = 0; i < nempty; i++) { + for (int j = 0; j < nempty; j++) { + String a = empties[i]; + String b = empties[j]; + checkRelate(a, b, "FFFFFFFF2"); + //-- currently in JTS empty geometries do NOT test equal + checkEquals(a, b, false); + } + } + } + + //================ Prepared Relate ============== + + public void testPreparedAA() { + String a = "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))"; + String b = "POLYGON((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))"; + checkPrepared(a, b); + } + + +} 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 new file mode 100644 index 0000000000..01a479ebe8 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelateNGTestCase.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022 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 org.locationtech.jts.geom.Geometry; + +import test.jts.GeometryTestCase; + +public abstract class RelateNGTestCase extends GeometryTestCase { + + private boolean isTrace = false; + + public RelateNGTestCase(String name) { + super(name); + } + + protected void checkIntersectsDisjoint(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.intersects(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.intersects(), wktb, wkta, expectedValue); + checkPredicate(RelatePredicate.disjoint(), wkta, wktb, ! expectedValue); + checkPredicate(RelatePredicate.disjoint(), wktb, wkta, ! expectedValue); + } + + protected void checkContainsWithin(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.contains(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.within(), wktb, wkta, expectedValue); + } + + protected void checkCoversCoveredBy(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.covers(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.coveredBy(), wktb, wkta, expectedValue); + } + + protected void checkCrosses(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.crosses(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.crosses(), wktb, wkta, expectedValue); + } + + protected void checkOverlaps(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.overlaps(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.overlaps(), wktb, wkta, expectedValue); + } + + protected void checkTouches(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.touches(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.touches(), wktb, wkta, expectedValue); + } + + protected void checkEquals(String wkta, String wktb, boolean expectedValue) { + checkPredicate(RelatePredicate.equalsTopo(), wkta, wktb, expectedValue); + checkPredicate(RelatePredicate.equalsTopo(), wktb, wkta, expectedValue); + } + + protected void checkRelate(String wkta, String wktb, String expectedValue) { + Geometry a = read(wkta); + Geometry b = read(wktb); + RelateMatrixPredicate pred = new RelateMatrixPredicate(); + TopologyPredicate predTrace = trace(pred); + RelateNG.relate(a, b, predTrace); + String actualVal = pred.getIM().toString(); + assertEquals(expectedValue, actualVal); + } + + protected void checkRelateMatches(String wkta, String wktb, String pattern, boolean expectedValue) { + TopologyPredicate pred = RelatePredicate.matches(pattern); + checkPredicate(pred, wkta, wktb, expectedValue); + } + + protected void checkPredicate(TopologyPredicate pred, String wkta, String wktb, boolean expectedValue) { + Geometry a = read(wkta); + Geometry b = read(wktb); + TopologyPredicate predTrace = trace(pred); + boolean actualVal = RelateNG.relate(a, b, predTrace); + assertEquals(expectedValue, actualVal); + } + + void checkPrepared(String wkta, String wktb) + { + Geometry a = read(wkta); + Geometry b = read(wktb); + RelateNG prep_a = RelateNG.prepare(a); + + assertEquals("equalsTopo", prep_a.evaluate(b, RelatePredicate.equalsTopo()), + RelateNG.relate(a, b, RelatePredicate.equalsTopo())); + + assertEquals("intersects", prep_a.evaluate(b, RelatePredicate.intersects()), + RelateNG.relate(a, b, RelatePredicate.intersects())); + assertEquals("disjoint", prep_a.evaluate(b, RelatePredicate.disjoint()), + RelateNG.relate(a, b, RelatePredicate.disjoint())); + assertEquals("covers", prep_a.evaluate(b, RelatePredicate.covers()), + RelateNG.relate(a, b, RelatePredicate.covers())); + assertEquals("coveredBy", prep_a.evaluate(b, RelatePredicate.coveredBy()), + RelateNG.relate(a, b, RelatePredicate.coveredBy())); + assertEquals("within", prep_a.evaluate(b, RelatePredicate.within()), + RelateNG.relate(a, b, RelatePredicate.within())); + assertEquals("contains", prep_a.evaluate(b, RelatePredicate.contains()), + RelateNG.relate(a, b, RelatePredicate.contains())); + assertEquals("crosses", prep_a.evaluate(b, RelatePredicate.crosses()), + RelateNG.relate(a, b, RelatePredicate.crosses())); + assertEquals("touches", prep_a.evaluate(b, RelatePredicate.touches()), + RelateNG.relate(a, b, RelatePredicate.touches())); + + assertEquals("relate", prep_a.evaluate(b).toString(), + RelateNG.relate(a, b).toString()); + } + + TopologyPredicate trace(TopologyPredicate pred) { + if (! isTrace) + return pred; + + System.out.println("----------- Pred: " + pred.name()); + + return TopologyPredicateTracer.trace(pred); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java new file mode 100644 index 0000000000..73bb3b05c2 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePointLocatorTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023 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 org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Location; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class RelatePointLocatorTest extends GeometryTestCase { + + public static void main(String args[]) { + TestRunner.run(RelatePointLocatorTest.class); + } + + public RelatePointLocatorTest(String name) { + super(name); + } + + String gcPLA = "GEOMETRYCOLLECTION (POINT (1 1), POINT (2 1), LINESTRING (3 1, 3 9), LINESTRING (4 1, 5 4, 7 1, 4 1), LINESTRING (12 12, 14 14), POLYGON ((6 5, 6 9, 9 9, 9 5, 6 5)), POLYGON ((10 10, 10 16, 16 16, 16 10, 10 10)), POLYGON ((11 11, 11 17, 17 17, 17 11, 11 11)), POLYGON ((12 12, 12 16, 16 16, 16 12, 12 12)))"; + + public void testPoint() { + //String wkt = "GEOMETRYCOLLECTION (POINT(0 0), POINT(1 1))"; + checkDimLocation(gcPLA, 1, 1, DimensionLocation.POINT_INTERIOR); + checkDimLocation(gcPLA, 0, 1, DimensionLocation.EXTERIOR); + } + + public void testPointInLine() { + checkDimLocation(gcPLA, 3, 8, DimensionLocation.LINE_INTERIOR); + } + + public void testPointInArea() { + checkDimLocation(gcPLA, 8, 8, DimensionLocation.AREA_INTERIOR); + } + + public void testLine() { + checkDimLocation(gcPLA, 3, 3, DimensionLocation.LINE_INTERIOR); + checkDimLocation(gcPLA, 3, 1, DimensionLocation.LINE_BOUNDARY); + } + + public void testLineInArea() { + checkDimLocation(gcPLA, 11, 11, DimensionLocation.AREA_INTERIOR); + checkDimLocation(gcPLA, 14, 14, DimensionLocation.AREA_INTERIOR); + } + + public void testArea() { + checkDimLocation(gcPLA, 8, 8, DimensionLocation.AREA_INTERIOR); + checkDimLocation(gcPLA, 9, 9, DimensionLocation.AREA_BOUNDARY); + } + + public void testAreaInArea() { + checkDimLocation(gcPLA, 11, 11, DimensionLocation.AREA_INTERIOR); + checkDimLocation(gcPLA, 12, 12, DimensionLocation.AREA_INTERIOR); + checkDimLocation(gcPLA, 10, 10, DimensionLocation.AREA_BOUNDARY); + checkDimLocation(gcPLA, 16, 16, DimensionLocation.AREA_INTERIOR); + } + + public void testLineNode() { + //checkNodeLocation(gcPLA, 12.1, 12.2, Location.INTERIOR); + checkNodeLocation(gcPLA, 3, 1, Location.BOUNDARY); + } + + public void testLineEndInGCLA() { + String wkt = "GEOMETRYCOLLECTION (POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)), LINESTRING (12 2, 0 2, 0 5, 5 5), LINESTRING (12 10, 12 2))"; + checkLineEndDimLocation(wkt, 5, 5, DimensionLocation.AREA_INTERIOR); + checkLineEndDimLocation(wkt, 12, 2, DimensionLocation.LINE_INTERIOR); + checkLineEndDimLocation(wkt, 12, 10, DimensionLocation.LINE_BOUNDARY); + } + + private void checkDimLocation(String wkt, double x, double y, int expectedDimLoc) { + Geometry geom = read(wkt); + RelatePointLocator locator = new RelatePointLocator(geom); + int actual = locator.locateWithDim(new Coordinate(x, y)); + assertEquals(expectedDimLoc, actual); + } + + private void checkLineEndDimLocation(String wkt, double x, double y, int expectedDimLoc) { + Geometry geom = read(wkt); + RelatePointLocator locator = new RelatePointLocator(geom); + int actual = locator.locateLineEndWithDim(new Coordinate(x, y)); + assertEquals(expectedDimLoc, actual); + } + + private void checkNodeLocation(String wkt, double x, double y, int expectedLoc) { + Geometry geom = read(wkt); + RelatePointLocator locator = new RelatePointLocator(geom); + int actual = locator.locateNode(new Coordinate(x, y), null); + assertEquals(expectedLoc, actual); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java new file mode 100644 index 0000000000..f1acc94e7d --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relateng/RelatePredicateTest.java @@ -0,0 +1,92 @@ +/* + * 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 org.locationtech.jts.geom.Dimension; + +import junit.framework.TestCase; +import junit.textui.TestRunner; + +public class RelatePredicateTest extends TestCase { + + private static final String A_EXT_B_INT = "***.***.1**"; + private static final String A_INT_B_INT = "1**.***.***"; + + public static void main(String args[]) { + TestRunner.run(RelatePredicateTest.class); + } + + public RelatePredicateTest(String name) { + super(name); + } + + public void testIntersects() { + checkPredicate(RelatePredicate.intersects(), A_INT_B_INT, true); + } + + public void testDisjoint() { + checkPredicate(RelatePredicate.intersects(), A_EXT_B_INT, false); + checkPredicate(RelatePredicate.disjoint(), A_EXT_B_INT, true); + } + + public void testCovers() { + checkPredicate(RelatePredicate.covers(), A_INT_B_INT, true); + checkPredicate(RelatePredicate.covers(), A_EXT_B_INT, false); + } + + public void testCoversFast() { + checkPredicatePartial(RelatePredicate.covers(), A_EXT_B_INT, false); + } + + public void testMatch() { + checkPredicate(RelatePredicate.matches("1***T*0**"), "1**.*2*.0**", true); + } + + //======================================================= + + private void checkPredicate(TopologyPredicate pred, String im, boolean expected) { + applyIM(im, pred); + checkPred(pred, expected); + } + + private void checkPredicatePartial(TopologyPredicate pred, String im, boolean expected) { + applyIM(im, pred); + boolean isKnown = pred.isKnown(); + assertTrue("predicate value is not known", isKnown); + checkPred(pred, expected); + } + + private void checkPred(TopologyPredicate pred, boolean expected) { + pred.finish(); + boolean actual = pred.value(); + assertEquals(expected, actual); + } + + private static void applyIM(String imIn, TopologyPredicate pred) { + String im = cleanIM(imIn); + for (int i = 0; i < 9; i++) { + int locA = i / 3; + int locB = i - 3 * locA; + char entry = im.charAt(i); + if (entry == '0' || entry == '1' || entry == '2') { + int dim = Dimension.toDimensionValue(entry); + pred.updateDimension(locA, locB, dim); + } + } + } + + private static String cleanIM(String im) { + String im1 = im.replaceAll("\\.", ""); + return im1; + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java index c260c1273b..08829c2dd5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/DouglasPeuckerSimplifierTest.java @@ -13,10 +13,7 @@ package org.locationtech.jts.simplify; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; -import junit.framework.TestCase; import test.jts.GeometryTestCase; @@ -34,125 +31,88 @@ public static void main(String[] args) { junit.textui.TestRunner.run(DouglasPeuckerSimplifierTest.class); } - public void testEmptyPolygon() throws Exception { - String geomStr = "POLYGON(EMPTY)"; - new GeometryOperationValidator( - DPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); + public void testPoint() { + checkDPNoChange("POINT (10 10)", 1); } - - public void testPoint() throws Exception { - String geomStr = "POINT (10 10)"; - new GeometryOperationValidator( - DPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); + + public void testPolygonEmpty() { + checkDPNoChange("POLYGON(EMPTY)", 1); } - - public void testPolygonNoReduction() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", - 10.0)) - .test(); - } - public void testPolygonReductionWithSplit() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", - 10.0)) - .test(); - } - public void testPolygonReduction() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((120 120, 121 121, 122 122, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0)) - .test(); - } - public void testPolygonWithTouchingHole() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0)) - .setExpectedResult("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))") - .test(); - } - public void testFlattishPolygon() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", - 10.0)) - .setExpectedResult("POLYGON EMPTY") - .test(); - } - public void testTinySquare() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", - 10.0)) - .test(); - } - public void testTinyHole() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))", - 30.0)) - .testEmpty(false); - } - public void testTinyLineString() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "LINESTRING (0 5, 1 5, 2 5, 5 5)", - 10.0)) - .test(); - } - public void testMultiPoint() throws Exception { - String geomStr = "MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 10.0)) - .setExpectedResult(geomStr) - .test(); - } - public void testMultiLineString() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); - } - public void testMultiLineStringWithEmpty() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "MULTILINESTRING( EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); - } - public void testMultiPolygonWithEmpty() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "MULTIPOLYGON (EMPTY, ((-36 91.5, 4.5 91.5, 4.5 57.5, -36 57.5, -36 91.5)), ((25.5 57.5, 61.5 57.5, 61.5 23.5, 25.5 23.5, 25.5 57.5)))", - 10.0)) - .test(); - } - public void testGeometryCollection() throws Exception { - new GeometryOperationValidator( - DPSimplifierResult.getResult( - "GEOMETRYCOLLECTION (" - + "MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)," - + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200))," - + "LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)" - + ")" - ,10.0)) - .test(); + public void testPolygonWithFlatVertices() { + checkDP("POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", + 10.0, + "POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))"); + } + + public void testPolygonReductionWithSplit() { + checkDP("POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", + 1, + "MULTIPOLYGON (((40 240, 160 240, 40 140, 40 240)), ((160 240, 280 240, 280 160, 160 240)))"); + } + + public void testPolygonReduction() { + checkDP("POLYGON ((120 120, 121 121, 122 122, 220 120, 180 199, 160 200, 140 199, 120 120))", + 10, + "POLYGON ((120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"); + } + + public void testPolygonWithTouchingHole() { + checkDP("POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 50 90, 80 80, 80 20))", + 10, + "POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 80 80, 80 20))"); + } + + public void testPolygonFlattish() { + checkDP("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", + 10, + "POLYGON EMPTY"); + } + + public void testPolygonTinySquare() { + checkDP("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", + 10, + "POLYGON EMPTY"); + } + + public void testPolygonTinyHole() { + checkDP("POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))", + 30, + "POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10))"); + } + + public void testLineStringTiny() { + checkDP("LINESTRING (0 5, 1 5, 2 5, 5 5)", + 10, + "LINESTRING (0 5, 5 5)"); + } + + public void testMultiPoint() { + checkDPNoChange("MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)", + 10); + } + + public void testMultiLineString() { + checkDP("MULTILINESTRING((0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))"); + } + + public void testMultiLineStringWithEmpty() { + checkDP("MULTILINESTRING( EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))"); + } + + public void testMultiPolygonWithEmpty() { + checkDP("MULTIPOLYGON (EMPTY, ((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))", + 10, + "MULTIPOLYGON (((10 90, 10 10, 90 10, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))"); + } + + public void testGeometryCollection() { + checkDPNoChange("GEOMETRYCOLLECTION (MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120), POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))", + 10.0); } /** @@ -183,25 +143,31 @@ public void testPolygonCollapseRemoved() { ); } + // see https://trac.osgeo.org/geos/ticket/1064 + public void testPolygonRemoveFlatEndpoint() { + checkDP( + "POLYGON ((42 42, 0 42, 0 100, 42 100, 100 42, 42 42))", + 1, + "POLYGON ((100 42, 0 42, 0 100, 42 100, 100 42))" + ); + } + + public void testPolygonEndpointCollapse() { + checkDP( + "POLYGON ((5 2, 9 1, 1 1, 5 2))", + 1, + "POLYGON EMPTY" + ); + } + private void checkDP(String wkt, double tolerance, String wktExpected) { Geometry geom = read(wkt); Geometry result = DouglasPeuckerSimplifier.simplify(geom, tolerance); Geometry expected = read(wktExpected); checkEqual(expected, result); } -} - -class DPSimplifierResult -{ - private static WKTReader rdr = new WKTReader(); - - public static Geometry[] getResult(String wkt, double tolerance) - throws ParseException - { - Geometry[] ioGeom = new Geometry[2]; - ioGeom[0] = rdr.read(wkt); - ioGeom[1] = DouglasPeuckerSimplifier.simplify(ioGeom[0], tolerance); - //System.out.println(ioGeom[1]); - return ioGeom; + + private void checkDPNoChange(String wkt, double tolerance) { + checkDP(wkt, tolerance, wkt); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java index c73f6b6649..f162c0841f 100644 --- a/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/simplify/TopologyPreservingSimplifierTest.java @@ -13,8 +13,6 @@ package org.locationtech.jts.simplify; import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; import test.jts.GeometryTestCase; @@ -33,159 +31,91 @@ public static void main(String[] args) { junit.textui.TestRunner.run(TopologyPreservingSimplifierTest.class); } - public void testEmptyPolygon() throws Exception { - String geomStr = "POLYGON(EMPTY)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); + public void testPoint() { + checkTPSNoChange("POINT (10 10)", 1); } - - public void testPoint() throws Exception { - String geomStr = "POINT (10 10)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 1)) - .setExpectedResult(geomStr) - .test(); - } - - - /** - * Test is from http://postgis.refractions.net/pipermail/postgis-users/2008-April/019327.html - * Exhibits the issue where simplified polygon shells can "jump" across - * holes, causing invalid topology. - * - * @throws Exception - */ - public void testMultiPolygonWithSmallComponents() throws Exception { - String geomStr = "MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.7300964 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13.7214718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.0439558,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.7408331 51.0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.7473063 51.0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563383 51.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,13.7492723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.737321 51.0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,13.7390933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))"; - boolean isPassed = new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 0.0057)) - .isAllTestsPassed(); - assertTrue(! isPassed); + + public void testPolygonEmpty() { + checkTPSNoChange("POLYGON(EMPTY)", 1); } - /** - * Test is from http://lists.jump-project.org/pipermail/jts-devel/2008-February/002350.html - * @throws Exception - */ - public void testPolygonWithSpike() throws Exception { - String geomStr = "POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312475.4 6646878.027, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312468.253 6646874.01, 3312463.52 6646875.779, 3312459.605 6646878.353))"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 2.0)) - .test(); + + public void testPolygonFlatVertices() throws Exception { + checkTPS("POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", + 10, + "POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))"); } + public void testPolygonNoReduction() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", - 10.0)) - .test(); + checkTPSNoChange("POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))", + 10); } + public void testPolygonNoReductionWithConflicts() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", - 10.0)) - .test(); + checkTPSNoChange("POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", + 10); } + public void testPolygonWithTouchingHole() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", - 10.0)) - .setExpectedResult("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))") - .test(); + checkTPS("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))", + 10, + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))"); } + public void testFlattishPolygon() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", - 10.0)) - .test(); + checkTPS("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))", + 10, + "POLYGON ((0 0, 50 0, 100 0, 70 1, 0 0))"); } + public void testPolygonWithFlattishHole() throws Exception { - String geomStr = "POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 20.0)) - .setExpectedResult(geomStr) - .test(); + checkTPS("POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))", + 20, + "POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))"); } + public void testTinySquare() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", - 10.0)) - .test(); + checkTPS("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", + 10, + "POLYGON ((0 0, 5 5, 5 0, 0 0))"); } + public void testTinyLineString() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "LINESTRING (0 5, 1 5, 2 5, 5 5)", - 10.0)) - .test(); + checkTPS("LINESTRING (0 5, 1 5, 2 5, 5 5)", + 10, + "LINESTRING (0 5, 5 5)"); } public void testTinyClosedLineString() throws Exception { - String geomStr = "LINESTRING (0 0, 5 0, 5 5, 0 0)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 10)) - .setExpectedResult(geomStr) - .test(); + checkTPSNoChange("LINESTRING (0 0, 5 0, 5 5, 0 0)", + 10); } public void testMultiPoint() throws Exception { - String geomStr = "MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)"; - new GeometryOperationValidator( - TPSimplifierResult.getResult( - geomStr, - 10.0)) - .setExpectedResult(geomStr) - .test(); + checkTPSNoChange("MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)", + 10); } public void testMultiLineString() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); + checkTPS("MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 50 1, 100 0))"); } + public void testMultiLineStringWithEmpty() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "MULTILINESTRING(EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", - 10.0)) - .test(); + checkTPS("MULTILINESTRING(EMPTY, (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )", + 10, + "MULTILINESTRING ((0 0, 100 0), (0 0, 50 1, 100 0))"); } + public void testMultiPolygonWithEmpty() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "MULTIPOLYGON (EMPTY, ((-36 91.5, 4.5 91.5, 4.5 57.5, -36 57.5, -36 91.5)), ((25.5 57.5, 61.5 57.5, 61.5 23.5, 25.5 23.5, 25.5 57.5)))", - 10.0)) - .test(); - } - public void testGeometryCollection() throws Exception { - new GeometryOperationValidator( - TPSimplifierResult.getResult( - "GEOMETRYCOLLECTION (" - + "MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)," - + "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200))," - + "LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)" - + ")" - ,10.0)) - .test(); + checkTPS("MULTIPOLYGON (EMPTY, ((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))", + 10, + "MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90)))"); + } + + public void testGeometryCollection() { + checkTPSNoChange("GEOMETRYCOLLECTION (MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120), POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))", + 10); } public void testNoCollapse_mL() throws Exception { @@ -204,25 +134,128 @@ public void testNoCollapseMany_mL() throws Exception { ); } + public void testNoCollapseSmallSquare() throws Exception { + checkTPS( + "POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))", + 100, + "POLYGON ((0 0, 5 5, 5 0, 0 0))" + ); + } + + public void testPolygonRemoveEndpoint() throws Exception { + checkTPS( + "POLYGON ((220 180, 261 175, 380 220, 300 40, 140 30, 30 220, 176 176, 220 180))", + 40, + "POLYGON ((30 220, 380 220, 300 40, 140 30, 30 220))" + ); + } + + public void testLinearRingRemoveEndpoint() throws Exception { + checkTPS( + "LINEARRING (220 180, 261 175, 380 220, 300 40, 140 30, 30 220, 176 176, 220 180)", + 40, + "LINEARRING (30 220, 380 220, 300 40, 140 30, 30 220)" + ); + } + + public void testPolygonKeepFlatEndpointWithTouch() throws Exception { + checkTPSNoChange("POLYGON ((0 0, 5 2.05, 10 0, 10 10, 0 10, 0 0), (5 2.1, 6 2, 6 4, 4 4, 4 2, 5 2.1))", + 0.1 ); + } + + public void testPolygonKeepEndpointWithCross() throws Exception { + checkTPS( + "POLYGON ((50 52, 60 50, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60, 40 50, 50 52))", + 10, + "POLYGON ((20 60, 50 52, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60))" + ); + } + + // see https://trac.osgeo.org/geos/ticket/1064 + public void testPolygonRemoveFlatEndpoint() throws Exception { + checkTPS( + "POLYGON ((42 42, 0 42, 0 100, 42 100, 100 42, 42 42))", + 1, + "POLYGON ((100 42, 0 42, 0 100, 42 100, 100 42))" + ); + } + + public void testPolygonManyFlatSegments() throws Exception { + checkTPS( + "POLYGON ((5 5, 7 5, 9 5, 9 1, 1 1, 1 5, 3 5, 5 5))", + 1, + "POLYGON ((9 5, 9 1, 1 1, 1 5, 9 5))" + ); + } + + //-- vertex is not removed due to overly-restrictive heuristic result length calculation? + public void testPolygonSize5NotSimplfied() throws Exception { + checkTPS( + "POLYGON ((10 90, 10 10, 90 10, 47 57, 10 90))", + 10, + "POLYGON ((10 90, 10 10, 90 10, 47 57, 10 90))" + ); + } + + /** + * Test is from http://postgis.refractions.net/pipermail/postgis-users/2008-April/019327.html + * Exhibits the issue where simplified polygon shells can "jump" across + * holes, causing invalid topology. + * + * @throws Exception + */ + public void testMultiPolygonWithSmallComponents() throws Exception { + checkTPS("MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.7300964 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13.7214718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.0439558,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.7408331 51.0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.7473063 51.0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563383 51.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,13.7492723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.737321 51.0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,13.7390933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))", + 0.0057, + "MULTIPOLYGON (((13.73095 51.024734, 13.7123153 51.041449, 13.7412027 51.0463256, 13.7552745 51.0566237, 13.7484397 51.0324582, 13.73095 51.024734)), ((13.7390933 51.0471421, 13.7369099 51.0474154, 13.7390933 51.047209, 13.7390933 51.0471421)), ((13.7367293 51.0470057, 13.7346615 51.0466892, 13.7347106 51.0471899, 13.7367293 51.0470057)))"); + } + + /** + * Test is from http://lists.jump-project.org/pipermail/jts-devel/2008-February/002350.html + * @throws Exception + */ + public void testPolygonWithSpike() throws Exception { + checkTPS("POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312475.4 6646878.027, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312468.253 6646874.01, 3312463.52 6646875.779, 3312459.605 6646878.353))", + 2, + "POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312459.605 6646878.353))"); + } + + public void testLineComponentCross() { + checkTPS("MULTILINESTRING ((0 0, 10 2, 20 0), (9 1, 11 1))", + 4, + "MULTILINESTRING ((0 0, 10 2, 20 0), (9 1, 11 1))"); + } + + public void testPolygonComponentCrossAtEndpoint() { + checkTPS("MULTIPOLYGON (((50 40, 40 60, 80 40, 0 0, 30 70, 50 40)), ((40 56, 40 57, 41 56, 40 56)))", + 30, + "MULTIPOLYGON (((50 40, 80 40, 0 0, 30 70, 50 40)), ((40 56, 40 57, 41 56, 40 56)))"); + } + + public void testPolygonIntersectingSegments() { + checkTPS("MULTIPOLYGON (((0.63 0.2, 0.35 0, 0.73 0.66, 0.63 0.2)), ((1.42 4.01, 3.45 0.7, 1.79 1.47, 0 0.57, 1.42 4.01)))", + 10, + "MULTIPOLYGON (((0.63 0.2, 0.35 0, 0.73 0.66, 0.63 0.2)), ((1.42 4.01, 3.45 0.7, 1.79 1.47, 0 0.57, 1.42 4.01)))"); + } + + //-- test added in https://github.com/libgeos/geos/pull/1110 + public void testRingEndpointRemoval() { + checkTPS("POLYGON ((-222601.33094265286 6299915.50260568, -222599.13611514607 6299917.747821213, -222599.09754554977 6299925.149899498, -222599.07870256738 6299925.234615005, -222595.52372420163 6299932.934861557, -222510 6300300, -221720.85158014414 6300132.680680807, -222448.77936063593 6299647.669870703, -222618.41756525903 6299886.966175825, -222618.40178141624 6299887.020684309, -222616.09648739762 6299892.144482262, -222601.33094265286 6299915.50260568), (-222456.68914400978 6299947.489843342, -222455.07603815367 6299939.772017001, -222455.07542453965 6299939.691595431, -222456.57057590774 6299931.950053182, -222462.75368034307 6299916.87367865, -222465.9575074078 6299911.582542387, -222465.99782740293 6299911.534645676, -222483.5296791599 6299864.079888968, -222484.09789251382 6299852.105440594, -222485.11401620077 6299846.692173677, -222485.13170293145 6299846.639453617, -222487.58585740798 6299841.7086228, -222490.56062790897 6299837.359974515, -222415.1394852571 6299926.6972160805, -222421.19226152284 6299963.389132584, -222467.0202338936 6299970.185860572, -222465.71145777934 6299968.200784476, -222465.69955208438 6299968.180154371, -222464.63560265294 6299966.053788407, -222456.68914400978 6299947.489843342))", + 20, + "POLYGON ((-222618.41756525903 6299886.966175825, -222510 6300300, -221720.85158014414 6300132.680680807, -222448.77936063593 6299647.669870703, -222618.41756525903 6299886.966175825), (-222467.0202338936 6299970.185860572, -222456.57057590774 6299931.950053182, -222490.56062790897 6299837.359974515, -222415.1394852571 6299926.6972160805, -222421.19226152284 6299963.389132584, -222467.0202338936 6299970.185860572))"); + } + private void checkTPS(String wkt, double tolerance, String wktExpected) { Geometry geom = read(wkt); Geometry actual = TopologyPreservingSimplifier.simplify(geom, tolerance); Geometry expected = read(wktExpected); + //TODO: add this once the "skipping over rings" problem is fixed + //checkValid(actual); checkEqual(expected, actual); } -} - -class TPSimplifierResult -{ - private static WKTReader rdr = new WKTReader(); - - public static Geometry[] getResult(String wkt, double tolerance) - throws ParseException - { - Geometry[] ioGeom = new Geometry[2]; - ioGeom[0] = rdr.read(wkt); - ioGeom[1] = TopologyPreservingSimplifier.simplify(ioGeom[0], tolerance); - //System.out.println(ioGeom[1]); - return ioGeom; + + private void checkTPSNoChange(String wkt, double tolerance) { + checkTPS(wkt, tolerance, wkt); } } + diff --git a/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java b/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java index 90dff39cc0..be0b90ead6 100644 --- a/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/triangulate/DelaunayTest.java @@ -11,95 +11,141 @@ */ package org.locationtech.jts.triangulate; +import org.locationtech.jts.algorithm.ConvexHull; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.io.ParseException; -import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.operation.overlayng.CoverageUnion; -import junit.framework.TestCase; import junit.textui.TestRunner; +import test.jts.GeometryTestCase; /** * Tests Delaunay Triangulation classes * */ -public class DelaunayTest extends TestCase { +public class DelaunayTest extends GeometryTestCase { public static void main(String args[]) { TestRunner.run(DelaunayTest.class); } private GeometryFactory geomFact = new GeometryFactory(); - private WKTReader reader = new WKTReader(); public DelaunayTest(String name) { super(name); } public void testTriangle() - throws ParseException { String wkt = "MULTIPOINT ((10 10 1), (10 20 2), (20 20 3))"; String expected = "MULTILINESTRING ((10 20, 20 20), (10 10, 10 20), (10 10, 20 20))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((10 20, 10 10, 20 20, 10 20)))"; - runDelaunay(wkt, true, expectedTri); + checkDelaunay(wkt, true, expectedTri); } public void testRandom() - throws ParseException { String wkt = "MULTIPOINT ((50 40), (140 70), (80 100), (130 140), (30 150), (70 180), (190 110), (120 20))"; String expected = "MULTILINESTRING ((70 180, 190 110), (30 150, 70 180), (30 150, 50 40), (50 40, 120 20), (190 110, 120 20), (120 20, 140 70), (190 110, 140 70), (130 140, 140 70), (130 140, 190 110), (70 180, 130 140), (80 100, 130 140), (70 180, 80 100), (30 150, 80 100), (50 40, 80 100), (80 100, 120 20), (80 100, 140 70))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((30 150, 50 40, 80 100, 30 150)), POLYGON ((30 150, 80 100, 70 180, 30 150)), POLYGON ((70 180, 80 100, 130 140, 70 180)), POLYGON ((70 180, 130 140, 190 110, 70 180)), POLYGON ((190 110, 130 140, 140 70, 190 110)), POLYGON ((190 110, 140 70, 120 20, 190 110)), POLYGON ((120 20, 140 70, 80 100, 120 20)), POLYGON ((120 20, 80 100, 50 40, 120 20)), POLYGON ((80 100, 140 70, 130 140, 80 100)))"; - runDelaunay(wkt, true, expectedTri); + checkDelaunay(wkt, true, expectedTri); } public void testGrid() - throws ParseException { String wkt = "MULTIPOINT ((10 10), (10 20), (20 20), (20 10), (20 0), (10 0), (0 0), (0 10), (0 20))"; String expected = "MULTILINESTRING ((10 20, 20 20), (0 20, 10 20), (0 10, 0 20), (0 0, 0 10), (0 0, 10 0), (10 0, 20 0), (20 0, 20 10), (20 10, 20 20), (10 20, 20 10), (10 10, 20 10), (10 10, 10 20), (10 10, 0 20), (10 10, 0 10), (10 0, 10 10), (0 10, 10 0), (10 10, 20 0))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); String expectedTri = "GEOMETRYCOLLECTION (POLYGON ((0 20, 0 10, 10 10, 0 20)), POLYGON ((0 20, 10 10, 10 20, 0 20)), POLYGON ((10 20, 10 10, 20 10, 10 20)), POLYGON ((10 20, 20 10, 20 20, 10 20)), POLYGON ((10 0, 20 0, 10 10, 10 0)), POLYGON ((10 0, 10 10, 0 10, 10 0)), POLYGON ((10 0, 0 10, 0 0, 10 0)), POLYGON ((10 10, 20 0, 20 10, 10 10)))"; - runDelaunay(wkt, true, expectedTri); + checkDelaunay(wkt, true, expectedTri); } public void testCircle() - throws ParseException { String wkt = "POLYGON ((42 30, 41.96 29.61, 41.85 29.23, 41.66 28.89, 41.41 28.59, 41.11 28.34, 40.77 28.15, 40.39 28.04, 40 28, 39.61 28.04, 39.23 28.15, 38.89 28.34, 38.59 28.59, 38.34 28.89, 38.15 29.23, 38.04 29.61, 38 30, 38.04 30.39, 38.15 30.77, 38.34 31.11, 38.59 31.41, 38.89 31.66, 39.23 31.85, 39.61 31.96, 40 32, 40.39 31.96, 40.77 31.85, 41.11 31.66, 41.41 31.41, 41.66 31.11, 41.85 30.77, 41.96 30.39, 42 30))"; String expected = "MULTILINESTRING ((41.66 31.11, 41.85 30.77), (41.41 31.41, 41.66 31.11), (41.11 31.66, 41.41 31.41), (40.77 31.85, 41.11 31.66), (40.39 31.96, 40.77 31.85), (40 32, 40.39 31.96), (39.61 31.96, 40 32), (39.23 31.85, 39.61 31.96), (38.89 31.66, 39.23 31.85), (38.59 31.41, 38.89 31.66), (38.34 31.11, 38.59 31.41), (38.15 30.77, 38.34 31.11), (38.04 30.39, 38.15 30.77), (38 30, 38.04 30.39), (38 30, 38.04 29.61), (38.04 29.61, 38.15 29.23), (38.15 29.23, 38.34 28.89), (38.34 28.89, 38.59 28.59), (38.59 28.59, 38.89 28.34), (38.89 28.34, 39.23 28.15), (39.23 28.15, 39.61 28.04), (39.61 28.04, 40 28), (40 28, 40.39 28.04), (40.39 28.04, 40.77 28.15), (40.77 28.15, 41.11 28.34), (41.11 28.34, 41.41 28.59), (41.41 28.59, 41.66 28.89), (41.66 28.89, 41.85 29.23), (41.85 29.23, 41.96 29.61), (41.96 29.61, 42 30), (41.96 30.39, 42 30), (41.85 30.77, 41.96 30.39), (41.66 31.11, 41.96 30.39), (41.41 31.41, 41.96 30.39), (41.41 28.59, 41.96 30.39), (41.41 28.59, 41.41 31.41), (38.59 28.59, 41.41 28.59), (38.59 28.59, 41.41 31.41), (38.59 28.59, 38.59 31.41), (38.59 31.41, 41.41 31.41), (38.59 31.41, 39.61 31.96), (39.61 31.96, 41.41 31.41), (39.61 31.96, 40.39 31.96), (40.39 31.96, 41.41 31.41), (40.39 31.96, 41.11 31.66), (38.04 30.39, 38.59 28.59), (38.04 30.39, 38.59 31.41), (38.04 30.39, 38.34 31.11), (38.04 29.61, 38.59 28.59), (38.04 29.61, 38.04 30.39), (39.61 28.04, 41.41 28.59), (38.59 28.59, 39.61 28.04), (38.89 28.34, 39.61 28.04), (40.39 28.04, 41.41 28.59), (39.61 28.04, 40.39 28.04), (41.96 29.61, 41.96 30.39), (41.41 28.59, 41.96 29.61), (41.66 28.89, 41.96 29.61), (40.39 28.04, 41.11 28.34), (38.04 29.61, 38.34 28.89), (38.89 31.66, 39.61 31.96))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); } public void testPolygonWithChevronHoles() - throws ParseException { String wkt = "POLYGON ((0 0, 0 200, 180 200, 180 0, 0 0), (20 180, 160 180, 160 20, 152.625 146.75, 20 180), (30 160, 150 30, 70 90, 30 160))"; String expected = "MULTILINESTRING ((0 200, 180 200), (0 0, 0 200), (0 0, 180 0), (180 200, 180 0), (152.625 146.75, 180 0), (152.625 146.75, 180 200), (152.625 146.75, 160 180), (160 180, 180 200), (0 200, 160 180), (20 180, 160 180), (0 200, 20 180), (20 180, 30 160), (30 160, 0 200), (0 0, 30 160), (30 160, 70 90), (0 0, 70 90), (70 90, 150 30), (150 30, 0 0), (150 30, 160 20), (0 0, 160 20), (160 20, 180 0), (152.625 146.75, 160 20), (150 30, 152.625 146.75), (70 90, 152.625 146.75), (30 160, 152.625 146.75), (30 160, 160 180))"; - runDelaunayEdges(wkt, expected); + checkDelaunayEdges(wkt, expected); } + // see https://github.com/libgeos/geos/issues/719 public void testFrameTooSmallBug() - throws ParseException { String wkt = "MULTIPOINT ((0 194), (66 151), (203 80), (273 43), (340 0))"; String expected = "GEOMETRYCOLLECTION (POLYGON ((0 194, 66 151, 203 80, 0 194)), POLYGON ((0 194, 203 80, 273 43, 0 194)), POLYGON ((273 43, 203 80, 340 0, 273 43)), POLYGON ((340 0, 203 80, 66 151, 340 0)))"; - runDelaunay(wkt, true, expected); + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/libgeos/geos/issues/719 + public void testNarrow_GEOS_719() + { + String wkt = "MULTIPOINT ((1139294.6389832513 8201313.534695469), (1139360.8549531854 8201271.189805277), (1139497.5995843115 8201199.995542546), (1139567.7837303514 8201163.348533507), (1139635.3942210067 8201119.902527407))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((1139294.6389832513 8201313.534695469, 1139360.8549531854 8201271.189805277, 1139497.5995843115 8201199.995542546, 1139294.6389832513 8201313.534695469)), POLYGON ((1139294.6389832513 8201313.534695469, 1139497.5995843115 8201199.995542546, 1139567.7837303514 8201163.348533507, 1139294.6389832513 8201313.534695469)), POLYGON ((1139567.7837303514 8201163.348533507, 1139497.5995843115 8201199.995542546, 1139635.3942210067 8201119.902527407, 1139567.7837303514 8201163.348533507)), POLYGON ((1139635.3942210067 8201119.902527407, 1139497.5995843115 8201199.995542546, 1139360.8549531854 8201271.189805277, 1139635.3942210067 8201119.902527407)))"; + checkDelaunay(wkt, true, expected); + } + + + public void testNarrowTriangle() + { + String wkt = "MULTIPOINT ((100 200), (200 190), (300 200))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((100 200, 300 200, 200 190, 100 200)))"; + checkDelaunay(wkt, true, expected); + } + + // seee https://github.com/locationtech/jts/issues/477 + public void testNarrow_GH477_1() + { + String wkt = "MULTIPOINT ((0 0), (1 0), (-1 0.05), (0 0))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((-1 0.05, 1 0, 0 0, -1 0.05)))"; + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/locationtech/jts/issues/477 + public void testNarrow_GH477_2() + { + String wkt = "MULTIPOINT ((0 0), (0 486), (1 486), (1 22), (2 22), (2 0))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((0 0, 0 486, 1 22, 0 0)), POLYGON ((0 0, 1 22, 2 0, 0 0)), POLYGON ((0 486, 1 486, 1 22, 0 486)), POLYGON ((1 22, 1 486, 2 22, 1 22)), POLYGON ((1 22, 2 22, 2 0, 1 22)))"; + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/libgeos/geos/issues/946 + public void testNarrow_GEOS_946() + { + String wkt = "MULTIPOINT ((113.56577197798602 22.80081530883069),(113.565723279387 22.800815316487014),(113.56571548761124 22.80081531771092),(113.56571548780202 22.800815317674463),(113.56577197817877 22.8008153088047),(113.56577197798602 22.80081530883069))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((113.56571548761124 22.80081531771092, 113.565723279387 22.800815316487014, 113.56571548780202 22.800815317674463, 113.56571548761124 22.80081531771092)), POLYGON ((113.56571548780202 22.800815317674463, 113.565723279387 22.800815316487014, 113.56577197817877 22.8008153088047, 113.56571548780202 22.800815317674463)), POLYGON ((113.565723279387 22.800815316487014, 113.56577197798602 22.80081530883069, 113.56577197817877 22.8008153088047, 113.565723279387 22.800815316487014)))"; + checkDelaunay(wkt, true, expected); + } + + // see https://github.com/shapely/shapely/issues/1873 + public void testNarrow_Shapely_1873() + { + String wkt = "MULTIPOINT ((584245.72096874 7549593.72686167), (584251.71398371 7549594.01629478), (584242.72446125 7549593.58214511), (584230.73978847 7549592.9760418), (584233.73581213 7549593.13045099), (584236.7318358 7549593.28486019), (584239.72795377 7549593.43742855), (584227.74314188 7549592.83423486))"; + String expected = "GEOMETRYCOLLECTION (POLYGON ((584227.74314188 7549592.83423486, 584233.73581213 7549593.13045099, 584230.73978847 7549592.9760418, 584227.74314188 7549592.83423486)), POLYGON ((584227.74314188 7549592.83423486, 584236.7318358 7549593.28486019, 584233.73581213 7549593.13045099, 584227.74314188 7549592.83423486)), POLYGON ((584227.74314188 7549592.83423486, 584239.72795377 7549593.43742855, 584236.7318358 7549593.28486019, 584227.74314188 7549592.83423486)), POLYGON ((584230.73978847 7549592.9760418, 584233.73581213 7549593.13045099, 584245.72096874 7549593.72686167, 584230.73978847 7549592.9760418)), POLYGON ((584230.73978847 7549592.9760418, 584245.72096874 7549593.72686167, 584251.71398371 7549594.01629478, 584230.73978847 7549592.9760418)), POLYGON ((584233.73581213 7549593.13045099, 584236.7318358 7549593.28486019, 584242.72446125 7549593.58214511, 584233.73581213 7549593.13045099)), POLYGON ((584233.73581213 7549593.13045099, 584242.72446125 7549593.58214511, 584245.72096874 7549593.72686167, 584233.73581213 7549593.13045099)), POLYGON ((584236.7318358 7549593.28486019, 584239.72795377 7549593.43742855, 584242.72446125 7549593.58214511, 584236.7318358 7549593.28486019)))"; + checkDelaunay(wkt, true, expected); + } + + public void testNarrowPoints() + { + String wkt = "MULTIPOINT ((2 204), (3 66), (1 96), (0 236), (3 173), (2 114), (3 201), (0 46), (1 181))"; + checkDelaunayHull(wkt); } static final double COMPARISON_TOLERANCE = 1.0e-7; - void runDelaunayEdges(String sitesWKT, String expectedWKT) - throws ParseException + void checkDelaunayEdges(String sitesWKT, String expectedWKT) { - runDelaunay(sitesWKT, false, expectedWKT); + checkDelaunay(sitesWKT, false, expectedWKT); } - void runDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) - throws ParseException + void checkDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) { - Geometry sites = reader.read(sitesWKT); + Geometry sites = read(sitesWKT); DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); builder.setSites(sites); @@ -112,9 +158,29 @@ void runDelaunay(String sitesWKT, boolean computeTriangles, String expectedWKT) } //System.out.println(result); - Geometry expected = reader.read(expectedWKT); + Geometry expected = read(expectedWKT); result.normalize(); expected.normalize(); - assertTrue(expected.equalsExact(result, COMPARISON_TOLERANCE)); + checkEqual(expected, result, COMPARISON_TOLERANCE); + } + + void checkDelaunayHull(String sitesWKT) + { + Geometry sites = read(sitesWKT); + DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); + builder.setSites(sites); + + Geometry result = builder.getTriangles(geomFact); + + //System.out.println(result); + + Geometry union = CoverageUnion.union(result); + ConvexHull ch = new ConvexHull(result); + Geometry convexHull = ch.getConvexHull(); + + //boolean isEqual = union.norm().equalsExact(convexHull.norm()); + boolean isEqual = union.equalsTopo(convexHull); + + assertTrue("hulls do not match", isEqual); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java b/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java index 6b978030f1..08bd2f0da7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/triangulate/VoronoiTest.java @@ -52,6 +52,16 @@ public void testSitesWithPointsOnSquareGrid() { runVoronoi(wkt); } + /** + * This test fails if the frame is forced to be convex via {@link IncrementalDelaunayTriangulator#forceConvex(boolean)}. + * It is also dependent on the frame size factor - a value of 10 causes failure, + * but larger values may not. + */ + public void testFrameDisableForceConvex() { + String wkt = "MULTIPOINT ((259 289), (46 194), (396 359), (243 349), (206 99), (470 40), (429 185), (54 9), (78 208), (457 406), (355 191), (346 497), (144 79), (35 459), (322 37), (181 371), (359 257), (57 331), (225 139), (475 245), (416 364), (155 477), (123 232), (102 141), (251 434))"; + runVoronoi(wkt); + } + static final double COMPARISON_TOLERANCE = 1.0e-7; private void runVoronoi(String sitesWKT) { @@ -74,6 +84,6 @@ void runVoronoi(String sitesWKT, boolean computePolys, String expectedWKT) Geometry expected = read(expectedWKT); result.normalize(); expected.normalize(); - assertTrue(expected.equalsExact(result, COMPARISON_TOLERANCE)); + checkEqual(expected, result, COMPARISON_TOLERANCE); } } diff --git a/modules/core/src/test/java/test/jts/GeometryTestCase.java b/modules/core/src/test/java/test/jts/GeometryTestCase.java index b6faa30fe0..de8201e17a 100644 --- a/modules/core/src/test/java/test/jts/GeometryTestCase.java +++ b/modules/core/src/test/java/test/jts/GeometryTestCase.java @@ -60,6 +60,10 @@ protected GeometryFactory getGeometryFactory() { return geomFactory; } + protected void checkValid(Geometry geom) { + assertTrue(geom.isValid()); + } + /** * Checks that the normalized values of the expected and actual * geometries are exactly equal. diff --git a/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java b/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java new file mode 100644 index 0000000000..989208119f --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/index/FlatbushPerfTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2019 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 test.jts.perf.index; + +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.index.SpatialIndex; +import org.locationtech.jts.index.hprtree.HPRtree; +import org.locationtech.jts.index.strtree.STRtree; +import org.locationtech.jts.util.Stopwatch; +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Reproduce the performance benchmark scenario that + * Flatbush + * uses, and run against spatial indexes. + */ +public class FlatbushPerfTest extends PerformanceTestCase { + private static final int NUM_ITEMS = 1_000_000; + private static final int NUM_QUERIES = 1_000; + private Envelope[] items; + private Envelope[] queries; + private HPRtree hprtree; + private STRtree strtree; + + public static void main(String[] args) { + PerformanceTestRunner.run(FlatbushPerfTest.class); + } + + public FlatbushPerfTest(String name) { + super(name); + setRunSize(new int[] { 1, 10, (int) (100 * Math.sqrt(0.1))}); + setRunIterations(1); + } + + private static Envelope randomBox(Random random, double boxSize) { + double x = random.nextDouble() * (100d - boxSize); + double y = random.nextDouble() * (100d - boxSize); + double x2 = x + random.nextDouble() * boxSize; + double y2 = y + random.nextDouble() * boxSize; + return new Envelope(x, x2, y, y2); + } + + public void setUp() + { + Random random = new Random(0); + items = new Envelope[NUM_ITEMS]; + + for (int i = 0; i < NUM_ITEMS; i++) { + items[i] = randomBox(random, 1); + } + + // warmup the jvm by building once and running queries + warmupQueries(createIndex(HPRtree::new, HPRtree::build)); + warmupQueries(createIndex(STRtree::new, STRtree::build)); + + Stopwatch sw = new Stopwatch(); + hprtree = createIndex(HPRtree::new, HPRtree::build); + System.out.println("HPRTree Build time = " + sw.getTimeString()); + + sw = new Stopwatch(); + strtree = createIndex(STRtree::new, STRtree::build); + System.out.println("STRTree Build time = " + sw.getTimeString()); + } + + private T createIndex(Supplier supplier, Consumer builder) { + T index = supplier.get(); + for (Envelope env : items) { + index.insert(env, env); + } + builder.accept(index); + return index; + } + + private void warmupQueries(SpatialIndex index) { + Random random = new Random(0); + CountItemVisitor visitor = new CountItemVisitor(); + for (int i = 0; i < NUM_QUERIES; i++) { + index.query(randomBox(random, 1), visitor); + } + } + + public void startRun(int size) + { + System.out.println("----- Query size: " + size); + Random random = new Random(0); + queries = new Envelope[NUM_QUERIES]; + for (int i = 0; i < NUM_QUERIES; i++) { + queries[i] = randomBox(random, size); + } + } + + public void runQueriesHPR() { + CountItemVisitor visitor = new CountItemVisitor(); + for (Envelope box : queries) { + hprtree.query(box, visitor); + } + System.out.println("HPRTree query result items = " + visitor.count); + } + + public void runQueriesSTR() { + CountItemVisitor visitor = new CountItemVisitor(); + for (Envelope box : queries) { + strtree.query(box, visitor); + } + System.out.println("STRTree query result items = " + visitor.count); + } +} diff --git a/modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java new file mode 100644 index 0000000000..7472502dfd --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/distance/PointPointDistancePerfTest.java @@ -0,0 +1,61 @@ +package test.jts.perf.operation.distance; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class PointPointDistancePerfTest extends PerformanceTestCase { + + + public static void main(String args[]) { + PerformanceTestRunner.run(PointPointDistancePerfTest.class); + } + + private Point[] grid; + + public PointPointDistancePerfTest(String name) { + super(name); + setRunSize(new int[] {10000}); + setRunIterations(1); + } + + public void startRun(int npts) + { + System.out.println("\n------- Running with # pts = " + npts); + grid = createPointGrid(new Envelope(0, 10., 0, 10), npts); + } + + private Point[] createPointGrid(Envelope envelope, int npts) { + List geoms = new ArrayList(); + GeometryFactory fact = new GeometryFactory(); + int nSide = (int) Math.sqrt(npts); + double xInc = envelope.getWidth() / nSide; + double yInc = envelope.getHeight() / nSide; + for (int i = 0; i < nSide; i++) { + for (int j = 0; j < nSide; j++) { + double x = envelope.getMinX() + i * xInc; + double y = envelope.getMinY() + i * yInc; + Point p = fact.createPoint(new Coordinate(x, y)); + geoms.add(p); + } + } + return GeometryFactory.toPointArray(geoms); + } + + public void runPoints() { + for (Geometry p1 : grid) { + for (Geometry p2 : grid) { + double dist = p1.distance(p2); + } + } + } + +} diff --git a/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java new file mode 100644 index 0000000000..5e041af852 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGLinesOverlappingPerfTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2022 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 test.jts.perf.operation.relateng; + +import static org.junit.Assert.assertEquals; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.util.SineStarFactory; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGLinesOverlappingPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGLinesOverlappingPerfTest.class); + } + + private static final int N_ITER = 1; + + static double ORG_X = 100; + static double ORG_Y = ORG_X; + static double SIZE = 2 * ORG_X; + static int N_ARMS = 6; + static double ARM_RATIO = 0.3; + + static int GRID_SIZE = 100; + static double GRID_CELL_SIZE = SIZE / GRID_SIZE; + + static int NUM_CASES = GRID_SIZE * GRID_SIZE; + + private static final int B_SIZE_FACTOR = 20; + private static final GeometryFactory factory = new GeometryFactory(); + + private Geometry geomA; + + private Geometry[] geomB; + + public RelateNGLinesOverlappingPerfTest(String name) { + super(name); + setRunSize(new int[] { 100, 1000, 10000, 100000, + 200000 }); + //setRunSize(new int[] { 200000 }); + setRunIterations(N_ITER); + } + + public void setUp() + { + System.out.println("RelateNG Overlapping Lines perf test"); + System.out.println("SineStar: origin: (" + + ORG_X + ", " + ORG_Y + ") size: " + SIZE + + " # arms: " + N_ARMS + " arm ratio: " + ARM_RATIO); + System.out.println("# Iterations: " + N_ITER); + System.out.println("# B geoms: " + NUM_CASES); + } + + public void startRun(int npts) + { + Geometry sineStar = SineStarFactory.create(new Coordinate(ORG_X, ORG_Y), SIZE, npts, N_ARMS, ARM_RATIO); + geomA = sineStar.getBoundary(); + + int nptsB = npts * B_SIZE_FACTOR / NUM_CASES; + if (nptsB < 10 ) nptsB = 10; + + geomB = createSineStarGrid(NUM_CASES, nptsB); + //geomB = createCircleGrid(NUM_CASES, nptsB); + + System.out.println("\n------- Running with A: line # pts = " + npts + + " B # pts = " + nptsB + " x " + NUM_CASES + " lines"); + + /* + if (npts == 999) { + System.out.println(geomA); + + for (Geometry g : geomB) { + System.out.println(g); + } + } +*/ + } + + public void runIntersectsOld() + { + for (Geometry b : geomB) { + geomA.intersects(b); + } + } + + public void runIntersectsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.intersects(b); + } + } + + public void runIntersectsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.intersects()); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + + private Geometry[] createSineStarGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Geometry geom = SineStarFactory.create(new Coordinate(x, y), GRID_CELL_SIZE, npts, N_ARMS, ARM_RATIO); + geoms[index++] = geom.getBoundary(); + } + } + return geoms; + } + + private Geometry[] createCircleGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Coordinate p = new Coordinate(x, y); + Geometry geom = factory.createPoint(p).buffer(GRID_CELL_SIZE / 2.0); + geoms[index++] = geom; + } + } + return geoms; + } + + +} 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 new file mode 100644 index 0000000000..8e924206db --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonPointsPerfTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2022 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 test.jts.perf.operation.relateng; + +import static org.junit.Assert.assertEquals; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.util.SineStarFactory; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGPolygonPointsPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGPolygonPointsPerfTest.class); + } + + private static final int N_ITER = 1; + + static double ORG_X = 100; + static double ORG_Y = ORG_X; + static double SIZE = 2 * ORG_X; + static int N_ARMS = 6; + static double ARM_RATIO = 0.3; + + static int GRID_SIZE = 100; + + private static GeometryFactory geomFact = new GeometryFactory(); + + private Geometry geomA; + private Geometry[] geomB; + + public RelateNGPolygonPointsPerfTest(String name) { + super(name); + setRunSize(new int[] { 100, 1000, 10000, 100000 }); + setRunIterations(N_ITER); + } + + public void setUp() + { + System.out.println("RelateNG perf test"); + System.out.println("SineStar: origin: (" + + ORG_X + ", " + ORG_Y + ") size: " + SIZE + + " # arms: " + N_ARMS + " arm ratio: " + ARM_RATIO); + System.out.println("# Iterations: " + N_ITER); + } + + public void startRun(int npts) + { + Geometry sineStar = SineStarFactory.create(new Coordinate(ORG_X, ORG_Y), SIZE, npts, N_ARMS, ARM_RATIO); + geomA = sineStar; + + geomB = createTestPoints(geomA.getEnvelopeInternal(), GRID_SIZE); + + System.out.println("\n------- Running with A: # pts = " + npts + + " B: " + geomB.length + " points"); + + /* + if (npts == 999) { + System.out.println(geomA); + + for (Geometry g : geomB) { + System.out.println(g); + } + } +*/ + } + + public void runIntersectsOld() + { + for (Geometry b : geomB) { + geomA.intersects(b); + } + } + + public void runIntersectsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.intersects(b); + } + } + + public void runIntersectsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.intersects()); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + + public void runContainsOld() + { + for (Geometry b : geomB) { + geomA.contains(b); + } + } + + public void runContainsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.contains(b); + } + } + + public void runContainsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.contains()); + } + } + + public void runContainsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.contains()); + } + } + + public void xrunContainsNGPrepValidate() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + boolean resultNG = rng.evaluate(b, RelatePredicate.contains()); + boolean resultOld = geomA.contains(b); + assertEquals(resultNG, resultOld); + } + } + + private Geometry[] createTestPoints(Envelope env, int nPtsOnSide) { + Geometry[] geoms = new Geometry[ nPtsOnSide * nPtsOnSide ]; + double baseX = env.getMinX(); + double deltaX = env.getWidth() / nPtsOnSide; + double baseY = env.getMinY(); + double deltaY = env.getHeight() / nPtsOnSide; + int index = 0; + for (int i = 0; i < nPtsOnSide; i++) { + for (int j = 0; j < nPtsOnSide; j++) { + double x = baseX + i * deltaX; + double y = baseY + i * deltaY; + Geometry geom = geomFact.createPoint(new Coordinate(x, y)); + geoms[index++] = geom; + } + } + return geoms; + } + + +} 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 new file mode 100644 index 0000000000..c625666df9 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsAdjacentPerfTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022 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 test.jts.perf.operation.relateng; + +import java.io.FileReader; +import java.util.List; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.io.WKTFileReader; +import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.operation.relateng.IntersectionMatrixPattern; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.TestFiles; +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGPolygonsAdjacentPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGPolygonsAdjacentPerfTest.class); + } + + WKTReader rdr = new WKTReader(); + + private static final int N_ITER = 10; + + private List polygons; + + public RelateNGPolygonsAdjacentPerfTest(String name) { + super(name); + setRunSize(new int[] { 1 }); + //setRunSize(new int[] { 20 }); + setRunIterations(N_ITER); + } + + public void setUp() throws Exception + { + String resource = "europe.wkt"; + //String resource = "world.wkt"; + loadPolygons(resource); + + System.out.println("RelateNG Performance Test - Adjacent Polygons "); + System.out.println("Dataset: " + resource); + + System.out.println("# geometries: " + polygons.size() + + " # pts: " + numPts(polygons)); + System.out.println("----------------------------------"); + } + + private static int numPts(List geoms) { + int n = 0; + for (Geometry g : geoms) { + n += g.getNumPoints(); + } + return n; + } + + private void loadPolygons(String resourceName) throws Exception { + String path = TestFiles.getResourceFilePath(resourceName); + WKTFileReader wktFileRdr = new WKTFileReader(new FileReader(path), rdr); + polygons = wktFileRdr.read(); + } + + public void startRun(int npts) + { + + } + + public void runIntersectsOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.intersects(b); + } + } + } + + public void runIntersectsOldPrep() + { + for (Geometry a : polygons) { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(a); + for (Geometry b : polygons) { + pgA.intersects(b); + } + } + } + + public void runIntersectsNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.intersects()); + } + } + } + + public void runIntersectsNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + } + + public void runTouchesOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.touches(b); + } + } + } + + public void runTouchesNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.touches()); + } + } + } + + public void runTouchesNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.touches()); + } + } + } + + public void runAdjacentOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.relate(b, IntersectionMatrixPattern.ADJACENT); + } + } + } + + public void runAdjacentNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + } + } + + public void runAdjacentNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.matches(IntersectionMatrixPattern.ADJACENT)); + } + } + } + + public void runInteriorIntersectsOld() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + a.relate(b, IntersectionMatrixPattern.INTERIOR_INTERSECTS); + } + } + } + + public void runInteriorIntersectsNG() + { + for (Geometry a : polygons) { + for (Geometry b : polygons) { + RelateNG.relate(a, b, RelatePredicate.matches(IntersectionMatrixPattern.INTERIOR_INTERSECTS)); + } + } + } + + public void runInteriorIntersectsNGPrep() + { + for (Geometry a : polygons) { + RelateNG rng = RelateNG.prepare(a); + for (Geometry b : polygons) { + rng.evaluate(b, RelatePredicate.matches(IntersectionMatrixPattern.INTERIOR_INTERSECTS)); + } + } + } + +} 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 new file mode 100644 index 0000000000..d1a9a79777 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/operation/relateng/RelateNGPolygonsOverlappingPerfTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2022 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 test.jts.perf.operation.relateng; + +import static org.junit.Assert.assertEquals; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.geom.util.SineStarFactory; +import org.locationtech.jts.operation.relateng.RelateNG; +import org.locationtech.jts.operation.relateng.RelatePredicate; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class RelateNGPolygonsOverlappingPerfTest +extends PerformanceTestCase +{ + + public static void main(String args[]) { + PerformanceTestRunner.run(RelateNGPolygonsOverlappingPerfTest.class); + } + + private static final int N_ITER = 1; + + static double ORG_X = 100; + static double ORG_Y = ORG_X; + static double SIZE = 2 * ORG_X; + static int N_ARMS = 6; + static double ARM_RATIO = 0.3; + + static int GRID_SIZE = 100; + static double GRID_CELL_SIZE = SIZE / GRID_SIZE; + + static int NUM_CASES = GRID_SIZE * GRID_SIZE; + + private static final int B_SIZE_FACTOR = 20; + private static final GeometryFactory factory = new GeometryFactory(); + + private Geometry geomA; + + private Geometry[] geomB; + + public RelateNGPolygonsOverlappingPerfTest(String name) { + super(name); + setRunSize(new int[] { 100, 1000, 10000, 100000, + 200000 }); + //setRunSize(new int[] { 200000 }); + setRunIterations(N_ITER); + } + + public void setUp() + { + System.out.println("RelateNG perf test"); + System.out.println("SineStar: origin: (" + + ORG_X + ", " + ORG_Y + ") size: " + SIZE + + " # arms: " + N_ARMS + " arm ratio: " + ARM_RATIO); + System.out.println("# Iterations: " + N_ITER); + System.out.println("# B geoms: " + NUM_CASES); + } + + public void startRun(int npts) + { + Geometry sineStar = SineStarFactory.create(new Coordinate(ORG_X, ORG_Y), SIZE, npts, N_ARMS, ARM_RATIO); + geomA = sineStar; + + int nptsB = npts * B_SIZE_FACTOR / NUM_CASES; + if (nptsB < 10 ) nptsB = 10; + + geomB = createSineStarGrid(NUM_CASES, nptsB); + //geomB = createCircleGrid(NUM_CASES, nptsB); + + System.out.println("\n------- Running with A: polygon # pts = " + npts + + " B # pts = " + nptsB + " x " + NUM_CASES + " polygons"); + + /* + if (npts == 999) { + System.out.println(geomA); + + for (Geometry g : geomB) { + System.out.println(g); + } + } +*/ + } + + public void runIntersectsOld() + { + for (Geometry b : geomB) { + geomA.intersects(b); + } + } + + public void runIntersectsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.intersects(b); + } + } + + public void runIntersectsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.intersects()); + } + } + + public void runIntersectsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.intersects()); + } + } + + public void runContainsOld() + { + for (Geometry b : geomB) { + geomA.contains(b); + } + } + + public void runContainsOldPrep() + { + PreparedGeometry pgA = PreparedGeometryFactory.prepare(geomA); + for (Geometry b : geomB) { + pgA.contains(b); + } + } + + public void runContainsNG() + { + for (Geometry b : geomB) { + RelateNG.relate(geomA, b, RelatePredicate.contains()); + } + } + + public void runContainsNGPrep() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + rng.evaluate(b, RelatePredicate.contains()); + } + } + + public void xrunContainsNGPrepValidate() + { + RelateNG rng = RelateNG.prepare(geomA); + for (Geometry b : geomB) { + boolean resultNG = rng.evaluate(b, RelatePredicate.contains()); + boolean resultOld = geomA.contains(b); + assertEquals(resultNG, resultOld); + } + } + + private Geometry[] createSineStarGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Geometry geom = SineStarFactory.create(new Coordinate(x, y), GRID_CELL_SIZE, npts, N_ARMS, ARM_RATIO); + geoms[index++] = geom; + } + } + return geoms; + } + + private Geometry[] createCircleGrid(int nGeoms, int npts) { + Geometry[] geoms = new Geometry[ NUM_CASES ]; + int index = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = 0; j < GRID_SIZE; j++) { + double x = GRID_CELL_SIZE/2 + i * GRID_CELL_SIZE; + double y = GRID_CELL_SIZE/2 + j * GRID_CELL_SIZE; + Coordinate p = new Coordinate(x, y); + Geometry geom = factory.createPoint(p).buffer(GRID_CELL_SIZE / 2.0); + geoms[index++] = geom; + } + } + return geoms; + } + + +} diff --git a/modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java b/modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java new file mode 100644 index 0000000000..5ebed18079 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/triangulate/DelaunayStressTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023 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 test.jts.perf.triangulate; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.algorithm.ConvexHull; +import org.locationtech.jts.algorithm.Orientation; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.PrecisionModel; +import org.locationtech.jts.operation.overlayng.CoverageUnion; +import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder; +import org.locationtech.jts.triangulate.VoronoiDiagramBuilder; +import org.locationtech.jts.util.Memory; +import org.locationtech.jts.util.Stopwatch; + +/** + * Test correctness of Delaunay computation with + * synthetic random datasets. + * + * @author Martin Davis + * + */ +public class DelaunayStressTest +{ + private static final int N_PTS = 50; + private static final int RUN_COUNT = 10000; + final static double SIDE_LEN = 1000.0; + final static double BASE_OFFSET = 0; + + public static void main(String args[]) { + DelaunayStressTest test = new DelaunayStressTest(); + test.run(); + } + + final static GeometryFactory geomFact = new GeometryFactory(); + private static final double WIDTH = 100; + private static final double HEIGHT = 100; + + + public void run() + { + for (int i = 0; i < RUN_COUNT; i++) { + System.out.println("Run # " + i); + run(N_PTS); + } + } + + public void run(int nPts) + { + List pts = randomPointsInGrid(nPts, BASE_OFFSET, BASE_OFFSET, WIDTH, HEIGHT, 1); + run(pts); + } + + public void run(List pts) + { + System.out.println("Base offset: " + BASE_OFFSET); + System.out.println("# pts: " + pts.size()); + Stopwatch sw = new Stopwatch(); + DelaunayTriangulationBuilder builder = new DelaunayTriangulationBuilder(); + builder.setSites(pts); + + Geometry tris = builder.getTriangles(geomFact); + checkDelaunay(tris); + + checkVoronoi(pts); + + System.out.println(" -- Time: " + sw.getTimeString() + + " Mem: " + Memory.usedTotalString()); +// System.out.println(g); + } + + private void checkVoronoi(List pts) { + VoronoiDiagramBuilder vdb = new VoronoiDiagramBuilder(); + vdb.setSites(pts); + vdb.getDiagram(geomFact); + + //-- for now simply confirm the Voronoi is computed with no failure + } + + private void checkDelaunay(Geometry tris) { + //TODO: check all elements are triangles + + //-- check triangulation is a coverage + //-- this will error if triangulation is not a valid coverage + Geometry union = CoverageUnion.union(tris); + + checkConvex(tris, union); + } + + private void checkConvex(Geometry tris, Geometry triHull) { + Geometry convexHull = convexHull(tris); + boolean isEqual = triHull.equalsTopo(convexHull); + + boolean isConvex = isConvex((Polygon) triHull); + + if (! isConvex) { + System.out.println("Tris:"); + System.out.println(tris); + System.out.println("Convex Hull:"); + System.out.println(convexHull); + throw new IllegalStateException("Delaunay triangulation is not convex"); + } + } + + private Geometry convexHull(Geometry tris) { + ConvexHull hull = new ConvexHull(tris); + return hull.getConvexHull(); + } + + private boolean isConvex(Polygon poly) { + Coordinate[] pts = poly.getCoordinates(); + for (int i = 0; i < pts.length - 1; i++) { + int iprev = i - 1; + if (iprev < 0) iprev = pts.length - 2; + int inext = i + 1; + //-- orientation must be CLOCKWISE or COLLINEAR + boolean isConvex = Orientation.COUNTERCLOCKWISE != Orientation.index(pts[iprev], pts[i], pts[inext]); + if (! isConvex) + return false; + } + return true; + } + static List randomPointsInGrid(int nPts, double basex, double basey, double width, double height, double scale) + { + PrecisionModel pm = null; + if (scale > 0) { + pm = new PrecisionModel(scale); + } + List pts = new ArrayList(); + + int nSide = (int) Math.sqrt(nPts) + 1; + + for (int i = 0; i < nSide; i++) { + for (int j = 0; j < nSide; j++) { + double x = basex + i * width + width * Math.random(); + double y = basey + j * height + height * Math.random(); + Coordinate p = new Coordinate(x, y); + round(p, pm); + pts.add(p); + } + } + return pts; + } + + private static void round(Coordinate p, PrecisionModel pm) { + if (pm == null) + return; + pm.makePrecise(p); + } + + static List randomPoints(int nPts, double sideLen) + { + List pts = new ArrayList(); + + for (int i = 0; i < nPts; i++) { + double x = sideLen * Math.random(); + double y = sideLen * Math.random(); + pts.add(new Coordinate(x, y)); + } + return pts; + } +} diff --git a/modules/example/pom.xml b/modules/example/pom.xml index cc4eadfe24..5dede0e79f 100644 --- a/modules/example/pom.xml +++ b/modules/example/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-example ${project.groupId}:${project.artifactId} diff --git a/modules/io/common/pom.xml b/modules/io/common/pom.xml index 7b2154cf5f..5ea29f37e7 100644 --- a/modules/io/common/pom.xml +++ b/modules/io/common/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-io - 1.20.0-SNAPSHOT + 1.20.0 org.locationtech.jts.io jts-io-common diff --git a/modules/io/ora/pom.xml b/modules/io/ora/pom.xml index 9d7ef909e4..0c92dd48c7 100644 --- a/modules/io/ora/pom.xml +++ b/modules/io/ora/pom.xml @@ -4,7 +4,7 @@ org.locationtech.jts jts-io - 1.20.0-SNAPSHOT + 1.20.0 org.locationtech.jts.io jts-io-ora diff --git a/modules/io/pom.xml b/modules/io/pom.xml index 337127fe15..7b6cbb13ec 100644 --- a/modules/io/pom.xml +++ b/modules/io/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-io ${project.groupId}:${project.artifactId} diff --git a/modules/lab/pom.xml b/modules/lab/pom.xml index 6b44fd95c4..e5d1ae11ec 100644 --- a/modules/lab/pom.xml +++ b/modules/lab/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-lab ${project.groupId}:${project.artifactId} diff --git a/modules/pom.xml b/modules/pom.xml index d02e81a457..4a948e776f 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts - 1.20.0-SNAPSHOT + 1.20.0 jts-modules ${project.groupId}:${project.artifactId} diff --git a/modules/tests/pom.xml b/modules/tests/pom.xml index 03b41f2940..15a33b70d5 100644 --- a/modules/tests/pom.xml +++ b/modules/tests/pom.xml @@ -3,7 +3,7 @@ org.locationtech.jts jts-modules - 1.20.0-SNAPSHOT + 1.20.0 jts-tests ${project.groupId}:${project.artifactId} diff --git a/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java b/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java index 43778e5e96..bd2b468cba 100644 --- a/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java +++ b/modules/tests/src/main/java/org/locationtech/jtstest/geomop/TestCaseGeometryFunctions.java @@ -23,6 +23,8 @@ import org.locationtech.jts.operation.polygonize.Polygonizer; import org.locationtech.jts.precision.GeometryPrecisionReducer; import org.locationtech.jts.precision.MinimumClearance; +import org.locationtech.jts.simplify.DouglasPeuckerSimplifier; +import org.locationtech.jts.simplify.TopologyPreservingSimplifier; /** * Geometry functions which @@ -79,6 +81,14 @@ public static Geometry polygonizeValidPolygonal(Geometry g) { return polygonize(g, true); } + public static Geometry simplifyDP(Geometry g, double distance) { + return DouglasPeuckerSimplifier.simplify(g, distance); + } + + public static Geometry simplifyTP(Geometry g, double distance) { + return TopologyPreservingSimplifier.simplify(g, distance); + } + public static Geometry reducePrecision(Geometry g, double scaleFactor) { return GeometryPrecisionReducer.reduce(g, new PrecisionModel(scaleFactor)); } diff --git a/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java b/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java index beed109b6a..04f14d048d 100644 --- a/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java +++ b/modules/tests/src/main/java/org/locationtech/jtstest/testrunner/SimpleReportWriter.java @@ -70,7 +70,7 @@ public void reportOnTest(Test test) id += " " + test.getArgument(i); } if (test.getExpectedResult() instanceof BooleanResult) { - id += ", " + test.getExpectedResult().toShortString(); + id += " -> " + test.getExpectedResult().toShortString(); } if (test.getDescription().length() > 0) { id += ", " + test.getDescription(); diff --git a/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml b/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml index 0172730256..1f408ec25a 100644 --- a/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml +++ b/modules/tests/src/test/resources/testxml/failure/TestBufferFailure.xml @@ -2,7 +2,7 @@ Various cases which have been reported or identified as causing buffer failures. - + com.vividsolutions.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml b/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml index 0df4c7472b..a7aafee202 100644 --- a/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml +++ b/modules/tests/src/test/resources/testxml/failure/TestReducePrecisionFailure.xml @@ -5,7 +5,7 @@ See https://github.com/libgeos/geos/issues/511 Cases cause a TopologyException. Expected result is just a placeholder - + This used to fail, but with a fix to SnapRoundingNoder now works diff --git a/modules/tests/src/test/resources/testxml/general/TestBoundary.xml b/modules/tests/src/test/resources/testxml/general/TestBoundary.xml index befea95c30..f3a6dff809 100644 --- a/modules/tests/src/test/resources/testxml/general/TestBoundary.xml +++ b/modules/tests/src/test/resources/testxml/general/TestBoundary.xml @@ -1,5 +1,4 @@ - P - point diff --git a/modules/tests/src/test/resources/testxml/general/TestBuffer.xml b/modules/tests/src/test/resources/testxml/general/TestBuffer.xml index 49b5dc238b..0cb3999e1b 100644 --- a/modules/tests/src/test/resources/testxml/general/TestBuffer.xml +++ b/modules/tests/src/test/resources/testxml/general/TestBuffer.xml @@ -2,7 +2,7 @@ Basic buffer test cases. - + org.locationtech.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml b/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml index b266343fc0..d3a4046c1d 100644 --- a/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml +++ b/modules/tests/src/test/resources/testxml/general/TestBufferMitredJoin.xml @@ -2,7 +2,7 @@ Test cases for buffers with mitred joins. - + org.locationtech.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/general/TestCentroid.xml b/modules/tests/src/test/resources/testxml/general/TestCentroid.xml index c078e8afde..009ccd5c0d 100644 --- a/modules/tests/src/test/resources/testxml/general/TestCentroid.xml +++ b/modules/tests/src/test/resources/testxml/general/TestCentroid.xml @@ -1,5 +1,4 @@ - P - empty diff --git a/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml b/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml index 7de9aa2935..7651a611ad 100644 --- a/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml +++ b/modules/tests/src/test/resources/testxml/general/TestConvexHull-big.xml @@ -1,5 +1,4 @@ - Big convex hull diff --git a/modules/tests/src/test/resources/testxml/general/TestDensify.xml b/modules/tests/src/test/resources/testxml/general/TestDensify.xml index 9945e63f7d..8a8a124fe3 100644 --- a/modules/tests/src/test/resources/testxml/general/TestDensify.xml +++ b/modules/tests/src/test/resources/testxml/general/TestDensify.xml @@ -1,5 +1,5 @@ - + P - single point diff --git a/modules/tests/src/test/resources/testxml/general/TestDistance.xml b/modules/tests/src/test/resources/testxml/general/TestDistance.xml index 4ce6d0fede..e6444ddac4 100644 --- a/modules/tests/src/test/resources/testxml/general/TestDistance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestDistance.xml @@ -1,11 +1,11 @@ - PeP - point to an empty point POINT(10 10) POINT EMPTY 0.0 + 0.0 @@ -13,6 +13,31 @@ POINT(10 10) POINT (10 0) 10.0 + 10.0 + + + + PP - point to multipoint + POINT(10 10) + MULTIPOINT ((10 0), (30 30)) + 10.0 + 10.0 + + + + PP - point to multipoint with empty element + POINT(10 10) + MULTIPOINT ((10 0), EMPTY) + 10.0 + 10.0 + + + + LL - line to empty line + LINESTRING (0 0, 0 10) + LINESTRING EMPTY + 0.0 + 0.0 @@ -20,6 +45,31 @@ LINESTRING (0 0, 0 10) LINESTRING (10 0, 10 10) 10.0 + 10.0 + + + + LL - line to multiline + LINESTRING (0 0, 0 10) + MULTILINESTRING ((10 0, 10 10), (50 50, 60 60)) + 10.0 + 10.0 + + + + LL - line to multiline with empty element + LINESTRING (0 0, 0 10) + MULTILINESTRING ((10 0, 10 10), EMPTY) + 10.0 + 10.0 + + + + PA - point to empty polygon + POINT (240 160) + POLYGON EMPTY + 0.0 + 0.0 @@ -27,6 +77,7 @@ POINT (240 160) POLYGON ((100 260, 340 180, 100 60, 180 160, 100 260)) 0.0 + 0.0 @@ -34,6 +85,7 @@ LINESTRING (40 300, 280 220, 60 160, 140 60) LINESTRING (140 360, 260 280, 240 120, 120 160) 0.0 + 0.0 @@ -41,6 +93,7 @@ POLYGON ((60 260, 260 180, 100 60, 60 160, 60 260)) POLYGON ((220 280, 120 160, 300 60, 360 220, 220 280)) 0.0 + 0.0 @@ -48,6 +101,7 @@ POLYGON ((100 320, 60 120, 240 180, 200 260, 100 320)) POLYGON ((420 320, 280 260, 400 100, 420 320)) 71.55417527999327 + 71.55417527999327 @@ -55,13 +109,23 @@ MULTIPOLYGON (((40 240, 160 320, 40 380, 40 240)), ((100 240, 240 60, 40 40, 100 240))) MULTIPOLYGON (((220 280, 120 160, 300 60, 360 220, 220 280)), ((240 380, 280 300, 420 340, 240 380))) 0.0 + 0.0 - mAmA - multipolygon with empty component + mAmA - multipolygon with empty element MULTIPOLYGON (EMPTY, ((98 200, 200 200, 200 99, 98 99, 98 200))) POLYGON ((300 200, 400 200, 400 100, 300 100, 300 200)) 100.0 + 100.0 + + + + GCGC - geometry collections with mixed dimensions + GEOMETRYCOLLECTION (LINESTRING (10 10, 50 10), POINT (90 10)) + GEOMETRYCOLLECTION (POLYGON ((90 20, 60 20, 60 50, 90 50, 90 20)), LINESTRING (10 50, 30 70)) + 10.0 + 10.0 diff --git a/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml b/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml index ed601d481a..b6450dbd0f 100644 --- a/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml +++ b/modules/tests/src/test/resources/testxml/general/TestInteriorPoint.xml @@ -1,5 +1,4 @@ - P - empty @@ -14,12 +13,19 @@ - P - single point + P - multipoint MULTIPOINT ((60 300), (200 200), (240 240), (200 300), (40 140), (80 240), (140 240), (100 160), (140 200), (60 200)) POINT (140 240) + + P - multipoint with EMPTY + MULTIPOINT((0 0), EMPTY) + + POINT (0 0) + + L - empty LINESTRING EMPTY @@ -31,7 +37,7 @@ L - linestring with single segment LINESTRING (0 0, 7 14) - POINT (7 14) + POINT (0 0) @@ -61,6 +67,13 @@ POINT (180 200) + + mL - multilinestring with empty + MULTILINESTRING ((0 0, 1 1), EMPTY) + + POINT (0 0) + + A - box POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)) @@ -86,7 +99,7 @@ A - polygon with horizontal segment at centre (narrower L shape) POLYGON ((0 2, 0 4, 3 4, 3 0, 2 0, 2 2, 0 2)) - POINT (2 3) + POINT (1.5 3) @@ -96,6 +109,14 @@ POINT (115 200) + + + mA - multipolygon with empty + MULTIPOLYGON (((0 2, 0 4, 3 4, 3 0, 2 0, 2 2, 0 2)), EMPTY) + + POINT (1.5 3) + + GC - collection of polygons, lines, points GEOMETRYCOLLECTION (POLYGON ((0 40, 40 40, 40 0, 0 0, 0 40)), diff --git a/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml b/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml index d770180bb9..4534811190 100644 --- a/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestMinimumClearance.xml @@ -1,5 +1,5 @@ - + P - empty point diff --git a/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml b/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml index 69f80339e4..4d5f9b73d1 100644 --- a/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml +++ b/modules/tests/src/test/resources/testxml/general/TestPolygonize.xml @@ -1,5 +1,4 @@ - P - single point diff --git a/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml b/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml index 254f1e7518..38b9d6ca28 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRectanglePredicate.xml @@ -1,5 +1,4 @@ - A disjoint diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml index d8694b908e..b73c12d077 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateAA.xml @@ -1,5 +1,4 @@ - AA disjoint @@ -99,7 +98,7 @@ (120 140, 260 140, 260 260, 120 260, 120 140)) - true + true true false @@ -232,4 +231,18 @@ false + + A/mA A-shells overlapping B-shell at A-vertex + + POLYGON ((100 60, 140 100, 100 140, 60 100, 100 60)) + + + MULTIPOLYGON (((80 40, 120 40, 120 80, 80 80, 80 40)), ((120 80, 160 80, 160 120, 120 120, 120 80)), ((80 120, 120 120, 120 160, 80 160, 80 120)), ((40 80, 80 80, 80 120, 40 120, 40 80))) + + + true + + true + + diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateAC.xml b/modules/tests/src/test/resources/testxml/general/TestRelateAC.xml deleted file mode 100644 index 99d593f9c0..0000000000 --- a/modules/tests/src/test/resources/testxml/general/TestRelateAC.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - AC A-shells overlapping B-shell at A-vertex - - POLYGON( - (100 60, 140 100, 100 140, 60 100, 100 60)) - - - MULTIPOLYGON( - ( - (80 40, 120 40, 120 80, 80 80, 80 40)), - ( - (120 80, 160 80, 160 120, 120 120, 120 80)), - ( - (80 120, 120 120, 120 160, 80 160, 80 120)), - ( - (40 80, 80 80, 80 120, 40 120, 40 80))) - - - true - - true - - - diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml index acc895468f..482dc7b2a4 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLA.xml @@ -1,5 +1,4 @@ - LA - intersection at NV: {A-Bdy, A-Int} = {B-Bdy, B-Int} diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLC.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLC.xml deleted file mode 100644 index 864ce95589..0000000000 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLC.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - LC - topographically equal with no boundary - - LINESTRING(0 0, 0 50, 50 50, 50 0, 0 0) - - - MULTILINESTRING( - (0 0, 0 50), - (0 50, 50 50), - (50 50, 50 0), - (50 0, 0 0)) - - - - true - - - - - - LC - equal with boundary intersection - - LINESTRING(0 0, 60 0, 60 60, 60 0, 120 0) - - - MULTILINESTRING( - (0 0, 60 0), - (60 0, 120 0), - (60 0, 60 60)) - - - - true - - - - - diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml index 4a4fb4289d..592443d1f9 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml @@ -365,4 +365,30 @@ false + + LmL - topographically equal with no boundary + + LINESTRING(0 0, 0 50, 50 50, 50 0, 0 0) + + + MULTILINESTRING((0 0, 0 50), (0 50, 50 50), (50 50, 50 0), (50 0, 0 0)) + + + true + + + + + LmL - equal with boundary intersection + + LINESTRING(0 0, 60 0, 60 60, 60 0, 120 0) + + + MULTILINESTRING((0 0, 60 0), (60 0, 120 0), (60 0, 60 60)) + + + true + + + diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml index 7d9e4c5376..4c2345a7d2 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePA.xml @@ -1,5 +1,4 @@ - PA - disjoint @@ -15,6 +14,16 @@ true + false + false + false + false + true + false + false + false + false + false @@ -31,6 +40,16 @@ true + false + false + false + true + false + false + true + false + false + false @@ -39,14 +58,23 @@ MULTIPOINT((0 20), (20 20)) - POLYGON( - (20 40, 20 0, 60 0, 60 40, 20 40)) + POLYGON((20 40, 20 0, 60 0, 60 40, 20 40)) true + false + false + false + false + false + false + true + false + true + false @@ -55,14 +83,23 @@ MULTIPOINT((20 20), (40 20)) - POLYGON( - (20 40, 20 0, 60 0, 60 40, 20 40)) + POLYGON((20 40, 20 0, 60 0, 60 40, 20 40)) true + false + true + false + false + false + false + true + false + false + true @@ -71,14 +108,23 @@ MULTIPOINT((80 260), (140 260), (180 260)) - POLYGON( - (40 320, 140 320, 140 200, 40 200, 40 320)) + POLYGON((40 320, 140 320, 140 200, 40 200, 40 320)) true + false + false + false + true + false + false + true + false + false + false @@ -98,6 +144,141 @@ true + false + true + false + false + false + false + true + false + true + false + + + + mPA - empty MultiPoint element for A + + MULTIPOINT(EMPTY,(0 0)) + + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + + true + + + false + true + false + false + false + false + true + false + false + true + + + + mPA - empty MultiPoint element for A, on boundary of B + + MULTIPOINT(EMPTY,(1 0)) + + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + + true + + + false + true + false + false + false + false + true + false + true + false + + + + mPA - empty MultiPoint element for B + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + MULTIPOINT(EMPTY,(0 0)) + + + + true + + + true + false + true + false + false + false + true + false + false + false + + + + mPA - empty MultiPoint element for B, on boundary of A + + POLYGON ((1 0,0 1,-1 0,0 -1, 1 0)) + + + MULTIPOINT(EMPTY,(1 0)) + + + + true + + + false + false + true + false + false + false + true + false + true + false + + + + PmA - empty MultiPolygon element + + POINT(0 0) + + + MULTIPOLYGON (EMPTY, ((1 0,0 1,-1 0,0 -1, 1 0))) + + + + true + + + false + true + false + false + false + false + true + false + false + true diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml index 13bb4dc095..07ddff57ac 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePL.xml @@ -1,5 +1,4 @@ - PL - disjoint diff --git a/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml b/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml index 738978b173..10bdf362be 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelatePP.xml @@ -1,5 +1,4 @@ - same point diff --git a/modules/tests/src/test/resources/testxml/general/TestSimple.xml b/modules/tests/src/test/resources/testxml/general/TestSimple.xml index 539cc00c78..c496ab7d94 100644 --- a/modules/tests/src/test/resources/testxml/general/TestSimple.xml +++ b/modules/tests/src/test/resources/testxml/general/TestSimple.xml @@ -1,5 +1,4 @@ - P - point @@ -49,6 +48,14 @@ + + mP - with empty element + + MULTIPOINT (EMPTY, (80 220), (160 220)) + + true + + L - simple line @@ -349,6 +356,14 @@ + + mL - with empty element + + MULTILINESTRING ((0 0, 100 100), EMPTY) + + true + + LR - valid ring @@ -453,6 +468,14 @@ MULTIPOLYGON (((100 100, 100 200, 200 100, 200 200, 100 100)), ((100 400, 200 40 + + mA - with empty element + +MULTIPOLYGON (((0 10, 10 10, 10 0, 0 0, 0 10)), EMPTY) + + true + + GC - all components simple @@ -477,4 +500,13 @@ GEOMETRYCOLLECTION (POLYGON ((100 100, 100 200, 200 100, 200 200, 100 100)), + + GC - with empty element + +GEOMETRYCOLLECTION (POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10)), + LINESTRING (100 300, 200 250), + POINT EMPTY) + true + + diff --git a/modules/tests/src/test/resources/testxml/general/TestSimplify.xml b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml new file mode 100644 index 0000000000..d00cc22e1e --- /dev/null +++ b/modules/tests/src/test/resources/testxml/general/TestSimplify.xml @@ -0,0 +1,231 @@ + + Test cases for DouglasPeuckerSimplifier and TopologyPreservingSimplifier + + + + P - point + POINT(10 10) + + + POINT(10 10) + + + POINT(10 10) + + + + + mP - point with EMPTY + MULTIPOINT( EMPTY, (10 10), (20 20)) + + + MULTIPOINT((10 10), (20 20)) + + + MULTIPOINT((10 10), (20 20)) + + + + + L - empty line + LINESTRING EMPTY + + + LINESTRING EMPTY + + + LINESTRING EMPTY + + + + + L - line + LINESTRING (10 10, 20 21, 30 30) + + + LINESTRING (10 10, 30 30) + + + LINESTRING (10 10, 30 30) + + + + + L - short line + LINESTRING (0 5, 1 5, 2 5, 5 5) + + + LINESTRING (0 5, 5 5) + + + LINESTRING (0 5, 5 5) + + + + + mL - lines with constrained topology + MULTILINESTRING ((10 60, 39 50, 70 60, 90 50), (35 55, 46 55), (65 55, 75 55), (10 40, 40 30, 70 40, 90 30)) + + + MULTILINESTRING ((10 60, 90 50), (35 55, 46 55), (65 55, 75 55), (10 40, 90 30)) + + + MULTILINESTRING ((10 60, 39 50, 70 60, 90 50), (35 55, 46 55), (65 55, 75 55), (10 40, 90 30)) + + + + + mL - lines with EMPTY + MULTILINESTRING(EMPTY, (10 10, 20 21, 30 30), (10 10, 10 30, 30 30)) + + + MULTILINESTRING ((10 10, 30 30), (10 10, 10 30, 30 30)) + + + MULTILINESTRING ((10 10, 30 30), (10 10, 10 30, 30 30)) + + + + + A - polygon with flat endpoint + POLYGON ((5 1, 1 1, 1 9, 9 9, 9 1, 5 1)) + + + POLYGON ((1 1, 1 9, 9 9, 9 1, 1 1)) + + + POLYGON ((1 1, 1 9, 9 9, 9 1, 1 1)) + + + + + A - polygon with multiple flat segments around endpoint + POLYGON ((5 5, 7 5, 9 5, 9 1, 1 1, 1 5, 3 5, 5 5)) + + + POLYGON ((9 5, 9 1, 1 1, 1 5, 9 5)) + + + POLYGON ((9 5, 9 1, 1 1, 1 5, 9 5)) + + + + + A - polygon simplification + POLYGON ((10 10, 10 90, 60.5 87, 90 90, 90 10, 12 12, 10 10)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10)) + + + + + A - polygon with edge collapse. + DP: polygon is split + TP: unchanged + + POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240)) + + + MULTIPOLYGON (((40 240, 160 240, 40 140, 40 240)), ((160 240, 280 240, 280 160, 160 240))) + + + POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240)) + + + + + A - polygon collapse for DP + POLYGON ((5 2, 9 1, 1 1, 5 2)) + + + POLYGON EMPTY + + + POLYGON ((5 2, 9 1, 1 1, 5 2)) + + + + + A - polygon with touching hole + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 50 90, 80 80, 80 20)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 80 80, 80 20)) + + + POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (80 20, 20 20, 20 80, 80 80, 80 20)) + + + + + A - polygon with large hole near edge. + DP: simplified and fixed + TP: unchanged + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (80 20, 20 20, 50 90, 80 20)) + + + POLYGON ((10 10, 10 80, 45.714285714285715 80, 20 20, 80 20, 54.285714285714285 80, 90 80, 90 10, 10 10)) + + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (80 20, 20 20, 50 90, 80 20)) + + + + + A - polygon with small hole near simplified edge + DP: hole is remmoved + TP: hole is preserved + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) + + + POLYGON ((10 10, 10 80, 90 80, 90 10, 10 10)) + + + POLYGON ((10 10, 10 80, 50 90, 90 80, 90 10, 10 10), (70 81, 30 81, 50 90, 70 81)) + + + + + mA - multipolygon with EMPTY + MULTIPOLYGON (EMPTY, ((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90))) + + + MULTIPOLYGON (((10 90, 10 10, 90 10, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90))) + + + MULTIPOLYGON (((10 90, 10 10, 90 10, 50 60, 10 90)), ((70 90, 90 90, 90 70, 70 70, 70 90))) + + + + + mA - multipolygon with small element removed + MULTIPOLYGON (((10 90, 10 10, 40 40, 90 10, 47 57, 10 90)), ((90 90, 90 85, 85 85, 85 90, 90 90))) + + + POLYGON ((10 90, 10 10, 40 40, 90 10, 10 90)) + + + MULTIPOLYGON (((10 90, 10 10, 40 40, 90 10, 10 90)), ((85 90, 90 85, 85 85, 85 90))) + + + + + GC - geometry collection + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 40 40, 90 10, 47 57, 10 90)), LINESTRING (30 90, 65 65, 90 30), MULTIPOINT ((80 90), (90 90))) + + + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 40 40, 90 10, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) + + + GEOMETRYCOLLECTION (POLYGON ((10 90, 10 10, 40 40, 90 10, 10 90)), LINESTRING (30 90, 90 30), MULTIPOINT ((80 90), (90 90))) + + + + + diff --git a/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml b/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml index 090477fc86..c8f333fda4 100644 --- a/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml +++ b/modules/tests/src/test/resources/testxml/general/TestUnaryUnion.xml @@ -1,6 +1,5 @@ Tests for Geometry.union() method (unary union) - P - point (showing merging of identical points) @@ -144,18 +143,6 @@ - - mA - multipolygon (invalid) with topology collapse - MULTIPOLYGON (((0 0, 150 0, 150 1, 0 0)), - ((180 0, 20 0, 20 100, 180 100, 180 0))) - - - - POLYGON ((150 0, 20 0, 20 100, 180 100, 180 0, 150 0)) - - - - P - empty Point POINT EMPTY diff --git a/modules/tests/src/test/resources/testxml/general/TestValid.xml b/modules/tests/src/test/resources/testxml/general/TestValid.xml index 7173219169..dc94b3efde 100644 --- a/modules/tests/src/test/resources/testxml/general/TestValid.xml +++ b/modules/tests/src/test/resources/testxml/general/TestValid.xml @@ -1,5 +1,4 @@ - P - point (valid) diff --git a/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml b/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml index a4a1e3511f..1438bcd279 100644 --- a/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml +++ b/modules/tests/src/test/resources/testxml/general/TestWithinDistance.xml @@ -1,5 +1,4 @@ - PP - disjoint points diff --git a/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml b/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml index 27d22b827b..c0f0857953 100644 --- a/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml +++ b/modules/tests/src/test/resources/testxml/misc/TestBufferExternal.xml @@ -4,7 +4,7 @@ in previous versions of JTS. The cases in this file should all pass in the current version of JTS. - + com.vividsolutions.jtstest.testrunner.BufferResultMatcher 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..f67b789a0a --- /dev/null +++ b/modules/tests/src/test/resources/testxml/misc/TestRelateGC.xml @@ -0,0 +1,601 @@ + + + + GC:L/GC:PL - a line with the same line in a collection with an empty polygon + + LINESTRING(0 0, 1 1) + + + GEOMETRYCOLLECTION(POLYGON EMPTY, LINESTRING(0 0, 1 1)) + + true + true + true + true + false + false + true + true + false + false + true + + + + 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 + + + + A/GC:mP with empty MultiPoint elements + + POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7)) + + + GEOMETRYCOLLECTION (MULTIPOINT (EMPTY, (5 5)), LINESTRING (1 9, 4 9)) + + true + false + false + false + true + false + false + true + false + false + false + + + + mA/GC:PL + + 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/A + + 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 on boundary of GC with line and point + + POINT(0 0) + + + GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0)) + + true + false + true + false + false + false + false + true + false + true + false + + + + + L/GC:A - line in interior of GC of overlapping polygons + + LINESTRING (3 7, 7 3) + + + GEOMETRYCOLLECTION (POLYGON ((1 9, 7 9, 7 3, 1 3, 1 9)), POLYGON ((9 1, 3 1, 3 7, 9 7, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common boundaries of 2 adjacent polygons + + POINT (4 3) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common node of 3 adjacent polygons + + POINT (5 4) + + + GEOMETRYCOLLECTION (POLYGON ((1 6, 5 4, 4 1, 1 6)), POLYGON ((4 1, 5 4, 9 6, 4 1)), POLYGON ((1 6, 9 6, 5 4, 1 6))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common node of 6 adjacent polygons, with holes at node + + POINT (6 6) + + +GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + P/GC:A - point on common node of 5 adjacent polygons, with holes at node and one not filled + + POINT (6 6) + + +GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1))) + + true + false + true + false + false + false + false + true + false + true + false + + + + L/GC:A - line on common boundaries of adjacent polygons + + LINESTRING (4 5, 4 2) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + L/GC:A - line on exterior boundaries of GC of overlapping polygons + + LINESTRING (2 6, 8 6) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + true + false + + + + GC:L/GC:A - lines covers boundaries of overlapping polygons + + GEOMETRYCOLLECTION (LINESTRING (2 6, 9 6, 9 1, 7 1), LINESTRING (8 1, 1 1, 1 6, 7 6)) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + true + false + + + + GC:A/GC:A - adjacent polygons contained by adjacent polygons + + GEOMETRYCOLLECTION (POLYGON ((2 2, 2 5, 4 5, 4 2, 2 2)), POLYGON ((8 2, 4 3, 4 4, 8 5, 8 2))) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + GC:A/P - adjacent polygons contain point at interior node + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POINT (5 5) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/P - adjacent polygons contain point on interior edge + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POINT (7 5) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/P - adjacent polygons cover point on exterior node + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POINT (9 5) + + true + false + false + true + false + false + false + true + false + true + false + + + + GC:A/L - adjacent polygons contain line touching interior node + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + LINESTRING (5 5, 7 7) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/L - adjacent polygons contain line along interior edge to boundary + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + LINESTRING (5 5, 9 5) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/GC:PL - adjacent polygons contain line and point + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + GEOMETRYCOLLECTION (POINT (5 5), LINESTRING (5 7, 7 7)) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/A - adjacent polygons containing polygon with endpoint inside + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7)) + + true + true + false + true + false + false + false + true + false + false + false + + + + GC:A/A - adjacent polygons overlapping polygon with shell outside and hole inside + + GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9))) + + + POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (2 8, 8 8, 8 2, 2 2, 2 8)) + + true + false + false + false + false + false + false + true + true + false + false + + + + GC:A/GC:A - overlapping polygons equal to overlapping polygons + + GEOMETRYCOLLECTION (POLYGON ((1 6, 9 6, 9 2, 1 2, 1 6)), POLYGON ((9 1, 1 1, 1 5, 9 5, 9 1))) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + true + true + true + false + false + true + true + false + false + true + + + + GC:A/GC:A - overlapping polygons contained by overlapping polygons + + GEOMETRYCOLLECTION (POLYGON ((4 4, 6 4, 6 3, 4 3, 4 4)), POLYGON ((2 5, 8 5, 8 2, 2 2, 2 5))) + + + GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1))) + + true + false + true + false + false + false + false + true + false + false + true + + + + + A/GC:A - polygon equal to nested overlapping polygons + + POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)) + + + GEOMETRYCOLLECTION ( + POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1)), + GEOMETRYCOLLECTION( + POLYGON ((1 5, 5 9, 9 9, 9 5, 5 1, 1 5)), + MULTIPOLYGON (((1 9, 5 9, 5 5, 1 5, 1 9)), ((9 1, 5 1, 5 5, 9 5, 9 1))) + ) + ) + + true + true + true + true + false + false + true + true + false + false + true + + + + GC:AmP/A - polygon with overlapping points equal to polygon + + GEOMETRYCOLLECTION (POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)), + MULTIPOINT(0 2, 0 5)) + + + POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)) + + true + true + true + true + false + false + true + true + false + false + true + + + + GC:AL/A - polygon with overlapping line equal to polygon + + GEOMETRYCOLLECTION (POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)), + LINESTRING (0 2, 0 5, 5 5)) + + + POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0)) + + true + true + true + true + false + false + true + true + false + false + true + + + diff --git a/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml b/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml index 8a7449497c..650225fd8a 100644 --- a/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml +++ b/modules/tests/src/test/resources/testxml/misc/geos-bug356-buffer.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/356 - + com.vividsolutions.jtstest.testrunner.BufferResultMatcher diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml index 3060558223..12aaf6f699 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-1046-union-lines.xml @@ -1,5 +1,5 @@ - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml index ad25099a8a..91000d1ccd 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-234.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/234 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml index 5d3f2dc050..3fff93dd6d 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-275.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/275 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml index a50f61a03e..1546b35f06 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-350.xml @@ -1,6 +1,6 @@ http://trac.osgeo.org/geos/ticket/350 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml index 7c6f353a72..2ceb0bd273 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-358.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/358 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml index 1ce0cc64ac..8545067a90 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-360.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/360 - + 1 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml index d15d275cf6..cd6b8d3845 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392-lines.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/392 - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml index 49267e606b..2cc7c34df8 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-392.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/392 - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml index 84179f5ab8..7e8bde84bf 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-398.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/398 - + http://trac.osgeo.org/geos/ticket/459 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml index d1a897dc61..fb2fa142c0 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-488.xml @@ -3,7 +3,7 @@ http://trac.osgeo.org/geos/ticket/488 - + 1E-7 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml index 13acd0ee25..0875192852 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-522.xml @@ -1,6 +1,6 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml index c3d77da550..e81cddebce 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-527.xml @@ -1,6 +1,6 @@ - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml index f947be5273..ec8b397c9d 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-599.xml @@ -2,7 +2,7 @@ http://trac.osgeo.org/geos/ticket/599 - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml index f5df9d2e11..d92f348d74 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-600-lines.xml @@ -1,5 +1,5 @@ - + .01 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml index e0a4b44c60..5eaec4bab3 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-615.xml @@ -1,6 +1,6 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml index 50342c5a75..4b643980dd 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-geos-994.xml @@ -1,5 +1,5 @@ - + 1 Unary union test from QGIS test suite. diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml index 5cc8068b16..24d83f8bfb 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-3.xml @@ -1,5 +1,5 @@ - + AA - OLD robustness failure (works with snapping) diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml index c52bb1004f..a1558a662f 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-misc-4.xml @@ -1,5 +1,5 @@ - + AA - causes failure due to snapping making input invalid (JTS 1.10) diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml index 0dce60417e..560ff45e32 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2055.xml @@ -1,5 +1,5 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml index 0a0719117f..a983fdaea0 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-2176.xml @@ -1,5 +1,5 @@ - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml index c76d0e15af..5640070a66 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4182-2.xml @@ -3,7 +3,7 @@ https://trac.osgeo.org/postgis/ticket/4182 Overlay Topology Exception - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml index 6e4c478fa2..2fb33c658b 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4538.xml @@ -3,7 +3,7 @@ https://trac.osgeo.org/postgis/ticket/4538 Overlay Topology Exception - + diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml index 256bb4aebd..5357e72432 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-pg-4738.xml @@ -3,7 +3,7 @@ https://trac.osgeo.org/postgis/ticket/4738 Union of LineStrings fails using simple noding. - + 1E-12 diff --git a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml index ec3fabc6c0..c3cc841ba6 100644 --- a/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml +++ b/modules/tests/src/test/resources/testxml/robust/overlay/TestOverlay-shapely-829.xml @@ -3,7 +3,7 @@ https://github.com/Toblerity/Shapely/issues/829 Overlay Topology Exception on union of polygons - + 0.01 diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml index bb9c3eae1d..a2e530a433 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateAA.xml @@ -14,16 +14,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -39,16 +39,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -64,16 +64,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -89,16 +89,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -114,16 +114,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -139,16 +139,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -164,16 +164,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -189,16 +189,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -214,16 +214,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -239,16 +239,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -264,16 +264,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -289,16 +289,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -314,16 +314,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -339,16 +339,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -365,16 +365,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -391,16 +391,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -416,16 +416,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -441,16 +441,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -466,16 +466,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -491,16 +491,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -516,16 +516,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -541,16 +541,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -566,16 +566,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -591,16 +591,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -616,16 +616,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -641,16 +641,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -666,16 +666,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -691,16 +691,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -716,16 +716,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -741,16 +741,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -766,16 +766,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -791,16 +791,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -816,16 +816,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -841,16 +841,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -866,16 +866,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -891,16 +891,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -916,16 +916,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -941,16 +941,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -966,16 +966,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -991,16 +991,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1016,16 +1016,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1041,16 +1041,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1066,16 +1066,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1091,16 +1091,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1116,16 +1116,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1141,16 +1141,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1166,16 +1166,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1191,16 +1191,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1216,16 +1216,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1241,16 +1241,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1266,16 +1266,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1291,16 +1291,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1316,16 +1316,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1362,16 +1362,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1414,16 +1414,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1439,16 +1439,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1464,16 +1464,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1489,16 +1489,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1514,16 +1514,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1539,16 +1539,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1564,16 +1564,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1589,16 +1589,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1614,16 +1614,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1639,16 +1639,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1664,16 +1664,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1689,16 +1689,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1714,16 +1714,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1739,16 +1739,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1764,16 +1764,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1789,16 +1789,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1814,16 +1814,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1839,16 +1839,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1864,16 +1864,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1889,16 +1889,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1914,16 +1914,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1939,16 +1939,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1965,16 +1965,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1991,16 +1991,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2017,16 +2017,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2043,16 +2043,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2069,16 +2069,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2095,16 +2095,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2121,16 +2121,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2147,16 +2147,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2173,16 +2173,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2199,16 +2199,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2225,16 +2225,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2251,16 +2251,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2278,16 +2278,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2304,16 +2304,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2330,16 +2330,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2356,16 +2356,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2383,16 +2383,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2412,16 +2412,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2440,16 +2440,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2468,16 +2468,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2496,16 +2496,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2524,16 +2524,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2552,16 +2552,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2580,16 +2580,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2612,16 +2612,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2643,16 +2643,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2678,16 +2678,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2713,16 +2713,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2748,16 +2748,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2783,16 +2783,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2818,16 +2818,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml index 526288b468..1e4feca114 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateAC.xml @@ -21,16 +21,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml index 3be916eae9..4f19f28800 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateLA.xml @@ -13,16 +13,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -37,16 +37,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -61,16 +61,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -85,16 +85,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -109,16 +109,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -133,16 +133,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -157,16 +157,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -181,16 +181,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -205,16 +205,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -229,16 +229,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -253,16 +253,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -277,16 +277,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -301,16 +301,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -325,16 +325,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -349,16 +349,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -373,16 +373,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -397,16 +397,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -421,16 +421,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -445,16 +445,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -470,16 +470,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -495,16 +495,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -520,16 +520,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -545,16 +545,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -570,16 +570,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -595,16 +595,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -620,16 +620,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -645,16 +645,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -670,16 +670,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -695,16 +695,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -720,16 +720,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -745,16 +745,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -771,16 +771,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -797,16 +797,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -823,16 +823,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -849,16 +849,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -875,16 +875,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -901,16 +901,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -927,16 +927,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -953,16 +953,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -980,16 +980,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1007,16 +1007,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1034,16 +1034,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1058,16 +1058,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1082,16 +1082,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1107,16 +1107,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1131,16 +1131,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1156,16 +1156,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1181,16 +1181,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1206,16 +1206,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1230,16 +1230,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1255,16 +1255,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1279,16 +1279,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1304,16 +1304,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1329,16 +1329,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1354,16 +1354,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1379,16 +1379,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1403,16 +1403,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1427,16 +1427,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1451,16 +1451,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1475,16 +1475,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1499,16 +1499,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1523,16 +1523,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1547,16 +1547,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1571,16 +1571,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1596,16 +1596,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1622,16 +1622,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1648,16 +1648,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1674,16 +1674,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1700,16 +1700,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1726,16 +1726,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1752,16 +1752,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1778,16 +1778,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1804,16 +1804,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1830,16 +1830,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1859,16 +1859,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1888,16 +1888,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1917,16 +1917,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml index c002318b33..0bb1205fb6 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateLC.xml @@ -16,16 +16,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -42,16 +42,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml b/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml index 0308d75e63..8124b25ab6 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelateLL.xml @@ -12,16 +12,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -35,16 +35,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -58,16 +58,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -81,16 +81,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -104,16 +104,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -127,16 +127,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -150,16 +150,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -173,16 +173,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -196,16 +196,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -219,16 +219,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -242,16 +242,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -265,16 +265,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -288,16 +288,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -311,16 +311,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -334,16 +334,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -357,16 +357,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -380,16 +380,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -403,16 +403,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -426,16 +426,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -449,16 +449,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -472,16 +472,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -495,16 +495,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -518,16 +518,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -541,16 +541,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -564,16 +564,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -587,16 +587,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -610,16 +610,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -633,16 +633,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -656,16 +656,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -679,16 +679,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -702,16 +702,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -725,16 +725,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -748,16 +748,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -771,16 +771,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -794,16 +794,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -817,16 +817,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -840,16 +840,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -863,16 +863,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -886,16 +886,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -909,16 +909,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -932,16 +932,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -955,16 +955,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -978,16 +978,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1001,16 +1001,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1024,16 +1024,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1047,16 +1047,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1071,16 +1071,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1102,16 +1102,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1125,16 +1125,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1148,16 +1148,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1171,16 +1171,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1194,16 +1194,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1217,16 +1217,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1240,16 +1240,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1263,16 +1263,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -1286,16 +1286,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1309,16 +1309,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1332,16 +1332,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1355,16 +1355,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -1378,16 +1378,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1401,16 +1401,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1424,16 +1424,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1447,16 +1447,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1470,16 +1470,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1493,16 +1493,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1516,16 +1516,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1539,16 +1539,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1562,16 +1562,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1585,16 +1585,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1609,16 +1609,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1632,16 +1632,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1655,16 +1655,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1678,16 +1678,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1701,16 +1701,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1724,16 +1724,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1747,16 +1747,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1770,16 +1770,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1793,16 +1793,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1816,16 +1816,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1839,16 +1839,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -1862,16 +1862,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1885,16 +1885,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1908,16 +1908,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1931,16 +1931,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1954,16 +1954,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -1977,16 +1977,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2000,16 +2000,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2023,16 +2023,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2046,16 +2046,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2069,16 +2069,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2092,16 +2092,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2115,16 +2115,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2138,16 +2138,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2161,16 +2161,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2184,16 +2184,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2207,16 +2207,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2230,16 +2230,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2253,16 +2253,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2276,16 +2276,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2299,16 +2299,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2322,16 +2322,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2345,16 +2345,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2368,16 +2368,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2391,16 +2391,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2414,16 +2414,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2439,16 +2439,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2462,16 +2462,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2485,16 +2485,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2508,16 +2508,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2531,16 +2531,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -2554,16 +2554,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2577,16 +2577,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2600,16 +2600,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2623,16 +2623,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2646,16 +2646,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2669,16 +2669,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2692,16 +2692,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2715,16 +2715,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2738,16 +2738,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2761,16 +2761,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2784,16 +2784,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2807,16 +2807,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2830,16 +2830,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2853,16 +2853,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -2876,16 +2876,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2899,16 +2899,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2922,16 +2922,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2945,16 +2945,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2968,16 +2968,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2991,16 +2991,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -3018,16 +3018,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -3045,16 +3045,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -3072,16 +3072,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -3100,16 +3100,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3127,16 +3127,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3154,16 +3154,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3182,16 +3182,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3210,16 +3210,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3238,16 +3238,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3265,16 +3265,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3292,16 +3292,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -3319,16 +3319,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -3346,16 +3346,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -3373,16 +3373,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml b/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml index 643e76603a..7d864c36bd 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelatePA.xml @@ -13,16 +13,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -37,16 +37,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -61,16 +61,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -85,16 +85,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -109,16 +109,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -133,16 +133,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -157,16 +157,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -181,16 +181,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -205,16 +205,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -230,16 +230,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -255,16 +255,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -280,16 +280,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -305,16 +305,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -330,16 +330,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -355,16 +355,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -380,16 +380,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -405,16 +405,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -430,16 +430,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -456,16 +456,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -482,16 +482,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -506,16 +506,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -530,16 +530,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -554,16 +554,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -578,16 +578,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -602,16 +602,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -626,16 +626,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -650,16 +650,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -674,16 +674,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -698,16 +698,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -722,16 +722,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -746,16 +746,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -770,16 +770,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -794,16 +794,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -818,16 +818,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -843,16 +843,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -868,16 +868,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -893,16 +893,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -920,16 +920,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -947,16 +947,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -974,16 +974,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1003,16 +1003,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml b/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml index 2e11fb7b9e..30972a07e2 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelatePL.xml @@ -12,16 +12,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -35,16 +35,16 @@ true - true - true - true - false - false - false - true - false - false - true + true + true + true + false + false + false + true + false + false + true @@ -58,16 +58,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -81,16 +81,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -104,16 +104,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -127,16 +127,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -150,16 +150,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -173,16 +173,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -196,16 +196,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -219,16 +219,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -242,16 +242,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -265,16 +265,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -288,16 +288,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -311,16 +311,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -334,16 +334,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -357,16 +357,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -380,16 +380,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -403,16 +403,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -426,16 +426,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -449,16 +449,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -472,16 +472,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -495,16 +495,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -518,16 +518,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -541,16 +541,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -564,16 +564,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -587,16 +587,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -610,16 +610,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -633,16 +633,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -657,16 +657,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -680,16 +680,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -703,16 +703,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -726,16 +726,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -749,16 +749,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -772,16 +772,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -795,16 +795,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -818,16 +818,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -841,16 +841,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -864,16 +864,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -887,16 +887,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -910,16 +910,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -933,16 +933,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -956,16 +956,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -979,16 +979,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1002,16 +1002,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1025,16 +1025,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1048,16 +1048,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1071,16 +1071,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1094,16 +1094,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1117,16 +1117,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1140,16 +1140,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1163,16 +1163,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1186,16 +1186,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1209,16 +1209,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1232,16 +1232,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1255,16 +1255,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1278,16 +1278,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1301,16 +1301,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1324,16 +1324,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1347,16 +1347,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1370,16 +1370,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1394,16 +1394,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1417,16 +1417,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1441,16 +1441,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1464,16 +1464,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1487,16 +1487,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1510,16 +1510,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1533,16 +1533,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1557,16 +1557,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1580,16 +1580,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1604,16 +1604,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1627,16 +1627,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1650,16 +1650,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1673,16 +1673,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1696,16 +1696,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1719,16 +1719,16 @@ true - false - true - false - false - false - false - true - false - true - false + false + true + false + false + false + false + true + false + true + false @@ -1742,16 +1742,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1765,16 +1765,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1788,16 +1788,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1811,16 +1811,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1834,16 +1834,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1857,16 +1857,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1880,16 +1880,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1903,16 +1903,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -1926,16 +1926,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1949,16 +1949,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -1972,16 +1972,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -1995,16 +1995,16 @@ true - false - false - false - false - false - false - true - false - true - false + false + false + false + false + false + false + true + false + true + false @@ -2018,16 +2018,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2041,16 +2041,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2064,16 +2064,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2087,16 +2087,16 @@ true - false - false - false - true - false - false - true - false - false - false + false + false + false + true + false + false + true + false + false + false @@ -2110,16 +2110,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2133,16 +2133,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2156,16 +2156,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2179,16 +2179,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2202,16 +2202,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2225,16 +2225,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2248,16 +2248,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -2271,16 +2271,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true diff --git a/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml b/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml index 847908e5ba..e4d062902e 100644 --- a/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml +++ b/modules/tests/src/test/resources/testxml/validate/TestRelatePP.xml @@ -12,16 +12,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -35,16 +35,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -58,16 +58,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -81,16 +81,16 @@ true - false - true - false - false - false - false - true - false - false - true + false + true + false + false + false + false + true + false + false + true @@ -104,16 +104,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -127,16 +127,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -150,16 +150,16 @@ true - false - false - false - false - true - false - false - false - false - false + false + false + false + false + true + false + false + false + false + false @@ -173,16 +173,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -196,16 +196,16 @@ true - true - true - true - false - false - true - true - false - false - true + true + true + true + false + false + true + true + false + false + true @@ -219,16 +219,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -242,16 +242,16 @@ true - true - false - true - false - false - false - true - false - false - false + true + false + true + false + false + false + true + false + false + false @@ -265,16 +265,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false @@ -288,16 +288,16 @@ true - false - false - false - false - false - false - true - true - false - false + false + false + false + false + false + false + true + true + false + false diff --git a/pom.xml b/pom.xml index 484b7dcf7c..42c1ae5a81 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.locationtech.jts jts - 1.20.0.rjm-SNAPSHOT + 1.20.0.rjm pom JTS Topology Suite