mirror of
https://github.com/django/django.git
synced 2025-07-04 09:49:12 +00:00
gis: geos: Added GeoJSON input/output support for GEOSGeometry
(requires GDAL 1.5+); Polygons may now be instantiated with tuples of coordinates instead of only LinearRing
instances; added extent
property; added coords
property alias for tuple
(and added tuple
for GeometryCollection
); made small optimizations to KML and coordinate array generators. Small GDAL protochange that didn't make last commit: get_envelope
is now generated w/the previously unused env_func
.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7114 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
6a692e8031
commit
ccf9baca86
@ -105,7 +105,5 @@ geom_transform = void_output(lgdal.OGR_G_Transform, [c_void_p, c_void_p])
|
||||
geom_transform_to = void_output(lgdal.OGR_G_TransformTo, [c_void_p, c_void_p])
|
||||
|
||||
# For retrieving the envelope of the geometry.
|
||||
get_envelope = lgdal.OGR_G_GetEnvelope
|
||||
get_envelope.restype = None
|
||||
get_envelope.argtypes = [c_void_p, POINTER(OGREnvelope)]
|
||||
get_envelope.errcheck = check_envelope
|
||||
get_envelope = env_func(lgdal.OGR_G_GetEnvelope, [c_void_p, POINTER(OGREnvelope)])
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
# Python, ctypes and types dependencies.
|
||||
import re
|
||||
from ctypes import addressof, byref, c_double, c_size_t
|
||||
from types import StringType, UnicodeType, IntType, FloatType, BufferType
|
||||
from types import UnicodeType
|
||||
|
||||
# GEOS-related dependencies.
|
||||
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
|
||||
@ -20,16 +20,17 @@ from django.contrib.gis.geos.prototypes import *
|
||||
# Trying to import GDAL libraries, if available. Have to place in
|
||||
# try/except since this package may be used outside GeoDjango.
|
||||
try:
|
||||
from django.contrib.gis.gdal import OGRGeometry, SpatialReference
|
||||
HAS_GDAL=True
|
||||
from django.contrib.gis.gdal import OGRGeometry, SpatialReference, GEOJSON
|
||||
HAS_GDAL = True
|
||||
except:
|
||||
HAS_GDAL=False
|
||||
HAS_GDAL, GEOJSON = False, False
|
||||
|
||||
# Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure
|
||||
# to prevent potentially malicious input from reaching the underlying C
|
||||
# library. Not a substitute for good web security programming practices.
|
||||
hex_regex = re.compile(r'^[0-9A-F]+$', re.I)
|
||||
wkt_regex = re.compile(r'^(SRID=(?P<srid>\d+);)?(?P<wkt>(POINT|LINESTRING|LINEARRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)[ACEGIMLONPSRUTY\d,\.\-\(\) ]+)$', re.I)
|
||||
json_regex = re.compile(r'^\{.+\}$')
|
||||
|
||||
class GEOSGeometry(object):
|
||||
"A class that, generally, encapsulates a GEOS geometry."
|
||||
@ -50,24 +51,29 @@ class GEOSGeometry(object):
|
||||
The `srid` keyword is used to specify the Source Reference Identifier
|
||||
(SRID) number for this Geometry. If not set, the SRID will be None.
|
||||
"""
|
||||
if isinstance(geo_input, basestring):
|
||||
if isinstance(geo_input, UnicodeType):
|
||||
# Encoding to ASCII, WKT or HEXEWKB doesn't need any more.
|
||||
geo_input = geo_input.encode('ascii')
|
||||
if isinstance(geo_input, StringType):
|
||||
if hex_regex.match(geo_input):
|
||||
# If the regex matches, the geometry is in HEX form.
|
||||
|
||||
wkt_m = wkt_regex.match(geo_input)
|
||||
if wkt_m:
|
||||
# Handling WKT input.
|
||||
if wkt_m.group('srid'): srid = int(wkt_m.group('srid'))
|
||||
g = from_wkt(wkt_m.group('wkt'))
|
||||
elif hex_regex.match(geo_input):
|
||||
# Handling HEXEWKB input.
|
||||
g = from_hex(geo_input, len(geo_input))
|
||||
else:
|
||||
m = wkt_regex.match(geo_input)
|
||||
if m:
|
||||
if m.group('srid'): srid = int(m.group('srid'))
|
||||
g = from_wkt(m.group('wkt'))
|
||||
elif GEOJSON and json_regex.match(geo_input):
|
||||
# Handling GeoJSON input.
|
||||
wkb_input = str(OGRGeometry(geo_input).wkb)
|
||||
g = from_wkb(wkb_input, len(wkb_input))
|
||||
else:
|
||||
raise ValueError('String or unicode input unrecognized as WKT EWKT, and HEXEWKB.')
|
||||
elif isinstance(geo_input, GEOM_PTR):
|
||||
# When the input is a pointer to a geomtry (GEOM_PTR).
|
||||
g = geo_input
|
||||
elif isinstance(geo_input, BufferType):
|
||||
elif isinstance(geo_input, buffer):
|
||||
# When the input is a buffer (WKB).
|
||||
wkb_input = str(geo_input)
|
||||
g = from_wkb(wkb_input, len(wkb_input))
|
||||
@ -191,6 +197,7 @@ class GEOSGeometry(object):
|
||||
@property
|
||||
def coord_seq(self):
|
||||
"Returns a clone of the coordinate sequence for this Geometry."
|
||||
if self.has_cs:
|
||||
return self._cs.clone()
|
||||
|
||||
#### Geometry Info ####
|
||||
@ -307,7 +314,7 @@ class GEOSGeometry(object):
|
||||
Returns true if the elements in the DE-9IM intersection matrix for the
|
||||
two Geometries match the elements in pattern.
|
||||
"""
|
||||
if not isinstance(pattern, StringType) or len(pattern) > 9:
|
||||
if not isinstance(pattern, str) or len(pattern) > 9:
|
||||
raise GEOSException('invalid intersection matrix pattern')
|
||||
return geos_relatepattern(self.ptr, other.ptr, pattern)
|
||||
|
||||
@ -360,6 +367,15 @@ class GEOSGeometry(object):
|
||||
# str(self.wkb).encode('hex')
|
||||
return to_hex(self.ptr, byref(c_size_t()))
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
"""
|
||||
Returns GeoJSON representation of this Geometry if GDAL 1.5+
|
||||
is installed.
|
||||
"""
|
||||
if GEOJSON: return self.ogr.json
|
||||
geojson = json
|
||||
|
||||
@property
|
||||
def wkb(self):
|
||||
"Returns the WKB of the Geometry as a buffer."
|
||||
@ -521,6 +537,21 @@ class GEOSGeometry(object):
|
||||
raise TypeError('distance() works only on other GEOS Geometries.')
|
||||
return geos_distance(self.ptr, other.ptr, byref(c_double()))
|
||||
|
||||
@property
|
||||
def extent(self):
|
||||
"""
|
||||
Returns the extent of this geometry as a 4-tuple, consisting of
|
||||
(xmin, ymin, xmax, ymax).
|
||||
"""
|
||||
env = self.envelope
|
||||
if isinstance(env, Point):
|
||||
xmin, ymin = env.tuple
|
||||
xmax, ymax = xmin, ymin
|
||||
else:
|
||||
xmin, ymin = env[0][0]
|
||||
xmax, ymax = env[0][2]
|
||||
return (xmin, ymin, xmax, ymax)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""
|
||||
|
@ -85,9 +85,13 @@ class GeometryCollection(GEOSGeometry):
|
||||
@property
|
||||
def kml(self):
|
||||
"Returns the KML for this Geometry Collection."
|
||||
kml = '<MultiGeometry>'
|
||||
for g in self: kml += g.kml
|
||||
return kml + '</MultiGeometry>'
|
||||
return '<MultiGeometry>%s</MultiGeometry>' % ''.join([g.kml for g in self])
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Returns a tuple of all the coordinates in this Geometry Collection"
|
||||
return tuple([g.tuple for g in self])
|
||||
coords = tuple
|
||||
|
||||
# MultiPoint, MultiLineString, and MultiPolygon class definitions.
|
||||
class MultiPoint(GeometryCollection):
|
||||
|
@ -153,14 +153,12 @@ class GEOSCoordSeq(object):
|
||||
# a Z dimension.
|
||||
if self.hasz: substr = '%s,%s,%s '
|
||||
else: substr = '%s,%s,0 '
|
||||
kml = '<coordinates>'
|
||||
for i in xrange(len(self)):
|
||||
kml += substr % self[i]
|
||||
return kml.strip() + '</coordinates>'
|
||||
return '<coordinates>%s</coordinates>' % \
|
||||
''.join([substr % self[i] for i in xrange(len(self))]).strip()
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Returns a tuple version of this coordinate sequence."
|
||||
n = self.size
|
||||
if n == 1: return self[0]
|
||||
else: return tuple(self[i] for i in xrange(n))
|
||||
else: return tuple([self[i] for i in xrange(n)])
|
||||
|
@ -4,7 +4,6 @@
|
||||
GEOSGeometry.
|
||||
"""
|
||||
from ctypes import c_uint, byref
|
||||
from types import FloatType, IntType, ListType, TupleType
|
||||
from django.contrib.gis.geos.base import GEOSGeometry
|
||||
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
|
||||
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
|
||||
@ -24,15 +23,15 @@ class Point(GEOSGeometry):
|
||||
>>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters
|
||||
"""
|
||||
|
||||
if isinstance(x, (TupleType, ListType)):
|
||||
if isinstance(x, (tuple, list)):
|
||||
# Here a tuple or list was passed in under the `x` parameter.
|
||||
ndim = len(x)
|
||||
if ndim < 2 or ndim > 3:
|
||||
raise TypeError('Invalid sequence parameter: %s' % str(x))
|
||||
coords = x
|
||||
elif isinstance(x, (IntType, FloatType)) and isinstance(y, (IntType, FloatType)):
|
||||
elif isinstance(x, (int, float, long)) and isinstance(y, (int, float, long)):
|
||||
# Here X, Y, and (optionally) Z were passed in individually, as parameters.
|
||||
if isinstance(z, (IntType, FloatType)):
|
||||
if isinstance(z, (int, float, long)):
|
||||
ndim = 3
|
||||
coords = [x, y, z]
|
||||
else:
|
||||
@ -103,7 +102,7 @@ class Point(GEOSGeometry):
|
||||
|
||||
# The tuple and coords properties
|
||||
tuple = property(get_coords, set_coords)
|
||||
coords = property(get_coords, set_coords)
|
||||
coords = tuple
|
||||
|
||||
class LineString(GEOSGeometry):
|
||||
|
||||
@ -124,7 +123,7 @@ class LineString(GEOSGeometry):
|
||||
if len(args) == 1: coords = args[0]
|
||||
else: coords = args
|
||||
|
||||
if isinstance(coords, (TupleType, ListType)):
|
||||
if isinstance(coords, (tuple, list)):
|
||||
# Getting the number of coords and the number of dimensions -- which
|
||||
# must stay the same, e.g., no LineString((1, 2), (1, 2, 3)).
|
||||
ncoords = len(coords)
|
||||
@ -133,7 +132,7 @@ class LineString(GEOSGeometry):
|
||||
self._checkdim(ndim)
|
||||
# Incrementing through each of the coordinates and verifying
|
||||
for i in xrange(1, ncoords):
|
||||
if not isinstance(coords[i], (TupleType, ListType, Point)):
|
||||
if not isinstance(coords[i], (tuple, list, Point)):
|
||||
raise TypeError('each coordinate should be a sequence (list or tuple)')
|
||||
if len(coords[i]) != ndim: raise TypeError('Dimension mismatch.')
|
||||
numpy_coords = False
|
||||
@ -193,6 +192,7 @@ class LineString(GEOSGeometry):
|
||||
def tuple(self):
|
||||
"Returns a tuple version of the geometry from the coordinate sequence."
|
||||
return self._cs.tuple
|
||||
coords = tuple
|
||||
|
||||
def _listarr(self, func):
|
||||
"""
|
||||
@ -236,12 +236,17 @@ class Polygon(GEOSGeometry):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initializes on an exterior ring and a sequence of holes (both
|
||||
instances of LinearRings.
|
||||
instances may be either LinearRing instances, or a tuple/list
|
||||
that may be constructed into a LinearRing).
|
||||
|
||||
Below are some examples of initialization, where shell, hole1, and
|
||||
hole2 are valid LinearRing geometries:
|
||||
Examples of initialization, where shell, hole1, and hole2 are
|
||||
valid LinearRing geometries:
|
||||
>>> poly = Polygon(shell, hole1, hole2)
|
||||
>>> poly = Polygon(shell, (hole1, hole2))
|
||||
|
||||
Example where a tuple parameters are used:
|
||||
>>> poly = Polygon(((0, 0), (0, 10), (10, 10), (0, 10), (0, 0)),
|
||||
((4, 4), (4, 6), (6, 6), (6, 4), (4, 4)))
|
||||
"""
|
||||
if not args:
|
||||
raise TypeError('Must provide at list one LinearRing instance to initialize Polygon.')
|
||||
@ -249,32 +254,38 @@ class Polygon(GEOSGeometry):
|
||||
# Getting the ext_ring and init_holes parameters from the argument list
|
||||
ext_ring = args[0]
|
||||
init_holes = args[1:]
|
||||
if len(init_holes) == 1 and isinstance(init_holes[0], (TupleType, ListType)):
|
||||
n_holes = len(init_holes)
|
||||
|
||||
# If initialized as Polygon(shell, (LinearRing, LinearRing)) [for backward-compatibility]
|
||||
if n_holes == 1 and isinstance(init_holes[0], (tuple, list)) and \
|
||||
(len(init_holes[0]) == 0 or isinstance(init_holes[0][0], LinearRing)):
|
||||
init_holes = init_holes[0]
|
||||
n_holes = len(init_holes)
|
||||
|
||||
# Ensuring the exterior ring parameter is a LinearRing object
|
||||
if not isinstance(ext_ring, LinearRing):
|
||||
raise TypeError('First argument for Polygon initialization must be a LinearRing.')
|
||||
# Ensuring the exterior ring and holes parameters are LinearRing objects
|
||||
# or may be instantiated into LinearRings.
|
||||
ext_ring = self._construct_ring(ext_ring, 'Exterior parameter must be a LinearRing or an object that can initialize a LinearRing.')
|
||||
holes_list = [] # Create new list, cause init_holes is a tuple.
|
||||
for i in xrange(n_holes):
|
||||
holes_list.append(self._construct_ring(init_holes[i], 'Holes parameter must be a sequence of LinearRings or objects that can initialize to LinearRings'))
|
||||
|
||||
# Making sure all of the holes are LinearRing objects
|
||||
if False in [isinstance(hole, LinearRing) for hole in init_holes]:
|
||||
raise TypeError('Holes parameter must be a sequence of LinearRings.')
|
||||
# Why another loop? Because if a TypeError is raised, cloned pointers will
|
||||
# be around that can't be cleaned up.
|
||||
holes = get_pointer_arr(n_holes)
|
||||
for i in xrange(n_holes): holes[i] = geom_clone(holes_list[i].ptr)
|
||||
|
||||
# Getting the holes array.
|
||||
nholes = len(init_holes)
|
||||
holes = get_pointer_arr(nholes)
|
||||
for i in xrange(nholes): holes[i] = geom_clone(init_holes[i].ptr)
|
||||
|
||||
# Getting the shell pointer address,
|
||||
# Getting the shell pointer address.
|
||||
shell = geom_clone(ext_ring.ptr)
|
||||
|
||||
# Calling with the GEOS createPolygon factory.
|
||||
super(Polygon, self).__init__(create_polygon(shell, byref(holes), c_uint(nholes)), **kwargs)
|
||||
super(Polygon, self).__init__(create_polygon(shell, byref(holes), c_uint(n_holes)), **kwargs)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Returns the ring at the specified index. The first index, 0, will always
|
||||
return the exterior ring. Indices > 0 will return the interior ring.
|
||||
Returns the ring at the specified index. The first index, 0, will
|
||||
always return the exterior ring. Indices > 0 will return the
|
||||
interior ring at the given index (e.g., poly[1] and poly[2] would
|
||||
return the first and second interior ring, respectively).
|
||||
"""
|
||||
if index == 0:
|
||||
return self.exterior_ring
|
||||
@ -330,6 +341,15 @@ class Polygon(GEOSGeometry):
|
||||
if index < 0 or index >= len(self):
|
||||
raise GEOSIndexError('invalid Polygon ring index: %s' % index)
|
||||
|
||||
def _construct_ring(self, param, msg=''):
|
||||
"Helper routine for trying to construct a ring from the given parameter."
|
||||
if isinstance(param, LinearRing): return param
|
||||
try:
|
||||
ring = LinearRing(param)
|
||||
return ring
|
||||
except TypeError:
|
||||
raise TypeError(msg)
|
||||
|
||||
def get_interior_ring(self, ring_i):
|
||||
"""
|
||||
Gets the interior ring at the specified index, 0 is for the first
|
||||
@ -355,18 +375,17 @@ class Polygon(GEOSGeometry):
|
||||
|
||||
# properties for the exterior ring/shell
|
||||
exterior_ring = property(get_ext_ring, set_ext_ring)
|
||||
shell = property(get_ext_ring, set_ext_ring)
|
||||
shell = exterior_ring
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
"Gets the tuple for each ring in this Polygon."
|
||||
return tuple(self[i].tuple for i in xrange(len(self)))
|
||||
return tuple([self[i].tuple for i in xrange(len(self))])
|
||||
coords = tuple
|
||||
|
||||
@property
|
||||
def kml(self):
|
||||
"Returns the KML representation of this Polygon."
|
||||
inner_kml = ''
|
||||
if self.num_interior_rings > 0:
|
||||
for i in xrange(self.num_interior_rings):
|
||||
inner_kml += "<innerBoundaryIs>%s</innerBoundaryIs>" % self[i+1].kml
|
||||
inner_kml = ''.join(["<innerBoundaryIs>%s</innerBoundaryIs>" % self[i+1].kml
|
||||
for i in xrange(self.num_interior_rings)])
|
||||
return "<Polygon><outerBoundaryIs>%s</outerBoundaryIs>%s</Polygon>" % (self[0].kml, inner_kml)
|
||||
|
@ -5,7 +5,7 @@ from django.contrib.gis.geos.base import HAS_GDAL
|
||||
from django.contrib.gis.tests.geometries import *
|
||||
|
||||
if HAS_NUMPY: from numpy import array
|
||||
if HAS_GDAL: from django.contrib.gis.gdal import OGRGeometry, SpatialReference, CoordTransform
|
||||
if HAS_GDAL: from django.contrib.gis.gdal import OGRGeometry, SpatialReference, CoordTransform, GEOJSON
|
||||
|
||||
class GEOSTest(unittest.TestCase):
|
||||
|
||||
@ -85,7 +85,16 @@ class GEOSTest(unittest.TestCase):
|
||||
self.assertEqual(srid, poly.shell.srid)
|
||||
self.assertEqual(srid, fromstr(poly.ewkt).srid) # Checking export
|
||||
|
||||
def test01i_eq(self):
|
||||
def test01i_json(self):
|
||||
"Testing GeoJSON input/output (via GDAL)."
|
||||
if not HAS_GDAL or not GEOJSON: return
|
||||
for g in json_geoms:
|
||||
geom = GEOSGeometry(g.wkt)
|
||||
self.assertEqual(g.json, geom.json)
|
||||
self.assertEqual(g.json, geom.geojson)
|
||||
self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json))
|
||||
|
||||
def test01j_eq(self):
|
||||
"Testing equivalence with WKT."
|
||||
p = fromstr('POINT(5 23)')
|
||||
self.assertEqual(p, p.wkt)
|
||||
@ -268,7 +277,7 @@ class GEOSTest(unittest.TestCase):
|
||||
|
||||
# Testing __getitem__ and __setitem__ on invalid indices
|
||||
self.assertRaises(GEOSIndexError, poly.__getitem__, len(poly))
|
||||
#self.assertRaises(GEOSIndexError, poly.__setitem__, len(poly), False)
|
||||
self.assertRaises(GEOSIndexError, poly.__setitem__, len(poly), False)
|
||||
self.assertRaises(GEOSIndexError, poly.__getitem__, -1)
|
||||
|
||||
# Testing __iter__
|
||||
@ -279,8 +288,16 @@ class GEOSTest(unittest.TestCase):
|
||||
# Testing polygon construction.
|
||||
self.assertRaises(TypeError, Polygon.__init__, 0, [1, 2, 3])
|
||||
self.assertRaises(TypeError, Polygon.__init__, 'foo')
|
||||
|
||||
# Polygon(shell, (hole1, ... holeN))
|
||||
rings = tuple(r for r in poly)
|
||||
self.assertEqual(poly, Polygon(rings[0], rings[1:]))
|
||||
|
||||
# Polygon(shell_tuple, hole_tuple1, ... , hole_tupleN)
|
||||
ring_tuples = tuple(r.tuple for r in poly)
|
||||
self.assertEqual(poly, Polygon(*ring_tuples))
|
||||
|
||||
# Constructing with tuples of LinearRings.
|
||||
self.assertEqual(poly.wkt, Polygon(*tuple(r for r in poly)).wkt)
|
||||
self.assertEqual(poly.wkt, Polygon(*tuple(LinearRing(r.tuple) for r in poly)).wkt)
|
||||
|
||||
@ -686,12 +703,27 @@ class GEOSTest(unittest.TestCase):
|
||||
t2.transform(SpatialReference('EPSG:2774'))
|
||||
ct = CoordTransform(SpatialReference('WGS84'), SpatialReference(2774))
|
||||
t3.transform(ct)
|
||||
|
||||
for p in (t1, t2, t3):
|
||||
prec = 3
|
||||
for p in (t1, t2, t3):
|
||||
self.assertAlmostEqual(trans.x, p.x, prec)
|
||||
self.assertAlmostEqual(trans.y, p.y, prec)
|
||||
|
||||
def test24_extent(self):
|
||||
"Testing `extent` method."
|
||||
# The xmin, ymin, xmax, ymax of the MultiPoint should be returned.
|
||||
mp = MultiPoint(Point(5, 23), Point(0, 0), Point(10, 50))
|
||||
self.assertEqual((0.0, 0.0, 10.0, 50.0), mp.extent)
|
||||
pnt = Point(5.23, 17.8)
|
||||
# Extent of points is just the point itself repeated.
|
||||
self.assertEqual((5.23, 17.8, 5.23, 17.8), pnt.extent)
|
||||
# Testing on the 'real world' Polygon.
|
||||
poly = fromstr(polygons[3].wkt)
|
||||
ring = poly.shell
|
||||
x, y = ring.x, ring.y
|
||||
xmin, ymin = min(x), min(y)
|
||||
xmax, ymax = max(x), max(y)
|
||||
self.assertEqual((xmin, ymin, xmax, ymax), poly.extent)
|
||||
|
||||
def suite():
|
||||
s = unittest.TestSuite()
|
||||
s.addTest(unittest.makeSuite(GEOSTest))
|
||||
|
Loading…
x
Reference in New Issue
Block a user