Index: geos/geometries.py =================================================================== --- geos/geometries.py (revision 2) +++ geos/geometries.py (revision 7) @@ -156,10 +156,12 @@ # Getting the correct initialization function if kwargs.get('ring', False): - func = create_linearring + self._init_func = create_linearring else: - func = create_linestring + self._init_func = create_linestring + func = self._init_func + # If SRID was passed in with the keyword arguments srid = kwargs.get('srid', None) @@ -168,13 +170,152 @@ super(LineString, self).__init__(func(cs.ptr), srid=srid) def __getitem__(self, index): - "Gets the point at the specified index." - return self._cs[index] + "Gets the coordinates of the point(s) at the specified index/slice." + if isinstance(index, slice): + return [self._cs[i] for i in xrange(*index.indices(len(self._cs)))] + else: + if index < 0: + index += len(self._cs) + return self._cs[index] + def __delitem__(self, index): + "Delete the point(s) at the specified index/slice." + if not isinstance(index, (int, long, slice)): + raise TypeError("%s is not a legal index" % index) + + # calculate new length and dimensions + currLen = len(self._cs) + if isinstance(index, (int, long)): + if index < 0: index += currLen + if index < 0 or currLen <= index: + raise GEOSIndexError('invalid GEOS Geometry index: %d' % index) + indexRange = [index] + else: + indexRange = range(*index.indices(currLen)) + + newLen = currLen - len(indexRange) + ndim = self._cs.dims + hasz = self._cs.hasz # I don't understand why these are different + + # create a new coordinate sequence and populate accordingly + cs = GEOSCoordSeq(create_cs(newLen, ndim), z=hasz) + new_i = 0 + for old_i in xrange(currLen): + if old_i in indexRange: continue + cs[new_i] = self._cs[old_i] + new_i += 1 + + ptr = self._init_func(cs.ptr) + if ptr: + destroy_geom(self.ptr) + self._ptr = ptr + self._post_init(self.srid) + else: + # can this happen? + raise GEOSException('Geometry resulting from slice deletion was invalid.') + def __setitem__(self, index, value): - "Sets the point at the specified index, e.g., line_str[0] = (1, 2)." - self._cs[index] = value + """Sets the point(s) at the specified index/slice, + e.g., line_str[0] = (1, 2).""" + if isinstance(index, slice): + try: + valueIter = iter(value) + except TypeError: + raise TypeError('can only assign an iterable') + # calculate length and dimensions + currLen = len(self._cs) + valueList = list(value) + start, stop, step = index.indices(currLen) + stop = max(0, stop) # stop will be -1 if out-of-bounds + # negative index is given + + # CAREFUL: index.step and step are not the same! + # step will never be None + # + if index.step is None: + # this is a simple slice, can assign slice of any length + # calculate new length + newLen = currLen - stop + start + len(valueList) + ndim = self._cs.dims + hasz = self._cs.hasz # not sure why these are different + + # create a new coordinate sequence and populate accordingly + cs = GEOSCoordSeq(create_cs(newLen, ndim), z=hasz) + new_i = 0 + for old_i in xrange(currLen + 1): + if old_i == start: + for val in valueList: + cs[new_i] = val + new_i += 1 + + if old_i < currLen: + if old_i < start or old_i >= stop: + cs[new_i] = self._cs[old_i] + new_i += 1 + + ptr = self._init_func(cs.ptr) + # Polygon.__setitem__ doesn't check this, but it seems + # that it can't hurt. + if ptr: + destroy_geom(self.ptr) + self._ptr = ptr + self._post_init(self.srid) + else: + raise GEOSException('Geometry resulting from slice deletion was invalid.') + + else: + indexList = range(start, stop, step) + # extended slice, only allow assigning slice of same size + if len(valueList) != len(indexList): + raise ValueError('attempt to assign sequence of size %d ' + 'to extended slice of size %d' + % (len(valueList), len(indexList))) + + # we're not changing the length of the sequence + # we can just iterate the indices and value and set them + for i, val in zip(indexList , valueList): + self._cs[i] = val + + else: + length = len(self._cs) + if index < 0: index += length + self._cs[index] = value + + def append(self, val): + "Standard list append method" + self[len(self):] = [val] + + def extend(self, vals): + "Standard list extend method" + self[len(self):] = vals + + def insert(self, index, val): + "Standard list insert method" + if not isinstance(index, (int, long)): + raise TypeError("%s is not a legal index" % index) + self[index:index] = [val] + + def pop(self, index=-1): + "Standard list pop method" + result = self[index] + del self[index] + return result + + def index(self, val): + "Standard list index method" + for i in xrange(0, len(self)): + if self[i] == val: return i + raise ValueError('%s not in geometry' % str(val)) + + def remove(self, val): + "Standard list remove method" + del self[self.index(val)] + + def count(self): + "Standard list count method" + return len(self) + def __iter__(self): "Allows iteration over this LineString." for i in xrange(len(self)): Index: tests/__init__.py =================================================================== --- tests/__init__.py (revision 2) +++ tests/__init__.py (revision 7) @@ -18,6 +18,7 @@ # Tests that do not require setting up and tearing down a spatial database. test_suite_names = [ 'test_geos', + 'test_geos_pymutable', 'test_measure', ] if HAS_GDAL: Index: tests/test_geos_pymutable.py =================================================================== --- tests/test_geos_pymutable.py (revision 0) +++ tests/test_geos_pymutable.py (revision 7) @@ -0,0 +1,174 @@ +import unittest +from pymutable_geometries import * +from django.contrib.gis.geos.error import GEOSIndexError + +class GEOSPyMutableTest(unittest.TestCase): + ''' + Tests Pythonic Mutability of Python GEOS geometry wrappers + get/set/delitem on a slice, normal list methods + ''' + + def test01_getslice(self): + 'Test getting a slice from a geometry' + for f in getslice_functions(): + for g in slice_geometries(): + self.assertEqual(f(g.coords), f(g.geom), f.__name__) + + def test02_getitem(self): + 'Test getting a single item from a geometry' + for g in slice_geometries(): + for i in seqrange(): + self.assertEqual(g.coords[i], g.geom[i]) + + def test03_getitem_indexException(self): + 'Test get single item with out-of-bounds index' + for g in slice_geometries(): + for i in SEQ_OUT_OF_BOUNDS: + self.assertRaises(GEOSIndexError, lambda: g.geom[i]) + + def test04_delitem_single(self): + 'Test delete single item from a geometry' + for i in seqrange(): + for g in slice_geometries(): + if g.geom.ring and i in SEQ_BOUNDS: continue + del g.coords[i] + del g.geom[i] + self.assertEqual(g.coords , g.geom[:]) + + def test05_delitem_slice(self): + 'Test delete slice from a geometry' + for f in delslice_functions(): + for g in slice_geometries(): + if g.geom.ring and not f.ring: continue + f(g.coords) + f(g.geom) + self.assertEqual(g.coords , g.geom[:], f.__name__) + + def test06_delitem_single_indexException(self): + 'Test delete single item with out-of-bounds index' + def func(x, i): del x[i] + for g in slice_geometries(): + for i in SEQ_OUT_OF_BOUNDS: + self.assertRaises(GEOSIndexError, func, g.geom, i) + + def test07_setitem_single(self): + "Test set single item (make sure we didn't break this)" + for i in seqrange(): + for g in slice_geometries(): + if g.geom.ring and i in SEQ_BOUNDS: continue + g.coords[i] = (3.14159, 3.14159) + g.geom[i] = (3.14159, 3.14159) + self.assertEqual(g.coords , g.geom[:]) + + def test08_setslice_simple(self): + 'Test setting a simple slice of a geometry' + for f in setslice_simple_functions(): + for g in slice_geometries(): + if g.geom.ring and not f.ring: continue + f(g.coords) + f(g.geom) + self.assertEqual(g.coords , g.geom[:], f.__name__) + + def test09_setslice_extended(self): + 'Test setting an extended slice of a geometry' + for f in setslice_extended_functions(): + for g in slice_geometries(): + if g.geom.ring and not f.ring: continue + f(g.coords) + f(g.geom) + self.assertEqual(g.coords , g.geom[:], f.__name__) + + def test10_setslice_extended_mismatched(self): + 'Test setting extended slice with array of mismatched length' + def func(x): x[2:8:2] = [(3.1415, 3.1415)] + for g in slice_geometries(): + self.assertRaises(ValueError, func, g.geom) + + def test11_setitem_single_indexException(self): + 'Test set single item with out-of-bounds index' + def func(x, i): x[i] = (3.1415, 3.1415) + for g in slice_geometries(): + for i in SEQ_OUT_OF_BOUNDS: + self.assertRaises(GEOSIndexError, func, g.geom, i) + + def test12_append(self): + 'Test list method append' + for g in slice_geometries(): + if g.geom.ring: continue + g.geom.append((200.0,200.0)) + g.coords.append((200.0,200.0)) + self.assertEqual(g.coords , g.geom[:]) + + def test13_extend(self): + 'Test list method extend' + for g in slice_geometries(): + points = random_coords(5) + if g.geom.ring: points[-1] = g.coords[0] + g.geom.extend(points) + g.coords.extend(points) + self.assertEqual(g.coords , g.geom[:]) + + def test14_insert(self): + 'Test list method insert' + for i in xrange(*SEQ_OUT_OF_BOUNDS): + for g in slice_geometries(): + if g.geom.ring and i in SEQ_BOUNDS + SEQ_OUT_OF_BOUNDS: + continue + g.geom.insert(i, (3141.5, 3141.5)) + g.coords.insert(i, (3141.5, 3141.5)) + self.assertEqual(g.coords , g.geom[:]) + + def test15_insert_typeError(self): + 'Test list method insert raises error on invalid index' + for g in slice_geometries(): + self.assertRaises(TypeError, g.geom.insert, + 'hi', (3141.5, 3141.5)) + + def test16_pop(self): + 'Test list method pop' + for i in seqrange(): + for g in slice_geometries(): + if g.geom.ring and i in SEQ_BOUNDS + SEQ_OUT_OF_BOUNDS: + continue + self.assertEqual(g.coords.pop(i), g.geom.pop(i)) + + def test16_index(self): + 'Test list method index' + for i in xrange(0, SEQ_LENGTH): + for g in slice_geometries(): + if g.geom.ring and i in SEQ_BOUNDS: continue + p = (200.0, 200.0) + g.geom[i] = p + self.assertEqual(i, g.geom.index(p)) + + def test17_index_ValueError(self): + 'Test list method raises ValueError if value not found' + for g in slice_geometries(): + self.assertRaises(ValueError, g.geom.index, (200.0,200.0)) + + def test18_remove(self): + 'Test list method remove' + for i in xrange(0, SEQ_LENGTH): + for g in slice_geometries(): + if g.geom.ring and i in SEQ_BOUNDS: continue + p = (200.0, 200.0) + g.geom[i] = p + g.coords[i] = p + g.geom.remove(p) + g.coords.remove(p) + self.assertEqual(g.coords, g.geom[:]) + + def test19_count(self): + 'Test list method count' + for g in slice_geometries(): + self.assertEqual(SEQ_LENGTH, g.geom.count()) +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(GEOSPyMutableTest)) + return s + +def run(verbosity=2): + unittest.TextTestRunner(verbosity=verbosity).run(suite()) + +if __name__ == '__main__': + run() Property changes on: tests/test_geos_pymutable.py ___________________________________________________________________ Name: svn:executable + * Index: tests/pymutable_geometries.py =================================================================== --- tests/pymutable_geometries.py (revision 0) +++ tests/pymutable_geometries.py (revision 7) @@ -0,0 +1,125 @@ +from django.contrib.gis.geos import * +from random import random + +SEQ_LENGTH = 10 +SEQ_RANGE = (-1 * SEQ_LENGTH, SEQ_LENGTH) +SEQ_BOUNDS = (-1 * SEQ_LENGTH, -1, 0, SEQ_LENGTH - 1) +SEQ_OUT_OF_BOUNDS = (-1 * SEQ_LENGTH -1 , SEQ_LENGTH) + +def seqrange(): return xrange(*SEQ_RANGE) + +class PyMutTestGeom: + "The Test Geometry class container." + def __init__(self, geom_type, coords, **kwargs): + self.coords = coords + self.geom = geom_type(coords) + + for key, value in kwargs.items(): + setattr(self, key, value) + +def random_coords(length=SEQ_LENGTH, dim=2, rng=(-50,50), type=float, + ring=False, round_coords=True): + if round_coords: + num = lambda: type(round(random() * (rng[1]-rng[0]) + rng[0])) + else: + num = lambda: type(random() * (rng[1]-rng[0]) + rng[0]) + + result = [ tuple( num() for axis in xrange(dim) ) + for index in xrange(length) ] + if ring: + result[-1] = result[0] + + return result + + +def slice_geometries(): + return ( + PyMutTestGeom(LineString, random_coords()), + PyMutTestGeom(LinearRing, random_coords(ring=True)), + ) + +def getslice_functions(): + return ( + lambda x: x[0:4], + lambda x: x[5:-1], + lambda x: x[6:2:-1], + lambda x: x[:], + lambda x: x[:3], + lambda x: x[::2], + lambda x: x[::-4], + lambda x: x[7:7], + lambda x: x[20:], + ) + +def delslice_functions(): + def ds_01(x): del x[0:4] + def ds_02(x): del x[5:-1] + def ds_03(x): del x[6:2:-1] + def ds_04(x): del x[:] # should this be allowed? + def ds_05(x): del x[:3] + def ds_06(x): del x[1:9:2] + def ds_07(x): del x[::-4] + def ds_08(x): del x[7:7] + def ds_09(x): del x[-7:-2] + + return mark_ring(vars(), 'ds_') + +def setslice_extended_functions(): + a = [(1.0,2.0),(1.0,2.0),(1.0,2.0)] + def sse_00(x): x[:3:1] = a + def sse_01(x): x[0:3:1] = a + def sse_02(x): x[2:5:1] = a + def sse_03(x): x[-3::1] = a + def sse_04(x): x[-4:-1:1] = a + def sse_05(x): x[8:5:-1] = a + def sse_06(x): x[-6:-9:-1] = a + def sse_07(x): x[:8:3] = a + def sse_08(x): x[1::3] = a + def sse_09(x): x[-2::-3] = a + def sse_10(x): x[7:1:-2] = a + def sse_11(x): x[2:8:2] = a + + return mark_ring(vars(), 'sse_') + +def setslice_simple_functions(): + a = [(1.0,2.0),(2.0,1.0),(2.0,2.0)] + def ss_00(x): x[:0] = a + def ss_01(x): x[:1] = a + def ss_02(x): x[:2] = a + def ss_03(x): x[:3] = a + def ss_04(x): x[-4:] = a + def ss_05(x): x[-3:] = a + def ss_06(x): x[-2:] = a + def ss_07(x): x[-1:] = a + def ss_08(x): x[5:] = a + def ss_09(x): x[:] = a + def ss_10(x): x[4:4] = a + def ss_11(x): x[4:5] = a + def ss_12(x): x[4:7] = a + def ss_13(x): x[4:8] = a + def ss_14(x): x[20:30] = a + def ss_15(x): x[-13:-8] = a + def ss_16(x): x[-13:-9] = a + def ss_17(x): x[-13:-10] = a + def ss_18(x): x[-13:-11] = a + def ss_19(x): x[10:] = a + + return mark_ring(vars(), 'ss_') + +def mark_ring(locals, name_pat, length=SEQ_LENGTH): + ''' + Accepts an array of functions which perform slice modifications + and labels each function as to whether or not it preserves ring-ness + ''' + func_array = [ val for name, val in locals.items() + if hasattr(val, '__call__') + and name.startswith(name_pat) ] + + for i in xrange(len(func_array)): + a = range(length) + a[-1] = a[0] + func_array[i](a) + ring = len(a) == 0 or (len(a) > 3 and a[-1] == a[0]) + func_array[i].ring = ring + + return func_array Property changes on: tests/pymutable_geometries.py ___________________________________________________________________ Name: svn:executable + *