├── .gitignore ├── .phpcs.xml ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── docs ├── _config.yml └── api.html ├── phpbench.json ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── phpunit.xml ├── src ├── Adapter │ ├── BinaryReader.php │ ├── BinaryWriter.php │ ├── EWKB.php │ ├── EWKT.php │ ├── GPX.php │ ├── GeoAdapter.php │ ├── GeoHash.php │ ├── GeoJSON.php │ ├── GeoRSS.php │ ├── GoogleGeocode.php │ ├── GpxTypes.php │ ├── KML.php │ ├── OSM.php │ ├── TWKB.php │ ├── WKB.php │ └── WKT.php ├── Exception │ ├── Exception.php │ ├── FileFormatException.php │ ├── IOException.php │ ├── InvalidGeometryException.php │ ├── InvalidXmlException.php │ └── UnsupportedMethodException.php ├── Geometry │ ├── Collection.php │ ├── Curve.php │ ├── Geometry.php │ ├── GeometryCollection.php │ ├── LineString.php │ ├── MultiCurve.php │ ├── MultiGeometry.php │ ├── MultiLineString.php │ ├── MultiPoint.php │ ├── MultiPolygon.php │ ├── MultiSurface.php │ ├── Point.php │ ├── Polygon.php │ └── Surface.php └── geoPHP.php └── tests ├── Benchmark └── Geometry │ ├── AbstractGeometryBench.php │ ├── GeometryCollectionBench.php │ ├── LineStringBench.php │ ├── PointBench.php │ └── PolygonBench.php ├── geometryPerformance.php ├── input ├── 20120702.gpx ├── an_empty_polygon.wkt ├── barret_spur.gpx ├── big_n_ugly.kml ├── box.georss ├── cdata.kml ├── circle.georss ├── countries_ne_110m.json ├── empty_point.wkt ├── fells_loop.gpx ├── geometrycollection.georss ├── geometrycollection.wkt ├── line.georss ├── linestring.wkt ├── long.geohash ├── multilinestring.ewkt ├── multilinestring.wkt ├── multipolygon.wkb ├── multipolygon.wkt ├── multipolygon2.wkt ├── opposite.gpx ├── path.kml ├── paths_big.json ├── pentagon.kml ├── point.georss ├── point.kml ├── point.wkt ├── point_earth.kml ├── polygon.georss ├── polygon.wkt ├── polygon2.wkt ├── polygon3.wkt ├── polygon4.wkt ├── polygon_spaces.wkt ├── route.gpx ├── short.geohash ├── simple_point.json └── track.gpx ├── postgis.php ├── test.php └── unit ├── Adapter ├── EWKTTest.php ├── GeoHashTest.php ├── WKTReaderTest.php └── WKTWriterTest.php ├── Exception ├── FileFormatExceptionTest.php ├── IOExceptionTest.php ├── InvalidGeometryException.php ├── InvalidXmlExceptionTest.php └── UnsupportedMethodExceptionTest.php ├── Geometry ├── CollectionTest.php ├── CurveTest.php ├── LineStringTest.php ├── MultiCurveTest.php ├── MultiGeometryTest.php ├── MultiPointTest.php ├── PointTest.php ├── PolygonTest.php └── SurfaceTest.php └── legacy ├── AdaptersTest.php ├── File20120702Test.php ├── GeosTest.php └── MethodsTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /build 3 | composer.phar 4 | .phpunit.result.cache 5 | phpstan.neon 6 | 7 | # JetBrains (PhpStorm) 8 | .idea/ 9 | -------------------------------------------------------------------------------- /.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for PHP_CodeSniffer itself. 4 | 5 | src 6 | tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | error 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | warning 34 | warning 35 | 36 | 37 | 38 | 39 | 40 | 1 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | error 79 | 80 | 81 | 82 | 83 | error 84 | 85 | 86 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # docs available at http://docs.travis-ci.com/user/languages/php/ 2 | language: php 3 | 4 | os: linux 5 | dist: bionic 6 | version: ~> 1 7 | 8 | before_script: 9 | - composer self-update 10 | 11 | cache: 12 | directories: 13 | - vendor 14 | - $HOME/.composer/cache/files 15 | - build/cache 16 | - /tmp/phpstan 17 | 18 | install: 19 | - composer validate 20 | - if [[ -z $PHPSTAN ]]; then export COMPOSER_REMOVE="phpstan/phpstan"; fi 21 | - if [[ $DISABLE_UNIT ]]; then export COMPOSER_REMOVE="$COMPOSER_REMOVE phpunit/phpunit"; fi 22 | - if [[ $DISABLE_BENCH ]]; then export COMPOSER_REMOVE="$COMPOSER_REMOVE phpbench/phpbench"; fi 23 | - if [[ $TRAVIS_PHP_VERSION = "nightly" ]]; then export COMPOSER_FLAGS="--ignore-platform-req=php+"; fi 24 | - | 25 | if [[ $COMPOSER_REMOVE ]]; then 26 | echo "Removing $COMPOSER_REMOVE dependencies" 27 | composer remove --dev --no-update --no-interaction $COMPOSER_FLAGS $COMPOSER_REMOVE 28 | fi 29 | - composer update --prefer-dist --no-interaction $COMPOSER_FLAGS 30 | 31 | # TODO Install geos library -- as a matrix test 32 | # TODO optionally set up a postgis database for testing 33 | 34 | script: 35 | - mkdir -p build/cache 36 | - if [[ -z $DISABLE_UNIT ]]; then composer run unit; fi 37 | - composer run test-input 38 | - composer run performance 39 | - if [[ -z $DISABLE_BENCH ]]; then vendor/bin/phpbench run --report=aggregate --progress=plain --tag=$TRAVIS_BRANCH; fi 40 | 41 | jobs: 42 | include: 43 | - stage: Smoke 44 | php: 8.1 45 | env: PHPCS=1 46 | script: composer run cs-warning 47 | - stage: Smoke 48 | php: 8.1 49 | env: PHPSTAN=1 50 | script: vendor/bin/phpstan analyse 51 | 52 | - stage: Test 53 | php: 7.1 54 | name: PHP 7.1 without Unit tests and Bench 55 | env: 56 | - DISABLE_UNIT=1 57 | - DISABLE_BENCH=1 58 | - stage: Test 59 | php: 7.2 60 | name: PHP 7.2 without Unit tests 61 | env: DISABLE_UNIT=1 62 | - stage: Test 63 | php: 7.3 64 | - stage: Test 65 | php: 7.4 66 | - stage: Test 67 | php: 8.0 68 | - stage: Test 69 | php: 8.1 70 | 71 | - stage: Test 72 | php: nightly 73 | 74 | allow_failures: 75 | - php: nightly 76 | 77 | fast_finish: true 78 | 79 | after_success: 80 | - | 81 | - if [[ -f "build/logs/clover.xml" && $TRAVIS_PHP_VERSION != "nightly" ]]; then 82 | travis_retry php vendor/bin/php-coveralls -v; 83 | fi -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "intelephense.environment.phpVersion": "7.1" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://travis-ci.com/funiq/geoPHP) 2 | [](https://coveralls.io/github/funiq/geoPHP?branch=development) 3 | 4 | GeoPHP is a open-source native PHP library for doing geometry operations. It is a fork of famous [geoPHP](https://github.com/phayes/geoPHP) library by Patrick Hayes. 5 | 6 | It is written entirely in PHP and can therefore run on shared hosts. It can read and write a wide variety of formats: WKT (EWKT), WKB (EWKB), TWKB, GeoJSON, 7 | KML, GPX, and GeoRSS. It works with all Simple-Feature geometries (Point, LineString, Polygon, GeometryCollection etc.) 8 | and can be used to get centroids, bounding-boxes, area, and a wide variety of other useful information. 9 | 10 | GeoPHP also helpfully wraps the GEOS php extension so that applications can get a transparent performance 11 | increase when GEOS is installed on the server. When GEOS is installed, geoPHP also becomes 12 | fully compliant with the OpenGIS® Implementation Standard for Geographic information. With GEOS you get the 13 | full-set of openGIS functions in PHP like Union, IsWithin, Touches etc. This means that applications 14 | get a useful "core-set" of geometry operations that work in all environments, and an "extended-set"of operations 15 | for environments that have GEOS installed. 16 | 17 | See the _getting started_ section below for references and examples of everything that geoPHP can do. 18 | 19 | Forks and contributions are welcome. Please open [issues](https://github.com/funiq/geoPHP/issues), send [pull](https://github.com/funiq/geoPHP/pulls) requests and I will merge them into the main branch. 20 | 21 | ## Getting Started 22 | 23 | ### Example usage 24 | 25 | ```php 26 | area(); 32 | $centroid = $polygon->centroid(); 33 | $centX = $centroid->x(); 34 | $centY = $centroid->y(); 35 | 36 | print "This polygon has an area of ".$area." and a centroid with X=".$centX." and Y=".$centY; 37 | 38 | // MultiPoint json example 39 | print ""; 40 | $json = 41 | '{ 42 | "type": "MultiPoint", 43 | "coordinates": [ 44 | [100.0, 0.0], [101.0, 1.0] 45 | ] 46 | }'; 47 | 48 | $multipoint = geoPHP::load($json, 'json'); 49 | $multipoint_points = $multipoint->getComponents(); 50 | $first_wkt = $multipoint_points[0]->out('wkt'); 51 | 52 | print "This multipoint has ".$multipoint->numGeometries()." points. The first point has a wkt representation of ".$first_wkt; 53 | ``` 54 | 55 | ### More Examples 56 | 57 | The Well Known Text (WKT) and Well Known Binary (WKB) support is ideal for integrating with MySQL's or PostGIS's spatial capability. 58 | Once you have SELECTed your data with `'AsText('geo_field')'` or `'AsBinary('geo_field')'`, you can put it straight into 59 | geoPHP (can be wkt or wkb, but must be the same as how you extracted it from your database): 60 | 61 | $geom = geoPHP::load($dbRow,'wkt'); 62 | 63 | You can collect multiple geometries into one (note that you must use wkt for this): 64 | 65 | $geom = geoPHP::load("GEOMETRYCOLLECTION(".$dbString1.",".$dbString2.")",'wkt'); 66 | 67 | Calling get components returns the sub-geometries within a geometry as an array. 68 | 69 | $geom2 = geoPHP::load("GEOMETRYCOLLECTION(LINESTRING(1 1,5 1,5 5,1 5,1 1),LINESTRING(2 2,2 3,3 3,3 2,2 2))"); 70 | $geomComponents = $geom2->getComponents(); //an array of the two linestring geometries 71 | $linestring1 = $geomComponents[0]->getComponents(); //an array of the first linestring's point geometries 72 | $linestring2 = $geomComponents[1]->getComponents(); 73 | echo $linestring1[0]->x() . ", " . $linestring1[0]->y(); //outputs '1, 1' 74 | 75 | An alternative is to use the `asArray()` method. Using the above geometry collection of two linestrings, 76 | 77 | $geometryArray = $geom2->asArray(); 78 | echo $geometryArray[0][0][0] . ", " . $geometryArray[0][0][1]; //outputs '1, 1' 79 | 80 | Clearly, more complex analysis is possible. 81 | 82 | echo $geom2->envelope()->area(); 83 | 84 | 85 | ### Working with PostGIS 86 | 87 | geoPHP, through it's EWKB adapter, has good integration with postGIS. Here's an example of reading and writing postGIS geometries 88 | 89 | ```php 90 | out('ewkb')); 112 | pg_query($connection, "INSERT INTO $table ($column) values (GeomFromWKB('$insert_string'))"); 113 | } 114 | 115 | // Using a direct SELECT and INSERTs in PostGIS without using wrapping functions 116 | $result = pg_fetch_all(pg_query($connection, "SELECT $column as geom FROM $table")); 117 | foreach ($result as $item) { 118 | $wkb = pack('H*',$item['geom']); // Unpacking the hex blob 119 | $geom = geoPHP::load($wkb, 'ewkb'); // We now have a geoPHP Geometry 120 | 121 | // To insert directly into postGIS we need to unpack the WKB 122 | $unpacked = unpack('H*', $geom->out('ewkb')); 123 | $insert_string = $unpacked[1]; 124 | pg_query($connection, "INSERT INTO $table ($column) values ('$insert_string')"); 125 | } 126 | ``` 127 | 128 | ## Documentation 129 | 130 | In progress… You can read the doc for original phayes/geoPHP at [geophp.net](https://geophp.net "GeoPHP homepage") 131 | 132 | ## Credit 133 | 134 | - Maintainer: Péter Báthory 135 | - Original author: Patrick Hayes 136 | 137 | Additional Contributors: 138 | * GeoMemes Research () 139 | * HighWire Press () and GeoScienceWorld () 140 | * Arnaud Renevier (gisconverter.php) 141 | * Dave Tarc 142 | * Elliott Hunston (documentation) 143 | 144 | This library is open-source and dual-licensed under both the Modified BSD License and GPLv2. Either license may be used at your option. 145 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "funiq/geophp", 3 | "license": [ 4 | "GPL-2.0-or-later", 5 | "BSD-3-Clause" 6 | ], 7 | "type": "library", 8 | "description": "Open-source native PHP library for doing geometry operations. Can read and write a wide variety of formats: (E)WKT, (E)WKB, TWKB, GeoJSON, KML, GPX, GeoRSS. Works with all Simple-Feature geometries (Point, LineString, Polygon...) and can be used to get centroids, bounding-boxes, area, etc.", 9 | "keywords": ["geophp", "gis", "geometry", "converter", "linestring", "polygon", "wkt", "wkb", "kml", "gpx", "geojson", "twkb"], 10 | "homepage": "https://github.com/funiq/geoPHP", 11 | "authors": [ 12 | { 13 | "name": "Patrick Hayes", 14 | "role": "Original creator" 15 | }, 16 | { 17 | "name": "Péter Báthory", 18 | "role": "Developer" 19 | } 20 | ], 21 | "autoload": { 22 | "psr-4": { 23 | "geoPHP\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "geoPHP\\Tests\\": "tests/" 29 | } 30 | }, 31 | "require": { 32 | "php": "^7.1|^8.0" 33 | }, 34 | "require-dev": { 35 | "phpunit/phpunit": "^8.0|^9.0", 36 | "php-coveralls/php-coveralls": "2.*", 37 | "squizlabs/php_codesniffer": "3.*", 38 | "phpstan/phpstan": "^1.4.0", 39 | "phpbench/phpbench": "^1.0" 40 | }, 41 | "suggest": { 42 | "ext-geos": "GEOS allows more advanced operations" 43 | }, 44 | "scripts": { 45 | "tests": [ 46 | "@performance", 47 | "@bench", 48 | "@unit", 49 | "@test-input" 50 | ], 51 | "analyze": [ 52 | "@cs-warning", 53 | "@stan" 54 | ], 55 | "all": [ 56 | "@analyze", 57 | "echo \"Let's cool down a bit before testing benchmark ...\" && sleep 1", 58 | "@tests" 59 | ], 60 | "cs": "phpcs", 61 | "cs-warning": "phpcs -w", 62 | "cs-fix": "phpcbf", 63 | "stan": "phpstan analyze --no-progress", 64 | "unit": [ 65 | "@putenv XDEBUG_MODE=coverage", 66 | "phpunit" 67 | ], 68 | "test-input": "cd tests && php test.php && cd ..", 69 | "performance": "cd tests && php geometryPerformance.php && cd ..", 70 | "bench": "vendor/bin/phpbench run --report=aggregate --sleep 10000" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /phpbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json", 3 | "runner.bootstrap": "vendor/autoload.php", 4 | "runner.path": "tests/Benchmark", 5 | "storage.xml_storage_path": "build/phpbench/", 6 | "runner.php_config": { 7 | "memory_limit": "1G", 8 | "xdebug.mode": "none" 9 | }, 10 | "runner.iterations": 10, 11 | "runner.retry_threshold": 10, 12 | "runner.revs": 50, 13 | "runner.assert": "mode(variant.time.avg) as ms <= mode(baseline.time.avg) as ms +/- 8%" 14 | } -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | parameters: 4 | level: 6 5 | paths: 6 | - src/ 7 | - tests/ 8 | inferPrivatePropertyTypeFromConstructor: true 9 | treatPhpDocTypesAsCertain: false 10 | reportUnmatchedIgnoredErrors: false 11 | ignoreErrors: 12 | - '#\:\:centroid\(\) should return geoPHP\\Geometry\\Point but returns geoPHP\\Geometry\\Geometry\|null.$#' 13 | - '#\:\:boundary\(\) should return geoPHP\\Geometry\\.+ but returns geoPHP\\Geometry\\Geometry\|null.$#' 14 | - '#Parameter \#1 \$connection of function pg_query expects resource, PgSql\\Connection given.$#' 15 | - '#Parameter \#1 \$result of function pg_fetch_all expects PgSql\\Result, resource\|false given.$#' -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | tests/unit/Geometry 12 | tests/unit/Exception 13 | tests/unit/Adapter 14 | 15 | 16 | 19 | 20 | src/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Adapter/BinaryReader.php: -------------------------------------------------------------------------------- 1 | 8 | * @since 2016-02-18 9 | * 10 | * This code is open-source and licenced under the Modified BSD License. 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | namespace geoPHP\Adapter; 16 | 17 | /** 18 | * Helper class BinaryReader 19 | * 20 | * A simple binary reader supporting both byte orders 21 | */ 22 | class BinaryReader 23 | { 24 | const BIG_ENDIAN = 0; 25 | const LITTLE_ENDIAN = 1; 26 | 27 | /** 28 | * @var resource 29 | */ 30 | private $buffer; 31 | 32 | /** 33 | * @var int 34 | */ 35 | private $endianness = 0; 36 | 37 | /** 38 | * BinaryReader constructor. 39 | * Opens a memory buffer with the given input 40 | * 41 | * @param string $input 42 | */ 43 | public function __construct(string $input) 44 | { 45 | // if (@is_readable($input)) { 46 | // $this->buffer = fopen($input, 'r+'); 47 | // } else { 48 | $this->buffer = fopen('php://memory', 'x+'); 49 | fwrite($this->buffer, $input); 50 | fseek($this->buffer, 0); 51 | // } 52 | } 53 | 54 | /** 55 | * Closes the memory buffer 56 | */ 57 | public function close(): void 58 | { 59 | fclose($this->buffer); 60 | } 61 | 62 | /** 63 | * @param int $endian self::BIG_ENDIAN or self::LITTLE_ENDIAN 64 | */ 65 | public function setEndianness(int $endian): void 66 | { 67 | $this->endianness = $endian === self::BIG_ENDIAN ? self::BIG_ENDIAN : self::LITTLE_ENDIAN; 68 | } 69 | 70 | /** 71 | * @return int Returns 0 if reader is in BigEndian mode or 1 if in LittleEndian mode 72 | */ 73 | public function getEndianness(): int 74 | { 75 | return $this->endianness; 76 | } 77 | 78 | /** 79 | * Reads a signed 8-bit integer from the buffer 80 | * @return int|null 81 | */ 82 | public function readSInt8(): ?int 83 | { 84 | $char = fread($this->buffer, 1); 85 | return $char !== '' ? current(unpack("c", $char)) : null; 86 | } 87 | 88 | /** 89 | * Reads an unsigned 8-bit integer from the buffer 90 | * @return int|null 91 | */ 92 | public function readUInt8(): ?int 93 | { 94 | $char = fread($this->buffer, 1); 95 | return $char !== '' ? current(unpack("C", $char)) : null; 96 | } 97 | 98 | /** 99 | * Reads an unsigned 32-bit integer from the buffer 100 | * @return int|null 101 | */ 102 | public function readUInt32() 103 | { 104 | $int32 = fread($this->buffer, 4); 105 | return $int32 !== '' ? current(unpack($this->endianness == self::LITTLE_ENDIAN ? 'V' : 'N', $int32)) : null; 106 | } 107 | 108 | /** 109 | * Reads one or more double values from the buffer 110 | * @param int $length How many double values to read. Default is 1 111 | * @return float[] 112 | */ 113 | public function readDoubles($length = 1) 114 | { 115 | $bin = fread($this->buffer, $length); 116 | return $this->endianness == self::LITTLE_ENDIAN 117 | ? array_values(unpack("d*", $bin)) 118 | : array_reverse(unpack("d*", strrev($bin))); 119 | } 120 | 121 | /** 122 | * Reads an unsigned base-128 varint from the buffer 123 | * 124 | * Ported from https://github.com/cschwarz/wkx/blob/master/lib/binaryreader.js 125 | * 126 | * @return int 127 | */ 128 | public function readUVarInt() 129 | { 130 | $result = 0; 131 | $bytesRead = 0; 132 | 133 | do { 134 | $nextByte = $this->readUInt8(); 135 | $result += ($nextByte & 0x7F) << (7 * $bytesRead); 136 | $bytesRead++; 137 | } while ($nextByte >= 0x80); 138 | return $result; 139 | } 140 | 141 | /** 142 | * Reads a signed base-128 varint from the buffer 143 | * 144 | * @return int 145 | */ 146 | public function readSVarInt() 147 | { 148 | return self::zigZagDecode($this->readUVarInt()); 149 | } 150 | 151 | /** 152 | * ZigZag decoding maps unsigned integers to signed integers 153 | * 154 | * @param int $value Encrypted positive integer value 155 | * @return int Decoded signed integer 156 | */ 157 | public static function zigZagDecode($value) 158 | { 159 | return ($value & 1) === 0 ? $value >> 1 : -($value >> 1) - 1; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Adapter/BinaryWriter.php: -------------------------------------------------------------------------------- 1 | 8 | * @since 2016-02-18 9 | * 10 | * This code is open-source and licenced under the Modified BSD License. 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | namespace geoPHP\Adapter; 16 | 17 | /** 18 | * Helper class BinaryWriter. 19 | * 20 | * A simple binary writer supporting both byte orders. 21 | */ 22 | class BinaryWriter 23 | { 24 | const BIG_ENDIAN = 0; 25 | const LITTLE_ENDIAN = 1; 26 | 27 | private $endianness = 0; 28 | 29 | /** 30 | * @param integer $endianness Constant value self::BIG_ENDIAN or self::LITTLE_ENDIAN. 31 | */ 32 | public function __construct(int $endianness = 0) 33 | { 34 | $this->endianness = $endianness === self::BIG_ENDIAN 35 | ? self::BIG_ENDIAN 36 | : self::LITTLE_ENDIAN; 37 | } 38 | 39 | /** 40 | * @return bool Returns true if Writer is in BigEndian mode 41 | */ 42 | public function isBigEndian(): bool 43 | { 44 | return $this->endianness === self::BIG_ENDIAN; 45 | } 46 | 47 | /** 48 | * @return bool Returns true if Writer is in LittleEndian mode 49 | */ 50 | public function isLittleEndian(): bool 51 | { 52 | return $this->endianness === self::LITTLE_ENDIAN; 53 | } 54 | 55 | /** 56 | * Writes a signed 8-bit integer 57 | * @param int|float $value 58 | * @return string The integer as a binary string 59 | */ 60 | public function writeSInt8($value): string 61 | { 62 | return pack('c', (int) $value); 63 | } 64 | 65 | /** 66 | * Writes an unsigned 8-bit integer 67 | * @param int|float $value 68 | * @return string The integer as a binary string 69 | */ 70 | public function writeUInt8($value): string 71 | { 72 | return pack('C', (int) $value); 73 | } 74 | 75 | /** 76 | * Writes an unsigned 32-bit integer 77 | * @param int|float $value 78 | * @return string The integer as a binary string 79 | */ 80 | public function writeUInt32($value): string 81 | { 82 | return pack($this->isLittleEndian() ? 'V' : 'N', (int) $value); 83 | } 84 | 85 | /** 86 | * Writes a double 87 | * @param float $value 88 | * @return string The floating point number as a binary string 89 | */ 90 | public function writeDouble($value): string 91 | { 92 | return $this->isLittleEndian() ? pack('d', (float) $value) : strrev(pack('d', (float) $value)); 93 | } 94 | 95 | /** 96 | * Writes a positive integer as an unsigned base-128 varint 97 | * 98 | * Ported from https://github.com/cschwarz/wkx/blob/master/lib/binaryreader.js 99 | * 100 | * @param int|float $value 101 | * @return string The integer as a binary string 102 | */ 103 | public function writeUVarInt($value): string 104 | { 105 | $value = (int) $value; 106 | $out = ''; 107 | 108 | while (($value & 0xFFFFFF80) !== 0) { 109 | $out .= $this->writeUInt8(($value & 0x7F) | 0x80); 110 | // Zero fill by 7 zero 111 | if ($value >= 0) { 112 | $value >>= 7; 113 | } else { 114 | $value = ((~$value) >> 7) ^ (0x7fffffff >> (7 - 1)); 115 | } 116 | } 117 | 118 | $out .= $this->writeUInt8($value & 0x7F); 119 | 120 | return $out; 121 | } 122 | 123 | /** 124 | * Writes an integer as a signed base-128 varint 125 | * @param int|float $value 126 | * @return string The integer as a binary string 127 | */ 128 | public function writeSVarInt($value): string 129 | { 130 | return $this->writeUVarInt(self::zigZagEncode((int) $value)); 131 | } 132 | 133 | /** 134 | * ZigZag encoding maps signed integers to unsigned integers 135 | * 136 | * @param int $value Signed integer 137 | * @return int Encoded positive integer value 138 | */ 139 | public static function zigZagEncode($value) 140 | { 141 | return ($value << 1) ^ ($value >> 31); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Adapter/EWKB.php: -------------------------------------------------------------------------------- 1 | SRID = $geometry->getSRID(); 15 | $this->hasSRID = $this->SRID !== null; 16 | return parent::write($geometry, $writeAsHex, $bigEndian); 17 | } 18 | 19 | protected function writeType(Geometry $geometry, bool $writeSRID = false): string 20 | { 21 | return parent::writeType($geometry, true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Adapter/EWKT.php: -------------------------------------------------------------------------------- 1 | getSRID(); 25 | if ($srid) { 26 | return 'SRID=' . $srid . ';' . $geometry->out('wkt'); 27 | } else { 28 | return $geometry->out('wkt'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Adapter/GeoAdapter.php: -------------------------------------------------------------------------------- 1 | type) || !is_string($input->type)) { 41 | throw new IOException('Invalid GeoJSON'); 42 | } 43 | 44 | return $this->processGeoJSON($input); 45 | } 46 | 47 | /** 48 | * @param stdClass $input 49 | * @return Geometry 50 | * @throws IOException 51 | */ 52 | public function processGeoJSON(stdClass $input): Geometry 53 | { 54 | // Check to see if it's a FeatureCollection 55 | if ($input->type == 'FeatureCollection' && isset($input->features)) { 56 | $geometries = []; 57 | foreach ($input->features as $feature) { 58 | $geometries[] = $this->processGeoJSON($feature); 59 | } 60 | return geoPHP::buildGeometry($geometries); 61 | } 62 | 63 | // Check to see if it's a Feature 64 | if ($input->type == 'Feature') { 65 | return $this->geoJSONFeatureToGeometry($input); 66 | } 67 | 68 | // It's a geometry - process it 69 | return $this->geoJSONObjectToGeometry($input); 70 | } 71 | 72 | /** 73 | * @param stdClass $input 74 | * @return int|null 75 | */ 76 | private function getSRID(stdClass $input): ?int 77 | { 78 | if (isset($input->crs->properties->name)) { 79 | // parse CRS codes in forms "EPSG:1234" and "urn:ogc:def:crs:EPSG::1234" 80 | preg_match('#EPSG[:]+(\d+)#', $input->crs->properties->name, $m); 81 | return isset($m[1]) ? (int) $m[1] : null; 82 | } 83 | return null; 84 | } 85 | 86 | /** 87 | * @param stdClass $obj 88 | * @return Geometry 89 | * @throws IOException 90 | */ 91 | private function geoJSONFeatureToGeometry(stdClass $obj): Geometry 92 | { 93 | $geometry = $this->processGeoJSON($obj->geometry); 94 | if (isset($obj->properties)) { 95 | foreach ($obj->properties as $property => $value) { 96 | $geometry->setData($property, $value); 97 | } 98 | } 99 | 100 | return $geometry; 101 | } 102 | 103 | /** 104 | * @param stdClass $obj 105 | * @return Geometry 106 | * @throws \Exception 107 | */ 108 | private function geoJSONObjectToGeometry(stdClass $obj): Geometry 109 | { 110 | $type = $obj->type; 111 | 112 | if ($type == 'GeometryCollection') { 113 | return $this->geoJSONObjectToGeometryCollection($obj); 114 | } 115 | $method = 'arrayTo' . $type; 116 | /** @var GeometryCollection $geometry */ 117 | $geometry = $this->$method($obj->coordinates); 118 | $geometry->setSRID($this->getSRID($obj)); 119 | return $geometry; 120 | } 121 | 122 | /** 123 | * @param array $coordinates Array of coordinates 124 | * @return Point 125 | */ 126 | private function arrayToPoint(array $coordinates): Point 127 | { 128 | switch (count($coordinates)) { 129 | case 2: 130 | return new Point($coordinates[0], $coordinates[1]); 131 | case 3: 132 | return new Point($coordinates[0], $coordinates[1], $coordinates[2]); 133 | case 4: 134 | return new Point($coordinates[0], $coordinates[1], $coordinates[2], $coordinates[3]); 135 | default: 136 | return new Point(); 137 | } 138 | } 139 | 140 | /** 141 | * @param array> $components 142 | * @return LineString 143 | */ 144 | private function arrayToLineString(array $components): LineString 145 | { 146 | $points = []; 147 | foreach ($components as $componentArray) { 148 | $points[] = $this->arrayToPoint($componentArray); 149 | } 150 | return new LineString($points); 151 | } 152 | 153 | /** 154 | * @param array>> $components 155 | * @return Polygon 156 | */ 157 | private function arrayToPolygon(array $components): Polygon 158 | { 159 | $lines = []; 160 | foreach ($components as $componentArray) { 161 | $lines[] = $this->arrayToLineString($componentArray); 162 | } 163 | return new Polygon($lines); 164 | } 165 | 166 | /** 167 | * @param array> $components 168 | * @return MultiPoint 169 | */ 170 | private function arrayToMultiPoint(array $components): MultiPoint 171 | { 172 | $points = []; 173 | foreach ($components as $componentArray) { 174 | $points[] = $this->arrayToPoint($componentArray); 175 | } 176 | return new MultiPoint($points); 177 | } 178 | 179 | /** 180 | * @param array>> $components 181 | * @return MultiLineString 182 | */ 183 | private function arrayToMultiLineString(array $components): MultiLineString 184 | { 185 | $lines = []; 186 | foreach ($components as $componentArray) { 187 | $lines[] = $this->arrayToLineString($componentArray); 188 | } 189 | return new MultiLineString($lines); 190 | } 191 | 192 | /** 193 | * @param array>>> $components 194 | * @return MultiPolygon 195 | */ 196 | private function arrayToMultiPolygon(array $components): MultiPolygon 197 | { 198 | $polygons = []; 199 | foreach ($components as $componentArray) { 200 | $polygons[] = $this->arrayToPolygon($componentArray); 201 | } 202 | return new MultiPolygon($polygons); 203 | } 204 | 205 | /** 206 | * @param stdClass $obj 207 | * @throws IOException 208 | * @return GeometryCollection 209 | */ 210 | private function geoJSONObjectToGeometryCollection(stdClass $obj): Geometry 211 | { 212 | $geometries = []; 213 | if (!property_exists($obj, 'geometries')) { 214 | throw new IOException('Invalid GeoJSON: GeometryCollection without geometry components'); 215 | } 216 | foreach ($obj->geometries ?: [] as $componentObject) { 217 | $geometries[] = $this->geoJSONObjectToGeometry($componentObject); 218 | } 219 | $collection = new GeometryCollection($geometries); 220 | $collection->setSRID($this->getSRID($obj)); 221 | return $collection; 222 | } 223 | 224 | /** 225 | * Serializes an object into a geojson string 226 | * 227 | * 228 | * @param Geometry $geometry The object to serialize 229 | * 230 | * @return string The GeoJSON string 231 | */ 232 | public function write(Geometry $geometry): string 233 | { 234 | return json_encode($this->geometryToGeoJsonArray($geometry)); 235 | } 236 | 237 | 238 | 239 | /** 240 | * Creates a geoJSON array. 241 | * 242 | * If the root geometry is a GeometryCollection, and any of its geometries has data, 243 | * the root element will be a FeatureCollection with Feature elements (with the data). 244 | * If the root geometry has data, it will be included in a Feature object that contains the data. 245 | * 246 | * The geometry should have geographical coordinates since CRS support has been removed from geoJSON 247 | * specification (RFC 7946). 248 | * The geometry should'nt be measured, since geoJSON specification (RFC 7946) only supports the dimensional 249 | * positions. 250 | * 251 | * @param Geometry $geometry 252 | * @param bool|null $isRoot Is geometry the root geometry? 253 | * @return array{type: string, geometries: array 254 | * }|array{type: string, geometry: array, properties: array 255 | * }|array{type: string, features: array 256 | * }|array{type: string, coordinates: array} 257 | */ 258 | public function geometryToGeoJsonArray(Geometry $geometry, ?bool $isRoot = true): array 259 | { 260 | if ($geometry->geometryType() === Geometry::GEOMETRY_COLLECTION) { 261 | $components = []; 262 | $isFeatureCollection = false; 263 | foreach ($geometry->getComponents() as $component) { 264 | if ($component->getData() !== null) { 265 | $isFeatureCollection = true; 266 | } 267 | $components[] = $this->geometryToGeoJsonArray($component, false); 268 | } 269 | if (!$isFeatureCollection || !$isRoot) { 270 | return [ 271 | 'type' => 'GeometryCollection', 272 | 'geometries' => $components 273 | ]; 274 | } else { 275 | $features = []; 276 | foreach ($geometry->getComponents() as $i => $component) { 277 | $features[] = [ 278 | 'type' => 'Feature', 279 | 'properties' => $component->getData(), 280 | 'geometry' => $components[$i], 281 | ]; 282 | } 283 | return [ 284 | 'type' => 'FeatureCollection', 285 | 'features' => $features 286 | ]; 287 | } 288 | } 289 | 290 | if ($isRoot && $geometry->getData() !== null) { 291 | return [ 292 | 'type' => 'Feature', 293 | 'properties' => $geometry->getData(), 294 | 'geometry' => [ 295 | 'type' => $geometry->geometryType(), 296 | 'coordinates' => $geometry->isEmpty() ? [] : $geometry->asArray() 297 | ] 298 | ]; 299 | } 300 | $object = [ 301 | 'type' => $geometry->geometryType(), 302 | 'coordinates' => $geometry->isEmpty() ? [] : $geometry->asArray() 303 | ]; 304 | return $object; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Adapter/GeoRSS.php: -------------------------------------------------------------------------------- 1 | /s', '', $georss); 51 | 52 | // Load into DOMDocument 53 | $this->xmlObject = new \DOMDocument(); 54 | $loadSuccess = @$this->xmlObject->loadXML($georss); 55 | 56 | if (!$loadSuccess) { 57 | throw new InvalidXmlException(); 58 | } 59 | 60 | $geometries = array_merge( 61 | $this->parsePoints(), 62 | $this->parseLines(), 63 | $this->parsePolygons(), 64 | $this->parseBoxes(), 65 | $this->parseCircles() 66 | ); 67 | 68 | return geoPHP::geometryReduce($geometries); 69 | } 70 | 71 | /** 72 | * @param string $string 73 | * @return Point[] 74 | */ 75 | protected function getPointsFromCoordinates(string $string): array 76 | { 77 | $coordinates = []; 78 | $latitudeAndLongitude = explode(' ', $string); 79 | $lat = 0; 80 | foreach ($latitudeAndLongitude as $key => $item) { 81 | if (!($key % 2)) { 82 | // It's a latitude 83 | $lat = is_numeric($item) ? $item : null; 84 | } else { 85 | // It's a longitude 86 | $lon = is_numeric($item) ? $item : null; 87 | $coordinates[] = new Point($lon, $lat); 88 | } 89 | } 90 | return $coordinates; 91 | } 92 | 93 | /** 94 | * @return Point[] 95 | */ 96 | protected function parsePoints(): array 97 | { 98 | $points = []; 99 | $pointElements = $this->xmlObject->getElementsByTagName('point'); 100 | foreach ($pointElements as $pt) { 101 | $pointArray = $this->getPointsFromCoordinates(trim($pt->firstChild->nodeValue)); 102 | $points[] = !empty($pointArray) ? $pointArray[0] : new Point(); 103 | } 104 | return $points; 105 | } 106 | 107 | /** 108 | * @return LineString[] 109 | */ 110 | protected function parseLines(): array 111 | { 112 | $lines = []; 113 | $lineElements = $this->xmlObject->getElementsByTagName('line'); 114 | foreach ($lineElements as $line) { 115 | $components = $this->getPointsFromCoordinates(trim($line->firstChild->nodeValue)); 116 | $lines[] = new LineString($components); 117 | } 118 | return $lines; 119 | } 120 | 121 | /** 122 | * @return Polygon[] 123 | */ 124 | protected function parsePolygons(): array 125 | { 126 | $polygons = []; 127 | $polygonElements = $this->xmlObject->getElementsByTagName('polygon'); 128 | foreach ($polygonElements as $polygon) { 129 | /** @noinspection PhpUndefinedMethodInspection */ 130 | if ($polygon->hasChildNodes()) { 131 | $points = $this->getPointsFromCoordinates(trim($polygon->firstChild->nodeValue)); 132 | $exteriorRing = new LineString($points); 133 | $polygons[] = new Polygon([$exteriorRing]); 134 | } else { 135 | // It's an EMPTY polygon 136 | $polygons[] = new Polygon(); 137 | } 138 | } 139 | return $polygons; 140 | } 141 | 142 | /** 143 | * Boxes are rendered into polygons 144 | * 145 | * @return Polygon[] 146 | */ 147 | protected function parseBoxes(): array 148 | { 149 | $polygons = []; 150 | $boxElements = $this->xmlObject->getElementsByTagName('box'); 151 | foreach ($boxElements as $box) { 152 | $parts = explode(' ', trim($box->firstChild->nodeValue)); 153 | $components = [ 154 | new Point($parts[3], $parts[2]), 155 | new Point($parts[3], $parts[0]), 156 | new Point($parts[1], $parts[0]), 157 | new Point($parts[1], $parts[2]), 158 | new Point($parts[3], $parts[2]), 159 | ]; 160 | $exteriorRing = new LineString($components); 161 | $polygons[] = new Polygon([$exteriorRing]); 162 | } 163 | return $polygons; 164 | } 165 | 166 | /** 167 | * Circles are rendered into points. 168 | * 169 | * @@TODO: Add good support once we have circular-string geometry support. 170 | * 171 | * @return Point[] 172 | */ 173 | protected function parseCircles(): array 174 | { 175 | $points = []; 176 | $circleElements = $this->xmlObject->getElementsByTagName('circle'); 177 | foreach ($circleElements as $circle) { 178 | $parts = explode(' ', trim($circle->firstChild->nodeValue)); 179 | $points[] = new Point($parts[1], $parts[0]); 180 | } 181 | return $points; 182 | } 183 | 184 | /** 185 | * Serialize geometries into a GeoRSS string. 186 | * 187 | * @param Geometry $geometry 188 | * @param boolean|string $namespace 189 | * @return string The georss string representation of the input geometries 190 | */ 191 | public function write(Geometry $geometry, $namespace = false): string 192 | { 193 | if ($namespace) { 194 | $this->nss = $namespace . ':'; 195 | } 196 | return $this->geometryToGeoRSS($geometry) ?: ''; 197 | } 198 | 199 | /** 200 | * @param Geometry $geometry 201 | * @return string|null 202 | */ 203 | protected function geometryToGeoRSS(Geometry $geometry): ?string 204 | { 205 | $type = $geometry->geometryType(); 206 | switch ($type) { 207 | case Geometry::POINT: 208 | /** @var Point $geometry */ 209 | return $this->pointToGeoRSS($geometry); 210 | case Geometry::LINE_STRING: 211 | /** @var LineString $geometry */ 212 | return $this->linestringToGeoRSS($geometry); 213 | case Geometry::POLYGON: 214 | /** @var Polygon $geometry */ 215 | return $this->polygonToGeoRSS($geometry); 216 | case Geometry::MULTI_POINT: 217 | case Geometry::MULTI_LINE_STRING: 218 | case Geometry::MULTI_POLYGON: 219 | case Geometry::GEOMETRY_COLLECTION: 220 | /** @var MultiGeometry $geometry */ 221 | return $this->collectionToGeoRSS($geometry); 222 | default: 223 | return null; 224 | } 225 | } 226 | 227 | /** 228 | * @param Point $geometry 229 | * @return string 230 | */ 231 | private function pointToGeoRSS(Point $geometry): string 232 | { 233 | return '<' . $this->nss . 'point>' . $geometry->y() . ' ' . $geometry->x() . '' . $this->nss . 'point>'; 234 | } 235 | 236 | /** 237 | * @param LineString $geometry 238 | * @return string 239 | */ 240 | private function linestringToGeoRSS(LineString $geometry): string 241 | { 242 | $output = '<' . $this->nss . 'line>'; 243 | foreach ($geometry->getComponents() as $k => $point) { 244 | $output .= $point->y() . ' ' . $point->x(); 245 | if ($k < ($geometry->numGeometries() - 1)) { 246 | $output .= ' '; 247 | } 248 | } 249 | $output .= '' . $this->nss . 'line>'; 250 | return $output; 251 | } 252 | 253 | /** 254 | * @param Polygon $geometry 255 | * @return string 256 | */ 257 | private function polygonToGeoRSS(Polygon $geometry): string 258 | { 259 | $output = '<' . $this->nss . 'polygon>'; 260 | $exteriorRing = $geometry->exteriorRing(); 261 | foreach ($exteriorRing->getComponents() as $k => $point) { 262 | $output .= $point->y() . ' ' . $point->x(); 263 | if ($k < ($exteriorRing->numGeometries() - 1)) { 264 | $output .= ' '; 265 | } 266 | } 267 | $output .= '' . $this->nss . 'polygon>'; 268 | return $output; 269 | } 270 | 271 | /** 272 | * @param MultiGeometry $geometry 273 | * @return string 274 | */ 275 | public function collectionToGeoRSS(MultiGeometry $geometry): string 276 | { 277 | $georss = '<' . $this->nss . 'where>'; 278 | $components = $geometry->getComponents(); 279 | foreach ($components as $component) { 280 | $georss .= $this->geometryToGeoRSS($component); 281 | } 282 | 283 | $georss .= '' . $this->nss . 'where>'; 284 | 285 | return $georss; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Adapter/GoogleGeocode.php: -------------------------------------------------------------------------------- 1 | 16 | * (c) Patrick Hayes 17 | * 18 | * This code is open-source and licenced under the Modified BSD License. 19 | * For the full copyright and license information, please view the LICENSE 20 | * file that was distributed with this source code. 21 | */ 22 | 23 | /** 24 | * PHP Google Geocoder Adapter 25 | * 26 | * 27 | * @package geoPHP 28 | * @author Patrick Hayes 29 | */ 30 | class GoogleGeocode implements GeoAdapter 31 | { 32 | /** @var \stdClass $result */ 33 | protected $result; 34 | 35 | /** 36 | * Makes a geocoding (lat/lon lookup) with an address string or array geometry objects. 37 | * Detailed documentation of response values can be found in: 38 | * 39 | * @see https://developers.google.com/maps/documentation/geocoding/requests-geocoding 40 | * 41 | * @param string $address Address to geocode. 42 | * @param string $apiKey Your application's Google Maps Geocoding API key. 43 | * @param string $returnType Type of Geometry to return. 44 | * Can either be 'points' or 'bounds' (polygon). 45 | * @param string[]|Geometry|null $bounds Limit the search area to within this region. 46 | * For example by default geocoding "Cairo" will return the location of Cairo Egypt. 47 | * If you pass a polygon of Illinois, it will return Cairo IL. 48 | * @param boolean $returnMultiple Return all results in a multipoint or multipolygon. 49 | * 50 | * @return Geometry 51 | * @throws IOException If geocoding fails. 52 | */ 53 | public function read( 54 | string $address, 55 | string $apiKey = null, 56 | string $returnType = 'point', 57 | $bounds = null, 58 | bool $returnMultiple = false 59 | ): Geometry { 60 | if ($bounds instanceof Geometry) { 61 | $bounds = $bounds->getBBox(); 62 | } 63 | if (is_array($bounds)) { 64 | $boundsString = '&bounds=' 65 | . $bounds['miny'] . ',' . $bounds['minx'] . '|' 66 | . $bounds['maxy'] . ',' . $bounds['maxx']; 67 | } else { 68 | $boundsString = ''; 69 | } 70 | 71 | $url = "http://maps.googleapis.com/maps/api/geocode/json"; 72 | $url .= '?address=' . urlencode($address); 73 | $url .= $boundsString . ($apiKey ? '&key=' . $apiKey : ''); 74 | $this->result = json_decode(@file_get_contents($url)); 75 | 76 | if ($this->result->status == 'OK') { 77 | if (!$returnMultiple) { 78 | if ($returnType == 'point') { 79 | return $this->getPoint(); 80 | } 81 | if ($returnType == 'bounds' || $returnType == 'polygon') { 82 | return $this->getPolygon(); 83 | } 84 | } else { 85 | if ($returnType == 'point') { 86 | $points = []; 87 | foreach ($this->result->results as $delta => $item) { 88 | $points[] = $this->getPoint($delta); 89 | } 90 | return new MultiPoint($points); 91 | } 92 | if ($returnType == 'bounds' || $returnType == 'polygon') { 93 | $polygons = []; 94 | foreach ($this->result->results as $delta => $item) { 95 | $polygons[] = $this->getPolygon($delta); 96 | } 97 | return new MultiPolygon($polygons); 98 | } 99 | } 100 | } elseif ($this->result->status == 'ZERO_RESULTS') { 101 | return new GeometryCollection(); 102 | } else { 103 | if ($this->result->status) { 104 | throw new IOException( 105 | 'Error in Google Reverse Geocoder: ' 106 | . $this->result->status 107 | . (isset($this->result->error_message) ? '. ' . $this->result->error_message : '') 108 | ); 109 | } else { 110 | throw new IOException('Unknown error in Google Reverse Geocoder'); 111 | } 112 | } 113 | return new GeometryCollection(); 114 | } 115 | 116 | /** 117 | * Makes a Reverse Geocoding (address lookup) with the (center) point of Geometry. 118 | * Detailed documentation of response values can be found in: 119 | * 120 | * @see https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding 121 | * 122 | * @param Geometry $geometry 123 | * @param string $apiKey Your application's Google Maps Geocoding API key. 124 | * @param string $returnType Should be either 'string' or 'array' or 'full'. 125 | * @param string $language The language in which to return results. 126 | * If not set, geocoder tries to use the native language of the domain. 127 | * 128 | * @return string A formatted address. 129 | * @throws IOException If geocoding fails 130 | */ 131 | public function write( 132 | Geometry $geometry, 133 | ?string $apiKey = null, 134 | ?string $returnType = 'string', 135 | ?string $language = null 136 | ): string { 137 | return $this->reverseGeocode($geometry, $apiKey, 'string', $language); 138 | } 139 | 140 | /** 141 | * Makes a Reverse Geocoding (address lookup) with the (center) point of Geometry. 142 | * Detailed documentation of response values can be found in: 143 | * 144 | * @see https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding 145 | * 146 | * @param Geometry $geometry 147 | * @param string $apiKey Your application's Google Maps Geocoding API key. 148 | * @param string $language The language in which to return results. 149 | * If not set, geocoder tries to use the native language of the domain. 150 | * 151 | * @return string[] Array of address components. 152 | * @throws IOException If geocoding fails 153 | */ 154 | public function writeAsArray( 155 | Geometry $geometry, 156 | ?string $apiKey = null, 157 | ?string $language = null 158 | ): array { 159 | return $this->reverseGeocode($geometry, $apiKey, 'array', $language); 160 | } 161 | 162 | /** 163 | * Makes a Reverse Geocoding (address lookup) with the (center) point of Geometry. 164 | * Detailed documentation of response values can be found in: 165 | * 166 | * @see https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding 167 | * 168 | * @param Geometry $geometry 169 | * @param string $apiKey Your application's Google Maps Geocoding API key. 170 | * @param string $returnType Should be either 'string' or 'array' or 'full' 171 | * @param string $language The language in which to return results. 172 | * If not set, geocoder tries to use the native language of the domain. 173 | * 174 | * @return string|string[] A formatted address or array of address components. 175 | * @throws IOException If geocoding fails 176 | */ 177 | protected function reverseGeocode( 178 | Geometry $geometry, 179 | ?string $apiKey = null, 180 | ?string $returnType = 'string', 181 | ?string $language = null 182 | ) { 183 | $centroid = $geometry->centroid(); 184 | $lat = $centroid->y(); 185 | $lon = $centroid->x(); 186 | 187 | $url = "http://maps.googleapis.com/maps/api/geocode/json"; 188 | /** @noinspection SpellCheckingInspection */ 189 | $url .= '?latlng=' . $lat . ',' . $lon; 190 | $url .= ($language ? '&language=' . $language : '') . ($apiKey ? '&key=' . $apiKey : ''); 191 | 192 | $this->result = json_decode(@file_get_contents($url)); 193 | 194 | if ($this->result->status == 'OK') { 195 | if ($returnType == 'string') { 196 | return $this->result->results[0]->formatted_address; 197 | } elseif ($returnType == 'array') { 198 | return $this->result->results[0]->address_components; 199 | } elseif ($returnType == 'full') { 200 | return $this->result->results[0]; 201 | } 202 | } elseif ($this->result->status == 'ZERO_RESULTS') { 203 | if ($returnType == 'string') { 204 | return ''; 205 | } 206 | if ($returnType == 'array') { 207 | return $this->result->results; 208 | } 209 | } else { 210 | if ($this->result->status) { 211 | throw new IOException( 212 | 'Error in Google Reverse Geocoder: ' 213 | . $this->result->status 214 | . (isset($this->result->error_message) ? '. ' . $this->result->error_message : '') 215 | ); 216 | } else { 217 | throw new IOException('Unknown error in Google Reverse Geocoder'); 218 | } 219 | } 220 | return ''; 221 | } 222 | 223 | private function getPoint(int $delta = 0): Point 224 | { 225 | $lat = $this->result->results[$delta]->geometry->location->lat; 226 | $lon = $this->result->results[$delta]->geometry->location->lng; 227 | return new Point($lon, $lat); 228 | } 229 | 230 | private function getPolygon(int $delta = 0): Polygon 231 | { 232 | $points = [ 233 | $this->getTopLeft($delta), 234 | $this->getTopRight($delta), 235 | $this->getBottomRight($delta), 236 | $this->getBottomLeft($delta), 237 | $this->getTopLeft($delta), 238 | ]; 239 | $outerRing = new LineString($points); 240 | return new Polygon([$outerRing]); 241 | } 242 | 243 | private function getTopLeft(int $delta = 0): Point 244 | { 245 | $lat = $this->result->results[$delta]->geometry->bounds->northeast->lat; 246 | $lon = $this->result->results[$delta]->geometry->bounds->southwest->lng; 247 | return new Point($lon, $lat); 248 | } 249 | 250 | private function getTopRight(int $delta = 0): Point 251 | { 252 | $lat = $this->result->results[$delta]->geometry->bounds->northeast->lat; 253 | $lon = $this->result->results[$delta]->geometry->bounds->northeast->lng; 254 | return new Point($lon, $lat); 255 | } 256 | 257 | private function getBottomLeft(int $delta = 0): Point 258 | { 259 | $lat = $this->result->results[$delta]->geometry->bounds->southwest->lat; 260 | $lon = $this->result->results[$delta]->geometry->bounds->southwest->lng; 261 | return new Point($lon, $lat); 262 | } 263 | 264 | private function getBottomRight(int $delta = 0): Point 265 | { 266 | $lat = $this->result->results[$delta]->geometry->bounds->southwest->lat; 267 | $lon = $this->result->results[$delta]->geometry->bounds->northeast->lng; 268 | return new Point($lon, $lat); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/Adapter/GpxTypes.php: -------------------------------------------------------------------------------- 1 | 16 | * @see http://www.topografix.com/gpx/1/1/#type_gpxType 17 | */ 18 | public static $gpxTypeElements = [ 19 | 'metadata', 'wpt', 'rte', 'trk' 20 | ]; 21 | 22 | /** 23 | * @var string[] Allowed elements in 24 | * @see http://www.topografix.com/gpx/1/1/#type_trkType 25 | */ 26 | public static $trkTypeElements = [ 27 | 'name', 'cmt', 'desc', 'src', 'link', 'number', 'type' 28 | ]; 29 | 30 | /** 31 | * same as trkTypeElements 32 | * @var string[] Allowed elements in 33 | * @see http://www.topografix.com/gpx/1/1/#type_rteType 34 | */ 35 | public static $rteTypeElements = [ 36 | 'name', 'cmt', 'desc', 'src', 'link', 'number', 'type' 37 | ]; 38 | 39 | /** 40 | * @var string[] Allowed elements in 41 | * @see http://www.topografix.com/gpx/1/1/#type_wptType 42 | */ 43 | public static $wptTypeElements = [ 44 | 'ele', 'time', 'magvar', 'geoidheight', 'name', 'cmt', 'desc', 'src', 'link', 'sym', 'type', 45 | 'fix', 'sat', 'hdop', 'vdop', 'pdop', 'ageofdgpsdata', 'dgpsid' 46 | ]; 47 | 48 | /** 49 | * @var string[] Same as wptType 50 | */ 51 | public static $trkptTypeElements = [ // same as wptTypeElements 52 | 'ele', 'time', 'magvar', 'geoidheight', 'name', 'cmt', 'desc', 'src', 'link', 'sym', 'type', 53 | 'fix', 'sat', 'hdop', 'vdop', 'pdop', 'ageofdgpsdata', 'dgpsid' 54 | ]; 55 | 56 | /** 57 | * @var string[] Same as wptType 58 | */ 59 | public static $rteptTypeElements = [ // same as wptTypeElements 60 | 'ele', 'time', 'magvar', 'geoidheight', 'name', 'cmt', 'desc', 'src', 'link', 'sym', 'type', 61 | 'fix', 'sat', 'hdop', 'vdop', 'pdop', 'ageofdgpsdata', 'dgpsid' 62 | ]; 63 | 64 | /** 65 | * @var string[] Allowed elements in 66 | * @see http://www.topografix.com/gpx/1/1/#type_metadataType 67 | */ 68 | public static $metadataTypeElements = [ 69 | 'name', 'desc', 'author', 'copyright', 'link', 'time', 'keywords', 'bounds' 70 | ]; 71 | 72 | /** 73 | * @var string[] Allowed elements in 74 | * @see http://www.topografix.com/gpx/1/1/#type_gpxType 75 | */ 76 | protected $allowedGpxTypeElements; 77 | 78 | /** 79 | * @var string[] Allowed elements in 80 | * @see http://www.topografix.com/gpx/1/1/#type_trkType 81 | */ 82 | protected $allowedTrkTypeElements; 83 | 84 | 85 | /** 86 | * same as trkTypeElements 87 | * @var string[] Allowed elements in 88 | * @see http://www.topografix.com/gpx/1/1/#type_rteType 89 | */ 90 | protected $allowedRteTypeElements = []; 91 | 92 | /** 93 | * @var string[] Same as wptType 94 | */ 95 | protected $allowedWptTypeElements = []; 96 | 97 | /** 98 | * @var string[] Same as wptType 99 | */ 100 | protected $allowedTrkptTypeElements = []; 101 | 102 | /** 103 | * @var string[] Same as wptType 104 | */ 105 | protected $allowedRteptTypeElements = []; 106 | 107 | /** 108 | * @var string[] Allowed elements in 109 | * @see http://www.topografix.com/gpx/1/1/#type_metadataType 110 | */ 111 | protected $allowedMetadataTypeElements = []; 112 | 113 | /** 114 | * GpxTypes constructor. 115 | * 116 | * @param array>|null $allowedElements Which elements can be used in each GPX type. 117 | * If not specified, every element defined in the GPX specification can be used. 118 | * Can be overwritten with an associative array, with type name in keys. 119 | * eg.: ['wptType' => ['ele', 'name'], 'trkptType' => ['ele'], 'metadataType' => null] 120 | */ 121 | public function __construct(?array $allowedElements = null) 122 | { 123 | $this->allowedGpxTypeElements = self::$gpxTypeElements; 124 | $this->allowedTrkTypeElements = self::$trkTypeElements; 125 | $this->allowedRteTypeElements = self::$rteTypeElements; 126 | $this->allowedWptTypeElements = self::$wptTypeElements; 127 | $this->allowedTrkptTypeElements = self::$trkTypeElements; 128 | $this->allowedRteptTypeElements = self::$rteptTypeElements; 129 | $this->allowedMetadataTypeElements = self::$metadataTypeElements; 130 | 131 | foreach ($allowedElements ?: [] as $type => $elements) { 132 | $elements = is_array($elements) ? $elements : [$elements]; 133 | $this->{'allowed' . ucfirst($type) . 'Elements'} = []; 134 | foreach ($this::${$type . 'Elements'} as $availableType) { 135 | if (in_array($availableType, $elements)) { 136 | $this->{'allowed' . ucfirst($type) . 'Elements'}[] = $availableType; 137 | } 138 | } 139 | } 140 | } 141 | 142 | /** 143 | * Returns an array of allowed elements for the given GPX type 144 | * eg. "gpxType" returns ['metadata', 'wpt', 'rte', 'trk'] 145 | * 146 | * @param string $type One of the following GPX types: 147 | * gpxType, trkType, rteType, wptType, trkptType, rteptType, metadataType 148 | * @return string[] 149 | */ 150 | public function get(string $type): array 151 | { 152 | $propertyName = 'allowed' . ucfirst($type) . 'Elements'; 153 | if (isset($this->{$propertyName})) { 154 | return $this->{$propertyName}; 155 | } 156 | return []; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | components) === 0; 78 | } 79 | 80 | /** 81 | * The boundary of a non-closed Curve consists of its two end Points. 82 | * End point are represented as a MultiPoint geometry. 83 | * 84 | * @see OGC SFA 6.1.6.1 85 | * 86 | * @return MultiPoint 87 | */ 88 | public function boundary(): ?Geometry 89 | { 90 | return $this->isEmpty() || $this->isClosed() 91 | ? new MultiPoint() 92 | : new MultiPoint([$this->startPoint(), $this->endPoint()]); 93 | } 94 | 95 | public function startPoint(): ?Point 96 | { 97 | if (!isset($this->startPoint)) { 98 | $this->startPoint = $this->components[0] ?? null; 99 | } 100 | return $this->startPoint; 101 | } 102 | 103 | public function endPoint(): ?Point 104 | { 105 | if (!isset($this->endPoint)) { 106 | $this->endPoint = $this->components[count($this->components) - 1] ?? null; 107 | } 108 | return $this->endPoint; 109 | } 110 | 111 | /** 112 | * A Curve is closed if its start Point is equal to its end Point. 113 | * 114 | * @see OGC SFA 6.1.6.1 115 | * 116 | * @return boolean 117 | */ 118 | public function isClosed(): bool 119 | { 120 | return !$this->isEmpty() 121 | ? $this->startPoint()->equals($this->endPoint()) 122 | : false; 123 | } 124 | 125 | public function isRing(): bool 126 | { 127 | return ($this->isClosed() && $this->isSimple()); 128 | } 129 | 130 | /** 131 | * @return Point[] 132 | */ 133 | public function getPoints(): array 134 | { 135 | return $this->getComponents(); 136 | } 137 | 138 | // Not valid for this geometry type 139 | // -------------------------------- 140 | public function area(): float 141 | { 142 | return 0.0; 143 | } 144 | 145 | public function exteriorRing(): ?LineString 146 | { 147 | return null; 148 | } 149 | 150 | public function numInteriorRings(): ?int 151 | { 152 | return null; 153 | } 154 | 155 | public function interiorRingN(int $n): ?LineString 156 | { 157 | return null; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Geometry/GeometryCollection.php: -------------------------------------------------------------------------------- 1 | getComponents() as $component) { 43 | if ($component->dimension() > $dimension) { 44 | $dimension = $component->dimension(); 45 | } 46 | } 47 | return $dimension; 48 | } 49 | 50 | /** 51 | * Not valid for this geometry type 52 | * @return null 53 | */ 54 | public function isSimple(): ?bool 55 | { 56 | return null; 57 | } 58 | 59 | /** 60 | * In a GeometryCollection, the centroid is equal to the centroid of 61 | * the set of component Geometries of highest dimension 62 | * (since the lower-dimension geometries contribute zero "weight" to the centroid). 63 | * 64 | * @return Point 65 | * @throws Exception 66 | */ 67 | public function centroid(): Point 68 | { 69 | if ($this->isEmpty()) { 70 | return new Point(); 71 | } 72 | 73 | if ($this->getGeos()) { 74 | // @codeCoverageIgnoreStart 75 | return geoPHP::geosToGeometry($this->getGeos()->centroid()); 76 | // @codeCoverageIgnoreEnd 77 | } 78 | 79 | $geometries = $this->explodeGeometries(); 80 | 81 | $highestDimension = 0; 82 | foreach ($geometries as $geometry) { 83 | if ($geometry->dimension() > $highestDimension) { 84 | $highestDimension = $geometry->dimension(); 85 | } 86 | if ($highestDimension === 2) { 87 | break; 88 | } 89 | } 90 | 91 | $highestDimensionGeometries = []; 92 | foreach ($geometries as $geometry) { 93 | if ($geometry->dimension() === $highestDimension) { 94 | $highestDimensionGeometries[] = $geometry; 95 | } 96 | } 97 | 98 | $reducedGeometry = geoPHP::geometryReduce($highestDimensionGeometries); 99 | if ($reducedGeometry->geometryType() === Geometry::GEOMETRY_COLLECTION) { 100 | throw new \Exception('Internal error: GeometryCollection->centroid() calculation failed.'); 101 | } 102 | return $reducedGeometry->centroid(); 103 | } 104 | 105 | /** 106 | * Returns every sub-geometry as a multidimensional array 107 | * 108 | * Because geometryCollections are heterogeneous we need to specify which type of geometries they contain. 109 | * We need to do this because, for example, there would be no way to tell the difference between a 110 | * MultiPoint or a LineString, since they share the same structure (collection 111 | * of points). So we need to call out the type explicitly. 112 | * 113 | * @return array}> 114 | */ 115 | public function asArray(): array 116 | { 117 | $array = []; 118 | foreach ($this->getComponents() as $component) { 119 | $array[] = [ 120 | 'type' => $component->geometryType(), 121 | 'components' => $component->asArray(), 122 | ]; 123 | } 124 | return $array; 125 | } 126 | 127 | /** 128 | * @return Geometry[] 129 | */ 130 | public function explodeGeometries(): array 131 | { 132 | $geometries = []; 133 | foreach ($this->components as $component) { 134 | if ($component instanceof GeometryCollection) { 135 | foreach ($component->explodeGeometries() as $subComponent) { 136 | $geometries[] = $subComponent; 137 | } 138 | } else { 139 | $geometries[] = $component; 140 | } 141 | } 142 | return $geometries; 143 | } 144 | 145 | // Not valid for this geometry 146 | public function boundary(): ?Geometry 147 | { 148 | return null; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Geometry/MultiCurve.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 63 | return false; 64 | } 65 | 66 | foreach ($this->getComponents() as $line) { 67 | if (!$line->isClosed()) { 68 | return false; 69 | } 70 | } 71 | return true; 72 | } 73 | 74 | /** 75 | * The boundary of a MultiCurve is obtained by applying the “mod 2” union rule: 76 | * A Point is in the boundary of a MultiCurve if it is in the boundaries of an odd number 77 | * of elements of the MultiCurve. 78 | * The boundary of a closed MultiCurve is always empty. 79 | * 80 | * @see OGC SFA 6.1.8.1 81 | * 82 | * @return Geometry|null 83 | */ 84 | public function boundary(): ?Geometry 85 | { 86 | if (geoPHP::isGeosInstalled()) { 87 | // @codeCoverageIgnoreStart 88 | return geoPHP::geosToGeometry($this->getGeos()->boundary()); 89 | // @codeCoverageIgnoreEnd 90 | } 91 | 92 | $points = []; 93 | foreach ($this->components as $line) { 94 | if (!$line->isEmpty() && !$line->isClosed()) { 95 | if (count($points) && $line->startPoint()->equals($points[count($points) - 1])) { 96 | array_pop($points); 97 | } else { 98 | $points[] = $line->startPoint(); 99 | } 100 | 101 | $points[] = $line->endPoint(); 102 | } 103 | } 104 | return new MultiPoint($points); 105 | } 106 | 107 | /** 108 | * A MultiCurve is simple if and only if all of its elements are simple and the only intersections 109 | * between any two elements occur at Points that are on the boundaries of both elements. 110 | * 111 | * @see OGC SFA 6.1.8.1 112 | * 113 | * @return bool 114 | */ 115 | public function isSimple(): bool 116 | { 117 | // @codeCoverageIgnoreStart 118 | return parent::isSimple(); 119 | // @codeCoverageIgnoreEnd 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Geometry/MultiGeometry.php: -------------------------------------------------------------------------------- 1 | getGeos()) { 46 | // @codeCoverageIgnoreStart 47 | /** @noinspection PhpUndefinedMethodInspection */ 48 | return $this->getGeos()->isSimple(); 49 | // @codeCoverageIgnoreEnd 50 | } 51 | 52 | foreach ($this->components as $component) { 53 | if (!$component->isSimple()) { 54 | return false; 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | 61 | /** 62 | * By default, the boundary of a collection is the boundary of it's components. 63 | * 64 | * @return Geometry 65 | */ 66 | public function boundary(): ?Geometry 67 | { 68 | if ($this->isEmpty()) { 69 | return new LineString(); 70 | } 71 | 72 | if ($this->getGeos()) { 73 | // @codeCoverageIgnoreStart 74 | /** @noinspection PhpUndefinedMethodInspection */ 75 | return geoPHP::geosToGeometry($this->getGeos()->boundary()); 76 | // @codeCoverageIgnoreEnd 77 | } 78 | 79 | $componentsBoundaries = []; 80 | foreach ($this->components as $component) { 81 | $componentsBoundaries[] = $component->boundary(); 82 | } 83 | return geoPHP::buildGeometry($componentsBoundaries); 84 | } 85 | 86 | public function area(): float 87 | { 88 | if ($this->getGeos()) { 89 | // @codeCoverageIgnoreStart 90 | /** @noinspection PhpUndefinedMethodInspection */ 91 | return $this->getGeos()->area(); 92 | // @codeCoverageIgnoreEnd 93 | } 94 | 95 | $area = 0.0; 96 | foreach ($this->components as $component) { 97 | $area += $component->area(); 98 | } 99 | return $area; 100 | } 101 | 102 | /** 103 | * Returns the length of this Collection in its associated spatial reference. 104 | * Eg. if Geometry is in geographical coordinate system it returns the length in degrees 105 | * @return float 106 | */ 107 | public function length(): float 108 | { 109 | $length = 0.0; 110 | foreach ($this->components as $component) { 111 | $length += $component->length(); 112 | } 113 | return $length; 114 | } 115 | 116 | public function length3D(): float 117 | { 118 | $length = 0.0; 119 | foreach ($this->components as $component) { 120 | $length += $component->length3D(); 121 | } 122 | return $length; 123 | } 124 | 125 | /** 126 | * Returns the degree based Geometry' length in meters. 127 | * 128 | * @param float $radius Default is the semi-major axis of WGS84. 129 | * @return float Length in meters. 130 | */ 131 | public function greatCircleLength(float $radius = geoPHP::EARTH_WGS84_SEMI_MAJOR_AXIS): float 132 | { 133 | $length = 0.0; 134 | foreach ($this->components as $component) { 135 | $length += $component->greatCircleLength($radius); 136 | } 137 | return $length; 138 | } 139 | 140 | public function haversineLength(): float 141 | { 142 | $length = 0.0; 143 | foreach ($this->components as $component) { 144 | $length += $component->haversineLength(); 145 | } 146 | return $length; 147 | } 148 | 149 | public function vincentyLength(): float 150 | { 151 | $length = 0.0; 152 | foreach ($this->components as $component) { 153 | $length += $component->vincentyLength(); 154 | } 155 | return $length; 156 | } 157 | 158 | public function minimumZ(): ?float 159 | { 160 | if (!$this->is3D()) { 161 | return null; 162 | } 163 | $min = PHP_INT_MAX; 164 | foreach ($this->components as $component) { 165 | $componentMin = $component->minimumZ(); 166 | if ($componentMin < $min) { 167 | $min = $componentMin; 168 | } 169 | } 170 | return $min < PHP_INT_MAX ? $min : null; 171 | } 172 | 173 | public function maximumZ(): ?float 174 | { 175 | if (!$this->is3D()) { 176 | return null; 177 | } 178 | $max = ~PHP_INT_MAX; 179 | foreach ($this->components as $component) { 180 | $componentMax = $component->maximumZ(); 181 | if ($componentMax > $max) { 182 | $max = $componentMax; 183 | } 184 | } 185 | return $max > ~PHP_INT_MAX ? $max : null; 186 | } 187 | 188 | public function zDifference(): ?float 189 | { 190 | if (!$this->is3D()) { 191 | return null; 192 | } 193 | $startPoint = $this->startPoint(); 194 | $endPoint = $this->endPoint(); 195 | if ($startPoint && $endPoint) { 196 | return (float) abs($startPoint->z() - $endPoint->z()); 197 | } else { 198 | return null; 199 | } 200 | } 201 | 202 | public function elevationGain(float $verticalTolerance = 0.0): float 203 | { 204 | $gain = 0.0; 205 | foreach ($this->components as $component) { 206 | $gain += $component->elevationGain($verticalTolerance); 207 | } 208 | return (float) $gain; 209 | } 210 | 211 | public function elevationLoss(float $verticalTolerance = 0.0): float 212 | { 213 | $loss = 0.0; 214 | foreach ($this->components as $component) { 215 | $loss += $component->elevationLoss($verticalTolerance); 216 | } 217 | return $loss; 218 | } 219 | 220 | public function minimumM(): ?float 221 | { 222 | if (!$this->isMeasured()) { 223 | return null; 224 | } 225 | $min = PHP_INT_MAX; 226 | foreach ($this->components as $component) { 227 | $componentMin = $component->minimumM(); 228 | if ($componentMin < $min) { 229 | $min = $componentMin; 230 | } 231 | } 232 | return $min < PHP_INT_MAX ? $min : null; 233 | } 234 | 235 | public function maximumM(): ?float 236 | { 237 | if (!$this->isMeasured()) { 238 | return null; 239 | } 240 | $max = ~PHP_INT_MAX; 241 | foreach ($this->components as $component) { 242 | $componentMax = $component->maximumM(); 243 | if ($componentMax > $max) { 244 | $max = $componentMax; 245 | } 246 | } 247 | return $max > ~PHP_INT_MAX ? $max : null; 248 | } 249 | 250 | 251 | 252 | public function startPoint(): ?Point 253 | { 254 | return null; 255 | } 256 | 257 | public function endPoint(): ?Point 258 | { 259 | return null; 260 | } 261 | 262 | public function isRing(): ?bool 263 | { 264 | return null; 265 | } 266 | 267 | public function isClosed(): ?bool 268 | { 269 | return null; 270 | } 271 | 272 | public function pointN(int $n): ?Point 273 | { 274 | return null; 275 | } 276 | 277 | public function exteriorRing(): ?LineString 278 | { 279 | return null; 280 | } 281 | 282 | public function numInteriorRings(): ?int 283 | { 284 | return null; 285 | } 286 | 287 | public function interiorRingN(int $n): ?LineString 288 | { 289 | return null; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/Geometry/MultiLineString.php: -------------------------------------------------------------------------------- 1 | >> $coordinateArray 37 | * Multi-dimensional array of coordinates. 38 | * 39 | * @throws InvalidGeometryException 40 | * 41 | * @return MultiLineString 42 | */ 43 | public static function fromArray(array $coordinateArray): MultiLineString 44 | { 45 | $points = []; 46 | foreach ($coordinateArray as $point) { 47 | $points[] = LineString::fromArray($point); 48 | } 49 | return new static($points); 50 | } 51 | 52 | public function geometryType(): string 53 | { 54 | return Geometry::MULTI_LINE_STRING; 55 | } 56 | 57 | public function centroid(): Point 58 | { 59 | if ($this->isEmpty()) { 60 | return new Point(); 61 | } 62 | 63 | if ($this->getGeos()) { 64 | // @codeCoverageIgnoreStart 65 | return geoPHP::geosToGeometry($this->getGeos()->centroid()); 66 | // @codeCoverageIgnoreEnd 67 | } 68 | 69 | $x = 0; 70 | $y = 0; 71 | $totalLength = 0; 72 | $componentLength = 0; 73 | $components = $this->getComponents(); 74 | foreach ($components as $line) { 75 | if ($line->isEmpty()) { 76 | continue; 77 | } 78 | $componentCentroid = $line->getCentroidAndLength($componentLength); 79 | $x += $componentCentroid->x() * $componentLength; 80 | $y += $componentCentroid->y() * $componentLength; 81 | $totalLength += $componentLength; 82 | } 83 | 84 | return $totalLength === 0 85 | ? $this->getPoints()[0] 86 | : new Point($x / $totalLength, $y / $totalLength); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Geometry/MultiPoint.php: -------------------------------------------------------------------------------- 1 | > $coordinateArray Multi-dimensional array of coordinates. 41 | * 42 | * @throws InvalidGeometryException 43 | * 44 | * @return MultiPoint 45 | */ 46 | public static function fromArray(array $coordinateArray): MultiPoint 47 | { 48 | $points = []; 49 | foreach ($coordinateArray as $point) { 50 | $points[] = Point::fromArray($point); 51 | } 52 | return new static($points); 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function geometryType(): string 59 | { 60 | return Geometry::MULTI_POINT; 61 | } 62 | 63 | /** 64 | * MultiPoint is 0-dimensional 65 | * @return int 0 66 | */ 67 | public function dimension(): int 68 | { 69 | return 0; 70 | } 71 | 72 | /** 73 | * A MultiPoint is simple if no two Points in the MultiPoint are equal 74 | * (have identical coordinate values in X and Y). 75 | * 76 | * @return bool 77 | */ 78 | public function isSimple(): ?bool 79 | { 80 | $componentCount = count($this->components); 81 | for ($i = 0; $i < $componentCount; $i++) { 82 | for ($j = $i + 1; $j < $componentCount; $j++) { 83 | if ($this->components[$i]->equals($this->components[$j])) { 84 | return false; 85 | } 86 | } 87 | } 88 | return true; 89 | } 90 | 91 | /** 92 | * The boundary of a MultiPoint is the empty set. 93 | * @return GeometryCollection 94 | */ 95 | public function boundary(): ?Geometry 96 | { 97 | return new GeometryCollection(); 98 | } 99 | 100 | public function centroid(): Point 101 | { 102 | if ($this->isEmpty()) { 103 | return new Point(); 104 | } 105 | 106 | if ($this->getGeos()) { 107 | // @codeCoverageIgnoreStart 108 | return geoPHP::geosToGeometry($this->getGeos()->centroid()); 109 | // @codeCoverageIgnoreEnd 110 | } 111 | 112 | $x = 0; 113 | $y = 0; 114 | foreach ($this->getComponents() as $component) { 115 | $x += $component->x(); 116 | $y += $component->y(); 117 | } 118 | return new Point($x / $this->numPoints(), $y / $this->numPoints()); 119 | } 120 | 121 | // Not valid for this geometry type 122 | // -------------------------------- 123 | public function explode(bool $toArray = false): ?array // @phpstan-ignore-line 124 | { 125 | return null; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Geometry/MultiPolygon.php: -------------------------------------------------------------------------------- 1 | >>> $coordinateArray 39 | * Multi-dimensional array of coordinates. 40 | * 41 | * @throws InvalidGeometryException 42 | * 43 | * @return MultiPolygon 44 | */ 45 | public static function fromArray(array $coordinateArray): MultiPolygon 46 | { 47 | $points = []; 48 | foreach ($coordinateArray as $point) { 49 | $points[] = Polygon::fromArray($point); 50 | } 51 | return new static($points); 52 | } 53 | 54 | public function geometryType(): string 55 | { 56 | return Geometry::MULTI_POLYGON; 57 | } 58 | 59 | public function centroid(): Point 60 | { 61 | if ($this->isEmpty()) { 62 | return new Point(); 63 | } 64 | 65 | if ($this->getGeos()) { 66 | // @codeCoverageIgnoreStart 67 | return geoPHP::geosToGeometry($this->getGeos()->centroid()); 68 | // @codeCoverageIgnoreEnd 69 | } 70 | 71 | $x = 0; 72 | $y = 0; 73 | $totalArea = 0; 74 | foreach ($this->getComponents() as $component) { 75 | if ($component->isEmpty()) { 76 | continue; 77 | } 78 | $componentArea = $component->area(); 79 | $totalArea += $componentArea; 80 | $componentCentroid = $component->centroid(); 81 | $x += $componentCentroid->x() * $componentArea; 82 | $y += $componentCentroid->y() * $componentArea; 83 | } 84 | return new Point($x / $totalArea, $y / $totalArea); 85 | } 86 | 87 | public function area(): float 88 | { 89 | if ($this->getGeos()) { 90 | // @codeCoverageIgnoreStart 91 | /** @noinspection PhpUndefinedMethodInspection */ 92 | return $this->getGeos()->area(); 93 | // @codeCoverageIgnoreEnd 94 | } 95 | 96 | $area = 0; 97 | foreach ($this->components as $component) { 98 | $area += $component->area(); 99 | } 100 | return $area; 101 | } 102 | 103 | public function boundary(): ?Geometry 104 | { 105 | $rings = []; 106 | foreach ($this->getComponents() as $component) { 107 | $rings = array_merge($rings, $component->components); 108 | } 109 | return new MultiLineString($rings); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Geometry/MultiSurface.php: -------------------------------------------------------------------------------- 1 | components) === 0; 57 | } 58 | 59 | public function startPoint(): ?Point 60 | { 61 | return null; 62 | } 63 | 64 | public function endPoint(): ?Point 65 | { 66 | return null; 67 | } 68 | 69 | public function pointN(int $n): ?Point 70 | { 71 | return null; 72 | } 73 | 74 | public function isClosed(): ?bool 75 | { 76 | return null; 77 | } 78 | 79 | public function isRing(): ?bool 80 | { 81 | return null; 82 | } 83 | 84 | public function length(): float 85 | { 86 | return 0.0; 87 | } 88 | 89 | public function length3D(): float 90 | { 91 | return 0.0; 92 | } 93 | 94 | public function haversineLength(): float 95 | { 96 | return 0.0; 97 | } 98 | 99 | public function vincentyLength(): float 100 | { 101 | return 0.0; 102 | } 103 | 104 | public function greatCircleLength(float $radius = null): float 105 | { 106 | return 0.0; 107 | } 108 | 109 | public function minimumZ(): ?float 110 | { 111 | return null; 112 | } 113 | 114 | public function maximumZ(): ?float 115 | { 116 | return null; 117 | } 118 | 119 | public function minimumM(): ?float 120 | { 121 | return null; 122 | } 123 | 124 | public function maximumM(): ?float 125 | { 126 | return null; 127 | } 128 | 129 | public function elevationGain(float $verticalTolerance = 0.0): ?float 130 | { 131 | return null; 132 | } 133 | 134 | public function elevationLoss(float $verticalTolerance = 0.0): ?float 135 | { 136 | return null; 137 | } 138 | 139 | public function zDifference(): ?float 140 | { 141 | return null; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Benchmark/Geometry/AbstractGeometryBench.php: -------------------------------------------------------------------------------- 1 | 38 | */ 39 | protected function createPolygonComponents(int $pointCount = 4, int $rings = 1): array 40 | { 41 | $components = []; 42 | for ($i = 0; $i < $rings; ++$i) { 43 | $components[] = $this->createLineString($pointCount, true); 44 | } 45 | 46 | return $components; 47 | } 48 | 49 | protected function createPolygon(int $pointCount = 4, int $rings = 1): Polygon 50 | { 51 | return new Polygon($this->createPolygonComponents($pointCount, $rings)); 52 | } 53 | 54 | protected function createMultiPolygon(int $pointCount = 4, int $rings = 1, int $polygons = 1): MultiPolygon 55 | { 56 | $components = []; 57 | for ($i = 0; $i < $polygons; ++$i) { 58 | $components[] = $this->createPolygon($pointCount, $rings); 59 | } 60 | 61 | return new MultiPolygon($components); 62 | } 63 | 64 | protected function createGeometryCollection(int $scale = 1): GeometryCollection 65 | { 66 | $gc1 = new GeometryCollection(array_fill(0, 1 * $scale, new Point(1, 2))); 67 | $gc2 = new GeometryCollection(array_fill(0, 1 * $scale, $gc1)); 68 | $gc3 = new GeometryCollection(array_fill(0, 1 * $scale, $gc2)); 69 | 70 | return new GeometryCollection( 71 | array_merge( 72 | array_fill(0, 10 * $scale, new Point(1, 2)), 73 | array_fill(0, 10 * $scale, $this->createLineString(10 * $scale)), 74 | array_fill(0, 10 * $scale, $this->createPolygon(2 * $scale + 4, 5)), 75 | array_fill(0, 1 * $scale, $this->createMultiPolygon($scale + 4, 5, 5)), 76 | array_fill(0, 1 * $scale, $gc3) 77 | ) 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Benchmark/Geometry/GeometryCollectionBench.php: -------------------------------------------------------------------------------- 1 | geometry = $this->createGeometryCollection(1); 15 | } 16 | 17 | public function setUpGeometryCollectionSmall(): void 18 | { 19 | $this->geometry = $this->createGeometryCollection(1); 20 | } 21 | 22 | /** 23 | * @BeforeMethods("setUpGeometryCollectionBig") 24 | */ 25 | public function benchGetPoints(): void 26 | { 27 | $this->geometry->getPoints(); 28 | } 29 | 30 | /** 31 | * @BeforeMethods("setUpGeometryCollectionBig") 32 | */ 33 | public function benchExplodeGeometries(): void 34 | { 35 | $this->geometry->explodeGeometries(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Benchmark/Geometry/LineStringBench.php: -------------------------------------------------------------------------------- 1 | geometry = $this->createLineString(50); 15 | } 16 | 17 | /** 18 | * @BeforeMethods("setUpLineString") 19 | * @Revs(100) 20 | */ 21 | public function benchInvertXY(): void 22 | { 23 | $this->geometry->invertXY(); 24 | } 25 | 26 | /** 27 | * @BeforeMethods("setUpLineString") 28 | * @Revs(200) 29 | */ 30 | public function benchIsEmpty(): void 31 | { 32 | $this->geometry->isEmpty(); 33 | } 34 | 35 | /** 36 | * @BeforeMethods("setUpLineString") 37 | */ 38 | public function benchIsSimple(): void 39 | { 40 | $this->geometry->isSimple(); 41 | } 42 | 43 | /** 44 | * @BeforeMethods("setUpLineString") 45 | */ 46 | public function benchAsArray(): void 47 | { 48 | $this->geometry->asArray(); 49 | } 50 | 51 | /** 52 | * @BeforeMethods("setUpLineString") 53 | */ 54 | public function benchGetBBox(): void 55 | { 56 | $this->geometry->getBBox(); 57 | } 58 | 59 | /** 60 | * @BeforeMethods("setUpLineString") 61 | */ 62 | public function benchExplode(): void 63 | { 64 | $this->geometry->explode(); 65 | } 66 | 67 | /** 68 | * @BeforeMethods("setUpLineString") 69 | */ 70 | public function benchExplodeTrue(): void 71 | { 72 | $this->geometry->explode(true); 73 | } 74 | 75 | /** 76 | * @BeforeMethods("setUpLineString") 77 | * @Revs(200) 78 | */ 79 | public function benchGeometryN(): void 80 | { 81 | $this->geometry->geometryN(10); 82 | } 83 | 84 | /** 85 | * @BeforeMethods("setUpLineString") 86 | * @Revs(200) 87 | */ 88 | public function benchEndPoint(): void 89 | { 90 | $this->geometry->endPoint(); 91 | } 92 | 93 | /** 94 | * @BeforeMethods("setUpLineString") 95 | */ 96 | public function benchLength(): void 97 | { 98 | $this->geometry->length(); 99 | } 100 | 101 | /** 102 | * @BeforeMethods("setUpLineString") 103 | */ 104 | public function benchLength3D(): void 105 | { 106 | $this->geometry->length3D(); 107 | } 108 | 109 | /** 110 | * @BeforeMethods("setUpLineString") 111 | */ 112 | public function benchGreatCircleLength(): void 113 | { 114 | $this->geometry->greatCircleLength(); 115 | } 116 | 117 | /** 118 | * @BeforeMethods("setUpLineString") 119 | */ 120 | public function benchHaversineLength(): void 121 | { 122 | $this->geometry->haversineLength(); 123 | } 124 | 125 | /** 126 | * @BeforeMethods("setUpLineString") 127 | */ 128 | public function benchVincentyLength(): void 129 | { 130 | $this->geometry->vincentyLength(); 131 | } 132 | 133 | /** 134 | * @BeforeMethods("setUpLineString") 135 | */ 136 | public function benchIsClosed(): void 137 | { 138 | $this->geometry->isClosed(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/Benchmark/Geometry/PointBench.php: -------------------------------------------------------------------------------- 1 | geometry = $this->createPolygon(100, 10); 15 | } 16 | 17 | public function setUpPolygonLarge(): void 18 | { 19 | $this->geometry = $this->createPolygon(1000, 100); 20 | } 21 | 22 | /** 23 | * @return array 24 | */ 25 | public function providePolygonComponents(): array 26 | { 27 | return [[$this->createPolygonComponents(10, 10)]]; 28 | } 29 | 30 | /** 31 | * @ParamProviders("providePolygonComponents") 32 | * 33 | * @param array $params 34 | */ 35 | public function benchCreatePolygon(array $params): void 36 | { 37 | new Polygon($params[0]); 38 | } 39 | 40 | /** 41 | * @BeforeMethods("setUpPolygonLarge") 42 | * @Revs(200) 43 | */ 44 | public function benchIsEmpty(): void 45 | { 46 | ($this->geometry->isEmpty()); 47 | } 48 | 49 | /** 50 | * @BeforeMethods("setUpPolygonLarge") 51 | * @Revs(200) 52 | */ 53 | public function benchExteriorRing(): void 54 | { 55 | $this->geometry->exteriorRing(); 56 | } 57 | 58 | /** 59 | * @BeforeMethods("setUpPolygonSmall") 60 | * @Revs(10) 61 | */ 62 | public function benchGetPoints(): void 63 | { 64 | $this->geometry->getPoints(); 65 | } 66 | 67 | /** 68 | * @BeforeMethods("setUpPolygonSmall") 69 | * @Revs(10) 70 | */ 71 | public function benchArea(): void 72 | { 73 | $this->geometry->area(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/geometryPerformance.php: -------------------------------------------------------------------------------- 1 | is3D(); 110 | } 111 | testEnd(); 112 | 113 | testStart("Adding points to LineString"); 114 | $lineString = new LineString($points); 115 | testEnd(); 116 | 117 | testStart("Test LineString::invertXY()"); 118 | $lineString->invertXY(); 119 | testEnd(); 120 | 121 | testStart("Test LineString::explode(true)"); 122 | $res = count($lineString->explode(true)); 123 | testEnd($res . ' segment'); 124 | 125 | testStart("Test LineString::explode()"); 126 | $res = count($lineString->explode()); 127 | testEnd($res . ' segment'); 128 | 129 | testStart("Test LineString::length()"); 130 | $res = $lineString->length(); 131 | testEnd($res); 132 | 133 | testStart("Test LineString::greatCircleLength()"); 134 | $res = $lineString->greatCircleLength(); 135 | testEnd($res); 136 | 137 | testStart("Test LineString::haversineLength()"); 138 | $res = $lineString->haversineLength(); 139 | testEnd($res); 140 | 141 | testStart("Test LineString::vincentyLength()"); 142 | $res = $lineString->vincentyLength(); 143 | testEnd($res); 144 | 145 | $shorterLine = new LineString(array_slice($points, 0, min($pointCount, 300))); 146 | testStart("Test LineString::isSimple() (300 points long line)"); 147 | $res = $shorterLine->isSimple(); 148 | testEnd($res ? 'simple' : 'not simple'); 149 | 150 | $ringPoints = array_slice($points, 0, min($pointCount, 500 - 1)); 151 | $ringPoints[] = $ringPoints[0]; 152 | $shortRing = new LineString($ringPoints); 153 | $rings = unserialize(serialize(array_fill(0, 50, $shortRing))); 154 | 155 | testStart("Creating Polygon (50 ring, each has 500 point)"); 156 | $polygon = new Polygon($rings); 157 | testEnd(); 158 | 159 | $components = unserialize( 160 | serialize( 161 | array_merge( 162 | $points, 163 | array_fill(0, 50, $lineString), 164 | array_fill(0, 50, $polygon) 165 | ) 166 | ) 167 | ); 168 | 169 | testStart("Creating GeometryCollection (10000 point + 50 LineString + 50 polygon)"); 170 | $collection = new GeometryCollection($components); 171 | testEnd(); 172 | 173 | testStart("GeometryCollection::getPoints()"); 174 | $res = $collection->getPoints(); 175 | testEnd(count($res)); 176 | } 177 | 178 | $startTime = microtime(true); 179 | $startMem = memory_get_usage() / 1024 / 1024; 180 | 181 | runPerformanceTests(); 182 | 183 | testEnd(null, true); 184 | 185 | if (microtime(true) - $startTime > MAX_RUN_TIME_SEC) { 186 | echo "\e[31mTOO SLOW!\e[39m\n" . PHP_EOL; 187 | exit(1); 188 | } else { 189 | echo "\e[32mOK\e[39m\n" . PHP_EOL; 190 | exit(0); 191 | } 192 | -------------------------------------------------------------------------------- /tests/input/an_empty_polygon.wkt: -------------------------------------------------------------------------------- 1 | POLYGON EMPTY -------------------------------------------------------------------------------- /tests/input/barret_spur.gpx: -------------------------------------------------------------------------------- 1 | 2 | 1374Vista Ridge TrailheadTrail Head 3 | 1777Wy'East Basin 4 | 1823Dollar Lake 5 | 2394Barrett SpurSummit 6 | 7 | Barrett Spur 1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Barrett Spur 2 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /tests/input/box.georss: -------------------------------------------------------------------------------- 1 | 42.943 -71.032 43.039 -69.856 2 | -------------------------------------------------------------------------------- /tests/input/cdata.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CDATA example 6 | 7 | CDATA Tags are useful! 9 | Text is more readable and 10 | easier to write when you can avoid using entity 11 | references. 12 | ]]> 13 | 14 | 15 | 102.595626,14.996729 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/input/circle.georss: -------------------------------------------------------------------------------- 1 | 42.943 -71.032 500 2 | -------------------------------------------------------------------------------- /tests/input/empty_point.wkt: -------------------------------------------------------------------------------- 1 | POINT EMPTY -------------------------------------------------------------------------------- /tests/input/geometrycollection.georss: -------------------------------------------------------------------------------- 1 | 2 | 4 | Earthquakes 5 | International earthquake observation labs 6 | 7 | 2005-12-13T18:30:02Z 8 | 9 | Dr. Thaddeus Remor 10 | tremor@quakelab.edu 11 | 12 | urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 13 | 14 | This has multiple georss entries in it 15 | 16 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 17 | 2005-08-17T07:02:32Z 18 | We just had a big one. 19 | 45.256 -71.92 20 | 42.256 -69.92 21 | 45.256 -110.45 46.46 -109.48 43.84 -109.86 22 | 45.256 -110.45 46.46 -109.48 43.84 -109.86 45.256 -110.45 23 | 24 | 25 | Another Entryd 26 | Another point. 27 | 46.256 -70.92 28 | 29 | -------------------------------------------------------------------------------- /tests/input/geometrycollection.wkt: -------------------------------------------------------------------------------- 1 | GEOMETRYCOLLECTION (POINT (4 6), LINESTRING (4 6,7 10)) 2 | -------------------------------------------------------------------------------- /tests/input/line.georss: -------------------------------------------------------------------------------- 1 | 45.256 -110.45 46.46 -109.48 43.84 -109.86 2 | -------------------------------------------------------------------------------- /tests/input/linestring.wkt: -------------------------------------------------------------------------------- 1 | LINESTRING (30 10, 10 30, 40 40) 2 | -------------------------------------------------------------------------------- /tests/input/long.geohash: -------------------------------------------------------------------------------- 1 | xn76urx4epb0 2 | -------------------------------------------------------------------------------- /tests/input/multilinestring.ewkt: -------------------------------------------------------------------------------- 1 | SRID=4269;MULTILINESTRING((-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)) 2 | -------------------------------------------------------------------------------- /tests/input/multilinestring.wkt: -------------------------------------------------------------------------------- 1 | MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10)) 2 | -------------------------------------------------------------------------------- /tests/input/multipolygon.wkb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funiq/geoPHP/f1c088ab9544072ee3236f0dfc2ec7c93d158546/tests/input/multipolygon.wkb -------------------------------------------------------------------------------- /tests/input/multipolygon.wkt: -------------------------------------------------------------------------------- 1 | MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5))) 2 | -------------------------------------------------------------------------------- /tests/input/multipolygon2.wkt: -------------------------------------------------------------------------------- 1 | MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)),((20 35, 45 20, 30 5, 10 10, 10 30, 20 35),(30 20, 20 25, 20 15, 30 20))) -------------------------------------------------------------------------------- /tests/input/opposite.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mons and its opposite 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/input/path.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Paths 5 | Examples of paths. Note that the tessellate tag is by default 6 | set to 0. If you want to create tessellated lines, they must be authored 7 | (or edited) directly in KML. 8 | 17 | 18 | Absolute Extruded 19 | Transparent green wall with yellow outlines 20 | #yellowLineGreenPoly 21 | 22 | 1 23 | 1 24 | absolute 25 | -112.2550785337791,36.07954952145647,2357 26 | -112.2549277039738,36.08117083492122,2357 27 | -112.2552505069063,36.08260761307279,2357 28 | -112.2564540158376,36.08395660588506,2357 29 | -112.2580238976449,36.08511401044813,2357 30 | -112.2595218489022,36.08584355239394,2357 31 | -112.2608216347552,36.08612634548589,2357 32 | -112.262073428656,36.08626019085147,2357 33 | -112.2633204928495,36.08621519860091,2357 34 | -112.2644963846444,36.08627897945274,2357 35 | -112.2656969554589,36.08649599090644,2357 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/input/pentagon.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Pentagon 5 | 6 | 1 7 | relativeToGround 8 | 9 | 10 | 11 | -77.05788457660967,38.87253259892824,100 12 | -77.05465973756702,38.87291016281703,100 13 | -77.05315536854791,38.87053267794386,100 14 | -77.05552622493516,38.868757801256,100 15 | -77.05844056290393,38.86996206506943,100 16 | -77.05788457660967,38.87253259892824,100 17 | 18 | 19 | 20 | 21 | 22 | 23 | -77.05668055019126,38.87154239798456,100 24 | -77.05542625960818,38.87167890344077,100 25 | -77.05485125901024,38.87076535397792,100 26 | -77.05577677433152,38.87008686581446,100 27 | -77.05691162017543,38.87054446963351,100 28 | -77.05668055019126,38.87154239798456,100 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/input/point.georss: -------------------------------------------------------------------------------- 1 | 2 | 4 | Earthquakes 5 | International earthquake observation labs 6 | 7 | 2005-12-13T18:30:02Z 8 | 9 | Dr. Thaddeus Remor 10 | tremor@quakelab.edu 11 | 12 | urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 13 | 14 | M 3.2, Mona Passage 15 | 16 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 17 | 2005-08-17T07:02:32Z 18 | We just had a big one. 19 | 45.256 -71.92 20 | 21 | -------------------------------------------------------------------------------- /tests/input/point.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple placemark 5 | Attached to the ground. Intelligently places itself 6 | at the height of the underlying terrain. 7 | 8 | -122.0822035425683,37.42228990140251,0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/input/point.wkt: -------------------------------------------------------------------------------- 1 | POINT (10 12) 2 | -------------------------------------------------------------------------------- /tests/input/point_earth.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple placemark 5 | Attached to the ground. Intelligently places itself 6 | at the height of the underlying terrain. 7 | 8 | 0,0,0 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/input/polygon.georss: -------------------------------------------------------------------------------- 1 | 2 | 45.256 -110.45 46.46 -109.48 43.84 -109.86 45.256 -110.45 3 | 4 | -------------------------------------------------------------------------------- /tests/input/polygon.wkt: -------------------------------------------------------------------------------- 1 | POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10)) 2 | -------------------------------------------------------------------------------- /tests/input/polygon2.wkt: -------------------------------------------------------------------------------- 1 | POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10), (20 30, 35 35, 30 20, 20 30)) 2 | -------------------------------------------------------------------------------- /tests/input/polygon3.wkt: -------------------------------------------------------------------------------- 1 | POLYGON ((-123.222653196 49.1529676585, -89.4726531957 49.3823707987, -81.0351531957 44.0875828344, -71.1914031957 44.3395630636, -62.0507781957 48.4583498573, -60.2929656957 45.0890334085, -78.9257781957 37.4399716272, -82.0898406957 31.3536343332, -81.3867156957 26.4312253295, -91.9335906957 29.8406412505, -98.2617156957 26.4312253295, -107.753903196 32.2499718728, -116.894528196 33.1375486348, -122.519528196 36.0313293064, -126.035153196 42.2935619329, -123.222653196 49.1529676585)) 2 | -------------------------------------------------------------------------------- /tests/input/polygon4.wkt: -------------------------------------------------------------------------------- 1 | POLYGON((4.8352495472368009 52.3561217600921438,4.8354139113045580 52.3561243429663534,4.8356082266282945 52.3561267417385281,4.8358010085903622 52.3561273083083663,4.8358010085903622 52.3561273083083663,4.8358035242637225 52.3559935212917722,4.8363777606561538 52.3559985348227173,4.8365863082998608 52.3560003600829731,4.8365523717335313 52.3570990145454189,4.8365884597636066 52.3572643433297529,4.8366320506970659 52.3574639095218686,4.8366736405531485 52.3576544056339870,4.8367264446828226 52.3578947700094304,4.8367922739966023 52.3581940807800450,4.8368228816936947 52.3583326871276356,4.8368228816936947 52.3583326871276356,4.8346348012064322 52.3583075969840550,4.8346348012064322 52.3583075969840550,4.8346348010943823 52.3583076059723282,4.8346348010943823 52.3583076059723282,4.8344931735728114 52.3583059732702338,4.8343773230572911 52.3583046496785585,4.8342182417472204 52.3583028092031384,4.8340047277034000 52.3583004442080195,4.8340047277034000 52.3583004442080195,4.8340047286008216 52.3583003723016063,4.8340047286008216 52.3583003723016063,4.8333843154510516 52.3582932434377639,4.8333843154510516 52.3582932434377639,4.8333915914677918 52.3580669388087898,4.8333968982183286 52.3578913129544787,4.8334415565569193 52.3563602568407660,4.8336003450092706 52.3563614767834267,4.8336013166539615 52.3563318721204567,4.8336013166539615 52.3563318721204567,4.8339582156582548 52.3563361223319603,4.8339656498645338 52.3561015845598732,4.8340692910524092 52.3561032110135258,4.8340692910524092 52.3561032110135258,4.8345511248958477 52.3561107854074095,4.8345511248958477 52.3561107854074095,4.8345513450958055 52.3561107864365809,4.8345513450958055 52.3561107864365809,4.8346742584771087 52.3561127181661092,4.8346742584771087 52.3561127181661092,4.8347750227755597 52.3561143035917596,4.8347750227755597 52.3561143035917596,4.8352495472368009 52.3561217600921438)) 2 | -------------------------------------------------------------------------------- /tests/input/polygon_spaces.wkt: -------------------------------------------------------------------------------- 1 | 2 | POLYGON ((30 10 , 10 20 , 20 40 , 40 40 , 30 10 )) 3 | -------------------------------------------------------------------------------- /tests/input/short.geohash: -------------------------------------------------------------------------------- 1 | xpssc0 2 | -------------------------------------------------------------------------------- /tests/input/simple_point.json: -------------------------------------------------------------------------------- 1 | {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-80.73029, 35.3936]}} -------------------------------------------------------------------------------- /tests/input/track.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Garmin International 7 | 8 | 2009-10-17T22:58:43Z 9 | 10 | 11 | Example GPX Document 12 | 13 | 14 | 4.46 15 | 2009-10-17T18:37:26Z 16 | 17 | 18 | 4.94 19 | 2009-10-17T18:37:31Z 20 | 21 | 22 | 6.87 23 | 2009-10-17T18:37:34Z 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/postgis.php: -------------------------------------------------------------------------------- 1 | setSRID(4326); 50 | test_postgis($table, $name, $format, $geometry, $connection, 'ewkb'); 51 | } 52 | } 53 | echo "Testing Done!\n"; 54 | } 55 | 56 | /** 57 | * @param string $table 58 | * @param string $name 59 | * @param string $type 60 | * @param Geometry $geom 61 | * @param resource|\PgSql\Connection $connection 62 | * @param string $format 63 | * @throws Exception 64 | */ 65 | function test_postgis(string $table, string $name, string $type, Geometry $geom, $connection, string $format): void 66 | { 67 | 68 | // Let's insert into the database using GeomFromWKB 69 | $insertString = pg_escape_bytea($geom->out($format)); 70 | 71 | pg_query( 72 | $connection, 73 | "INSERT INTO $table (name, type, geom) values ('$name', '$type', ST_GeomFromWKB('$insertString'))" 74 | ); 75 | 76 | // SELECT using asBinary PostGIS 77 | $result = pg_fetch_all( 78 | pg_query( 79 | $connection, 80 | "SELECT ST_AsBinary(geom) as geom FROM $table WHERE name='$name'" 81 | ) 82 | ) ?: []; 83 | 84 | foreach ($result as $item) { 85 | $wkb = pg_unescape_bytea($item['geom']); // Make sure to unescape the hex blob 86 | $geom = geoPHP::load($wkb, $format); // We now a full geoPHP Geometry object 87 | } 88 | 89 | // SELECT and INSERT directly, with no wrapping functions 90 | $result = pg_fetch_all(pg_query($connection, "SELECT geom as geom FROM $table WHERE name='$name'")) ?: []; 91 | foreach ($result as $item) { 92 | $geom = geoPHP::load($item['geom'], $format, true); // We now have a geoPHP Geometry 93 | 94 | // Let's re-insert directly into postGIS 95 | $insertString = $geom->out($format, true); 96 | pg_query($connection, "INSERT INTO $table (name, type, geom) values ('$name', '$type', '$insertString')"); 97 | } 98 | 99 | // SELECT and INSERT using as EWKT (ST_GeomFromEWKT and ST_AsEWKT) 100 | $result = pg_fetch_all( 101 | pg_query( 102 | $connection, 103 | "SELECT ST_AsEWKT(geom) as geom FROM $table WHERE name='$name'" 104 | ) 105 | ) ?: []; 106 | 107 | foreach ($result as $item) { 108 | $wkt = $item['geom']; // Make sure to unescape the hex blob 109 | $geom = geoPHP::load($item['geom'], 'ewkt'); // We now a full geoPHP Geometry object 110 | 111 | // Let's re-insert directly into postGIS 112 | $insertString = $geom->out('ewkt'); 113 | pg_query( 114 | $connection, 115 | "INSERT INTO $table (name, type, geom) values ('$name', '$type', ST_GeomFromEWKT('$insertString'))" 116 | ); 117 | } 118 | } 119 | 120 | run_test(); 121 | -------------------------------------------------------------------------------- /tests/unit/Adapter/EWKTTest.php: -------------------------------------------------------------------------------- 1 | read($wkt); 40 | 41 | $this->assertInstanceOf(Geometry::class, $geometry); 42 | $this->assertEquals($srid, $geometry->getSRID()); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function providerReadValidEwkt(): array 49 | { 50 | return [ 51 | [ 52 | 'SRID=3857;POINT(1 2)', 53 | 3857, 54 | ], 55 | [ 56 | 'SRID=4326;POINT(19.0 47.0 100)', 57 | 4326, 58 | ], 59 | ]; 60 | } 61 | 62 | /** 63 | * @dataProvider providerWriteValidEwkt 64 | * 65 | * @covers ::write 66 | */ 67 | public function testWritingValidEwkt(string $expectedWkt, Geometry $geometry, ?int $srid): void 68 | { 69 | $geometry->setSRID($srid); 70 | 71 | $wkt = (new EWKT())->write($geometry); 72 | 73 | $this->assertEquals($expectedWkt, $wkt); 74 | } 75 | 76 | /** 77 | * @return array{array{string, Geometry, int}} 78 | */ 79 | public function providerWriteValidEwkt() 80 | { 81 | return [ 82 | [ 83 | 'SRID=3857;POINT (1 2)', 84 | new Point(1, 2), 85 | 3857, 86 | ], 87 | [ 88 | 'SRID=4326;POINT Z (19.1 47.1 100)', 89 | new Point(19.1, 47.1, 100), 90 | 4326, 91 | ], 92 | [ 93 | 'POINT (1 2)', 94 | new Point(1, 2), 95 | null, 96 | ], 97 | [ 98 | 'SRID=23700;POINT EMPTY', 99 | new Point(), 100 | 23700, 101 | ], 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/unit/Adapter/GeoHashTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 22 | 'xne', 23 | GeoHash::adjacent('xn7', 'top'), 24 | 'Did not find correct top adjacent geohash for xn7' 25 | ); 26 | $this->assertEquals( 27 | 'xnk', 28 | GeoHash::adjacent('xn7', 'right'), 29 | 'Did not find correct right adjacent geohash for xn7' 30 | ); 31 | $this->assertEquals( 32 | 'xn5', 33 | GeoHash::adjacent('xn7', 'bottom'), 34 | 'Did not find correct bottom adjacent geohash for xn7' 35 | ); 36 | $this->assertEquals( 37 | 'xn6', 38 | GeoHash::adjacent('xn7', 'left'), 39 | 'Did not find correct left adjacent geohash for xn7' 40 | ); 41 | $this->assertEquals( 42 | 'xnd', 43 | GeoHash::adjacent(GeoHash::adjacent('xn7', 'left'), 'top'), 44 | 'Did not find correct top-left adjacent geohash for xn7' 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/Exception/FileFormatExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, $e); 20 | $this->assertInstanceOf(IOException::class, $e); 21 | 22 | $this->assertSame(0, $e->getCode()); 23 | $this->assertNull($e->getPrevious()); 24 | $this->assertSame('IO error: ', $e->getMessage()); 25 | } 26 | 27 | public function testConstruct(): void 28 | { 29 | $previousStub = $this->getMockForAbstractClass(Exception::class); 30 | $e = new FileFormatException("Error message", '', 1, $previousStub); 31 | 32 | $this->assertSame('IO error: Error message Data: ""', $e->getMessage()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Exception/IOExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, $e); 20 | $this->assertInstanceOf(RuntimeException::class, $e); 21 | 22 | $this->assertSame(0, $e->getCode()); 23 | $this->assertNull($e->getPrevious()); 24 | $this->assertSame('IO error: ', $e->getMessage()); 25 | } 26 | 27 | public function testConstruct(): void 28 | { 29 | $previousStub = $this->getMockForAbstractClass(Exception::class); 30 | $e = new IOException("Error message", 1, $previousStub); 31 | 32 | $this->assertSame('IO error: Error message', $e->getMessage()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Exception/InvalidGeometryException.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, $e); 20 | $this->assertInstanceOf(RuntimeException::class, $e); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/Exception/InvalidXmlExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, $e); 20 | $this->assertInstanceOf(IOException::class, $e); 21 | 22 | $this->assertSame(0, $e->getCode()); 23 | $this->assertNull($e->getPrevious()); 24 | $this->assertSame('IO error: Invalid XML.', $e->getMessage()); 25 | } 26 | 27 | public function testConstruct(): void 28 | { 29 | $previousStub = $this->getMockForAbstractClass(Exception::class); 30 | $e = new InvalidXmlException("Error message", 1, $previousStub); 31 | 32 | $this->assertSame('IO error: Invalid XML. Error message', $e->getMessage()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/unit/Exception/UnsupportedMethodExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Exception::class, $e); 20 | $this->assertInstanceOf(LogicException::class, $e); 21 | 22 | $this->assertSame(0, $e->getCode()); 23 | $this->assertNull($e->getPrevious()); 24 | $this->assertSame('Method Test() is not supported yet.', $e->getMessage()); 25 | } 26 | 27 | public function testConstruct(): void 28 | { 29 | $previousStub = $this->getMockForAbstractClass(Exception::class); 30 | $e = new UnsupportedMethodException("Test", "Error message", 1, $previousStub); 31 | 32 | $this->assertSame('Method Test() is not supported yet. Error message', $e->getMessage()); 33 | } 34 | 35 | public function testGeos(): void 36 | { 37 | $e = UnsupportedMethodException::geos("Test"); 38 | 39 | $this->assertInstanceOf(UnsupportedMethodException::class, $e); 40 | $this->assertSame('Method Test() is not supported yet. Please install GEOS extension.', $e->getMessage()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/Geometry/CurveTest.php: -------------------------------------------------------------------------------- 1 | > $coordinateArray 28 | * @return Point[] 29 | */ 30 | private function createPoints(array $coordinateArray): array 31 | { 32 | $points = []; 33 | foreach ($coordinateArray as $point) { 34 | $points[] = Point::fromArray($point); 35 | } 36 | return $points; 37 | } 38 | 39 | /** 40 | * @return array>>> 41 | */ 42 | public function providerValidComponents(): array 43 | { 44 | return [ 45 | 'empty' => 46 | [[]], 47 | 'with two points' => 48 | [[[0, 0], [1, 1]]], 49 | 'CURVE Z' => 50 | [[[0, 0, 0], [1, 1, 1]]], 51 | 'CURVE M' => 52 | [[[0, 0, null, 0], [1, 1, null, 1]]], 53 | 'CURVE ZM' => 54 | [[[0, 0, 0, 0], [1, 1, 1, 1]]], 55 | 'CURVE of 5 points' => 56 | [[[0, 0], [1, 1], [2, 2], [3, 3], [4, 4]]], 57 | ]; 58 | } 59 | 60 | /** 61 | * @dataProvider providerValidComponents 62 | * @covers ::__construct 63 | * 64 | * @param array> $points 65 | */ 66 | public function testConstructor(array $points): void 67 | { 68 | $curveStub = $this->getMockForAbstractClass(Curve::class, [$this->createPoints($points)]); 69 | 70 | $this->assertNotNull($curveStub); 71 | } 72 | 73 | /** 74 | * @covers ::__construct 75 | */ 76 | public function testConstructorEmptyComponentThrowsException(): void 77 | { 78 | $this->expectException(InvalidGeometryException::class); 79 | $this->expectExceptionMessageMatches('/Cannot create a collection of empty Points.+/'); 80 | 81 | // Empty points 82 | $this->getMockForAbstractClass(Curve::class, [[new Point(), new Point(), new Point()]]); 83 | } 84 | 85 | /** 86 | * @covers ::__construct 87 | */ 88 | public function testConstructorSinglePointThrowsException(): void 89 | { 90 | $this->expectException(InvalidGeometryException::class); 91 | $this->expectExceptionMessageMatches('/Cannot construct a [a-zA-Z_\\\\]+Curve.* with a single point/'); 92 | 93 | $this->getMockForAbstractClass(Curve::class, [[new Point(1, 2)]]); 94 | } 95 | 96 | /** 97 | * @covers ::__construct 98 | */ 99 | public function testConstructorWrongComponentTypeThrowsException(): void 100 | { 101 | $this->expectException(InvalidGeometryException::class); 102 | $this->expectExceptionMessageMatches('/Cannot construct .+Curve.*\. Expected .+Point components, got.+/'); 103 | 104 | // @phpstan-ignore-next-line 105 | $this->getMockForAbstractClass(Curve::class, [[new LineString(), new LineString()]]); 106 | } 107 | 108 | /** 109 | * @covers ::geometryType 110 | */ 111 | public function testGeometryType(): void 112 | { 113 | $curveStub = $this->getMockForAbstractClass(Curve::class, []); 114 | 115 | $this->assertEquals(LineString::CURVE, $curveStub->geometryType()); 116 | 117 | $this->assertInstanceOf(Curve::class, $curveStub); 118 | $this->assertInstanceOf(\geoPHP\Geometry\Collection::class, $curveStub); 119 | $this->assertInstanceOf(\geoPHP\Geometry\Geometry::class, $curveStub); 120 | } 121 | 122 | /** 123 | * @return array, MultiPoint}> 124 | */ 125 | public function providerBoundary(): array 126 | { 127 | return [ 128 | 'Empty' => [[], new MultiPoint()], 129 | 'Closed' => [[[1, 2], [3, 4], [1, 2]], new MultiPoint()], 130 | '3 points' => [[[1, 2], [3, 4], [5, 6]], new MultiPoint([new Point(1, 2), new Point(5, 6)])], 131 | ]; 132 | } 133 | 134 | /** 135 | * @dataProvider providerBoundary 136 | * @covers ::boundary 137 | * 138 | * @param array $components 139 | * @param MultiPoint $expectedBoundary 140 | */ 141 | public function testBoundary(array $components, MultiPoint $expectedBoundary): void 142 | { 143 | $curveStub = $this->getMockForAbstractClass(Curve::class, [$this->createPoints($components)]); 144 | 145 | $this->assertEquals($expectedBoundary, $curveStub->boundary()); 146 | } 147 | 148 | /** 149 | * @return array, ?Point, ?Point}> 150 | */ 151 | public function providerStartEndPoint(): array 152 | { 153 | return [ 154 | 'Empty' => [[], null, null], 155 | '3 points' => [[[1, 2], [3, 4], [5, 6]], new Point(1, 2), new Point(5, 6)], 156 | 'Closed' => [[[1, 2], [3, 4], [1, 2]], new Point(1, 2), new Point(1, 2)], 157 | ]; 158 | } 159 | 160 | /** 161 | * @dataProvider providerStartEndPoint 162 | * @covers ::startPoint 163 | * @covers ::endPoint 164 | * 165 | * @param array $components 166 | * @param Point|null $expectedStartPoint 167 | * @param Point|null $expectedEndPoint 168 | */ 169 | public function testStartEndPoint(array $components, ?Point $expectedStartPoint, ?Point $expectedEndPoint): void 170 | { 171 | $curveStub = $this->getMockForAbstractClass(Curve::class, [$this->createPoints($components)]); 172 | 173 | $this->assertEquals($expectedStartPoint, $curveStub->startPoint()); 174 | $this->assertEquals($expectedEndPoint, $curveStub->endPoint()); 175 | } 176 | 177 | /** 178 | * @return array, bool}> 179 | */ 180 | public function providerIsClosed(): array 181 | { 182 | return [ 183 | 'Empty' => [[], false], 184 | '3 points' => [[[1, 2], [3, 4], [5, 6]], false], 185 | 'Closed' => [[[1, 2], [3, 4], [1, 2]], true], 186 | 'Enough close' => [[[1, 2], [3, 4], [1.0000000001, 2.0000000001]], true], 187 | 'Not enough close' => [[[1, 2], [3, 4], [1.00001, 2.00001]], false], 188 | ]; 189 | } 190 | 191 | /** 192 | * @dataProvider providerIsClosed 193 | * @covers ::isClosed 194 | * 195 | * @param array $components 196 | * @param bool $isClosed 197 | */ 198 | public function testIsClosed(array $components, bool $isClosed): void 199 | { 200 | $curveStub = $this->getMockForAbstractClass(Curve::class, [$this->createPoints($components)]); 201 | 202 | $this->assertEquals($isClosed, $curveStub->isClosed()); 203 | } 204 | 205 | /** 206 | * @return array, bool}> 207 | */ 208 | public function providerIsRing(): array 209 | { 210 | return [ 211 | 'Empty' => [[], false], 212 | '3 points' => [[[1, 2], [3, 4], [5, 6]], false], 213 | 'Closed' => [[[1, 2], [3, 4], [1, 2]], true], 214 | 'Enough close' => [[[1, 2], [3, 4], [1.0000000001, 2.0000000001]], true], 215 | 'Not enough close' => [[[1, 2], [3, 4], [1.00001, 2.00001]], false], 216 | ]; 217 | } 218 | 219 | /** 220 | * @dataProvider providerIsClosed 221 | * @covers ::isClosed 222 | * 223 | * @param array $components 224 | * @param bool $isClosed 225 | */ 226 | public function testIsRing(array $components, bool $isClosed): void 227 | { 228 | $curveStub = $this->getMockForAbstractClass(Curve::class, [$this->createPoints($components)]); 229 | 230 | $this->assertEquals($isClosed, $curveStub->isClosed()); 231 | } 232 | 233 | /** 234 | * @dataProvider providerValidComponents 235 | * @covers ::getPoints 236 | * 237 | * @param array $components 238 | */ 239 | public function testGetPoints(array $components): void 240 | { 241 | $componentPoints = $this->createPoints($components); 242 | $curveStub = $this->getMockForAbstractClass(Curve::class, [$componentPoints]); 243 | 244 | $this->assertEquals($componentPoints, $curveStub->getPoints()); 245 | } 246 | 247 | /** 248 | * @covers ::area 249 | * @covers ::exteriorRing 250 | * @covers ::numInteriorRings 251 | * @covers ::interiorRingN 252 | */ 253 | public function testTrivialMethods(): void 254 | { 255 | $stub = $this->getMockForAbstractClass(Curve::class, [[]]); 256 | 257 | $this->assertSame(0.0, $stub->area()); 258 | 259 | $this->assertSame(null, $stub->exteriorRing()); 260 | 261 | $this->assertSame(null, $stub->numInteriorRings()); 262 | 263 | $this->assertSame(null, $stub->interiorRingN(1)); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /tests/unit/Geometry/MultiCurveTest.php: -------------------------------------------------------------------------------- 1 | >> $coordinateArray 25 | * @return Curve[] 26 | */ 27 | private function createCurves(array $coordinateArray): array 28 | { 29 | $curves = []; 30 | foreach ($coordinateArray as $curvePoints) { 31 | $points = []; 32 | foreach ($curvePoints as $coordinates) { 33 | $points[] = Point::fromArray($coordinates); 34 | } 35 | $curves[] = $this->getMockForAbstractClass(Curve::class, [$points]); 36 | } 37 | return $curves; 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function providerValidComponents(): array 44 | { 45 | return [ 46 | 'empty' => 47 | [[]], 48 | 'two curves with two points' => 49 | [[[[0, 0], [1, 1]], [[2, 2], [3, 3]]]], 50 | 'two curves, second is empty' => 51 | [[[[0, 0], [1, 1]], []]], 52 | ]; 53 | } 54 | 55 | /** 56 | * @dataProvider providerValidComponents 57 | * @covers ::__construct 58 | * 59 | * @param array>> $points 60 | */ 61 | public function testConstructor(array $points): void 62 | { 63 | $stub = $this->getMockForAbstractClass(MultiCurve::class, [$this->createCurves($points)]); 64 | 65 | $this->assertNotNull($stub); 66 | } 67 | 68 | /** 69 | * @covers ::__construct 70 | */ 71 | public function testConstructorWrongComponentTypeThrowsException(): void 72 | { 73 | $this->expectException(InvalidGeometryException::class); 74 | $this->expectExceptionMessageMatches('/Cannot construct .+Curve.*\. Expected .+Curve components, got.+/'); 75 | 76 | // @phpstan-ignore-next-line 77 | $this->getMockForAbstractClass(MultiCurve::class, [[new Point(), new Point()]]); 78 | } 79 | 80 | /** 81 | * @covers ::__construct 82 | * @covers ::geometryType 83 | * @covers ::dimension 84 | */ 85 | public function testGeometryType(): void 86 | { 87 | $stub = $this->getMockForAbstractClass(MultiCurve::class, []); 88 | 89 | $this->assertEquals(Geometry::MULTI_CURVE, $stub->geometryType()); 90 | 91 | $this->assertInstanceOf(MultiCurve::class, $stub); 92 | $this->assertInstanceOf(\geoPHP\Geometry\MultiGeometry::class, $stub); 93 | $this->assertInstanceOf(\geoPHP\Geometry\Collection::class, $stub); 94 | $this->assertInstanceOf(\geoPHP\Geometry\Geometry::class, $stub); 95 | 96 | $this->assertSame(1, $stub->dimension()); 97 | } 98 | 99 | /** 100 | * @return array 101 | */ 102 | public function providerIsClosed(): array 103 | { 104 | return [ 105 | 'empty' => 106 | [[], false], 107 | 'two ring' => 108 | [[[[0, 0], [1, 1], [0, 0]], [[2, 2], [3, 3], [2, 2]]], true], 109 | 'two curve forming a ring' => 110 | [[[[0, 0], [1, 1], [2, 2]], [[2, 2], [3, 3], [0, 0]]], false], 111 | 'two curves, second is not closed' => 112 | [[[[0, 0], [1, 1], [0, 0]], [[2, 2], [3, 3]]], false], 113 | ]; 114 | } 115 | 116 | /** 117 | * @dataProvider providerIsClosed 118 | * @covers ::isClosed 119 | * 120 | * @param array $components 121 | */ 122 | public function testIsClosed(array $components, bool $isClosed): void 123 | { 124 | $stub = $this->getMockForAbstractClass(MultiCurve::class, [$this->createCurves($components)]); 125 | 126 | $this->assertSame($isClosed, $stub->isClosed()); 127 | } 128 | 129 | /** 130 | * @return array 131 | */ 132 | public function providerBoundary(): array 133 | { 134 | return [ 135 | 'empty' => [ 136 | [], 137 | new MultiPoint() 138 | ], 139 | 'two curves' => [ 140 | [[[1, 1], [2, 2]], [[5, 5], [6, 6]]], 141 | MultiPoint::fromArray([[1, 1], [2, 2], [5, 5], [6, 6]]) 142 | ], 143 | 'two curves, second is closed' => [ 144 | [[[1, 1], [2, 2]], [[5, 5], [5, 5]]], 145 | MultiPoint::fromArray([[1, 1], [2, 2]]) 146 | ], 147 | 'connecting curves, "mod 2 rule"' => [ 148 | [[[1, 1], [2, 2]], [[2, 2], [3, 3]]], 149 | MultiPoint::fromArray([[1, 1], [3, 3]]) 150 | ], 151 | 'complex example' => [ 152 | [[[1, 1], [2, 2]], [[2, 2], [3, 3], [4, 4], [4, 4], [3, 3], [5, 5]]], 153 | MultiPoint::fromArray([[1, 1], [5, 5]]) 154 | ], 155 | ]; 156 | } 157 | 158 | /** 159 | * @dataProvider providerBoundary 160 | * @covers ::boundary 161 | * 162 | * @param array $components 163 | */ 164 | public function testBoundary(array $components, Geometry $expectedBoundary): void 165 | { 166 | $stub = $this->getMockForAbstractClass(MultiCurve::class, [$this->createCurves($components)]); 167 | 168 | $this->assertEquals($expectedBoundary, $stub->boundary()); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/unit/Geometry/MultiGeometryTest.php: -------------------------------------------------------------------------------- 1 | getMockForAbstractClass(Geometry::class, []); 26 | 27 | $this->assertInstanceOf( 28 | MultiGeometry::class, 29 | $this->getMockForAbstractClass(MultiGeometry::class, [[$geom1]]) 30 | ); 31 | 32 | $this->assertInstanceOf( 33 | MultiGeometry::class, 34 | $this->getMockForAbstractClass(MultiGeometry::class, [[$geom1], Geometry::class]) 35 | ); 36 | 37 | $this->assertInstanceOf( 38 | MultiGeometry::class, 39 | $this->getMockForAbstractClass(MultiGeometry::class, [[$geom1], Geometry::class, true]) 40 | ); 41 | } 42 | 43 | /** 44 | * @return array 45 | */ 46 | public function providerIsSimple(): array 47 | { 48 | return [ 49 | 'empty' => 50 | [[], true], 51 | 'two point' => 52 | [[new Point(), new Point()], true], 53 | 'two curves, second is empty' => 54 | [[[[0, 0], [1, 1]], []]], 55 | ]; 56 | } 57 | 58 | public function testIsSimple(): void 59 | { 60 | $simpleGeom = $this->getMockForAbstractClass(Geometry::class); 61 | $simpleGeom->expects($this->any()) 62 | ->method('isSimple') 63 | ->willReturn(true); 64 | $nonSimpleGeom = $this->getMockForAbstractClass(Geometry::class); 65 | $nonSimpleGeom->expects($this->any()) 66 | ->method('isSimple') 67 | ->willReturn(false); 68 | 69 | // Empty components => simple 70 | $stub = $this->getMockForAbstractClass(MultiGeometry::class, [[]]); 71 | $this->assertTrue($stub->isSimple()); 72 | 73 | // Simple components => simple 74 | $stub = $this->getMockForAbstractClass(MultiGeometry::class, [[$simpleGeom, $simpleGeom]]); 75 | $this->assertTrue($stub->isSimple()); 76 | 77 | // One simple and one non simple component => not simple 78 | $stub = $this->getMockForAbstractClass(MultiGeometry::class, [[$simpleGeom, $nonSimpleGeom]]); 79 | $this->assertFalse($stub->isSimple()); 80 | 81 | // Two non simple component => not simple 82 | $stub = $this->getMockForAbstractClass(MultiGeometry::class, [[$nonSimpleGeom, $nonSimpleGeom]]); 83 | $this->assertFalse($stub->isSimple()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/unit/Geometry/MultiPointTest.php: -------------------------------------------------------------------------------- 1 | >> 25 | */ 26 | public function providerValidComponents(): array 27 | { 28 | return [ 29 | 'no components' => [[]], 30 | 'empty Point comp' => [[new Point()]], 31 | 'xy' => [[new Point(1, 2)]], 32 | '2 xy' => [[new Point(1, 2), new Point(3, 4)]], 33 | '2 xyzm' => [[new Point(1, 2, 3, 4), new Point(5, 6, 7, 8)]], 34 | 'one is empty' => [[new Point(), new Point(1, 2)]], 35 | ]; 36 | } 37 | 38 | /** 39 | * @dataProvider providerValidComponents 40 | * @covers ::__construct 41 | * 42 | * @param array $points 43 | */ 44 | public function testValidComponents(array $points): void 45 | { 46 | $multiPoint = new MultiPoint($points); 47 | 48 | $this->assertNotNull($multiPoint); 49 | 50 | $this->assertInstanceOf(MultiPoint::class, $multiPoint); 51 | } 52 | 53 | /** 54 | * @return array> 55 | */ 56 | public function providerInvalidComponents(): array 57 | { 58 | return [ 59 | 'LineString component' => [[LineString::fromArray([[1,2],[3,4]])]], 60 | 'string component' => [["text"]], 61 | ]; 62 | } 63 | 64 | /** 65 | * @dataProvider providerInvalidComponents 66 | * @covers ::__construct 67 | * 68 | * @param array $components 69 | */ 70 | public function testConstructorWithInvalidComponents($components): void 71 | { 72 | $this->expectException(InvalidGeometryException::class); 73 | 74 | new MultiPoint($components); 75 | } 76 | 77 | /** 78 | * @covers ::fromArray 79 | */ 80 | public function testFromArray(): void 81 | { 82 | $this->assertEquals( 83 | MultiPoint::fromArray([[1,2,3,4], [5,6,7,8]]), 84 | new MultiPoint([new Point(1, 2, 3, 4), new Point(5, 6, 7, 8)]) 85 | ); 86 | } 87 | 88 | /** 89 | * @covers ::__construct 90 | * @covers ::geometryType 91 | */ 92 | public function testGeometryType(): void 93 | { 94 | $multiPoint = new MultiPoint(); 95 | 96 | $this->assertEquals(\geoPHP\Geometry\Geometry::MULTI_POINT, $multiPoint->geometryType()); 97 | 98 | $this->assertInstanceOf('\geoPHP\Geometry\MultiPoint', $multiPoint); 99 | $this->assertInstanceOf('\geoPHP\Geometry\MultiGeometry', $multiPoint); 100 | $this->assertInstanceOf('\geoPHP\Geometry\Geometry', $multiPoint); 101 | } 102 | 103 | /** 104 | * @covers ::is3D 105 | */ 106 | public function testIs3D(): void 107 | { 108 | $this->assertFalse((new MultiPoint([new Point(1, 2)]))->is3D()); 109 | $this->assertTrue((new MultiPoint([new Point(1, 2, 3)]))->is3D()); 110 | $this->assertTrue((new MultiPoint([new Point(1, 2, 3, 4)]))->is3D()); 111 | } 112 | 113 | /** 114 | * @covers ::isMeasured 115 | */ 116 | public function testIsMeasured(): void 117 | { 118 | $this->assertFalse((new MultiPoint([new Point(1, 2)]))->isMeasured()); 119 | $this->assertFalse((new MultiPoint([new Point(1, 2, 3)]))->isMeasured()); 120 | $this->assertTrue((new MultiPoint([new Point(1, 2, 3, 4)]))->isMeasured()); 121 | } 122 | 123 | /** 124 | * @return array 125 | */ 126 | public function providerCentroid(): array 127 | { 128 | return [ 129 | [[], []], 130 | [[[0, 0], [0, 10]], [0, 5]] 131 | ]; 132 | } 133 | 134 | /** 135 | * @dataProvider providerCentroid 136 | * @covers ::centroid 137 | * 138 | * @param array $components 139 | * @param array $expectedCentroid 140 | */ 141 | public function testCentroid(array $components, array $expectedCentroid): void 142 | { 143 | $multiPoint = MultiPoint::fromArray($components); 144 | $centroid = $multiPoint->centroid(); 145 | 146 | $this->assertEquals(Point::fromArray($expectedCentroid), $centroid); 147 | } 148 | 149 | /** 150 | * @return array{array{array, bool}} 151 | */ 152 | public function providerIsSimple(): array 153 | { 154 | return [ 155 | [[], true], 156 | [[[0, 0], [0, 10]], true], 157 | [[[1, 1], [2, 2], [1, 3], [1, 2], [2, 1]], true], 158 | [[[0, 10], [0, 10]], false], 159 | ]; 160 | } 161 | 162 | /** 163 | * @dataProvider providerIsSimple 164 | * @covers ::isSimple 165 | * 166 | * @param array $points 167 | * @param bool $result 168 | */ 169 | public function testIsSimple(array $points, bool $result): void 170 | { 171 | $multiPoint = MultiPoint::fromArray($points); 172 | 173 | $this->assertSame($result, $multiPoint->isSimple()); 174 | } 175 | 176 | /** 177 | * @return array> 178 | */ 179 | public function providerNumPoints(): array 180 | { 181 | return [ 182 | 'no components' => [0, []], 183 | 'empty Point comp' => [0, [new Point()]], 184 | 'xy' => [1, [new Point(1, 2)]], 185 | '2 xy' => [2, [new Point(1, 2), new Point(3, 4)]], 186 | 'one is empty' => [1, [new Point(), new Point(1, 2)]], 187 | ]; 188 | } 189 | 190 | /** 191 | * @dataProvider providerNumPoints 192 | * @covers ::numPoints 193 | * 194 | * @param Point[]|array{} $points 195 | */ 196 | public function testNumPoints(int $expectedNum, array $points): void 197 | { 198 | $multiPoint = new MultiPoint($points); 199 | 200 | $this->assertEquals($expectedNum, $multiPoint->numPoints()); 201 | } 202 | 203 | /** 204 | * @dataProvider providerValidComponents 205 | * @covers ::dimension 206 | * @covers ::boundary 207 | * @covers ::explode 208 | * 209 | * @param array $points 210 | */ 211 | public function testTrivialAndNotValidMethods(array $points): void 212 | { 213 | $point = new MultiPoint($points); 214 | 215 | $this->assertSame(0, $point->dimension()); 216 | 217 | $this->assertEquals(new \geoPHP\Geometry\GeometryCollection(), $point->boundary()); 218 | 219 | $this->assertNull($point->explode()); 220 | 221 | $this->assertTrue($point->isSimple()); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/unit/Geometry/PolygonTest.php: -------------------------------------------------------------------------------- 1 | $coordinateArray 25 | * @return LineString[] 26 | */ 27 | private function createComponents(array $coordinateArray): array 28 | { 29 | $lines = []; 30 | foreach ($coordinateArray as $point) { 31 | $lines[] = LineString::fromArray($point); 32 | } 33 | return $lines; 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function providerConstructorValidComponents(): array 40 | { 41 | return [ 42 | 'empty' => 43 | [[]], 44 | 'of 4 points' => 45 | [[[[0, 0], [0, 1], [1, 1], [0, 0]]]], 46 | 'Polygon Z' => 47 | [[[[0, 0, 0], [0, 1, 1], [1, 1, 2], [0, 0, 3]]]], 48 | 'Polygon M' => 49 | [[[[0, 0, null, 0], [0, 1, null, 1], [1, 1, null, 2], [0, 0, null, 3]]]], 50 | 'Polygon ZM' => 51 | [[[[0, 0, 0, 0], [0, 1, 1, 1], [1, 1, 2, 2], [0, 0, 3, 3]]]], 52 | 'Polygon with two rings' => 53 | [[[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]], [[2, 2], [2, 4], [3, 4], [2, 2]]]], 54 | ]; 55 | } 56 | 57 | /** 58 | * @dataProvider providerConstructorValidComponents 59 | * @covers ::__construct 60 | * 61 | * @param array $points 62 | */ 63 | public function testConstructor(array $points): void 64 | { 65 | $polygon = new Polygon($this->createComponents($points)); 66 | 67 | $this->assertNotNull($polygon); 68 | $this->assertInstanceOf(Polygon::class, $polygon); 69 | } 70 | 71 | /** 72 | * @covers ::__construct 73 | */ 74 | public function testConstructorNonArrayComponentTypeError(): void 75 | { 76 | $this->expectException(\TypeError::class); 77 | $this->expectErrorMessageMatches('/Argument #?1 .+ type array, string given/'); 78 | 79 | // @phpstan-ignore-next-line 80 | new Polygon('foo'); 81 | } 82 | 83 | /** 84 | * @covers ::__construct 85 | */ 86 | public function testConstructorEmptyComponent(): void 87 | { 88 | $this->expectException(InvalidGeometryException::class); 89 | $this->expectErrorMessageMatches('/Cannot create a collection of empty LineStrings/'); 90 | 91 | new Polygon([new LineString()]); 92 | } 93 | 94 | /** 95 | * @return array> 96 | */ 97 | public function providerConstructorFewPoints(): array 98 | { 99 | return [ 100 | 'two points' => [LineString::fromArray([[1, 2], [2, 3]])], 101 | 'three points' => [LineString::fromArray([[1, 2], [2, 3], [4, 5]])], 102 | ]; 103 | } 104 | 105 | /** 106 | * @dataProvider providerConstructorFewPoints 107 | * @covers ::__construct 108 | */ 109 | public function testConstructorFewPointThrowsException(LineString $component): void 110 | { 111 | $this->expectException(InvalidGeometryException::class); 112 | $this->expectExceptionMessageMatches( 113 | '/Cannot create Polygon: Invalid number of points in LinearRing. Found \d+, expected more than 3/' 114 | ); 115 | 116 | new Polygon([$component]); 117 | } 118 | 119 | /** 120 | * @covers ::__construct 121 | */ 122 | public function testConstructorWrongComponentTypeThrowsException(): void 123 | { 124 | $this->expectException(InvalidGeometryException::class); 125 | $this->expectExceptionMessageMatches('/Cannot construct .+Polygon\. Expected .+LineString components, got.+/'); 126 | 127 | // @phpstan-ignore-next-line 128 | new Polygon([new Point()]); 129 | } 130 | 131 | /** 132 | * @return array 133 | */ 134 | public function providerValidComponents(): array 135 | { 136 | $ring1Points = [[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]; 137 | $ring2Points = [[1, 1], [1, 9], [5, 9], [1, 1]]; 138 | $ring3Points = [[2, 2], [2, 8], [5, 8], [2, 2]]; 139 | 140 | $ring1 = new LineString( 141 | [new Point(0, 0), new Point(0, 10), new Point(10, 10), new Point(10, 0), new Point(0, 0)] 142 | ); 143 | $ring2 = new LineString([new Point(1, 1), new Point(1, 9), new Point(5, 9), new Point(1, 1)]); 144 | $ring3 = new LineString([new Point(2, 2), new Point(2, 8), new Point(5, 8), new Point(2, 2)]); 145 | 146 | return [ 147 | 'empty' => [ 148 | [], 149 | new Polygon(), 150 | [ 151 | 'exteriorRing' => new LineString(), 152 | 'interiorRings' => [], 153 | 'boundaryType' => Geometry::LINE_STRING, 154 | ] 155 | ], 156 | 'one ring' => [ 157 | [$ring1Points], 158 | new Polygon([$ring1]), 159 | [ 160 | 'exteriorRing' => $ring1, 161 | 'interiorRings' => [], 162 | 'boundaryType' => Geometry::LINE_STRING, 163 | ] 164 | ], 165 | 'two ring' => [ 166 | [$ring1Points, $ring2Points], 167 | new Polygon([$ring1, $ring2]), 168 | [ 169 | 'exteriorRing' => $ring1, 170 | 'interiorRings' => [1 => $ring2], 171 | 'boundaryType' => Geometry::MULTI_LINE_STRING, 172 | ] 173 | ], 174 | 'three ring' => [ 175 | [$ring1Points, $ring2Points, $ring3Points], 176 | new Polygon([$ring1, $ring2, $ring3]), 177 | [ 178 | 'exteriorRing' => $ring1, 179 | 'interiorRings' => [1 => $ring2, 2 => $ring3], 180 | 'boundaryType' => Geometry::MULTI_LINE_STRING, 181 | ] 182 | ], 183 | ]; 184 | } 185 | 186 | /** 187 | * @dataProvider providerValidComponents 188 | * @covers ::fromArray 189 | * 190 | * @param array>> $points 191 | * @param Polygon $expectedGeometry 192 | */ 193 | public function testFromArray(array $points, Polygon $expectedGeometry): void 194 | { 195 | $fromArray = Polygon::fromArray($points); 196 | 197 | $this->assertEquals($expectedGeometry, $fromArray); 198 | } 199 | 200 | /** 201 | * @covers ::geometryType 202 | */ 203 | public function testGeometryType(): void 204 | { 205 | $polygon = new Polygon(); 206 | 207 | $this->assertEquals(Polygon::POLYGON, $polygon->geometryType()); 208 | 209 | $this->assertInstanceOf(Polygon::class, $polygon); 210 | $this->assertInstanceOf(\geoPHP\Geometry\Surface::class, $polygon); 211 | $this->assertInstanceOf(\geoPHP\Geometry\Collection::class, $polygon); 212 | $this->assertInstanceOf(\geoPHP\Geometry\Geometry::class, $polygon); 213 | } 214 | 215 | /** 216 | * @covers ::dimension 217 | */ 218 | public function testDimension(): void 219 | { 220 | $polygon = new Polygon(); 221 | 222 | $this->assertSame(2, $polygon->dimension()); 223 | } 224 | 225 | /** 226 | * @dataProvider providerValidComponents 227 | * @covers ::exteriorRing 228 | * 229 | * @param array> $points 230 | * @param Polygon $geometry 231 | * @param array $results 232 | */ 233 | public function testExteriorRing(array $points, Polygon $geometry, array $results): void 234 | { 235 | $this->assertEquals($results['exteriorRing'], $geometry->exteriorRing()); 236 | } 237 | 238 | /** 239 | * @dataProvider providerValidComponents 240 | * @covers ::numInteriorRings 241 | * 242 | * @param array> $points 243 | * @param Polygon $geometry 244 | * @param array $results 245 | */ 246 | public function testNumInteriorRings(array $points, Polygon $geometry, array $results): void 247 | { 248 | $this->assertSame(count($results['interiorRings']), $geometry->numInteriorRings()); 249 | } 250 | 251 | /** 252 | * @dataProvider providerValidComponents 253 | * @covers ::interiorRingN 254 | * 255 | * @param array> $points 256 | * @param Polygon $geometry 257 | * @param array $results 258 | */ 259 | public function testInteriorRingN(array $points, Polygon $geometry, array $results): void 260 | { 261 | if (!count($results['interiorRings'])) { 262 | $this->expectNotToPerformAssertions(); 263 | } 264 | foreach ($results['interiorRings'] as $num => $ring) { 265 | $this->assertEquals($ring, $geometry->interiorRingN($num)); 266 | } 267 | } 268 | 269 | /** 270 | * @dataProvider providerValidComponents 271 | * @covers ::boundary 272 | * 273 | * @param array> $points 274 | * @param Polygon $geometry 275 | * @param array $results 276 | */ 277 | public function testBoundary(array $points, Polygon $geometry, array $results): void 278 | { 279 | $boundary = $geometry->boundary(); 280 | 281 | $this->assertInstanceOf(Geometry::class, $boundary); 282 | $this->assertEquals($results['boundaryType'], $boundary->geometryType()); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /tests/unit/Geometry/SurfaceTest.php: -------------------------------------------------------------------------------- 1 | getMockForAbstractClass(Surface::class, [[]]); 27 | 28 | $this->assertNotNull($stub); 29 | } 30 | 31 | /** 32 | * @covers ::geometryType 33 | */ 34 | public function testGeometryType(): void 35 | { 36 | $stub = $this->getMockForAbstractClass(Surface::class, [[]]); 37 | 38 | $this->assertEquals(Polygon::SURFACE, $stub->geometryType()); 39 | 40 | $this->assertInstanceOf(Surface::class, $stub); 41 | $this->assertInstanceOf(\geoPHP\Geometry\Collection::class, $stub); 42 | $this->assertInstanceOf(Geometry::class, $stub); 43 | } 44 | 45 | /** 46 | * @covers ::dimension 47 | */ 48 | public function testDimension(): void 49 | { 50 | $stub = $this->getMockForAbstractClass(Surface::class, [[]]); 51 | 52 | $this->assertSame(2, $stub->dimension()); 53 | } 54 | 55 | /** 56 | * @covers ::isEmpty 57 | */ 58 | public function testIsEmpty(): void 59 | { 60 | $stub = $this->getMockForAbstractClass(Surface::class, [[]]); 61 | $this->assertTrue($stub->isEmpty()); 62 | 63 | $geometryStub = $this->getMockForAbstractClass(Geometry::class, []); 64 | $stub = $this->getMockForAbstractClass(Surface::class, [[$geometryStub]]); 65 | $this->assertFalse($stub->isEmpty()); 66 | } 67 | 68 | /** 69 | * @covers ::startPoint 70 | * @covers ::endPoint 71 | * @covers ::pointN 72 | * @covers ::isClosed 73 | * @covers ::isRing 74 | * @covers ::length 75 | * @covers ::length3D 76 | * @covers ::haversineLength 77 | * @covers ::vincentyLength 78 | * @covers ::greatCircleLength 79 | * @covers ::minimumZ 80 | * @covers ::maximumZ 81 | * @covers ::minimumM 82 | * @covers ::maximumM 83 | * @covers ::elevationGain 84 | * @covers ::elevationLoss 85 | * @covers ::zDifference 86 | */ 87 | public function testTrivialMethods(): void 88 | { 89 | $stub = $this->getMockForAbstractClass(Surface::class, [[]]); 90 | 91 | $this->assertSame(null, $stub->startPoint()); 92 | 93 | $this->assertSame(null, $stub->endPoint()); 94 | 95 | $this->assertSame(null, $stub->pointN(1)); 96 | 97 | $this->assertSame(null, $stub->isClosed()); 98 | 99 | $this->assertSame(null, $stub->isRing()); 100 | 101 | $this->assertSame(0.0, $stub->length()); 102 | 103 | $this->assertSame(0.0, $stub->length3D()); 104 | 105 | $this->assertSame(0.0, $stub->haversineLength()); 106 | 107 | $this->assertSame(0.0, $stub->vincentyLength()); 108 | 109 | $this->assertSame(0.0, $stub->greatCircleLength()); 110 | 111 | $this->assertSame(null, $stub->minimumZ()); 112 | 113 | $this->assertSame(null, $stub->maximumZ()); 114 | 115 | $this->assertSame(null, $stub->minimumM()); 116 | 117 | $this->assertSame(null, $stub->maximumM()); 118 | 119 | $this->assertSame(null, $stub->elevationGain()); 120 | 121 | $this->assertSame(null, $stub->elevationLoss()); 122 | 123 | $this->assertSame(null, $stub->zDifference()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/unit/legacy/AdaptersTest.php: -------------------------------------------------------------------------------- 1 | $adapterClass) { 22 | if ($adapterKey == 'google_geocode') { 23 | //Don't test google geocoder regularly. Comment to test 24 | continue; 25 | } 26 | $output = $geometry->out($adapterKey); 27 | $this->assertNotNull($output, "Empty output on " . $adapterKey); 28 | if ($output) { 29 | $adapterName = 'geoPHP\\Adapter\\' . $adapterClass; 30 | /** @var \geoPHP\Adapter\GeoAdapter $adapterLoader */ 31 | $adapterLoader = new $adapterName(); 32 | $testGeom1 = $adapterLoader->read($output); 33 | $testGeom2 = $adapterLoader->read($testGeom1->out($adapterKey)); 34 | $this->assertEquals( 35 | $testGeom1->out('wkt'), 36 | $testGeom2->out('wkt'), 37 | "Mismatched adapter output in " . $adapterClass . ' (test file: ' . $file . ')' 38 | ); 39 | } 40 | } 41 | 42 | // Test to make sure adapter work the same wether GEOS is ON or OFF 43 | // Cannot test methods if GEOS is not intstalled 44 | if (!geoPHP::isGeosInstalled()) { 45 | return; 46 | } 47 | 48 | foreach (geoPHP::getAdapterMap() as $adapterKey => $adapterClass) { 49 | if ($adapterKey === 'google_geocode') { 50 | //Don't test google geocoder regularly. Comment to test 51 | continue; 52 | } 53 | // Turn GEOS on 54 | geoPHP::enableGeos(); 55 | 56 | $output = $geometry->out($adapterKey); 57 | if ($output) { 58 | $adapterName = 'geoPHP\\Adapter\\' . $adapterClass; 59 | $adapterLoader = new $adapterName(); 60 | 61 | $testGeom1 = $adapterLoader->read($output); 62 | 63 | // Turn GEOS off 64 | geoPHP::disableGeos(); 65 | 66 | $testGeom2 = $adapterLoader->read($output); 67 | 68 | // Turn GEOS back On 69 | geoPHP::enableGeos(); 70 | 71 | // Check to make sure a both are the same with geos and without 72 | $msg = "Mismatched adapter output between GEOS and NORM in " . $adapterClass 73 | . ' (test file: ' . $file . ')'; 74 | $this->assertEquals($testGeom1->out('wkt'), $testGeom2->out('wkt'), $msg); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/unit/legacy/GeosTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('GEOS not installed'); 15 | } 16 | 17 | foreach (scandir('tests/input') as $file) { 18 | $parts = explode('.', $file); 19 | if ($parts[0]) { 20 | if ($parts[0] == 'countries_ne_110m') { 21 | // Due to a bug in GEOS we have to skip some tests 22 | // It drops TopologyException for valid geometries 23 | // https://trac.osgeo.org/geos/ticket/737 24 | // continue; 25 | } 26 | 27 | $format = $parts[1]; 28 | $value = file_get_contents('tests/input/' . $file); 29 | //echo "\nloading: " . $file . " for format: " . $format; 30 | $geometry = geoPHP::load($value, $format); 31 | 32 | $geosMethods = [ 33 | ['name' => 'getGeos'], 34 | ['name' => 'flushGeosCache'], 35 | ['name' => 'pointOnSurface'], 36 | ['name' => 'equals', 'argument' => $geometry], 37 | ['name' => 'equalsExact', 'argument' => $geometry], 38 | ['name' => 'relate', 'argument' => $geometry], 39 | ['name' => 'checkValidity'], 40 | ['name' => 'isSimple'], 41 | ['name' => 'buffer', 'argument' => '10'], 42 | ['name' => 'intersection', 'argument' => $geometry], 43 | ['name' => 'convexHull'], 44 | ['name' => 'difference', 'argument' => $geometry], 45 | ['name' => 'symDifference', 'argument' => $geometry], 46 | ['name' => 'union', 'argument' => $geometry], 47 | ['name' => 'simplify', 'argument' => '0'], 48 | ['name' => 'disjoint', 'argument' => $geometry], 49 | ['name' => 'touches', 'argument' => $geometry], 50 | ['name' => 'intersects', 'argument' => $geometry], 51 | ['name' => 'crosses', 'argument' => $geometry], 52 | ['name' => 'within', 'argument' => $geometry], 53 | ['name' => 'contains', 'argument' => $geometry], 54 | ['name' => 'overlaps', 'argument' => $geometry], 55 | ['name' => 'covers', 'argument' => $geometry], 56 | ['name' => 'coveredBy', 'argument' => $geometry], 57 | ['name' => 'distance', 'argument' => $geometry], 58 | ['name' => 'hausdorffDistance', 'argument' => $geometry], 59 | ]; 60 | 61 | foreach ($geosMethods as $method) { 62 | $argument = null; 63 | $methodName = $method['name']; 64 | if (isset($method['argument'])) { 65 | $argument = $method['argument']; 66 | } 67 | $errorMessage = 'Failed on "' . $methodName . '" method with test file "' . $file . '"'; 68 | 69 | // GEOS don't like empty points 70 | if ($geometry->geometryType() == 'Point' && $geometry->isEmpty()) { 71 | continue; 72 | } 73 | 74 | switch ($methodName) { 75 | case 'geos': 76 | $this->assertInstanceOf('GEOSGeometry', $geometry->$methodName($argument), $errorMessage); 77 | break; 78 | case 'equals': 79 | case 'equalsExact': 80 | case 'disjoint': 81 | case 'touches': 82 | case 'intersects': 83 | case 'crosses': 84 | case 'within': 85 | case 'contains': 86 | case 'overlaps': 87 | case 'covers': 88 | case 'coveredBy': 89 | $this->assertIsBool($geometry->$methodName($argument), $errorMessage); 90 | break; 91 | case 'pointOnSurface': 92 | case 'buffer': 93 | case 'intersection': 94 | case 'convexHull': 95 | case 'difference': 96 | case 'symDifference': 97 | case 'union': 98 | case 'simplify': 99 | $this->assertInstanceOf(Geometry::class, $geometry->$methodName($argument), $errorMessage); 100 | break; 101 | case 'distance': 102 | case 'hausdorffDistance': 103 | $this->assertIsFloat($geometry->$methodName($argument), $errorMessage); 104 | break; 105 | case 'relate': 106 | $this->assertMatchesRegularExpression( 107 | '/[0-9TF]{9}/', 108 | $geometry->$methodName($argument), 109 | $errorMessage 110 | ); 111 | break; 112 | case 'checkValidity': 113 | $this->assertArrayHasKey('valid', $geometry->$methodName($argument), $errorMessage); 114 | break; 115 | case 'isSimple': 116 | if ($geometry->geometryType() == 'GeometryCollection') { 117 | $this->assertNull($geometry->$methodName($argument), $errorMessage); 118 | } else { 119 | $this->assertNotNull($geometry->$methodName($argument), $errorMessage); 120 | } 121 | break; 122 | default: 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | --------------------------------------------------------------------------------
Text is more readable and 10 | easier to write when you can avoid using entity 11 | references.