Skip to content
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

Added in Geospatial support in a similar fashion to GeoDjango #198

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ dist/
.tox

MANIFEST
.idea
9 changes: 8 additions & 1 deletion django_mongodb_engine/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import warnings

from django.conf import settings
from django.contrib.gis.db.backends.base import BaseSpatialOperations
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.signals import connection_created
from django.db.utils import DatabaseError
Expand Down Expand Up @@ -40,7 +41,7 @@ class DatabaseFeatures(NonrelDatabaseFeatures):
supports_long_model_names = False


class DatabaseOperations(NonrelDatabaseOperations):
class DatabaseOperations(NonrelDatabaseOperations, BaseSpatialOperations):
compiler_module = __name__.rsplit('.', 1)[0] + '.compiler'

def max_name_length(self):
Expand Down Expand Up @@ -153,6 +154,12 @@ def _value_from_db(self, value, field, field_kind, db_type):
return super(DatabaseOperations, self)._value_from_db(
value, field, field_kind, db_type)

def geometry_columns(self):
return None

def spatial_ref_sys(self):
return None


class DatabaseClient(NonrelDatabaseClient):
pass
Expand Down
10 changes: 10 additions & 0 deletions django_mongodb_engine/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.db.utils import DatabaseError, IntegrityError
from django.utils.encoding import smart_str
from django.utils.tree import Node
from django.contrib.gis.db.models.sql.compiler import GeoSQLCompiler as BaseGeoSQLCompiler

from pymongo import ASCENDING, DESCENDING
from pymongo.errors import PyMongoError, DuplicateKeyError
Expand Down Expand Up @@ -64,6 +65,11 @@ def get_selected_fields(query):

# Date OPs.
'year': lambda val: {'$gte': val[0], '$lt': val[1]},

# Spatial
'within': lambda val: val,
'intersects': lambda val: val,
'near': lambda val: val,
}

NEGATED_OPERATORS_MAP = {
Expand Down Expand Up @@ -444,3 +450,7 @@ def execute_update(self, update_spec, multi=True, **kwargs):

class SQLDeleteCompiler(NonrelDeleteCompiler, SQLCompiler):
pass


class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler):
pass
7 changes: 7 additions & 0 deletions django_mongodb_engine/contrib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import sys
from django.contrib.gis.db.models import GeoManager

from django.db import models, connections
from django.db.models.query import QuerySet
from django.db.models.sql.query import Query as SQLQuery
from django_mongodb_engine.query import MongoGeoQuerySet


ON_PYPY = hasattr(sys, 'pypy_version_info')
Expand Down Expand Up @@ -175,3 +177,8 @@ def distinct(self, *args, **kwargs):
database.
"""
return self.get_query_set().distinct(*args, **kwargs)


class GeoMongoDBManager(MongoDBManager, GeoManager):
def get_queryset(self):
return MongoGeoQuerySet(self.model, using=self._db)
167 changes: 167 additions & 0 deletions django_mongodb_engine/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import json
from django.contrib.gis import forms
from django.contrib.gis.db.models.proxy import GeometryProxy
from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.geos import Polygon, LineString, Point
from django.db import connections, models
from django.utils import six
from django.utils.translation import ugettext_lazy as _

from gridfs import GridFS
from gridfs.errors import NoFile
Expand Down Expand Up @@ -172,3 +179,163 @@ def _property_get(self, model):
[], ['^django_mongodb_engine\.fields\.GridFSString'])
except ImportError:
pass


class MongoGeometryProxy(GeometryProxy):
def __set__(self, obj, value):
if isinstance(value, dict):
value = json.dumps(value)
super(MongoGeometryProxy, self).__set__(obj, value)


class GeometryField(models.Field):
"""
The base GIS field -- maps to the OpenGIS Specification Geometry type.

Based loosely on django.contrib.gis.db.models.fields.GeometryField
"""

# The OpenGIS Geometry name.
geom_type = 'GEOMETRY'
form_class = forms.GeometryField

lookup_types = {
'within': {'operator': '$geoWithin', 'types': [Polygon]},
'intersects': {'operator': '$geoIntersects', 'types': [Point, LineString, Polygon]},
'near': {'operator': '$near', 'types': [Point]},
}

description = _("The base GIS field -- maps to the OpenGIS Specification Geometry type.")

def __init__(self, verbose_name=None, dim=2, **kwargs):
"""
The initialization function for geometry fields. Takes the following
as keyword arguments:

dim:
The number of dimensions for this geometry. Defaults to 2.
"""

# Mongo GeoJSON are WGS84
self.srid = 4326

# Setting the dimension of the geometry field.
self.dim = dim

# Setting the verbose_name keyword argument with the positional
# first parameter, so this works like normal fields.
kwargs['verbose_name'] = verbose_name

super(GeometryField, self).__init__(**kwargs)

def to_python(self, value):
if isinstance(value, Geometry):
return value
elif isinstance(value, dict):
return Geometry(json.dumps(value), self.srid)
elif isinstance(value, (bytes, six.string_types)):
return Geometry(value, self.srid)
raise ValueError('Could not convert to python geometry from value type "%s".' % type(value))

def get_prep_value(self, value):
if isinstance(value, Geometry):
return json.loads(value.json)
elif isinstance(value, six.string_types):
return json.loads(value)
raise ValueError('Could not prep geometry from value type "%s".' % type(value))

def get_srid(self, geom):
"""
Returns the default SRID for the given geometry, taking into account
the SRID set for the field. For example, if the input geometry
has no SRID, then that of the field will be returned.
"""
gsrid = geom.srid # SRID of given geometry.
if gsrid is None or self.srid == -1 or (gsrid == -1 and self.srid != -1):
return self.srid
else:
return gsrid

### Routines overloaded from Field ###
def contribute_to_class(self, cls, name, virtual_only=False):
super(GeometryField, self).contribute_to_class(cls, name, virtual_only)

# Setup for lazy-instantiated Geometry object.
setattr(cls, self.attname, MongoGeometryProxy(Geometry, self))

def db_type(self, connection):
return self.geom_type

def formfield(self, **kwargs):
defaults = {'form_class': self.form_class,
'geom_type': self.geom_type,
'srid': self.srid,
}
defaults.update(kwargs)
if (self.dim > 2 and not 'widget' in kwargs and
not getattr(defaults['form_class'].widget, 'supports_3d', False)):
defaults['widget'] = forms.Textarea
return super(GeometryField, self).formfield(**defaults)

def get_prep_lookup(self, lookup_type, value):
if lookup_type not in self.lookup_types:
raise ValueError('Unknown lookup type "%s".' % lookup_type)
lookup_info = self.lookup_types[lookup_type]
if not isinstance(value, Geometry):
raise ValueError('Geometry value is of unsupported type "%s".' % type(value))
if type(value) not in lookup_info['types']:
raise ValueError('"%s" lookup requires a value of geometry type(s) %s.' %
(lookup_type, ','.join([str(ltype) for ltype in lookup_info['types']])))
geom_query = {'$geometry': json.loads(value.json)}
# some queries may have additional query params; e.g.:
# $near optionally takes $minDistance and $maxDistance
if hasattr(value, 'extra_params'):
geom_query.update(value.extra_params)
return {lookup_info['operator']: geom_query}

def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
# this was already handled by get_prep_lookup...
return value


# The OpenGIS Geometry Type Fields
class PointField(GeometryField):
geom_type = 'POINT'
form_class = forms.PointField
description = _("Point")


class LineStringField(GeometryField):
geom_type = 'LINESTRING'
form_class = forms.LineStringField
description = _("Line string")


class PolygonField(GeometryField):
geom_type = 'POLYGON'
form_class = forms.PolygonField
description = _("Polygon")


class MultiPointField(GeometryField):
geom_type = 'MULTIPOINT'
form_class = forms.MultiPointField
description = _("Multi-point")


class MultiLineStringField(GeometryField):
geom_type = 'MULTILINESTRING'
form_class = forms.MultiLineStringField
description = _("Multi-line string")


class MultiPolygonField(GeometryField):
geom_type = 'MULTIPOLYGON'
form_class = forms.MultiPolygonField
description = _("Multi polygon")


class GeometryCollectionField(GeometryField):
geom_type = 'GEOMETRYCOLLECTION'
form_class = forms.GeometryCollectionField
description = _("Geometry collection")
14 changes: 14 additions & 0 deletions django_mongodb_engine/query.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from warnings import warn
from django.contrib.gis.db.models.query import GeoQuerySet
from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode

from djangotoolbox.fields import RawField, AbstractIterableField, \
EmbeddedModelField
Expand All @@ -24,3 +26,15 @@ def as_q(self, field):
else:
raise TypeError("Can not use A() queries on %s." %
field.__class__.__name__)


class MongoGeoQuery(GeoQuery):
def __init__(self, model, where=GeoWhereNode):
super(MongoGeoQuery, self).__init__(model, where)
self.query_terms |= set(['near'])


class MongoGeoQuerySet(GeoQuerySet):
def __init__(self, model=None, query=None, using=None):
super(MongoGeoQuerySet, self).__init__(model=model, query=query, using=using)
self.query = query or MongoGeoQuery(self.model)
Empty file added tests/gis/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions tests/gis/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.db import models
from django_mongodb_engine.contrib import GeoMongoDBManager
from django_mongodb_engine.fields import GeometryField


class GeometryModel(models.Model):
geom = GeometryField()

objects = GeoMongoDBManager()

class MongoMeta:
indexes = [{'fields': [('geom', '2dsphere')]}]
Loading