mirror of https://github.com/django/django.git
Fixed #11433 -- 3D geometry fields are now supported with PostGIS; EWKB is now used by `PostGISAdaptor`.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@11742 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
6c61ca3d74
commit
32d0730b89
|
@ -18,18 +18,21 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
|
||||||
distance_spheroid=DISTANCE_SPHEROID,
|
distance_spheroid=DISTANCE_SPHEROID,
|
||||||
envelope=ENVELOPE,
|
envelope=ENVELOPE,
|
||||||
extent=EXTENT,
|
extent=EXTENT,
|
||||||
|
extent3d=EXTENT3D,
|
||||||
gis_terms=POSTGIS_TERMS,
|
gis_terms=POSTGIS_TERMS,
|
||||||
geojson=ASGEOJSON,
|
geojson=ASGEOJSON,
|
||||||
gml=ASGML,
|
gml=ASGML,
|
||||||
intersection=INTERSECTION,
|
intersection=INTERSECTION,
|
||||||
kml=ASKML,
|
kml=ASKML,
|
||||||
length=LENGTH,
|
length=LENGTH,
|
||||||
|
length3d=LENGTH3D,
|
||||||
length_spheroid=LENGTH_SPHEROID,
|
length_spheroid=LENGTH_SPHEROID,
|
||||||
make_line=MAKE_LINE,
|
make_line=MAKE_LINE,
|
||||||
mem_size=MEM_SIZE,
|
mem_size=MEM_SIZE,
|
||||||
num_geom=NUM_GEOM,
|
num_geom=NUM_GEOM,
|
||||||
num_points=NUM_POINTS,
|
num_points=NUM_POINTS,
|
||||||
perimeter=PERIMETER,
|
perimeter=PERIMETER,
|
||||||
|
perimeter3d=PERIMETER3D,
|
||||||
point_on_surface=POINT_ON_SURFACE,
|
point_on_surface=POINT_ON_SURFACE,
|
||||||
scale=SCALE,
|
scale=SCALE,
|
||||||
select=GEOM_SELECT,
|
select=GEOM_SELECT,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
|
This object provides quoting for GEOS geometries into PostgreSQL/PostGIS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB
|
from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_EWKB
|
||||||
from psycopg2 import Binary
|
from psycopg2 import Binary
|
||||||
from psycopg2.extensions import ISQLQuote
|
from psycopg2.extensions import ISQLQuote
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class PostGISAdaptor(object):
|
||||||
"Initializes on the geometry."
|
"Initializes on the geometry."
|
||||||
# Getting the WKB (in string form, to allow easy pickling of
|
# Getting the WKB (in string form, to allow easy pickling of
|
||||||
# the adaptor) and the SRID from the geometry.
|
# the adaptor) and the SRID from the geometry.
|
||||||
self.wkb = str(geom.wkb)
|
self.ewkb = str(geom.ewkb)
|
||||||
self.srid = geom.srid
|
self.srid = geom.srid
|
||||||
|
|
||||||
def __conform__(self, proto):
|
def __conform__(self, proto):
|
||||||
|
@ -30,7 +30,7 @@ class PostGISAdaptor(object):
|
||||||
def getquoted(self):
|
def getquoted(self):
|
||||||
"Returns a properly quoted string for use in PostgreSQL/PostGIS."
|
"Returns a properly quoted string for use in PostgreSQL/PostGIS."
|
||||||
# Want to use WKB, so wrap with psycopg2 Binary() to quote properly.
|
# Want to use WKB, so wrap with psycopg2 Binary() to quote properly.
|
||||||
return "%s(E%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1)
|
return "%s(E%s)" % (GEOM_FROM_EWKB, Binary(self.ewkb))
|
||||||
|
|
||||||
def prepare_database_save(self, unused):
|
def prepare_database_save(self, unused):
|
||||||
return self
|
return self
|
||||||
|
|
|
@ -63,17 +63,21 @@ if MAJOR_VERSION >= 1:
|
||||||
DISTANCE_SPHERE = get_func('distance_sphere')
|
DISTANCE_SPHERE = get_func('distance_sphere')
|
||||||
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
||||||
ENVELOPE = get_func('Envelope')
|
ENVELOPE = get_func('Envelope')
|
||||||
EXTENT = get_func('extent')
|
EXTENT = get_func('Extent')
|
||||||
|
EXTENT3D = get_func('Extent3D')
|
||||||
GEOM_FROM_TEXT = get_func('GeomFromText')
|
GEOM_FROM_TEXT = get_func('GeomFromText')
|
||||||
|
GEOM_FROM_EWKB = get_func('GeomFromEWKB')
|
||||||
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
||||||
INTERSECTION = get_func('Intersection')
|
INTERSECTION = get_func('Intersection')
|
||||||
LENGTH = get_func('Length')
|
LENGTH = get_func('Length')
|
||||||
|
LENGTH3D = get_func('Length3D')
|
||||||
LENGTH_SPHEROID = get_func('length_spheroid')
|
LENGTH_SPHEROID = get_func('length_spheroid')
|
||||||
MAKE_LINE = get_func('MakeLine')
|
MAKE_LINE = get_func('MakeLine')
|
||||||
MEM_SIZE = get_func('mem_size')
|
MEM_SIZE = get_func('mem_size')
|
||||||
NUM_GEOM = get_func('NumGeometries')
|
NUM_GEOM = get_func('NumGeometries')
|
||||||
NUM_POINTS = get_func('npoints')
|
NUM_POINTS = get_func('npoints')
|
||||||
PERIMETER = get_func('Perimeter')
|
PERIMETER = get_func('Perimeter')
|
||||||
|
PERIMETER3D = get_func('Perimeter3D')
|
||||||
POINT_ON_SURFACE = get_func('PointOnSurface')
|
POINT_ON_SURFACE = get_func('PointOnSurface')
|
||||||
SCALE = get_func('Scale')
|
SCALE = get_func('Scale')
|
||||||
SNAP_TO_GRID = get_func('SnapToGrid')
|
SNAP_TO_GRID = get_func('SnapToGrid')
|
||||||
|
|
|
@ -24,6 +24,9 @@ class Collect(GeoAggregate):
|
||||||
class Extent(GeoAggregate):
|
class Extent(GeoAggregate):
|
||||||
name = 'Extent'
|
name = 'Extent'
|
||||||
|
|
||||||
|
class Extent3D(GeoAggregate):
|
||||||
|
name = 'Extent3D'
|
||||||
|
|
||||||
class MakeLine(GeoAggregate):
|
class MakeLine(GeoAggregate):
|
||||||
name = 'MakeLine'
|
name = 'MakeLine'
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,9 @@ class GeoManager(Manager):
|
||||||
def extent(self, *args, **kwargs):
|
def extent(self, *args, **kwargs):
|
||||||
return self.get_query_set().extent(*args, **kwargs)
|
return self.get_query_set().extent(*args, **kwargs)
|
||||||
|
|
||||||
|
def extent3d(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().extent3d(*args, **kwargs)
|
||||||
|
|
||||||
def geojson(self, *args, **kwargs):
|
def geojson(self, *args, **kwargs):
|
||||||
return self.get_query_set().geojson(*args, **kwargs)
|
return self.get_query_set().geojson(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,14 @@ class GeoQuerySet(QuerySet):
|
||||||
"""
|
"""
|
||||||
return self._spatial_aggregate(aggregates.Extent, **kwargs)
|
return self._spatial_aggregate(aggregates.Extent, **kwargs)
|
||||||
|
|
||||||
|
def extent3d(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the aggregate extent, in 3D, of the features in the
|
||||||
|
GeoQuerySet. It is returned as a 6-tuple, comprising:
|
||||||
|
(xmin, ymin, zmin, xmax, ymax, zmax).
|
||||||
|
"""
|
||||||
|
return self._spatial_aggregate(aggregates.Extent3D, **kwargs)
|
||||||
|
|
||||||
def geojson(self, precision=8, crs=False, bbox=False, **kwargs):
|
def geojson(self, precision=8, crs=False, bbox=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns a GeoJSON representation of the geomtry field in a `geojson`
|
Returns a GeoJSON representation of the geomtry field in a `geojson`
|
||||||
|
@ -524,12 +532,14 @@ class GeoQuerySet(QuerySet):
|
||||||
else:
|
else:
|
||||||
dist_att = Distance.unit_attname(geo_field.units_name)
|
dist_att = Distance.unit_attname(geo_field.units_name)
|
||||||
|
|
||||||
# Shortcut booleans for what distance function we're using.
|
# Shortcut booleans for what distance function we're using and
|
||||||
|
# whether the geometry field is 3D.
|
||||||
distance = func == 'distance'
|
distance = func == 'distance'
|
||||||
length = func == 'length'
|
length = func == 'length'
|
||||||
perimeter = func == 'perimeter'
|
perimeter = func == 'perimeter'
|
||||||
if not (distance or length or perimeter):
|
if not (distance or length or perimeter):
|
||||||
raise ValueError('Unknown distance function: %s' % func)
|
raise ValueError('Unknown distance function: %s' % func)
|
||||||
|
geom_3d = geo_field.dim == 3
|
||||||
|
|
||||||
# The field's get_db_prep_lookup() is used to get any
|
# The field's get_db_prep_lookup() is used to get any
|
||||||
# extra distance parameters. Here we set up the
|
# extra distance parameters. Here we set up the
|
||||||
|
@ -604,7 +614,7 @@ class GeoQuerySet(QuerySet):
|
||||||
# some error checking is required.
|
# some error checking is required.
|
||||||
if not isinstance(geo_field, PointField):
|
if not isinstance(geo_field, PointField):
|
||||||
raise ValueError('Spherical distance calculation only supported on PointFields.')
|
raise ValueError('Spherical distance calculation only supported on PointFields.')
|
||||||
if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
|
if not str(SpatialBackend.Geometry(buffer(params[0].ewkb)).geom_type) == 'Point':
|
||||||
raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
|
raise ValueError('Spherical distance calculation only supported with Point Geometry parameters')
|
||||||
# The `function` procedure argument needs to be set differently for
|
# The `function` procedure argument needs to be set differently for
|
||||||
# geodetic distance calculations.
|
# geodetic distance calculations.
|
||||||
|
@ -617,9 +627,16 @@ class GeoQuerySet(QuerySet):
|
||||||
elif length or perimeter:
|
elif length or perimeter:
|
||||||
procedure_fmt = '%(geo_col)s'
|
procedure_fmt = '%(geo_col)s'
|
||||||
if geodetic and length:
|
if geodetic and length:
|
||||||
# There's no `length_sphere`
|
# There's no `length_sphere`, and `length_spheroid` also
|
||||||
|
# works on 3D geometries.
|
||||||
procedure_fmt += ',%(spheroid)s'
|
procedure_fmt += ',%(spheroid)s'
|
||||||
procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
|
procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
|
||||||
|
elif geom_3d and SpatialBackend.postgis:
|
||||||
|
# Use 3D variants of perimeter and length routines on PostGIS.
|
||||||
|
if perimeter:
|
||||||
|
procedure_args.update({'function' : SpatialBackend.perimeter3d})
|
||||||
|
elif length:
|
||||||
|
procedure_args.update({'function' : SpatialBackend.length3d})
|
||||||
|
|
||||||
# Setting up the settings for `_spatial_attribute`.
|
# Setting up the settings for `_spatial_attribute`.
|
||||||
s = {'select_field' : DistanceField(dist_att),
|
s = {'select_field' : DistanceField(dist_att),
|
||||||
|
|
|
@ -11,6 +11,9 @@ geo_template = '%(function)s(%(field)s)'
|
||||||
def convert_extent(box):
|
def convert_extent(box):
|
||||||
raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
|
raise NotImplementedError('Aggregate extent not implemented for this spatial backend.')
|
||||||
|
|
||||||
|
def convert_extent3d(box):
|
||||||
|
raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.')
|
||||||
|
|
||||||
def convert_geom(wkt, geo_field):
|
def convert_geom(wkt, geo_field):
|
||||||
raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
|
raise NotImplementedError('Aggregate method not implemented for this spatial backend.')
|
||||||
|
|
||||||
|
@ -23,6 +26,14 @@ if SpatialBackend.postgis:
|
||||||
xmax, ymax = map(float, ur.split())
|
xmax, ymax = map(float, ur.split())
|
||||||
return (xmin, ymin, xmax, ymax)
|
return (xmin, ymin, xmax, ymax)
|
||||||
|
|
||||||
|
def convert_extent3d(box3d):
|
||||||
|
# Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)";
|
||||||
|
# parsing out and returning as a 4-tuple.
|
||||||
|
ll, ur = box3d[6:-1].split(',')
|
||||||
|
xmin, ymin, zmin = map(float, ll.split())
|
||||||
|
xmax, ymax, zmax = map(float, ur.split())
|
||||||
|
return (xmin, ymin, zmin, xmax, ymax, zmax)
|
||||||
|
|
||||||
def convert_geom(hex, geo_field):
|
def convert_geom(hex, geo_field):
|
||||||
if hex: return SpatialBackend.Geometry(hex)
|
if hex: return SpatialBackend.Geometry(hex)
|
||||||
else: return None
|
else: return None
|
||||||
|
@ -94,7 +105,7 @@ class Collect(GeoAggregate):
|
||||||
sql_function = SpatialBackend.collect
|
sql_function = SpatialBackend.collect
|
||||||
|
|
||||||
class Extent(GeoAggregate):
|
class Extent(GeoAggregate):
|
||||||
is_extent = True
|
is_extent = '2D'
|
||||||
sql_function = SpatialBackend.extent
|
sql_function = SpatialBackend.extent
|
||||||
|
|
||||||
if SpatialBackend.oracle:
|
if SpatialBackend.oracle:
|
||||||
|
@ -102,6 +113,10 @@ if SpatialBackend.oracle:
|
||||||
Extent.conversion_class = GeomField
|
Extent.conversion_class = GeomField
|
||||||
Extent.sql_template = '%(function)s(%(field)s)'
|
Extent.sql_template = '%(function)s(%(field)s)'
|
||||||
|
|
||||||
|
class Extent3D(GeoAggregate):
|
||||||
|
is_extent = '3D'
|
||||||
|
sql_function = SpatialBackend.extent3d
|
||||||
|
|
||||||
class MakeLine(GeoAggregate):
|
class MakeLine(GeoAggregate):
|
||||||
conversion_class = GeomField
|
conversion_class = GeomField
|
||||||
sql_function = SpatialBackend.make_line
|
sql_function = SpatialBackend.make_line
|
||||||
|
|
|
@ -262,7 +262,10 @@ class GeoQuery(sql.Query):
|
||||||
"""
|
"""
|
||||||
if isinstance(aggregate, self.aggregates_module.GeoAggregate):
|
if isinstance(aggregate, self.aggregates_module.GeoAggregate):
|
||||||
if aggregate.is_extent:
|
if aggregate.is_extent:
|
||||||
return self.aggregates_module.convert_extent(value)
|
if aggregate.is_extent == '3D':
|
||||||
|
return self.aggregates_module.convert_extent3d(value)
|
||||||
|
else:
|
||||||
|
return self.aggregates_module.convert_extent(value)
|
||||||
else:
|
else:
|
||||||
return self.aggregates_module.convert_geom(value, aggregate.source)
|
return self.aggregates_module.convert_geom(value, aggregate.source)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -373,9 +373,10 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
||||||
@property
|
@property
|
||||||
def hex(self):
|
def hex(self):
|
||||||
"""
|
"""
|
||||||
Returns the HEX of the Geometry -- please note that the SRID is not
|
Returns the WKB of this Geometry in hexadecimal form. Please note
|
||||||
included in this representation, because it is not a part of the
|
that the SRID and Z values are not included in this representation
|
||||||
OGC specification (use the `hexewkb` property instead).
|
because it is not a part of the OGC specification (use the `hexewkb`
|
||||||
|
property instead).
|
||||||
"""
|
"""
|
||||||
# A possible faster, all-python, implementation:
|
# A possible faster, all-python, implementation:
|
||||||
# str(self.wkb).encode('hex')
|
# str(self.wkb).encode('hex')
|
||||||
|
@ -384,9 +385,9 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
||||||
@property
|
@property
|
||||||
def hexewkb(self):
|
def hexewkb(self):
|
||||||
"""
|
"""
|
||||||
Returns the HEXEWKB of this Geometry. This is an extension of the WKB
|
Returns the EWKB of this Geometry in hexadecimal form. This is an
|
||||||
specification that includes SRID and Z values taht are a part of this
|
extension of the WKB specification that includes SRID and Z values
|
||||||
geometry.
|
that are a part of this geometry.
|
||||||
"""
|
"""
|
||||||
if self.hasz:
|
if self.hasz:
|
||||||
if not GEOS_PREPARE:
|
if not GEOS_PREPARE:
|
||||||
|
|
|
@ -9,9 +9,10 @@ def geo_suite():
|
||||||
some backends).
|
some backends).
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.gis.geos import GEOS_PREPARE
|
||||||
from django.contrib.gis.gdal import HAS_GDAL
|
from django.contrib.gis.gdal import HAS_GDAL
|
||||||
from django.contrib.gis.utils import HAS_GEOIP
|
from django.contrib.gis.utils import HAS_GEOIP
|
||||||
from django.contrib.gis.tests.utils import mysql
|
from django.contrib.gis.tests.utils import postgis, mysql
|
||||||
|
|
||||||
# The test suite.
|
# The test suite.
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
|
@ -32,6 +33,10 @@ def geo_suite():
|
||||||
if not mysql:
|
if not mysql:
|
||||||
test_apps.append('distapp')
|
test_apps.append('distapp')
|
||||||
|
|
||||||
|
# Only PostGIS using GEOS 3.1+ can support 3D so far.
|
||||||
|
if postgis and GEOS_PREPARE:
|
||||||
|
test_apps.append('geo3d')
|
||||||
|
|
||||||
if HAS_GDAL:
|
if HAS_GDAL:
|
||||||
# These tests require GDAL.
|
# These tests require GDAL.
|
||||||
test_suite_names.extend(['test_spatialrefsys', 'test_geoforms'])
|
test_suite_names.extend(['test_spatialrefsys', 'test_geoforms'])
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
from django.contrib.gis.db import models
|
||||||
|
|
||||||
|
class City3D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
point = models.PointField(dim=3)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Interstate2D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
line = models.LineStringField(srid=4269)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Interstate3D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
line = models.LineStringField(dim=3, srid=4269)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class InterstateProj2D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
line = models.LineStringField(srid=32140)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class InterstateProj3D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
line = models.LineStringField(dim=3, srid=32140)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Polygon2D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
poly = models.PolygonField(srid=32140)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Polygon3D(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
poly = models.PolygonField(dim=3, srid=32140)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Point2D(models.Model):
|
||||||
|
point = models.PointField()
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
class Point3D(models.Model):
|
||||||
|
point = models.PointField(dim=3)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
class MultiPoint3D(models.Model):
|
||||||
|
mpoint = models.MultiPointField(dim=3)
|
||||||
|
objects = models.GeoManager()
|
|
@ -0,0 +1,234 @@
|
||||||
|
import os, re, unittest
|
||||||
|
from django.contrib.gis.db.models import Union, Extent3D
|
||||||
|
from django.contrib.gis.geos import GEOSGeometry, Point, Polygon
|
||||||
|
from django.contrib.gis.utils import LayerMapping, LayerMapError
|
||||||
|
|
||||||
|
from models import City3D, Interstate2D, Interstate3D, \
|
||||||
|
InterstateProj2D, InterstateProj3D, \
|
||||||
|
Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D
|
||||||
|
|
||||||
|
data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data'))
|
||||||
|
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).
|
||||||
|
city_dict = dict((name, coords) for name, coords in city_data)
|
||||||
|
|
||||||
|
# 3D freeway data derived from the National Elevation Dataset:
|
||||||
|
# http://seamless.usgs.gov/products/9arc.php
|
||||||
|
interstate_data = (
|
||||||
|
('I-45',
|
||||||
|
'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)',
|
||||||
|
( 11.339, 4.536, 9.762, 12.448, 10.457, 9.418, 14.858,
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bounding box polygon for inner-loop of Houston (in projected coordinate
|
||||||
|
# system 32140), with elevation values from the National Elevation Dataset
|
||||||
|
# (see above).
|
||||||
|
bbox_wkt = 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))'
|
||||||
|
bbox_z = (21.71, 13.21, 9.12, 16.40, 21.71)
|
||||||
|
def gen_bbox():
|
||||||
|
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)
|
||||||
|
return bbox_2d, bbox_3d
|
||||||
|
|
||||||
|
class Geo3DTest(unittest.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.refractions.net/documentation/manual-1.4/ch08.html#PostGIS_3D_Functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test01_3d(self):
|
||||||
|
"Test the creation of 3D models."
|
||||||
|
# 3D models for the rest of the tests will be populated in here.
|
||||||
|
# For each 3D data set create model (and 2D version if necessary),
|
||||||
|
# retrieve, and assert geometry is in 3D and contains the expected
|
||||||
|
# 3D values.
|
||||||
|
for name, pnt_data in city_data:
|
||||||
|
x, y, z = pnt_data
|
||||||
|
pnt = Point(x, y, z, srid=4326)
|
||||||
|
City3D.objects.create(name=name, point=pnt)
|
||||||
|
city = City3D.objects.get(name=name)
|
||||||
|
self.failUnless(city.point.hasz)
|
||||||
|
self.assertEqual(z, city.point.z)
|
||||||
|
|
||||||
|
# Interstate (2D / 3D and Geographic/Projected variants)
|
||||||
|
for name, line, exp_z in interstate_data:
|
||||||
|
line_3d = GEOSGeometry(line, srid=4269)
|
||||||
|
# Using `hex` attribute because it omits 3D.
|
||||||
|
line_2d = GEOSGeometry(line_3d.hex, srid=4269)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Retrieving and making sure it's 3D and has expected
|
||||||
|
# Z values -- shouldn't change because of coordinate system.
|
||||||
|
interstate = Interstate3D.objects.get(name=name)
|
||||||
|
interstate_proj = InterstateProj3D.objects.get(name=name)
|
||||||
|
for i in [interstate, interstate_proj]:
|
||||||
|
self.failUnless(i.line.hasz)
|
||||||
|
self.assertEqual(exp_z, tuple(i.line.z))
|
||||||
|
|
||||||
|
# Creating 3D Polygon.
|
||||||
|
bbox2d, bbox3d = gen_bbox()
|
||||||
|
Polygon2D.objects.create(name='2D BBox', poly=bbox2d)
|
||||||
|
Polygon3D.objects.create(name='3D BBox', poly=bbox3d)
|
||||||
|
p3d = Polygon3D.objects.get(name='3D BBox')
|
||||||
|
self.failUnless(p3d.poly.hasz)
|
||||||
|
self.assertEqual(bbox3d, p3d.poly)
|
||||||
|
|
||||||
|
def test01a_3d_layermapping(self):
|
||||||
|
"Testing LayerMapping on 3D models."
|
||||||
|
from models import Point2D, Point3D
|
||||||
|
|
||||||
|
point_mapping = {'point' : 'POINT'}
|
||||||
|
mpoint_mapping = {'mpoint' : 'MULTIPOINT'}
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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())
|
||||||
|
|
||||||
|
def test02a_kml(self):
|
||||||
|
"Test GeoQuerySet.kml() with Z values."
|
||||||
|
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>$')
|
||||||
|
self.failUnless(ref_kml_regex.match(h.kml))
|
||||||
|
|
||||||
|
def test02b_geojson(self):
|
||||||
|
"Test GeoQuerySet.geojson() with Z values."
|
||||||
|
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+)?\]}$')
|
||||||
|
self.failUnless(ref_json_regex.match(h.geojson))
|
||||||
|
|
||||||
|
def test03a_union(self):
|
||||||
|
"Testing the Union aggregate of 3D models."
|
||||||
|
# PostGIS query that returned the reference EWKT for this test:
|
||||||
|
# `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;`
|
||||||
|
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)'
|
||||||
|
ref_union = GEOSGeometry(ref_ewkt)
|
||||||
|
union = City3D.objects.aggregate(Union('point'))['point__union']
|
||||||
|
self.failUnless(union.hasz)
|
||||||
|
self.assertEqual(ref_union, union)
|
||||||
|
|
||||||
|
def test03b_extent(self):
|
||||||
|
"Testing the Extent3D aggregate for 3D models."
|
||||||
|
# `SELECT ST_Extent3D(point) FROM geo3d_city3d;`
|
||||||
|
ref_extent3d = (-123.305196, -41.315268, 14,174.783117, 48.462611, 1433)
|
||||||
|
extent1 = City3D.objects.aggregate(Extent3D('point'))['point__extent3d']
|
||||||
|
extent2 = City3D.objects.extent3d()
|
||||||
|
|
||||||
|
def check_extent3d(extent3d, tol=6):
|
||||||
|
for ref_val, ext_val in zip(ref_extent3d, extent3d):
|
||||||
|
self.assertAlmostEqual(ref_val, ext_val, tol)
|
||||||
|
|
||||||
|
for e3d in [extent1, extent2]:
|
||||||
|
check_extent3d(e3d)
|
||||||
|
|
||||||
|
def test04_perimeter(self):
|
||||||
|
"Testing GeoQuerySet.perimeter() on 3D fields."
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def test05_length(self):
|
||||||
|
"Testing GeoQuerySet.length() 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];`
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test06_scale(self):
|
||||||
|
"Testing GeoQuerySet.scale() on Z values."
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def test07_translate(self):
|
||||||
|
"Testing GeoQuerySet.translate() on Z values."
|
||||||
|
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)
|
||||||
|
|
||||||
|
def suite():
|
||||||
|
s = unittest.TestSuite()
|
||||||
|
s.addTest(unittest.makeSuite(Geo3DTest))
|
||||||
|
return s
|
|
@ -0,0 +1 @@
|
||||||
|
# Create your views here.
|
|
@ -10,7 +10,7 @@ if HAS_GDAL:
|
||||||
try:
|
try:
|
||||||
# LayerMapping requires DJANGO_SETTINGS_MODULE to be set,
|
# LayerMapping requires DJANGO_SETTINGS_MODULE to be set,
|
||||||
# so this needs to be in try/except.
|
# so this needs to be in try/except.
|
||||||
from django.contrib.gis.utils.layermapping import LayerMapping
|
from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,9 @@ class LayerMapping(object):
|
||||||
MULTI_TYPES = {1 : OGRGeomType('MultiPoint'),
|
MULTI_TYPES = {1 : OGRGeomType('MultiPoint'),
|
||||||
2 : OGRGeomType('MultiLineString'),
|
2 : OGRGeomType('MultiLineString'),
|
||||||
3 : OGRGeomType('MultiPolygon'),
|
3 : OGRGeomType('MultiPolygon'),
|
||||||
|
OGRGeomType('Point25D').num : OGRGeomType('MultiPoint25D'),
|
||||||
|
OGRGeomType('LineString25D').num : OGRGeomType('MultiLineString25D'),
|
||||||
|
OGRGeomType('Polygon25D').num : OGRGeomType('MultiPolygon25D'),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Acceptable Django field types and corresponding acceptable OGR
|
# Acceptable Django field types and corresponding acceptable OGR
|
||||||
|
@ -282,19 +285,28 @@ class LayerMapping(object):
|
||||||
if self.geom_field:
|
if self.geom_field:
|
||||||
raise LayerMapError('LayerMapping does not support more than one GeometryField per model.')
|
raise LayerMapError('LayerMapping does not support more than one GeometryField per model.')
|
||||||
|
|
||||||
|
# Getting the coordinate dimension of the geometry field.
|
||||||
|
coord_dim = model_field.dim
|
||||||
|
|
||||||
try:
|
try:
|
||||||
gtype = OGRGeomType(ogr_name)
|
if coord_dim == 3:
|
||||||
|
gtype = OGRGeomType(ogr_name + '25D')
|
||||||
|
else:
|
||||||
|
gtype = OGRGeomType(ogr_name)
|
||||||
except OGRException:
|
except OGRException:
|
||||||
raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name)
|
raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name)
|
||||||
|
|
||||||
# Making sure that the OGR Layer's Geometry is compatible.
|
# Making sure that the OGR Layer's Geometry is compatible.
|
||||||
ltype = self.layer.geom_type
|
ltype = self.layer.geom_type
|
||||||
if not (gtype == ltype or self.make_multi(ltype, model_field)):
|
if not (ltype.name.startswith(gtype.name) or self.make_multi(ltype, model_field)):
|
||||||
raise LayerMapError('Invalid mapping geometry; model has %s, feature has %s.' % (fld_name, gtype))
|
raise LayerMapError('Invalid mapping geometry; model has %s%s, layer is %s.' %
|
||||||
|
(fld_name, (coord_dim == 3 and '(dim=3)') or '', ltype))
|
||||||
|
|
||||||
# Setting the `geom_field` attribute w/the name of the model field
|
# Setting the `geom_field` attribute w/the name of the model field
|
||||||
# that is a Geometry.
|
# that is a Geometry. Also setting the coordinate dimension
|
||||||
|
# attribute.
|
||||||
self.geom_field = field_name
|
self.geom_field = field_name
|
||||||
|
self.coord_dim = coord_dim
|
||||||
fields_val = model_field
|
fields_val = model_field
|
||||||
elif isinstance(model_field, models.ForeignKey):
|
elif isinstance(model_field, models.ForeignKey):
|
||||||
if isinstance(ogr_name, dict):
|
if isinstance(ogr_name, dict):
|
||||||
|
@ -482,6 +494,10 @@ class LayerMapping(object):
|
||||||
if necessary (for example if the model field is MultiPolygonField while
|
if necessary (for example if the model field is MultiPolygonField while
|
||||||
the mapped shapefile only contains Polygons).
|
the mapped shapefile only contains Polygons).
|
||||||
"""
|
"""
|
||||||
|
# Downgrade a 3D geom to a 2D one, if necessary.
|
||||||
|
if self.coord_dim != geom.coord_dim:
|
||||||
|
geom.coord_dim = self.coord_dim
|
||||||
|
|
||||||
if self.make_multi(geom.geom_type, model_field):
|
if self.make_multi(geom.geom_type, model_field):
|
||||||
# Constructing a multi-geometry type to contain the single geometry
|
# Constructing a multi-geometry type to contain the single geometry
|
||||||
multi_type = self.MULTI_TYPES[geom.geom_type.num]
|
multi_type = self.MULTI_TYPES[geom.geom_type.num]
|
||||||
|
|
Loading…
Reference in New Issue