1"""Points and related utilities
2"""
3
4from ctypes import c_double
5import warnings
6
7from shapely.coords import CoordinateSequence
8from shapely.errors import DimensionError, ShapelyDeprecationWarning
9from shapely.geos import lgeos
10from shapely.geometry.base import BaseGeometry, geos_geom_from_py
11from shapely.geometry.proxy import CachingGeometryProxy
12
13__all__ = ['Point', 'asPoint']
14
15
16class Point(BaseGeometry):
17    """
18    A zero dimensional feature
19
20    A point has zero length and zero area.
21
22    Attributes
23    ----------
24    x, y, z : float
25        Coordinate values
26
27    Example
28    -------
29      >>> p = Point(1.0, -1.0)
30      >>> print(p)
31      POINT (1 -1)
32      >>> p.y
33      -1.0
34      >>> p.x
35      1.0
36    """
37
38    def __init__(self, *args):
39        """
40        Parameters
41        ----------
42        There are 2 cases:
43
44        1) 1 parameter: this must satisfy the numpy array protocol.
45        2) 2 or more parameters: x, y, z : float
46            Easting, northing, and elevation.
47        """
48        BaseGeometry.__init__(self)
49        if len(args) > 0:
50            if len(args) == 1:
51                geom, n = geos_point_from_py(args[0])
52            elif len(args) > 3:
53                raise TypeError(
54                    "Point() takes at most 3 arguments ({} given)".format(len(args))
55                )
56            else:
57                geom, n = geos_point_from_py(tuple(args))
58            self._set_geom(geom)
59            self._ndim = n
60
61    # Coordinate getters and setters
62
63    @property
64    def x(self):
65        """Return x coordinate."""
66        return self.coords[0][0]
67
68    @property
69    def y(self):
70        """Return y coordinate."""
71        return self.coords[0][1]
72
73    @property
74    def z(self):
75        """Return z coordinate."""
76        if self._ndim != 3:
77            raise DimensionError("This point has no z coordinate.")
78        return self.coords[0][2]
79
80    @property
81    def __geo_interface__(self):
82        return {
83            'type': 'Point',
84            'coordinates': self.coords[0]
85            }
86
87    def svg(self, scale_factor=1., fill_color=None, opacity=None):
88        """Returns SVG circle element for the Point geometry.
89
90        Parameters
91        ==========
92        scale_factor : float
93            Multiplication factor for the SVG circle diameter.  Default is 1.
94        fill_color : str, optional
95            Hex string for fill color. Default is to use "#66cc99" if
96            geometry is valid, and "#ff3333" if invalid.
97        opacity : float
98            Float number between 0 and 1 for color opacity. Defaul value is 0.6
99        """
100        if self.is_empty:
101            return '<g />'
102        if fill_color is None:
103            fill_color = "#66cc99" if self.is_valid else "#ff3333"
104        if opacity is None:
105            opacity = 0.6
106        return (
107            '<circle cx="{0.x}" cy="{0.y}" r="{1}" '
108            'stroke="#555555" stroke-width="{2}" fill="{3}" opacity="{4}" />'
109            ).format(self, 3. * scale_factor, 1. * scale_factor, fill_color, opacity)
110
111    @property
112    def _ctypes(self):
113        if not self._ctypes_data:
114            array_type = c_double * self._ndim
115            array = array_type()
116            xy = self.coords[0]
117            array[0] = xy[0]
118            array[1] = xy[1]
119            if self._ndim == 3:
120                array[2] = xy[2]
121            self._ctypes_data = array
122        return self._ctypes_data
123
124    def _array_interface(self):
125        """Provide the Numpy array protocol."""
126        if self.is_empty:
127            ai = {'version': 3, 'typestr': '<f8', 'shape': (0,), 'data': (c_double * 0)()}
128        else:
129            ai = self._array_interface_base
130            ai.update({'shape': (self._ndim,)})
131        return ai
132
133    def array_interface(self):
134        """Provide the Numpy array protocol."""
135        warnings.warn(
136            "The 'array_interface' method is deprecated and will be removed "
137            "in Shapely 2.0.",
138            ShapelyDeprecationWarning, stacklevel=2)
139        return self._array_interface()
140
141    @property
142    def __array_interface__(self):
143        warnings.warn(
144            "The array interface is deprecated and will no longer work in "
145            "Shapely 2.0. Convert the '.coords' to a numpy array instead.",
146            ShapelyDeprecationWarning, stacklevel=3)
147        return self._array_interface()
148
149    @property
150    def bounds(self):
151        """Returns minimum bounding region (minx, miny, maxx, maxy)"""
152        try:
153            xy = self.coords[0]
154        except IndexError:
155            return ()
156        return (xy[0], xy[1], xy[0], xy[1])
157
158    # Coordinate access
159
160    def _get_coords(self):
161        """Access to geometry's coordinates (CoordinateSequence)"""
162        return CoordinateSequence(self)
163
164    def _set_coords(self, *args):
165        warnings.warn(
166            "Setting the 'coords' to mutate a Geometry in place is deprecated,"
167            " and will not be possible any more in Shapely 2.0",
168            ShapelyDeprecationWarning, stacklevel=2)
169        self._empty()
170        if len(args) == 1:
171            geom, n = geos_point_from_py(args[0])
172        elif len(args) > 3:
173            raise TypeError("Point() takes at most 3 arguments ({} given)".format(len(args)))
174        else:
175            geom, n = geos_point_from_py(tuple(args))
176        self._set_geom(geom)
177        self._ndim = n
178
179    coords = property(_get_coords, _set_coords)
180
181    @property
182    def xy(self):
183        """Separate arrays of X and Y coordinate values
184
185        Example:
186          >>> x, y = Point(0, 0).xy
187          >>> list(x)
188          [0.0]
189          >>> list(y)
190          [0.0]
191        """
192        return self.coords.xy
193
194
195class PointAdapter(CachingGeometryProxy, Point):
196
197    _other_owned = False
198
199    def __init__(self, context):
200        warnings.warn(
201            "The proxy geometries (through the 'asShape()', 'asPoint()' or "
202            "'PointAdapter()' constructors) are deprecated and will be "
203            "removed in Shapely 2.0. Use the 'shape()' function or the "
204            "standard 'Point()' constructor instead.",
205            ShapelyDeprecationWarning, stacklevel=4)
206        self.context = context
207        self.factory = geos_point_from_py
208
209    @property
210    def _ndim(self):
211        try:
212            # From array protocol
213            array = self.context.__array_interface__
214            n = array['shape'][0]
215            assert n == 2 or n == 3
216            return n
217        except AttributeError:
218            # Fall back on list
219            return len(self.context)
220
221    @property
222    def __array_interface__(self):
223        """Provide the Numpy array protocol."""
224        try:
225            return self.context.__array_interface__
226        except AttributeError:
227            return self.array_interface()
228
229    def _get_coords(self):
230        """Access to geometry's coordinates (CoordinateSequence)"""
231        return CoordinateSequence(self)
232
233    def _set_coords(self, ob):
234        raise NotImplementedError("Adapters can not modify their sources")
235
236    coords = property(_get_coords)
237
238
239def asPoint(context):
240    """Adapt an object to the Point interface"""
241    return PointAdapter(context)
242
243
244def geos_point_from_py(ob, update_geom=None, update_ndim=0):
245    """Create a GEOS geom from an object that is a Point, a coordinate sequence
246    or that provides the array interface.
247
248    Returns the GEOS geometry and the number of its dimensions.
249    """
250    if isinstance(ob, Point):
251        return geos_geom_from_py(ob)
252
253    # Accept either (x, y) or [(x, y)]
254    if not hasattr(ob, '__getitem__'):  # generators
255        ob = list(ob)
256
257    if isinstance(ob[0], tuple):
258        coords = ob[0]
259    else:
260        coords = ob
261    n = len(coords)
262    dx = c_double(coords[0])
263    dy = c_double(coords[1])
264    dz = None
265    if n == 3:
266        dz = c_double(coords[2])
267
268    if update_geom:
269        cs = lgeos.GEOSGeom_getCoordSeq(update_geom)
270        if n != update_ndim:
271            raise ValueError(
272                "Wrong coordinate dimensions; this geometry has dimensions: "
273                "%d" % update_ndim)
274    else:
275        cs = lgeos.GEOSCoordSeq_create(1, n)
276
277    # Because of a bug in the GEOS C API, always set X before Y
278    lgeos.GEOSCoordSeq_setX(cs, 0, dx)
279    lgeos.GEOSCoordSeq_setY(cs, 0, dy)
280    if n == 3:
281        lgeos.GEOSCoordSeq_setZ(cs, 0, dz)
282
283    if update_geom:
284        return None
285    else:
286        return lgeos.GEOSGeom_createPoint(cs), n
287
288
289def update_point_from_py(geom, ob):
290    geos_point_from_py(ob, geom._geom, geom._ndim)
291