1from django.contrib.gis.geos import prototypes as capi
2from django.contrib.gis.geos.coordseq import GEOSCoordSeq
3from django.contrib.gis.geos.error import GEOSException
4from django.contrib.gis.geos.geometry import GEOSGeometry, LinearGeometryMixin
5from django.contrib.gis.geos.point import Point
6from django.contrib.gis.shortcuts import numpy
7
8
9class LineString(LinearGeometryMixin, GEOSGeometry):
10 _init_func = capi.create_linestring
11 _minlength = 2
12 has_cs = True
13
14 def __init__(self, *args, **kwargs):
15 """
16 Initialize on the given sequence -- may take lists, tuples, NumPy arrays
17 of X,Y pairs, or Point objects. If Point objects are used, ownership is
18 _not_ transferred to the LineString object.
19
20 Examples:
21 ls = LineString((1, 1), (2, 2))
22 ls = LineString([(1, 1), (2, 2)])
23 ls = LineString(array([(1, 1), (2, 2)]))
24 ls = LineString(Point(1, 1), Point(2, 2))
25 """
26 # If only one argument provided, set the coords array appropriately
27 if len(args) == 1:
28 coords = args[0]
29 else:
30 coords = args
31
32 if not (
33 isinstance(coords, (tuple, list))
34 or numpy
35 and isinstance(coords, numpy.ndarray)
36 ):
37 raise TypeError("Invalid initialization input for LineStrings.")
38
39 # If SRID was passed in with the keyword arguments
40 srid = kwargs.get("srid")
41
42 ncoords = len(coords)
43 if not ncoords:
44 super().__init__(self._init_func(None), srid=srid)
45 return
46
47 if ncoords < self._minlength:
48 raise ValueError(
49 "%s requires at least %d points, got %s."
50 % (
51 self.__class__.__name__,
52 self._minlength,
53 ncoords,
54 )
55 )
56
57 numpy_coords = not isinstance(coords, (tuple, list))
58 if numpy_coords:
59 shape = coords.shape # Using numpy's shape.
60 if len(shape) != 2:
61 raise TypeError("Too many dimensions.")
62 self._checkdim(shape[1])
63 ndim = shape[1]
64 else:
65 # Getting the number of coords and the number of dimensions -- which
66 # must stay the same, e.g., no LineString((1, 2), (1, 2, 3)).
67 ndim = None
68 # Incrementing through each of the coordinates and verifying
69 for coord in coords:
70 if not isinstance(coord, (tuple, list, Point)):
71 raise TypeError(
72 "Each coordinate should be a sequence (list or tuple)"
73 )
74
75 if ndim is None:
76 ndim = len(coord)
77 self._checkdim(ndim)
78 elif len(coord) != ndim:
79 raise TypeError("Dimension mismatch.")
80
81 # Creating a coordinate sequence object because it is easier to
82 # set the points using its methods.
83 cs = GEOSCoordSeq(capi.create_cs(ncoords, ndim), z=bool(ndim == 3))
84 point_setter = cs._set_point_3d if ndim == 3 else cs._set_point_2d
85
86 for i in range(ncoords):
87 if numpy_coords:
88 point_coords = coords[i, :]
89 elif isinstance(coords[i], Point):
90 point_coords = coords[i].tuple
91 else:
92 point_coords = coords[i]
93 point_setter(i, point_coords)
94
95 # Calling the base geometry initialization with the returned pointer
96 # from the function.
97 super().__init__(self._init_func(cs.ptr), srid=srid)
98
99 def __iter__(self):
100 "Allow iteration over this LineString."
101 for i in range(len(self)):
102 yield self[i]
103
104 def __len__(self):
105 "Return the number of points in this LineString."
106 return len(self._cs)
107
108 def _get_single_external(self, index):
109 return self._cs[index]
110
111 _get_single_internal = _get_single_external
112
113 def _set_list(self, length, items):
114 ndim = self._cs.dims
115 hasz = self._cs.hasz # I don't understand why these are different
116 srid = self.srid
117
118 # create a new coordinate sequence and populate accordingly
119 cs = GEOSCoordSeq(capi.create_cs(length, ndim), z=hasz)
120 for i, c in enumerate(items):
121 cs[i] = c
122
123 ptr = self._init_func(cs.ptr)
124 if ptr:
125 capi.destroy_geom(self.ptr)
126 self.ptr = ptr
127 if srid is not None:
128 self.srid = srid
129 self._post_init()
130 else:
131 # can this happen?
132 raise GEOSException("Geometry resulting from slice deletion was invalid.")
133
134 def _set_single(self, index, value):
135 self._cs[index] = value
136
137 def _checkdim(self, dim):
138 if dim not in (2, 3):
139 raise TypeError("Dimension mismatch.")
140
141 # #### Sequence Properties ####
142 @property
143 def tuple(self):
144 "Return a tuple version of the geometry from the coordinate sequence."
145 return self._cs.tuple
146
147 coords = tuple
148
149 def _listarr(self, func):
150 """
151 Return a sequence (list) corresponding with the given function.
152 Return a numpy array if possible.
153 """
154 lst = [func(i) for i in range(len(self))]
155 if numpy:
156 return numpy.array(lst) # ARRRR!
157 else:
158 return lst
159
160 @property
161 def array(self):
162 "Return a numpy array for the LineString."
163 return self._listarr(self._cs.__getitem__)
164
165 @property
166 def x(self):
167 "Return a list or numpy array of the X variable."
168 return self._listarr(self._cs.getX)
169
170 @property
171 def y(self):
172 "Return a list or numpy array of the Y variable."
173 return self._listarr(self._cs.getY)
174
175 @property
176 def z(self):
177 "Return a list or numpy array of the Z variable."
178 if not self.hasz:
179 return None
180 else:
181 return self._listarr(self._cs.getZ)
182
183
184# LinearRings are LineStrings used within Polygons.
185class LinearRing(LineString):
186 _minlength = 4
187 _init_func = capi.create_linearring
188
189 @property
190 def is_counterclockwise(self):
191 if self.empty:
192 raise ValueError("Orientation of an empty LinearRing cannot be determined.")
193 return self._cs.is_counterclockwise