From e6fd37c4a1658813d4a272db2d93f4c8a372480b Mon Sep 17 00:00:00 2001 From: Karl Wright Date: Tue, 5 Apr 2016 03:41:48 -0400 Subject: [PATCH] LUCENE-7176: Hide GeoPath implementation in a factory/interface. --- .../spatial/spatial4j/Geo3dRptTest.java | 26 +- .../Geo3dShapeRectRelationTestCase.java | 15 +- .../Geo3dShapeWGS84ModelRectRelationTest.java | 15 +- .../apache/lucene/spatial3d/Geo3DPoint.java | 4 +- .../lucene/spatial3d/geom/GeoBaseCircle.java | 2 +- .../lucene/spatial3d/geom/GeoBasePath.java | 34 + .../apache/lucene/spatial3d/geom/GeoPath.java | 776 +---------------- .../lucene/spatial3d/geom/GeoPathFactory.java | 39 + .../spatial3d/geom/GeoStandardPath.java | 797 ++++++++++++++++++ .../lucene/spatial3d/TestGeo3DPoint.java | 13 +- .../lucene/spatial3d/geom/GeoPathTest.java | 36 +- 11 files changed, 928 insertions(+), 829 deletions(-) create mode 100644 lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBasePath.java mode change 100755 => 100644 lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java create mode 100644 lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java create mode 100755 lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dRptTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dRptTest.java index aacea2cf5ba..004139e4bfb 100644 --- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dRptTest.java +++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dRptTest.java @@ -29,7 +29,7 @@ import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.apache.lucene.spatial.query.SpatialOperation; import org.apache.lucene.spatial.serialized.SerializedDVStrategy; import org.apache.lucene.spatial3d.geom.GeoBBoxFactory; -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.GeoPolygonFactory; import org.apache.lucene.spatial3d.geom.GeoShape; @@ -95,12 +95,12 @@ public class Geo3dRptTest extends RandomSpatialOpStrategyTestCase { points.add(new GeoPoint(PlanetModel.SPHERE, -57 * DEGREES_TO_RADIANS, 146 * DEGREES_TO_RADIANS)); points.add(new GeoPoint(PlanetModel.SPHERE, 14 * DEGREES_TO_RADIANS, -180 * DEGREES_TO_RADIANS)); points.add(new GeoPoint(PlanetModel.SPHERE, -15 * DEGREES_TO_RADIANS, 153 * DEGREES_TO_RADIANS)); - final GeoPath path = new GeoPath(PlanetModel.SPHERE, 29 * DEGREES_TO_RADIANS); - path.addPoint(55.0 * DEGREES_TO_RADIANS, -26.0 * DEGREES_TO_RADIANS); - path.addPoint(-90.0 * DEGREES_TO_RADIANS, 0.0); - path.addPoint(54.0 * DEGREES_TO_RADIANS, 165.0 * DEGREES_TO_RADIANS); - path.addPoint(-90.0 * DEGREES_TO_RADIANS, 0.0); - path.done(); + final GeoPoint[] pathPoints = new GeoPoint[] { + new GeoPoint(PlanetModel.SPHERE, 55.0 * DEGREES_TO_RADIANS, -26.0 * DEGREES_TO_RADIANS), + new GeoPoint(PlanetModel.SPHERE, -90.0 * DEGREES_TO_RADIANS, 0.0), + new GeoPoint(PlanetModel.SPHERE, 54.0 * DEGREES_TO_RADIANS, 165.0 * DEGREES_TO_RADIANS), + new GeoPoint(PlanetModel.SPHERE, -90.0 * DEGREES_TO_RADIANS, 0.0)}; + final GeoShape path = GeoPathFactory.makeGeoPath(PlanetModel.SPHERE, 29 * DEGREES_TO_RADIANS, pathPoints); final Shape shape = new Geo3dShape(path,ctx); final Rectangle rect = ctx.makeRectangle(131, 143, 39, 54); testOperation(rect,SpatialOperation.Intersects,shape,true); @@ -199,14 +199,14 @@ public class Geo3dRptTest extends RandomSpatialOpStrategyTestCase { // Paths final int pointCount = random().nextInt(5) + 1; final double width = (random().nextInt(89)+1) * DEGREES_TO_RADIANS; + final GeoPoint[] points = new GeoPoint[pointCount]; while (true) { + for (int i = 0; i < pointCount; i++) { + final Point nextPoint = randomPoint(); + points[i] = new GeoPoint(PlanetModel.SPHERE, nextPoint.getY() * DEGREES_TO_RADIANS, nextPoint.getX() * DEGREES_TO_RADIANS); + } try { - final GeoPath path = new GeoPath(PlanetModel.SPHERE, width); - for (int i = 0; i < pointCount; i++) { - final Point nextPoint = randomPoint(); - path.addPoint(nextPoint.getY() * DEGREES_TO_RADIANS, nextPoint.getX() * DEGREES_TO_RADIANS); - } - path.done(); + final GeoShape path = GeoPathFactory.makeGeoPath(PlanetModel.SPHERE, width, points); return new Geo3dShape(path, ctx); } catch (IllegalArgumentException e) { // This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java index 2c41f79b880..f8e51d17ed9 100644 --- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java +++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeRectRelationTestCase.java @@ -30,7 +30,7 @@ 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.GeoPath; +import org.apache.lucene.spatial3d.geom.GeoPathFactory; import org.apache.lucene.spatial3d.geom.GeoPoint; import org.apache.lucene.spatial3d.geom.GeoPolygonFactory; import org.apache.lucene.spatial3d.geom.GeoShape; @@ -221,14 +221,15 @@ public abstract class Geo3dShapeRectRelationTestCase extends RandomizedShapeTest final Circle pointZone = ctx.makeCircle(centerPoint, maxDistance); final int pointCount = random().nextInt(5) + 1; final double width = (random().nextInt(89)+1) * DEGREES_TO_RADIANS; + final GeoPoint[] points = new GeoPoint[pointCount]; while (true) { + for (int i = 0; i < pointCount; i++) { + final Point nextPoint = randomPointIn(pointZone); + points[i] = new GeoPoint(planetModel, nextPoint.getY() * DEGREES_TO_RADIANS, nextPoint.getX() * DEGREES_TO_RADIANS); + } + try { - final GeoPath path = new GeoPath(planetModel, width); - for (int i = 0; i < pointCount; i++) { - final Point nextPoint = randomPointIn(pointZone); - path.addPoint(nextPoint.getY() * DEGREES_TO_RADIANS, nextPoint.getX() * DEGREES_TO_RADIANS); - } - path.done(); + final GeoShape path = GeoPathFactory.makeGeoPath(planetModel, width, points); return new Geo3dShape(planetModel, path, ctx); } catch (IllegalArgumentException e) { // This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where diff --git a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java index 00a0903ff36..43b88ca5912 100644 --- a/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java +++ b/lucene/spatial-extras/src/test/org/apache/lucene/spatial/spatial4j/Geo3dShapeWGS84ModelRectRelationTest.java @@ -21,6 +21,7 @@ import org.apache.lucene.spatial3d.geom.GeoBBox; import org.apache.lucene.spatial3d.geom.GeoBBoxFactory; import org.apache.lucene.spatial3d.geom.GeoCircleFactory; import org.apache.lucene.spatial3d.geom.GeoCircle; +import org.apache.lucene.spatial3d.geom.GeoPathFactory; import org.apache.lucene.spatial3d.geom.GeoPath; import org.apache.lucene.spatial3d.geom.GeoPoint; import org.apache.lucene.spatial3d.geom.PlanetModel; @@ -36,9 +37,9 @@ public class Geo3dShapeWGS84ModelRectRelationTest extends Geo3dShapeRectRelation public void testFailure1() { final GeoBBox rect = GeoBBoxFactory.makeGeoBBox(planetModel, 90 * RADIANS_PER_DEGREE, 74 * RADIANS_PER_DEGREE, 40 * RADIANS_PER_DEGREE, 60 * RADIANS_PER_DEGREE); - final GeoPath path = new GeoPath(planetModel, 4 * RADIANS_PER_DEGREE); - path.addPoint(84.4987594274 * RADIANS_PER_DEGREE, -22.8345484402 * RADIANS_PER_DEGREE); - path.done(); + final GeoPoint[] pathPoints = new GeoPoint[] { + new GeoPoint(planetModel, 84.4987594274 * RADIANS_PER_DEGREE, -22.8345484402 * RADIANS_PER_DEGREE)}; + final GeoPath path = GeoPathFactory.makeGeoPath(planetModel, 4 * RADIANS_PER_DEGREE, pathPoints); assertTrue(GeoArea.DISJOINT == rect.getRelationship(path)); // This is what the test failure claimed... //assertTrue(GeoArea.CONTAINS == rect.getRelationship(path)); @@ -75,10 +76,10 @@ public class Geo3dShapeWGS84ModelRectRelationTest extends Geo3dShapeRectRelation */ final GeoBBox rect = GeoBBoxFactory.makeGeoBBox(planetModel, 16 * RADIANS_PER_DEGREE, 16 * RADIANS_PER_DEGREE, 4 * RADIANS_PER_DEGREE, 36 * RADIANS_PER_DEGREE); final GeoPoint pt = new GeoPoint(planetModel, 16 * RADIANS_PER_DEGREE, 23.81626064835212 * RADIANS_PER_DEGREE); - final GeoPath path = new GeoPath(planetModel, 88 * RADIANS_PER_DEGREE); - path.addPoint(46.6369060853 * RADIANS_PER_DEGREE, -79.8452213228 * RADIANS_PER_DEGREE); - path.addPoint(54.9779334519 * RADIANS_PER_DEGREE, 132.029177424 * RADIANS_PER_DEGREE); - path.done(); + final GeoPoint[] pathPoints = new GeoPoint[]{ + new GeoPoint(planetModel, 46.6369060853 * RADIANS_PER_DEGREE, -79.8452213228 * RADIANS_PER_DEGREE), + new GeoPoint(planetModel, 54.9779334519 * RADIANS_PER_DEGREE, 132.029177424 * RADIANS_PER_DEGREE)}; + final GeoPath path = GeoPathFactory.makeGeoPath(planetModel, 88 * RADIANS_PER_DEGREE, pathPoints); System.out.println("rect=" + rect); // Rectangle is within path (this is wrong; it's on the other side. Should be OVERLAPS) assertTrue(GeoArea.OVERLAPS == rect.getRelationship(path)); diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java index 944ce39b97d..be8990f9508 100644 --- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java @@ -31,7 +31,7 @@ import org.apache.lucene.spatial3d.geom.PlanetModel; import org.apache.lucene.spatial3d.geom.GeoCircleFactory; import org.apache.lucene.spatial3d.geom.GeoBBoxFactory; import org.apache.lucene.spatial3d.geom.GeoPolygonFactory; -import org.apache.lucene.spatial3d.geom.GeoPath; +import org.apache.lucene.spatial3d.geom.GeoPathFactory; import org.apache.lucene.spatial3d.geom.GeoCompositePolygon; import org.apache.lucene.spatial3d.geom.GeoPolygon; import org.apache.lucene.search.Query; @@ -172,7 +172,7 @@ public final class Geo3DPoint extends Field { GeoUtils.checkLongitude(longitude); points[i] = new GeoPoint(PlanetModel.WGS84, fromDegrees(latitude), fromDegrees(longitude)); } - final GeoShape shape = new GeoPath(PlanetModel.WGS84, fromMeters(pathWidthMeters), points); + final GeoShape shape = GeoPathFactory.makeGeoPath(PlanetModel.WGS84, fromMeters(pathWidthMeters), points); return newShapeQuery(field, shape); } diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCircle.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCircle.java index b34313a495b..0599c91aa5c 100644 --- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCircle.java +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBaseCircle.java @@ -19,7 +19,7 @@ package org.apache.lucene.spatial3d.geom; /** * GeoCircles have all the characteristics of GeoBaseDistanceShapes, plus GeoSizeable. * - * @lucene.experimental + * @lucene.internal */ abstract class GeoBaseCircle extends GeoBaseDistanceShape implements GeoCircle { diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBasePath.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBasePath.java new file mode 100644 index 00000000000..c726b3af2ba --- /dev/null +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoBasePath.java @@ -0,0 +1,34 @@ +/* + * 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; + +/** + * GeoPaths have all the characteristics of GeoBaseDistanceShapes. + * + * @lucene.internal + */ +abstract class GeoBasePath extends GeoBaseDistanceShape implements GeoPath { + + /** Constructor. + *@param planetModel is the planet model to use. + */ + public GeoBasePath(final PlanetModel planetModel) { + super(planetModel); + } + +} + diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java old mode 100755 new mode 100644 index a5b8b9b0660..48a613bf258 --- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPath.java @@ -16,782 +16,10 @@ */ package org.apache.lucene.spatial3d.geom; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** - * GeoShape representing a path across the surface of the globe, - * with a specified half-width. Path is described by a series of points. - * Distances are measured from the starting point along the path, and then at right - * angles to the path. + * Interface describing a path. * * @lucene.experimental */ -public class GeoPath extends GeoBaseDistanceShape { - /** The cutoff angle (width) */ - protected final double cutoffAngle; - - /** Sine of cutoff angle */ - protected final double sinAngle; - /** Cosine of cutoff angle */ - protected final double cosAngle; - - /** The original list of path points */ - protected final List points = new ArrayList(); - - /** A list of SegmentEndpoints */ - protected List endPoints; - /** A list of PathSegments */ - protected List segments; - - /** A point on the edge */ - protected GeoPoint[] edgePoints; - - /** Set to true if path has been completely constructed */ - protected boolean isDone = false; - - /** Constructor. - *@param planetModel is the planet model. - *@param maxCutoffAngle is the width of the path, measured as an angle. - *@param pathPoints are the points in the path. - */ - public GeoPath(final PlanetModel planetModel, final double maxCutoffAngle, final GeoPoint[] pathPoints) { - this(planetModel, maxCutoffAngle); - Collections.addAll(points, pathPoints); - done(); - } - - /** Piece-wise constructor. Use in conjunction with addPoint() and done(). - *@param planetModel is the planet model. - *@param maxCutoffAngle is the width of the path, measured as an angle. - */ - public GeoPath(final PlanetModel planetModel, final double maxCutoffAngle) { - super(planetModel); - if (maxCutoffAngle <= 0.0 || maxCutoffAngle > Math.PI * 0.5) - throw new IllegalArgumentException("Cutoff angle out of bounds"); - this.cutoffAngle = maxCutoffAngle; - this.cosAngle = Math.cos(maxCutoffAngle); - this.sinAngle = Math.sin(maxCutoffAngle); - } - - /** Add a point to the path. - *@param lat is the latitude of the point. - *@param lon is the longitude of the point. - */ - public void addPoint(final double lat, final double lon) { - if (isDone) - throw new IllegalStateException("Can't call addPoint() if done() already called"); - points.add(new GeoPoint(planetModel, lat, lon)); - } - - /** Complete the path. - */ - 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"); - 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(); - - // First, build all segments. We'll then go back and build corresponding segment endpoints. - GeoPoint lastPoint = null; - for (final GeoPoint end : points) { - if (lastPoint != null) { - final Plane normalizedConnectingPlane = new Plane(lastPoint, end); - if (normalizedConnectingPlane == null) { - continue; - } - segments.add(new PathSegment(planetModel, lastPoint, end, normalizedConnectingPlane, cutoffOffset)); - } - lastPoint = end; - } - - if (segments.size() == 0) { - // Simple circle - 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 GeoPoint point = points.get(0); - - // Construct normal plane - final Plane normalPlane = Plane.constructNormalizedZPlane(upperPoint, lowerPoint, point); - - final SegmentEndpoint onlyEndpoint = new SegmentEndpoint(point, normalPlane, upperPoint, lowerPoint); - endPoints.add(onlyEndpoint); - this.edgePoints = new GeoPoint[]{onlyEndpoint.circlePlane.getSampleIntersectionPoint(planetModel, normalPlane)}; - return; - } - - // Create segment endpoints. Use an appropriate constructor for the start and end of the path. - for (int i = 0; i < segments.size(); i++) { - final PathSegment currentSegment = segments.get(i); - - if (i == 0) { - // Starting endpoint - final SegmentEndpoint startEndpoint = new SegmentEndpoint(currentSegment.start, - currentSegment.startCutoffPlane, currentSegment.ULHC, currentSegment.LLHC); - endPoints.add(startEndpoint); - this.edgePoints = new GeoPoint[]{currentSegment.ULHC}; - continue; - } - - // General intersection case - final PathSegment prevSegment = segments.get(i-1); - // We construct four separate planes, and evaluate which one includes all interior points with least overlap - final SidedPlane candidate1 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, prevSegment.URHC, currentSegment.ULHC, currentSegment.LLHC); - final SidedPlane candidate2 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, currentSegment.ULHC, currentSegment.LLHC, prevSegment.LRHC); - final SidedPlane candidate3 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, currentSegment.LLHC, prevSegment.LRHC, prevSegment.URHC); - final SidedPlane candidate4 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, prevSegment.LRHC, prevSegment.URHC, currentSegment.ULHC); - - if (candidate1 == null && candidate2 == null && candidate3 == null && candidate4 == null) { - // The planes are identical. We wouldn't need a circle at all except for the possibility of - // backing up, which is hard to detect here. - final SegmentEndpoint midEndpoint = new SegmentEndpoint(currentSegment.start, - prevSegment.endCutoffPlane, currentSegment.startCutoffPlane, currentSegment.ULHC, currentSegment.LLHC); - //don't need a circle at all. Special constructor... - endPoints.add(midEndpoint); - } else { - endPoints.add(new SegmentEndpoint(currentSegment.start, - prevSegment.endCutoffPlane, currentSegment.startCutoffPlane, - prevSegment.URHC, prevSegment.LRHC, - currentSegment.ULHC, currentSegment.LLHC, - candidate1, candidate2, candidate3, candidate4)); - } - } - // Do final endpoint - final PathSegment lastSegment = segments.get(segments.size()-1); - endPoints.add(new SegmentEndpoint(lastSegment.end, - lastSegment.endCutoffPlane, lastSegment.URHC, lastSegment.LRHC)); - - } - - @Override - protected double distance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { - // Algorithm: - // (1) If the point is within any of the segments along the path, return that value. - // (2) If the point is within any of the segment end circles along the path, return that value. - double currentDistance = 0.0; - for (PathSegment segment : segments) { - double distance = segment.pathDistance(planetModel, distanceStyle, x,y,z); - if (distance != Double.MAX_VALUE) - return currentDistance + distance; - currentDistance += segment.fullPathDistance(distanceStyle); - } - - int segmentIndex = 0; - currentDistance = 0.0; - for (SegmentEndpoint endpoint : endPoints) { - double distance = endpoint.pathDistance(distanceStyle, x, y, z); - if (distance != Double.MAX_VALUE) - return currentDistance + distance; - if (segmentIndex < segments.size()) - currentDistance += segments.get(segmentIndex++).fullPathDistance(distanceStyle); - } - - return Double.MAX_VALUE; - } - - @Override - protected double outsideDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { - double minDistance = Double.MAX_VALUE; - for (final SegmentEndpoint endpoint : endPoints) { - final double newDistance = endpoint.outsideDistance(distanceStyle, x,y,z); - if (newDistance < minDistance) - minDistance = newDistance; - } - for (final PathSegment segment : segments) { - final double newDistance = segment.outsideDistance(planetModel, distanceStyle, x, y, z); - if (newDistance < minDistance) - minDistance = newDistance; - } - return minDistance; - } - - @Override - public boolean isWithin(final double x, final double y, final double z) { - for (SegmentEndpoint pathPoint : endPoints) { - if (pathPoint.isWithin(x, y, z)) - return true; - } - for (PathSegment pathSegment : segments) { - if (pathSegment.isWithin(x, y, z)) - return true; - } - return false; - } - - @Override - public GeoPoint[] getEdgePoints() { - return edgePoints; - } - - @Override - public boolean intersects(final Plane plane, final GeoPoint[] notablePoints, final Membership... bounds) { - // We look for an intersection with any of the exterior edges of the path. - // We also have to look for intersections with the cones described by the endpoints. - // Return "true" if any such intersections are found. - - // For plane intersections, the basic idea is to come up with an equation of the line that is - // the intersection (if any). Then, find the intersections with the unit sphere (if any). If - // any of the intersection points are within the bounds, then we've detected an intersection. - // Well, sort of. We can detect intersections also due to overlap of segments with each other. - // But that's an edge case and we won't be optimizing for it. - //System.err.println(" Looking for intersection of plane "+plane+" with path "+this); - for (final SegmentEndpoint pathPoint : endPoints) { - if (pathPoint.intersects(planetModel, plane, notablePoints, bounds)) { - return true; - } - } - - for (final PathSegment pathSegment : segments) { - if (pathSegment.intersects(planetModel, plane, notablePoints, bounds)) { - return true; - } - } - - return false; - } - - @Override - public void getBounds(Bounds bounds) { - super.getBounds(bounds); - // For building bounds, order matters. We want to traverse - // never more than 180 degrees longitude at a pop or we risk having the - // bounds object get itself inverted. So do the edges first. - for (PathSegment pathSegment : segments) { - pathSegment.getBounds(planetModel, bounds); - } - for (SegmentEndpoint pathPoint : endPoints) { - pathPoint.getBounds(planetModel, bounds); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof GeoPath)) - return false; - GeoPath p = (GeoPath) o; - if (!super.equals(p)) - return false; - if (cutoffAngle != p.cutoffAngle) - return false; - return points.equals(p.points); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - long temp = Double.doubleToLongBits(cutoffAngle); - result = 31 * result + (int) (temp ^ (temp >>> 32)); - result = 31 * result + points.hashCode(); - return result; - } - - @Override - public String toString() { - return "GeoPath: {planetmodel=" + planetModel+", width=" + cutoffAngle + "(" + cutoffAngle * 180.0 / Math.PI + "), points={" + points + "}}"; - } - - /** - * This is precalculated data for segment endpoint. - * Note well: This is not necessarily a circle. There are four cases: - * (1) The path consists of a single endpoint. In this case, we build a simple circle with the proper cutoff offset. - * (2) This is the end of a path. The circle plane must be constructed to go through two supplied points and be perpendicular to a connecting plane. - * (2.5) Intersection, but the path on both sides is linear. We generate a circle, but we use the cutoff planes to limit its influence in the straight line case. - * (3) This is an intersection in a path. We are supplied FOUR planes. If there are intersections within bounds for both upper and lower, then - * we generate no circle at all. If there is one intersection only, then we generate a plane that includes that intersection, as well as the remaining - * cutoff plane/edge plane points. - */ - public static class SegmentEndpoint { - /** The center point of the endpoint */ - public final GeoPoint point; - /** A plane describing the circle */ - public final SidedPlane circlePlane; - /** Pertinent cutoff planes from adjoining segments */ - public final Membership[] cutoffPlanes; - /** Notable points for this segment endpoint */ - public final GeoPoint[] notablePoints; - /** No notable points from the circle itself */ - public final static GeoPoint[] circlePoints = new GeoPoint[0]; - /** Null membership */ - public final static Membership[] NO_MEMBERSHIP = new Membership[0]; - - /** Base case. Does nothing at all. - */ - public SegmentEndpoint(final GeoPoint point) { - this.point = point; - this.circlePlane = null; - this.cutoffPlanes = null; - this.notablePoints = null; - } - - /** Constructor for case (1). - * Generate a simple circle cutoff plane. - *@param point is the center point. - *@param upperPoint is a point that must be on the circle plane. - *@param lowerPoint is another point that must be on the circle plane. - */ - public SegmentEndpoint(final GeoPoint point, final Plane normalPlane, final GeoPoint upperPoint, final GeoPoint lowerPoint) { - this.point = 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 = NO_MEMBERSHIP; - this.notablePoints = circlePoints; - } - - /** Constructor for case (2). - * Generate an endpoint, given a single cutoff plane plus upper and lower edge points. - *@param point is the center point. - *@param cutoffPlane is the plane from the adjoining path segment marking the boundary between this endpoint and that segment. - *@param topEdgePoint is a point on the cutoffPlane that should be also on the circle plane. - *@param bottomEdgePoint is another point on the cutoffPlane that should be also on the circle plane. - */ - public SegmentEndpoint(final GeoPoint point, - final SidedPlane cutoffPlane, final GeoPoint topEdgePoint, final GeoPoint bottomEdgePoint) { - this.point = point; - this.cutoffPlanes = new Membership[]{new SidedPlane(cutoffPlane)}; - this.notablePoints = new GeoPoint[]{topEdgePoint, bottomEdgePoint}; - // 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. - this.circlePlane = SidedPlane.constructNormalizedPerpendicularSidedPlane(point, cutoffPlane, topEdgePoint, bottomEdgePoint); - } - - /** Constructor for case (2.5). - * Generate an endpoint, given two cutoff planes plus upper and lower edge points. - *@param point is the center. - *@param cutoffPlane1 is one adjoining path segment cutoff plane. - *@param cutoffPlane2 is another adjoining path segment cutoff plane. - *@param topEdgePoint is a point on the cutoffPlane that should be also on the circle plane. - *@param bottomEdgePoint is another point on the cutoffPlane that should be also on the circle plane. - */ - public SegmentEndpoint(final GeoPoint point, - final SidedPlane cutoffPlane1, final SidedPlane cutoffPlane2, final GeoPoint topEdgePoint, final GeoPoint bottomEdgePoint) { - this.point = point; - this.cutoffPlanes = new Membership[]{new SidedPlane(cutoffPlane1), new SidedPlane(cutoffPlane2)}; - this.notablePoints = new GeoPoint[]{topEdgePoint, bottomEdgePoint}; - // 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. - this.circlePlane = SidedPlane.constructNormalizedPerpendicularSidedPlane(point, cutoffPlane1, topEdgePoint, bottomEdgePoint); - } - - /** Constructor for case (3). - * Generate an endpoint for an intersection, given four points. - *@param point is the center. - *@param prevCutoffPlane is the previous adjoining segment cutoff plane. - *@param nextCutoffPlane is the next path segment cutoff plane. - *@param notCand2Point is a point NOT on candidate2. - *@param notCand1Point is a point NOT on candidate1. - *@param notCand3Point is a point NOT on candidate3. - *@param notCand4Point is a point NOT on candidate4. - *@param candidate1 one of four candidate circle planes. - *@param candidate2 one of four candidate circle planes. - *@param candidate3 one of four candidate circle planes. - *@param candidate4 one of four candidate circle planes. - */ - public SegmentEndpoint(final GeoPoint point, - final SidedPlane prevCutoffPlane, final SidedPlane nextCutoffPlane, - final GeoPoint notCand2Point, final GeoPoint notCand1Point, - final GeoPoint notCand3Point, final GeoPoint notCand4Point, - final SidedPlane candidate1, final SidedPlane candidate2, final SidedPlane candidate3, final SidedPlane candidate4) { - // Note: What we really need is a single plane that goes through all four points. - // Since that's not possible in the ellipsoid case (because three points determine a plane, not four), we - // need an approximation that at least creates a boundary that has no interruptions. - // There are three obvious choices for the third point: either (a) one of the two remaining points, or (b) the top or bottom edge - // intersection point. (a) has no guarantee of continuity, while (b) is capable of producing something very far from a circle if - // the angle between segments is acute. - // The solution is to look for the side (top or bottom) that has an intersection within the shape. We use the two points from - // the opposite side to determine the plane, AND we pick the third to be either of the two points on the intersecting side - // PROVIDED that the other point is within the final circle we come up with. - this.point = point; - - // We construct four separate planes, and evaluate which one includes all interior points with least overlap - // (Constructed beforehand because we need them for degeneracy check) - - final boolean cand1IsOtherWithin = candidate1!=null?candidate1.isWithin(notCand1Point):false; - final boolean cand2IsOtherWithin = candidate2!=null?candidate2.isWithin(notCand2Point):false; - final boolean cand3IsOtherWithin = candidate3!=null?candidate3.isWithin(notCand3Point):false; - final boolean cand4IsOtherWithin = candidate4!=null?candidate4.isWithin(notCand4Point):false; - - if (cand1IsOtherWithin && cand2IsOtherWithin && cand3IsOtherWithin && cand4IsOtherWithin) { - // The only way we should see both within is if all four points are coplanar. In that case, we default to the simplest treatment. - this.circlePlane = candidate1; // doesn't matter which - this.notablePoints = new GeoPoint[]{notCand2Point, notCand3Point, notCand1Point, notCand4Point}; - this.cutoffPlanes = new Membership[]{new SidedPlane(prevCutoffPlane), new SidedPlane(nextCutoffPlane)}; - } else if (cand1IsOtherWithin) { - // Use candidate1, and DON'T include prevCutoffPlane in the cutoff planes list - this.circlePlane = candidate1; - this.notablePoints = new GeoPoint[]{notCand2Point, notCand3Point, notCand4Point}; - this.cutoffPlanes = new Membership[]{new SidedPlane(nextCutoffPlane)}; - } else if (cand2IsOtherWithin) { - // Use candidate2 - this.circlePlane = candidate2; - this.notablePoints = new GeoPoint[]{notCand3Point, notCand4Point, notCand1Point}; - this.cutoffPlanes = new Membership[]{new SidedPlane(nextCutoffPlane)}; - } else if (cand3IsOtherWithin) { - this.circlePlane = candidate3; - this.notablePoints = new GeoPoint[]{notCand4Point, notCand1Point, notCand2Point}; - this.cutoffPlanes = new Membership[]{new SidedPlane(prevCutoffPlane)}; - } else if (cand4IsOtherWithin) { - this.circlePlane = candidate4; - this.notablePoints = new GeoPoint[]{notCand1Point, notCand2Point, notCand3Point}; - this.cutoffPlanes = new Membership[]{new SidedPlane(prevCutoffPlane)}; - } else { - // dunno what happened - throw new RuntimeException("Couldn't come up with a plane through three points that included the fourth"); - } - } - - /** Check if point is within this endpoint. - *@param point is the point. - *@return true of within. - */ - public boolean isWithin(final Vector point) { - if (circlePlane == null) - return false; - if (!circlePlane.isWithin(point)) - return false; - for (final Membership m : cutoffPlanes) { - if (!m.isWithin(point)) { - return false; - } - } - return true; - } - - /** Check if point is within this endpoint. - *@param x is the point x. - *@param y is the point y. - *@param z is the point z. - *@return true of within. - */ - public boolean isWithin(final double x, final double y, final double z) { - if (circlePlane == null) - return false; - if (!circlePlane.isWithin(x, y, z)) - return false; - for (final Membership m : cutoffPlanes) { - if (!m.isWithin(x,y,z)) { - return false; - } - } - return true; - } - - /** Compute interior path distance. - *@param distanceStyle is the distance style. - *@param x is the point x. - *@param y is the point y. - *@param z is the point z. - *@return the distance metric. - */ - public double pathDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { - if (!isWithin(x,y,z)) - return Double.MAX_VALUE; - return distanceStyle.computeDistance(this.point, x, y, z); - } - - /** Compute external distance. - *@param distanceStyle is the distance style. - *@param x is the point x. - *@param y is the point y. - *@param z is the point z. - *@return the distance metric. - */ - public double outsideDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { - return distanceStyle.computeDistance(this.point, x, y, z); - } - - /** Determine if this endpoint intersects a specified plane. - *@param planetModel is the planet model. - *@param p is the plane. - *@param notablePoints are the points associated with the plane. - *@param bounds are any bounds which the intersection must lie within. - *@return true if there is a matching intersection. - */ - public boolean intersects(final PlanetModel planetModel, final Plane p, final GeoPoint[] notablePoints, final Membership[] bounds) { - //System.err.println(" looking for intersection between plane "+p+" and circle "+circlePlane+" on proper side of "+cutoffPlanes+" within "+bounds); - if (circlePlane == null) - return false; - return circlePlane.intersects(planetModel, p, notablePoints, this.notablePoints, bounds, this.cutoffPlanes); - } - - /** Get the bounds for a segment endpoint. - *@param planetModel is the planet model. - *@param bounds are the bounds to be modified. - */ - public void getBounds(final PlanetModel planetModel, Bounds bounds) { - bounds.addPoint(point); - if (circlePlane == null) - return; - bounds.addPlane(planetModel, circlePlane); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof SegmentEndpoint)) - return false; - SegmentEndpoint other = (SegmentEndpoint) o; - return point.equals(other.point); - } - - @Override - public int hashCode() { - return point.hashCode(); - } - - @Override - public String toString() { - return point.toString(); - } - } - - /** - * This is the pre-calculated data for a path segment. - */ - public static class PathSegment { - /** Starting point of the segment */ - public final GeoPoint start; - /** End point of the segment */ - public final GeoPoint end; - /** Place to keep any complete segment distances we've calculated so far */ - public final Map fullDistanceCache = new HashMap(); - /** Normalized plane connecting the two points and going through world center */ - public final Plane normalizedConnectingPlane; - /** Cutoff plane parallel to connecting plane representing one side of the path segment */ - public final SidedPlane upperConnectingPlane; - /** Cutoff plane parallel to connecting plane representing the other side of the path segment */ - public final SidedPlane lowerConnectingPlane; - /** Plane going through the center and start point, marking the start edge of the segment */ - public final SidedPlane startCutoffPlane; - /** Plane going through the center and end point, marking the end edge of the segment */ - public final SidedPlane endCutoffPlane; - /** Upper right hand corner of segment */ - public final GeoPoint URHC; - /** Lower right hand corner of segment */ - public final GeoPoint LRHC; - /** Upper left hand corner of segment */ - public final GeoPoint ULHC; - /** Lower left hand corner of segment */ - public final GeoPoint LLHC; - /** Notable points for the upper connecting plane */ - public final GeoPoint[] upperConnectingPlanePoints; - /** Notable points for the lower connecting plane */ - public final GeoPoint[] lowerConnectingPlanePoints; - /** Notable points for the start cutoff plane */ - public final GeoPoint[] startCutoffPlanePoints; - /** Notable points for the end cutoff plane */ - public final GeoPoint[] endCutoffPlanePoints; - - /** Construct a path segment. - *@param planetModel is the planet model. - *@param start is the starting point. - *@param end is the ending point. - *@param normalizedConnectingPlane is the connecting plane. - *@param planeBoundingOffset is the linear offset from the connecting plane to either side. - */ - public PathSegment(final PlanetModel planetModel, final GeoPoint start, final GeoPoint end, - final Plane normalizedConnectingPlane, final double planeBoundingOffset) { - this.start = start; - this.end = end; - this.normalizedConnectingPlane = normalizedConnectingPlane; - - // Either start or end should be on the correct side - upperConnectingPlane = new SidedPlane(start, normalizedConnectingPlane, -planeBoundingOffset); - lowerConnectingPlane = new SidedPlane(start, normalizedConnectingPlane, planeBoundingOffset); - // Cutoff planes use opposite endpoints as correct side examples - startCutoffPlane = new SidedPlane(end, normalizedConnectingPlane, start); - endCutoffPlane = new SidedPlane(start, normalizedConnectingPlane, end); - final Membership[] upperSide = new Membership[]{upperConnectingPlane}; - final Membership[] lowerSide = new Membership[]{lowerConnectingPlane}; - final Membership[] startSide = new Membership[]{startCutoffPlane}; - final Membership[] endSide = new Membership[]{endCutoffPlane}; - GeoPoint[] points; - points = upperConnectingPlane.findIntersections(planetModel, startCutoffPlane, lowerSide, endSide); - if (points.length == 0) { - throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); - } - this.ULHC = points[0]; - points = upperConnectingPlane.findIntersections(planetModel, endCutoffPlane, lowerSide, startSide); - if (points.length == 0) { - throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); - } - this.URHC = points[0]; - points = lowerConnectingPlane.findIntersections(planetModel, startCutoffPlane, upperSide, endSide); - if (points.length == 0) { - throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); - } - this.LLHC = points[0]; - points = lowerConnectingPlane.findIntersections(planetModel, endCutoffPlane, upperSide, startSide); - if (points.length == 0) { - throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); - } - this.LRHC = points[0]; - upperConnectingPlanePoints = new GeoPoint[]{ULHC, URHC}; - lowerConnectingPlanePoints = new GeoPoint[]{LLHC, LRHC}; - startCutoffPlanePoints = new GeoPoint[]{ULHC, LLHC}; - endCutoffPlanePoints = new GeoPoint[]{URHC, LRHC}; - } - - /** Compute the full distance along this path segment. - *@param distanceStyle is the distance style. - *@return the distance metric. - */ - public double fullPathDistance(final DistanceStyle distanceStyle) { - synchronized (fullDistanceCache) { - Double dist = fullDistanceCache.get(distanceStyle); - if (dist == null) { - dist = new Double(distanceStyle.computeDistance(start, end.x, end.y, end.z)); - fullDistanceCache.put(distanceStyle, dist); - } - return dist.doubleValue(); - } - } - - /** Check if point is within this segment. - *@param point is the point. - *@return true of within. - */ - public boolean isWithin(final Vector point) { - return startCutoffPlane.isWithin(point) && - endCutoffPlane.isWithin(point) && - upperConnectingPlane.isWithin(point) && - lowerConnectingPlane.isWithin(point); - } - - /** Check if point is within this segment. - *@param x is the point x. - *@param y is the point y. - *@param z is the point z. - *@return true of within. - */ - public boolean isWithin(final double x, final double y, final double z) { - return startCutoffPlane.isWithin(x, y, z) && - endCutoffPlane.isWithin(x, y, z) && - upperConnectingPlane.isWithin(x, y, z) && - lowerConnectingPlane.isWithin(x, y, z); - } - - /** Compute interior path distance. - *@param planetModel is the planet model. - *@param distanceStyle is the distance style. - *@param x is the point x. - *@param y is the point y. - *@param z is the point z. - *@return the distance metric. - */ - public double pathDistance(final PlanetModel planetModel, final DistanceStyle distanceStyle, final double x, final double y, final double z) { - if (!isWithin(x,y,z)) - return Double.MAX_VALUE; - - // (1) Compute normalizedPerpPlane. If degenerate, then return point distance from start to point. - // Want no allocations or expensive operations! so we do this the hard way - final double perpX = normalizedConnectingPlane.y * z - normalizedConnectingPlane.z * y; - final double perpY = normalizedConnectingPlane.z * x - normalizedConnectingPlane.x * z; - final double perpZ = normalizedConnectingPlane.x * y - normalizedConnectingPlane.y * x; - final double magnitude = Math.sqrt(perpX * perpX + perpY * perpY + perpZ * perpZ); - if (Math.abs(magnitude) < Vector.MINIMUM_RESOLUTION) - return distanceStyle.computeDistance(start, x,y,z); - final double normFactor = 1.0/magnitude; - final Plane normalizedPerpPlane = new Plane(perpX * normFactor, perpY * normFactor, perpZ * normFactor, 0.0); - - // Old computation: too expensive, because it calculates the intersection point twice. - //return distanceStyle.computeDistance(planetModel, normalizedConnectingPlane, x, y, z, startCutoffPlane, endCutoffPlane) + - // distanceStyle.computeDistance(planetModel, normalizedPerpPlane, start.x, start.y, start.z, upperConnectingPlane, lowerConnectingPlane); - - final GeoPoint[] intersectionPoints = normalizedConnectingPlane.findIntersections(planetModel, normalizedPerpPlane); - GeoPoint thePoint; - if (intersectionPoints.length == 0) - throw new RuntimeException("Can't find world intersection for point x="+x+" y="+y+" z="+z); - else if (intersectionPoints.length == 1) - thePoint = intersectionPoints[0]; - else { - if (startCutoffPlane.isWithin(intersectionPoints[0]) && endCutoffPlane.isWithin(intersectionPoints[0])) - thePoint = intersectionPoints[0]; - else if (startCutoffPlane.isWithin(intersectionPoints[1]) && endCutoffPlane.isWithin(intersectionPoints[1])) - thePoint = intersectionPoints[1]; - else - throw new RuntimeException("Can't find world intersection for point x="+x+" y="+y+" z="+z); - } - return distanceStyle.computeDistance(thePoint, x, y, z) + distanceStyle.computeDistance(start, thePoint.x, thePoint.y, thePoint.z); - } - - /** Compute external distance. - *@param planetModel is the planet model. - *@param distanceStyle is the distance style. - *@param x is the point x. - *@param y is the point y. - *@param z is the point z. - *@return the distance metric. - */ - public double outsideDistance(final PlanetModel planetModel, final DistanceStyle distanceStyle, final double x, final double y, final double z) { - final double upperDistance = distanceStyle.computeDistance(planetModel, upperConnectingPlane, x,y,z, lowerConnectingPlane, startCutoffPlane, endCutoffPlane); - final double lowerDistance = distanceStyle.computeDistance(planetModel, lowerConnectingPlane, x,y,z, upperConnectingPlane, startCutoffPlane, endCutoffPlane); - final double startDistance = distanceStyle.computeDistance(planetModel, startCutoffPlane, x,y,z, endCutoffPlane, lowerConnectingPlane, upperConnectingPlane); - final double endDistance = distanceStyle.computeDistance(planetModel, endCutoffPlane, x,y,z, startCutoffPlane, lowerConnectingPlane, upperConnectingPlane); - final double ULHCDistance = distanceStyle.computeDistance(ULHC, x,y,z); - final double URHCDistance = distanceStyle.computeDistance(URHC, x,y,z); - final double LLHCDistance = distanceStyle.computeDistance(LLHC, x,y,z); - final double LRHCDistance = distanceStyle.computeDistance(LRHC, x,y,z); - return Math.min( - Math.min( - Math.min(upperDistance,lowerDistance), - Math.min(startDistance,endDistance)), - Math.min( - Math.min(ULHCDistance, URHCDistance), - Math.min(LLHCDistance, LRHCDistance))); - } - - /** Determine if this endpoint intersects a specified plane. - *@param planetModel is the planet model. - *@param p is the plane. - *@param notablePoints are the points associated with the plane. - *@param bounds are any bounds which the intersection must lie within. - *@return true if there is a matching intersection. - */ - public boolean intersects(final PlanetModel planetModel, final Plane p, final GeoPoint[] notablePoints, final Membership[] bounds) { - return upperConnectingPlane.intersects(planetModel, p, notablePoints, upperConnectingPlanePoints, bounds, lowerConnectingPlane, startCutoffPlane, endCutoffPlane) || - lowerConnectingPlane.intersects(planetModel, p, notablePoints, lowerConnectingPlanePoints, bounds, upperConnectingPlane, startCutoffPlane, endCutoffPlane); - } - - /** Get the bounds for a segment endpoint. - *@param planetModel is the planet model. - *@param bounds are the bounds to be modified. - */ - public void getBounds(final PlanetModel planetModel, Bounds bounds) { - // We need to do all bounding planes as well as corner points - bounds.addPoint(start).addPoint(end).addPoint(ULHC).addPoint(URHC).addPoint(LRHC).addPoint(LLHC); - bounds.addPlane(planetModel, upperConnectingPlane, lowerConnectingPlane, startCutoffPlane, endCutoffPlane); - bounds.addPlane(planetModel, lowerConnectingPlane, upperConnectingPlane, startCutoffPlane, endCutoffPlane); - bounds.addPlane(planetModel, startCutoffPlane, endCutoffPlane, upperConnectingPlane, lowerConnectingPlane); - bounds.addPlane(planetModel, endCutoffPlane, startCutoffPlane, upperConnectingPlane, lowerConnectingPlane); - } - - } - +public interface GeoPath extends GeoDistanceShape { } diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java new file mode 100644 index 00000000000..3fd7cf7d669 --- /dev/null +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoPathFactory.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * Class which constructs a GeoPath representing an arbitrary path. + * + * @lucene.experimental + */ +public class GeoPathFactory { + private GeoPathFactory() { + } + + /** + * Create a GeoPath of the right kind given the specified information. + * @param planetModel is the planet model. + * @param maxCutoffAngle is the width of the path, measured as an angle. + * @param pathPoints are the points in the path. + * @return a GeoPath corresponding to what was specified. + */ + public static GeoPath makeGeoPath(final PlanetModel planetModel, final double maxCutoffAngle, final GeoPoint[] pathPoints) { + return new GeoStandardPath(planetModel, maxCutoffAngle, pathPoints); + } + +} diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java new file mode 100755 index 00000000000..1546439a01d --- /dev/null +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/GeoStandardPath.java @@ -0,0 +1,797 @@ +/* + * 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.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * GeoShape representing a path across the surface of the globe, + * with a specified half-width. Path is described by a series of points. + * Distances are measured from the starting point along the path, and then at right + * angles to the path. + * + * @lucene.internal + */ +class GeoStandardPath extends GeoBasePath { + /** The cutoff angle (width) */ + protected final double cutoffAngle; + + /** Sine of cutoff angle */ + protected final double sinAngle; + /** Cosine of cutoff angle */ + protected final double cosAngle; + + /** The original list of path points */ + protected final List points = new ArrayList(); + + /** A list of SegmentEndpoints */ + protected List endPoints; + /** A list of PathSegments */ + protected List segments; + + /** A point on the edge */ + protected GeoPoint[] edgePoints; + + /** Set to true if path has been completely constructed */ + protected boolean isDone = false; + + /** Constructor. + *@param planetModel is the planet model. + *@param maxCutoffAngle is the width of the path, measured as an angle. + *@param pathPoints are the points in the path. + */ + public GeoStandardPath(final PlanetModel planetModel, final double maxCutoffAngle, final GeoPoint[] pathPoints) { + this(planetModel, maxCutoffAngle); + Collections.addAll(points, pathPoints); + done(); + } + + /** Piece-wise constructor. Use in conjunction with addPoint() and done(). + *@param planetModel is the planet model. + *@param maxCutoffAngle is the width of the path, measured as an angle. + */ + public GeoStandardPath(final PlanetModel planetModel, final double maxCutoffAngle) { + super(planetModel); + if (maxCutoffAngle <= 0.0 || maxCutoffAngle > Math.PI * 0.5) + throw new IllegalArgumentException("Cutoff angle out of bounds"); + this.cutoffAngle = maxCutoffAngle; + this.cosAngle = Math.cos(maxCutoffAngle); + this.sinAngle = Math.sin(maxCutoffAngle); + } + + /** Add a point to the path. + *@param lat is the latitude of the point. + *@param lon is the longitude of the point. + */ + public void addPoint(final double lat, final double lon) { + if (isDone) + throw new IllegalStateException("Can't call addPoint() if done() already called"); + points.add(new GeoPoint(planetModel, lat, lon)); + } + + /** Complete the path. + */ + 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"); + 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(); + + // First, build all segments. We'll then go back and build corresponding segment endpoints. + GeoPoint lastPoint = null; + for (final GeoPoint end : points) { + if (lastPoint != null) { + final Plane normalizedConnectingPlane = new Plane(lastPoint, end); + if (normalizedConnectingPlane == null) { + continue; + } + segments.add(new PathSegment(planetModel, lastPoint, end, normalizedConnectingPlane, cutoffOffset)); + } + lastPoint = end; + } + + if (segments.size() == 0) { + // Simple circle + 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 GeoPoint point = points.get(0); + + // Construct normal plane + final Plane normalPlane = Plane.constructNormalizedZPlane(upperPoint, lowerPoint, point); + + final SegmentEndpoint onlyEndpoint = new SegmentEndpoint(point, normalPlane, upperPoint, lowerPoint); + endPoints.add(onlyEndpoint); + this.edgePoints = new GeoPoint[]{onlyEndpoint.circlePlane.getSampleIntersectionPoint(planetModel, normalPlane)}; + return; + } + + // Create segment endpoints. Use an appropriate constructor for the start and end of the path. + for (int i = 0; i < segments.size(); i++) { + final PathSegment currentSegment = segments.get(i); + + if (i == 0) { + // Starting endpoint + final SegmentEndpoint startEndpoint = new SegmentEndpoint(currentSegment.start, + currentSegment.startCutoffPlane, currentSegment.ULHC, currentSegment.LLHC); + endPoints.add(startEndpoint); + this.edgePoints = new GeoPoint[]{currentSegment.ULHC}; + continue; + } + + // General intersection case + final PathSegment prevSegment = segments.get(i-1); + // We construct four separate planes, and evaluate which one includes all interior points with least overlap + final SidedPlane candidate1 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, prevSegment.URHC, currentSegment.ULHC, currentSegment.LLHC); + final SidedPlane candidate2 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, currentSegment.ULHC, currentSegment.LLHC, prevSegment.LRHC); + final SidedPlane candidate3 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, currentSegment.LLHC, prevSegment.LRHC, prevSegment.URHC); + final SidedPlane candidate4 = SidedPlane.constructNormalizedThreePointSidedPlane(currentSegment.start, prevSegment.LRHC, prevSegment.URHC, currentSegment.ULHC); + + if (candidate1 == null && candidate2 == null && candidate3 == null && candidate4 == null) { + // The planes are identical. We wouldn't need a circle at all except for the possibility of + // backing up, which is hard to detect here. + final SegmentEndpoint midEndpoint = new SegmentEndpoint(currentSegment.start, + prevSegment.endCutoffPlane, currentSegment.startCutoffPlane, currentSegment.ULHC, currentSegment.LLHC); + //don't need a circle at all. Special constructor... + endPoints.add(midEndpoint); + } else { + endPoints.add(new SegmentEndpoint(currentSegment.start, + prevSegment.endCutoffPlane, currentSegment.startCutoffPlane, + prevSegment.URHC, prevSegment.LRHC, + currentSegment.ULHC, currentSegment.LLHC, + candidate1, candidate2, candidate3, candidate4)); + } + } + // Do final endpoint + final PathSegment lastSegment = segments.get(segments.size()-1); + endPoints.add(new SegmentEndpoint(lastSegment.end, + lastSegment.endCutoffPlane, lastSegment.URHC, lastSegment.LRHC)); + + } + + @Override + protected double distance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { + // Algorithm: + // (1) If the point is within any of the segments along the path, return that value. + // (2) If the point is within any of the segment end circles along the path, return that value. + double currentDistance = 0.0; + for (PathSegment segment : segments) { + double distance = segment.pathDistance(planetModel, distanceStyle, x,y,z); + if (distance != Double.MAX_VALUE) + return currentDistance + distance; + currentDistance += segment.fullPathDistance(distanceStyle); + } + + int segmentIndex = 0; + currentDistance = 0.0; + for (SegmentEndpoint endpoint : endPoints) { + double distance = endpoint.pathDistance(distanceStyle, x, y, z); + if (distance != Double.MAX_VALUE) + return currentDistance + distance; + if (segmentIndex < segments.size()) + currentDistance += segments.get(segmentIndex++).fullPathDistance(distanceStyle); + } + + return Double.MAX_VALUE; + } + + @Override + protected double outsideDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { + double minDistance = Double.MAX_VALUE; + for (final SegmentEndpoint endpoint : endPoints) { + final double newDistance = endpoint.outsideDistance(distanceStyle, x,y,z); + if (newDistance < minDistance) + minDistance = newDistance; + } + for (final PathSegment segment : segments) { + final double newDistance = segment.outsideDistance(planetModel, distanceStyle, x, y, z); + if (newDistance < minDistance) + minDistance = newDistance; + } + return minDistance; + } + + @Override + public boolean isWithin(final double x, final double y, final double z) { + for (SegmentEndpoint pathPoint : endPoints) { + if (pathPoint.isWithin(x, y, z)) + return true; + } + for (PathSegment pathSegment : segments) { + if (pathSegment.isWithin(x, y, z)) + return true; + } + return false; + } + + @Override + public GeoPoint[] getEdgePoints() { + return edgePoints; + } + + @Override + public boolean intersects(final Plane plane, final GeoPoint[] notablePoints, final Membership... bounds) { + // We look for an intersection with any of the exterior edges of the path. + // We also have to look for intersections with the cones described by the endpoints. + // Return "true" if any such intersections are found. + + // For plane intersections, the basic idea is to come up with an equation of the line that is + // the intersection (if any). Then, find the intersections with the unit sphere (if any). If + // any of the intersection points are within the bounds, then we've detected an intersection. + // Well, sort of. We can detect intersections also due to overlap of segments with each other. + // But that's an edge case and we won't be optimizing for it. + //System.err.println(" Looking for intersection of plane "+plane+" with path "+this); + for (final SegmentEndpoint pathPoint : endPoints) { + if (pathPoint.intersects(planetModel, plane, notablePoints, bounds)) { + return true; + } + } + + for (final PathSegment pathSegment : segments) { + if (pathSegment.intersects(planetModel, plane, notablePoints, bounds)) { + return true; + } + } + + return false; + } + + @Override + public void getBounds(Bounds bounds) { + super.getBounds(bounds); + // For building bounds, order matters. We want to traverse + // never more than 180 degrees longitude at a pop or we risk having the + // bounds object get itself inverted. So do the edges first. + for (PathSegment pathSegment : segments) { + pathSegment.getBounds(planetModel, bounds); + } + for (SegmentEndpoint pathPoint : endPoints) { + pathPoint.getBounds(planetModel, bounds); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof GeoStandardPath)) + return false; + GeoStandardPath p = (GeoStandardPath) o; + if (!super.equals(p)) + return false; + if (cutoffAngle != p.cutoffAngle) + return false; + return points.equals(p.points); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + long temp = Double.doubleToLongBits(cutoffAngle); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + points.hashCode(); + return result; + } + + @Override + public String toString() { + return "GeoStandardPath: {planetmodel=" + planetModel+", width=" + cutoffAngle + "(" + cutoffAngle * 180.0 / Math.PI + "), points={" + points + "}}"; + } + + /** + * This is precalculated data for segment endpoint. + * Note well: This is not necessarily a circle. There are four cases: + * (1) The path consists of a single endpoint. In this case, we build a simple circle with the proper cutoff offset. + * (2) This is the end of a path. The circle plane must be constructed to go through two supplied points and be perpendicular to a connecting plane. + * (2.5) Intersection, but the path on both sides is linear. We generate a circle, but we use the cutoff planes to limit its influence in the straight line case. + * (3) This is an intersection in a path. We are supplied FOUR planes. If there are intersections within bounds for both upper and lower, then + * we generate no circle at all. If there is one intersection only, then we generate a plane that includes that intersection, as well as the remaining + * cutoff plane/edge plane points. + */ + public static class SegmentEndpoint { + /** The center point of the endpoint */ + public final GeoPoint point; + /** A plane describing the circle */ + public final SidedPlane circlePlane; + /** Pertinent cutoff planes from adjoining segments */ + public final Membership[] cutoffPlanes; + /** Notable points for this segment endpoint */ + public final GeoPoint[] notablePoints; + /** No notable points from the circle itself */ + public final static GeoPoint[] circlePoints = new GeoPoint[0]; + /** Null membership */ + public final static Membership[] NO_MEMBERSHIP = new Membership[0]; + + /** Base case. Does nothing at all. + */ + public SegmentEndpoint(final GeoPoint point) { + this.point = point; + this.circlePlane = null; + this.cutoffPlanes = null; + this.notablePoints = null; + } + + /** Constructor for case (1). + * Generate a simple circle cutoff plane. + *@param point is the center point. + *@param upperPoint is a point that must be on the circle plane. + *@param lowerPoint is another point that must be on the circle plane. + */ + public SegmentEndpoint(final GeoPoint point, final Plane normalPlane, final GeoPoint upperPoint, final GeoPoint lowerPoint) { + this.point = 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 = NO_MEMBERSHIP; + this.notablePoints = circlePoints; + } + + /** Constructor for case (2). + * Generate an endpoint, given a single cutoff plane plus upper and lower edge points. + *@param point is the center point. + *@param cutoffPlane is the plane from the adjoining path segment marking the boundary between this endpoint and that segment. + *@param topEdgePoint is a point on the cutoffPlane that should be also on the circle plane. + *@param bottomEdgePoint is another point on the cutoffPlane that should be also on the circle plane. + */ + public SegmentEndpoint(final GeoPoint point, + final SidedPlane cutoffPlane, final GeoPoint topEdgePoint, final GeoPoint bottomEdgePoint) { + this.point = point; + this.cutoffPlanes = new Membership[]{new SidedPlane(cutoffPlane)}; + this.notablePoints = new GeoPoint[]{topEdgePoint, bottomEdgePoint}; + // 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. + this.circlePlane = SidedPlane.constructNormalizedPerpendicularSidedPlane(point, cutoffPlane, topEdgePoint, bottomEdgePoint); + } + + /** Constructor for case (2.5). + * Generate an endpoint, given two cutoff planes plus upper and lower edge points. + *@param point is the center. + *@param cutoffPlane1 is one adjoining path segment cutoff plane. + *@param cutoffPlane2 is another adjoining path segment cutoff plane. + *@param topEdgePoint is a point on the cutoffPlane that should be also on the circle plane. + *@param bottomEdgePoint is another point on the cutoffPlane that should be also on the circle plane. + */ + public SegmentEndpoint(final GeoPoint point, + final SidedPlane cutoffPlane1, final SidedPlane cutoffPlane2, final GeoPoint topEdgePoint, final GeoPoint bottomEdgePoint) { + this.point = point; + this.cutoffPlanes = new Membership[]{new SidedPlane(cutoffPlane1), new SidedPlane(cutoffPlane2)}; + this.notablePoints = new GeoPoint[]{topEdgePoint, bottomEdgePoint}; + // 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. + this.circlePlane = SidedPlane.constructNormalizedPerpendicularSidedPlane(point, cutoffPlane1, topEdgePoint, bottomEdgePoint); + } + + /** Constructor for case (3). + * Generate an endpoint for an intersection, given four points. + *@param point is the center. + *@param prevCutoffPlane is the previous adjoining segment cutoff plane. + *@param nextCutoffPlane is the next path segment cutoff plane. + *@param notCand2Point is a point NOT on candidate2. + *@param notCand1Point is a point NOT on candidate1. + *@param notCand3Point is a point NOT on candidate3. + *@param notCand4Point is a point NOT on candidate4. + *@param candidate1 one of four candidate circle planes. + *@param candidate2 one of four candidate circle planes. + *@param candidate3 one of four candidate circle planes. + *@param candidate4 one of four candidate circle planes. + */ + public SegmentEndpoint(final GeoPoint point, + final SidedPlane prevCutoffPlane, final SidedPlane nextCutoffPlane, + final GeoPoint notCand2Point, final GeoPoint notCand1Point, + final GeoPoint notCand3Point, final GeoPoint notCand4Point, + final SidedPlane candidate1, final SidedPlane candidate2, final SidedPlane candidate3, final SidedPlane candidate4) { + // Note: What we really need is a single plane that goes through all four points. + // Since that's not possible in the ellipsoid case (because three points determine a plane, not four), we + // need an approximation that at least creates a boundary that has no interruptions. + // There are three obvious choices for the third point: either (a) one of the two remaining points, or (b) the top or bottom edge + // intersection point. (a) has no guarantee of continuity, while (b) is capable of producing something very far from a circle if + // the angle between segments is acute. + // The solution is to look for the side (top or bottom) that has an intersection within the shape. We use the two points from + // the opposite side to determine the plane, AND we pick the third to be either of the two points on the intersecting side + // PROVIDED that the other point is within the final circle we come up with. + this.point = point; + + // We construct four separate planes, and evaluate which one includes all interior points with least overlap + // (Constructed beforehand because we need them for degeneracy check) + + final boolean cand1IsOtherWithin = candidate1!=null?candidate1.isWithin(notCand1Point):false; + final boolean cand2IsOtherWithin = candidate2!=null?candidate2.isWithin(notCand2Point):false; + final boolean cand3IsOtherWithin = candidate3!=null?candidate3.isWithin(notCand3Point):false; + final boolean cand4IsOtherWithin = candidate4!=null?candidate4.isWithin(notCand4Point):false; + + if (cand1IsOtherWithin && cand2IsOtherWithin && cand3IsOtherWithin && cand4IsOtherWithin) { + // The only way we should see both within is if all four points are coplanar. In that case, we default to the simplest treatment. + this.circlePlane = candidate1; // doesn't matter which + this.notablePoints = new GeoPoint[]{notCand2Point, notCand3Point, notCand1Point, notCand4Point}; + this.cutoffPlanes = new Membership[]{new SidedPlane(prevCutoffPlane), new SidedPlane(nextCutoffPlane)}; + } else if (cand1IsOtherWithin) { + // Use candidate1, and DON'T include prevCutoffPlane in the cutoff planes list + this.circlePlane = candidate1; + this.notablePoints = new GeoPoint[]{notCand2Point, notCand3Point, notCand4Point}; + this.cutoffPlanes = new Membership[]{new SidedPlane(nextCutoffPlane)}; + } else if (cand2IsOtherWithin) { + // Use candidate2 + this.circlePlane = candidate2; + this.notablePoints = new GeoPoint[]{notCand3Point, notCand4Point, notCand1Point}; + this.cutoffPlanes = new Membership[]{new SidedPlane(nextCutoffPlane)}; + } else if (cand3IsOtherWithin) { + this.circlePlane = candidate3; + this.notablePoints = new GeoPoint[]{notCand4Point, notCand1Point, notCand2Point}; + this.cutoffPlanes = new Membership[]{new SidedPlane(prevCutoffPlane)}; + } else if (cand4IsOtherWithin) { + this.circlePlane = candidate4; + this.notablePoints = new GeoPoint[]{notCand1Point, notCand2Point, notCand3Point}; + this.cutoffPlanes = new Membership[]{new SidedPlane(prevCutoffPlane)}; + } else { + // dunno what happened + throw new RuntimeException("Couldn't come up with a plane through three points that included the fourth"); + } + } + + /** Check if point is within this endpoint. + *@param point is the point. + *@return true of within. + */ + public boolean isWithin(final Vector point) { + if (circlePlane == null) + return false; + if (!circlePlane.isWithin(point)) + return false; + for (final Membership m : cutoffPlanes) { + if (!m.isWithin(point)) { + return false; + } + } + return true; + } + + /** Check if point is within this endpoint. + *@param x is the point x. + *@param y is the point y. + *@param z is the point z. + *@return true of within. + */ + public boolean isWithin(final double x, final double y, final double z) { + if (circlePlane == null) + return false; + if (!circlePlane.isWithin(x, y, z)) + return false; + for (final Membership m : cutoffPlanes) { + if (!m.isWithin(x,y,z)) { + return false; + } + } + return true; + } + + /** Compute interior path distance. + *@param distanceStyle is the distance style. + *@param x is the point x. + *@param y is the point y. + *@param z is the point z. + *@return the distance metric. + */ + public double pathDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { + if (!isWithin(x,y,z)) + return Double.MAX_VALUE; + return distanceStyle.computeDistance(this.point, x, y, z); + } + + /** Compute external distance. + *@param distanceStyle is the distance style. + *@param x is the point x. + *@param y is the point y. + *@param z is the point z. + *@return the distance metric. + */ + public double outsideDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) { + return distanceStyle.computeDistance(this.point, x, y, z); + } + + /** Determine if this endpoint intersects a specified plane. + *@param planetModel is the planet model. + *@param p is the plane. + *@param notablePoints are the points associated with the plane. + *@param bounds are any bounds which the intersection must lie within. + *@return true if there is a matching intersection. + */ + public boolean intersects(final PlanetModel planetModel, final Plane p, final GeoPoint[] notablePoints, final Membership[] bounds) { + //System.err.println(" looking for intersection between plane "+p+" and circle "+circlePlane+" on proper side of "+cutoffPlanes+" within "+bounds); + if (circlePlane == null) + return false; + return circlePlane.intersects(planetModel, p, notablePoints, this.notablePoints, bounds, this.cutoffPlanes); + } + + /** Get the bounds for a segment endpoint. + *@param planetModel is the planet model. + *@param bounds are the bounds to be modified. + */ + public void getBounds(final PlanetModel planetModel, Bounds bounds) { + bounds.addPoint(point); + if (circlePlane == null) + return; + bounds.addPlane(planetModel, circlePlane); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SegmentEndpoint)) + return false; + SegmentEndpoint other = (SegmentEndpoint) o; + return point.equals(other.point); + } + + @Override + public int hashCode() { + return point.hashCode(); + } + + @Override + public String toString() { + return point.toString(); + } + } + + /** + * This is the pre-calculated data for a path segment. + */ + public static class PathSegment { + /** Starting point of the segment */ + public final GeoPoint start; + /** End point of the segment */ + public final GeoPoint end; + /** Place to keep any complete segment distances we've calculated so far */ + public final Map fullDistanceCache = new HashMap(); + /** Normalized plane connecting the two points and going through world center */ + public final Plane normalizedConnectingPlane; + /** Cutoff plane parallel to connecting plane representing one side of the path segment */ + public final SidedPlane upperConnectingPlane; + /** Cutoff plane parallel to connecting plane representing the other side of the path segment */ + public final SidedPlane lowerConnectingPlane; + /** Plane going through the center and start point, marking the start edge of the segment */ + public final SidedPlane startCutoffPlane; + /** Plane going through the center and end point, marking the end edge of the segment */ + public final SidedPlane endCutoffPlane; + /** Upper right hand corner of segment */ + public final GeoPoint URHC; + /** Lower right hand corner of segment */ + public final GeoPoint LRHC; + /** Upper left hand corner of segment */ + public final GeoPoint ULHC; + /** Lower left hand corner of segment */ + public final GeoPoint LLHC; + /** Notable points for the upper connecting plane */ + public final GeoPoint[] upperConnectingPlanePoints; + /** Notable points for the lower connecting plane */ + public final GeoPoint[] lowerConnectingPlanePoints; + /** Notable points for the start cutoff plane */ + public final GeoPoint[] startCutoffPlanePoints; + /** Notable points for the end cutoff plane */ + public final GeoPoint[] endCutoffPlanePoints; + + /** Construct a path segment. + *@param planetModel is the planet model. + *@param start is the starting point. + *@param end is the ending point. + *@param normalizedConnectingPlane is the connecting plane. + *@param planeBoundingOffset is the linear offset from the connecting plane to either side. + */ + public PathSegment(final PlanetModel planetModel, final GeoPoint start, final GeoPoint end, + final Plane normalizedConnectingPlane, final double planeBoundingOffset) { + this.start = start; + this.end = end; + this.normalizedConnectingPlane = normalizedConnectingPlane; + + // Either start or end should be on the correct side + upperConnectingPlane = new SidedPlane(start, normalizedConnectingPlane, -planeBoundingOffset); + lowerConnectingPlane = new SidedPlane(start, normalizedConnectingPlane, planeBoundingOffset); + // Cutoff planes use opposite endpoints as correct side examples + startCutoffPlane = new SidedPlane(end, normalizedConnectingPlane, start); + endCutoffPlane = new SidedPlane(start, normalizedConnectingPlane, end); + final Membership[] upperSide = new Membership[]{upperConnectingPlane}; + final Membership[] lowerSide = new Membership[]{lowerConnectingPlane}; + final Membership[] startSide = new Membership[]{startCutoffPlane}; + final Membership[] endSide = new Membership[]{endCutoffPlane}; + GeoPoint[] points; + points = upperConnectingPlane.findIntersections(planetModel, startCutoffPlane, lowerSide, endSide); + if (points.length == 0) { + throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); + } + this.ULHC = points[0]; + points = upperConnectingPlane.findIntersections(planetModel, endCutoffPlane, lowerSide, startSide); + if (points.length == 0) { + throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); + } + this.URHC = points[0]; + points = lowerConnectingPlane.findIntersections(planetModel, startCutoffPlane, upperSide, endSide); + if (points.length == 0) { + throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); + } + this.LLHC = points[0]; + points = lowerConnectingPlane.findIntersections(planetModel, endCutoffPlane, upperSide, startSide); + if (points.length == 0) { + throw new IllegalArgumentException("Some segment boundary points are off the ellipsoid; path too wide"); + } + this.LRHC = points[0]; + upperConnectingPlanePoints = new GeoPoint[]{ULHC, URHC}; + lowerConnectingPlanePoints = new GeoPoint[]{LLHC, LRHC}; + startCutoffPlanePoints = new GeoPoint[]{ULHC, LLHC}; + endCutoffPlanePoints = new GeoPoint[]{URHC, LRHC}; + } + + /** Compute the full distance along this path segment. + *@param distanceStyle is the distance style. + *@return the distance metric. + */ + public double fullPathDistance(final DistanceStyle distanceStyle) { + synchronized (fullDistanceCache) { + Double dist = fullDistanceCache.get(distanceStyle); + if (dist == null) { + dist = new Double(distanceStyle.computeDistance(start, end.x, end.y, end.z)); + fullDistanceCache.put(distanceStyle, dist); + } + return dist.doubleValue(); + } + } + + /** Check if point is within this segment. + *@param point is the point. + *@return true of within. + */ + public boolean isWithin(final Vector point) { + return startCutoffPlane.isWithin(point) && + endCutoffPlane.isWithin(point) && + upperConnectingPlane.isWithin(point) && + lowerConnectingPlane.isWithin(point); + } + + /** Check if point is within this segment. + *@param x is the point x. + *@param y is the point y. + *@param z is the point z. + *@return true of within. + */ + public boolean isWithin(final double x, final double y, final double z) { + return startCutoffPlane.isWithin(x, y, z) && + endCutoffPlane.isWithin(x, y, z) && + upperConnectingPlane.isWithin(x, y, z) && + lowerConnectingPlane.isWithin(x, y, z); + } + + /** Compute interior path distance. + *@param planetModel is the planet model. + *@param distanceStyle is the distance style. + *@param x is the point x. + *@param y is the point y. + *@param z is the point z. + *@return the distance metric. + */ + public double pathDistance(final PlanetModel planetModel, final DistanceStyle distanceStyle, final double x, final double y, final double z) { + if (!isWithin(x,y,z)) + return Double.MAX_VALUE; + + // (1) Compute normalizedPerpPlane. If degenerate, then return point distance from start to point. + // Want no allocations or expensive operations! so we do this the hard way + final double perpX = normalizedConnectingPlane.y * z - normalizedConnectingPlane.z * y; + final double perpY = normalizedConnectingPlane.z * x - normalizedConnectingPlane.x * z; + final double perpZ = normalizedConnectingPlane.x * y - normalizedConnectingPlane.y * x; + final double magnitude = Math.sqrt(perpX * perpX + perpY * perpY + perpZ * perpZ); + if (Math.abs(magnitude) < Vector.MINIMUM_RESOLUTION) + return distanceStyle.computeDistance(start, x,y,z); + final double normFactor = 1.0/magnitude; + final Plane normalizedPerpPlane = new Plane(perpX * normFactor, perpY * normFactor, perpZ * normFactor, 0.0); + + // Old computation: too expensive, because it calculates the intersection point twice. + //return distanceStyle.computeDistance(planetModel, normalizedConnectingPlane, x, y, z, startCutoffPlane, endCutoffPlane) + + // distanceStyle.computeDistance(planetModel, normalizedPerpPlane, start.x, start.y, start.z, upperConnectingPlane, lowerConnectingPlane); + + final GeoPoint[] intersectionPoints = normalizedConnectingPlane.findIntersections(planetModel, normalizedPerpPlane); + GeoPoint thePoint; + if (intersectionPoints.length == 0) + throw new RuntimeException("Can't find world intersection for point x="+x+" y="+y+" z="+z); + else if (intersectionPoints.length == 1) + thePoint = intersectionPoints[0]; + else { + if (startCutoffPlane.isWithin(intersectionPoints[0]) && endCutoffPlane.isWithin(intersectionPoints[0])) + thePoint = intersectionPoints[0]; + else if (startCutoffPlane.isWithin(intersectionPoints[1]) && endCutoffPlane.isWithin(intersectionPoints[1])) + thePoint = intersectionPoints[1]; + else + throw new RuntimeException("Can't find world intersection for point x="+x+" y="+y+" z="+z); + } + return distanceStyle.computeDistance(thePoint, x, y, z) + distanceStyle.computeDistance(start, thePoint.x, thePoint.y, thePoint.z); + } + + /** Compute external distance. + *@param planetModel is the planet model. + *@param distanceStyle is the distance style. + *@param x is the point x. + *@param y is the point y. + *@param z is the point z. + *@return the distance metric. + */ + public double outsideDistance(final PlanetModel planetModel, final DistanceStyle distanceStyle, final double x, final double y, final double z) { + final double upperDistance = distanceStyle.computeDistance(planetModel, upperConnectingPlane, x,y,z, lowerConnectingPlane, startCutoffPlane, endCutoffPlane); + final double lowerDistance = distanceStyle.computeDistance(planetModel, lowerConnectingPlane, x,y,z, upperConnectingPlane, startCutoffPlane, endCutoffPlane); + final double startDistance = distanceStyle.computeDistance(planetModel, startCutoffPlane, x,y,z, endCutoffPlane, lowerConnectingPlane, upperConnectingPlane); + final double endDistance = distanceStyle.computeDistance(planetModel, endCutoffPlane, x,y,z, startCutoffPlane, lowerConnectingPlane, upperConnectingPlane); + final double ULHCDistance = distanceStyle.computeDistance(ULHC, x,y,z); + final double URHCDistance = distanceStyle.computeDistance(URHC, x,y,z); + final double LLHCDistance = distanceStyle.computeDistance(LLHC, x,y,z); + final double LRHCDistance = distanceStyle.computeDistance(LRHC, x,y,z); + return Math.min( + Math.min( + Math.min(upperDistance,lowerDistance), + Math.min(startDistance,endDistance)), + Math.min( + Math.min(ULHCDistance, URHCDistance), + Math.min(LLHCDistance, LRHCDistance))); + } + + /** Determine if this endpoint intersects a specified plane. + *@param planetModel is the planet model. + *@param p is the plane. + *@param notablePoints are the points associated with the plane. + *@param bounds are any bounds which the intersection must lie within. + *@return true if there is a matching intersection. + */ + public boolean intersects(final PlanetModel planetModel, final Plane p, final GeoPoint[] notablePoints, final Membership[] bounds) { + return upperConnectingPlane.intersects(planetModel, p, notablePoints, upperConnectingPlanePoints, bounds, lowerConnectingPlane, startCutoffPlane, endCutoffPlane) || + lowerConnectingPlane.intersects(planetModel, p, notablePoints, lowerConnectingPlanePoints, bounds, upperConnectingPlane, startCutoffPlane, endCutoffPlane); + } + + /** Get the bounds for a segment endpoint. + *@param planetModel is the planet model. + *@param bounds are the bounds to be modified. + */ + public void getBounds(final PlanetModel planetModel, Bounds bounds) { + // We need to do all bounding planes as well as corner points + bounds.addPoint(start).addPoint(end).addPoint(ULHC).addPoint(URHC).addPoint(LRHC).addPoint(LLHC); + bounds.addPlane(planetModel, upperConnectingPlane, lowerConnectingPlane, startCutoffPlane, endCutoffPlane); + bounds.addPlane(planetModel, lowerConnectingPlane, upperConnectingPlane, startCutoffPlane, endCutoffPlane); + bounds.addPlane(planetModel, startCutoffPlane, endCutoffPlane, upperConnectingPlane, lowerConnectingPlane); + bounds.addPlane(planetModel, endCutoffPlane, startCutoffPlane, upperConnectingPlane, lowerConnectingPlane); + } + + } + +} diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java index 398458c907f..3caf039a2d0 100644 --- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java +++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java @@ -40,7 +40,7 @@ import org.apache.lucene.spatial3d.geom.GeoArea; import org.apache.lucene.spatial3d.geom.GeoAreaFactory; import org.apache.lucene.spatial3d.geom.GeoBBoxFactory; import org.apache.lucene.spatial3d.geom.GeoCircleFactory; -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.GeoPolygonFactory; import org.apache.lucene.spatial3d.geom.GeoShape; @@ -625,13 +625,12 @@ public class TestGeo3DPoint extends LuceneTestCase { // Paths final int pointCount = random().nextInt(5) + 1; final double width = toRadians(random().nextInt(89)+1); + final GeoPoint[] points = new GeoPoint[pointCount]; + for (int i = 0; i < pointCount; i++) { + points[i] = new GeoPoint(planetModel, toRadians(randomLat()), toRadians(randomLon())); + } try { - final GeoPath path = new GeoPath(planetModel, width); - for (int i = 0; i < pointCount; i++) { - path.addPoint(toRadians(randomLat()), toRadians(randomLon())); - } - path.done(); - return path; + return GeoPathFactory.makeGeoPath(planetModel, width, points); } catch (IllegalArgumentException e) { // This is what happens when we create a shape that is invalid. Although it is conceivable that there are cases where // the exception is thrown incorrectly, we aren't going to be able to do that in this random test. diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/GeoPathTest.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/GeoPathTest.java index 37460699359..e68e3f48779 100755 --- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/GeoPathTest.java +++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/geom/GeoPathTest.java @@ -28,9 +28,9 @@ public class GeoPathTest { @Test public void testPathDistance() { // Start with a really simple case - GeoPath p; + GeoStandardPath p; GeoPoint gp; - p = new GeoPath(PlanetModel.SPHERE, 0.1); + p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); p.addPoint(0.0, 0.0); p.addPoint(0.0, 0.1); p.addPoint(0.0, 0.2); @@ -49,7 +49,7 @@ public class GeoPathTest { assertEquals(0.0 + 0.05, p.computeDistance(DistanceStyle.ARC,gp), 0.000001); // Compute path distances now - p = new GeoPath(PlanetModel.SPHERE, 0.1); + p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); p.addPoint(0.0, 0.0); p.addPoint(0.0, 0.1); p.addPoint(0.0, 0.2); @@ -60,7 +60,7 @@ public class GeoPathTest { assertEquals(0.12, p.computeDistance(DistanceStyle.ARC,gp), 0.000001); // Now try a vertical path, and make sure distances are as expected - p = new GeoPath(PlanetModel.SPHERE, 0.1); + p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); p.addPoint(-Math.PI * 0.25, -0.5); p.addPoint(Math.PI * 0.25, -0.5); p.done(); @@ -77,9 +77,9 @@ public class GeoPathTest { @Test public void testPathPointWithin() { // Tests whether we can properly detect whether a point is within a path or not - GeoPath p; + GeoStandardPath p; GeoPoint gp; - p = new GeoPath(PlanetModel.SPHERE, 0.1); + p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); // Build a diagonal path crossing the equator p.addPoint(-0.2, -0.2); p.addPoint(0.2, 0.2); @@ -101,7 +101,7 @@ public class GeoPathTest { gp = new GeoPoint(PlanetModel.SPHERE, 0.0, Math.PI); assertFalse(p.isWithin(gp)); // Repeat the test, but across the terminator - p = new GeoPath(PlanetModel.SPHERE, 0.1); + p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); // Build a diagonal path crossing the equator p.addPoint(-0.2, Math.PI - 0.2); p.addPoint(0.2, -Math.PI + 0.2); @@ -128,8 +128,8 @@ public class GeoPathTest { @Test public void testGetRelationship() { GeoArea rect; - GeoPath p; - GeoPath c; + GeoStandardPath p; + GeoStandardPath c; GeoPoint point; GeoPoint pointApprox; int relationship; @@ -137,7 +137,7 @@ public class GeoPathTest { PlanetModel planetModel; planetModel = new PlanetModel(1.151145876105594, 0.8488541238944061); - c = new GeoPath(planetModel, 0.008726646259971648); + c = new GeoStandardPath(planetModel, 0.008726646259971648); c.addPoint(-0.6925658899376476, 0.6316613927914589); c.addPoint(0.27828548161836364, 0.6785795524104564); c.done(); @@ -148,7 +148,7 @@ public class GeoPathTest { // Start by testing the basic kinds of relationship, increasing in order of difficulty. - p = new GeoPath(PlanetModel.SPHERE, 0.1); + p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); p.addPoint(-0.3, -0.3); p.addPoint(0.3, 0.3); p.done(); @@ -179,7 +179,7 @@ public class GeoPathTest { @Test public void testPathBounds() { - GeoPath c; + GeoStandardPath c; LatLonBounds b; XYZBounds xyzb; GeoPoint point; @@ -188,7 +188,7 @@ public class GeoPathTest { PlanetModel planetModel; planetModel = new PlanetModel(0.751521665790406,1.248478334209594); - c = new GeoPath(planetModel, 0.7504915783575618); + c = new GeoStandardPath(planetModel, 0.7504915783575618); c.addPoint(0.10869761172400265, 0.08895880215465272); c.addPoint(0.22467878641991612, 0.10972973084229565); c.addPoint(-0.7398772468744732, -0.4465812941383364); @@ -202,10 +202,10 @@ public class GeoPathTest { relationship = area.getRelationship(c); assertTrue(relationship == GeoArea.WITHIN || relationship == GeoArea.OVERLAPS); assertTrue(area.isWithin(point)); - // No longer true due to fixed GeoPath waypoints. + // No longer true due to fixed GeoStandardPath waypoints. //assertTrue(c.isWithin(point)); - c = new GeoPath(PlanetModel.WGS84, 0.6894050545377601); + c = new GeoStandardPath(PlanetModel.WGS84, 0.6894050545377601); c.addPoint(-0.0788176065762948, 0.9431251741731624); c.addPoint(0.510387871458147, 0.5327078872484678); c.addPoint(-0.5624521609859962, 1.5398841746888388); @@ -224,7 +224,7 @@ public class GeoPathTest { assertTrue(relationship == GeoArea.WITHIN || relationship == GeoArea.OVERLAPS); assertTrue(area.isWithin(point)); - c = new GeoPath(PlanetModel.WGS84, 0.7766715171374766); + c = new GeoStandardPath(PlanetModel.WGS84, 0.7766715171374766); c.addPoint(-0.2751718361148076, -0.7786721269011477); c.addPoint(0.5728375851539309, -1.2700115736820465); c.done(); @@ -240,7 +240,7 @@ public class GeoPathTest { assertTrue(relationship == GeoArea.WITHIN || relationship == GeoArea.OVERLAPS); assertTrue(area.isWithin(point)); - c = new GeoPath(PlanetModel.SPHERE, 0.1); + c = new GeoStandardPath(PlanetModel.SPHERE, 0.1); c.addPoint(-0.3, -0.3); c.addPoint(0.3, 0.3); c.done(); @@ -260,7 +260,7 @@ public class GeoPathTest { @Test public void testCoLinear() { // p1: (12,-90), p2: (11, -55), (129, -90) - GeoPath p = new GeoPath(PlanetModel.SPHERE, 0.1); + GeoStandardPath p = new GeoStandardPath(PlanetModel.SPHERE, 0.1); p.addPoint(toRadians(-90), toRadians(12));//south pole p.addPoint(toRadians(-55), toRadians(11)); p.addPoint(toRadians(-90), toRadians(129));//south pole again