Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions nitrite-spatial/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
</dependency>
<dependency>
<groupId>net.sf.geographiclib</groupId>
<artifactId>GeographicLib-Java</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.dizitart.no2.spatial;

import net.sf.geographiclib.Geodesic;
import net.sf.geographiclib.GeodesicData;
import net.sf.geographiclib.GeodesicMask;
import org.locationtech.jts.geom.Coordinate;
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 {

/**
* Bitmask specifying which results should be returned from the "direct" calculation.
* We don't need to know the azimuth at s2, so we can save a couple of CPU cycles by not asking for it!
* <p/>
* See javadoc at {@link Geodesic#Direct(double, double, double, double, int)} for more details.
*/
public static final int OUTMASK = GeodesicMask.STANDARD ^ GeodesicMask.AZIMUTH;

@Override
public Polygon createCircle()
{
Envelope env = dim.getEnvelope();
double radiusInMeters = env.getWidth() / 2.0;
double ctrX = dim.getCentre().x;
double ctrY = dim.getCentre().y;

Coordinate[] pts = new Coordinate[nPts + 1];
int i;
for (i = 0; i < nPts; i++) {
// because this is the "azimuth" value, it starts at "geodetic north" and proceeds clockwise
double azimuthInDegrees = i * (360.0 / nPts);
GeodesicData directResult = Geodesic.WGS84.Direct(ctrX, ctrY, azimuthInDegrees, radiusInMeters, OUTMASK);
double lat = directResult.lat2;
double lon = directResult.lon2;
pts[i] = coord(lat, lon);
}
pts[i] = new Coordinate(pts[0]);

LinearRing ring = geomFact.createLinearRing(pts);
return geomFact.createPolygon(ring);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,49 @@

package org.dizitart.no2.spatial;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.util.GeometricShapeFactory;
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 <a href="https://epsg.io/4326">SRID 4326</a> */
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) {
GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
GeometricShapeFactoryExt shapeFactory = new GeometricShapeFactoryExt();
shapeFactory.setNumPoints(64);
shapeFactory.setCentre(center);
shapeFactory.setSize(radius * 2);
Expand All @@ -46,4 +69,56 @@ private static Geometry createCircle(Coordinate center, double radius) {
public String toString() {
return "(" + getField() + " nears " + getValue() + ")";
}

@Override
public List<Filter> 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) {
super(field, circle);
}

@Override
public boolean apply(Pair<NitriteId, Document> 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");
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ public LinkedHashSet<NitriteId> findNitriteIds(FindPlan findPlan) {
Geometry geometry = spatialFilter.getValue();
BoundingBox boundingBox = fromGeometry(geometry);

// NOTE: It looks like we are painted into something of a corner here! The index only knows the bounding boxes,
// because that's how an R-Tree is meant to work. And that's fine until we have to deal with the reality
// of the fact that these are actually arbitrary geometries and the users are crafting queries that are
// meant to test whether they are within each other or not.
//
// The query *cannot* be satisfied by simply checking bounding boxes and calling it a day.
//
// The closest thing I see so far to a solution is for us to *split the filter into two steps* and either:
// (a) plumb the Map<NitriteId,Document> from ReadOperations down to here, so that we can look at the actual
// geometry values; or (b) treat this as an initial "fast filtering" step and then plan to have something
// in ReadOperations (or even further up the call stack) take the next step and perform the more
// computationally intensive geometry check.

if (filter instanceof WithinFilter) {
keys = indexMap.findContainedKeys(boundingBox);
} else if (filter instanceof IntersectsFilter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,4 +48,5 @@ public List<?> applyOnIndex(IndexMap indexMap) {
public String toString() {
return "(" + getField() + " within " + getValue() + ")";
}

}
Loading
Loading