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..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
@@ -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
@@ -46,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 8905fe197..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;
@@ -79,6 +78,63 @@ public void testWithin() throws ParseException {
assertEquals(cursor1.toList().stream().map(this::trimMeta).collect(Collectors.toList()), Collections.singletonList(doc1));
}
+ @Test
+ @Ignore
+ public void testWithinTriangleNotJustTestingBoundingBox() throws ParseException {
+
+ /*
+ (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();
+ Geometry search = reader.read("POLYGON((490 490, 530 490, 490 530, 490 490))");
+
+ 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));
+// 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..a4f2d8618
--- /dev/null
+++ b/nitrite/src/main/java/org/dizitart/no2/filters/FlattenableFilter.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ *
+ * [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();
+}