/* * 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; /** * An object for accumulating latitude/longitude bounds information. * * @lucene.experimental */ public class LatLonBounds implements Bounds { /** Set to true if no longitude bounds can be stated */ private boolean noLongitudeBound = false; /** Set to true if no top latitude bound can be stated */ private boolean noTopLatitudeBound = false; /** Set to true if no bottom latitude bound can be stated */ private boolean noBottomLatitudeBound = false; /** If non-null, the minimum latitude bound */ private Double minLatitude = null; /** If non-null, the maximum latitude bound */ private Double maxLatitude = null; // For longitude bounds, this class needs to worry about keeping track of the distinction // between left-side bounds and right-side bounds. Points are always submitted in pairs // which have a maximum longitude separation of Math.PI. It's therefore always possible // to determine which point represents a left bound, and which point represents a right // bound. // // The next problem is how to compare two of the same kind of bound, e.g. two left bounds. // We need to keep track of the leftmost longitude of the shape, but since this is a circle, // this is arbitrary. What we could try to do instead would be to find a pair of (left,right) bounds such // that: // (1) all other bounds are within, and // (2) the left minus right distance is minimized // Unfortunately, there are still shapes that cannot be summarized in this way correctly. // For example. consider a spiral that entirely circles the globe; we might arbitrarily choose // lat/lon bounds that do not in fact circle the globe. // // One way to handle the longitude issue correctly is therefore to stipulate that we // walk the bounds of the shape in some kind of connected order. Each point or circle is therefore // added in a sequence. We also need an interior point to make sure we have the right // choice of longitude bounds. But even with this, we still can't always choose whether the actual shape // goes right or left. // // We can make the specification truly general by submitting the following in order: // addSide(PlaneSide side, Membership... constraints) // ... // This is unambiguous, but I still can't see yet how this would help compute the bounds. The plane // solution would in general seem to boil down to the same logic that relies on points along the path // to define the shape boundaries. I guess the one thing that you do know for a bounded edge is that // the endpoints are actually connected. But it is not clear whether relationship helps in any way. // // In any case, if we specify shapes by a sequence of planes, we should stipulate that multiple sequences // are allowed, provided they progressively tile an area of the sphere that is connected and sequential. // For example, paths do alternating rectangles and circles, in sequence. Each sequence member is // described by a sequence of planes. I think it would also be reasonable to insist that the first segment // of a shape overlap or adjoin the previous shape. // // Here's a way to think about it that might help: Traversing every edge should grow the longitude bounds // in the direction of the traversal. So if the traversal is always known to be less than PI in total longitude // angle, then it is possible to use the endpoints to determine the unambiguous extension of the envelope. // For example, say you are currently at longitude -0.5. The next point is at longitude PI-0.1. You could say // that the difference in longitude going one way around would be beter than the distance the other way // around, and therefore the longitude envelope should be extended accordingly. But in practice, when an // edge goes near a pole and may be inclined as well, the longer longitude change might be the right path, even // if the arc length is short. So this too doesn't work. // // Given we have a hard time making an exact match, here's the current proposal. The proposal is a // heuristic, based on the idea that most areas are small compared to the circumference of the globe. // We keep track of the last point we saw, and take each point as it arrives, and compute its longitude. // Then, we have a choice as to which way to expand the envelope: we can expand by going to the left or // to the right. We choose the direction with the least longitude difference. (If we aren't sure, // and can recognize that, we can set "unconstrained in longitude".) /** If non-null, the left longitude bound */ private Double leftLongitude = null; /** If non-null, the right longitude bound */ private Double rightLongitude = null; /** Construct an empty bounds object */ public LatLonBounds() { } // Accessor methods /** Get maximum latitude, if any. *@return maximum latitude or null. */ public Double getMaxLatitude() { return maxLatitude; } /** Get minimum latitude, if any. *@return minimum latitude or null. */ public Double getMinLatitude() { return minLatitude; } /** Get left longitude, if any. *@return left longitude, or null. */ public Double getLeftLongitude() { return leftLongitude; } /** Get right longitude, if any. *@return right longitude, or null. */ public Double getRightLongitude() { return rightLongitude; } // Degenerate case check /** Check if there's no longitude bound. *@return true if no longitude bound. */ public boolean checkNoLongitudeBound() { return noLongitudeBound; } /** Check if there's no top latitude bound. *@return true if no top latitude bound. */ public boolean checkNoTopLatitudeBound() { return noTopLatitudeBound; } /** Check if there's no bottom latitude bound. *@return true if no bottom latitude bound. */ public boolean checkNoBottomLatitudeBound() { return noBottomLatitudeBound; } // Modification methods @Override public Bounds addPlane(final PlanetModel planetModel, final Plane plane, final Membership... bounds) { plane.recordBounds(planetModel, this, bounds); return this; } @Override public Bounds addHorizontalPlane(final PlanetModel planetModel, final double latitude, final Plane horizontalPlane, final Membership... bounds) { if (!noTopLatitudeBound || !noBottomLatitudeBound) { addLatitudeBound(latitude); } return this; } @Override public Bounds addVerticalPlane(final PlanetModel planetModel, final double longitude, final Plane verticalPlane, final Membership... bounds) { if (!noLongitudeBound) { addLongitudeBound(longitude); } return this; } @Override public Bounds isWide() { return noLongitudeBound(); } @Override public Bounds addXValue(final GeoPoint point) { if (!noLongitudeBound) { // Get a longitude value addLongitudeBound(point.getLongitude()); } return this; } @Override public Bounds addYValue(final GeoPoint point) { if (!noLongitudeBound) { // Get a longitude value addLongitudeBound(point.getLongitude()); } return this; } @Override public Bounds addZValue(final GeoPoint point) { if (!noTopLatitudeBound || !noBottomLatitudeBound) { // Compute a latitude value double latitude = point.getLatitude(); addLatitudeBound(latitude); } return this; } @Override public Bounds addIntersection(final PlanetModel planetModel, final Plane plane1, final Plane plane2, final Membership... bounds) { plane1.recordBounds(planetModel, this, plane2, bounds); return this; } @Override public Bounds addPoint(GeoPoint point) { if (!noLongitudeBound) { // Get a longitude value addLongitudeBound(point.getLongitude()); } if (!noTopLatitudeBound || !noBottomLatitudeBound) { // Compute a latitude value addLatitudeBound(point.getLatitude()); } return this; } @Override public Bounds noLongitudeBound() { noLongitudeBound = true; leftLongitude = null; rightLongitude = null; return this; } @Override public Bounds noTopLatitudeBound() { noTopLatitudeBound = true; maxLatitude = null; return this; } @Override public Bounds noBottomLatitudeBound() { noBottomLatitudeBound = true; minLatitude = null; return this; } @Override public Bounds noBound(final PlanetModel planetModel) { return noLongitudeBound().noTopLatitudeBound().noBottomLatitudeBound(); } // Protected methods /** Update latitude bound. *@param latitude is the latitude. */ private void addLatitudeBound(double latitude) { if (!noTopLatitudeBound && (maxLatitude == null || latitude > maxLatitude)) maxLatitude = latitude; if (!noBottomLatitudeBound && (minLatitude == null || latitude < minLatitude)) minLatitude = latitude; } /** Update longitude bound. *@param longitude is the new longitude value. */ private void addLongitudeBound(double longitude) { // If this point is within the current bounds, we're done; otherwise // expand one side or the other. if (leftLongitude == null && rightLongitude == null) { leftLongitude = longitude; rightLongitude = longitude; } else { // Compute whether we're to the right of the left value. But the left value may be greater than // the right value. double currentLeftLongitude = leftLongitude; double currentRightLongitude = rightLongitude; if (currentRightLongitude < currentLeftLongitude) currentRightLongitude += 2.0 * Math.PI; // We have a range to look at that's going in the right way. // Now, do the same trick with the computed longitude. if (longitude < currentLeftLongitude) longitude += 2.0 * Math.PI; if (longitude < currentLeftLongitude || longitude > currentRightLongitude) { // Outside of current bounds. Consider carefully how we'll expand. double leftExtensionAmt; double rightExtensionAmt; if (longitude < currentLeftLongitude) { leftExtensionAmt = currentLeftLongitude - longitude; } else { leftExtensionAmt = currentLeftLongitude + 2.0 * Math.PI - longitude; } if (longitude > currentRightLongitude) { rightExtensionAmt = longitude - currentRightLongitude; } else { rightExtensionAmt = longitude + 2.0 * Math.PI - currentRightLongitude; } if (leftExtensionAmt < rightExtensionAmt) { currentLeftLongitude = leftLongitude - leftExtensionAmt; while (currentLeftLongitude <= -Math.PI) { currentLeftLongitude += 2.0 * Math.PI; } leftLongitude = currentLeftLongitude; } else { currentRightLongitude = rightLongitude + rightExtensionAmt; while (currentRightLongitude > Math.PI) { currentRightLongitude -= 2.0 * Math.PI; } rightLongitude = currentRightLongitude; } } } double testRightLongitude = rightLongitude; if (testRightLongitude < leftLongitude) testRightLongitude += Math.PI * 2.0; if (testRightLongitude - leftLongitude >= Math.PI) { noLongitudeBound = true; leftLongitude = null; rightLongitude = null; } } }