Passed
Pull Request — master (#4)
by Mark
01:50
created

geoPHP   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 417
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 198
dl 0
loc 417
rs 2
c 0
b 0
f 0
wmc 82

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getAdapterMap() 0 3 1
C buildGeometry() 0 55 16
B load() 0 44 11
A geosToGeometry() 0 16 3
A explodeCollections() 0 8 4
A getGeometryList() 0 3 1
A geosInstalled() 0 15 4
C geometryReduce() 0 58 13
D detectFormat() 0 95 29

How to fix   Complexity   

Complex Class

Complex classes like geoPHP often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use geoPHP, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the GeoPHP package.
5
 * Copyright (c) 2011 - 2016 Patrick Hayes and contributors
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace geoPHP;
11
12
use geoPHP\Adapter\GeoHash;
13
use geoPHP\Geometry\Collection;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Collection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use geoPHP\Geometry\Geometry;
15
use geoPHP\Geometry\GeometryCollection;
16
17
// @codingStandardsIgnoreLine
18
class geoPHP
19
{
20
    // Earth radius constants in meters
21
22
    /** WGS84 semi-major axis (a), aka equatorial radius */
23
    const EARTH_WGS84_SEMI_MAJOR_AXIS = 6378137.0;
24
    /** WGS84 semi-minor axis (b), aka polar radius */
25
    const EARTH_WGS84_SEMI_MINOR_AXIS = 6356752.314245;
26
    /** WGS84 inverse flattening */
27
    const EARTH_WGS84_FLATTENING      = 298.257223563;
28
29
    /** WGS84 semi-major axis (a), aka equatorial radius */
30
    const EARTH_GRS80_SEMI_MAJOR_AXIS = 6378137.0;
31
    /** GRS80 semi-minor axis */
32
    const EARTH_GRS80_SEMI_MINOR_AXIS = 6356752.314140;
33
    /** GRS80 inverse flattening */
34
    const EARTH_GRS80_FLATTENING      = 298.257222100882711;
35
36
    /** IUGG mean radius R1 = (2a + b) / 3 */
37
    const EARTH_MEAN_RADIUS           = 6371008.8;
38
    /** IUGG R2: Earth's authalic ("equal area") radius is the radius of a hypothetical perfect sphere
39
     * which has the same surface area as the reference ellipsoid. */
40
    const EARTH_AUTHALIC_RADIUS       = 6371007.2;
41
42
    const CLASS_NAMESPACE = 'geoPHP\\';
43
44
    private static $adapterMap = [
45
            'wkt'            => 'WKT',
46
            'ewkt'           => 'EWKT',
47
            'wkb'            => 'WKB',
48
            'ewkb'           => 'EWKB',
49
            'json'           => 'GeoJSON',
50
            'geojson'        => 'GeoJSON',
51
            'kml'            => 'KML',
52
            'gpx'            => 'GPX',
53
            'georss'         => 'GeoRSS',
54
            'google_geocode' => 'GoogleGeocode',
55
            'geohash'        => 'GeoHash',
56
            'twkb'           => 'TWKB',
57
            'osm'            => 'OSM',
58
    ];
59
60
    public static function getAdapterMap()
61
    {
62
        return self::$adapterMap;
63
    }
64
65
    private static $geometryList = [
66
            'point'              => 'Point',
67
            'linestring'         => 'LineString',
68
            'polygon'            => 'Polygon',
69
            'multipoint'         => 'MultiPoint',
70
            'multilinestring'    => 'MultiLineString',
71
            'multipolygon'       => 'MultiPolygon',
72
            'geometrycollection' => 'GeometryCollection',
73
    ];
74
75
    public static function getGeometryList()
76
    {
77
        return self::$geometryList;
78
    }
79
80
    /**
81
     * Converts data to Geometry using geo adapters
82
     *
83
     * If $data is an array, all passed in values will be combined into a single geometry
84
     *
85
     * @param mixed $data The data in any supported format, including geoPHP Geometry
86
     * @var null|string $type Data type. Tries to detect if omitted
87
     * @var mixed|null $otherArgs Arguments will be passed to the geo adapter
88
     *
89
     * @return Collection|Geometry
90
     * @throws \Exception
91
     */
92
    public static function load($data)
93
    {
94
        $args = func_get_args();
95
96
        $data = array_shift($args);
97
        $type = count($args) && @array_key_exists($args[0], self::$adapterMap) ? strtolower(array_shift($args)) : null;
98
99
        // Auto-detect type if needed
100
        if (!$type) {
101
            // If the user is trying to load a Geometry from a Geometry... Just pass it back
102
            if (is_object($data)) {
103
                if ($data instanceof Geometry) {
104
                    return $data;
105
                }
106
            }
107
108
            $detected = geoPHP::detectFormat($data);
109
            if (!$detected) {
110
                throw new \Exception("Can not detect format");
111
            }
112
            $format = explode(':', $detected);
113
            $type = array_shift($format);
114
            $args = $format ?: $args;
115
        }
116
117
        if (!array_key_exists($type, self::$adapterMap)) {
118
            throw new \Exception('geoPHP could not find an adapter of type ' . htmlentities($type));
119
        }
120
        $adapterType = self::CLASS_NAMESPACE . 'Adapter\\' . self::$adapterMap[$type];
121
122
        $adapter = new $adapterType();
123
124
        // Data is not an array, just pass it normally
125
        if (!is_array($data)) {
126
            $result = call_user_func_array([$adapter, "read"], array_merge([$data], $args));
127
        } else { // Data is an array, combine all passed in items into a single geometry
128
            $geometries = [];
129
            foreach ($data as $item) {
130
                $geometries[] = call_user_func_array([$adapter, "read"], array_merge($item, $args));
131
            }
132
            $result = geoPHP::buildGeometry($geometries);
133
        }
134
135
        return $result;
136
    }
137
138
    public static function geosInstalled($force = null)
139
    {
140
        static $geosInstalled = null;
141
        if ($force !== null) {
142
            $geosInstalled = $force;
143
        }
144
        if (getenv('GEOS_DISABLED') == 1) {
145
            $geosInstalled = false;
146
        }
147
        if ($geosInstalled !== null) {
148
            return $geosInstalled;
149
        }
150
        $geosInstalled = class_exists('GEOSGeometry', false);
151
152
        return $geosInstalled;
153
    }
154
155
    /**
156
     * @param \GEOSGeometry $geos
157
     * @return Geometry|null
158
     * @throws \Exception
159
     * @codeCoverageIgnore
160
     */
161
    public static function geosToGeometry($geos)
162
    {
163
        if (!geoPHP::geosInstalled()) {
164
            return null;
165
        }
166
        /** @noinspection PhpUndefinedClassInspection */
167
        $wkbWriter = new \GEOSWKBWriter();
168
        /** @noinspection PhpUndefinedMethodInspection */
169
        $wkb = $wkbWriter->writeHEX($geos);
170
        $geometry = geoPHP::load($wkb, 'wkb', true);
171
        if ($geometry) {
172
            $geometry->setGeos($geos);
173
            return $geometry;
174
        }
175
176
        return null;
177
    }
178
179
    /**
180
     * Reduce a geometry, or an array of geometries, into their 'lowest' available common geometry.
181
     * For example a GeometryCollection of only points will become a MultiPoint
182
     * A multi-point containing a single point will return a point.
183
     * An array of geometries can be passed and they will be compiled into a single geometry
184
     *
185
     * @param Geometry|Geometry[]|GeometryCollection|GeometryCollection[] $geometries
186
     * @return Geometry|false
187
     */
188
    public static function geometryReduce($geometries)
189
    {
190
        if (empty($geometries)) {
191
            return false;
192
        }
193
        /*
194
         * If it is a single geometry
195
         */
196
        if ($geometries instanceof Geometry) {
197
            // If the geometry cannot even theoretically be reduced more, then pass it back
198
            $singleGeometries = ['Point', 'LineString', 'Polygon'];
199
            if (in_array($geometries->geometryType(), $singleGeometries)) {
200
                return $geometries;
201
            }
202
203
            // If it is a multi-geometry, check to see if it just has one member
204
            // If it does, then pass the member, if not, then just pass back the geometry
205
            if (strpos($geometries->geometryType(), 'Multi') === 0) {
206
                $components = $geometries->getComponents();
207
                if (count($components) == 1) {
208
                    return $components[0];
209
                } else {
210
                    return $geometries;
211
                }
212
            }
213
        } elseif (is_array($geometries) && count($geometries) == 1) {
214
            // If it's an array of one, then just parse the one
215
            return geoPHP::geometryReduce(array_shift($geometries));
216
        }
217
218
        if (!is_array($geometries)) {
219
            $geometries = [$geometries];
220
        }
221
        /**
222
         * So now we either have an array of geometries
223
         * @var Geometry[]|GeometryCollection[] $geometries
224
         */
225
226
        $reducedGeometries = [];
227
        $geometryTypes = [];
228
        self::explodeCollections($geometries, $reducedGeometries, $geometryTypes);
229
230
        $geometryTypes = array_unique($geometryTypes);
231
        if (empty($geometryTypes)) {
232
            return false;
233
        }
234
        if (count($geometryTypes) == 1) {
235
            if (count($reducedGeometries) == 1) {
236
                return $reducedGeometries[0];
237
            } else {
238
                $class = self::CLASS_NAMESPACE .
239
                    'Geometry\\' .
240
                    (strstr($geometryTypes[0], 'Multi') ? '' : 'Multi') .
241
                    $geometryTypes[0];
242
                return new $class($reducedGeometries);
243
            }
244
        } else {
245
            return new GeometryCollection($reducedGeometries);
246
        }
247
    }
248
249
    /**
250
     * @param Geometry[]|GeometryCollection[] $unreduced
251
     */
252
    private static function explodeCollections($unreduced, &$reduced, &$types)
253
    {
254
        foreach ($unreduced as $item) {
255
            if ($item->geometryType() == 'GeometryCollection' || strpos($item->geometryType(), 'Multi') === 0) {
256
                self::explodeCollections($item->getComponents(), $reduced, $types);
257
            } else {
258
                $reduced[] = $item;
259
                $types[] = $item->geometryType();
260
            }
261
        }
262
    }
263
264
    /**
265
     * Build an appropriate Geometry, MultiGeometry, or GeometryCollection to contain the Geometries in it.
266
     *
267
     * @see geos::geom::GeometryFactory::buildGeometry
268
     *
269
     * @param Geometry|Geometry[]|GeometryCollection|GeometryCollection[] $geometries
270
     * @return Geometry A Geometry of the "smallest", "most type-specific" class that can contain the elements.
271
     * @throws \Exception
272
     */
273
    public static function buildGeometry($geometries)
274
    {
275
        if (empty($geometries)) {
276
            return new GeometryCollection();
277
        }
278
279
        /* If it is a single geometry */
280
        if ($geometries instanceof Geometry) {
281
            return $geometries;
282
        } elseif (!is_array($geometries)) {
283
            return null;
284
            //FIXME should be: throw new \Exception('Input is not a Geometry or array of Geometries');
285
        } elseif (count($geometries) == 1) {
286
            // If it's an array of one, then just parse the one
287
            return geoPHP::buildGeometry(array_shift($geometries));
288
        }
289
290
        /**
291
         * So now we either have an array of geometries
292
         * @var Geometry[]|GeometryCollection[] $geometries
293
         */
294
295
        $geometryTypes = [];
296
        $hasData = false;
297
        foreach ($geometries as $item) {
298
            if ($item) {
299
                $geometryTypes[] = $item->geometryType();
300
                if ($item->getData() !== null) {
301
                    $hasData = true;
302
                }
303
            }
304
        }
305
        $geometryTypes = array_unique($geometryTypes);
306
        if (empty($geometryTypes)) {
307
            return null;
308
            // FIXME normally it never happens. Should be refactored
309
        }
310
        if (count($geometryTypes) == 1 && !$hasData) {
311
            if ($geometryTypes[0] === Geometry::GEOMETRY_COLLECTION) {
312
                return new GeometryCollection($geometries);
313
            }
314
            if (count($geometries) == 1) {
315
                return $geometries[0];
316
            } else {
317
                $newType = (strpos($geometryTypes[0], 'Multi') !== false ? '' : 'Multi') . $geometryTypes[0];
318
                foreach ($geometries as $geometry) {
319
                    if ($geometry->isEmpty()) {
320
                        return new GeometryCollection($geometries);
321
                    }
322
                }
323
                $class = self::CLASS_NAMESPACE . 'Geometry\\' . $newType;
324
                return new $class($geometries);
325
            }
326
        } else {
327
            return new GeometryCollection($geometries);
328
        }
329
    }
330
331
    /**
332
     * Detect a format given a value. This function is meant to be SPEEDY.
333
     * It could make a mistake in XML detection if you are mixing or using namespaces in weird ways
334
     * (ie, KML inside an RSS feed)
335
     *
336
     * @param mixed $input
337
     *
338
     * @return string|false
339
     */
340
    public static function detectFormat(&$input)
341
    {
342
        $mem = fopen('php://memory', 'x+');
343
        fwrite($mem, $input, 11); // Write 11 bytes - we can detect the vast majority of formats in the first 11 bytes
344
        fseek($mem, 0);
345
346
        $bin = fread($mem, 11);
347
        $bytes = unpack("c*", $bin);
348
349
        // If bytes is empty, then we were passed empty input
350
        if (empty($bytes)) {
351
            return false;
352
        }
353
354
        // First char is a tab, space or carriage-return. trim it and try again
355
        if ($bytes[1] == 9 || $bytes[1] == 10 || $bytes[1] == 32) {
356
            $input = ltrim($input);
357
            return geoPHP::detectFormat($input);
358
        }
359
360
        // Detect WKB or EWKB -- first byte is 1 (little endian indicator)
361
        if ($bytes[1] == 1 || $bytes[1] == 0) {
362
            $wkbType = current(unpack($bytes[1] == 1 ? 'V' : 'N', substr($bin, 1, 4)));
363
            if (array_search($wkbType & 0xF, Adapter\WKB::$typeMap)) {
364
                // If SRID byte is TRUE (1), it's EWKB
365
                if ($wkbType & Adapter\WKB::SRID_MASK == Adapter\WKB::SRID_MASK) {
0 ignored issues
show
Bug introduced by
Are you sure you want to use the bitwise & or did you mean &&?
Loading history...
366
                    return 'ewkb';
367
                } else {
368
                    return 'wkb';
369
                }
370
            }
371
        }
372
373
        // Detect HEX encoded WKB or EWKB (PostGIS format) -- first byte is 48, second byte is 49 (hex '01' => first-byte = 1)
374
        // The shortest possible WKB string (LINESTRING EMPTY) is 18 hex-chars (9 encoded bytes) long
375
        // This differentiates it from a geohash, which is always shorter than 13 characters.
376
        if ($bytes[1] == 48 && ($bytes[2] == 49 || $bytes[2] == 48) && strlen($input) > 12) {
377
            if ((current(unpack($bytes[2] == 49 ? 'V' : 'N', hex2bin(substr($bin, 2, 8)))) & Adapter\WKB::SRID_MASK) == Adapter\WKB::SRID_MASK) {
378
                return 'ewkb:true';
379
            } else {
380
                return 'wkb:true';
381
            }
382
        }
383
384
        // Detect GeoJSON - first char starts with {
385
        if ($bytes[1] == 123) {
386
            return 'json';
387
        }
388
389
        // Detect EWKT - strats with "SRID=number;"
390
        if (substr($input, 0, 5) === 'SRID=') {
391
            return 'ewkt';
392
        }
393
394
        // Detect WKT - starts with a geometry type name
395
        if (Adapter\WKT::isWktType(strstr($input, ' ', true))) {
396
            return 'wkt';
397
        }
398
399
        // Detect XML -- first char is <
400
        if ($bytes[1] == 60) {
401
            // grab the first 1024 characters
402
            $string = substr($input, 0, 1024);
403
            if (strpos($string, '<kml') !== false) {
404
                return 'kml';
405
            }
406
            if (strpos($string, '<coordinate') !== false) {
407
                return 'kml';
408
            }
409
            if (strpos($string, '<gpx') !== false) {
410
                return 'gpx';
411
            }
412
            if (strpos($string, '<osm ') !== false) {
413
                return 'osm';
414
            }
415
            if (preg_match('/<[a-z]{3,20}>/', $string) !== false) {
416
                return 'georss';
417
            }
418
        }
419
420
        // We need an 8 byte string for geohash and unpacked WKB / WKT
421
        fseek($mem, 0);
422
        $string = trim(fread($mem, 8));
423
424
        // Detect geohash - geohash ONLY contains lowercase chars and numerics
425
        preg_match('/[' . GeoHash::$characterTable . ']+/', $string, $matches);
426
        if (isset($matches[0]) && $matches[0] == $string && strlen($input) <= 13) {
427
            return 'geohash';
428
        }
429
430
        preg_match('/^[a-f0-9]+$/', $string, $matches);
431
        if (isset($matches[0])) {
432
            return 'twkb:true';
433
        } else {
434
            return 'twkb';
435
        }
436
437
        // What do you get when you cross an elephant with a rhino?
438
        // http://youtu.be/RCBn5J83Poc
439
    }
440
}
441