Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merged the gis branch into trunk.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8219 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 79e68c225b926302ebb29c808dda8afa49856f5c 1 parent d0f57e7
@jbronn jbronn authored
Showing with 14,939 additions and 0 deletions.
  1. BIN  django/contrib/admin/media/img/gis/move_vertex_off.png
  2. BIN  django/contrib/admin/media/img/gis/move_vertex_on.png
  3. 0  django/contrib/gis/__init__.py
  4. +12 −0 django/contrib/gis/admin/__init__.py
  5. +128 −0 django/contrib/gis/admin/options.py
  6. +92 −0 django/contrib/gis/admin/widgets.py
  7. 0  django/contrib/gis/db/__init__.py
  8. +18 −0 django/contrib/gis/db/backend/__init__.py
  9. +14 −0 django/contrib/gis/db/backend/adaptor.py
  10. +29 −0 django/contrib/gis/db/backend/base.py
  11. +13 −0 django/contrib/gis/db/backend/mysql/__init__.py
  12. +5 −0 django/contrib/gis/db/backend/mysql/creation.py
  13. +53 −0 django/contrib/gis/db/backend/mysql/field.py
  14. +59 −0 django/contrib/gis/db/backend/mysql/query.py
  15. +31 −0 django/contrib/gis/db/backend/oracle/__init__.py
  16. +5 −0 django/contrib/gis/db/backend/oracle/adaptor.py
  17. +8 −0 django/contrib/gis/db/backend/oracle/creation.py
  18. +103 −0 django/contrib/gis/db/backend/oracle/field.py
  19. +49 −0 django/contrib/gis/db/backend/oracle/models.py
  20. +154 −0 django/contrib/gis/db/backend/oracle/query.py
  21. +42 −0 django/contrib/gis/db/backend/postgis/__init__.py
  22. +33 −0 django/contrib/gis/db/backend/postgis/adaptor.py
  23. +224 −0 django/contrib/gis/db/backend/postgis/creation.py
  24. +95 −0 django/contrib/gis/db/backend/postgis/field.py
  25. +54 −0 django/contrib/gis/db/backend/postgis/management.py
  26. +58 −0 django/contrib/gis/db/backend/postgis/models.py
  27. +287 −0 django/contrib/gis/db/backend/postgis/query.py
  28. +52 −0 django/contrib/gis/db/backend/util.py
  29. +17 −0 django/contrib/gis/db/models/__init__.py
  30. +214 −0 django/contrib/gis/db/models/fields/__init__.py
  31. +82 −0 django/contrib/gis/db/models/manager.py
  32. +11 −0 django/contrib/gis/db/models/mixin.py
  33. +62 −0 django/contrib/gis/db/models/proxy.py
  34. +617 −0 django/contrib/gis/db/models/query.py
  35. +2 −0  django/contrib/gis/db/models/sql/__init__.py
  36. +327 −0 django/contrib/gis/db/models/sql/query.py
  37. +64 −0 django/contrib/gis/db/models/sql/where.py
  38. +1 −0  django/contrib/gis/forms/__init__.py
  39. +37 −0 django/contrib/gis/forms/fields.py
  40. +28 −0 django/contrib/gis/gdal/LICENSE
  41. +51 −0 django/contrib/gis/gdal/__init__.py
  42. +138 −0 django/contrib/gis/gdal/datasource.py
  43. +66 −0 django/contrib/gis/gdal/driver.py
  44. +134 −0 django/contrib/gis/gdal/envelope.py
  45. +41 −0 django/contrib/gis/gdal/error.py
  46. +115 −0 django/contrib/gis/gdal/feature.py
  47. +179 −0 django/contrib/gis/gdal/field.py
  48. +643 −0 django/contrib/gis/gdal/geometries.py
  49. +73 −0 django/contrib/gis/gdal/geomtype.py
  50. +187 −0 django/contrib/gis/gdal/layer.py
  51. +83 −0 django/contrib/gis/gdal/libgdal.py
  52. 0  django/contrib/gis/gdal/prototypes/__init__.py
  53. +68 −0 django/contrib/gis/gdal/prototypes/ds.py
  54. +125 −0 django/contrib/gis/gdal/prototypes/errcheck.py
  55. +116 −0 django/contrib/gis/gdal/prototypes/generation.py
  56. +109 −0 django/contrib/gis/gdal/prototypes/geom.py
  57. +71 −0 django/contrib/gis/gdal/prototypes/srs.py
  58. +360 −0 django/contrib/gis/gdal/srs.py
  59. +27 −0 django/contrib/gis/geos/LICENSE
  60. +69 −0 django/contrib/gis/geos/__init__.py
  61. +608 −0 django/contrib/gis/geos/base.py
  62. +105 −0 django/contrib/gis/geos/collections.py
  63. +164 −0 django/contrib/gis/geos/coordseq.py
  64. +20 −0 django/contrib/gis/geos/error.py
  65. +391 −0 django/contrib/gis/geos/geometries.py
  66. +126 −0 django/contrib/gis/geos/libgeos.py
  67. +33 −0 django/contrib/gis/geos/prototypes/__init__.py
  68. +82 −0 django/contrib/gis/geos/prototypes/coordseq.py
  69. +76 −0 django/contrib/gis/geos/prototypes/errcheck.py
  70. +111 −0 django/contrib/gis/geos/prototypes/geom.py
  71. +27 −0 django/contrib/gis/geos/prototypes/misc.py
  72. +43 −0 django/contrib/gis/geos/prototypes/predicates.py
  73. +35 −0 django/contrib/gis/geos/prototypes/topology.py
  74. 0  django/contrib/gis/management/__init__.py
  75. +15 −0 django/contrib/gis/management/base.py
  76. 0  django/contrib/gis/management/commands/__init__.py
  77. +190 −0 django/contrib/gis/management/commands/inspectdb.py
  78. +119 −0 django/contrib/gis/management/commands/ogrinspect.py
  79. 0  django/contrib/gis/maps/__init__.py
  80. +61 −0 django/contrib/gis/maps/google/__init__.py
  81. +138 −0 django/contrib/gis/maps/google/gmap.py
  82. +220 −0 django/contrib/gis/maps/google/overlays.py
  83. +164 −0 django/contrib/gis/maps/google/zoom.py
  84. 0  django/contrib/gis/maps/openlayers/__init__.py
  85. +329 −0 django/contrib/gis/measure.py
  86. +284 −0 django/contrib/gis/models.py
  87. +29 −0 django/contrib/gis/oldforms/__init__.py
  88. +12 −0 django/contrib/gis/shortcuts.py
  89. +55 −0 django/contrib/gis/sitemaps.py
  90. +37 −0 django/contrib/gis/templates/gis/admin/openlayers.html
  91. +157 −0 django/contrib/gis/templates/gis/admin/openlayers.js
  92. +2 −0  django/contrib/gis/templates/gis/admin/osm.html
  93. +2 −0  django/contrib/gis/templates/gis/admin/osm.js
  94. +34 −0 django/contrib/gis/templates/gis/google/js/google-map.js
  95. +6 −0 django/contrib/gis/templates/gis/kml/base.kml
  96. +8 −0 django/contrib/gis/templates/gis/kml/placemarks.kml
  97. +148 −0 django/contrib/gis/tests/__init__.py
  98. BIN  django/contrib/gis/tests/data/test_point/test_point.dbf
  99. +1 −0  django/contrib/gis/tests/data/test_point/test_point.prj
  100. BIN  django/contrib/gis/tests/data/test_point/test_point.shp
  101. BIN  django/contrib/gis/tests/data/test_point/test_point.shx
  102. BIN  django/contrib/gis/tests/data/test_poly/test_poly.dbf
  103. +1 −0  django/contrib/gis/tests/data/test_poly/test_poly.prj
  104. BIN  django/contrib/gis/tests/data/test_poly/test_poly.shp
  105. BIN  django/contrib/gis/tests/data/test_poly/test_poly.shx
  106. +4 −0 django/contrib/gis/tests/data/test_vrt/test_vrt.csv
  107. +7 −0 django/contrib/gis/tests/data/test_vrt/test_vrt.vrt
  108. 0  django/contrib/gis/tests/distapp/__init__.py
  109. +33 −0 django/contrib/gis/tests/distapp/data.py
  110. +42 −0 django/contrib/gis/tests/distapp/models.py
  111. +296 −0 django/contrib/gis/tests/distapp/tests.py
  112. 0  django/contrib/gis/tests/geoapp/__init__.py
  113. +33 −0 django/contrib/gis/tests/geoapp/models.py
  114. +8 −0 django/contrib/gis/tests/geoapp/sql/city.mysql.sql
  115. +8 −0 django/contrib/gis/tests/geoapp/sql/city.oracle.sql
  116. +8 −0 django/contrib/gis/tests/geoapp/sql/city.postgresql_psycopg2.sql
  117. +1 −0  django/contrib/gis/tests/geoapp/sql/co.wkt
  118. +4 −0 django/contrib/gis/tests/geoapp/sql/country.mysql.sql
  119. +4 −0 django/contrib/gis/tests/geoapp/sql/country.postgresql_psycopg2.sql
  120. +1 −0  django/contrib/gis/tests/geoapp/sql/ks.wkt
  121. +1 −0  django/contrib/gis/tests/geoapp/sql/nz.wkt
  122. +5 −0 django/contrib/gis/tests/geoapp/sql/state.mysql.sql
  123. +5 −0 django/contrib/gis/tests/geoapp/sql/state.postgresql_psycopg2.sql
  124. +1 −0  django/contrib/gis/tests/geoapp/sql/tx.wkt
  125. +564 −0 django/contrib/gis/tests/geoapp/tests.py
  126. +179 −0 django/contrib/gis/tests/geoapp/tests_mysql.py
  127. +157 −0 django/contrib/gis/tests/geometries.py
  128. 0  django/contrib/gis/tests/layermap/__init__.py
  129. BIN  django/contrib/gis/tests/layermap/cities/cities.dbf
  130. +1 −0  django/contrib/gis/tests/layermap/cities/cities.prj
  131. BIN  django/contrib/gis/tests/layermap/cities/cities.shp
  132. BIN  django/contrib/gis/tests/layermap/cities/cities.shx
  133. BIN  django/contrib/gis/tests/layermap/counties/counties.dbf
  134. BIN  django/contrib/gis/tests/layermap/counties/counties.shp
  135. BIN  django/contrib/gis/tests/layermap/counties/counties.shx
  136. BIN  django/contrib/gis/tests/layermap/interstates/interstates.dbf
  137. +1 −0  django/contrib/gis/tests/layermap/interstates/interstates.prj
  138. BIN  django/contrib/gis/tests/layermap/interstates/interstates.shp
  139. BIN  django/contrib/gis/tests/layermap/interstates/interstates.shx
  140. +52 −0 django/contrib/gis/tests/layermap/models.py
  141. +249 −0 django/contrib/gis/tests/layermap/tests.py
  142. 0  django/contrib/gis/tests/relatedapp/__init__.py
  143. +12 −0 django/contrib/gis/tests/relatedapp/models.py
  144. +98 −0 django/contrib/gis/tests/relatedapp/tests.py
  145. +1 −0  django/contrib/gis/tests/relatedapp/tests_mysql.py
  146. +28 −0 django/contrib/gis/tests/test_gdal.py
  147. +40 −0 django/contrib/gis/tests/test_gdal_driver.py
  148. +181 −0 django/contrib/gis/tests/test_gdal_ds.py
  149. +45 −0 django/contrib/gis/tests/test_gdal_envelope.py
  150. +403 −0 django/contrib/gis/tests/test_gdal_geom.py
  151. +169 −0 django/contrib/gis/tests/test_gdal_srs.py
  152. +104 −0 django/contrib/gis/tests/test_geoip.py
  153. +775 −0 django/contrib/gis/tests/test_geos.py
  154. +333 −0 django/contrib/gis/tests/test_measure.py
  155. +90 −0 django/contrib/gis/tests/test_spatialrefsys.py
  156. +22 −0 django/contrib/gis/tests/utils.py
  157. +25 −0 django/contrib/gis/utils/__init__.py
  158. +344 −0 django/contrib/gis/utils/geoip.py
  159. +677 −0 django/contrib/gis/utils/layermapping.py
  160. +53 −0 django/contrib/gis/utils/ogrinfo.py
  161. +225 −0 django/contrib/gis/utils/ogrinspect.py
  162. +27 −0 django/contrib/gis/utils/srs.py
  163. +55 −0 django/contrib/gis/utils/wkt.py
View
BIN  django/contrib/admin/media/img/gis/move_vertex_off.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  django/contrib/admin/media/img/gis/move_vertex_on.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
0  django/contrib/gis/__init__.py
No changes.
View
12 django/contrib/gis/admin/__init__.py
@@ -0,0 +1,12 @@
+# Getting the normal admin routines, classes, and `site` instance.
+from django.contrib.admin import autodiscover, site, StackedInline, TabularInline, HORIZONTAL, VERTICAL
+
+# Geographic admin options classes and widgets.
+from django.contrib.gis.admin.options import GeoModelAdmin
+from django.contrib.gis.admin.widgets import OpenLayersWidget
+
+try:
+ from django.contrib.gis.admin.options import OSMGeoAdmin
+ HAS_OSM = True
+except ImportError:
+ HAS_OSM = False
View
128 django/contrib/gis/admin/options.py
@@ -0,0 +1,128 @@
+from django.conf import settings
+from django.contrib.admin import ModelAdmin
+from django.contrib.gis.admin.widgets import OpenLayersWidget
+from django.contrib.gis.gdal import OGRGeomType
+from django.contrib.gis.db import models
+
+class GeoModelAdmin(ModelAdmin):
+ """
+ The administration options class for Geographic models. Map settings
+ may be overloaded from their defaults to create custom maps.
+ """
+ # The default map settings that may be overloaded -- still subject
+ # to API changes.
+ default_lon = 0
+ default_lat = 0
+ default_zoom = 4
+ display_wkt = False
+ display_srid = False
+ extra_js = []
+ num_zoom = 18
+ max_zoom = False
+ min_zoom = False
+ units = False
+ max_resolution = False
+ max_extent = False
+ modifiable = True
+ mouse_position = True
+ scale_text = True
+ layerswitcher = True
+ scrollable = True
+ admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
+ map_width = 600
+ map_height = 400
+ map_srid = 4326
+ map_template = 'gis/admin/openlayers.html'
+ openlayers_url = 'http://openlayers.org/api/2.6/OpenLayers.js'
+ wms_url = 'http://labs.metacarta.com/wms/vmap0'
+ wms_layer = 'basic'
+ wms_name = 'OpenLayers WMS'
+ debug = False
+ widget = OpenLayersWidget
+
+ def _media(self):
+ "Injects OpenLayers JavaScript into the admin."
+ media = super(GeoModelAdmin, self)._media()
+ media.add_js([self.openlayers_url])
+ media.add_js(self.extra_js)
+ return media
+ media = property(_media)
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ """
+ Overloaded from ModelAdmin so that an OpenLayersWidget is used
+ for viewing/editing GeometryFields.
+ """
+ if isinstance(db_field, models.GeometryField):
+ # Setting the widget with the newly defined widget.
+ kwargs['widget'] = self.get_map_widget(db_field)
+ return db_field.formfield(**kwargs)
+ else:
+ return super(GeoModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+
+ def get_map_widget(self, db_field):
+ """
+ Returns a subclass of the OpenLayersWidget (or whatever was specified
+ in the `widget` attribute) using the settings from the attributes set
+ in this class.
+ """
+ is_collection = db_field._geom in ('MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION')
+ if is_collection:
+ if db_field._geom == 'GEOMETRYCOLLECTION': collection_type = 'Any'
+ else: collection_type = OGRGeomType(db_field._geom.replace('MULTI', ''))
+ else:
+ collection_type = 'None'
+
+ class OLMap(self.widget):
+ template = self.map_template
+ geom_type = db_field._geom
+ params = {'admin_media_prefix' : self.admin_media_prefix,
+ 'default_lon' : self.default_lon,
+ 'default_lat' : self.default_lat,
+ 'default_zoom' : self.default_zoom,
+ 'display_wkt' : self.debug or self.display_wkt,
+ 'geom_type' : OGRGeomType(db_field._geom),
+ 'field_name' : db_field.name,
+ 'is_collection' : is_collection,
+ 'scrollable' : self.scrollable,
+ 'layerswitcher' : self.layerswitcher,
+ 'collection_type' : collection_type,
+ 'is_linestring' : db_field._geom in ('LINESTRING', 'MULTILINESTRING'),
+ 'is_polygon' : db_field._geom in ('POLYGON', 'MULTIPOLYGON'),
+ 'is_point' : db_field._geom in ('POINT', 'MULTIPOINT'),
+ 'num_zoom' : self.num_zoom,
+ 'max_zoom' : self.max_zoom,
+ 'min_zoom' : self.min_zoom,
+ 'units' : self.units, #likely shoud get from object
+ 'max_resolution' : self.max_resolution,
+ 'max_extent' : self.max_extent,
+ 'modifiable' : self.modifiable,
+ 'mouse_position' : self.mouse_position,
+ 'scale_text' : self.scale_text,
+ 'map_width' : self.map_width,
+ 'map_height' : self.map_height,
+ 'srid' : self.map_srid,
+ 'display_srid' : self.display_srid,
+ 'wms_url' : self.wms_url,
+ 'wms_layer' : self.wms_layer,
+ 'wms_name' : self.wms_name,
+ 'debug' : self.debug,
+ }
+ return OLMap
+
+# Using the Beta OSM in the admin requires the following:
+# (1) The Google Maps Mercator projection needs to be added
+# to your `spatial_ref_sys` table. You'll need at least GDAL 1.5:
+# >>> from django.contrib.gis.gdal import SpatialReference
+# >>> from django.contrib.gis.utils import add_postgis_srs
+# >>> add_postgis_srs(SpatialReference(900913)) # Adding the Google Projection
+from django.contrib.gis import gdal
+if gdal.HAS_GDAL:
+ class OSMGeoAdmin(GeoModelAdmin):
+ map_template = 'gis/admin/osm.html'
+ extra_js = ['http://openstreetmap.org/openlayers/OpenStreetMap.js']
+ num_zoom = 20
+ map_srid = 900913
+ max_extent = '-20037508,-20037508,20037508,20037508'
+ max_resolution = 156543.0339
+ units = 'm'
View
92 django/contrib/gis/admin/widgets.py
@@ -0,0 +1,92 @@
+from django.contrib.gis.gdal import OGRException
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+from django.forms.widgets import Textarea
+from django.template.loader import render_to_string
+
+class OpenLayersWidget(Textarea):
+ """
+ Renders an OpenLayers map using the WKT of the geometry.
+ """
+ def render(self, name, value, attrs=None):
+ # Update the template parameters with any attributes passed in.
+ if attrs: self.params.update(attrs)
+
+ # Defaulting the WKT value to a blank string -- this
+ # will be tested in the JavaScript and the appropriate
+ # interfaace will be constructed.
+ self.params['wkt'] = ''
+
+ # If a string reaches here (via a validation error on another
+ # field) then just reconstruct the Geometry.
+ if isinstance(value, basestring):
+ try:
+ value = GEOSGeometry(value)
+ except (GEOSException, ValueError):
+ value = None
+
+ if value and value.geom_type.upper() != self.geom_type:
+ value = None
+
+ # Constructing the dictionary of the map options.
+ self.params['map_options'] = self.map_options()
+
+ # Constructing the JavaScript module name using the ID of
+ # the GeometryField (passed in via the `attrs` keyword).
+ self.params['module'] = 'geodjango_%s' % self.params['field_name']
+
+ if value:
+ # Transforming the geometry to the projection used on the
+ # OpenLayers map.
+ srid = self.params['srid']
+ if value.srid != srid:
+ try:
+ value.transform(srid)
+ wkt = value.wkt
+ except OGRException:
+ wkt = ''
+ else:
+ wkt = value.wkt
+
+ # Setting the parameter WKT with that of the transformed
+ # geometry.
+ self.params['wkt'] = wkt
+
+ return render_to_string(self.template, self.params)
+
+ def map_options(self):
+ "Builds the map options hash for the OpenLayers template."
+
+ # JavaScript construction utilities for the Bounds and Projection.
+ def ol_bounds(extent):
+ return 'new OpenLayers.Bounds(%s)' % str(extent)
+ def ol_projection(srid):
+ return 'new OpenLayers.Projection("EPSG:%s")' % srid
+
+ # An array of the parameter name, the name of their OpenLayers
+ # counterpart, and the type of variable they are.
+ map_types = [('srid', 'projection', 'srid'),
+ ('display_srid', 'displayProjection', 'srid'),
+ ('units', 'units', str),
+ ('max_resolution', 'maxResolution', float),
+ ('max_extent', 'maxExtent', 'bounds'),
+ ('num_zoom', 'numZoomLevels', int),
+ ('max_zoom', 'maxZoomLevels', int),
+ ('min_zoom', 'minZoomLevel', int),
+ ]
+
+ # Building the map options hash.
+ map_options = {}
+ for param_name, js_name, option_type in map_types:
+ if self.params.get(param_name, False):
+ if option_type == 'srid':
+ value = ol_projection(self.params[param_name])
+ elif option_type == 'bounds':
+ value = ol_bounds(self.params[param_name])
+ elif option_type in (float, int):
+ value = self.params[param_name]
+ elif option_type in (str,):
+ value = '"%s"' % self.params[param_name]
+ else:
+ raise TypeError
+ map_options[js_name] = value
+ return map_options
View
0  django/contrib/gis/db/__init__.py
No changes.
View
18 django/contrib/gis/db/backend/__init__.py
@@ -0,0 +1,18 @@
+"""
+ 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_spatial_db, get_geo_where_clause, SpatialBackend
+elif settings.DATABASE_ENGINE == 'oracle':
+ from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
+elif settings.DATABASE_ENGINE == 'mysql':
+ from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
+else:
+ raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
View
14 django/contrib/gis/db/backend/adaptor.py
@@ -0,0 +1,14 @@
+class WKTAdaptor(object):
+ """
+ This provides an adaptor for Geometries sent to the
+ MySQL and Oracle database backends.
+ """
+ def __init__(self, geom):
+ self.wkt = geom.wkt
+ self.srid = geom.srid
+
+ def __eq__(self, other):
+ return self.wkt == other.wkt and self.srid == other.srid
+
+ def __str__(self):
+ return self.wkt
View
29 django/contrib/gis/db/backend/base.py
@@ -0,0 +1,29 @@
+"""
+ 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
+
+
+
+
View
13 django/contrib/gis/db/backend/mysql/__init__.py
@@ -0,0 +1,13 @@
+__all__ = ['create_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_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)
View
5 django/contrib/gis/db/backend/mysql/creation.py
@@ -0,0 +1,5 @@
+from django.test.utils import create_test_db
+
+def create_spatial_db(test=True, verbosity=1, autoclobber=False):
+ if not test: raise NotImplementedError('This uses `create_test_db` from test/utils.py')
+ create_test_db(verbosity, autoclobber)
View
53 django/contrib/gis/db/backend/mysql/field.py
@@ -0,0 +1,53 @@
+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._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
+
+ 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
View
59 django/contrib/gis/db/backend/mysql/query.py
@@ -0,0 +1,59 @@
+"""
+ 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))
View
31 django/contrib/gis/db/backend/oracle/__init__.py
@@ -0,0 +1,31 @@
+__all__ = ['create_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_spatial_db
+from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
+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,
+ 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,
+ )
View
5 django/contrib/gis/db/backend/oracle/adaptor.py
@@ -0,0 +1,5 @@
+from cx_Oracle import CLOB
+from django.contrib.gis.db.backend.adaptor import WKTAdaptor
+
+class OracleSpatialAdaptor(WKTAdaptor):
+ input_size = CLOB
View
8 django/contrib/gis/db/backend/oracle/creation.py
@@ -0,0 +1,8 @@
+from django.db.backends.oracle.creation import create_test_db
+
+def create_spatial_db(test=True, verbosity=1, autoclobber=False):
+ "A wrapper over the Oracle `create_test_db` routine."
+ if not test: raise NotImplementedError('This uses `create_test_db` from db/backends/oracle/creation.py')
+ from django.conf import settings
+ from django.db import connection
+ create_test_db(settings, connection, verbosity, autoclobber)
View
103 django/contrib/gis/db/backend/oracle/field.py
@@ -0,0 +1,103 @@
+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._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 '%s'
+ 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
View
49 django/contrib/gis/db/backend/oracle/models.py
@@ -0,0 +1,49 @@
+"""
+ The GeometryColumns and SpatialRefSys models for the Oracle spatial
+ backend.
+
+ It should be noted that Oracle Spatial does not have database tables
+ named according to the OGC standard, so the closest analogs are used.
+ 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.models import SpatialRefSysMixin
+
+class GeometryColumns(models.Model):
+ "Maps to the Oracle USER_SDO_GEOM_METADATA table."
+ table_name = models.CharField(max_length=32)
+ column_name = models.CharField(max_length=1024)
+ srid = models.IntegerField(primary_key=True)
+ # TODO: Add support for `diminfo` column (type MDSYS.SDO_DIM_ARRAY).
+ class Meta:
+ db_table = 'USER_SDO_GEOM_METADATA'
+
+ @classmethod
+ def table_name_col(cls):
+ return 'table_name'
+
+ def __unicode__(self):
+ return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid)
+
+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()
+
+ class Meta:
+ # TODO: Figure out way to have this be MDSYS.CS_SRS without
+ # having django's quoting mess up the SQL.
+ db_table = 'CS_SRS'
+
+ @property
+ def wkt(self):
+ return self.wktext
+
+ @classmethod
+ def wkt_col(cls):
+ return 'wktext'
View
154 django/contrib/gis/db/backend/oracle/query.py
@@ -0,0 +1,154 @@
+"""
+ 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))
View
42 django/contrib/gis/db/backend/postgis/__init__.py
@@ -0,0 +1,42 @@
+__all__ = ['create_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_spatial_db
+from django.contrib.gis.db.backend.postgis.field import PostGISField
+from django.contrib.gis.db.backend.postgis.query import *
+
+SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
+ area=AREA,
+ centroid=CENTROID,
+ difference=DIFFERENCE,
+ distance=DISTANCE,
+ distance_functions=DISTANCE_FUNCTIONS,
+ distance_sphere=DISTANCE_SPHERE,
+ distance_spheroid=DISTANCE_SPHEROID,
+ envelope=ENVELOPE,
+ extent=EXTENT,
+ gis_terms=POSTGIS_TERMS,
+ gml=ASGML,
+ intersection=INTERSECTION,
+ kml=ASKML,
+ length=LENGTH,
+ length_spheroid=LENGTH_SPHEROID,
+ make_line=MAKE_LINE,
+ mem_size=MEM_SIZE,
+ num_geom=NUM_GEOM,
+ num_points=NUM_POINTS,
+ perimeter=PERIMETER,
+ 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,
+ version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
+ Adaptor=PostGISAdaptor,
+ Field=PostGISField,
+ )
View
33 django/contrib/gis/db/backend/postgis/adaptor.py
@@ -0,0 +1,33 @@
+"""
+ This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
+"""
+
+from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB
+from psycopg2 import Binary
+from psycopg2.extensions import ISQLQuote
+
+class PostGISAdaptor(object):
+ def __init__(self, geom):
+ "Initializes on the geometry."
+ # Getting the WKB (in string form, to allow easy pickling of
+ # the adaptor) and the SRID from the geometry.
+ self.wkb = str(geom.wkb)
+ self.srid = geom.srid
+
+ def __conform__(self, proto):
+ # Does the given protocol conform to what Psycopg2 expects?
+ if proto == ISQLQuote:
+ return self
+ else:
+ raise Exception('Error implementing psycopg2 protocol. Is psycopg2 installed?')
+
+ def __eq__(self, other):
+ return (self.wkb == other.wkb) and (self.srid == other.srid)
+
+ def __str__(self):
+ return self.getquoted()
+
+ 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(%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1)
View
224 django/contrib/gis/db/backend/postgis/creation.py
@@ -0,0 +1,224 @@
+from django.conf import settings
+from django.core.management import call_command
+from django.db import connection
+from django.test.utils import _set_autocommit, TEST_DATABASE_PREFIX
+import os, re, sys
+
+def getstatusoutput(cmd):
+ "A simpler version of getstatusoutput that works on win32 platforms."
+ stdin, stdout, stderr = os.popen3(cmd)
+ output = stdout.read()
+ if output.endswith('\n'): output = output[:-1]
+ status = stdin.close()
+ return status, output
+
+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."
+
+ # Constructing the necessary SQL to create the database (the DATABASE_USER
+ # must possess the privileges to create a database)
+ create_sql = 'CREATE DATABASE %s' % connection.ops.quote_name(db_name)
+ if settings.DATABASE_USER:
+ create_sql += ' OWNER %s' % settings.DATABASE_USER
+
+ cursor = connection.cursor()
+ _set_autocommit(connection)
+
+ try:
+ # Trying to create the database first.
+ cursor.execute(create_sql)
+ #print create_sql
+ except Exception, e:
+ # Drop and recreate, if necessary.
+ 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.')
+foo = _create_with_cursor
+
+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.
+ """
+
+ # Getting the command-line options for the shell command
+ options = get_cmd_options(False)
+ 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_spatial_db(test=False, 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 != 'postgresql_psycopg2':
+ raise Exception('Spatial database creation only supported postgresql_psycopg2 platform.')
+
+ # Getting the spatial database name
+ if test:
+ db_name = get_spatial_db(test=True)
+ _create_with_cursor(db_name, verbosity=verbosity, autoclobber=autoclobber)
+ else:
+ db_name = get_spatial_db()
+ _create_with_shell(db_name, verbosity=verbosity, autoclobber=autoclobber)
+
+ # 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
+
+ # 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)
View
95 django/contrib/gis/db/backend/postgis/field.py
@@ -0,0 +1,95 @@
+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)) + ', ' + \
+ 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._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)
View
54 django/contrib/gis/db/backend/postgis/management.py
@@ -0,0 +1,54 @@
+"""
+ 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)
View
58 django/contrib/gis/db/backend/postgis/models.py
@@ -0,0 +1,58 @@
+"""
+ The GeometryColumns and SpatialRefSys models for the PostGIS backend.
+"""
+from django.db import models
+from django.contrib.gis.models import SpatialRefSysMixin
+
+# Checking for the presence of GDAL (needed for the SpatialReference object)
+from django.contrib.gis.gdal import HAS_GDAL
+if HAS_GDAL:
+ from django.contrib.gis.gdal import SpatialReference
+
+class GeometryColumns(models.Model):
+ """
+ The 'geometry_columns' table from the PostGIS. See the PostGIS
+ documentation at Ch. 4.2.2.
+ """
+ f_table_catalog = models.CharField(max_length=256)
+ f_table_schema = models.CharField(max_length=256)
+ f_table_name = models.CharField(max_length=256)
+ f_geometry_column = models.CharField(max_length=256)
+ coord_dimension = models.IntegerField()
+ srid = models.IntegerField(primary_key=True)
+ type = models.CharField(max_length=30)
+
+ class Meta:
+ db_table = 'geometry_columns'
+
+ @classmethod
+ def table_name_col(cls):
+ "Class method for returning the table name column for this model."
+ return 'f_table_name'
+
+ def __unicode__(self):
+ return "%s.%s - %dD %s field (SRID: %d)" % \
+ (self.f_table_name, self.f_geometry_column,
+ self.coord_dimension, self.type, self.srid)
+
+class SpatialRefSys(models.Model, SpatialRefSysMixin):
+ """
+ The 'spatial_ref_sys' table from PostGIS. See the PostGIS
+ documentaiton at Ch. 4.2.1.
+ """
+ srid = models.IntegerField(primary_key=True)
+ auth_name = models.CharField(max_length=256)
+ auth_srid = models.IntegerField()
+ srtext = models.CharField(max_length=2048)
+ proj4text = models.CharField(max_length=2048)
+
+ class Meta:
+ db_table = 'spatial_ref_sys'
+
+ @property
+ def wkt(self):
+ return self.srtext
+
+ @classmethod
+ def wkt_col(cls):
+ return 'srtext'
View
287 django/contrib/gis/db/backend/postgis/query.py
@@ -0,0 +1,287 @@
+"""
+ 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.contrib.gis.measure import Distance
+from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple
+from django.contrib.gis.db.backend.util import SpatialOperation, SpatialFunction
+qn = connection.ops.quote_name
+
+# Getting the PostGIS version information
+POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_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')
+ ASKML = get_func('AsKML')
+ ASGML = get_func('AsGML')
+ ASSVG = get_func('AsSVG')
+ CENTROID = get_func('Centroid')
+ 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')
+ GEOM_FROM_TEXT = get_func('GeomFromText')
+ GEOM_FROM_WKB = get_func('GeomFromWKB')
+ INTERSECTION = get_func('Intersection')
+ LENGTH = get_func('Length')
+ 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')
+ POINT_ON_SURFACE = get_func('PointOnSurface')
+ SCALE = get_func('Scale')
+ SYM_DIFFERENCE = get_func('SymDifference')
+ TRANSFORM = get_func('Transform')
+ TRANSLATE = get_func('Translate')
+
+ # Special cases for union and KML methods.
+ if MINOR_VERSION1 < 3:
+ UNIONAGG = 'GeomUnion'
+ UNION = 'Union'
+ else:
+ UNIONAGG = 'ST_Union'
+ UNION = 'ST_Union'
+
+ if MINOR_VERSION1 == 1:
+ ASKML = 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))
View
52 django/contrib/gis/db/backend/util.py
@@ -0,0 +1,52 @@
+from types import UnicodeType
+
+def gqn(val):
+ """
+ The geographic quote name function; used for quoting tables and
+ geometries (they use single rather than the double quotes of the
+ backend quotename function).
+ """
+ if isinstance(val, basestring):
+ if isinstance(val, UnicodeType): val = val.encode('ascii')
+ return "'%s'" % val
+ else:
+ return str(val)
+
+class SpatialOperation(object):
+ """
+ Base class for generating spatial SQL.
+ """
+ def __init__(self, function='', operator='', result='', beg_subst='', end_subst=''):
+ 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
+
+ @property
+ def sql_subst(self):
+ return ''.join([self.beg_subst, self.end_subst])
+
+ def as_sql(self, geo_col):
+ return self.sql_subst % self.params(geo_col)
+
+ def params(self, geo_col):
+ return (geo_col, self.operator)
+
+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)
+
+ def params(self, geo_col):
+ return (self.function, geo_col)
View
17 django/contrib/gis/db/models/__init__.py
@@ -0,0 +1,17 @@
+# Want to get everything from the 'normal' models package.
+from django.db.models import *
+
+# The GeoManager
+from django.contrib.gis.db.models.manager import GeoManager
+
+# The GeoQ object
+from django.contrib.gis.db.models.query import GeoQ
+
+# The geographic-enabled fields.
+from django.contrib.gis.db.models.fields import \
+ GeometryField, PointField, LineStringField, PolygonField, \
+ MultiPointField, MultiLineStringField, MultiPolygonField, \
+ GeometryCollectionField
+
+# The geographic mixin class.
+from mixin import GeoMixin
View
214 django/contrib/gis/db/models/fields/__init__.py
@@ -0,0 +1,214 @@
+from django.contrib.gis import forms
+from django.db import connection
+# Getting the SpatialBackend container and the geographic quoting method.
+from django.contrib.gis.db.backend import SpatialBackend, gqn
+# GeometryProxy, GEOS, Distance, and oldforms imports.
+from django.contrib.gis.db.models.proxy import GeometryProxy
+from django.contrib.gis.measure import Distance
+from django.contrib.gis.oldforms import WKTField
+
+# The `get_srid_info` function gets SRID information from the spatial
+# reference system table w/o using the ORM.
+from django.contrib.gis.models import get_srid_info
+
+#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
+class GeometryField(SpatialBackend.Field):
+ "The base GIS field -- maps to the OpenGIS Specification Geometry type."
+
+ # The OpenGIS Geometry name.
+ _geom = 'GEOMETRY'
+
+ # Geodetic units.
+ geodetic_units = ('Decimal Degree', 'degree')
+
+ def __init__(self, srid=4326, spatial_index=True, dim=2, **kwargs):
+ """
+ The initialization function for geometry fields. Takes the following
+ as keyword arguments:
+
+ srid:
+ The spatial reference system identifier, an OGC standard.
+ Defaults to 4326 (WGS84).
+
+ spatial_index:
+ Indicates whether to create a spatial index. Defaults to True.
+ Set this instead of 'db_index' for geographic fields since index
+ creation is different for geometry columns.
+
+ dim:
+ The number of dimensions for this geometry. Defaults to 2.
+ """
+
+ # Setting the index flag with the value of the `spatial_index` keyword.
+ self._index = spatial_index
+
+ # Setting the SRID and getting the units. Unit information must be
+ # easily available in the field instance for distance queries.
+ self._srid = srid
+ self._unit, self._unit_name, self._spheroid = get_srid_info(srid)
+
+ # Setting the dimension of the geometry field.
+ self._dim = dim
+
+ super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
+
+ ### Routines specific to GeometryField ###
+ @property
+ def geodetic(self):
+ """
+ Returns true if this field's SRID corresponds with a coordinate
+ system that uses non-projected units (e.g., latitude/longitude).
+ """
+ return self._unit_name in self.geodetic_units
+
+ def get_distance(self, dist_val, lookup_type):
+ """
+ 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
+
+ if isinstance(dist, Distance):
+ if self.geodetic:
+ # 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.')
+ # Spherical distance calculation parameter should be in meters.
+ dist_param = dist.m
+ else:
+ dist_param = getattr(dist, Distance.unit_attname(self._unit_name))
+ 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':
+ # 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]
+ else:
+ return [dist_param]
+
+ def get_geometry(self, value):
+ """
+ Retrieves the geometry, setting the default SRID from the given
+ lookup parameters.
+ """
+ if isinstance(value, (tuple, list)):
+ geom = value[0]
+ else:
+ geom = value
+
+ # When the input is not a GEOS geometry, attempt to construct one
+ # from the given string input.
+ if isinstance(geom, SpatialBackend.Geometry):
+ pass
+ elif isinstance(geom, basestring):
+ try:
+ geom = SpatialBackend.Geometry(geom)
+ except SpatialBackend.GeometryException:
+ raise ValueError('Could not create geometry from lookup value: %s' % str(value))
+ else:
+ raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
+
+ # Assigning the SRID value.
+ geom.srid = self.get_srid(geom)
+
+ return geom
+
+ def get_srid(self, geom):
+ """
+ Returns the default SRID for the given geometry, taking into account
+ the SRID set for the field. For example, if the input geometry
+ has no SRID, then that of the field will be returned.
+ """
+ gsrid = geom.srid # SRID of given geometry.
+ if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1):
+ return self._srid
+ else:
+ return gsrid
+
+ ### Routines overloaded from Field ###
+ def contribute_to_class(self, cls, name):
+ super(GeometryField, self).contribute_to_class(cls, name)
+
+ # Setup for lazy-instantiated Geometry object.
+ setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class' : forms.GeometryField,
+ 'geom_type' : self._geom,
+ 'null' : self.null,
+ }
+ defaults.update(kwargs)
+ return super(GeometryField, self).formfield(**defaults)
+
+ def get_db_prep_lookup(self, lookup_type, value):
+ """
+ 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).
+ """
+ if lookup_type in SpatialBackend.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 isinstance(value, (tuple, list)):
+ if lookup_type in SpatialBackend.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
+ else:
+ # Otherwise, making sure any other parameters are properly quoted.
+ where += map(gqn, value[1:])
+ return where, params
+ else:
+ raise TypeError("Field has invalid lookup: %s" % lookup_type)
+
+ def get_db_prep_save(self, value):
+ "Prepares the value for saving in the database."
+ if value is None:
+ return None
+ else:
+ return SpatialBackend.Adaptor(self.get_geometry(value))
+
+ def get_manipulator_field_objs(self):
+ "Using the WKTField (oldforms) to be our manipulator."
+ return [WKTField]
+
+# The OpenGIS Geometry Type Fields
+class PointField(GeometryField):
+ _geom = 'POINT'
+
+class LineStringField(GeometryField):
+ _geom = 'LINESTRING'
+
+class PolygonField(GeometryField):
+ _geom = 'POLYGON'
+
+class MultiPointField(GeometryField):
+ _geom = 'MULTIPOINT'
+
+class MultiLineStringField(GeometryField):
+ _geom = 'MULTILINESTRING'
+
+class MultiPolygonField(GeometryField):
+ _geom = 'MULTIPOLYGON'
+
+class GeometryCollectionField(GeometryField):
+ _geom = 'GEOMETRYCOLLECTION'
View
82 django/contrib/gis/db/models/manager.py
@@ -0,0 +1,82 @@
+from django.db.models.manager import Manager
+from django.contrib.gis.db.models.query import GeoQuerySet
+
+class GeoManager(Manager):
+ "Overrides Manager to return Geographic QuerySets."
+
+ # This manager should be used for queries on related fields
+ # so that geometry columns on Oracle and MySQL are selected
+ # properly.
+ use_for_related_fields = True
+
+ def get_query_set(self):
+ return GeoQuerySet(model=self.model)
+
+ def area(self, *args, **kwargs):
+ return self.get_query_set().area(*args, **kwargs)
+
+ def centroid(self, *args, **kwargs):
+ return self.get_query_set().centroid(*args, **kwargs)
+
+ def difference(self, *args, **kwargs):
+ return self.get_query_set().difference(*args, **kwargs)
+
+ def distance(self, *args, **kwargs):
+ return self.get_query_set().distance(*args, **kwargs)
+
+ def envelope(self, *args, **kwargs):
+ return self.get_query_set().envelope(*args, **kwargs)
+
+ def extent(self, *args, **kwargs):
+ return self.get_query_set().extent(*args, **kwargs)
+
+ def gml(self, *args, **kwargs):
+ return self.get_query_set().gml(*args, **kwargs)
+
+ def intersection(self, *args, **kwargs):
+ return self.get_query_set().intersection(*args, **kwargs)
+
+ def kml(self, *args, **kwargs):
+ return self.get_query_set().kml(*args, **kwargs)
+
+ def length(self, *args, **kwargs):
+ return self.get_query_set().length(*args, **kwargs)
+
+ 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)
+
+ def num_geom(self, *args, **kwargs):
+ return self.get_query_set().num_geom(*args, **kwargs)
+
+ def num_points(self, *args, **kwargs):
+ return self.get_query_set().num_points(*args, **kwargs)
+