LUCENE-6544: Geo3D: Regularize path & polygon construction, add PlanetModel.surfaceDistance(), cache lat & lon in GeoPoint, add thread-safety where missing -- Geo3dShape.

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1686285 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
David Wayne Smiley 2015-06-18 17:42:48 +00:00
parent 54980cef40
commit 0340838276
13 changed files with 337 additions and 164 deletions

View File

@ -95,6 +95,11 @@ New Features
* LUCENE-6577: Give earlier and better error message for invalid CRC.
(Robert Muir)
* LUCENE-6544: Geo3D: (1) Regularize path & polygon construction, (2) add
PlanetModel.surfaceDistance() (ellipsoidal calculation), (3) cache lat & lon
in GeoPoint, (4) add thread-safety where missing -- Geo3dShape. (Karl Wright,
David Smiley)
API Changes
* LUCENE-6508: Simplify Lock api, there is now just

View File

@ -18,6 +18,7 @@ package org.apache.lucene.spatial.spatial4j;
*/
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import com.spatial4j.core.shape.Point;
import com.spatial4j.core.shape.Rectangle;
import com.spatial4j.core.shape.Shape;
@ -31,20 +32,21 @@ import org.apache.lucene.spatial.spatial4j.geo3d.GeoShape;
import org.apache.lucene.spatial.spatial4j.geo3d.PlanetModel;
/**
* A 3D planar geometry based Spatial4j Shape implementation.
* A Spatial4j Shape wrapping a {@link GeoShape} ("Geo3D") -- a 3D planar geometry based Spatial4j Shape implementation.
* Geo3D implements shapes on the surface of a sphere or ellipsoid.
*
* @lucene.experimental
*/
public class Geo3dShape implements Shape {
/** The required size of this adjustment depends on the actual planetary model chosen.
* This value is big enough to account for WGS84. */
protected static final double ROUNDOFF_ADJUSTMENT = 0.05;
public final SpatialContext ctx;
public final GeoShape shape;
public final PlanetModel planetModel;
private Rectangle boundingBox = null;
public final static double RADIANS_PER_DEGREE = Math.PI / 180.0;
public final static double DEGREES_PER_RADIAN = 1.0 / RADIANS_PER_DEGREE;
private volatile Rectangle boundingBox = null; // lazy initialized
public Geo3dShape(final GeoShape shape, final SpatialContext ctx) {
this(PlanetModel.SPHERE, shape, ctx);
@ -72,10 +74,10 @@ public class Geo3dShape implements Shape {
protected SpatialRelation relate(Rectangle r) {
// Construct the right kind of GeoArea first
GeoArea geoArea = GeoAreaFactory.makeGeoArea(planetModel,
r.getMaxY() * RADIANS_PER_DEGREE,
r.getMinY() * RADIANS_PER_DEGREE,
r.getMinX() * RADIANS_PER_DEGREE,
r.getMaxX() * RADIANS_PER_DEGREE);
r.getMaxY() * DistanceUtils.DEGREES_TO_RADIANS,
r.getMinY() * DistanceUtils.DEGREES_TO_RADIANS,
r.getMinX() * DistanceUtils.DEGREES_TO_RADIANS,
r.getMaxX() * DistanceUtils.DEGREES_TO_RADIANS);
int relationship = geoArea.getRelationship(shape);
if (relationship == GeoArea.WITHIN)
return SpatialRelation.WITHIN;
@ -91,7 +93,7 @@ public class Geo3dShape implements Shape {
protected SpatialRelation relate(Point p) {
// Create a GeoPoint
GeoPoint point = new GeoPoint(planetModel, p.getY()*RADIANS_PER_DEGREE, p.getX()*RADIANS_PER_DEGREE);
GeoPoint point = new GeoPoint(planetModel, p.getY()* DistanceUtils.DEGREES_TO_RADIANS, p.getX()* DistanceUtils.DEGREES_TO_RADIANS);
if (shape.isWithin(point)) {
// Point within shape
return SpatialRelation.CONTAINS;
@ -99,13 +101,12 @@ public class Geo3dShape implements Shape {
return SpatialRelation.DISJOINT;
}
// The required size of this adjustment depends on the actual planetary model chosen.
// This value is big enough to account for WGS84.
protected final double ROUNDOFF_ADJUSTMENT = 0.05;
@Override
public Rectangle getBoundingBox() {
if (boundingBox == null) {
Rectangle bbox = this.boundingBox;//volatile read once
if (bbox == null) {
Bounds bounds = shape.getBounds(null);
double leftLon;
double rightLon;
@ -113,24 +114,25 @@ public class Geo3dShape implements Shape {
leftLon = -180.0;
rightLon = 180.0;
} else {
leftLon = bounds.getLeftLongitude().doubleValue() * DEGREES_PER_RADIAN;
rightLon = bounds.getRightLongitude().doubleValue() * DEGREES_PER_RADIAN;
leftLon = bounds.getLeftLongitude().doubleValue() * DistanceUtils.RADIANS_TO_DEGREES;
rightLon = bounds.getRightLongitude().doubleValue() * DistanceUtils.RADIANS_TO_DEGREES;
}
double minLat;
if (bounds.checkNoBottomLatitudeBound()) {
minLat = -90.0;
} else {
minLat = bounds.getMinLatitude().doubleValue() * DEGREES_PER_RADIAN;
minLat = bounds.getMinLatitude().doubleValue() * DistanceUtils.RADIANS_TO_DEGREES;
}
double maxLat;
if (bounds.checkNoTopLatitudeBound()) {
maxLat = 90.0;
} else {
maxLat = bounds.getMaxLatitude().doubleValue() * DEGREES_PER_RADIAN;
maxLat = bounds.getMaxLatitude().doubleValue() * DistanceUtils.RADIANS_TO_DEGREES;
}
boundingBox = new RectangleImpl(leftLon, rightLon, minLat, maxLat, ctx).getBuffered(ROUNDOFF_ADJUSTMENT, ctx);
bbox = new RectangleImpl(leftLon, rightLon, minLat, maxLat, ctx).getBuffered(ROUNDOFF_ADJUSTMENT, ctx);
this.boundingBox = bbox;
}
return boundingBox;
return bbox;
}
@Override
@ -140,17 +142,17 @@ public class Geo3dShape implements Shape {
@Override
public double getArea(SpatialContext ctx) {
throw new RuntimeException("Unimplemented");
throw new UnsupportedOperationException();
}
@Override
public Point getCenter() {
throw new RuntimeException("Unimplemented");
throw new UnsupportedOperationException();
}
@Override
public Shape getBuffered(double distance, SpatialContext ctx) {
return this;
throw new UnsupportedOperationException();
}
@Override
@ -160,7 +162,7 @@ public class Geo3dShape implements Shape {
@Override
public String toString() {
return "Geo3dShape{planetmodel=" + planetModel+", shape="+shape + '}';
return "Geo3dShape{planetmodel=" + planetModel + ", shape=" + shape + '}';
}
@Override
@ -168,7 +170,7 @@ public class Geo3dShape implements Shape {
if (!(other instanceof Geo3dShape))
return false;
Geo3dShape tr = (Geo3dShape)other;
return tr.planetModel.equals(planetModel) && tr.shape.equals(shape);
return tr.ctx.equals(ctx) && tr.planetModel.equals(planetModel) && tr.shape.equals(shape);
}
@Override

View File

@ -39,31 +39,40 @@ public class GeoCircle extends GeoBaseExtendedShape implements GeoDistanceShape,
throw new IllegalArgumentException("Cutoff angle out of bounds");
final double cosAngle = Math.cos(cutoffAngle);
this.center = new GeoPoint(planetModel, lat, lon);
final double magnitude = center.magnitude();
// In an ellipsoidal world, cutoff distances make no sense, unfortunately. Only membership
// can be used to make in/out determination.
this.cutoffAngle = cutoffAngle;
// The plane's normal vector needs to be normalized, since we compute D on that basis
this.circlePlane = new SidedPlane(center, center.normalize(), -cosAngle * magnitude);
// Compute two points on the circle, with the right angle from the center. We'll use these
// to obtain the perpendicular plane to the circle.
double upperLat = lat + cutoffAngle;
double upperLon = lon;
if (upperLat > Math.PI * 0.5) {
upperLon += Math.PI;
if (upperLon > Math.PI)
upperLon -= 2.0 * Math.PI;
upperLat = Math.PI - upperLat;
}
double lowerLat = lat - cutoffAngle;
double lowerLon = lon;
if (lowerLat < -Math.PI * 0.5) {
lowerLon += Math.PI;
if (lowerLon > Math.PI)
lowerLon -= 2.0 * Math.PI;
lowerLat = -Math.PI - lowerLat;
}
final GeoPoint upperPoint = new GeoPoint(planetModel, upperLat, upperLon);
final GeoPoint lowerPoint = new GeoPoint(planetModel, lowerLat, lowerLon);
// Construct normal plane
final Plane normalPlane = new Plane(upperPoint, center);
// Construct a sided plane that goes through the two points and whose normal is in the normalPlane.
this.circlePlane = SidedPlane.constructNormalizedPerpendicularSidedPlane(center, normalPlane, upperPoint, lowerPoint);
if (circlePlane == null)
throw new RuntimeException("Couldn't construct circle plane. Cutoff angle = "+cutoffAngle+"; upperPoint = "+upperPoint+"; lowerPoint = "+lowerPoint);
// Compute a point on the circle boundary.
if (cutoffAngle == Math.PI)
this.edgePoints = new GeoPoint[0];
else {
// We already have circle plane, which is the definitive determination of the edge of the "circle".
// Next, compute vertical plane going through origin and the center point (C = 0, D = 0).
Plane verticalPlane = Plane.constructNormalizedVerticalPlane(this.center.x, this.center.y);
if (verticalPlane == null) {
verticalPlane = new Plane(1.0,0.0);
}
// Finally, use Plane.findIntersections() to find the intersection points.
final GeoPoint edgePoint = this.circlePlane.getSampleIntersectionPoint(planetModel, verticalPlane);
if (edgePoint == null) {
throw new RuntimeException("Could not find edge point for circle at lat="+lat+" lon="+lon+" cutoffAngle="+cutoffAngle+" planetModel="+planetModel);
}
//if (Math.abs(circlePlane.evaluate(edgePoint)) > 1e-10)
// throw new RuntimeException("Computed an edge point that does not satisfy circlePlane equation! "+circlePlane.evaluate(edgePoint));
this.edgePoints = new GeoPoint[]{edgePoint};
this.edgePoints = new GeoPoint[]{upperPoint};
}
}

View File

@ -34,13 +34,14 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
protected final BitSet isInternalEdges;
protected SidedPlane[] edges = null;
protected boolean[] internalEdges = null;
protected GeoPoint[][] notableEdgePoints = null;
protected GeoPoint[] edgePoints = null;
protected double fullDistance = 0.0;
protected boolean isDone = false;
/**
* Create a convex polygon from a list of points. The first point must be on the
* external edge.
@ -48,19 +49,20 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
public GeoConvexPolygon(final PlanetModel planetModel, final List<GeoPoint> pointList) {
super(planetModel);
this.points = pointList;
this.isInternalEdges = null;
donePoints(false);
this.isInternalEdges = new BitSet();
done(false);
}
/**
* Create a convex polygon from a list of points, keeping track of which boundaries
* are internal. This is used when creating a polygon as a building block for another shape.
*/
public GeoConvexPolygon(final PlanetModel planetModel, final List<GeoPoint> pointList, final BitSet internalEdgeFlags, final boolean returnEdgeInternal) {
public GeoConvexPolygon(final PlanetModel planetModel, final List<GeoPoint> pointList, final BitSet internalEdgeFlags,
final boolean returnEdgeInternal) {
super(planetModel);
this.points = pointList;
this.isInternalEdges = internalEdgeFlags;
donePoints(returnEdgeInternal);
done(returnEdgeInternal);
}
/**
@ -69,16 +71,9 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
*/
public GeoConvexPolygon(final PlanetModel planetModel, final double startLatitude, final double startLongitude) {
super(planetModel);
points = new ArrayList<GeoPoint>();
points = new ArrayList<>();
isInternalEdges = new BitSet();
// Argument checking
if (startLatitude > Math.PI * 0.5 || startLatitude < -Math.PI * 0.5)
throw new IllegalArgumentException("Latitude out of range");
if (startLongitude < -Math.PI || startLongitude > Math.PI)
throw new IllegalArgumentException("Longitude out of range");
final GeoPoint p = new GeoPoint(planetModel, startLatitude, startLongitude);
points.add(p);
points.add(new GeoPoint(planetModel, startLatitude, startLongitude));
}
/**
@ -87,36 +82,39 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
*
* @param latitude is the latitude of the next point.
* @param longitude is the longitude of the next point.
* @param isInternalEdge is true if the edge just added should be considered "internal", and not
* @param isInternalEdge is true if the edge just added with this point should be considered "internal", and not
* intersected as part of the intersects() operation.
*/
public void addPoint(final double latitude, final double longitude, final boolean isInternalEdge) {
// Argument checking
if (latitude > Math.PI * 0.5 || latitude < -Math.PI * 0.5)
throw new IllegalArgumentException("Latitude out of range");
if (longitude < -Math.PI || longitude > Math.PI)
throw new IllegalArgumentException("Longitude out of range");
final GeoPoint p = new GeoPoint(planetModel, latitude, longitude);
isInternalEdges.set(points.size(), isInternalEdge);
points.add(p);
if (isDone)
throw new IllegalStateException("Can't call addPoint() if done() already called");
if (isInternalEdge)
isInternalEdges.set(points.size() - 1);
points.add(new GeoPoint(planetModel, latitude, longitude));
}
/**
* Finish the polygon, by connecting the last added point with the starting point.
*/
public void donePoints(final boolean isInternalReturnEdge) {
public void done(final boolean isInternalReturnEdge) {
if (isDone)
throw new IllegalStateException("Can't call done() more than once");
// If fewer than 3 points, can't do it.
if (points.size() < 3)
throw new IllegalArgumentException("Polygon needs at least three points.");
if (isInternalReturnEdge)
isInternalEdges.set(points.size() - 1);
isDone = true;
// Time to construct the planes. If the polygon is truly convex, then any adjacent point
// to a segment can provide an interior measurement.
edges = new SidedPlane[points.size()];
notableEdgePoints = new GeoPoint[points.size()][];
internalEdges = new boolean[points.size()];
for (int i = 0; i < points.size(); i++) {
final GeoPoint start = points.get(i);
final boolean isInternalEdge = (isInternalEdges != null ? (i == isInternalEdges.size() ? isInternalReturnEdge : isInternalEdges.get(i)) : false);
final GeoPoint end = points.get(legalIndex(i + 1));
final double distance = start.arcDistance(end);
if (distance > fullDistance)
@ -126,7 +124,6 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
//System.out.println("Created edge "+sp+" using start="+start+" end="+end+" check="+check);
edges[i] = sp;
notableEdgePoints[i] = new GeoPoint[]{start, end};
internalEdges[i] = isInternalEdge;
}
createCenterPoint();
}
@ -183,7 +180,7 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
for (int edgeIndex = 0; edgeIndex < edges.length; edgeIndex++) {
final SidedPlane edge = edges[edgeIndex];
final GeoPoint[] points = this.notableEdgePoints[edgeIndex];
if (!internalEdges[edgeIndex]) {
if (!isInternalEdges.get(edgeIndex)) {
//System.err.println(" non-internal edge "+edge);
// Edges flagged as 'internal only' are excluded from the matching
// Construct boundaries
@ -250,14 +247,9 @@ public class GeoConvexPolygon extends GeoBaseExtendedShape implements GeoMembers
GeoConvexPolygon other = (GeoConvexPolygon) o;
if (!super.equals(other))
return false;
if (other.points.size() != points.size())
if (!other.isInternalEdges.equals(isInternalEdges))
return false;
for (int i = 0; i < points.size(); i++) {
if (!other.points.get(i).equals(points.get(i)))
return false;
}
return true;
return (other.points.equals(points));
}
@Override

View File

@ -18,6 +18,7 @@ package org.apache.lucene.spatial.spatial4j.geo3d;
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@ -31,16 +32,25 @@ import java.util.List;
public class GeoPath extends GeoBaseExtendedShape implements GeoDistanceShape {
public final double cutoffAngle;
public final double sinAngle;
public final double cosAngle;
public final double sinAngle; // sine of cutoffAngle
public final double cosAngle; // cosine of cutoffAngle
public final List<GeoPoint> points = new ArrayList<GeoPoint>();
public final List<SegmentEndpoint> endPoints = new ArrayList<SegmentEndpoint>();
public final List<PathSegment> segments = new ArrayList<PathSegment>();
public List<SegmentEndpoint> endPoints;
public List<PathSegment> segments;
public GeoPoint[] edgePoints = null;
public GeoPoint[] edgePoints;
public boolean isDone = false;
public GeoPath(final PlanetModel planetModel, final double maxCutoffAngle, final GeoPoint[] pathPoints) {
this(planetModel, maxCutoffAngle);
Collections.addAll(points, pathPoints);
done();
}
public GeoPath(final PlanetModel planetModel, final double maxCutoffAngle) {
super(planetModel);
if (maxCutoffAngle <= 0.0 || maxCutoffAngle > Math.PI * 0.5)
@ -51,17 +61,21 @@ public class GeoPath extends GeoBaseExtendedShape implements GeoDistanceShape {
}
public void addPoint(double lat, double lon) {
if (lat < -Math.PI * 0.5 || lat > Math.PI * 0.5)
throw new IllegalArgumentException("Latitude out of range");
if (lon < -Math.PI || lon > Math.PI)
throw new IllegalArgumentException("Longitude out of range");
if (isDone)
throw new IllegalStateException("Can't call addPoint() if done() already called");
points.add(new GeoPoint(planetModel, lat, lon));
}
public void done() {
if (isDone)
throw new IllegalStateException("Can't call done() twice");
if (points.size() == 0)
throw new IllegalArgumentException("Path must have at least one point");
// Compute an offset to use for all segments. This will be based on the minimum magnitude of
isDone = true;
endPoints = new ArrayList<>(points.size());
segments = new ArrayList<>(points.size());
// Compute an offset to use for all segments. This will be based on the minimum magnitude of
// the entire ellipsoid.
final double cutoffOffset = this.sinAngle * planetModel.getMinimumMagnitude();
@ -80,21 +94,32 @@ public class GeoPath extends GeoBaseExtendedShape implements GeoDistanceShape {
if (segments.size() == 0) {
// Simple circle
final SegmentEndpoint onlyEndpoint = new SegmentEndpoint(points.get(0), cutoffOffset);
double lat = points.get(0).getLatitude();
double lon = points.get(0).getLongitude();
// Compute two points on the circle, with the right angle from the center. We'll use these
// to obtain the perpendicular plane to the circle.
double upperLat = lat + cutoffAngle;
double upperLon = lon;
if (upperLat > Math.PI * 0.5) {
upperLon += Math.PI;
if (upperLon > Math.PI)
upperLon -= 2.0 * Math.PI;
upperLat = Math.PI - upperLat;
}
double lowerLat = lat - cutoffAngle;
double lowerLon = lon;
if (lowerLat < -Math.PI * 0.5) {
lowerLon += Math.PI;
if (lowerLon > Math.PI)
lowerLon -= 2.0 * Math.PI;
lowerLat = -Math.PI - lowerLat;
}
final GeoPoint upperPoint = new GeoPoint(planetModel, upperLat, upperLon);
final GeoPoint lowerPoint = new GeoPoint(planetModel, lowerLat, lowerLon);
final SegmentEndpoint onlyEndpoint = new SegmentEndpoint(points.get(0), upperPoint, lowerPoint);
endPoints.add(onlyEndpoint);
// Find an edgepoint
// We already have circle plane, which is the definitive determination of the edge of the "circle".
// Next, compute vertical plane going through origin and the center point (C = 0, D = 0).
Plane verticalPlane = Plane.constructNormalizedVerticalPlane(onlyEndpoint.point.x, onlyEndpoint.point.y);
if (verticalPlane == null) {
verticalPlane = new Plane(1.0,0.0);
}
// Finally, use Plane.findIntersections() to find the intersection points.
final GeoPoint edgePoint = onlyEndpoint.circlePlane.getSampleIntersectionPoint(planetModel, verticalPlane);
if (edgePoint == null) {
throw new RuntimeException("Could not find edge point for path endpoint="+onlyEndpoint.point+" cutoffOffset="+cutoffOffset+" planetModel="+planetModel);
}
this.edgePoints = new GeoPoint[]{edgePoint};
this.edgePoints = new GeoPoint[]{upperPoint};
return;
}
@ -386,17 +411,9 @@ public class GeoPath extends GeoBaseExtendedShape implements GeoDistanceShape {
GeoPath p = (GeoPath) o;
if (!super.equals(p))
return false;
if (endPoints.size() != p.endPoints.size())
return false;
if (cutoffAngle != p.cutoffAngle)
return false;
for (int i = 0; i < endPoints.size(); i++) {
SegmentEndpoint point = endPoints.get(i);
SegmentEndpoint point2 = p.endPoints.get(i);
if (!point.equals(point2))
return false;
}
return true;
return points.equals(p.points);
}
@Override
@ -404,7 +421,7 @@ public class GeoPath extends GeoBaseExtendedShape implements GeoDistanceShape {
int result = super.hashCode();
long temp = Double.doubleToLongBits(cutoffAngle);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + endPoints.hashCode();
result = 31 * result + points.hashCode();
return result;
}
@ -443,11 +460,12 @@ public class GeoPath extends GeoBaseExtendedShape implements GeoDistanceShape {
/** Constructor for case (1).
* Generate a simple circle cutoff plane.
*/
public SegmentEndpoint(final GeoPoint point, final double cutoffOffset) {
public SegmentEndpoint(final GeoPoint point, final GeoPoint upperPoint, final GeoPoint lowerPoint) {
this.point = point;
final double magnitude = point.magnitude();
// Normalize vector to make D value correct
this.circlePlane = new SidedPlane(point, point.normalize(), -Math.sqrt(magnitude * magnitude - cutoffOffset * cutoffOffset));
// Construct normal plane
final Plane normalPlane = new Plane(upperPoint, point);
// Construct a sided plane that goes through the two points and whose normal is in the normalPlane.
this.circlePlane = SidedPlane.constructNormalizedPerpendicularSidedPlane(point, normalPlane, upperPoint, lowerPoint);
this.cutoffPlanes = new Membership[0];
this.notablePoints = new GeoPoint[0];
}

View File

@ -24,9 +24,34 @@ package org.apache.lucene.spatial.spatial4j.geo3d;
*/
public class GeoPoint extends Vector {
// By making lazily-evaluated variables be "volatile", we guarantee atomicity when they
// are updated. This is necessary if we are using these classes in a multi-thread fashion,
// because we don't try to synchronize for the lazy computation.
/** This is the lazily-evaluated magnitude. Some constructors include it, but others don't, and
* we try not to create extra computation by always computing it. */
protected double magnitude = Double.NEGATIVE_INFINITY;
* we try not to create extra computation by always computing it. Does not need to be
* synchronized for thread safety, because depends wholly on immutable variables of this class. */
protected volatile double magnitude = Double.NEGATIVE_INFINITY;
/** Lazily-evaluated latitude. Does not need to be
* synchronized for thread safety, because depends wholly on immutable variables of this class. */
protected volatile double latitude = Double.NEGATIVE_INFINITY;
/** Lazily-evaluated longitude. Does not need to be
* synchronized for thread safety, because depends wholly on immutable variables of this class. */
protected volatile double longitude = Double.NEGATIVE_INFINITY;
/** Construct a GeoPoint from the trig functions of a lat and lon pair.
* @param planetModel is the planetModel to put the point on.
* @param sinLat is the sin of the latitude.
* @param sinLon is the sin of the longitude.
* @param cosLat is the cos of the latitude.
* @param cosLon is the cos of the longitude.
* @param lat is the latitude.
* @param lon is the longitude.
*/
public GeoPoint(final PlanetModel planetModel, final double sinLat, final double sinLon, final double cosLat, final double cosLon, final double lat, final double lon) {
this(computeDesiredEllipsoidMagnitude(planetModel, cosLat * cosLon, cosLat * sinLon, sinLat),
cosLat * cosLon, cosLat * sinLon, sinLat, lat, lon);
}
/** Construct a GeoPoint from the trig functions of a lat and lon pair.
* @param planetModel is the planetModel to put the point on.
@ -46,7 +71,26 @@ public class GeoPoint extends Vector {
* @param lon is the longitude.
*/
public GeoPoint(final PlanetModel planetModel, final double lat, final double lon) {
this(planetModel, Math.sin(lat), Math.sin(lon), Math.cos(lat), Math.cos(lon));
this(planetModel, Math.sin(lat), Math.sin(lon), Math.cos(lat), Math.cos(lon), lat, lon);
}
/** Construct a GeoPoint from a unit (x,y,z) vector and a magnitude.
* @param magnitude is the desired magnitude, provided to put the point on the ellipsoid.
* @param x is the unit x value.
* @param y is the unit y value.
* @param z is the unit z value.
* @param lat is the latitude.
* @param lon is the longitude.
*/
public GeoPoint(final double magnitude, final double x, final double y, final double z, double lat, double lon) {
super(x * magnitude, y * magnitude, z * magnitude);
this.magnitude = magnitude;
if (lat > Math.PI * 0.5 || lat < -Math.PI * 0.5)
throw new IllegalArgumentException("Latitude out of range");
if (lon < -Math.PI || lon > Math.PI)
throw new IllegalArgumentException("Longitude out of range");
this.latitude = lat;
this.longitude = lon;
}
/** Construct a GeoPoint from a unit (x,y,z) vector and a magnitude.
@ -71,6 +115,8 @@ public class GeoPoint extends Vector {
}
/** Compute an arc distance between two points.
* Note: this is an angular distance, and not a surface distance, and is therefore independent of planet model.
* For surface distance, see {@link org.apache.lucene.spatial.spatial4j.geo3d.PlanetModel#surfaceDistance(GeoPoint, GeoPoint)}
* @param v is the second point.
* @return the angle, in radians, between the two points.
*/
@ -79,19 +125,27 @@ public class GeoPoint extends Vector {
}
/** Compute the latitude for the point.
*@return the latitude.
* @return the latitude.
*/
public double getLatitude() {
return Math.asin(z / magnitude() );
double lat = this.latitude;//volatile-read once
if (lat == Double.NEGATIVE_INFINITY)
this.latitude = lat = Math.asin(z / magnitude());
return lat;
}
/** Compute the longitude for the point.
* @return the longitude value. Uses 0.0 if there is no computable longitude.
*/
public double getLongitude() {
if (Math.abs(x) < MINIMUM_RESOLUTION && Math.abs(y) < MINIMUM_RESOLUTION)
return 0.0;
return Math.atan2(y,x);
double lon = this.longitude;//volatile-read once
if (lon == Double.NEGATIVE_INFINITY) {
if (Math.abs(x) < MINIMUM_RESOLUTION && Math.abs(y) < MINIMUM_RESOLUTION)
this.longitude = lon = 0.0;
else
this.longitude = lon = Math.atan2(y,x);
}
return lon;
}
/** Compute the linear magnitude of the point.
@ -99,9 +153,10 @@ public class GeoPoint extends Vector {
*/
@Override
public double magnitude() {
if (this.magnitude == Double.NEGATIVE_INFINITY) {
this.magnitude = super.magnitude();
double mag = this.magnitude;//volatile-read once
if (mag == Double.NEGATIVE_INFINITY) {
this.magnitude = mag = super.magnitude();
}
return magnitude;
return mag;
}
}

View File

@ -29,7 +29,7 @@ public class PlanetModel {
/** Mean radius */
public static final double WGS84_MEAN = 6371009.0;
/** Polar radius */
public static final double WGS84_POLAR = 6356752.3;
public static final double WGS84_POLAR = 6356752.314245;
/** Equatorial radius */
public static final double WGS84_EQUATORIAL = 6378137.0;
/** Planet model corresponding to WGS84 */
@ -45,6 +45,8 @@ public class PlanetModel {
public final double inverseC;
public final double inverseAbSquared;
public final double inverseCSquared;
public final double flattening;
public final double squareRatio;
// We do NOT include radius, because all computations in geo3d are in radians, not meters.
// Compute north and south pole for planet model, since these are commonly used.
@ -56,10 +58,12 @@ public class PlanetModel {
this.c = c;
this.inverseAb = 1.0 / ab;
this.inverseC = 1.0 / c;
this.flattening = (ab - c) * inverseAb;
this.squareRatio = (ab * ab - c * c) / (c * c);
this.inverseAbSquared = inverseAb * inverseAb;
this.inverseCSquared = inverseC * inverseC;
this.NORTH_POLE = new GeoPoint(c, 0.0, 0.0, 1.0);
this.SOUTH_POLE = new GeoPoint(c, 0.0, 0.0, -1.0);
this.NORTH_POLE = new GeoPoint(c, 0.0, 0.0, 1.0, Math.PI * 0.5, 0.0);
this.SOUTH_POLE = new GeoPoint(c, 0.0, 0.0, -1.0, -Math.PI * 0.5, 0.0);
}
/** Find the minimum magnitude of all points on the ellipsoid.
@ -74,6 +78,67 @@ public class PlanetModel {
return Math.max(this.ab, this.c);
}
/** Compute surface distance between two points.
* @param p1 is the first point.
* @param p2 is the second point.
* @return the adjusted angle, when multiplied by the mean earth radius, yields a surface distance. This will differ
* from GeoPoint.arcDistance() only when the planet model is not a sphere. @see {@link org.apache.lucene.spatial.spatial4j.geo3d.GeoPoint#arcDistance(GeoPoint)}
*/
public double surfaceDistance(final GeoPoint p1, final GeoPoint p2) {
final double latA = p1.getLatitude();
final double lonA = p1.getLongitude();
final double latB = p2.getLatitude();
final double lonB = p2.getLongitude();
final double L = lonB - lonA;
final double oF = 1.0 - this.flattening;
final double U1 = Math.atan(oF * Math.tan(latA));
final double U2 = Math.atan(oF * Math.tan(latB));
final double sU1 = Math.sin(U1);
final double cU1 = Math.cos(U1);
final double sU2 = Math.sin(U2);
final double cU2 = Math.cos(U2);
double sigma, sinSigma, cosSigma;
double cos2Alpha, cos2SigmaM;
double lambda = L;
double iters = 100;
do {
final double sinLambda = Math.sin(lambda);
final double cosLambda = Math.cos(lambda);
sinSigma = Math.sqrt((cU2 * sinLambda) * (cU2 * sinLambda) + (cU1 * sU2 - sU1 * cU2 * cosLambda)
* (cU1 * sU2 - sU1 * cU2 * cosLambda));
if (Math.abs(sinSigma) < Vector.MINIMUM_RESOLUTION)
return 0.0;
cosSigma = sU1 * sU2 + cU1 * cU2 * cosLambda;
sigma = Math.atan2(sinSigma, cosSigma);
final double sinAlpha = cU1 * cU2 * sinLambda / sinSigma;
cos2Alpha = 1.0 - sinAlpha * sinAlpha;
cos2SigmaM = cosSigma - 2.0 * sU1 * sU2 / cos2Alpha;
final double c = this.flattening * 0.625 * cos2Alpha * (4.0 + this.flattening * (4.0 - 3.0 * cos2Alpha));
final double lambdaP = lambda;
lambda = L + (1.0 - c) * this.flattening * sinAlpha * (sigma + c * sinSigma * (cos2SigmaM + c * cosSigma *
(-1.0 + 2.0 * cos2SigmaM * cos2SigmaM)));
if (Math.abs(lambda - lambdaP) < Vector.MINIMUM_RESOLUTION)
break;
} while (--iters > 0);
if (iters == 0)
return 0.0;
final double uSq = cos2Alpha * this.squareRatio;
final double A = 1.0 + uSq * 0.00006103515625 * (4096.0 + uSq * (-768.0 + uSq * (320.0 - 175.0 * uSq)));
final double B = uSq * 0.0009765625 * (256.0 + uSq * (-128.0 + uSq * (74.0 - 47.0 * uSq)));
final double deltaSigma = B * sinSigma * (cos2SigmaM + B * 0.25 * (cosSigma * (-1.0 + 2.0 * cos2SigmaM * cos2SigmaM) - B * 0.16666666666666666666667 * cos2SigmaM
* (-3.0 + 4.0 * sinSigma * sinSigma) * (-3.0 + 4.0 * cos2SigmaM * cos2SigmaM)));
return this.c * A * (sigma - deltaSigma);
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof PlanetModel))

View File

@ -91,6 +91,8 @@ public class SidedPlane extends Plane implements Membership {
final Vector normalVector, final Vector point1, final Vector point2) {
final Vector pointsVector = new Vector(point1.x - point2.x, point1.y - point2.y, point1.z - point2.z);
final Vector newNormalVector = new Vector(normalVector, pointsVector).normalize();
if (newNormalVector == null)
return null;
// To construct the plane, we now just need D, which is simply the negative of the evaluation of the circle normal vector at one of the points.
return new SidedPlane(insidePoint, newNormalVector, -newNormalVector.dotProduct(point1));
}

View File

@ -24,6 +24,7 @@ import java.util.Random;
import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import com.spatial4j.core.shape.Circle;
import com.spatial4j.core.shape.Point;
import org.apache.lucene.spatial.spatial4j.geo3d.Bounds;
import org.apache.lucene.spatial.spatial4j.geo3d.GeoBBox;
@ -173,17 +174,16 @@ public abstract class Geo3dShapeRectRelationTestCase extends RandomizedShapeTest
protected Geo3dShape generateRandomShape(Point nearP) {
final Point centerPoint = randomPoint();
final int maxDistance = random().nextInt(160) + 20;
final Circle pointZone = ctx.makeCircle(centerPoint, maxDistance);
final int vertexCount = random().nextInt(3) + 3;
while (true) {
final List<GeoPoint> geoPoints = new ArrayList<>();
while (geoPoints.size() < vertexCount) {
final Point point = randomPoint();
if (ctx.getDistCalc().distance(point,centerPoint) > maxDistance)
continue;
final Point point = randomPointIn(pointZone);
final GeoPoint gPt = new GeoPoint(planetModel, point.getY() * DEGREES_TO_RADIANS, point.getX() * DEGREES_TO_RADIANS);
geoPoints.add(gPt);
}
final int convexPointIndex = random().nextInt(vertexCount); //If we get this wrong, hopefully we get IllegalArgumentException
final int convexPointIndex = random().nextInt(vertexCount); //If we get this wrong, hopefully we get IllegalArgumentException
try {
final GeoShape shape = GeoPolygonFactory.makeGeoPolygon(planetModel, geoPoints, convexPointIndex);
return new Geo3dShape(planetModel, shape, ctx);
@ -217,18 +217,15 @@ public abstract class Geo3dShapeRectRelationTestCase extends RandomizedShapeTest
protected Geo3dShape generateRandomShape(Point nearP) {
final Point centerPoint = randomPoint();
final int maxDistance = random().nextInt(160) + 20;
final Circle pointZone = ctx.makeCircle(centerPoint, maxDistance);
final int pointCount = random().nextInt(5) + 1;
final double width = (random().nextInt(89)+1) * DEGREES_TO_RADIANS;
while (true) {
try {
final GeoPath path = new GeoPath(planetModel, width);
int i = 0;
while (i < pointCount) {
final Point nextPoint = randomPoint();
if (ctx.getDistCalc().distance(nextPoint,centerPoint) > maxDistance)
continue;
while (path.points.size() < pointCount) {
final Point nextPoint = randomPointIn(pointZone);
path.addPoint(nextPoint.getY() * DEGREES_TO_RADIANS, nextPoint.getX() * DEGREES_TO_RADIANS);
i++;
}
path.done();
return new Geo3dShape(planetModel, path, ctx);

View File

@ -53,6 +53,7 @@ public class GeoBBoxTest {
public void testBBoxPointWithin() {
GeoBBox box;
GeoPoint gp;
// Standard normal Rect box, not crossing dateline
box = GeoBBoxFactory.makeGeoBBox(PlanetModel.SPHERE, 0.0, -Math.PI * 0.25, -1.0, 1.0);
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, 0.0);
@ -65,6 +66,7 @@ public class GeoBBoxTest {
assertFalse(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -1.1);
assertFalse(box.isWithin(gp));
// Standard normal Rect box, crossing dateline
box = GeoBBoxFactory.makeGeoBBox(PlanetModel.SPHERE, 0.0, -Math.PI * 0.25, Math.PI - 1.0, -Math.PI + 1.0);
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI);
@ -75,8 +77,9 @@ public class GeoBBoxTest {
assertFalse(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI + 1.1);
assertFalse(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI - 1.1);
assertFalse(box.isWithin(gp));
//bad lon: gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI - 1.1);
//assertFalse(box.isWithin(gp));
// Latitude zone rectangle
box = GeoBBoxFactory.makeGeoBBox(PlanetModel.SPHERE, 0.0, -Math.PI * 0.25, -Math.PI, Math.PI);
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI);
@ -87,8 +90,9 @@ public class GeoBBoxTest {
assertFalse(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI + 1.1);
assertTrue(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI - 1.1);
assertTrue(box.isWithin(gp));
//bad lon: gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI - 1.1);
//assertTrue(box.isWithin(gp));
// World
box = GeoBBoxFactory.makeGeoBBox(PlanetModel.SPHERE, Math.PI * 0.5, -Math.PI * 0.5, -Math.PI, Math.PI);
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI);
@ -99,8 +103,8 @@ public class GeoBBoxTest {
assertTrue(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI + 1.1);
assertTrue(box.isWithin(gp));
gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI - 1.1);
assertTrue(box.isWithin(gp));
//bad lat: gp = new GeoPoint(PlanetModel.SPHERE, -0.1, -Math.PI - 1.1);
//assertTrue(box.isWithin(gp));
}

View File

@ -34,7 +34,7 @@ public class GeoConvexPolygonTest {
c.addPoint(0.0, -0.6, false);
c.addPoint(0.1, -0.5, false);
c.addPoint(0.0, -0.4, false);
c.donePoints(false);
c.done(false);
// Sample some points within
gp = new GeoPoint(PlanetModel.SPHERE, 0.0, -0.5);
assertTrue(c.isWithin(gp));
@ -73,7 +73,7 @@ public class GeoConvexPolygonTest {
c.addPoint(0.0, -0.6, false);
c.addPoint(0.1, -0.5, false);
c.addPoint(0.0, -0.4, false);
c.donePoints(false);
c.done(false);
b = c.getBounds(null);
assertFalse(b.checkNoLongitudeBound());

View File

@ -57,10 +57,10 @@ public class GeoModelTest {
assertFalse(bounds.checkNoTopLatitudeBound());
assertFalse(bounds.checkNoLongitudeBound());
assertFalse(bounds.checkNoBottomLatitudeBound());
assertEquals(1.20, bounds.getMaxLatitude(), 0.01);
assertEquals(Math.PI * 0.25 - 0.01, bounds.getMinLatitude(), 0.01);
assertEquals(-0.36, bounds.getLeftLongitude(), 0.01);
assertEquals(0.36, bounds.getRightLongitude(), 0.01);
assertEquals(Math.PI * 0.25 + 0.01, bounds.getMaxLatitude(), 0.00001);
assertEquals(Math.PI * 0.25 - 0.01, bounds.getMinLatitude(), 0.00001);
assertEquals(-0.0125, bounds.getLeftLongitude(), 0.0001);
assertEquals(0.0125, bounds.getRightLongitude(), 0.0001);
circle = new GeoCircle(scaledModel, Math.PI * 0.125, 0.0, 0.01);
assertTrue(circle.isWithin(point2));
@ -70,11 +70,11 @@ public class GeoModelTest {
assertFalse(bounds.checkNoLongitudeBound());
assertFalse(bounds.checkNoTopLatitudeBound());
assertFalse(bounds.checkNoBottomLatitudeBound());
// Asymmetric, as expected
assertEquals(Math.PI * 0.125 - 0.01, bounds.getMinLatitude(), 0.01);
assertEquals(0.74, bounds.getMaxLatitude(), 0.01);
assertEquals(-0.18, bounds.getLeftLongitude(), 0.01);
assertEquals(0.18, bounds.getRightLongitude(), 0.01);
// Symmetric, as expected
assertEquals(Math.PI * 0.125 - 0.01, bounds.getMinLatitude(), 0.00001);
assertEquals(Math.PI * 0.125 + 0.01, bounds.getMaxLatitude(), 0.00001);
assertEquals(-0.0089, bounds.getLeftLongitude(), 0.0001);
assertEquals(0.0089, bounds.getRightLongitude(), 0.0001);
}

View File

@ -30,10 +30,10 @@ public class GeoPointTest extends LuceneTestCase {
@Test
public void testConversion() {
testPointRoundTrip(PlanetModel.SPHERE, 90, 0, 1e-12);
testPointRoundTrip(PlanetModel.SPHERE, -90, 0, 1e-12);
testPointRoundTrip(PlanetModel.WGS84, 90, 0, 1e-12);
testPointRoundTrip(PlanetModel.WGS84, -90, 0, 1e-12);
testPointRoundTrip(PlanetModel.SPHERE, 90 * DistanceUtils.DEGREES_TO_RADIANS, 0, 1e-6);
testPointRoundTrip(PlanetModel.SPHERE, -90 * DistanceUtils.DEGREES_TO_RADIANS, 0, 1e-6);
testPointRoundTrip(PlanetModel.WGS84, 90 * DistanceUtils.DEGREES_TO_RADIANS, 0, 1e-6);
testPointRoundTrip(PlanetModel.WGS84, -90 * DistanceUtils.DEGREES_TO_RADIANS, 0, 1e-6);
final int times = atLeast(100);
for (int i = 0; i < times; i++) {
@ -46,11 +46,35 @@ public class GeoPointTest extends LuceneTestCase {
protected void testPointRoundTrip(PlanetModel planetModel, double pLat, double pLon, double epsilon) {
final GeoPoint p1 = new GeoPoint(planetModel, pLat, pLon);
final GeoPoint p2 = new GeoPoint(planetModel, p1.getLatitude(), p1.getLongitude());
double dist = p1.arcDistance(p2);
// In order to force the reverse conversion, we have to construct a geopoint from just x,y,z
final GeoPoint p2 = new GeoPoint(p1.x, p1.y, p1.z);
// Now, construct the final point based on getLatitude() and getLongitude()
final GeoPoint p3 = new GeoPoint(planetModel, p2.getLatitude(), p2.getLongitude());
double dist = p1.arcDistance(p3);
assertEquals(0, dist, epsilon);
}
@Test
public void testSurfaceDistance() {
final int times = atLeast(100);
for (int i = 0; i < times; i++) {
final double p1Lat = (randomFloat() * 180.0 - 90.0) * DistanceUtils.DEGREES_TO_RADIANS;
final double p1Lon = (randomFloat() * 360.0 - 180.0) * DistanceUtils.DEGREES_TO_RADIANS;
final double p2Lat = (randomFloat() * 180.0 - 90.0) * DistanceUtils.DEGREES_TO_RADIANS;
final double p2Lon = (randomFloat() * 360.0 - 180.0) * DistanceUtils.DEGREES_TO_RADIANS;
final GeoPoint p1 = new GeoPoint(PlanetModel.SPHERE, p1Lat, p1Lon);
final GeoPoint p2 = new GeoPoint(PlanetModel.SPHERE, p2Lat, p2Lon);
final double arcDistance = p1.arcDistance(p2);
// Compute ellipsoid distance; it should agree for a sphere
final double surfaceDistance = PlanetModel.SPHERE.surfaceDistance(p1,p2);
assertEquals(arcDistance, surfaceDistance, 1e-6);
}
}
@Test(expected = IllegalArgumentException.class)
public void testBadLatLon() {
new GeoPoint(PlanetModel.SPHERE, 50.0, 32.2);
}
}