Django

Code

Changeset 5881

Show
Ignore:
Timestamp:
08/12/07 20:33:26 (1 year ago)
Author:
jbronn
Message:

gis: added transform() manager method; _get_sql_clause() moved to GeoQuerySet?; cleaned up PostGIS backend for 1.3.0; added tests for 'equals' and 'relate' lookups.

Files:

Legend:

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

    r5776 r5881  
    1919if settings.DATABASE_ENGINE == 'postgresql_psycopg2': 
    2020    # PostGIS is the spatial database, getting the rquired modules, renaming as necessary. 
    21     from postgis import \ 
     21    from django.contrib.gis.db.backend.postgis import \ 
    2222        PostGISField as GeoBackendField, \ 
    2323        POSTGIS_TERMS as GIS_TERMS, \ 
    24         create_spatial_db, get_geo_where_clause 
     24        create_spatial_db, geo_quotename, get_geo_where_clause 
    2525else: 
    2626    raise NotImplementedError, 'No Geographic Backend exists for %s' % settings.DATABASE_NAME 
     
    250250        # with the get_geo_where_clause() 
    251251        if hasattr(field, '_geom'): 
     252            # Do we have multiple arguments, e.g., ST_Relate, ST_DWithin lookup types 
     253            #  need more than argument. 
     254            multiple_args = isinstance(value, tuple) 
     255 
    252256            # Getting the geographic where clause. 
    253257            gwc = get_geo_where_clause(lookup_type, current_table + '.', column, value) 
    254258 
    255259            # Getting the geographic parameters from the field. 
    256             geo_params = field.get_db_prep_lookup(lookup_type, value) 
     260            if multiple_args: 
     261                geo_params = field.get_db_prep_lookup(lookup_type, value[0]) 
     262            else: 
     263                geo_params = field.get_db_prep_lookup(lookup_type, value) 
    257264      
    258265            # If a dictionary was passed back from the field modify the where clause. 
    259             if isinstance(geo_params, dict): 
    260                 gwc = gwc % geo_params['where'] 
    261                 geo_params = geo_params['params'] 
     266            param_dict = isinstance(geo_params, dict) 
     267            if param_dict: 
     268                subst_list = geo_params['where'] 
     269                if multiple_args: subst_list += map(geo_quotename, value[1:]) 
     270                geo_params = geo_params['params']   
     271                gwc = gwc % tuple(subst_list) 
     272            elif multiple_args: 
     273                # Modify the where clause if we have multiple arguments -- the  
     274                #  first substitution will be for another placeholder (for the  
     275                #  geometry) since it is already apart of geo_params. 
     276                subst_list = ['%s'] 
     277                subst_list += map(geo_quotename, value[1:]) 
     278                gwc = gwc % tuple(subst_list) 
     279                 
     280            # Finally, appending onto the WHERE clause, and extending with any  
     281            #  additional parameters. 
    262282            where.append(gwc) 
    263283            params.extend(geo_params) 
  • django/branches/gis/django/contrib/gis/db/backend/postgis/field.py

    r5776 r5881  
    11from django.db.models.fields import Field # Django base Field class 
    22from django.contrib.gis.geos import GEOSGeometry, GEOSException  
     3from django.contrib.gis.db.backend.postgis.query import POSTGIS_TERMS, geo_quotename as quotename 
    34from types import StringType 
    4 from query import POSTGIS_TERMS, quotename 
    55 
    66class PostGISField(Field): 
     
    6767 
    6868    def get_db_prep_lookup(self, lookup_type, value): 
    69         "Returns field's value prepared for database lookup, accepts WKT and GEOS Geometries for the value." 
     69        """Returns field's value prepared for database lookup, accepts WKT and  
     70        GEOS Geometries for the value.""" 
    7071        if lookup_type in POSTGIS_TERMS: 
    7172            if lookup_type == 'isnull': return [value] # special case for NULL geometries. 
     
    7475                # GEOSGeometry instance passed in. 
    7576                if value.srid != self._srid: 
    76                     # Returning a dictionary instructs the parse_lookup() to add what's in the 'where' key 
    77                     #  to the where parameters, since we need to transform the geometry in the query. 
    78                     return {'where' : "Transform(%s,%s)", 
     77                    # Returning a dictionary instructs the parse_lookup() to add  
     78                    # what's in the 'where' key to the where parameters, since we  
     79                    # need to transform the geometry in the query. 
     80                    return {'where' : ["ST_Transform(%s,%s)"], 
    7981                            'params' : [value, self._srid] 
    8082                            } 
     
    103105        if isinstance(value, GEOSGeometry) and value.srid != self._srid: 
    104106            # Adding Transform() to the SQL placeholder. 
    105             return 'Transform(%%s, %s)' % self._srid 
     107            return 'ST_Transform(%%s, %s)' % self._srid 
    106108        else: 
    107109            return '%s' 
  • django/branches/gis/django/contrib/gis/db/backend/postgis/__init__.py

    r5806 r5881  
    22  The PostGIS spatial database backend module. 
    33""" 
    4 from query import \ 
    5     get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \ 
     4from django.contrib.gis.db.backend.postgis.query import \ 
     5    get_geo_where_clause, geo_quotename, \ 
     6    GEOM_FUNC_PREFIX, POSTGIS_TERMS, \ 
    67    MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 
    7 from creation import create_spatial_db 
    8 from field import PostGISField 
     8from django.contrib.gis.db.backend.postgis.creation import create_spatial_db 
     9from django.contrib.gis.db.backend.postgis.field import PostGISField 
    910 
    1011# Whether PostGIS has AsKML() support. 
  • django/branches/gis/django/contrib/gis/db/backend/postgis/management.py

    r5776 r5881  
    44  See PostGIS docs at Ch. 6.2.1 for more information on these functions. 
    55""" 
     6import re 
    67 
    78def _get_postgis_func(func): 
     
    1415    return row[0] 
    1516 
     17### PostGIS management functions ### 
    1618def postgis_geos_version(): 
    1719    "Returns the version of the GEOS library used with PostGIS." 
     
    3436    return _get_postgis_func('postgis_full_version') 
    3537 
     38### Routines for parsing output of management functions. ### 
     39version_regex = re.compile('^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') 
     40def postgis_version_tuple(): 
     41    "Returns the PostGIS version as a tuple." 
    3642 
     43    # Getting the PostGIS version 
     44    version = postgis_lib_version() 
     45    m = version_regex.match(version) 
     46    if m: 
     47        major = int(m.group('major')) 
     48        minor1 = int(m.group('minor1')) 
     49        minor2 = int(m.group('minor2')) 
     50    else: 
     51        raise Exception, 'Could not parse PostGIS version string: %s' % version 
     52 
     53    return (version, major, minor1, minor2) 
     54 
     55 
     56     
  • django/branches/gis/django/contrib/gis/db/backend/postgis/query.py

    r5776 r5881  
    44""" 
    55from django.db import backend 
    6 from management import postgis_lib_version 
     6from django.contrib.gis.db.backend.postgis.management import postgis_version_tuple 
     7from types import StringType, UnicodeType 
    78 
    8 # Getting the PostGIS version 
    9 POSTGIS_VERSION = postgis_lib_version() 
    10 MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = map(int, POSTGIS_VERSION.split('.')) 
     9# Getting the PostGIS version information 
     10POSTGIS_VERSION, MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 = postgis_version_tuple() 
    1111 
    1212# The supported PostGIS versions. 
    13 #  TODO: Confirm tests with PostGIS versions 1.1.x -- should work.  Versions <= 1.0.x didn't use GEOS C API. 
    14 if MAJOR_VERSION != 1 or MINOR_VERSION1 <= 1: 
     13#  TODO: Confirm tests with PostGIS versions 1.1.x -- should work.   
     14#        Versions <= 1.0.x do not use GEOS C API, and will not be supported. 
     15if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1): 
    1516    raise Exception, 'PostGIS version %s not supported.' % POSTGIS_VERSION 
    1617 
     
    1819# operators come from Section 6.2.2 of the official PostGIS documentation. 
    1920POSTGIS_OPERATORS = { 
    20     # The "&<" operator returns true if A's bounding box overlaps or is to the left of B's bounding box. 
     21    # The "&<" operator returns true if A's bounding box overlaps or 
     22    #  is to the left of B's bounding box. 
    2123    'overlaps_left' : '&<', 
    22     # The "&>" operator returns true if A's bounding box overlaps or is to the right of B's bounding box. 
     24    # The "&>" operator returns true if A's bounding box overlaps or 
     25    #  is to the right of B's bounding box. 
    2326    'overlaps_right' : '&>', 
    24     # The "<<" operator returns true if A's bounding box is strictly to the left of B's bounding box. 
     27    # The "<<" operator returns true if A's bounding box is strictly 
     28    #  to the left of B's bounding box. 
    2529    'left' : '<<', 
    26     # The ">>" operator returns true if A's bounding box is strictly to the right of B's bounding box. 
     30    # The ">>" operator returns true if A's bounding box is strictly 
     31    #  to the right of B's bounding box. 
    2732    'right' : '>>', 
    28     # The "&<|" operator returns true if A's bounding box overlaps or is below B's bounding box. 
     33    # The "&<|" operator returns true if A's bounding box overlaps or 
     34    #  is below B's bounding box. 
    2935    'overlaps_below' : '&<|', 
    30     # The "|&>" operator returns true if A's bounding box overlaps or is above B's bounding box. 
     36    # The "|&>" operator returns true if A's bounding box overlaps or 
     37    #  is above B's bounding box. 
    3138    'overlaps_above' : '|&>', 
    32     # The "<<|" operator returns true if A's bounding box is strictly below B's bounding box. 
     39    # The "<<|" operator returns true if A's bounding box is strictly 
     40    #  below B's bounding box. 
    3341    'strictly_below' : '<<|', 
    34     # The "|>>" operator returns true if A's bounding box is strictly above B's bounding box. 
     42    # The "|>>" operator returns true if A's bounding box is strictly 
     43    # above B's bounding box. 
    3544    'strictly_above' : '|>>', 
    36     # The "~=" operator is the "same as" operator. It tests actual geometric equality of two features. So if 
    37     # A and B are the same feature, vertex-by-vertex, the operator returns true. 
     45    # The "~=" operator is the "same as" operator. It tests actual 
     46    #  geometric equality of two features. So if A and B are the same feature, 
     47    #  vertex-by-vertex, the operator returns true. 
    3848    'same_as' : '~=', 
    3949    'exact' : '~=', 
    40     # The "@" operator returns true if A's bounding box is completely contained by B's bounding box. 
     50    # The "@" operator returns true if A's bounding box is completely contained 
     51    #  by B's bounding box. 
    4152    'contained' : '@', 
    42     # The "~" operator returns true if A's bounding box completely contains B's bounding box. 
     53    # The "~" operator returns true if A's bounding box completely contains 
     54    #  by B's bounding box. 
    4355    'bbcontains' : '~', 
    44     # The "&&" operator is the "overlaps" operator. If A's bounding boux overlaps B's bounding box the 
    45     # operator returns true
     56    # The "&&" operator returns true if A's bounding box overlaps 
     57    # B's bounding box
    4658    'bboverlaps' : '&&', 
    4759    } 
    4860 
    49 # PostGIS Geometry Relationship Functions -- most of these use GEOS. 
    50 
    51 # For PostGIS >= 1.2.2 these routines will do a bounding box query first before calling 
    52 #  the more expensive GEOS routines (called 'inline index magic'). 
    53 
    54 POSTGIS_GEOMETRY_FUNCTIONS = { 
    55     'equals' : '%sEquals', 
    56     'disjoint' : '%sDisjoint', 
    57     'touches' : '%sTouches', 
    58     'crosses' : '%sCrosses', 
    59     'within' : '%sWithin', 
    60     'overlaps' : '%sOverlaps', 
    61     'contains' : '%sContains', 
    62     'intersects' : '%sIntersects', 
    63     'relate' : ('%sRelate', str), 
    64     } 
    65  
    66 # Versions of PostGIS >= 1.2.2 changed their naming convention to be 'SQL-MM-centric'. 
    67 #  Practically, this means that 'ST_' is appended to geometry function names. 
    68 if MINOR_VERSION1 >= 2 and MINOR_VERSION2 >= 2: 
    69     # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2. 
    70     POSTGIS_GEOMETRY_FUNCTIONS.update( 
    71         {'dwithin' : ('%sDWithin', float), 
    72          'coveredby' : '%sCoveredBy', 
    73          'covers' : '%sCovers', 
    74          } 
    75         ) 
     61# Versions of PostGIS >= 1.2.2 changed their naming convention to be 
     62#  'SQL-MM-centric' to conform with the ISO standard. Practically, this  
     63#  means that 'ST_' is prefixes geometry function names. 
     64if MAJOR_VERSION > 1 or (MAJOR_VERSION == 1 and (MINOR_VERSION1 > 2 or (MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 2))): 
    7665    GEOM_FUNC_PREFIX = 'ST_' 
    7766else: 
    7867    GEOM_FUNC_PREFIX = '' 
    7968 
    80 # Updating with the geometry function prefix. 
    81 for k, v in POSTGIS_GEOMETRY_FUNCTIONS.items(): 
    82     if isinstance(v, tuple): 
    83         v = list(v) 
    84         v[0] = v[0] % GEOM_FUNC_PREFIX 
    85         v = tuple(v) 
    86     else: 
    87         v = v % GEOM_FUNC_PREFIX 
    88     POSTGIS_GEOMETRY_FUNCTIONS[k] = v 
     69# For PostGIS >= 1.2.2 the following lookup types will do a bounding box query 
     70#  first before calling the more computationally expensive GEOS routines (called 
     71#  "inline index magic"): 
     72#    'touches', 'crosses', 'contains', 'intersects', 'within', 'overlaps', and 
     73#    'covers'. 
     74POSTGIS_GEOMETRY_FUNCTIONS = { 
     75    'equals' : 'Equals', 
     76    'disjoint' : 'Disjoint', 
     77    'touches' : 'Touches', 
     78    'crosses' : 'Crosses', 
     79    'within' : 'Within', 
     80    'overlaps' : 'Overlaps', 
     81    'contains' : 'Contains', 
     82    'intersects' : 'Intersects', 
     83    'relate' : ('Relate', str), 
     84    } 
     85 
     86if GEOM_FUNC_PREFIX == 'ST_': 
     87    # Adding the GEOM_FUNC_PREFIX to the lookup functions. 
     88    for lookup, func in POSTGIS_GEOMETRY_FUNCTIONS.items(): 
     89        if isinstance(func, tuple): 
     90            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = (GEOM_FUNC_PREFIX + func[0], func[1]) 
     91        else: 
     92            POSTGIS_GEOMETRY_FUNCTIONS[lookup] = GEOM_FUNC_PREFIX + func 
     93 
     94    # The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+ 
     95    POSTGIS_GEOMETRY_FUNCTIONS.update( 
     96        {'dwithin' : ('ST_DWithin', float), 
     97         'coveredby' : 'ST_CoveredBy', 
     98         'covers' : 'ST_Covers', 
     99         } 
     100        ) 
    89101 
    90102# Any other lookup types that do not require a mapping. 
    91103MISC_TERMS = ['isnull'] 
    92  
    93 # The quotation used for postgis (uses single quotes). 
    94 def quotename(value, dbl=False): 
    95     if dbl: return '"%s"' % value 
    96     else: return "'%s'" % value 
    97104 
    98105# These are the PostGIS-customized QUERY_TERMS -- a list of the lookup types 
     
    102109POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') 
    103110POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable 
     111 
     112### PostGIS-specific Methods ### 
     113def get_geom_func(lookup_type): 
     114    func_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] 
     115    if isinstance(func_info, tuple): 
     116        return func_info[0] 
     117    else: 
     118        return func_info 
    104119 
    105120def get_geo_where_clause(lookup_type, table_prefix, field_name, value): 
     
    117132    # See if a PostGIS Geometry function matches the lookup type next 
    118133    try: 
    119         return '%s(%s%s, %%s)' % (POSTGIS_GEOMETRY_FUNCTIONS[lookup_type], table_prefix, field_name) 
     134        lookup_info = POSTGIS_GEOMETRY_FUNCTIONS[lookup_type] 
    120135    except KeyError: 
    121136        pass 
     137    else: 
     138        # Lookup types that are tuples take tuple arguments, e.g., 'relate' and  
     139        #  'dwithin' lookup types. 
     140        if isinstance(lookup_info, tuple): 
     141            # First element of tuple is lookup type, second element is the type 
     142            #  of the expected argument (e.g., str, float) 
     143            func, arg_type = lookup_info 
     144 
     145            # Ensuring that a tuple _value_ was passed in from the user 
     146            if not isinstance(value, tuple) or len(value) != 2:  
     147                raise TypeError, '2-element tuple required for %s lookup type.' % lookup_type 
     148             
     149            # Ensuring the argument type matches what we expect. 
     150            if not isinstance(value[1], arg_type): 
     151                raise TypeError, 'Argument type should be %s, got %s instead.' % (arg_type, type(value[1])) 
     152             
     153            return "%s(%s%s, %%s, %%s)" % (func, table_prefix, field_name) 
     154        else: 
     155            # Returning the SQL necessary for the geometry function call. For example:  
     156            #  ST_Contains("geoapp_country"."poly", ST_GeomFromText(..)) 
     157            return '%s(%s%s, %%s)' % (lookup_info, table_prefix, field_name) 
    122158     
    123159    # Handling 'isnull' lookup type 
     
    126162 
    127163    raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) 
     164 
     165def geo_quotename(value, dbl=False): 
     166    "Returns the quotation used for PostGIS on a given value (uses single quotes by default)." 
     167    if isinstance(value, (StringType, UnicodeType)): 
     168        if dbl: return '"%s"' % value 
     169        else: return "'%s'" % value 
     170    else: 
     171        return str(value) 
  • django/branches/gis/django/contrib/gis/db/models/manager.py

    r5806 r5881  
    1010    def kml(self, field_name, **kwargs): 
    1111        return self.get_query_set().kml(field_name, **kwargs) 
     12 
     13    def transform(self, field_name, **kwargs): 
     14        return self.get_query_set().transform(field_name, **kwargs) 
  • django/branches/gis/django/contrib/gis/db/models/query.py

    r5806 r5881  
    22from django.core.exceptions import ImproperlyConfigured 
    33from django.db import backend 
    4 from django.db.models.query import Q, QuerySet 
     4from django.db.models.query import Q, QuerySet, handle_legacy_orderlist, quote_only_if_word 
    55from django.db.models.fields import FieldDoesNotExist 
     6from django.utils.datastructures import SortedDict 
    67from django.contrib.gis.db.models.fields import GeometryField 
    78from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend. 
     
    1718    "Geographical-enabled QuerySet object." 
    1819 
     20    #### Overloaded QuerySet Routines #### 
    1921    def __init__(self, model=None): 
    2022        super(GeoQuerySet, self).__init__(model=model) 
     
    2224        # We only want to use the GeoQ object for our queries 
    2325        self._filters = GeoQ() 
     26 
     27        # For replacement fields in the SELECT. 
     28        self._custom_select = {} 
    2429 
    2530    def _filter_or_exclude(self, mapper, *args, **kwargs): 
     
    3439        clone = self._clone() 
    3540        if len(kwargs) > 0: 
    36             clone._filters = clone._filters & mapper(GeoQ(**kwargs)) # Using the GeoQ object for our filters instead 
     41            # Using the GeoQ object for our filters instead 
     42            clone._filters = clone._filters & mapper(GeoQ(**kwargs)) 
    3743        if len(args) > 0: 
    3844            clone._filters = clone._filters & reduce(operator.and_, map(mapper, args)) 
    3945        return clone 
    4046 
     47    def _get_sql_clause(self): 
     48        opts = self.model._meta 
     49 
     50        # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. 
     51        select = [] 
     52 
     53        # This is the only component of this routine that is customized for the  
     54        #  GeoQuerySet. Specifically, this allows operations to be done on fields  
     55        #  in the SELECT, overriding their values -- this is different from using  
     56        #  QuerySet.extra(select=foo) because extra() adds an  an _additional_  
     57        #  field to be selected.  Used in returning transformed geometries. 
     58        for f in opts.fields: 
     59            if f.column in self._custom_select: select.append(self._custom_select[f.column]) 
     60            else: select.append(self._field_column(f)) 
     61 
     62        tables = [quote_only_if_word(t) for t in self._tables] 
     63        joins = SortedDict() 
     64        where = self._where[:] 
     65        params = self._params[:] 
     66 
     67        # Convert self._filters into SQL. 
     68        joins2, where2, params2 = self._filters.get_sql(opts) 
     69        joins.update(joins2) 
     70        where.extend(where2) 
     71        params.extend(params2) 
     72 
     73        # Add additional tables and WHERE clauses based on select_related. 
     74        if self._select_related: 
     75            fill_table_cache(opts, select, tables, where, 
     76                             old_prefix=opts.db_table, 
     77                             cache_tables_seen=[opts.db_table], 
     78                             max_depth=self._max_related_depth) 
     79 
     80        # Add any additional SELECTs. 
     81        if self._select: 
     82            select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), backend.quote_name(s[0])) for s in self._select.items()]) 
     83 
     84        # Start composing the body of the SQL statement. 
     85        sql = [" FROM", backend.quote_name(opts.db_table)] 
     86 
     87        # Compose the join dictionary into SQL describing the joins. 
     88        if joins: 
     89            sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition) 
     90                            for (alias, (table, join_type, condition)) in joins.items()])) 
     91 
     92        # Compose the tables clause into SQL. 
     93        if tables: 
     94            sql.append(", " + ", ".join(tables)) 
     95 
     96        # Compose the where clause into SQL. 
     97        if where: 
     98            sql.append(where and "WHERE " + " AND ".join(where)) 
     99 
     100        # ORDER BY clause 
     101        order_by = [] 
     102        if self._order_by is not None: 
     103            ordering_to_use = self._order_by 
     104        else: 
     105            ordering_to_use = opts.ordering 
     106        for f in handle_legacy_orderlist(ordering_to_use): 
     107            if f == '?': # Special case. 
     108                order_by.append(backend.get_random_function_sql()) 
     109            else: 
     110                if f.startswith('-'): 
     111                    col_name = f[1:] 
     112                    order = "DESC" 
     113                else: 
     114                    col_name = f 
     115                    order = "ASC" 
     116                if "." in col_name: 
     117                    table_prefix, col_name = col_name.split('.', 1) 
     118                    table_prefix = backend.quote_name(table_prefix) + '.' 
     119                else: 
     120                    # Use the database table as a column prefix if it wasn't given, 
     121                    # and if the requested column isn't a custom SELECT. 
     122                    if "." not in col_name and col_name not in (self._select or ()): 
     123                        table_prefix = backend.quote_name(opts.db_table) + '.' 
     124                    else: 
     125                        table_prefix = '' 
     126                order_by.append('%s%s %s' % (table_prefix, backend.quote_name(orderfield2column(col_name, opts)), order)) 
     127        if order_by: 
     128            sql.append("ORDER BY " + ", ".join(order_by)) 
     129 
     130        # LIMIT and OFFSET clauses 
     131        if self._limit is not None: 
     132            sql.append("%s " % backend.get_limit_offset_sql(self._limit, self._offset)) 
     133        else: 
     134            assert self._offset is None, "'offset' is not allowed without 'limit'" 
     135 
     136        return select, " ".join(sql), params 
     137 
     138    def _clone(self, klass=None, **kwargs): 
     139        c = super(GeoQuerySet, self)._clone(klass, **kwargs) 
     140        c._custom_select = self._custom_select 
     141        return c 
     142 
     143    #### Methods specific to the GeoQuerySet #### 
     144    def _field_column(self, field): 
     145        return "%s.%s" % (backend.quote_name(self.model._meta.db_table), 
     146                          backend.quote_name(field.column)) 
     147     
    41148    def kml(self, field_name, precision=8): 
    42149        """Returns KML representation of the given field name in a `kml`  
     
    52159        if not isinstance(field, GeometryField): 
    53160            raise TypeError, 'KML output only available on GeometryField fields.' 
    54         field_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table), 
    55                                backend.quote_name(field.column)) 
     161        field_col = self._field_column(field) 
    56162         
    57163        # Adding the AsKML function call to the SELECT part of the SQL. 
    58164        return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)}) 
     165 
     166    def transform(self, field_name, srid=4326): 
     167        """Transforms the given geometry field to the given SRID.  If no SRID is 
     168        provided, the transformation will default to using 4326 (WGS84).""" 
     169        field = self.model._meta.get_field(field_name) 
     170        if not isinstance(field, GeometryField): 
     171            raise TypeError, 'ST_Transform() only available for GeometryField fields.' 
     172 
     173        # Setting the key for the field's column with the custom SELECT SQL to  
     174        #  override the geometry column returned from the database. 
     175        self._custom_select[field.column] = \ 
     176            '(ST_Transform(%s, %s)) AS %s' % (self._field_column(field), srid,  
     177                                              backend.quote_name(field.column)) 
     178        return self._clone() 
     179 
     180     
  • django/branches/gis/django/contrib/gis/tests/geoapp/tests.py

    r5806 r5881  
    8686        self.assertEqual('<Point><coordinates>-104.609252,38.255001,0</coordinates></Point>', ptown.kml) 
    8787 
     88    def test04_transform(self): 
     89        "Testing the transform() queryset method." 
     90 
     91        # Pre-transformed points for Houston and Pueblo. 
     92        htown = fromstr('POINT(1947516.83115183 6322297.06040572)', srid=3084) 
     93        ptown = fromstr('POINT(992363.390841912 481455.395105533)', srid=2774) 
     94 
     95        # Asserting the result of the transform operation with the values in 
     96        #  the pre-transformed points. 
     97        h = City.objects.transform('point', srid=htown.srid).get(name='Houston') 
     98        self.assertAlmostEqual(htown.x, h.point.x, 8) 
     99        self.assertAlmostEqual(htown.y, h.point.y, 8) 
     100 
     101        p = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo') 
     102        self.assertAlmostEqual(ptown.x, p.point.x, 8) 
     103        self.assertAlmostEqual(ptown.y, p.point.y, 8) 
     104 
    88105    def test10_contains_contained(self): 
    89106        "Testing the 'contained' and 'contains' lookup types." 
     
    201218        for c in qs: self.assertEqual(True, c.name in cities) 
    202219 
     220    def test14_equals(self): 
     221        "Testing the 'same_as' and 'equals' lookup types." 
     222        pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326) 
     223        c1 = City.objects.get(point=pnt) 
     224        c2 = City.objects.get(point__same_as=pnt) 
     225        c3 = City.objects.get(point__equals=pnt) 
     226        for c in [c1, c2, c3]: self.assertEqual('Houston', c.name) 
     227 
     228    def test15_relate(self): 
     229        "Testing the 'relate' lookup type." 
     230        # To make things more interesting, we will have our Texas reference point in  
     231        #  different SRIDs. 
     232        pnt1 = fromstr('POINT (649287.0363174345111474 4177429.4494686722755432)', srid=2847) 
     233        pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326) 
     234 
     235        # Testing bad argument tuples that should return a TypeError 
     236        bad_args = [(pnt1, 0), (pnt2, 'T*T***FF*', 0), (23, 'foo')] 
     237        for args in bad_args: 
     238            try: 
     239                qs = Country.objects.filter(mpoly__relate=args) 
     240                cnt = qs.count() 
     241            except TypeError: 
     242                pass 
     243            else: 
     244                self.fail('Expected a TypeError') 
     245 
     246        # 'T*T***FF*' => Contains() 
     247        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T*T***FF*')).name) 
     248        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T*T***FF*')).name) 
     249 
     250        # 'T*F**F***' => Within() 
     251        ks = State.objects.get(name='Kansas') 
     252        self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T*F**F***')).name) 
     253 
     254        # 'T********' => Intersects() 
     255        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt1, 'T********')).name) 
     256        self.assertEqual('Texas', Country.objects.get(mpoly__relate=(pnt2, 'T********')).name) 
     257        self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, 'T********')).name) 
     258 
    203259def suite(): 
    204260    s = unittest.TestSuite()