Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #11433 -- 3D geometry fields are now supported with PostGIS; EW…

…KB is now used by `PostGISAdaptor`.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11742 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 32d0730b8965b272368763d75803fa0472d97177 1 parent 6c61ca3
Justin Bronn authored November 16, 2009
3  django/contrib/gis/db/backend/postgis/__init__.py
@@ -18,18 +18,21 @@
18 18
                                     distance_spheroid=DISTANCE_SPHEROID,
19 19
                                     envelope=ENVELOPE,
20 20
                                     extent=EXTENT,
  21
+                                    extent3d=EXTENT3D,
21 22
                                     gis_terms=POSTGIS_TERMS,
22 23
                                     geojson=ASGEOJSON,
23 24
                                     gml=ASGML,
24 25
                                     intersection=INTERSECTION,
25 26
                                     kml=ASKML,
26 27
                                     length=LENGTH,
  28
+                                    length3d=LENGTH3D,
27 29
                                     length_spheroid=LENGTH_SPHEROID,
28 30
                                     make_line=MAKE_LINE,
29 31
                                     mem_size=MEM_SIZE,
30 32
                                     num_geom=NUM_GEOM,
31 33
                                     num_points=NUM_POINTS,
32 34
                                     perimeter=PERIMETER,
  35
+                                    perimeter3d=PERIMETER3D,
33 36
                                     point_on_surface=POINT_ON_SURFACE,
34 37
                                     scale=SCALE,
35 38
                                     select=GEOM_SELECT,
6  django/contrib/gis/db/backend/postgis/adaptor.py
@@ -2,7 +2,7 @@
2 2
  This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
3 3
 """
4 4
 
5  
-from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB
  5
+from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_EWKB
6 6
 from psycopg2 import Binary
7 7
 from psycopg2.extensions import ISQLQuote
8 8
 
@@ -11,7 +11,7 @@ def __init__(self, geom):
11 11
         "Initializes on the geometry."
12 12
         # Getting the WKB (in string form, to allow easy pickling of
13 13
         # the adaptor) and the SRID from the geometry.
14  
-        self.wkb = str(geom.wkb)
  14
+        self.ewkb = str(geom.ewkb)
15 15
         self.srid = geom.srid
16 16
 
17 17
     def __conform__(self, proto):
@@ -30,7 +30,7 @@ def __str__(self):
30 30
     def getquoted(self):
31 31
         "Returns a properly quoted string for use in PostgreSQL/PostGIS."
32 32
         # Want to use WKB, so wrap with psycopg2 Binary() to quote properly.
33  
-        return "%s(E%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1)
  33
+        return "%s(E%s)" % (GEOM_FROM_EWKB, Binary(self.ewkb))
34 34
 
35 35
     def prepare_database_save(self, unused):
36 36
         return self
6  django/contrib/gis/db/backend/postgis/query.py
@@ -63,17 +63,21 @@ def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
63 63
     DISTANCE_SPHERE = get_func('distance_sphere')
64 64
     DISTANCE_SPHEROID = get_func('distance_spheroid')
65 65
     ENVELOPE = get_func('Envelope')
66  
-    EXTENT = get_func('extent')
  66
+    EXTENT = get_func('Extent')
  67
+    EXTENT3D = get_func('Extent3D')
67 68
     GEOM_FROM_TEXT = get_func('GeomFromText')
  69
+    GEOM_FROM_EWKB = get_func('GeomFromEWKB')
68 70
     GEOM_FROM_WKB = get_func('GeomFromWKB')
69 71
     INTERSECTION = get_func('Intersection')
70 72
     LENGTH = get_func('Length')
  73
+    LENGTH3D = get_func('Length3D')
71 74
     LENGTH_SPHEROID = get_func('length_spheroid')
72 75
     MAKE_LINE = get_func('MakeLine')
73 76
     MEM_SIZE = get_func('mem_size')
74 77
     NUM_GEOM = get_func('NumGeometries')
75 78
     NUM_POINTS = get_func('npoints')
76 79
     PERIMETER = get_func('Perimeter')
  80
+    PERIMETER3D = get_func('Perimeter3D')
77 81
     POINT_ON_SURFACE = get_func('PointOnSurface')
78 82
     SCALE = get_func('Scale')
79 83
     SNAP_TO_GRID = get_func('SnapToGrid')
3  django/contrib/gis/db/models/aggregates.py
@@ -24,6 +24,9 @@ class Collect(GeoAggregate):
24 24
 class Extent(GeoAggregate):
25 25
     name = 'Extent'
26 26
 
  27
+class Extent3D(GeoAggregate):
  28
+    name = 'Extent3D'
  29
+
27 30
 class MakeLine(GeoAggregate):
28 31
     name = 'MakeLine'
29 32
 
3  django/contrib/gis/db/models/manager.py
@@ -34,6 +34,9 @@ def envelope(self, *args, **kwargs):
34 34
     def extent(self, *args, **kwargs):
35 35
         return self.get_query_set().extent(*args, **kwargs)
36 36
 
  37
+    def extent3d(self, *args, **kwargs):
  38
+        return self.get_query_set().extent3d(*args, **kwargs)
  39
+
37 40
     def geojson(self, *args, **kwargs):
38 41
         return self.get_query_set().geojson(*args, **kwargs)
39 42
 
23  django/contrib/gis/db/models/query.py
@@ -110,6 +110,14 @@ def extent(self, **kwargs):
110 110
         """
111 111
         return self._spatial_aggregate(aggregates.Extent, **kwargs)
112 112
 
  113
+    def extent3d(self, **kwargs):
  114
+        """
  115
+        Returns the aggregate extent, in 3D, of the features in the
  116
+        GeoQuerySet. It is returned as a 6-tuple, comprising:
  117
+          (xmin, ymin, zmin, xmax, ymax, zmax).
  118
+        """
  119
+        return self._spatial_aggregate(aggregates.Extent3D, **kwargs)
  120
+
113 121
     def geojson(self, precision=8, crs=False, bbox=False, **kwargs):
114 122
         """
115 123
         Returns a GeoJSON representation of the geomtry field in a `geojson`
@@ -524,12 +532,14 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
524 532
         else:
525 533
             dist_att = Distance.unit_attname(geo_field.units_name)
526 534
 
527  
-        # Shortcut booleans for what distance function we're using.
  535
+        # Shortcut booleans for what distance function we're using and
  536
+        # whether the geometry field is 3D.
528 537
         distance = func == 'distance'
529 538
         length = func == 'length'
530 539
         perimeter = func == 'perimeter'
531 540
         if not (distance or length or perimeter):
532 541
             raise ValueError('Unknown distance function: %s' % func)
  542
+        geom_3d = geo_field.dim == 3
533 543
 
534 544
         # The field's get_db_prep_lookup() is used to get any
535 545
         # extra distance parameters.  Here we set up the
@@ -604,7 +614,7 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
604 614
                     # some error checking is required.
605 615
                     if not isinstance(geo_field, PointField):
606 616
                         raise ValueError('Spherical distance calculation only supported on PointFields.')
607  
-                    if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
  617
+                    if not str(SpatialBackend.Geometry(buffer(params[0].ewkb)).geom_type) == 'Point':
608 618
                         raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
609 619
                     # The `function` procedure argument needs to be set differently for
610 620
                     # geodetic distance calculations.
@@ -617,9 +627,16 @@ def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, *
617 627
             elif length or perimeter:
618 628
                 procedure_fmt = '%(geo_col)s'
619 629
                 if geodetic and length:
620  
-                    # There's no `length_sphere`
  630
+                    # There's no `length_sphere`, and `length_spheroid` also
  631
+                    # works on 3D geometries.
621 632
                     procedure_fmt += ',%(spheroid)s'
622 633
                     procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
  634
+                elif geom_3d and SpatialBackend.postgis:
  635
+                    # Use 3D variants of perimeter and length routines on PostGIS.
  636
+                    if perimeter:
  637
+                        procedure_args.update({'function' : SpatialBackend.perimeter3d})
  638
+                    elif length:
  639
+                        procedure_args.update({'function' : SpatialBackend.length3d})
623 640
 
624 641
         # Setting up the settings for `_spatial_attribute`.
625 642
         s = {'select_field' : DistanceField(dist_att),
17  django/contrib/gis/db/models/sql/aggregates.py
@@ -11,6 +11,9 @@
11 11
 def convert_extent(box):
12 12
     raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
13 13
 
  14
+def convert_extent3d(box):
  15
+    raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.')
  16
+
14 17
 def convert_geom(wkt, geo_field):
15 18
     raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
16 19
 
@@ -23,6 +26,14 @@ def convert_extent(box):
23 26
         xmax, ymax = map(float, ur.split())
24 27
         return (xmin, ymin, xmax, ymax)
25 28
 
  29
+    def convert_extent3d(box3d):
  30
+        # Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)";
  31
+        # parsing out and returning as a 4-tuple.
  32
+        ll, ur = box3d[6:-1].split(',')
  33
+        xmin, ymin, zmin = map(float, ll.split())
  34
+        xmax, ymax, zmax = map(float, ur.split())
  35
+        return (xmin, ymin, zmin, xmax, ymax, zmax)
  36
+
26 37
     def convert_geom(hex, geo_field):
27 38
         if hex: return SpatialBackend.Geometry(hex)
28 39
         else: return None
@@ -94,7 +105,7 @@ class Collect(GeoAggregate):
94 105
     sql_function = SpatialBackend.collect
95 106
 
96 107
 class Extent(GeoAggregate):
97  
-    is_extent = True
  108
+    is_extent = '2D'
98 109
     sql_function = SpatialBackend.extent
99 110
 
100 111
 if SpatialBackend.oracle:
@@ -102,6 +113,10 @@ class Extent(GeoAggregate):
102 113
     Extent.conversion_class = GeomField
103 114
     Extent.sql_template = '%(function)s(%(field)s)'
104 115
 
  116
+class Extent3D(GeoAggregate):
  117
+    is_extent = '3D'
  118
+    sql_function = SpatialBackend.extent3d
  119
+
105 120
 class MakeLine(GeoAggregate):
106 121
     conversion_class = GeomField
107 122
     sql_function = SpatialBackend.make_line
5  django/contrib/gis/db/models/sql/query.py
@@ -262,7 +262,10 @@ def resolve_aggregate(self, value, aggregate):
262 262
         """
263 263
         if isinstance(aggregate, self.aggregates_module.GeoAggregate):
264 264
             if aggregate.is_extent:
265  
-                return self.aggregates_module.convert_extent(value)
  265
+                if aggregate.is_extent == '3D':
  266
+                    return self.aggregates_module.convert_extent3d(value)
  267
+                else:
  268
+                    return self.aggregates_module.convert_extent(value)
266 269
             else:
267 270
                 return self.aggregates_module.convert_geom(value, aggregate.source)
268 271
         else:
13  django/contrib/gis/geos/geometry.py
@@ -373,9 +373,10 @@ def wkt(self):
373 373
     @property
374 374
     def hex(self):
375 375
         """
376  
-        Returns the HEX of the Geometry -- please note that the SRID is not
377  
-        included in this representation, because it is not a part of the
378  
-        OGC specification (use the `hexewkb` property instead).
  376
+        Returns the WKB of this Geometry in hexadecimal form.  Please note
  377
+        that the SRID and Z values are not included in this representation
  378
+        because it is not a part of the OGC specification (use the `hexewkb` 
  379
+        property instead).
379 380
         """
380 381
         # A possible faster, all-python, implementation:
381 382
         #  str(self.wkb).encode('hex')
@@ -384,9 +385,9 @@ def hex(self):
384 385
     @property
385 386
     def hexewkb(self):
386 387
         """
387  
-        Returns the HEXEWKB of this Geometry.  This is an extension of the WKB
388  
-        specification that includes SRID and Z values taht are a part of this
389  
-        geometry.
  388
+        Returns the EWKB of this Geometry in hexadecimal form.  This is an 
  389
+        extension of the WKB specification that includes SRID and Z values 
  390
+        that are a part of this geometry.
390 391
         """
391 392
         if self.hasz:
392 393
             if not GEOS_PREPARE:
7  django/contrib/gis/tests/__init__.py
@@ -9,9 +9,10 @@ def geo_suite():
9 9
     some backends).
10 10
     """
11 11
     from django.conf import settings
  12
+    from django.contrib.gis.geos import GEOS_PREPARE
12 13
     from django.contrib.gis.gdal import HAS_GDAL
13 14
     from django.contrib.gis.utils import HAS_GEOIP
14  
-    from django.contrib.gis.tests.utils import mysql
  15
+    from django.contrib.gis.tests.utils import postgis, mysql
15 16
 
16 17
     # The test suite.
17 18
     s = unittest.TestSuite()
@@ -32,6 +33,10 @@ def geo_suite():
32 33
     if not mysql:
33 34
         test_apps.append('distapp')
34 35
 
  36
+    # Only PostGIS using GEOS 3.1+ can support 3D so far.
  37
+    if postgis and GEOS_PREPARE:
  38
+        test_apps.append('geo3d')
  39
+
35 40
     if HAS_GDAL:
36 41
         # These tests require GDAL.
37 42
         test_suite_names.extend(['test_spatialrefsys', 'test_geoforms'])
69  django/contrib/gis/tests/geo3d/models.py
... ...
@@ -0,0 +1,69 @@
  1
+from django.contrib.gis.db import models
  2
+
  3
+class City3D(models.Model):
  4
+    name = models.CharField(max_length=30)
  5
+    point = models.PointField(dim=3)
  6
+    objects = models.GeoManager()
  7
+
  8
+    def __unicode__(self):
  9
+        return self.name
  10
+
  11
+class Interstate2D(models.Model):
  12
+    name = models.CharField(max_length=30)
  13
+    line = models.LineStringField(srid=4269)
  14
+    objects = models.GeoManager()
  15
+
  16
+    def __unicode__(self):
  17
+        return self.name
  18
+
  19
+class Interstate3D(models.Model):
  20
+    name = models.CharField(max_length=30)
  21
+    line = models.LineStringField(dim=3, srid=4269)
  22
+    objects = models.GeoManager()
  23
+
  24
+    def __unicode__(self):
  25
+        return self.name
  26
+
  27
+class InterstateProj2D(models.Model):
  28
+    name = models.CharField(max_length=30)
  29
+    line = models.LineStringField(srid=32140)
  30
+    objects = models.GeoManager()
  31
+
  32
+    def __unicode__(self):
  33
+        return self.name
  34
+
  35
+class InterstateProj3D(models.Model):
  36
+    name = models.CharField(max_length=30)
  37
+    line = models.LineStringField(dim=3, srid=32140)
  38
+    objects = models.GeoManager()
  39
+
  40
+    def __unicode__(self):
  41
+        return self.name
  42
+
  43
+class Polygon2D(models.Model):
  44
+    name = models.CharField(max_length=30)
  45
+    poly = models.PolygonField(srid=32140)
  46
+    objects = models.GeoManager()
  47
+    
  48
+    def __unicode__(self):
  49
+        return self.name
  50
+
  51
+class Polygon3D(models.Model):
  52
+    name = models.CharField(max_length=30)
  53
+    poly = models.PolygonField(dim=3, srid=32140)
  54
+    objects = models.GeoManager()
  55
+    
  56
+    def __unicode__(self):
  57
+        return self.name
  58
+
  59
+class Point2D(models.Model):
  60
+    point = models.PointField()
  61
+    objects = models.GeoManager()
  62
+
  63
+class Point3D(models.Model):
  64
+    point = models.PointField(dim=3)
  65
+    objects = models.GeoManager()
  66
+
  67
+class MultiPoint3D(models.Model):
  68
+    mpoint = models.MultiPointField(dim=3)
  69
+    objects = models.GeoManager()
234  django/contrib/gis/tests/geo3d/tests.py
... ...
@@ -0,0 +1,234 @@
  1
+import os, re, unittest
  2
+from django.contrib.gis.db.models import Union, Extent3D
  3
+from django.contrib.gis.geos import GEOSGeometry, Point, Polygon
  4
+from django.contrib.gis.utils import LayerMapping, LayerMapError
  5
+
  6
+from models import City3D, Interstate2D, Interstate3D, \
  7
+    InterstateProj2D, InterstateProj3D, \
  8
+    Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D
  9
+
  10
+data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data'))
  11
+city_file = os.path.join(data_path, 'cities', 'cities.shp')
  12
+vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt')
  13
+
  14
+# The coordinates of each city, with Z values corresponding to their
  15
+# altitude in meters.
  16
+city_data = (
  17
+    ('Houston', (-95.363151, 29.763374, 18)),
  18
+    ('Dallas', (-96.801611, 32.782057, 147)),
  19
+    ('Oklahoma City', (-97.521157, 34.464642, 380)),
  20
+    ('Wellington', (174.783117, -41.315268, 14)),
  21
+    ('Pueblo', (-104.609252, 38.255001, 1433)),
  22
+    ('Lawrence', (-95.235060, 38.971823, 251)),
  23
+    ('Chicago', (-87.650175, 41.850385, 181)),
  24
+    ('Victoria', (-123.305196, 48.462611, 15)),
  25
+)
  26
+
  27
+# Reference mapping of city name to its altitude (Z value).
  28
+city_dict = dict((name, coords) for name, coords in city_data)
  29
+
  30
+# 3D freeway data derived from the National Elevation Dataset: 
  31
+#  http://seamless.usgs.gov/products/9arc.php
  32
+interstate_data = (
  33
+    ('I-45', 
  34
+     'LINESTRING(-95.3708481 29.7765870 11.339,-95.3694580 29.7787980 4.536,-95.3690305 29.7797359 9.762,-95.3691886 29.7812450 12.448,-95.3696447 29.7850144 10.457,-95.3702511 29.7868518 9.418,-95.3706724 29.7881286 14.858,-95.3711632 29.7896157 15.386,-95.3714525 29.7936267 13.168,-95.3717848 29.7955007 15.104,-95.3717719 29.7969804 16.516,-95.3717305 29.7982117 13.923,-95.3717254 29.8000778 14.385,-95.3719875 29.8013539 15.160,-95.3720575 29.8026785 15.544,-95.3721321 29.8040912 14.975,-95.3722074 29.8050998 15.688,-95.3722779 29.8060430 16.099,-95.3733818 29.8076750 15.197,-95.3741563 29.8103686 17.268,-95.3749458 29.8129927 19.857,-95.3763564 29.8144557 15.435)',
  35
+     ( 11.339,   4.536,   9.762,  12.448,  10.457,   9.418,  14.858,
  36
+       15.386,  13.168,  15.104,  16.516,  13.923,  14.385,  15.16 ,
  37
+       15.544,  14.975,  15.688,  16.099,  15.197,  17.268,  19.857,
  38
+       15.435),
  39
+     ),
  40
+    )
  41
+
  42
+# Bounding box polygon for inner-loop of Houston (in projected coordinate
  43
+# system 32140), with elevation values from the National Elevation Dataset
  44
+# (see above).
  45
+bbox_wkt = 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))'
  46
+bbox_z = (21.71, 13.21, 9.12, 16.40, 21.71)
  47
+def gen_bbox():
  48
+    bbox_2d = GEOSGeometry(bbox_wkt, srid=32140)
  49
+    bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140)    
  50
+    return bbox_2d, bbox_3d
  51
+
  52
+class Geo3DTest(unittest.TestCase):
  53
+    """
  54
+    Only a subset of the PostGIS routines are 3D-enabled, and this TestCase
  55
+    tries to test the features that can handle 3D and that are also 
  56
+    available within GeoDjango.  For more information, see the PostGIS docs
  57
+    on the routines that support 3D:
  58
+
  59
+    http://postgis.refractions.net/documentation/manual-1.4/ch08.html#PostGIS_3D_Functions
  60
+    """
  61
+
  62
+    def test01_3d(self):
  63
+        "Test the creation of 3D models."
  64
+        # 3D models for the rest of the tests will be populated in here.
  65
+        # For each 3D data set create model (and 2D version if necessary), 
  66
+        # retrieve, and assert geometry is in 3D and contains the expected
  67
+        # 3D values.
  68
+        for name, pnt_data in city_data:
  69
+            x, y, z = pnt_data
  70
+            pnt = Point(x, y, z, srid=4326)
  71
+            City3D.objects.create(name=name, point=pnt)
  72
+            city = City3D.objects.get(name=name)
  73
+            self.failUnless(city.point.hasz)
  74
+            self.assertEqual(z, city.point.z)
  75
+
  76
+        # Interstate (2D / 3D and Geographic/Projected variants)
  77
+        for name, line, exp_z in interstate_data:
  78
+            line_3d = GEOSGeometry(line, srid=4269)
  79
+            # Using `hex` attribute because it omits 3D.
  80
+            line_2d = GEOSGeometry(line_3d.hex, srid=4269)
  81
+
  82
+            # Creating a geographic and projected version of the
  83
+            # interstate in both 2D and 3D.
  84
+            Interstate3D.objects.create(name=name, line=line_3d)
  85
+            InterstateProj3D.objects.create(name=name, line=line_3d)
  86
+            Interstate2D.objects.create(name=name, line=line_2d)
  87
+            InterstateProj2D.objects.create(name=name, line=line_2d)
  88
+
  89
+            # Retrieving and making sure it's 3D and has expected
  90
+            # Z values -- shouldn't change because of coordinate system.
  91
+            interstate = Interstate3D.objects.get(name=name)
  92
+            interstate_proj = InterstateProj3D.objects.get(name=name)
  93
+            for i in [interstate, interstate_proj]:
  94
+                self.failUnless(i.line.hasz)
  95
+                self.assertEqual(exp_z, tuple(i.line.z))
  96
+
  97
+        # Creating 3D Polygon.
  98
+        bbox2d, bbox3d = gen_bbox()
  99
+        Polygon2D.objects.create(name='2D BBox', poly=bbox2d)
  100
+        Polygon3D.objects.create(name='3D BBox', poly=bbox3d)
  101
+        p3d = Polygon3D.objects.get(name='3D BBox')
  102
+        self.failUnless(p3d.poly.hasz)
  103
+        self.assertEqual(bbox3d, p3d.poly)
  104
+
  105
+    def test01a_3d_layermapping(self):
  106
+        "Testing LayerMapping on 3D models."
  107
+        from models import Point2D, Point3D
  108
+
  109
+        point_mapping = {'point' : 'POINT'}
  110
+        mpoint_mapping = {'mpoint' : 'MULTIPOINT'}
  111
+
  112
+        # The VRT is 3D, but should still be able to map sans the Z.
  113
+        lm = LayerMapping(Point2D, vrt_file, point_mapping, transform=False)
  114
+        lm.save()
  115
+        self.assertEqual(3, Point2D.objects.count())
  116
+
  117
+        # The city shapefile is 2D, and won't be able to fill the coordinates
  118
+        # in the 3D model -- thus, a LayerMapError is raised.
  119
+        self.assertRaises(LayerMapError, LayerMapping,
  120
+                          Point3D, city_file, point_mapping, transform=False)
  121
+        
  122
+        # 3D model should take 3D data just fine.
  123
+        lm = LayerMapping(Point3D, vrt_file, point_mapping, transform=False)
  124
+        lm.save()
  125
+        self.assertEqual(3, Point3D.objects.count())
  126
+
  127
+        # Making sure LayerMapping.make_multi works right, by converting
  128
+        # a Point25D into a MultiPoint25D.
  129
+        lm = LayerMapping(MultiPoint3D, vrt_file, mpoint_mapping, transform=False)
  130
+        lm.save()
  131
+        self.assertEqual(3, MultiPoint3D.objects.count())
  132
+
  133
+    def test02a_kml(self):
  134
+        "Test GeoQuerySet.kml() with Z values."
  135
+        h = City3D.objects.kml(precision=6).get(name='Houston')
  136
+        # KML should be 3D.
  137
+        # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';`
  138
+        ref_kml_regex = re.compile(r'^<Point><coordinates>-95.363\d+,29.763\d+,18</coordinates></Point>$')
  139
+        self.failUnless(ref_kml_regex.match(h.kml))
  140
+
  141
+    def test02b_geojson(self):
  142
+        "Test GeoQuerySet.geojson() with Z values."
  143
+        h = City3D.objects.geojson(precision=6).get(name='Houston')
  144
+        # GeoJSON should be 3D
  145
+        # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';`
  146
+        ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$')
  147
+        self.failUnless(ref_json_regex.match(h.geojson))
  148
+
  149
+    def test03a_union(self):
  150
+        "Testing the Union aggregate of 3D models."
  151
+        # PostGIS query that returned the reference EWKT for this test:
  152
+        #  `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;`
  153
+        ref_ewkt = 'SRID=4326;MULTIPOINT(-123.305196 48.462611 15,-104.609252 38.255001 1433,-97.521157 34.464642 380,-96.801611 32.782057 147,-95.363151 29.763374 18,-95.23506 38.971823 251,-87.650175 41.850385 181,174.783117 -41.315268 14)'
  154
+        ref_union = GEOSGeometry(ref_ewkt)
  155
+        union = City3D.objects.aggregate(Union('point'))['point__union']
  156
+        self.failUnless(union.hasz)
  157
+        self.assertEqual(ref_union, union)
  158
+
  159
+    def test03b_extent(self):
  160
+        "Testing the Extent3D aggregate for 3D models."
  161
+        # `SELECT ST_Extent3D(point) FROM geo3d_city3d;`
  162
+        ref_extent3d = (-123.305196, -41.315268, 14,174.783117, 48.462611, 1433)
  163
+        extent1 = City3D.objects.aggregate(Extent3D('point'))['point__extent3d']
  164
+        extent2 = City3D.objects.extent3d()
  165
+
  166
+        def check_extent3d(extent3d, tol=6):
  167
+            for ref_val, ext_val in zip(ref_extent3d, extent3d):
  168
+                self.assertAlmostEqual(ref_val, ext_val, tol)
  169
+
  170
+        for e3d in [extent1, extent2]:
  171
+            check_extent3d(e3d)
  172
+
  173
+    def test04_perimeter(self):
  174
+        "Testing GeoQuerySet.perimeter() on 3D fields."
  175
+        # Reference query for values below:
  176
+        #  `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;`
  177
+        ref_perim_3d = 76859.2620451
  178
+        ref_perim_2d = 76859.2577803
  179
+        tol = 6
  180
+        self.assertAlmostEqual(ref_perim_2d,
  181
+                               Polygon2D.objects.perimeter().get(name='2D BBox').perimeter.m,
  182
+                               tol)
  183
+        self.assertAlmostEqual(ref_perim_3d,
  184
+                               Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m,
  185
+                               tol)
  186
+
  187
+    def test05_length(self):
  188
+        "Testing GeoQuerySet.length() on 3D fields."
  189
+        # ST_Length_Spheroid Z-aware, and thus does not need to use
  190
+        # a separate function internally.
  191
+        # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]') 
  192
+        #    FROM geo3d_interstate[2d|3d];`
  193
+        tol = 3
  194
+        ref_length_2d = 4368.1721949481
  195
+        ref_length_3d = 4368.62547052088
  196
+        self.assertAlmostEqual(ref_length_2d,
  197
+                               Interstate2D.objects.length().get(name='I-45').length.m,
  198
+                               tol)
  199
+        self.assertAlmostEqual(ref_length_3d,
  200
+                               Interstate3D.objects.length().get(name='I-45').length.m,
  201
+                               tol)
  202
+
  203
+        # Making sure `ST_Length3D` is used on for a projected
  204
+        # and 3D model rather than `ST_Length`.
  205
+        # `SELECT ST_Length(line) FROM geo3d_interstateproj2d;`
  206
+        ref_length_2d = 4367.71564892392
  207
+        # `SELECT ST_Length3D(line) FROM geo3d_interstateproj3d;`
  208
+        ref_length_3d = 4368.16897234101
  209
+        self.assertAlmostEqual(ref_length_2d,
  210
+                               InterstateProj2D.objects.length().get(name='I-45').length.m,
  211
+                               tol)
  212
+        self.assertAlmostEqual(ref_length_3d,
  213
+                               InterstateProj3D.objects.length().get(name='I-45').length.m,
  214
+                               tol)
  215
+        
  216
+    def test06_scale(self):
  217
+        "Testing GeoQuerySet.scale() on Z values."
  218
+        # Mapping of City name to reference Z values.
  219
+        zscales = (-3, 4, 23)
  220
+        for zscale in zscales:
  221
+            for city in City3D.objects.scale(1.0, 1.0, zscale):
  222
+                self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z)
  223
+
  224
+    def test07_translate(self):
  225
+        "Testing GeoQuerySet.translate() on Z values."
  226
+        ztranslations = (5.23, 23, -17)
  227
+        for ztrans in ztranslations:
  228
+            for city in City3D.objects.translate(0, 0, ztrans):
  229
+                self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)
  230
+
  231
+def suite():
  232
+    s = unittest.TestSuite()
  233
+    s.addTest(unittest.makeSuite(Geo3DTest))
  234
+    return s
1  django/contrib/gis/tests/geo3d/views.py
... ...
@@ -0,0 +1 @@
  1
+# Create your views here.
2  django/contrib/gis/utils/__init__.py
@@ -10,7 +10,7 @@
10 10
     try:
11 11
         # LayerMapping requires DJANGO_SETTINGS_MODULE to be set, 
12 12
         # so this needs to be in try/except.
13  
-        from django.contrib.gis.utils.layermapping import LayerMapping
  13
+        from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError
14 14
     except:
15 15
         pass
16 16
     
24  django/contrib/gis/utils/layermapping.py
@@ -133,6 +133,9 @@ class LayerMapping(object):
133 133
     MULTI_TYPES = {1 : OGRGeomType('MultiPoint'),
134 134
                    2 : OGRGeomType('MultiLineString'),
135 135
                    3 : OGRGeomType('MultiPolygon'),
  136
+                   OGRGeomType('Point25D').num : OGRGeomType('MultiPoint25D'),
  137
+                   OGRGeomType('LineString25D').num : OGRGeomType('MultiLineString25D'),
  138
+                   OGRGeomType('Polygon25D').num : OGRGeomType('MultiPolygon25D'),
136 139
                    }
137 140
 
138 141
     # Acceptable Django field types and corresponding acceptable OGR
@@ -282,19 +285,28 @@ def check_ogr_fld(ogr_map_fld):
282 285
                 if self.geom_field:
283 286
                     raise LayerMapError('LayerMapping does not support more than one GeometryField per model.')
284 287
 
  288
+                # Getting the coordinate dimension of the geometry field.
  289
+                coord_dim = model_field.dim
  290
+
285 291
                 try:
286  
-                    gtype = OGRGeomType(ogr_name)
  292
+                    if coord_dim == 3:
  293
+                        gtype = OGRGeomType(ogr_name + '25D')
  294
+                    else:
  295
+                        gtype = OGRGeomType(ogr_name)
287 296
                 except OGRException:
288 297
                     raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name)
289 298
 
290 299
                 # Making sure that the OGR Layer's Geometry is compatible.
291 300
                 ltype = self.layer.geom_type
292  
-                if not (gtype == ltype or self.make_multi(ltype, model_field)):
293  
-                    raise LayerMapError('Invalid mapping geometry; model has %s, feature has %s.' % (fld_name, gtype))
  301
+                if not (ltype.name.startswith(gtype.name) or self.make_multi(ltype, model_field)):
  302
+                    raise LayerMapError('Invalid mapping geometry; model has %s%s, layer is %s.' % 
  303
+                                        (fld_name, (coord_dim == 3 and '(dim=3)') or '', ltype))
294 304
 
295 305
                 # Setting the `geom_field` attribute w/the name of the model field
296  
-                # that is a Geometry.
  306
+                # that is a Geometry.  Also setting the coordinate dimension
  307
+                # attribute.
297 308
                 self.geom_field = field_name
  309
+                self.coord_dim = coord_dim
298 310
                 fields_val = model_field
299 311
             elif isinstance(model_field, models.ForeignKey):
300 312
                 if isinstance(ogr_name, dict):
@@ -482,6 +494,10 @@ def verify_geom(self, geom, model_field):
482 494
         if necessary (for example if the model field is MultiPolygonField while
483 495
         the mapped shapefile only contains Polygons).
484 496
         """
  497
+        # Downgrade a 3D geom to a 2D one, if necessary.
  498
+        if self.coord_dim != geom.coord_dim:
  499
+            geom.coord_dim = self.coord_dim
  500
+
485 501
         if self.make_multi(geom.geom_type, model_field):
486 502
             # Constructing a multi-geometry type to contain the single geometry
487 503
             multi_type = self.MULTI_TYPES[geom.geom_type.num]
0  gis/tests/geo3d/__init__.py b/django/contrib/gis/tests/geo3d/__init__.py
No changes.

0 notes on commit 32d0730

Please sign in to comment.
Something went wrong with that request. Please try again.