1<?php
2
3namespace geoPHP\Geometry;
4
5use geoPHP\Exception\InvalidGeometryException;
6
7/**
8 * A Point is a 0-dimensional geometric object and represents a single location in coordinate space.
9 * A Point has an x-coordinate value, a y-coordinate value.
10 * If called for by the associated Spatial Reference System, it may also have coordinate values for z and m.
11 */
12class Point extends Geometry
13{
14
15    protected \$x = null;
16
17    protected \$y = null;
18
19    protected \$z = null;
20
21    protected \$m = null;
22
23    /**
24     * Constructor
25     *
26     * @param int|float|null \$x The x coordinate (or longitude)
27     * @param int|float|null \$y The y coordinate (or latitude)
28     * @param int|float|null \$z The z coordinate (or altitude) - optional
29     * @param int|float|null \$m Measure - optional
30     * @throws \Exception
31     */
32    public function __construct(\$x = null, \$y = null, \$z = null, \$m = null)
33    {
34        // If X or Y is null than it is an empty point
35        if (\$x !== null && \$y !== null) {
36            // Basic validation on x and y
37            if (!is_numeric(\$x) || !is_numeric(\$y)) {
38                throw new InvalidGeometryException("Cannot construct Point. x and y should be numeric");
39            }
40
41            // Convert to float in case they are passed in as a string or integer etc.
42            \$this->x = floatval(\$x);
43            \$this->y = floatval(\$y);
44        }
45
46        // Check to see if this point has Z (height) value
47        if (\$z !== null) {
48            if (!is_numeric(\$z)) {
49                throw new InvalidGeometryException("Cannot construct Point. z should be numeric");
50            }
51            \$this->hasZ = true;
52            \$this->z = \$this->x !== null ? floatval(\$z) : null;
53        }
54
55        // Check to see if this is a measure
56        if (\$m !== null) {
57            if (!is_numeric(\$m)) {
58                throw new InvalidGeometryException("Cannot construct Point. m should be numeric");
59            }
60            \$this->isMeasured = true;
61            \$this->m = \$this->x !== null ? floatval(\$m) : null;
62        }
63    }
64
65    /**
66     * @param array \$coordinates
67     * @return Point
68     * @throws \Exception
69     */
70    public static function fromArray(\$coordinates)
71    {
72        /** @noinspection PhpIncompatibleReturnTypeInspection */
73        return (new \ReflectionClass(get_called_class()))->newInstanceArgs(\$coordinates);
74    }
75
76    public function geometryType()
77    {
78        return Geometry::POINT;
79    }
80
81    public function dimension()
82    {
83        return 0;
84    }
85
86    /**
87     * Get X (longitude) coordinate
88     *
89     * @return float The X coordinate
90     */
91    public function x()
92    {
93        return \$this->x;
94    }
95
96    /**
97     * Returns Y (latitude) coordinate
98     *
99     * @return float The Y coordinate
100     */
101    public function y()
102    {
103        return \$this->y;
104    }
105
106    /**
107     * Returns Z (altitude) coordinate
108     *
109     * @return float The Z coordinate or NULL is not a 3D point
110     */
111    public function z()
112    {
113        return \$this->z;
114    }
115
116    /**
117     * Returns M (measured) value
118     *
119     * @return float The measured value
120     */
121    public function m()
122    {
123        return \$this->m;
124    }
125
126    /**
127     * Inverts x and y coordinates
128     * Useful with old applications still using lng lat
129     *
130     * @return self
131     * */
132    public function invertXY()
133    {
134        \$x = \$this->x;
135        \$this->x = \$this->y;
136        \$this->y = \$x;
137        \$this->setGeos(null);
138        return \$this;
139    }
140
141    // A point's centroid is itself
142    public function centroid()
143    {
144        return \$this;
145    }
146
147    public function getBBox()
148    {
149        return [
150                'maxy' => \$this->y(),
151                'miny' => \$this->y(),
152                'maxx' => \$this->x(),
153                'minx' => \$this->x(),
154        ];
155    }
156
157    /**
158     * @return array
159     */
160    public function asArray()
161    {
162        if (\$this->isEmpty()) {
163            return [NAN, NAN];
164        }
165        if (!\$this->hasZ && !\$this->isMeasured) {
166            return [\$this->x, \$this->y];
167        }
168        if (\$this->hasZ && \$this->isMeasured) {
169            return [\$this->x, \$this->y, \$this->z, \$this->m];
170        }
171        if (\$this->hasZ) {
172            return [\$this->x, \$this->y, \$this->z];
173        }
174        // if (\$this->isMeasured)
175        return [\$this->x, \$this->y, null, \$this->m];
176    }
177
178    /**
179     * The boundary of a Point is the empty set.
180     * @return GeometryCollection
181     */
182    public function boundary()
183    {
184        return new GeometryCollection();
185    }
186
187    /**
188     * @return bool
189     */
190    public function isEmpty()
191    {
192        return \$this->x === null;
193    }
194
195    /**
196     * @return int Returns always 1
197     */
198    public function numPoints()
199    {
200        return 1;
201    }
202
203    /**
204     * @return Point[]
205     */
206    public function getPoints()
207    {
208        return [\$this];
209    }
210
211    /**
212     * @return Point[]
213     */
214    public function getComponents()
215    {
216        return [\$this];
217    }
218
219    /**
220     * Determines weather the specified geometry is spatially equal to this Point
221     *
222     * Because of limited floating point precision in PHP, equality can be only approximated
223
224
225     *
226     * @param Point|Geometry \$geometry
227     *
228     * @return boolean
229     */
230    public function equals(\$geometry)
231    {
232        return \$geometry->geometryType() === Geometry::POINT
233            ? (abs(\$this->x() - \$geometry->x()) <= 1.0E-9 && abs(\$this->y() - \$geometry->y()) <= 1.0E-9)
234            : false;
235    }
236
237    public function isSimple()
238    {
239        return true;
240    }
241
242    public function flatten()
243    {
244        \$this->z = null;
245        \$this->m = null;
246        \$this->hasZ = false;
247        \$this->isMeasured = false;
248        \$this->setGeos(null);
249    }
250
251    /**
252     * @param Geometry|Collection \$geometry
253     * @return float|null
254     */
255    public function distance(\$geometry)
256    {
257        if (\$this->isEmpty() || \$geometry->isEmpty()) {
258            return null;
259        }
260        if (\$this->getGeos()) {
261            // @codeCoverageIgnoreStart
262            /** @noinspection PhpUndefinedMethodInspection */
263            return \$this->getGeos()->distance(\$geometry->getGeos());
264            // @codeCoverageIgnoreEnd
265        }
266        if (\$geometry->geometryType() == Geometry::POINT) {
267            return sqrt(
268                pow((\$this->x() - \$geometry->x()), 2)
269                + pow((\$this->y() - \$geometry->y()), 2)
270            );
271        }
272        if (\$geometry instanceof MultiGeometry) {
273            \$distance = null;
274            foreach (\$geometry->getComponents() as \$component) {
275                \$checkDistance = \$this->distance(\$component);
276                if (\$checkDistance === 0.0) {
277                    return 0.0;
278                }
279                if (\$checkDistance === null) {
280                    continue;
281                }
282                if (\$distance === null || \$checkDistance < \$distance) {
283                    \$distance = \$checkDistance;
284                }
285            }
286            return \$distance;
287        } else {
288            // For LineString, Polygons, MultiLineString and MultiPolygon. the nearest point might be a vertex,
289            // but it could also be somewhere along a line-segment that makes up the geometry (between vertices).
290            // Here we brute force check all line segments that make up these geometries
291            \$distance = null;
292            foreach (\$geometry->explode(true) as \$seg) {
293
294
295                \$x1 = \$seg[0]->x();
296                \$y1 = \$seg[0]->y();
297                \$x2 = \$seg[1]->x();
298                \$y2 = \$seg[1]->y();
299                \$px = \$x2 - \$x1;
300                \$py = \$y2 - \$y1;
301                \$d = (\$px * \$px) + (\$py * \$py);
302                if (\$d == 0) {
303                    // Line-segment's endpoints are identical. This is merely a point masquerading as a line-segment.
304                    \$checkDistance = \$this->distance(\$seg[1]);
305                } else {
306                    \$x3 = \$this->x();
307                    \$y3 = \$this->y();
308                    \$u =  (((\$x3 - \$x1) * \$px) + ((\$y3 - \$y1) * \$py)) / \$d;
309                    if (\$u > 1) {
310                        \$u = 1;
311                    }
312                    if (\$u < 0) {
313                        \$u = 0;
314                    }
315                    \$x = \$x1 + (\$u * \$px);
316                    \$y = \$y1 + (\$u * \$py);
317                    \$dx = \$x - \$x3;
318                    \$dy = \$y - \$y3;
319                    \$checkDistance = sqrt((\$dx * \$dx) + (\$dy * \$dy));
320                }
321                if (\$checkDistance === 0.0) {
322                    return 0.0;
323                }
324                if (\$distance === null || \$checkDistance < \$distance) {
325                    \$distance = \$checkDistance;
326                }
327            }
328            return \$distance;
329        }
330    }
331
332    public function minimumZ()
333    {
334        return \$this->hasZ ? \$this->z() : null;
335    }
336
337    public function maximumZ()
338    {
339        return \$this->hasZ ? \$this->z() : null;
340    }
341
342    public function minimumM()
343    {
344        return \$this->isMeasured ? \$this->m() : null;
345    }
346
347    public function maximumM()
348    {
349        return \$this->isMeasured ? \$this->m() : null;
350    }
351
352    /* The following methods are not valid for this geometry type */
353
354    public function area()
355    {
356        return 0.0;
357    }
358
359    public function length()
360    {
361        return 0.0;
362    }
363
364    public function length3D()
365    {
366        return 0.0;
367    }
368
369    public function greatCircleLength(\$radius = null)
370    {
371        return 0.0;
372    }
373
374    public function haversineLength()
375    {
376        return 0.0;
377    }
378
379    public function zDifference()
380    {
381        return null;
382    }
383
384    public function elevationGain(\$verticalTolerance = 0)
385    {
386        return null;
387    }
388
389    public function elevationLoss(\$verticalTolerance = 0)
390    {
391        return null;
392    }
393
394    public function numGeometries()
395    {
396        return null;
397    }
398
399    public function geometryN(\$n)
400    {
401        return null;
402    }
403
404    public function startPoint()
405    {
406        return null;
407    }
408
409    public function endPoint()
410    {
411        return null;
412    }
413
414    public function isRing()
415    {
416        return null;
417    }
418
419    public function isClosed()
420    {
421        return null;
422    }
423
424    public function pointN(\$n)
425    {
426        return null;
427    }
428
429    public function exteriorRing()
430    {
431        return null;
432    }
433
434    public function numInteriorRings()
435    {
436        return null;
437    }
438
439    public function interiorRingN(\$n)
440    {
441        return null;
442    }
443
444    /**
445     * @param bool|false \$toArray
446     * @return null
447     */
448    public function explode(\$toArray = false)
449    {
450        return null;
451    }
452}
453