code: /*******************************************************************************
   * 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 org.locationtech.jts.algorithm.CGAlgorithms;
  import org.locationtech.jts.geom.*;
  import org.locationtech.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 (aka anti-meridian) 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;
        GeometricShapeFactory gsf = new GeometricShapeFactory(geometryFactory);
        gsf.setWidth(circle.getBoundingBox().getWidth());
        gsf.setHeight(circle.getBoundingBox().getHeight());
        gsf.setNumPoints(4*25);//multiple of 4 is best
        gsf.setCentre(new Coordinate(circle.getCenter().getX(), circle.getCenter().getY()));
        Geometry geom = gsf.createCircle();
        if (circle.getBoundingBox().getCrossesDateLine())
          // wrap the geometry in a JtsGeometry to handle date line wrapping
          geom = new JtsGeometry(geom, (JtsSpatialContext) getSpatialContext(),false, false).getGeom();
        return geom;
      }
      //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));
      if(bufferDistance!=0) {
        return shape.getBuffered(bufferDistance, ctx);
      }
      return 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 org.locationtech.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 org.locationtech.jts.geom.Point) {
        org.locationtech.jts.geom.Point pt = (org.locationtech.jts.geom.Point) geom;
        if (pt.isEmpty()) {
          return pointXY(Double.NaN, Double.NaN);
        } else {
          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;
        }
      }
      return jtsGeom;
    }
  
    /**
     * INTERNAL
     * @see #makeShape(org.locationtech.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) {
      JtsGeometry jtsGeom = new JtsGeometry(geom, (JtsSpatialContext) ctx, dateline180Check, allowMultiOverlap);
      if (isAutoIndex()) {
        jtsGeom.index();
      }
      return jtsGeom;
    }
  
    /**
     * 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());
    }
  
  }
  