tests.py 16.8 KB
Newer Older
1
from __future__ import unicode_literals
2

3 4
import os
import re
5
from unittest import skipUnless
6

7
from django.contrib.gis.db.models import Extent3D, Union
8 9 10
from django.contrib.gis.db.models.functions import (
    AsGeoJSON, AsKML, Length, Perimeter, Scale, Translate,
)
11
from django.contrib.gis.gdal import HAS_GDAL
12
from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon
13
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
14
from django.utils._os import upath
15
from django.utils.deprecation import RemovedInDjango20Warning
16

17 18 19 20
from .models import (
    City3D, Interstate2D, Interstate3D, InterstateProj2D, InterstateProj3D,
    MultiPoint3D, Point2D, Point3D, Polygon2D, Polygon3D,
)
21

22 23
if HAS_GDAL:
    from django.contrib.gis.utils import LayerMapping, LayerMapError
24

25

26
data_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data'))
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
city_file = os.path.join(data_path, 'cities', 'cities.shp')
vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt')

# The coordinates of each city, with Z values corresponding to their
# altitude in meters.
city_data = (
    ('Houston', (-95.363151, 29.763374, 18)),
    ('Dallas', (-96.801611, 32.782057, 147)),
    ('Oklahoma City', (-97.521157, 34.464642, 380)),
    ('Wellington', (174.783117, -41.315268, 14)),
    ('Pueblo', (-104.609252, 38.255001, 1433)),
    ('Lawrence', (-95.235060, 38.971823, 251)),
    ('Chicago', (-87.650175, 41.850385, 181)),
    ('Victoria', (-123.305196, 48.462611, 15)),
)

# Reference mapping of city name to its altitude (Z value).
44
city_dict = {name: coords for name, coords in city_data}
45

46
# 3D freeway data derived from the National Elevation Dataset:
47 48
#  http://seamless.usgs.gov/products/9arc.php
interstate_data = (
49
    ('I-45',
50 51 52 53 54 55 56 57 58 59 60
     '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)',
61
     (11.339, 4.536, 9.762, 12.448, 10.457, 9.418, 14.858,
Loic Bistuer's avatar
Loic Bistuer committed
62 63 64
      15.386, 13.168, 15.104, 16.516, 13.923, 14.385, 15.16,
      15.544, 14.975, 15.688, 16.099, 15.197, 17.268, 19.857,
      15.435),
65
     ),
66
)
67 68 69 70

# Bounding box polygon for inner-loop of Houston (in projected coordinate
# system 32140), with elevation values from the National Elevation Dataset
# (see above).
71
bbox_data = (
72 73
    'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,'
    '942051.75 4208366.38,941527.97 4225693.20))',
74 75 76
    (21.71, 13.21, 9.12, 16.40, 21.71)
)

77

78
class Geo3DLoadingHelper(object):
79
    def _load_interstate_data(self):
80 81 82
        # Interstate (2D / 3D and Geographic/Projected variants)
        for name, line, exp_z in interstate_data:
            line_3d = GEOSGeometry(line, srid=4269)
83
            line_2d = LineString([l[:2] for l in line_3d.coords], srid=4269)
84 85 86 87 88 89 90 91

            # Creating a geographic and projected version of the
            # interstate in both 2D and 3D.
            Interstate3D.objects.create(name=name, line=line_3d)
            InterstateProj3D.objects.create(name=name, line=line_3d)
            Interstate2D.objects.create(name=name, line=line_2d)
            InterstateProj2D.objects.create(name=name, line=line_2d)

92 93 94 95 96 97 98 99 100 101 102
    def _load_city_data(self):
        for name, pnt_data in city_data:
            City3D.objects.create(name=name, point=Point(*pnt_data, srid=4326))

    def _load_polygon_data(self):
        bbox_wkt, bbox_z = bbox_data
        bbox_2d = GEOSGeometry(bbox_wkt, srid=32140)
        bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140)
        Polygon2D.objects.create(name='2D BBox', poly=bbox_2d)
        Polygon3D.objects.create(name='3D BBox', poly=bbox_3d)

103 104 105 106 107 108 109 110 111 112 113 114 115

@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
@skipUnlessDBFeature("gis_enabled", "supports_3d_storage")
class Geo3DTest(Geo3DLoadingHelper, TestCase):
    """
    Only a subset of the PostGIS routines are 3D-enabled, and this TestCase
    tries to test the features that can handle 3D and that are also
    available within GeoDjango.  For more information, see the PostGIS docs
    on the routines that support 3D:

    http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions
    """

116 117 118 119 120 121 122
    def test_3d_hasz(self):
        """
        Make sure data is 3D and has expected Z values -- shouldn't change
        because of coordinate system.
        """
        self._load_interstate_data()
        for name, line, exp_z in interstate_data:
123 124 125
            interstate = Interstate3D.objects.get(name=name)
            interstate_proj = InterstateProj3D.objects.get(name=name)
            for i in [interstate, interstate_proj]:
126
                self.assertTrue(i.line.hasz)
127 128
                self.assertEqual(exp_z, tuple(i.line.z))

129 130 131 132 133 134 135 136 137 138 139 140
        self._load_city_data()
        for name, pnt_data in city_data:
            city = City3D.objects.get(name=name)
            z = pnt_data[2]
            self.assertTrue(city.point.hasz)
            self.assertEqual(z, city.point.z)

    def test_3d_polygons(self):
        """
        Test the creation of polygon 3D models.
        """
        self._load_polygon_data()
141
        p3d = Polygon3D.objects.get(name='3D BBox')
142
        self.assertTrue(p3d.poly.hasz)
143 144
        self.assertIsInstance(p3d.poly, Polygon)
        self.assertEqual(p3d.poly.srid, 32140)
145

146 147 148 149
    def test_3d_layermapping(self):
        """
        Testing LayerMapping on 3D models.
        """
150 151
        point_mapping = {'point': 'POINT'}
        mpoint_mapping = {'mpoint': 'MULTIPOINT'}
152 153 154 155 156 157 158 159 160 161

        # The VRT is 3D, but should still be able to map sans the Z.
        lm = LayerMapping(Point2D, vrt_file, point_mapping, transform=False)
        lm.save()
        self.assertEqual(3, Point2D.objects.count())

        # The city shapefile is 2D, and won't be able to fill the coordinates
        # in the 3D model -- thus, a LayerMapError is raised.
        self.assertRaises(LayerMapError, LayerMapping,
                          Point3D, city_file, point_mapping, transform=False)
162

163 164 165 166 167 168 169 170 171 172 173
        # 3D model should take 3D data just fine.
        lm = LayerMapping(Point3D, vrt_file, point_mapping, transform=False)
        lm.save()
        self.assertEqual(3, Point3D.objects.count())

        # Making sure LayerMapping.make_multi works right, by converting
        # a Point25D into a MultiPoint25D.
        lm = LayerMapping(MultiPoint3D, vrt_file, mpoint_mapping, transform=False)
        lm.save()
        self.assertEqual(3, MultiPoint3D.objects.count())

174
    @ignore_warnings(category=RemovedInDjango20Warning)
175 176 177 178 179
    def test_kml(self):
        """
        Test GeoQuerySet.kml() with Z values.
        """
        self._load_city_data()
180 181 182 183
        h = City3D.objects.kml(precision=6).get(name='Houston')
        # KML should be 3D.
        # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';`
        ref_kml_regex = re.compile(r'^<Point><coordinates>-95.363\d+,29.763\d+,18</coordinates></Point>$')
184
        self.assertTrue(ref_kml_regex.match(h.kml))
185

186
    @ignore_warnings(category=RemovedInDjango20Warning)
187 188 189 190 191
    def test_geojson(self):
        """
        Test GeoQuerySet.geojson() with Z values.
        """
        self._load_city_data()
192 193 194 195
        h = City3D.objects.geojson(precision=6).get(name='Houston')
        # GeoJSON should be 3D
        # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';`
        ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$')
196
        self.assertTrue(ref_json_regex.match(h.geojson))
197

198
    @skipUnlessDBFeature("supports_3d_functions")
199 200 201 202
    def test_union(self):
        """
        Testing the Union aggregate of 3D models.
        """
203 204
        # PostGIS query that returned the reference EWKT for this test:
        #  `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;`
205
        self._load_city_data()
206 207 208 209 210
        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)'
        )
211 212
        ref_union = GEOSGeometry(ref_ewkt)
        union = City3D.objects.aggregate(Union('point'))['point__union']
213
        self.assertTrue(union.hasz)
214
        # Ordering of points in the resulting geometry may vary between implementations
215
        self.assertSetEqual({p.ewkt for p in ref_union}, {p.ewkt for p in union})
216

217
    @skipUnlessDBFeature("supports_3d_functions")
218 219 220 221 222
    def test_extent(self):
        """
        Testing the Extent3D aggregate for 3D models.
        """
        self._load_city_data()
223
        # `SELECT ST_Extent3D(point) FROM geo3d_city3d;`
224
        ref_extent3d = (-123.305196, -41.315268, 14, 174.783117, 48.462611, 1433)
225
        extent = City3D.objects.aggregate(Extent3D('point'))['point__extent3d']
226 227 228 229 230

        def check_extent3d(extent3d, tol=6):
            for ref_val, ext_val in zip(ref_extent3d, extent3d):
                self.assertAlmostEqual(ref_val, ext_val, tol)

231
        check_extent3d(extent)
232
        self.assertIsNone(City3D.objects.none().aggregate(Extent3D('point'))['point__extent3d'])
233

234
    @ignore_warnings(category=RemovedInDjango20Warning)
235
    @skipUnlessDBFeature("supports_3d_functions")
236 237 238 239 240
    def test_perimeter(self):
        """
        Testing GeoQuerySet.perimeter() on 3D fields.
        """
        self._load_polygon_data()
241 242 243 244 245 246 247 248 249 250 251 252
        # Reference query for values below:
        #  `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;`
        ref_perim_3d = 76859.2620451
        ref_perim_2d = 76859.2577803
        tol = 6
        self.assertAlmostEqual(ref_perim_2d,
                               Polygon2D.objects.perimeter().get(name='2D BBox').perimeter.m,
                               tol)
        self.assertAlmostEqual(ref_perim_3d,
                               Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m,
                               tol)

253
    @ignore_warnings(category=RemovedInDjango20Warning)
254
    @skipUnlessDBFeature("supports_3d_functions")
255 256 257 258
    def test_length(self):
        """
        Testing GeoQuerySet.length() on 3D fields.
        """
259 260
        # ST_Length_Spheroid Z-aware, and thus does not need to use
        # a separate function internally.
261
        # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]')
262
        #    FROM geo3d_interstate[2d|3d];`
263
        self._load_interstate_data()
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        tol = 3
        ref_length_2d = 4368.1721949481
        ref_length_3d = 4368.62547052088
        self.assertAlmostEqual(ref_length_2d,
                               Interstate2D.objects.length().get(name='I-45').length.m,
                               tol)
        self.assertAlmostEqual(ref_length_3d,
                               Interstate3D.objects.length().get(name='I-45').length.m,
                               tol)

        # Making sure `ST_Length3D` is used on for a projected
        # and 3D model rather than `ST_Length`.
        # `SELECT ST_Length(line) FROM geo3d_interstateproj2d;`
        ref_length_2d = 4367.71564892392
        # `SELECT ST_Length3D(line) FROM geo3d_interstateproj3d;`
        ref_length_3d = 4368.16897234101
        self.assertAlmostEqual(ref_length_2d,
                               InterstateProj2D.objects.length().get(name='I-45').length.m,
                               tol)
        self.assertAlmostEqual(ref_length_3d,
                               InterstateProj3D.objects.length().get(name='I-45').length.m,
                               tol)
286

287
    @ignore_warnings(category=RemovedInDjango20Warning)
288
    @skipUnlessDBFeature("supports_3d_functions")
289 290 291 292 293
    def test_scale(self):
        """
        Testing GeoQuerySet.scale() on Z values.
        """
        self._load_city_data()
294 295 296 297 298 299
        # Mapping of City name to reference Z values.
        zscales = (-3, 4, 23)
        for zscale in zscales:
            for city in City3D.objects.scale(1.0, 1.0, zscale):
                self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z)

300
    @ignore_warnings(category=RemovedInDjango20Warning)
301
    @skipUnlessDBFeature("supports_3d_functions")
302 303 304 305 306
    def test_translate(self):
        """
        Testing GeoQuerySet.translate() on Z values.
        """
        self._load_city_data()
307 308 309 310
        ztranslations = (5.23, 23, -17)
        for ztrans in ztranslations:
            for city in City3D.objects.translate(0, 0, ztrans):
                self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400


@skipUnless(HAS_GDAL, "GDAL is required for Geo3DTest.")
@skipUnlessDBFeature("gis_enabled", "supports_3d_functions")
class Geo3DFunctionsTests(Geo3DLoadingHelper, TestCase):
    def test_kml(self):
        """
        Test KML() function with Z values.
        """
        self._load_city_data()
        h = City3D.objects.annotate(kml=AsKML('point', precision=6)).get(name='Houston')
        # KML should be 3D.
        # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';`
        ref_kml_regex = re.compile(r'^<Point><coordinates>-95.363\d+,29.763\d+,18</coordinates></Point>$')
        self.assertTrue(ref_kml_regex.match(h.kml))

    def test_geojson(self):
        """
        Test GeoJSON() function with Z values.
        """
        self._load_city_data()
        h = City3D.objects.annotate(geojson=AsGeoJSON('point', precision=6)).get(name='Houston')
        # GeoJSON should be 3D
        # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';`
        ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$')
        self.assertTrue(ref_json_regex.match(h.geojson))

    def test_perimeter(self):
        """
        Testing Perimeter() function on 3D fields.
        """
        self._load_polygon_data()
        # Reference query for values below:
        #  `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;`
        ref_perim_3d = 76859.2620451
        ref_perim_2d = 76859.2577803
        tol = 6
        poly2d = Polygon2D.objects.annotate(perimeter=Perimeter('poly')).get(name='2D BBox')
        self.assertAlmostEqual(ref_perim_2d, poly2d.perimeter.m, tol)
        poly3d = Polygon3D.objects.annotate(perimeter=Perimeter('poly')).get(name='3D BBox')
        self.assertAlmostEqual(ref_perim_3d, poly3d.perimeter.m, tol)

    def test_length(self):
        """
        Testing Length() function on 3D fields.
        """
        # ST_Length_Spheroid Z-aware, and thus does not need to use
        # a separate function internally.
        # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]')
        #    FROM geo3d_interstate[2d|3d];`
        self._load_interstate_data()
        tol = 3
        ref_length_2d = 4368.1721949481
        ref_length_3d = 4368.62547052088
        inter2d = Interstate2D.objects.annotate(length=Length('line')).get(name='I-45')
        self.assertAlmostEqual(ref_length_2d, inter2d.length.m, tol)
        inter3d = Interstate3D.objects.annotate(length=Length('line')).get(name='I-45')
        self.assertAlmostEqual(ref_length_3d, inter3d.length.m, tol)

        # Making sure `ST_Length3D` is used on for a projected
        # and 3D model rather than `ST_Length`.
        # `SELECT ST_Length(line) FROM geo3d_interstateproj2d;`
        ref_length_2d = 4367.71564892392
        # `SELECT ST_Length3D(line) FROM geo3d_interstateproj3d;`
        ref_length_3d = 4368.16897234101
        inter2d = InterstateProj2D.objects.annotate(length=Length('line')).get(name='I-45')
        self.assertAlmostEqual(ref_length_2d, inter2d.length.m, tol)
        inter3d = InterstateProj3D.objects.annotate(length=Length('line')).get(name='I-45')
        self.assertAlmostEqual(ref_length_3d, inter3d.length.m, tol)

    def test_scale(self):
        """
        Testing Scale() function on Z values.
        """
        self._load_city_data()
        # Mapping of City name to reference Z values.
        zscales = (-3, 4, 23)
        for zscale in zscales:
            for city in City3D.objects.annotate(scale=Scale('point', 1.0, 1.0, zscale)):
                self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z)

    def test_translate(self):
        """
        Testing Translate() function on Z values.
        """
        self._load_city_data()
        ztranslations = (5.23, 23, -17)
        for ztrans in ztranslations:
            for city in City3D.objects.annotate(translate=Translate('point', 0, 0, ztrans)):
                self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z)