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