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     * @see: http://php.net/manual/en/function.bccomp.php
224     * @see: http://php.net/manual/en/language.types.float.php
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                // As per http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
294                // and http://paulbourke.net/geometry/pointline/
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