junit
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeoNearFilter.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeoNearFilter.java
new file mode 100644
index 000000000..f282d624b
--- /dev/null
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeoNearFilter.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2017-2020. Nitrite author or authors.
+ *
+ * Licensed 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.dizitart.no2.spatial;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.util.GeometricShapeFactory;
+
+/**
+ * Spatial filter for finding geometries near a geographic point,
+ * using geodesic distance on Earth's surface (WGS84 ellipsoid).
+ *
+ * This filter is specifically designed for geographic coordinates (lat/long).
+ * It always uses geodesic distance calculations, eliminating the ambiguity
+ * of {@link NearFilter}'s auto-detection.
+ *
+ * Usage Example:
+ * {@code
+ * GeoPoint center = new GeoPoint(45.0, -93.2650); // Minneapolis
+ * collection.find(where("location").geoNear(center, 5000.0)); // 5km radius
+ * }
+ *
+ * Distance Units: The distance parameter must be in meters.
+ *
+ * Accuracy: This filter uses two-pass query execution for accurate results:
+ * Phase 1 performs a fast R-tree bounding box search, and Phase 2 refines results using
+ * precise JTS geometric operations to eliminate false positives.
+ *
+ * @since 4.3.3
+ * @author Anindya Chatterjee
+ * @see GeoPoint
+ * @see NearFilter
+ */
+class GeoNearFilter extends WithinFilter {
+
+ /**
+ * Creates a filter to find geometries near a GeoPoint.
+ *
+ * @param field the field to filter on
+ * @param point the geographic point to check proximity to
+ * @param distanceMeters the maximum distance in meters
+ */
+ GeoNearFilter(String field, GeoPoint point, Double distanceMeters) {
+ super(field, createGeodesicCircle(point.getCoordinate(), distanceMeters));
+ }
+
+ /**
+ * Creates a filter to find geometries near a coordinate.
+ * The coordinate is validated to ensure it represents a valid geographic point.
+ *
+ * @param field the field to filter on
+ * @param point the coordinate to check proximity to (x=longitude, y=latitude)
+ * @param distanceMeters the maximum distance in meters
+ * @throws IllegalArgumentException if coordinates are not valid geographic coordinates
+ */
+ GeoNearFilter(String field, Coordinate point, Double distanceMeters) {
+ super(field, createGeodesicCircle(validateAndGetCoordinate(point), distanceMeters));
+ }
+
+ private static Coordinate validateAndGetCoordinate(Coordinate coord) {
+ double lat = coord.getY();
+ double lon = coord.getX();
+
+ if (lat < -90.0 || lat > 90.0) {
+ throw new IllegalArgumentException(
+ "GeoNearFilter requires valid latitude (-90 to 90), got: " + lat);
+ }
+ if (lon < -180.0 || lon > 180.0) {
+ throw new IllegalArgumentException(
+ "GeoNearFilter requires valid longitude (-180 to 180), got: " + lon);
+ }
+
+ return coord;
+ }
+
+ private static Geometry createGeodesicCircle(Coordinate center, double radiusMeters) {
+ GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
+ shapeFactory.setNumPoints(64);
+ shapeFactory.setCentre(center);
+
+ // Always use geodesic calculations for GeoNearFilter
+ double radiusInDegrees = GeodesicUtils.metersToDegreesRadius(center, radiusMeters);
+ shapeFactory.setSize(radiusInDegrees * 2);
+ return shapeFactory.createCircle();
+ }
+
+ @Override
+ public String toString() {
+ return "(" + getField() + " geoNear " + getValue() + ")";
+ }
+}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeoPoint.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeoPoint.java
new file mode 100644
index 000000000..df47fdedd
--- /dev/null
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeoPoint.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2017-2020. Nitrite author or authors.
+ *
+ * Licensed 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.dizitart.no2.spatial;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.PrecisionModel;
+
+import java.io.Serializable;
+
+/**
+ * Represents a geographic point with latitude and longitude coordinates
+ * on Earth's surface (WGS84 ellipsoid).
+ *
+ * This class provides explicit type safety for geographic coordinates,
+ * eliminating the ambiguity of auto-detection. It validates coordinates
+ * on construction and provides clear latitude/longitude accessors.
+ *
+ * Usage Example:
+ * {@code
+ * // Create a geographic point for Minneapolis
+ * GeoPoint minneapolis = new GeoPoint(45.0, -93.2650);
+ *
+ * // Use with GeoNearFilter
+ * collection.find(where("location").geoNear(minneapolis, 5000.0));
+ * }
+ *
+ * Coordinate Order: Constructor takes (latitude, longitude)
+ * which differs from JTS Point (x, y) = (longitude, latitude) to avoid confusion.
+ *
+ * @since 4.3.3
+ * @author Anindya Chatterjee
+ */
+public class GeoPoint implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private static final GeometryFactory FACTORY = new GeometryFactory(new PrecisionModel(), 4326);
+ private final Point point;
+ private final double latitude;
+ private final double longitude;
+
+ /**
+ * Creates a new GeoPoint with the specified geographic coordinates.
+ *
+ * @param latitude the latitude in degrees (-90 to 90)
+ * @param longitude the longitude in degrees (-180 to 180)
+ * @throws IllegalArgumentException if coordinates are out of valid range
+ */
+ public GeoPoint(double latitude, double longitude) {
+ validateCoordinates(latitude, longitude);
+ this.latitude = latitude;
+ this.longitude = longitude;
+ this.point = FACTORY.createPoint(new Coordinate(longitude, latitude));
+ }
+
+ /**
+ * Creates a GeoPoint from a JTS Coordinate.
+ * The coordinate's Y value is treated as latitude and X as longitude.
+ *
+ * @param coordinate the coordinate (x=longitude, y=latitude)
+ * @throws IllegalArgumentException if coordinates are out of valid range
+ */
+ public GeoPoint(Coordinate coordinate) {
+ this(coordinate.getY(), coordinate.getX());
+ }
+
+ private void validateCoordinates(double latitude, double longitude) {
+ if (latitude < -90.0 || latitude > 90.0) {
+ throw new IllegalArgumentException(
+ "Latitude must be between -90 and 90 degrees, got: " + latitude);
+ }
+ if (longitude < -180.0 || longitude > 180.0) {
+ throw new IllegalArgumentException(
+ "Longitude must be between -180 and 180 degrees, got: " + longitude);
+ }
+ }
+
+ /**
+ * Gets the latitude in degrees.
+ *
+ * @return the latitude (-90 to 90)
+ */
+ public double getLatitude() {
+ return latitude;
+ }
+
+ /**
+ * Gets the longitude in degrees.
+ *
+ * @return the longitude (-180 to 180)
+ */
+ public double getLongitude() {
+ return longitude;
+ }
+
+ /**
+ * Gets the underlying JTS Point.
+ *
+ * @return the JTS Point representation
+ */
+ public Point getPoint() {
+ return point;
+ }
+
+ /**
+ * Gets the coordinate of this GeoPoint.
+ *
+ * @return the coordinate (x=longitude, y=latitude)
+ */
+ public Coordinate getCoordinate() {
+ return point.getCoordinate();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GeoPoint(lat=%.6f, lon=%.6f)", latitude, longitude);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+ GeoPoint other = (GeoPoint) obj;
+ return Double.compare(latitude, other.latitude) == 0
+ && Double.compare(longitude, other.longitude) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ long latBits = Double.doubleToLongBits(latitude);
+ long lonBits = Double.doubleToLongBits(longitude);
+ return (int) (latBits ^ (latBits >>> 32) ^ lonBits ^ (lonBits >>> 32));
+ }
+}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeodesicUtils.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeodesicUtils.java
new file mode 100644
index 000000000..b7ff259d9
--- /dev/null
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeodesicUtils.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2017-2020. Nitrite author or authors.
+ *
+ * Licensed 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.dizitart.no2.spatial;
+
+import net.sf.geographiclib.Geodesic;
+import net.sf.geographiclib.GeodesicData;
+import org.locationtech.jts.geom.Coordinate;
+
+/**
+ * Utility class for geodesic distance calculations on Earth's surface.
+ * This class handles the conversion between meters and degrees of latitude/longitude,
+ * accounting for the curvature of the Earth using the WGS84 ellipsoid model.
+ *
+ * This class is used internally by {@link NearFilter} for backward compatibility
+ * with auto-detection. For new code, use {@link GeoPoint} and {@link GeoNearFilter}
+ * for explicit geographic coordinate handling.
+ *
+ * @since 4.0
+ * @author Anindya Chatterjee
+ */
+class GeodesicUtils {
+ private static final Geodesic WGS84 = Geodesic.WGS84;
+
+ /**
+ * Determines if coordinates appear to be geographic (lat/long) rather than Cartesian.
+ * This is a heuristic check based on valid lat/long ranges:
+ * - Latitude: -90 to 90
+ * - Longitude: -180 to 180
+ *
+ * Limitation: This heuristic may incorrectly classify Cartesian
+ * coordinates that happen to fall within ±90°/±180° range (e.g., game world coordinates).
+ *
+ * Recommendation: For new code, use {@link GeoPoint} and
+ * {@link GeoNearFilter} to explicitly indicate geographic coordinates and avoid
+ * auto-detection ambiguity.
+ *
+ * @param center the coordinate to check
+ * @return true if the coordinate appears to be geographic, false otherwise
+ */
+ static boolean isGeographic(Coordinate center) {
+ double x = center.getX();
+ double y = center.getY();
+
+ // Check if coordinates fall within valid lat/long ranges
+ // We use slightly relaxed bounds to be conservative
+ return Math.abs(y) <= 90.0 && Math.abs(x) <= 180.0;
+ }
+
+ /**
+ * Calculates the approximate radius in degrees for a given distance in meters
+ * at a specific geographic coordinate. This accounts for the fact that one degree
+ * of longitude varies with latitude.
+ *
+ * This method calculates geodesic distances in both E-W and N-S directions and
+ * returns the maximum to ensure complete circular coverage. Combined with the
+ * two-pass query execution in {@link SpatialIndex}, this provides accurate results
+ * while maintaining performance.
+ *
+ * @param center the center coordinate (longitude, latitude)
+ * @param radiusMeters the radius in meters
+ * @return the approximate radius in degrees
+ */
+ static double metersToDegreesRadius(Coordinate center, double radiusMeters) {
+ double lat = center.getY();
+ double lon = center.getX();
+
+ // Calculate how many degrees we need to go in different directions
+ // to cover the specified radius in meters
+
+ // East-West: Calculate a point at the given distance east
+ GeodesicData eastPoint = WGS84.Direct(lat, lon, 90.0, radiusMeters);
+ double lonDiff = Math.abs(eastPoint.lon2 - lon);
+
+ // North-South: Calculate a point at the given distance north
+ GeodesicData northPoint = WGS84.Direct(lat, lon, 0.0, radiusMeters);
+ double latDiff = Math.abs(northPoint.lat2 - lat);
+
+ // Use the maximum of the two to ensure we cover the full circle
+ // This creates a slightly larger search area but ensures we don't miss points
+ return Math.max(lonDiff, latDiff);
+ }
+}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/NearFilter.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/NearFilter.java
index fe4d4764e..ef2c1a414 100644
--- a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/NearFilter.java
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/NearFilter.java
@@ -22,6 +22,23 @@
import org.locationtech.jts.util.GeometricShapeFactory;
/**
+ * A spatial filter that finds geometries near a point within a specified distance.
+ *
+ * This filter automatically detects whether the coordinates are geographic (latitude/longitude)
+ * or Cartesian, and applies the appropriate distance calculation:
+ *
+ * - Geographic coordinates (within ±90° lat, ±180° lon): Uses geodesic distance
+ * on Earth's WGS84 ellipsoid, with distance specified in meters.
+ * - Cartesian coordinates (outside those bounds): Uses simple Euclidean distance,
+ * with distance in the same units as the coordinates.
+ *
+ *
+ * Recommendation: For new code, use {@link GeoPoint} and {@link GeoNearFilter}
+ * for explicit geographic coordinate handling with better type safety and no auto-detection ambiguity.
+ *
+ * Note: Combined with two-pass query execution in {@link SpatialIndex},
+ * this filter provides accurate results by eliminating false positives from bounding box approximation.
+ *
* @since 4.0
* @author Anindya Chatterjee
*/
@@ -34,11 +51,24 @@ class NearFilter extends WithinFilter {
super(field, createCircle(point.getCoordinate(), distance));
}
- private static Geometry createCircle(Coordinate center, double radius) {
+ private static Geometry createCircle(Coordinate center, double radiusMeters) {
GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
shapeFactory.setNumPoints(64);
shapeFactory.setCentre(center);
- shapeFactory.setSize(radius * 2);
+
+ // Determine if we're dealing with geographic coordinates (lat/long)
+ // or simple Cartesian coordinates
+ double radiusInDegrees;
+ if (GeodesicUtils.isGeographic(center)) {
+ // Convert meters to degrees accounting for Earth's curvature
+ radiusInDegrees = GeodesicUtils.metersToDegreesRadius(center, radiusMeters);
+ } else {
+ // For non-geographic coordinates, use the radius as-is
+ // This maintains backward compatibility with existing tests
+ radiusInDegrees = radiusMeters;
+ }
+
+ shapeFactory.setSize(radiusInDegrees * 2);
return shapeFactory.createCircle();
}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialFluentFilter.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialFluentFilter.java
index 9f3e78ba3..c160b9be3 100644
--- a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialFluentFilter.java
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialFluentFilter.java
@@ -91,4 +91,40 @@ public Filter near(Coordinate point, Double distance) {
public Filter near(Point point, Double distance) {
return new NearFilter(field, point, distance);
}
+
+ /**
+ * Creates a spatial filter for geographic coordinates that matches documents
+ * where the spatial data is near the specified point, using geodesic distance.
+ *
+ * This method is specifically for geographic coordinates (latitude/longitude)
+ * and always uses geodesic distance calculations on Earth's WGS84 ellipsoid.
+ *
+ * Usage Example:
+ * {@code
+ * GeoPoint minneapolis = new GeoPoint(45.0, -93.2650);
+ * collection.find(where("location").geoNear(minneapolis, 5000.0)); // 5km radius
+ * }
+ *
+ * @param point the geographic point to check proximity to
+ * @param distanceMeters the maximum distance in meters
+ * @return the new {@link Filter} instance
+ */
+ public Filter geoNear(GeoPoint point, Double distanceMeters) {
+ return new GeoNearFilter(field, point, distanceMeters);
+ }
+
+ /**
+ * Creates a spatial filter for geographic coordinates that matches documents
+ * where the spatial data is near the specified coordinate, using geodesic distance.
+ *
+ * The coordinate is validated to ensure it represents valid geographic coordinates.
+ *
+ * @param point the coordinate to check proximity to (x=longitude, y=latitude)
+ * @param distanceMeters the maximum distance in meters
+ * @return the new {@link Filter} instance
+ * @throws IllegalArgumentException if coordinates are not valid geographic coordinates
+ */
+ public Filter geoNear(Coordinate point, Double distanceMeters) {
+ return new GeoNearFilter(field, point, distanceMeters);
+ }
}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialIndex.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialIndex.java
index 33f5e0fd1..2d612d3a4 100644
--- a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialIndex.java
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialIndex.java
@@ -32,6 +32,7 @@
import org.dizitart.no2.index.BoundingBox;
import org.dizitart.no2.index.IndexDescriptor;
import org.dizitart.no2.index.NitriteIndex;
+import org.dizitart.no2.store.NitriteMap;
import org.dizitart.no2.store.NitriteRTree;
import org.dizitart.no2.store.NitriteStore;
import org.locationtech.jts.geom.Envelope;
@@ -131,29 +132,117 @@ public LinkedHashSet findNitriteIds(FindPlan findPlan) {
throw new FilterException("Spatial filter must be the first filter for index scan");
}
- RecordStream keys;
+ // Phase 1: R-tree bounding box search (fast but may include false positives)
+ RecordStream candidateKeys;
NitriteRTree indexMap = findIndexMap();
SpatialFilter spatialFilter = (SpatialFilter) filter;
- Geometry geometry = spatialFilter.getValue();
- BoundingBox boundingBox = fromGeometry(geometry);
+ Geometry searchGeometry = spatialFilter.getValue();
+ BoundingBox boundingBox = fromGeometry(searchGeometry);
- if (filter instanceof WithinFilter) {
- keys = indexMap.findContainedKeys(boundingBox);
+ if (filter instanceof WithinFilter || filter instanceof GeoNearFilter) {
+ // For Within/Near filters, we want points that intersect the search geometry
+ // Note: We use intersecting here because we're searching for points WITHIN a circle
+ // The R-tree stores point bounding boxes, and we want those that overlap with
+ // the circle's bbox
+ candidateKeys = indexMap.findIntersectingKeys(boundingBox);
} else if (filter instanceof IntersectsFilter) {
- keys = indexMap.findIntersectingKeys(boundingBox);
+ candidateKeys = indexMap.findIntersectingKeys(boundingBox);
} else {
throw new FilterException("Unsupported spatial filter " + filter);
}
- LinkedHashSet nitriteIds = new LinkedHashSet<>();
- if (keys != null) {
- for (NitriteId nitriteId : keys) {
- nitriteIds.add(nitriteId);
+ LinkedHashSet results = new LinkedHashSet<>();
+ if (candidateKeys == null) {
+ return results;
+ }
+
+ // Phase 2: Geometry refinement (precise filtering using actual JTS operations)
+ // This eliminates false positives from the bounding box approximation
+ for (NitriteId nitriteId : candidateKeys) {
+ if (matchesGeometryFilter(nitriteId, spatialFilter, searchGeometry)) {
+ results.add(nitriteId);
}
}
- return nitriteIds;
+ return results;
+ }
+
+ /**
+ * Performs precise geometry matching using JTS operations.
+ * This is the second pass that eliminates false positives from the R-tree bbox search.
+ *
+ * @param nitriteId the document ID to check
+ * @param filter the spatial filter being applied
+ * @param searchGeometry the geometry to search with
+ * @return true if the stored geometry matches the filter criteria
+ */
+ private boolean matchesGeometryFilter(NitriteId nitriteId, SpatialFilter filter, Geometry searchGeometry) {
+ try {
+ // Retrieve the stored geometry for this document
+ Geometry storedGeometry = getStoredGeometry(nitriteId);
+
+ if (storedGeometry == null) {
+ // If geometry is null, it matches only if the search is for null/empty
+ return searchGeometry == null;
+ }
+
+ // Apply the appropriate JTS geometric operation based on filter type
+ if (filter instanceof WithinFilter || filter instanceof GeoNearFilter) {
+ // For Within and Near filters: the search geometry should contain the stored geometry
+ // OR the stored geometry should be within the search geometry
+ // For point-in-circle queries, we want to check if the point is within the circle
+ boolean result = searchGeometry.contains(storedGeometry) || searchGeometry.covers(storedGeometry);
+ return result;
+ } else if (filter instanceof IntersectsFilter) {
+ // For Intersects filter: geometries must intersect
+ return searchGeometry.intersects(storedGeometry);
+ }
+
+ return false;
+ } catch (Exception e) {
+ // If there's an error (e.g., invalid geometry), exclude this result
+ return false;
+ }
+ }
+
+ /**
+ * Retrieves the stored geometry from the collection for a given document ID.
+ *
+ * @param nitriteId the document ID
+ * @return the stored geometry, or null if not found
+ */
+ private Geometry getStoredGeometry(NitriteId nitriteId) {
+ try {
+ // Get the collection map name from the index descriptor
+ String collectionName = indexDescriptor.getCollectionName();
+
+ // Open the collection's document map
+ NitriteMap documentMap =
+ nitriteStore.openMap(collectionName, NitriteId.class, Document.class);
+
+ // Retrieve the document
+ Document document = documentMap.get(nitriteId);
+ if (document == null) {
+ return null;
+ }
+
+ // Get the field name from the index descriptor
+ Fields fields = indexDescriptor.getFields();
+ List fieldNames = fields.getFieldNames();
+ if (fieldNames.isEmpty()) {
+ return null;
+ }
+
+ String fieldName = fieldNames.get(0);
+ Object fieldValue = document.get(fieldName);
+
+ // Parse the geometry from the field value
+ return parseGeometry(fieldName, fieldValue);
+ } catch (Exception e) {
+ // If there's an error retrieving the geometry, return null
+ return null;
+ }
}
private NitriteRTree findIndexMap() {
@@ -165,14 +254,25 @@ private Geometry parseGeometry(String field, Object fieldValue) {
if (fieldValue == null) return null;
if (fieldValue instanceof String) {
return GeometryUtils.fromString((String) fieldValue);
+ } else if (fieldValue instanceof GeoPoint) {
+ // Handle GeoPoint - get its underlying Point geometry
+ return ((GeoPoint) fieldValue).getPoint();
} else if (fieldValue instanceof Geometry) {
return (Geometry) fieldValue;
} else if (fieldValue instanceof Document) {
- // in case of document, check if it contains geometry field
+ // in case of document, check if it contains geometry field or lat/lon fields
// GeometryConverter convert a geometry to document with geometry field
+ // GeoPointConverter converts to document with latitude/longitude fields
Document document = (Document) fieldValue;
if (document.containsField("geometry")) {
return GeometryUtils.fromString(document.get("geometry", String.class));
+ } else if (document.containsField("latitude") && document.containsField("longitude")) {
+ // Reconstruct GeoPoint from lat/lon
+ Double lat = document.get("latitude", Double.class);
+ Double lon = document.get("longitude", Double.class);
+ if (lat != null && lon != null) {
+ return new GeoPoint(lat, lon).getPoint();
+ }
}
}
throw new IndexingException("Field " + field + " does not contain Geometry data");
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialModule.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialModule.java
index c08448b32..1b99c1a7f 100644
--- a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialModule.java
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialModule.java
@@ -18,6 +18,7 @@
import org.dizitart.no2.common.module.NitriteModule;
import org.dizitart.no2.common.module.NitritePlugin;
+import org.dizitart.no2.spatial.converter.GeoPointConverter;
import org.dizitart.no2.spatial.converter.GeometryConverter;
import java.util.Set;
@@ -41,6 +42,6 @@ public class SpatialModule implements NitriteModule {
*/
@Override
public Set plugins() {
- return setOf(new SpatialIndexer(), new GeometryConverter());
+ return setOf(new SpatialIndexer(), new GeometryConverter(), new GeoPointConverter());
}
}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/converter/GeoPointConverter.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/converter/GeoPointConverter.java
new file mode 100644
index 000000000..460f12eb1
--- /dev/null
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/converter/GeoPointConverter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2017-2020. Nitrite author or authors.
+ *
+ * Licensed 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.dizitart.no2.spatial.converter;
+
+import org.dizitart.no2.collection.Document;
+import org.dizitart.no2.common.mapper.EntityConverter;
+import org.dizitart.no2.common.mapper.NitriteMapper;
+import org.dizitart.no2.spatial.GeoPoint;
+
+/**
+ * Converter for {@link GeoPoint} to/from Nitrite {@link Document}.
+ *
+ * Stores GeoPoint as a document with latitude and longitude fields.
+ *
+ * @since 4.3.3
+ * @author Anindya Chatterjee
+ */
+public class GeoPointConverter implements EntityConverter {
+
+ @Override
+ public Class getEntityType() {
+ return GeoPoint.class;
+ }
+
+ @Override
+ public Document toDocument(GeoPoint entity, NitriteMapper nitriteMapper) {
+ return Document.createDocument("latitude", entity.getLatitude())
+ .put("longitude", entity.getLongitude());
+ }
+
+ @Override
+ public GeoPoint fromDocument(Document document, NitriteMapper nitriteMapper) {
+ Double latitude = document.get("latitude", Double.class);
+ Double longitude = document.get("longitude", Double.class);
+
+ if (latitude == null || longitude == null) {
+ return null;
+ }
+
+ return new GeoPoint(latitude, longitude);
+ }
+}
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoPointTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoPointTest.java
new file mode 100644
index 000000000..c0fb8d650
--- /dev/null
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoPointTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2017-2020. Nitrite author or authors.
+ *
+ * Licensed 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.dizitart.no2.spatial;
+
+import org.dizitart.no2.collection.Document;
+import org.dizitart.no2.collection.DocumentCursor;
+import org.dizitart.no2.collection.NitriteCollection;
+import org.dizitart.no2.filters.FluentFilter;
+import org.dizitart.no2.index.IndexOptions;
+import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+
+import static org.dizitart.no2.collection.Document.createDocument;
+import static org.dizitart.no2.spatial.SpatialFluentFilter.where;
+import static org.dizitart.no2.spatial.SpatialIndexer.SPATIAL_INDEX;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Test cases for GeoPoint and GeoNearFilter functionality.
+ *
+ * @author Anindya Chatterjee
+ */
+public class GeoPointTest extends BaseSpatialTest {
+
+ @Test
+ public void testGeoPointCreation() {
+ GeoPoint point = new GeoPoint(45.0, -93.2650);
+ assertEquals(45.0, point.getLatitude(), 0.0001);
+ assertEquals(-93.2650, point.getLongitude(), 0.0001);
+ }
+
+ @Test
+ public void testGeoPointFromCoordinate() {
+ Coordinate coord = new Coordinate(-93.2650, 45.0); // x=lon, y=lat
+ GeoPoint point = new GeoPoint(coord);
+ assertEquals(45.0, point.getLatitude(), 0.0001);
+ assertEquals(-93.2650, point.getLongitude(), 0.0001);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGeoPointInvalidLatitudeTooHigh() {
+ new GeoPoint(91.0, 0.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGeoPointInvalidLatitudeTooLow() {
+ new GeoPoint(-91.0, 0.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGeoPointInvalidLongitudeTooHigh() {
+ new GeoPoint(0.0, 181.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGeoPointInvalidLongitudeTooLow() {
+ new GeoPoint(0.0, -181.0);
+ }
+
+ @Test
+ public void testGeoPointBoundaryValues() {
+ // Test boundary values
+ GeoPoint northPole = new GeoPoint(90.0, 0.0);
+ assertEquals(90.0, northPole.getLatitude(), 0.0001);
+
+ GeoPoint southPole = new GeoPoint(-90.0, 0.0);
+ assertEquals(-90.0, southPole.getLatitude(), 0.0001);
+
+ GeoPoint dateLine = new GeoPoint(0.0, 180.0);
+ assertEquals(180.0, dateLine.getLongitude(), 0.0001);
+
+ GeoPoint antiMeridian = new GeoPoint(0.0, -180.0);
+ assertEquals(-180.0, antiMeridian.getLongitude(), 0.0001);
+ }
+
+ @Test
+ public void testGeoPointSerialization() {
+ NitriteCollection testCollection = db.getCollection("geo_point_test");
+
+ GeoPoint minneapolis = new GeoPoint(45.0, -93.2650);
+ Document doc = createDocument("name", "Minneapolis")
+ .put("location", minneapolis);
+
+ testCollection.insert(doc);
+
+ Document retrieved = testCollection.find().firstOrNull();
+ assertNotNull(retrieved);
+
+ GeoPoint retrievedPoint = retrieved.get("location", GeoPoint.class);
+ assertNotNull(retrievedPoint);
+ assertEquals(45.0, retrievedPoint.getLatitude(), 0.0001);
+ assertEquals(-93.2650, retrievedPoint.getLongitude(), 0.0001);
+
+ testCollection.remove(FluentFilter.where("name").eq("Minneapolis"));
+ }
+
+ @Test
+ public void testGeoNearFilterWithGeoPoint() {
+ NitriteCollection testCollection = db.getCollection("geo_near_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ GeoPoint center = new GeoPoint(0.001, 0.001);
+ GeoPoint point1km = new GeoPoint(0.001, 0.011); // ~1.1km east
+ GeoPoint point5km = new GeoPoint(0.001, 0.051); // ~5.7km east
+ GeoPoint point100km = new GeoPoint(0.001, 1.001); // ~111km east
+
+ Document docCenter = createDocument("name", "center").put("location", center);
+ Document doc1km = createDocument("name", "1km").put("location", point1km);
+ Document doc5km = createDocument("name", "5km").put("location", point5km);
+ Document doc100km = createDocument("name", "100km").put("location", point100km);
+
+ testCollection.insert(docCenter, doc1km, doc5km, doc100km);
+
+ // Test: Within 2km should find center and 1km
+ DocumentCursor within2km = testCollection.find(where("location").geoNear(center, 2000.0));
+ assertEquals("Should find 2 points within 2km", 2, within2km.size());
+
+ // Test: Within 10km should find center, 1km, and 5km
+ DocumentCursor within10km = testCollection.find(where("location").geoNear(center, 10000.0));
+ assertEquals("Should find 3 points within 10km", 3, within10km.size());
+
+ // Test: Within 200km should find all points
+ DocumentCursor within200km = testCollection.find(where("location").geoNear(center, 200000.0));
+ assertEquals("Should find all 4 points within 200km", 4, within200km.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("1km"));
+ testCollection.remove(FluentFilter.where("name").eq("5km"));
+ testCollection.remove(FluentFilter.where("name").eq("100km"));
+ }
+
+ @Test
+ public void testGeoNearFilterWithCoordinate() {
+ NitriteCollection testCollection = db.getCollection("geo_near_coord_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ GeoPoint center = new GeoPoint(0.001, 0.001);
+ GeoPoint nearby = new GeoPoint(0.010, 0.001); // ~1km north
+
+ testCollection.insert(
+ createDocument("name", "center").put("location", center),
+ createDocument("name", "nearby").put("location", nearby)
+ );
+
+ // Test with Coordinate instead of GeoPoint
+ Coordinate centerCoord = new Coordinate(0.001, 0.001); // lon, lat
+ DocumentCursor results = testCollection.find(where("location").geoNear(centerCoord, 2000.0));
+
+ assertEquals("Should find 2 points within 2km", 2, results.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("nearby"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGeoNearFilterInvalidCoordinate() {
+ NitriteCollection testCollection = db.getCollection("invalid_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ // Invalid latitude > 90
+ Coordinate invalid = new Coordinate(0.0, 100.0);
+ testCollection.find(where("location").geoNear(invalid, 1000.0));
+ }
+
+ @Test
+ public void testGeoNearFilterMidLatitude() {
+ NitriteCollection testCollection = db.getCollection("geo_near_midlat_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ // Test at 45°N where longitude degrees are shorter
+ GeoPoint center = new GeoPoint(45.0, -93.2650);
+ GeoPoint nearby = new GeoPoint(45.01, -93.2650); // ~1.1km north
+ GeoPoint faraway = new GeoPoint(45.0, -92.2650); // ~80km east
+
+ testCollection.insert(
+ createDocument("name", "center").put("location", center),
+ createDocument("name", "nearby").put("location", nearby),
+ createDocument("name", "faraway").put("location", faraway)
+ );
+
+ DocumentCursor within2km = testCollection.find(where("location").geoNear(center, 2000.0));
+ assertEquals("Should find 2 points within 2km at 45°N", 2, within2km.size());
+
+ DocumentCursor within100km = testCollection.find(where("location").geoNear(center, 100000.0));
+ assertEquals("Should find all 3 points within 100km", 3, within100km.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("nearby"));
+ testCollection.remove(FluentFilter.where("name").eq("faraway"));
+ }
+
+ @Test
+ public void testGeoPointToString() {
+ GeoPoint point = new GeoPoint(45.123456, -93.654321);
+ String str = point.toString();
+ assertNotNull(str);
+ // Should contain formatted lat/lon
+ assert(str.contains("45.123456"));
+ assert(str.contains("-93.654321"));
+ }
+}
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeodesicNearFilterTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeodesicNearFilterTest.java
new file mode 100644
index 000000000..fa3a64ba6
--- /dev/null
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeodesicNearFilterTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2017-2020. Nitrite author or authors.
+ *
+ * Licensed 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.dizitart.no2.spatial;
+
+import org.dizitart.no2.collection.Document;
+import org.dizitart.no2.collection.DocumentCursor;
+import org.dizitart.no2.collection.NitriteCollection;
+import org.dizitart.no2.filters.FluentFilter;
+import org.dizitart.no2.index.IndexOptions;
+import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.io.WKTReader;
+
+import static org.dizitart.no2.collection.Document.createDocument;
+import static org.dizitart.no2.spatial.SpatialFluentFilter.where;
+import static org.dizitart.no2.spatial.SpatialIndexer.SPATIAL_INDEX;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test cases for NearFilter with real-world geodesic coordinates.
+ * These tests verify that the NearFilter properly handles lat/long coordinates
+ * on Earth's surface and correctly converts meters to degrees.
+ *
+ * @author Anindya Chatterjee
+ */
+public class GeodesicNearFilterTest extends BaseSpatialTest {
+
+ @Test
+ public void testNearFilterAtEquator() throws ParseException {
+ WKTReader reader = new WKTReader();
+
+ // Use slightly offset center to avoid potential R-tree edge case at exactly (0,0)
+ // Center point near equator in Atlantic Ocean
+ Point centerPoint = (Point) reader.read("POINT (0.001 0.001)");
+
+ // Point approximately 1km east: at equator, 1 degree ≈ 111km
+ // So 0.01 degrees ≈ 1.11km from (0,0), and similar from (0.001, 0.001)
+ Point point1kmEast = (Point) reader.read("POINT (0.011 0.001)");
+
+ // Point approximately 111km east (1 degree at equator)
+ Point point111kmEast = (Point) reader.read("POINT (1.001 0.001)");
+
+ // Point approximately 222km east (2 degrees at equator)
+ Point point222kmEast = (Point) reader.read("POINT (2.001 0.001)");
+
+ NitriteCollection testCollection = db.getCollection("geodesic_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ Document docCenter = createDocument("name", "center")
+ .put("location", centerPoint);
+ Document doc1km = createDocument("name", "1km_east")
+ .put("location", point1kmEast);
+ Document doc111km = createDocument("name", "111km_east")
+ .put("location", point111kmEast);
+ Document doc222km = createDocument("name", "222km_east")
+ .put("location", point222kmEast);
+
+ testCollection.insert(docCenter, doc1km, doc111km, doc222km);
+
+ // Test 1: Within 2km should return center and 1km_east only
+ DocumentCursor within2km = testCollection.find(where("location").near(centerPoint, 2000.0));
+ assertEquals("Should find 2 points within 2km", 2, within2km.size());
+
+ // Test 2: Within 20cm should return only center
+ DocumentCursor within20cm = testCollection.find(where("location").near(centerPoint, 0.2));
+ assertEquals("Should find only center within 20cm", 1, within20cm.size());
+
+ // Test 3: Within 150km should return center, 1km, and 111km
+ DocumentCursor within150km = testCollection.find(where("location").near(centerPoint, 150000.0));
+ assertEquals("Should find 3 points within 150km", 3, within150km.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("1km_east"));
+ testCollection.remove(FluentFilter.where("name").eq("111km_east"));
+ testCollection.remove(FluentFilter.where("name").eq("222km_east"));
+ }
+
+ @Test
+ public void testNearFilterWithCoordinate() throws ParseException {
+ WKTReader reader = new WKTReader();
+
+ Point centerPoint = (Point) reader.read("POINT (0.001 0.001)");
+ Coordinate centerCoord = centerPoint.getCoordinate();
+
+ Point point500m = (Point) reader.read("POINT (0.006 0.001)");
+ Point point5km = (Point) reader.read("POINT (0.051 0.001)");
+
+ NitriteCollection testCollection = db.getCollection("geodesic_coord_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ Document docCenter = createDocument("name", "center")
+ .put("location", centerPoint);
+ Document doc500m = createDocument("name", "500m_east")
+ .put("location", point500m);
+ Document doc5km = createDocument("name", "5km_east")
+ .put("location", point5km);
+
+ testCollection.insert(docCenter, doc500m, doc5km);
+
+ // Test with Coordinate instead of Point
+ DocumentCursor within1km = testCollection.find(where("location").near(centerCoord, 1000.0));
+ assertEquals("Should find 2 points within 1km", 2, within1km.size());
+
+ DocumentCursor within10km = testCollection.find(where("location").near(centerCoord, 10000.0));
+ assertEquals("Should find all 3 points within 10km", 3, within10km.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("500m_east"));
+ testCollection.remove(FluentFilter.where("name").eq("5km_east"));
+ }
+
+ @Test
+ public void testNearFilterAtMidLatitude() throws ParseException {
+ WKTReader reader = new WKTReader();
+
+ // Center point at 45°N (e.g., near Minneapolis, MN)
+ // At 45°N, longitude degrees are shorter: ~78.8km per degree
+ Point centerPoint = (Point) reader.read("POINT (-93.2650 45.0000)");
+
+ // Point approximately 1km east at 45°N
+ Point point1kmEast = (Point) reader.read("POINT (-93.2523 45.0000)");
+
+ // Point approximately 80km east
+ Point point80kmEast = (Point) reader.read("POINT (-92.2650 45.0000)");
+
+ NitriteCollection testCollection = db.getCollection("geodesic_midlat_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ Document docCenter = createDocument("name", "center")
+ .put("location", centerPoint);
+ Document doc1km = createDocument("name", "1km_east")
+ .put("location", point1kmEast);
+ Document doc80km = createDocument("name", "80km_east")
+ .put("location", point80kmEast);
+
+ testCollection.insert(docCenter, doc1km, doc80km);
+
+ // Within 2km should find center and 1km_east
+ DocumentCursor within2km = testCollection.find(where("location").near(centerPoint, 2000.0));
+ assertEquals("Should find 2 points within 2km at 45°N", 2, within2km.size());
+
+ // Within 100km should find all points
+ DocumentCursor within100km = testCollection.find(where("location").near(centerPoint, 100000.0));
+ assertEquals("Should find all 3 points within 100km", 3, within100km.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("1km_east"));
+ testCollection.remove(FluentFilter.where("name").eq("80km_east"));
+ }
+
+ @Test
+ public void testNearFilterNorthSouth() throws ParseException {
+ WKTReader reader = new WKTReader();
+
+ // Test north-south distances (latitude changes)
+ // These are consistent across all longitudes: ~111km per degree
+ Point centerPoint = (Point) reader.read("POINT (0.001 0.001)");
+
+ // Point approximately 1km north
+ Point point1kmNorth = (Point) reader.read("POINT (0.001 0.010)");
+
+ // Point approximately 111km north (1 degree)
+ Point point111kmNorth = (Point) reader.read("POINT (0.001 1.001)");
+
+ NitriteCollection testCollection = db.getCollection("geodesic_ns_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ Document docCenter = createDocument("name", "center")
+ .put("location", centerPoint);
+ Document doc1km = createDocument("name", "1km_north")
+ .put("location", point1kmNorth);
+ Document doc111km = createDocument("name", "111km_north")
+ .put("location", point111kmNorth);
+
+ testCollection.insert(docCenter, doc1km, doc111km);
+
+ // Within 2km should find center and 1km_north
+ DocumentCursor within2km = testCollection.find(where("location").near(centerPoint, 2000.0));
+ assertEquals("Should find 2 points within 2km", 2, within2km.size());
+
+ // Within 200km should find all points
+ DocumentCursor within200km = testCollection.find(where("location").near(centerPoint, 200000.0));
+ assertEquals("Should find all 3 points within 200km", 3, within200km.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("1km_north"));
+ testCollection.remove(FluentFilter.where("name").eq("111km_north"));
+ }
+
+ @Test
+ public void testNearFilterSmallDistances() throws ParseException {
+ WKTReader reader = new WKTReader();
+
+ Point centerPoint = (Point) reader.read("POINT (0.001 0.001)");
+
+ // Very small distances
+ Point point10m = (Point) reader.read("POINT (0.00109 0.001)"); // ~10m
+ Point point100m = (Point) reader.read("POINT (0.0019 0.001)"); // ~100m
+
+ NitriteCollection testCollection = db.getCollection("geodesic_small_test");
+ testCollection.createIndex(IndexOptions.indexOptions(SPATIAL_INDEX), "location");
+
+ Document docCenter = createDocument("name", "center")
+ .put("location", centerPoint);
+ Document doc10m = createDocument("name", "10m_east")
+ .put("location", point10m);
+ Document doc100m = createDocument("name", "100m_east")
+ .put("location", point100m);
+
+ testCollection.insert(docCenter, doc10m, doc100m);
+
+ // Within 50m should find center and 10m only
+ DocumentCursor within50m = testCollection.find(where("location").near(centerPoint, 50.0));
+ assertEquals("Should find 2 points within 50m", 2, within50m.size());
+
+ // Within 5m should find only center
+ DocumentCursor within5m = testCollection.find(where("location").near(centerPoint, 5.0));
+ assertEquals("Should find only center within 5m", 1, within5m.size());
+
+ testCollection.remove(FluentFilter.where("name").eq("center"));
+ testCollection.remove(FluentFilter.where("name").eq("10m_east"));
+ testCollection.remove(FluentFilter.where("name").eq("100m_east"));
+ }
+}