Skip to content

Commit 4264937

Browse files
NoahStapptimgraham
authored andcommitted
INTPYTHON-835 Add support for GIS lookups
1 parent b3a4245 commit 4264937

File tree

9 files changed

+461
-18
lines changed

9 files changed

+461
-18
lines changed

django_mongodb_backend/gis/features.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
class GISFeatures(BaseSpatialFeatures):
66
has_spatialrefsys_table = False
7+
supports_distance_geodetic = False
8+
supports_dwithin_distance_expr = False
79
supports_transform = False
810

911
@cached_property
@@ -49,9 +51,9 @@ def django_test_skips(self):
4951
# Error: Index already exists with a different name
5052
"gis_tests.geoapp.test_indexes.SchemaIndexesTests.test_index_name",
5153
},
52-
"GIS lookups not supported.": {
53-
"gis_tests.geoapp.tests.GeoModelTest.test_gis_query_as_string",
54-
"gis_tests.geoapp.tests.GeoLookupTest.test_gis_lookups_with_complex_expressions",
54+
"Subqueries not supported.": {
55+
"gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation",
56+
"gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery",
5557
},
5658
"GeoJSONSerializer doesn't support ObjectId.": {
5759
"gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option",
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
from django.contrib.gis.db.models.lookups import GISLookup
1+
from django.contrib.gis.db.models.lookups import DistanceLookupFromFunction, GISLookup
22
from django.db import NotSupportedError
33

4+
from django_mongodb_backend.query_utils import process_lhs, process_rhs
45

5-
def gis_lookup(self, compiler, connection, as_expr=False): # noqa: ARG001
6-
raise NotSupportedError(f"MongoDB does not support the {self.lookup_name} lookup.")
6+
7+
def gis_lookup(self, compiler, connection, as_expr=False):
8+
if as_expr:
9+
raise NotSupportedError("MongoDB does not support GIS lookups in expressions.")
10+
lhs_mql = process_lhs(self, compiler, connection, as_expr=as_expr)
11+
rhs_mql = process_rhs(self, compiler, connection, as_expr=as_expr)
12+
if "subquery" in rhs_mql or "subquery" in lhs_mql:
13+
raise NotSupportedError("MongoDB does not support GIS lookups in subqueries.")
14+
try:
15+
rhs_op = self.get_rhs_op(connection, rhs_mql)
16+
except KeyError as e:
17+
raise NotSupportedError(f"MongoDB does not support the '{self.lookup_name}' lookup.") from e
18+
return rhs_op.as_mql(lhs_mql, rhs_mql, self.rhs_params)
719

820

921
def register_lookups():
1022
GISLookup.as_mql = gis_lookup
23+
DistanceLookupFromFunction.as_mql = gis_lookup

django_mongodb_backend/gis/operations.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
from django.contrib.gis import geos
22
from django.contrib.gis.db import models
33
from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations
4+
from django.contrib.gis.measure import Distance
45

56
from .adapter import Adapter
7+
from .operators import (
8+
Contains,
9+
Disjoint,
10+
DistanceGT,
11+
DistanceGTE,
12+
DistanceLT,
13+
DistanceLTE,
14+
DWithin,
15+
Intersects,
16+
Within,
17+
)
618

719

820
class GISOperations(BaseSpatialOperations):
@@ -16,9 +28,17 @@ class GISOperations(BaseSpatialOperations):
1628
models.Union,
1729
)
1830

19-
@property
20-
def gis_operators(self):
21-
return {}
31+
gis_operators = {
32+
"contains": Contains(),
33+
"disjoint": Disjoint(),
34+
"distance_gt": DistanceGT(),
35+
"distance_gte": DistanceGTE(),
36+
"distance_lt": DistanceLT(),
37+
"distance_lte": DistanceLTE(),
38+
"dwithin": DWithin(),
39+
"intersects": Intersects(),
40+
"within": Within(),
41+
}
2242

2343
unsupported_functions = {
2444
"Area",
@@ -97,3 +117,15 @@ def converter(value, expression, connection): # noqa: ARG001
97117
return geom_class(*value["coordinates"], srid=srid)
98118

99119
return converter
120+
121+
def get_distance(self, f, value, lookup_type):
122+
value = value[0]
123+
if isinstance(value, Distance):
124+
if f.geodetic(self.connection):
125+
raise ValueError(
126+
"Only numeric values of radian units are allowed on geodetic distance queries."
127+
)
128+
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
129+
else:
130+
dist_param = value
131+
return [dist_param]
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from django.db import NotSupportedError
2+
3+
4+
class Operator:
5+
def as_sql(self, connection, lookup, template_params, sql_params):
6+
# Return some dummy value to prevent str(queryset.query) from crashing.
7+
# The output of as_sql() is meaningless for this no-SQL backend.
8+
return self.name, []
9+
10+
11+
class Contains(Operator):
12+
name = "contains"
13+
14+
def as_mql(self, field, value, params=None):
15+
value_type = value["type"]
16+
if value_type != "Point":
17+
raise NotSupportedError(
18+
"MongoDB does not support contains on non-Point query geometries."
19+
)
20+
return {
21+
field: {
22+
"$geoIntersects": {
23+
"$geometry": {
24+
"type": value_type,
25+
"coordinates": value["coordinates"],
26+
}
27+
}
28+
}
29+
}
30+
31+
32+
class Disjoint(Operator):
33+
name = "disjoint"
34+
35+
def as_mql(self, field, value, params=None):
36+
return {
37+
field: {
38+
"$not": {
39+
"$geoIntersects": {
40+
"$geometry": {
41+
"type": value["type"],
42+
"coordinates": value["coordinates"],
43+
}
44+
}
45+
}
46+
}
47+
}
48+
49+
50+
class DistanceBase(Operator):
51+
name = "distance_base"
52+
53+
def as_mql(self, field, value, params=None):
54+
distance = params[0].m if hasattr(params[0], "m") else params[0]
55+
if self.name == "distance_gt" or self.name == "distance_gte":
56+
cmd = {
57+
field: {
58+
"$not": {
59+
"$geoWithin": {
60+
"$centerSphere": [
61+
value["coordinates"],
62+
distance / 6378100, # radius of earth in meters
63+
],
64+
}
65+
}
66+
}
67+
}
68+
else:
69+
cmd = {
70+
field: {
71+
"$geoWithin": {
72+
"$centerSphere": [
73+
value["coordinates"],
74+
distance / 6378100, # radius of earth in meters
75+
],
76+
}
77+
}
78+
}
79+
return cmd
80+
81+
82+
class DistanceGT(DistanceBase):
83+
name = "distance_gt"
84+
85+
86+
class DistanceGTE(DistanceBase):
87+
name = "distance_gte"
88+
89+
90+
class DistanceLT(DistanceBase):
91+
name = "distance_lt"
92+
93+
94+
class DistanceLTE(DistanceBase):
95+
name = "distance_lte"
96+
97+
98+
class DWithin(Operator):
99+
name = "dwithin"
100+
101+
def as_mql(self, field, value, params=None):
102+
return {field: {"$geoWithin": {"$centerSphere": [value["coordinates"], params[0]]}}}
103+
104+
105+
class Intersects(Operator):
106+
name = "intersects"
107+
108+
def as_mql(self, field, value, params=None):
109+
return {
110+
field: {
111+
"$geoIntersects": {
112+
"$geometry": {
113+
"type": value["type"],
114+
"coordinates": value["coordinates"],
115+
}
116+
}
117+
}
118+
}
119+
120+
121+
class Within(Operator):
122+
name = "within"
123+
124+
def as_mql(self, field, value, params=None):
125+
return {
126+
field: {
127+
"$geoWithin": {
128+
"$geometry": {
129+
"type": value["type"],
130+
"coordinates": value["coordinates"],
131+
}
132+
}
133+
}
134+
}

docs/ref/contrib/gis.rst

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,26 @@ Each model field stores data as :doc:`GeoJSON objects
1818
All fields have a :doc:`2dsphere index
1919
<manual:core/indexes/index-types/geospatial/2dsphere>` created on them.
2020

21-
You can use any of the :ref:`geospatial query operators
21+
The following :ref:`spatial lookups <django:spatial-lookups>` are supported:
22+
23+
- :lookup:`contains <gis-contains>`
24+
- :lookup:`disjoint`
25+
- :lookup:`distance_gt`
26+
- :lookup:`distance_gte`
27+
- :lookup:`distance_lt`
28+
- :lookup:`distance_lte`
29+
- :lookup:`dwithin`
30+
- :lookup:`intersects`
31+
- :lookup:`within`
32+
33+
You can also use any of the :ref:`geospatial query operators
2234
<manual:geospatial-query-operators>` or the :ref:`geospatial aggregation
2335
pipeline stage <geospatial-aggregation>` in :meth:`.raw_aggregate` queries.
2436

37+
.. versionadded:: 6.0.1
38+
39+
Support for spatial lookups was added.
40+
2541
Configuration
2642
=============
2743

@@ -39,6 +55,8 @@ Limitations
3955
(:attr:`BaseSpatialField.srid
4056
<django.contrib.gis.db.models.BaseSpatialField.srid>`)
4157
besides `4326 (WGS84) <https://spatialreference.org/ref/epsg/4326/>`_.
42-
- None of the :doc:`GIS QuerySet APIs <django:ref/contrib/gis/geoquerysets>`
43-
(lookups, aggregates, and database functions) are supported.
58+
- Spatial lookups don't support subqueries or expressions.
59+
- :ref:`GIS aggregate functions <gis-aggregation-functions>` and
60+
:doc:`geographic database functions <django:ref/contrib/gis/functions>`
61+
aren't supported.
4462
- :class:`~django.contrib.gis.db.models.RasterField` isn't supported.

docs/releases/6.0.x.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ Django MongoDB Backend 6.0.x
1010
New features
1111
------------
1212

13-
- ...
13+
- Support for some spatial lookups is added: ``contains``, ``disjoint``,
14+
``distance_gt``, ``distance_gte``, ``distance_lt``, ``distance_lte``,
15+
``dwithin``, ``intersects``, and ``within``.
1416

1517
Bug fixes
1618
---------
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
[
2+
{
3+
"pk": "000000000000000000000001",
4+
"model": "gis_tests_.city",
5+
"fields": {
6+
"name": "Houston",
7+
"point": "POINT (-95.363151 29.763374)"
8+
}
9+
},
10+
{
11+
"pk": "000000000000000000000002",
12+
"model": "gis_tests_.city",
13+
"fields": {
14+
"name": "Dallas",
15+
"point": "POINT (-96.801611 32.782057)"
16+
}
17+
},
18+
{
19+
"pk": "000000000000000000000003",
20+
"model": "gis_tests_.city",
21+
"fields": {
22+
"name": "Oklahoma City",
23+
"point": "POINT (-97.521157 34.464642)"
24+
}
25+
},
26+
{
27+
"pk": "000000000000000000000004",
28+
"model": "gis_tests_.city",
29+
"fields": {
30+
"name": "Wellington",
31+
"point": "POINT (174.783117 -41.315268)"
32+
}
33+
},
34+
{
35+
"pk": "000000000000000000000005",
36+
"model": "gis_tests_.city",
37+
"fields": {
38+
"name": "Pueblo",
39+
"point": "POINT (-104.609252 38.255001)"
40+
}
41+
},
42+
{
43+
"pk": "000000000000000000000006",
44+
"model": "gis_tests_.city",
45+
"fields": {
46+
"name": "Lawrence",
47+
"point": "POINT (-95.235060 38.971823)"
48+
}
49+
},
50+
{
51+
"pk": "000000000000000000000007",
52+
"model": "gis_tests_.city",
53+
"fields": {
54+
"name": "Chicago",
55+
"point": "POINT (-87.650175 41.850385)"
56+
}
57+
},
58+
{
59+
"pk": "000000000000000000000008",
60+
"model": "gis_tests_.city",
61+
"fields": {
62+
"name": "Victoria",
63+
"point": "POINT (-123.305196 48.462611)"
64+
}
65+
},
66+
{
67+
"pk": "000000000000000000000001",
68+
"model": "gis_tests_.zipcode",
69+
"fields" : {
70+
"code" : "77002",
71+
"poly" : "POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95.360915 29.771827, -95.354615 29.771827, -95.351515 29.772527, -95.350915 29.765327, -95.351015 29.762436, -95.350115 29.760328, -95.347515 29.758528, -95.352315 29.753928, -95.356415 29.756328, -95.358215 29.754028, -95.360215 29.756328, -95.363415 29.757128, -95.364014 29.75638, -95.363415 29.753928, -95.360015 29.751828, -95.361815 29.749528, -95.362715 29.750028, -95.367516 29.744128, -95.369316 29.745128, -95.373916 29.744128, -95.380116 29.738028, -95.387916 29.727929, -95.388516 29.729629, -95.387916 29.732129, -95.382916 29.737428, -95.376616 29.742228, -95.372616 29.747228, -95.378601 29.750846, -95.378616 29.752028, -95.378616 29.754428, -95.376016 29.754528, -95.374616 29.759828, -95.373616 29.761128, -95.371916 29.763928, -95.372316 29.768727, -95.365884 29.76791, -95.366015 29.767127, -95.358715 29.765327, -95.358615 29.766327, -95.359115 29.767227, -95.360215 29.767027, -95.362783 29.768267, -95.365315 29.770527, -95.365015 29.772327))"
72+
}
73+
},
74+
{
75+
"pk": "000000000000000000000002",
76+
"model": "gis_tests_.zipcode",
77+
"fields" : {
78+
"code" : "77005",
79+
"poly" : "POLYGON ((-95.447918 29.727275, -95.428017 29.728729, -95.421117 29.729029, -95.418617 29.727629, -95.418517 29.726429, -95.402117 29.726629, -95.402117 29.725729, -95.395316 29.725729, -95.391916 29.726229, -95.389716 29.725829, -95.396517 29.715429, -95.397517 29.715929, -95.400917 29.711429, -95.411417 29.715029, -95.418417 29.714729, -95.418317 29.70623, -95.440818 29.70593, -95.445018 29.70683, -95.446618 29.70763, -95.447418 29.71003, -95.447918 29.727275))"
80+
}
81+
},
82+
{
83+
"pk": "000000000000000000000003",
84+
"model": "gis_tests_.zipcode",
85+
"fields" : {
86+
"code" : "77025",
87+
"poly" : "POLYGON ((-95.418317 29.70623, -95.414717 29.706129, -95.414617 29.70533, -95.418217 29.70533, -95.419817 29.69533, -95.419484 29.694196, -95.417166 29.690901, -95.414517 29.69433, -95.413317 29.69263, -95.412617 29.68973, -95.412817 29.68753, -95.414087 29.685055, -95.419165 29.685428, -95.421617 29.68513, -95.425717 29.67983, -95.425017 29.67923, -95.424517 29.67763, -95.427418 29.67763, -95.438018 29.664631, -95.436713 29.664411, -95.440118 29.662231, -95.439218 29.661031, -95.437718 29.660131, -95.435718 29.659731, -95.431818 29.660331, -95.441418 29.656631, -95.441318 29.656331, -95.441818 29.656131, -95.441718 29.659031, -95.441118 29.661031, -95.446718 29.656431, -95.446518 29.673431, -95.446918 29.69013, -95.447418 29.71003, -95.446618 29.70763, -95.445018 29.70683, -95.440818 29.70593, -95.418317 29.70623))"
88+
}
89+
},
90+
{
91+
"pk": "000000000000000000000004",
92+
"model": "gis_tests_.zipcode",
93+
"fields" : {
94+
"code" : "77401",
95+
"poly" : "POLYGON ((-95.447918 29.727275, -95.447418 29.71003, -95.446918 29.69013, -95.454318 29.68893, -95.475819 29.68903, -95.475819 29.69113, -95.484419 29.69103, -95.484519 29.69903, -95.480419 29.70133, -95.480419 29.69833, -95.474119 29.69833, -95.474119 29.70453, -95.472719 29.71283, -95.468019 29.71293, -95.468219 29.720229, -95.464018 29.720229, -95.464118 29.724529, -95.463018 29.725929, -95.459818 29.726129, -95.459918 29.720329, -95.451418 29.720429, -95.451775 29.726303, -95.451318 29.727029, -95.447918 29.727275))"
96+
}
97+
}
98+
]

0 commit comments

Comments
 (0)