geoPHP::version()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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