View Javadoc
1   /*******************************************************************************
2    * Copyright (c) 2015 Voyager Search and MITRE
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Apache License, Version 2.0 which
5    * accompanies this distribution and is available at
6    *    http://www.apache.org/licenses/LICENSE-2.0.txt
7    ******************************************************************************/
8   
9   package org.locationtech.spatial4j.shape.jts;
10  
11  import org.locationtech.spatial4j.context.SpatialContext;
12  import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
13  import org.locationtech.spatial4j.distance.CartesianDistCalc;
14  import org.locationtech.spatial4j.exception.InvalidShapeException;
15  import org.locationtech.spatial4j.shape.*;
16  import org.locationtech.spatial4j.shape.Point;
17  import org.locationtech.spatial4j.shape.impl.BBoxCalculator;
18  import org.locationtech.spatial4j.shape.impl.BufferedLineString;
19  import org.locationtech.spatial4j.shape.impl.RectangleImpl;
20  import org.locationtech.jts.geom.*;
21  import org.locationtech.jts.geom.prep.PreparedGeometry;
22  import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
23  import org.locationtech.jts.operation.union.UnaryUnionOp;
24  import org.locationtech.jts.operation.valid.IsValidOp;
25  
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.List;
29  
30  /**
31   * Wraps a JTS {@link Geometry} (i.e. may be a polygon or basically anything).
32   * JTS does a great deal of the hard work, but there is work here in handling
33   * dateline (aka anti-meridian) wrap.
34   */
35  public class JtsGeometry extends BaseShape<JtsSpatialContext> {
36    /** System property boolean that can disable auto validation in an assert. */
37    public static final String SYSPROP_ASSERT_VALIDATE = "spatial4j.JtsGeometry.assertValidate";
38  
39    private final Geometry geom;//cannot be a direct instance of GeometryCollection as it doesn't support relate()
40    private final boolean hasArea;
41    private final Rectangle bbox;
42    protected PreparedGeometry preparedGeometry;
43    protected boolean validated = false;
44  
45    public JtsGeometry(Geometry geom, JtsSpatialContext ctx, boolean dateline180Check, boolean allowMultiOverlap) {
46      super(ctx);
47      //GeometryCollection isn't supported in relate()
48      if (geom.getClass().equals(GeometryCollection.class)) {
49        geom = narrowCollectionIfPossible((GeometryCollection)geom);
50        if (geom == null) {
51          throw new IllegalArgumentException("JtsGeometry does not support GeometryCollection but does support its subclasses.");
52        }
53      }
54  
55      //NOTE: All this logic is fairly expensive. There are some short-circuit checks though.
56      if (geom.isEmpty()) {
57        bbox = new RectangleImpl(Double.NaN, Double.NaN, Double.NaN, Double.NaN, this.ctx);
58      } else if (ctx.isGeo()) {
59        //Unwraps the geometry across the dateline so it exceeds the standard geo bounds (-180 to +180).
60        if (dateline180Check)
61          geom = unwrapDateline(geom);//returns same or new geom
62        //If given multiple overlapping polygons, fix it by union
63        if (allowMultiOverlap)
64          geom = unionGeometryCollection(geom);//returns same or new geom
65  
66        //Cuts an unwrapped geometry back into overlaid pages in the standard geo bounds.
67        geom = cutUnwrappedGeomInto360(geom);//returns same or new geom
68        assert geom.getEnvelopeInternal().getWidth() <= 360;
69        assert ! geom.getClass().equals(GeometryCollection.class) : "GeometryCollection unsupported";//double check
70  
71        //Compute bbox
72        bbox = computeGeoBBox(geom);
73      } else {//not geo
74        //If given multiple overlapping polygons, fix it by union
75        if (allowMultiOverlap)
76          geom = unionGeometryCollection(geom);//returns same or new geom
77  
78        Envelope env = geom.getEnvelopeInternal();
79        bbox = new RectangleImpl(env.getMinX(), env.getMaxX(), env.getMinY(), env.getMaxY(), ctx);
80      }
81      geom.getEnvelopeInternal();//ensure envelope is cached internally, which is lazy evaluated. Keeps this thread-safe.
82  
83      this.geom = geom;
84      assert assertValidate();//kinda expensive but caches valid state
85  
86      this.hasArea = !((geom instanceof Lineal) || (geom instanceof Puntal));
87    }
88  
89    /**
90     * Attempts to retype a geometry collection under the following circumstances, returning
91     * null if the collection can not be retyped.
92     * <ul>
93     *    <li>Single object collections are collapsed down to the object.</li>
94     *    <li>Homogenous collections are recast as the appropriate subclass.</li>
95     * </ul>
96     *
97     * @see GeometryFactory#buildGeometry(Collection)
98     */
99    private Geometry narrowCollectionIfPossible(GeometryCollection gc) {
100     List<Geometry> geoms = new ArrayList<>();
101     for (int i = 0; i < gc.getNumGeometries(); i++) {
102       geoms.add(gc.getGeometryN(i));
103     }
104 
105     Geometry result = gc.getFactory().buildGeometry(geoms);
106     return !result.getClass().equals(GeometryCollection.class) ? result : null;
107   }
108 
109   /** called via assertion */
110   private boolean assertValidate() {
111     String assertValidate = System.getProperty(SYSPROP_ASSERT_VALIDATE);
112     if (assertValidate == null || Boolean.parseBoolean(assertValidate))
113       validate();
114     return true;
115   }
116 
117   /**
118    * Validates the shape, throwing a descriptive error if it isn't valid. Note that this
119    * is usually called automatically by default, but that can be disabled.
120    *
121    * @throws InvalidShapeException with descriptive error if the shape isn't valid
122    */
123   public void validate() throws InvalidShapeException {
124     if (!validated) {
125       IsValidOp isValidOp = new IsValidOp(geom);
126       if (!isValidOp.isValid())
127         throw new InvalidShapeException(isValidOp.getValidationError().toString());
128       validated = true;
129     }
130   }
131 
132   /**
133    * Determines if the shape has been indexed.
134    */
135   boolean isIndexed() {
136     return preparedGeometry != null;
137   }
138 
139   /**
140    * Adds an index to this class internally to compute spatial relations faster. In JTS this
141    * is called a {@link org.locationtech.jts.geom.prep.PreparedGeometry}.  This
142    * isn't done by default because it takes some time to do the optimization, and it uses more
143    * memory.  Calling this method isn't thread-safe so be careful when this is done. If it was
144    * already indexed then nothing happens.
145    */
146   public void index() {
147     if (preparedGeometry == null)
148       preparedGeometry = PreparedGeometryFactory.prepare(geom);
149   }
150 
151   @Override
152   public boolean isEmpty() {
153     return bbox.isEmpty(); // fast
154   }
155 
156   /** Given {@code geoms} which has already been checked for being in world
157    * bounds, return the minimal longitude range of the bounding box.
158    */
159   protected Rectangle computeGeoBBox(Geometry geoms) {
160     final Envelope env = geoms.getEnvelopeInternal();//for minY & maxY (simple)
161     if (ctx.isGeo() && env.getWidth() > 180 && geoms.getNumGeometries() > 1)  {
162       // This is ShapeCollection's bbox algorithm
163       BBoxCalculator bboxCalc = new BBoxCalculator(ctx);
164       for (int i = 0; i < geoms.getNumGeometries(); i++ ) {
165         Envelope envI = geoms.getGeometryN(i).getEnvelopeInternal();
166         bboxCalc.expandXRange(envI.getMinX(), envI.getMaxX());
167         if (bboxCalc.doesXWorldWrap())
168           break; // can't grow any bigger
169       }
170       return new RectangleImpl(bboxCalc.getMinX(), bboxCalc.getMaxX(), env.getMinY(), env.getMaxY(), ctx);
171     } else {
172       return new RectangleImpl(env.getMinX(), env.getMaxX(), env.getMinY(), env.getMaxY(), ctx);
173     }
174   }
175 
176   @Override
177   public JtsGeometry getBuffered(double distance, SpatialContext ctx) {
178     //TODO doesn't work correctly across the dateline. The buffering needs to happen
179     // when it's transiently unrolled, prior to being sliced.
180     return this.ctx.makeShape(geom.buffer(distance), true, true);
181   }
182 
183   @Override
184   public boolean hasArea() {
185     return hasArea;
186   }
187 
188   @Override
189   public double getArea(SpatialContext ctx) {
190     double geomArea = geom.getArea();
191     if (ctx == null || geomArea == 0)
192       return geomArea;
193     //Use the area proportional to how filled the bbox is.
194     double bboxArea = getBoundingBox().getArea(null);//plain 2d area
195     assert bboxArea >= geomArea;
196     double filledRatio = geomArea / bboxArea;
197     return getBoundingBox().getArea(ctx) * filledRatio;
198     // (Future: if we know we use an equal-area projection then we don't need to
199     //  estimate)
200   }
201 
202   @Override
203   public Rectangle getBoundingBox() {
204     return bbox;
205   }
206 
207   @Override
208   public JtsPoint getCenter() {
209     if (isEmpty()) //geom.getCentroid == null
210       return new JtsPoint(ctx.getGeometryFactory().createPoint((Coordinate)null), ctx);
211     return new JtsPoint(geom.getCentroid(), ctx);
212   }
213 
214   @Override
215   public SpatialRelation relate(Shape other) {
216     if (other instanceof Point)
217       return relate((Point)other);
218     else if (other instanceof Rectangle)
219       return relate((Rectangle) other);
220     else if (other instanceof Circle)
221       return relate((Circle) other);
222     else if (other instanceof JtsGeometry)
223       return relate((JtsGeometry) other);
224     else if (other instanceof BufferedLineString)
225       throw new UnsupportedOperationException("Can't use BufferedLineString with JtsGeometry");
226     return other.relate(this).transpose();
227   }
228 
229   public SpatialRelation relate(Point pt) {
230     if (!getBoundingBox().relate(pt).intersects())
231       return SpatialRelation.DISJOINT;
232     Geometry ptGeom;
233     if (pt instanceof JtsPoint)
234       ptGeom = ((JtsPoint)pt).getGeom();
235     else
236       ptGeom = ctx.getGeometryFactory().createPoint(new Coordinate(pt.getX(), pt.getY()));
237     return relate(ptGeom);//is point-optimized
238   }
239 
240   public SpatialRelation relate(Rectangle rectangle) {
241     SpatialRelation bboxR = bbox.relate(rectangle);
242     if (bboxR == SpatialRelation.WITHIN || bboxR == SpatialRelation.DISJOINT)
243       return bboxR;
244     // FYI, the right answer could still be DISJOINT or WITHIN, but we don't know yet.
245     return relate(ctx.getGeometryFrom(rectangle));
246   }
247 
248   public SpatialRelation relate(final Circle circle) {
249     SpatialRelation bboxR = bbox.relate(circle);
250     if (bboxR == SpatialRelation.WITHIN || bboxR == SpatialRelation.DISJOINT)
251       return bboxR;
252     // The result could be anything still.
253 
254     final SpatialRelation[] result = {null};
255     // Visit each geometry (this geom might contain others).
256     geom.apply(new GeometryFilter() {
257 
258       // We use cartesian math.  It's a limitation/assumption when working with JTS.  When geo=true (i.e. we're using
259       //   WGS84 instead of a projected coordinate system), the errors here will be pretty terrible east-west.  At
260       //   60 degrees latitude, the circle will work as if it has half the width it should.
261       //   Instead, consider converting the circle to a polygon first (not great but better), or projecting both first.
262       //   Ideally, use Geo3D.
263       final CartesianDistCalc calcSqd = CartesianDistCalc.INSTANCE_SQUARED;
264       final double radiusSquared = circle.getRadius() * circle.getRadius();
265       final Geometry ctrGeom = ctx.getGeometryFrom(circle.getCenter());
266 
267       @Override
268       public void filter(Geometry geom) {
269         if (result[0] == SpatialRelation.INTERSECTS || result[0] == SpatialRelation.CONTAINS) {
270           // a previous filter(geom) call had a result that won't be changed no matter how this geom relates
271           return;
272         }
273 
274         if (geom instanceof Polygon) {
275           Polygon polygon = (Polygon) geom;
276           SpatialRelation rel = relateEnclosedRing((LinearRing) polygon.getExteriorRing());
277           // if rel == INTERSECTS or WITHIN or DISJOINT; done.  But CONTAINS...
278           if (rel == SpatialRelation.CONTAINS) {
279             // if the poly outer ring contains the circle, check the holes. Could become DISJOINT or INTERSECTS.
280             HOLE_LOOP: for (int i = 0; i < polygon.getNumInteriorRing(); i++){
281               // TODO short-circuit based on the hole's bbox if it's disjoint or within the circle.
282               switch (relateEnclosedRing((LinearRing) polygon.getInteriorRingN(i))) {
283                 case WITHIN:// fall through
284                 case INTERSECTS:
285                   rel = SpatialRelation.INTERSECTS;
286                   break HOLE_LOOP;
287                 case CONTAINS:
288                   rel = SpatialRelation.DISJOINT;
289                   break HOLE_LOOP;
290                 //case DISJOINT: break; // continue hole loop
291               }
292             }
293           }
294           result[0] = rel.combine(result[0]);
295         } else if (geom instanceof LineString) {
296           LineString lineString = (LineString) geom;
297           SpatialRelation rel = relateLineString(lineString);
298           result[0] = rel.combine(result[0]);
299         } else if (geom instanceof org.locationtech.jts.geom.Point) {
300           org.locationtech.jts.geom.Point point = (org.locationtech.jts.geom.Point) geom;
301           SpatialRelation rel =
302                   calcSqd.distance(circle.getCenter(), point.getX(), point.getY()) > radiusSquared
303                           ? SpatialRelation.DISJOINT : SpatialRelation.WITHIN;
304           result[0] = rel.combine(result[0]);
305         }
306         // else it's going to be some GeometryCollection and we'll visit the contents.
307       }
308 
309       /** As if the ring is the outer ring of a polygon */
310       SpatialRelation relateEnclosedRing(LinearRing ring) {
311         SpatialRelation rel = relateLineString(ring);
312         if (rel == SpatialRelation.DISJOINT
313                 && ctx.getGeometryFactory().createPolygon(ring, null).contains(ctrGeom)) {
314           // If it contains the circle center point, then the result is CONTAINS
315           rel = SpatialRelation.CONTAINS;
316         }
317         return rel;
318       }
319 
320       SpatialRelation relateLineString(LineString lineString) {
321         final CoordinateSequence seq = lineString.getCoordinateSequence();
322         final boolean isRing = lineString instanceof LinearRing;
323         int numOutside = 0;
324         // Compare the coordinates:
325         for (int i = 0, numComparisons = 0; i < seq.size(); i++) {
326           if (i == 0 && isRing) {
327             continue;
328           }
329           numComparisons++;
330           boolean outside = calcSqd.distance(circle.getCenter(), seq.getX(i), seq.getY(i)) > radiusSquared;
331           if (outside) {
332             numOutside++;
333           }
334           // If the comparisons have a mix of outside/inside, then we can short-circuit INTERSECTS.
335           if (numComparisons != numOutside && numOutside != 0) {
336             assert numComparisons > 1;
337             return SpatialRelation.INTERSECTS;
338           }
339         }
340         // Either all vertices are outside or inside, by this stage.
341         if (numOutside == 0) { // all inside
342           return SpatialRelation.WITHIN.combine(result[0]);
343         }
344         // They are all outside.
345         // Check the edges (line segments) to see if any are inside.
346         for (int i = 1; i < seq.size(); i++) {
347           boolean outside = calcSqd.distanceToLineSegment(
348                   circle.getCenter(), seq.getX(i-1), seq.getY(i-1), seq.getX(i), seq.getY(i))
349                   > radiusSquared;
350           if (!outside) {
351             return SpatialRelation.INTERSECTS;
352           }
353         }
354         return SpatialRelation.DISJOINT;
355       }
356     });
357 
358     return result[0] == null ? SpatialRelation.DISJOINT : result[0];
359   }
360 
361   public SpatialRelation relate(JtsGeometry jtsGeometry) {
362     //don't bother checking bbox since geom.relate() does this already
363     return relate(jtsGeometry.geom);
364   }
365 
366   protected SpatialRelation relate(Geometry oGeom) {
367     //see http://docs.geotools.org/latest/userguide/library/jts/dim9.html#preparedgeometry
368     if (oGeom instanceof org.locationtech.jts.geom.Point) {
369       if (preparedGeometry != null)
370         return preparedGeometry.disjoint(oGeom) ? SpatialRelation.DISJOINT : SpatialRelation.CONTAINS;
371       return geom.disjoint(oGeom) ? SpatialRelation.DISJOINT : SpatialRelation.CONTAINS;
372     }
373     if (preparedGeometry == null)
374       return intersectionMatrixToSpatialRelation(geom.relate(oGeom));
375     else if (preparedGeometry.covers(oGeom))
376       return SpatialRelation.CONTAINS;
377     else if (preparedGeometry.coveredBy(oGeom))
378       return SpatialRelation.WITHIN;
379     else if (preparedGeometry.intersects(oGeom))
380       return SpatialRelation.INTERSECTS;
381     return SpatialRelation.DISJOINT;
382   }
383 
384   public static SpatialRelation intersectionMatrixToSpatialRelation(IntersectionMatrix matrix) {
385     //As indicated in SpatialRelation javadocs, Spatial4j CONTAINS & WITHIN are
386     // OGC's COVERS & COVEREDBY
387     if (matrix.isCovers())
388       return SpatialRelation.CONTAINS;
389     else if (matrix.isCoveredBy())
390       return SpatialRelation.WITHIN;
391     else if (matrix.isDisjoint())
392       return SpatialRelation.DISJOINT;
393     return SpatialRelation.INTERSECTS;
394   }
395 
396   @Override
397   public String toString() {
398     return geom.toString();
399   }
400 
401   @Override
402   public boolean equals(Object o) {
403     if (this == o) return true;
404     if (o == null || getClass() != o.getClass()) return false;
405     JtsGeometry that = (JtsGeometry) o;
406     return geom.equalsExact(that.geom);//fast equality for normalized geometries
407   }
408 
409   @Override
410   public int hashCode() {
411     //FYI if geometry.equalsExact(that.geometry), then their envelopes are the same.
412     return geom.getEnvelopeInternal().hashCode();
413   }
414 
415   public Geometry getGeom() {
416     return geom;
417   }
418 
419   /**
420    * If <code>geom</code> spans the dateline (aka anti-meridian), then this modifies it to be a
421    * valid JTS geometry that extends to the right of the standard -180 to +180
422    * width such that some points are greater than +180 but some remain less.
423    *
424    * @return The same geometry or a new one if it was unwrapped
425    */
426   private static Geometry unwrapDateline(Geometry geom) {
427     if (geom.getEnvelopeInternal().getWidth() < 180)
428       return geom;//can't possibly cross the dateline
429 
430     // if a multi-geom:  (this is purely an optimization to avoid cloning more than we need to)
431     if (geom instanceof GeometryCollection) {
432       if (geom instanceof MultiPoint) {
433         return geom; // always safe since no point crosses the dateline (on it is okay)
434       }
435       GeometryCollection gc = (GeometryCollection) geom;
436       List<Geometry> list = new ArrayList<>(gc.getNumGeometries());
437       boolean didUnwrap = false;
438       for (int n = 0; n < gc.getNumGeometries(); n++) {
439         Geometry geometryN = gc.getGeometryN(n);
440         Geometry geometryUnwrapped = unwrapDateline(geometryN); // recursion
441         list.add(geometryUnwrapped);
442         didUnwrap |= (geometryUnwrapped != geometryN);
443       }
444       return !didUnwrap ? geom : geom.getFactory().buildGeometry(list);
445     }
446 
447     // a geom (not multi):
448 
449     Geometry newGeom = geom.copy(); // clone
450 
451     final int[] crossings = {0};//an array so that an inner class can modify it.
452     newGeom.apply(new GeometryFilter() {
453       @Override
454       public void filter(Geometry geom) {
455         int cross;
456         if (geom instanceof LineString) {//note: LinearRing extends LineString
457           if (geom.getEnvelopeInternal().getWidth() < 180)
458             return;//can't possibly cross the dateline
459           cross = unwrapDateline((LineString) geom);
460         } else if (geom instanceof Polygon) {
461           if (geom.getEnvelopeInternal().getWidth() < 180)
462             return;//can't possibly cross the dateline
463           cross = unwrapDateline((Polygon) geom);
464         } else {
465           // The only other JTS subclass of Geometry is a Point, which can't cross anything.
466           //  If the geom is something custom, we don't know what else to do but return.
467           return;
468         }
469         crossings[0] = Math.max(crossings[0], cross);
470       }
471     });//geom.apply()
472 
473     if (crossings[0] > 0) {
474       newGeom.geometryChanged();
475       return newGeom;
476     } else {
477       return geom; // original
478     }
479   }
480 
481   /** See {@link #unwrapDateline(Geometry)}. */
482   private static int unwrapDateline(Polygon poly) {
483     LineString exteriorRing = poly.getExteriorRing();
484     int cross = unwrapDateline(exteriorRing);
485     if (cross > 0) {
486       //TODO TEST THIS! Maybe bug if doesn't cross but is in another page?
487       for(int i = 0; i < poly.getNumInteriorRing(); i++) {
488         LineString innerLineString = poly.getInteriorRingN(i);
489         unwrapDateline(innerLineString);
490         for(int shiftCount = 0; ! exteriorRing.contains(innerLineString); shiftCount++) {
491           if (shiftCount > cross)
492             throw new IllegalArgumentException("The inner ring doesn't appear to be within the exterior: "
493                 +exteriorRing+" inner: "+innerLineString);
494           shiftGeomByX(innerLineString, 360);
495         }
496       }
497     }
498     return cross;
499   }
500 
501   /** See {@link #unwrapDateline(Geometry)}. */
502   private static int unwrapDateline(LineString lineString) {
503     CoordinateSequence cseq = lineString.getCoordinateSequence();
504     int size = cseq.size();
505     if (size <= 1)
506       return 0;
507 
508     int shiftX = 0;//invariant: == shiftXPage*360
509     int shiftXPage = 0;
510     int shiftXPageMin = 0/* <= 0 */, shiftXPageMax = 0; /* >= 0 */
511     double prevX = cseq.getX(0);
512     for(int i = 1; i < size; i++) {
513       double thisX_orig = cseq.getX(i);
514       assert thisX_orig >= -180 && thisX_orig <= 180 : "X not in geo bounds";
515       double thisX = thisX_orig + shiftX;
516       if (prevX - thisX > 180) {//cross dateline from left to right
517         thisX += 360;
518         shiftX += 360;
519         shiftXPage += 1;
520         shiftXPageMax = Math.max(shiftXPageMax,shiftXPage);
521       } else if (thisX - prevX > 180) {//cross dateline from right to left
522         thisX -= 360;
523         shiftX -= 360;
524         shiftXPage -= 1;
525         shiftXPageMin = Math.min(shiftXPageMin,shiftXPage);
526       }
527       if (shiftXPage != 0)
528         cseq.setOrdinate(i, CoordinateSequence.X, thisX);
529       prevX = thisX;
530     }
531     if (lineString instanceof LinearRing) {
532       assert cseq.getCoordinate(0).equals(cseq.getCoordinate(size-1));
533       assert shiftXPage == 0;//starts and ends at 0
534     }
535     assert shiftXPageMax >= 0 && shiftXPageMin <= 0;
536     //Unfortunately we are shifting again; it'd be nice to be smarter and shift once
537     shiftGeomByX(lineString, shiftXPageMin * -360);
538     int crossings = shiftXPageMax - shiftXPageMin;
539     return crossings;
540   }
541 
542   private static void shiftGeomByX(Geometry geom, final int xShift) {
543     if (xShift == 0)
544       return;
545     geom.apply(new CoordinateSequenceFilter() {
546       @Override
547       public void filter(CoordinateSequence seq, int i) {
548         seq.setOrdinate(i, CoordinateSequence.X, seq.getX(i) + xShift );
549       }
550 
551       @Override public boolean isDone() { return false; }
552 
553       @Override public boolean isGeometryChanged() { return true; }
554     });
555   }
556 
557   private static Geometry unionGeometryCollection(Geometry geom) {
558     if (geom instanceof GeometryCollection) {
559       return geom.union();
560     }
561     return geom;
562   }
563 
564   /**
565    * This "pages" through standard geo boundaries offset by multiples of 360
566    * longitudinally that intersect geom, and the intersecting results of a page
567    * and the geom are shifted into the standard -180 to +180 and added to a new
568    * geometry that is returned.
569    */
570   private static Geometry cutUnwrappedGeomInto360(Geometry geom) {
571     Envelope geomEnv = geom.getEnvelopeInternal();
572     if (geomEnv.getMinX() >= -180 && geomEnv.getMaxX() <= 180)
573       return geom;
574     assert geom.isValid() : "geom";
575 
576     List<Geometry> geomList = new ArrayList<>();
577     //page 0 is the standard -180 to 180 range
578     int startPage = (int) Math.floor((geomEnv.getMinX() + 180) / 360);
579     for (int page = startPage; true; page++) {
580       double minX = -180 + page * 360;
581       if (geomEnv.getMaxX() <= minX)
582         break;
583       Geometry rect = geom.getFactory().toGeometry(new Envelope(minX, minX + 360, -90, 90));
584       assert rect.isValid() : "rect";
585       Geometry pageGeom = rect.intersection(geom);//JTS is doing some hard work
586       assert pageGeom.isValid() : "pageGeom";
587 
588       if (page != 0) {
589         pageGeom = pageGeom.copy(); // because shiftGeomByX modifies the underlying coordinates shared by geom.
590         shiftGeomByX(pageGeom, page * -360);
591       }
592       geomList.add(pageGeom);
593     }
594     return UnaryUnionOp.union(geomList);
595   }
596 
597 //  private static Geometry removePolyHoles(Geometry geom) {
598 //    //TODO this does a deep copy of geom even if no changes needed; be smarter
599 //    GeometryTransformer gTrans = new GeometryTransformer() {
600 //      @Override
601 //      protected Geometry transformPolygon(Polygon geom, Geometry parent) {
602 //        if (geom.getNumInteriorRing() == 0)
603 //          return geom;
604 //        return factory.createPolygon((LinearRing) geom.getExteriorRing(),null);
605 //      }
606 //    };
607 //    return gTrans.transform(geom);
608 //  }
609 //
610 //  private static Geometry snapAndClean(Geometry geom) {
611 //    return new GeometrySnapper(geom).snapToSelf(GeometrySnapper.computeOverlaySnapTolerance(geom), true);
612 //  }
613 }