Skip to content
Open
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
16 changes: 14 additions & 2 deletions django_mongodb_backend/gis/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
class GISFeatures(BaseSpatialFeatures):
has_spatialrefsys_table = False
supports_transform = False
supports_distance_geodetic = False

@cached_property
def django_test_expected_failures(self):
Expand Down Expand Up @@ -39,6 +40,9 @@ def django_test_skips(self):
# SouthTexasCity fixture objects use SRID 2278 which is ignored
# by the patched version of loaddata in the Django fork.
"gis_tests.distapp.tests.DistanceTest.test_init",
"gis_tests.distapp.tests.DistanceTest.test_distance_lookups",
"gis_tests.distapp.tests.DistanceTest.test_distance_lookups_with_expression_rhs",
"gis_tests.distapp.tests.DistanceTest.test_distance_annotation_group_by",
},
"ImproperlyConfigured isn't raised when using RasterField": {
# Normally RasterField.db_type() raises an error, but MongoDB
Expand All @@ -49,9 +53,17 @@ def django_test_skips(self):
# Error: Index already exists with a different name
"gis_tests.geoapp.test_indexes.SchemaIndexesTests.test_index_name",
},
"GIS lookups not supported.": {
"gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string",
"GIS Union not supported.": {
"gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions",
"gis_tests.distapp.tests.DistanceTest.test_dwithin",
},
"Cannot use a non-Point geometry with distance lookups.": {
"gis_tests.distapp.tests.DistanceTest.test_dwithin_with_expression_rhs"
},
"Subqueries not supported.": {
"gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation",
"gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery",
"gis_tests.distapp.tests.DistanceTest.test_dwithin_subquery",
},
"GeoJSONSerializer doesn't support ObjectId.": {
"gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option",
Expand Down
17 changes: 13 additions & 4 deletions django_mongodb_backend/gis/lookups.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from django.contrib.gis.db.models.lookups import GISLookup
from django.contrib.gis.db.models.lookups import DistanceLookupFromFunction, GISLookup
from django.db import NotSupportedError

from django_mongodb_backend.query_utils import process_lhs, process_rhs

def gis_lookup(self, compiler, connection, as_expr=False): # noqa: ARG001
raise NotSupportedError(f"MongoDB does not support the {self.lookup_name} lookup.")

def _gis_lookup(self, compiler, connection, as_expr=False):
lhs_mql = process_lhs(self, compiler, connection, as_expr=as_expr)
rhs_mql = process_rhs(self, compiler, connection, as_expr=as_expr)
try:
rhs_op = self.get_rhs_op(connection, rhs_mql)
except KeyError as e:
raise NotSupportedError(f"MongoDB does not support the '{self.lookup_name}' lookup.") from e
return rhs_op.as_mql(lhs_mql, rhs_mql, self.rhs_params)


def register_lookups():
GISLookup.as_mql = gis_lookup
GISLookup.as_mql = _gis_lookup
DistanceLookupFromFunction.as_mql = _gis_lookup
120 changes: 118 additions & 2 deletions django_mongodb_backend/gis/operations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,105 @@
from django.contrib.gis import geos
from django.contrib.gis.db import models
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
from django.contrib.gis.measure import Distance
from django.db import NotSupportedError
from django.db.backends.base.operations import BaseDatabaseOperations

from .adapter import Adapter
from .utils import SpatialOperator


class GISOperations(BaseSpatialOperations):
def _gis_within_operator(field, value, op=None, params=None): # noqa: ARG001
return {
field: {
"$geoWithin": {
"$geometry": {
"type": value["type"],
"coordinates": value["coordinates"],
}
}
}
}


def _gis_intersects_operator(field, value, op=None, params=None): # noqa: ARG001
return {
field: {
"$geoIntersects": {
"$geometry": {
"type": value["type"],
"coordinates": value["coordinates"],
}
}
}
}


def _gis_disjoint_operator(field, value, op=None, params=None): # noqa: ARG001
return {
field: {
"$not": {
"$geoIntersects": {
"$geometry": {
"type": value["type"],
"coordinates": value["coordinates"],
}
}
}
}
}


def _gis_contains_operator(field, value, op=None, params=None): # noqa: ARG001
value_type = value["type"]
if value_type != "Point":
raise NotSupportedError("MongoDB does not support contains on non-Point query geometries.")
return {
field: {
"$geoIntersects": {
"$geometry": {
"type": value_type,
"coordinates": value["coordinates"],
}
}
}
}


def _gis_distance_operator(field, value, op=None, params=None):
distance = params[0].m if hasattr(params[0], "m") else params[0]
if op == "distance_gt" or op == "distance_gte":
cmd = {
field: {
"$not": {
"$geoWithin": {
"$centerSphere": [
value["coordinates"],
distance / 6378100, # radius of earth in meters
],
}
}
}
}
else:
cmd = {
field: {
"$geoWithin": {
"$centerSphere": [
value["coordinates"],
distance / 6378100, # radius of earth in meters
],
}
}
}
return cmd


def _gis_dwithin_operator(field, value, op=None, params=None): # noqa: ARG001
return {field: {"$geoWithin": {"$centerSphere": [value["coordinates"], params[0]]}}}


class GISOperations(BaseSpatialOperations, BaseDatabaseOperations):
Adapter = Adapter

disallowed_aggregates = (
Expand All @@ -18,7 +112,17 @@ class GISOperations(BaseSpatialOperations):

@property
def gis_operators(self):
return {}
return {
"contains": SpatialOperator("contains", _gis_contains_operator),
"intersects": SpatialOperator("intersects", _gis_intersects_operator),
"disjoint": SpatialOperator("disjoint", _gis_disjoint_operator),
"within": SpatialOperator("within", _gis_within_operator),
"distance_gt": SpatialOperator("distance_gt", _gis_distance_operator),
"distance_gte": SpatialOperator("distance_gte", _gis_distance_operator),
"distance_lt": SpatialOperator("distance_lt", _gis_distance_operator),
"distance_lte": SpatialOperator("distance_lte", _gis_distance_operator),
"dwithin": SpatialOperator("dwithin", _gis_dwithin_operator),
}

unsupported_functions = {
"Area",
Expand Down Expand Up @@ -95,3 +199,15 @@ def converter(value, expression, connection): # noqa: ARG001
return geom_class(*value["coordinates"], srid=srid)

return converter

def get_distance(self, f, value, lookup_type):
value = value[0]
if isinstance(value, Distance):
if f.geodetic(self.connection):
raise ValueError(
"Only numeric values of degree units are allowed on geodetic distance queries."
)
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
else:
dist_param = value
return [dist_param]
19 changes: 19 additions & 0 deletions django_mongodb_backend/gis/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
A collection of utility routines and classes used by the spatial
backend.
"""

from django.contrib.gis.db.backends.utils import SpatialOperator as _SpatialOperator


class SpatialOperator(_SpatialOperator):
"""
Class encapsulating the behavior specific to a GIS operation (used by lookups).
"""

def __init__(self, op=None, func=None):
self.op = op
self.func = func

def as_mql(self, lhs, rhs, params=None):
return self.func(lhs, rhs, self.op, params)
47 changes: 46 additions & 1 deletion tests/gis_tests_/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,51 @@
@skipUnlessDBFeature("gis_enabled")
class LookupTests(TestCase):
def test_unsupported_lookups(self):
msg = "MongoDB does not support the same_as lookup."
msg = "MongoDB does not support the 'same_as' lookup."
with self.assertRaisesMessage(NotSupportedError, msg):
City.objects.get(point__same_as=Point(95, 30))

def test_within_lookup(self):
city = City.objects.create(point=Point(95, 30))
qs = City.objects.filter(point__within=Point(95, 30).buffer(10))
self.assertIn(city, qs)

def test_intersects_lookup(self):
city = City.objects.create(point=Point(95, 30))
qs = City.objects.filter(point__intersects=Point(95, 30).buffer(10))
self.assertIn(city, qs)

def test_disjoint_lookup(self):
city = City.objects.create(point=Point(50, 30))
qs = City.objects.filter(point__disjoint=Point(100, 50))
self.assertIn(city, qs)

def test_contains_lookup(self):
city = City.objects.create(point=Point(95, 30))
qs = City.objects.filter(point__contains=Point(95, 30))
self.assertIn(city, qs)

def test_distance_gt_lookup(self):
city = City.objects.create(point=Point(95, 30))
qs = City.objects.filter(point__distance_gt=(Point(0, 0), 100))
self.assertIn(city, qs)

def test_distance_lt_lookup(self):
city = City.objects.create(point=Point(40.7589, -73.9851))
qs = City.objects.filter(point__distance_lt=(Point(40.7670, -73.9820), 1000))
self.assertIn(city, qs)

def test_distance_gte_lookup(self):
city = City.objects.create(point=Point(95, 30))
qs = City.objects.filter(point__distance_gt=(Point(0, 0), 100))
self.assertIn(city, qs)

def test_distance_lte_lookup(self):
city = City.objects.create(point=Point(40.7589, -73.9851))
qs = City.objects.filter(point__distance_lt=(Point(40.7670, -73.9820), 1000))
self.assertIn(city, qs)

def test_dwithin_lookup(self):
city = City.objects.create(point=Point(40.7589, -73.9851))
qs = City.objects.filter(point__dwithin=(Point(40.7670, -73.9820), 1000))
self.assertIn(city, qs)