Django

Code

Changeset 6467

Show
Ignore:
Timestamp:
10/08/07 13:18:17 (1 year ago)
Author:
jbronn
Message:

gis: spatial-database compatibility and usability changes:

(1) The SpatialRefSys? and GeometryColumns? models and GeometryProxy? have been moved to the PostGIS backend because their functionality depends on the spatial databse.
(2) The GeoMixin? is no longer required, as all the functionality contributed by the extra instance methods has been moved to the GeometryProxy?.
(3) The _post_create_sql field now returns a tuple of SQL statements, instead of a string.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/gis/django/contrib/gis/db/backend/__init__.py

    r6441 r6467  
    11""" 
    2   This module provides the backend for spatial SQL construction with Django. 
    3  
    4   Specifically, this module will import the correct routines and modules 
    5     needed for GeoDjango 
    6  
    7    (1) GeoBackEndField, a base class needed for GeometryField. 
    8    (2) The parse_lookup() function, used for spatial SQL construction by 
    9        the GeoQuerySet. 
    10        
    11   Currently only PostGIS is supported, but someday backends will be added for 
    12    additional spatial databases (e.g., Oracle, DB2). 
     2 This module provides the backend for spatial SQL construction with Django. 
     3 
     4 Specifically, this module will import the correct routines and modules 
     5 needed for GeoDjango. 
     6  
     7 (1) GeoBackEndField, a base class needed for GeometryField. 
     8 (2) GeometryProxy, for lazy-instantiated geometries from the  
     9     database output. 
     10 (3) GIS_TERMS, a list of acceptable geographic lookup types for  
     11     the backend. 
     12 (4) The `parse_lookup` function, used for spatial SQL construction by 
     13     the GeoQuerySet. 
     14 (5) The `create_spatial_db`, `geo_quotename`, and `get_geo_where_clause` 
     15     routines (needed by `parse_lookup`. 
     16 
     17 Currently only PostGIS is supported, but someday backends will be added for 
     18 additional spatial databases (e.g., Oracle, DB2). 
    1319""" 
    1420from django.conf import settings 
     
    2228 
    2329if settings.DATABASE_ENGINE == 'postgresql_psycopg2': 
    24     # PostGIS is the spatial database, getting the rquired modules, renaming as necessary. 
     30    # PostGIS is the spatial database, getting the rquired modules,  
     31    # renaming as necessary. 
    2532    from django.contrib.gis.db.backend.postgis import \ 
    2633        PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \ 
     34        PostGISProxy as GeometryProxy, \ 
    2735        create_spatial_db, geo_quotename, get_geo_where_clause, \ 
    2836        ASGML, ASKML, UNION 
  • django/branches/gis/django/contrib/gis/db/backend/postgis/field.py

    r6439 r6467  
    4545 
    4646    def _post_create_sql(self, style, db_table): 
    47         """Returns SQL that will be executed after the model has been 
     47        """ 
     48        Returns SQL that will be executed after the model has been 
    4849        created. Geometry columns must be added after creation with the 
    49         PostGIS AddGeometryColumn() function.""" 
     50        PostGIS AddGeometryColumn() function. 
     51        """ 
    5052 
    5153        # Getting the AddGeometryColumn() SQL necessary to create a PostGIS 
     
    5557        # If the user wants to index this data, then get the indexing SQL as well. 
    5658        if self._index: 
    57             return '%s\n%s' % (post_sql, self._geom_index(style, db_table)) 
     59            return (post_sql, self._geom_index(style, db_table)) 
    5860        else: 
    59             return post_sql 
     61            return (post_sql,) 
    6062 
    6163    def _post_delete_sql(self, style, db_table): 
     
    6769        return sql 
    6870 
     71    def db_type(self): 
     72        """ 
     73        PostGIS geometry columns are added by stored procedures, should be 
     74        None. 
     75        """ 
     76        return None 
     77 
    6978    def get_db_prep_lookup(self, lookup_type, value): 
    70         """Returns field's value prepared for database lookup, accepts WKT and  
    71         GEOS Geometries for the value.""" 
     79        """ 
     80        Returns field's value prepared for database lookup, accepts WKT and  
     81        GEOS Geometries for the value. 
     82        """ 
    7283        if lookup_type in POSTGIS_TERMS: 
    7384            if lookup_type == 'isnull': return [value] # special case for NULL geometries. 
     
    102113            raise TypeError('Geometry Proxy should only return GEOSGeometry objects.') 
    103114 
     115    def get_internal_type(self): 
     116        """ 
     117        Returns NoField because a stored procedure is used by PostGIS to create the 
     118        """ 
     119        return 'NoField' 
     120 
    104121    def get_placeholder(self, value): 
    105122        """ 
  • django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py

    r6439 r6467  
    88from django.contrib.gis.db.backend.postgis.creation import create_spatial_db 
    99from django.contrib.gis.db.backend.postgis.field import PostGISField 
     10from django.contrib.gis.db.backend.postgis.proxy import PostGISProxy 
    1011 
    1112# Functions used by GeoManager methods, and not via lookup types. 
  • django/branches/gis/django/contrib/gis/db/backend/postgis/proxy.py

    r6243 r6467  
    11""" 
    2   The GeometryProxy object, allows for lazy-geometries.  The proxy uses 
    3   Python descriptors for instantiating and setting GEOS Geometry objects 
    4   corresponding to geographic model fields. 
     2 The GeometryProxy object, allows for lazy-geometries.  The proxy uses 
     3  Python descriptors for instantiating and setting GEOS Geometry objects 
     4  corresponding to geographic model fields. 
    55 
    6   Thanks to Robert Coup for providing this functionality (see #4322). 
     6 Thanks to Robert Coup for providing this functionality (see #4322). 
    77""" 
    88 
    99from types import NoneType, StringType, UnicodeType 
    10 from django.contrib.gis.geos import GEOSGeometry, GEOSException  
    1110 
    12 # TODO: docstrings 
    13 class GeometryProxy(object):  
    14     def __init__(self, field):  
    15         "Proxy initializes on the given GeometryField." 
     11class PostGISProxy(object):  
     12    def __init__(self, klass, field):  
     13        """ 
     14        Proxy initializes on the given Geometry class (not an instance) and  
     15        the GeometryField. 
     16        """ 
    1617        self._field = field  
     18        self._klass = klass 
    1719      
    1820    def __get__(self, obj, type=None):  
     21        """ 
     22        This accessor retrieves the geometry, initializing it using the geometry 
     23        class specified during initialization and the HEXEWKB value of the field.   
     24        Currently, only GEOS or OGR geometries are supported. 
     25        """ 
    1926        # Getting the value of the field. 
    2027        geom_value = obj.__dict__[self._field.attname]  
    21  
    22         if isinstance(geom_value, GEOSGeometry):  
    23             # If the value of the field is None, or is already a GEOS Geometry 
    24             #  no more work is needed. 
     28         
     29        if isinstance(geom_value, self._klass):  
    2530            geom = geom_value 
    2631        elif (geom_value is None) or (geom_value==''): 
     
    2934            # Otherwise, a GEOSGeometry object is built using the field's contents, 
    3035            #  and the model's corresponding attribute is set. 
    31             geom = GEOSGeometry(geom_value) 
     36            geom = self._klass(geom_value) 
    3237            setattr(obj, self._field.attname, geom)  
    3338        return geom  
    3439      
    35     def __set__(self, obj, value):  
    36         if isinstance(value, GEOSGeometry) and (value.geom_type.upper() == self._field._geom): 
    37             # Getting set with GEOS Geometry; geom_type must match that of the field. 
    38  
    39             # If value's SRID is not set, setting it to the field's SRID. 
     40    def __set__(self, obj, value): 
     41        """ 
     42        This accessor sets the proxied geometry with the geometry class 
     43        specified during initialization.  Values of None, HEXEWKB, or WKT may 
     44        be used to set the geometry as well. 
     45        """ 
     46        # The OGC Geometry type of the field. 
     47        gtype = self._field._geom 
     48         
     49        # The geometry type must match that of the field -- unless the 
     50        # general GeometryField is used. 
     51        if isinstance(value, self._klass) and (str(value.geom_type).upper() == gtype or gtype == 'GEOMETRY'): 
     52            # Assigning the SRID to the geometry. 
    4053            if value.srid is None: value.srid = self._field._srid 
    4154        elif isinstance(value, (NoneType, StringType, UnicodeType)): 
    42             # Getting set with None, WKT, or HEX 
     55            # Set with None, WKT, or HEX 
    4356            pass 
    4457        else: 
    45             raise TypeError, 'cannot set %s GeometryProxy with value of type: %s' % (self._field._geom, type(value)) 
     58            raise TypeError('cannot set %s GeometryProxy with value of type: %s' % (obj.__class__.__name__, type(value))) 
     59 
     60        # Setting the objects dictionary with the value, and returning. 
    4661        obj.__dict__[self._field.attname] = value  
    4762        return value  
  • django/branches/gis/django/contrib/gis/db/models/fields/__init__.py

    r6018 r6467  
    1 from django.contrib.gis.db.backend import GeoBackendField # depends on the spatial database backend. 
    2 from django.contrib.gis.db.models.proxy import GeometryProxy 
     1from django.conf import settings 
     2from django.contrib.gis.db.backend import GeoBackendField, GeometryProxy # these depend on the spatial database backend. 
    33from django.contrib.gis.oldforms import WKTField 
    4 from django.utils.functional import curry 
     4from django.contrib.gis.geos import GEOSGeometry 
    55 
    6 #TODO: Flesh out widgets
     6#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies
    77class GeometryField(GeoBackendField): 
    88    "The base GIS field -- maps to the OpenGIS Specification Geometry type." 
     
    3232        super(GeometryField, self).contribute_to_class(cls, name) 
    3333 
    34         # setup for lazy-instantiated GEOSGeometry objects 
    35         setattr(cls, self.attname, GeometryProxy(self)) 
    36  
    37         # Adding needed accessor functions 
    38         setattr(cls, 'get_%s_geos' % self.name, curry(cls._get_GEOM_geos, field=self)) 
    39         setattr(cls, 'get_%s_ogr' % self.name, curry(cls._get_GEOM_ogr, field=self, srid=self._srid)) 
    40         setattr(cls, 'get_%s_srid' % self.name, curry(cls._get_GEOM_srid, srid=self._srid)) 
    41         setattr(cls, 'get_%s_srs' % self.name, curry(cls._get_GEOM_srs, srid=self._srid)) 
    42         setattr(cls, 'get_%s_wkt' % self.name, curry(cls._get_GEOM_wkt, field=self)) 
    43         setattr(cls, 'get_%s_centroid' % self.name, curry(cls._get_GEOM_centroid, field=self)) 
    44         setattr(cls, 'get_%s_area' % self.name, curry(cls._get_GEOM_area, field=self)) 
     34        # Setup for lazy-instantiated GEOSGeometry object. 
     35        setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self)) 
    4536         
    46     def get_internal_type(self): 
    47         return "NoField" 
    48  
    49     def db_type(self): 
    50         # Geometry columns are added by stored procedures, and thus should 
    51         #  be None. 
    52         return None 
    53                                                                  
    5437    def get_manipulator_field_objs(self): 
    5538        "Using the WKTField (defined above) to be our manipulator." 
  • django/branches/gis/django/contrib/gis/db/models/mixin.py

    r6413 r6467  
    1 from warnings import warn 
    2  
    3 # GEOS is a requirement, GDAL is not 
    4 from django.contrib.gis.geos import GEOSGeometry 
    5 from django.contrib.gis.gdal import HAS_GDAL 
    6 if HAS_GDAL: 
    7     from django.contrib.gis.gdal import OGRGeometry, SpatialReference 
    8  
    91# Until model subclassing is a possibility, a mixin class is used to add 
    102# the necessary functions that may be contributed for geographic objects. 
    113class GeoMixin: 
    12     "The Geographic Mixin class provides routines for geographic objects." 
    13  
    14     # A subclass of Model is specifically needed so that these geographic 
    15     # routines are present for instantiations of the models. 
    16     def _get_GEOM_geos(self, field): 
    17         "Returns a GEOS Python object for the geometry." 
    18         warn("use model.%s" % field.attname, DeprecationWarning)  
    19         return getattr(self, field.attname) 
    20  
    21     def _get_GEOM_ogr(self, field, srid): 
    22         "Returns an OGR Python object for the geometry." 
    23         if HAS_GDAL: 
    24             return OGRGeometry(getattr(self, field.attname).wkt, 
    25                                SpatialReference('EPSG:%d' % srid)) 
    26         else: 
    27             raise Exception, "GDAL is not installed!" 
    28  
    29     def _get_GEOM_srid(self, srid): 
    30         "Returns the spatial reference identifier (SRID) of the geometry." 
    31         warn("use model.geometry_field.srid", DeprecationWarning) 
    32         return srid 
    33  
    34     def _get_GEOM_srs(self, srid): 
    35         "Returns ane OGR Spatial Reference object of the geometry." 
    36         if HAS_GDAL: 
    37             return SpatialReference('EPSG:%d' % srid) 
    38         else: 
    39             raise Exception, "GDAL is not installed!" 
    40  
    41     def _get_GEOM_wkt(self, field): 
    42         "Returns the WKT of the geometry." 
    43         warn("use model.%s.centroid.wkt" % field.attname, DeprecationWarning)  
    44         return getattr(self, field.attname).wkt 
    45  
    46     def _get_GEOM_centroid(self, field): 
    47         "Returns the centroid of the geometry, in WKT." 
    48         warn("use model.%s.centroid.wkt" % field.attname, DeprecationWarning)  
    49         return getattr(self, field.attname).centroid.wkt 
    50      
    51     def _get_GEOM_area(self, field): 
    52         "Returns the area of the geometry, in projected units." 
    53         warn("use model.%s.area" % field.attname, DeprecationWarning) 
    54         return getattr(self, field.attname).area 
     4    """ 
     5    The Geographic Mixin class provides routines for geographic objects, 
     6    however, it is no longer necessary, since all of its previous functions  
     7    may now be accessed via the GeometryProxy.  This mixin is only provided 
     8    for backwards-compatibility purposes, and will be eventually removed 
     9    (unless the need arises again). 
     10    """ 
     11    pass 
  • django/branches/gis/django/contrib/gis/models.py

    r6427 r6467  
    11""" 
    2  Models for the PostGIS/OGC database tables. 
     2 Imports the SpatialRefSys and GeometryColumns models dependent on the 
     3 spatial database backend. 
    34""" 
    45import re 
    5 from django.db import model
     6from django.conf import setting
    67 
    78# Checking for the presence of GDAL (needed for the SpatialReference object) 
     
    1617spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),') 
    1718 
    18 # This is the global 'geometry_columns' from PostGIS. 
    19 #   See PostGIS Documentation at Ch. 4.2.2 
    20 class GeometryColumns(models.Model): 
    21     f_table_catalog = models.CharField(maxlength=256) 
    22     f_table_schema = models.CharField(maxlength=256) 
    23     f_table_name = models.CharField(maxlength=256, primary_key=True) 
    24     f_geometry_column = models.CharField(maxlength=256) 
    25     coord_dimension = models.IntegerField() 
    26     srid = models.IntegerField() 
    27     type = models.CharField(maxlength=30) 
    28  
    29     class Meta: 
    30         db_table = 'geometry_columns' 
    31  
    32     def __str__(self): 
    33         return "%s.%s - %dD %s field (SRID: %d)" % \ 
    34                (self.f_table_name, self.f_geometry_column, 
    35                 self.coord_dimension, self.type, self.srid) 
    36  
    37 # This is the global 'spatial_ref_sys' table from PostGIS. 
    38 #   See PostGIS Documentation at Ch. 4.2.1 
    39 class SpatialRefSys(models.Model): 
    40     srid = models.IntegerField(primary_key=True) 
    41     auth_name = models.CharField(maxlength=256) 
    42     auth_srid = models.IntegerField() 
    43     srtext = models.CharField(maxlength=2048) 
    44     proj4text = models.CharField(maxlength=2048) 
    45  
    46     class Meta: 
    47         db_table = 'spatial_ref_sys' 
    48  
     19class SpatialRefSysMixin(object): 
     20    """ 
     21    The SpatialRefSysMixin is a class used by the database-dependent 
     22    SpatialRefSys objects to reduce redundnant code. 
     23    """ 
    4924    @property 
    5025    def srs(self): 
     
    5934                # Attempting to cache a SpatialReference object. 
    6035 
    61                 # Trying to get from WKT first 
     36                # Trying to get from WKT first. 
    6237                try: 
    63                     self._srs = SpatialReference(self.srtext, 'wkt'
    64                     return self._srs.clone() 
    65                 except Exception, msg1
     38                    self._srs = SpatialReference(self.wkt
     39                    return self.srs 
     40                except Exception, msg
    6641                    pass 
     42                 
     43                raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) 
     44        else: 
     45            raise Exception('GDAL is not installed.') 
    6746 
    68                 # Trying the proj4 text next 
    69                 try: 
    70                     self._srs = SpatialReference(self.proj4text, 'proj4') 
    71                     return self._srs.clone() 
    72                 except Exception, msg2: 
    73                     pass 
    74  
    75                 raise Exception, 'Could not get an OSR Spatial Reference:\n\tWKT error: %s\n\tPROJ.4 error: %s' % (msg1, msg2) 
    76         else: 
    77             raise Exception, 'GDAL is not installed!' 
    78                                                                                  
    7947    @property 
    8048    def ellipsoid(self): 
     
    8654            return self.srs.ellipsoid 
    8755        else: 
    88             m = spheroid_regex.match(self.srtext) 
     56            m = spheroid_regex.match(self.wkt) 
    8957            if m: return (float(m.group('major')), float(m.group('flattening'))) 
    9058            else: return None 
     
    140108        return self.srs.angular_name 
    141109 
    142     def __str__(self): 
     110    def __unicode__(self): 
    143111        """ 
    144112        Returns the string representation.  If GDAL is installed, 
    145113        it will be 'pretty' OGC WKT. 
    146114        """ 
    147         if HAS_GDAL: return str(self.srs) 
    148         else: return "%d:%s " % (self.srid, self.auth_name) 
     115        try: 
     116            return unicode(self.srs) 
     117        except: 
     118            return unicode(self.srtext) 
     119 
     120# The SpatialRefSys and GeometryColumns models 
     121if settings.DATABASE_ENGINE == 'postgresql_psycopg2': 
     122    from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys 
     123else: 
     124    raise NotImplementedError('No SpatialRefSys or GeometryColumns models for backend: %s' % settings.DATABASE_ENGINE) 
  • django/branches/gis/django/contrib/gis/tests/geoapp/models.py

    r5773 r6467  
    11from django.contrib.gis.db import models 
    22 
    3 class Country(models.Model, models.GeoMixin): 
    4     name = models.CharField(maxlength=30) 
     3class Country(models.Model): 
     4    name = models.CharField(max_length=30) 
    55    mpoly = models.MultiPolygonField() # SRID, by default, is 4326 
    66    objects = models.GeoManager() 
    77 
    8 class City(models.Model, models.GeoMixin): 
    9     name = models.CharField(maxlength=30) 
     8class City(models.Model): 
     9    name = models.CharField(max_length=30) 
    1010    point = models.PointField()  
    1111    objects = models.GeoManager() 
    1212 
    13 class State(models.Model, models.GeoMixin): 
    14     name = models.CharField(maxlength=30) 
     13class State(models.Model): 
     14    name = models.CharField(max_length=30) 
    1515    poly = models.PolygonField(null=True) # Allowing NULL geometries here. 
    1616    objects = models.GeoManager() 
     17 
     18class Feature(models.Model): 
     19    name = models.CharField(max_length=20) 
     20    geom = models.GeometryField() 
     21    objects = models.GeoManager() 
  • django/branches/gis/django/contrib/gis/tests/geoapp/tests.py

    r6441 r6467  
    11import unittest 
    2 from models import Country, City, State 
    3 from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon 
     2from models import Country, City, State, Feature 
     3from django.contrib.gis.geos import * 
     4from django.contrib.gis import gdal 
    45 
    56class GeoModelTest(unittest.TestCase): 
     
    6061        self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None 
    6162        nullstate.save() 
    62         self.assertEqual(ply, State.objects.get(name='NullState').poly) 
    63          
     63 
     64        ns = State.objects.get(name='NullState') 
     65        self.assertEqual(ply, ns.poly) 
     66         
     67        # Testing the `ogr` and `srs` lazy-geometry properties. 
     68        if gdal.HAS_GDAL: 
     69            self.assertEqual(True, isinstance(ns.poly.ogr, gdal.OGRGeometry)) 
     70            self.assertEqual(ns.poly.wkb, ns.poly.ogr.wkb) 
     71            self.assertEqual(True, isinstance(ns.poly.srs, gdal.SpatialReference)) 
     72            self.assertEqual('WGS 84', ns.poly.srs.name) 
     73 
    6474        # Changing the interior ring on the poly attribute. 
    6575        new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30)) 
     
    278288        qs = City.objects.filter(name='NotACity') 
    279289        self.assertEqual(None, qs.union('point')) 
     290 
     291    def test18_geometryfield(self): 
     292        "Testing GeometryField." 
     293        f1 = Feature(name='Point', geom=Point(1, 1)) 
     294        f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))) 
     295        f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))) 
     296        f4 = Feature(name='GeometryCollection',  
     297                     geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),  
     298                                             Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))) 
     299        f1.save() 
     300        f2.save() 
     301        f3.save() 
     302        f4.save() 
     303 
     304        f_1 = Feature.objects.get(name='Point') 
     305        self.assertEqual(True, isinstance(f_1.geom, Point)) 
     306        self.assertEqual((1.0, 1.0), f_1.geom.tuple) 
     307        f_2 = Feature.objects.get(name='LineString') 
     308        self.assertEqual(True, isinstance(f_2.geom, LineString)) 
     309        self.assertEqual(((0.0, 0.0), (1.0, 1.0), (5.0, 5.0)), f_2.geom.tuple) 
     310 
     311        f_3 = Feature.objects.get(name='Polygon') 
     312        self.assertEqual(True, isinstance(f_3.geom, Polygon)) 
     313        f_4 = Feature.objects.get(name='GeometryCollection') 
     314        self.assertEqual(True, isinstance(f_4.geom, GeometryCollection)) 
     315        self.assertEqual(f_3.geom, f_4.geom[2]) 
    280316     
    281317def suite(): 
  • django/branches/gis/django/core/management/sql.py

    r6395 r6467  
    405405    for f in opts.fields: 
    406406        if hasattr(f, '_post_create_sql'): 
    407             output.append(f._post_create_sql(style, model._meta.db_table)) 
     407            output.extend(f._post_create_sql(style, model._meta.db_table)) 
    408408 
    409409    # Some backends can't execute more than one SQL statement at a time,