Fixed #1142 -- Added multiple database support.
This monster of a patch is the result of Alex Gaynor's 2009 Google Summer of Code project. Congratulations to Alex for a job well done. Big thanks also go to: * Justin Bronn for keeping GIS in line with the changes, * Karen Tracey and Jani Tiainen for their help testing Oracle support * Brett Hoerner, Jon Loyens, and Craig Kimmerer for their feedback. * Malcolm Treddinick for his guidance during the GSoC submission process. * Simon Willison for driving the original design process * Cal Henderson for complaining about ponies he wanted. ... and everyone else too numerous to mention that helped to bring this feature into fruition. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11952 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
7ef212af14
commit
ff60c5f9de
|
@ -131,6 +131,9 @@ DATABASE_HOST = '' # Set to empty string for localhost. Not used wit
|
|||
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
|
||||
DATABASE_OPTIONS = {} # Set to empty dictionary for default.
|
||||
|
||||
DATABASES = {
|
||||
}
|
||||
|
||||
# The email backend to use. For possible shortcuts see django.core.mail.
|
||||
# The default is to use the SMTP backend.
|
||||
# Third-party backends can be specified by providing a Python path
|
||||
|
|
|
@ -9,12 +9,16 @@ ADMINS = (
|
|||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
DATABASE_NAME = '' # Or path to database file if using sqlite3.
|
||||
DATABASE_USER = '' # Not used with sqlite3.
|
||||
DATABASE_PASSWORD = '' # Not used with sqlite3.
|
||||
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
|
||||
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': '', # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
|
|
|
@ -141,8 +141,9 @@ class BaseModelAdmin(object):
|
|||
"""
|
||||
Get a form Field for a ForeignKey.
|
||||
"""
|
||||
db = kwargs.get('using')
|
||||
if db_field.name in self.raw_id_fields:
|
||||
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
|
||||
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel, using=db)
|
||||
elif db_field.name in self.radio_fields:
|
||||
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
|
||||
'class': get_ul_class(self.radio_fields[db_field.name]),
|
||||
|
@ -159,9 +160,10 @@ class BaseModelAdmin(object):
|
|||
# a field in admin.
|
||||
if not db_field.rel.through._meta.auto_created:
|
||||
return None
|
||||
db = kwargs.get('using')
|
||||
|
||||
if db_field.name in self.raw_id_fields:
|
||||
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
|
||||
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
|
||||
kwargs['help_text'] = ''
|
||||
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
|
||||
kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
|
||||
|
@ -739,7 +741,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
form_validated = False
|
||||
new_object = self.model()
|
||||
prefixes = {}
|
||||
for FormSet in self.get_formsets(request):
|
||||
for FormSet, inline in zip(self.get_formsets(request), self.inline_instances):
|
||||
prefix = FormSet.get_default_prefix()
|
||||
prefixes[prefix] = prefixes.get(prefix, 0) + 1
|
||||
if prefixes[prefix] != 1:
|
||||
|
@ -747,7 +749,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
formset = FormSet(data=request.POST, files=request.FILES,
|
||||
instance=new_object,
|
||||
save_as_new=request.POST.has_key("_saveasnew"),
|
||||
prefix=prefix)
|
||||
prefix=prefix, queryset=inline.queryset(request))
|
||||
formsets.append(formset)
|
||||
if all_valid(formsets) and form_validated:
|
||||
self.save_model(request, new_object, form, change=False)
|
||||
|
@ -770,12 +772,14 @@ class ModelAdmin(BaseModelAdmin):
|
|||
initial[k] = initial[k].split(",")
|
||||
form = ModelForm(initial=initial)
|
||||
prefixes = {}
|
||||
for FormSet in self.get_formsets(request):
|
||||
for FormSet, inline in zip(self.get_formsets(request),
|
||||
self.inline_instances):
|
||||
prefix = FormSet.get_default_prefix()
|
||||
prefixes[prefix] = prefixes.get(prefix, 0) + 1
|
||||
if prefixes[prefix] != 1:
|
||||
prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||||
formset = FormSet(instance=self.model(), prefix=prefix)
|
||||
formset = FormSet(instance=self.model(), prefix=prefix,
|
||||
queryset=inline.queryset(request))
|
||||
formsets.append(formset)
|
||||
|
||||
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
|
||||
|
@ -837,13 +841,16 @@ class ModelAdmin(BaseModelAdmin):
|
|||
form_validated = False
|
||||
new_object = obj
|
||||
prefixes = {}
|
||||
for FormSet in self.get_formsets(request, new_object):
|
||||
for FormSet, inline in zip(self.get_formsets(request, new_object),
|
||||
self.inline_instances):
|
||||
prefix = FormSet.get_default_prefix()
|
||||
prefixes[prefix] = prefixes.get(prefix, 0) + 1
|
||||
if prefixes[prefix] != 1:
|
||||
prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||||
formset = FormSet(request.POST, request.FILES,
|
||||
instance=new_object, prefix=prefix)
|
||||
instance=new_object, prefix=prefix,
|
||||
queryset=inline.queryset(request))
|
||||
|
||||
formsets.append(formset)
|
||||
|
||||
if all_valid(formsets) and form_validated:
|
||||
|
@ -859,12 +866,13 @@ class ModelAdmin(BaseModelAdmin):
|
|||
else:
|
||||
form = ModelForm(instance=obj)
|
||||
prefixes = {}
|
||||
for FormSet in self.get_formsets(request, obj):
|
||||
for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):
|
||||
prefix = FormSet.get_default_prefix()
|
||||
prefixes[prefix] = prefixes.get(prefix, 0) + 1
|
||||
if prefixes[prefix] != 1:
|
||||
prefix = "%s-%s" % (prefix, prefixes[prefix])
|
||||
formset = FormSet(instance=obj, prefix=prefix)
|
||||
formset = FormSet(instance=obj, prefix=prefix,
|
||||
queryset=inline.queryset(request))
|
||||
formsets.append(formset)
|
||||
|
||||
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
|
||||
|
@ -1187,6 +1195,9 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||
form = self.get_formset(request).form
|
||||
return [(None, {'fields': form.base_fields.keys()})]
|
||||
|
||||
def queryset(self, request):
|
||||
return self.model._default_manager.all()
|
||||
|
||||
class StackedInline(InlineModelAdmin):
|
||||
template = 'admin/edit_inline/stacked.html'
|
||||
|
||||
|
|
|
@ -102,8 +102,9 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
|||
A Widget for displaying ForeignKeys in the "raw_id" interface rather than
|
||||
in a <select> box.
|
||||
"""
|
||||
def __init__(self, rel, attrs=None):
|
||||
def __init__(self, rel, attrs=None, using=None):
|
||||
self.rel = rel
|
||||
self.db = using
|
||||
super(ForeignKeyRawIdWidget, self).__init__(attrs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
|
@ -148,7 +149,7 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
|||
|
||||
def label_for_value(self, value):
|
||||
key = self.rel.get_related_field().name
|
||||
obj = self.rel.to._default_manager.get(**{key: value})
|
||||
obj = self.rel.to._default_manager.using(self.db).get(**{key: value})
|
||||
return ' <strong>%s</strong>' % escape(truncate_words(obj, 14))
|
||||
|
||||
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
||||
|
@ -156,8 +157,8 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
|||
A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
|
||||
in a <select multiple> box.
|
||||
"""
|
||||
def __init__(self, rel, attrs=None):
|
||||
super(ManyToManyRawIdWidget, self).__init__(rel, attrs)
|
||||
def __init__(self, rel, attrs=None, using=None):
|
||||
super(ManyToManyRawIdWidget, self).__init__(rel, attrs, using=None)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
attrs['class'] = 'vManyToManyRawIdAdminField'
|
||||
|
|
|
@ -3,19 +3,15 @@ import urllib
|
|||
|
||||
from django.contrib import auth
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.db import models, DEFAULT_DB_ALIAS
|
||||
from django.db.models.manager import EmptyManager
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.hashcompat import md5_constructor, sha_constructor
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
UNUSABLE_PASSWORD = '!' # This will never be a valid hash
|
||||
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set # Python 2.3 fallback
|
||||
UNUSABLE_PASSWORD = '!' # This will never be a valid hash
|
||||
|
||||
def get_hexdigest(algorithm, salt, raw_password):
|
||||
"""
|
||||
|
@ -114,7 +110,7 @@ class UserManager(models.Manager):
|
|||
user.set_password(password)
|
||||
else:
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
user.save(using=self.db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username, email, password):
|
||||
|
@ -122,7 +118,7 @@ class UserManager(models.Manager):
|
|||
u.is_staff = True
|
||||
u.is_active = True
|
||||
u.is_superuser = True
|
||||
u.save()
|
||||
u.save(using=self.db)
|
||||
return u
|
||||
|
||||
def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'):
|
||||
|
@ -319,7 +315,7 @@ class User(models.Model):
|
|||
try:
|
||||
app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.')
|
||||
model = models.get_model(app_label, model_name)
|
||||
self._profile_cache = model._default_manager.get(user__id__exact=self.id)
|
||||
self._profile_cache = model._default_manager.using(self._state.db).get(user__id__exact=self.id)
|
||||
self._profile_cache.user = self
|
||||
except (ImportError, ImproperlyConfigured):
|
||||
raise SiteProfileNotAvailable
|
||||
|
|
|
@ -28,7 +28,7 @@ class CommentSecurityForm(forms.Form):
|
|||
initial = {}
|
||||
initial.update(self.generate_security_data())
|
||||
super(CommentSecurityForm, self).__init__(data=data, initial=initial)
|
||||
|
||||
|
||||
def security_errors(self):
|
||||
"""Return just those errors associated with security"""
|
||||
errors = ErrorDict()
|
||||
|
@ -107,13 +107,13 @@ class CommentDetailsForm(CommentSecurityForm):
|
|||
"""
|
||||
if not self.is_valid():
|
||||
raise ValueError("get_comment_object may only be called on valid forms")
|
||||
|
||||
|
||||
CommentModel = self.get_comment_model()
|
||||
new = CommentModel(**self.get_comment_create_data())
|
||||
new = self.check_for_duplicate_comment(new)
|
||||
|
||||
|
||||
return new
|
||||
|
||||
|
||||
def get_comment_model(self):
|
||||
"""
|
||||
Get the comment model to create with this form. Subclasses in custom
|
||||
|
@ -121,7 +121,7 @@ class CommentDetailsForm(CommentSecurityForm):
|
|||
check_for_duplicate_comment to provide custom comment models.
|
||||
"""
|
||||
return Comment
|
||||
|
||||
|
||||
def get_comment_create_data(self):
|
||||
"""
|
||||
Returns the dict of data to be used to create a comment. Subclasses in
|
||||
|
@ -140,13 +140,15 @@ class CommentDetailsForm(CommentSecurityForm):
|
|||
is_public = True,
|
||||
is_removed = False,
|
||||
)
|
||||
|
||||
|
||||
def check_for_duplicate_comment(self, new):
|
||||
"""
|
||||
Check that a submitted comment isn't a duplicate. This might be caused
|
||||
by someone posting a comment twice. If it is a dup, silently return the *previous* comment.
|
||||
"""
|
||||
possible_duplicates = self.get_comment_model()._default_manager.filter(
|
||||
possible_duplicates = self.get_comment_model()._default_manager.using(
|
||||
self.target_object._state.db
|
||||
).filter(
|
||||
content_type = new.content_type,
|
||||
object_pk = new.object_pk,
|
||||
user_name = new.user_name,
|
||||
|
@ -156,7 +158,7 @@ class CommentDetailsForm(CommentSecurityForm):
|
|||
for old in possible_duplicates:
|
||||
if old.submit_date.date() == new.submit_date.date() and old.comment == new.comment:
|
||||
return old
|
||||
|
||||
|
||||
return new
|
||||
|
||||
def clean_comment(self):
|
||||
|
|
|
@ -79,10 +79,10 @@ class Comment(BaseCommentAbstractModel):
|
|||
def __unicode__(self):
|
||||
return "%s: %s..." % (self.name, self.comment[:50])
|
||||
|
||||
def save(self, force_insert=False, force_update=False):
|
||||
def save(self, *args, **kwargs):
|
||||
if self.submit_date is None:
|
||||
self.submit_date = datetime.datetime.now()
|
||||
super(Comment, self).save(force_insert, force_update)
|
||||
super(Comment, self).save(*args, **kwargs)
|
||||
|
||||
def _get_userinfo(self):
|
||||
"""
|
||||
|
@ -185,7 +185,7 @@ class CommentFlag(models.Model):
|
|||
return "%s flag of comment ID %s by %s" % \
|
||||
(self.flag, self.comment_id, self.user.username)
|
||||
|
||||
def save(self, force_insert=False, force_update=False):
|
||||
def save(self, *args, **kwargs):
|
||||
if self.flag_date is None:
|
||||
self.flag_date = datetime.datetime.now()
|
||||
super(CommentFlag, self).save(force_insert, force_update)
|
||||
super(CommentFlag, self).save(*args, **kwargs)
|
||||
|
|
|
@ -25,7 +25,7 @@ class CommentPostBadRequest(http.HttpResponseBadRequest):
|
|||
|
||||
@csrf_protect
|
||||
@require_POST
|
||||
def post_comment(request, next=None):
|
||||
def post_comment(request, next=None, using=None):
|
||||
"""
|
||||
Post a comment.
|
||||
|
||||
|
@ -50,7 +50,7 @@ def post_comment(request, next=None):
|
|||
return CommentPostBadRequest("Missing content_type or object_pk field.")
|
||||
try:
|
||||
model = models.get_model(*ctype.split(".", 1))
|
||||
target = model._default_manager.get(pk=object_pk)
|
||||
target = model._default_manager.using(using).get(pk=object_pk)
|
||||
except TypeError:
|
||||
return CommentPostBadRequest(
|
||||
"Invalid content_type value: %r" % escape(ctype))
|
||||
|
|
|
@ -5,7 +5,7 @@ Classes allowing "generic" relations through ContentType and object-id fields.
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import connection
|
||||
from django.db.models import signals
|
||||
from django.db import models
|
||||
from django.db import models, DEFAULT_DB_ALIAS
|
||||
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
|
||||
from django.db.models.loading import get_model
|
||||
from django.forms import ModelForm
|
||||
|
@ -45,14 +45,14 @@ class GenericForeignKey(object):
|
|||
kwargs[self.ct_field] = self.get_content_type(obj=value)
|
||||
kwargs[self.fk_field] = value._get_pk_val()
|
||||
|
||||
def get_content_type(self, obj=None, id=None):
|
||||
def get_content_type(self, obj=None, id=None, using=None):
|
||||
# Convenience function using get_model avoids a circular import when
|
||||
# using this model
|
||||
ContentType = get_model("contenttypes", "contenttype")
|
||||
if obj:
|
||||
return ContentType.objects.get_for_model(obj)
|
||||
return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
|
||||
elif id:
|
||||
return ContentType.objects.get_for_id(id)
|
||||
return ContentType.objects.db_manager(using).get_for_id(id)
|
||||
else:
|
||||
# This should never happen. I love comments like this, don't you?
|
||||
raise Exception("Impossible arguments to GFK.get_content_type!")
|
||||
|
@ -73,7 +73,7 @@ class GenericForeignKey(object):
|
|||
f = self.model._meta.get_field(self.ct_field)
|
||||
ct_id = getattr(instance, f.get_attname(), None)
|
||||
if ct_id:
|
||||
ct = self.get_content_type(id=ct_id)
|
||||
ct = self.get_content_type(id=ct_id, using=instance._state.db)
|
||||
try:
|
||||
rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
|
||||
except ObjectDoesNotExist:
|
||||
|
@ -149,7 +149,7 @@ class GenericRelation(RelatedField, Field):
|
|||
def get_internal_type(self):
|
||||
return "ManyToManyField"
|
||||
|
||||
def db_type(self):
|
||||
def db_type(self, connection):
|
||||
# Since we're simulating a ManyToManyField, in effect, best return the
|
||||
# same db_type as well.
|
||||
return None
|
||||
|
@ -201,7 +201,7 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||
join_table = qn(self.field.m2m_db_table()),
|
||||
source_col_name = qn(self.field.m2m_column_name()),
|
||||
target_col_name = qn(self.field.m2m_reverse_name()),
|
||||
content_type = ContentType.objects.get_for_model(instance),
|
||||
content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance),
|
||||
content_type_field_name = self.field.content_type_field_name,
|
||||
object_id_field_name = self.field.object_id_field_name
|
||||
)
|
||||
|
@ -247,7 +247,7 @@ def create_generic_related_manager(superclass):
|
|||
'%s__pk' % self.content_type_field_name : self.content_type.id,
|
||||
'%s__exact' % self.object_id_field_name : self.pk_val,
|
||||
}
|
||||
return superclass.get_query_set(self).filter(**query)
|
||||
return superclass.get_query_set(self).using(self.instance._state.db).filter(**query)
|
||||
|
||||
def add(self, *objs):
|
||||
for obj in objs:
|
||||
|
@ -255,17 +255,17 @@ def create_generic_related_manager(superclass):
|
|||
raise TypeError, "'%s' instance expected" % self.model._meta.object_name
|
||||
setattr(obj, self.content_type_field_name, self.content_type)
|
||||
setattr(obj, self.object_id_field_name, self.pk_val)
|
||||
obj.save()
|
||||
obj.save(using=self.instance._state.db)
|
||||
add.alters_data = True
|
||||
|
||||
def remove(self, *objs):
|
||||
for obj in objs:
|
||||
obj.delete()
|
||||
obj.delete(using=self.instance._state.db)
|
||||
remove.alters_data = True
|
||||
|
||||
def clear(self):
|
||||
for obj in self.all():
|
||||
obj.delete()
|
||||
obj.delete(using=self.instance._state.db)
|
||||
clear.alters_data = True
|
||||
|
||||
def create(self, **kwargs):
|
||||
|
|
|
@ -7,21 +7,22 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
|
|||
Creates content types for models in the given app, removing any model
|
||||
entries that no longer have a matching model class.
|
||||
"""
|
||||
db = kwargs['db']
|
||||
ContentType.objects.clear_cache()
|
||||
content_types = list(ContentType.objects.filter(app_label=app.__name__.split('.')[-2]))
|
||||
content_types = list(ContentType.objects.using(db).filter(app_label=app.__name__.split('.')[-2]))
|
||||
app_models = get_models(app)
|
||||
if not app_models:
|
||||
return
|
||||
for klass in app_models:
|
||||
opts = klass._meta
|
||||
try:
|
||||
ct = ContentType.objects.get(app_label=opts.app_label,
|
||||
model=opts.object_name.lower())
|
||||
ct = ContentType.objects.using(db).get(app_label=opts.app_label,
|
||||
model=opts.object_name.lower())
|
||||
content_types.remove(ct)
|
||||
except ContentType.DoesNotExist:
|
||||
ct = ContentType(name=smart_unicode(opts.verbose_name_raw),
|
||||
app_label=opts.app_label, model=opts.object_name.lower())
|
||||
ct.save()
|
||||
ct.save(using=db)
|
||||
if verbosity >= 2:
|
||||
print "Adding content type '%s | %s'" % (ct.app_label, ct.model)
|
||||
# The presence of any remaining content types means the supplied app has an
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.db import models
|
||||
from django.db import models, DEFAULT_DB_ALIAS
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import smart_unicode
|
||||
|
||||
|
@ -10,7 +10,7 @@ class ContentTypeManager(models.Manager):
|
|||
|
||||
def get_by_natural_key(self, app_label, model):
|
||||
try:
|
||||
ct = self.__class__._cache[(app_label, model)]
|
||||
ct = self.__class__._cache[self.db][(app_label, model)]
|
||||
except KeyError:
|
||||
ct = self.get(app_label=app_label, model=model)
|
||||
return ct
|
||||
|
@ -27,7 +27,7 @@ class ContentTypeManager(models.Manager):
|
|||
opts = model._meta
|
||||
key = (opts.app_label, opts.object_name.lower())
|
||||
try:
|
||||
ct = self.__class__._cache[key]
|
||||
ct = self.__class__._cache[self.db][key]
|
||||
except KeyError:
|
||||
# Load or create the ContentType entry. The smart_unicode() is
|
||||
# needed around opts.verbose_name_raw because name_raw might be a
|
||||
|
@ -37,7 +37,7 @@ class ContentTypeManager(models.Manager):
|
|||
model = opts.object_name.lower(),
|
||||
defaults = {'name': smart_unicode(opts.verbose_name_raw)},
|
||||
)
|
||||
self._add_to_cache(ct)
|
||||
self._add_to_cache(self.db, ct)
|
||||
|
||||
return ct
|
||||
|
||||
|
@ -47,12 +47,12 @@ class ContentTypeManager(models.Manager):
|
|||
(though ContentTypes are obviously not created on-the-fly by get_by_id).
|
||||
"""
|
||||
try:
|
||||
ct = self.__class__._cache[id]
|
||||
ct = self.__class__._cache[self.db][id]
|
||||
except KeyError:
|
||||
# This could raise a DoesNotExist; that's correct behavior and will
|
||||
# make sure that only correct ctypes get stored in the cache dict.
|
||||
ct = self.get(pk=id)
|
||||
self._add_to_cache(ct)
|
||||
self._add_to_cache(self.db, ct)
|
||||
return ct
|
||||
|
||||
def clear_cache(self):
|
||||
|
@ -64,12 +64,12 @@ class ContentTypeManager(models.Manager):
|
|||
"""
|
||||
self.__class__._cache.clear()
|
||||
|
||||
def _add_to_cache(self, ct):
|
||||
def _add_to_cache(self, using, ct):
|
||||
"""Insert a ContentType into the cache."""
|
||||
model = ct.model_class()
|
||||
key = (model._meta.app_label, model._meta.object_name.lower())
|
||||
self.__class__._cache[key] = ct
|
||||
self.__class__._cache[ct.id] = ct
|
||||
self.__class__._cache.setdefault(using, {})[key] = ct
|
||||
self.__class__._cache.setdefault(using, {})[ct.id] = ct
|
||||
|
||||
class ContentType(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
@ -99,7 +99,7 @@ class ContentType(models.Model):
|
|||
method. The ObjectNotExist exception, if thrown, will not be caught,
|
||||
so code that calls this method should catch it.
|
||||
"""
|
||||
return self.model_class()._default_manager.get(**kwargs)
|
||||
return self.model_class()._default_manager.using(self._state.db).get(**kwargs)
|
||||
|
||||
def natural_key(self):
|
||||
return (self.app_label, self.model)
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
"""
|
||||
This module provides the backend for spatial SQL construction with Django.
|
||||
|
||||
Specifically, this module will import the correct routines and modules
|
||||
needed for GeoDjango to interface with the spatial database.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
|
||||
# Retrieving the necessary settings from the backend.
|
||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||
from django.contrib.gis.db.backend.postgis import create_test_spatial_db, get_geo_where_clause, SpatialBackend
|
||||
elif settings.DATABASE_ENGINE == 'oracle':
|
||||
from django.contrib.gis.db.backend.oracle import create_test_spatial_db, get_geo_where_clause, SpatialBackend
|
||||
elif settings.DATABASE_ENGINE == 'mysql':
|
||||
from django.contrib.gis.db.backend.mysql import create_test_spatial_db, get_geo_where_clause, SpatialBackend
|
||||
elif settings.DATABASE_ENGINE == 'sqlite3':
|
||||
from django.contrib.gis.db.backend.spatialite import create_test_spatial_db, get_geo_where_clause, SpatialBackend
|
||||
else:
|
||||
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
|
@ -1,26 +0,0 @@
|
|||
"""
|
||||
This module holds the base `SpatialBackend` object, which is
|
||||
instantiated by each spatial backend with the features it has.
|
||||
"""
|
||||
# TODO: Create a `Geometry` protocol and allow user to use
|
||||
# different Geometry objects -- for now we just use GEOSGeometry.
|
||||
from django.contrib.gis.geos import GEOSGeometry, GEOSException
|
||||
|
||||
class BaseSpatialBackend(object):
|
||||
Geometry = GEOSGeometry
|
||||
GeometryException = GEOSException
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs.setdefault('distance_functions', {})
|
||||
kwargs.setdefault('limited_where', {})
|
||||
for k, v in kwargs.iteritems(): setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
All attributes of the spatial backend return False by default.
|
||||
"""
|
||||
try:
|
||||
return self.__dict__[name]
|
||||
except KeyError:
|
||||
return False
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||
|
||||
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
|
||||
from django.contrib.gis.db.backend.mysql.creation import create_test_spatial_db
|
||||
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField
|
||||
from django.contrib.gis.db.backend.mysql.query import *
|
||||
|
||||
SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True,
|
||||
gis_terms=MYSQL_GIS_TERMS,
|
||||
select=GEOM_SELECT,
|
||||
Adaptor=WKTAdaptor,
|
||||
Field=MySQLGeoField)
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
def create_test_spatial_db(verbosity=1, autoclobber=False):
|
||||
"A wrapper over the MySQL `create_test_db` method."
|
||||
from django.db import connection
|
||||
connection.creation.create_test_db(verbosity, autoclobber)
|
|
@ -1,53 +0,0 @@
|
|||
from django.db import connection
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
|
||||
|
||||
# Quotename & geographic quotename, respectively.
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
class MySQLGeoField(Field):
|
||||
"""
|
||||
The backend-specific geographic field for MySQL.
|
||||
"""
|
||||
|
||||
def _geom_index(self, style, db_table):
|
||||
"""
|
||||
Creates a spatial index for the geometry column. If MyISAM tables are
|
||||
used an R-Tree index is created, otherwise a B-Tree index is created.
|
||||
Thus, for best spatial performance, you should use MyISAM tables
|
||||
(which do not support transactions). For more information, see Ch.
|
||||
16.6.1 of the MySQL 5.0 documentation.
|
||||
"""
|
||||
|
||||
# Getting the index name.
|
||||
idx_name = '%s_%s_id' % (db_table, self.column)
|
||||
|
||||
sql = (style.SQL_KEYWORD('CREATE SPATIAL INDEX ') +
|
||||
style.SQL_TABLE(qn(idx_name)) +
|
||||
style.SQL_KEYWORD(' ON ') +
|
||||
style.SQL_TABLE(qn(db_table)) + '(' +
|
||||
style.SQL_FIELD(qn(self.column)) + ');')
|
||||
return sql
|
||||
|
||||
def post_create_sql(self, style, db_table):
|
||||
"""
|
||||
Returns SQL that will be executed after the model has been
|
||||
created.
|
||||
"""
|
||||
# Getting the geometric index for this Geometry column.
|
||||
if self.spatial_index:
|
||||
return (self._geom_index(style, db_table),)
|
||||
else:
|
||||
return ()
|
||||
|
||||
def db_type(self):
|
||||
"The OpenGIS name is returned for the MySQL database column type."
|
||||
return self.geom_type
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
The placeholder here has to include MySQL's WKT constructor. Because
|
||||
MySQL does not support spatial transformations, there is no need to
|
||||
modify the placeholder based on the contents of the given value.
|
||||
"""
|
||||
return '%s(%%s)' % GEOM_FROM_TEXT
|
|
@ -1,59 +0,0 @@
|
|||
"""
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for MySQL.
|
||||
|
||||
Please note that MySQL only supports bounding box queries, also
|
||||
known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
|
||||
indices may only be used on MyISAM tables -- if you need
|
||||
transactions, take a look at PostGIS.
|
||||
"""
|
||||
from django.db import connection
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
# To ease implementation, WKT is passed to/from MySQL.
|
||||
GEOM_FROM_TEXT = 'GeomFromText'
|
||||
GEOM_FROM_WKB = 'GeomFromWKB'
|
||||
GEOM_SELECT = 'AsText(%s)'
|
||||
|
||||
# WARNING: MySQL is NOT compliant w/the OpenGIS specification and
|
||||
# _every_ one of these lookup types is on the _bounding box_ only.
|
||||
MYSQL_GIS_FUNCTIONS = {
|
||||
'bbcontains' : 'MBRContains', # For consistency w/PostGIS API
|
||||
'bboverlaps' : 'MBROverlaps', # .. ..
|
||||
'contained' : 'MBRWithin', # .. ..
|
||||
'contains' : 'MBRContains',
|
||||
'disjoint' : 'MBRDisjoint',
|
||||
'equals' : 'MBREqual',
|
||||
'exact' : 'MBREqual',
|
||||
'intersects' : 'MBRIntersects',
|
||||
'overlaps' : 'MBROverlaps',
|
||||
'same_as' : 'MBREqual',
|
||||
'touches' : 'MBRTouches',
|
||||
'within' : 'MBRWithin',
|
||||
}
|
||||
|
||||
# This lookup type does not require a mapping.
|
||||
MISC_TERMS = ['isnull']
|
||||
|
||||
# Assacceptable lookup types for Oracle spatial.
|
||||
MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
|
||||
MYSQL_GIS_TERMS += MISC_TERMS
|
||||
MYSQL_GIS_TERMS = dict((term, None) for term in MYSQL_GIS_TERMS) # Making dictionary
|
||||
|
||||
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
|
||||
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
|
||||
# Getting the quoted field as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_alias), qn(name))
|
||||
|
||||
# See if a MySQL Geometry function matches the lookup type next
|
||||
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
|
||||
if lookup_info:
|
||||
return "%s(%s, %%s)" % (lookup_info, geo_col)
|
||||
|
||||
# Handling 'isnull' lookup type
|
||||
# TODO: Is this needed because MySQL cannot handle NULL
|
||||
# geometries in its spatial indices.
|
||||
if lookup_type == 'isnull':
|
||||
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
|
@ -1,35 +0,0 @@
|
|||
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||
|
||||
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
|
||||
from django.contrib.gis.db.backend.oracle.creation import create_test_spatial_db
|
||||
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
|
||||
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
|
||||
from django.contrib.gis.db.backend.oracle.query import *
|
||||
|
||||
SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
|
||||
area=AREA,
|
||||
centroid=CENTROID,
|
||||
difference=DIFFERENCE,
|
||||
distance=DISTANCE,
|
||||
distance_functions=DISTANCE_FUNCTIONS,
|
||||
extent=EXTENT,
|
||||
gis_terms=ORACLE_SPATIAL_TERMS,
|
||||
gml=ASGML,
|
||||
intersection=INTERSECTION,
|
||||
length=LENGTH,
|
||||
limited_where = {'relate' : None},
|
||||
num_geom=NUM_GEOM,
|
||||
num_points=NUM_POINTS,
|
||||
perimeter=LENGTH,
|
||||
point_on_surface=POINT_ON_SURFACE,
|
||||
select=GEOM_SELECT,
|
||||
sym_difference=SYM_DIFFERENCE,
|
||||
transform=TRANSFORM,
|
||||
unionagg=UNIONAGG,
|
||||
union=UNION,
|
||||
Adaptor=OracleSpatialAdaptor,
|
||||
Field=OracleSpatialField,
|
||||
GeometryColumns=GeometryColumns,
|
||||
SpatialRefSys=SpatialRefSys,
|
||||
)
|
|
@ -1,5 +0,0 @@
|
|||
from cx_Oracle import CLOB
|
||||
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
|
||||
|
||||
class OracleSpatialAdaptor(WKTAdaptor):
|
||||
input_size = CLOB
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
def create_test_spatial_db(verbosity=1, autoclobber=False):
|
||||
"A wrapper over the Oracle `create_test_db` routine."
|
||||
from django.db import connection
|
||||
connection.creation.create_test_db(verbosity, autoclobber)
|
|
@ -1,102 +0,0 @@
|
|||
from django.db import connection
|
||||
from django.db.backends.util import truncate_name
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
from django.contrib.gis.db.backend.oracle.query import TRANSFORM
|
||||
|
||||
# Quotename & geographic quotename, respectively.
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
class OracleSpatialField(Field):
|
||||
"""
|
||||
The backend-specific geographic field for Oracle Spatial.
|
||||
"""
|
||||
|
||||
empty_strings_allowed = False
|
||||
|
||||
def __init__(self, extent=(-180.0, -90.0, 180.0, 90.0), tolerance=0.05, **kwargs):
|
||||
"""
|
||||
Oracle Spatial backend needs to have the extent -- for projected coordinate
|
||||
systems _you must define the extent manually_, since the coordinates are
|
||||
for geodetic systems. The `tolerance` keyword specifies the tolerance
|
||||
for error (in meters), and defaults to 0.05 (5 centimeters).
|
||||
"""
|
||||
# Oracle Spatial specific keyword arguments.
|
||||
self._extent = extent
|
||||
self._tolerance = tolerance
|
||||
# Calling the Django field initialization.
|
||||
super(OracleSpatialField, self).__init__(**kwargs)
|
||||
|
||||
def _add_geom(self, style, db_table):
|
||||
"""
|
||||
Adds this geometry column into the Oracle USER_SDO_GEOM_METADATA
|
||||
table.
|
||||
"""
|
||||
# Checking the dimensions.
|
||||
# TODO: Add support for 3D geometries.
|
||||
if self.dim != 2:
|
||||
raise Exception('3D geometries not yet supported on Oracle Spatial backend.')
|
||||
|
||||
# Constructing the SQL that will be used to insert information about
|
||||
# the geometry column into the USER_GSDO_GEOM_METADATA table.
|
||||
meta_sql = (style.SQL_KEYWORD('INSERT INTO ') +
|
||||
style.SQL_TABLE('USER_SDO_GEOM_METADATA') +
|
||||
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) +
|
||||
style.SQL_KEYWORD(' VALUES ') + '(\n ' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ',\n ' +
|
||||
style.SQL_FIELD(gqn(self.column)) + ',\n ' +
|
||||
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' +
|
||||
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
|
||||
("('LONG', %s, %s, %s),\n " % (self._extent[0], self._extent[2], self._tolerance)) +
|
||||
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
|
||||
("('LAT', %s, %s, %s)\n ),\n" % (self._extent[1], self._extent[3], self._tolerance)) +
|
||||
' %s\n );' % self.srid)
|
||||
return meta_sql
|
||||
|
||||
def _geom_index(self, style, db_table):
|
||||
"Creates an Oracle Geometry index (R-tree) for this geometry field."
|
||||
|
||||
# Getting the index name, Oracle doesn't allow object
|
||||
# names > 30 characters.
|
||||
idx_name = truncate_name('%s_%s_id' % (db_table, self.column), 30)
|
||||
|
||||
sql = (style.SQL_KEYWORD('CREATE INDEX ') +
|
||||
style.SQL_TABLE(qn(idx_name)) +
|
||||
style.SQL_KEYWORD(' ON ') +
|
||||
style.SQL_TABLE(qn(db_table)) + '(' +
|
||||
style.SQL_FIELD(qn(self.column)) + ') ' +
|
||||
style.SQL_KEYWORD('INDEXTYPE IS ') +
|
||||
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';')
|
||||
return sql
|
||||
|
||||
def post_create_sql(self, style, db_table):
|
||||
"""
|
||||
Returns SQL that will be executed after the model has been
|
||||
created.
|
||||
"""
|
||||
# Getting the meta geometry information.
|
||||
post_sql = self._add_geom(style, db_table)
|
||||
|
||||
# Getting the geometric index for this Geometry column.
|
||||
if self.spatial_index:
|
||||
return (post_sql, self._geom_index(style, db_table))
|
||||
else:
|
||||
return (post_sql,)
|
||||
|
||||
def db_type(self):
|
||||
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
|
||||
return 'MDSYS.SDO_GEOMETRY'
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
SDO_CS.TRANSFORM() function call.
|
||||
"""
|
||||
if value is None:
|
||||
return 'NULL'
|
||||
elif value.srid != self.srid:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self.srid)
|
||||
else:
|
||||
return 'SDO_GEOMETRY(%%s, %s)' % self.srid
|
|
@ -1,154 +0,0 @@
|
|||
"""
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for Oracle Spatial.
|
||||
|
||||
Please note that WKT support is broken on the XE version, and thus
|
||||
this backend will not work on such platforms. Specifically, XE lacks
|
||||
support for an internal JVM, and Java libraries are required to use
|
||||
the WKT constructors.
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from django.db import connection
|
||||
from django.contrib.gis.db.backend.util import SpatialFunction
|
||||
from django.contrib.gis.measure import Distance
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
# The GML, distance, transform, and union procedures.
|
||||
AREA = 'SDO_GEOM.SDO_AREA'
|
||||
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
|
||||
CENTROID = 'SDO_GEOM.SDO_CENTROID'
|
||||
DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE'
|
||||
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
|
||||
EXTENT = 'SDO_AGGR_MBR'
|
||||
INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION'
|
||||
LENGTH = 'SDO_GEOM.SDO_LENGTH'
|
||||
NUM_GEOM = 'SDO_UTIL.GETNUMELEM'
|
||||
NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES'
|
||||
POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE'
|
||||
SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR'
|
||||
TRANSFORM = 'SDO_CS.TRANSFORM'
|
||||
UNION = 'SDO_GEOM.SDO_UNION'
|
||||
UNIONAGG = 'SDO_AGGR_UNION'
|
||||
|
||||
# We want to get SDO Geometries as WKT because it is much easier to
|
||||
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
||||
# However, this adversely affects performance (i.e., Java is called
|
||||
# to convert to WKT on every query). If someone wishes to write a
|
||||
# SDO_GEOMETRY(...) parser in Python, let me know =)
|
||||
GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
||||
|
||||
#### Classes used in constructing Oracle spatial SQL ####
|
||||
class SDOOperation(SpatialFunction):
|
||||
"Base class for SDO* Oracle operations."
|
||||
def __init__(self, func, **kwargs):
|
||||
kwargs.setdefault('operator', '=')
|
||||
kwargs.setdefault('result', 'TRUE')
|
||||
kwargs.setdefault('end_subst', ") %s '%s'")
|
||||
super(SDOOperation, self).__init__(func, **kwargs)
|
||||
|
||||
class SDODistance(SpatialFunction):
|
||||
"Class for Distance queries."
|
||||
def __init__(self, op, tolerance=0.05):
|
||||
super(SDODistance, self).__init__(DISTANCE, end_subst=', %s) %%s %%s' % tolerance,
|
||||
operator=op, result='%%s')
|
||||
|
||||
class SDOGeomRelate(SpatialFunction):
|
||||
"Class for using SDO_GEOM.RELATE."
|
||||
def __init__(self, mask, tolerance=0.05):
|
||||
# SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
|
||||
# Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
|
||||
end_subst = "%s%s) %s '%s'" % (', %%s, ', tolerance, '=', mask)
|
||||
beg_subst = "%%s(%%s, '%s'" % mask
|
||||
super(SDOGeomRelate, self).__init__('SDO_GEOM.RELATE', beg_subst=beg_subst, end_subst=end_subst)
|
||||
|
||||
class SDORelate(SpatialFunction):
|
||||
"Class for using SDO_RELATE."
|
||||
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
|
||||
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
|
||||
def __init__(self, mask):
|
||||
func = 'SDO_RELATE'
|
||||
if not self.mask_regex.match(mask):
|
||||
raise ValueError('Invalid %s mask: "%s"' % (func, mask))
|
||||
super(SDORelate, self).__init__(func, end_subst=", 'mask=%s') = 'TRUE'" % mask)
|
||||
|
||||
#### Lookup type mapping dictionaries of Oracle spatial operations ####
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : (SDODistance('>'), dtypes),
|
||||
'distance_gte' : (SDODistance('>='), dtypes),
|
||||
'distance_lt' : (SDODistance('<'), dtypes),
|
||||
'distance_lte' : (SDODistance('<='), dtypes),
|
||||
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE',
|
||||
beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes),
|
||||
}
|
||||
|
||||
ORACLE_GEOMETRY_FUNCTIONS = {
|
||||
'contains' : SDOOperation('SDO_CONTAINS'),
|
||||
'coveredby' : SDOOperation('SDO_COVEREDBY'),
|
||||
'covers' : SDOOperation('SDO_COVERS'),
|
||||
'disjoint' : SDOGeomRelate('DISJOINT'),
|
||||
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
|
||||
'equals' : SDOOperation('SDO_EQUAL'),
|
||||
'exact' : SDOOperation('SDO_EQUAL'),
|
||||
'overlaps' : SDOOperation('SDO_OVERLAPS'),
|
||||
'same_as' : SDOOperation('SDO_EQUAL'),
|
||||
'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
|
||||
'touches' : SDOOperation('SDO_TOUCH'),
|
||||
'within' : SDOOperation('SDO_INSIDE'),
|
||||
}
|
||||
ORACLE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||
|
||||
# This lookup type does not require a mapping.
|
||||
MISC_TERMS = ['isnull']
|
||||
|
||||
# Acceptable lookup types for Oracle spatial.
|
||||
ORACLE_SPATIAL_TERMS = ORACLE_GEOMETRY_FUNCTIONS.keys()
|
||||
ORACLE_SPATIAL_TERMS += MISC_TERMS
|
||||
ORACLE_SPATIAL_TERMS = dict((term, None) for term in ORACLE_SPATIAL_TERMS) # Making dictionary for fast lookups
|
||||
|
||||
#### The `get_geo_where_clause` function for Oracle ####
|
||||
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
|
||||
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
|
||||
# Getting the quoted table name as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_alias), qn(name))
|
||||
|
||||
# See if a Oracle Geometry function matches the lookup type next
|
||||
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
|
||||
if lookup_info:
|
||||
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||
# 'dwithin' lookup types.
|
||||
if isinstance(lookup_info, tuple):
|
||||
# First element of tuple is lookup type, second element is the type
|
||||
# of the expected argument (e.g., str, float)
|
||||
sdo_op, arg_type = lookup_info
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(geo_annot.value, tuple):
|
||||
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
if len(geo_annot.value) != 2:
|
||||
raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(geo_annot.value[1], arg_type):
|
||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
|
||||
|
||||
if lookup_type == 'relate':
|
||||
# The SDORelate class handles construction for these queries,
|
||||
# and verifies the mask argument.
|
||||
return sdo_op(geo_annot.value[1]).as_sql(geo_col)
|
||||
else:
|
||||
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
|
||||
return sdo_op.as_sql(geo_col)
|
||||
else:
|
||||
# Lookup info is a SDOOperation instance, whose `as_sql` method returns
|
||||
# the SQL necessary for the geometry function call. For example:
|
||||
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
|
||||
return lookup_info.as_sql(geo_col)
|
||||
elif lookup_type == 'isnull':
|
||||
# Handling 'isnull' lookup type
|
||||
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
|
@ -1,51 +0,0 @@
|
|||
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||
|
||||
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
||||
from django.contrib.gis.db.backend.postgis.creation import create_test_spatial_db
|
||||
from django.contrib.gis.db.backend.postgis.field import PostGISField
|
||||
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
|
||||
from django.contrib.gis.db.backend.postgis.query import *
|
||||
|
||||
SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
|
||||
area=AREA,
|
||||
centroid=CENTROID,
|
||||
collect=COLLECT,
|
||||
difference=DIFFERENCE,
|
||||
distance=DISTANCE,
|
||||
distance_functions=DISTANCE_FUNCTIONS,
|
||||
distance_sphere=DISTANCE_SPHERE,
|
||||
distance_spheroid=DISTANCE_SPHEROID,
|
||||
envelope=ENVELOPE,
|
||||
extent=EXTENT,
|
||||
extent3d=EXTENT3D,
|
||||
gis_terms=POSTGIS_TERMS,
|
||||
geojson=ASGEOJSON,
|
||||
gml=ASGML,
|
||||
intersection=INTERSECTION,
|
||||
kml=ASKML,
|
||||
length=LENGTH,
|
||||
length3d=LENGTH3D,
|
||||
length_spheroid=LENGTH_SPHEROID,
|
||||
make_line=MAKE_LINE,
|
||||
mem_size=MEM_SIZE,
|
||||
num_geom=NUM_GEOM,
|
||||
num_points=NUM_POINTS,
|
||||
perimeter=PERIMETER,
|
||||
perimeter3d=PERIMETER3D,
|
||||
point_on_surface=POINT_ON_SURFACE,
|
||||
scale=SCALE,
|
||||
select=GEOM_SELECT,
|
||||
snap_to_grid=SNAP_TO_GRID,
|
||||
svg=ASSVG,
|
||||
sym_difference=SYM_DIFFERENCE,
|
||||
transform=TRANSFORM,
|
||||
translate=TRANSLATE,
|
||||
union=UNION,
|
||||
unionagg=UNIONAGG,
|
||||
version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
|
||||
Adaptor=PostGISAdaptor,
|
||||
Field=PostGISField,
|
||||
GeometryColumns=GeometryColumns,
|
||||
SpatialRefSys=SpatialRefSys,
|
||||
)
|
|
@ -1,231 +0,0 @@
|
|||
import os, re, sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
from django.db.backends.creation import TEST_DATABASE_PREFIX
|
||||
from django.contrib.gis.db.backend.util import getstatusoutput
|
||||
|
||||
def create_lang(db_name, verbosity=1):
|
||||
"Sets up the pl/pgsql language on the given database."
|
||||
|
||||
# Getting the command-line options for the shell command
|
||||
options = get_cmd_options(db_name)
|
||||
|
||||
# Constructing the 'createlang' command.
|
||||
createlang_cmd = 'createlang %splpgsql' % options
|
||||
if verbosity >= 1: print createlang_cmd
|
||||
|
||||
# Must have database super-user privileges to execute createlang -- it must
|
||||
# also be in your path.
|
||||
status, output = getstatusoutput(createlang_cmd)
|
||||
|
||||
# Checking the status of the command, 0 => execution successful
|
||||
if status:
|
||||
raise Exception("Error executing 'plpgsql' command: %s\n" % output)
|
||||
|
||||
def _create_with_cursor(db_name, verbosity=1, autoclobber=False):
|
||||
"Creates database with psycopg2 cursor."
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
# Constructing the necessary SQL to create the database.
|
||||
create_sql = 'CREATE DATABASE %s' % qn(db_name)
|
||||
|
||||
# If there's a template database for PostGIS set, then use it.
|
||||
if hasattr(settings, 'POSTGIS_TEMPLATE'):
|
||||
create_sql += ' TEMPLATE %s' % qn(settings.POSTGIS_TEMPLATE)
|
||||
|
||||
# The DATABASE_USER must possess the privileges to create a spatial database.
|
||||
if settings.DATABASE_USER:
|
||||
create_sql += ' OWNER %s' % qn(settings.DATABASE_USER)
|
||||
|
||||
cursor = connection.cursor()
|
||||
connection.creation.set_autocommit()
|
||||
|
||||
try:
|
||||
# Trying to create the database first.
|
||||
cursor.execute(create_sql)
|
||||
except Exception, e:
|
||||
if 'already exists' in e.pgerror.lower():
|
||||
# Database already exists, drop and recreate if user agrees.
|
||||
if not autoclobber:
|
||||
confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name)
|
||||
if autoclobber or confirm == 'yes':
|
||||
if verbosity >= 1: print 'Destroying old spatial database...'
|
||||
drop_db(db_name)
|
||||
if verbosity >= 1: print 'Creating new spatial database...'
|
||||
cursor.execute(create_sql)
|
||||
else:
|
||||
raise Exception('Spatial database creation canceled.')
|
||||
else:
|
||||
raise Exception('Spatial database creation failed: "%s"' % e.pgerror.strip())
|
||||
|
||||
created_regex = re.compile(r'^createdb: database creation failed: ERROR: database ".+" already exists')
|
||||
def _create_with_shell(db_name, verbosity=1, autoclobber=False):
|
||||
"""
|
||||
If no spatial database already exists, then using a cursor will not work.
|
||||
Thus, a `createdb` command will be issued through the shell to bootstrap
|
||||
creation of the spatial database.
|
||||
|
||||
TODO: Actually allow this method to be used without a spatial database
|
||||
in place first.
|
||||
"""
|
||||
# Getting the command-line options for the shell command
|
||||
options = get_cmd_options(False)
|
||||
if hasattr(settings, 'POSTGIS_TEMPLATE'):
|
||||
options += '-T %s ' % settings.POSTGIS_TEMPlATE
|
||||
|
||||
create_cmd = 'createdb -O %s %s%s' % (settings.DATABASE_USER, options, db_name)
|
||||
if verbosity >= 1: print create_cmd
|
||||
|
||||
# Attempting to create the database.
|
||||
status, output = getstatusoutput(create_cmd)
|
||||
|
||||
if status:
|
||||
if created_regex.match(output):
|
||||
if not autoclobber:
|
||||
confirm = raw_input("\nIt appears the database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % db_name)
|
||||
if autoclobber or confirm == 'yes':
|
||||
if verbosity >= 1: print 'Destroying old spatial database...'
|
||||
drop_cmd = 'dropdb %s%s' % (options, db_name)
|
||||
status, output = getstatusoutput(drop_cmd)
|
||||
if status != 0:
|
||||
raise Exception('Could not drop database %s: %s' % (db_name, output))
|
||||
if verbosity >= 1: print 'Creating new spatial database...'
|
||||
status, output = getstatusoutput(create_cmd)
|
||||
if status != 0:
|
||||
raise Exception('Could not create database after dropping: %s' % output)
|
||||
else:
|
||||
raise Exception('Spatial Database Creation canceled.')
|
||||
else:
|
||||
raise Exception('Unknown error occurred in creating database: %s' % output)
|
||||
|
||||
def create_test_spatial_db(verbosity=1, autoclobber=False, interactive=False):
|
||||
"Creates a test spatial database based on the settings."
|
||||
|
||||
# Making sure we're using PostgreSQL and psycopg2
|
||||
if settings.DATABASE_ENGINE != 'postgresql_psycopg2':
|
||||
raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.')
|
||||
|
||||
# Getting the spatial database name
|
||||
db_name = get_spatial_db(test=True)
|
||||
_create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber)
|
||||
|
||||
# If a template database is used, then don't need to do any of the following.
|
||||
if not hasattr(settings, 'POSTGIS_TEMPLATE'):
|
||||
# Creating the db language, does not need to be done on NT platforms
|
||||
# since the PostGIS installer enables this capability.
|
||||
if os.name != 'nt':
|
||||
create_lang(db_name, verbosity=verbosity)
|
||||
|
||||
# Now adding in the PostGIS routines.
|
||||
load_postgis_sql(db_name, verbosity=verbosity)
|
||||
|
||||
if verbosity >= 1: print 'Creation of spatial database %s successful.' % db_name
|
||||
|
||||
# Closing the connection
|
||||
connection.close()
|
||||
settings.DATABASE_NAME = db_name
|
||||
connection.settings_dict["DATABASE_NAME"] = db_name
|
||||
can_rollback = connection.creation._rollback_works()
|
||||
settings.DATABASE_SUPPORTS_TRANSACTIONS = can_rollback
|
||||
connection.settings_dict["DATABASE_SUPPORTS_TRANSACTIONS"] = can_rollback
|
||||
|
||||
# Syncing the database
|
||||
call_command('syncdb', verbosity=verbosity, interactive=interactive)
|
||||
|
||||
def drop_db(db_name=False, test=False):
|
||||
"""
|
||||
Drops the given database (defaults to what is returned from
|
||||
get_spatial_db()). All exceptions are propagated up to the caller.
|
||||
"""
|
||||
if not db_name: db_name = get_spatial_db(test=test)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('DROP DATABASE %s' % connection.ops.quote_name(db_name))
|
||||
|
||||
def get_cmd_options(db_name):
|
||||
"Obtains the command-line PostgreSQL connection options for shell commands."
|
||||
# The db_name parameter is optional
|
||||
options = ''
|
||||
if db_name:
|
||||
options += '-d %s ' % db_name
|
||||
if settings.DATABASE_USER:
|
||||
options += '-U %s ' % settings.DATABASE_USER
|
||||
if settings.DATABASE_HOST:
|
||||
options += '-h %s ' % settings.DATABASE_HOST
|
||||
if settings.DATABASE_PORT:
|
||||
options += '-p %s ' % settings.DATABASE_PORT
|
||||
return options
|
||||
|
||||
def get_spatial_db(test=False):
|
||||
"""
|
||||
Returns the name of the spatial database. The 'test' keyword may be set
|
||||
to return the test spatial database name.
|
||||
"""
|
||||
if test:
|
||||
if settings.TEST_DATABASE_NAME:
|
||||
test_db_name = settings.TEST_DATABASE_NAME
|
||||
else:
|
||||
test_db_name = TEST_DATABASE_PREFIX + settings.DATABASE_NAME
|
||||
return test_db_name
|
||||
else:
|
||||
if not settings.DATABASE_NAME:
|
||||
raise Exception('must configure DATABASE_NAME in settings.py')
|
||||
return settings.DATABASE_NAME
|
||||
|
||||
def load_postgis_sql(db_name, verbosity=1):
|
||||
"""
|
||||
This routine loads up the PostGIS SQL files lwpostgis.sql and
|
||||
spatial_ref_sys.sql.
|
||||
"""
|
||||
# Getting the path to the PostGIS SQL
|
||||
try:
|
||||
# POSTGIS_SQL_PATH may be placed in settings to tell GeoDjango where the
|
||||
# PostGIS SQL files are located. This is especially useful on Win32
|
||||
# platforms since the output of pg_config looks like "C:/PROGRA~1/..".
|
||||
sql_path = settings.POSTGIS_SQL_PATH
|
||||
except AttributeError:
|
||||
status, sql_path = getstatusoutput('pg_config --sharedir')
|
||||
if status:
|
||||
sql_path = '/usr/local/share'
|
||||
|
||||
# The PostGIS SQL post-creation files.
|
||||
lwpostgis_file = os.path.join(sql_path, 'lwpostgis.sql')
|
||||
srefsys_file = os.path.join(sql_path, 'spatial_ref_sys.sql')
|
||||
if not os.path.isfile(lwpostgis_file):
|
||||
raise Exception('Could not find PostGIS function definitions in %s' % lwpostgis_file)
|
||||
if not os.path.isfile(srefsys_file):
|
||||
raise Exception('Could not find PostGIS spatial reference system definitions in %s' % srefsys_file)
|
||||
|
||||
# Getting the psql command-line options, and command format.
|
||||
options = get_cmd_options(db_name)
|
||||
cmd_fmt = 'psql %s-f "%%s"' % options
|
||||
|
||||
# Now trying to load up the PostGIS functions
|
||||
cmd = cmd_fmt % lwpostgis_file
|
||||
if verbosity >= 1: print cmd
|
||||
status, output = getstatusoutput(cmd)
|
||||
if status:
|
||||
raise Exception('Error in loading PostGIS lwgeometry routines.')
|
||||
|
||||
# Now trying to load up the Spatial Reference System table
|
||||
cmd = cmd_fmt % srefsys_file
|
||||
if verbosity >= 1: print cmd
|
||||
status, output = getstatusoutput(cmd)
|
||||
if status:
|
||||
raise Exception('Error in loading PostGIS spatial_ref_sys table.')
|
||||
|
||||
# Setting the permissions because on Windows platforms the owner
|
||||
# of the spatial_ref_sys and geometry_columns tables is always
|
||||
# the postgres user, regardless of how the db is created.
|
||||
if os.name == 'nt': set_permissions(db_name)
|
||||
|
||||
def set_permissions(db_name):
|
||||
"""
|
||||
Sets the permissions on the given database to that of the user specified
|
||||
in the settings. Needed specifically for PostGIS on Win32 platforms.
|
||||
"""
|
||||
cursor = connection.cursor()
|
||||
user = settings.DATABASE_USER
|
||||
cursor.execute('ALTER TABLE geometry_columns OWNER TO %s' % user)
|
||||
cursor.execute('ALTER TABLE spatial_ref_sys OWNER TO %s' % user)
|
|
@ -1,95 +0,0 @@
|
|||
from django.db import connection
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
|
||||
|
||||
# Quotename & geographic quotename, respectively
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
class PostGISField(Field):
|
||||
"""
|
||||
The backend-specific geographic field for PostGIS.
|
||||
"""
|
||||
|
||||
def _add_geom(self, style, db_table):
|
||||
"""
|
||||
Constructs the addition of the geometry to the table using the
|
||||
AddGeometryColumn(...) PostGIS (and OGC standard) stored procedure.
|
||||
|
||||
Takes the style object (provides syntax highlighting) and the
|
||||
database table as parameters.
|
||||
"""
|
||||
sql = (style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_TABLE('AddGeometryColumn') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(self.column)) + ', ' +
|
||||
style.SQL_FIELD(str(self.srid)) + ', ' +
|
||||
style.SQL_COLTYPE(gqn(self.geom_type)) + ', ' +
|
||||
style.SQL_KEYWORD(str(self.dim)) + ');')
|
||||
|
||||
if not self.null:
|
||||
# Add a NOT NULL constraint to the field
|
||||
sql += ('\n' +
|
||||
style.SQL_KEYWORD('ALTER TABLE ') +
|
||||
style.SQL_TABLE(qn(db_table)) +
|
||||
style.SQL_KEYWORD(' ALTER ') +
|
||||
style.SQL_FIELD(qn(self.column)) +
|
||||
style.SQL_KEYWORD(' SET NOT NULL') + ';')
|
||||
return sql
|
||||
|
||||
def _geom_index(self, style, db_table,
|
||||
index_type='GIST', index_opts='GIST_GEOMETRY_OPS'):
|
||||
"Creates a GiST index for this geometry field."
|
||||
sql = (style.SQL_KEYWORD('CREATE INDEX ') +
|
||||
style.SQL_TABLE(qn('%s_%s_id' % (db_table, self.column))) +
|
||||
style.SQL_KEYWORD(' ON ') +
|
||||
style.SQL_TABLE(qn(db_table)) +
|
||||
style.SQL_KEYWORD(' USING ') +
|
||||
style.SQL_COLTYPE(index_type) + ' ( ' +
|
||||
style.SQL_FIELD(qn(self.column)) + ' ' +
|
||||
style.SQL_KEYWORD(index_opts) + ' );')
|
||||
return sql
|
||||
|
||||
def post_create_sql(self, style, db_table):
|
||||
"""
|
||||
Returns SQL that will be executed after the model has been
|
||||
created. Geometry columns must be added after creation with the
|
||||
PostGIS AddGeometryColumn() function.
|
||||
"""
|
||||
|
||||
# Getting the AddGeometryColumn() SQL necessary to create a PostGIS
|
||||
# geometry field.
|
||||
post_sql = self._add_geom(style, db_table)
|
||||
|
||||
# If the user wants to index this data, then get the indexing SQL as well.
|
||||
if self.spatial_index:
|
||||
return (post_sql, self._geom_index(style, db_table))
|
||||
else:
|
||||
return (post_sql,)
|
||||
|
||||
def _post_delete_sql(self, style, db_table):
|
||||
"Drops the geometry column."
|
||||
sql = (style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_KEYWORD('DropGeometryColumn') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(self.column)) + ');')
|
||||
return sql
|
||||
|
||||
def db_type(self):
|
||||
"""
|
||||
PostGIS geometry columns are added by stored procedures, should be
|
||||
None.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
ST_Transform() function call.
|
||||
"""
|
||||
if value is None or value.srid == self.srid:
|
||||
return '%s'
|
||||
else:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return '%s(%%s, %s)' % (TRANSFORM, self.srid)
|
|
@ -1,54 +0,0 @@
|
|||
"""
|
||||
This utility module is for obtaining information about the PostGIS
|
||||
installation.
|
||||
|
||||
See PostGIS docs at Ch. 6.2.1 for more information on these functions.
|
||||
"""
|
||||
import re
|
||||
|
||||
def _get_postgis_func(func):
|
||||
"Helper routine for calling PostGIS functions and returning their result."
|
||||
from django.db import connection
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT %s()' % func)
|
||||
row = cursor.fetchone()
|
||||
cursor.close()
|
||||
return row[0]
|
||||
|
||||
### PostGIS management functions ###
|
||||
def postgis_geos_version():
|
||||
"Returns the version of the GEOS library used with PostGIS."
|
||||
return _get_postgis_func('postgis_geos_version')
|
||||
|
||||
def postgis_lib_version():
|
||||
"Returns the version number of the PostGIS library used with PostgreSQL."
|
||||
return _get_postgis_func('postgis_lib_version')
|
||||
|
||||
def postgis_proj_version():
|
||||
"Returns the version of the PROJ.4 library used with PostGIS."
|
||||
return _get_postgis_func('postgis_proj_version')
|
||||
|
||||
def postgis_version():
|
||||
"Returns PostGIS version number and compile-time options."
|
||||
return _get_postgis_func('postgis_version')
|
||||
|
||||
def postgis_full_version():
|
||||
"Returns PostGIS version number and compile-time options."
|
||||
return _get_postgis_func('postgis_full_version')
|
||||
|
||||
### Routines for parsing output of management functions. ###
|
||||
version_regex = re.compile('^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
||||
def postgis_version_tuple():
|
||||
"Returns the PostGIS version as a tuple."
|
||||
|
||||
# Getting the PostGIS version
|
||||
version = postgis_lib_version()
|
||||
m = version_regex.match(version)
|
||||
if m:
|
||||
major = int(m.group('major'))
|
||||
minor1 = int(m.group('minor1'))
|
||||
minor2 = int(m.group('minor2'))
|
||||
else:
|
||||
raise Exception('Could not parse PostGIS version string: %s' % version)
|
||||
|
||||
return (version, major, minor1, minor2)
|
|
@ -1,313 +0,0 @@
|
|||
"""
|
||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||
routine for PostGIS.
|
||||
"""
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from django.db import connection
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
|
||||
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
# Get the PostGIS version information.
|
||||
# To avoid the need to do a database query to determine the PostGIS version
|
||||
# each time the server starts up, one can optionally specify a
|
||||
# POSTGIS_VERSION setting. This setting is intentionally undocumented and
|
||||
# should be considered experimental, because an upcoming GIS backend
|
||||
# refactoring might remove the need for it.
|
||||
if hasattr(settings, 'POSTGIS_VERSION') and settings.POSTGIS_VERSION is not None:
|
||||
version_tuple = settings.POSTGIS_VERSION
|
||||
else:
|
||||
# This import is intentionally within the 'else' so that it isn't executed
|
||||
# if the POSTGIS_VERSION setting is available.
|
||||
from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
|
||||
version_tuple = postgis_version_tuple()
|
||||
POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = version_tuple
|
||||
|
||||
# The supported PostGIS versions.
|
||||
# TODO: Confirm tests with PostGIS versions 1.1.x -- should work.
|
||||
# Versions <= 1.0.x do not use GEOS C API, and will not be supported.
|
||||
if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
|
||||
raise Exception('PostGIS version %s not supported.' % POSTGIS_VERSION)
|
||||
|
||||
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
||||
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
||||
# means that 'ST_' prefixes geometry function names.
|
||||
GEOM_FUNC_PREFIX = ''
|
||||
if MAJOR_VERSION >= 1:
|
||||
if (MINOR_VERSION1 > 2 or
|
||||
(MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2)):
|
||||
GEOM_FUNC_PREFIX = 'ST_'
|
||||
|
||||
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
|
||||
|
||||
# Custom selection not needed for PostGIS because GEOS geometries are
|
||||
# instantiated directly from the HEXEWKB returned by default. If
|
||||
# WKT is needed for some reason in the future, this value may be changed,
|
||||
# e.g,, 'AsText(%s)'.
|
||||
GEOM_SELECT = None
|
||||
|
||||
# Functions used by the GeoManager & GeoQuerySet
|
||||
AREA = get_func('Area')
|
||||
ASGEOJSON = get_func('AsGeoJson')
|
||||
ASKML = get_func('AsKML')
|
||||
ASGML = get_func('AsGML')
|
||||
ASSVG = get_func('AsSVG')
|
||||
CENTROID = get_func('Centroid')
|
||||
COLLECT = get_func('Collect')
|
||||
DIFFERENCE = get_func('Difference')
|
||||
DISTANCE = get_func('Distance')
|
||||
DISTANCE_SPHERE = get_func('distance_sphere')
|
||||
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
||||
ENVELOPE = get_func('Envelope')
|
||||
EXTENT = get_func('Extent')
|
||||
EXTENT3D = get_func('Extent3D')
|
||||
GEOM_FROM_TEXT = get_func('GeomFromText')
|
||||
GEOM_FROM_EWKB = get_func('GeomFromEWKB')
|
||||
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
||||
INTERSECTION = get_func('Intersection')
|
||||
LENGTH = get_func('Length')
|
||||
LENGTH3D = get_func('Length3D')
|
||||
LENGTH_SPHEROID = get_func('length_spheroid')
|
||||
MAKE_LINE = get_func('MakeLine')
|
||||
MEM_SIZE = get_func('mem_size')
|
||||
NUM_GEOM = get_func('NumGeometries')
|
||||
NUM_POINTS = get_func('npoints')
|
||||
PERIMETER = get_func('Perimeter')
|
||||
PERIMETER3D = get_func('Perimeter3D')
|
||||
POINT_ON_SURFACE = get_func('PointOnSurface')
|
||||
SCALE = get_func('Scale')
|
||||
SNAP_TO_GRID = get_func('SnapToGrid')
|
||||
SYM_DIFFERENCE = get_func('SymDifference')
|
||||
TRANSFORM = get_func('Transform')
|
||||
TRANSLATE = get_func('Translate')
|
||||
|
||||
# Special cases for union, KML, and GeoJSON methods.
|
||||
if MINOR_VERSION1 < 3:
|
||||
UNIONAGG = 'GeomUnion'
|
||||
UNION = 'Union'
|
||||
else:
|
||||
UNIONAGG = 'ST_Union'
|
||||
UNION = 'ST_Union'
|
||||
|
||||
if MINOR_VERSION1 == 1:
|
||||
ASKML = False
|
||||
|
||||
# Only 1.3.4+ have AsGeoJson.
|
||||
if (MINOR_VERSION1 < 3 or
|
||||
(MINOR_VERSION1 == 3 and MINOR_VERSION2 < 4)):
|
||||
ASGEOJSON = False
|
||||
else:
|
||||
raise NotImplementedError('PostGIS versions < 1.0 are not supported.')
|
||||
|
||||
#### Classes used in constructing PostGIS spatial SQL ####
|
||||
class PostGISOperator(SpatialOperation):
|
||||
"For PostGIS operators (e.g. `&&`, `~`)."
|
||||
def __init__(self, operator):
|
||||
super(PostGISOperator, self).__init__(operator=operator, beg_subst='%s %s %%s')
|
||||
|
||||
class PostGISFunction(SpatialFunction):
|
||||
"For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
|
||||
def __init__(self, function, **kwargs):
|
||||
super(PostGISFunction, self).__init__(get_func(function), **kwargs)
|
||||
|
||||
class PostGISFunctionParam(PostGISFunction):
|
||||
"For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
|
||||
def __init__(self, func):
|
||||
super(PostGISFunctionParam, self).__init__(func, end_subst=', %%s)')
|
||||
|
||||
class PostGISDistance(PostGISFunction):
|
||||
"For PostGIS distance operations."
|
||||
dist_func = 'Distance'
|
||||
def __init__(self, operator):
|
||||
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
|
||||
operator=operator, result='%%s')
|
||||
|
||||
class PostGISSpheroidDistance(PostGISFunction):
|
||||
"For PostGIS spherical distance operations (using the spheroid)."
|
||||
dist_func = 'distance_spheroid'
|
||||
def __init__(self, operator):
|
||||
# An extra parameter in `end_subst` is needed for the spheroid string.
|
||||
super(PostGISSpheroidDistance, self).__init__(self.dist_func,
|
||||
beg_subst='%s(%s, %%s, %%s',
|
||||
end_subst=') %s %s',
|
||||
operator=operator, result='%%s')
|
||||
|
||||
class PostGISSphereDistance(PostGISFunction):
|
||||
"For PostGIS spherical distance operations."
|
||||
dist_func = 'distance_sphere'
|
||||
def __init__(self, operator):
|
||||
super(PostGISSphereDistance, self).__init__(self.dist_func, end_subst=') %s %s',
|
||||
operator=operator, result='%%s')
|
||||
|
||||
class PostGISRelate(PostGISFunctionParam):
|
||||
"For PostGIS Relate(<geom>, <pattern>) calls."
|
||||
pattern_regex = re.compile(r'^[012TF\*]{9}$')
|
||||
def __init__(self, pattern):
|
||||
if not self.pattern_regex.match(pattern):
|
||||
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
|
||||
super(PostGISRelate, self).__init__('Relate')
|
||||
|
||||
#### Lookup type mapping dictionaries of PostGIS operations. ####
|
||||
|
||||
# PostGIS-specific operators. The commented descriptions of these
|
||||
# operators come from Section 6.2.2 of the official PostGIS documentation.
|
||||
POSTGIS_OPERATORS = {
|
||||
# The "&<" operator returns true if A's bounding box overlaps or
|
||||
# is to the left of B's bounding box.
|
||||
'overlaps_left' : PostGISOperator('&<'),
|
||||
# The "&>" operator returns true if A's bounding box overlaps or
|
||||
# is to the right of B's bounding box.
|
||||
'overlaps_right' : PostGISOperator('&>'),
|
||||
# The "<<" operator returns true if A's bounding box is strictly
|
||||
# to the left of B's bounding box.
|
||||
'left' : PostGISOperator('<<'),
|
||||
# The ">>" operator returns true if A's bounding box is strictly
|
||||
# to the right of B's bounding box.
|
||||
'right' : PostGISOperator('>>'),
|
||||
# The "&<|" operator returns true if A's bounding box overlaps or
|
||||
# is below B's bounding box.
|
||||
'overlaps_below' : PostGISOperator('&<|'),
|
||||
# The "|&>" operator returns true if A's bounding box overlaps or
|
||||
# is above B's bounding box.
|
||||
'overlaps_above' : PostGISOperator('|&>'),
|
||||
# The "<<|" operator returns true if A's bounding box is strictly
|
||||
# below B's bounding box.
|
||||
'strictly_below' : PostGISOperator('<<|'),
|
||||
# The "|>>" operator returns true if A's bounding box is strictly
|
||||
# above B's bounding box.
|
||||
'strictly_above' : PostGISOperator('|>>'),
|
||||
# The "~=" operator is the "same as" operator. It tests actual
|
||||
# geometric equality of two features. So if A and B are the same feature,
|
||||
# vertex-by-vertex, the operator returns true.
|
||||
'same_as' : PostGISOperator('~='),
|
||||
'exact' : PostGISOperator('~='),
|
||||
# The "@" operator returns true if A's bounding box is completely contained
|
||||
# by B's bounding box.
|
||||
'contained' : PostGISOperator('@'),
|
||||
# The "~" operator returns true if A's bounding box completely contains
|
||||
# by B's bounding box.
|
||||
'bbcontains' : PostGISOperator('~'),
|
||||
# The "&&" operator returns true if A's bounding box overlaps
|
||||
# B's bounding box.
|
||||
'bboverlaps' : PostGISOperator('&&'),
|
||||
}
|
||||
|
||||
# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query
|
||||
# first before calling the more computationally expensive GEOS routines (called
|
||||
# "inline index magic"):
|
||||
# 'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and
|
||||
# 'covers'.
|
||||
POSTGIS_GEOMETRY_FUNCTIONS = {
|
||||
'equals' : PostGISFunction('Equals'),
|
||||
'disjoint' : PostGISFunction('Disjoint'),
|
||||
'touches' : PostGISFunction('Touches'),
|
||||
'crosses' : PostGISFunction('Crosses'),
|
||||
'within' : PostGISFunction('Within'),
|
||||
'overlaps' : PostGISFunction('Overlaps'),
|
||||
'contains' : PostGISFunction('Contains'),
|
||||
'intersects' : PostGISFunction('Intersects'),
|
||||
'relate' : (PostGISRelate, basestring),
|
||||
}
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
def get_dist_ops(operator):
|
||||
"Returns operations for both regular and spherical distances."
|
||||
return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||
'distance_lt' : (get_dist_ops('<'), dtypes),
|
||||
'distance_lte' : (get_dist_ops('<='), dtypes),
|
||||
}
|
||||
|
||||
if GEOM_FUNC_PREFIX == 'ST_':
|
||||
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
||||
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
||||
{'coveredby' : PostGISFunction('CoveredBy'),
|
||||
'covers' : PostGISFunction('Covers'),
|
||||
})
|
||||
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
|
||||
|
||||
# Distance functions are a part of PostGIS geometry functions.
|
||||
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||
|
||||
# Any other lookup types that do not require a mapping.
|
||||
MISC_TERMS = ['isnull']
|
||||
|
||||
# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types
|
||||
# allowed for geographic queries.
|
||||
POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first
|
||||
POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions
|
||||
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
|
||||
POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups
|
||||
|
||||
# For checking tuple parameters -- not very pretty but gets job done.
|
||||
def exactly_two(val): return val == 2
|
||||
def two_to_three(val): return val >= 2 and val <=3
|
||||
def num_params(lookup_type, val):
|
||||
if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
|
||||
else: return exactly_two(val)
|
||||
|
||||
#### The `get_geo_where_clause` function for PostGIS. ####
|
||||
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
|
||||
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
|
||||
# Getting the quoted field as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_alias), qn(name))
|
||||
if lookup_type in POSTGIS_OPERATORS:
|
||||
# See if a PostGIS operator matches the lookup type.
|
||||
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
|
||||
elif lookup_type in POSTGIS_GEOMETRY_FUNCTIONS:
|
||||
# See if a PostGIS geometry function matches the lookup type.
|
||||
tmp = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type]
|
||||
|
||||
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||
# distance lookups.
|
||||
if isinstance(tmp, tuple):
|
||||
# First element of tuple is the PostGISOperation instance, and the
|
||||
# second element is either the type or a tuple of acceptable types
|
||||
# that may passed in as further parameters for the lookup type.
|
||||
op, arg_type = tmp
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(geo_annot.value, (tuple, list)):
|
||||
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Number of valid tuple parameters depends on the lookup type.
|
||||
nparams = len(geo_annot.value)
|
||||
if not num_params(lookup_type, nparams):
|
||||
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(geo_annot.value[1], arg_type):
|
||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
|
||||
|
||||
# For lookup type `relate`, the op instance is not yet created (has
|
||||
# to be instantiated here to check the pattern parameter).
|
||||
if lookup_type == 'relate':
|
||||
op = op(geo_annot.value[1])
|
||||
elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin':
|
||||
if geo_annot.geodetic:
|
||||
# Geodetic distances are only availble from Points to PointFields.
|
||||
if geo_annot.geom_type != 'POINT':
|
||||
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
|
||||
if geo_annot.value[0].geom_typeid != 0:
|
||||
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
|
||||
# Setting up the geodetic operation appropriately.
|
||||
if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2]
|
||||
else: op = op[1]
|
||||
else:
|
||||
op = op[0]
|
||||
else:
|
||||
op = tmp
|
||||
# Calling the `as_sql` function on the operation instance.
|
||||
return op.as_sql(geo_col)
|
||||
elif lookup_type == 'isnull':
|
||||
# Handling 'isnull' lookup type
|
||||
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
|
@ -1,60 +0,0 @@
|
|||
__all__ = ['create_test_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||
|
||||
from ctypes.util import find_library
|
||||
from django.conf import settings
|
||||
from django.db.backends.signals import connection_created
|
||||
|
||||
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||
from django.contrib.gis.db.backend.spatialite.adaptor import SpatiaLiteAdaptor
|
||||
from django.contrib.gis.db.backend.spatialite.creation import create_test_spatial_db
|
||||
from django.contrib.gis.db.backend.spatialite.field import SpatiaLiteField
|
||||
from django.contrib.gis.db.backend.spatialite.models import GeometryColumns, SpatialRefSys
|
||||
from django.contrib.gis.db.backend.spatialite.query import *
|
||||
|
||||
# Here we are figuring out the path to the SpatiLite library (`libspatialite`).
|
||||
# If it's not in the system PATH, it may be set manually in the settings via
|
||||
# the `SPATIALITE_LIBRARY_PATH` setting.
|
||||
spatialite_lib = getattr(settings, 'SPATIALITE_LIBRARY_PATH', find_library('spatialite'))
|
||||
if spatialite_lib:
|
||||
def initialize_spatialite(sender=None, **kwargs):
|
||||
"""
|
||||
This function initializes the pysqlite2 connection to enable the
|
||||
loading of extensions, and to load up the SpatiaLite library
|
||||
extension.
|
||||
"""
|
||||
from django.db import connection
|
||||
connection.connection.enable_load_extension(True)
|
||||
connection.cursor().execute("SELECT load_extension(%s)", (spatialite_lib,))
|
||||
connection_created.connect(initialize_spatialite)
|
||||
else:
|
||||
# No SpatiaLite library found.
|
||||
raise Exception('Unable to locate SpatiaLite, needed to use GeoDjango with sqlite3.')
|
||||
|
||||
SpatialBackend = BaseSpatialBackend(name='spatialite', spatialite=True,
|
||||
area=AREA,
|
||||
centroid=CENTROID,
|
||||
contained=CONTAINED,
|
||||
difference=DIFFERENCE,
|
||||
distance=DISTANCE,
|
||||
distance_functions=DISTANCE_FUNCTIONS,
|
||||
envelope=ENVELOPE,
|
||||
from_text=GEOM_FROM_TEXT,
|
||||
gis_terms=SPATIALITE_TERMS,
|
||||
intersection=INTERSECTION,
|
||||
length=LENGTH,
|
||||
num_geom=NUM_GEOM,
|
||||
num_points=NUM_POINTS,
|
||||
point_on_surface=POINT_ON_SURFACE,
|
||||
scale=SCALE,
|
||||
select=GEOM_SELECT,
|
||||
svg=ASSVG,
|
||||
sym_difference=SYM_DIFFERENCE,
|
||||
transform=TRANSFORM,
|
||||
translate=TRANSLATE,
|
||||
union=UNION,
|
||||
unionagg=UNIONAGG,
|
||||
Adaptor=SpatiaLiteAdaptor,
|
||||
Field=SpatiaLiteField,
|
||||
GeometryColumns=GeometryColumns,
|
||||
SpatialRefSys=SpatialRefSys,
|
||||
)
|
|
@ -1,61 +0,0 @@
|
|||
import os
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
|
||||
def spatialite_init_file():
|
||||
# SPATIALITE_SQL may be placed in settings to tell
|
||||
# GeoDjango to use a specific user-supplied file.
|
||||
return getattr(settings, 'SPATIALITE_SQL', 'init_spatialite-2.3.sql')
|
||||
|
||||
def create_test_spatial_db(verbosity=1, autoclobber=False, interactive=False):
|
||||
"Creates a spatial database based on the settings."
|
||||
|
||||
# Making sure we're using PostgreSQL and psycopg2
|
||||
if settings.DATABASE_ENGINE != 'sqlite3':
|
||||
raise Exception('SpatiaLite database creation only supported on sqlite3 platform.')
|
||||
|
||||
# Getting the test database name using the SQLite backend's
|
||||
# `_create_test_db`. Unless `TEST_DATABASE_NAME` is defined,
|
||||
# it returns ":memory:".
|
||||
db_name = connection.creation._create_test_db(verbosity, autoclobber)
|
||||
|
||||
# Closing out the current connection to the database set in
|
||||
# originally in the settings. This makes it so `initialize_spatialite`
|
||||
# function will be run on the connection for the _test_ database instead.
|
||||
connection.close()
|
||||
|
||||
# Point to the new database
|
||||
settings.DATABASE_NAME = db_name
|
||||
connection.settings_dict["DATABASE_NAME"] = db_name
|
||||
can_rollback = connection.creation._rollback_works()
|
||||
settings.DATABASE_SUPPORTS_TRANSACTIONS = can_rollback
|
||||
connection.settings_dict["DATABASE_SUPPORTS_TRANSACTIONS"] = can_rollback
|
||||
|
||||
# Finally, loading up the SpatiaLite SQL file.
|
||||
load_spatialite_sql(db_name, verbosity=verbosity)
|
||||
|
||||
if verbosity >= 1:
|
||||
print 'Creation of spatial database %s successful.' % db_name
|
||||
|
||||
# Syncing the database
|
||||
call_command('syncdb', verbosity=verbosity, interactive=interactive)
|
||||
|
||||
def load_spatialite_sql(db_name, verbosity=1):
|
||||
"""
|
||||
This routine loads up the SpatiaLite SQL file.
|
||||
"""
|
||||
# Getting the location of the SpatiaLite SQL file, and confirming
|
||||
# it exists.
|
||||
spatialite_sql = spatialite_init_file()
|
||||
if not os.path.isfile(spatialite_sql):
|
||||
raise Exception('Could not find the SpatiaLite initialization SQL file: %s' % spatialite_sql)
|
||||
|
||||
# Opening up the SpatiaLite SQL initialization file and executing
|
||||
# as a script.
|
||||
sql_fh = open(spatialite_sql, 'r')
|
||||
try:
|
||||
cur = connection.cursor()
|
||||
cur.executescript(sql_fh.read())
|
||||
finally:
|
||||
sql_fh.close()
|
|
@ -1,82 +0,0 @@
|
|||
from django.db.models.fields import Field # Django base Field class
|
||||
|
||||
# Quotename & geographic quotename, respectively
|
||||
from django.db import connection
|
||||
qn = connection.ops.quote_name
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
from django.contrib.gis.db.backend.spatialite.query import GEOM_FROM_TEXT, TRANSFORM
|
||||
|
||||
class SpatiaLiteField(Field):
|
||||
"""
|
||||
The backend-specific geographic field for SpatiaLite.
|
||||
"""
|
||||
|
||||
def _add_geom(self, style, db_table):
|
||||
"""
|
||||
Constructs the addition of the geometry to the table using the
|
||||
AddGeometryColumn(...) OpenGIS stored procedure.
|
||||
|
||||
Takes the style object (provides syntax highlighting) and the
|
||||
database table as parameters.
|
||||
"""
|
||||
sql = (style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_TABLE('AddGeometryColumn') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(self.column)) + ', ' +
|
||||
style.SQL_FIELD(str(self.srid)) + ', ' +
|
||||
style.SQL_COLTYPE(gqn(self.geom_type)) + ', ' +
|
||||
style.SQL_KEYWORD(str(self.dim)) + ', ' +
|
||||
style.SQL_KEYWORD(str(int(not self.null))) +
|
||||
');')
|
||||
return sql
|
||||
|
||||
def _geom_index(self, style, db_table):
|
||||
"Creates a spatial index for this geometry field."
|
||||
sql = (style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_TABLE('CreateSpatialIndex') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(self.column)) + ');')
|
||||
return sql
|
||||
|
||||
def post_create_sql(self, style, db_table):
|
||||
"""
|
||||
Returns SQL that will be executed after the model has been
|
||||
created. Geometry columns must be added after creation with the
|
||||
OpenGIS AddGeometryColumn() function.
|
||||
"""
|
||||
# Getting the AddGeometryColumn() SQL necessary to create a OpenGIS
|
||||
# geometry field.
|
||||
post_sql = self._add_geom(style, db_table)
|
||||
|
||||
# If the user wants to index this data, then get the indexing SQL as well.
|
||||
if self.spatial_index:
|
||||
return (post_sql, self._geom_index(style, db_table))
|
||||
else:
|
||||
return (post_sql,)
|
||||
|
||||
def _post_delete_sql(self, style, db_table):
|
||||
"Drops the geometry column."
|
||||
sql = (style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_KEYWORD('DropGeometryColumn') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(self.column)) + ');')
|
||||
return sql
|
||||
|
||||
def db_type(self):
|
||||
"""
|
||||
SpatiaLite geometry columns are added by stored procedures;
|
||||
should be None.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
Transform() and GeomFromText() function call(s).
|
||||
"""
|
||||
if value is None or value.srid == self.srid:
|
||||
return '%s(%%s,%s)' % (GEOM_FROM_TEXT, self.srid)
|
||||
else:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return '%s(%s(%%s,%s), %s)' % (TRANSFORM, GEOM_FROM_TEXT, value.srid, self.srid)
|
|
@ -1,160 +0,0 @@
|
|||
"""
|
||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||
routine for SpatiaLite.
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from django.db import connection
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
GEOM_SELECT = 'AsText(%s)'
|
||||
|
||||
# Dummy func, in case we need it later:
|
||||
def get_func(str):
|
||||
return str
|
||||
|
||||
# Functions used by the GeoManager & GeoQuerySet
|
||||
AREA = get_func('Area')
|
||||
ASSVG = get_func('AsSVG')
|
||||
CENTROID = get_func('Centroid')
|
||||
CONTAINED = get_func('MbrWithin')
|
||||
DIFFERENCE = get_func('Difference')
|
||||
DISTANCE = get_func('Distance')
|
||||
ENVELOPE = get_func('Envelope')
|
||||
GEOM_FROM_TEXT = get_func('GeomFromText')
|
||||
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
||||
INTERSECTION = get_func('Intersection')
|
||||
LENGTH = get_func('GLength') # OpenGis defines Length, but this conflicts with an SQLite reserved keyword
|
||||
NUM_GEOM = get_func('NumGeometries')
|
||||
NUM_POINTS = get_func('NumPoints')
|
||||
POINT_ON_SURFACE = get_func('PointOnSurface')
|
||||
SCALE = get_func('ScaleCoords')
|
||||
SYM_DIFFERENCE = get_func('SymDifference')
|
||||
TRANSFORM = get_func('Transform')
|
||||
TRANSLATE = get_func('ShiftCoords')
|
||||
UNION = 'GUnion'# OpenGis defines Union, but this conflicts with an SQLite reserved keyword
|
||||
UNIONAGG = 'GUnion'
|
||||
|
||||
#### Classes used in constructing SpatiaLite spatial SQL ####
|
||||
class SpatiaLiteOperator(SpatialOperation):
|
||||
"For SpatiaLite operators (e.g. `&&`, `~`)."
|
||||
def __init__(self, operator):
|
||||
super(SpatiaLiteOperator, self).__init__(operator=operator, beg_subst='%s %s %%s')
|
||||
|
||||
class SpatiaLiteFunction(SpatialFunction):
|
||||
"For SpatiaLite function calls."
|
||||
def __init__(self, function, **kwargs):
|
||||
super(SpatiaLiteFunction, self).__init__(get_func(function), **kwargs)
|
||||
|
||||
class SpatiaLiteFunctionParam(SpatiaLiteFunction):
|
||||
"For SpatiaLite functions that take another parameter."
|
||||
def __init__(self, func):
|
||||
super(SpatiaLiteFunctionParam, self).__init__(func, end_subst=', %%s)')
|
||||
|
||||
class SpatiaLiteDistance(SpatiaLiteFunction):
|
||||
"For SpatiaLite distance operations."
|
||||
dist_func = 'Distance'
|
||||
def __init__(self, operator):
|
||||
super(SpatiaLiteDistance, self).__init__(self.dist_func, end_subst=') %s %s',
|
||||
operator=operator, result='%%s')
|
||||
|
||||
class SpatiaLiteRelate(SpatiaLiteFunctionParam):
|
||||
"For SpatiaLite Relate(<geom>, <pattern>) calls."
|
||||
pattern_regex = re.compile(r'^[012TF\*]{9}$')
|
||||
def __init__(self, pattern):
|
||||
if not self.pattern_regex.match(pattern):
|
||||
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
|
||||
super(SpatiaLiteRelate, self).__init__('Relate')
|
||||
|
||||
|
||||
SPATIALITE_GEOMETRY_FUNCTIONS = {
|
||||
'equals' : SpatiaLiteFunction('Equals'),
|
||||
'disjoint' : SpatiaLiteFunction('Disjoint'),
|
||||
'touches' : SpatiaLiteFunction('Touches'),
|
||||
'crosses' : SpatiaLiteFunction('Crosses'),
|
||||
'within' : SpatiaLiteFunction('Within'),
|
||||
'overlaps' : SpatiaLiteFunction('Overlaps'),
|
||||
'contains' : SpatiaLiteFunction('Contains'),
|
||||
'intersects' : SpatiaLiteFunction('Intersects'),
|
||||
'relate' : (SpatiaLiteRelate, basestring),
|
||||
# Retruns true if B's bounding box completely contains A's bounding box.
|
||||
'contained' : SpatiaLiteFunction('MbrWithin'),
|
||||
# Returns true if A's bounding box completely contains B's bounding box.
|
||||
'bbcontains' : SpatiaLiteFunction('MbrContains'),
|
||||
# Returns true if A's bounding box overlaps B's bounding box.
|
||||
'bboverlaps' : SpatiaLiteFunction('MbrOverlaps'),
|
||||
# These are implemented here as synonyms for Equals
|
||||
'same_as' : SpatiaLiteFunction('Equals'),
|
||||
'exact' : SpatiaLiteFunction('Equals'),
|
||||
}
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
def get_dist_ops(operator):
|
||||
"Returns operations for regular distances; spherical distances are not currently supported."
|
||||
return (SpatiaLiteDistance(operator),)
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||
'distance_lt' : (get_dist_ops('<'), dtypes),
|
||||
'distance_lte' : (get_dist_ops('<='), dtypes),
|
||||
}
|
||||
|
||||
# Distance functions are a part of SpatiaLite geometry functions.
|
||||
SPATIALITE_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||
|
||||
# Any other lookup types that do not require a mapping.
|
||||
MISC_TERMS = ['isnull']
|
||||
|
||||
# These are the SpatiaLite-customized QUERY_TERMS -- a list of the lookup types
|
||||
# allowed for geographic queries.
|
||||
SPATIALITE_TERMS = SPATIALITE_GEOMETRY_FUNCTIONS.keys() # Getting the Geometry Functions
|
||||
SPATIALITE_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
|
||||
SPATIALITE_TERMS = dict((term, None) for term in SPATIALITE_TERMS) # Making a dictionary for fast lookups
|
||||
|
||||
#### The `get_geo_where_clause` function for SpatiaLite. ####
|
||||
def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
|
||||
"Returns the SQL WHERE clause for use in SpatiaLite SQL construction."
|
||||
# Getting the quoted field as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_alias), qn(name))
|
||||
if lookup_type in SPATIALITE_GEOMETRY_FUNCTIONS:
|
||||
# See if a SpatiaLite geometry function matches the lookup type.
|
||||
tmp = SPATIALITE_GEOMETRY_FUNCTIONS[lookup_type]
|
||||
|
||||
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||
# distance lookups.
|
||||
if isinstance(tmp, tuple):
|
||||
# First element of tuple is the SpatiaLiteOperation instance, and the
|
||||
# second element is either the type or a tuple of acceptable types
|
||||
# that may passed in as further parameters for the lookup type.
|
||||
op, arg_type = tmp
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(geo_annot.value, (tuple, list)):
|
||||
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Number of valid tuple parameters depends on the lookup type.
|
||||
if len(geo_annot.value) != 2:
|
||||
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(geo_annot.value[1], arg_type):
|
||||
raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
|
||||
|
||||
# For lookup type `relate`, the op instance is not yet created (has
|
||||
# to be instantiated here to check the pattern parameter).
|
||||
if lookup_type == 'relate':
|
||||
op = op(geo_annot.value[1])
|
||||
elif lookup_type in DISTANCE_FUNCTIONS:
|
||||
op = op[0]
|
||||
else:
|
||||
op = tmp
|
||||
# Calling the `as_sql` function on the operation instance.
|
||||
return op.as_sql(geo_col)
|
||||
elif lookup_type == 'isnull':
|
||||
# Handling 'isnull' lookup type
|
||||
return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
|
@ -1,4 +1,4 @@
|
|||
class WKTAdaptor(object):
|
||||
class WKTAdapter(object):
|
||||
"""
|
||||
This provides an adaptor for Geometries sent to the
|
||||
MySQL and Oracle database backends.
|
|
@ -0,0 +1,327 @@
|
|||
"""
|
||||
Base/mixin classes for the spatial backend database operations and the
|
||||
`SpatialRefSys` model the backend.
|
||||
"""
|
||||
import re
|
||||
from django.conf import settings
|
||||
from django.contrib.gis import gdal
|
||||
|
||||
class BaseSpatialOperations(object):
|
||||
"""
|
||||
This module holds the base `BaseSpatialBackend` object, which is
|
||||
instantiated by each spatial database backend with the features
|
||||
it has.
|
||||
"""
|
||||
distance_functions = {}
|
||||
geometry_functions = {}
|
||||
geometry_operators = {}
|
||||
geography_operators = {}
|
||||
geography_functions = {}
|
||||
gis_terms = {}
|
||||
|
||||
# Quick booleans for the type of this spatial backend, and
|
||||
# an attribute for the spatial database version tuple (if applicable)
|
||||
postgis = False
|
||||
spatialite = False
|
||||
mysql = False
|
||||
oracle = False
|
||||
spatial_version = None
|
||||
|
||||
# How the geometry column should be selected.
|
||||
select = None
|
||||
|
||||
# Does the spatial database have a geography type?
|
||||
geography = False
|
||||
|
||||
area = False
|
||||
centroid = False
|
||||
difference = False
|
||||
distance = False
|
||||
distance_sphere = False
|
||||
distance_spheroid = False
|
||||
envelope = False
|
||||
force_rhr = False
|
||||
mem_size = False
|
||||
bounding_circle = False
|
||||
num_geom = False
|
||||
num_points = False
|
||||
perimeter = False
|
||||
perimeter3d = False
|
||||
point_on_surface = False
|
||||
polygonize = False
|
||||
scale = False
|
||||
snap_to_grid = False
|
||||
sym_difference = False
|
||||
transform = False
|
||||
translate = False
|
||||
union = False
|
||||
|
||||
# Aggregates
|
||||
collect = False
|
||||
extent = False
|
||||
extent3d = False
|
||||
make_line = False
|
||||
unionagg = False
|
||||
|
||||
# Serialization
|
||||
geohash = False
|
||||
geojson = False
|
||||
gml = False
|
||||
kml = False
|
||||
svg = False
|
||||
|
||||
# Constructors
|
||||
from_text = False
|
||||
from_wkb = False
|
||||
|
||||
# Default conversion functions for aggregates; will be overridden if implemented
|
||||
# for the spatial backend.
|
||||
def convert_extent(self, box):
|
||||
raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
|
||||
|
||||
def convert_extent3d(self, box):
|
||||
raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.')
|
||||
|
||||
def convert_geom(self, geom_val, geom_field):
|
||||
raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
|
||||
|
||||
# For quoting column values, rather than columns.
|
||||
def geo_quote_name(self, name):
|
||||
if isinstance(name, unicode):
|
||||
name = name.encode('ascii')
|
||||
return "'%s'" % name
|
||||
|
||||
# GeometryField operations
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Returns the database column type for the geometry field on
|
||||
the spatial backend.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Returns the distance parameters for the given geometry field,
|
||||
lookup value, and lookup type.
|
||||
"""
|
||||
raise NotImplementedError('Distance operations not available on this spatial backend.')
|
||||
|
||||
def get_geom_placeholder(self, f, value):
|
||||
"""
|
||||
Returns the placeholder for the given geometry field with the given
|
||||
value. Depending on the spatial backend, the placeholder may contain a
|
||||
stored procedure call to the transformation function of the spatial
|
||||
backend.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# Spatial SQL Construction
|
||||
def spatial_aggregate_sql(self, agg):
|
||||
raise NotImplementedError('Aggregate support not implemented for this spatial backend.')
|
||||
|
||||
def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
|
||||
raise NotImplmentedError
|
||||
|
||||
class SpatialRefSysMixin(object):
|
||||
"""
|
||||
The SpatialRefSysMixin is a class used by the database-dependent
|
||||
SpatialRefSys objects to reduce redundnant code.
|
||||
"""
|
||||
# For pulling out the spheroid from the spatial reference string. This
|
||||
# regular expression is used only if the user does not have GDAL installed.
|
||||
# TODO: Flattening not used in all ellipsoids, could also be a minor axis,
|
||||
# or 'b' parameter.
|
||||
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
|
||||
|
||||
# For pulling out the units on platforms w/o GDAL installed.
|
||||
# TODO: Figure out how to pull out angular units of projected coordinate system and
|
||||
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
|
||||
# distance queries.
|
||||
units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$')
|
||||
|
||||
@property
|
||||
def srs(self):
|
||||
"""
|
||||
Returns a GDAL SpatialReference object, if GDAL is installed.
|
||||
"""
|
||||
if gdal.HAS_GDAL:
|
||||
# TODO: Is caching really necessary here? Is complexity worth it?
|
||||
if hasattr(self, '_srs'):
|
||||
# Returning a clone of the cached SpatialReference object.
|
||||
return self._srs.clone()
|
||||
else:
|
||||
# Attempting to cache a SpatialReference object.
|
||||
|
||||
# Trying to get from WKT first.
|
||||
try:
|
||||
self._srs = gdal.SpatialReference(self.wkt)
|
||||
return self.srs
|
||||
except Exception, msg:
|
||||
pass
|
||||
|
||||
try:
|
||||
self._srs = gdal.SpatialReference(self.proj4text)
|
||||
return self.srs
|
||||
except Exception, msg:
|
||||
pass
|
||||
|
||||
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
|
||||
else:
|
||||
raise Exception('GDAL is not installed.')
|
||||
|
||||
@property
|
||||
def ellipsoid(self):
|
||||
"""
|
||||
Returns a tuple of the ellipsoid parameters:
|
||||
(semimajor axis, semiminor axis, and inverse flattening).
|
||||
"""
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.ellipsoid
|
||||
else:
|
||||
m = self.spheroid_regex.match(self.wkt)
|
||||
if m: return (float(m.group('major')), float(m.group('flattening')))
|
||||
else: return None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"Returns the projection name."
|
||||
return self.srs.name
|
||||
|
||||
@property
|
||||
def spheroid(self):
|
||||
"Returns the spheroid name for this spatial reference."
|
||||
return self.srs['spheroid']
|
||||
|
||||
@property
|
||||
def datum(self):
|
||||
"Returns the datum for this spatial reference."
|
||||
return self.srs['datum']
|
||||
|
||||
@property
|
||||
def projected(self):
|
||||
"Is this Spatial Reference projected?"
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.projected
|
||||
else:
|
||||
return self.wkt.startswith('PROJCS')
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
"Is this Spatial Reference local?"
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.local
|
||||
else:
|
||||
return self.wkt.startswith('LOCAL_CS')
|
||||
|
||||
@property
|
||||
def geographic(self):
|
||||
"Is this Spatial Reference geographic?"
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.geographic
|
||||
else:
|
||||
return self.wkt.startswith('GEOGCS')
|
||||
|
||||
@property
|
||||
def linear_name(self):
|
||||
"Returns the linear units name."
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.linear_name
|
||||
elif self.geographic:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit_name')
|
||||
|
||||
@property
|
||||
def linear_units(self):
|
||||
"Returns the linear units."
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.linear_units
|
||||
elif self.geographic:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit')
|
||||
|
||||
@property
|
||||
def angular_name(self):
|
||||
"Returns the name of the angular units."
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.angular_name
|
||||
elif self.projected:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit_name')
|
||||
|
||||
@property
|
||||
def angular_units(self):
|
||||
"Returns the angular units."
|
||||
if gdal.HAS_GDAL:
|
||||
return self.srs.angular_units
|
||||
elif self.projected:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit')
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"Returns a tuple of the units and the name."
|
||||
if self.projected or self.local:
|
||||
return (self.linear_units, self.linear_name)
|
||||
elif self.geographic:
|
||||
return (self.angular_units, self.angular_name)
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
@classmethod
|
||||
def get_units(cls, wkt):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrive the units on the given WKT, without having to use
|
||||
any of the database fields.
|
||||
"""
|
||||
if gdal.HAS_GDAL:
|
||||
return gdal.SpatialReference(wkt).units
|
||||
else:
|
||||
m = cls.units_regex.match(wkt)
|
||||
return m.group('unit'), m.group('unit_name')
|
||||
|
||||
@classmethod
|
||||
def get_spheroid(cls, wkt, string=True):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
||||
"""
|
||||
if gdal.HAS_GDAL:
|
||||
srs = gdal.SpatialReference(wkt)
|
||||
sphere_params = srs.ellipsoid
|
||||
sphere_name = srs['spheroid']
|
||||
else:
|
||||
m = cls.spheroid_regex.match(wkt)
|
||||
if m:
|
||||
sphere_params = (float(m.group('major')), float(m.group('flattening')))
|
||||
sphere_name = m.group('name')
|
||||
else:
|
||||
return None
|
||||
|
||||
if not string:
|
||||
return sphere_name, sphere_params
|
||||
else:
|
||||
# `string` parameter used to place in format acceptable by PostGIS
|
||||
if len(sphere_params) == 3:
|
||||
radius, flattening = sphere_params[0], sphere_params[2]
|
||||
else:
|
||||
radius, flattening = sphere_params
|
||||
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Returns the string representation. If GDAL is installed,
|
||||
it will be 'pretty' OGC WKT.
|
||||
"""
|
||||
try:
|
||||
return unicode(self.srs)
|
||||
except:
|
||||
return unicode(self.wkt)
|
|
@ -0,0 +1,11 @@
|
|||
from django.db.backends.mysql.base import *
|
||||
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper
|
||||
from django.contrib.gis.db.backends.mysql.creation import MySQLCreation
|
||||
from django.contrib.gis.db.backends.mysql.operations import MySQLOperations
|
||||
|
||||
class DatabaseWrapper(MySQLDatabaseWrapper):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DatabaseWrapper, self).__init__(*args, **kwargs)
|
||||
self.creation = MySQLCreation(self)
|
||||
self.ops = MySQLOperations()
|
|
@ -0,0 +1,18 @@
|
|||
from django.db.backends.mysql.creation import DatabaseCreation
|
||||
|
||||
class MySQLCreation(DatabaseCreation):
|
||||
|
||||
def sql_indexes_for_field(self, model, f, style):
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
output = super(MySQLCreation, self).sql_indexes_for_field(model, f, style)
|
||||
|
||||
if isinstance(f, GeometryField):
|
||||
qn = self.connection.ops.quote_name
|
||||
db_table = model._meta.db_table
|
||||
idx_name = '%s_%s_id' % (db_table, f.column)
|
||||
output.append(style.SQL_KEYWORD('CREATE SPATIAL INDEX ') +
|
||||
style.SQL_TABLE(qn(idx_name)) +
|
||||
style.SQL_KEYWORD(' ON ') +
|
||||
style.SQL_TABLE(qn(db_table)) + '(' +
|
||||
style.SQL_FIELD(qn(f.column)) + ');')
|
||||
return output
|
|
@ -0,0 +1,64 @@
|
|||
from django.db.backends.mysql.base import DatabaseOperations
|
||||
|
||||
from django.contrib.gis.db.backends.adapter import WKTAdapter
|
||||
from django.contrib.gis.db.backends.base import BaseSpatialOperations
|
||||
|
||||
class MySQLOperations(DatabaseOperations, BaseSpatialOperations):
|
||||
|
||||
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
|
||||
mysql = True
|
||||
name = 'mysql'
|
||||
select = 'AsText(%s)'
|
||||
from_wkb = 'GeomFromWKB'
|
||||
from_text = 'GeomFromText'
|
||||
|
||||
Adapter = WKTAdapter
|
||||
|
||||
geometry_functions = {
|
||||
'bbcontains' : 'MBRContains', # For consistency w/PostGIS API
|
||||
'bboverlaps' : 'MBROverlaps', # .. ..
|
||||
'contained' : 'MBRWithin', # .. ..
|
||||
'contains' : 'MBRContains',
|
||||
'disjoint' : 'MBRDisjoint',
|
||||
'equals' : 'MBREqual',
|
||||
'exact' : 'MBREqual',
|
||||
'intersects' : 'MBRIntersects',
|
||||
'overlaps' : 'MBROverlaps',
|
||||
'same_as' : 'MBREqual',
|
||||
'touches' : 'MBRTouches',
|
||||
'within' : 'MBRWithin',
|
||||
}
|
||||
|
||||
gis_terms = dict([(term, None) for term in geometry_functions.keys() + ['isnull']])
|
||||
|
||||
def geo_db_type(self, f):
|
||||
return f.geom_type
|
||||
|
||||
def get_geom_placeholder(self, value, srid):
|
||||
"""
|
||||
The placeholder here has to include MySQL's WKT constructor. Because
|
||||
MySQL does not support spatial transformations, there is no need to
|
||||
modify the placeholder based on the contents of the given value.
|
||||
"""
|
||||
if hasattr(value, 'expression'):
|
||||
placeholder = '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
|
||||
else:
|
||||
placeholder = '%s(%%s)' % self.from_text
|
||||
return placeholder
|
||||
|
||||
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
|
||||
alias, col, db_type = lvalue
|
||||
|
||||
geo_col = '%s.%s' % (qn(alias), qn(col))
|
||||
|
||||
lookup_info = self.geometry_functions.get(lookup_type, False)
|
||||
if lookup_info:
|
||||
return "%s(%s, %s)" % (lookup_info, geo_col,
|
||||
self.get_geom_placeholder(value, field.srid))
|
||||
|
||||
# TODO: Is this really necessary? MySQL can't handle NULL geometries
|
||||
# in its spatial indexes anyways.
|
||||
if lookup_type == 'isnull':
|
||||
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
|
@ -0,0 +1,5 @@
|
|||
from cx_Oracle import CLOB
|
||||
from django.contrib.gis.db.backends.adapter import WKTAdapter
|
||||
|
||||
class OracleSpatialAdapter(WKTAdapter):
|
||||
input_size = CLOB
|
|
@ -0,0 +1,10 @@
|
|||
from django.db.backends.oracle.base import *
|
||||
from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper
|
||||
from django.contrib.gis.db.backends.oracle.creation import OracleCreation
|
||||
from django.contrib.gis.db.backends.oracle.operations import OracleOperations
|
||||
|
||||
class DatabaseWrapper(OracleDatabaseWrapper):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DatabaseWrapper, self).__init__(*args, **kwargs)
|
||||
self.creation = OracleCreation(self)
|
||||
self.ops = OracleOperations(self)
|
|
@ -0,0 +1,44 @@
|
|||
from django.contrib.gis.db.models.sql.compiler import GeoSQLCompiler as BaseGeoSQLCompiler
|
||||
from django.db.backends.oracle import compiler
|
||||
|
||||
SQLCompiler = compiler.SQLCompiler
|
||||
|
||||
class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
|
||||
def placeholder(self, field, val):
|
||||
if field is None:
|
||||
# A field value of None means the value is raw.
|
||||
return val
|
||||
elif hasattr(field, 'get_placeholder'):
|
||||
# Some fields (e.g. geo fields) need special munging before
|
||||
# they can be inserted.
|
||||
ph = field.get_placeholder(val, self.connection)
|
||||
if ph == 'NULL':
|
||||
# If the placeholder returned is 'NULL', then we need to
|
||||
# to remove None from the Query parameters. Specifically,
|
||||
# cx_Oracle will assume a CHAR type when a placeholder ('%s')
|
||||
# is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use
|
||||
# 'NULL' for the value, and remove None from the query params.
|
||||
# See also #10888.
|
||||
param_idx = self.query.columns.index(field.column)
|
||||
params = list(self.query.params)
|
||||
params.pop(param_idx)
|
||||
self.query.params = tuple(params)
|
||||
return ph
|
||||
else:
|
||||
# Return the common case for the placeholder
|
||||
return '%s'
|
||||
|
||||
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler):
|
||||
pass
|
|
@ -0,0 +1,42 @@
|
|||
from django.db.backends.oracle.creation import DatabaseCreation
|
||||
from django.db.backends.util import truncate_name
|
||||
|
||||
class OracleCreation(DatabaseCreation):
|
||||
|
||||
def sql_indexes_for_field(self, model, f, style):
|
||||
"Return any spatial index creation SQL for the field."
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
|
||||
output = super(OracleCreation, self).sql_indexes_for_field(model, f, style)
|
||||
|
||||
if isinstance(f, GeometryField):
|
||||
gqn = self.connection.ops.geo_quote_name
|
||||
qn = self.connection.ops.quote_name
|
||||
db_table = model._meta.db_table
|
||||
|
||||
output.append(style.SQL_KEYWORD('INSERT INTO ') +
|
||||
style.SQL_TABLE('USER_SDO_GEOM_METADATA') +
|
||||
' (%s, %s, %s, %s)\n ' % tuple(map(qn, ['TABLE_NAME', 'COLUMN_NAME', 'DIMINFO', 'SRID'])) +
|
||||
style.SQL_KEYWORD(' VALUES ') + '(\n ' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ',\n ' +
|
||||
style.SQL_FIELD(gqn(f.column)) + ',\n ' +
|
||||
style.SQL_KEYWORD("MDSYS.SDO_DIM_ARRAY") + '(\n ' +
|
||||
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
|
||||
("('LONG', %s, %s, %s),\n " % (f._extent[0], f._extent[2], f._tolerance)) +
|
||||
style.SQL_KEYWORD("MDSYS.SDO_DIM_ELEMENT") +
|
||||
("('LAT', %s, %s, %s)\n ),\n" % (f._extent[1], f._extent[3], f._tolerance)) +
|
||||
' %s\n );' % f.srid)
|
||||
|
||||
if f.spatial_index:
|
||||
# Getting the index name, Oracle doesn't allow object
|
||||
# names > 30 characters.
|
||||
idx_name = truncate_name('%s_%s_id' % (db_table, f.column), 30)
|
||||
|
||||
output.append(style.SQL_KEYWORD('CREATE INDEX ') +
|
||||
style.SQL_TABLE(qn(idx_name)) +
|
||||
style.SQL_KEYWORD(' ON ') +
|
||||
style.SQL_TABLE(qn(db_table)) + '(' +
|
||||
style.SQL_FIELD(qn(f.column)) + ') ' +
|
||||
style.SQL_KEYWORD('INDEXTYPE IS ') +
|
||||
style.SQL_TABLE('MDSYS.SPATIAL_INDEX') + ';')
|
||||
return output
|
|
@ -7,7 +7,9 @@
|
|||
For example, the `USER_SDO_GEOM_METADATA` is used for the GeometryColumns
|
||||
model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
from django.contrib.gis.db.backends.base import SpatialRefSysMixin
|
||||
|
||||
class GeometryColumns(models.Model):
|
||||
"Maps to the Oracle USER_SDO_GEOM_METADATA table."
|
||||
|
@ -39,17 +41,20 @@ class GeometryColumns(models.Model):
|
|||
def __unicode__(self):
|
||||
return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid)
|
||||
|
||||
class SpatialRefSys(models.Model):
|
||||
class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"Maps to the Oracle MDSYS.CS_SRS table."
|
||||
cs_name = models.CharField(max_length=68)
|
||||
srid = models.IntegerField(primary_key=True)
|
||||
auth_srid = models.IntegerField()
|
||||
auth_name = models.CharField(max_length=256)
|
||||
wktext = models.CharField(max_length=2046)
|
||||
#cs_bounds = models.GeometryField() # TODO
|
||||
# Optional geometry representing the bounds of this coordinate
|
||||
# system. By default, all are NULL in the table.
|
||||
cs_bounds = models.PolygonField(null=True)
|
||||
objects = models.GeoManager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
app_label = 'gis'
|
||||
db_table = 'CS_SRS'
|
||||
managed = False
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for Oracle Spatial.
|
||||
|
||||
Please note that WKT support is broken on the XE version, and thus
|
||||
this backend will not work on such platforms. Specifically, XE lacks
|
||||
support for an internal JVM, and Java libraries are required to use
|
||||
the WKT constructors.
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.backends.oracle.base import DatabaseOperations
|
||||
from django.contrib.gis.db.backends.base import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter
|
||||
from django.contrib.gis.db.backends.util import SpatialFunction
|
||||
from django.contrib.gis.geometry.backend import Geometry
|
||||
from django.contrib.gis.measure import Distance
|
||||
|
||||
class SDOOperation(SpatialFunction):
|
||||
"Base class for SDO* Oracle operations."
|
||||
sql_template = "%(function)s(%(geo_col)s, %(geometry)s) %(operator)s '%(result)s'"
|
||||
|
||||
def __init__(self, func, **kwargs):
|
||||
kwargs.setdefault('operator', '=')
|
||||
kwargs.setdefault('result', 'TRUE')
|
||||
super(SDOOperation, self).__init__(func, **kwargs)
|
||||
|
||||
class SDODistance(SpatialFunction):
|
||||
"Class for Distance queries."
|
||||
sql_template = ('%(function)s(%(geo_col)s, %(geometry)s, %(tolerance)s) '
|
||||
'%(operator)s %(result)s')
|
||||
dist_func = 'SDO_GEOM.SDO_DISTANCE'
|
||||
def __init__(self, op, tolerance=0.05):
|
||||
super(SDODistance, self).__init__(self.dist_func,
|
||||
tolerance=tolerance,
|
||||
operator=op, result='%s')
|
||||
|
||||
class SDODWithin(SpatialFunction):
|
||||
dwithin_func = 'SDO_WITHIN_DISTANCE'
|
||||
sql_template = "%(function)s(%(geo_col)s, %(geometry)s, %%s) = 'TRUE'"
|
||||
def __init__(self):
|
||||
super(SDODWithin, self).__init__(self.dwithin_func)
|
||||
|
||||
class SDOGeomRelate(SpatialFunction):
|
||||
"Class for using SDO_GEOM.RELATE."
|
||||
relate_func = 'SDO_GEOM.RELATE'
|
||||
sql_template = ("%(function)s(%(geo_col)s, '%(mask)s', %(geometry)s, "
|
||||
"%(tolerance)s) %(operator)s '%(mask)s'")
|
||||
def __init__(self, mask, tolerance=0.05):
|
||||
# SDO_GEOM.RELATE(...) has a peculiar argument order: column, mask, geom, tolerance.
|
||||
# Moreover, the runction result is the mask (e.g., 'DISJOINT' instead of 'TRUE').
|
||||
super(SDOGeomRelate, self).__init__(self.relate_func, operator='=',
|
||||
mask=mask, tolerance=tolerance)
|
||||
|
||||
class SDORelate(SpatialFunction):
|
||||
"Class for using SDO_RELATE."
|
||||
masks = 'TOUCH|OVERLAPBDYDISJOINT|OVERLAPBDYINTERSECT|EQUAL|INSIDE|COVEREDBY|CONTAINS|COVERS|ANYINTERACT|ON'
|
||||
mask_regex = re.compile(r'^(%s)(\+(%s))*$' % (masks, masks), re.I)
|
||||
sql_template = "%(function)s(%(geo_col)s, %(geometry)s, 'mask=%(mask)s)' = 'TRUE'"
|
||||
relate_func = 'SDO_RELATE'
|
||||
def __init__(self, mask):
|
||||
if not self.mask_regex.match(mask):
|
||||
raise ValueError('Invalid %s mask: "%s"' % (self.relate_func, mask))
|
||||
super(SDORelate, self).__init__(self.relate_func, mask=mask)
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
|
||||
class OracleOperations(DatabaseOperations, BaseSpatialOperations):
|
||||
compiler_module = "django.contrib.gis.db.backends.oracle.compiler"
|
||||
|
||||
name = 'oracle'
|
||||
oracle = True
|
||||
valid_aggregates = dict([(a, None) for a in ('Union', 'Extent')])
|
||||
|
||||
Adapter = OracleSpatialAdapter
|
||||
|
||||
area = 'SDO_GEOM.SDO_AREA'
|
||||
gml= 'SDO_UTIL.TO_GMLGEOMETRY'
|
||||
centroid = 'SDO_GEOM.SDO_CENTROID'
|
||||
difference = 'SDO_GEOM.SDO_DIFFERENCE'
|
||||
distance = 'SDO_GEOM.SDO_DISTANCE'
|
||||
extent= 'SDO_AGGR_MBR'
|
||||
intersection= 'SDO_GEOM.SDO_INTERSECTION'
|
||||
length = 'SDO_GEOM.SDO_LENGTH'
|
||||
num_geom = 'SDO_UTIL.GETNUMELEM'
|
||||
num_points = 'SDO_UTIL.GETNUMVERTICES'
|
||||
perimeter = length
|
||||
point_on_surface = 'SDO_GEOM.SDO_POINTONSURFACE'
|
||||
sym_difference = 'SDO_GEOM.SDO_XOR'
|
||||
transform = 'SDO_CS.TRANSFORM'
|
||||
union = 'SDO_GEOM.SDO_UNION'
|
||||
unionagg = 'SDO_AGGR_UNION'
|
||||
|
||||
# We want to get SDO Geometries as WKT because it is much easier to
|
||||
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
||||
# However, this adversely affects performance (i.e., Java is called
|
||||
# to convert to WKT on every query). If someone wishes to write a
|
||||
# SDO_GEOMETRY(...) parser in Python, let me know =)
|
||||
select = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
||||
|
||||
distance_functions = {
|
||||
'distance_gt' : (SDODistance('>'), dtypes),
|
||||
'distance_gte' : (SDODistance('>='), dtypes),
|
||||
'distance_lt' : (SDODistance('<'), dtypes),
|
||||
'distance_lte' : (SDODistance('<='), dtypes),
|
||||
'dwithin' : (SDODWithin(), dtypes),
|
||||
}
|
||||
|
||||
geometry_functions = {
|
||||
'contains' : SDOOperation('SDO_CONTAINS'),
|
||||
'coveredby' : SDOOperation('SDO_COVEREDBY'),
|
||||
'covers' : SDOOperation('SDO_COVERS'),
|
||||
'disjoint' : SDOGeomRelate('DISJOINT'),
|
||||
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
|
||||
'equals' : SDOOperation('SDO_EQUAL'),
|
||||
'exact' : SDOOperation('SDO_EQUAL'),
|
||||
'overlaps' : SDOOperation('SDO_OVERLAPS'),
|
||||
'same_as' : SDOOperation('SDO_EQUAL'),
|
||||
'relate' : (SDORelate, basestring), # Oracle uses a different syntax, e.g., 'mask=inside+touch'
|
||||
'touches' : SDOOperation('SDO_TOUCH'),
|
||||
'within' : SDOOperation('SDO_INSIDE'),
|
||||
}
|
||||
geometry_functions.update(distance_functions)
|
||||
|
||||
gis_terms = ['isnull']
|
||||
gis_terms += geometry_functions.keys()
|
||||
gis_terms = dict([(term, None) for term in gis_terms])
|
||||
|
||||
def __init__(self, connection):
|
||||
super(OracleOperations, self).__init__()
|
||||
self.connection = connection
|
||||
|
||||
def convert_extent(self, clob):
|
||||
if clob:
|
||||
# Generally, Oracle returns a polygon for the extent -- however,
|
||||
# it can return a single point if there's only one Point in the
|
||||
# table.
|
||||
ext_geom = Geometry(clob.read())
|
||||
gtype = str(ext_geom.geom_type)
|
||||
if gtype == 'Polygon':
|
||||
# Construct the 4-tuple from the coordinates in the polygon.
|
||||
shell = ext_geom.shell
|
||||
ll, ur = shell[0][:2], shell[2][:2]
|
||||
elif gtype == 'Point':
|
||||
ll = ext_geom.coords[:2]
|
||||
ur = ll
|
||||
else:
|
||||
raise Exception('Unexpected geometry type returned for extent: %s' % gtype)
|
||||
xmin, ymin = ll
|
||||
xmax, ymax = ur
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
else:
|
||||
return None
|
||||
|
||||
def convert_geom(self, clob, geo_field):
|
||||
if clob:
|
||||
return Geometry(clob.read(), geo_field.srid)
|
||||
else:
|
||||
return None
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Returns the geometry database type for Oracle. Unlike other spatial
|
||||
backends, no stored procedure is necessary and it's the same for all
|
||||
geometry types.
|
||||
"""
|
||||
return 'MDSYS.SDO_GEOMETRY'
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Returns the distance parameters given the value and the lookup type.
|
||||
On Oracle, geometry columns with a geodetic coordinate system behave
|
||||
implicitly like a geography column, and thus meters will be used as
|
||||
the distance parameter on them.
|
||||
"""
|
||||
if not value:
|
||||
return []
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
|
||||
else:
|
||||
dist_param = value
|
||||
|
||||
# dwithin lookups on oracle require a special string parameter
|
||||
# that starts with "distance=".
|
||||
if lookup_type == 'dwithin':
|
||||
dist_param = 'distance=%s' % dist_param
|
||||
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
SDO_CS.TRANSFORM() function call.
|
||||
"""
|
||||
if value is None:
|
||||
return 'NULL'
|
||||
|
||||
def transform_value(val, srid):
|
||||
return val.srid != srid
|
||||
|
||||
if hasattr(value, 'expression'):
|
||||
if transform_value(value, f.srid):
|
||||
placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
|
||||
else:
|
||||
placeholder = '%s'
|
||||
# No geometry value used for F expression, substitue in
|
||||
# the column name instead.
|
||||
return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
|
||||
else:
|
||||
if transform_value(value, f.srid):
|
||||
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (self.transform, value.srid, f.srid)
|
||||
else:
|
||||
return 'SDO_GEOMETRY(%%s, %s)' % f.srid
|
||||
|
||||
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
|
||||
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
|
||||
alias, col, db_type = lvalue
|
||||
|
||||
# Getting the quoted table name as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(alias), qn(col))
|
||||
|
||||
# See if a Oracle Geometry function matches the lookup type next
|
||||
lookup_info = self.geometry_functions.get(lookup_type, False)
|
||||
if lookup_info:
|
||||
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||
# 'dwithin' lookup types.
|
||||
if isinstance(lookup_info, tuple):
|
||||
# First element of tuple is lookup type, second element is the type
|
||||
# of the expected argument (e.g., str, float)
|
||||
sdo_op, arg_type = lookup_info
|
||||
geom = value[0]
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(value, tuple):
|
||||
raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
if len(value) != 2:
|
||||
raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(value[1], arg_type):
|
||||
raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||
|
||||
if lookup_type == 'relate':
|
||||
# The SDORelate class handles construction for these queries,
|
||||
# and verifies the mask argument.
|
||||
return sdo_op(value[1]).as_sql(geo_col, self.get_geom_placeholder(field, geom))
|
||||
else:
|
||||
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
|
||||
return sdo_op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
|
||||
else:
|
||||
# Lookup info is a SDOOperation instance, whose `as_sql` method returns
|
||||
# the SQL necessary for the geometry function call. For example:
|
||||
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
|
||||
return lookup_info.as_sql(geo_col, self.get_geom_placeholder(field, value))
|
||||
elif lookup_type == 'isnull':
|
||||
# Handling 'isnull' lookup type
|
||||
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
||||
|
||||
def spatial_aggregate_sql(self, agg):
|
||||
"""
|
||||
Returns the spatial aggregate SQL template and function for the
|
||||
given Aggregate instance.
|
||||
"""
|
||||
agg_name = agg.__class__.__name__.lower()
|
||||
if agg_name == 'union' : agg_name += 'agg'
|
||||
if agg.is_extent:
|
||||
sql_template = '%(function)s(%(field)s)'
|
||||
else:
|
||||
sql_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))'
|
||||
sql_function = getattr(self, agg_name)
|
||||
return self.select % sql_template, sql_function
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
from django.contrib.gis.db.backends.oracle.models import GeometryColumns
|
||||
return GeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
|
||||
return SpatialRefSys
|
|
@ -2,11 +2,10 @@
|
|||
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
|
||||
"""
|
||||
|
||||
from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_EWKB
|
||||
from psycopg2 import Binary
|
||||
from psycopg2.extensions import ISQLQuote
|
||||
|
||||
class PostGISAdaptor(object):
|
||||
class PostGISAdapter(object):
|
||||
def __init__(self, geom):
|
||||
"Initializes on the geometry."
|
||||
# Getting the WKB (in string form, to allow easy pickling of
|
||||
|
@ -22,7 +21,7 @@ class PostGISAdaptor(object):
|
|||
raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?')
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.wkb == other.wkb) and (self.srid == other.srid)
|
||||
return (self.ewkb == other.ewkb) and (self.srid == other.srid)
|
||||
|
||||
def __str__(self):
|
||||
return self.getquoted()
|
||||
|
@ -30,7 +29,7 @@ class PostGISAdaptor(object):
|
|||
def getquoted(self):
|
||||
"Returns a properly quoted string for use in PostgreSQL/PostGIS."
|
||||
# Want to use WKB, so wrap with psycopg2 Binary() to quote properly.
|
||||
return "%s(E%s)" % (GEOM_FROM_EWKB, Binary(self.ewkb))
|
||||
return 'ST_GeomFromEWKB(E%s)' % Binary(self.ewkb)
|
||||
|
||||
def prepare_database_save(self, unused):
|
||||
return self
|
|
@ -0,0 +1,10 @@
|
|||
from django.db.backends.postgresql_psycopg2.base import *
|
||||
from django.db.backends.postgresql_psycopg2.base import DatabaseWrapper as Psycopg2DatabaseWrapper
|
||||
from django.contrib.gis.db.backends.postgis.creation import PostGISCreation
|
||||
from django.contrib.gis.db.backends.postgis.operations import PostGISOperations
|
||||
|
||||
class DatabaseWrapper(Psycopg2DatabaseWrapper):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DatabaseWrapper, self).__init__(*args, **kwargs)
|
||||
self.creation = PostGISCreation(self)
|
||||
self.ops = PostGISOperations(self)
|
|
@ -0,0 +1,60 @@
|
|||
from django.conf import settings
|
||||
from django.db.backends.postgresql.creation import DatabaseCreation
|
||||
|
||||
class PostGISCreation(DatabaseCreation):
|
||||
geom_index_type = 'GIST'
|
||||
geom_index_opts = 'GIST_GEOMETRY_OPS'
|
||||
|
||||
def sql_indexes_for_field(self, model, f, style):
|
||||
"Return any spatial index creation SQL for the field."
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
|
||||
output = super(PostGISCreation, self).sql_indexes_for_field(model, f, style)
|
||||
|
||||
if isinstance(f, GeometryField):
|
||||
gqn = self.connection.ops.geo_quote_name
|
||||
qn = self.connection.ops.quote_name
|
||||
db_table = model._meta.db_table
|
||||
|
||||
if f.geography:
|
||||
# Geogrophy columns are created normally.
|
||||
pass
|
||||
else:
|
||||
# Geometry columns are created by `AddGeometryColumn`
|
||||
# stored procedure.
|
||||
output.append(style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_TABLE('AddGeometryColumn') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(f.column)) + ', ' +
|
||||
style.SQL_FIELD(str(f.srid)) + ', ' +
|
||||
style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' +
|
||||
style.SQL_KEYWORD(str(f.dim)) + ');')
|
||||
|
||||
if not f.null:
|
||||
# Add a NOT NULL constraint to the field
|
||||
output.append(style.SQL_KEYWORD('ALTER TABLE ') +
|
||||
style.SQL_TABLE(qn(db_table)) +
|
||||
style.SQL_KEYWORD(' ALTER ') +
|
||||
style.SQL_FIELD(qn(f.column)) +
|
||||
style.SQL_KEYWORD(' SET NOT NULL') + ';')
|
||||
|
||||
|
||||
if f.spatial_index:
|
||||
# Spatial indexes created the same way for both Geometry and
|
||||
# Geography columns
|
||||
if f.geography:
|
||||
index_opts = ''
|
||||
else:
|
||||
index_opts = ' ' + style.SQL_KEYWORD(self.geom_index_opts)
|
||||
output.append(style.SQL_KEYWORD('CREATE INDEX ') +
|
||||
style.SQL_TABLE(qn('%s_%s_id' % (db_table, f.column))) +
|
||||
style.SQL_KEYWORD(' ON ') +
|
||||
style.SQL_TABLE(qn(db_table)) +
|
||||
style.SQL_KEYWORD(' USING ') +
|
||||
style.SQL_COLTYPE(self.geom_index_type) + ' ( ' +
|
||||
style.SQL_FIELD(qn(f.column)) + index_opts + ' );')
|
||||
return output
|
||||
|
||||
def sql_table_creation_suffix(self):
|
||||
qn = self.connection.ops.quote_name
|
||||
return ' TEMPLATE %s' % qn(getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis'))
|
|
@ -2,6 +2,7 @@
|
|||
The GeometryColumns and SpatialRefSys models for the PostGIS backend.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.gis.db.backends.base import SpatialRefSysMixin
|
||||
|
||||
class GeometryColumns(models.Model):
|
||||
"""
|
||||
|
@ -42,7 +43,7 @@ class GeometryColumns(models.Model):
|
|||
(self.f_table_name, self.f_geometry_column,
|
||||
self.coord_dimension, self.type, self.srid)
|
||||
|
||||
class SpatialRefSys(models.Model):
|
||||
class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"""
|
||||
The 'spatial_ref_sys' table from PostGIS. See the PostGIS
|
||||
documentaiton at Ch. 4.2.1.
|
||||
|
@ -54,7 +55,7 @@ class SpatialRefSys(models.Model):
|
|||
proj4text = models.CharField(max_length=2048)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
app_label = 'gis'
|
||||
db_table = 'spatial_ref_sys'
|
||||
managed = False
|
||||
|
|
@ -0,0 +1,570 @@
|
|||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.gis.db.backends.base import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
|
||||
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
|
||||
from django.contrib.gis.geometry.backend import Geometry
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.postgresql.operations import DatabaseOperations
|
||||
from django.db.backends.postgresql_psycopg2.base import Database
|
||||
|
||||
#### Classes used in constructing PostGIS spatial SQL ####
|
||||
class PostGISOperator(SpatialOperation):
|
||||
"For PostGIS operators (e.g. `&&`, `~`)."
|
||||
def __init__(self, operator):
|
||||
super(PostGISOperator, self).__init__(operator=operator)
|
||||
|
||||
class PostGISFunction(SpatialFunction):
|
||||
"For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
|
||||
def __init__(self, prefix, function, **kwargs):
|
||||
super(PostGISFunction, self).__init__(prefix + function, **kwargs)
|
||||
|
||||
class PostGISFunctionParam(PostGISFunction):
|
||||
"For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
|
||||
sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
|
||||
|
||||
class PostGISDistance(PostGISFunction):
|
||||
"For PostGIS distance operations."
|
||||
dist_func = 'Distance'
|
||||
sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
|
||||
|
||||
def __init__(self, prefix, operator):
|
||||
super(PostGISDistance, self).__init__(prefix, self.dist_func,
|
||||
operator=operator)
|
||||
|
||||
class PostGISSpheroidDistance(PostGISFunction):
|
||||
"For PostGIS spherical distance operations (using the spheroid)."
|
||||
dist_func = 'distance_spheroid'
|
||||
sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
|
||||
def __init__(self, prefix, operator):
|
||||
# An extra parameter in `end_subst` is needed for the spheroid string.
|
||||
super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
|
||||
operator=operator)
|
||||
|
||||
class PostGISSphereDistance(PostGISDistance):
|
||||
"For PostGIS spherical distance operations."
|
||||
dist_func = 'distance_sphere'
|
||||
|
||||
class PostGISRelate(PostGISFunctionParam):
|
||||
"For PostGIS Relate(<geom>, <pattern>) calls."
|
||||
pattern_regex = re.compile(r'^[012TF\*]{9}$')
|
||||
def __init__(self, prefix, pattern):
|
||||
if not self.pattern_regex.match(pattern):
|
||||
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
|
||||
super(PostGISRelate, self).__init__(prefix, 'Relate')
|
||||
|
||||
|
||||
class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
||||
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
|
||||
name = 'postgis'
|
||||
postgis = True
|
||||
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
||||
valid_aggregates = dict([(k, None) for k in
|
||||
('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
|
||||
|
||||
Adapter = PostGISAdapter
|
||||
|
||||
def __init__(self, connection):
|
||||
super(PostGISOperations, self).__init__(connection)
|
||||
|
||||
# Trying to get the PostGIS version because the function
|
||||
# signatures will depend on the version used. The cost
|
||||
# here is a database query to determine the version, which
|
||||
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
|
||||
# comprising user-supplied values for the major, minor, and
|
||||
# subminor revision of PostGIS.
|
||||
try:
|
||||
if hasattr(settings, 'POSTGIS_VERSION'):
|
||||
vtup = settings.POSTGIS_VERSION
|
||||
if len(vtup) == 3:
|
||||
# The user-supplied PostGIS version.
|
||||
version = vtup
|
||||
else:
|
||||
# This was the old documented way, but it's stupid to
|
||||
# include the string.
|
||||
version = vtup[1:4]
|
||||
else:
|
||||
vtup = self.postgis_version_tuple()
|
||||
version = vtup[1:]
|
||||
|
||||
# Getting the prefix -- even though we don't officially support
|
||||
# PostGIS 1.2 anymore, keeping it anyway in case a prefix change
|
||||
# for something else is necessary.
|
||||
if version >= (1, 2, 2):
|
||||
prefix = 'ST_'
|
||||
else:
|
||||
prefix = ''
|
||||
|
||||
self.geom_func_prefix = prefix
|
||||
self.spatial_version = version
|
||||
except Database.ProgrammingError:
|
||||
raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". '
|
||||
'GeoDjango requires at least PostGIS version 1.3. '
|
||||
'Was the database created from a spatial database '
|
||||
'template?' % self.connection.settings_dict['NAME']
|
||||
)
|
||||
except Exception, e:
|
||||
# TODO: Raise helpful exceptions as they become known.
|
||||
raise
|
||||
|
||||
# PostGIS-specific operators. The commented descriptions of these
|
||||
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
|
||||
self.geometry_operators = {
|
||||
# The "&<" operator returns true if A's bounding box overlaps or
|
||||
# is to the left of B's bounding box.
|
||||
'overlaps_left' : PostGISOperator('&<'),
|
||||
# The "&>" operator returns true if A's bounding box overlaps or
|
||||
# is to the right of B's bounding box.
|
||||
'overlaps_right' : PostGISOperator('&>'),
|
||||
# The "<<" operator returns true if A's bounding box is strictly
|
||||
# to the left of B's bounding box.
|
||||
'left' : PostGISOperator('<<'),
|
||||
# The ">>" operator returns true if A's bounding box is strictly
|
||||
# to the right of B's bounding box.
|
||||
'right' : PostGISOperator('>>'),
|
||||
# The "&<|" operator returns true if A's bounding box overlaps or
|
||||
# is below B's bounding box.
|
||||
'overlaps_below' : PostGISOperator('&<|'),
|
||||
# The "|&>" operator returns true if A's bounding box overlaps or
|
||||
# is above B's bounding box.
|
||||
'overlaps_above' : PostGISOperator('|&>'),
|
||||
# The "<<|" operator returns true if A's bounding box is strictly
|
||||
# below B's bounding box.
|
||||
'strictly_below' : PostGISOperator('<<|'),
|
||||
# The "|>>" operator returns true if A's bounding box is strictly
|
||||
# above B's bounding box.
|
||||
'strictly_above' : PostGISOperator('|>>'),
|
||||
# The "~=" operator is the "same as" operator. It tests actual
|
||||
# geometric equality of two features. So if A and B are the same feature,
|
||||
# vertex-by-vertex, the operator returns true.
|
||||
'same_as' : PostGISOperator('~='),
|
||||
'exact' : PostGISOperator('~='),
|
||||
# The "@" operator returns true if A's bounding box is completely contained
|
||||
# by B's bounding box.
|
||||
'contained' : PostGISOperator('@'),
|
||||
# The "~" operator returns true if A's bounding box completely contains
|
||||
# by B's bounding box.
|
||||
'bbcontains' : PostGISOperator('~'),
|
||||
# The "&&" operator returns true if A's bounding box overlaps
|
||||
# B's bounding box.
|
||||
'bboverlaps' : PostGISOperator('&&'),
|
||||
}
|
||||
|
||||
self.geometry_functions = {
|
||||
'equals' : PostGISFunction(prefix, 'Equals'),
|
||||
'disjoint' : PostGISFunction(prefix, 'Disjoint'),
|
||||
'touches' : PostGISFunction(prefix, 'Touches'),
|
||||
'crosses' : PostGISFunction(prefix, 'Crosses'),
|
||||
'within' : PostGISFunction(prefix, 'Within'),
|
||||
'overlaps' : PostGISFunction(prefix, 'Overlaps'),
|
||||
'contains' : PostGISFunction(prefix, 'Contains'),
|
||||
'intersects' : PostGISFunction(prefix, 'Intersects'),
|
||||
'relate' : (PostGISRelate, basestring),
|
||||
}
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
def get_dist_ops(operator):
|
||||
"Returns operations for both regular and spherical distances."
|
||||
return {'cartesian' : PostGISDistance(prefix, operator),
|
||||
'sphere' : PostGISSphereDistance(prefix, operator),
|
||||
'spheroid' : PostGISSpheroidDistance(prefix, operator),
|
||||
}
|
||||
self.distance_functions = {
|
||||
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||
'distance_lt' : (get_dist_ops('<'), dtypes),
|
||||
'distance_lte' : (get_dist_ops('<='), dtypes),
|
||||
}
|
||||
|
||||
# Versions 1.2.2+ have KML serialization support.
|
||||
if version < (1, 2, 2):
|
||||
ASKML = False
|
||||
else:
|
||||
ASKML = 'ST_AsKML'
|
||||
self.geometry_functions.update(
|
||||
{'coveredby' : PostGISFunction(prefix, 'CoveredBy'),
|
||||
'covers' : PostGISFunction(prefix, 'Covers'),
|
||||
})
|
||||
self.distance_functions['dwithin'] = (PostGISFunctionParam(prefix, 'DWithin'), dtypes)
|
||||
|
||||
# Adding the distance functions to the geometries lookup.
|
||||
self.geometry_functions.update(self.distance_functions)
|
||||
|
||||
# The union aggregate and topology operation use the same signature
|
||||
# in versions 1.3+.
|
||||
if version < (1, 3, 0):
|
||||
UNIONAGG = 'GeomUnion'
|
||||
UNION = 'Union'
|
||||
else:
|
||||
UNIONAGG = 'ST_Union'
|
||||
UNION = 'ST_Union'
|
||||
|
||||
# Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
|
||||
if version < (1, 3, 4):
|
||||
GEOJSON = False
|
||||
else:
|
||||
GEOJSON = prefix + 'AsGeoJson'
|
||||
|
||||
# ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
|
||||
if version >= (1, 4, 0):
|
||||
GEOHASH = 'ST_GeoHash'
|
||||
MAKELINE = 'ST_MakeLine'
|
||||
BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
|
||||
self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
|
||||
else:
|
||||
GEOHASH, MAKELINE, BOUNDINGCIRCLE = False, False, False
|
||||
|
||||
# Geography type support added in 1.5.
|
||||
if version >= (1, 5, 0):
|
||||
self.geography = True
|
||||
# Only a subset of the operators and functions are available
|
||||
# for the geography type.
|
||||
self.geography_functions = self.distance_functions.copy()
|
||||
self.geography_functions.update({
|
||||
'coveredby' : self.geometry_functions['coveredby'],
|
||||
'covers' : self.geometry_functions['covers'],
|
||||
'intersects' : self.geometry_functions['intersects'],
|
||||
})
|
||||
self.geography_operators = {
|
||||
'bboverlaps' : PostGISOperator('&&'),
|
||||
'exact' : PostGISOperator('~='),
|
||||
'same_as' : PostGISOperator('~='),
|
||||
}
|
||||
|
||||
# Creating a dictionary lookup of all GIS terms for PostGIS.
|
||||
gis_terms = ['isnull']
|
||||
gis_terms += self.geometry_operators.keys()
|
||||
gis_terms += self.geometry_functions.keys()
|
||||
self.gis_terms = dict([(term, None) for term in gis_terms])
|
||||
|
||||
self.area = prefix + 'Area'
|
||||
self.bounding_circle = BOUNDINGCIRCLE
|
||||
self.centroid = prefix + 'Centroid'
|
||||
self.collect = prefix + 'Collect'
|
||||
self.difference = prefix + 'Difference'
|
||||
self.distance = prefix + 'Distance'
|
||||
self.distance_sphere = prefix + 'distance_sphere'
|
||||
self.distance_spheroid = prefix + 'distance_spheroid'
|
||||
self.envelope = prefix + 'Envelope'
|
||||
self.extent = prefix + 'Extent'
|
||||
self.extent3d = prefix + 'Extent3D'
|
||||
self.geohash = GEOHASH
|
||||
self.geojson = GEOJSON
|
||||
self.gml = prefix + 'AsGML'
|
||||
self.intersection = prefix + 'Intersection'
|
||||
self.kml = ASKML
|
||||
self.length = prefix + 'Length'
|
||||
self.length3d = prefix + 'Length3D'
|
||||
self.length_spheroid = prefix + 'length_spheroid'
|
||||
self.makeline = MAKELINE
|
||||
self.mem_size = prefix + 'mem_size'
|
||||
self.num_geom = prefix + 'NumGeometries'
|
||||
self.num_points =prefix + 'npoints'
|
||||
self.perimeter = prefix + 'Perimeter'
|
||||
self.perimeter3d = prefix + 'Perimeter3D'
|
||||
self.point_on_surface = prefix + 'PointOnSurface'
|
||||
self.polygonize = prefix + 'Polygonize'
|
||||
self.scale = prefix + 'Scale'
|
||||
self.snap_to_grid = prefix + 'SnapToGrid'
|
||||
self.svg = prefix + 'AsSVG'
|
||||
self.sym_difference = prefix + 'SymDifference'
|
||||
self.transform = prefix + 'Transform'
|
||||
self.translate = prefix + 'Translate'
|
||||
self.union = UNION
|
||||
self.unionagg = UNIONAGG
|
||||
|
||||
def check_aggregate_support(self, aggregate):
|
||||
"""
|
||||
Checks if the given aggregate name is supported (that is, if it's
|
||||
in `self.valid_aggregates`).
|
||||
"""
|
||||
agg_name = aggregate.__class__.__name__
|
||||
return agg_name in self.valid_aggregates
|
||||
|
||||
def convert_extent(self, box):
|
||||
"""
|
||||
Returns a 4-tuple extent for the `Extent` aggregate by converting
|
||||
the bounding box text returned by PostGIS (`box` argument), for
|
||||
example: "BOX(-90.0 30.0, -85.0 40.0)".
|
||||
"""
|
||||
ll, ur = box[4:-1].split(',')
|
||||
xmin, ymin = map(float, ll.split())
|
||||
xmax, ymax = map(float, ur.split())
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
def convert_extent3d(self, box3d):
|
||||
"""
|
||||
Returns a 6-tuple extent for the `Extent3D` aggregate by converting
|
||||
the 3d bounding-box text returnded by PostGIS (`box3d` argument), for
|
||||
example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
|
||||
"""
|
||||
ll, ur = box3d[6:-1].split(',')
|
||||
xmin, ymin, zmin = map(float, ll.split())
|
||||
xmax, ymax, zmax = map(float, ur.split())
|
||||
return (xmin, ymin, zmin, xmax, ymax, zmax)
|
||||
|
||||
def convert_geom(self, hex, geo_field):
|
||||
"""
|
||||
Converts the geometry returned from PostGIS aggretates.
|
||||
"""
|
||||
if hex:
|
||||
return Geometry(hex)
|
||||
else:
|
||||
return None
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Return the database field type for the given geometry field.
|
||||
Typically this is `None` because geometry columns are added via
|
||||
the `AddGeometryColumn` stored procedure, unless the field
|
||||
has been specified to be of geography type instead.
|
||||
"""
|
||||
if f.geography:
|
||||
if not self.geography:
|
||||
raise NotImplementedError('PostGIS 1.5 required for geography column support.')
|
||||
|
||||
if f.srid != 4326:
|
||||
raise NotImplementedError('PostGIS 1.5 supports geography columns '
|
||||
'only with an SRID of 4326.')
|
||||
|
||||
return 'geography(%s,%d)'% (f.geom_type, f.srid)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_distance(self, f, dist_val, lookup_type):
|
||||
"""
|
||||
Retrieve the distance parameters for the given geometry field,
|
||||
distance lookup value, and the distance lookup type.
|
||||
|
||||
This is the most complex implementation of the spatial backends due to
|
||||
what is supported on geodetic geometry columns vs. what's available on
|
||||
projected geometry columns. In addition, it has to take into account
|
||||
the newly introduced geography column type introudced in PostGIS 1.5.
|
||||
"""
|
||||
# Getting the distance parameter and any options.
|
||||
if len(dist_val) == 1:
|
||||
value, option = dist_val[0], None
|
||||
else:
|
||||
value, option = dist_val
|
||||
|
||||
# Shorthand boolean flags.
|
||||
geodetic = f.geodetic(self.connection)
|
||||
geography = f.geography and self.geography
|
||||
|
||||
if isinstance(value, Distance):
|
||||
if geography:
|
||||
dist_param = value.m
|
||||
elif geodetic:
|
||||
if lookup_type == 'dwithin':
|
||||
raise ValueError('Only numeric values of degree units are '
|
||||
'allowed on geographic DWithin queries.')
|
||||
dist_param = value.m
|
||||
else:
|
||||
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
|
||||
else:
|
||||
# Assuming the distance is in the units of the field.
|
||||
dist_param = value
|
||||
|
||||
if (not geography and geodetic and lookup_type != 'dwithin'
|
||||
and option == 'spheroid'):
|
||||
# using distance_spheroid requires the spheroid of the field as
|
||||
# a parameter.
|
||||
return [f._spheroid, dist_param]
|
||||
else:
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
ST_Transform() function call.
|
||||
"""
|
||||
if value is None or value.srid == f.srid:
|
||||
placeholder = '%s'
|
||||
else:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
|
||||
|
||||
if hasattr(value, 'expression'):
|
||||
# If this is an F expression, then we don't really want
|
||||
# a placeholder and instead substitute in the column
|
||||
# of the expression.
|
||||
placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
|
||||
|
||||
return placeholder
|
||||
|
||||
def _get_postgis_func(self, func):
|
||||
"""
|
||||
Helper routine for calling PostGIS functions and returning their result.
|
||||
"""
|
||||
cursor = self.connection._cursor()
|
||||
try:
|
||||
cursor.execute('SELECT %s()' % func)
|
||||
row = cursor.fetchone()
|
||||
except:
|
||||
# Responsibility of callers to perform error handling.
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
return row[0]
|
||||
|
||||
def postgis_geos_version(self):
|
||||
"Returns the version of the GEOS library used with PostGIS."
|
||||
return self._get_postgis_func('postgis_geos_version')
|
||||
|
||||
def postgis_lib_version(self):
|
||||
"Returns the version number of the PostGIS library used with PostgreSQL."
|
||||
return self._get_postgis_func('postgis_lib_version')
|
||||
|
||||
def postgis_proj_version(self):
|
||||
"Returns the version of the PROJ.4 library used with PostGIS."
|
||||
return self._get_postgis_func('postgis_proj_version')
|
||||
|
||||
def postgis_version(self):
|
||||
"Returns PostGIS version number and compile-time options."
|
||||
return self._get_postgis_func('postgis_version')
|
||||
|
||||
def postgis_full_version(self):
|
||||
"Returns PostGIS version number and compile-time options."
|
||||
return self._get_postgis_func('postgis_full_version')
|
||||
|
||||
def postgis_version_tuple(self):
|
||||
"""
|
||||
Returns the PostGIS version as a tuple (version string, major,
|
||||
minor, subminor).
|
||||
"""
|
||||
# Getting the PostGIS version
|
||||
version = self.postgis_lib_version()
|
||||
m = self.version_regex.match(version)
|
||||
|
||||
if m:
|
||||
major = int(m.group('major'))
|
||||
minor1 = int(m.group('minor1'))
|
||||
minor2 = int(m.group('minor2'))
|
||||
else:
|
||||
raise Exception('Could not parse PostGIS version string: %s' % version)
|
||||
|
||||
return (version, major, minor1, minor2)
|
||||
|
||||
def num_params(self, lookup_type, num_param):
|
||||
"""
|
||||
Helper routine that returns a boolean indicating whether the number of
|
||||
parameters is correct for the lookup type.
|
||||
"""
|
||||
def exactly_two(np): return np == 2
|
||||
def two_to_three(np): return np >= 2 and np <=3
|
||||
if (lookup_type in self.distance_functions and
|
||||
lookup_type != 'dwithin'):
|
||||
return two_to_three(num_param)
|
||||
else:
|
||||
return exactly_two(num_param)
|
||||
|
||||
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
|
||||
"""
|
||||
Constructs spatial SQL from the given lookup value tuple a
|
||||
(alias, col, db_type), the lookup type string, lookup value, and
|
||||
the geometry field.
|
||||
"""
|
||||
alias, col, db_type = lvalue
|
||||
|
||||
# Getting the quoted geometry column.
|
||||
geo_col = '%s.%s' % (qn(alias), qn(col))
|
||||
|
||||
if lookup_type in self.geometry_operators:
|
||||
if field.geography and not lookup_type in self.geography_operators:
|
||||
raise ValueError('PostGIS geography does not support the '
|
||||
'"%s" lookup.' % lookup_type)
|
||||
# Handling a PostGIS operator.
|
||||
op = self.geometry_operators[lookup_type]
|
||||
return op.as_sql(geo_col, self.get_geom_placeholder(field, value))
|
||||
elif lookup_type in self.geometry_functions:
|
||||
if field.geography and not lookup_type in self.geography_functions:
|
||||
raise ValueError('PostGIS geography type does not support the '
|
||||
'"%s" lookup.' % lookup_type)
|
||||
|
||||
# See if a PostGIS geometry function matches the lookup type.
|
||||
tmp = self.geometry_functions[lookup_type]
|
||||
|
||||
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||
# distance lookups.
|
||||
if isinstance(tmp, tuple):
|
||||
# First element of tuple is the PostGISOperation instance, and the
|
||||
# second element is either the type or a tuple of acceptable types
|
||||
# that may passed in as further parameters for the lookup type.
|
||||
op, arg_type = tmp
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(value, (tuple, list)):
|
||||
raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Geometry is first element of lookup tuple.
|
||||
geom = value[0]
|
||||
|
||||
# Number of valid tuple parameters depends on the lookup type.
|
||||
nparams = len(value)
|
||||
if not self.num_params(lookup_type, nparams):
|
||||
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(value[1], arg_type):
|
||||
raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||
|
||||
# For lookup type `relate`, the op instance is not yet created (has
|
||||
# to be instantiated here to check the pattern parameter).
|
||||
if lookup_type == 'relate':
|
||||
op = op(self.geom_func_prefix, value[1])
|
||||
elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
|
||||
if not field.geography and field.geodetic(self.connection):
|
||||
# Geodetic distances are only availble from Points to PointFields.
|
||||
if field.geom_type != 'POINT':
|
||||
raise ValueError('PostGIS spherical operations are only valid on PointFields.')
|
||||
|
||||
if str(geom.geom_type) != 'Point':
|
||||
raise ValueError('PostGIS geometry distance parameter is required to be of type Point.')
|
||||
|
||||
# Setting up the geodetic operation appropriately.
|
||||
if nparams == 3 and value[2] == 'spheroid':
|
||||
op = op['spheroid']
|
||||
else:
|
||||
op = op['sphere']
|
||||
else:
|
||||
op = op['cartesian']
|
||||
else:
|
||||
op = tmp
|
||||
geom = value
|
||||
|
||||
# Calling the `as_sql` function on the operation instance.
|
||||
return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
|
||||
|
||||
elif lookup_type == 'isnull':
|
||||
# Handling 'isnull' lookup type
|
||||
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
||||
|
||||
def spatial_aggregate_sql(self, agg):
|
||||
"""
|
||||
Returns the spatial aggregate SQL template and function for the
|
||||
given Aggregate instance.
|
||||
"""
|
||||
agg_name = agg.__class__.__name__
|
||||
if not self.check_aggregate_support(agg):
|
||||
raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name)
|
||||
agg_name = agg_name.lower()
|
||||
if agg_name == 'union': agg_name += 'agg'
|
||||
sql_template = '%(function)s(%(field)s)'
|
||||
sql_function = getattr(self, agg_name)
|
||||
return sql_template, sql_function
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
from django.contrib.gis.db.backends.postgis.models import GeometryColumns
|
||||
return GeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
|
||||
return SpatialRefSys
|
|
@ -1,7 +1,7 @@
|
|||
from django.db.backends.sqlite3.base import Database
|
||||
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
|
||||
from django.contrib.gis.db.backends.adapter import WKTAdapter
|
||||
|
||||
class SpatiaLiteAdaptor(WKTAdaptor):
|
||||
class SpatiaLiteAdapter(WKTAdapter):
|
||||
"SQLite adaptor for geometry objects."
|
||||
def __conform__(self, protocol):
|
||||
if protocol is Database.PrepareProtocol:
|
|
@ -0,0 +1,73 @@
|
|||
from ctypes.util import find_library
|
||||
from django.conf import settings
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.sqlite3.base import *
|
||||
from django.db.backends.sqlite3.base import DatabaseWrapper as SqliteDatabaseWrapper, \
|
||||
_sqlite_extract, _sqlite_date_trunc, _sqlite_regexp
|
||||
from django.contrib.gis.db.backends.spatialite.client import SpatiaLiteClient
|
||||
from django.contrib.gis.db.backends.spatialite.creation import SpatiaLiteCreation
|
||||
from django.contrib.gis.db.backends.spatialite.operations import SpatiaLiteOperations
|
||||
|
||||
|
||||
class DatabaseWrapper(SqliteDatabaseWrapper):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Before we get too far, make sure pysqlite 2.5+ is installed.
|
||||
if Database.version_info < (2, 5, 0):
|
||||
raise ImproperlyConfigured('Only versions of pysqlite 2.5+ are '
|
||||
'compatible with SpatiaLite and GeoDjango.')
|
||||
|
||||
# Trying to find the location of the SpatiaLite library.
|
||||
# Here we are figuring out the path to the SpatiaLite library
|
||||
# (`libspatialite`). If it's not in the system library path (e.g., it
|
||||
# cannot be found by `ctypes.util.find_library`), then it may be set
|
||||
# manually in the settings via the `SPATIALITE_LIBRARY_PATH` setting.
|
||||
self.spatialite_lib = getattr(settings, 'SPATIALITE_LIBRARY_PATH',
|
||||
find_library('spatialite'))
|
||||
if not self.spatialite_lib:
|
||||
raise ImproperlyConfigured('Unable to locate the SpatiaLite library. '
|
||||
'Make sure it is in your library path, or set '
|
||||
'SPATIALITE_LIBRARY_PATH in your settings.'
|
||||
)
|
||||
super(DatabaseWrapper, self).__init__(*args, **kwargs)
|
||||
self.ops = SpatiaLiteOperations(self)
|
||||
self.client = SpatiaLiteClient(self)
|
||||
self.creation = SpatiaLiteCreation(self)
|
||||
|
||||
def _cursor(self):
|
||||
if self.connection is None:
|
||||
settings_dict = self.settings_dict
|
||||
if not settings_dict['NAME']:
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
raise ImproperlyConfigured, "Please fill out the database NAME in the settings module before using the database."
|
||||
kwargs = {
|
||||
'database': settings_dict['NAME'],
|
||||
'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
|
||||
}
|
||||
kwargs.update(settings_dict['OPTIONS'])
|
||||
self.connection = Database.connect(**kwargs)
|
||||
# Register extract, date_trunc, and regexp functions.
|
||||
self.connection.create_function("django_extract", 2, _sqlite_extract)
|
||||
self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc)
|
||||
self.connection.create_function("regexp", 2, _sqlite_regexp)
|
||||
|
||||
try:
|
||||
self.connection.enable_load_extension(True)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('The pysqlite library does not support C extension loading. '
|
||||
'Both SQLite and pysqlite must be configured to allow '
|
||||
'the loading of extensions to use SpatiaLite.'
|
||||
)
|
||||
|
||||
connection_created.send(sender=self.__class__)
|
||||
return self.connection.cursor(factory=SQLiteCursorWrapper)
|
||||
|
||||
def load_spatialite(self):
|
||||
"""
|
||||
Loads the SpatiaLite library.
|
||||
"""
|
||||
try:
|
||||
self._cursor().execute("SELECT load_extension(%s)", (self.spatialite_lib,))
|
||||
except Exception, msg:
|
||||
raise ImproperlyConfigured('Unable to load the SpatiaLite extension '
|
||||
'"%s" because: %s' % (self.spatialite_lib, msg))
|
|
@ -0,0 +1,5 @@
|
|||
from django.db.backends.sqlite3.client import DatabaseClient
|
||||
|
||||
class SpatiaLiteClient(DatabaseClient):
|
||||
executable_name = 'spatialite'
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import os
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management import call_command
|
||||
from django.db.backends.sqlite3.creation import DatabaseCreation
|
||||
|
||||
class SpatiaLiteCreation(DatabaseCreation):
|
||||
|
||||
def create_test_db(self, verbosity=1, autoclobber=False):
|
||||
"""
|
||||
Creates a test database, prompting the user for confirmation if the
|
||||
database already exists. Returns the name of the test database created.
|
||||
|
||||
This method is overloaded to load up the SpatiaLite initialization
|
||||
SQL prior to calling the `syncdb` command.
|
||||
"""
|
||||
if verbosity >= 1:
|
||||
print "Creating test database '%s'..." % self.connection.alias
|
||||
|
||||
test_database_name = self._create_test_db(verbosity, autoclobber)
|
||||
|
||||
self.connection.close()
|
||||
|
||||
self.connection.settings_dict["NAME"] = test_database_name
|
||||
can_rollback = self._rollback_works()
|
||||
self.connection.settings_dict["SUPPORTS_TRANSACTIONS"] = can_rollback
|
||||
# Need to load the SpatiaLite library and initializatin SQL before running `syncdb`.
|
||||
self.connection.load_spatialite()
|
||||
self.load_spatialite_sql()
|
||||
call_command('syncdb', verbosity=verbosity, interactive=False, database=self.connection.alias)
|
||||
|
||||
if settings.CACHE_BACKEND.startswith('db://'):
|
||||
from django.core.cache import parse_backend_uri
|
||||
_, cache_name, _ = parse_backend_uri(settings.CACHE_BACKEND)
|
||||
call_command('createcachetable', cache_name)
|
||||
|
||||
# Get a cursor (even though we don't need one yet). This has
|
||||
# the side effect of initializing the test database.
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
return test_database_name
|
||||
|
||||
def sql_indexes_for_field(self, model, f, style):
|
||||
"Return any spatial index creation SQL for the field."
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
|
||||
output = super(SpatiaLiteCreation, self).sql_indexes_for_field(model, f, style)
|
||||
|
||||
if isinstance(f, GeometryField):
|
||||
gqn = self.connection.ops.geo_quote_name
|
||||
qn = self.connection.ops.quote_name
|
||||
db_table = model._meta.db_table
|
||||
|
||||
output.append(style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_TABLE('AddGeometryColumn') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(f.column)) + ', ' +
|
||||
style.SQL_FIELD(str(f.srid)) + ', ' +
|
||||
style.SQL_COLTYPE(gqn(f.geom_type)) + ', ' +
|
||||
style.SQL_KEYWORD(str(f.dim)) + ', ' +
|
||||
style.SQL_KEYWORD(str(int(not f.null))) +
|
||||
');')
|
||||
|
||||
if f.spatial_index:
|
||||
output.append(style.SQL_KEYWORD('SELECT ') +
|
||||
style.SQL_TABLE('CreateSpatialIndex') + '(' +
|
||||
style.SQL_TABLE(gqn(db_table)) + ', ' +
|
||||
style.SQL_FIELD(gqn(f.column)) + ');')
|
||||
|
||||
return output
|
||||
|
||||
def load_spatialite_sql(self):
|
||||
"""
|
||||
This routine loads up the SpatiaLite SQL file.
|
||||
"""
|
||||
# Getting the location of the SpatiaLite SQL file, and confirming
|
||||
# it exists.
|
||||
spatialite_sql = self.spatialite_init_file()
|
||||
if not os.path.isfile(spatialite_sql):
|
||||
raise ImproperlyConfigured('Could not find the required SpatiaLite initialization '
|
||||
'SQL file (necessary for testing): %s' % spatialite_sql)
|
||||
|
||||
# Opening up the SpatiaLite SQL initialization file and executing
|
||||
# as a script.
|
||||
sql_fh = open(spatialite_sql, 'r')
|
||||
try:
|
||||
cur = self.connection._cursor()
|
||||
cur.executescript(sql_fh.read())
|
||||
finally:
|
||||
sql_fh.close()
|
||||
|
||||
def spatialite_init_file(self):
|
||||
# SPATIALITE_SQL may be placed in settings to tell GeoDjango
|
||||
# to use a specific path to the SpatiaLite initilization SQL.
|
||||
return getattr(settings, 'SPATIALITE_SQL',
|
||||
'init_spatialite-%s.%s.sql' %
|
||||
self.connection.ops.spatial_version[:2])
|
|
@ -2,6 +2,7 @@
|
|||
The GeometryColumns and SpatialRefSys models for the SpatiaLite backend.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.contrib.gis.db.backends.base import SpatialRefSysMixin
|
||||
|
||||
class GeometryColumns(models.Model):
|
||||
"""
|
||||
|
@ -40,7 +41,7 @@ class GeometryColumns(models.Model):
|
|||
(self.f_table_name, self.f_geometry_column,
|
||||
self.coord_dimension, self.type, self.srid)
|
||||
|
||||
class SpatialRefSys(models.Model):
|
||||
class SpatialRefSys(models.Model, SpatialRefSysMixin):
|
||||
"""
|
||||
The 'spatial_ref_sys' table from SpatiaLite.
|
||||
"""
|
||||
|
@ -54,8 +55,8 @@ class SpatialRefSys(models.Model):
|
|||
def wkt(self):
|
||||
from django.contrib.gis.gdal import SpatialReference
|
||||
return SpatialReference(self.proj4text).wkt
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
app_label = 'gis'
|
||||
db_table = 'spatial_ref_sys'
|
||||
managed = False
|
|
@ -0,0 +1,329 @@
|
|||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.gis.db.backends.base import BaseSpatialOperations
|
||||
from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
|
||||
from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter
|
||||
from django.contrib.gis.geometry.backend import Geometry
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.sqlite3.base import DatabaseOperations
|
||||
|
||||
class SpatiaLiteOperator(SpatialOperation):
|
||||
"For SpatiaLite operators (e.g. `&&`, `~`)."
|
||||
def __init__(self, operator):
|
||||
super(SpatiaLiteOperator, self).__init__(operator=operator)
|
||||
|
||||
class SpatiaLiteFunction(SpatialFunction):
|
||||
"For SpatiaLite function calls."
|
||||
def __init__(self, function, **kwargs):
|
||||
super(SpatiaLiteFunction, self).__init__(function, **kwargs)
|
||||
|
||||
class SpatiaLiteFunctionParam(SpatiaLiteFunction):
|
||||
"For SpatiaLite functions that take another parameter."
|
||||
sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
|
||||
|
||||
class SpatiaLiteDistance(SpatiaLiteFunction):
|
||||
"For SpatiaLite distance operations."
|
||||
dist_func = 'Distance'
|
||||
sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
|
||||
|
||||
def __init__(self, operator):
|
||||
super(SpatiaLiteDistance, self).__init__(self.dist_func,
|
||||
operator=operator)
|
||||
|
||||
class SpatiaLiteRelate(SpatiaLiteFunctionParam):
|
||||
"For SpatiaLite Relate(<geom>, <pattern>) calls."
|
||||
pattern_regex = re.compile(r'^[012TF\*]{9}$')
|
||||
def __init__(self, pattern):
|
||||
if not self.pattern_regex.match(pattern):
|
||||
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
|
||||
super(SpatiaLiteRelate, self).__init__('Relate')
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
def get_dist_ops(operator):
|
||||
"Returns operations for regular distances; spherical distances are not currently supported."
|
||||
return (SpatiaLiteDistance(operator),)
|
||||
|
||||
class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations):
|
||||
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
|
||||
name = 'spatialite'
|
||||
spatialite = True
|
||||
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
||||
valid_aggregates = dict([(k, None) for k in
|
||||
('Extent', 'Union')])
|
||||
|
||||
Adapter = SpatiaLiteAdapter
|
||||
|
||||
area = 'Area'
|
||||
centroid = 'Centroid'
|
||||
contained = 'MbrWithin'
|
||||
difference = 'Difference'
|
||||
distance = 'Distance'
|
||||
envelope = 'Envelope'
|
||||
intersection = 'Intersection'
|
||||
length = 'GLength' # OpenGis defines Length, but this conflicts with an SQLite reserved keyword
|
||||
num_geom = 'NumGeometries'
|
||||
num_points = 'NumPoints'
|
||||
point_on_surface = 'PointOnSurface'
|
||||
scale = 'ScaleCoords'
|
||||
svg = 'AsSVG'
|
||||
sym_difference = 'SymDifference'
|
||||
transform = 'Transform'
|
||||
translate = 'ShiftCoords'
|
||||
union = 'GUnion' # OpenGis defines Union, but this conflicts with an SQLite reserved keyword
|
||||
unionagg = 'GUnion'
|
||||
|
||||
from_text = 'GeomFromText'
|
||||
from_wkb = 'GeomFromWKB'
|
||||
select = 'AsText(%s)'
|
||||
|
||||
geometry_functions = {
|
||||
'equals' : SpatiaLiteFunction('Equals'),
|
||||
'disjoint' : SpatiaLiteFunction('Disjoint'),
|
||||
'touches' : SpatiaLiteFunction('Touches'),
|
||||
'crosses' : SpatiaLiteFunction('Crosses'),
|
||||
'within' : SpatiaLiteFunction('Within'),
|
||||
'overlaps' : SpatiaLiteFunction('Overlaps'),
|
||||
'contains' : SpatiaLiteFunction('Contains'),
|
||||
'intersects' : SpatiaLiteFunction('Intersects'),
|
||||
'relate' : (SpatiaLiteRelate, basestring),
|
||||
# Retruns true if B's bounding box completely contains A's bounding box.
|
||||
'contained' : SpatiaLiteFunction('MbrWithin'),
|
||||
# Returns true if A's bounding box completely contains B's bounding box.
|
||||
'bbcontains' : SpatiaLiteFunction('MbrContains'),
|
||||
# Returns true if A's bounding box overlaps B's bounding box.
|
||||
'bboverlaps' : SpatiaLiteFunction('MbrOverlaps'),
|
||||
# These are implemented here as synonyms for Equals
|
||||
'same_as' : SpatiaLiteFunction('Equals'),
|
||||
'exact' : SpatiaLiteFunction('Equals'),
|
||||
}
|
||||
|
||||
distance_functions = {
|
||||
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||
'distance_lt' : (get_dist_ops('<'), dtypes),
|
||||
'distance_lte' : (get_dist_ops('<='), dtypes),
|
||||
}
|
||||
geometry_functions.update(distance_functions)
|
||||
|
||||
def __init__(self, connection):
|
||||
super(DatabaseOperations, self).__init__()
|
||||
self.connection = connection
|
||||
|
||||
# Load the spatialite library (must be done before getting the
|
||||
# SpatiaLite version).
|
||||
self.connection.load_spatialite()
|
||||
|
||||
try:
|
||||
vtup = self.spatialite_version_tuple()
|
||||
version = vtup[1:]
|
||||
if version < (2, 3, 1):
|
||||
raise ImproperlyConfigured('GeoDjango only supports SpatiaLite versions '
|
||||
'2.3.1 and above')
|
||||
self.spatial_version = version
|
||||
except ImproperlyConfigured:
|
||||
raise
|
||||
except Exception, msg:
|
||||
raise ImproperlyConfigured('Cannot determine the SpatiaLite version for the "%s" '
|
||||
'database (error was "%s"). Was the SpatiaLite initialization '
|
||||
'SQL loaded on this database?' %
|
||||
(self.connection.settings_dict['NAME'], msg))
|
||||
|
||||
# Creating the GIS terms dictionary.
|
||||
gis_terms = ['isnull']
|
||||
gis_terms += self.geometry_functions.keys()
|
||||
self.gis_terms = dict([(term, None) for term in gis_terms])
|
||||
|
||||
def check_aggregate_support(self, aggregate):
|
||||
"""
|
||||
Checks if the given aggregate name is supported (that is, if it's
|
||||
in `self.valid_aggregates`).
|
||||
"""
|
||||
agg_name = aggregate.__class__.__name__
|
||||
return agg_name in self.valid_aggregates
|
||||
|
||||
def convert_geom(self, wkt, geo_field):
|
||||
"""
|
||||
Converts geometry WKT returned from a SpatiaLite aggregate.
|
||||
"""
|
||||
if wkt:
|
||||
return Geometry(wkt, geo_field.srid)
|
||||
else:
|
||||
return None
|
||||
|
||||
def geo_db_type(self, f):
|
||||
"""
|
||||
Returns None because geometry columnas are added via the
|
||||
`AddGeometryColumn` stored procedure on SpatiaLite.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_distance(self, f, value, lookup_type):
|
||||
"""
|
||||
Returns the distance parameters for the given geometry field,
|
||||
lookup value, and lookup type. SpatiaLite only supports regular
|
||||
cartesian-based queries (no spheroid/sphere calculations for point
|
||||
geometries like PostGIS).
|
||||
"""
|
||||
if not value:
|
||||
return []
|
||||
value = value[0]
|
||||
if isinstance(value, Distance):
|
||||
if f.geodetic(self.connection):
|
||||
raise ValueError('SpatiaLite does not support distance queries on '
|
||||
'geometry fields with a geodetic coordinate system. '
|
||||
'Distance objects; use a numeric value of your '
|
||||
'distance in degrees instead.')
|
||||
else:
|
||||
dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
|
||||
else:
|
||||
dist_param = value
|
||||
return [dist_param]
|
||||
|
||||
def get_geom_placeholder(self, f, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
Transform() and GeomFromText() function call(s).
|
||||
"""
|
||||
def transform_value(value, srid):
|
||||
return not (value is None or value.srid == srid)
|
||||
if hasattr(value, 'expression'):
|
||||
if transform_value(value, f.srid):
|
||||
placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
|
||||
else:
|
||||
placeholder = '%s'
|
||||
# No geometry value used for F expression, substitue in
|
||||
# the column name instead.
|
||||
return placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
|
||||
else:
|
||||
if transform_value(value, f.srid):
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return '%s(%s(%%s,%s), %s)' % (self.transform, self.from_text, value.srid, f.srid)
|
||||
else:
|
||||
return '%s(%%s,%s)' % (self.from_text, f.srid)
|
||||
|
||||
def _get_spatialite_func(self, func):
|
||||
"""
|
||||
Helper routine for calling PostGIS functions and returning their result.
|
||||
"""
|
||||
cursor = self.connection._cursor()
|
||||
try:
|
||||
cursor.execute('SELECT %s()' % func)
|
||||
row = cursor.fetchone()
|
||||
except:
|
||||
# TODO: raise helpful exception here.
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
return row[0]
|
||||
|
||||
def geos_version(self):
|
||||
"Returns the version of GEOS used by SpatiaLite as a string."
|
||||
return self._get_spatialite_func('geos_version')
|
||||
|
||||
def proj4_version(self):
|
||||
"Returns the version of the PROJ.4 library used by SpatiaLite."
|
||||
return self._get_spatialite_func('proj4_version')
|
||||
|
||||
def spatialite_version(self):
|
||||
"Returns the SpatiaLite library version as a string."
|
||||
return self._get_spatialite_func('spatialite_version')
|
||||
|
||||
def spatialite_version_tuple(self):
|
||||
"""
|
||||
Returns the SpatiaLite version as a tuple (version string, major,
|
||||
minor, subminor).
|
||||
"""
|
||||
# Getting the PostGIS version
|
||||
version = self.spatialite_version()
|
||||
m = self.version_regex.match(version)
|
||||
|
||||
if m:
|
||||
major = int(m.group('major'))
|
||||
minor1 = int(m.group('minor1'))
|
||||
minor2 = int(m.group('minor2'))
|
||||
else:
|
||||
raise Exception('Could not parse SpatiaLite version string: %s' % version)
|
||||
|
||||
return (version, major, minor1, minor2)
|
||||
|
||||
def spatial_aggregate_sql(self, agg):
|
||||
"""
|
||||
Returns the spatial aggregate SQL template and function for the
|
||||
given Aggregate instance.
|
||||
"""
|
||||
agg_name = agg.__class__.__name__
|
||||
if not self.check_aggregate_support(agg):
|
||||
raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name)
|
||||
agg_name = agg_name.lower()
|
||||
if agg_name == 'union': agg_name += 'agg'
|
||||
sql_template = self.select % '%(function)s(%(field)s)'
|
||||
sql_function = getattr(self, agg_name)
|
||||
return sql_template, sql_function
|
||||
|
||||
def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
|
||||
"""
|
||||
Returns the SpatiaLite-specific SQL for the given lookup value
|
||||
[a tuple of (alias, column, db_type)], lookup type, lookup
|
||||
value, and the model field.
|
||||
"""
|
||||
alias, col, db_type = lvalue
|
||||
|
||||
# Getting the quoted field as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(alias), qn(col))
|
||||
|
||||
if lookup_type in self.geometry_functions:
|
||||
# See if a SpatiaLite geometry function matches the lookup type.
|
||||
tmp = self.geometry_functions[lookup_type]
|
||||
|
||||
# Lookup types that are tuples take tuple arguments, e.g., 'relate' and
|
||||
# distance lookups.
|
||||
if isinstance(tmp, tuple):
|
||||
# First element of tuple is the SpatiaLiteOperation instance, and the
|
||||
# second element is either the type or a tuple of acceptable types
|
||||
# that may passed in as further parameters for the lookup type.
|
||||
op, arg_type = tmp
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(value, (tuple, list)):
|
||||
raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Geometry is first element of lookup tuple.
|
||||
geom = value[0]
|
||||
|
||||
# Number of valid tuple parameters depends on the lookup type.
|
||||
if len(value) != 2:
|
||||
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
|
||||
|
||||
# Ensuring the argument type matches what we expect.
|
||||
if not isinstance(value[1], arg_type):
|
||||
raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
|
||||
|
||||
# For lookup type `relate`, the op instance is not yet created (has
|
||||
# to be instantiated here to check the pattern parameter).
|
||||
if lookup_type == 'relate':
|
||||
op = op(value[1])
|
||||
elif lookup_type in self.distance_functions:
|
||||
op = op[0]
|
||||
else:
|
||||
op = tmp
|
||||
geom = value
|
||||
# Calling the `as_sql` function on the operation instance.
|
||||
return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
|
||||
elif lookup_type == 'isnull':
|
||||
# Handling 'isnull' lookup type
|
||||
return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
|
||||
|
||||
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
|
||||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
from django.contrib.gis.db.backends.spatialite.models import GeometryColumns
|
||||
return GeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys
|
||||
return SpatialRefSys
|
|
@ -19,7 +19,7 @@ def getstatusoutput(cmd):
|
|||
|
||||
def gqn(val):
|
||||
"""
|
||||
The geographic quote name function; used for quoting tables and
|
||||
The geographic quote name function; used for quoting tables and
|
||||
geometries (they use single rather than the double quotes of the
|
||||
backend quotename function).
|
||||
"""
|
||||
|
@ -33,37 +33,38 @@ class SpatialOperation(object):
|
|||
"""
|
||||
Base class for generating spatial SQL.
|
||||
"""
|
||||
def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''):
|
||||
sql_template = '%(geo_col)s %(operator)s %(geometry)s'
|
||||
|
||||
def __init__(self, function='', operator='', result='', **kwargs):
|
||||
self.function = function
|
||||
self.operator = operator
|
||||
self.result = result
|
||||
self.beg_subst = beg_subst
|
||||
try:
|
||||
# Try and put the operator and result into to the
|
||||
# end substitution.
|
||||
self.end_subst = end_subst % (operator, result)
|
||||
except TypeError:
|
||||
self.end_subst = end_subst
|
||||
self.extra = kwargs
|
||||
|
||||
@property
|
||||
def sql_subst(self):
|
||||
return ''.join([self.beg_subst, self.end_subst])
|
||||
def as_sql(self, geo_col, geometry='%s'):
|
||||
return self.sql_template % self.params(geo_col, geometry)
|
||||
|
||||
def as_sql(self, geo_col):
|
||||
return self.sql_subst % self.params(geo_col)
|
||||
|
||||
def params(self, geo_col):
|
||||
return (geo_col, self.operator)
|
||||
def params(self, geo_col, geometry):
|
||||
params = {'function' : self.function,
|
||||
'geo_col' : geo_col,
|
||||
'geometry' : geometry,
|
||||
'operator' : self.operator,
|
||||
'result' : self.result,
|
||||
}
|
||||
params.update(self.extra)
|
||||
return params
|
||||
|
||||
class SpatialFunction(SpatialOperation):
|
||||
"""
|
||||
Base class for generating spatial SQL related to a function.
|
||||
"""
|
||||
def __init__(self, func, beg_subst='%s(%s, %%s', end_subst=')', result='', operator=''):
|
||||
# Getting the function prefix.
|
||||
kwargs = {'function' : func, 'operator' : operator, 'result' : result,
|
||||
'beg_subst' : beg_subst, 'end_subst' : end_subst,}
|
||||
super(SpatialFunction, self).__init__(**kwargs)
|
||||
sql_template = '%(function)s(%(geo_col)s, %(geometry)s)'
|
||||
|
||||
def params(self, geo_col):
|
||||
return (self.function, geo_col)
|
||||
def __init__(self, func, result='', operator='', **kwargs):
|
||||
# Getting the function prefix.
|
||||
default = {'function' : func,
|
||||
'operator' : operator,
|
||||
'result' : result
|
||||
}
|
||||
kwargs.update(default)
|
||||
super(SpatialFunction, self).__init__(**kwargs)
|
|
@ -1,34 +1,17 @@
|
|||
from django.db.models import Aggregate
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.db.models.sql import GeomField
|
||||
|
||||
class GeoAggregate(Aggregate):
|
||||
|
||||
def add_to_query(self, query, alias, col, source, is_summary):
|
||||
if hasattr(source, 'geom_type'):
|
||||
# Doing additional setup on the Query object for spatial aggregates.
|
||||
aggregate = getattr(query.aggregates_module, self.name)
|
||||
|
||||
# Adding a conversion class instance and any selection wrapping
|
||||
# SQL (e.g., needed by Oracle).
|
||||
if aggregate.conversion_class is GeomField:
|
||||
query.extra_select_fields[alias] = GeomField()
|
||||
if SpatialBackend.select:
|
||||
query.custom_select[alias] = SpatialBackend.select
|
||||
|
||||
super(GeoAggregate, self).add_to_query(query, alias, col, source, is_summary)
|
||||
|
||||
class Collect(GeoAggregate):
|
||||
class Collect(Aggregate):
|
||||
name = 'Collect'
|
||||
|
||||
class Extent(GeoAggregate):
|
||||
class Extent(Aggregate):
|
||||
name = 'Extent'
|
||||
|
||||
class Extent3D(GeoAggregate):
|
||||
class Extent3D(Aggregate):
|
||||
name = 'Extent3D'
|
||||
|
||||
class MakeLine(GeoAggregate):
|
||||
class MakeLine(Aggregate):
|
||||
name = 'MakeLine'
|
||||
|
||||
class Union(GeoAggregate):
|
||||
class Union(Aggregate):
|
||||
name = 'Union'
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
from django.db.models.fields import Field
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.gis import forms
|
||||
# Getting the SpatialBackend container and the geographic quoting method.
|
||||
from django.contrib.gis.db.backend import SpatialBackend, gqn
|
||||
# GeometryProxy, GEOS, and Distance imports.
|
||||
from django.contrib.gis.db.models.proxy import GeometryProxy
|
||||
from django.contrib.gis.geometry.backend import Geometry, GeometryException
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.db.models.sql.expressions import SQLEvaluator
|
||||
|
||||
# Local cache of the spatial_ref_sys table, which holds static data.
|
||||
# This exists so that we don't have to hit the database each time.
|
||||
_srid_cache = {}
|
||||
# This exists so that we don't have to hit the database each time
|
||||
# we construct a distance query.
|
||||
_srid_cache = {'postgis' : {},
|
||||
'oracle' : {},
|
||||
'spatialite' : {},
|
||||
}
|
||||
|
||||
def get_srid_info(srid):
|
||||
def get_srid_info(srid, connection):
|
||||
"""
|
||||
Returns the units, unit name, and spheroid WKT associated with the
|
||||
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
|
||||
|
@ -18,19 +22,26 @@ def get_srid_info(srid):
|
|||
"""
|
||||
global _srid_cache
|
||||
|
||||
if SpatialBackend.mysql:
|
||||
# No `spatial_ref_sys` table in MySQL.
|
||||
if connection.ops.mysql:
|
||||
return None, None, None
|
||||
|
||||
if not srid in _srid_cache:
|
||||
from django.contrib.gis.models import SpatialRefSys
|
||||
name = connection.ops.name
|
||||
if not srid in _srid_cache[name]:
|
||||
if connection.ops.postgis:
|
||||
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
|
||||
elif connection.ops.oracle:
|
||||
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
|
||||
elif connection.ops.spatialite:
|
||||
from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys
|
||||
sr = SpatialRefSys.objects.get(srid=srid)
|
||||
units, units_name = sr.units
|
||||
spheroid = SpatialRefSys.get_spheroid(sr.wkt)
|
||||
_srid_cache[srid] = (units, units_name, spheroid)
|
||||
_srid_cache[name][srid] = (units, units_name, spheroid)
|
||||
|
||||
return _srid_cache[srid]
|
||||
return _srid_cache[name][srid]
|
||||
|
||||
class GeometryField(SpatialBackend.Field):
|
||||
class GeometryField(Field):
|
||||
"The base GIS field -- maps to the OpenGIS Specification Geometry type."
|
||||
|
||||
# The OpenGIS Geometry name.
|
||||
|
@ -41,7 +52,8 @@ class GeometryField(SpatialBackend.Field):
|
|||
|
||||
description = _("The base GIS field -- maps to the OpenGIS Specification Geometry type.")
|
||||
|
||||
def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, **kwargs):
|
||||
def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2,
|
||||
geography=False, **kwargs):
|
||||
"""
|
||||
The initialization function for geometry fields. Takes the following
|
||||
as keyword arguments:
|
||||
|
@ -57,6 +69,15 @@ class GeometryField(SpatialBackend.Field):
|
|||
|
||||
dim:
|
||||
The number of dimensions for this geometry. Defaults to 2.
|
||||
|
||||
extent:
|
||||
Customize the extent, in a 4-tuple of WGS 84 coordinates, for the
|
||||
geometry field entry in the `USER_SDO_GEOM_METADATA` table. Defaults
|
||||
to (-180.0, -90.0, 180.0, 90.0).
|
||||
|
||||
tolerance:
|
||||
Define the tolerance, in meters, to use for the geometry field
|
||||
entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.
|
||||
"""
|
||||
|
||||
# Setting the index flag with the value of the `spatial_index` keyword.
|
||||
|
@ -73,121 +94,116 @@ class GeometryField(SpatialBackend.Field):
|
|||
# first parameter, so this works like normal fields.
|
||||
kwargs['verbose_name'] = verbose_name
|
||||
|
||||
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
||||
# Is this a geography rather than a geometry column?
|
||||
self.geography = geography
|
||||
|
||||
# The following properties are used to get the units, their name, and
|
||||
# Oracle-specific private attributes for creating the entrie in
|
||||
# `USER_SDO_GEOM_METADATA`
|
||||
self._extent = kwargs.pop('extent', (-180.0, -90.0, 180.0, 90.0))
|
||||
self._tolerance = kwargs.pop('tolerance', 0.05)
|
||||
|
||||
super(GeometryField, self).__init__(**kwargs)
|
||||
|
||||
# The following functions are used to get the units, their name, and
|
||||
# the spheroid corresponding to the SRID of the GeometryField.
|
||||
def _get_srid_info(self):
|
||||
def _get_srid_info(self, connection):
|
||||
# Get attributes from `get_srid_info`.
|
||||
self._units, self._units_name, self._spheroid = get_srid_info(self.srid)
|
||||
self._units, self._units_name, self._spheroid = get_srid_info(self.srid, connection)
|
||||
|
||||
@property
|
||||
def spheroid(self):
|
||||
def spheroid(self, connection):
|
||||
if not hasattr(self, '_spheroid'):
|
||||
self._get_srid_info()
|
||||
self._get_srid_info(connection)
|
||||
return self._spheroid
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
def units(self, connection):
|
||||
if not hasattr(self, '_units'):
|
||||
self._get_srid_info()
|
||||
self._get_srid_info(connection)
|
||||
return self._units
|
||||
|
||||
@property
|
||||
def units_name(self):
|
||||
def units_name(self, connection):
|
||||
if not hasattr(self, '_units_name'):
|
||||
self._get_srid_info()
|
||||
self._get_srid_info(connection)
|
||||
return self._units_name
|
||||
|
||||
# The following properties are for formerly private variables that are now
|
||||
# public for GeometryField. Because of their use by third-party applications,
|
||||
# a deprecation warning is issued to notify them to use new attribute name.
|
||||
def _deprecated_warning(self, old_name, new_name):
|
||||
from warnings import warn
|
||||
warn('The `%s` attribute name is deprecated, please update your code to use `%s` instead.' %
|
||||
(old_name, new_name))
|
||||
|
||||
@property
|
||||
def _geom(self):
|
||||
self._deprecated_warning('_geom', 'geom_type')
|
||||
return self.geom_type
|
||||
|
||||
@property
|
||||
def _index(self):
|
||||
self._deprecated_warning('_index', 'spatial_index')
|
||||
return self.spatial_index
|
||||
|
||||
@property
|
||||
def _srid(self):
|
||||
self._deprecated_warning('_srid', 'srid')
|
||||
return self.srid
|
||||
|
||||
### Routines specific to GeometryField ###
|
||||
@property
|
||||
def geodetic(self):
|
||||
def geodetic(self, connection):
|
||||
"""
|
||||
Returns true if this field's SRID corresponds with a coordinate
|
||||
system that uses non-projected units (e.g., latitude/longitude).
|
||||
"""
|
||||
return self.units_name in self.geodetic_units
|
||||
return self.units_name(connection) in self.geodetic_units
|
||||
|
||||
def get_distance(self, dist_val, lookup_type):
|
||||
def get_distance(self, value, lookup_type, connection):
|
||||
"""
|
||||
Returns a distance number in units of the field. For example, if
|
||||
`D(km=1)` was passed in and the units of the field were in meters,
|
||||
then 1000 would be returned.
|
||||
"""
|
||||
# Getting the distance parameter and any options.
|
||||
if len(dist_val) == 1: dist, option = dist_val[0], None
|
||||
else: dist, option = dist_val
|
||||
return connection.ops.get_distance(self, value, lookup_type)
|
||||
|
||||
if isinstance(dist, Distance):
|
||||
if self.geodetic:
|
||||
if self.geodetic(connection):
|
||||
# Won't allow Distance objects w/DWithin lookups on PostGIS.
|
||||
if SpatialBackend.postgis and lookup_type == 'dwithin':
|
||||
raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
|
||||
if connection.ops.postgis and lookup_type == 'dwithin':
|
||||
raise ValueError('Only numeric values of degree units are allowed on geographic DWithin queries.')
|
||||
|
||||
# Spherical distance calculation parameter should be in meters.
|
||||
dist_param = dist.m
|
||||
else:
|
||||
dist_param = getattr(dist, Distance.unit_attname(self.units_name))
|
||||
dist_param = getattr(dist, Distance.unit_attname(self.units_name(connection)))
|
||||
else:
|
||||
# Assuming the distance is in the units of the field.
|
||||
dist_param = dist
|
||||
|
||||
if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid':
|
||||
if connection.ops.oracle and lookup_type == 'dwithin':
|
||||
dist_param = 'distance=%s' % dist_param
|
||||
|
||||
if connection.ops.postgis and self.geodetic(connection) and lookup_type != 'dwithin' and option == 'spheroid':
|
||||
# On PostGIS, by default `ST_distance_sphere` is used; but if the
|
||||
# accuracy of `ST_distance_spheroid` is needed than the spheroid
|
||||
# needs to be passed to the SQL stored procedure.
|
||||
return [gqn(self._spheroid), dist_param]
|
||||
return [self._spheroid, dist_param]
|
||||
else:
|
||||
return [dist_param]
|
||||
|
||||
def get_geometry(self, value):
|
||||
def get_prep_value(self, value):
|
||||
"""
|
||||
Retrieves the geometry, setting the default SRID from the given
|
||||
lookup parameters.
|
||||
Spatial lookup values are either a parameter that is (or may be
|
||||
converted to) a geometry, or a sequence of lookup values that
|
||||
begins with a geometry. This routine will setup the geometry
|
||||
value properly, and preserve any other lookup parameters before
|
||||
returning to the caller.
|
||||
"""
|
||||
if isinstance(value, (tuple, list)):
|
||||
if isinstance(value, SQLEvaluator):
|
||||
return value
|
||||
elif isinstance(value, (tuple, list)):
|
||||
geom = value[0]
|
||||
seq_value = True
|
||||
else:
|
||||
geom = value
|
||||
seq_value = False
|
||||
|
||||
# When the input is not a GEOS geometry, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(geom, SpatialBackend.Geometry):
|
||||
if isinstance(geom, Geometry):
|
||||
pass
|
||||
elif isinstance(geom, basestring):
|
||||
elif isinstance(geom, basestring) or hasattr(geom, '__geo_interface__'):
|
||||
try:
|
||||
geom = SpatialBackend.Geometry(geom)
|
||||
except SpatialBackend.GeometryException:
|
||||
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
|
||||
geom = Geometry(geom)
|
||||
except GeometryException:
|
||||
raise ValueError('Could not create geometry from lookup value.')
|
||||
else:
|
||||
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
|
||||
raise ValueError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
|
||||
|
||||
# Assigning the SRID value.
|
||||
geom.srid = self.get_srid(geom)
|
||||
|
||||
return geom
|
||||
if seq_value:
|
||||
lookup_val = [geom]
|
||||
lookup_val.extend(value[1:])
|
||||
return tuple(lookup_val)
|
||||
else:
|
||||
return geom
|
||||
|
||||
def get_srid(self, geom):
|
||||
"""
|
||||
|
@ -206,7 +222,10 @@ class GeometryField(SpatialBackend.Field):
|
|||
super(GeometryField, self).contribute_to_class(cls, name)
|
||||
|
||||
# Setup for lazy-instantiated Geometry object.
|
||||
setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
|
||||
setattr(cls, self.attname, GeometryProxy(Geometry, self))
|
||||
|
||||
def db_type(self, connection):
|
||||
return connection.ops.geo_db_type(self)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class' : forms.GeometryField,
|
||||
|
@ -217,46 +236,56 @@ class GeometryField(SpatialBackend.Field):
|
|||
defaults.update(kwargs)
|
||||
return super(GeometryField, self).formfield(**defaults)
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value):
|
||||
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
|
||||
"""
|
||||
Returns the spatial WHERE clause and associated parameters for the
|
||||
given lookup type and value. The value will be prepared for database
|
||||
lookup (e.g., spatial transformation SQL will be added if necessary).
|
||||
Prepare for the database lookup, and return any spatial parameters
|
||||
necessary for the query. This includes wrapping any geometry
|
||||
parameters with a backend-specific adapter and formatting any distance
|
||||
parameters into the correct units for the coordinate system of the
|
||||
field.
|
||||
"""
|
||||
if lookup_type in SpatialBackend.gis_terms:
|
||||
if lookup_type in connection.ops.gis_terms:
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull': return [], []
|
||||
|
||||
# Get the geometry with SRID; defaults SRID to that of the field
|
||||
# if it is None.
|
||||
geom = self.get_geometry(value)
|
||||
|
||||
# Getting the WHERE clause list and the associated params list. The params
|
||||
# list is populated with the Adaptor wrapping the Geometry for the
|
||||
# backend. The WHERE clause list contains the placeholder for the adaptor
|
||||
# (e.g. any transformation SQL).
|
||||
where = [self.get_placeholder(geom)]
|
||||
params = [SpatialBackend.Adaptor(geom)]
|
||||
if lookup_type == 'isnull':
|
||||
return []
|
||||
|
||||
# Populating the parameters list, and wrapping the Geometry
|
||||
# with the Adapter of the spatial backend.
|
||||
if isinstance(value, (tuple, list)):
|
||||
if lookup_type in SpatialBackend.distance_functions:
|
||||
params = [connection.ops.Adapter(value[0])]
|
||||
if lookup_type in connection.ops.distance_functions:
|
||||
# Getting the distance parameter in the units of the field.
|
||||
where += self.get_distance(value[1:], lookup_type)
|
||||
elif lookup_type in SpatialBackend.limited_where:
|
||||
pass
|
||||
params += self.get_distance(value[1:], lookup_type, connection)
|
||||
else:
|
||||
# Otherwise, making sure any other parameters are properly quoted.
|
||||
where += map(gqn, value[1:])
|
||||
return where, params
|
||||
params += value[1:]
|
||||
elif isinstance(value, SQLEvaluator):
|
||||
params = []
|
||||
else:
|
||||
params = [connection.ops.Adapter(value)]
|
||||
|
||||
return params
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
def get_prep_lookup(self, lookup_type, value):
|
||||
if lookup_type == 'isnull':
|
||||
return bool(value)
|
||||
else:
|
||||
return self.get_prep_value(value)
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
"Prepares the value for saving in the database."
|
||||
if value is None:
|
||||
return None
|
||||
else:
|
||||
return SpatialBackend.Adaptor(self.get_geometry(value))
|
||||
return connection.ops.Adapter(self.get_prep_value(value))
|
||||
|
||||
def get_placeholder(self, value, connection):
|
||||
"""
|
||||
Returns the placeholder for the geometry column for the
|
||||
given value.
|
||||
"""
|
||||
return connection.ops.get_geom_placeholder(self, value)
|
||||
|
||||
# The OpenGIS Geometry Type Fields
|
||||
class PointField(GeometryField):
|
|
@ -1,6 +1,5 @@
|
|||
from django.db.models.manager import Manager
|
||||
from django.contrib.gis.db.models.query import GeoQuerySet
|
||||
from django.contrib.gis.db.models.sql.subqueries import insert_query
|
||||
|
||||
class GeoManager(Manager):
|
||||
"Overrides Manager to return Geographic QuerySets."
|
||||
|
@ -54,7 +53,7 @@ class GeoManager(Manager):
|
|||
|
||||
def make_line(self, *args, **kwargs):
|
||||
return self.get_query_set().make_line(*args, **kwargs)
|
||||
|
||||
|
||||
def mem_size(self, *args, **kwargs):
|
||||
return self.get_query_set().mem_size(*args, **kwargs)
|
||||
|
||||
|
@ -93,6 +92,3 @@ class GeoManager(Manager):
|
|||
|
||||
def unionagg(self, *args, **kwargs):
|
||||
return self.get_query_set().unionagg(*args, **kwargs)
|
||||
|
||||
def _insert(self, values, **kwargs):
|
||||
return insert_query(self.model, values, **kwargs)
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import connection
|
||||
from django.db import connections
|
||||
from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet
|
||||
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.db.models import aggregates
|
||||
from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField
|
||||
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
|
||||
from django.contrib.gis.geometry.backend import Geometry
|
||||
from django.contrib.gis.measure import Area, Distance
|
||||
|
||||
class GeoQuerySet(QuerySet):
|
||||
"The Geographic QuerySet."
|
||||
|
||||
### Methods overloaded from QuerySet ###
|
||||
def __init__(self, model=None, query=None):
|
||||
def __init__(self, model=None, query=None, using=None):
|
||||
super(GeoQuerySet, self).__init__(model=model, query=query)
|
||||
self.query = query or GeoQuery(self.model, connection)
|
||||
self.query = query or GeoQuery(self.model)
|
||||
|
||||
def values(self, *fields):
|
||||
return self._clone(klass=GeoValuesQuerySet, setup=True, _fields=fields)
|
||||
|
@ -42,14 +41,16 @@ class GeoQuerySet(QuerySet):
|
|||
'geo_field' : geo_field,
|
||||
'setup' : False,
|
||||
}
|
||||
if SpatialBackend.oracle:
|
||||
connection = connections[self.db]
|
||||
backend = connection.ops
|
||||
if backend.oracle:
|
||||
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
|
||||
s['procedure_args']['tolerance'] = tolerance
|
||||
s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
|
||||
elif SpatialBackend.postgis or SpatialBackend.spatialite:
|
||||
if not geo_field.geodetic:
|
||||
elif backend.postgis or backend.spatialite:
|
||||
if not geo_field.geodetic(connection):
|
||||
# Getting the area units of the geographic field.
|
||||
s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name))
|
||||
s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection)))
|
||||
else:
|
||||
# TODO: Do we want to support raw number areas for geodetic fields?
|
||||
raise Exception('Area on geodetic coordinate systems not supported.')
|
||||
|
@ -127,16 +128,16 @@ class GeoQuerySet(QuerySet):
|
|||
the coordinate reference system and the bounding box to be included
|
||||
in the GeoJSON representation of the geometry.
|
||||
"""
|
||||
if not SpatialBackend.postgis or not SpatialBackend.geojson:
|
||||
backend = connections[self.db].ops
|
||||
if not backend.geojson:
|
||||
raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.')
|
||||
|
||||
|
||||
if not isinstance(precision, (int, long)):
|
||||
raise TypeError('Precision keyword must be set with an integer.')
|
||||
|
||||
|
||||
# Setting the options flag -- which depends on which version of
|
||||
# PostGIS we're using.
|
||||
major, minor1, minor2 = SpatialBackend.version
|
||||
if major >=1 and (minor1 >= 4):
|
||||
if backend.spatial_version >= (1, 4, 0):
|
||||
options = 0
|
||||
if crs and bbox: options = 3
|
||||
elif bbox: options = 1
|
||||
|
@ -146,7 +147,7 @@ class GeoQuerySet(QuerySet):
|
|||
if crs and bbox: options = 3
|
||||
elif crs: options = 1
|
||||
elif bbox: options = 2
|
||||
s = {'desc' : 'GeoJSON',
|
||||
s = {'desc' : 'GeoJSON',
|
||||
'procedure_args' : {'precision' : precision, 'options' : options},
|
||||
'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s',
|
||||
}
|
||||
|
@ -157,12 +158,12 @@ class GeoQuerySet(QuerySet):
|
|||
Returns GML representation of the given field in a `gml` attribute
|
||||
on each element of the GeoQuerySet.
|
||||
"""
|
||||
backend = connections[self.db].ops
|
||||
s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
|
||||
if SpatialBackend.postgis:
|
||||
if backend.postgis:
|
||||
# PostGIS AsGML() aggregate function parameter order depends on the
|
||||
# version -- uggh.
|
||||
major, minor1, minor2 = SpatialBackend.version
|
||||
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
|
||||
if backend.spatial_version > (1, 3, 1):
|
||||
procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
|
||||
else:
|
||||
procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
|
||||
|
@ -248,7 +249,7 @@ class GeoQuerySet(QuerySet):
|
|||
Scales the geometry to a new size by multiplying the ordinates
|
||||
with the given x,y,z scale factors.
|
||||
"""
|
||||
if SpatialBackend.spatialite:
|
||||
if connections[self.db].ops.spatialite:
|
||||
if z != 0.0:
|
||||
raise NotImplementedError('SpatiaLite does not support 3D scaling.')
|
||||
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
|
||||
|
@ -308,10 +309,10 @@ class GeoQuerySet(QuerySet):
|
|||
terms of relative moves (rather than absolute).
|
||||
|
||||
`precision` => May be used to set the maximum number of decimal
|
||||
digits used in output (defaults to 8).
|
||||
digits used in output (defaults to 8).
|
||||
"""
|
||||
relative = int(bool(relative))
|
||||
if not isinstance(precision, (int, long)):
|
||||
if not isinstance(precision, (int, long)):
|
||||
raise TypeError('SVG precision keyword argument must be an integer.')
|
||||
s = {'desc' : 'SVG',
|
||||
'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
|
||||
|
@ -333,7 +334,7 @@ class GeoQuerySet(QuerySet):
|
|||
Translates the geometry to a new location using the given numeric
|
||||
parameters as offsets.
|
||||
"""
|
||||
if SpatialBackend.spatialite:
|
||||
if connections[self.db].ops.spatialite:
|
||||
if z != 0.0:
|
||||
raise NotImplementedError('SpatiaLite does not support 3D translation.')
|
||||
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s',
|
||||
|
@ -368,7 +369,7 @@ class GeoQuerySet(QuerySet):
|
|||
|
||||
# Setting the key for the field's column with the custom SELECT SQL to
|
||||
# override the geometry column returned from the database.
|
||||
custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
|
||||
custom_sel = '%s(%s, %s)' % (connections[self.db].ops.transform, geo_col, srid)
|
||||
# TODO: Should we have this as an alias?
|
||||
# custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
|
||||
self.query.transformed_srid = srid # So other GeoQuerySet methods
|
||||
|
@ -396,9 +397,13 @@ class GeoQuerySet(QuerySet):
|
|||
Performs set up for executing the spatial function.
|
||||
"""
|
||||
# Does the spatial backend support this?
|
||||
func = getattr(SpatialBackend, att, False)
|
||||
connection = connections[self.db]
|
||||
func = getattr(connection.ops, att, False)
|
||||
if desc is None: desc = att
|
||||
if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
|
||||
if not func:
|
||||
raise NotImplementedError('%s stored procedure not available on '
|
||||
'the %s backend.' %
|
||||
(desc, connection.ops.name))
|
||||
|
||||
# Initializing the procedure arguments.
|
||||
procedure_args = {'function' : func}
|
||||
|
@ -442,7 +447,7 @@ class GeoQuerySet(QuerySet):
|
|||
# Adding any keyword parameters for the Aggregate object. Oracle backends
|
||||
# in particular need an additional `tolerance` parameter.
|
||||
agg_kwargs = {}
|
||||
if SpatialBackend.oracle: agg_kwargs['tolerance'] = tolerance
|
||||
if connections[self.db].ops.oracle: agg_kwargs['tolerance'] = tolerance
|
||||
|
||||
# Calling the QuerySet.aggregate, and returning only the value of the aggregate.
|
||||
return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg']
|
||||
|
@ -479,6 +484,9 @@ class GeoQuerySet(QuerySet):
|
|||
settings.setdefault('procedure_fmt', '%(geo_col)s')
|
||||
settings.setdefault('select_params', [])
|
||||
|
||||
connection = connections[self.db]
|
||||
backend = connection.ops
|
||||
|
||||
# Performing setup for the spatial column, unless told not to.
|
||||
if settings.get('setup', True):
|
||||
default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
|
||||
|
@ -491,13 +499,16 @@ class GeoQuerySet(QuerySet):
|
|||
|
||||
# Special handling for any argument that is a geometry.
|
||||
for name in settings['geom_args']:
|
||||
# Using the field's get_db_prep_lookup() to get any needed
|
||||
# transformation SQL -- we pass in a 'dummy' `contains` lookup.
|
||||
where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
|
||||
# Using the field's get_placeholder() routine to get any needed
|
||||
# transformation SQL.
|
||||
geom = geo_field.get_prep_value(settings['procedure_args'][name])
|
||||
params = geo_field.get_db_prep_lookup('contains', geom, connection=connection)
|
||||
geom_placeholder = geo_field.get_placeholder(geom, connection)
|
||||
|
||||
# Replacing the procedure format with that of any needed
|
||||
# transformation SQL.
|
||||
old_fmt = '%%(%s)s' % name
|
||||
new_fmt = where[0] % '%%s'
|
||||
new_fmt = geom_placeholder % '%%s'
|
||||
settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
|
||||
settings['select_params'].extend(params)
|
||||
|
||||
|
@ -507,8 +518,10 @@ class GeoQuerySet(QuerySet):
|
|||
# If the result of this function needs to be converted.
|
||||
if settings.get('select_field', False):
|
||||
sel_fld = settings['select_field']
|
||||
if isinstance(sel_fld, GeomField) and SpatialBackend.select:
|
||||
self.query.custom_select[model_att] = SpatialBackend.select
|
||||
if isinstance(sel_fld, GeomField) and backend.select:
|
||||
self.query.custom_select[model_att] = backend.select
|
||||
if connection.ops.oracle:
|
||||
sel_fld.empty_strings_allowed = False
|
||||
self.query.extra_select_fields[model_att] = sel_fld
|
||||
|
||||
# Finally, setting the extra selection attribute with
|
||||
|
@ -527,10 +540,14 @@ class GeoQuerySet(QuerySet):
|
|||
# If geodetic defaulting distance attribute to meters (Oracle and
|
||||
# PostGIS spherical distances return meters). Otherwise, use the
|
||||
# units of the geometry field.
|
||||
if geo_field.geodetic:
|
||||
connection = connections[self.db]
|
||||
geodetic = geo_field.geodetic(connection)
|
||||
geography = geo_field.geography
|
||||
|
||||
if geodetic:
|
||||
dist_att = 'm'
|
||||
else:
|
||||
dist_att = Distance.unit_attname(geo_field.units_name)
|
||||
dist_att = Distance.unit_attname(geo_field.units_name(connection))
|
||||
|
||||
# Shortcut booleans for what distance function we're using and
|
||||
# whether the geometry field is 3D.
|
||||
|
@ -546,19 +563,24 @@ class GeoQuerySet(QuerySet):
|
|||
# parameters that will be passed in to field's function.
|
||||
lookup_params = [geom or 'POINT (0 0)', 0]
|
||||
|
||||
# Getting the spatial backend operations.
|
||||
backend = connection.ops
|
||||
|
||||
# If the spheroid calculation is desired, either by the `spheroid`
|
||||
# keyword or when calculating the length of geodetic field, make
|
||||
# sure the 'spheroid' distance setting string is passed in so we
|
||||
# get the correct spatial stored procedure.
|
||||
if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
|
||||
if spheroid or (backend.postgis and geodetic and
|
||||
(not geography) and length):
|
||||
lookup_params.append('spheroid')
|
||||
where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
|
||||
lookup_params = geo_field.get_prep_value(lookup_params)
|
||||
params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection)
|
||||
|
||||
# The `geom_args` flag is set to true if a geometry parameter was
|
||||
# passed in.
|
||||
geom_args = bool(geom)
|
||||
|
||||
if SpatialBackend.oracle:
|
||||
if backend.oracle:
|
||||
if distance:
|
||||
procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
|
||||
elif length or perimeter:
|
||||
|
@ -568,12 +590,10 @@ class GeoQuerySet(QuerySet):
|
|||
# Getting whether this field is in units of degrees since the field may have
|
||||
# been transformed via the `transform` GeoQuerySet method.
|
||||
if self.query.transformed_srid:
|
||||
u, unit_name, s = get_srid_info(self.query.transformed_srid)
|
||||
u, unit_name, s = get_srid_info(self.query.transformed_srid, connection)
|
||||
geodetic = unit_name in geo_field.geodetic_units
|
||||
else:
|
||||
geodetic = geo_field.geodetic
|
||||
|
||||
if SpatialBackend.spatialite and geodetic:
|
||||
if backend.spatialite and geodetic:
|
||||
raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.')
|
||||
|
||||
if distance:
|
||||
|
@ -583,14 +603,14 @@ class GeoQuerySet(QuerySet):
|
|||
# (which will transform to the original SRID of the field rather
|
||||
# than to what was transformed to).
|
||||
geom_args = False
|
||||
procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
|
||||
procedure_fmt = '%s(%%(geo_col)s, %s)' % (backend.transform, self.query.transformed_srid)
|
||||
if geom.srid is None or geom.srid == self.query.transformed_srid:
|
||||
# If the geom parameter srid is None, it is assumed the coordinates
|
||||
# are in the transformed units. A placeholder is used for the
|
||||
# geometry parameter. `GeomFromText` constructor is also needed
|
||||
# to wrap geom placeholder for SpatiaLite.
|
||||
if SpatialBackend.spatialite:
|
||||
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.from_text, self.query.transformed_srid)
|
||||
if backend.spatialite:
|
||||
procedure_fmt += ', %s(%%%%s, %s)' % (backend.from_text, self.query.transformed_srid)
|
||||
else:
|
||||
procedure_fmt += ', %%s'
|
||||
else:
|
||||
|
@ -598,45 +618,45 @@ class GeoQuerySet(QuerySet):
|
|||
# so wrapping the geometry placeholder in transformation SQL.
|
||||
# SpatiaLite also needs geometry placeholder wrapped in `GeomFromText`
|
||||
# constructor.
|
||||
if SpatialBackend.spatialite:
|
||||
procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (SpatialBackend.transform, SpatialBackend.from_text,
|
||||
if backend.spatialite:
|
||||
procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (backend.transform, backend.from_text,
|
||||
geom.srid, self.query.transformed_srid)
|
||||
else:
|
||||
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
|
||||
procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, self.query.transformed_srid)
|
||||
else:
|
||||
# `transform()` was not used on this GeoQuerySet.
|
||||
procedure_fmt = '%(geo_col)s,%(geom)s'
|
||||
|
||||
if geodetic:
|
||||
if not geography and geodetic:
|
||||
# Spherical distance calculation is needed (because the geographic
|
||||
# field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
|
||||
# procedures may only do queries from point columns to point geometries
|
||||
# some error checking is required.
|
||||
if not isinstance(geo_field, PointField):
|
||||
raise ValueError('Spherical distance calculation only supported on PointFields.')
|
||||
if not str(SpatialBackend.Geometry(buffer(params[0].ewkb)).geom_type) == 'Point':
|
||||
if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point':
|
||||
raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
|
||||
# The `function` procedure argument needs to be set differently for
|
||||
# geodetic distance calculations.
|
||||
if spheroid:
|
||||
# Call to distance_spheroid() requires spheroid param as well.
|
||||
procedure_fmt += ',%(spheroid)s'
|
||||
procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
|
||||
procedure_fmt += ",'%(spheroid)s'"
|
||||
procedure_args.update({'function' : backend.distance_spheroid, 'spheroid' : params[1]})
|
||||
else:
|
||||
procedure_args.update({'function' : SpatialBackend.distance_sphere})
|
||||
procedure_args.update({'function' : backend.distance_sphere})
|
||||
elif length or perimeter:
|
||||
procedure_fmt = '%(geo_col)s'
|
||||
if geodetic and length:
|
||||
if not geography and geodetic and length:
|
||||
# There's no `length_sphere`, and `length_spheroid` also
|
||||
# works on 3D geometries.
|
||||
procedure_fmt += ',%(spheroid)s'
|
||||
procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
|
||||
elif geom_3d and SpatialBackend.postgis:
|
||||
procedure_fmt += ",'%(spheroid)s'"
|
||||
procedure_args.update({'function' : backend.length_spheroid, 'spheroid' : params[1]})
|
||||
elif geom_3d and backend.postgis:
|
||||
# Use 3D variants of perimeter and length routines on PostGIS.
|
||||
if perimeter:
|
||||
procedure_args.update({'function' : SpatialBackend.perimeter3d})
|
||||
procedure_args.update({'function' : backend.perimeter3d})
|
||||
elif length:
|
||||
procedure_args.update({'function' : SpatialBackend.length3d})
|
||||
procedure_args.update({'function' : backend.length3d})
|
||||
|
||||
# Setting up the settings for `_spatial_attribute`.
|
||||
s = {'select_field' : DistanceField(dist_att),
|
||||
|
@ -651,7 +671,7 @@ class GeoQuerySet(QuerySet):
|
|||
elif geom:
|
||||
# The geometry is passed in as a parameter because we handled
|
||||
# transformation conditions in this routine.
|
||||
s['select_params'] = [SpatialBackend.Adaptor(geom)]
|
||||
s['select_params'] = [backend.Adapter(geom)]
|
||||
return self._spatial_attribute(func, s, **kwargs)
|
||||
|
||||
def _geom_attribute(self, func, tolerance=0.05, **kwargs):
|
||||
|
@ -660,7 +680,7 @@ class GeoQuerySet(QuerySet):
|
|||
Geometry attribute (e.g., `centroid`, `point_on_surface`).
|
||||
"""
|
||||
s = {'select_field' : GeomField(),}
|
||||
if SpatialBackend.oracle:
|
||||
if connections[self.db].ops.oracle:
|
||||
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
|
||||
s['procedure_args'] = {'tolerance' : tolerance}
|
||||
return self._spatial_attribute(func, s, **kwargs)
|
||||
|
@ -677,7 +697,7 @@ class GeoQuerySet(QuerySet):
|
|||
'procedure_fmt' : '%(geo_col)s,%(geom)s',
|
||||
'procedure_args' : {'geom' : geom},
|
||||
}
|
||||
if SpatialBackend.oracle:
|
||||
if connections[self.db].ops.oracle:
|
||||
s['procedure_fmt'] += ',%(tolerance)s'
|
||||
s['procedure_args']['tolerance'] = tolerance
|
||||
return self._spatial_attribute(func, s, **kwargs)
|
||||
|
@ -694,16 +714,17 @@ class GeoQuerySet(QuerySet):
|
|||
# If so, it'll have to be added to the select related information
|
||||
# (e.g., if 'location__point' was given as the field name).
|
||||
self.query.add_select_related([field_name])
|
||||
self.query.pre_sql_setup()
|
||||
compiler = self.query.get_compiler(self.db)
|
||||
compiler.pre_sql_setup()
|
||||
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
|
||||
return self.query._field_column(geo_field, rel_table)
|
||||
return compiler._field_column(geo_field, rel_table)
|
||||
elif not geo_field in opts.local_fields:
|
||||
# This geographic field is inherited from another model, so we have to
|
||||
# use the db table for the _parent_ model instead.
|
||||
tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name)
|
||||
return self.query._field_column(geo_field, parent_model._meta.db_table)
|
||||
return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table)
|
||||
else:
|
||||
return self.query._field_column(geo_field)
|
||||
return self.query.get_compiler(self.db)._field_column(geo_field)
|
||||
|
||||
class GeoValuesQuerySet(ValuesQuerySet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
|
@ -1,84 +1,10 @@
|
|||
from django.db.models.sql.aggregates import *
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
from django.contrib.gis.db.models.sql.conversion import GeomField
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
|
||||
# Default SQL template for spatial aggregates.
|
||||
geo_template = '%(function)s(%(field)s)'
|
||||
|
||||
# Default conversion functions for aggregates; will be overridden if implemented
|
||||
# for the spatial backend.
|
||||
def convert_extent(box):
|
||||
raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
|
||||
|
||||
def convert_extent3d(box):
|
||||
raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.')
|
||||
|
||||
def convert_geom(wkt, geo_field):
|
||||
raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
|
||||
|
||||
if SpatialBackend.postgis:
|
||||
def convert_extent(box):
|
||||
# Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
|
||||
# parsing out and returning as a 4-tuple.
|
||||
ll, ur = box[4:-1].split(',')
|
||||
xmin, ymin = map(float, ll.split())
|
||||
xmax, ymax = map(float, ur.split())
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
def convert_extent3d(box3d):
|
||||
# Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)";
|
||||
# parsing out and returning as a 4-tuple.
|
||||
ll, ur = box3d[6:-1].split(',')
|
||||
xmin, ymin, zmin = map(float, ll.split())
|
||||
xmax, ymax, zmax = map(float, ur.split())
|
||||
return (xmin, ymin, zmin, xmax, ymax, zmax)
|
||||
|
||||
def convert_geom(hex, geo_field):
|
||||
if hex: return SpatialBackend.Geometry(hex)
|
||||
else: return None
|
||||
elif SpatialBackend.oracle:
|
||||
# Oracle spatial aggregates need a tolerance.
|
||||
geo_template = '%(function)s(SDOAGGRTYPE(%(field)s,%(tolerance)s))'
|
||||
|
||||
def convert_extent(clob):
|
||||
if clob:
|
||||
# Generally, Oracle returns a polygon for the extent -- however,
|
||||
# it can return a single point if there's only one Point in the
|
||||
# table.
|
||||
ext_geom = SpatialBackend.Geometry(clob.read())
|
||||
gtype = str(ext_geom.geom_type)
|
||||
if gtype == 'Polygon':
|
||||
# Construct the 4-tuple from the coordinates in the polygon.
|
||||
shell = ext_geom.shell
|
||||
ll, ur = shell[0][:2], shell[2][:2]
|
||||
elif gtype == 'Point':
|
||||
ll = ext_geom.coords[:2]
|
||||
ur = ll
|
||||
else:
|
||||
raise Exception('Unexpected geometry type returned for extent: %s' % gtype)
|
||||
xmin, ymin = ll
|
||||
xmax, ymax = ur
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
else:
|
||||
return None
|
||||
|
||||
def convert_geom(clob, geo_field):
|
||||
if clob:
|
||||
return SpatialBackend.Geometry(clob.read(), geo_field.srid)
|
||||
else:
|
||||
return None
|
||||
elif SpatialBackend.spatialite:
|
||||
# SpatiaLite returns WKT.
|
||||
def convert_geom(wkt, geo_field):
|
||||
if wkt:
|
||||
return SpatialBackend.Geometry(wkt, geo_field.srid)
|
||||
else:
|
||||
return None
|
||||
|
||||
class GeoAggregate(Aggregate):
|
||||
# Overriding the SQL template with the geographic one.
|
||||
sql_template = geo_template
|
||||
# Default SQL template for spatial aggregates.
|
||||
sql_template = '%(function)s(%(field)s)'
|
||||
|
||||
# Conversion class, if necessary.
|
||||
conversion_class = None
|
||||
|
@ -86,41 +12,50 @@ class GeoAggregate(Aggregate):
|
|||
# Flags for indicating the type of the aggregate.
|
||||
is_extent = False
|
||||
|
||||
def __init__(self, col, source=None, is_summary=False, **extra):
|
||||
def __init__(self, col, source=None, is_summary=False, tolerance=0.05, **extra):
|
||||
super(GeoAggregate, self).__init__(col, source, is_summary, **extra)
|
||||
|
||||
if not self.is_extent and SpatialBackend.oracle:
|
||||
self.extra.setdefault('tolerance', 0.05)
|
||||
# Required by some Oracle aggregates.
|
||||
self.tolerance = tolerance
|
||||
|
||||
# Can't use geographic aggregates on non-geometry fields.
|
||||
if not isinstance(self.source, GeometryField):
|
||||
raise ValueError('Geospatial aggregates only allowed on geometry fields.')
|
||||
|
||||
# Making sure the SQL function is available for this spatial backend.
|
||||
if not self.sql_function:
|
||||
raise NotImplementedError('This aggregate functionality not implemented for your spatial backend.')
|
||||
def as_sql(self, qn, connection):
|
||||
"Return the aggregate, rendered as SQL."
|
||||
|
||||
if connection.ops.oracle:
|
||||
self.extra['tolerance'] = self.tolerance
|
||||
|
||||
if hasattr(self.col, 'as_sql'):
|
||||
field_name = self.col.as_sql(qn, connection)
|
||||
elif isinstance(self.col, (list, tuple)):
|
||||
field_name = '.'.join([qn(c) for c in self.col])
|
||||
else:
|
||||
field_name = self.col
|
||||
|
||||
sql_template, sql_function = connection.ops.spatial_aggregate_sql(self)
|
||||
|
||||
params = {
|
||||
'function': sql_function,
|
||||
'field': field_name
|
||||
}
|
||||
params.update(self.extra)
|
||||
|
||||
return sql_template % params
|
||||
|
||||
class Collect(GeoAggregate):
|
||||
conversion_class = GeomField
|
||||
sql_function = SpatialBackend.collect
|
||||
pass
|
||||
|
||||
class Extent(GeoAggregate):
|
||||
is_extent = '2D'
|
||||
sql_function = SpatialBackend.extent
|
||||
|
||||
if SpatialBackend.oracle:
|
||||
# Have to change Extent's attributes here for Oracle.
|
||||
Extent.conversion_class = GeomField
|
||||
Extent.sql_template = '%(function)s(%(field)s)'
|
||||
|
||||
class Extent3D(GeoAggregate):
|
||||
is_extent = '3D'
|
||||
sql_function = SpatialBackend.extent3d
|
||||
|
||||
class MakeLine(GeoAggregate):
|
||||
conversion_class = GeomField
|
||||
sql_function = SpatialBackend.make_line
|
||||
pass
|
||||
|
||||
class Union(GeoAggregate):
|
||||
conversion_class = GeomField
|
||||
sql_function = SpatialBackend.unionagg
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
from itertools import izip
|
||||
from django.db.backends.util import truncate_name
|
||||
from django.db.models.sql import compiler
|
||||
from django.db.models.sql.constants import TABLE_NAME
|
||||
from django.db.models.sql.query import get_proxied_model
|
||||
|
||||
SQLCompiler = compiler.SQLCompiler
|
||||
|
||||
class GeoSQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
def get_columns(self, with_aliases=False):
|
||||
"""
|
||||
Return the list of columns to use in the select statement. If no
|
||||
columns have been specified, returns all columns relating to fields in
|
||||
the model.
|
||||
|
||||
If 'with_aliases' is true, any column names that are duplicated
|
||||
(without the table names) are given unique aliases. This is needed in
|
||||
some cases to avoid ambiguitity with nested queries.
|
||||
|
||||
This routine is overridden from Query to handle customized selection of
|
||||
geometry columns.
|
||||
"""
|
||||
qn = self.quote_name_unless_alias
|
||||
qn2 = self.connection.ops.quote_name
|
||||
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
|
||||
for alias, col in self.query.extra_select.iteritems()]
|
||||
aliases = set(self.query.extra_select.keys())
|
||||
if with_aliases:
|
||||
col_aliases = aliases.copy()
|
||||
else:
|
||||
col_aliases = set()
|
||||
if self.query.select:
|
||||
only_load = self.deferred_to_columns()
|
||||
# This loop customized for GeoQuery.
|
||||
for col, field in izip(self.query.select, self.query.select_fields):
|
||||
if isinstance(col, (list, tuple)):
|
||||
alias, column = col
|
||||
table = self.query.alias_map[alias][TABLE_NAME]
|
||||
if table in only_load and col not in only_load[table]:
|
||||
continue
|
||||
r = self.get_field_select(field, alias, column)
|
||||
if with_aliases:
|
||||
if col[1] in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
result.append('%s AS %s' % (r, c_alias))
|
||||
aliases.add(c_alias)
|
||||
col_aliases.add(c_alias)
|
||||
else:
|
||||
result.append('%s AS %s' % (r, qn2(col[1])))
|
||||
aliases.add(r)
|
||||
col_aliases.add(col[1])
|
||||
else:
|
||||
result.append(r)
|
||||
aliases.add(r)
|
||||
col_aliases.add(col[1])
|
||||
else:
|
||||
result.append(col.as_sql(qn=qn))
|
||||
|
||||
if hasattr(col, 'alias'):
|
||||
aliases.add(col.alias)
|
||||
col_aliases.add(col.alias)
|
||||
|
||||
elif self.query.default_cols:
|
||||
cols, new_aliases = self.get_default_columns(with_aliases,
|
||||
col_aliases)
|
||||
result.extend(cols)
|
||||
aliases.update(new_aliases)
|
||||
|
||||
max_name_length = self.connection.ops.max_name_length()
|
||||
result.extend([
|
||||
'%s%s' % (
|
||||
self.get_extra_select_format(alias) % aggregate.as_sql(qn=qn, connection=self.connection),
|
||||
alias is not None
|
||||
and ' AS %s' % qn(truncate_name(alias, max_name_length))
|
||||
or ''
|
||||
)
|
||||
for alias, aggregate in self.query.aggregate_select.items()
|
||||
])
|
||||
|
||||
# This loop customized for GeoQuery.
|
||||
for (table, col), field in izip(self.query.related_select_cols, self.query.related_select_fields):
|
||||
r = self.get_field_select(field, table, col)
|
||||
if with_aliases and col in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
result.append('%s AS %s' % (r, c_alias))
|
||||
aliases.add(c_alias)
|
||||
col_aliases.add(c_alias)
|
||||
else:
|
||||
result.append(r)
|
||||
aliases.add(r)
|
||||
col_aliases.add(col)
|
||||
|
||||
self._select_aliases = aliases
|
||||
return result
|
||||
|
||||
def get_default_columns(self, with_aliases=False, col_aliases=None,
|
||||
start_alias=None, opts=None, as_pairs=False):
|
||||
"""
|
||||
Computes the default columns for selecting every field in the base
|
||||
model. Will sometimes be called to pull in related models (e.g. via
|
||||
select_related), in which case "opts" and "start_alias" will be given
|
||||
to provide a starting point for the traversal.
|
||||
|
||||
Returns a list of strings, quoted appropriately for use in SQL
|
||||
directly, as well as a set of aliases used in the select statement (if
|
||||
'as_pairs' is True, returns a list of (alias, col_name) pairs instead
|
||||
of strings as the first component and None as the second component).
|
||||
|
||||
This routine is overridden from Query to handle customized selection of
|
||||
geometry columns.
|
||||
"""
|
||||
result = []
|
||||
if opts is None:
|
||||
opts = self.query.model._meta
|
||||
aliases = set()
|
||||
only_load = self.deferred_to_columns()
|
||||
# Skip all proxy to the root proxied model
|
||||
proxied_model = get_proxied_model(opts)
|
||||
|
||||
if start_alias:
|
||||
seen = {None: start_alias}
|
||||
for field, model in opts.get_fields_with_model():
|
||||
if start_alias:
|
||||
try:
|
||||
alias = seen[model]
|
||||
except KeyError:
|
||||
if model is proxied_model:
|
||||
alias = start_alias
|
||||
else:
|
||||
link_field = opts.get_ancestor_link(model)
|
||||
alias = self.query.join((start_alias, model._meta.db_table,
|
||||
link_field.column, model._meta.pk.column))
|
||||
seen[model] = alias
|
||||
else:
|
||||
# If we're starting from the base model of the queryset, the
|
||||
# aliases will have already been set up in pre_sql_setup(), so
|
||||
# we can save time here.
|
||||
alias = self.query.included_inherited_models[model]
|
||||
table = self.query.alias_map[alias][TABLE_NAME]
|
||||
if table in only_load and field.column not in only_load[table]:
|
||||
continue
|
||||
if as_pairs:
|
||||
result.append((alias, field.column))
|
||||
aliases.add(alias)
|
||||
continue
|
||||
# This part of the function is customized for GeoQuery. We
|
||||
# see if there was any custom selection specified in the
|
||||
# dictionary, and set up the selection format appropriately.
|
||||
field_sel = self.get_field_select(field, alias)
|
||||
if with_aliases and field.column in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
result.append('%s AS %s' % (field_sel, c_alias))
|
||||
col_aliases.add(c_alias)
|
||||
aliases.add(c_alias)
|
||||
else:
|
||||
r = field_sel
|
||||
result.append(r)
|
||||
aliases.add(r)
|
||||
if with_aliases:
|
||||
col_aliases.add(field.column)
|
||||
return result, aliases
|
||||
|
||||
def resolve_columns(self, row, fields=()):
|
||||
"""
|
||||
This routine is necessary so that distances and geometries returned
|
||||
from extra selection SQL get resolved appropriately into Python
|
||||
objects.
|
||||
"""
|
||||
values = []
|
||||
aliases = self.query.extra_select.keys()
|
||||
if self.query.aggregates:
|
||||
# If we have an aggregate annotation, must extend the aliases
|
||||
# so their corresponding row values are included.
|
||||
aliases.extend([None for i in xrange(len(self.query.aggregates))])
|
||||
|
||||
# Have to set a starting row number offset that is used for
|
||||
# determining the correct starting row index -- needed for
|
||||
# doing pagination with Oracle.
|
||||
rn_offset = 0
|
||||
if self.connection.ops.oracle:
|
||||
if self.query.high_mark is not None or self.query.low_mark: rn_offset = 1
|
||||
index_start = rn_offset + len(aliases)
|
||||
|
||||
# Converting any extra selection values (e.g., geometries and
|
||||
# distance objects added by GeoQuerySet methods).
|
||||
values = [self.query.convert_values(v,
|
||||
self.query.extra_select_fields.get(a, None),
|
||||
self.connection)
|
||||
for v, a in izip(row[rn_offset:index_start], aliases)]
|
||||
if self.connection.ops.oracle or getattr(self.query, 'geo_values', False):
|
||||
# We resolve the rest of the columns if we're on Oracle or if
|
||||
# the `geo_values` attribute is defined.
|
||||
for value, field in izip(row[index_start:], fields):
|
||||
values.append(self.query.convert_values(value, field, self.connection))
|
||||
else:
|
||||
values.extend(row[index_start:])
|
||||
return tuple(values)
|
||||
|
||||
#### Routines unique to GeoQuery ####
|
||||
def get_extra_select_format(self, alias):
|
||||
sel_fmt = '%s'
|
||||
if alias in self.query.custom_select:
|
||||
sel_fmt = sel_fmt % self.query.custom_select[alias]
|
||||
return sel_fmt
|
||||
|
||||
def get_field_select(self, field, alias=None, column=None):
|
||||
"""
|
||||
Returns the SELECT SQL string for the given field. Figures out
|
||||
if any custom selection SQL is needed for the column The `alias`
|
||||
keyword may be used to manually specify the database table where
|
||||
the column exists, if not in the model associated with this
|
||||
`GeoQuery`. Similarly, `column` may be used to specify the exact
|
||||
column name, rather than using the `column` attribute on `field`.
|
||||
"""
|
||||
sel_fmt = self.get_select_format(field)
|
||||
if field in self.query.custom_select:
|
||||
field_sel = sel_fmt % self.query.custom_select[field]
|
||||
else:
|
||||
field_sel = sel_fmt % self._field_column(field, alias, column)
|
||||
return field_sel
|
||||
|
||||
def get_select_format(self, fld):
|
||||
"""
|
||||
Returns the selection format string, depending on the requirements
|
||||
of the spatial backend. For example, Oracle and MySQL require custom
|
||||
selection formats in order to retrieve geometries in OGC WKT. For all
|
||||
other fields a simple '%s' format string is returned.
|
||||
"""
|
||||
if self.connection.ops.select and hasattr(fld, 'geom_type'):
|
||||
# This allows operations to be done on fields in the SELECT,
|
||||
# overriding their values -- used by the Oracle and MySQL
|
||||
# spatial backends to get database values as WKT, and by the
|
||||
# `transform` method.
|
||||
sel_fmt = self.connection.ops.select
|
||||
|
||||
# Because WKT doesn't contain spatial reference information,
|
||||
# the SRID is prefixed to the returned WKT to ensure that the
|
||||
# transformed geometries have an SRID different than that of the
|
||||
# field -- this is only used by `transform` for Oracle and
|
||||
# SpatiaLite backends.
|
||||
if self.query.transformed_srid and ( self.connection.ops.oracle or
|
||||
self.connection.ops.spatialite ):
|
||||
sel_fmt = "'SRID=%d;'||%s" % (self.query.transformed_srid, sel_fmt)
|
||||
else:
|
||||
sel_fmt = '%s'
|
||||
return sel_fmt
|
||||
|
||||
# Private API utilities, subject to change.
|
||||
def _field_column(self, field, table_alias=None, column=None):
|
||||
"""
|
||||
Helper function that returns the database column for the given field.
|
||||
The table and column are returned (quoted) in the proper format, e.g.,
|
||||
`"geoapp_city"."point"`. If `table_alias` is not specified, the
|
||||
database table associated with the model of this `GeoQuery` will be
|
||||
used. If `column` is specified, it will be used instead of the value
|
||||
in `field.column`.
|
||||
"""
|
||||
if table_alias is None: table_alias = self.query.model._meta.db_table
|
||||
return "%s.%s" % (self.quote_name_unless_alias(table_alias),
|
||||
self.connection.ops.quote_name(column or field.column))
|
||||
|
||||
class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLAggregateCompiler(compiler.SQLAggregateCompiler, GeoSQLCompiler):
|
||||
pass
|
||||
|
||||
class SQLDateCompiler(compiler.SQLDateCompiler, GeoSQLCompiler):
|
||||
pass
|
|
@ -2,15 +2,13 @@
|
|||
This module holds simple classes used by GeoQuery.convert_values
|
||||
to convert geospatial values from the database.
|
||||
"""
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
|
||||
class BaseField(object):
|
||||
empty_strings_allowed = True
|
||||
def get_internal_type(self):
|
||||
"Overloaded method so OracleQuery.convert_values doesn't balk."
|
||||
return None
|
||||
|
||||
if SpatialBackend.oracle: BaseField.empty_strings_allowed = False
|
||||
|
||||
class AreaField(BaseField):
|
||||
"Wrapper for Area values."
|
||||
def __init__(self, area_att):
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
from itertools import izip
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.db.models.query import sql
|
||||
from django.db.models.fields.related import ForeignKey
|
||||
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates_module
|
||||
from django.contrib.gis.db.models.sql import aggregates as gis_aggregates
|
||||
from django.contrib.gis.db.models.sql.conversion import AreaField, DistanceField, GeomField
|
||||
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
||||
from django.contrib.gis.geometry.backend import Geometry
|
||||
from django.contrib.gis.measure import Area, Distance
|
||||
|
||||
# Valid GIS query types.
|
||||
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
|
||||
ALL_TERMS.update(SpatialBackend.gis_terms)
|
||||
|
||||
# Pulling out other needed constants/routines to avoid attribute lookups.
|
||||
TABLE_NAME = sql.constants.TABLE_NAME
|
||||
get_proxied_model = sql.query.get_proxied_model
|
||||
ALL_TERMS = dict([(x, None) for x in (
|
||||
'bbcontains', 'bboverlaps', 'contained', 'contains',
|
||||
'contains_properly', 'coveredby', 'covers', 'crosses', 'disjoint',
|
||||
'distance_gt', 'distance_gte', 'distance_lt', 'distance_lte',
|
||||
'dwithin', 'equals', 'exact',
|
||||
'intersects', 'overlaps', 'relate', 'same_as', 'touches', 'within',
|
||||
'left', 'right', 'overlaps_left', 'overlaps_right',
|
||||
'overlaps_above', 'overlaps_below',
|
||||
'strictly_above', 'strictly_below'
|
||||
)])
|
||||
ALL_TERMS.update(sql.constants.QUERY_TERMS)
|
||||
|
||||
class GeoQuery(sql.Query):
|
||||
"""
|
||||
|
@ -23,11 +27,13 @@ class GeoQuery(sql.Query):
|
|||
"""
|
||||
# Overridding the valid query terms.
|
||||
query_terms = ALL_TERMS
|
||||
aggregates_module = gis_aggregates_module
|
||||
aggregates_module = gis_aggregates
|
||||
|
||||
compiler = 'GeoSQLCompiler'
|
||||
|
||||
#### Methods overridden from the base Query class ####
|
||||
def __init__(self, model, conn):
|
||||
super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode)
|
||||
def __init__(self, model, where=GeoWhereNode):
|
||||
super(GeoQuery, self).__init__(model, where)
|
||||
# The following attributes are customized for the GeoQuerySet.
|
||||
# The GeoWhereNode and SpatialBackend classes contain backend-specific
|
||||
# routines and functions.
|
||||
|
@ -35,13 +41,6 @@ class GeoQuery(sql.Query):
|
|||
self.transformed_srid = None
|
||||
self.extra_select_fields = {}
|
||||
|
||||
if SpatialBackend.oracle:
|
||||
# Have to override this so that GeoQuery, instead of OracleQuery,
|
||||
# is returned when unpickling.
|
||||
def __reduce__(self):
|
||||
callable, args, data = super(GeoQuery, self).__reduce__()
|
||||
return (unpickle_geoquery, (), data)
|
||||
|
||||
def clone(self, *args, **kwargs):
|
||||
obj = super(GeoQuery, self).clone(*args, **kwargs)
|
||||
# Customized selection dictionary and transformed srid flag have
|
||||
|
@ -51,199 +50,14 @@ class GeoQuery(sql.Query):
|
|||
obj.extra_select_fields = self.extra_select_fields.copy()
|
||||
return obj
|
||||
|
||||
def get_columns(self, with_aliases=False):
|
||||
"""
|
||||
Return the list of columns to use in the select statement. If no
|
||||
columns have been specified, returns all columns relating to fields in
|
||||
the model.
|
||||
|
||||
If 'with_aliases' is true, any column names that are duplicated
|
||||
(without the table names) are given unique aliases. This is needed in
|
||||
some cases to avoid ambiguitity with nested queries.
|
||||
|
||||
This routine is overridden from Query to handle customized selection of
|
||||
geometry columns.
|
||||
"""
|
||||
qn = self.quote_name_unless_alias
|
||||
qn2 = self.connection.ops.quote_name
|
||||
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col[0], qn2(alias))
|
||||
for alias, col in self.extra_select.iteritems()]
|
||||
aliases = set(self.extra_select.keys())
|
||||
if with_aliases:
|
||||
col_aliases = aliases.copy()
|
||||
else:
|
||||
col_aliases = set()
|
||||
if self.select:
|
||||
only_load = self.deferred_to_columns()
|
||||
# This loop customized for GeoQuery.
|
||||
for col, field in izip(self.select, self.select_fields):
|
||||
if isinstance(col, (list, tuple)):
|
||||
alias, column = col
|
||||
table = self.alias_map[alias][TABLE_NAME]
|
||||
if table in only_load and col not in only_load[table]:
|
||||
continue
|
||||
r = self.get_field_select(field, alias, column)
|
||||
if with_aliases:
|
||||
if col[1] in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
result.append('%s AS %s' % (r, c_alias))
|
||||
aliases.add(c_alias)
|
||||
col_aliases.add(c_alias)
|
||||
else:
|
||||
result.append('%s AS %s' % (r, qn2(col[1])))
|
||||
aliases.add(r)
|
||||
col_aliases.add(col[1])
|
||||
else:
|
||||
result.append(r)
|
||||
aliases.add(r)
|
||||
col_aliases.add(col[1])
|
||||
else:
|
||||
result.append(col.as_sql(quote_func=qn))
|
||||
|
||||
if hasattr(col, 'alias'):
|
||||
aliases.add(col.alias)
|
||||
col_aliases.add(col.alias)
|
||||
|
||||
elif self.default_cols:
|
||||
cols, new_aliases = self.get_default_columns(with_aliases,
|
||||
col_aliases)
|
||||
result.extend(cols)
|
||||
aliases.update(new_aliases)
|
||||
|
||||
result.extend([
|
||||
'%s%s' % (
|
||||
self.get_extra_select_format(alias) % aggregate.as_sql(quote_func=qn),
|
||||
alias is not None and ' AS %s' % alias or ''
|
||||
)
|
||||
for alias, aggregate in self.aggregate_select.items()
|
||||
])
|
||||
|
||||
# This loop customized for GeoQuery.
|
||||
for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
|
||||
r = self.get_field_select(field, table, col)
|
||||
if with_aliases and col in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
result.append('%s AS %s' % (r, c_alias))
|
||||
aliases.add(c_alias)
|
||||
col_aliases.add(c_alias)
|
||||
else:
|
||||
result.append(r)
|
||||
aliases.add(r)
|
||||
col_aliases.add(col)
|
||||
|
||||
self._select_aliases = aliases
|
||||
return result
|
||||
|
||||
def get_default_columns(self, with_aliases=False, col_aliases=None,
|
||||
start_alias=None, opts=None, as_pairs=False):
|
||||
"""
|
||||
Computes the default columns for selecting every field in the base
|
||||
model. Will sometimes be called to pull in related models (e.g. via
|
||||
select_related), in which case "opts" and "start_alias" will be given
|
||||
to provide a starting point for the traversal.
|
||||
|
||||
Returns a list of strings, quoted appropriately for use in SQL
|
||||
directly, as well as a set of aliases used in the select statement (if
|
||||
'as_pairs' is True, returns a list of (alias, col_name) pairs instead
|
||||
of strings as the first component and None as the second component).
|
||||
|
||||
This routine is overridden from Query to handle customized selection of
|
||||
geometry columns.
|
||||
"""
|
||||
result = []
|
||||
if opts is None:
|
||||
opts = self.model._meta
|
||||
aliases = set()
|
||||
only_load = self.deferred_to_columns()
|
||||
# Skip all proxy to the root proxied model
|
||||
proxied_model = get_proxied_model(opts)
|
||||
|
||||
if start_alias:
|
||||
seen = {None: start_alias}
|
||||
for field, model in opts.get_fields_with_model():
|
||||
if start_alias:
|
||||
try:
|
||||
alias = seen[model]
|
||||
except KeyError:
|
||||
if model is proxied_model:
|
||||
alias = start_alias
|
||||
else:
|
||||
link_field = opts.get_ancestor_link(model)
|
||||
alias = self.join((start_alias, model._meta.db_table,
|
||||
link_field.column, model._meta.pk.column))
|
||||
seen[model] = alias
|
||||
else:
|
||||
# If we're starting from the base model of the queryset, the
|
||||
# aliases will have already been set up in pre_sql_setup(), so
|
||||
# we can save time here.
|
||||
alias = self.included_inherited_models[model]
|
||||
table = self.alias_map[alias][TABLE_NAME]
|
||||
if table in only_load and field.column not in only_load[table]:
|
||||
continue
|
||||
if as_pairs:
|
||||
result.append((alias, field.column))
|
||||
aliases.add(alias)
|
||||
continue
|
||||
# This part of the function is customized for GeoQuery. We
|
||||
# see if there was any custom selection specified in the
|
||||
# dictionary, and set up the selection format appropriately.
|
||||
field_sel = self.get_field_select(field, alias)
|
||||
if with_aliases and field.column in col_aliases:
|
||||
c_alias = 'Col%d' % len(col_aliases)
|
||||
result.append('%s AS %s' % (field_sel, c_alias))
|
||||
col_aliases.add(c_alias)
|
||||
aliases.add(c_alias)
|
||||
else:
|
||||
r = field_sel
|
||||
result.append(r)
|
||||
aliases.add(r)
|
||||
if with_aliases:
|
||||
col_aliases.add(field.column)
|
||||
return result, aliases
|
||||
|
||||
def resolve_columns(self, row, fields=()):
|
||||
"""
|
||||
This routine is necessary so that distances and geometries returned
|
||||
from extra selection SQL get resolved appropriately into Python
|
||||
objects.
|
||||
"""
|
||||
values = []
|
||||
aliases = self.extra_select.keys()
|
||||
if self.aggregates:
|
||||
# If we have an aggregate annotation, must extend the aliases
|
||||
# so their corresponding row values are included.
|
||||
aliases.extend([None for i in xrange(len(self.aggregates))])
|
||||
|
||||
# Have to set a starting row number offset that is used for
|
||||
# determining the correct starting row index -- needed for
|
||||
# doing pagination with Oracle.
|
||||
rn_offset = 0
|
||||
if SpatialBackend.oracle:
|
||||
if self.high_mark is not None or self.low_mark: rn_offset = 1
|
||||
index_start = rn_offset + len(aliases)
|
||||
|
||||
# Converting any extra selection values (e.g., geometries and
|
||||
# distance objects added by GeoQuerySet methods).
|
||||
values = [self.convert_values(v, self.extra_select_fields.get(a, None))
|
||||
for v, a in izip(row[rn_offset:index_start], aliases)]
|
||||
if SpatialBackend.oracle or getattr(self, 'geo_values', False):
|
||||
# We resolve the rest of the columns if we're on Oracle or if
|
||||
# the `geo_values` attribute is defined.
|
||||
for value, field in izip(row[index_start:], fields):
|
||||
values.append(self.convert_values(value, field))
|
||||
else:
|
||||
values.extend(row[index_start:])
|
||||
return tuple(values)
|
||||
|
||||
def convert_values(self, value, field):
|
||||
"""
|
||||
Using the same routines that Oracle does we can convert our
|
||||
def convert_values(self, value, field, connection):
|
||||
""" Using the same routines that Oracle does we can convert our
|
||||
extra selection objects into Geometry and Distance objects.
|
||||
TODO: Make converted objects 'lazy' for less overhead.
|
||||
"""
|
||||
if SpatialBackend.oracle:
|
||||
if connection.ops.oracle:
|
||||
# Running through Oracle's first.
|
||||
value = super(GeoQuery, self).convert_values(value, field or GeomField())
|
||||
value = super(GeoQuery, self).convert_values(value, field or GeomField(), connection)
|
||||
|
||||
if isinstance(field, DistanceField):
|
||||
# Using the field's distance attribute, can instantiate
|
||||
|
@ -252,10 +66,20 @@ class GeoQuery(sql.Query):
|
|||
elif isinstance(field, AreaField):
|
||||
value = Area(**{field.area_att : value})
|
||||
elif isinstance(field, (GeomField, GeometryField)) and value:
|
||||
value = SpatialBackend.Geometry(value)
|
||||
value = Geometry(value)
|
||||
return value
|
||||
|
||||
def resolve_aggregate(self, value, aggregate):
|
||||
def get_aggregation(self, using):
|
||||
# Remove any aggregates marked for reduction from the subquery
|
||||
# and move them to the outer AggregateQuery.
|
||||
connection = connections[using]
|
||||
for alias, aggregate in self.aggregate_select.items():
|
||||
if isinstance(aggregate, gis_aggregates.GeoAggregate):
|
||||
if not getattr(aggregate, 'is_extent', False) or connection.ops.oracle:
|
||||
self.extra_select_fields[alias] = GeomField()
|
||||
return super(GeoQuery, self).get_aggregation(using)
|
||||
|
||||
def resolve_aggregate(self, value, aggregate, connection):
|
||||
"""
|
||||
Overridden from GeoQuery's normalize to handle the conversion of
|
||||
GeoAggregate objects.
|
||||
|
@ -263,77 +87,15 @@ class GeoQuery(sql.Query):
|
|||
if isinstance(aggregate, self.aggregates_module.GeoAggregate):
|
||||
if aggregate.is_extent:
|
||||
if aggregate.is_extent == '3D':
|
||||
return self.aggregates_module.convert_extent3d(value)
|
||||
return connection.ops.convert_extent3d(value)
|
||||
else:
|
||||
return self.aggregates_module.convert_extent(value)
|
||||
return connection.ops.convert_extent(value)
|
||||
else:
|
||||
return self.aggregates_module.convert_geom(value, aggregate.source)
|
||||
return connection.ops.convert_geom(value, aggregate.source)
|
||||
else:
|
||||
return super(GeoQuery, self).resolve_aggregate(value, aggregate)
|
||||
|
||||
#### Routines unique to GeoQuery ####
|
||||
def get_extra_select_format(self, alias):
|
||||
sel_fmt = '%s'
|
||||
if alias in self.custom_select:
|
||||
sel_fmt = sel_fmt % self.custom_select[alias]
|
||||
return sel_fmt
|
||||
|
||||
def get_field_select(self, field, alias=None, column=None):
|
||||
"""
|
||||
Returns the SELECT SQL string for the given field. Figures out
|
||||
if any custom selection SQL is needed for the column The `alias`
|
||||
keyword may be used to manually specify the database table where
|
||||
the column exists, if not in the model associated with this
|
||||
`GeoQuery`. Similarly, `column` may be used to specify the exact
|
||||
column name, rather than using the `column` attribute on `field`.
|
||||
"""
|
||||
sel_fmt = self.get_select_format(field)
|
||||
if field in self.custom_select:
|
||||
field_sel = sel_fmt % self.custom_select[field]
|
||||
else:
|
||||
field_sel = sel_fmt % self._field_column(field, alias, column)
|
||||
return field_sel
|
||||
|
||||
def get_select_format(self, fld):
|
||||
"""
|
||||
Returns the selection format string, depending on the requirements
|
||||
of the spatial backend. For example, Oracle and MySQL require custom
|
||||
selection formats in order to retrieve geometries in OGC WKT. For all
|
||||
other fields a simple '%s' format string is returned.
|
||||
"""
|
||||
if SpatialBackend.select and hasattr(fld, 'geom_type'):
|
||||
# This allows operations to be done on fields in the SELECT,
|
||||
# overriding their values -- used by the Oracle and MySQL
|
||||
# spatial backends to get database values as WKT, and by the
|
||||
# `transform` method.
|
||||
sel_fmt = SpatialBackend.select
|
||||
|
||||
# Because WKT doesn't contain spatial reference information,
|
||||
# the SRID is prefixed to the returned WKT to ensure that the
|
||||
# transformed geometries have an SRID different than that of the
|
||||
# field -- this is only used by `transform` for Oracle and
|
||||
# SpatiaLite backends.
|
||||
if self.transformed_srid and ( SpatialBackend.oracle or
|
||||
SpatialBackend.spatialite ):
|
||||
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
|
||||
else:
|
||||
sel_fmt = '%s'
|
||||
return sel_fmt
|
||||
return super(GeoQuery, self).resolve_aggregate(value, aggregate, connection)
|
||||
|
||||
# Private API utilities, subject to change.
|
||||
def _field_column(self, field, table_alias=None, column=None):
|
||||
"""
|
||||
Helper function that returns the database column for the given field.
|
||||
The table and column are returned (quoted) in the proper format, e.g.,
|
||||
`"geoapp_city"."point"`. If `table_alias` is not specified, the
|
||||
database table associated with the model of this `GeoQuery` will be
|
||||
used. If `column` is specified, it will be used instead of the value
|
||||
in `field.column`.
|
||||
"""
|
||||
if table_alias is None: table_alias = self.model._meta.db_table
|
||||
return "%s.%s" % (self.quote_name_unless_alias(table_alias),
|
||||
self.connection.ops.quote_name(column or field.column))
|
||||
|
||||
def _geo_field(self, field_name=None):
|
||||
"""
|
||||
Returns the first Geometry field encountered; or specified via the
|
||||
|
@ -350,12 +112,3 @@ class GeoQuery(sql.Query):
|
|||
# Otherwise, check by the given field name -- which may be
|
||||
# a lookup to a _related_ geographic field.
|
||||
return GeoWhereNode._check_geo_field(self.model._meta, field_name)
|
||||
|
||||
if SpatialBackend.oracle:
|
||||
def unpickle_geoquery():
|
||||
"""
|
||||
Utility function, called by Python's unpickling machinery, that handles
|
||||
unpickling of GeoQuery subclasses of OracleQuery.
|
||||
"""
|
||||
return GeoQuery.__new__(GeoQuery)
|
||||
unpickle_geoquery.__safe_for_unpickling__ = True
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.db.models.query import insert_query
|
||||
|
||||
if SpatialBackend.oracle:
|
||||
from django.db import connection
|
||||
from django.db.models.sql.subqueries import InsertQuery
|
||||
|
||||
class OracleGeoInsertQuery(InsertQuery):
|
||||
def insert_values(self, insert_values, raw_values=False):
|
||||
"""
|
||||
This routine is overloaded from InsertQuery so that no parameter is
|
||||
passed into cx_Oracle for NULL geometries. The reason is that
|
||||
cx_Oracle has no way to bind Oracle object values (like
|
||||
MDSYS.SDO_GEOMETRY).
|
||||
"""
|
||||
placeholders, values = [], []
|
||||
for field, val in insert_values:
|
||||
if hasattr(field, 'get_placeholder'):
|
||||
ph = field.get_placeholder(val)
|
||||
else:
|
||||
ph = '%s'
|
||||
|
||||
placeholders.append(ph)
|
||||
self.columns.append(field.column)
|
||||
|
||||
# If 'NULL' for the placeholder, omit appending None
|
||||
# to the values list (which is used for db params).
|
||||
if not ph == 'NULL':
|
||||
values.append(val)
|
||||
if raw_values:
|
||||
self.values.extend(values)
|
||||
else:
|
||||
self.params += tuple(values)
|
||||
self.values.extend(placeholders)
|
||||
|
||||
def insert_query(model, values, return_id=False, raw_values=False):
|
||||
query = OracleGeoInsertQuery(model, connection)
|
||||
query.insert_values(values, raw_values)
|
||||
return query.execute_sql(return_id)
|
|
@ -1,23 +1,31 @@
|
|||
from django.db import connection
|
||||
from django.db.models.fields import Field, FieldDoesNotExist
|
||||
from django.db.models.sql.constants import LOOKUP_SEP
|
||||
from django.db.models.sql.expressions import SQLEvaluator
|
||||
from django.db.models.sql.where import WhereNode
|
||||
from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
|
||||
from django.db.models.sql.where import Constraint, WhereNode
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
qn = connection.ops.quote_name
|
||||
|
||||
class GeoAnnotation(object):
|
||||
class GeoConstraint(Constraint):
|
||||
"""
|
||||
The annotation used for GeometryFields; basically a placeholder
|
||||
for metadata needed by the `get_geo_where_clause` of the spatial
|
||||
backend.
|
||||
This subclass overrides `process` to better handle geographic SQL
|
||||
construction.
|
||||
"""
|
||||
def __init__(self, field, value, where):
|
||||
self.geodetic = field.geodetic
|
||||
self.geom_type = field.geom_type
|
||||
self.value = value
|
||||
self.where = tuple(where)
|
||||
def __init__(self, init_constraint):
|
||||
self.alias = init_constraint.alias
|
||||
self.col = init_constraint.col
|
||||
self.field = init_constraint.field
|
||||
|
||||
def process(self, lookup_type, value, connection):
|
||||
if isinstance(value, SQLEvaluator):
|
||||
# Make sure the F Expression destination field exists, and
|
||||
# set an `srid` attribute with the same as that of the
|
||||
# destination.
|
||||
geo_fld = GeoWhereNode._check_geo_field(value.opts, value.expression.name)
|
||||
if not geo_fld:
|
||||
raise ValueError('No geographic field found in expression.')
|
||||
value.srid = geo_fld.srid
|
||||
db_type = self.field.db_type(connection=connection)
|
||||
params = self.field.get_db_prep_lookup(lookup_type, value, connection=connection)
|
||||
return (self.alias, self.col, db_type), params
|
||||
|
||||
class GeoWhereNode(WhereNode):
|
||||
"""
|
||||
|
@ -25,81 +33,26 @@ class GeoWhereNode(WhereNode):
|
|||
these are tied to the GeoQuery class that created it.
|
||||
"""
|
||||
def add(self, data, connector):
|
||||
"""
|
||||
This is overridden from the regular WhereNode to handle the
|
||||
peculiarties of GeometryFields, because they need a special
|
||||
annotation object that contains the spatial metadata from the
|
||||
field to generate the spatial SQL.
|
||||
"""
|
||||
if not isinstance(data, (list, tuple)):
|
||||
return super(WhereNode, self).add(data, connector)
|
||||
if isinstance(data, (list, tuple)):
|
||||
obj, lookup_type, value = data
|
||||
if ( isinstance(obj, Constraint) and
|
||||
isinstance(obj.field, GeometryField) ):
|
||||
data = (GeoConstraint(obj), lookup_type, value)
|
||||
super(GeoWhereNode, self).add(data, connector)
|
||||
|
||||
obj, lookup_type, value = data
|
||||
col, field = obj.col, obj.field
|
||||
|
||||
if not hasattr(field, "geom_type"):
|
||||
# Not a geographic field, so call `WhereNode.add`.
|
||||
return super(GeoWhereNode, self).add(data, connector)
|
||||
def make_atom(self, child, qn, connection):
|
||||
lvalue, lookup_type, value_annot, params_or_value = child
|
||||
if isinstance(lvalue, GeoConstraint):
|
||||
data, params = lvalue.process(lookup_type, params_or_value, connection)
|
||||
spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn)
|
||||
return spatial_sql, params
|
||||
else:
|
||||
if isinstance(value, SQLEvaluator):
|
||||
# Getting the geographic field to compare with from the expression.
|
||||
geo_fld = self._check_geo_field(value.opts, value.expression.name)
|
||||
if not geo_fld:
|
||||
raise ValueError('No geographic field found in expression.')
|
||||
|
||||
# Get the SRID of the geometry field that the expression was meant
|
||||
# to operate on -- it's needed to determine whether transformation
|
||||
# SQL is necessary.
|
||||
srid = geo_fld.srid
|
||||
|
||||
# Getting the quoted representation of the geometry column that
|
||||
# the expression is operating on.
|
||||
geo_col = '%s.%s' % tuple(map(qn, value.cols[value.expression]))
|
||||
|
||||
# If it's in a different SRID, we'll need to wrap in
|
||||
# transformation SQL.
|
||||
if not srid is None and srid != field.srid and SpatialBackend.transform:
|
||||
placeholder = '%s(%%s, %s)' % (SpatialBackend.transform, field.srid)
|
||||
else:
|
||||
placeholder = '%s'
|
||||
|
||||
# Setting these up as if we had called `field.get_db_prep_lookup()`.
|
||||
where = [placeholder % geo_col]
|
||||
params = ()
|
||||
else:
|
||||
# `GeometryField.get_db_prep_lookup` returns a where clause
|
||||
# substitution array in addition to the parameters.
|
||||
where, params = field.get_db_prep_lookup(lookup_type, value)
|
||||
|
||||
# The annotation will be a `GeoAnnotation` object that
|
||||
# will contain the necessary geometry field metadata for
|
||||
# the `get_geo_where_clause` to construct the appropriate
|
||||
# spatial SQL when `make_atom` is called.
|
||||
annotation = GeoAnnotation(field, value, where)
|
||||
return super(WhereNode, self).add(((obj.alias, col, field.db_type()), lookup_type, annotation, params), connector)
|
||||
|
||||
def make_atom(self, child, qn):
|
||||
obj, lookup_type, value_annot, params = child
|
||||
|
||||
if isinstance(value_annot, GeoAnnotation):
|
||||
if lookup_type in SpatialBackend.gis_terms:
|
||||
# Getting the geographic where clause; substitution parameters
|
||||
# will be populated in the GeoFieldSQL object returned by the
|
||||
# GeometryField.
|
||||
alias, col, db_type = obj
|
||||
gwc = get_geo_where_clause(alias, col, lookup_type, value_annot)
|
||||
return gwc % value_annot.where, params
|
||||
else:
|
||||
raise TypeError('Invalid lookup type: %r' % lookup_type)
|
||||
else:
|
||||
# If not a GeometryField, call the `make_atom` from the
|
||||
# base class.
|
||||
return super(GeoWhereNode, self).make_atom(child, qn)
|
||||
return super(GeoWhereNode, self).make_atom(child, qn, connection)
|
||||
|
||||
@classmethod
|
||||
def _check_geo_field(cls, opts, lookup):
|
||||
"""
|
||||
Utility for checking the given lookup with the given model options.
|
||||
Utility for checking the given lookup with the given model options.
|
||||
The lookup is a string either specifying the geographic field, e.g.
|
||||
'point, 'the_geom', or a related lookup on a geographic field like
|
||||
'address__point'.
|
||||
|
@ -121,7 +74,7 @@ class GeoWhereNode(WhereNode):
|
|||
# If the field list is still around, then it means that the
|
||||
# lookup was for a geometry field across a relationship --
|
||||
# thus we keep on getting the related model options and the
|
||||
# model field associated with the next field in the list
|
||||
# model field associated with the next field in the list
|
||||
# until there's no more left.
|
||||
while len(field_list):
|
||||
opts = geo_fld.rel.to._meta
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos')
|
||||
|
||||
try:
|
||||
module = import_module('.%s' % geom_backend, 'django.contrib.gis.geometry.backend')
|
||||
except ImportError, e:
|
||||
try:
|
||||
module = import_module(geom_backend)
|
||||
except ImportError, e_user:
|
||||
raise ImproperlyConfigured('Could not import user-defined GEOMETRY_BACKEND '
|
||||
'"%s".' % geom_backend)
|
||||
|
||||
try:
|
||||
Geometry = module.Geometry
|
||||
GeometryException = module.GeometryException
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('Cannot import Geometry from the "%s" '
|
||||
'geometry backend.' % geom_backend)
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib.gis.geos import \
|
||||
GEOSGeometry as Geometry, \
|
||||
GEOSException as GeometryException
|
|
@ -1,233 +0,0 @@
|
|||
"""
|
||||
Imports the SpatialRefSys and GeometryColumns models dependent on the
|
||||
spatial database backend.
|
||||
"""
|
||||
import re
|
||||
from django.conf import settings
|
||||
|
||||
# Checking for the presence of GDAL (needed for the SpatialReference object)
|
||||
from django.contrib.gis.gdal import HAS_GDAL, PYTHON23
|
||||
if HAS_GDAL:
|
||||
from django.contrib.gis.gdal import SpatialReference
|
||||
|
||||
class SpatialRefSysMixin(object):
|
||||
"""
|
||||
The SpatialRefSysMixin is a class used by the database-dependent
|
||||
SpatialRefSys objects to reduce redundnant code.
|
||||
"""
|
||||
# For pulling out the spheroid from the spatial reference string. This
|
||||
# regular expression is used only if the user does not have GDAL installed.
|
||||
# TODO: Flattening not used in all ellipsoids, could also be a minor axis,
|
||||
# or 'b' parameter.
|
||||
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
|
||||
|
||||
# For pulling out the units on platforms w/o GDAL installed.
|
||||
# TODO: Figure out how to pull out angular units of projected coordinate system and
|
||||
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
|
||||
# distance queries.
|
||||
units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$')
|
||||
|
||||
def srs(self):
|
||||
"""
|
||||
Returns a GDAL SpatialReference object, if GDAL is installed.
|
||||
"""
|
||||
if HAS_GDAL:
|
||||
# TODO: Is caching really necessary here? Is complexity worth it?
|
||||
if hasattr(self, '_srs'):
|
||||
# Returning a clone of the cached SpatialReference object.
|
||||
return self._srs.clone()
|
||||
else:
|
||||
# Attempting to cache a SpatialReference object.
|
||||
|
||||
# Trying to get from WKT first.
|
||||
try:
|
||||
self._srs = SpatialReference(self.wkt)
|
||||
return self.srs
|
||||
except Exception, msg:
|
||||
pass
|
||||
|
||||
try:
|
||||
self._srs = SpatialReference(self.proj4text)
|
||||
return self.srs
|
||||
except Exception, msg:
|
||||
pass
|
||||
|
||||
raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg))
|
||||
else:
|
||||
raise Exception('GDAL is not installed.')
|
||||
srs = property(srs)
|
||||
|
||||
def ellipsoid(self):
|
||||
"""
|
||||
Returns a tuple of the ellipsoid parameters:
|
||||
(semimajor axis, semiminor axis, and inverse flattening).
|
||||
"""
|
||||
if HAS_GDAL:
|
||||
return self.srs.ellipsoid
|
||||
else:
|
||||
m = self.spheroid_regex.match(self.wkt)
|
||||
if m: return (float(m.group('major')), float(m.group('flattening')))
|
||||
else: return None
|
||||
ellipsoid = property(ellipsoid)
|
||||
|
||||
def name(self):
|
||||
"Returns the projection name."
|
||||
return self.srs.name
|
||||
name = property(name)
|
||||
|
||||
def spheroid(self):
|
||||
"Returns the spheroid name for this spatial reference."
|
||||
return self.srs['spheroid']
|
||||
spheroid = property(spheroid)
|
||||
|
||||
def datum(self):
|
||||
"Returns the datum for this spatial reference."
|
||||
return self.srs['datum']
|
||||
datum = property(datum)
|
||||
|
||||
def projected(self):
|
||||
"Is this Spatial Reference projected?"
|
||||
if HAS_GDAL:
|
||||
return self.srs.projected
|
||||
else:
|
||||
return self.wkt.startswith('PROJCS')
|
||||
projected = property(projected)
|
||||
|
||||
def local(self):
|
||||
"Is this Spatial Reference local?"
|
||||
if HAS_GDAL:
|
||||
return self.srs.local
|
||||
else:
|
||||
return self.wkt.startswith('LOCAL_CS')
|
||||
local = property(local)
|
||||
|
||||
def geographic(self):
|
||||
"Is this Spatial Reference geographic?"
|
||||
if HAS_GDAL:
|
||||
return self.srs.geographic
|
||||
else:
|
||||
return self.wkt.startswith('GEOGCS')
|
||||
geographic = property(geographic)
|
||||
|
||||
def linear_name(self):
|
||||
"Returns the linear units name."
|
||||
if HAS_GDAL:
|
||||
return self.srs.linear_name
|
||||
elif self.geographic:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit_name')
|
||||
linear_name = property(linear_name)
|
||||
|
||||
def linear_units(self):
|
||||
"Returns the linear units."
|
||||
if HAS_GDAL:
|
||||
return self.srs.linear_units
|
||||
elif self.geographic:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit')
|
||||
linear_units = property(linear_units)
|
||||
|
||||
def angular_name(self):
|
||||
"Returns the name of the angular units."
|
||||
if HAS_GDAL:
|
||||
return self.srs.angular_name
|
||||
elif self.projected:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit_name')
|
||||
angular_name = property(angular_name)
|
||||
|
||||
def angular_units(self):
|
||||
"Returns the angular units."
|
||||
if HAS_GDAL:
|
||||
return self.srs.angular_units
|
||||
elif self.projected:
|
||||
return None
|
||||
else:
|
||||
m = self.units_regex.match(self.wkt)
|
||||
return m.group('unit')
|
||||
angular_units = property(angular_units)
|
||||
|
||||
def units(self):
|
||||
"Returns a tuple of the units and the name."
|
||||
if self.projected or self.local:
|
||||
return (self.linear_units, self.linear_name)
|
||||
elif self.geographic:
|
||||
return (self.angular_units, self.angular_name)
|
||||
else:
|
||||
return (None, None)
|
||||
units = property(units)
|
||||
|
||||
def get_units(cls, wkt):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrive the units on the given WKT, without having to use
|
||||
any of the database fields.
|
||||
"""
|
||||
if HAS_GDAL:
|
||||
return SpatialReference(wkt).units
|
||||
else:
|
||||
m = cls.units_regex.match(wkt)
|
||||
return m.group('unit'), m.group('unit_name')
|
||||
get_units = classmethod(get_units)
|
||||
|
||||
def get_spheroid(cls, wkt, string=True):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
||||
"""
|
||||
if HAS_GDAL:
|
||||
srs = SpatialReference(wkt)
|
||||
sphere_params = srs.ellipsoid
|
||||
sphere_name = srs['spheroid']
|
||||
else:
|
||||
m = cls.spheroid_regex.match(wkt)
|
||||
if m:
|
||||
sphere_params = (float(m.group('major')), float(m.group('flattening')))
|
||||
sphere_name = m.group('name')
|
||||
else:
|
||||
return None
|
||||
|
||||
if not string:
|
||||
return sphere_name, sphere_params
|
||||
else:
|
||||
# `string` parameter used to place in format acceptable by PostGIS
|
||||
if len(sphere_params) == 3:
|
||||
radius, flattening = sphere_params[0], sphere_params[2]
|
||||
else:
|
||||
radius, flattening = sphere_params
|
||||
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
|
||||
get_spheroid = classmethod(get_spheroid)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Returns the string representation. If GDAL is installed,
|
||||
it will be 'pretty' OGC WKT.
|
||||
"""
|
||||
try:
|
||||
return unicode(self.srs)
|
||||
except:
|
||||
return unicode(self.wkt)
|
||||
|
||||
# Django test suite on 2.3 platforms will choke on code inside this
|
||||
# conditional.
|
||||
if not PYTHON23:
|
||||
try:
|
||||
# try/except'ing the importation of SpatialBackend. Have to fail
|
||||
# silently because this module may be inadvertently invoked by
|
||||
# non-GeoDjango users (e.g., when the Django test suite executes
|
||||
# the models.py of all contrib apps).
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
if SpatialBackend.mysql: raise Exception
|
||||
|
||||
# Exposing the SpatialRefSys and GeometryColumns models.
|
||||
class SpatialRefSys(SpatialBackend.SpatialRefSys, SpatialRefSysMixin):
|
||||
pass
|
||||
GeometryColumns = SpatialBackend.GeometryColumns
|
||||
except:
|
||||
pass
|
|
@ -1,11 +1,11 @@
|
|||
from django.http import HttpResponse, Http404
|
||||
from django.template import loader
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core import urlresolvers
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||
from django.db.models import get_model
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
from django.db import connections, DEFAULT_DB_ALIAS
|
||||
from django.db.models import get_model
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
from django.contrib.gis.shortcuts import render_to_kml, render_to_kmz
|
||||
|
@ -25,7 +25,7 @@ def index(request, sitemaps):
|
|||
pages = site.paginator.num_pages
|
||||
sitemap_url = urlresolvers.reverse('django.contrib.gis.sitemaps.views.sitemap', kwargs={'section': section})
|
||||
sites.append('%s://%s%s' % (protocol, current_site.domain, sitemap_url))
|
||||
|
||||
|
||||
if pages > 1:
|
||||
for page in range(2, pages+1):
|
||||
sites.append('%s://%s%s?p=%s' % (protocol, current_site.domain, sitemap_url, page))
|
||||
|
@ -59,7 +59,7 @@ def sitemap(request, sitemaps, section=None):
|
|||
xml = smart_str(loader.render_to_string('gis/sitemaps/geo_sitemap.xml', {'urlset': urls}))
|
||||
return HttpResponse(xml, mimetype='application/xml')
|
||||
|
||||
def kml(request, label, model, field_name=None, compress=False):
|
||||
def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB_ALIAS):
|
||||
"""
|
||||
This view generates KML for the given app label, model, and field name.
|
||||
|
||||
|
@ -79,17 +79,19 @@ def kml(request, label, model, field_name=None, compress=False):
|
|||
except:
|
||||
raise Http404('Invalid geometry field.')
|
||||
|
||||
if SpatialBackend.postgis:
|
||||
connection = connections[using]
|
||||
|
||||
if connection.ops.postgis:
|
||||
# PostGIS will take care of transformation.
|
||||
placemarks = klass._default_manager.kml(field_name=field_name)
|
||||
placemarks = klass._default_manager.using(using).kml(field_name=field_name)
|
||||
else:
|
||||
# There's no KML method on Oracle or MySQL, so we use the `kml`
|
||||
# attribute of the lazy geometry instead.
|
||||
placemarks = []
|
||||
if SpatialBackend.oracle:
|
||||
qs = klass._default_manager.transform(4326, field_name=field_name)
|
||||
if connection.ops.oracle:
|
||||
qs = klass._default_manager.using(using).transform(4326, field_name=field_name)
|
||||
else:
|
||||
qs = klass._default_manager.all()
|
||||
qs = klass._default_manager.using(using).all()
|
||||
for mod in qs:
|
||||
setattr(mod, 'kml', getattr(mod, field_name).kml)
|
||||
placemarks.append(mod)
|
||||
|
@ -101,8 +103,8 @@ def kml(request, label, model, field_name=None, compress=False):
|
|||
render = render_to_kml
|
||||
return render('gis/kml/placemarks.kml', {'places' : placemarks})
|
||||
|
||||
def kmz(request, label, model, field_name=None):
|
||||
def kmz(request, label, model, field_name=None, using=DEFAULT_DB_ALIAS):
|
||||
"""
|
||||
This view returns KMZ for the given app label, model, and field name.
|
||||
"""
|
||||
return kml(request, label, model, field_name, True)
|
||||
return kml(request, label, model, field_name, compress=True, using=using)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import sys, unittest
|
||||
from django.test.simple import run_tests
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
def geo_suite():
|
||||
|
@ -14,12 +15,11 @@ def geo_suite():
|
|||
from django.contrib.gis.utils import HAS_GEOIP
|
||||
from django.contrib.gis.tests.utils import postgis, mysql
|
||||
|
||||
# The test suite.
|
||||
s = unittest.TestSuite()
|
||||
gis_tests = []
|
||||
|
||||
# Adding the GEOS tests.
|
||||
from django.contrib.gis.geos import tests as geos_tests
|
||||
s.addTest(geos_tests.suite())
|
||||
gis_tests.append(geos_tests.suite())
|
||||
|
||||
# Tests that require use of a spatial database (e.g., creation of models)
|
||||
test_apps = ['geoapp', 'relatedapp']
|
||||
|
@ -44,7 +44,7 @@ def geo_suite():
|
|||
|
||||
# Adding the GDAL tests.
|
||||
from django.contrib.gis.gdal import tests as gdal_tests
|
||||
s.addTest(gdal_tests.suite())
|
||||
gis_tests.append(gdal_tests.suite())
|
||||
else:
|
||||
print >>sys.stderr, "GDAL not available - no tests requiring GDAL will be run."
|
||||
|
||||
|
@ -55,9 +55,9 @@ def geo_suite():
|
|||
# in the `test_suite_names`.
|
||||
for suite_name in test_suite_names:
|
||||
tsuite = import_module('django.contrib.gis.tests.' + suite_name)
|
||||
s.addTest(tsuite.suite())
|
||||
gis_tests.append(tsuite.suite())
|
||||
|
||||
return s, test_apps
|
||||
return gis_tests, test_apps
|
||||
|
||||
def run_gis_tests(test_labels, **kwargs):
|
||||
"""
|
||||
|
@ -83,21 +83,13 @@ def run_gis_tests(test_labels, **kwargs):
|
|||
# Setting the URLs.
|
||||
settings.ROOT_URLCONF = 'django.contrib.gis.tests.urls'
|
||||
|
||||
# Creating the test suite, adding the test models to INSTALLED_APPS, and
|
||||
# adding the model test suites to our suite package.
|
||||
gis_suite, test_apps = geo_suite()
|
||||
# Creating the test suite, adding the test models to INSTALLED_APPS
|
||||
# so they will be tested.
|
||||
gis_tests, test_apps = geo_suite()
|
||||
for test_model in test_apps:
|
||||
module_name = 'django.contrib.gis.tests.%s' % test_model
|
||||
if mysql:
|
||||
test_module = 'tests_mysql'
|
||||
else:
|
||||
test_module = 'tests'
|
||||
new_installed.append(module_name)
|
||||
|
||||
# Getting the model test suite
|
||||
tsuite = import_module(module_name + '.' + test_module)
|
||||
gis_suite.addTest(tsuite.suite())
|
||||
|
||||
# Resetting the loaded flag to take into account what we appended to
|
||||
# the INSTALLED_APPS (since this routine is invoked through
|
||||
# django/core/management, it caches the apps; this ensures that syncdb
|
||||
|
@ -105,67 +97,13 @@ def run_gis_tests(test_labels, **kwargs):
|
|||
settings.INSTALLED_APPS = new_installed
|
||||
loading.cache.loaded = False
|
||||
|
||||
kwargs['extra_tests'] = gis_tests
|
||||
|
||||
# Running the tests using the GIS test runner.
|
||||
result = run_tests(test_labels, suite=gis_suite, **kwargs)
|
||||
result = run_tests(test_labels, **kwargs)
|
||||
|
||||
# Restoring modified settings.
|
||||
settings.INSTALLED_APPS = old_installed
|
||||
settings.ROOT_URLCONF = old_root_urlconf
|
||||
|
||||
return result
|
||||
|
||||
def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite=None):
|
||||
"""
|
||||
Set `TEST_RUNNER` in your settings with this routine in order to
|
||||
scaffold test spatial databases correctly for your GeoDjango models.
|
||||
For more documentation, please consult the following URL:
|
||||
http://geodjango.org/docs/testing.html.
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.models import get_app, get_apps
|
||||
from django.test.simple import build_suite, build_test, reorder_suite, TestCase
|
||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||
|
||||
# The `create_test_spatial_db` routine abstracts away all the steps needed
|
||||
# to properly construct a spatial database for the backend.
|
||||
from django.contrib.gis.db.backend import create_test_spatial_db
|
||||
|
||||
# Setting up for testing.
|
||||
setup_test_environment()
|
||||
settings.DEBUG = False
|
||||
old_name = settings.DATABASE_NAME
|
||||
|
||||
# Creating the test spatial database.
|
||||
create_test_spatial_db(verbosity=verbosity, autoclobber=not interactive)
|
||||
|
||||
# The suite may be passed in manually, e.g., when we run the GeoDjango test,
|
||||
# we want to build it and pass it in due to some customizations. Otherwise,
|
||||
# the normal test suite creation process from `django.test.simple.run_tests`
|
||||
# is used to create the test suite.
|
||||
if suite is None:
|
||||
suite = unittest.TestSuite()
|
||||
if test_labels:
|
||||
for label in test_labels:
|
||||
if '.' in label:
|
||||
suite.addTest(build_test(label))
|
||||
else:
|
||||
app = get_app(label)
|
||||
suite.addTest(build_suite(app))
|
||||
else:
|
||||
for app in get_apps():
|
||||
suite.addTest(build_suite(app))
|
||||
|
||||
for test in extra_tests:
|
||||
suite.addTest(test)
|
||||
|
||||
suite = reorder_suite(suite, (TestCase,))
|
||||
|
||||
# Executing the tests (including the model tests), and destorying the
|
||||
# test database after the tests have completed.
|
||||
result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
|
||||
connection.creation.destroy_test_db(old_name, verbosity)
|
||||
teardown_test_environment()
|
||||
|
||||
# Returning the total failures and errors
|
||||
return len(result.failures) + len(result.errors)
|
||||
|
|
|
@ -96,9 +96,9 @@ class DistanceTest(unittest.TestCase):
|
|||
# Creating the query set.
|
||||
qs = AustraliaCity.objects.order_by('name')
|
||||
if type_error:
|
||||
# A TypeError should be raised on PostGIS when trying to pass
|
||||
# A ValueError should be raised on PostGIS when trying to pass
|
||||
# Distance objects into a DWithin query using a geodetic field.
|
||||
self.assertRaises(TypeError, AustraliaCity.objects.filter, point__dwithin=(self.au_pnt, dist))
|
||||
self.assertRaises(ValueError, AustraliaCity.objects.filter(point__dwithin=(self.au_pnt, dist)).count)
|
||||
else:
|
||||
self.assertEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist))))
|
||||
|
||||
|
@ -237,15 +237,15 @@ class DistanceTest(unittest.TestCase):
|
|||
# Oracle doesn't have this limitation -- PostGIS only allows geodetic
|
||||
# distance queries from Points to PointFields.
|
||||
mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
|
||||
self.assertRaises(TypeError,
|
||||
self.assertRaises(ValueError, len,
|
||||
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
|
||||
# Too many params (4 in this case) should raise a ValueError.
|
||||
self.assertRaises(ValueError,
|
||||
AustraliaCity.objects.filter, point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4'))
|
||||
self.assertRaises(ValueError, len,
|
||||
AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')))
|
||||
|
||||
# Not enough params should raise a ValueError.
|
||||
self.assertRaises(ValueError,
|
||||
AustraliaCity.objects.filter, point__distance_lte=('POINT(5 23)',))
|
||||
self.assertRaises(ValueError, len,
|
||||
AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)',)))
|
||||
|
||||
# Getting all cities w/in 550 miles of Hobart.
|
||||
hobart = AustraliaCity.objects.get(name='Hobart')
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,8 +0,0 @@
|
|||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Houston', GeomFromText('POINT (-95.363151 29.763374)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Dallas', GeomFromText('POINT (-96.801611 32.782057)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Oklahoma City', GeomFromText('POINT (-97.521157 34.464642)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Wellington', GeomFromText('POINT (174.783117 -41.315268)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Pueblo', GeomFromText('POINT (-104.609252 38.255001)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Lawrence', GeomFromText('POINT (-95.235060 38.971823)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Chicago', GeomFromText('POINT (-87.650175 41.850385)'));
|
||||
INSERT INTO geoapp_city (`name`, `point`) VALUES ('Victoria', GeomFromText('POINT (-123.305196 48.462611)'));
|
|
@ -1,8 +0,0 @@
|
|||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Houston', SDO_GEOMETRY('POINT (-95.363151 29.763374)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Dallas', SDO_GEOMETRY('POINT (-96.801611 32.782057)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Oklahoma City', SDO_GEOMETRY('POINT (-97.521157 34.464642)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Wellington', SDO_GEOMETRY('POINT (174.783117 -41.315268)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Pueblo', SDO_GEOMETRY('POINT (-104.609252 38.255001)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Lawrence', SDO_GEOMETRY('POINT (-95.235060 38.971823)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Chicago', SDO_GEOMETRY('POINT (-87.650175 41.850385)', 4326));
|
||||
INSERT INTO GEOAPP_CITY ("NAME", "POINT") VALUES ('Victoria', SDO_GEOMETRY('POINT (-123.305196 48.462611)', 4326));
|
|
@ -1,8 +0,0 @@
|
|||
INSERT INTO geoapp_city ("name", "point") VALUES ('Houston', 'SRID=4326;POINT (-95.363151 29.763374)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Dallas', 'SRID=4326;POINT (-96.801611 32.782057)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Oklahoma City', 'SRID=4326;POINT (-97.521157 34.464642)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Wellington', 'SRID=4326;POINT (174.783117 -41.315268)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Pueblo', 'SRID=4326;POINT (-104.609252 38.255001)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Lawrence', 'SRID=4326;POINT (-95.235060 38.971823)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Chicago', 'SRID=4326;POINT (-87.650175 41.850385)');
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Victoria', 'SRID=4326;POINT (-123.305196 48.462611)');
|
|
@ -1,8 +0,0 @@
|
|||
INSERT INTO geoapp_city ("name", "point") VALUES ('Houston', GeomFromText('POINT (-95.363151 29.763374)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Dallas', GeomFromText('POINT (-96.801611 32.782057)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Oklahoma City', GeomFromText('POINT (-97.521157 34.464642)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Wellington', GeomFromText('POINT (174.783117 -41.315268)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Pueblo', GeomFromText('POINT (-104.609252 38.255001)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Lawrence', GeomFromText('POINT (-95.235060 38.971823)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Chicago', GeomFromText('POINT (-87.650175 41.850385)', 4326));
|
||||
INSERT INTO geoapp_city ("name", "point") VALUES ('Victoria', GeomFromText('POINT (-123.305196 48.462611)', 4326));
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,4 @@
|
|||
import os, unittest
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_postgis, no_spatialite
|
||||
from django.contrib.gis.shortcuts import render_to_kmz
|
||||
from models import City
|
||||
|
|
|
@ -1,49 +1,29 @@
|
|||
import os, unittest
|
||||
import re, os, unittest
|
||||
from django.db import connection
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.geos import *
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.contrib.gis.tests.utils import no_oracle, no_postgis, no_spatialite
|
||||
from django.contrib.gis.tests.utils import \
|
||||
no_mysql, no_oracle, no_postgis, no_spatialite, \
|
||||
mysql, oracle, postgis, spatialite
|
||||
from django.test import TestCase
|
||||
|
||||
from models import Country, City, PennsylvaniaCity, State
|
||||
|
||||
if not SpatialBackend.spatialite:
|
||||
if not spatialite:
|
||||
from models import Feature, MinusOneSRID
|
||||
|
||||
# TODO: Some tests depend on the success/failure of previous tests, these should
|
||||
# be decoupled. This flag is an artifact of this problem, and makes debugging easier;
|
||||
# specifically, the DISABLE flag will disables all tests, allowing problem tests to
|
||||
# be examined individually.
|
||||
DISABLE = False
|
||||
class GeoModelTest(TestCase):
|
||||
|
||||
class GeoModelTest(unittest.TestCase):
|
||||
|
||||
def test01_initial_sql(self):
|
||||
"Testing geographic initial SQL."
|
||||
if DISABLE: return
|
||||
if SpatialBackend.oracle:
|
||||
# Oracle doesn't allow strings longer than 4000 characters
|
||||
# in SQL files, and I'm stumped on how to use Oracle BFILE's
|
||||
# in PLSQL, so we set up the larger geometries manually, rather
|
||||
# than relying on the initial SQL.
|
||||
|
||||
# Routine for returning the path to the data files.
|
||||
data_dir = os.path.join(os.path.dirname(__file__), 'sql')
|
||||
def get_file(wkt_file):
|
||||
return os.path.join(data_dir, wkt_file)
|
||||
State(name='Puerto Rico', poly=None).save()
|
||||
State(name='Colorado', poly=fromfile(get_file('co.wkt'))).save()
|
||||
State(name='Kansas', poly=fromfile(get_file('ks.wkt'))).save()
|
||||
Country(name='Texas', mpoly=fromfile(get_file('tx.wkt'))).save()
|
||||
Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt'))).save()
|
||||
|
||||
# Ensuring that data was loaded from initial SQL.
|
||||
def test01_fixtures(self):
|
||||
"Testing geographic model initialization from fixtures."
|
||||
# Ensuring that data was loaded from initial data fixtures.
|
||||
self.assertEqual(2, Country.objects.count())
|
||||
self.assertEqual(8, City.objects.count())
|
||||
self.assertEqual(3, State.objects.count())
|
||||
self.assertEqual(2, State.objects.count())
|
||||
|
||||
def test02_proxy(self):
|
||||
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
||||
if DISABLE: return
|
||||
## Testing on a Point
|
||||
pnt = Point(0, 0)
|
||||
nullcity = City(name='NullCity', point=pnt)
|
||||
|
@ -110,11 +90,13 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertEqual(ply, State.objects.get(name='NullState').poly)
|
||||
ns.delete()
|
||||
|
||||
@no_oracle # Oracle does not support KML.
|
||||
@no_spatialite # SpatiaLite does not support KML.
|
||||
def test03a_kml(self):
|
||||
"Testing KML output from the database using GeoQuerySet.kml()."
|
||||
if DISABLE: return
|
||||
# Only PostGIS supports KML serialization
|
||||
if not postgis:
|
||||
self.assertRaises(NotImplementedError, State.objects.all().kml, field_name='poly')
|
||||
return
|
||||
|
||||
# Should throw a TypeError when trying to obtain KML from a
|
||||
# non-geometry field.
|
||||
qs = City.objects.all()
|
||||
|
@ -122,14 +104,10 @@ class GeoModelTest(unittest.TestCase):
|
|||
|
||||
# The reference KML depends on the version of PostGIS used
|
||||
# (the output stopped including altitude in 1.3.3).
|
||||
major, minor1, minor2 = SpatialBackend.version
|
||||
ref_kml1 = '<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>'
|
||||
ref_kml2 = '<Point><coordinates>-104.609252,38.255001</coordinates></Point>'
|
||||
if major == 1:
|
||||
if minor1 > 3 or (minor1 == 3 and minor2 >= 3): ref_kml = ref_kml2
|
||||
else: ref_kml = ref_kml1
|
||||
if connection.ops.spatial_version >= (1, 3, 3):
|
||||
ref_kml = '<Point><coordinates>-104.609252,38.255001</coordinates></Point>'
|
||||
else:
|
||||
ref_kml = ref_kml2
|
||||
ref_kml = '<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>'
|
||||
|
||||
# Ensuring the KML is as expected.
|
||||
ptown1 = City.objects.kml(field_name='point', precision=9).get(name='Pueblo')
|
||||
|
@ -137,19 +115,20 @@ class GeoModelTest(unittest.TestCase):
|
|||
for ptown in [ptown1, ptown2]:
|
||||
self.assertEqual(ref_kml, ptown.kml)
|
||||
|
||||
@no_spatialite # SpatiaLite does not support GML.
|
||||
def test03b_gml(self):
|
||||
"Testing GML output from the database using GeoQuerySet.gml()."
|
||||
if DISABLE: return
|
||||
if mysql or spatialite:
|
||||
self.assertRaises(NotImplementedError, Country.objects.all().gml, field_name='mpoly')
|
||||
return
|
||||
|
||||
# Should throw a TypeError when tyring to obtain GML from a
|
||||
# non-geometry field.
|
||||
# non-geometry field.
|
||||
qs = City.objects.all()
|
||||
self.assertRaises(TypeError, qs.gml, field_name='name')
|
||||
ptown1 = City.objects.gml(field_name='point', precision=9).get(name='Pueblo')
|
||||
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
|
||||
|
||||
import re
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
# No precision parameter for Oracle :-/
|
||||
gml_regex = re.compile(r'^<gml:Point srsName="SDO:4326" xmlns:gml="http://www.opengis.net/gml"><gml:coordinates decimal="\." cs="," ts=" ">-104.60925\d+,38.25500\d+ </gml:coordinates></gml:Point>')
|
||||
for ptown in [ptown1, ptown2]:
|
||||
|
@ -159,17 +138,14 @@ class GeoModelTest(unittest.TestCase):
|
|||
for ptown in [ptown1, ptown2]:
|
||||
self.failUnless(gml_regex.match(ptown.gml))
|
||||
|
||||
@no_spatialite
|
||||
@no_oracle
|
||||
def test03c_geojson(self):
|
||||
"Testing GeoJSON output from the database using GeoQuerySet.geojson()."
|
||||
if DISABLE: return
|
||||
# PostGIS only supports GeoJSON on 1.3.4+
|
||||
if not SpatialBackend.geojson:
|
||||
# Only PostGIS 1.3.4+ supports GeoJSON.
|
||||
if not connection.ops.geojson:
|
||||
self.assertRaises(NotImplementedError, Country.objects.all().geojson, field_name='mpoly')
|
||||
return
|
||||
|
||||
major, minor1, minor2 = SpatialBackend.version
|
||||
if major >=1 and minor1 >= 4:
|
||||
if connection.ops.spatial_version >= (1, 4, 0):
|
||||
pueblo_json = '{"type":"Point","coordinates":[-104.609252,38.255001]}'
|
||||
houston_json = '{"type":"Point","crs":{"type":"name","properties":{"name":"EPSG:4326"}},"coordinates":[-95.363151,29.763374]}'
|
||||
victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.305196,48.462611]}'
|
||||
|
@ -179,10 +155,10 @@ class GeoModelTest(unittest.TestCase):
|
|||
houston_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"coordinates":[-95.36315100,29.76337400]}'
|
||||
victoria_json = '{"type":"Point","bbox":[-123.30519600,48.46261100,-123.30519600,48.46261100],"coordinates":[-123.30519600,48.46261100]}'
|
||||
chicago_json = '{"type":"Point","crs":{"type":"EPSG","properties":{"EPSG":4326}},"bbox":[-87.65018,41.85039,-87.65018,41.85039],"coordinates":[-87.65018,41.85039]}'
|
||||
|
||||
|
||||
# Precision argument should only be an integer
|
||||
self.assertRaises(TypeError, City.objects.geojson, precision='foo')
|
||||
|
||||
|
||||
# Reference queries and values.
|
||||
# SELECT ST_AsGeoJson("geoapp_city"."point", 8, 0) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Pueblo';
|
||||
self.assertEqual(pueblo_json, City.objects.geojson().get(name='Pueblo').geojson)
|
||||
|
@ -200,11 +176,13 @@ class GeoModelTest(unittest.TestCase):
|
|||
# 1.(3|4).x: SELECT ST_AsGeoJson("geoapp_city"."point", 5, 3) FROM "geoapp_city" WHERE "geoapp_city"."name" = 'Chicago';
|
||||
# Finally, we set every available keyword.
|
||||
self.assertEqual(chicago_json, City.objects.geojson(bbox=True, crs=True, precision=5).get(name='Chicago').geojson)
|
||||
|
||||
@no_oracle
|
||||
|
||||
def test03d_svg(self):
|
||||
"Testing SVG output using GeoQuerySet.svg()."
|
||||
if DISABLE: return
|
||||
if mysql or oracle:
|
||||
self.assertRaises(NotImplementedError, City.objects.svg)
|
||||
return
|
||||
|
||||
self.assertRaises(TypeError, City.objects.svg, precision='foo')
|
||||
# SELECT AsSVG(geoapp_city.point, 0, 8) FROM geoapp_city WHERE name = 'Pueblo';
|
||||
svg1 = 'cx="-104.609252" cy="-38.255001"'
|
||||
|
@ -214,9 +192,9 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertEqual(svg1, City.objects.svg().get(name='Pueblo').svg)
|
||||
self.assertEqual(svg2, City.objects.svg(relative=5).get(name='Pueblo').svg)
|
||||
|
||||
@no_mysql
|
||||
def test04_transform(self):
|
||||
"Testing the transform() GeoManager method."
|
||||
if DISABLE: return
|
||||
# Pre-transformed points for Houston and Pueblo.
|
||||
htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084)
|
||||
ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774)
|
||||
|
@ -224,7 +202,7 @@ class GeoModelTest(unittest.TestCase):
|
|||
|
||||
# Asserting the result of the transform operation with the values in
|
||||
# the pre-transformed points. Oracle does not have the 3084 SRID.
|
||||
if not SpatialBackend.oracle:
|
||||
if not oracle:
|
||||
h = City.objects.transform(htown.srid).get(name='Houston')
|
||||
self.assertEqual(3084, h.point.srid)
|
||||
self.assertAlmostEqual(htown.x, h.point.x, prec)
|
||||
|
@ -237,10 +215,10 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertAlmostEqual(ptown.x, p.point.x, prec)
|
||||
self.assertAlmostEqual(ptown.y, p.point.y, prec)
|
||||
|
||||
@no_mysql
|
||||
@no_spatialite # SpatiaLite does not have an Extent function
|
||||
def test05_extent(self):
|
||||
"Testing the `extent` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
# Reference query:
|
||||
# `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
|
||||
# => BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203)
|
||||
|
@ -252,11 +230,12 @@ class GeoModelTest(unittest.TestCase):
|
|||
for val, exp in zip(extent, expected):
|
||||
self.assertAlmostEqual(exp, val, 4)
|
||||
|
||||
# Only PostGIS has support for the MakeLine aggregate.
|
||||
@no_mysql
|
||||
@no_oracle
|
||||
@no_spatialite # SpatiaLite does not have a MakeLine function
|
||||
@no_spatialite
|
||||
def test06_make_line(self):
|
||||
"Testing the `make_line` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
# Ensuring that a `TypeError` is raised on models without PointFields.
|
||||
self.assertRaises(TypeError, State.objects.make_line)
|
||||
self.assertRaises(TypeError, Country.objects.make_line)
|
||||
|
@ -265,34 +244,26 @@ class GeoModelTest(unittest.TestCase):
|
|||
ref_line = GEOSGeometry('LINESTRING(-95.363151 29.763374,-96.801611 32.782057,-97.521157 34.464642,174.783117 -41.315268,-104.609252 38.255001,-95.23506 38.971823,-87.650175 41.850385,-123.305196 48.462611)', srid=4326)
|
||||
self.assertEqual(ref_line, City.objects.make_line())
|
||||
|
||||
@no_mysql
|
||||
def test09_disjoint(self):
|
||||
"Testing the `disjoint` lookup type."
|
||||
if DISABLE: return
|
||||
ptown = City.objects.get(name='Pueblo')
|
||||
qs1 = City.objects.filter(point__disjoint=ptown.point)
|
||||
self.assertEqual(7, qs1.count())
|
||||
|
||||
if not (SpatialBackend.postgis or SpatialBackend.spatialite):
|
||||
# TODO: Do NULL columns bork queries on PostGIS? The following
|
||||
# error is encountered:
|
||||
# psycopg2.ProgrammingError: invalid memory alloc request size 4294957297
|
||||
#
|
||||
# Similarly, on SpatiaLite Puerto Rico is also returned (could be a
|
||||
# manifestation of
|
||||
qs2 = State.objects.filter(poly__disjoint=ptown.point)
|
||||
self.assertEqual(1, qs2.count())
|
||||
self.assertEqual('Kansas', qs2[0].name)
|
||||
qs2 = State.objects.filter(poly__disjoint=ptown.point)
|
||||
self.assertEqual(1, qs2.count())
|
||||
self.assertEqual('Kansas', qs2[0].name)
|
||||
|
||||
def test10_contains_contained(self):
|
||||
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
|
||||
if DISABLE: return
|
||||
# Getting Texas, yes we were a country -- once ;)
|
||||
texas = Country.objects.get(name='Texas')
|
||||
|
||||
# Seeing what cities are in Texas, should get Houston and Dallas,
|
||||
# and Oklahoma City because 'contained' only checks on the
|
||||
# _bounding box_ of the Geometries.
|
||||
if not SpatialBackend.oracle:
|
||||
if not oracle:
|
||||
qs = City.objects.filter(point__contained=texas.mpoly)
|
||||
self.assertEqual(3, qs.count())
|
||||
cities = ['Houston', 'Dallas', 'Oklahoma City']
|
||||
|
@ -313,30 +284,31 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertEqual('New Zealand', nz.name)
|
||||
|
||||
# Spatialite 2.3 thinks that Lawrence is in Puerto Rico (a NULL geometry).
|
||||
if not SpatialBackend.spatialite:
|
||||
if not spatialite:
|
||||
ks = State.objects.get(poly__contains=lawrence.point)
|
||||
self.assertEqual('Kansas', ks.name)
|
||||
|
||||
# Pueblo and Oklahoma City (even though OK City is within the bounding box of Texas)
|
||||
# are not contained in Texas or New Zealand.
|
||||
# are not contained in Texas or New Zealand.
|
||||
self.assertEqual(0, len(Country.objects.filter(mpoly__contains=pueblo.point))) # Query w/GEOSGeometry object
|
||||
self.assertEqual(0, len(Country.objects.filter(mpoly__contains=okcity.point.wkt))) # Qeury w/WKT
|
||||
self.assertEqual((mysql and 1) or 0,
|
||||
len(Country.objects.filter(mpoly__contains=okcity.point.wkt))) # Qeury w/WKT
|
||||
|
||||
# OK City is contained w/in bounding box of Texas.
|
||||
if not SpatialBackend.oracle:
|
||||
if not oracle:
|
||||
qs = Country.objects.filter(mpoly__bbcontains=okcity.point)
|
||||
self.assertEqual(1, len(qs))
|
||||
self.assertEqual('Texas', qs[0].name)
|
||||
|
||||
@no_mysql
|
||||
def test11_lookup_insert_transform(self):
|
||||
"Testing automatic transform for lookups and inserts."
|
||||
if DISABLE: return
|
||||
# San Antonio in 'WGS84' (SRID 4326)
|
||||
sa_4326 = 'POINT (-98.493183 29.424170)'
|
||||
wgs_pnt = fromstr(sa_4326, srid=4326) # Our reference point in WGS84
|
||||
|
||||
# Oracle doesn't have SRID 3084, using 41157.
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
# San Antonio in 'Texas 4205, Southern Zone (1983, meters)' (SRID 41157)
|
||||
# Used the following Oracle SQL to get this value:
|
||||
# SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM(SDO_GEOMETRY('POINT (-98.493183 29.424170)', 4326), 41157)) FROM DUAL;
|
||||
|
@ -351,15 +323,14 @@ class GeoModelTest(unittest.TestCase):
|
|||
# `SDO_OVERLAPBDYINTERSECT` operates differently from
|
||||
# `ST_Intersects`, so contains is used instead.
|
||||
nad_pnt = fromstr(nad_wkt, srid=nad_srid)
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
tx = Country.objects.get(mpoly__contains=nad_pnt)
|
||||
else:
|
||||
tx = Country.objects.get(mpoly__intersects=nad_pnt)
|
||||
self.assertEqual('Texas', tx.name)
|
||||
|
||||
# Creating San Antonio. Remember the Alamo.
|
||||
sa = City(name='San Antonio', point=nad_pnt)
|
||||
sa.save()
|
||||
sa = City.objects.create(name='San Antonio', point=nad_pnt)
|
||||
|
||||
# Now verifying that San Antonio was transformed correctly
|
||||
sa = City.objects.get(name='San Antonio')
|
||||
|
@ -369,14 +340,17 @@ class GeoModelTest(unittest.TestCase):
|
|||
# If the GeometryField SRID is -1, then we shouldn't perform any
|
||||
# transformation if the SRID of the input geometry is different.
|
||||
# SpatiaLite does not support missing SRID values.
|
||||
if not SpatialBackend.spatialite:
|
||||
if not spatialite:
|
||||
m1 = MinusOneSRID(geom=Point(17, 23, srid=4326))
|
||||
m1.save()
|
||||
self.assertEqual(-1, m1.geom.srid)
|
||||
|
||||
@no_mysql
|
||||
def test12_null_geometries(self):
|
||||
"Testing NULL geometry support, and the `isnull` lookup type."
|
||||
if DISABLE: return
|
||||
# Creating a state with a NULL boundary.
|
||||
State.objects.create(name='Puerto Rico')
|
||||
|
||||
# Querying for both NULL and Non-NULL values.
|
||||
nullqs = State.objects.filter(poly__isnull=True)
|
||||
validqs = State.objects.filter(poly__isnull=False)
|
||||
|
@ -401,27 +375,28 @@ class GeoModelTest(unittest.TestCase):
|
|||
State.objects.filter(name='Northern Mariana Islands').update(poly=None)
|
||||
self.assertEqual(None, State.objects.get(name='Northern Mariana Islands').poly)
|
||||
|
||||
@no_oracle # No specific `left` or `right` operators in Oracle.
|
||||
@no_spatialite # No `left` or `right` operators in SpatiaLite.
|
||||
# Only PostGIS has `left` and `right` lookup types.
|
||||
@no_mysql
|
||||
@no_oracle
|
||||
@no_spatialite
|
||||
def test13_left_right(self):
|
||||
"Testing the 'left' and 'right' lookup types."
|
||||
if DISABLE: return
|
||||
# Left: A << B => true if xmax(A) < xmin(B)
|
||||
# Right: A >> B => true if xmin(A) > xmax(B)
|
||||
# See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
|
||||
# See: BOX2D_left() and BOX2D_right() in lwgeom_box2dfloat4.c in PostGIS source.
|
||||
|
||||
# Getting the borders for Colorado & Kansas
|
||||
co_border = State.objects.get(name='Colorado').poly
|
||||
ks_border = State.objects.get(name='Kansas').poly
|
||||
|
||||
# Note: Wellington has an 'X' value of 174, so it will not be considered
|
||||
# to the left of CO.
|
||||
# to the left of CO.
|
||||
|
||||
# These cities should be strictly to the right of the CO border.
|
||||
cities = ['Houston', 'Dallas', 'San Antonio', 'Oklahoma City',
|
||||
cities = ['Houston', 'Dallas', 'Oklahoma City',
|
||||
'Lawrence', 'Chicago', 'Wellington']
|
||||
qs = City.objects.filter(point__right=co_border)
|
||||
self.assertEqual(7, len(qs))
|
||||
self.assertEqual(6, len(qs))
|
||||
for c in qs: self.assertEqual(True, c.name in cities)
|
||||
|
||||
# These cities should be strictly to the right of the KS border.
|
||||
|
@ -442,16 +417,16 @@ class GeoModelTest(unittest.TestCase):
|
|||
|
||||
def test14_equals(self):
|
||||
"Testing the 'same_as' and 'equals' lookup types."
|
||||
if DISABLE: return
|
||||
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
|
||||
c1 = City.objects.get(point=pnt)
|
||||
c2 = City.objects.get(point__same_as=pnt)
|
||||
c3 = City.objects.get(point__equals=pnt)
|
||||
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
|
||||
|
||||
@no_mysql
|
||||
def test15_relate(self):
|
||||
"Testing the 'relate' lookup type."
|
||||
if DISABLE: return
|
||||
return
|
||||
# To make things more interesting, we will have our Texas reference point in
|
||||
# different SRIDs.
|
||||
pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', srid=2847)
|
||||
|
@ -459,19 +434,20 @@ class GeoModelTest(unittest.TestCase):
|
|||
|
||||
# Not passing in a geometry as first param shoud
|
||||
# raise a type error when initializing the GeoQuerySet
|
||||
self.assertRaises(TypeError, Country.objects.filter, mpoly__relate=(23, 'foo'))
|
||||
self.assertRaises(ValueError, Country.objects.filter(mpoly__relate=(23, 'foo')).count)
|
||||
|
||||
# Making sure the right exception is raised for the given
|
||||
# bad arguments.
|
||||
for bad_args, e in [((pnt1, 0), TypeError), ((pnt2, 'T*T***FF*', 0), ValueError)]:
|
||||
for bad_args, e in [((pnt1, 0), ValueError), ((pnt2, 'T*T***FF*', 0), ValueError)]:
|
||||
qs = Country.objects.filter(mpoly__relate=bad_args)
|
||||
self.assertRaises(e, qs.count)
|
||||
|
||||
# Relate works differently for the different backends.
|
||||
if SpatialBackend.postgis or SpatialBackend.spatialite:
|
||||
if postgis or spatialite:
|
||||
contains_mask = 'T*T***FF*'
|
||||
within_mask = 'T*F**F***'
|
||||
intersects_mask = 'T********'
|
||||
elif SpatialBackend.oracle:
|
||||
elif oracle:
|
||||
contains_mask = 'contains'
|
||||
within_mask = 'inside'
|
||||
# TODO: This is not quite the same as the PostGIS mask above
|
||||
|
@ -486,24 +462,23 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, within_mask)).name)
|
||||
|
||||
# Testing intersection relation mask.
|
||||
if not SpatialBackend.oracle:
|
||||
if not oracle:
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, intersects_mask)).name)
|
||||
self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, intersects_mask)).name)
|
||||
self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name)
|
||||
|
||||
def test16_createnull(self):
|
||||
"Testing creating a model instance and the geometry being None"
|
||||
if DISABLE: return
|
||||
c = City()
|
||||
self.assertEqual(c.point, None)
|
||||
|
||||
@no_mysql
|
||||
def test17_unionagg(self):
|
||||
"Testing the `unionagg` (aggregate union) GeoManager method."
|
||||
if DISABLE: return
|
||||
tx = Country.objects.get(name='Texas').mpoly
|
||||
# Houston, Dallas, San Antonio -- Oracle has different order.
|
||||
union1 = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
|
||||
union2 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374,-98.493183 29.424170)')
|
||||
# Houston, Dallas -- Oracle has different order.
|
||||
union1 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)')
|
||||
union2 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374)')
|
||||
qs = City.objects.filter(point__within=tx)
|
||||
self.assertRaises(TypeError, qs.unionagg, 'name')
|
||||
# Using `field_name` keyword argument in one query and specifying an
|
||||
|
@ -512,7 +487,7 @@ class GeoModelTest(unittest.TestCase):
|
|||
u1 = qs.unionagg(field_name='point')
|
||||
u2 = qs.order_by('name').unionagg()
|
||||
tol = 0.00001
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
union = union2
|
||||
else:
|
||||
union = union1
|
||||
|
@ -523,8 +498,7 @@ class GeoModelTest(unittest.TestCase):
|
|||
|
||||
@no_spatialite # SpatiaLite does not support abstract geometry columns
|
||||
def test18_geometryfield(self):
|
||||
"Testing GeometryField."
|
||||
if DISABLE: return
|
||||
"Testing the general GeometryField."
|
||||
Feature(name='Point', geom=Point(1, 1)).save()
|
||||
Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))).save()
|
||||
Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))).save()
|
||||
|
@ -545,60 +519,62 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
|
||||
self.assertEqual(f_3.geom, f_4.geom[2])
|
||||
|
||||
@no_mysql
|
||||
def test19_centroid(self):
|
||||
"Testing the `centroid` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
qs = State.objects.exclude(poly__isnull=True).centroid()
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
tol = 0.1
|
||||
elif SpatialBackend.spatialite:
|
||||
elif spatialite:
|
||||
tol = 0.000001
|
||||
else:
|
||||
tol = 0.000000001
|
||||
for s in qs:
|
||||
self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol))
|
||||
|
||||
@no_mysql
|
||||
def test20_pointonsurface(self):
|
||||
"Testing the `point_on_surface` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
# Reference values.
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
# SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) FROM GEOAPP_COUNTRY;
|
||||
ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326),
|
||||
'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326),
|
||||
}
|
||||
|
||||
elif SpatialBackend.postgis or SpatialBackend.spatialite:
|
||||
elif postgis or spatialite:
|
||||
# Using GEOSGeometry to compute the reference point on surface values
|
||||
# -- since PostGIS also uses GEOS these should be the same.
|
||||
ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface,
|
||||
'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface
|
||||
}
|
||||
for cntry in Country.objects.point_on_surface():
|
||||
if SpatialBackend.spatialite:
|
||||
|
||||
for c in Country.objects.point_on_surface():
|
||||
if spatialite:
|
||||
# XXX This seems to be a WKT-translation-related precision issue?
|
||||
tol = 0.00001
|
||||
else: tol = 0.000000001
|
||||
self.assertEqual(True, ref[cntry.name].equals_exact(cntry.point_on_surface, tol))
|
||||
else:
|
||||
tol = 0.000000001
|
||||
self.assertEqual(True, ref[c.name].equals_exact(c.point_on_surface, tol))
|
||||
|
||||
@no_mysql
|
||||
@no_oracle
|
||||
def test21_scale(self):
|
||||
"Testing the `scale` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
xfac, yfac = 2, 3
|
||||
tol = 5 # XXX The low precision tolerance is for SpatiaLite
|
||||
qs = Country.objects.scale(xfac, yfac, model_att='scaled')
|
||||
for c in qs:
|
||||
for p1, p2 in zip(c.mpoly, c.scaled):
|
||||
for r1, r2 in zip(p1, p2):
|
||||
for c1, c2 in zip(r1.coords, r2.coords):
|
||||
# XXX The low precision is for SpatiaLite
|
||||
self.assertAlmostEqual(c1[0] * xfac, c2[0], 5)
|
||||
self.assertAlmostEqual(c1[1] * yfac, c2[1], 5)
|
||||
self.assertAlmostEqual(c1[0] * xfac, c2[0], tol)
|
||||
self.assertAlmostEqual(c1[1] * yfac, c2[1], tol)
|
||||
|
||||
@no_mysql
|
||||
@no_oracle
|
||||
def test22_translate(self):
|
||||
"Testing the `translate` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
xfac, yfac = 5, -23
|
||||
qs = Country.objects.translate(xfac, yfac, model_att='translated')
|
||||
for c in qs:
|
||||
|
@ -609,57 +585,60 @@ class GeoModelTest(unittest.TestCase):
|
|||
self.assertAlmostEqual(c1[0] + xfac, c2[0], 5)
|
||||
self.assertAlmostEqual(c1[1] + yfac, c2[1], 5)
|
||||
|
||||
@no_mysql
|
||||
def test23_numgeom(self):
|
||||
"Testing the `num_geom` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
# Both 'countries' only have two geometries.
|
||||
for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom)
|
||||
for c in City.objects.filter(point__isnull=False).num_geom():
|
||||
# Oracle will return 1 for the number of geometries on non-collections,
|
||||
# whereas PostGIS will return None.
|
||||
if SpatialBackend.postgis: self.assertEqual(None, c.num_geom)
|
||||
else: self.assertEqual(1, c.num_geom)
|
||||
if postgis:
|
||||
self.assertEqual(None, c.num_geom)
|
||||
else:
|
||||
self.assertEqual(1, c.num_geom)
|
||||
|
||||
@no_mysql
|
||||
@no_spatialite # SpatiaLite can only count vertices in LineStrings
|
||||
def test24_numpoints(self):
|
||||
"Testing the `num_points` GeoQuerySet method."
|
||||
if DISABLE: return
|
||||
for c in Country.objects.num_points():
|
||||
self.assertEqual(c.mpoly.num_points, c.num_points)
|
||||
if not SpatialBackend.oracle:
|
||||
|
||||
if not oracle:
|
||||
# Oracle cannot count vertices in Point geometries.
|
||||
for c in City.objects.num_points(): self.assertEqual(1, c.num_points)
|
||||
|
||||
@no_mysql
|
||||
def test25_geoset(self):
|
||||
"Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods."
|
||||
if DISABLE: return
|
||||
geom = Point(5, 23)
|
||||
tol = 1
|
||||
qs = Country.objects.all().difference(geom).sym_difference(geom).union(geom)
|
||||
|
||||
# XXX For some reason SpatiaLite does something screwey with the Texas geometry here. Also,
|
||||
# XXX it doesn't like the null intersection.
|
||||
if SpatialBackend.spatialite:
|
||||
if spatialite:
|
||||
qs = qs.exclude(name='Texas')
|
||||
else:
|
||||
qs = qs.intersection(geom)
|
||||
|
||||
|
||||
for c in qs:
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
# Should be able to execute the queries; however, they won't be the same
|
||||
# as GEOS (because Oracle doesn't use GEOS internally like PostGIS or
|
||||
# SpatiaLite).
|
||||
pass
|
||||
else:
|
||||
self.assertEqual(c.mpoly.difference(geom), c.difference)
|
||||
if not SpatialBackend.spatialite:
|
||||
if not spatialite:
|
||||
self.assertEqual(c.mpoly.intersection(geom), c.intersection)
|
||||
self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
|
||||
self.assertEqual(c.mpoly.union(geom), c.union)
|
||||
|
||||
@no_mysql
|
||||
def test26_inherited_geofields(self):
|
||||
"Test GeoQuerySet methods on inherited Geometry fields."
|
||||
if DISABLE: return
|
||||
# Creating a Pennsylvanian city.
|
||||
mansfield = PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)')
|
||||
|
||||
|
@ -669,13 +648,12 @@ class GeoModelTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(1, qs.count())
|
||||
for pc in qs: self.assertEqual(32128, pc.point.srid)
|
||||
|
||||
@no_spatialite
|
||||
|
||||
@no_mysql
|
||||
@no_oracle
|
||||
@no_spatialite
|
||||
def test27_snap_to_grid(self):
|
||||
"Testing GeoQuerySet.snap_to_grid()."
|
||||
if DISABLE: return
|
||||
|
||||
# Let's try and break snap_to_grid() with bad combinations of arguments.
|
||||
for bad_args in ((), range(3), range(5)):
|
||||
self.assertRaises(ValueError, Country.objects.snap_to_grid, *bad_args)
|
||||
|
|
|
@ -1,186 +0,0 @@
|
|||
"""
|
||||
A limited test module is used for a limited spatial database.
|
||||
"""
|
||||
import os, unittest
|
||||
from models import Country, City, State, Feature
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.geos import *
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
class GeoModelTest(unittest.TestCase):
|
||||
|
||||
def test01_initial_sql(self):
|
||||
"Testing geographic initial SQL."
|
||||
# Ensuring that data was loaded from initial SQL.
|
||||
self.assertEqual(2, Country.objects.count())
|
||||
self.assertEqual(8, City.objects.count())
|
||||
self.assertEqual(2, State.objects.count())
|
||||
|
||||
def test02_proxy(self):
|
||||
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
||||
#### Testing on a Point
|
||||
pnt = Point(0, 0)
|
||||
nullcity = City(name='NullCity', point=pnt)
|
||||
nullcity.save()
|
||||
|
||||
# Making sure TypeError is thrown when trying to set with an
|
||||
# incompatible type.
|
||||
for bad in [5, 2.0, LineString((0, 0), (1, 1))]:
|
||||
try:
|
||||
nullcity.point = bad
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
self.fail('Should throw a TypeError')
|
||||
|
||||
# Now setting with a compatible GEOS Geometry, saving, and ensuring
|
||||
# the save took, notice no SRID is explicitly set.
|
||||
new = Point(5, 23)
|
||||
nullcity.point = new
|
||||
|
||||
# Ensuring that the SRID is automatically set to that of the
|
||||
# field after assignment, but before saving.
|
||||
self.assertEqual(4326, nullcity.point.srid)
|
||||
nullcity.save()
|
||||
|
||||
# Ensuring the point was saved correctly after saving
|
||||
self.assertEqual(new, City.objects.get(name='NullCity').point)
|
||||
|
||||
# Setting the X and Y of the Point
|
||||
nullcity.point.x = 23
|
||||
nullcity.point.y = 5
|
||||
# Checking assignments pre & post-save.
|
||||
self.assertNotEqual(Point(23, 5), City.objects.get(name='NullCity').point)
|
||||
nullcity.save()
|
||||
self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point)
|
||||
nullcity.delete()
|
||||
|
||||
#### Testing on a Polygon
|
||||
shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0))
|
||||
inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40))
|
||||
|
||||
# Creating a State object using a built Polygon
|
||||
ply = Polygon(shell, inner)
|
||||
nullstate = State(name='NullState', poly=ply)
|
||||
self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None
|
||||
nullstate.save()
|
||||
|
||||
ns = State.objects.get(name='NullState')
|
||||
self.assertEqual(ply, ns.poly)
|
||||
|
||||
# Testing the `ogr` and `srs` lazy-geometry properties.
|
||||
if gdal.HAS_GDAL:
|
||||
self.assertEqual(True, isinstance(ns.poly.ogr, gdal.OGRGeometry))
|
||||
self.assertEqual(ns.poly.wkb, ns.poly.ogr.wkb)
|
||||
self.assertEqual(True, isinstance(ns.poly.srs, gdal.SpatialReference))
|
||||
self.assertEqual('WGS 84', ns.poly.srs.name)
|
||||
|
||||
# Changing the interior ring on the poly attribute.
|
||||
new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30))
|
||||
ns.poly[1] = new_inner
|
||||
ply[1] = new_inner
|
||||
self.assertEqual(4326, ns.poly.srid)
|
||||
ns.save()
|
||||
self.assertEqual(ply, State.objects.get(name='NullState').poly)
|
||||
ns.delete()
|
||||
|
||||
def test03_contains_contained(self):
|
||||
"Testing the 'contained', 'contains', and 'bbcontains' lookup types."
|
||||
# Getting Texas, yes we were a country -- once ;)
|
||||
texas = Country.objects.get(name='Texas')
|
||||
|
||||
# Seeing what cities are in Texas, should get Houston and Dallas,
|
||||
# and Oklahoma City because MySQL 'within' only checks on the
|
||||
# _bounding box_ of the Geometries.
|
||||
qs = City.objects.filter(point__within=texas.mpoly)
|
||||
self.assertEqual(3, qs.count())
|
||||
cities = ['Houston', 'Dallas', 'Oklahoma City']
|
||||
for c in qs: self.assertEqual(True, c.name in cities)
|
||||
|
||||
# Pulling out some cities.
|
||||
houston = City.objects.get(name='Houston')
|
||||
wellington = City.objects.get(name='Wellington')
|
||||
pueblo = City.objects.get(name='Pueblo')
|
||||
okcity = City.objects.get(name='Oklahoma City')
|
||||
lawrence = City.objects.get(name='Lawrence')
|
||||
|
||||
# Now testing contains on the countries using the points for
|
||||
# Houston and Wellington.
|
||||
tx = Country.objects.get(mpoly__contains=houston.point) # Query w/GEOSGeometry
|
||||
nz = Country.objects.get(mpoly__contains=wellington.point.hex) # Query w/EWKBHEX
|
||||
ks = State.objects.get(poly__contains=lawrence.point)
|
||||
self.assertEqual('Texas', tx.name)
|
||||
self.assertEqual('New Zealand', nz.name)
|
||||
self.assertEqual('Kansas', ks.name)
|
||||
|
||||
# Pueblo is not contained in Texas or New Zealand.
|
||||
self.assertEqual(0, len(Country.objects.filter(mpoly__contains=pueblo.point))) # Query w/GEOSGeometry object
|
||||
|
||||
# OK City is contained w/in bounding box of Texas.
|
||||
qs = Country.objects.filter(mpoly__bbcontains=okcity.point)
|
||||
self.assertEqual(1, len(qs))
|
||||
self.assertEqual('Texas', qs[0].name)
|
||||
|
||||
def test04_disjoint(self):
|
||||
"Testing the `disjoint` lookup type."
|
||||
ptown = City.objects.get(name='Pueblo')
|
||||
qs1 = City.objects.filter(point__disjoint=ptown.point)
|
||||
self.assertEqual(7, qs1.count())
|
||||
# TODO: This query should work in MySQL, but it appears the
|
||||
# `MBRDisjoint` function doesn't work properly (I went down
|
||||
# to the SQL level for debugging and still got bogus answers).
|
||||
#qs2 = State.objects.filter(poly__disjoint=ptown.point)
|
||||
#self.assertEqual(1, qs2.count())
|
||||
#self.assertEqual('Kansas', qs2[0].name)
|
||||
|
||||
def test05_equals(self):
|
||||
"Testing the 'same_as' and 'equals' lookup types."
|
||||
pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
|
||||
c1 = City.objects.get(point=pnt)
|
||||
c2 = City.objects.get(point__same_as=pnt)
|
||||
c3 = City.objects.get(point__equals=pnt)
|
||||
for c in [c1, c2, c3]: self.assertEqual('Houston', c.name)
|
||||
|
||||
def test06_geometryfield(self):
|
||||
"Testing GeometryField."
|
||||
f1 = Feature(name='Point', geom=Point(1, 1))
|
||||
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
|
||||
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
|
||||
f4 = Feature(name='GeometryCollection',
|
||||
geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
|
||||
Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))))
|
||||
f1.save()
|
||||
f2.save()
|
||||
f3.save()
|
||||
f4.save()
|
||||
|
||||
f_1 = Feature.objects.get(name='Point')
|
||||
self.assertEqual(True, isinstance(f_1.geom, Point))
|
||||
self.assertEqual((1.0, 1.0), f_1.geom.tuple)
|
||||
f_2 = Feature.objects.get(name='LineString')
|
||||
self.assertEqual(True, isinstance(f_2.geom, LineString))
|
||||
self.assertEqual(((0.0, 0.0), (1.0, 1.0), (5.0, 5.0)), f_2.geom.tuple)
|
||||
|
||||
f_3 = Feature.objects.get(name='Polygon')
|
||||
self.assertEqual(True, isinstance(f_3.geom, Polygon))
|
||||
f_4 = Feature.objects.get(name='GeometryCollection')
|
||||
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
|
||||
self.assertEqual(f_3.geom, f_4.geom[2])
|
||||
|
||||
def test07_mysql_limitations(self):
|
||||
"Testing that union(), kml(), gml() raise exceptions."
|
||||
self.assertRaises(ImproperlyConfigured, City.objects.union, Point(5, 23), field_name='point')
|
||||
self.assertRaises(ImproperlyConfigured, State.objects.all().kml, field_name='poly')
|
||||
self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, field_name='mpoly')
|
||||
|
||||
from test_feeds import GeoFeedTest
|
||||
from test_regress import GeoRegressionTests
|
||||
from test_sitemaps import GeoSitemapTest
|
||||
|
||||
def suite():
|
||||
s = unittest.TestSuite()
|
||||
s.addTest(unittest.makeSuite(GeoModelTest))
|
||||
s.addTest(unittest.makeSuite(GeoFeedTest))
|
||||
s.addTest(unittest.makeSuite(GeoSitemapTest))
|
||||
s.addTest(unittest.makeSuite(GeoRegressionTests))
|
||||
return s
|
|
@ -1,15 +1,19 @@
|
|||
import os, unittest
|
||||
import os
|
||||
import unittest
|
||||
from decimal import Decimal
|
||||
from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey
|
||||
from django.contrib.gis.gdal import DataSource
|
||||
|
||||
from django.utils.copycompat import copy
|
||||
|
||||
shp_path = os.path.dirname(__file__)
|
||||
city_shp = os.path.join(shp_path, '../data/cities/cities.shp')
|
||||
co_shp = os.path.join(shp_path, '../data/counties/counties.shp')
|
||||
inter_shp = os.path.join(shp_path, '../data/interstates/interstates.shp')
|
||||
from django.contrib.gis.gdal import DataSource
|
||||
from django.contrib.gis.tests.utils import mysql
|
||||
from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey
|
||||
|
||||
from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping
|
||||
|
||||
shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data'))
|
||||
city_shp = os.path.join(shp_path, 'cities', 'cities.shp')
|
||||
co_shp = os.path.join(shp_path, 'counties', 'counties.shp')
|
||||
inter_shp = os.path.join(shp_path, 'interstates', 'interstates.shp')
|
||||
|
||||
# Dictionaries to hold what's expected in the county shapefile.
|
||||
NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo']
|
||||
|
@ -84,7 +88,7 @@ class LayerMapTest(unittest.TestCase):
|
|||
lm.save(silent=True, strict=True)
|
||||
except InvalidDecimal:
|
||||
# No transactions for geoms on MySQL; delete added features.
|
||||
if SpatialBackend.mysql: Interstate.objects.all().delete()
|
||||
if mysql: Interstate.objects.all().delete()
|
||||
else:
|
||||
self.fail('Should have failed on strict import with invalid decimal values.')
|
||||
|
||||
|
@ -149,7 +153,7 @@ class LayerMapTest(unittest.TestCase):
|
|||
self.assertRaises(e, LayerMapping, County, co_shp, co_mapping, transform=False, unique=arg)
|
||||
|
||||
# No source reference system defined in the shapefile, should raise an error.
|
||||
if not SpatialBackend.mysql:
|
||||
if not mysql:
|
||||
self.assertRaises(LayerMapError, LayerMapping, County, co_shp, co_mapping)
|
||||
|
||||
# Passing in invalid ForeignKey mapping parameters -- must be a dictionary
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
from tests import *
|
|
@ -1,8 +1,8 @@
|
|||
import os, unittest
|
||||
from django.contrib.gis.geos import *
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
from django.contrib.gis.db.models import Collect, Count, Extent, F, Union
|
||||
from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite
|
||||
from django.contrib.gis.geometry.backend import Geometry
|
||||
from django.contrib.gis.tests.utils import mysql, oracle, postgis, spatialite, no_mysql, no_oracle, no_spatialite
|
||||
from django.conf import settings
|
||||
from models import City, Location, DirectoryEntry, Parcel, Book, Author
|
||||
|
||||
|
@ -95,7 +95,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||
# geometries than PostGIS. The second union aggregate is for a union
|
||||
# query that includes limiting information in the WHERE clause (in other
|
||||
# words a `.filter()` precedes the call to `.unionagg()`).
|
||||
if SpatialBackend.oracle:
|
||||
if oracle:
|
||||
ref_u1 = MultiPoint(p3, p1, p2, srid=4326)
|
||||
ref_u2 = MultiPoint(p3, p2, srid=4326)
|
||||
else:
|
||||
|
@ -144,7 +144,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||
self.assertEqual(1, len(qs))
|
||||
self.assertEqual('P2', qs[0].name)
|
||||
|
||||
if not SpatialBackend.mysql:
|
||||
if not mysql:
|
||||
# This time center2 is in a different coordinate system and needs
|
||||
# to be wrapped in transformation SQL.
|
||||
qs = Parcel.objects.filter(center2__within=F('border1'))
|
||||
|
@ -157,7 +157,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||
self.assertEqual(1, len(qs))
|
||||
self.assertEqual('P1', qs[0].name)
|
||||
|
||||
if not SpatialBackend.mysql:
|
||||
if not mysql:
|
||||
# This time the city column should be wrapped in transformation SQL.
|
||||
qs = Parcel.objects.filter(border2__contains=F('city__location__point'))
|
||||
self.assertEqual(1, len(qs))
|
||||
|
@ -175,8 +175,8 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||
for m, d, t in zip(gqs, gvqs, gvlqs):
|
||||
# The values should be Geometry objects and not raw strings returned
|
||||
# by the spatial database.
|
||||
self.failUnless(isinstance(d['point'], SpatialBackend.Geometry))
|
||||
self.failUnless(isinstance(t[1], SpatialBackend.Geometry))
|
||||
self.failUnless(isinstance(d['point'], Geometry))
|
||||
self.failUnless(isinstance(t[1], Geometry))
|
||||
self.assertEqual(m.point, d['point'])
|
||||
self.assertEqual(m.point, t[1])
|
||||
|
||||
|
@ -238,7 +238,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||
# as Dallas.
|
||||
dallas = City.objects.get(name='Dallas')
|
||||
ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location)
|
||||
|
||||
|
||||
# Count annotation should be 2 for the Dallas location now.
|
||||
loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id)
|
||||
self.assertEqual(2, loc.num_cities)
|
||||
|
@ -279,11 +279,11 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||
def test14_collect(self):
|
||||
"Testing the `collect` GeoQuerySet method and `Collect` aggregate."
|
||||
# Reference query:
|
||||
# SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN
|
||||
# "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id")
|
||||
# SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN
|
||||
# "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id")
|
||||
# WHERE "relatedapp_city"."state" = 'TX';
|
||||
ref_geom = fromstr('MULTIPOINT(-97.516111 33.058333,-96.801611 32.782057,-95.363151 29.763374,-96.801611 32.782057)')
|
||||
|
||||
|
||||
c1 = City.objects.filter(state='TX').collect(field_name='location__point')
|
||||
c2 = City.objects.filter(state='TX').aggregate(Collect('location__point'))['location__point__collect']
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
from tests import *
|
|
@ -1,8 +1,7 @@
|
|||
import unittest
|
||||
from django.contrib.gis.db.backend import SpatialBackend
|
||||
|
||||
from django.db import connection
|
||||
from django.contrib.gis.tests.utils import mysql, no_mysql, oracle, postgis, spatialite
|
||||
if not mysql:
|
||||
from django.contrib.gis.models import SpatialRefSys
|
||||
|
||||
test_srs = ({'srid' : 4326,
|
||||
'auth_name' : ('EPSG', True),
|
||||
|
@ -28,9 +27,12 @@ test_srs = ({'srid' : 4326,
|
|||
},
|
||||
)
|
||||
|
||||
if SpatialBackend.postgis:
|
||||
major, minor1, minor2 = SpatialBackend.version
|
||||
POSTGIS_14 = major >=1 and minor1 >= 4
|
||||
if oracle:
|
||||
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
|
||||
elif postgis:
|
||||
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
|
||||
elif spatialite:
|
||||
from django.contrib.gis.db.backends.spatialite.models import SpatialRefSys
|
||||
|
||||
class SpatialRefSysTest(unittest.TestCase):
|
||||
|
||||
|
@ -52,7 +54,7 @@ class SpatialRefSysTest(unittest.TestCase):
|
|||
|
||||
# No proj.4 and different srtext on oracle backends :(
|
||||
if postgis:
|
||||
if POSTGIS_14:
|
||||
if connection.ops.spatial_version >= (1, 4, 0):
|
||||
srtext = sd['srtext14']
|
||||
else:
|
||||
srtext = sd['srtext']
|
||||
|
@ -79,7 +81,7 @@ class SpatialRefSysTest(unittest.TestCase):
|
|||
self.assertEqual(sd['proj4'], srs.proj4)
|
||||
# No `srtext` field in the `spatial_ref_sys` table in SpatiaLite
|
||||
if not spatialite:
|
||||
if POSTGIS_14:
|
||||
if connection.ops.spatial_version >= (1, 4, 0):
|
||||
srtext = sd['srtext14']
|
||||
else:
|
||||
srtext = sd['srtext']
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from django.conf import settings
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
|
||||
# function that will pass a test.
|
||||
def pass_test(*args): return
|
||||
|
||||
def no_backend(test_func, backend):
|
||||
"Use this decorator to disable test on specified backend."
|
||||
if settings.DATABASE_ENGINE == backend:
|
||||
if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].rsplit('.')[-1] == backend:
|
||||
return pass_test
|
||||
else:
|
||||
return test_func
|
||||
|
@ -13,12 +14,13 @@ def no_backend(test_func, backend):
|
|||
# Decorators to disable entire test functions for specific
|
||||
# spatial backends.
|
||||
def no_oracle(func): return no_backend(func, 'oracle')
|
||||
def no_postgis(func): return no_backend(func, 'postgresql_psycopg2')
|
||||
def no_postgis(func): return no_backend(func, 'postgis')
|
||||
def no_mysql(func): return no_backend(func, 'mysql')
|
||||
def no_spatialite(func): return no_backend(func, 'sqlite3')
|
||||
def no_spatialite(func): return no_backend(func, 'spatialite')
|
||||
|
||||
# Shortcut booleans to omit only portions of tests.
|
||||
oracle = settings.DATABASE_ENGINE == 'oracle'
|
||||
postgis = settings.DATABASE_ENGINE == 'postgresql_psycopg2'
|
||||
mysql = settings.DATABASE_ENGINE == 'mysql'
|
||||
spatialite = settings.DATABASE_ENGINE == 'sqlite3'
|
||||
_default_db = settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'].rsplit('.')[-1]
|
||||
oracle = _default_db == 'oracle'
|
||||
postgis = _default_db == 'postgis'
|
||||
mysql = _default_db == 'mysql'
|
||||
spatialite = _default_db == 'spatialite'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue