Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.88% covered (success)
96.88%
62 / 64
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Polygon
96.88% covered (success)
96.88%
62 / 64
91.67% covered (success)
91.67%
11 / 12
34
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pointInPolygon
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isPointInPolygon
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
17.12
 isOnVertex
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getInteriorAngleSum
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getExteriorAngleSum
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSurface
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSignedSurface
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getPerimeter
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getBarycenter
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 getRegularAreaByLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRegularAreaByRadius
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Math\Geometry\Shape\D2
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Math\Geometry\Shape\D2;
16
17/**
18 * Polygon class.
19 *
20 * @package phpOMS\Math\Geometry\Shape\D2
21 * @license OMS License 2.0
22 * @link    https://jingga.app
23 * @since   1.0.0
24 */
25final class Polygon implements D2ShapeInterface
26{
27    /**
28     * Epsilon for float comparison.
29     *
30     * @var float
31     * @since 1.0.0
32     */
33    public const EPSILON = 4.88e-04;
34
35    /**
36     * Coordinates.
37     *
38     * These coordinates define the polygon
39     *
40     * @var array<int, array{x:int|float, y:int|float}>
41     * @since 1.0.0
42     */
43    private array $coord = [];
44
45    /**
46     * Constructor.
47     *
48     * @param array<int, array{x:int|float, y:int|float}> $coord 2 Dimensional coordinate array where the indices are x and y
49     *
50     * @example Polygon([['x' => 1, 'y' => 2], ['x' => ...], ...])
51     *
52     * @since 1.0.0
53     */
54    public function __construct(array $coord)
55    {
56        $this->coord = $coord;
57    }
58
59    /**
60     * Point polygon relative position
61     *
62     * @param array{x:int|float, y:int|float} $point Point location
63     *
64     * @return int -1 inside polygon 0 on vertice 1 outside
65     *
66     * @since 1.0.0
67     */
68    public function pointInPolygon(array $point) : int
69    {
70        $coord   = $this->coord;
71        $coord[] = $this->coord[0];
72
73        return self::isPointInPolygon($point, $coord);
74    }
75
76    /**
77     * Point polygon relative position
78     *
79     * @param array{x:int|float, y:int|float}             $point   Point location
80     * @param array<int, array{x:int|float, y:int|float}> $polygon Polygon definition
81     *
82     * @return int -1 inside polygon 0 on vertice 1 outside
83     *
84     * @link http://erich.realtimerendering.com/ptinpoly/
85     * @since  1.0.0
86     */
87    public static function isPointInPolygon(array $point, array $polygon) : int
88    {
89        $length = \count($polygon);
90
91        // Polygon has to start and end with same point
92        if ($polygon[0]['x'] !== $polygon[$length - 1]['x'] || $polygon[0]['y'] !== $polygon[$length - 1]['y']) {
93            $polygon[] = $polygon[0];
94        }
95
96        // On vertex?
97        if (self::isOnVertex($point, $polygon)) {
98            return 0;
99        }
100
101        // Inside or ontop?
102        $countIntersect = 0;
103        $polygonCount   = \count($polygon);
104
105        for ($i = 1; $i < $polygonCount; ++$i) {
106            $vertex1 = $polygon[$i - 1];
107            $vertex2 = $polygon[$i];
108
109            if (\abs($vertex1['y'] - $vertex2['y']) < self::EPSILON
110                && \abs($vertex1['y'] - $point['y']) < self::EPSILON
111                && $point['x'] > \min($vertex1['x'], $vertex2['x'])
112                && $point['x'] < \max($vertex1['x'], $vertex2['x'])
113            ) {
114                return 0; // boundary
115            }
116
117            if ($point['y'] > \min($vertex1['y'], $vertex2['y'])
118                && $point['y'] <= \max($vertex1['y'], $vertex2['y'])
119                && $point['x'] <= \max($vertex1['x'], $vertex2['x'])
120                && \abs($vertex1['y'] - $vertex2['y']) >= self::EPSILON
121            ) {
122                $xinters = ($point['y'] - $vertex1['y']) * ($vertex2['x'] - $vertex1['x']) / ($vertex2['y'] - $vertex1['y']) + $vertex1['x'];
123
124                if (\abs($xinters - $point['x']) < self::EPSILON) {
125                    return 0; // boundary
126                }
127
128                if (\abs($vertex1['x'] - $vertex2['x']) < self::EPSILON || $point['x'] < $xinters) {
129                    ++$countIntersect;
130                }
131            }
132        }
133
134        if ($countIntersect % 2 !== 0) {
135            return -1;
136        }
137
138        return 1;
139    }
140
141    /**
142     * Is point on vertex?
143     *
144     * @param array{x:int|float, y:int|float}             $point   Point location
145     * @param array<int, array{x:int|float, y:int|float}> $polygon Polygon definition
146     *
147     * @return bool
148     *
149     * @since 1.0.0
150     */
151    private static function isOnVertex(array $point, array $polygon) : bool
152    {
153        foreach ($polygon as $vertex) {
154            if (\abs($point['x'] - $vertex['x']) < self::EPSILON && \abs($point['y'] - $vertex['y']) < self::EPSILON) {
155                return true;
156            }
157        }
158
159        return false;
160    }
161
162    /**
163     * Get interior angle sum
164     *
165     * @return int
166     *
167     * @since 1.0.0
168     */
169    public function getInteriorAngleSum() : int
170    {
171        return (\count($this->coord) - 2) * 180;
172    }
173
174    /**
175     * Get exterior angle sum
176     *
177     * @return int
178     *
179     * @since 1.0.0
180     */
181    public function getExteriorAngleSum() : int
182    {
183        return 360;
184    }
185
186    /**
187     * Get surface area
188     *
189     * @return float
190     *
191     * @since 1.0.0
192     */
193    public function getSurface() : float
194    {
195        return \abs($this->getSignedSurface());
196    }
197
198    /**
199     * Get signed surface area
200     *
201     * @return float
202     *
203     * @since 1.0.0
204     */
205    private function getSignedSurface() : float
206    {
207        $count   = \count($this->coord);
208        $surface = 0;
209
210        for ($i = 0; $i < $count - 1; ++$i) {
211            $surface += $this->coord[$i]['x'] * $this->coord[$i + 1]['y'] - $this->coord[$i + 1]['x'] * $this->coord[$i]['y'];
212        }
213
214        $surface += $this->coord[$count - 1]['x'] * $this->coord[0]['y'] - $this->coord[0]['x'] * $this->coord[$count - 1]['y'];
215
216        return $surface / 2;
217    }
218
219    /**
220     * Get perimeter
221     *
222     * @return float
223     *
224     * @since 1.0.0
225     */
226    public function getPerimeter() : float
227    {
228        $count     = \count($this->coord);
229        $perimeter = \sqrt(($this->coord[0]['x'] - $this->coord[$count - 1]['x']) ** 2 + ($this->coord[0]['y'] - $this->coord[$count - 1]['y']) ** 2);
230
231        for ($i = 0; $i < $count - 1; ++$i) {
232            $perimeter += \sqrt(($this->coord[$i + 1]['x'] - $this->coord[$i]['x']) ** 2 + ($this->coord[$i + 1]['y'] - $this->coord[$i]['y']) ** 2);
233        }
234
235        return $perimeter;
236    }
237
238    /**
239     * Get barycenter
240     *
241     * @return array{x:int|float, y:int|float}
242     *
243     * @since 1.0.0
244     */
245    public function getBarycenter() : array
246    {
247        $barycenter = ['x' => 0, 'y' => 0];
248        $count      = \count($this->coord);
249
250        for ($i = 0; $i < $count - 1; ++$i) {
251            $mult             = ($this->coord[$i]['x'] * $this->coord[$i + 1]['y'] - $this->coord[$i + 1]['x'] * $this->coord[$i]['y']);
252            $barycenter['x'] += ($this->coord[$i]['x'] + $this->coord[$i + 1]['x']) * $mult;
253            $barycenter['y'] += ($this->coord[$i]['y'] + $this->coord[$i + 1]['y']) * $mult;
254        }
255
256        $mult             = ($this->coord[$count - 1]['x'] * $this->coord[0]['y'] - $this->coord[0]['x'] * $this->coord[$count - 1]['y']);
257        $barycenter['x'] += ($this->coord[$count - 1]['x'] + $this->coord[0]['x']) * $mult;
258        $barycenter['y'] += ($this->coord[$count - 1]['y'] + $this->coord[0]['y']) * $mult;
259
260        $surface = $this->getSignedSurface();
261
262        $barycenter['x'] = 1 / (6 * $surface) * $barycenter['x'];
263        $barycenter['y'] = 1 / (6 * $surface) * $barycenter['y'];
264
265        return $barycenter;
266    }
267
268    /**
269     * Get area by side length
270     *
271     * @param float $length Side length
272     * @param int   $sides  Number of sides
273     *
274     * @return float
275     *
276     * @since 1.0.0
277     */
278    public static function getRegularAreaByLength(float $length, int $sides) : float
279    {
280        return $length ** 2 * $sides / (4 * \tan(\M_PI / $sides));
281    }
282
283    /**
284     * Get area by radius
285     *
286     * @param float $r     Radius
287     * @param int   $sides Number of sides
288     *
289     * @return float
290     *
291     * @since 1.0.0
292     */
293    public static function getRegularAreaByRadius(float $r, int $sides) : float
294    {
295        return $r ** 2 * $sides * \tan(\M_PI / $sides);
296    }
297}