Test Failed
Push — master ( 851fa4...bbb078 )
by Swen
02:59
created

geoPHP::buildGeometry()   B

Complexity

Conditions 11
Paths 19

Size

Total Lines 48
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 1
Metric Value
eloc 25
c 6
b 1
f 1
dl 0
loc 48
rs 7.3166
cc 11
nc 19
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * This file is part of the GeoPHP package.
4
 * Copyright (c) 2011 - 2016 Patrick Hayes and contributors
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
namespace geoPHP;
10
11
use geoPHP\Adapter\GeoHash;
12
use geoPHP\Geometry\Collection;
13
use geoPHP\Geometry\Geometry;
14
use geoPHP\Geometry\GeometryCollection;
15
16
// @codingStandardsIgnoreLine
17
class geoPHP
18
{
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
25
    /** WGS84 semi-minor axis (b), aka polar radius */
26
    const EARTH_WGS84_SEMI_MINOR_AXIS = 6356752.314245;
27
28
    /** WGS84 inverse flattening */
29
    const EARTH_WGS84_FLATTENING = 298.257223563;
30
31
    /** WGS84 semi-major axis (a), aka equatorial radius */
32
    const EARTH_GRS80_SEMI_MAJOR_AXIS = 6378137.0;
33
34
    /** GRS80 semi-minor axis */
35
    const EARTH_GRS80_SEMI_MINOR_AXIS = 6356752.314140;
36
37
    /** GRS80 inverse flattening */
38
    const EARTH_GRS80_FLATTENING = 298.257222100882711;
39
40
    /** IUGG mean radius R1 = (2a + b) / 3 */
41
    const EARTH_MEAN_RADIUS = 6371008.8;
42
43
    /** IUGG R2: Earth's authalic ("equal area") radius is the radius of a hypothetical perfect sphere
44
     * which has the same surface area as the reference ellipsoid. */
45
    const EARTH_AUTHALIC_RADIUS = 6371007.2;
46
47
    /**
48
     * @var array<string, string>
49
     */
50
    private static $adapterMap = [
51
        'wkt' => 'WKT',
52
        'ewkt' => 'EWKT',
53
        'wkb' => 'WKB',
54
        'ewkb' => 'EWKB',
55
        'json' => 'GeoJSON',
56
        'geojson' => 'GeoJSON',
57
        'kml' => 'KML',
58
        'gpx' => 'GPX',
59
        'georss' => 'GeoRSS',
60
        'google_geocode' => 'GoogleGeocode',
61
        'geohash' => 'GeoHash',
62
        'twkb' => 'TWKB',
63
        'osm' => 'OSM'
64
    ];
65
66
    /**
67
     * @var array<string, string>
68
     */
69
    private static $geometryList = [
70
        'point' => 'Point',
71
        'linestring' => 'LineString',
72
        'polygon' => 'Polygon',
73
        'multipoint' => 'MultiPoint',
74
        'multilinestring' => 'MultiLineString',
75
        'multipolygon' => 'MultiPolygon',
76
        'geometrycollection' => 'GeometryCollection'
77
    ];
78
79
    /**
80
     * @return string
81
     */
82
    public static function version(): string
83
    {
84
        return '1.4';
85
    }
86
    
87
    /**
88
     * @return array<string, string> returns the supported adapter-map, e.g. ['wkt' => 'WKT',...]
89
     */
90
    public static function getAdapterMap(): array
91
    {
92
        return self::$adapterMap;
93
    }
94
95
    /**
96
     * @return array<string, string> returns the mapped geometry-list, e.g. ['point' => 'Point',...]
97
     */
98
    public static function getGeometryList(): array
99
    {
100
        return self::$geometryList;
101
    }
102
103
    /**
104
     * Converts data to Geometry using geo adapters
105
     * If $data is an array, all passed in values will be combined into a single geometry.
106
     *
107
     * @param mixed $data The data in any supported format, including geoPHP Geometry.
108
     * @var null|string $type Data type. Tries to detect it if omitted.
109
     * @var null|mixed $otherArgs Arguments will be passed to the geo adapter.
110
     *
111
     * @return Collection|Geometry|null
112
     * @throws \Exception
113
     */
114
    public static function load($data)
115
    {
116
        $args = func_get_args();
117
118
        $data = array_shift($args);
119
        $type = count($args) && array_key_exists($args[0], self::$adapterMap) ? strtolower(array_shift($args)) : null;
120
121
        // Auto-detect type if needed
122
        if (!$type) {
123
            // If the user is trying to load a Geometry from a Geometry... Just pass it back
124
            if (is_object($data)) {
125
                if ($data instanceof Geometry) {
126
                    return $data;
127
                }
128
            }
129
130
            $detected = geoPHP::detectFormat($data);
131
            if (!$detected) {
132
                throw new \Exception("Unable to detect the data format.");
133
            }
134
            $format = explode(':', $detected);
135
            $type = array_shift($format);
136
            $args = $format ?: $args;
137
        }
138
139
        if (!array_key_exists($type, self::$adapterMap)) {
140
            throw new \Exception('geoPHP could not find an adapter of type ' . htmlentities($type));
141
        }
142
        
143
        $adapterType = '\\geoPHP\\Adapter\\' . self::$adapterMap[$type];
144
        $adapter = new $adapterType();
145
146
        // Data is not an array, just pass it normally
147
        if (!is_array($data)) {
148
            $result = call_user_func_array([$adapter, "read"], array_merge([$data], $args));
149
        } else {
150
            // Data is an array, combine all passed in items into a single geometry
151
            $geometries = [];
152
            foreach ($data as $item) {
153
                $geometries[] = call_user_func_array([$adapter, "read"], array_merge($item, $args));
154
            }
155
            $result = geoPHP::buildGeometry($geometries);
156
        }
157
158
        return $result;
159
    }
160
161
    /**
162
     * @staticvar bool $geosInstalled
163
     * @param bool $force
164
     * @return bool
165
     */
166
    public static function geosInstalled($force = null): bool
167
    {
168
        static $geosInstalled = null;
169
        
170
        if ($force !== null) {
171
            $geosInstalled = $force;
172
        }
173
        if (getenv('GEOS_DISABLED') == 1) {
174
            $geosInstalled = false;
175
        }
176
        if ($geosInstalled !== null) {
177
            return $geosInstalled;
178
        }
179
        
180
        $geosInstalled = class_exists('GEOSGeometry', false);
181
182
        return $geosInstalled;
183
    }
184
185
    /**
186
     * @param \GEOSGeometry $geos
187
     * @return Geometry|null
188
     * @throws \Exception
189
     * @codeCoverageIgnore
190
     */
191
    public static function geosToGeometry($geos)
192
    {
193
        if (!geoPHP::geosInstalled()) {
194
            return null;
195
        }
196
        /** @noinspection PhpUndefinedClassInspection */
197
        $wkbWriter = new \GEOSWKBWriter();
198
        /** @noinspection PhpUndefinedMethodInspection */
199
        $wkb = $wkbWriter->writeHEX($geos);
200
        $geometry = geoPHP::load($wkb, 'wkb', true);
201
        if ($geometry) {
202
            $geometry->setGeos($geos);
203
            return $geometry;
204
        }
205
206
        return null;
207
    }
208
209
    /**
210
     * Reduce a geometry, or an array of geometries, into their 'lowest' available common geometry.
211
     * For example a GeometryCollection of only points will become a MultiPoint
212
     * A multi-point containing a single point will return a point.
213
     * An array of geometries can be passed and they will be compiled into a single geometry
214
     *
215
     * @param Geometry|Geometry[]|GeometryCollection|GeometryCollection[] $geometries
216
     * @return mixed Geometry|false
217
     */
218
    public static function geometryReduce($geometries)
219
    {
220
        if (empty($geometries)) {
221
            return false;
222
        }
223
224
        // If it is a single geometry
225
        if ($geometries instanceof Geometry) {
226
            // If the geometry cannot even theoretically be reduced more, then pass it back
227
            $singleGeometries = ['Point', 'LineString', 'Polygon'];
228
            if (in_array($geometries->geometryType(), $singleGeometries)) {
229
                return $geometries;
230
            }
231
232
            // If it is a multi-geometry, check to see if it just has one member
233
            // If it does, then pass the member, if not, then just pass back the geometry
234
            if (strpos($geometries->geometryType(), 'Multi') === 0) {
235
                $components = $geometries->getComponents();
236
                if (count($components) === 1) {
237
                    return $components[0];
238
                } else {
239
                    return $geometries;
240
                }
241
            }
242
        } elseif (is_array($geometries) && count($geometries) === 1) {
243
            // If it's an array of one, then just parse the one
244
            return geoPHP::geometryReduce(array_shift($geometries));
245
        }
246
247
        if (!is_array($geometries)) {
248
            $geometries = [$geometries];
249
        }
250
        
251
        // So now we either have an array of geometries
252
        // @var Geometry[]|GeometryCollection[] $geometries
253
        $reducedGeometries = [];
254
        $geometryTypes = [];
255
        self::explodeCollections($geometries, $reducedGeometries, $geometryTypes);
0 ignored issues
show
Bug introduced by
It seems like $geometries can also be of type geoPHP\Geometry\Geometry and geoPHP\Geometry\GeometryCollection and geoPHP\Geometry\Geometry...eoPHP\Geometry\Geometry; however, parameter $unreduced of geoPHP\geoPHP::explodeCollections() does only seem to accept geoPHP\Geometry\Geometry...PHP\Geometry\Geometry[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

255
        self::explodeCollections(/** @scrutinizer ignore-type */ $geometries, $reducedGeometries, $geometryTypes);
Loading history...
256
257
        $geometryTypes = array_unique($geometryTypes);
258
        
259
        if (count($geometryTypes) === 1) {
260
            if (count($reducedGeometries) === 1) {
261
                return $reducedGeometries[0];
262
            } else {
263
                $class = '\\geoPHP\\Geometry\\' .
264
                        (strpos($geometryTypes[0], 'Multi') === false ? 'Multi' : '') .
265
                        $geometryTypes[0];
266
                return new $class($reducedGeometries);
267
            }
268
        }
269
        
270
        return new GeometryCollection($reducedGeometries);
271
    }
272
273
    /**
274
     * @param Geometry[]|GeometryCollection[] $unreduced
275
     * @param Geometry[]|GeometryCollection[] $reduced
276
     * @param array<string> $types
277
     * @return void
278
     */
279
    private static function explodeCollections($unreduced, &$reduced, &$types)
280
    {
281
        foreach ($unreduced as $item) {
282
            $geometryType = $item->geometryType();
283
            if ($geometryType === 'GeometryCollection' || strpos($geometryType, 'Multi') === 0) {
284
                self::explodeCollections($item->getComponents(), $reduced, $types);
285
            } else {
286
                $reduced[] = $item;
287
                $types[] = $geometryType;
288
            }
289
        }
290
    }
291
292
    /**
293
     * Build an appropriate Geometry, MultiGeometry, or GeometryCollection to contain the Geometries in it.
294
     *
295
     * @see geos::geom::GeometryFactory::buildGeometry
296
     *
297
     * @param Geometry|Geometry[]|GeometryCollection|GeometryCollection[]|null[] $geometries
298
     * @return Geometry A Geometry of the "smallest", "most type-specific" class that can contain the elements.
299
     * @throws \Exception
300
     */
301
    public static function buildGeometry($geometries)
302
    {
303
        if (empty($geometries)) {
304
            return new GeometryCollection();
305
        }
306
307
        // If it is a single geometry
308
        if ($geometries instanceof Geometry) {
309
            return $geometries;
310
        } else if (!is_array($geometries)) {
311
            throw new \Exception('Input is not a Geometry or array of Geometries');
312
        } elseif (count($geometries) === 1) {
313
            // If it's an array of one, then just parse the one
314
            return self::buildGeometry(array_shift($geometries));
315
        }
316
317
        // So now we either have an array of geometries
318
        /** @var Geometry[]|GeometryCollection[]|null $geometries */
319
        $geometryTypes = [];
320
        $validGeometries = [];
321
        foreach ($geometries as $geometry) {
322
            if ($geometry) {
323
                $geometryTypes[] = $geometry->geometryType();
324
                $validGeometries[] = $geometry;
325
            }
326
        }
327
        $geometryTypes = array_unique($geometryTypes);
328
        
329
        // should never happen
330
        if (empty($geometryTypes)) {
331
            throw new \Exception('Empty geometries or unknown types of geometries given.');
332
        }
333
        
334
        // only one geometry-type
335
        if (count($geometryTypes) === 1) {
336
            $geometryType = 'Multi' . str_ireplace('Multi', '', $geometryTypes[0]);
0 ignored issues
show
Bug introduced by
Are you sure str_ireplace('Multi', '', $geometryTypes[0]) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

336
            $geometryType = 'Multi' . /** @scrutinizer ignore-type */ str_ireplace('Multi', '', $geometryTypes[0]);
Loading history...
337
            foreach ($validGeometries as $geometry) {
338
                if ($geometry->isEmpty()) {
339
                    return new GeometryCollection($validGeometries);
340
                }
341
            }
342
            // only non-empty geometries
343
            $class = '\\geoPHP\\Geometry\\' . $geometryType;
344
            return new $class($validGeometries);
345
        }
346
        
347
        // multiple geometry-types
348
        return new GeometryCollection($validGeometries);
349
    }
350
351
    /**
352
     * Detect a format given a value. This function is meant to be SPEEDY.
353
     * It could make a mistake in XML detection if you are mixing or using namespaces in weird ways
354
     * (i.e. KML inside an RSS feed)
355
     *
356
     * @param mixed $input
357
     * @return string|false
358
     */
359
    public static function detectFormat($input)
360
    {
361
        $mem = fopen('php://memory', 'x+');
362
        fwrite($mem, $input, 11); // Write 11 bytes - we can detect the vast majority of formats in the first 11 bytes
363
        fseek($mem, 0);
364
365
        $bin = fread($mem, 11);
366
        $bytes = unpack("c*", $bin);
367
368
        // If bytes is empty, then we were passed empty input
369
        if (empty($bytes)) {
370
            return false;
371
        }
372
373
        // First char is a tab, space or carriage-return. trim it and try again
374
        if (in_array($bytes[1], [9,10,32], true)) {
375
            $input = ltrim($input);
376
            return geoPHP::detectFormat($input);
377
        }
378
379
        // Detect WKB or EWKB -- first byte is 1 (little endian indicator)
380
        if ($bytes[1] == 1 || $bytes[1] == 0) {
381
            $wkbType = current(unpack($bytes[1] == 1 ? 'V' : 'N', substr($bin, 1, 4)));
382
            if (array_search($wkbType & 0xF, Adapter\WKB::$typeMap)) {
383
                // If SRID byte is TRUE (1), it's EWKB
384
                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...
385
                    return 'ewkb';
386
                } else {
387
                    return 'wkb';
388
                }
389
            }
390
        }
391
392
        // Detect HEX encoded WKB or EWKB (PostGIS format) -- first byte is 48, second byte is 49 (hex '01' => first-byte = 1)
393
        // The shortest possible WKB string (LINESTRING EMPTY) is 18 hex-chars (9 encoded bytes) long
394
        // This differentiates it from a geohash, which is always shorter than 13 characters.
395
        if ($bytes[1] == 48 && ($bytes[2] == 49 || $bytes[2] == 48) && strlen($input) > 12) {
396
            $mask = current(unpack($bytes[2] == 49 ? 'V' : 'N', hex2bin(substr($bin, 2, 8)))) & Adapter\WKB::SRID_MASK;
397
            return $mask == Adapter\WKB::SRID_MASK ? 'ewkb:true' : 'wkb:true';
398
        }
399
400
        // Detect GeoJSON - first char starts with {
401
        if ($bytes[1] == 123) {
402
            return 'json';
403
        }
404
405
        // Detect EWKT - strats with "SRID=number;"
406
        if (substr($input, 0, 5) === 'SRID=') {
407
            return 'ewkt';
408
        }
409
410
        // Detect WKT - starts with a geometry type name
411
        if (Adapter\WKT::isWktType(strstr($input, ' ', true))) {
412
            return 'wkt';
413
        }
414
415
        // Detect XML -- first char is <
416
        if ($bytes[1] == 60) {
417
            // grab the first 1024 characters
418
            $string = substr($input, 0, 1024);
419
            if (strpos($string, '<kml') !== false) {
420
                return 'kml';
421
            }
422
            if (strpos($string, '<coordinate') !== false) {
423
                return 'kml';
424
            }
425
            if (strpos($string, '<gpx') !== false) {
426
                return 'gpx';
427
            }
428
            if (strpos($string, '<osm ') !== false) {
429
                return 'osm';
430
            }
431
            if (preg_match('/<[a-z]{3,20}>/', $string) !== false) {
432
                return 'georss';
433
            }
434
        }
435
436
        // We need an 8 byte string for geohash and unpacked WKB / WKT
437
        fseek($mem, 0);
438
        $string = trim(fread($mem, 8));
439
440
        // Detect geohash - geohash ONLY contains lowercase chars and numerics
441
        $matches = [];
442
        preg_match('/[' . GeoHash::$characterTable . ']+/', $string, $matches);
443
        if (isset($matches[0]) && $matches[0] == $string && strlen($input) <= 13) {
444
            return 'geohash';
445
        }
446
        preg_match('/^[a-f0-9]+$/', $string, $matches);
447
        
448
        return isset($matches[0]) ? 'twkb:true' : 'twkb';
449
    }
450
}
451