repository;
+
+ static WKTReader reader = new WKTReader();
+ protected SpatialData obj2_950m_ESE, obj3_930m_W, obj4_2750m_WSW;
+ protected Document doc2_950m_ESE, doc3_930m_W, doc4_2750m_WSW;
+
+ @Rule
+ public Retry retry = new Retry(3);
+
+ /**
+ * I chose a set of simple landmarks in a major city at high latitude, near 60°N,
+ * such that the separation between them is primarily east-west.
+ *
+ * At the equator, 1 degree of either latitude or longitude measures approx. 111km wide.
+ * However, at 60°N, 1 degree of longitude is only half as wide. (cf. cos(60°) == 0.5)
+ *
+ * This math is not exact enough for the needs of a geographer, but it's close enough to create
+ * simple test cases that can distinguish whether we are properly converting meters to/from degrees,
+ * including accounting for the curvature of the Earth.
+ */
+ public static class TestLocations {
+ static Point centerPt = readPoint("POINT(59.91437 10.73402)"); // National Theater (Oslo)
+ static Point pt2_950m_ESE = readPoint("POINT(59.9115306 10.7501574)"); // Olso Central Station
+ static Point pt3_930m_W = readPoint("POINT(59.91433 10.71730)"); // National Library of Norway
+ static Point pt4_2750m_WSW = readPoint("POINT(59.90749 10.68670)"); // Norwegian Museum of Cultural Hist.
+ }
+
+ @SneakyThrows
+ protected static Point readPoint(String wkt) {
+ return (Point) reader.read(wkt);
+ }
+
+ @Before
+ public void before() throws ParseException {
+ fileName = getRandomTempDbFile();
+ db = createDb(fileName);
+
+ collection = db.getCollection("test");
+ repository = db.getRepository(SpatialData.class);
+
+ insertObjects();
+ insertDocuments();
+ }
+
+ protected void insertObjects() throws ParseException {
+ obj2_950m_ESE = new SpatialData(2L, TestLocations.pt2_950m_ESE);
+ obj3_930m_W = new SpatialData(3L, TestLocations.pt3_930m_W);
+ obj4_2750m_WSW = new SpatialData(4L, TestLocations.pt4_2750m_WSW);
+ repository.insert(obj2_950m_ESE, obj3_930m_W, obj4_2750m_WSW);
+ }
+
+ protected void insertDocuments() throws ParseException {
+ doc2_950m_ESE = createDocument("key", 2L).put("location", TestLocations.pt2_950m_ESE);
+ doc3_930m_W = createDocument("key", 3L).put("location", TestLocations.pt3_930m_W);
+ doc4_2750m_WSW = createDocument("key", 4L).put("location", TestLocations.pt4_2750m_WSW);
+
+ collection.insert(doc2_950m_ESE, doc3_930m_W, doc4_2750m_WSW);
+ }
+
+ @After
+ public void after() {
+ if (db != null && !db.isClosed()) {
+ db.close();
+ }
+
+ deleteDb(fileName);
+ }
+
+ protected Document trimMeta(Document document) {
+ document.remove(DOC_ID);
+ document.remove(DOC_REVISION);
+ document.remove(DOC_MODIFIED);
+ document.remove(DOC_SOURCE);
+ return document;
+ }
+}
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java
index c22870786..bc34d672b 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java
@@ -16,7 +16,9 @@
package org.dizitart.no2.spatial;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
import org.dizitart.no2.collection.Document;
import org.dizitart.no2.common.mapper.EntityConverter;
import org.dizitart.no2.common.mapper.NitriteMapper;
@@ -30,6 +32,8 @@
* @author Anindya Chatterjee
*/
@Data
+@AllArgsConstructor
+@NoArgsConstructor
@Index(fields = "geometry", type = SPATIAL_INDEX)
public class SpatialData {
@Id
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
index 8905fe197..bc20ee3da 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
@@ -79,6 +79,31 @@ public void testWithin() throws ParseException {
assertEquals(cursor1.toList().stream().map(this::trimMeta).collect(Collectors.toList()), Collections.singletonList(doc1));
}
+ @Test
+ public void testWithinTriangleNotJustTestingBoundingBox() throws ParseException {
+
+ /*
+ (490, 530) * ┄┄┄┄┄.
+ │\ ┆
+ │ \ x<---(520, 520)
+ │ \ ┆
+ │ \ ┆
+ │ x<-\---(500, 505)
+ │ \┆
+ (490, 490) *──────* (530, 490)
+ */
+
+ WKTReader reader = new WKTReader();
+ Geometry search = reader.read("POLYGON((490 490, 530 490 , 490 530, 490 490))");
+
+ SpatialData outsidePoint = new SpatialData(7L, reader.read("POINT(520 520)"));
+ repository.insert(outsidePoint);
+
+ Cursor cursor = repository.find(where("geometry").within(search));
+ assertEquals(cursor.size(), 1);
+ assertFalse(cursor.toList().contains(outsidePoint));
+ }
+
@Test
public void testNearPoint() throws ParseException {
WKTReader reader = new WKTReader();
From 095b32f4cab6d8721b8293404d942f037dba689a Mon Sep 17 00:00:00 2001
From: Leo Gertsenshteyn <146586+leoger@users.noreply.github.com>
Date: Fri, 13 Dec 2024 22:51:37 -0800
Subject: [PATCH 2/5] minor cleanup
---
.../jts/util/GeometricShapeFactoryExt.java | 16 ++++++++++++++++
.../no2/spatial/GeoSpatialFindNearTest.java | 2 +-
.../dizitart/no2/spatial/SpatialIndexTest.java | 15 +++++++--------
3 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java b/nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java
index 15fb1ecec..d418f083c 100644
--- a/nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java
+++ b/nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2017-2024 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.locationtech.jts.util;
import net.sf.geographiclib.Geodesic;
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java
index 12bcd0097..f6ffbaf63 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017-2021 Nitrite author or authors.
+ * Copyright (c) 2017-2024 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.
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
index bc20ee3da..c2d42b655 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
@@ -83,14 +83,13 @@ public void testWithin() throws ParseException {
public void testWithinTriangleNotJustTestingBoundingBox() throws ParseException {
/*
- (490, 530) * ┄┄┄┄┄.
- │\ ┆
- │ \ x<---(520, 520)
- │ \ ┆
- │ \ ┆
- │ x<-\---(500, 505)
- │ \┆
- (490, 490) *──────* (530, 490)
+ (490, 530) * - - -
+ │\ -
+ │ \ x <── (520, 520); outside triangle but within triangle's bounding box
+ │ \ -
+ │ x <── (500, 505); inside triangle
+ │ \-
+ (490, 490) *─────* (530, 490)
*/
WKTReader reader = new WKTReader();
From fa2ed69f89ab740a24feda21465504e4bf2efacd Mon Sep 17 00:00:00 2001
From: Leo Gertsenshteyn <146586+leoger@users.noreply.github.com>
Date: Fri, 13 Dec 2024 23:01:09 -0800
Subject: [PATCH 3/5] move GeometricShapeFactoryExt to nitrite package
namespace
---
.../no2/spatial}/GeometricShapeFactoryExt.java | 3 ++-
.../src/main/java/org/dizitart/no2/spatial/NearFilter.java | 1 -
2 files changed, 2 insertions(+), 2 deletions(-)
rename nitrite-spatial/src/main/java/org/{locationtech/jts/util => dizitart/no2/spatial}/GeometricShapeFactoryExt.java (96%)
diff --git a/nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeometricShapeFactoryExt.java
similarity index 96%
rename from nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java
rename to nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeometricShapeFactoryExt.java
index d418f083c..90c5ce266 100644
--- a/nitrite-spatial/src/main/java/org/locationtech/jts/util/GeometricShapeFactoryExt.java
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/GeometricShapeFactoryExt.java
@@ -14,7 +14,7 @@
* limitations under the License.
*
*/
-package org.locationtech.jts.util;
+package org.dizitart.no2.spatial;
import net.sf.geographiclib.Geodesic;
import net.sf.geographiclib.GeodesicData;
@@ -23,6 +23,7 @@
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.util.GeometricShapeFactory;
/** Extends the JTS GeometricShapeFactory to add geodetic "small circle" geometry. */
public class GeometricShapeFactoryExt extends GeometricShapeFactory {
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 5e602b806..a424299fc 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
@@ -19,7 +19,6 @@
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
-import org.locationtech.jts.util.GeometricShapeFactoryExt;
/**
* @since 4.0
From b5f2486cdd2c4fd59c37a0ee0661d6795c998715 Mon Sep 17 00:00:00 2001
From: Leo Gertsenshteyn <146586+leoger@users.noreply.github.com>
Date: Mon, 20 Jan 2025 23:08:53 -0800
Subject: [PATCH 4/5] WIP with working NearFilter unit tests
---
.../org/dizitart/no2/spatial/NearFilter.java | 84 ++++++++-
.../no2/spatial/SpatialFluentFilter.java | 1 +
.../dizitart/no2/spatial/WithinFilter.java | 9 +
.../spatial/CompoundFilterExampleTest.java | 159 ++++++++++++++++++
.../no2/spatial/GeoSpatialFindNearTest.java | 6 +-
.../org/dizitart/no2/spatial/SpatialData.java | 2 +-
.../no2/spatial/SpatialIndexTest.java | 48 +++++-
.../collection/operation/FindOptimizer.java | 16 +-
.../org/dizitart/no2/filters/AndFilter.java | 2 +-
.../no2/filters/FlattenableFilter.java | 29 ++++
10 files changed, 327 insertions(+), 29 deletions(-)
create mode 100644 nitrite-spatial/src/test/java/org/dizitart/no2/spatial/CompoundFilterExampleTest.java
create mode 100644 nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
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 a424299fc..a8a3c75cd 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
@@ -16,21 +16,45 @@
package org.dizitart.no2.spatial;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.Point;
+import net.sf.geographiclib.Geodesic;
+import net.sf.geographiclib.GeodesicData;
+import net.sf.geographiclib.GeodesicMask;
+import org.dizitart.no2.collection.Document;
+import org.dizitart.no2.collection.NitriteId;
+import org.dizitart.no2.common.tuples.Pair;
+import org.dizitart.no2.exceptions.FilterException;
+import org.dizitart.no2.filters.FlattenableFilter;
+import org.dizitart.no2.filters.FieldBasedFilter;
+import org.dizitart.no2.filters.Filter;
+import org.locationtech.jts.geom.*;
+
+import java.util.List;
+
+import static org.locationtech.jts.geom.PrecisionModel.FLOATING;
/**
* @since 4.0
* @author Anindya Chatterjee
*/
-class NearFilter extends WithinFilter {
- NearFilter(String field, Coordinate point, Double distance) {
- super(field, createCircle(point, distance));
+class NearFilter extends IntersectsFilter implements FlattenableFilter {
+ private Point center;
+ private Double distance;
+
+ /** Uses full "double" floating-point precision, and SRID 4326 */
+ private static GeometryFactory geometryFactory =
+ new GeometryFactory(new PrecisionModel(FLOATING), 4326);
+
+
+ NearFilter(String field, Coordinate center, Double distance) {
+ super(field, createCircle(center, distance));
+ this.center = geometryFactory.createPoint(center);
+ this.distance = distance;
}
- NearFilter(String field, Point point, Double distance) {
- super(field, createCircle(point.getCoordinate(), distance));
+ NearFilter(String field, Point center, Double distance) {
+ super(field, createCircle(center.getCoordinate(), distance));
+ this.center = center;
+ this.distance = distance;
}
private static Geometry createCircle(Coordinate center, double radius) {
@@ -45,4 +69,48 @@ private static Geometry createCircle(Coordinate center, double radius) {
public String toString() {
return "(" + getField() + " nears " + getValue() + ")";
}
+
+ @Override
+ public List getFilters() {
+ return List.of(
+ new IntersectsFilter(getField(), getValue()),
+ new NonIndexNearFilter(getField(), getValue()));
+ }
+
+ public class NonIndexNearFilter extends FieldBasedFilter {
+
+ protected NonIndexNearFilter(String field, Geometry circle) {
+ super(field, circle);
+ }
+
+ @Override
+ public boolean apply(Pair element) {
+ Document document = element.getSecond();
+ Object fieldValue = document.get(getField());
+
+ if (fieldValue == null) {
+ return false;
+ } else if (fieldValue instanceof Geometry) {
+ if (fieldValue instanceof Point) {
+ Point pointValue = (Point) fieldValue;
+ Point centerPoint = NearFilter.this.center;
+ GeodesicData inverseResult =
+ Geodesic.WGS84.Inverse(
+ centerPoint.getX(), centerPoint.getY(),
+ pointValue.getX(), pointValue.getY(),
+ GeodesicMask.DISTANCE);
+ return inverseResult.s12 <= NearFilter.this.distance;
+ } else {
+ // TODO this doesn't seem to work??
+ Geometry elemGeo = (Geometry) fieldValue;
+ Geometry filterGeo = (Geometry) getValue();
+ return filterGeo.intersects(elemGeo);
+ }
+ } else {
+ throw new FilterException(getField() + " does not contain Geometry value");
+ }
+ }
+
+ }
+
}
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..911b86bf9 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
@@ -89,6 +89,7 @@ public Filter near(Coordinate point, Double distance) {
* @return the new {@link Filter} instance
*/
public Filter near(Point point, Double distance) {
+// return new NearFilter(field, point, distance);
return new NearFilter(field, point, distance);
}
}
diff --git a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/WithinFilter.java b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/WithinFilter.java
index fe25ea741..13225d2c9 100644
--- a/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/WithinFilter.java
+++ b/nitrite-spatial/src/main/java/org/dizitart/no2/spatial/WithinFilter.java
@@ -16,10 +16,18 @@
package org.dizitart.no2.spatial;
+import org.dizitart.no2.collection.Document;
+import org.dizitart.no2.collection.NitriteId;
+import org.dizitart.no2.common.tuples.Pair;
+import org.dizitart.no2.exceptions.FilterException;
+import org.dizitart.no2.filters.FieldBasedFilter;
import org.dizitart.no2.index.IndexMap;
import org.locationtech.jts.geom.Geometry;
import java.util.List;
+import java.util.regex.Matcher;
+
+import static org.dizitart.no2.common.util.Numbers.compare;
/**
* @since 4.0
@@ -40,4 +48,5 @@ public List> applyOnIndex(IndexMap indexMap) {
public String toString() {
return "(" + getField() + " within " + getValue() + ")";
}
+
}
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/CompoundFilterExampleTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/CompoundFilterExampleTest.java
new file mode 100644
index 000000000..93515f199
--- /dev/null
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/CompoundFilterExampleTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2017-2021 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 lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.dizitart.no2.Nitrite;
+import org.dizitart.no2.collection.Document;
+import org.dizitart.no2.collection.NitriteCollection;
+import org.dizitart.no2.common.mapper.EntityConverter;
+import org.dizitart.no2.common.mapper.NitriteMapper;
+import org.dizitart.no2.common.mapper.SimpleNitriteMapper;
+import org.dizitart.no2.filters.Filter;
+import org.dizitart.no2.filters.FluentFilter;
+import org.dizitart.no2.repository.Cursor;
+import org.dizitart.no2.repository.ObjectRepository;
+import org.dizitart.no2.repository.annotations.Index;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.locationtech.jts.io.ParseException;
+
+import java.security.SecureRandom;
+
+import static org.dizitart.no2.collection.Document.createDocument;
+import static org.dizitart.no2.common.Constants.DOC_ID;
+import static org.dizitart.no2.common.Constants.DOC_MODIFIED;
+import static org.dizitart.no2.common.Constants.DOC_REVISION;
+import static org.dizitart.no2.common.Constants.DOC_SOURCE;
+import static org.dizitart.no2.index.IndexType.NON_UNIQUE;
+import static org.dizitart.no2.spatial.TestUtil.createDb;
+import static org.dizitart.no2.spatial.TestUtil.deleteDb;
+import static org.dizitart.no2.spatial.TestUtil.getRandomTempDbFile;
+
+public class CompoundFilterExampleTest {
+ private String fileName;
+ protected Nitrite db;
+ protected NitriteCollection collection;
+ protected ObjectRepository repository;
+
+ @Data
+ @AllArgsConstructor
+ @NoArgsConstructor
+ @Index(fields = "indexString", type = NON_UNIQUE)
+ public static class PartiallyIndexedData {
+ private String indexStr;
+ private String otherStr;
+
+ public static class PidConverter implements EntityConverter {
+
+ @Override
+ public Class getEntityType() {
+ return PartiallyIndexedData.class;
+ }
+
+ @Override
+ public Document toDocument(PartiallyIndexedData entity, NitriteMapper nitriteMapper) {
+ return Document
+ .createDocument("indexStr", entity.getIndexStr())
+ .put("otherStr", entity.getOtherStr());
+ }
+
+ @Override
+ public PartiallyIndexedData fromDocument(Document document, NitriteMapper nitriteMapper) {
+ return new PartiallyIndexedData(
+ document.get("indexStr", String.class),
+ document.get("otherStr", String.class)
+ );
+ }
+ }
+ }
+
+ @Rule
+ public Retry retry = new Retry(3);
+
+
+ @Before
+ public void before() throws ParseException {
+ fileName = getRandomTempDbFile();
+ db = createDb(fileName);
+ try (SimpleNitriteMapper documentMapper = (SimpleNitriteMapper) db.getConfig().nitriteMapper()) {
+ documentMapper.registerEntityConverter(new PartiallyIndexedData.PidConverter());
+ }
+ repository = db.getRepository(PartiallyIndexedData.class);
+
+ insertObjects();
+ }
+
+ // Method to generate a random alphanumeric string of given length
+ public static String randomAlphanumeric(int length) {
+ // Define characters to choose from (alphanumeric)
+ String characters = "abcdefghijklmnopqrstuvwxyz";
+ SecureRandom random = new SecureRandom(new byte[] {0, 0, 0});
+
+ // StringBuilder to build the random string
+ StringBuilder randomString = new StringBuilder(length);
+
+ // Generate the random string
+ for (int i = 0; i < length; i++) {
+ int randomIndex = random.nextInt(characters.length());
+ randomString.append(characters.charAt(randomIndex));
+ }
+
+ // Return the random string
+ return randomString.toString();
+ }
+
+ protected void insertObjects() throws ParseException {
+ for (int i = 0; i < 10_000; i++) {
+ repository.insert(new PartiallyIndexedData(
+ randomAlphanumeric(2),
+ randomAlphanumeric(2)));
+ }
+ }
+
+ @After
+ public void after() {
+ if (db != null && !db.isClosed()) {
+ db.close();
+ }
+
+ deleteDb(fileName);
+ }
+
+ @Test
+ public void testMixedQuery() {
+ Cursor cursor = repository.find(Filter.and(
+ FluentFilter.where("indexStr").eq("aa"),
+ FluentFilter.where("otherStr").gt("kk")
+ ));
+ System.out.println("cursor.getFindPlan() = " + cursor.getFindPlan());
+ System.out.println("cursor.toList() = " + cursor.toList());
+ }
+
+ protected Document trimMeta(Document document) {
+ document.remove(DOC_ID);
+ document.remove(DOC_REVISION);
+ document.remove(DOC_MODIFIED);
+ document.remove(DOC_SOURCE);
+ return document;
+ }
+}
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java
index f6ffbaf63..90166b192 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/GeoSpatialFindNearTest.java
@@ -169,12 +169,12 @@ public void testGeoLibrary_Distance_SanityCheck() {
// All of these distances should be within 10 metres of what was estimated on Google Earth.
GeodesicData s12 = WGS84.Inverse(centerPt.getX(), centerPt.getY(), pt2_950m_ESE.getX(), pt2_950m_ESE.getY());
- assertEquals(s12.s12, 950.0, EPSILON_10M);
+ assertEquals(950.0, s12.s12, EPSILON_10M);
GeodesicData s13 = WGS84.Inverse(centerPt.getX(), centerPt.getY(), pt3_930m_W.getX(), pt3_930m_W.getY());
- assertEquals(s13.s12, 930.0, EPSILON_10M);
+ assertEquals(930.0, s13.s12, EPSILON_10M);
GeodesicData s14 = WGS84.Inverse(centerPt.getX(), centerPt.getY(), pt4_2750m_WSW.getX(), pt4_2750m_WSW.getY());
- assertEquals(s14.s12, 2750.0, EPSILON_10M);
+ assertEquals(2750.0, s14.s12, EPSILON_10M);
}
}
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java
index bc34d672b..a2d6d18a1 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialData.java
@@ -50,7 +50,7 @@ public Class getEntityType() {
@Override
public Document toDocument(SpatialData entity, NitriteMapper nitriteMapper) {
return Document.createDocument("id", entity.getId())
- .put("geometry", nitriteMapper.tryConvert(entity.getGeometry(), Document.class));
+ .put("geometry", nitriteMapper.tryConvert(entity.getGeometry(), Geometry.class));
}
@Override
diff --git a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
index c2d42b655..dab6e3d5c 100644
--- a/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
+++ b/nitrite-spatial/src/test/java/org/dizitart/no2/spatial/SpatialIndexTest.java
@@ -25,10 +25,9 @@
import org.dizitart.no2.index.IndexOptions;
import org.dizitart.no2.mvstore.MVStoreModule;
import org.dizitart.no2.repository.Cursor;
+import org.junit.Ignore;
import org.junit.Test;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
@@ -80,6 +79,7 @@ public void testWithin() throws ParseException {
}
@Test
+ @Ignore
public void testWithinTriangleNotJustTestingBoundingBox() throws ParseException {
/*
@@ -93,16 +93,48 @@ public void testWithinTriangleNotJustTestingBoundingBox() throws ParseException
*/
WKTReader reader = new WKTReader();
- Geometry search = reader.read("POLYGON((490 490, 530 490 , 490 530, 490 490))");
+ Geometry search = reader.read("POLYGON((490 490, 530 490, 490 530, 490 490))");
- SpatialData outsidePoint = new SpatialData(7L, reader.read("POINT(520 520)"));
- repository.insert(outsidePoint);
+ SpatialData insidePoint = new SpatialData(7L, reader.read("POINT(500 505)"));
+ SpatialData outsidePoint = new SpatialData(8L, reader.read("POINT(529 529)"));
+ repository.insert(insidePoint,outsidePoint);
- Cursor cursor = repository.find(where("geometry").within(search));
- assertEquals(cursor.size(), 1);
+ Cursor cursor = repository.find(where("geometry").within(search));
+// Cursor cursor = repository.find(
+// and(where("geometry").within(search), where("geometry").within(search.getBoundary())));
+ assertEquals(1, cursor.size());
assertFalse(cursor.toList().contains(outsidePoint));
}
+ @Test
+ public void testNearGeometry_TriangleNearPoint() throws ParseException {
+ /*
+ x (1ºN, 2ºE)
+ │\
+ (1ºN, 1ºE) x─x (2ºN, 1ºE)
+
+ x (0ºN, 0ºE)
+ */
+
+ // given
+ WKTReader reader = new WKTReader();
+ SpatialData triangle = new SpatialData(7L, reader.read("POLYGON((1 1, 1 2, 2 1, 1 1))"));
+ repository.insert(triangle);
+
+ // Define a radius that should include the near corner (with 20% "safety" margin), but not the entire triangle
+ double sqrt2 = 1.4142d; // i.e. the distance from (0,0) to (1,1)
+ double metersPerDegreeAtEquator = 111_000d;
+ double radiusThatShouldIncludeNearCornerOfTriangle = 1.2 * sqrt2 * metersPerDegreeAtEquator;
+
+ // when
+ Cursor cursor = repository.find(
+ where("geometry").near(new Coordinate(0d, 0d), radiusThatShouldIncludeNearCornerOfTriangle));
+
+ // then
+ assertEquals(1, cursor.size());
+ assertTrue(cursor.toList().contains(triangle));
+ }
+
@Test
public void testNearPoint() throws ParseException {
WKTReader reader = new WKTReader();
diff --git a/nitrite/src/main/java/org/dizitart/no2/collection/operation/FindOptimizer.java b/nitrite/src/main/java/org/dizitart/no2/collection/operation/FindOptimizer.java
index 935cc69e3..2d6954504 100644
--- a/nitrite/src/main/java/org/dizitart/no2/collection/operation/FindOptimizer.java
+++ b/nitrite/src/main/java/org/dizitart/no2/collection/operation/FindOptimizer.java
@@ -53,8 +53,8 @@ public FindPlan optimize(Filter filter,
}
private FindPlan createFilterPlan(Collection indexDescriptors, Filter filter) {
- if (filter instanceof AndFilter) {
- List filters = flattenAndFilter((AndFilter) filter);
+ if (filter instanceof FlattenableFilter) {
+ List filters = flattenFilter((FlattenableFilter) filter);
return createAndPlan(indexDescriptors, filters);
} else if (filter instanceof OrFilter) {
return createOrPlan(indexDescriptors, ((OrFilter) filter).getFilters());
@@ -64,12 +64,12 @@ private FindPlan createFilterPlan(Collection indexDescriptors,
}
}
- private List flattenAndFilter(AndFilter andFilter) {
+ private List flattenFilter(FlattenableFilter flattenableFilter) {
List flattenedFilters = new ArrayList<>();
- if (andFilter != null) {
- for (Filter filter : andFilter.getFilters()) {
- if (filter instanceof AndFilter) {
- List filters = flattenAndFilter((AndFilter) filter);
+ if (flattenableFilter != null) {
+ for (Filter filter : flattenableFilter.getFilters()) {
+ if (filter instanceof FlattenableFilter) {
+ List filters = flattenFilter((FlattenableFilter) filter);
flattenedFilters.addAll(filters);
} else {
flattenedFilters.add(filter);
@@ -174,7 +174,7 @@ private void planForIndexOnlyFilters(FindPlan findPlan, Set in
if (filter instanceof IndexOnlyFilter) {
IndexOnlyFilter indexScanFilter = (IndexOnlyFilter) filter;
if (isCompatibleFilter(indexOnlyFilters, indexScanFilter)) {
- // if filter is compatible with already identified index only filter then add
+ // if filter is compatible with already identified index-only filter then add
indexOnlyFilters.add(indexScanFilter);
} else {
throw new FilterException("A query can not have multiple index only filters");
diff --git a/nitrite/src/main/java/org/dizitart/no2/filters/AndFilter.java b/nitrite/src/main/java/org/dizitart/no2/filters/AndFilter.java
index d971a50ab..5398c26dc 100644
--- a/nitrite/src/main/java/org/dizitart/no2/filters/AndFilter.java
+++ b/nitrite/src/main/java/org/dizitart/no2/filters/AndFilter.java
@@ -27,7 +27,7 @@
* @since 1.0
*/
@Getter
-public class AndFilter extends LogicalFilter {
+public class AndFilter extends LogicalFilter implements FlattenableFilter {
AndFilter(Filter... filters) {
super(filters);
diff --git a/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java b/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
new file mode 100644
index 000000000..800c5aa8d
--- /dev/null
+++ b/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
@@ -0,0 +1,29 @@
+/*
+ * 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.filters;
+
+import java.util.List;
+
+/**
+ * Represents a filter which can be flattened or otherwise consists of multiple constituent filters.
+ *
+ * // TODO verify the "@since" version?
+ * @since 4.4.0
+ */
+public interface FlattenableFilter {
+ List getFilters();
+}
From 44d2f4aaa71660408c224f24cfcf9384d8f1d4e5 Mon Sep 17 00:00:00 2001
From: Leo Gertsenshteyn <146586+leoger@users.noreply.github.com>
Date: Tue, 21 Jan 2025 10:16:12 -0800
Subject: [PATCH 5/5] adding in-line PR comments
---
.../org/dizitart/no2/spatial/NearFilter.java | 8 +++++++
.../no2/filters/FlattenableFilter.java | 24 +++++++++++++++++--
2 files changed, 30 insertions(+), 2 deletions(-)
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 a8a3c75cd..d8fc2ca75 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
@@ -73,10 +73,18 @@ public String toString() {
@Override
public List getFilters() {
return List.of(
+ // [PR note] Use of "IntersectsFilter" was an arbitrary choice. Any of the misbehaving filters that
+ // are really just doing a bounding box test within the spatial index would have worked.
+ // The important thing for now was to not accidentally produce *recursive* flattening, and at the
+ // time I wrote this line, I was still planning to edit WithinFilter to have it also implement
+ // the FlattenableFilter interface
new IntersectsFilter(getField(), getValue()),
new NonIndexNearFilter(getField(), getValue()));
}
+ // [PR note] This is probably the first time in years I've used a non-static inner class. I think we
+ // should prefer to avoid the pattern in the final code, but it saved some boiler-plate here for the
+ // proof-of-concept.
public class NonIndexNearFilter extends FieldBasedFilter {
protected NonIndexNearFilter(String field, Geometry circle) {
diff --git a/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java b/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
index 800c5aa8d..a4f2d8618 100644
--- a/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
+++ b/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
@@ -21,9 +21,29 @@
/**
* Represents a filter which can be flattened or otherwise consists of multiple constituent filters.
*
- * // TODO verify the "@since" version?
- * @since 4.4.0
+ * [PR notes]
+ *
+ * - We can't use {@code instanceof SomeClass} to trigger the application of "and"-like flattening,
+ * because any classes defined in submodules aren't available here in the `nitrite` module at compile-time.
+ * that interface of course needs to be here in the `nitrite` module, just like IndexOnlyFilter is.
+ *
+ * - There are plenty of things we could call this. "Andlike" was the first thing I came up with but
+ * that sounded terrible. Flattenable is at least a "functional" naming, in that it says what it is, but this
+ * also feels like it's asking for a {@code flatten} method to be added to the interface. But that then implies
+ * that some of the logic in FindOptimizer would end up distributed across multiple classes. In its current state,
+ * it's very helpful that all the related logic is right there in one file.
+ *
+ * - If we want to keep {@code getFilters} as the interface, then names like CompoundFilter and CompositeFilter
+ * come to mind. However, that would leave FindOptimizer with a strange asymmetry where `and` is just a special
+ * case of this interface but `or` is still its own thing with separate handling.
+ *
+ *
+ * // TODO add a "@since" tag
*/
public interface FlattenableFilter {
+ /**
+ * [PR note] Clearly this is not the ideal contract for this interface, but it allowed existing code to
+ * be leveraged to get the proof-of-concept working.
+ */
List getFilters();
}