2023-10-10 17:00:18 +08:00

261 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Location;
use Location\Distance\DistanceInterface;
use Location\Formatter\Polygon\FormatterInterface;
class Polygon implements GeometryInterface
{
use GetBoundsTrait;
/**
* @var array<Coordinate>
*/
protected $points = [];
/**
* @param Coordinate $point
*
* @return void
*/
public function addPoint(Coordinate $point): void
{
$this->points[] = $point;
}
/**
* @param array<Coordinate> $points
*/
public function addPoints(array $points): void
{
foreach ($points as $point) {
$this->addPoint($point);
}
}
/**
* @return array<Coordinate>
*/
public function getPoints(): array
{
return $this->points;
}
/**
* Return all polygon point's latitudes.
*
* @return array<float>
*/
public function getLats(): array
{
$lats = [];
foreach ($this->points as $point) {
/** @var Coordinate $point */
$lats[] = $point->getLat();
}
return $lats;
}
/**
* Return all polygon point's longitudes.
*
* @return array<float>
*/
public function getLngs(): array
{
$lngs = [];
foreach ($this->points as $point) {
/** @var Coordinate $point */
$lngs[] = $point->getLng();
}
return $lngs;
}
/**
* @return int
*/
public function getNumberOfPoints(): int
{
return count($this->points);
}
/**
* @param FormatterInterface $formatter
*
* @return string
*/
public function format(FormatterInterface $formatter): string
{
return $formatter->format($this);
}
/**
* @return array<Line>
*/
public function getSegments(): array
{
$length = count($this->points);
$segments = [];
if ($length <= 1) {
return $segments;
}
for ($i = 1; $i < $length; $i++) {
$segments[] = new Line($this->points[$i - 1], $this->points[$i]);
}
// to close the polygon we have to add the final segment between
// the last point and the first point
$segments[] = new Line($this->points[$i - 1], $this->points[0]);
return $segments;
}
/**
* Determine if given geometry is contained inside the polygon. This is
* assumed to be true, if each point of the geometry is inside the polygon.
*
* Edge cases:
*
* - it's not detected when a line between two points is outside the polygon
* - @see contains() for more restrictions
*
* @param GeometryInterface $geometry
*
* @return bool
*/
public function containsGeometry(GeometryInterface $geometry): bool
{
$geometryInPolygon = true;
foreach ($geometry->getPoints() as $point) {
$geometryInPolygon = $geometryInPolygon && $this->contains($point);
}
return $geometryInPolygon;
}
/**
* Determine if given point is contained inside the polygon. Uses the PNPOLY
* algorithm by W. Randolph Franklin. Therfore some edge cases may not give the
* expected results, e.g. if the point resides on the polygon boundary.
*
* @see https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
*
* For special cases this calculation leads to wrong results:
*
* - if the polygons spans over the longitude boundaries at 180/-180 degrees
*
* @param Coordinate $point
*
* @return bool
*/
public function contains(Coordinate $point): bool
{
$numberOfPoints = $this->getNumberOfPoints();
$polygonLats = $this->getLats();
$polygonLngs = $this->getLngs();
$polygonContainsPoint = false;
for ($node = 0, $altNode = $numberOfPoints - 1; $node < $numberOfPoints; $altNode = $node++) {
$condition = ($polygonLngs[$node] > $point->getLng()) !== ($polygonLngs[$altNode] > $point->getLng())
&& ($point->getLat() < ($polygonLats[$altNode] - $polygonLats[$node])
* ($point->getLng() - $polygonLngs[$node])
/ ($polygonLngs[$altNode] - $polygonLngs[$node]) + $polygonLats[$node]);
if ($condition) {
$polygonContainsPoint = !$polygonContainsPoint;
}
}
return $polygonContainsPoint;
}
/**
* Calculates the polygon perimeter.
*
* @param DistanceInterface $calculator instance of distance calculation class
*
* @return float
*/
public function getPerimeter(DistanceInterface $calculator): float
{
$perimeter = 0.0;
if (count($this->points) < 2) {
return $perimeter;
}
foreach ($this->getSegments() as $segment) {
$perimeter += $segment->getLength($calculator);
}
return $perimeter;
}
/**
* Calculates the polygon area.
*
* This algorithm gives inaccurate results as it ignores
* ellipsoid parameters other than to arithmetic mean radius.
* The error should be < 1 % for small areas.
*
* @return float
*/
public function getArea(): float
{
$area = 0;
if ($this->getNumberOfPoints() <= 2) {
return $area;
}
$referencePoint = $this->points[0];
$radius = $referencePoint->getEllipsoid()->getArithmeticMeanRadius();
$segments = $this->getSegments();
foreach ($segments as $segment) {
$point1 = $segment->getPoint1();
$point2 = $segment->getPoint2();
$x1 = deg2rad($point1->getLng() - $referencePoint->getLng()) * cos(deg2rad($point1->getLat()));
$y1 = deg2rad($point1->getLat() - $referencePoint->getLat());
$x2 = deg2rad($point2->getLng() - $referencePoint->getLng()) * cos(deg2rad($point2->getLat()));
$y2 = deg2rad($point2->getLat() - $referencePoint->getLat());
$area += ($x2 * $y1 - $x1 * $y2);
}
$area *= 0.5 * $radius ** 2;
return abs($area);
}
/**
* Create a new polygon with reversed order of points, i. e. reversed
* polygon direction.
*
* @return Polygon
*/
public function getReverse(): Polygon
{
$reversed = new self();
foreach (array_reverse($this->points) as $point) {
$reversed->addPoint($point);
}
return $reversed;
}
}