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

org.apache.lucene.spatial3d.geom.GeoExactCircle Maven / Gradle / Ivy

There is a newer version: 10.0.0
Show newest version
/*
 * 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.spatial3d.geom;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Circular area with a center and a radius that represents the surface distance to the center. The
 * circle is divided in sectors where the circle edge is approximated using Vincenty formulae. The
 * higher is the precision the more sectors are needed to describe the shape and therefore a penalty
 * in performance.
 *
 * @lucene.experimental
 */
class GeoExactCircle extends GeoBaseCircle {
  /** Center of circle */
  protected final GeoPoint center;

  /** Radius of circle */
  protected final double radius;

  /** Actual accuracy */
  protected final double actualAccuracy;

  /** A point that is on the edge of the circle */
  protected final GeoPoint[] edgePoints;

  /** Slices of the circle */
  protected final List circleSlices;

  /**
   * Constructor.
   *
   * @param planetModel is the planet model.
   * @param lat is the center latitude.
   * @param lon is the center longitude.
   * @param radius is the surface radius for the circle.
   * @param accuracy is the allowed error value (linear distance). Maximum accuracy is 1e-12.
   */
  public GeoExactCircle(
      final PlanetModel planetModel,
      final double lat,
      final double lon,
      final double radius,
      final double accuracy) {
    super(planetModel);
    if (lat < -Math.PI * 0.5 || lat > Math.PI * 0.5) {
      throw new IllegalArgumentException("Latitude out of bounds");
    }
    if (lon < -Math.PI || lon > Math.PI) {
      throw new IllegalArgumentException("Longitude out of bounds");
    }
    if (radius < 0.0) {
      throw new IllegalArgumentException("Radius out of bounds");
    }
    if (radius < Vector.MINIMUM_RESOLUTION) {
      throw new IllegalArgumentException("Radius cannot be effectively zero");
    }
    if (planetModel.minimumPoleDistance - radius < Vector.MINIMUM_RESOLUTION) {
      throw new IllegalArgumentException(
          "Radius out of bounds. It cannot be bigger than "
              + planetModel.minimumPoleDistance
              + " for this planet model");
    }

    this.center = new GeoPoint(planetModel, lat, lon);
    this.radius = radius;

    if (accuracy < Vector.MINIMUM_RESOLUTION) {
      actualAccuracy = Vector.MINIMUM_RESOLUTION;
    } else {
      actualAccuracy = accuracy;
    }

    // We construct approximation planes until we have a low enough error estimate
    final List slices = new ArrayList<>(100);
    // Construct four cardinal points, and then we'll build the first two planes
    final GeoPoint northPoint = planetModel.surfacePointOnBearing(center, radius, 0.0);
    final GeoPoint southPoint = planetModel.surfacePointOnBearing(center, radius, Math.PI);
    final GeoPoint eastPoint = planetModel.surfacePointOnBearing(center, radius, Math.PI * 0.5);
    final GeoPoint westPoint = planetModel.surfacePointOnBearing(center, radius, Math.PI * 1.5);

    final GeoPoint edgePoint;
    if (planetModel.zScaling > planetModel.xyScaling) {
      // z can be greater than x or y, so ellipse is longer in height than width
      slices.add(
          new ApproximationSlice(
              center, eastPoint, Math.PI * 0.5, westPoint, Math.PI * -0.5, northPoint, 0.0, true));
      slices.add(
          new ApproximationSlice(
              center,
              westPoint,
              Math.PI * 1.5,
              eastPoint,
              Math.PI * 0.5,
              southPoint,
              Math.PI,
              true));
      edgePoint = eastPoint;
    } else {
      // z will be less than x or y, so ellipse is shorter than it is tall
      slices.add(
          new ApproximationSlice(
              center, northPoint, 0.0, southPoint, Math.PI, eastPoint, Math.PI * 0.5, true));
      slices.add(
          new ApproximationSlice(
              center,
              southPoint,
              Math.PI,
              northPoint,
              Math.PI * 2.0,
              westPoint,
              Math.PI * 1.5,
              true));
      edgePoint = northPoint;
    }
    // System.out.println("Edgepoint = " + edgePoint);

    this.circleSlices = new ArrayList<>();

    // Now, iterate over slices until we have converted all of them into safe SidedPlanes.
    while (slices.size() > 0) {
      // Peel off a slice from the back
      final ApproximationSlice thisSlice = slices.remove(slices.size() - 1);
      // Assess it to see if it is OK as it is, or needs to be split.
      // To do this, we need to look at the part of the circle that will have the greatest error.
      // We will need to compute bearing points for these.
      final double interpPoint1Bearing =
          (thisSlice.point1Bearing + thisSlice.middlePointBearing) * 0.5;
      final GeoPoint interpPoint1 =
          planetModel.surfacePointOnBearing(center, radius, interpPoint1Bearing);
      final double interpPoint2Bearing =
          (thisSlice.point2Bearing + thisSlice.middlePointBearing) * 0.5;
      final GeoPoint interpPoint2 =
          planetModel.surfacePointOnBearing(center, radius, interpPoint2Bearing);

      // Is this point on the plane? (that is, is the approximation good enough?)
      if (!thisSlice.mustSplit
          && Math.abs(thisSlice.plane.evaluate(interpPoint1)) < actualAccuracy
          && Math.abs(thisSlice.plane.evaluate(interpPoint2)) < actualAccuracy) {
        circleSlices.add(
            new CircleSlice(
                thisSlice.plane,
                thisSlice.endPoint1,
                thisSlice.endPoint2,
                center,
                thisSlice.middlePoint));
        // assert thisSlice.plane.isWithin(center);
      } else {
        // Split the plane into two, and add it back to the end
        slices.add(
            new ApproximationSlice(
                center,
                thisSlice.endPoint1,
                thisSlice.point1Bearing,
                thisSlice.middlePoint,
                thisSlice.middlePointBearing,
                interpPoint1,
                interpPoint1Bearing,
                false));
        slices.add(
            new ApproximationSlice(
                center,
                thisSlice.middlePoint,
                thisSlice.middlePointBearing,
                thisSlice.endPoint2,
                thisSlice.point2Bearing,
                interpPoint2,
                interpPoint2Bearing,
                false));
      }
    }

    this.edgePoints = new GeoPoint[] {edgePoint};

    // System.out.println("Is edgepoint within? " + isWithin(edgePoint));
  }

  /**
   * Constructor for deserialization.
   *
   * @param planetModel is the planet model.
   * @param inputStream is the input stream.
   */
  public GeoExactCircle(final PlanetModel planetModel, final InputStream inputStream)
      throws IOException {
    this(
        planetModel,
        SerializableObject.readDouble(inputStream),
        SerializableObject.readDouble(inputStream),
        SerializableObject.readDouble(inputStream),
        SerializableObject.readDouble(inputStream));
  }

  @Override
  public void write(final OutputStream outputStream) throws IOException {
    SerializableObject.writeDouble(outputStream, center.getLatitude());
    SerializableObject.writeDouble(outputStream, center.getLongitude());
    SerializableObject.writeDouble(outputStream, radius);
    SerializableObject.writeDouble(outputStream, actualAccuracy);
  }

  @Override
  public double getRadius() {
    return radius;
  }

  @Override
  public GeoPoint getCenter() {
    return center;
  }

  @Override
  protected double distance(
      final DistanceStyle distanceStyle, final double x, final double y, final double z) {
    return distanceStyle.computeDistance(this.center, x, y, z);
  }

  @Override
  protected void distanceBounds(
      final Bounds bounds, final DistanceStyle distanceStyle, final double distanceValue) {
    // TBD: Compute actual bounds based on distance
    getBounds(bounds);
  }

  @Override
  protected double outsideDistance(
      final DistanceStyle distanceStyle, final double x, final double y, final double z) {
    double outsideDistance = Double.POSITIVE_INFINITY;
    for (final CircleSlice slice : circleSlices) {
      final double distance =
          distanceStyle.computeDistance(
              planetModel, slice.circlePlane, x, y, z, slice.plane1, slice.plane2);
      if (distance < outsideDistance) {
        outsideDistance = distance;
      }
    }
    return outsideDistance;
  }

  @Override
  public boolean isWithin(final double x, final double y, final double z) {
    for (final CircleSlice slice : circleSlices) {
      if (slice.circlePlane.isWithin(x, y, z)
          && slice.plane1.isWithin(x, y, z)
          && slice.plane2.isWithin(x, y, z)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public GeoPoint[] getEdgePoints() {
    return edgePoints;
  }

  @Override
  public boolean intersects(
      final Plane p, final GeoPoint[] notablePoints, final Membership... bounds) {
    for (final CircleSlice slice : circleSlices) {
      if (slice.circlePlane.intersects(
          planetModel,
          p,
          notablePoints,
          slice.notableEdgePoints,
          bounds,
          slice.plane1,
          slice.plane2)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean intersects(GeoShape geoShape) {
    for (final CircleSlice slice : circleSlices) {
      if (geoShape.intersects(
          slice.circlePlane, slice.notableEdgePoints, slice.plane1, slice.plane2)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void getBounds(Bounds bounds) {
    super.getBounds(bounds);
    bounds.addPoint(center);
    for (final CircleSlice slice : circleSlices) {
      bounds.addPlane(planetModel, slice.circlePlane, slice.plane1, slice.plane2);
      for (final GeoPoint point : slice.notableEdgePoints) {
        bounds.addPoint(point);
      }
    }
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof GeoExactCircle)) {
      return false;
    }
    GeoExactCircle other = (GeoExactCircle) o;
    return super.equals(other)
        && other.center.equals(center)
        && other.radius == radius
        && other.actualAccuracy == actualAccuracy;
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + center.hashCode();
    long temp = Double.doubleToLongBits(radius);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(actualAccuracy);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

  @Override
  public String toString() {
    return "GeoExactCircle: {planetmodel="
        + planetModel
        + ", center="
        + center
        + ", radius="
        + radius
        + "("
        + radius * 180.0 / Math.PI
        + "), accuracy="
        + actualAccuracy
        + "}";
  }

  /** A temporary description of a section of circle. */
  protected static class ApproximationSlice {
    public final SidedPlane plane;
    public final GeoPoint endPoint1;
    public final double point1Bearing;
    public final GeoPoint endPoint2;
    public final double point2Bearing;
    public final GeoPoint middlePoint;
    public final double middlePointBearing;
    public final boolean mustSplit;

    public ApproximationSlice(
        final GeoPoint center,
        final GeoPoint endPoint1,
        final double point1Bearing,
        final GeoPoint endPoint2,
        final double point2Bearing,
        final GeoPoint middlePoint,
        final double middlePointBearing,
        final boolean mustSplit) {
      this.endPoint1 = endPoint1;
      this.point1Bearing = point1Bearing;
      this.endPoint2 = endPoint2;
      this.point2Bearing = point2Bearing;
      this.middlePoint = middlePoint;
      this.middlePointBearing = middlePointBearing;
      this.mustSplit = mustSplit;
      // Construct the plane going through the three given points
      this.plane =
          SidedPlane.constructNormalizedThreePointSidedPlane(
              center, endPoint1, endPoint2, middlePoint);
      if (this.plane == null) {
        throw new IllegalArgumentException(
            "Either circle is too small or accuracy is too high; could not construct a plane with endPoint1="
                + endPoint1
                + " bearing "
                + point1Bearing
                + ", endPoint2="
                + endPoint2
                + " bearing "
                + point2Bearing
                + ", middle="
                + middlePoint
                + " bearing "
                + middlePointBearing);
      }
      if (this.plane.isWithin(-center.x, -center.y, -center.z)) {
        // Plane is bogus, we cannot build the circle
        throw new IllegalArgumentException(
            "Could not construct a valid plane for this planet model with endPoint1="
                + endPoint1
                + " bearing "
                + point1Bearing
                + ", endPoint2="
                + endPoint2
                + " bearing "
                + point2Bearing
                + ", middle="
                + middlePoint
                + " bearing "
                + middlePointBearing);
      }
    }

    @Override
    public String toString() {
      return "{end point 1 = "
          + endPoint1
          + " bearing 1 = "
          + point1Bearing
          + " end point 2 = "
          + endPoint2
          + " bearing 2 = "
          + point2Bearing
          + " middle point = "
          + middlePoint
          + " middle bearing = "
          + middlePointBearing
          + "}";
    }
  }

  /** A description of a section of circle. */
  protected static class CircleSlice {
    final GeoPoint[] notableEdgePoints;
    public final SidedPlane circlePlane;
    public final SidedPlane plane1;
    public final SidedPlane plane2;

    public CircleSlice(
        SidedPlane circlePlane,
        GeoPoint endPoint1,
        GeoPoint endPoint2,
        GeoPoint center,
        GeoPoint check) {
      this.circlePlane = circlePlane;
      this.plane1 = new SidedPlane(check, endPoint1, center);
      this.plane2 = new SidedPlane(check, endPoint2, center);
      this.notableEdgePoints = new GeoPoint[] {endPoint1, endPoint2};
    }

    @Override
    public String toString() {
      return "{circle plane = "
          + circlePlane
          + " plane 1 = "
          + plane1
          + " plane 2 = "
          + plane2
          + " notable edge points = "
          + Arrays.toString(notableEdgePoints)
          + "}";
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy