| /* |
| * 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.geo; |
| |
| import java.util.Locale; |
| import java.util.Random; |
| |
| import org.apache.lucene.util.LuceneTestCase; |
| import org.apache.lucene.util.SloppyMath; |
| |
| /** |
| * Tests class for methods in GeoUtils |
| * |
| * @lucene.experimental |
| */ |
| public class TestGeoUtils extends LuceneTestCase { |
| |
| // We rely heavily on GeoUtils.circleToBBox so we test it here: |
| public void testRandomCircleToBBox() throws Exception { |
| int iters = atLeast(100); |
| for(int iter=0;iter<iters;iter++) { |
| |
| double centerLat = GeoTestUtil.nextLatitude(); |
| double centerLon = GeoTestUtil.nextLongitude(); |
| |
| final double radiusMeters; |
| if (random().nextBoolean()) { |
| // Approx 4 degrees lon at the equator: |
| radiusMeters = random().nextDouble() * 444000; |
| } else { |
| radiusMeters = random().nextDouble() * 50000000; |
| } |
| |
| // TODO: randomly quantize radius too, to provoke exact math errors? |
| |
| Rectangle bbox = Rectangle.fromPointDistance(centerLat, centerLon, radiusMeters); |
| |
| int numPointsToTry = 1000; |
| for(int i=0;i<numPointsToTry;i++) { |
| |
| double point[] = GeoTestUtil.nextPointNear(bbox); |
| double lat = point[0]; |
| double lon = point[1]; |
| |
| double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); |
| |
| // Haversin says it's within the circle: |
| boolean haversinSays = distanceMeters <= radiusMeters; |
| |
| // BBox says its within the box: |
| boolean bboxSays; |
| if (bbox.crossesDateline()) { |
| if (lat >= bbox.minLat && lat <= bbox.maxLat) { |
| bboxSays = lon <= bbox.maxLon || lon >= bbox.minLon; |
| } else { |
| bboxSays = false; |
| } |
| } else { |
| bboxSays = lat >= bbox.minLat && lat <= bbox.maxLat && lon >= bbox.minLon && lon <= bbox.maxLon; |
| } |
| |
| if (haversinSays) { |
| if (bboxSays == false) { |
| System.out.println("centerLat=" + centerLat + " centerLon=" + centerLon + " radiusMeters=" + radiusMeters); |
| System.out.println(" bbox: lat=" + bbox.minLat + " to " + bbox.maxLat + " lon=" + bbox.minLon + " to " + bbox.maxLon); |
| System.out.println(" point: lat=" + lat + " lon=" + lon); |
| System.out.println(" haversin: " + distanceMeters); |
| fail("point was within the distance according to haversin, but the bbox doesn't contain it"); |
| } |
| } else { |
| // it's fine if haversin said it was outside the radius and bbox said it was inside the box |
| } |
| } |
| } |
| } |
| |
| // similar to testRandomCircleToBBox, but different, less evil, maybe simpler |
| public void testBoundingBoxOpto() { |
| int iters = atLeast(100); |
| for (int i = 0; i < iters; i++) { |
| double lat = GeoTestUtil.nextLatitude(); |
| double lon = GeoTestUtil.nextLongitude(); |
| double radius = 50000000 * random().nextDouble(); |
| Rectangle box = Rectangle.fromPointDistance(lat, lon, radius); |
| final Rectangle box1; |
| final Rectangle box2; |
| if (box.crossesDateline()) { |
| box1 = new Rectangle(box.minLat, box.maxLat, -180, box.maxLon); |
| box2 = new Rectangle(box.minLat, box.maxLat, box.minLon, 180); |
| } else { |
| box1 = box; |
| box2 = null; |
| } |
| |
| for (int j = 0; j < 1000; j++) { |
| double point[] = GeoTestUtil.nextPointNear(box); |
| double lat2 = point[0]; |
| double lon2 = point[1]; |
| // if the point is within radius, then it should be in our bounding box |
| if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { |
| assertTrue(lat >= box.minLat && lat <= box.maxLat); |
| assertTrue(lon >= box1.minLon && lon <= box1.maxLon || (box2 != null && lon >= box2.minLon && lon <= box2.maxLon)); |
| } |
| } |
| } |
| } |
| |
| // test we can use haversinSortKey() for distance queries. |
| public void testHaversinOpto() { |
| int iters = atLeast(100); |
| for (int i = 0; i < iters; i++) { |
| double lat = GeoTestUtil.nextLatitude(); |
| double lon = GeoTestUtil.nextLongitude(); |
| double radius = 50000000 * random().nextDouble(); |
| Rectangle box = Rectangle.fromPointDistance(lat, lon, radius); |
| |
| if (box.maxLon - lon < 90 && lon - box.minLon < 90) { |
| double minPartialDistance = Math.max(SloppyMath.haversinSortKey(lat, lon, lat, box.maxLon), |
| SloppyMath.haversinSortKey(lat, lon, box.maxLat, lon)); |
| |
| for (int j = 0; j < 10000; j++) { |
| double point[] = GeoTestUtil.nextPointNear(box); |
| double lat2 = point[0]; |
| double lon2 = point[1]; |
| // if the point is within radius, then it should be <= our sort key |
| if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { |
| assertTrue(SloppyMath.haversinSortKey(lat, lon, lat2, lon2) <= minPartialDistance); |
| } |
| } |
| } |
| } |
| } |
| |
| /** Test infinite radius covers whole earth */ |
| public void testInfiniteRect() { |
| for (int i = 0; i < 1000; i++) { |
| double centerLat = GeoTestUtil.nextLatitude(); |
| double centerLon = GeoTestUtil.nextLongitude(); |
| Rectangle rect = Rectangle.fromPointDistance(centerLat, centerLon, Double.POSITIVE_INFINITY); |
| assertEquals(-180.0, rect.minLon, 0.0D); |
| assertEquals(180.0, rect.maxLon, 0.0D); |
| assertEquals(-90.0, rect.minLat, 0.0D); |
| assertEquals(90.0, rect.maxLat, 0.0D); |
| assertFalse(rect.crossesDateline()); |
| } |
| } |
| |
| public void testAxisLat() { |
| double earthCircumference = 2D * Math.PI * GeoUtils.EARTH_MEAN_RADIUS_METERS; |
| assertEquals(90, Rectangle.axisLat(0, earthCircumference / 4), 0.0D); |
| |
| for (int i = 0; i < 100; ++i) { |
| boolean reallyBig = random().nextInt(10) == 0; |
| final double maxRadius = reallyBig ? 1.1 * earthCircumference : earthCircumference / 8; |
| final double radius = maxRadius * random().nextDouble(); |
| double prevAxisLat = Rectangle.axisLat(0.0D, radius); |
| for (double lat = 0.1D; lat < 90D; lat += 0.1D) { |
| double nextAxisLat = Rectangle.axisLat(lat, radius); |
| Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius); |
| double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon); |
| if (nextAxisLat < GeoUtils.MAX_LAT_INCL) { |
| assertEquals("lat = " + lat, dist, radius, 0.1D); |
| } |
| assertTrue("lat = " + lat, prevAxisLat <= nextAxisLat); |
| prevAxisLat = nextAxisLat; |
| } |
| |
| prevAxisLat = Rectangle.axisLat(-0.0D, radius); |
| for (double lat = -0.1D; lat > -90D; lat -= 0.1D) { |
| double nextAxisLat = Rectangle.axisLat(lat, radius); |
| Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius); |
| double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon); |
| if (nextAxisLat > GeoUtils.MIN_LAT_INCL) { |
| assertEquals("lat = " + lat, dist, radius, 0.1D); |
| } |
| assertTrue("lat = " + lat, prevAxisLat >= nextAxisLat); |
| prevAxisLat = nextAxisLat; |
| } |
| } |
| } |
| |
| // TODO: does not really belong here, but we test it like this for now |
| // we can make a fake IndexReader to send boxes directly to Point visitors instead? |
| public void testCircleOpto() throws Exception { |
| Random random = random(); |
| int iters = atLeast(random, 3); |
| for (int i = 0; i < iters; i++) { |
| // circle |
| final double centerLat = -90 + 180.0 * random.nextDouble(); |
| final double centerLon = -180 + 360.0 * random.nextDouble(); |
| final double radius = 50_000_000D * random.nextDouble(); |
| final Rectangle box = Rectangle.fromPointDistance(centerLat, centerLon, radius); |
| // TODO: remove this leniency! |
| if (box.crossesDateline()) { |
| --i; // try again... |
| continue; |
| } |
| final double axisLat = Rectangle.axisLat(centerLat, radius); |
| |
| int innerIters = atLeast(100); |
| for (int k = 0; k < innerIters; ++k) { |
| double[] latBounds = {-90, box.minLat, axisLat, box.maxLat, 90}; |
| double[] lonBounds = {-180, box.minLon, centerLon, box.maxLon, 180}; |
| // first choose an upper left corner |
| int maxLatRow = random.nextInt(4); |
| double latMax = randomInRange(random, latBounds[maxLatRow], latBounds[maxLatRow + 1]); |
| int minLonCol = random.nextInt(4); |
| double lonMin = randomInRange(random, lonBounds[minLonCol], lonBounds[minLonCol + 1]); |
| // now choose a lower right corner |
| int minLatMaxRow = maxLatRow == 3 ? 3 : maxLatRow + 1; // make sure it will at least cross into the bbox |
| int minLatRow = random.nextInt(minLatMaxRow); |
| double latMin = randomInRange(random, latBounds[minLatRow], Math.min(latBounds[minLatRow + 1], latMax)); |
| int maxLonMinCol = Math.max(minLonCol, 1); // make sure it will at least cross into the bbox |
| int maxLonCol = maxLonMinCol + random.nextInt(4 - maxLonMinCol); |
| double lonMax = randomInRange(random, Math.max(lonBounds[maxLonCol], lonMin), lonBounds[maxLonCol + 1]); |
| |
| assert latMax >= latMin; |
| assert lonMax >= lonMin; |
| |
| if (isDisjoint(centerLat, centerLon, radius, axisLat, latMin, latMax, lonMin, lonMax)) { |
| // intersects says false: test a ton of points |
| for (int j = 0; j < 200; j++) { |
| double lat = latMin + (latMax - latMin) * random.nextDouble(); |
| double lon = lonMin + (lonMax - lonMin) * random.nextDouble(); |
| |
| if (random.nextBoolean()) { |
| // explicitly test an edge |
| int edge = random.nextInt(4); |
| if (edge == 0) { |
| lat = latMin; |
| } else if (edge == 1) { |
| lat = latMax; |
| } else if (edge == 2) { |
| lon = lonMin; |
| } else if (edge == 3) { |
| lon = lonMax; |
| } |
| } |
| double distance = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); |
| try { |
| assertTrue(String.format(Locale.ROOT, "\nisDisjoint(\n" + |
| "centerLat=%s\n" + |
| "centerLon=%s\n" + |
| "radius=%s\n" + |
| "latMin=%s\n" + |
| "latMax=%s\n" + |
| "lonMin=%s\n" + |
| "lonMax=%s) == false BUT\n" + |
| "haversin(%s, %s, %s, %s) = %s\nbbox=%s", |
| centerLat, centerLon, radius, latMin, latMax, lonMin, lonMax, |
| centerLat, centerLon, lat, lon, distance, Rectangle.fromPointDistance(centerLat, centerLon, radius)), |
| distance > radius); |
| } catch (AssertionError e) { |
| EarthDebugger ed = new EarthDebugger(); |
| ed.addRect(latMin, latMax, lonMin, lonMax); |
| ed.addCircle(centerLat, centerLon, radius, true); |
| System.out.println(ed.finish()); |
| throw e; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| static double randomInRange(Random random, double min, double max) { |
| return min + (max - min) * random.nextDouble(); |
| } |
| |
| static boolean isDisjoint(double centerLat, double centerLon, double radius, double axisLat, double latMin, double latMax, double lonMin, double lonMax) { |
| if ((centerLon < lonMin || centerLon > lonMax) && (axisLat+ Rectangle.AXISLAT_ERROR < latMin || axisLat- Rectangle.AXISLAT_ERROR > latMax)) { |
| // circle not fully inside / crossing axis |
| if (SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMin) > radius && |
| SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMax) > radius && |
| SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMin) > radius && |
| SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMax) > radius) { |
| // no points inside |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| public void testWithin90LonDegrees() { |
| assertTrue(GeoUtils.within90LonDegrees(0, -80, 80)); |
| assertFalse(GeoUtils.within90LonDegrees(0, -100, 80)); |
| assertFalse(GeoUtils.within90LonDegrees(0, -80, 100)); |
| |
| assertTrue(GeoUtils.within90LonDegrees(-150, 140, 170)); |
| assertFalse(GeoUtils.within90LonDegrees(-150, 120, 150)); |
| |
| assertTrue(GeoUtils.within90LonDegrees(150, -170, -140)); |
| assertFalse(GeoUtils.within90LonDegrees(150, -150, -120)); |
| } |
| } |