Completed
Pull Request — develop (#16)
by Jimmy
03:29 queued 01:48
created

GeometryProxy::getRawGeometry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Spinen\Geometry\Support;
4
5
use Geometry;
6
use InvalidArgumentException;
7
use RuntimeException;
8
9
/**
10
 * Class GeometryProxy
11
 *
12
 * Proxy class to "wrap" the geoPHP classes into class that we can add functionality.
13
 *
14
 * @package Spinen\Geometry
15
 *
16
 * @method mixed toEwkb() Returns the geometry in EWKB format.
17
 * @method mixed toEwkt() Returns the geometry in EWKT format.
18
 * @method mixed toGeoHash() Returns the geometry in GeoHash format.
19
 * @method mixed toGeoJson() Returns the geometry in GeoJSON format.
20
 * @method mixed toGeoRss() Returns the geometry in GeoRSS format.
21
 * @method mixed toGoogleGeocode() Returns the geometry in GoogleGeocode format.
22
 * @method mixed toGpx() Returns the geometry in GPX format.
23
 * @method mixed toJson() Returns the geometry in GeoJSON format.
24
 * @method mixed toKml() Returns the geometry in KML format.
25
 * @method mixed toWkb() Returns the geometry in WKB format.
26
 * @method mixed toWkt() Returns the geometry in WKT format.
27
 * @property float acres The acres with in +/-1%.
28
 * @property array coordinates The points that define the shape.
29
 * @property float square_meters The square meters with in +/-1%.
30
 */
31
class GeometryProxy
32
{
33
    /**
34
     * Cache the area to not have to loop through the calculations each time that it is needed.
35
     *
36
     * @var float
37
     */
38
    protected $cached_area = null;
39
40
    /**
41
     * The geometry to proxy.
42
     *
43
     * @var Geometry
44
     */
45
    protected $geometry;
46
47
    /**
48
     * Cached array version of the geometry.
49
     *
50
     * @var array | null
51
     */
52
    protected $geometry_array = null;
53
54
    /**
55
     * Instance of TypeMapper.
56
     *
57
     * @var TypeMapper
58
     */
59
    protected $mapper;
60
61
    /**
62
     * Polygon constructor.
63
     *
64
     * @param mixed      $geometry
65
     * @param TypeMapper $mapper
66
     */
67
    public function __construct($geometry, TypeMapper $mapper)
68
    {
69
        $this->geometry = $geometry;
70
        $this->mapper = $mapper;
71
    }
72
73
    /**
74
     * Magic method to allow methods that are not specifically defined.
75
     *
76
     * This is where we look to see if the class that we are proxing has the method that is being called, and if it
77
     * does, then pass the call to the class under proxy.  If a method is defined in our class, then it gets called
78
     * first, so you can "extend" the classes by defining methods that overwrite the "parent" there.
79
     *
80
     * @param string $name
81
     * @param array  $arguments
82
     *
83
     * @return mixed
84
     * @throws InvalidArgumentException
85
     */
86
    public function __call($name, $arguments)
87
    {
88
        // Sugar to make to<Format>() work
89 View Code Duplication
        if (preg_match("/^to([A-Z][A-z]*)/u", $name, $parts) && 0 === count($arguments)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
90
            return $this->geometry->out($this->mapper->map($parts[1]));
91
        }
92
93
        // Call the method on the class being proxied
94
        if (method_exists($this->geometry, $name)) {
95
            return call_user_func_array(
96
                [$this->geometry, $name], array_map([$this, 'exposeRawIfAvailable'], $arguments)
97
            );
98
        }
99
100
        throw new RuntimeException(sprintf("Call to undefined method %s::%s().", __CLASS__, $name));
101
    }
102
103
    /**
104
     * @param string $name
105
     *
106
     * @return mixed
107
     */
108
    public function __get($name)
109
    {
110
        // Properties on the geometry
111
        if (isset($this->toArray()[$name])) {
112
            return $this->toArray()[$name];
113
        }
114
115
        // Shortcut to the getters
116
        if (method_exists($this, 'get' . studly_case($name))) {
117
            return $this->{'get' . studly_case($name)}();
118
        }
119
120
        throw new RuntimeException(sprintf("Undefined property: %s", $name));
121
    }
122
123
    /**
124
     * If using the object as a string, just return the json.
125
     *
126
     * @return string
127
     */
128
    public function __toString()
129
    {
130
        return $this->toJson();
131
    }
132
133
    /**
134
     * Figure out what index to use in the ringArea calculation
135
     *
136
     * @param int $index
137
     * @param int $length
138
     *
139
     * @return array
140
     */
141
    private function determineCoordinateIndices($index, $length)
142
    {
143
        // i = N-2
144 View Code Duplication
        if ($index === ($length - 2)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
145
            return [$length - 2, $length - 1, 0];
146
        }
147
148
        // i = N-1
149 View Code Duplication
        if ($index === ($length - 1)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
150
            return [$length - 1, 0, 1];
151
        }
152
153
        // i = 0 to N-3
154
        return [$index, $index + 1, $index + 2];
155
    }
156
157
    /**
158
     * If the object passed in has a getRawGeometry, call it
159
     *
160
     * @param $argument
161
     *
162
     * @return mixed
163
     */
164
    protected function exposeRawIfAvailable($argument)
165
    {
166
        return (method_exists($argument, 'getRawGeometry'))
167
            ? $argument->getRawGeometry()
168
            : $argument;
169
    }
170
171
    /**
172
     * Calculate the acres
173
     *
174
     * @return float
175
     */
176
    public function getAcres()
177
    {
178
        return $this->square_meters * 0.000247105381;
179
    }
180
181
    /**
182
     * Expose the underlying Geometry object
183
     *
184
     * @return Geometry
185
     */
186
    public function getRawGeometry()
187
    {
188
        return $this->geometry;
189
    }
190
191
    /**
192
     * Calculate the square meters
193
     *
194
     * @return float
195
     */
196
    public function getSquareMeters()
197
    {
198
        if (!is_null($this->cached_area)) {
199
            return $this->cached_area;
200
        }
201
202
        $this->cached_area = 0.0;
203
204
        foreach ($this->coordinates as $coordinate) {
205
            $this->cached_area += $this->ringArea($coordinate);
206
        }
207
208
        return $this->cached_area;
209
    }
210
211
    /**
212
     * Convert degrees to radians
213
     *
214
     * I know that there is a built in function, but I read that it was very slow & to use this.
215
     *
216
     * @param $degrees
217
     *
218
     * @return float
219
     */
220
    private function radians($degrees)
221
    {
222
        return $degrees * M_PI / 180;
223
    }
224
225
    /**
226
     * Estimate the area of a ring
227
     *
228
     * Calculate the approximate area of the polygon were it projected onto
229
     *     the earth.  Note that this area will be positive if ring is oriented
230
     *     clockwise, otherwise it will be negative.
231
     *
232
     * Reference:
233
     * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
234
     *     Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion
235
     *     Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409
236
     *
237
     * @return float
238
     * @see https://github.com/mapbox/geojson-area/blob/master/index.js#L55
239
     */
240
    public function ringArea($coordinates)
241
    {
242
        $area = 0.0;
243
244
        $length = count($coordinates);
245
246
        if ($length <= 2) {
247
            return $area;
248
        }
249
250
        for ($i = 0; $i < $length; $i ++) {
251
            list($lower_index, $middle_index, $upper_index) = $this->determineCoordinateIndices($i, $length);
252
253
            $point1 = $coordinates[$lower_index];
254
            $point2 = $coordinates[$middle_index];
255
            $point3 = $coordinates[$upper_index];
256
257
            $area += ($this->radians($point3[0]) - $this->radians($point1[0])) * sin($this->radians($point2[1]));
258
        }
259
260
        return $area * 6378137 * 6378137 / 2;
261
    }
262
263
    /**
264
     * Build array of the object
265
     *
266
     * Cache the result, so that we don't decode it on every call.
267
     *
268
     * @return array
269
     */
270
    public function toArray()
271
    {
272
        if (is_null($this->geometry_array)) {
273
            $this->geometry_array = (array)json_decode($this->toJson(), true);
274
        }
275
276
        return $this->geometry_array;
277
    }
278
}
279