|
1 | 1 | /* |
2 | | - * Copyright (c) 2016 Vivid Solutions. |
| 2 | + * Copyright (c) 2026 Martin Davis. |
3 | 3 | * |
4 | 4 | * All rights reserved. This program and the accompanying materials |
5 | 5 | * are made available under the terms of the Eclipse Public License 2.0 |
|
17 | 17 |
|
18 | 18 | import org.locationtech.jts.geom.Coordinate; |
19 | 19 | import org.locationtech.jts.geom.CoordinateArrays; |
| 20 | +import org.locationtech.jts.geom.CoordinateList; |
20 | 21 | import org.locationtech.jts.geom.Envelope; |
21 | 22 | import org.locationtech.jts.geom.Geometry; |
22 | 23 | import org.locationtech.jts.geom.GeometryCollection; |
23 | 24 | import org.locationtech.jts.geom.GeometryFactory; |
| 25 | +import org.locationtech.jts.geom.LinearRing; |
24 | 26 | import org.locationtech.jts.geom.Polygon; |
| 27 | +import org.locationtech.jts.noding.snap.SnappingPointIndex; |
25 | 28 | import org.locationtech.jts.triangulate.quadedge.QuadEdgeSubdivision; |
26 | 29 |
|
27 | 30 |
|
|
44 | 47 | */ |
45 | 48 | public class VoronoiDiagramBuilder |
46 | 49 | { |
47 | | - private Collection siteCoords; |
| 50 | + /** |
| 51 | + * A very small factor which detects short Voronoi cell segments |
| 52 | + * which might be caused by nearly-cocircular site circumcentres. |
| 53 | + */ |
| 54 | + private static final double SHORT_SEG_TOLERANCE_FACTOR = 1.0e-10; |
| 55 | + |
| 56 | + private Collection siteCoords; |
48 | 57 | private double tolerance = 0.0; |
49 | 58 | private QuadEdgeSubdivision subdiv = null; |
50 | 59 | private Envelope clipEnv = null; |
@@ -160,39 +169,141 @@ public Geometry getDiagram(GeometryFactory geomFact) |
160 | 169 | create(); |
161 | 170 | Geometry polys = subdiv.getVoronoiDiagram(geomFact); |
162 | 171 |
|
| 172 | + //System.out.println(polys); |
| 173 | + //System.out.println( subdiv.getTriangles(true, geomFact) ); |
| 174 | + |
163 | 175 | /* |
164 | | - System.out.println(polys); |
165 | | - Geometry tris = subdiv.getTriangles(true, geomFact); |
166 | | - System.out.println(tris); |
167 | 176 | if (! subdiv.isFrameDelaunay()) { |
168 | | - throw new IllegalStateException("Triangulation frame is not Delaunay"); |
169 | | - } |
| 177 | + throw new IllegalStateException("Triangulation frame is not Delaunay"); |
| 178 | + } |
170 | 179 | //*/ |
171 | | - |
172 | | - //-- clip polys to diagramEnv |
173 | | - return clipGeometryCollection(polys, diagramEnv); |
174 | | - } |
175 | 180 |
|
| 181 | + Geometry polysClean = clean(polys); |
| 182 | + //Geometry polysClean = polys; // TESTING ONLY |
| 183 | + |
| 184 | + //-- clip cell polygons to diagram boundary |
| 185 | + return clipGeometryCollection(polysClean, diagramEnv); |
| 186 | + } |
| 187 | + |
176 | 188 | private static Geometry clipGeometryCollection(Geometry geom, Envelope clipEnv) |
177 | | - { |
178 | | - Geometry clipPoly = geom.getFactory().toGeometry(clipEnv); |
179 | | - List clipped = new ArrayList(); |
180 | | - for (int i = 0; i < geom.getNumGeometries(); i++) { |
181 | | - Geometry g = geom.getGeometryN(i); |
182 | | - Geometry result = null; |
183 | | - // don't clip unless necessary |
184 | | - if (clipEnv.contains(g.getEnvelopeInternal())) |
185 | | - result = g; |
186 | | - else if (clipEnv.intersects(g.getEnvelopeInternal())) { |
187 | | - result = clipPoly.intersection(g); |
188 | | - // keep vertex key info |
189 | | - result.setUserData(g.getUserData()); |
190 | | - } |
| 189 | + { |
| 190 | + Geometry clipPoly = geom.getFactory().toGeometry(clipEnv); |
| 191 | + List<Geometry> clipped = new ArrayList<Geometry>(); |
| 192 | + for (int i = 0; i < geom.getNumGeometries(); i++) { |
| 193 | + Geometry g = geom.getGeometryN(i); |
| 194 | + Geometry result = null; |
| 195 | + // don't clip unless necessary |
| 196 | + if (clipEnv.contains(g.getEnvelopeInternal())) |
| 197 | + result = g; |
| 198 | + else if (clipEnv.intersects(g.getEnvelopeInternal())) { |
| 199 | + result = clipPoly.intersection(g); |
| 200 | + // keep vertex key info |
| 201 | + result.setUserData(g.getUserData()); |
| 202 | + } |
191 | 203 |
|
192 | | - if (result != null && ! result.isEmpty()) { |
193 | | - clipped.add(result); |
194 | | - } |
| 204 | + if (result != null && ! result.isEmpty()) { |
| 205 | + clipped.add(result); |
| 206 | + } |
| 207 | + } |
| 208 | + return geom.getFactory().createGeometryCollection(GeometryFactory.toGeometryArray(clipped)); |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * Cleans diagram polygons to fix invalid topology caused by robustness errors, |
| 213 | + * |
| 214 | + * @param polys a GeometryCollection containing the raw polygons for the diagram |
| 215 | + * @return the clean polygons |
| 216 | + */ |
| 217 | + private Geometry clean(Geometry polys) { |
| 218 | + /** |
| 219 | + * Check for a diagram polygon with a very short edge which is invalid. |
| 220 | + * This can indicate invalid diagram topology caused by nearly cocircular input points. |
| 221 | + * This is an efficient test which should not trigger on most typical datasets. |
| 222 | + * |
| 223 | + * If found, snap the polygons to fix the topology. |
| 224 | + * This is a heuristic fix, but should generally restore correct topology |
| 225 | + * with very little effect on the diagram geometry. |
| 226 | + * |
| 227 | + * See https://github.com/locationtech/jts/issues/1171 |
| 228 | + */ |
| 229 | + double segmentLenTolerance = SHORT_SEG_TOLERANCE_FACTOR * diagramEnv.getDiameter(); |
| 230 | + if (hasInvalidPolygonWithShortEdge(polys, segmentLenTolerance)) { |
| 231 | + //System.out.println("SNAPPING!"); |
| 232 | + Geometry polysSnap = snap(polys, segmentLenTolerance); |
| 233 | + return polysSnap; |
195 | 234 | } |
196 | | - return geom.getFactory().createGeometryCollection(GeometryFactory.toGeometryArray(clipped)); |
197 | | - } |
| 235 | + return polys; |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Tests for a polygon with a very short edge which is invalid. |
| 240 | + * This check is efficient for valid input, |
| 241 | + * since that is unlikely to contain very short edges. |
| 242 | + * |
| 243 | + * @param polys |
| 244 | + * @param segmentLenTolerance |
| 245 | + * @return true if a short edge in an invalid polygon is found |
| 246 | + */ |
| 247 | + private static boolean hasInvalidPolygonWithShortEdge(Geometry polys, double segmentLenTolerance) { |
| 248 | + for (int i = 0; i < polys.getNumGeometries(); i++) { |
| 249 | + Polygon poly = (Polygon) polys.getGeometryN(i); |
| 250 | + if (hasShortSegment(poly, segmentLenTolerance)) { |
| 251 | + if (! poly.isValid()) |
| 252 | + return true; |
| 253 | + } |
| 254 | + } |
| 255 | + return false; |
| 256 | + } |
| 257 | + |
| 258 | + /** |
| 259 | + * Tests if a polygon shell contains a short edge. |
| 260 | + * |
| 261 | + * @param poly a polygon |
| 262 | + * @param segmentLenTolerance the minimum segment length |
| 263 | + * @return true if the polygon has a short edge |
| 264 | + */ |
| 265 | + private static boolean hasShortSegment(Polygon poly, double segmentLenTolerance) { |
| 266 | + LinearRing ring = poly.getExteriorRing(); |
| 267 | + Coordinate prev = ring.getCoordinateN(0); |
| 268 | + for (int i = 1; i < ring.getNumPoints(); i++) { |
| 269 | + Coordinate p = ring.getCoordinateN(i); |
| 270 | + if (p.distance(prev) < segmentLenTolerance) |
| 271 | + return true; |
| 272 | + prev = p; |
| 273 | + } |
| 274 | + return false; |
| 275 | + } |
| 276 | + |
| 277 | + /** |
| 278 | + * Snaps the vertices of a collection of polygons to eliminate short edges. |
| 279 | + * Using a snapping map for all vertices ensures that adjacent polygons |
| 280 | + * match after snapping. |
| 281 | + * |
| 282 | + * @param polys a GeometryCollection of single-ring polygons |
| 283 | + * @param snapTolerance the snapping tolerance |
| 284 | + * @return a GeometryCollection of snapped polygons |
| 285 | + */ |
| 286 | + private static Geometry snap(Geometry polys, double snapTolerance) { |
| 287 | + SnappingPointIndex snapMap = new SnappingPointIndex(snapTolerance); |
| 288 | + List<Polygon> polysSnap = new ArrayList<Polygon>(); |
| 289 | + for (int i = 0; i < polys.getNumGeometries(); i++) { |
| 290 | + Polygon polySnap = snapPolygon((Polygon) polys.getGeometryN(i), snapMap); |
| 291 | + polysSnap.add(polySnap); |
| 292 | + } |
| 293 | + GeometryFactory geomFact = polys.getFactory(); |
| 294 | + return geomFact.createGeometryCollection(GeometryFactory.toGeometryArray(polysSnap)); |
| 295 | + } |
| 296 | + |
| 297 | + private static Polygon snapPolygon(Polygon poly, SnappingPointIndex snapMap) { |
| 298 | + CoordinateList ptsSnap = new CoordinateList(); |
| 299 | + //-- voronoi polygons do not contain holes |
| 300 | + Coordinate[] pts = poly.getExteriorRing().getCoordinates(); |
| 301 | + for (Coordinate pt : pts) { |
| 302 | + Coordinate snapPt = snapMap.snap(pt); |
| 303 | + ptsSnap.add(snapPt.copy(), false); |
| 304 | + } |
| 305 | + Polygon polySnap = poly.getFactory().createPolygon(ptsSnap.toCoordinateArray()); |
| 306 | + return polySnap; |
| 307 | + } |
| 308 | + |
198 | 309 | } |
0 commit comments