All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.lucene.spatial.spatial4j.Geo3dShapeFactory Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.lucene.spatial.spatial4j;

import java.util.ArrayList;
import java.util.List;

import com.google.common.geometry.S2Cell;
import com.google.common.geometry.S2CellId;
import com.google.common.geometry.S2Point;
import org.apache.lucene.spatial.prefix.tree.S2ShapeFactory;
import org.apache.lucene.spatial3d.geom.GeoBBox;
import org.apache.lucene.spatial3d.geom.GeoBBoxFactory;
import org.apache.lucene.spatial3d.geom.GeoCircle;
import org.apache.lucene.spatial3d.geom.GeoCircleFactory;
import org.apache.lucene.spatial3d.geom.GeoCompositeAreaShape;
import org.apache.lucene.spatial3d.geom.GeoPath;
import org.apache.lucene.spatial3d.geom.GeoPathFactory;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoPointShape;
import org.apache.lucene.spatial3d.geom.GeoPointShapeFactory;
import org.apache.lucene.spatial3d.geom.GeoPolygon;
import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
import org.apache.lucene.spatial3d.geom.GeoS2ShapeFactory;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.exception.InvalidShapeException;
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.ShapeCollection;

/**
 * Geo3d implementation of {@link S2ShapeFactory}
 *
 * @lucene.experimental
 */
public class Geo3dShapeFactory implements S2ShapeFactory {

  private final boolean normWrapLongitude;
  private SpatialContext context;
  private PlanetModel planetModel;

  /**
   * Default accuracy for circles when not using the unit sphere.
   * It is equivalent to ~10m on the surface of the earth.
   */
  private static final double DEFAULT_CIRCLE_ACCURACY = 1e-4;
  private double circleAccuracy = DEFAULT_CIRCLE_ACCURACY;

  @SuppressWarnings("unchecked")
  public Geo3dShapeFactory(SpatialContext context, SpatialContextFactory factory) {
    this.context = context;
    this.planetModel = ((Geo3dSpatialContextFactory) factory).planetModel;
    this.normWrapLongitude = context.isGeo() && factory.normWrapLongitude;
  }

  @Override
  public SpatialContext getSpatialContext() {
    return context;
  }

  /**
   * Set the accuracy for circles in decimal degrees. Note that accuracy has no effect
   * when the planet model is a sphere. In that case, circles are always fully precise.
   *
   * @param circleAccuracy the provided accuracy in decimal degrees.
   */
  public void setCircleAccuracy(double circleAccuracy) {
    this.circleAccuracy = circleAccuracy;
  }

  @Override
  public boolean isNormWrapLongitude() {
    return normWrapLongitude;
  }

  @Override
  public double normX(double x) {
    if (this.normWrapLongitude) {
      x = DistanceUtils.normLonDEG(x);
    }
    return x;
  }

  @Override
  public double normY(double y) {
    return y;
  }

  @Override
  public double normZ(double z) {
    return z;
  }

  @Override
  public double normDist(double distance) {
    return distance;
  }

  @Override
  public void verifyX(double x) {
    Rectangle bounds = this.context.getWorldBounds();
    if (x < bounds.getMinX() || x > bounds.getMaxX()) {
      throw new InvalidShapeException("Bad X value " + x + " is not in boundary " + bounds);
    }
  }

  @Override
  public void verifyY(double y) {
    Rectangle bounds = this.context.getWorldBounds();
    if (y < bounds.getMinY() || y > bounds.getMaxY()) {
      throw new InvalidShapeException("Bad Y value " + y + " is not in boundary " + bounds);
    }
  }

  @Override
  public void verifyZ(double v) {
  }

  @Override
  public Point pointXY(double x, double y) {
    GeoPointShape point = GeoPointShapeFactory.makeGeoPointShape(planetModel,
        y * DistanceUtils.DEGREES_TO_RADIANS,
        x * DistanceUtils.DEGREES_TO_RADIANS);
    return new Geo3dPointShape(point, context);
  }

  @Override
  public Point pointXYZ(double x, double y, double z) {
    GeoPoint point = new GeoPoint(x, y, z);
    GeoPointShape pointShape = GeoPointShapeFactory.makeGeoPointShape(planetModel,
        point.getLatitude(),
        point.getLongitude());
    return new Geo3dPointShape(pointShape, context);
    //throw new UnsupportedOperationException();
  }

  @Override
  public Rectangle rect(Point point, Point point1) {
    return rect(point.getX(), point1.getX(), point.getY(), point1.getY());
  }

  @Override
  public Rectangle rect(double minX, double maxX, double minY, double maxY) {
    GeoBBox bBox = GeoBBoxFactory.makeGeoBBox(planetModel,
        maxY * DistanceUtils.DEGREES_TO_RADIANS,
        minY * DistanceUtils.DEGREES_TO_RADIANS,
        minX * DistanceUtils.DEGREES_TO_RADIANS,
        maxX * DistanceUtils.DEGREES_TO_RADIANS);
    return new Geo3dRectangleShape(bBox, context, minX, maxX, minY, maxY);
  }

  @Override
  public Circle circle(double x, double y, double distance) {
    GeoCircle circle;
    if (planetModel.isSphere()) {
      circle = GeoCircleFactory.makeGeoCircle(planetModel,
          y * DistanceUtils.DEGREES_TO_RADIANS,
          x * DistanceUtils.DEGREES_TO_RADIANS,
          distance * DistanceUtils.DEGREES_TO_RADIANS);
    }
    else {
      //accuracy is defined as a linear distance in this class. At tiny distances, linear distance
      //can be approximated to surface distance in radians.
      circle = GeoCircleFactory.makeExactGeoCircle(planetModel,
          y * DistanceUtils.DEGREES_TO_RADIANS,
          x * DistanceUtils.DEGREES_TO_RADIANS,
          distance * DistanceUtils.DEGREES_TO_RADIANS,
          circleAccuracy * DistanceUtils.DEGREES_TO_RADIANS);

    }
    return new Geo3dCircleShape(circle, context);
  }

  @Override
  public Circle circle(Point point, double distance) {
    return circle(point.getX(), point.getY(), distance);
  }

  @Override
  public Shape lineString(List list, double distance) {
    LineStringBuilder builder = lineString();
    for (Point point : list) {
      builder.pointXY(point.getX(), point.getY());
    }
    builder.buffer(distance);
    return builder.build();
  }

  @Override
  public  ShapeCollection multiShape(List list) {
    throw new UnsupportedOperationException();
  }

  @Override
  public LineStringBuilder lineString() {
    return new Geo3dLineStringBuilder();
  }

  @Override
  public PolygonBuilder polygon() {
    return new Geo3dPolygonBuilder();
  }

  @Override
  public  MultiShapeBuilder multiShape(Class aClass) {
    return new Geo3dMultiShapeBuilder<>();
  }

  @Override
  public MultiPointBuilder multiPoint() {
    return new Geo3dMultiPointBuilder();
  }

  @Override
  public MultiLineStringBuilder multiLineString() {
    return new Geo3dMultiLineBuilder();
  }

  @Override
  public MultiPolygonBuilder multiPolygon() {
    return new Geo3dMultiPolygonBuilder();
  }

  @Override
  public Shape getS2CellShape(S2CellId cellId) {
    S2Cell cell = new S2Cell(cellId);
    GeoPoint point1 = getGeoPoint(cell.getVertexRaw(0));
    GeoPoint point2 = getGeoPoint(cell.getVertexRaw(1));
    GeoPoint point3 = getGeoPoint(cell.getVertexRaw(2));
    GeoPoint point4 = getGeoPoint(cell.getVertexRaw(3));
    return new Geo3dShape<>(GeoS2ShapeFactory.makeGeoS2Shape(planetModel, point1, point2, point3, point4), context);
  }

  private GeoPoint getGeoPoint(S2Point point) {
    return planetModel.createSurfacePoint(point.get(0), point.get(1), point.get(2));
  }

  /**
   * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.PointsBuilder} interface to
   * generate {@link GeoPoint}.
   *
   * @param  is normally this object
   */
  private class Geo3dPointBuilder implements PointsBuilder {

    List points = new ArrayList<>();

    @SuppressWarnings("unchecked")
    @Override
    public T pointXY(double x, double y) {
      GeoPoint point = new GeoPoint(planetModel, y * DistanceUtils.DEGREES_TO_RADIANS, x * DistanceUtils.DEGREES_TO_RADIANS);
      points.add(point);
      return (T) this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public T pointXYZ(double x, double y, double z) {
      GeoPoint point = new GeoPoint(x, y, z);
      if (!points.contains(point)) {
        points.add(point);
      }
      return (T) this;
    }
  }

  /**
   * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.LineStringBuilder} to generate
   * line strings.
   */
  private class Geo3dLineStringBuilder extends Geo3dPointBuilder implements LineStringBuilder {

    double distance = 0;

    @Override
    public LineStringBuilder buffer(double distance) {
      this.distance = distance;
      return this;
    }

    @Override
    public Shape build() {
      GeoPath path = GeoPathFactory.makeGeoPath(planetModel, distance, points.toArray(new GeoPoint[points.size()]));
      return new Geo3dShape<>(path, context);
    }
  }

  /**
   * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.PolygonBuilder} to generate
   * polygons.
   */
  private class Geo3dPolygonBuilder extends Geo3dPointBuilder implements PolygonBuilder {

    List polyHoles = new ArrayList<>();

    @Override
    public HoleBuilder hole() {
      return new Geo3dHoleBuilder();
    }

    class Geo3dHoleBuilder extends Geo3dPointBuilder implements PolygonBuilder.HoleBuilder {
      @Override
      public PolygonBuilder endHole() {
        polyHoles.add(new GeoPolygonFactory.PolygonDescription(points));
        return Geo3dPolygonBuilder.this;
      }
    }

    @SuppressWarnings("unchecked")
    @Override
    public Shape build() {
      GeoPolygonFactory.PolygonDescription description = new GeoPolygonFactory.PolygonDescription(points, polyHoles);
      GeoPolygon polygon = GeoPolygonFactory.makeGeoPolygon(planetModel, description);
      if (polygon == null) {
        throw new InvalidShapeException("Invalid polygon, all points are coplanar");
      }
      return new Geo3dShape<>(polygon, context);
    }

    @Override
    public Shape buildOrRect() {
      return build();
    }
  }

  private class Geo3dMultiPointBuilder extends Geo3dPointBuilder implements MultiPointBuilder {

    @Override
    public Shape build() {
      GeoCompositeAreaShape areaShape = new GeoCompositeAreaShape(planetModel);
      for (GeoPoint point : points) {
        GeoPointShape pointShape = GeoPointShapeFactory.makeGeoPointShape(planetModel, point.getLatitude(), point.getLongitude());
        areaShape.addShape(pointShape);
      }
      return new Geo3dShape<>(areaShape, context);
    }
  }

  /**
   * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiLineStringBuilder} to generate
   * multi-lines
   */
  private class Geo3dMultiLineBuilder implements MultiLineStringBuilder {

    List builders = new ArrayList<>();

    @Override
    public LineStringBuilder lineString() {
      return new Geo3dLineStringBuilder();
    }

    @Override
    public MultiLineStringBuilder add(LineStringBuilder lineStringBuilder) {
      builders.add(lineStringBuilder);
      return this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Shape build() {
      GeoCompositeAreaShape areaShape = new GeoCompositeAreaShape(planetModel);
      for (LineStringBuilder builder : builders) {
        Geo3dShape shape = (Geo3dShape) builder.build();
        areaShape.addShape(shape.shape);
      }
      return new Geo3dShape<>(areaShape, context);
    }
  }

  /**
   * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiPolygonBuilder} to generate
   * multi-polygons. We have chosen to use a composite shape but
   * it might be possible to use GeoComplexPolygon.
   */
  private class Geo3dMultiPolygonBuilder implements MultiPolygonBuilder {

    List builders = new ArrayList<>();

    @Override
    public PolygonBuilder polygon() {
      return new Geo3dPolygonBuilder();
    }

    @Override
    public MultiPolygonBuilder add(PolygonBuilder polygonBuilder) {
      builders.add(polygonBuilder);
      return this;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Shape build() {
      GeoCompositeAreaShape areaShape = new GeoCompositeAreaShape(planetModel);
      for (PolygonBuilder builder : builders) {
        Geo3dShape shape = (Geo3dShape) builder.build();
        areaShape.addShape(shape.shape);
      }
      return new Geo3dShape<>(areaShape, context);
    }
  }

  /**
   * Geo3d implementation of {@link org.locationtech.spatial4j.shape.ShapeFactory.MultiShapeBuilder} to generate
   * geometry collections.
   *
   * @param  is the type of shapes.
   */
  private class Geo3dMultiShapeBuilder implements MultiShapeBuilder {

    GeoCompositeAreaShape composite = new GeoCompositeAreaShape(planetModel);

    @Override
    public MultiShapeBuilder add(T shape) {
      Geo3dShape areaShape = (Geo3dShape) shape;
      composite.addShape(areaShape.shape);
      return this;
    }

    @Override
    public Shape build() {
      return new Geo3dShape<>(composite, context);
    }
  }
}