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