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.DatelineRule;
13  import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
14  import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
15  import org.locationtech.spatial4j.context.jts.ValidationRule;
16  import org.locationtech.spatial4j.exception.InvalidShapeException;
17  import org.locationtech.spatial4j.io.ShapeReader;
18  import org.locationtech.spatial4j.shape.Circle;
19  import org.locationtech.spatial4j.shape.Point;
20  import org.locationtech.spatial4j.shape.Rectangle;
21  import org.locationtech.spatial4j.shape.Shape;
22  import org.locationtech.spatial4j.shape.impl.ShapeFactoryImpl;
23  import org.locationtech.jts.algorithm.CGAlgorithms;
24  import org.locationtech.jts.geom.*;
25  import org.locationtech.jts.util.GeometricShapeFactory;
26  
27  import java.util.ArrayList;
28  import java.util.Collection;
29  import java.util.List;
30  
31  /**
32   * Enhances {@link ShapeFactoryImpl} with support for Polygons
33   * using <a href="https://sourceforge.net/projects/jts-topo-suite/">JTS</a>.
34   * To the extent possible, our {@link JtsGeometry} adds some amount of geodetic support over
35   * vanilla JTS which only has a Euclidean (flat plane) model.
36   */
37  public class JtsShapeFactory extends ShapeFactoryImpl {
38  
39    protected static final LinearRing[] EMPTY_HOLES = new LinearRing[0];
40  
41    protected final GeometryFactory geometryFactory;
42  
43    protected final boolean allowMultiOverlap;
44    protected final boolean useJtsPoint;
45    protected final boolean useJtsLineString;
46    protected final boolean useJtsMulti;
47    protected final DatelineRule datelineRule;
48    protected final ValidationRule validationRule;
49    protected final boolean autoIndex;
50  
51    /**
52     * Called by {@link org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory#newSpatialContext()}.
53     */
54    public JtsShapeFactory(JtsSpatialContext ctx, JtsSpatialContextFactory factory) {
55      super(ctx, factory);
56      this.geometryFactory = factory.getGeometryFactory();
57  
58      this.allowMultiOverlap = factory.allowMultiOverlap;
59      this.useJtsPoint = factory.useJtsPoint;
60      this.useJtsLineString = factory.useJtsLineString;
61      this.useJtsMulti = factory.useJtsMulti;
62      this.datelineRule = factory.datelineRule;
63      this.validationRule = factory.validationRule;
64      this.autoIndex = factory.autoIndex;
65    }
66  
67    /**
68     * If geom might be a multi geometry of some kind, then might multiple
69     * component geometries overlap? Strict OGC says this is invalid but we
70     * can accept it by computing the union. Note: Our ShapeCollection mostly
71     * doesn't care but it has a method related to this
72     * {@link org.locationtech.spatial4j.shape.ShapeCollection#relateContainsShortCircuits()}.
73     */
74    public boolean isAllowMultiOverlap() {
75      return allowMultiOverlap;
76    }
77  
78    /**
79     * Returns the rule used to handle geometry objects that have dateline (aka anti-meridian) crossing considerations.
80     */
81    public DatelineRule getDatelineRule() {
82      return datelineRule;
83    }
84  
85    /**
86     * Returns the rule used to handle errors when creating a JTS {@link Geometry}, particularly after it has been
87     * read from one of the {@link ShapeReader}s.
88     */
89    public ValidationRule getValidationRule() {
90      return validationRule;
91    }
92  
93    /**
94     * If JtsGeometry shapes should be automatically "prepared" (i.e. optimized) when read via from a {@link ShapeReader}.
95     *
96     * @see org.locationtech.spatial4j.shape.jts.JtsGeometry#index()
97     */
98    public boolean isAutoIndex() {
99      return autoIndex;
100   }
101 
102   @Override
103   public double normX(double x) {
104     x = super.normX(x);
105     return geometryFactory.getPrecisionModel().makePrecise(x);
106   }
107 
108   @Override
109   public double normY(double y) {
110     y = super.normY(y);
111     return geometryFactory.getPrecisionModel().makePrecise(y);
112   }
113 
114   @Override
115   public double normZ(double z) {
116     z = super.normZ(z);
117     return geometryFactory.getPrecisionModel().makePrecise(z);
118   }
119 
120   @Override
121   public double normDist(double d) {
122     return geometryFactory.getPrecisionModel().makePrecise(d);
123   }
124 
125   /**
126    * Gets a JTS {@link Geometry} for the given {@link Shape}. Some shapes hold a
127    * JTS geometry whereas new ones must be created for the rest.
128    * @param shape Not null
129    * @return Not null
130    */
131   public Geometry getGeometryFrom(Shape shape) {
132     if (shape instanceof JtsGeometry) {
133       return ((JtsGeometry)shape).getGeom();
134     }
135     if (shape instanceof JtsPoint) {
136       return ((JtsPoint) shape).getGeom();
137     }
138     if (shape instanceof Point) {
139       Point point = (Point) shape;
140       return geometryFactory.createPoint(new Coordinate(point.getX(),point.getY()));
141     }
142     if (shape instanceof Rectangle) {
143       Rectangle r = (Rectangle)shape;
144       if (r.getCrossesDateLine()) {
145         Collection<Geometry> pair = new ArrayList<>(2);
146         pair.add(geometryFactory.toGeometry(new Envelope(
147                 r.getMinX(), ctx.getWorldBounds().getMaxX(), r.getMinY(), r.getMaxY())));
148         pair.add(geometryFactory.toGeometry(new Envelope(
149                 ctx.getWorldBounds().getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY())));
150         return geometryFactory.buildGeometry(pair);//a MultiPolygon or MultiLineString
151       } else {
152         return geometryFactory.toGeometry(new Envelope(r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY()));
153       }
154     }
155     if (shape instanceof Circle) {
156       // FYI Some interesting code for this is here:
157       //  http://docs.codehaus.org/display/GEOTDOC/01+How+to+Create+a+Geometry#01HowtoCreateaGeometry-CreatingaCircle
158       //TODO This should ideally have a geodetic version
159       Circle circle = (Circle)shape;
160       GeometricShapeFactory gsf = new GeometricShapeFactory(geometryFactory);
161       gsf.setWidth(circle.getBoundingBox().getWidth());
162       gsf.setHeight(circle.getBoundingBox().getHeight());
163       gsf.setNumPoints(4*25);//multiple of 4 is best
164       gsf.setCentre(new Coordinate(circle.getCenter().getX(), circle.getCenter().getY()));
165       Geometry geom = gsf.createCircle();
166       if (circle.getBoundingBox().getCrossesDateLine())
167         // wrap the geometry in a JtsGeometry to handle date line wrapping
168         geom = new JtsGeometry(geom, (JtsSpatialContext) getSpatialContext(),false, false).getGeom();
169       return geom;
170     }
171     //TODO add BufferedLineString
172     throw new InvalidShapeException("can't make Geometry from: " + shape);
173   }
174 
175   /** Should {@link #pointXY(double, double)} return {@link JtsPoint}? */
176   public boolean useJtsPoint() {
177     return useJtsPoint;
178   }
179 
180   @Override
181   public Point pointXY(double x, double y) {
182     return pointXYZ(x, y, Coordinate.NULL_ORDINATE);
183   }
184 
185   @Override
186   public Point pointXYZ(double x, double y, double z) {
187     if (!useJtsPoint())
188       return super.pointXY(x, y);// ignore z
189     //A Jts Point is fairly heavyweight!  TODO could/should we optimize this? SingleCoordinateSequence
190     verifyX(x);
191     verifyY(y);
192     verifyZ(z);
193     // verifyZ(z)?
194     Coordinate coord = Double.isNaN(x) ? null : new Coordinate(x, y, z);
195     return new JtsPoint(geometryFactory.createPoint(coord), (JtsSpatialContext) ctx);
196   }
197 
198   /** Should {@link #lineString(java.util.List,double)} return {@link JtsGeometry}? */
199   public boolean useJtsLineString() {
200     //BufferedLineString doesn't yet do dateline cross, and can't yet be relate()'ed with a
201     // JTS geometry
202     return useJtsLineString;
203   }
204 
205   @Override
206   public Shape lineString(List<Point> points, double bufferDistance) {
207     if (!useJtsLineString())
208       return super.lineString(points, bufferDistance);
209     //convert List<Point> to Coordinate[]
210     Coordinate[] coords = new Coordinate[points.size()];
211     for (int i = 0; i < coords.length; i++) {
212       Point p = points.get(i);
213       if (p instanceof JtsPoint) {
214         JtsPoint jtsPoint = (JtsPoint) p;
215         coords[i] = jtsPoint.getGeom().getCoordinate();
216       } else {
217         coords[i] = new Coordinate(p.getX(), p.getY());
218       }
219     }
220     JtsGeometry shape = makeShape(geometryFactory.createLineString(coords));
221     if(bufferDistance!=0) {
222       return shape.getBuffered(bufferDistance, ctx);
223     }
224     return shape;
225   }
226 
227   @Override
228   public LineStringBuilder lineString() {
229     if (!useJtsLineString())
230       return super.lineString();
231     return new JtsLineStringBuilder();
232   }
233 
234   private class JtsLineStringBuilder extends CoordinatesAccumulator<JtsLineStringBuilder>
235           implements LineStringBuilder {
236     protected double bufDistance;
237 
238     public JtsLineStringBuilder() {
239     }
240 
241     @Override
242     public LineStringBuilder buffer(double distance) {
243       this.bufDistance = distance;
244       return this;
245     }
246 
247     @Override
248     public Shape build() {
249       Geometry geom = buildLineStringGeom();
250       if (bufDistance != 0.0) {
251         geom = geom.buffer(bufDistance);
252       }
253       return makeShape(geom);
254     }
255 
256     LineString buildLineStringGeom() {
257       return geometryFactory.createLineString(getCoordsArray());
258     }
259   }
260 
261   @Override
262   public PolygonBuilder polygon() {
263     return new JtsPolygonBuilder();
264   }
265 
266   private class JtsPolygonBuilder extends CoordinatesAccumulator<JtsPolygonBuilder>
267           implements PolygonBuilder {
268 
269     List<LinearRing> holes;// lazy instantiated
270 
271     @Override
272     public JtsHoleBuilder hole() {
273       return new JtsHoleBuilder();
274     }
275 
276     private class JtsHoleBuilder extends CoordinatesAccumulator<JtsHoleBuilder>
277             implements PolygonBuilder.HoleBuilder {
278 
279       @Override
280       public JtsPolygonBuilder endHole() {
281         LinearRing linearRing = geometryFactory.createLinearRing(getCoordsArray());
282         if (JtsPolygonBuilder.this.holes == null) {
283           JtsPolygonBuilder.this.holes = new ArrayList<>(4);//short
284         }
285         JtsPolygonBuilder.this.holes.add(linearRing);
286         return JtsPolygonBuilder.this;
287       }
288     }
289 
290     @Override
291     public Shape build() {
292       return makeShapeFromGeometry(buildPolygonGeom());
293     }
294 
295     @Override
296     public Shape buildOrRect() {
297       Polygon geom = buildPolygonGeom();
298       if (geom.isRectangle()) {
299         return makeRectFromRectangularPoly(geom);
300       }
301       return makeShapeFromGeometry(geom);
302     }
303 
304     Polygon buildPolygonGeom() {
305       LinearRing outerRing = geometryFactory.createLinearRing(getCoordsArray());
306       LinearRing[] holeRings = holes == null ? EMPTY_HOLES : holes.toArray(new LinearRing[this.holes.size()]);
307       return geometryFactory.createPolygon(outerRing, holeRings);
308     }
309 
310   } // class JtsPolygonBuilder
311 
312   private abstract class CoordinatesAccumulator<T extends CoordinatesAccumulator> {
313     protected List<Coordinate> coordinates = new ArrayList<>();
314 
315     public T pointXY(double x, double y) {
316       return pointXYZ(x, y, Coordinate.NULL_ORDINATE);
317     }
318 
319     public T pointXYZ(double x, double y, double z) {
320       verifyX(x);
321       verifyY(y);
322       coordinates.add(new Coordinate(x, y, z));
323       return getThis();
324     }
325 
326     // TODO would be be useful to add other ways of providing points?  e.g. point(Coordinate)?
327 
328     // TODO consider wrapping the List<Coordinate> in a custom CoordinateSequence and then (conditionally) use
329     //  geometryFactory's coordinateSequenceFactory to create a new CS if configured to do so.
330     //  Also consider instead natively storing the double[] and then auto-expanding on pointXY* as needed.
331     protected Coordinate[] getCoordsArray() {
332       return coordinates.toArray(new Coordinate[coordinates.size()]);
333     }
334 
335     @SuppressWarnings("unchecked")
336     protected T getThis() { return (T) this; }
337   }
338 
339   /** Whether {@link #multiPoint()}, {@link #multiLineString()}, and {@link #multiPolygon()} should all use JTS's
340    * subclasses of {@link GeometryCollection} instead of Spatial4j's basic impl.  The general {@link #multiShape(Class)}
341    * will never use {@link GeometryCollection} because that class doesn't support relations. */
342   public boolean useJtsMulti() {
343     return useJtsMulti;
344   }
345 
346   @Override
347   public MultiPointBuilder multiPoint() {
348     if (!useJtsMulti) {
349       return super.multiPoint();
350     }
351     return new JtsMultiPointBuilder();
352   }
353 
354   private class JtsMultiPointBuilder extends CoordinatesAccumulator<JtsMultiPointBuilder> implements MultiPointBuilder {
355     @Override
356     public Shape build() {
357       return makeShape(geometryFactory.createMultiPoint(getCoordsArray()));
358     }
359   }
360 
361   @Override
362   public MultiLineStringBuilder multiLineString() {
363     if (!useJtsMulti) {
364       return super.multiLineString();
365     }
366     return new JtsMultiLineStringBuilder();
367   }
368 
369   private class JtsMultiLineStringBuilder implements MultiLineStringBuilder {
370     List<LineString> geoms = new ArrayList<>();
371 
372     @Override
373     public LineStringBuilder lineString() {
374       return new JtsLineStringBuilder();
375     }
376 
377     @Override
378     public MultiLineStringBuilder add(LineStringBuilder lineStringBuilder) {
379       geoms.add(((JtsLineStringBuilder)lineStringBuilder).buildLineStringGeom());
380       return this;
381     }
382 
383     @Override
384     public Shape build() {
385       return makeShape(geometryFactory.createMultiLineString(geoms.toArray(new LineString[geoms.size()])));
386     }
387   }
388 
389   @Override
390   public MultiPolygonBuilder multiPolygon() {
391     if (!useJtsMulti) {
392       return super.multiPolygon();
393     }
394     return new JtsMultiPolygonBuilder();
395   }
396 
397   private class JtsMultiPolygonBuilder implements MultiPolygonBuilder {
398     List<Polygon> geoms = new ArrayList<>();
399 
400     @Override
401     public PolygonBuilder polygon() {
402       return new JtsPolygonBuilder();
403     }
404 
405     @Override
406     public MultiPolygonBuilder add(PolygonBuilder polygonBuilder) {
407       geoms.add(((JtsPolygonBuilder)polygonBuilder).buildPolygonGeom());
408       return this;
409     }
410 
411     @Override
412     public Shape build() {
413       return makeShape(geometryFactory.createMultiPolygon(geoms.toArray(new Polygon[geoms.size()])));
414     }
415   }
416 
417   @Override
418   public <T extends Shape> MultiShapeBuilder<T> multiShape(Class<T> shapeClass) {
419     if (!useJtsMulti()) {
420       return super.multiShape(shapeClass);
421     }
422     return new JtsMultiShapeBuilder<>();
423   }
424 
425   // TODO: once we have typed shapes for Polygons & LineStrings, this logic could move to the superclass
426   // (not JTS specific) and the multi* builders could take a Shape
427   private class JtsMultiShapeBuilder<T extends Shape> extends GeneralShapeMultiShapeBuilder<T> {
428     @Override
429     public Shape build() {
430       Class<?> last = null;
431       List<Geometry> geoms = new ArrayList<>(shapes.size());
432       for(Shape s : shapes) {
433         if (last != null && last != s.getClass()) {
434           return super.build();
435         }
436         if (s instanceof JtsGeometry) {
437           geoms.add(((JtsGeometry)s).getGeom());
438         } else if (s instanceof JtsPoint) {
439           geoms.add(((JtsPoint)s).getGeom());
440         } else {
441           return super.build();
442         }
443         last = s.getClass();
444       }
445 
446       return makeShapeFromGeometry(geometryFactory.buildGeometry(geoms));
447     }
448   }
449 
450   /**
451    * INTERNAL Usually creates a JtsGeometry, potentially validating, repairing, and indexing ("preparing"). This method
452    * is intended for use by {@link ShapeReader} instances.
453    *
454    * If given a direct instance of {@link GeometryCollection} then it's contents will be
455    * recursively converted and then the resulting list will be passed to
456    * {@link SpatialContext#makeCollection(List)} and returned.
457    *
458    * If given a {@link org.locationtech.jts.geom.Point} then {@link SpatialContext#makePoint(double, double)}
459    * is called, which will return a {@link JtsPoint} if {@link JtsSpatialContext#useJtsPoint()}; otherwise
460    * a standard Spatial4j Point is returned.
461    *
462    * If given a {@link LineString} and if {@link JtsSpatialContext#useJtsLineString()} is true then
463    * then the geometry's parts are exposed to call {@link SpatialContext#makeLineString(List)}.
464    */
465   // TODO should this be called always (consistent but sometimes not needed?)
466   //   v.s. only from a ShapeReader (pre-ShapeFactory behavior)
467   public Shape makeShapeFromGeometry(Geometry geom) {
468     if (geom instanceof GeometryCollection) {
469       // Direct instances of GeometryCollection can't be wrapped in JtsGeometry but can be expanded into
470       //  a ShapeCollection.
471       if (!useJtsMulti || geom.getClass() == GeometryCollection.class) {
472         List<Shape> shapes = new ArrayList<>(geom.getNumGeometries());
473         for (int i = 0; i < geom.getNumGeometries(); i++) {
474           Geometry geomN = geom.getGeometryN(i);
475           shapes.add(makeShapeFromGeometry(geomN));//recursion
476         }
477         return multiShape(shapes);
478       }
479     } else if (geom instanceof org.locationtech.jts.geom.Point) {
480       org.locationtech.jts.geom.Point pt = (org.locationtech.jts.geom.Point) geom;
481       if (pt.isEmpty()) {
482         return pointXY(Double.NaN, Double.NaN);
483       } else {
484         return pointXY(pt.getX(), pt.getY());
485       }
486     } else if (geom instanceof LineString) {
487       if (!useJtsLineString()) {
488         LineString lineString = (LineString) geom;
489         List<Point> points = new ArrayList<>(lineString.getNumPoints());
490         for (int i = 0; i < lineString.getNumPoints(); i++) {
491           Coordinate coord = lineString.getCoordinateN(i);
492           points.add(pointXY(coord.x, coord.y));
493         }
494         return lineString(points, 0);
495       }
496     }
497 
498     JtsGeometry jtsGeom;
499     try {
500       jtsGeom = makeShape(geom);
501       if (getValidationRule() != ValidationRule.none)
502         jtsGeom.validate();
503     } catch (RuntimeException e) {
504       // repair:
505       if (getValidationRule() == ValidationRule.repairConvexHull) {
506         jtsGeom = makeShape(geom.convexHull());
507       } else if (getValidationRule() == ValidationRule.repairBuffer0) {
508         jtsGeom = makeShape(geom.buffer(0));
509       } else {
510         // TODO there are other smarter things we could do like repairing inner holes and
511         // subtracting
512         // from outer repaired shell; but we needn't try too hard.
513         throw e;
514       }
515     }
516     return jtsGeom;
517   }
518 
519   /**
520    * INTERNAL
521    * @see #makeShape(org.locationtech.jts.geom.Geometry)
522    *
523    * @param geom Non-null
524    * @param dateline180Check if both this is true and {@link SpatialContext#isGeo()}, then JtsGeometry will check
525    *                         for adjacent coordinates greater than 180 degrees longitude apart, and
526    *                         it will do tricks to make that line segment (and the shape as a whole)
527    *                         cross the dateline even though JTS doesn't have geodetic support.
528    * @param allowMultiOverlap See {@link #isAllowMultiOverlap()}.
529    */
530   public JtsGeometry makeShape(Geometry geom, boolean dateline180Check, boolean allowMultiOverlap) {
531     JtsGeometry jtsGeom = new JtsGeometry(geom, (JtsSpatialContext) ctx, dateline180Check, allowMultiOverlap);
532     if (isAutoIndex()) {
533       jtsGeom.index();
534     }
535     return jtsGeom;
536   }
537 
538   /**
539    * INTERNAL: Creates a {@link Shape} from a JTS {@link Geometry}. Generally, this shouldn't be
540    * called when one of the other factory methods are available, such as for points. The caller
541    * needs to have done some verification/normalization of the coordinates by now, if any.  Also,
542    * note that direct instances of {@link GeometryCollection} isn't supported.
543    *
544    * Instead of calling this method, consider {@link #makeShapeFromGeometry(Geometry)}
545    * which
546    */
547   public JtsGeometry makeShape(Geometry geom) {
548     return makeShape(geom, datelineRule != DatelineRule.none, allowMultiOverlap);
549   }
550 
551   public GeometryFactory getGeometryFactory() {
552     return geometryFactory;
553   }
554 
555   /**
556    * INTERNAL: Returns a Rectangle of the JTS {@link Envelope} (bounding box) of the given {@code geom}.  This asserts
557    * that {@link Geometry#isRectangle()} is true.  This method reacts to the {@link DatelineRule} setting.
558    * @param geom non-null
559    * @return the equivalent Rectangle.
560    */
561   public Rectangle makeRectFromRectangularPoly(Geometry geom) {
562     // TODO although, might want to never convert if there's a semantic difference (e.g.
563     //  geodetically)? Should have a setting for that.
564     assert geom.isRectangle();
565     Envelope env = geom.getEnvelopeInternal();
566     boolean crossesDateline = false;
567     if (ctx.isGeo() && getDatelineRule() != DatelineRule.none) {
568       if (getDatelineRule() == DatelineRule.ccwRect) {
569         // If JTS says it is clockwise, then it's actually a dateline crossing rectangle.
570         crossesDateline = !CGAlgorithms.isCCW(geom.getCoordinates());
571       } else {
572         crossesDateline = env.getWidth() > 180;
573       }
574     }
575     if (crossesDateline)
576       return rect(env.getMaxX(), env.getMinX(), env.getMinY(), env.getMaxY());
577     else
578       return rect(env.getMinX(), env.getMaxX(), env.getMinY(), env.getMaxY());
579   }
580 
581 }