diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 0564ad7348..7a1cab251b 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -1,7 +1,17 @@ """ The PostGIS spatial database backend module. """ -from query import get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS +from query import \ + get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \ + MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2 from creation import create_spatial_db from field import PostGISField +# Whether PostGIS has AsKML() support. +if MAJOR_VERSION == 1: + # AsKML() only supported in versions 1.2.1+ + if MINOR_VERSION1 == 3: + ASKML = 'ST_AsKML' + elif MINOR_VERSION1 == 2 and MINOR_VERSION2 >= 1: + ASKML = 'AsKML' + diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 5faa259a12..e01f1aa724 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -7,5 +7,5 @@ class GeoManager(Manager): def get_query_set(self): return GeoQuerySet(model=self.model) - def kml(self, field_name): - return self.get_query_set().kml(field_name) + def kml(self, field_name, **kwargs): + return self.get_query_set().kml(field_name, **kwargs) diff --git a/django/contrib/gis/db/models/proxy.py b/django/contrib/gis/db/models/proxy.py index de366d0186..e8fbb87bd4 100644 --- a/django/contrib/gis/db/models/proxy.py +++ b/django/contrib/gis/db/models/proxy.py @@ -1,30 +1,45 @@ """ - The GeometryProxy object, allows for lazy-geometries. + The GeometryProxy object, allows for lazy-geometries. The proxy uses + Python descriptors for instantiating and setting GEOS Geometry objects + corresponding to geographic model fields. Thanks to Robert Coup for providing this functionality (see #4322). """ -# GEOS Routines +from types import NoneType, StringType, UnicodeType from django.contrib.gis.geos import GEOSGeometry, GEOSException -# TODO: docstrings & comments +# TODO: docstrings class GeometryProxy(object): def __init__(self, field): + "Proxy initializes on the given GeometryField." self._field = field def __get__(self, obj, type=None): + # Getting the value of the field. geom_value = obj.__dict__[self._field.attname] + if (geom_value is None) or (isinstance(geom_value, GEOSGeometry)): + # If the value of the field is None, or is already a GEOS Geometry + # no more work is needed. geom = geom_value else: - geom = GEOSGeometry(geom_value) + # Otherwise, a GEOSGeometry object is built using the field's contents, + # and the model's corresponding attribute is set. + geom = GEOSGeometry(geom_value) setattr(obj, self._field.attname, geom) return geom def __set__(self, obj, value): - if isinstance(value, GEOSGeometry): - if value and ((value.srid is None) and (self._field._srid is not None)): - value.srid = self._field._srid - + if isinstance(value, GEOSGeometry) and (value.geom_type.upper() == self._field._geom): + # Getting set with GEOS Geometry; geom_type must match that of the field. + + # If value's SRID is not set, setting it to the field's SRID. + if value.srid is None: value.srid = self._field._srid + elif isinstance(value, (NoneType, StringType, UnicodeType)): + # Getting set with None, WKT, or HEX + pass + else: + raise TypeError, 'cannot set %s GeometryProxy with value of type: %s' % (self._field._geom, type(value)) obj.__dict__[self._field.attname] = value return value diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index dab1f04eb6..6bf51fc231 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,9 +1,10 @@ -from django.db.models.query import Q, QuerySet +import operator +from django.core.exceptions import ImproperlyConfigured from django.db import backend +from django.db.models.query import Q, QuerySet +from django.db.models.fields import FieldDoesNotExist from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.backend import parse_lookup # parse_lookup depends on the spatial database backend. -from django.db.models.fields import FieldDoesNotExist -import operator class GeoQ(Q): "Geographical query encapsulation object." @@ -37,10 +38,21 @@ class GeoQuerySet(QuerySet): clone._filters = clone._filters & reduce(operator.and_, map(mapper, args)) return clone - def kml(self, field_name): - field = self.model._meta.get_field(field_name) + def kml(self, field_name, precision=8): + """Returns KML representation of the given field name in a `kml` + attribute on each element of the QuerySet.""" + # Is KML output supported? + try: + from django.contrib.gis.db.backend.postgis import ASKML + except ImportError: + raise ImproperlyConfigured, 'AsKML() only available in PostGIS versions 1.2.1 and greater.' + # Is the given field name a geographic field? + field = self.model._meta.get_field(field_name) + if not isinstance(field, GeometryField): + raise TypeError, 'KML output only available on GeometryField fields.' field_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table), - backend.quote_name(field.column)) + backend.quote_name(field.column)) - return self.extra(select={'kml':'AsKML(%s,6)' % field_col}) + # Adding the AsKML function call to the SELECT part of the SQL. + return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)}) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index dcc5e681ff..f416b5b355 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -1,10 +1,10 @@ import unittest from models import Country, City, State -from django.contrib.gis.geos import fromstr +from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon class GeoModelTest(unittest.TestCase): - def test001_initial_sql(self): + def test01_initial_sql(self): "Testing geographic initial SQL." # Ensuring that data was loaded from initial SQL. @@ -12,7 +12,80 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(8, City.objects.count()) self.assertEqual(3, State.objects.count()) - def test002_contains_contained(self): + def test02_proxy(self): + "Testing Lazy-Geometry support (using the GeometryProxy)." + #### Testing on a Point + pnt = Point(0, 0) + nullcity = City(name='NullCity', point=pnt) + nullcity.save() + + # Making sure TypeError is thrown when trying to set with an + # incompatible type. + for bad in [5, 2.0, LineString((0, 0), (1, 1))]: + try: + nullcity.point = bad + except TypeError: + pass + else: + self.fail('Should throw a TypeError') + + # Now setting with a compatible GEOS Geometry, saving, and ensuring + # the save took, notice no SRID is explicitly set. + new = Point(5, 23) + nullcity.point = new + + # Ensuring that the SRID is automatically set to that of the + # field after assignment, but before saving. + self.assertEqual(4326, nullcity.point.srid) + nullcity.save() + + # Ensuring the point was saved correctly after saving + self.assertEqual(new, City.objects.get(name='NullCity').point) + + # Setting the X and Y of the Point + nullcity.point.x = 23 + nullcity.point.y = 5 + # Checking assignments pre & post-save. + self.assertNotEqual(Point(23, 5), City.objects.get(name='NullCity').point) + nullcity.save() + self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point) + nullcity.delete() + + #### Testing on a Polygon + shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0)) + inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40)) + + # Creating a State object using a built Polygon + ply = Polygon(shell.clone(), inner.clone()) + nullstate = State(name='NullState', poly=ply) + self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None + nullstate.save() + self.assertEqual(ply, State.objects.get(name='NullState').poly) + + # Changing the interior ring on the poly attribute. + new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30)) + nullstate.poly[1] = new_inner.clone() + ply[1] = new_inner + self.assertEqual(4326, nullstate.poly.srid) + nullstate.save() + self.assertEqual(ply, State.objects.get(name='NullState').poly) + nullstate.delete() + + def test03_kml(self): + "Testing KML output from the database using GeoManager.kml()." + # Should throw an error trying to get KML from a non-geometry field. + try: + qs = City.objects.all().kml('name') + except TypeError: + pass + else: + self.fail('Expected a TypeError exception') + + # Ensuring the KML is as expected. + ptown = City.objects.kml('point', precision=9).get(name='Pueblo') + self.assertEqual('-104.609252,38.255001,0', ptown.kml) + + def test10_contains_contained(self): "Testing the 'contained' and 'contains' lookup types." # Getting Texas, yes we were a country -- once ;) @@ -47,7 +120,7 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(0, len(Country.objects.filter(mpoly__contains=pueblo.point))) # Query w/GEOSGeometry object self.assertEqual(0, len(Country.objects.filter(mpoly__contains=okcity.point.wkt))) # Qeury w/WKT - def test003_lookup_insert_transform(self): + def test11_lookup_insert_transform(self): "Testing automatic transform for lookups and inserts." # San Antonio in 'WGS84' (SRID 4326) and 'NAD83(HARN) / Texas Centric Lambert Conformal' (SRID 3084) @@ -69,7 +142,7 @@ class GeoModelTest(unittest.TestCase): self.assertAlmostEqual(wgs_pnt.x, sa.point.x, 6) self.assertAlmostEqual(wgs_pnt.y, sa.point.y, 6) - def test004_null_geometries(self): + def test12_null_geometries(self): "Testing NULL geometry support." # Querying for both NULL and Non-NULL values. @@ -90,7 +163,7 @@ class GeoModelTest(unittest.TestCase): nmi = State(name='Northern Mariana Islands', poly=None) nmi.save() - def test005_left_right(self): + def test13_left_right(self): "Testing the 'left' and 'right' lookup types." # Left: A << B => true if xmax(A) < xmin(B) diff --git a/django/contrib/gis/utils/LayerMapping.py b/django/contrib/gis/utils/LayerMapping.py index 07708b3b52..facb4975b3 100644 --- a/django/contrib/gis/utils/LayerMapping.py +++ b/django/contrib/gis/utils/LayerMapping.py @@ -268,7 +268,7 @@ class LayerMapping: # Creating the CoordTransform object ct = CoordTransform(self.source_srs, target_srs) except Exception, msg: - raise Exception, 'Could not translate between the data source and model geometry.' + raise Exception, 'Could not translate between the data source and model geometry: %s' % msg for feat in self.layer: # The keyword arguments for model construction