/*******************************************************************************
* Copyright (c) 2015 Voyager Search and MITRE
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache License, Version 2.0 which
* accompanies this distribution and is available at
* http://www.apache.org/licenses/LICENSE-2.0.txt
******************************************************************************/
package org.locationtech.spatial4j.shape.jts;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.jts.DatelineRule;
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.locationtech.spatial4j.context.jts.ValidationRule;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.io.ShapeReader;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.impl.ShapeFactoryImpl;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.util.GeometricShapeFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Enhances {@link ShapeFactoryImpl} with support for Polygons
* using <a href="https://sourceforge.net/projects/jts-topo-suite/">JTS</a>.
* To the extent possible, our {@link JtsGeometry} adds some amount of geodetic support over
* vanilla JTS which only has a Euclidean (flat plane) model.
*/
public class JtsShapeFactory extends ShapeFactoryImpl {
protected static final LinearRing[] EMPTY_HOLES = new LinearRing[0];
protected final GeometryFactory geometryFactory;
protected final boolean allowMultiOverlap;
protected final boolean useJtsPoint;
protected final boolean useJtsLineString;
protected final boolean useJtsMulti;
protected final DatelineRule datelineRule;
protected final ValidationRule validationRule;
protected final boolean autoIndex;
/**
* Called by {@link org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory#newSpatialContext()}.
*/
public JtsShapeFactory(JtsSpatialContext ctx, JtsSpatialContextFactory factory) {
super(ctx, factory);
this.geometryFactory = factory.getGeometryFactory();
this.allowMultiOverlap = factory.allowMultiOverlap;
this.useJtsPoint = factory.useJtsPoint;
this.useJtsLineString = factory.useJtsLineString;
this.useJtsMulti = factory.useJtsMulti;
this.datelineRule = factory.datelineRule;
this.validationRule = factory.validationRule;
this.autoIndex = factory.autoIndex;
}
/**
* If geom might be a multi geometry of some kind, then might multiple
* component geometries overlap? Strict OGC says this is invalid but we
* can accept it by computing the union. Note: Our ShapeCollection mostly
* doesn't care but it has a method related to this
* {@link org.locationtech.spatial4j.shape.ShapeCollection#relateContainsShortCircuits()}.
*/
public boolean isAllowMultiOverlap() {
return allowMultiOverlap;
}
/**
* Returns the rule used to handle geometry objects that have dateline crossing considerations.
*/
public DatelineRule getDatelineRule() {
return datelineRule;
}
/**
* Returns the rule used to handle errors when creating a JTS {@link Geometry}, particularly after it has been
* read from one of the {@link ShapeReader}s.
*/
public ValidationRule getValidationRule() {
return validationRule;
}
/**
* If JtsGeometry shapes should be automatically "prepared" (i.e. optimized) when read via from a {@link ShapeReader}.
*
* @see org.locationtech.spatial4j.shape.jts.JtsGeometry#index()
*/
public boolean isAutoIndex() {
return autoIndex;
}
@Override
public double normX(double x) {
x = super.normX(x);
return geometryFactory.getPrecisionModel().makePrecise(x);
}
@Override
public double normY(double y) {
y = super.normY(y);
return geometryFactory.getPrecisionModel().makePrecise(y);
}
@Override
public double normZ(double z) {
z = super.normZ(z);
return geometryFactory.getPrecisionModel().makePrecise(z);
}
@Override
public double normDist(double d) {
return geometryFactory.getPrecisionModel().makePrecise(d);
}
/**
* Gets a JTS {@link Geometry} for the given {@link Shape}. Some shapes hold a
* JTS geometry whereas new ones must be created for the rest.
* @param shape Not null
* @return Not null
*/
public Geometry getGeometryFrom(Shape shape) {
if (shape instanceof JtsGeometry) {
return ((JtsGeometry)shape).getGeom();
}
if (shape instanceof JtsPoint) {
return ((JtsPoint) shape).getGeom();
}
if (shape instanceof Point) {
Point point = (Point) shape;
return geometryFactory.createPoint(new Coordinate(point.getX(),point.getY()));
}
if (shape instanceof Rectangle) {
Rectangle r = (Rectangle)shape;
if (r.getCrossesDateLine()) {
Collection<Geometry> pair = new ArrayList<>(2);
pair.add(geometryFactory.toGeometry(new Envelope(
r.getMinX(), ctx.getWorldBounds().getMaxX(), r.getMinY(), r.getMaxY())));
pair.add(geometryFactory.toGeometry(new Envelope(
ctx.getWorldBounds().getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY())));
return geometryFactory.buildGeometry(pair);//a MultiPolygon or MultiLineString
} else {
return geometryFactory.toGeometry(new Envelope(r.getMinX(), r.getMaxX(), r.getMinY(), r.getMaxY()));
}
}
if (shape instanceof Circle) {
// FYI Some interesting code for this is here:
// http://docs.codehaus.org/display/GEOTDOC/01+How+to+Create+a+Geometry#01HowtoCreateaGeometry-CreatingaCircle
//TODO This should ideally have a geodetic version
Circle circle = (Circle)shape;
if (circle.getBoundingBox().getCrossesDateLine())
throw new IllegalArgumentException("Doesn't support dateline cross yet: "+circle);//TODO
GeometricShapeFactory gsf = new GeometricShapeFactory(geometryFactory);
gsf.setSize(circle.getBoundingBox().getWidth());
gsf.setNumPoints(4*25);//multiple of 4 is best
gsf.setCentre(new Coordinate(circle.getCenter().getX(), circle.getCenter().getY()));
return gsf.createCircle();
}
//TODO add BufferedLineString
throw new InvalidShapeException("can't make Geometry from: " + shape);
}
/** Should {@link #pointXY(double, double)} return {@link JtsPoint}? */
public boolean useJtsPoint() {
return useJtsPoint;
}
@Override
public Point pointXY(double x, double y) {
return pointXYZ(x, y, Coordinate.NULL_ORDINATE);
}
@Override
public Point pointXYZ(double x, double y, double z) {
if (!useJtsPoint())
return super.pointXY(x, y);// ignore z
//A Jts Point is fairly heavyweight! TODO could/should we optimize this? SingleCoordinateSequence
verifyX(x);
verifyY(y);
verifyZ(z);
// verifyZ(z)?
Coordinate coord = Double.isNaN(x) ? null : new Coordinate(x, y, z);
return new JtsPoint(geometryFactory.createPoint(coord), (JtsSpatialContext) ctx);
}
/** Should {@link #lineString(java.util.List,double)} return {@link JtsGeometry}? */
public boolean useJtsLineString() {
//BufferedLineString doesn't yet do dateline cross, and can't yet be relate()'ed with a
// JTS geometry
return useJtsLineString;
}
@Override
public Shape lineString(List<Point> points, double bufferDistance) {
if (!useJtsLineString())
return super.lineString(points, bufferDistance);
//convert List<Point> to Coordinate[]
Coordinate[] coords = new Coordinate[points.size()];
for (int i = 0; i < coords.length; i++) {
Point p = points.get(i);
if (p instanceof JtsPoint) {
JtsPoint jtsPoint = (JtsPoint) p;
coords[i] = jtsPoint.getGeom().getCoordinate();
} else {
coords[i] = new Coordinate(p.getX(), p.getY());
}
}
JtsGeometry shape = makeShape(geometryFactory.createLineString(coords));
return bufferDistance != 0 ? shape.getBuffered(0, ctx) : shape;
}
@Override
public LineStringBuilder lineString() {
if (!useJtsLineString())
return super.lineString();
return new JtsLineStringBuilder();
}
private class JtsLineStringBuilder extends CoordinatesAccumulator<JtsLineStringBuilder>
implements LineStringBuilder {
protected double bufDistance;
public JtsLineStringBuilder() {
}
@Override
public LineStringBuilder buffer(double distance) {
this.bufDistance = distance;
return this;
}
@Override
public Shape build() {
Geometry geom = buildLineStringGeom();
if (bufDistance != 0.0) {
geom = geom.buffer(bufDistance);
}
return makeShape(geom);
}
LineString buildLineStringGeom() {
return geometryFactory.createLineString(getCoordsArray());
}
}
@Override
public PolygonBuilder polygon() {
return new JtsPolygonBuilder();
}
private class JtsPolygonBuilder extends CoordinatesAccumulator<JtsPolygonBuilder>
implements PolygonBuilder {
List<LinearRing> holes;// lazy instantiated
@Override
public JtsHoleBuilder hole() {
return new JtsHoleBuilder();
}
private class JtsHoleBuilder extends CoordinatesAccumulator<JtsHoleBuilder>
implements PolygonBuilder.HoleBuilder {
@Override
public JtsPolygonBuilder endHole() {
LinearRing linearRing = geometryFactory.createLinearRing(getCoordsArray());
if (JtsPolygonBuilder.this.holes == null) {
JtsPolygonBuilder.this.holes = new ArrayList<>(4);//short
}
JtsPolygonBuilder.this.holes.add(linearRing);
return JtsPolygonBuilder.this;
}
}
@Override
public Shape build() {
return makeShapeFromGeometry(buildPolygonGeom());
}
@Override
public Shape buildOrRect() {
Polygon geom = buildPolygonGeom();
if (geom.isRectangle()) {
return makeRectFromRectangularPoly(geom);
}
return makeShapeFromGeometry(geom);
}
Polygon buildPolygonGeom() {
LinearRing outerRing = geometryFactory.createLinearRing(getCoordsArray());
LinearRing[] holeRings = holes == null ? EMPTY_HOLES : holes.toArray(new LinearRing[this.holes.size()]);
return geometryFactory.createPolygon(outerRing, holeRings);
}
} // class JtsPolygonBuilder
private abstract class CoordinatesAccumulator<T extends CoordinatesAccumulator> {
protected List<Coordinate> coordinates = new ArrayList<>();
public T pointXY(double x, double y) {
return pointXYZ(x, y, Coordinate.NULL_ORDINATE);
}
public T pointXYZ(double x, double y, double z) {
verifyX(x);
verifyY(y);
coordinates.add(new Coordinate(x, y, z));
return getThis();
}
// TODO would be be useful to add other ways of providing points? e.g. point(Coordinate)?
// TODO consider wrapping the List<Coordinate> in a custom CoordinateSequence and then (conditionally) use
// geometryFactory's coordinateSequenceFactory to create a new CS if configured to do so.
// Also consider instead natively storing the double[] and then auto-expanding on pointXY* as needed.
protected Coordinate[] getCoordsArray() {
return coordinates.toArray(new Coordinate[coordinates.size()]);
}
@SuppressWarnings("unchecked")
protected T getThis() { return (T) this; }
}
/** Whether {@link #multiPoint()}, {@link #multiLineString()}, and {@link #multiPolygon()} should all use JTS's
* subclasses of {@link GeometryCollection} instead of Spatial4j's basic impl. The general {@link #multiShape(Class)}
* will never use {@link GeometryCollection} because that class doesn't support relations. */
public boolean useJtsMulti() {
return useJtsMulti;
}
@Override
public MultiPointBuilder multiPoint() {
if (!useJtsMulti) {
return super.multiPoint();
}
return new JtsMultiPointBuilder();
}
private class JtsMultiPointBuilder extends CoordinatesAccumulator<JtsMultiPointBuilder> implements MultiPointBuilder {
@Override
public Shape build() {
return makeShape(geometryFactory.createMultiPoint(getCoordsArray()));
}
}
@Override
public MultiLineStringBuilder multiLineString() {
if (!useJtsMulti) {
return super.multiLineString();
}
return new JtsMultiLineStringBuilder();
}
private class JtsMultiLineStringBuilder implements MultiLineStringBuilder {
List<LineString> geoms = new ArrayList<>();
@Override
public LineStringBuilder lineString() {
return new JtsLineStringBuilder();
}
@Override
public MultiLineStringBuilder add(LineStringBuilder lineStringBuilder) {
geoms.add(((JtsLineStringBuilder)lineStringBuilder).buildLineStringGeom());
return this;
}
@Override
public Shape build() {
return makeShape(geometryFactory.createMultiLineString(geoms.toArray(new LineString[geoms.size()])));
}
}
@Override
public MultiPolygonBuilder multiPolygon() {
if (!useJtsMulti) {
return super.multiPolygon();
}
return new JtsMultiPolygonBuilder();
}
private class JtsMultiPolygonBuilder implements MultiPolygonBuilder {
List<Polygon> geoms = new ArrayList<>();
@Override
public PolygonBuilder polygon() {
return new JtsPolygonBuilder();
}
@Override
public MultiPolygonBuilder add(PolygonBuilder polygonBuilder) {
geoms.add(((JtsPolygonBuilder)polygonBuilder).buildPolygonGeom());
return this;
}
@Override
public Shape build() {
return makeShape(geometryFactory.createMultiPolygon(geoms.toArray(new Polygon[geoms.size()])));
}
}
@Override
public <T extends Shape> MultiShapeBuilder<T> multiShape(Class<T> shapeClass) {
if (!useJtsMulti()) {
return super.multiShape(shapeClass);
}
return new JtsMultiShapeBuilder<>();
}
// TODO: once we have typed shapes for Polygons & LineStrings, this logic could move to the superclass
// (not JTS specific) and the multi* builders could take a Shape
private class JtsMultiShapeBuilder<T extends Shape> extends GeneralShapeMultiShapeBuilder<T> {
@Override
public Shape build() {
Class<?> last = null;
List<Geometry> geoms = new ArrayList<>(shapes.size());
for(Shape s : shapes) {
if (last != null && last != s.getClass()) {
return super.build();
}
if (s instanceof JtsGeometry) {
geoms.add(((JtsGeometry)s).getGeom());
} else if (s instanceof JtsPoint) {
geoms.add(((JtsPoint)s).getGeom());
} else {
return super.build();
}
last = s.getClass();
}
return makeShapeFromGeometry(geometryFactory.buildGeometry(geoms));
}
}
/**
* INTERNAL Usually creates a JtsGeometry, potentially validating, repairing, and indexing ("preparing"). This method
* is intended for use by {@link ShapeReader} instances.
*
* If given a direct instance of {@link GeometryCollection} then it's contents will be
* recursively converted and then the resulting list will be passed to
* {@link SpatialContext#makeCollection(List)} and returned.
*
* If given a {@link com.vividsolutions.jts.geom.Point} then {@link SpatialContext#makePoint(double, double)}
* is called, which will return a {@link JtsPoint} if {@link JtsSpatialContext#useJtsPoint()}; otherwise
* a standard Spatial4j Point is returned.
*
* If given a {@link LineString} and if {@link JtsSpatialContext#useJtsLineString()} is true then
* then the geometry's parts are exposed to call {@link SpatialContext#makeLineString(List)}.
*/
// TODO should this be called always (consistent but sometimes not needed?)
// v.s. only from a ShapeReader (pre-ShapeFactory behavior)
public Shape makeShapeFromGeometry(Geometry geom) {
if (geom instanceof GeometryCollection) {
// Direct instances of GeometryCollection can't be wrapped in JtsGeometry but can be expanded into
// a ShapeCollection.
if (!useJtsMulti || geom.getClass() == GeometryCollection.class) {
List<Shape> shapes = new ArrayList<>(geom.getNumGeometries());
for (int i = 0; i < geom.getNumGeometries(); i++) {
Geometry geomN = geom.getGeometryN(i);
shapes.add(makeShapeFromGeometry(geomN));//recursion
}
return multiShape(shapes);
}
} else if (geom instanceof com.vividsolutions.jts.geom.Point) {
com.vividsolutions.jts.geom.Point pt = (com.vividsolutions.jts.geom.Point) geom;
return pointXY(pt.getX(), pt.getY());
} else if (geom instanceof LineString) {
if (!useJtsLineString()) {
LineString lineString = (LineString) geom;
List<Point> points = new ArrayList<>(lineString.getNumPoints());
for (int i = 0; i < lineString.getNumPoints(); i++) {
Coordinate coord = lineString.getCoordinateN(i);
points.add(pointXY(coord.x, coord.y));
}
return lineString(points, 0);
}
}
JtsGeometry jtsGeom;
try {
jtsGeom = makeShape(geom);
if (getValidationRule() != ValidationRule.none)
jtsGeom.validate();
} catch (RuntimeException e) {
// repair:
if (getValidationRule() == ValidationRule.repairConvexHull) {
jtsGeom = makeShape(geom.convexHull());
} else if (getValidationRule() == ValidationRule.repairBuffer0) {
jtsGeom = makeShape(geom.buffer(0));
} else {
// TODO there are other smarter things we could do like repairing inner holes and
// subtracting
// from outer repaired shell; but we needn't try too hard.
throw e;
}
}
if (isAutoIndex())
jtsGeom.index();
return jtsGeom;
}
/**
* INTERNAL
* @see #makeShape(com.vividsolutions.jts.geom.Geometry)
*
* @param geom Non-null
* @param dateline180Check if both this is true and {@link SpatialContext#isGeo()}, then JtsGeometry will check
* for adjacent coordinates greater than 180 degrees longitude apart, and
* it will do tricks to make that line segment (and the shape as a whole)
* cross the dateline even though JTS doesn't have geodetic support.
* @param allowMultiOverlap See {@link #isAllowMultiOverlap()}.
*/
public JtsGeometry makeShape(Geometry geom, boolean dateline180Check, boolean allowMultiOverlap) {
return new JtsGeometry(geom, (JtsSpatialContext) ctx, dateline180Check, allowMultiOverlap);
}
/**
* INTERNAL: Creates a {@link Shape} from a JTS {@link Geometry}. Generally, this shouldn't be
* called when one of the other factory methods are available, such as for points. The caller
* needs to have done some verification/normalization of the coordinates by now, if any. Also,
* note that direct instances of {@link GeometryCollection} isn't supported.
*
* Instead of calling this method, consider {@link #makeShapeFromGeometry(Geometry)}
* which
*/
public JtsGeometry makeShape(Geometry geom) {
return makeShape(geom, datelineRule != DatelineRule.none, allowMultiOverlap);
}
public GeometryFactory getGeometryFactory() {
return geometryFactory;
}
/**
* INTERNAL: Returns a Rectangle of the JTS {@link Envelope} (bounding box) of the given {@code geom}. This asserts
* that {@link Geometry#isRectangle()} is true. This method reacts to the {@link DatelineRule} setting.
* @param geom non-null
* @return the equivalent Rectangle.
*/
public Rectangle makeRectFromRectangularPoly(Geometry geom) {
// TODO although, might want to never convert if there's a semantic difference (e.g.
// geodetically)? Should have a setting for that.
assert geom.isRectangle();
Envelope env = geom.getEnvelopeInternal();
boolean crossesDateline = false;
if (ctx.isGeo() && getDatelineRule() != DatelineRule.none) {
if (getDatelineRule() == DatelineRule.ccwRect) {
// If JTS says it is clockwise, then it's actually a dateline crossing rectangle.
crossesDateline = !CGAlgorithms.isCCW(geom.getCoordinates());
} else {
crossesDateline = env.getWidth() > 180;
}
}
if (crossesDateline)
return rect(env.getMaxX(), env.getMinX(), env.getMinY(), env.getMaxY());
else
return rect(env.getMinX(), env.getMaxX(), env.getMinY(), env.getMaxY());
}
}