├── VERSION ├── pint.json ├── src ├── Geometries │ ├── Point.php │ ├── Polygon.php │ ├── LineString.php │ ├── MultiPoint.php │ ├── MultiPolygon.php │ ├── MultiLineString.php │ └── GeometryCollection.php ├── GeometryFacade.php ├── GeometryServiceProvider.php ├── Support │ ├── TypeMapper.php │ └── GeometryProxy.php └── Geometry.php ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── composer.json └── readme.md /VERSION: -------------------------------------------------------------------------------- 1 | 2.9.2 2 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "no_superfluous_phpdoc_tags": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Geometries/Point.php: -------------------------------------------------------------------------------- 1 | app->singleton('geometry', function ($app) { 32 | return $app->make(Geometry::class, ['geometry' => new geoPHP(), 'mapper' => new TypeMapper(), 'app' => $app]); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Support/TypeMapper.php: -------------------------------------------------------------------------------- 1 | 'ewkb', 19 | 'Ewkt' => 'ewkt', 20 | 'GeoHash' => 'geohash', 21 | 'GeoJson' => 'geojson', 22 | 'GeoRss' => 'georss', 23 | 'GoogleGeocode' => 'google_geocode', 24 | 'Gpx' => 'gpx', 25 | 'Json' => 'json', 26 | 'Kml' => 'kml', 27 | 'Wkb' => 'wkb', 28 | 'Wkt' => 'wkt', 29 | ]; 30 | 31 | /** 32 | * StudlyCase of the method name. 33 | * 34 | * Look it up in the types to make sure that it is defined & map it to the string that geoPHP expects. 35 | * 36 | * @throws InvalidArgumentException 37 | */ 38 | public function map(string $type): string 39 | { 40 | if (in_array($type, array_keys($this->types))) { 41 | return $this->types[$type]; 42 | } 43 | 44 | throw new InvalidArgumentException(sprintf('Unknown geometry type of [%s] was provided.', $type)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | php: [8.1, 8.2] 16 | stability: [prefer-lowest, prefer-stable] 17 | 18 | name: PHP ${{ matrix.php }} - ${{ matrix.stability }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.composer/cache 28 | key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | coverage: pcov 35 | 36 | - name: Install dependencies 37 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-suggest 38 | 39 | - name: PHP Security Checker 40 | uses: symfonycorp/security-checker-action@v4 41 | if: ${{ matrix.stability == 'prefer-stable' }} 42 | 43 | - name: Execute tests 44 | run: vendor/bin/phpunit --coverage-clover=coverage.clover --verbose 45 | 46 | - name: Upload Code Coverage 47 | run: wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spinen/laravel-geometry", 3 | "description": "Wrapper over the geoPHP Class to make it integrate with Laravel better.", 4 | "keywords": [ 5 | "geometry", 6 | "geoPHP", 7 | "laravel", 8 | "library", 9 | "spinen" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Thai-Son Le", 15 | "email": "thaison.le@spinen.com" 16 | }, 17 | { 18 | "name": "Jimmy Puckett", 19 | "email": "jimmy.puckett@spinen.com" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1", 24 | "illuminate/contracts": "~8|~9|~10|~11|^12.0", 25 | "illuminate/support": "~8|~9|~10|~11|^12.0", 26 | "phayes/geophp": "~1.2" 27 | }, 28 | "require-dev": { 29 | "laravel/pint": "^1.6", 30 | "mockery/mockery": "^1.5.1", 31 | "phpunit/phpunit": "^9.6.5", 32 | "psy/psysh": "^0.11.1|^0.12", 33 | "symfony/var-dumper": "^6.2|^7.2" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Spinen\\Geometry\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Spinen\\Geometry\\": "tests" 43 | } 44 | }, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "Spinen\\Geometry\\GeometryServiceProvider" 49 | ], 50 | "aliases": { 51 | "Geo": "Spinen\\Geometry\\GeometryFacade" 52 | } 53 | } 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "allow-plugins": { 58 | "symfony/thanks": false 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SPINEN's Laravel Geometry 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/spinen/laravel-geometry/v/stable)](https://packagist.org/packages/spinen/laravel-geometry) 4 | [![Total Downloads](https://poser.pugx.org/spinen/laravel-geometry/downloads)](https://packagist.org/packages/spinen/laravel-geometry) 5 | [![Latest Unstable Version](https://poser.pugx.org/spinen/laravel-geometry/v/unstable)](https://packagist.org/packages/spinen/laravel-geometry) 6 | [![License](https://poser.pugx.org/spinen/laravel-geometry/license)](https://packagist.org/packages/spinen/laravel-geometry) 7 | 8 | Wrapper over the geoPHP Class to make it integrate with Laravel better. 9 | 10 | ## Build Status 11 | 12 | | Branch | Status | Coverage | Code Quality | 13 | | ------ | :----: | :------: | :----------: | 14 | | Develop | [![Build Status](https://github.com/spinen/laravel-geometry/workflows/CI/badge.svg?branch=develop)](https://github.com/spinen/laravel-geometry/workflows/CI/badge.svg?branch=develop) | [![Code Coverage](https://scrutinizer-ci.com/g/spinen/laravel-geometry/badges/coverage.png?b=develop)](https://scrutinizer-ci.com/g/spinen/laravel-geometry/?branch=develop) | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/spinen/laravel-geometry/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/spinen/laravel-geometry/?branch=develop) | 15 | | Master | [![Build Status](https://github.com/spinen/laravel-geometry/workflows/CI/badge.svg?branch=master)](https://github.com/spinen/laravel-geometry/workflows/CI/badge.svg?branch=master) | [![Code Coverage](https://scrutinizer-ci.com/g/spinen/laravel-geometry/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/spinen/laravel-geometry/?branch=master) | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/spinen/laravel-geometry/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/spinen/laravel-geometry/?branch=master) | 16 | 17 | *** 18 | 19 | ## Prerequisite 20 | 21 | ### NOTES 22 | 23 | #### 1) If you need to use < php7.2, please stay with version 1.x 24 | 25 | #### 2) Aside from Laravel >= 5.5, the package below is required 26 | 27 | * [phayes/geophp](https://github.com/phayes/geoPHP) 28 | 29 | *** 30 | 31 | ## Install 32 | 33 | Install Geometry: 34 | 35 | ```bash 36 | $ composer require spinen/laravel-geometry 37 | ``` 38 | 39 | The package uses the auto registration feature. 40 | 41 | *** 42 | 43 | ## Using the package 44 | 45 | The Geometry Class exposes parseType methods where "Type" is StudlyCase of the geometry type that geoPHP supports. Here is a full list... 46 | 47 | * parseEwkb($geometry) 48 | * parseEwkt($geometry) 49 | * parseGeoHash($geometry) 50 | * parseGeoJson($geometry) 51 | * parseGeoRss($geometry) 52 | * parseGoogleGeocode($geometry) 53 | * parseGpx($geometry) 54 | * parseJson($geometry) 55 | * parseKml($geometry) 56 | * parseWkb($geometry) 57 | * parseWkt($geometry) 58 | 59 | The geometries are wrapped in a `Spinen\Geometry\Geometries` namespace with a little sugar to be able to do 60 | 61 | * toEwkb() 62 | * toEwkt() 63 | * toGeoHash() 64 | * toGeoJson() 65 | * toGeoRss() 66 | * toGoogleGeocode() 67 | * toGpx() 68 | * toJson() 69 | * toKml() 70 | * toWkb() 71 | * toWkt() 72 | 73 | In addition to the above export methods, we have added a ```toArray``` that gives an array from the toJson method. For convenience, we have exposed all of the properties of the geometry through a getter, so you have direct access to the property without having ask for the keys in the array. 74 | 75 | *** 76 | 77 | ## Area of the polygon 78 | 79 | We are estimating the area in meters squared & acres. We expect the estimation to be within 1%, so it is not very accurate. We essentially refactored a js method that Mapbox has in their [geojson-area package](https://github.com/mapbox/geojson-area/blob/v0.2.1/index.js#L55) . You get the area by calling the ```getAcres``` or ```getSquareMeters```. There is a shortcut to them as properties, so you can read the "acres" or "square_meters" property. 80 | 81 | *** 82 | 83 | ## Example 84 | 85 | ```php 86 | // Area of Polygon 87 | $points = [[1,1], [2,2], [3,2], [3,4]]; 88 | 89 | $geoJson = '{"type":"Polygon", "coordinates":[' . json_encode($points) . ']}'; 90 | 91 | $geo = new geoPHP(); 92 | $mapper = new Spinen\Geometry\Support\TypeMapper(); 93 | $geometry = new Spinen\Geometry\Geometry($geo, $mapper); 94 | 95 | $collection = $geometry->parseGeoJson($geoJson); // see above for more parse options 96 | 97 | $squareMeters = $collection->getSquareMeters(); 98 | $acres = $collection->getAcres(); 99 | ``` 100 | -------------------------------------------------------------------------------- /src/Geometry.php: -------------------------------------------------------------------------------- 1 | geoPhp = $geoPhp; 52 | $this->mapper = $mapper; 53 | $this->app = $app; 54 | } 55 | 56 | /** 57 | * Magic method to allow methods that are not specifically defined. 58 | * 59 | * Allow parseStudlyCaseOfType i.e. parseWkt or parseGeoJson to be called & mapped to the load method. 60 | * 61 | * @throws RuntimeException 62 | */ 63 | public function __call(string $name, array $arguments): mixed 64 | { 65 | // Sugar to make parse() work 66 | if (preg_match('/^parse([A-Z][A-z]*)/u', $name, $parts) && 1 === count($arguments)) { 67 | return $this->parse($arguments[0], $parts[1]); 68 | } 69 | 70 | throw new RuntimeException(sprintf('Call to undefined method %s::%s().', __CLASS__, $name)); 71 | } 72 | 73 | /** 74 | * Build the name to the proxy geometry class. 75 | * 76 | * @throws InvalidArgumentException|RuntimeException 77 | */ 78 | public function buildGeometryClassName(?GlobalGeometry $geometry): string 79 | { 80 | if (is_null($geometry)) { 81 | throw new InvalidArgumentException('The geometry object cannot be null when building the name to the proxy class.'); 82 | } 83 | 84 | $class = __NAMESPACE__.'\Geometries\\'.get_class($geometry); 85 | 86 | if (class_exists($class)) { 87 | return $class; 88 | } 89 | 90 | throw new RuntimeException(sprintf('There proxy class [%s] is not defined.', $class)); 91 | } 92 | 93 | /** 94 | * Call geoPHP to load the data. 95 | * 96 | * @throws InvalidArgumentException|Exception 97 | */ 98 | protected function loadGeometry(object|string $data, ?string $type): GlobalGeometry 99 | { 100 | $geometry = is_null($type) 101 | ? $this->geoPhp->load($data) 102 | : $this->geoPhp->load($data, $this->mapper->map($type)); 103 | 104 | if (! $geometry) { 105 | throw new InvalidArgumentException('Could not parse the supplied data.'); 106 | } 107 | 108 | return $geometry; 109 | } 110 | 111 | /** 112 | * Pass the data to geoPHP to convert to the correct geometry type. 113 | * 114 | * @throws InvalidArgumentException|Exception 115 | */ 116 | public function parse(object|string $data, ?string $type = null): GeometryProxy 117 | { 118 | $geometry = $this->loadGeometry($data, $type); 119 | 120 | if (is_null($geometry)) { 121 | throw new InvalidArgumentException('Could not parse the supplied data.'); 122 | } 123 | 124 | $geometry_class = $this->buildGeometryClassName($geometry); 125 | 126 | // If running in Laravel, then use the IoC 127 | return is_null($this->app) 128 | ? new $geometry_class($geometry, $this->mapper) 129 | : $this->app->make($geometry_class, ['geometry' => $geometry, 'mapper' => $this->mapper]); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Support/GeometryProxy.php: -------------------------------------------------------------------------------- 1 | geometry = $geometry; 59 | $this->mapper = $mapper; 60 | } 61 | 62 | /** 63 | * Magic method to allow methods that are not specifically defined. 64 | * 65 | * This is where we look to see if the class that we are proxing has the method that is being called, and if it 66 | * does, then pass the call to the class under proxy. If a method is defined in our class, then it gets called 67 | * first, so you can "extend" the classes by defining methods that overwrite the "parent" there. 68 | * 69 | * @throws InvalidArgumentException 70 | */ 71 | public function __call(string $name, array $arguments) 72 | { 73 | // Sugar to make to() work 74 | if (preg_match('/^to([A-Z][A-z]*)/u', $name, $parts) && 0 === count($arguments)) { 75 | return $this->geometry->out($this->mapper->map($parts[1])); 76 | } 77 | 78 | // Call the method on the class being proxied 79 | if (method_exists($this->geometry, $name)) { 80 | return call_user_func_array( 81 | [$this->geometry, $name], array_map([$this, 'exposeRawIfAvailable'], $arguments) 82 | ); 83 | } 84 | 85 | throw new RuntimeException(sprintf('Call to undefined method %s::%s().', __CLASS__, $name)); 86 | } 87 | 88 | /** 89 | * Expose the getters 90 | */ 91 | public function __get(string $name) 92 | { 93 | // Properties on the geometry 94 | if (isset($this->toArray()[$name])) { 95 | return $this->toArray()[$name]; 96 | } 97 | 98 | // Shortcut to the getters 99 | if (method_exists($this, 'get'.Str::studly($name))) { 100 | return $this->{'get'.Str::studly($name)}(); 101 | } 102 | 103 | throw new RuntimeException(sprintf('Undefined property: %s', $name)); 104 | } 105 | 106 | /** 107 | * If using the object as a string, just return the json. 108 | */ 109 | public function __toString(): string 110 | { 111 | return $this->toJson(); 112 | } 113 | 114 | /** 115 | * Figure out what index to use in the ringArea calculation 116 | */ 117 | private function determineCoordinateIndices(int $index, int $length): array 118 | { 119 | // i = N-2 120 | if ($index === ($length - 2)) { 121 | return [$length - 2, $length - 1, 0]; 122 | } 123 | 124 | // i = N-1 125 | if ($index === ($length - 1)) { 126 | return [$length - 1, 0, 1]; 127 | } 128 | 129 | // i = 0 to N-3 130 | return [$index, $index + 1, $index + 2]; 131 | } 132 | 133 | /** 134 | * If the object passed in has a getRawGeometry, call it 135 | */ 136 | protected function exposeRawIfAvailable($argument) 137 | { 138 | return ((\is_string($argument) || \is_object($argument)) && method_exists($argument, 'getRawGeometry')) 139 | ? $argument->getRawGeometry() 140 | : $argument; 141 | } 142 | 143 | /** 144 | * Calculate the acres 145 | */ 146 | public function getAcres(): float 147 | { 148 | return $this->square_meters * 0.000247105381; 149 | } 150 | 151 | /** 152 | * Expose the underlying Geometry object 153 | */ 154 | public function getRawGeometry(): GlobalGeometry 155 | { 156 | return $this->geometry; 157 | } 158 | 159 | /** 160 | * Calculate the square meters 161 | */ 162 | public function getSquareMeters(): float 163 | { 164 | if (! is_null($this->cached_area)) { 165 | return $this->cached_area; 166 | } 167 | 168 | $this->cached_area = 0.0; 169 | 170 | foreach ($this->coordinates as $coordinate) { 171 | $this->cached_area += $this->ringArea($coordinate); 172 | } 173 | 174 | return $this->cached_area; 175 | } 176 | 177 | /** 178 | * Convert degrees to radians 179 | * 180 | * I know that there is a built in function, but I read that it was very slow & to use this. 181 | */ 182 | private function radians(float|int $degrees): float 183 | { 184 | return $degrees * M_PI / 180; 185 | } 186 | 187 | /** 188 | * Estimate the area of a ring 189 | * 190 | * Calculate the approximate area of the polygon were it projected onto 191 | * the earth. Note that this area will be positive if ring is oriented 192 | * clockwise, otherwise it will be negative. 193 | * 194 | * Reference: 195 | * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for 196 | * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion 197 | * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409 198 | * 199 | * @see https://github.com/mapbox/geojson-area/blob/master/index.js#L55 200 | */ 201 | public function ringArea($coordinates): float 202 | { 203 | $area = 0.0; 204 | 205 | $length = count($coordinates); 206 | 207 | if ($length <= 2) { 208 | return $area; 209 | } 210 | 211 | for ($i = 0; $i < $length; $i++) { 212 | [$lower_index, $middle_index, $upper_index] = $this->determineCoordinateIndices($i, $length); 213 | 214 | $point1 = $coordinates[$lower_index]; 215 | $point2 = $coordinates[$middle_index]; 216 | $point3 = $coordinates[$upper_index]; 217 | 218 | $area += ($this->radians($point3[0]) - $this->radians($point1[0])) * sin($this->radians($point2[1])); 219 | } 220 | 221 | return $area * 6378137 * 6378137 / 2; 222 | } 223 | 224 | /** 225 | * Build array of the object 226 | * 227 | * Cache the result, so that we don't decode it on every call. 228 | */ 229 | public function toArray(): array 230 | { 231 | if (is_null($this->geometry_array)) { 232 | $this->geometry_array = (array) json_decode($this->toJson(), true); 233 | } 234 | 235 | return $this->geometry_array; 236 | } 237 | } 238 | --------------------------------------------------------------------------------