├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ ├── com │ └── eatthepath │ │ └── jeospatial │ │ ├── BoundingBoxPointFilter.java │ │ ├── GeospatialIndex.java │ │ ├── GeospatialPoint.java │ │ ├── HaversineDistanceFunction.java │ │ ├── SimpleGeospatialPoint.java │ │ ├── VPTreeGeospatialIndex.java │ │ └── package-info.java │ └── overview.html └── test └── java └── com └── eatthepath └── jeospatial ├── BoundingBoxPointFilterTest.java ├── ExampleApp.java ├── HaversineDistanceFunctionTest.java ├── VPTreeConstructionBenchmark.java ├── VPTreeGeospatialIndexTest.java ├── VPTreeGeospatialPointIndexTest.java └── VPTreeQueryBenchmark.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | /target 14 | 15 | # Eclipse project files 16 | .classpath 17 | .project 18 | .settings/ 19 | 20 | # IntelliJ project files 21 | .idea/ 22 | *.iml 23 | 24 | # Generated output 25 | doc/ 26 | 27 | # OS detritus 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | - openjdk7 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Jon Chambers All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 18 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 19 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 20 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 21 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jeospatial 2 | 3 | jeospatial is a simple geospatial index library for Java. It aims to provide an easy-to-use and reasonably-high-performance set of tools for solving the _k_-nearest-neighbor problem on the earth's surface. 4 | 5 | Geospatial indices in this library are implemented using [vantage point trees](http://pnylab.com/pny/papers/vptree/main.html) (or vp-trees), which are binary space partitioning data structures that operate on any metric space. Construction of a geospatial index executes in _O(n log(n))_ time and searches against that index execute in _O(log(n))_ time. As a practical point of reference, it takes about a half second to construct a vp-tree-backed index that contains 100,000 geospatial points on a 2012 MacBook Pro, and that index has a search throughput of about 3,000 queries/second. By contrast, putting all of the points in a list, sorting by distance from a query point, and grabbing the first N results has a search throughput of about 0.6 queries/second. 6 | 7 | ## Major concepts 8 | 9 | The two most important interfaces in the jeospatial library are in the `com.eatthepath.jeospatial` package. 10 | 11 | The `GeospatialPoint` interface defines a single point on the earth's surface; concrete implementations of the `GeospatialPoint` interface can be found in the `com.eatthepath.jeospatial.util` package. 12 | 13 | The `GeospatialIndex` interface defines the behavioral contract for classes that index collections of `GeospatialPoints` and provide facilities for performing nearest-neighbor searches among those points. The `VPTreeGeospatialIndex` class is a concrete implementation of the `GeospatialIndex` interface and can be found in the `com.eatthepath.jeospatial.vptree` package. 14 | 15 | For additional details, see the [API documentation](http://jchambers.github.com/jeospatial/javadoc). 16 | 17 | ## Examples 18 | 19 | ### Finding nearest neighbors 20 | 21 | Let's say we have a list of all of the ZIP codes (postal codes) in the United States and we want to find the ten closest ZIP codes to some point in the world (let's say [Davis Square in Somerville, MA, USA](http://maps.google.com/maps?q=Davis+Square,+Somerville,+MA&hl=en&sll=42.39358,-71.116902&sspn=0.010824,0.017509&oq=Davis+Square,+Somer&t=w&hnear=Davis+Square,+Somerville,+Middlesex,+Massachusetts&z=15)). We might do something like this: 22 | 23 | ```java 24 | VPTree index = new VPTree(zipCodes); 25 | 26 | // Pick a query point (Davis Square in Somerville, MA, USA) 27 | final GeospatialPoint davisSquare = new GeospatialPoint() { ... }; 28 | 29 | // Find the ten nearest zip codes to Davis Square 30 | List nearestZipCodes = index.getNearestNeighbors(davisSquare, 10); 31 | ``` 32 | 33 | The `nearestZipCodes` list will have ten elements; the first will be the closest zip code to Davis Square, the second will be the second closest, and so on. 34 | 35 | ### Finding all neighbors within a certain radius 36 | 37 | Assuming we have the same set of zip codes, we might want to find all of the zip codes that are within a fixed distance Davis Square. For example: 38 | 39 | ```java 40 | // Find all zip codes within ten kilometers of Davis Square 41 | final List zipCodesWithinRange = 42 | index.getAllNeighborsWithinDistance(davisSquare, 10 * 1000); 43 | ``` 44 | 45 | The `zipCodesWithinRange` list will contain all of the zip codes—sorted in order of increasing distance from Davis Square—that are within ten kilometers of Davis Square. 46 | 47 | ### Finding points inside a bounding box 48 | 49 | If you're working with a section of a map with a cylindrical projection (e.g. a Google or Bing map), you might want to find all of the zip codes that are visible in that section of the map. A set of bounding box search methods comes in handy here: 50 | 51 | ```java 52 | // Find all of the zip codes in a bounding "box" 53 | final List inBoundingBox = 54 | index.getAllPointsInBoundingBox(-75, -70, 43, 42); 55 | ``` 56 | 57 | As might expected, the `inBoundingBox` list contains all of the zip codes that fall between the longitude lines of -75 and -70 degrees and the latitude lines of 42 and 43 degrees. Other variants of the `getAllPointsInBoundingBox` method allow for sorting the results by proximity to some point and applying additional search criteria. 58 | 59 | ## License 60 | 61 | jeospatial is an open-source project provided under the [BSD License](http://www.opensource.org/licenses/bsd-license.php). 62 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.eatthepath 6 | jeospatial 7 | 0.2-SNAPSHOT 8 | jeospatial 9 | A geospatial point index library for Java 10 | 11 | 12 | 13 | The MIT License (MIT) 14 | http://opensource.org/licenses/MIT 15 | repo 16 | 17 | 18 | 19 | 20 | org.sonatype.oss 21 | oss-parent 22 | 7 23 | 24 | 25 | 26 | UTF-8 27 | 28 | 29 | 30 | 31 | com.eatthepath 32 | jvptree 33 | 0.3.0 34 | 35 | 36 | 37 | junit 38 | junit 39 | 4.13.1 40 | test 41 | 42 | 43 | 44 | pl.pragmatists 45 | JUnitParams 46 | 1.1.1 47 | test 48 | 49 | 50 | 51 | org.openjdk.jmh 52 | jmh-core 53 | 1.21 54 | test 55 | 56 | 57 | 58 | org.openjdk.jmh 59 | jmh-generator-annprocess 60 | 1.21 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-jar-plugin 70 | 3.0.2 71 | 72 | 73 | **/.gitignore 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 3.1 82 | 83 | 1.7 84 | 1.7 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-source-plugin 90 | 2.2.1 91 | 92 | 93 | attach-sources 94 | 95 | jar 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | benchmark 106 | 107 | 108 | runBenchmarks 109 | true 110 | 111 | 112 | 113 | 114 | 115 | org.codehaus.mojo 116 | exec-maven-plugin 117 | 118 | 119 | run-benchmarks 120 | integration-test 121 | 122 | exec 123 | 124 | 125 | test 126 | java 127 | 128 | -classpath 129 | 130 | org.openjdk.jmh.Main 131 | .* 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | release-sign-artifacts 142 | 143 | 144 | performRelease 145 | true 146 | 147 | 148 | 149 | 150 | 151 | org.apache.maven.plugins 152 | maven-compiler-plugin 153 | 3.1 154 | 155 | 1.7 156 | 1.7 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-javadoc-plugin 162 | 2.9.1 163 | 164 | 165 | attach-javadocs 166 | 167 | jar 168 | 169 | 170 | 171 | 172 | 173 | org.apache.maven.plugins 174 | maven-source-plugin 175 | 2.2.1 176 | 177 | 178 | attach-sources 179 | 180 | jar 181 | 182 | 183 | 184 | 185 | 186 | org.apache.maven.plugins 187 | maven-gpg-plugin 188 | 1.1 189 | 190 | 191 | sign-artifacts 192 | verify 193 | 194 | sign 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | jon 207 | Jon Chambers 208 | jon.chambers@gmail.com 209 | https://github.com/jchambers 210 | 211 | developer 212 | 213 | -5 214 | 215 | 216 | 2012 217 | https://github.com/jchambers/jeospatial 218 | 219 | scm:git:https://github.com/jchambers/jeospatial.git 220 | scm:git:git@github.com:jchambers/jeospatial.git 221 | https://github.com/jchambers/jeospatial 222 | 223 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/BoundingBoxPointFilter.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import com.eatthepath.jvptree.PointFilter; 4 | 5 | /** 6 | * A point filter that accepts only points that fall within a given bounding box. 7 | * 8 | * @author Jon Chambers 9 | */ 10 | class BoundingBoxPointFilter implements PointFilter { 11 | 12 | private final double south; 13 | private final double west; 14 | private final double north; 15 | private final double east; 16 | 17 | BoundingBoxPointFilter(final double south, final double west, final double north, final double east) { 18 | this.south = south; 19 | this.west = west; 20 | this.north = north; 21 | this.east = east; 22 | } 23 | 24 | @Override 25 | public boolean allowPoint(final GeospatialPoint point) { 26 | if (point.getLatitude() <= this.north && point.getLatitude() >= this.south) { 27 | 28 | // If the point is inside the bounding box, it will be shorter to get to the point by traveling east 29 | // from the western boundary than by traveling east from the eastern boundary. 30 | if (this.getDegreesEastFromMeridian(this.west, point) <= this.getDegreesEastFromMeridian(this.east, point)) { 31 | 32 | // Similarly, it should be shorter to get to the point by traveling west from the eastern boundary 33 | // than by traveling west from the western boundary. 34 | return this.getDegreesWestFromMeridian(this.east, point) <= this.getDegreesWestFromMeridian(this.west, point); 35 | } 36 | } 37 | 38 | return false; 39 | } 40 | 41 | /** 42 | * Calculates the minimum eastward angle traveled from a meridian to a point. If the point is coincident with the 43 | * meridian, this method returns 360 degrees. 44 | * 45 | * @param longitude the line of longitude at which to begin travel 46 | * @param point the point to which to travel 47 | * 48 | * @return the eastward-traveling distance between the line and the point in degrees 49 | */ 50 | private double getDegreesEastFromMeridian(final double longitude, final GeospatialPoint point) { 51 | return point.getLongitude() > longitude 52 | ? point.getLongitude() - longitude : Math.abs(360 - (point.getLongitude() - longitude)); 53 | } 54 | 55 | /** 56 | * Calculates the minimum westward angle traveled from a meridian to a point. If the point is coincident with the 57 | * meridian, this method returns 360 degrees. 58 | * 59 | * @param longitude the line of longitude at which to begin travel 60 | * @param point the point to which to travel 61 | * 62 | * @return the westward-traveling distance between the line and the point in degrees 63 | */ 64 | private double getDegreesWestFromMeridian(final double longitude, final GeospatialPoint point) { 65 | return point.getLongitude() < longitude 66 | ? longitude - point.getLongitude() : Math.abs(360 - (longitude - point.getLongitude())); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/GeospatialIndex.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import com.eatthepath.jvptree.PointFilter; 4 | import com.eatthepath.jvptree.SpatialIndex; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * A collection of points on the earth's surface that can be searched efficiently to find points near a given query 10 | * point. 11 | * 12 | * @author Jon Chambers 13 | */ 14 | public interface GeospatialIndex extends SpatialIndex { 15 | 16 | /** 17 | * Returns a list of all points in the index within the given bounding "box." A point is considered to be inside the 18 | * box if its latitude falls between the given north and south limits (inclusive) and its longitude falls between 19 | * the east and west limits (inclusive). The order of the returned list is not prescribed. 20 | * 21 | * @param south the southern limit of the bounding box in degrees 22 | * @param west the western limit of the bounding box in degrees 23 | * @param north the northern limit of the bounding box in degrees 24 | * @param east the eastern limit of the bounding box in degrees 25 | * 26 | * @return a list of points in the index within the given bounding box 27 | * 28 | * @throws IllegalArgumentException if the north or south limits fall outside of the range -90 to +90 (inclusive) or 29 | * if the northern limit is south of the southern limit (or vice versa) 30 | */ 31 | List getAllPointsInBoundingBox(double south, double west, double north, double east); 32 | 33 | /** 34 | * Returns a list of all points in the index within the given bounding "box" that are accepted by the given filter. 35 | * A point is considered to be inside the box if its latitude falls between the given north and south limits 36 | * (inclusive) and its longitude falls between the east and west limits (inclusive). The order of the returned list 37 | * is not prescribed. 38 | * 39 | * @param south the southern limit of the bounding box in degrees 40 | * @param west the western limit of the bounding box in degrees 41 | * @param north the northern limit of the bounding box in degrees 42 | * @param east the eastern limit of the bounding box in degrees 43 | * @param filter a filter to apply to each element to determine if it should be included in the list of elements in 44 | * the given bounding box 45 | * 46 | * @return a list of points in the index within the given bounding box 47 | * 48 | * @throws IllegalArgumentException if the north or south limits fall outside of the range -90 to +90 (inclusive) or 49 | * if the northern limit is south of the southern limit (or vice versa) 50 | */ 51 | List getAllPointsInBoundingBox(double south, double west, double north, double east, final PointFilter filter); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/GeospatialPoint.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | /** 4 | * A geospatial point is a single point on the earth's surface. 5 | * 6 | * @author Jon Chambers 7 | */ 8 | public interface GeospatialPoint { 9 | 10 | /** 11 | * Returns the latitude of this point. 12 | * 13 | * @return the latitude of this point in degrees 14 | */ 15 | double getLatitude(); 16 | 17 | /** 18 | * Returns the longitude of this point. The returned longitude should be 19 | * normalized to the range -180 degrees (inclusive) to 180 degrees 20 | * (exclusive). 21 | * 22 | * @return the longitude of this point in degrees 23 | */ 24 | double getLongitude(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/HaversineDistanceFunction.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import com.eatthepath.jvptree.DistanceFunction; 4 | 5 | /** 6 | * A distance function that calculates the "great circle" distance between two points on the earth's surface using the 7 | * Haversine formula. 8 | * 9 | * @author Jon Chambers 10 | */ 11 | class HaversineDistanceFunction implements DistanceFunction { 12 | 13 | private static final double EARTH_RADIUS = 6371e3; // meters 14 | 15 | /** 16 | * Returns the "great cricle" distance in meters between two points on the earth's surface. 17 | * 18 | * @param firstPoint the first geospatial point 19 | * @param secondPoint the second geospatial point 20 | * 21 | * @return the "great circle" distance in meters between the given points 22 | */ 23 | public double getDistance(final GeospatialPoint firstPoint, final GeospatialPoint secondPoint) { 24 | final double lat1 = Math.toRadians(firstPoint.getLatitude()); 25 | final double lon1 = Math.toRadians(firstPoint.getLongitude()); 26 | final double lat2 = Math.toRadians(secondPoint.getLatitude()); 27 | final double lon2 = Math.toRadians(secondPoint.getLongitude()); 28 | 29 | final double angle = 2 * Math.asin(Math.min(1, Math.sqrt(haversine(lat2 - lat1) + Math.cos(lat1) * Math.cos(lat2) * haversine(lon2 - lon1)))); 30 | 31 | return angle * HaversineDistanceFunction.EARTH_RADIUS; 32 | } 33 | 34 | /** 35 | * Returns the haversine of the given angle. 36 | * 37 | * @param theta the angle, in radians, for which to calculate the haversine 38 | * 39 | * @return the haversine of the given angle 40 | * 41 | * @see Versine - Wikipedia 42 | */ 43 | private static double haversine(final double theta) { 44 | final double x = Math.sin(theta / 2); 45 | return (x * x); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/SimpleGeospatialPoint.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | /** 4 | * A simple, immutable implementation of the {@link GeospatialPoint} interface. 5 | */ 6 | public class SimpleGeospatialPoint implements GeospatialPoint { 7 | 8 | private final double latitude; 9 | private final double longitude; 10 | 11 | /** 12 | * Constructs a new, immutable geospatial point with the given latitude and longitude. 13 | * 14 | * @param latitude the latitude (in degrees) of this point 15 | * @param longitude the longitude (in degrees) of this point 16 | */ 17 | public SimpleGeospatialPoint(final double latitude, final double longitude) { 18 | this.latitude = latitude; 19 | this.longitude = longitude; 20 | } 21 | 22 | @Override 23 | public double getLatitude() { 24 | return this.latitude; 25 | } 26 | 27 | @Override 28 | public double getLongitude() { 29 | return this.longitude; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/VPTreeGeospatialIndex.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import java.util.*; 4 | 5 | import com.eatthepath.jvptree.PointFilter; 6 | import com.eatthepath.jvptree.VPTree; 7 | 8 | public class VPTreeGeospatialIndex extends VPTree implements GeospatialIndex { 9 | 10 | private static final PointFilter NO_OP_POINT_FILTER = new PointFilter() { 11 | @Override 12 | public boolean allowPoint(final Object point) { 13 | return true; 14 | } 15 | }; 16 | 17 | private static final HaversineDistanceFunction HAVERSINE_DISTANCE_FUNCTION = new HaversineDistanceFunction(); 18 | 19 | public VPTreeGeospatialIndex() { 20 | super(HAVERSINE_DISTANCE_FUNCTION); 21 | } 22 | 23 | public VPTreeGeospatialIndex(final Collection points) { 24 | super(HAVERSINE_DISTANCE_FUNCTION, points); 25 | } 26 | 27 | public List getAllPointsInBoundingBox(final double south, final double west, final double north, final double east) { 28 | //noinspection unchecked 29 | return getAllPointsInBoundingBox(south, west, north, east, NO_OP_POINT_FILTER); 30 | } 31 | 32 | @Override 33 | public List getAllPointsInBoundingBox(final double south, final double west, final double north, final double east, final PointFilter filter) { 34 | final GeospatialPoint centroid; 35 | { 36 | final double southRad = Math.toRadians(south); 37 | final double northRad = Math.toRadians(north); 38 | final double westRad = Math.toRadians(west); 39 | final double eastRad = Math.toRadians(east); 40 | 41 | // Via https://www.movable-type.co.uk/scripts/latlong.html 42 | final double Bx = Math.cos(northRad) * Math.cos(eastRad - westRad); 43 | final double By = Math.cos(northRad) * Math.sin(eastRad - westRad); 44 | 45 | final double latitudeRad = Math.atan2(Math.sin(southRad) + Math.sin(northRad), Math.sqrt((Math.cos(southRad) + Bx) * (Math.cos(southRad) + Bx) + (By * By))); 46 | final double longitudeRad = westRad + Math.atan2(By, Math.cos(southRad) + Bx); 47 | 48 | centroid = new SimpleGeospatialPoint(Math.toDegrees(latitudeRad), Math.toDegrees(longitudeRad)); 49 | } 50 | 51 | // TODO There's almost certainly a more efficient way to figure this out 52 | final double searchRadius = Collections.max(Arrays.asList( 53 | HAVERSINE_DISTANCE_FUNCTION.getDistance(centroid, new SimpleGeospatialPoint(south, west)), 54 | HAVERSINE_DISTANCE_FUNCTION.getDistance(centroid, new SimpleGeospatialPoint(north, west)), 55 | HAVERSINE_DISTANCE_FUNCTION.getDistance(centroid, new SimpleGeospatialPoint(north, east)), 56 | HAVERSINE_DISTANCE_FUNCTION.getDistance(centroid, new SimpleGeospatialPoint(south, east)))); 57 | 58 | final BoundingBoxPointFilter boundingBoxPointFilter = new BoundingBoxPointFilter(south, west, north, east); 59 | 60 | final PointFilter combinedFilter = new PointFilter() { 61 | 62 | @Override 63 | public boolean allowPoint(final E point) { 64 | return filter.allowPoint(point) && boundingBoxPointFilter.allowPoint(point); 65 | } 66 | }; 67 | 68 | return this.getAllWithinDistance(centroid, searchRadius, combinedFilter); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/eatthepath/jeospatial/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides core interfaces for the jeospatial library. 3 | */ 4 | package com.eatthepath.jeospatial; -------------------------------------------------------------------------------- /src/main/java/overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | jeospatial API Specification 4 | 5 | 6 | 7 |

Jeospatial is a simple geospatial point database library for Java. It aims to provide an easy-to-use and reasonably-high-performance set of tools for solving the k-nearest-neighbor problem on the earth's surface.

8 | 9 |

The jeospatial project is hosted at https://github.com/jchambers/jeospatial. Please use the project page to report any issues with the library or its documentation.

10 | 11 | -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/BoundingBoxPointFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import junitparams.JUnitParamsRunner; 4 | import junitparams.Parameters; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | 8 | import static org.junit.Assert.*; 9 | 10 | @RunWith(JUnitParamsRunner.class) 11 | public class BoundingBoxPointFilterTest { 12 | 13 | private static final double SOUTH = 0; 14 | private static final double WEST = -20; 15 | private static final double NORTH = 40; 16 | private static final double EAST = 30; 17 | 18 | private static final BoundingBoxPointFilter BOUNDING_BOX_POINT_FILTER = 19 | new BoundingBoxPointFilter(SOUTH, WEST, NORTH, EAST); 20 | 21 | @Test 22 | @Parameters(method = "getParametersForTestAllowPoint") 23 | public void testAllowPoint(final GeospatialPoint point, final boolean expectAllowed) { 24 | if (expectAllowed) { 25 | assertTrue(BOUNDING_BOX_POINT_FILTER.allowPoint(point)); 26 | } else { 27 | assertFalse(BOUNDING_BOX_POINT_FILTER.allowPoint(point)); 28 | } 29 | } 30 | 31 | private Object getParametersForTestAllowPoint() { 32 | return new Object[][] { 33 | // Southwest corner 34 | { new SimpleGeospatialPoint(SOUTH, WEST), true }, 35 | 36 | // Northwest corner 37 | { new SimpleGeospatialPoint(NORTH, WEST), true }, 38 | 39 | // Northeast corner 40 | { new SimpleGeospatialPoint(NORTH, EAST), true }, 41 | 42 | // Southeast corner 43 | { new SimpleGeospatialPoint(SOUTH, EAST), true }, 44 | 45 | // Middle-ish 46 | { new SimpleGeospatialPoint(10, -15), true }, 47 | 48 | // Too far east 49 | { new SimpleGeospatialPoint(NORTH, EAST + 1), false }, 50 | 51 | // Too far west 52 | { new SimpleGeospatialPoint(NORTH, WEST - 1), false }, 53 | 54 | // Too far north 55 | { new SimpleGeospatialPoint(NORTH + 1, EAST), false }, 56 | 57 | // Too far south 58 | { new SimpleGeospatialPoint(SOUTH - 1, EAST), false }, 59 | }; 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/ExampleApp.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import java.util.List; 4 | 5 | import com.eatthepath.jeospatial.GeospatialIndex; 6 | import com.eatthepath.jeospatial.VPTreeGeospatialIndex; 7 | 8 | /** 9 | * A very simple test application that shows the very basics of using a 10 | * geospatial point database. 11 | * 12 | * @author Jon Chambers 13 | */ 14 | public class ExampleApp { 15 | private static class ZipCode implements GeospatialPoint { 16 | private final int code; 17 | private final String city; 18 | private final String state; 19 | 20 | private final double latitude; 21 | private final double longitude; 22 | 23 | /** 24 | * Constructs a new {@code ZipCode} with the given numeric zip code, city name, state abbreviation, latitude, and 25 | * longitude. 26 | * 27 | * @param code the five-digit numeric zip code for the region 28 | * @param city the name of the city in which this zip code is located 29 | * @param state the two-letter abbreviation of the state in which this zip code is located 30 | * @param latitude the latitude of the approximate center of this region 31 | * @param longitude the longitude of the approximate center of this region 32 | */ 33 | public ZipCode(final int code, final String city, final String state, final double latitude, final double longitude) { 34 | this.code = code; 35 | this.city = city; 36 | this.state = state; 37 | 38 | this.latitude = latitude; 39 | this.longitude = longitude; 40 | } 41 | 42 | /** 43 | * Returns the numeric five-digit code associated with this region. 44 | * 45 | * @return the numeric zip code of this region 46 | */ 47 | public int getCode() { 48 | return this.code; 49 | } 50 | 51 | /** 52 | * Returns the name of the city in which this zip code is located. 53 | * 54 | * @return the name of the city in which this zip code is located 55 | */ 56 | public String getCity() { 57 | return this.city; 58 | } 59 | 60 | /** 61 | * Returns the two-letter abbreviation of the state in which this zip code 62 | * is located. 63 | * 64 | * @return the two-letter abbreviation of the state in which this zip code 65 | * is located 66 | */ 67 | public String getState() { 68 | return this.state; 69 | } 70 | 71 | @Override 72 | public double getLatitude() { 73 | return this.latitude; 74 | } 75 | 76 | @Override 77 | public double getLongitude() { 78 | return this.longitude; 79 | } 80 | } 81 | 82 | private static final GeospatialPoint DAVIS_SQUARE = 83 | new SimpleGeospatialPoint(42.396745, -71.122479); 84 | 85 | @SuppressWarnings("unused") 86 | public static void getTenClosestNeighbors(final List zipCodes) { 87 | final GeospatialIndex index = new VPTreeGeospatialIndex<>(zipCodes); 88 | 89 | // Find the ten nearest zip codes to Davis Square 90 | final List nearestZipCodes = index.getNearestNeighbors(DAVIS_SQUARE, 10); 91 | } 92 | 93 | @SuppressWarnings("unused") 94 | public static void getAllWithinRange(final List zipCodes) { 95 | final GeospatialIndex index = new VPTreeGeospatialIndex<>(zipCodes); 96 | 97 | // Find all zip codes within ten kilometers of Davis Square 98 | final List zipCodesWithinRange = index.getAllWithinDistance(DAVIS_SQUARE, 10e3); 99 | } 100 | 101 | @SuppressWarnings("unused") 102 | public static void getAllInBoundingBox(final List zipCodes) { 103 | final GeospatialIndex index = new VPTreeGeospatialIndex<>(zipCodes); 104 | 105 | // Find all of the zip codes in a bounding "box" 106 | final List inBoundingBox = index.getAllPointsInBoundingBox(-75, -70, 43, 42); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/HaversineDistanceFunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | public class HaversineDistanceFunctionTest { 8 | 9 | @Test 10 | public void testGetDistance() { 11 | final HaversineDistanceFunction distanceFunction = 12 | new HaversineDistanceFunction(); 13 | 14 | final SimpleGeospatialPoint BOS = new SimpleGeospatialPoint(42.3631, -71.0064); 15 | final SimpleGeospatialPoint LAX = new SimpleGeospatialPoint(33.9425, -118.4072); 16 | 17 | assertEquals("Distance from point to self must be zero.", 18 | 0, distanceFunction.getDistance(BOS, BOS), 0); 19 | 20 | assertEquals("Distance from A to B must be equal to distance from B to A.", 21 | distanceFunction.getDistance(BOS, LAX), distanceFunction.getDistance(LAX, BOS), 0); 22 | 23 | assertEquals("Distance between BOS and LAX should be within 1km of 4,193km.", 24 | 4193000, distanceFunction.getDistance(BOS, LAX), 1000); 25 | 26 | final SimpleGeospatialPoint a = new SimpleGeospatialPoint(0, 0); 27 | final SimpleGeospatialPoint b = new SimpleGeospatialPoint(0, 180); 28 | 29 | assertEquals("Distance between diametrically opposed points should be within 1m of 2,001,5086m.", 30 | 20015086, distanceFunction.getDistance(a, b), 1); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/VPTreeConstructionBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014, Oracle America, Inc. 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, 9 | * this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * * Neither the name of Oracle nor the names of its contributors may be used 16 | * to endorse or promote products derived from this software without 17 | * specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 29 | * THE POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | package com.eatthepath.jeospatial; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | import java.util.Random; 37 | 38 | import org.openjdk.jmh.annotations.Benchmark; 39 | import org.openjdk.jmh.annotations.Param; 40 | import org.openjdk.jmh.annotations.Scope; 41 | import org.openjdk.jmh.annotations.State; 42 | 43 | @State(Scope.Thread) 44 | public class VPTreeConstructionBenchmark { 45 | 46 | @Param({"100000"}) 47 | public int pointCount; 48 | 49 | private final Random random = new Random(); 50 | 51 | @Benchmark 52 | public VPTreeGeospatialIndex benchmarkConstructVpTree() { 53 | final List points = new ArrayList<>(this.pointCount); 54 | 55 | for (int i = 0; i < this.pointCount; i++) { 56 | points.add(this.createRandomPoint()); 57 | } 58 | 59 | return new VPTreeGeospatialIndex<>(points); 60 | } 61 | 62 | private GeospatialPoint createRandomPoint() { 63 | final double latitude = (this.random.nextDouble() * 180.0) - 90; 64 | final double longitude = (this.random.nextDouble() * 360.0) - 180; 65 | 66 | return new SimpleGeospatialPoint(latitude, longitude); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/VPTreeGeospatialIndexTest.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import com.eatthepath.jvptree.PointFilter; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | public class VPTreeGeospatialIndexTest { 14 | 15 | private static final double SOUTH = 0; 16 | private static final double WEST = -20; 17 | private static final double NORTH = 40; 18 | private static final double EAST = 30; 19 | 20 | private static class PossiblyFancyGeospatialPoint extends SimpleGeospatialPoint { 21 | 22 | private final boolean fancy; 23 | 24 | public PossiblyFancyGeospatialPoint(final double latitude, final double longitude, final boolean fancy) { 25 | super(latitude, longitude); 26 | 27 | this.fancy = fancy; 28 | } 29 | 30 | public boolean isFancy() { 31 | return fancy; 32 | } 33 | } 34 | 35 | @Test 36 | public void testGetAllPointsInBoundingBox() { 37 | final List pointsInBox = Arrays.asList( 38 | new PossiblyFancyGeospatialPoint(SOUTH, WEST, true), 39 | new PossiblyFancyGeospatialPoint(NORTH, WEST, true), 40 | new PossiblyFancyGeospatialPoint(NORTH, EAST, false), 41 | new PossiblyFancyGeospatialPoint(SOUTH, EAST, false)); 42 | 43 | final List fancyPointsInBox = Arrays.asList( 44 | pointsInBox.get(0), 45 | pointsInBox.get(1)); 46 | 47 | final List pointsOutsideOfBox = Arrays.asList( 48 | new PossiblyFancyGeospatialPoint(SOUTH - 1, WEST - 1, true), 49 | new PossiblyFancyGeospatialPoint(NORTH + 1, WEST - 1, false), 50 | new PossiblyFancyGeospatialPoint(NORTH + 1, EAST + 1, false), 51 | new PossiblyFancyGeospatialPoint(SOUTH - 1, EAST + 1, true)); 52 | 53 | final VPTreeGeospatialIndex vpTree = new VPTreeGeospatialIndex<>(); 54 | vpTree.addAll(pointsInBox); 55 | vpTree.addAll(pointsOutsideOfBox); 56 | 57 | { 58 | final List pointsFromQuery = 59 | vpTree.getAllPointsInBoundingBox(SOUTH, WEST, NORTH, EAST); 60 | 61 | assertEquals(pointsInBox.size(), pointsFromQuery.size()); 62 | assertTrue(pointsFromQuery.containsAll(pointsInBox)); 63 | } 64 | 65 | { 66 | final PointFilter fancyFilter = new PointFilter() { 67 | 68 | @Override 69 | public boolean allowPoint(final PossiblyFancyGeospatialPoint point) { 70 | return point.isFancy(); 71 | } 72 | }; 73 | 74 | final List pointsFromQuery = 75 | vpTree.getAllPointsInBoundingBox(SOUTH, WEST, NORTH, EAST, fancyFilter); 76 | 77 | assertEquals(fancyPointsInBox.size(), pointsFromQuery.size()); 78 | assertTrue(pointsFromQuery.containsAll(fancyPointsInBox)); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/VPTreeGeospatialPointIndexTest.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.Test; 8 | 9 | public class VPTreeGeospatialPointIndexTest { 10 | 11 | @Test 12 | public void testGetAllPointsInBoundingBox() { 13 | final List points = java.util.Arrays.asList(new SimpleGeospatialPoint[] { 14 | new SimpleGeospatialPoint(-5, -5), 15 | new SimpleGeospatialPoint(-4, -4), 16 | new SimpleGeospatialPoint(-3, -3), 17 | new SimpleGeospatialPoint(-2, -2), 18 | new SimpleGeospatialPoint(-1, -1), 19 | new SimpleGeospatialPoint(0, 0), 20 | new SimpleGeospatialPoint(1, 1), 21 | new SimpleGeospatialPoint(2, 2), 22 | new SimpleGeospatialPoint(3, 3), 23 | new SimpleGeospatialPoint(4, 4), 24 | new SimpleGeospatialPoint(5, 5), 25 | new SimpleGeospatialPoint(-2, 0), 26 | new SimpleGeospatialPoint(2, 0), 27 | new SimpleGeospatialPoint(0, -2), 28 | new SimpleGeospatialPoint(0, 2) 29 | }); 30 | 31 | final VPTreeGeospatialIndex index = 32 | new VPTreeGeospatialIndex<>(points); 33 | 34 | final List pointsInBox = index.getAllPointsInBoundingBox(-2, -2, 2, 2); 35 | 36 | assertEquals(9, pointsInBox.size()); 37 | 38 | for (final SimpleGeospatialPoint point : pointsInBox) { 39 | assertTrue(point.getLatitude() >= -2); 40 | assertTrue(point.getLatitude() <= 2); 41 | assertTrue(point.getLongitude() >= -2); 42 | assertTrue(point.getLongitude() <= 2); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/eatthepath/jeospatial/VPTreeQueryBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.eatthepath.jeospatial; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Random; 7 | 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.Param; 10 | import org.openjdk.jmh.annotations.Scope; 11 | import org.openjdk.jmh.annotations.Setup; 12 | import org.openjdk.jmh.annotations.State; 13 | 14 | import com.eatthepath.jvptree.DistanceComparator; 15 | 16 | @State(Scope.Thread) 17 | public class VPTreeQueryBenchmark { 18 | 19 | @Param({"100000"}) 20 | public int pointCount; 21 | 22 | private List points; 23 | private VPTreeGeospatialIndex index; 24 | 25 | private final Random random = new Random(); 26 | private final HaversineDistanceFunction distanceFunction = new HaversineDistanceFunction(); 27 | 28 | private static final int RESULT_SET_SIZE = 32; 29 | 30 | @Setup 31 | public void setUp() { 32 | this.points = new ArrayList<>(this.pointCount); 33 | 34 | for (int i = 0; i < this.pointCount; i++) { 35 | this.points.add(this.createRandomPoint()); 36 | } 37 | 38 | this.index = new VPTreeGeospatialIndex<>(this.points); 39 | } 40 | 41 | @Benchmark 42 | public List benchmarkNaiveSearch() { 43 | Collections.sort(this.points, new DistanceComparator<>(this.createRandomPoint(), this.distanceFunction)); 44 | return this.points.subList(0, RESULT_SET_SIZE); 45 | } 46 | 47 | @Benchmark 48 | public List benchmarkQueryTree() { 49 | return this.index.getNearestNeighbors(this.createRandomPoint(), RESULT_SET_SIZE); 50 | } 51 | 52 | private GeospatialPoint createRandomPoint() { 53 | final double latitude = (this.random.nextDouble() * 180.0) - 90; 54 | final double longitude = (this.random.nextDouble() * 360.0) - 180; 55 | 56 | return new SimpleGeospatialPoint(latitude, longitude); 57 | } 58 | } 59 | --------------------------------------------------------------------------------