-
Notifications
You must be signed in to change notification settings - Fork 31
INTPYTHON-835 Add support for GIS lookups #448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
c97a4fe
b3a905f
e56bf17
38ed40a
462a54d
86b1e75
f4b724f
5ad9f60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,9 @@ | |
| class GISFeatures(BaseSpatialFeatures): | ||
| has_spatialrefsys_table = False | ||
| supports_transform = False | ||
| supports_distance_geodetic = False | ||
| has_Distance_function = False | ||
| has_Union_function = False | ||
timgraham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| @cached_property | ||
| def django_test_expected_failures(self): | ||
|
|
@@ -39,6 +42,11 @@ 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", | ||
| "gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_simple", | ||
| "gis_tests.distapp.tests.DistanceFunctionsTests.test_distance_order_by", | ||
timgraham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| "ImproperlyConfigured isn't raised when using RasterField": { | ||
| # Normally RasterField.db_type() raises an error, but MongoDB | ||
|
|
@@ -49,10 +57,13 @@ 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", | ||
| }, | ||
|
Comment on lines
56
to
62
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self that we should add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a couple other places where tests not marked as requiring a feature use it. Maybe the authors didn't expect a backend to implement only the slice of operations that we do?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. The skipping is generally just best effort... what's needed for the built-in backends. |
||
| "Subqueries not supported.": { | ||
| "gis_tests.geoapp.tests.GeoLookupTest.test_subquery_annotation", | ||
| "gis_tests.geoapp.tests.GeoQuerySetTest.test_within_subquery", | ||
| }, | ||
| "GeoJSONSerializer doesn't support ObjectId.": { | ||
| "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_fields_option", | ||
| "gis_tests.geoapp.test_serializers.GeoJSONSerializerTests.test_geometry_field_option", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,15 @@ | ||
| from django.contrib.gis.db.models.lookups import GISLookup | ||
| from django.db import NotSupportedError | ||
| from django.contrib.gis.db.models.lookups import DistanceLookupFromFunction, GISLookup | ||
|
|
||
| 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): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding an underscore prefix is not needed. |
||
| lhs_mql = process_lhs(self, compiler, connection, as_expr=as_expr) | ||
| rhs_mql = process_rhs(self, compiler, connection, as_expr=as_expr) | ||
| rhs_op = self.get_rhs_op(connection, rhs_mql) | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,110 @@ | ||
| import warnings | ||
|
|
||
| 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.backends.base.operations import BaseDatabaseOperations | ||
|
|
||
| from .adapter import Adapter | ||
| from .utils import SpatialOperator | ||
|
|
||
|
|
||
| def _gis_within_operator(field, value, op=None, params=None): | ||
| print(f"Within value: {value}") | ||
| return { | ||
| field: { | ||
| "$geoWithin": { | ||
| "$geometry": { | ||
| "type": value["type"], | ||
| "coordinates": value["coordinates"], | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| def _gis_intersects_operator(field, value, op=None, params=None): | ||
| return { | ||
| field: { | ||
| "$geoIntersects": { | ||
| "$geometry": { | ||
| "type": value["type"], | ||
| "coordinates": value["coordinates"], | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| def _gis_disjoint_operator(field, value, op=None, params=None): | ||
| return { | ||
| field: { | ||
| "$not": { | ||
| "$geoIntersects": { | ||
| "$geometry": { | ||
| "type": value["type"], | ||
| "coordinates": value["coordinates"], | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| def _gis_contains_operator(field, value, op=None, params=None): | ||
| value_type = value["type"] | ||
| if value_type != "Point": | ||
| warnings.warn( | ||
| "MongoDB does not support strict contains on non-Point query geometries. Results will be for intersection." | ||
| ) | ||
timgraham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return { | ||
| field: { | ||
| "$geoIntersects": { | ||
| "$geometry": { | ||
| "type": value_type, | ||
| "coordinates": value["coordinates"], | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| class GISOperations(BaseSpatialOperations): | ||
|
|
||
| def _gis_distance_operator(field, value, op=None, params=None): | ||
| print(f"Distance: {params}") | ||
| if hasattr(params[0], "m"): | ||
| distance = params[0].m | ||
| else: | ||
| distance = 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 | ||
| ], | ||
| } | ||
| } | ||
| } | ||
| print(f"Command: {cmd}") | ||
| return cmd | ||
|
|
||
|
|
||
| class GISOperations(BaseSpatialOperations, BaseDatabaseOperations): | ||
| Adapter = Adapter | ||
|
|
||
| disallowed_aggregates = ( | ||
|
|
@@ -18,7 +117,16 @@ 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), | ||
| } | ||
|
|
||
| unsupported_functions = { | ||
| "Area", | ||
|
|
@@ -33,7 +141,6 @@ def gis_operators(self): | |
| "Centroid", | ||
| "ClosestPoint", | ||
| "Difference", | ||
| "Distance", | ||
timgraham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "Envelope", | ||
| "ForcePolygonCW", | ||
| "FromWKB", | ||
|
|
@@ -95,3 +202,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." | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| ) | ||
| dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection))) | ||
| else: | ||
| dist_param = value | ||
| return [dist_param] | ||
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please alphabetize about
supports_transform.