11import 'package:dart_jts/dart_jts.dart' ;
22import 'package:nitrite/nitrite.dart' ;
3+ import 'package:nitrite_spatial/src/geom_utils.dart' ;
34import 'package:nitrite_spatial/src/indexer.dart' ;
45
6+ /// Copies NitriteFilter context (nitriteConfig, collectionName, objectFilter) from source to target
7+ void _copyFilterContext (NitriteFilter source, NitriteFilter target) {
8+ if (source.nitriteConfig != null ) {
9+ target.nitriteConfig = source.nitriteConfig;
10+ target.collectionName = source.collectionName;
11+ target.objectFilter = source.objectFilter;
12+ }
13+ }
14+
515/// The abstract base class for all spatial filters in Nitrite.
616///
717/// A spatial filter is used to query Nitrite database for
@@ -31,9 +41,61 @@ abstract class SpatialFilter extends IndexOnlyFilter {
3141 }
3242}
3343
34- ///@nodoc
35- class WithinFilter extends SpatialFilter {
36- WithinFilter (super .field, super .value);
44+ /// A non-index filter that validates actual geometry relationships.
45+ /// This filter is used as the second stage of spatial filtering after
46+ /// the R-Tree index has filtered candidates based on bounding boxes.
47+ class _GeometryValidationFilter extends FieldBasedFilter {
48+ final bool Function (Geometry , Geometry ) _validator;
49+
50+ _GeometryValidationFilter (super .field, super .value, this ._validator);
51+
52+ @override
53+ bool apply (Document doc) {
54+ var fieldValue = doc.get (field);
55+ if (fieldValue == null ) {
56+ return false ;
57+ }
58+
59+ Geometry ? documentGeometry;
60+ if (fieldValue is Geometry ) {
61+ documentGeometry = fieldValue;
62+ } else if (fieldValue is String ) {
63+ // Try to parse WKT string
64+ try {
65+ var reader = WKTReader ();
66+ documentGeometry = reader.read (fieldValue);
67+ } catch (e) {
68+ return false ;
69+ }
70+ } else if (fieldValue is Document ) {
71+ // For entity repositories, geometry is stored as a Document with serialized string
72+ try {
73+ var geometryString = fieldValue['geometry' ] as String ? ;
74+ if (geometryString != null ) {
75+ var deserialized = GeometrySerializer .deserialize (geometryString);
76+ if (deserialized != null ) {
77+ documentGeometry = deserialized;
78+ }
79+ }
80+ } catch (e) {
81+ return false ;
82+ }
83+ } else {
84+ return false ;
85+ }
86+
87+ if (documentGeometry == null ) {
88+ return false ;
89+ }
90+
91+ return _validator (documentGeometry, value as Geometry );
92+ }
93+ }
94+
95+ /// Internal implementation of WithinFilter for index scanning only.
96+ /// Does not implement FlattenableFilter to avoid infinite recursion.
97+ class WithinIndexFilter extends SpatialFilter {
98+ WithinIndexFilter (super .field, super .value);
3799
38100 @override
39101 Stream <dynamic > applyOnIndex (IndexMap indexMap) {
@@ -47,9 +109,10 @@ class WithinFilter extends SpatialFilter {
47109 }
48110}
49111
50- ///@nodoc
51- class IntersectsFilter extends SpatialFilter {
52- IntersectsFilter (super .field, super .value);
112+ /// Internal implementation of IntersectsFilter for index scanning only.
113+ /// Does not implement FlattenableFilter to avoid infinite recursion.
114+ class IntersectsIndexFilter extends SpatialFilter {
115+ IntersectsIndexFilter (super .field, super .value);
53116
54117 @override
55118 Stream <dynamic > applyOnIndex (IndexMap indexMap) {
@@ -64,17 +127,183 @@ class IntersectsFilter extends SpatialFilter {
64127}
65128
66129///@nodoc
67- class NearFilter extends WithinFilter {
130+ class WithinFilter extends NitriteFilter implements FlattenableFilter {
131+ final String field;
132+ final Geometry geometry;
133+
134+ WithinFilter (this .field, this .geometry);
135+
136+ @override
137+ bool apply (Document doc) {
138+ // For non-indexed queries, apply the validation filter directly
139+ var validationFilter = _GeometryValidationFilter (
140+ field,
141+ geometry,
142+ (docGeom, filterGeom) => docGeom.within (filterGeom),
143+ );
144+ _copyFilterContext (this , validationFilter);
145+ return validationFilter.apply (doc);
146+ }
147+
148+ @override
149+ List <Filter > getFilters () {
150+ // Return two filters: one for index scan, one for validation
151+ return [
152+ WithinIndexFilter (field, geometry),
153+ _GeometryValidationFilter (
154+ field,
155+ geometry,
156+ (docGeom, filterGeom) => docGeom.within (filterGeom),
157+ ),
158+ ];
159+ }
160+
161+ @override
162+ String toString () {
163+ return '($field within $geometry )' ;
164+ }
165+ }
166+
167+ ///@nodoc
168+ class IntersectsFilter extends NitriteFilter implements FlattenableFilter {
169+ final String field;
170+ final Geometry geometry;
171+
172+ IntersectsFilter (this .field, this .geometry);
173+
174+ @override
175+ bool apply (Document doc) {
176+ // For non-indexed queries, apply the validation filter directly
177+ var validationFilter = _GeometryValidationFilter (
178+ field,
179+ geometry,
180+ (docGeom, filterGeom) => docGeom.intersects (filterGeom),
181+ );
182+ _copyFilterContext (this , validationFilter);
183+ return validationFilter.apply (doc);
184+ }
185+
186+ @override
187+ List <Filter > getFilters () {
188+ // Return two filters: one for index scan, one for validation
189+ return [
190+ IntersectsIndexFilter (field, geometry),
191+ _GeometryValidationFilter (
192+ field,
193+ geometry,
194+ (docGeom, filterGeom) => docGeom.intersects (filterGeom),
195+ ),
196+ ];
197+ }
198+
199+ @override
200+ String toString () {
201+ return '($field intersects $geometry )' ;
202+ }
203+ }
204+
205+ ///@nodoc
206+ class NearFilter extends NitriteFilter implements FlattenableFilter {
207+ final String field;
208+ final Geometry circle;
209+ final Coordinate center;
210+ final double radius;
211+
68212 factory NearFilter (String field, Coordinate center, double radius) {
69- var geometry = _createCircle (center, radius);
70- return NearFilter ._(field, geometry );
213+ var circle = _createCircle (center, radius);
214+ return NearFilter ._(field, circle, center, radius );
71215 }
72216
73217 factory NearFilter .fromPoint (String field, Point point, double radius) {
74- return NearFilter ._(field, _createCircle (point.getCoordinate (), radius));
218+ var center = point.getCoordinate ();
219+ var circle = _createCircle (center, radius);
220+ return NearFilter ._(field, circle, center! , radius);
221+ }
222+
223+ NearFilter ._(this .field, this .circle, this .center, this .radius);
224+
225+ @override
226+ bool apply (Document doc) {
227+ // For non-indexed queries, apply the validation filter directly
228+ var validationFilter = _NearValidationFilter (field, center, radius);
229+ _copyFilterContext (this , validationFilter);
230+ return validationFilter.apply (doc);
75231 }
76232
77- NearFilter ._(super .field, super .geometry);
233+ @override
234+ List <Filter > getFilters () {
235+ // Return two filters: one for index scan (using within), one for distance validation
236+ return [
237+ WithinIndexFilter (field, circle),
238+ _NearValidationFilter (field, center, radius),
239+ ];
240+ }
241+
242+ @override
243+ String toString () {
244+ return '($field near $center within $radius )' ;
245+ }
246+ }
247+
248+ /// Validation filter for near queries that checks actual distance.
249+ class _NearValidationFilter extends NitriteFilter {
250+ final String field;
251+ final Coordinate center;
252+ final double radius;
253+
254+ _NearValidationFilter (this .field, this .center, this .radius);
255+
256+ @override
257+ bool apply (Document doc) {
258+ var fieldValue = doc.get (field);
259+ if (fieldValue == null ) {
260+ return false ;
261+ }
262+
263+ Geometry ? documentGeometry;
264+ if (fieldValue is Geometry ) {
265+ documentGeometry = fieldValue;
266+ } else if (fieldValue is String ) {
267+ try {
268+ var reader = WKTReader ();
269+ documentGeometry = reader.read (fieldValue);
270+ } catch (e) {
271+ return false ;
272+ }
273+ } else if (fieldValue is Document ) {
274+ // For entity repositories, geometry is stored as a Document with serialized string
275+ try {
276+ var geometryString = fieldValue['geometry' ] as String ? ;
277+ if (geometryString != null ) {
278+ var deserialized = GeometrySerializer .deserialize (geometryString);
279+ if (deserialized != null ) {
280+ documentGeometry = deserialized;
281+ }
282+ }
283+ } catch (e) {
284+ return false ;
285+ }
286+ } else {
287+ return false ;
288+ }
289+
290+ if (documentGeometry == null ) {
291+ return false ;
292+ }
293+
294+ // For near queries, check if the geometry is within the distance
295+ // For points, check direct distance. For other geometries, check if they intersect the circle.
296+ if (documentGeometry is Point ) {
297+ var coord = documentGeometry.getCoordinate ();
298+ if (coord == null ) return false ;
299+ var distance = center.distance (coord);
300+ return distance <= radius;
301+ } else {
302+ // For non-point geometries, check if they intersect the circle
303+ var circle = _createCircle (center, radius);
304+ return documentGeometry.intersects (circle);
305+ }
306+ }
78307}
79308
80309Geometry _createCircle (Coordinate ? center, double radius) {
0 commit comments