├── .github └── workflows │ ├── .editorconfig │ └── ci.yml ├── .gitattributes ├── art └── geohash_neighbor_points.png ├── .gitignore ├── src ├── Exception │ ├── ExceptionInterface.php │ ├── RuntimeException.php │ ├── NotConvergingException.php │ ├── InvalidArgumentException.php │ └── NotMatchingEllipsoidException.php ├── CoordinateCouple.php ├── CLI │ ├── Output │ │ └── ConsoleOutput.php │ ├── Formatter │ │ └── OutputFormatter.php │ ├── GeotoolsApplication.php │ └── Command │ │ ├── Geohash │ │ ├── Decode.php │ │ └── Encode.php │ │ ├── Convert │ │ ├── UTM.php │ │ ├── DM.php │ │ └── DMS.php │ │ ├── Vertex │ │ ├── FinalBearing.php │ │ ├── FinalCardinal.php │ │ ├── InitialBearing.php │ │ ├── InitialCardinal.php │ │ ├── Middle.php │ │ └── Destination.php │ │ ├── Distance │ │ ├── Flat.php │ │ ├── Vincenty.php │ │ ├── Haversine.php │ │ ├── GreatCircle.php │ │ └── All.php │ │ └── Geocoder │ │ ├── Command.php │ │ ├── Reverse.php │ │ └── Geocode.php ├── Polygon │ ├── PolygonInterface.php │ └── MultiPolygon.php ├── BoundingBox │ ├── BoundingBoxInterface.php │ └── BoundingBox.php ├── Distance │ ├── DistanceInterface.php │ └── Distance.php ├── Geohash │ ├── GeohashInterface.php │ ├── TenTen.php │ └── Geohash.php ├── Vertex │ ├── VertexInterface.php │ └── Vertex.php ├── Coordinate │ ├── CoordinateInterface.php │ ├── CoordinateCollection.php │ ├── Coordinate.php │ └── Ellipsoid.php ├── Batch │ ├── BatchResult.php │ ├── BatchInterface.php │ ├── BatchGeocoded.php │ └── Batch.php ├── GeometryInterface.php ├── Geotools.php ├── GeotoolsInterface.php ├── ArrayCollection.php ├── Convert │ ├── ConvertInterface.php │ └── Convert.php └── GeometryCollection.php ├── phpunit.xml.dist ├── CONTRIBUTING.md ├── LICENSE ├── composer.json ├── bin └── geotools └── CHANGELOG.md /.github/workflows/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.yml] 2 | indent_size = 2 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests export-ignore 2 | .scrutinizer.yml export-ignore 3 | .travis.yml export-ignore 4 | -------------------------------------------------------------------------------- /art/geohash_neighbor_points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thephpleague/geotools/HEAD/art/geohash_neighbor_points.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor/ 3 | composer.lock 4 | composer.phar 5 | phpunit.xml 6 | php-cs-fixer.phar 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Exception; 13 | 14 | /** 15 | * Exception interface 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | interface ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Exception; 13 | 14 | /** 15 | * RuntimeException class 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | class RuntimeException extends \RuntimeException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/NotConvergingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Exception; 13 | 14 | /** 15 | * NotConvergingException class 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | class NotConvergingException extends \RuntimeException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Exception; 13 | 14 | /** 15 | * InvalidArgumentException class 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/NotMatchingEllipsoidException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Exception; 13 | 14 | /** 15 | * NotMatchingEllipsoidException class 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | class NotMatchingEllipsoidException extends \InvalidArgumentException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src/ 6 | 7 | 8 | 9 | 10 | ./tests/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/CoordinateCouple.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | 16 | /** 17 | * Coordinate couple Trait 18 | * 19 | * @author Rémi San 20 | */ 21 | trait CoordinateCouple 22 | { 23 | /** 24 | * The origin coordinate. 25 | * 26 | * @var CoordinateInterface 27 | */ 28 | protected $from; 29 | 30 | /** 31 | * The destination coordinate. 32 | * 33 | * @var CoordinateInterface 34 | */ 35 | protected $to; 36 | } 37 | -------------------------------------------------------------------------------- /src/CLI/Output/ConsoleOutput.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Output; 13 | 14 | use League\Geotools\CLI\Formatter\OutputFormatter; 15 | use Symfony\Component\Console\Formatter\OutputFormatterInterface; 16 | 17 | /** 18 | * @author Antoine Corcy 19 | */ 20 | class ConsoleOutput extends \Symfony\Component\Console\Output\ConsoleOutput 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null) 26 | { 27 | parent::__construct($verbosity, $decorated, new OutputFormatter); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTING 2 | ============ 3 | 4 | Contributions are **welcome** and be fully **credited** <3 5 | 6 | **Geotools** will use the [Symfony2 Coding Standard](http://symfony.com/doc/current/contributing/code/standards.html). 7 | The easiest way to apply these conventions is to install [PHP_CodeSniffer](http://pear.php.net/package/PHP_CodeSniffer) 8 | and the [Opensky Symfony2 Coding Standard](https://github.com/opensky/Symfony2-coding-standard). 9 | 10 | You may be interested in [PHP Coding Standards Fixer](https://github.com/fabpot/PHP-CS-Fixer). 11 | 12 | Installation 13 | ------------ 14 | 15 | ``` bash 16 | $ pear install PHP_CodeSniffer 17 | $ cd `pear config-get php_dir`/PHP/CodeSniffer/Standards 18 | $ git clone git://github.com/opensky/Symfony2-coding-standard.git Symfony2 19 | $ phpcs --config-set default_standard Symfony2 20 | ``` 21 | 22 | Usage 23 | ----- 24 | 25 | ``` bash 26 | $ phpcs src/ 27 | ``` 28 | 29 | **Happy coding** ! 30 | -------------------------------------------------------------------------------- /src/CLI/Formatter/OutputFormatter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Formatter; 13 | 14 | use Symfony\Component\Console\Formatter\OutputFormatterStyle; 15 | 16 | /** 17 | * @author Antoine Corcy 18 | */ 19 | class OutputFormatter extends \Symfony\Component\Console\Formatter\OutputFormatter 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function __construct($decorated = false, array $styles = array()) 25 | { 26 | $this->setStyle('label', new OutputFormatterStyle('yellow', 'black')); 27 | $this->setStyle('value', new OutputFormatterStyle('green', 'black')); 28 | 29 | parent::__construct($decorated, $styles); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Polygon/PolygonInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Polygon; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | use League\Geotools\GeometryInterface; 16 | 17 | /** 18 | * @author Gabriel Bull 19 | */ 20 | interface PolygonInterface extends GeometryInterface 21 | { 22 | /** 23 | * @param CoordinateInterface $coordinate 24 | * @return boolean 25 | */ 26 | public function pointInPolygon(CoordinateInterface $coordinate); 27 | 28 | /** 29 | * @param CoordinateInterface $coordinate 30 | * @return boolean 31 | */ 32 | public function pointOnBoundary(CoordinateInterface $coordinate); 33 | 34 | /** 35 | * @param CoordinateInterface $coordinate 36 | * @return boolean 37 | */ 38 | public function pointOnVertex(CoordinateInterface $coordinate); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Antoine Corcy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/BoundingBox/BoundingBoxInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\BoundingBox; 13 | 14 | use League\Geotools\Polygon\PolygonInterface; 15 | 16 | /** 17 | * @author Gabriel Bull 18 | */ 19 | interface BoundingBoxInterface 20 | { 21 | /** 22 | * @return float|string|integer 23 | */ 24 | public function getNorth(); 25 | 26 | /** 27 | * @return float|string|integer 28 | */ 29 | public function getEast(); 30 | 31 | /** 32 | * @return float|string|integer 33 | */ 34 | public function getSouth(); 35 | 36 | /** 37 | * @return float|string|integer 38 | */ 39 | public function getWest(); 40 | 41 | /** 42 | * @return PolygonInterface 43 | */ 44 | public function getAsPolygon(); 45 | 46 | /** 47 | * @param BoundingBoxInterface $boundingBox 48 | * @return BoundingBoxInterface 49 | */ 50 | public function merge(BoundingBoxInterface $boundingBox); 51 | } 52 | -------------------------------------------------------------------------------- /src/CLI/GeotoolsApplication.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI; 13 | 14 | use League\Geotools\CLI\Output\ConsoleOutput; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Output\OutputInterface; 17 | use Symfony\Component\Console\Application; 18 | /** 19 | * @author Antoine Corcy 20 | */ 21 | class GeotoolsApplication extends Application 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private $logo = ' 27 | ________ __ .__ 28 | / _____/ ____ _____/ |_ ____ ____ | | ______ 29 | / \ ____/ __ \/ _ \ __\/ _ \ / _ \| | / ___/ 30 | \ \_\ \ ___( <_> ) | ( <_> | <_> ) |__\___ \ 31 | \______ /\___ >____/|__| \____/ \____/|____/____ > 32 | \/ \/ \/ 33 | 34 | '; 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getHelp(): string 40 | { 41 | return $this->logo . parent::getHelp(); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function run(?InputInterface $input = null, ?OutputInterface $output = null): int 48 | { 49 | return parent::run($input, new ConsoleOutput); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Distance/DistanceInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Distance; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | 16 | /** 17 | * Distance interface 18 | * 19 | * @author Antoine Corcy 20 | */ 21 | interface DistanceInterface 22 | { 23 | /** 24 | * Set the origin coordinate 25 | * 26 | * @param CoordinateInterface $from The origin coordinate 27 | * 28 | * @return DistanceInterface 29 | */ 30 | public function setFrom(CoordinateInterface $from); 31 | 32 | /** 33 | * Get the origin coordinate 34 | * 35 | * @return CoordinateInterface 36 | */ 37 | public function getFrom(); 38 | 39 | /** 40 | * Set the destination coordinate 41 | * 42 | * @param CoordinateInterface $to The destination coordinate 43 | * 44 | * @return DistanceInterface 45 | */ 46 | public function setTo(CoordinateInterface $to); 47 | 48 | /** 49 | * Get the destination coordinate 50 | * 51 | * @return CoordinateInterface 52 | */ 53 | public function getTo(); 54 | 55 | /** 56 | * Set the user unit 57 | * 58 | * @param string $unit Set the unit 59 | * 60 | * @return DistanceInterface 61 | */ 62 | public function in($unit); 63 | } 64 | -------------------------------------------------------------------------------- /src/CLI/Command/Geohash/Decode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Geohash; 13 | 14 | use League\Geotools\Geotools; 15 | use Symfony\Component\Console\Input\InputArgument; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | /** 20 | * Command-line geohash:decode class 21 | * 22 | * @author Antoine Corcy 23 | */ 24 | class Decode extends \Symfony\Component\Console\Command\Command 25 | { 26 | protected function configure() 27 | { 28 | $this 29 | ->setName('geohash:decode') 30 | ->setDescription('Decode a geo hash string to a coordinate') 31 | ->addArgument('geohash', InputArgument::REQUIRED, 'The geo hash to decode to coordinate') 32 | ->setHelp(<<Example: %command.full_name% spey61y 34 | EOT 35 | ); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $geotools = new Geotools; 41 | $coordinate = $geotools->geohash()->decode($input->getArgument('geohash'))->getCoordinate(); 42 | 43 | $output->writeln(sprintf( 44 | '%s, %s', 45 | $coordinate->getLatitude(), $coordinate->getLongitude() 46 | )); 47 | return 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Geohash/GeohashInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Geohash; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | use League\Geotools\Exception\InvalidArgumentException; 16 | use League\Geotools\Exception\RuntimeException; 17 | 18 | /** 19 | * Geohash interface 20 | * 21 | * @author Antoine Corcy 22 | */ 23 | interface GeohashInterface 24 | { 25 | /** 26 | * Returns a geo hash string. 27 | * 28 | * @param CoordinateInterface $coordinate The coordinate to encode. 29 | * @param int $length The length of the hash between 1 and 12 by default. 30 | * 31 | * @return GeohashInterface 32 | * 33 | * @throws InvalidArgumentException 34 | */ 35 | public function encode(CoordinateInterface $coordinate, $length): GeohashInterface; 36 | 37 | /** 38 | * Returns the decoded geo hash to it's center. 39 | * Note that the coordinate that you used to generate the geo hash may be 40 | * anywhere in the geo hash's bounding box and therefore you should not expect 41 | * them to be identical. 42 | * 43 | * @param string $geohash The geo hash string to decode. 44 | * 45 | * @return GeohashInterface 46 | * 47 | * @throws InvalidArgumentException 48 | * @throws RuntimeException 49 | */ 50 | public function decode($geohash): GeohashInterface; 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: [ '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] 14 | strategy: [ 'highest' ] 15 | sf_version: [''] 16 | include: 17 | - php: 7.3 18 | sf_version: '4.*' 19 | - php: 7.4 20 | strategy: 'lowest' 21 | - php: 8.0 22 | sf_version: '6.*' 23 | - php: 8.1 24 | sf_version: '6.*' 25 | - php: 8.2 26 | sf_version: '6.*' 27 | - php: 8.3 28 | sf_version: '7.*' 29 | - php: 8.4 30 | sf_version: '7.*' 31 | 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v3 35 | 36 | - name: Generate locales 37 | run: sudo apt-get update && sudo apt-get install tzdata locales -y && sudo locale-gen sv_SE && sudo locale-gen sv_SE.UTF-8 && sudo locale-gen en_US && sudo locale-gen en_US.UTF-8 38 | 39 | - name: Set up PHP 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: ${{ matrix.php }} 43 | coverage: none 44 | 45 | - name: Download dependencies 46 | uses: ramsey/composer-install@v2 47 | env: 48 | SYMFONY_REQUIRE: ${{ matrix.sf_version }} 49 | with: 50 | dependency-versions: ${{ matrix.strategy }} 51 | composer-options: --no-interaction --prefer-dist --optimize-autoloader 52 | 53 | - name: Run tests 54 | run: ./vendor/bin/phpunit 55 | -------------------------------------------------------------------------------- /src/Vertex/VertexInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Vertex; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | 16 | /** 17 | * Vertex interface 18 | * 19 | * @author Antoine Corcy 20 | */ 21 | interface VertexInterface 22 | { 23 | /** 24 | * Set the origin coordinate. 25 | * 26 | * @param CoordinateInterface $from The origin coordinate. 27 | * 28 | * @return VertexInterface 29 | */ 30 | public function setFrom(CoordinateInterface $from); 31 | 32 | /** 33 | * Get the origin coordinate. 34 | * 35 | * @return CoordinateInterface 36 | */ 37 | public function getFrom(); 38 | 39 | /** 40 | * Set the destination coordinate. 41 | * 42 | * @param CoordinateInterface $to The destination coordinate. 43 | * 44 | * @return VertexInterface 45 | */ 46 | public function setTo(CoordinateInterface $to); 47 | 48 | /** 49 | * Get the destination coordinate. 50 | * 51 | * @return CoordinateInterface 52 | */ 53 | public function getTo(); 54 | 55 | /** 56 | * Get the gradient (slope) of the vertex. 57 | * 58 | * @return integer 59 | */ 60 | public function getGradient(); 61 | 62 | /** 63 | * Get the ordinate (longitude) of the point where vertex intersects with the ordinate-axis (Prime-Meridian) of the coordinate system. 64 | * 65 | * @return integer 66 | */ 67 | public function getOrdinateIntercept(); 68 | } 69 | -------------------------------------------------------------------------------- /src/Polygon/MultiPolygon.php: -------------------------------------------------------------------------------- 1 | elements as $polygon) { 28 | if ($polygon->pointInPolygon($coordinate)) { 29 | return true; 30 | } 31 | } 32 | 33 | return false; 34 | } 35 | 36 | /** 37 | * @param CoordinateInterface $coordinate 38 | * @return boolean 39 | */ 40 | public function pointOnBoundary(CoordinateInterface $coordinate) 41 | { 42 | /** @var PolygonInterface $polygon */ 43 | foreach ($this->elements as $polygon) { 44 | if ($polygon->pointOnBoundary($coordinate)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * @param CoordinateInterface $coordinate 54 | * @return boolean 55 | */ 56 | public function pointOnVertex(CoordinateInterface $coordinate) 57 | { 58 | /** @var PolygonInterface $polygon */ 59 | foreach ($this->elements as $polygon) { 60 | if ($polygon->pointOnVertex($coordinate)) { 61 | return true; 62 | } 63 | } 64 | 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Coordinate/CoordinateInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Coordinate; 13 | 14 | /** 15 | * Coordinate Interface 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | interface CoordinateInterface 20 | { 21 | /** 22 | * Normalizes a latitude to the (-90, 90) range. 23 | * Latitudes below -90.0 or above 90.0 degrees are capped, not wrapped. 24 | * 25 | * @param double $latitude The latitude to normalize 26 | * 27 | * @return string 28 | */ 29 | public function normalizeLatitude($latitude); 30 | 31 | /** 32 | * Normalizes a longitude to the (-180, 180) range. 33 | * Longitudes below -180.0 or abode 180.0 degrees are wrapped. 34 | * 35 | * @param double $longitude The longitude to normalize 36 | * 37 | * @return string 38 | */ 39 | public function normalizeLongitude($longitude); 40 | 41 | /** 42 | * Set the latitude. 43 | * 44 | * @param double $latitude 45 | */ 46 | public function setLatitude($latitude); 47 | 48 | /** 49 | * Get the latitude. 50 | * 51 | * @return string 52 | */ 53 | public function getLatitude(); 54 | 55 | /** 56 | * Set the longitude. 57 | * 58 | * @param double $longitude 59 | */ 60 | public function setLongitude($longitude); 61 | 62 | /** 63 | * Get the longitude. 64 | * 65 | * @return string 66 | */ 67 | public function getLongitude(); 68 | 69 | /** 70 | * Get the Ellipsoid. 71 | * 72 | * @return Ellipsoid 73 | */ 74 | public function getEllipsoid(); 75 | } 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "league/geotools", 3 | "type" : "library", 4 | "description" : "Geo-related tools PHP 7.3+ library", 5 | "keywords" : ["geotools", "geometry", "geocoder", "geocoding", "geoip", "bounds", "distance", "batch", "async", "geohash", "geolocation", "latlong", "latitude", "longitude"], 6 | "license" : "MIT", 7 | 8 | "authors" : [{ 9 | "name" : "Antoine Corcy", 10 | "email" : "contact@sbin.dk", 11 | "homepage" : "http://sbin.dk", 12 | "role" : "Developer" 13 | }], 14 | 15 | "support" : { 16 | "issues" : "https://github.com/thephpleague/geotools/issues", 17 | "source" : "https://github.com/thephpleague/geotools" 18 | }, 19 | 20 | "replace" : { 21 | "toin0u/geotools" : "*" 22 | }, 23 | 24 | "require" : { 25 | "php" : "^7.3 || ^7.4 || ^8.0", 26 | "psr/cache" : "^1.0 || ^2.0 || ^3.0", 27 | "willdurand/geocoder" : "^4.5 || ^5.0", 28 | "react/promise" : "^2.8 || ^3.0", 29 | "symfony/console" : "^4.4 || ^5.0 || ^6.0 || ^7.0", 30 | "symfony/property-access" : "^4.4 || ^5.0 || ^6.0 || ^7.0", 31 | "symfony/serializer" : "^4.4 || ^5.0 || ^6.0 || ^7.0", 32 | "react/event-loop" : "^1.0", 33 | "php-http/discovery" : "^1.0", 34 | "ext-bcmath": "*", 35 | "ext-json": "*" 36 | }, 37 | 38 | "require-dev" : { 39 | "phpunit/phpunit" : "^9.5", 40 | "cache/array-adapter" : "^1.0" 41 | }, 42 | 43 | "autoload" : { 44 | "psr-4" : { 45 | "League\\Geotools\\" : "src" 46 | } 47 | }, 48 | 49 | "autoload-dev" : { 50 | "psr-4" : { 51 | "League\\Geotools\\Tests\\" : "tests" 52 | } 53 | }, 54 | 55 | "bin" : ["bin/geotools"], 56 | "config": { 57 | "allow-plugins": { 58 | "php-http/discovery": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Batch/BatchResult.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Batch; 13 | 14 | use Geocoder\Location; 15 | 16 | /** 17 | * BatchResult class 18 | * 19 | * @author Antoine Corcy 20 | */ 21 | class BatchResult 22 | { 23 | /** 24 | * The name of the provider. 25 | * 26 | * @var string 27 | */ 28 | protected $providerName; 29 | 30 | /** 31 | * The query. 32 | * 33 | * @var string 34 | */ 35 | protected $query; 36 | 37 | /** 38 | * The exception message. 39 | * 40 | * @var string 41 | */ 42 | protected $exception; 43 | 44 | 45 | /** 46 | * Construct a Geocoded object with the provider name, its query and exception if any. 47 | * 48 | * @param string $providerName The name of the provider. 49 | * @param string $query The query. 50 | * @param string $exception The exception message if any. 51 | */ 52 | public function __construct($providerName, $query, $exception = '') 53 | { 54 | $this->providerName = $providerName; 55 | $this->query = $query; 56 | $this->exception = $exception; 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | public function createFromAddress(Location $address) 63 | { 64 | $result = $this->newInstance(); 65 | $result->setAddress($address); 66 | 67 | return $result; 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function newInstance() 74 | { 75 | $batchGeocoded = new BatchGeocoded; 76 | 77 | $batchGeocoded->setProviderName($this->providerName); 78 | $batchGeocoded->setQuery($this->query); 79 | $batchGeocoded->setExceptionMessage($this->exception); 80 | 81 | return $batchGeocoded; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/CLI/Command/Geohash/Encode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Geohash; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Geohash\Geohash; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line geohash:encode class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Encode extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $this 32 | ->setName('geohash:encode') 33 | ->setDescription('Encode a coordinate to a geo hash string, the length is 12 by default') 34 | ->addArgument('coordinate', InputArgument::REQUIRED, 'The "Lat,Long" coordinate to encode') 35 | ->addOption('length', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_OPTIONAL, 36 | sprintf('If set, the length between %s and %s of the encoded coordinate', Geohash::MIN_LENGTH, 37 | Geohash::MAX_LENGTH), 12) 38 | ->setHelp(<<Example: %command.full_name% "40° 26.7717, -79° 56.93172" --length=3 40 | EOT 41 | ); 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output): int 45 | { 46 | $coordinate = new Coordinate($input->getArgument('coordinate')); 47 | $geotools = new Geotools; 48 | 49 | $output->writeln(sprintf( 50 | '%s', 51 | $geotools->geohash()->encode($coordinate, $input->getOption('length'))->getGeohash() 52 | )); 53 | return 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Batch/BatchInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Batch; 13 | 14 | use Geocoder\Collection; 15 | use League\Geotools\Exception\InvalidArgumentException; 16 | use Psr\Cache\CacheItemPoolInterface; 17 | 18 | /** 19 | * Batch interface 20 | * 21 | * @author Antoine Corcy 22 | */ 23 | interface BatchInterface 24 | { 25 | /** 26 | * Set an array of closures to geocode. 27 | * If a provider throws an exception it will return an empty AddressCollection. 28 | * 29 | * @param string|array $values A value or an array of values to geocode. 30 | * 31 | * @return BatchInterface 32 | * 33 | * @throws InvalidArgumentException 34 | */ 35 | public function geocode($values); 36 | 37 | /** 38 | * Set an array of closures to reverse geocode. 39 | * If a provider throws an exception it will return an empty AddressCollection. 40 | * 41 | * @param \League\Geotools\Coordinate\CoordinateInterface|array $coordinates A coordinate or an array of coordinates to reverse. 42 | * 43 | * @return BatchInterface 44 | * 45 | * @throws InvalidArgumentException 46 | */ 47 | public function reverse($coordinates); 48 | 49 | /** 50 | * Returns an array of AddressCollection processed in serie. 51 | * 52 | * @return Collection[] 53 | * 54 | * @throws \Exception 55 | */ 56 | public function serie(); 57 | 58 | /** 59 | * Returns an array of AddressCollection processed in parallel. 60 | * 61 | * @return Collection[] 62 | * 63 | * @throws \Exception 64 | */ 65 | public function parallel(); 66 | 67 | /** 68 | * Set the cache object to use. 69 | * 70 | * @param CacheItemPoolInterface $cache The cache object to use. 71 | * 72 | * @return BatchInterface 73 | */ 74 | public function setCache(CacheItemPoolInterface $cache); 75 | } 76 | -------------------------------------------------------------------------------- /src/Geohash/TenTen.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Geohash; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | 16 | /** 17 | * TenTen class 18 | * 19 | * @see http://blog.jgc.org/2006/07/simple-code-for-entering-latitude-and.html 20 | * @see http://blog.jgc.org/2010/06/1010-code.html 21 | * 22 | * @author Antoine Corcy 23 | */ 24 | class TenTen 25 | { 26 | /** 27 | * The alphabet base. 28 | * 29 | * @var integer 30 | */ 31 | const BASE = 29; 32 | 33 | /** 34 | * The used alphabet. 35 | * 36 | * @var string 37 | */ 38 | private $alphabet = 'ABCDEFGHJKMNPQRVWXY0123456789'; 39 | 40 | /** 41 | * Encode the coordinate via the 10:10 algorithm. 42 | * 43 | * @param CoordinateInterface $coordinate The coordinate to encode. 44 | * @return string 45 | */ 46 | public function encode(CoordinateInterface $coordinate) 47 | { 48 | $latitude = floor(($coordinate->getLatitude() + 90.0) * 10000.0); 49 | $longitude = floor(($coordinate->getLongitude() + 180.0) * 10000.0); 50 | 51 | $position = $latitude * 3600000.0 + $longitude; 52 | $ttNumber = $position * self::BASE; 53 | $checkDigit = 0; 54 | 55 | for ($i = 1; $i < 10; ++$i) { 56 | $checkDigit += ($position % self::BASE) * $i; 57 | $position = floor($position / self::BASE); 58 | } 59 | 60 | $checkDigit %= self::BASE; 61 | 62 | $ttNumber += $checkDigit; 63 | $ttNumber = floor($ttNumber); 64 | 65 | $tt = ''; 66 | for ($i = 0; $i < 10; ++$i) { 67 | $digit = $ttNumber % self::BASE; 68 | if ($i === 4 || $i === 7) { 69 | $tt = ' ' . $tt; 70 | } 71 | $tt = $this->alphabet[$digit] . $tt; 72 | 73 | $ttNumber = floor($ttNumber / self::BASE); 74 | } 75 | 76 | return $tt; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /bin/geotools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | function includeIfExists($file) 14 | { 15 | if (file_exists($file)) { 16 | return include $file; 17 | } 18 | } 19 | 20 | if (!extension_loaded('curl') || !function_exists('curl_init')) { 21 | die(<<setName('Geotools :: Geo-related tools PHP library'); 50 | $console->add(new Convert\DM); 51 | $console->add(new Convert\DMS); 52 | $console->add(new Convert\UTM); 53 | $console->add(new Distance\All); 54 | $console->add(new Distance\Flat); 55 | $console->add(new Distance\GreatCircle); 56 | $console->add(new Distance\Haversine); 57 | $console->add(new Distance\Vincenty); 58 | $console->add(new Geocoder\Geocode); 59 | $console->add(new Geocoder\Reverse); 60 | $console->add(new Geohash\Decode); 61 | $console->add(new Geohash\Encode); 62 | $console->add(new Vertex\Destination); 63 | $console->add(new Vertex\FinalBearing); 64 | $console->add(new Vertex\FinalCardinal); 65 | $console->add(new Vertex\InitialBearing); 66 | $console->add(new Vertex\InitialCardinal); 67 | $console->add(new Vertex\Middle); 68 | $console->run(); 69 | -------------------------------------------------------------------------------- /src/CLI/Command/Convert/UTM.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Convert; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line convert:utm class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class UTM extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('convert:utm') 35 | ->setDescription('Convert decimal degrees coordinates in the Universal Transverse Mercator projection') 36 | ->addArgument('coordinate', InputArgument::REQUIRED, 'The "Lat,Long" coordinate') 37 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 38 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 39 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 41 | 42 | Example with CLARKE_1866 ellipsoid: 43 | 44 | %command.full_name% "40.446195, -79.948862" --ellipsoid=CLARKE_1866 45 | EOT 46 | ); 47 | } 48 | 49 | protected function execute(InputInterface $input, OutputInterface $output): int 50 | { 51 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 52 | $coordinate = new Coordinate($input->getArgument('coordinate'), $ellipsoid); 53 | $geotools = new Geotools; 54 | 55 | $output->writeln(sprintf( 56 | '%s', 57 | $geotools->convert($coordinate)->toUniversalTransverseMercator() 58 | )); 59 | return 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/GeometryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools; 13 | 14 | use League\Geotools\BoundingBox\BoundingBoxInterface; 15 | use League\Geotools\Coordinate\Coordinate; 16 | use League\Geotools\Coordinate\CoordinateCollection; 17 | use League\Geotools\Coordinate\Ellipsoid; 18 | 19 | /** 20 | * @author Rémi San 21 | */ 22 | interface GeometryInterface 23 | { 24 | /** 25 | * Returns the geometry type. 26 | * 27 | * @return string 28 | */ 29 | public function getGeometryType(); 30 | 31 | /** 32 | * Returns the ellipsoid of the geometry. 33 | * 34 | * @return Ellipsoid 35 | */ 36 | public function getEllipsoid(); 37 | 38 | /** 39 | * Returns the precision of the geometry. 40 | * 41 | * @return integer 42 | */ 43 | public function getPrecision(); 44 | 45 | /** 46 | * Returns a vertex of this Geometry (usually, but not necessarily, the first one). 47 | * The returned coordinate should not be assumed to be an actual Coordinate object used in 48 | * the internal representation. 49 | * 50 | * @return Coordinate if there's a coordinate in the collection 51 | * @return null if this Geometry is empty 52 | */ 53 | public function getCoordinate(); 54 | 55 | /** 56 | * Returns a collection containing the values of all the vertices for this geometry. 57 | * If the geometry is a composite, the array will contain all the vertices 58 | * for the components, in the order in which the components occur in the geometry. 59 | * 60 | *@return CoordinateCollection the vertices of this Geometry 61 | */ 62 | public function getCoordinates(); 63 | 64 | /** 65 | * Returns true if the geometry is empty. 66 | * 67 | * @return boolean 68 | */ 69 | public function isEmpty(); 70 | 71 | /** 72 | * Returns the bounding box of the Geometry 73 | * 74 | * @return BoundingBoxInterface 75 | */ 76 | public function getBoundingBox(); 77 | } 78 | -------------------------------------------------------------------------------- /src/Geotools.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | use League\Geotools\Distance\Distance; 16 | use League\Geotools\Vertex\Vertex; 17 | use League\Geotools\Batch\Batch; 18 | use League\Geotools\Geohash\Geohash; 19 | use League\Geotools\Convert\Convert; 20 | use Geocoder\Geocoder; 21 | 22 | /** 23 | * Geotools class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Geotools implements GeotoolsInterface 28 | { 29 | /** 30 | * The cardinal points / directions (the four cardinal directions, 31 | * the four ordinal directions, plus eight further divisions). 32 | * 33 | * @var array 34 | */ 35 | public static $cardinalPoints = array( 36 | 'N', 'NNE', 'NE', 'ENE', 37 | 'E', 'ESE', 'SE', 'SSE', 38 | 'S', 'SSW', 'SW', 'WSW', 39 | 'W', 'WNW', 'NW', 'NNW', 40 | 'N' 41 | ); 42 | 43 | /** 44 | * Latitude bands in the UTM coordinate system. 45 | * @see http://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system 46 | * 47 | * @var array 48 | */ 49 | public static $latitudeBands = array( 50 | 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'X' 51 | ); 52 | 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | public function distance() 58 | { 59 | return new Distance; 60 | } 61 | 62 | /** 63 | * {@inheritDoc} 64 | */ 65 | public function vertex() 66 | { 67 | return new Vertex; 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | public function batch(Geocoder $geocoder) 74 | { 75 | return new Batch($geocoder); 76 | } 77 | 78 | /** 79 | * {@inheritDoc} 80 | */ 81 | public function geohash() 82 | { 83 | return new Geohash; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public function convert(CoordinateInterface $coordinates) 90 | { 91 | return new Convert($coordinates); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/CLI/Command/Vertex/FinalBearing.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line vertex:final-bearing class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class FinalBearing extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('vertex:final-bearing') 35 | ->setDescription('Compute the final bearing in degrees between 2 coordinates') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 40 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 42 | 43 | Example with GRS_1980 ellipsoid: 44 | 45 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=GRS_1980 46 | EOT 47 | ); 48 | } 49 | 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 53 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 54 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 55 | 56 | $geotools = new Geotools; 57 | 58 | $output->writeln(sprintf( 59 | '%s', 60 | $geotools->vertex()->setFrom($from)->setTo($to)->finalBearing() 61 | )); 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CLI/Command/Vertex/FinalCardinal.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line vertex:final-cardinal class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class FinalCardinal extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('vertex:final-cardinal') 35 | ->setDescription('Compute the final cardinal point (direction) between 2 coordinates') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 40 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 42 | 43 | Example with GRS_1967 ellipsoid: 44 | 45 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=GRS_1967 46 | EOT 47 | ); 48 | } 49 | 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 53 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 54 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 55 | 56 | $geotools = new Geotools; 57 | 58 | $output->writeln(sprintf( 59 | '%s', 60 | $geotools->vertex()->setFrom($from)->setTo($to)->finalCardinal() 61 | )); 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CLI/Command/Vertex/InitialBearing.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line vertex:initial-bearing class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class InitialBearing extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('vertex:initial-bearing') 35 | ->setDescription('Compute the initial bearing in degrees between 2 coordinates') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 40 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 42 | 43 | Example with FISCHER_1968 ellipsoid: 44 | 45 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=FISCHER_1968 46 | EOT 47 | ); 48 | } 49 | 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 53 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 54 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 55 | 56 | $geotools = new Geotools; 57 | 58 | $output->writeln(sprintf( 59 | '%s', 60 | $geotools->vertex()->setFrom($from)->setTo($to)->initialBearing() 61 | )); 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CLI/Command/Vertex/InitialCardinal.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line vertex:initial-cardinal class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class InitialCardinal extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('vertex:initial-cardinal') 35 | ->setDescription('Compute the initial cardinal point (direction) between 2 coordinates') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 40 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 42 | 43 | Example with INTERNATIONAL ellipsoid: 44 | 45 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=INTERNATIONAL 46 | EOT 47 | ); 48 | } 49 | 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 53 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 54 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 55 | 56 | $geotools = new Geotools; 57 | 58 | $output->writeln(sprintf( 59 | '%s', 60 | $geotools->vertex()->setFrom($from)->setTo($to)->initialCardinal() 61 | )); 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CLI/Command/Vertex/Middle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line vertex:middle class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Middle extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('vertex:middle') 35 | ->setDescription('Compute the half-way coordinate between 2 coordinates') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 40 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 42 | 43 | Example with KRASSOVSKY ellipsoid: 44 | 45 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=KRASSOVSKY 46 | EOT 47 | ); 48 | } 49 | 50 | protected function execute(InputInterface $input, OutputInterface $output): int 51 | { 52 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 53 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 54 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 55 | 56 | $geotools = new Geotools; 57 | $middle = $geotools->vertex()->setFrom($from)->setTo($to)->middle(); 58 | 59 | $output->writeln(sprintf( 60 | '%s, %s', 61 | $middle->getLatitude(), $middle->getLongitude() 62 | )); 63 | return 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CLI/Command/Convert/DM.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Convert; 13 | 14 | use League\Geotools\Convert\ConvertInterface; 15 | use League\Geotools\Coordinate\Coordinate; 16 | use League\Geotools\Coordinate\Ellipsoid; 17 | use League\Geotools\Geotools; 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Input\InputOption; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | 23 | /** 24 | * Command-line convert:dm class 25 | * 26 | * @author Antoine Corcy 27 | */ 28 | class DM extends \Symfony\Component\Console\Command\Command 29 | { 30 | protected function configure() 31 | { 32 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 33 | 34 | $this 35 | ->setName('convert:dm') 36 | ->setDescription('Convert and format decimal degrees coordinates to decimal minutes coordinate') 37 | ->addArgument('coordinate', InputArgument::REQUIRED, 'The "Lat,Long" coordinate') 38 | ->addOption('format', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the format of the converted decimal minutes coordinate', ConvertInterface::DEFAULT_DM_FORMAT) 40 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 41 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 42 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 44 | 45 | Example with an output format: 46 | 47 | %command.full_name% "40.446195, -79.948862" --format="%P%D°%N %p%d°%n" 48 | 49 | Example with FISCHER_1968 ellipsoid: 50 | 51 | %command.full_name% "40.446195, -79.948862" --ellipsoid=FISCHER_1968 52 | EOT 53 | ); 54 | } 55 | 56 | protected function execute(InputInterface $input, OutputInterface $output): int 57 | { 58 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 59 | $coordinate = new Coordinate($input->getArgument('coordinate'), $ellipsoid); 60 | $geotools = new Geotools; 61 | 62 | $output->writeln(sprintf( 63 | '%s', 64 | $geotools->convert($coordinate)->toDecimalMinutes($input->getOption('format')) 65 | )); 66 | return 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/CLI/Command/Vertex/Destination.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line point:destination class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Destination extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('vertex:destination') 35 | ->setDescription('Compute the destination coordinate with given bearing in degrees and a distance in meters') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('bearing', InputArgument::REQUIRED, 'The initial bearing in degrees') 38 | ->addArgument('distance', InputArgument::REQUIRED, 'The distance from the origin coordinate in meters') 39 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 40 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 41 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 43 | 44 | Example with SOUTH_AMERICAN_1969 ellipsoid, 25 degrees and 10000 meters: 45 | 46 | %command.full_name% "40° 26.7717, -79° 56.93172" 25 10000 --ellipsoid=SOUTH_AMERICAN_1969 47 | EOT 48 | ); 49 | } 50 | 51 | protected function execute(InputInterface $input, OutputInterface $output): int 52 | { 53 | $from = new Coordinate($input->getArgument('origin'), Ellipsoid::createFromName($input->getOption('ellipsoid'))); 54 | $geotools = new Geotools; 55 | 56 | $destination = $geotools->vertex()->setFrom($from); 57 | $destination = $destination->destination($input->getArgument('bearing'), $input->getArgument('distance')); 58 | 59 | $output->writeln(sprintf( 60 | '%s, %s', 61 | $destination->getLatitude(), $destination->getLongitude() 62 | )); 63 | return 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/CLI/Command/Convert/DMS.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Convert; 13 | 14 | use League\Geotools\Convert\ConvertInterface; 15 | use League\Geotools\Coordinate\Coordinate; 16 | use League\Geotools\Coordinate\Ellipsoid; 17 | use League\Geotools\Geotools; 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Input\InputOption; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | 23 | /** 24 | * Command-line convert:dms class 25 | * 26 | * @author Antoine Corcy 27 | */ 28 | class DMS extends \Symfony\Component\Console\Command\Command 29 | { 30 | protected function configure() 31 | { 32 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 33 | 34 | $this 35 | ->setName('convert:dms') 36 | ->setDescription('Convert and format decimal degrees coordinates to degrees minutes seconds coordinate') 37 | ->addArgument('coordinate', InputArgument::REQUIRED, 'The "Lat,Long" coordinate') 38 | ->addOption('format', null, InputOption::VALUE_REQUIRED, 39 | 'If set, the format of the converted degrees minutes seconds coordinate', 40 | ConvertInterface::DEFAULT_DMS_FORMAT) 41 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 42 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 43 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 45 | 46 | Example with an output format: 47 | 48 | %command.full_name% "40.446195, -79.948862" --format="%P%D:%M:%S, %p%d:%m:%s" 49 | 50 | Example with BESSEL_1841 ellipsoid: 51 | 52 | %command.full_name% "40.446195, -79.948862" --ellipsoid=BESSEL_1841 53 | EOT 54 | ); 55 | } 56 | 57 | protected function execute(InputInterface $input, OutputInterface $output): int 58 | { 59 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 60 | $coordinate = new Coordinate($input->getArgument('coordinate'), $ellipsoid); 61 | $geotools = new Geotools; 62 | 63 | $output->writeln(sprintf( 64 | '%s', 65 | $geotools->convert($coordinate)->toDegreesMinutesSeconds($input->getOption('format')) 66 | )); 67 | return 0; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/GeotoolsInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools; 13 | 14 | use Geocoder\Geocoder as GeocoderInterface; 15 | use League\Geotools\Batch\BatchInterface; 16 | use League\Geotools\Convert\ConvertInterface; 17 | use League\Geotools\Coordinate\CoordinateInterface; 18 | use League\Geotools\Vertex\VertexInterface; 19 | use League\Geotools\Distance\DistanceInterface; 20 | use League\Geotools\Geohash\GeohashInterface; 21 | 22 | /** 23 | * Geotools interface 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | interface GeotoolsInterface 28 | { 29 | /** 30 | * Transverse Mercator is not the same as UTM. 31 | * A scale factor is required to convert between them. 32 | * 33 | * @var double 34 | */ 35 | const UTM_SCALE_FACTOR = 0.9996; 36 | 37 | /** 38 | * The ratio meters per mile. 39 | * 40 | * @var double 41 | */ 42 | const METERS_PER_MILE = 1609.344; 43 | 44 | /** 45 | * The ratio feet per meter. 46 | * 47 | * @var double 48 | */ 49 | const FEET_PER_METER = 0.3048; 50 | 51 | /** 52 | * The kilometer unit. 53 | * 54 | * @var string 55 | */ 56 | const KILOMETER_UNIT = 'km'; 57 | 58 | /** 59 | * The mile unit. 60 | * 61 | * @var string 62 | */ 63 | const MILE_UNIT = 'mi'; 64 | 65 | /** 66 | * The feet unit. 67 | * 68 | * @var string 69 | */ 70 | const FOOT_UNIT = 'ft'; 71 | 72 | /** 73 | * Returns an instance of Distance. 74 | * 75 | * @return DistanceInterface 76 | */ 77 | public function distance(); 78 | 79 | /** 80 | * Returns an instance of Vertex. 81 | * 82 | * @return VertexInterface 83 | */ 84 | public function vertex(); 85 | 86 | /** 87 | * Returns an instance of Batch. 88 | * 89 | * @param GeocoderInterface $geocoder The Geocoder instance to use. 90 | * 91 | * @return BatchInterface 92 | */ 93 | public function batch(GeocoderInterface $geocoder); 94 | 95 | /** 96 | * Returns an instance of Geohash. 97 | * 98 | * @return GeohashInterface 99 | */ 100 | public function geohash(); 101 | 102 | /** 103 | * Returns an instance of Convert. 104 | * 105 | * @param CoordinateInterface $coordinates The coordinates to convert. 106 | * 107 | * @return ConvertInterface 108 | */ 109 | public function convert(CoordinateInterface $coordinates); 110 | } 111 | -------------------------------------------------------------------------------- /src/Coordinate/CoordinateCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Coordinate; 13 | 14 | use League\Geotools\ArrayCollection; 15 | use League\Geotools\Exception\InvalidArgumentException; 16 | 17 | /** 18 | * @author Gabriel Bull 19 | */ 20 | class CoordinateCollection extends ArrayCollection 21 | { 22 | /** 23 | * @var Ellipsoid 24 | */ 25 | private $ellipsoid; 26 | 27 | /** 28 | * CoordinateCollection constructor. 29 | * 30 | * @param CoordinateInterface[] $coordinates 31 | * @param ?Ellipsoid $ellipsoid 32 | */ 33 | public function __construct(array $coordinates = array(), ?Ellipsoid $ellipsoid = null) 34 | { 35 | if ($ellipsoid) { 36 | $this->ellipsoid = $ellipsoid; 37 | } elseif (count($coordinates) > 0) { 38 | $this->ellipsoid = reset($coordinates)->getEllipsoid(); 39 | } 40 | 41 | $this->checkCoordinatesArray($coordinates); 42 | 43 | parent::__construct($coordinates); 44 | } 45 | 46 | /** 47 | * @param string $key 48 | * @param CoordinateInterface $value 49 | */ 50 | public function set($key, $value) 51 | { 52 | $this->checkEllipsoid($value); 53 | $this->elements[$key] = $value; 54 | } 55 | 56 | /** 57 | * @param CoordinateInterface $value 58 | * @return bool 59 | */ 60 | public function add($value) 61 | { 62 | $this->checkEllipsoid($value); 63 | $this->elements[] = $value; 64 | 65 | return true; 66 | } 67 | 68 | /** 69 | * @return Ellipsoid 70 | */ 71 | public function getEllipsoid() 72 | { 73 | return $this->ellipsoid; 74 | } 75 | 76 | /** 77 | * @param array CoordinateInterface[] $coordinates 78 | */ 79 | private function checkCoordinatesArray(array $coordinates) 80 | { 81 | foreach ($coordinates as $coordinate) { 82 | $this->checkEllipsoid($coordinate); 83 | } 84 | } 85 | 86 | /** 87 | * @param CoordinateInterface $coordinate 88 | * 89 | * @throws InvalidArgumentException 90 | */ 91 | private function checkEllipsoid(CoordinateInterface $coordinate) 92 | { 93 | if ($this->ellipsoid === null) { 94 | $this->ellipsoid = $coordinate->getEllipsoid(); 95 | } 96 | 97 | if ($coordinate->getEllipsoid() != $this->ellipsoid) { 98 | throw new InvalidArgumentException("Ellipsoids don't match"); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/CLI/Command/Distance/Flat.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Distance; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line distance:flat class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Flat extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('distance:flat') 35 | ->setDescription('Compute the distance between 2 coordinates using the flat algorithm, in meters by default') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('km', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in kilometers') 39 | ->addOption('mi', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in miles') 40 | ->addOption('ft', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in feet') 41 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 42 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 43 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 45 | 46 | Example with WGS60 ellipsoid and output in kilometers: 47 | 48 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=WGS60 --km 49 | EOT 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 56 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 57 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 58 | 59 | $geotools = new Geotools; 60 | $distance = $geotools->distance()->setFrom($from)->setTo($to); 61 | 62 | if ($input->getOption('km')) { 63 | $distance->in('km'); 64 | } 65 | 66 | if ($input->getOption('mi')) { 67 | $distance->in('mi'); 68 | } 69 | 70 | if ($input->getOption('ft')) { 71 | $distance->in('ft'); 72 | } 73 | 74 | $output->writeln(sprintf('%s', $distance->flat())); 75 | return 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CLI/Command/Distance/Vincenty.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Distance; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line distance:vincenty class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Vincenty extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('distance:vincenty') 35 | ->setDescription('Compute the distance between 2 coordinates using the vincenty algorithm, in meters by default') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('km', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in kilometers') 39 | ->addOption('mi', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in miles') 40 | ->addOption('ft', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in feet') 41 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 42 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 43 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 45 | 46 | Example with WGS72 ellipsoid and output in miles: 47 | 48 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=WGS72 --mi 49 | EOT 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 56 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 57 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 58 | 59 | $geotools = new Geotools; 60 | $distance = $geotools->distance()->setFrom($from)->setTo($to); 61 | 62 | if ($input->getOption('km')) { 63 | $distance->in('km'); 64 | } 65 | 66 | if ($input->getOption('mi')) { 67 | $distance->in('mi'); 68 | } 69 | 70 | if ($input->getOption('ft')) { 71 | $distance->in('ft'); 72 | } 73 | 74 | $output->writeln(sprintf('%s', $distance->vincenty())); 75 | return 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CLI/Command/Distance/Haversine.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Distance; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line distance:haversine class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Haversine extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('distance:haversine') 35 | ->setDescription('Compute the distance between 2 coordinates using the haversine algorithm, in meters by default') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('km', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in kilometers') 39 | ->addOption('mi', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in miles') 40 | ->addOption('ft', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in feet') 41 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 42 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 43 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 45 | 46 | Example with WGS66 ellipsoid and output in feet: 47 | 48 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=WGS66 --ft 49 | EOT 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 56 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 57 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 58 | 59 | $geotools = new Geotools; 60 | $distance = $geotools->distance()->setFrom($from)->setTo($to); 61 | 62 | if ($input->getOption('km')) { 63 | $distance->in('km'); 64 | } 65 | 66 | if ($input->getOption('mi')) { 67 | $distance->in('mi'); 68 | } 69 | 70 | if ($input->getOption('ft')) { 71 | $distance->in('ft'); 72 | } 73 | 74 | $output->writeln(sprintf('%s', $distance->haversine())); 75 | return 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CLI/Command/Distance/GreatCircle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Distance; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line distance:great-circle class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class GreatCircle extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('distance:great-circle') 35 | ->setDescription('Compute the distance between 2 coordinates using the spherical trigonometry, in meters by default') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('km', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in kilometers') 39 | ->addOption('mi', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in miles') 40 | ->addOption('ft', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in feet') 41 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 42 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 43 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 45 | 46 | Example with WGS60 ellipsoid and output in kilometers: 47 | 48 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=WGS60 --km 49 | EOT 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 56 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 57 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 58 | 59 | $geotools = new Geotools; 60 | $distance = $geotools->distance()->setFrom($from)->setTo($to); 61 | 62 | if ($input->getOption('km')) { 63 | $distance->in('km'); 64 | } 65 | 66 | if ($input->getOption('mi')) { 67 | $distance->in('mi'); 68 | } 69 | 70 | if ($input->getOption('ft')) { 71 | $distance->in('ft'); 72 | } 73 | 74 | $output->writeln(sprintf('%s', $distance->greatCircle())); 75 | return 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CLI/Command/Distance/All.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Distance; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\Ellipsoid; 16 | use League\Geotools\Geotools; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line distance:all class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class All extends \Symfony\Component\Console\Command\Command 28 | { 29 | protected function configure() 30 | { 31 | $availableEllipsoids = Ellipsoid::getAvailableEllipsoidNames(); 32 | 33 | $this 34 | ->setName('distance:all') 35 | ->setDescription('Compute the distance between 2 coordinates using all algorithms, in meters by default') 36 | ->addArgument('origin', InputArgument::REQUIRED, 'The origin "Lat,Long" coordinate') 37 | ->addArgument('destination', InputArgument::REQUIRED, 'The destination "Lat,Long" coordinate') 38 | ->addOption('km', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in kilometers') 39 | ->addOption('mi', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in miles') 40 | ->addOption('ft', null, InputOption::VALUE_NONE, 'If set, the distance will be shown in feet') 41 | ->addOption('ellipsoid', null, InputOption::VALUE_REQUIRED, 42 | 'If set, the name of the ellipsoid to use', Ellipsoid::WGS84) 43 | ->setHelp(<<Available ellipsoids: $availableEllipsoids 45 | 46 | Example with AIRY ellipsoid: 47 | 48 | %command.full_name% "40° 26.7717, -79° 56.93172" "30°16′57″N 029°48′32″W" --ellipsoid=AIRY 49 | EOT 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output): int 54 | { 55 | $ellipsoid = Ellipsoid::createFromName($input->getOption('ellipsoid')); 56 | $from = new Coordinate($input->getArgument('origin'), $ellipsoid); 57 | $to = new Coordinate($input->getArgument('destination'), $ellipsoid); 58 | 59 | $geotools = new Geotools; 60 | $distance = $geotools->distance()->setFrom($from)->setTo($to); 61 | 62 | if ($input->getOption('km')) { 63 | $distance->in('km'); 64 | } 65 | 66 | if ($input->getOption('mi')) { 67 | $distance->in('mi'); 68 | } 69 | 70 | if ($input->getOption('ft')) { 71 | $distance->in('ft'); 72 | } 73 | 74 | $result = array(); 75 | $result[] = sprintf(' %s', $distance->flat()); 76 | $result[] = sprintf(' %s', $distance->haversine()); 77 | $result[] = sprintf(' %s', $distance->vincenty()); 78 | 79 | $output->writeln($result); 80 | return 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ArrayCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools; 13 | 14 | /** 15 | * @author Gabriel Bull 16 | */ 17 | class ArrayCollection implements \Countable, \IteratorAggregate, \ArrayAccess, \JsonSerializable 18 | { 19 | /** 20 | * @var array 21 | */ 22 | protected $elements; 23 | 24 | /** 25 | * @param array $elements 26 | */ 27 | public function __construct(array $elements = array()) 28 | { 29 | $this->elements = $elements; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function toArray() 36 | { 37 | return $this->elements; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | #[\ReturnTypeWillChange] 44 | public function jsonSerialize() 45 | { 46 | return $this->elements; 47 | } 48 | 49 | /** 50 | * {@inheritDoc} 51 | */ 52 | #[\ReturnTypeWillChange] 53 | public function offsetExists($offset) 54 | { 55 | return isset($this->elements[$offset]) || array_key_exists($offset, $this->elements); 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | #[\ReturnTypeWillChange] 62 | public function offsetGet($offset) 63 | { 64 | return $this->get($offset); 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | */ 70 | #[\ReturnTypeWillChange] 71 | public function offsetSet($offset, $value) 72 | { 73 | $this->set($offset, $value); 74 | } 75 | 76 | /** 77 | * {@inheritDoc} 78 | */ 79 | #[\ReturnTypeWillChange] 80 | public function offsetUnset($offset) 81 | { 82 | return $this->remove($offset); 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | #[\ReturnTypeWillChange] 89 | public function count() 90 | { 91 | return count($this->elements); 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | #[\ReturnTypeWillChange] 98 | public function getIterator() 99 | { 100 | return new \ArrayIterator($this->elements); 101 | } 102 | 103 | /** 104 | * @param string $key 105 | * @return null|mixed 106 | */ 107 | public function get($key) 108 | { 109 | if (isset($this->elements[$key])) { 110 | return $this->elements[$key]; 111 | } 112 | 113 | return null; 114 | } 115 | 116 | /** 117 | * @param string $key 118 | * @param mixed $value 119 | */ 120 | public function set($key, $value) 121 | { 122 | $this->elements[$key] = $value; 123 | } 124 | 125 | /** 126 | * @param mixed $value 127 | * @return bool 128 | */ 129 | public function add($value) 130 | { 131 | $this->elements[] = $value; 132 | 133 | return true; 134 | } 135 | 136 | /** 137 | * @param string $key 138 | * @return null|mixed 139 | */ 140 | public function remove($key) 141 | { 142 | if (isset($this->elements[$key]) || array_key_exists($key, $this->elements)) { 143 | $removed = $this->elements[$key]; 144 | unset($this->elements[$key]); 145 | 146 | return $removed; 147 | } 148 | 149 | return null; 150 | } 151 | 152 | /** 153 | * @param ArrayCollection $collection 154 | * @return ArrayCollection 155 | */ 156 | public function merge(ArrayCollection $collection) 157 | { 158 | $merged = clone $this; 159 | 160 | foreach ($collection as $key => $element) { 161 | if (is_int($key)) { 162 | $merged->add($element); 163 | } else { 164 | $merged->set($key, $element); 165 | } 166 | } 167 | 168 | return $merged; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Convert/ConvertInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Convert; 13 | 14 | /** 15 | * Convert interface 16 | * 17 | * @author Antoine Corcy 18 | */ 19 | interface ConvertInterface 20 | { 21 | /** 22 | * Round precision for decimal minutes values. 23 | * 24 | * @var integer 25 | */ 26 | const DECIMAL_MINUTES_PRECISION = 5; 27 | 28 | /** 29 | * Round precision for degree decimal minutes values. 30 | * 31 | * @var integer 32 | */ 33 | const DEGREE_DECIMAL_MINUTES_PRECISION = 3; 34 | 35 | /** 36 | * Round mode ofr decimal minutes values. 37 | * 38 | * @var string 39 | */ 40 | const DECIMAL_MINUTES_MODE = PHP_ROUND_HALF_EVEN; 41 | 42 | /** 43 | * The default format for degrees minutes seconds coordinates. 44 | * 45 | * @var string 46 | */ 47 | const DEFAULT_DMS_FORMAT = '%D°%M′%S″%L, %d°%m′%s″%l'; 48 | 49 | /** 50 | * The default format for decimal minutes coordinates. 51 | * 52 | * @var string 53 | */ 54 | const DEFAULT_DM_FORMAT = '%P%D %N%L, %p%d %n%l'; 55 | 56 | /** 57 | * The default format for degree decimal minutes coordinates. 58 | * 59 | * @var string 60 | */ 61 | const DEFAULT_DDM_FORMAT = '%L %P%D° %N %l %p%d° %n'; 62 | 63 | /** 64 | * The sign of the latitude. 65 | * 66 | * @var string 67 | */ 68 | const LATITUDE_SIGN = '%P'; 69 | 70 | /** 71 | * The direction of the latitude. 72 | * 73 | * @var string 74 | */ 75 | const LATITUDE_DIRECTION = '%L'; 76 | 77 | /** 78 | * Latitude in degrees. 79 | * 80 | * @var string 81 | */ 82 | const LATITUDE_DEGREES = '%D'; 83 | 84 | /** 85 | * Latitude in minutes. 86 | * 87 | * @var string 88 | */ 89 | const LATITUDE_MINUTES = '%M'; 90 | 91 | /** 92 | * Latitude in decimal minutes. 93 | * 94 | * @var string 95 | */ 96 | const LATITUDE_DECIMAL_MINUTES = '%N'; 97 | 98 | /** 99 | * Latitude in seconds. 100 | * 101 | * @var string 102 | */ 103 | const LATITUDE_SECONDS = '%S'; 104 | 105 | /** 106 | * The sign of the longitude. 107 | * 108 | * @var string 109 | */ 110 | const LONGITUDE_SIGN = '%p'; 111 | 112 | /** 113 | * The direction of the longitude. 114 | * 115 | * @var string 116 | */ 117 | const LONGITUDE_DIRECTION = '%l'; 118 | 119 | /** 120 | * Longitude in degrees. 121 | * 122 | * @var string 123 | */ 124 | const LONGITUDE_DEGREES = '%d'; 125 | 126 | /** 127 | * Longitude in minutes. 128 | * 129 | * @var string 130 | */ 131 | const LONGITUDE_MINUTES = '%m'; 132 | 133 | /** 134 | * Longitude in decimal minutes. 135 | * 136 | * @var string 137 | */ 138 | const LONGITUDE_DECIMAL_MINUTES = '%n'; 139 | 140 | /** 141 | * Longitude in seconds. 142 | * 143 | * @var string 144 | */ 145 | const LONGITUDE_SECONDS = '%s'; 146 | 147 | /** 148 | * Convert and format a decimal degree coordinate to degrees minutes seconds coordinate. 149 | * 150 | * @param string $format The way to format the DMS coordinate. 151 | * 152 | * @return string Converted and formatted string. 153 | */ 154 | public function toDegreesMinutesSeconds($format = ConvertInterface::DEFAULT_DMS_FORMAT); 155 | 156 | /** 157 | * Convert and format a decimal degree coordinate to decimal minutes coordinate. 158 | * 159 | * @param string $format The way to format the DMS coordinate. 160 | * 161 | * @return string Converted and formatted string. 162 | */ 163 | public function toDecimalMinutes($format = ConvertInterface::DEFAULT_DM_FORMAT); 164 | 165 | /** 166 | * Convert and format a decimal degree coordinate to degree decimal minutes coordinate. 167 | * 168 | * @param string $format The way to format the DDM coordinate. 169 | * 170 | * @return string Converted and formatted string. 171 | */ 172 | public function toDegreeDecimalMinutes($format = ConvertInterface::DEFAULT_DDM_FORMAT); 173 | 174 | /** 175 | * Converts a WGS84 decimal degrees coordinate in the Universal Transverse Mercator projection (UTM). 176 | * 177 | * @return string The converted UTM coordinate in meters. 178 | * 179 | * @see http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.HTM 180 | * @see http://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system 181 | */ 182 | public function toUniversalTransverseMercator(); 183 | } 184 | -------------------------------------------------------------------------------- /src/CLI/Command/Geocoder/Command.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Geocoder; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | 16 | /** 17 | * Command class 18 | * 19 | * @author Antoine Corcy 20 | */ 21 | class Command extends \Symfony\Component\Console\Command\Command 22 | { 23 | /** 24 | * Available providers. 25 | * 26 | * @var array 27 | */ 28 | private $providers = [ 29 | 'free_geo_ip' => 'FreeGeoIp', 30 | 'host_ip' => 'HostIp', 31 | 'ip_info_db' => 'IpInfoDb', 32 | 'google_maps' => 'GoogleMaps', 33 | 'google_maps_business' => 'GoogleMapsBusiness', 34 | 'bing_maps' => 'BingMaps', 35 | 'openstreetmap' => 'OpenStreetMap', 36 | 'cloudmade' => 'CloudMade', 37 | 'geoip' => 'Geoip', 38 | 'map_quest' => 'MapQuest', 39 | 'oio_rest' => 'OIORest', 40 | 'geocoder_ca' => 'GeocoderCa', 41 | 'geocoder_us' => 'GeocoderUs', 42 | 'ign_openls' => 'IGNOpenLS', 43 | 'data_science_toolkit' => 'DataScienceToolkit', 44 | 'yandex' => 'Yandex', 45 | 'geo_plugin' => 'GeoPlugin', 46 | 'geo_ips' => 'GeoIPs', 47 | 'maxmind' => 'MaxMind', 48 | 'geonames' => 'Geonames', 49 | 'ip_geo_base' => 'IpGeoBase', 50 | 'baidu' => 'Baidu', 51 | 'tomtom' => 'TomTom', 52 | 'arcgis_online' => 'ArcGISOnline', 53 | ]; 54 | 55 | /** 56 | * Available dumpers. 57 | * 58 | * @var array 59 | */ 60 | private $dumpers = [ 61 | 'gpx' => 'Gpx', 62 | 'geojson' => 'GeoJson', 63 | 'kml' => 'Kml', 64 | 'wkb' => 'Wkb', 65 | 'wkt' => 'Wkt', 66 | ]; 67 | 68 | 69 | /** 70 | * @param string $factoryCallable 71 | * 72 | * @return CacheItemPoolInterface 73 | */ 74 | protected function getCache($factoryCallable) 75 | { 76 | $factoryCallable = $this->lowerize((trim($factoryCallable))); 77 | if (!is_callable($factoryCallable)) { 78 | throw new \LogicException(sprintf('Cache must be called with a valid callable on the format "Example\Acme::create". "%s" given.', $factoryCallable)); 79 | } 80 | 81 | $psr6 = call_user_func($factoryCallable); 82 | if (!$psr6 instanceof CacheItemPoolInterface) { 83 | throw new \LogicException('Return value of factory callable must be a CacheItemPoolInterface'); 84 | } 85 | 86 | return $psr6; 87 | } 88 | 89 | /** 90 | * Returns the provider class name. 91 | * The default provider is Google Maps. 92 | * 93 | * @param string $provider The name of the provider to use. 94 | * 95 | * @return string The name of the provider class to use. 96 | */ 97 | protected function getProvider($provider) 98 | { 99 | $provider = $this->lowerize((trim($provider))); 100 | $provider = array_key_exists($provider, $this->providers) 101 | ? $this->providers[$provider] 102 | : $this->providers['google_maps']; 103 | 104 | return '\\Geocoder\\Provider\\' . $provider . '\\' . $provider; 105 | } 106 | 107 | /** 108 | * Returns the list of available providers sorted by alphabetical order. 109 | * 110 | * @return string The list of available providers comma separated. 111 | */ 112 | protected function getProviders() 113 | { 114 | ksort($this->providers); 115 | 116 | return implode(', ', array_keys($this->providers)); 117 | } 118 | 119 | /** 120 | * Returns the dumper class name. 121 | * The default dumper is WktDumper. 122 | * 123 | * @param string $dumper The name of the dumper to use. 124 | * 125 | * @return string The name of the dumper class to use. 126 | */ 127 | protected function getDumper($dumper) 128 | { 129 | $dumper = $this->lowerize((trim($dumper))); 130 | $dumper = array_key_exists($dumper, $this->dumpers) 131 | ? $this->dumpers[$dumper] 132 | : $this->dumpers['wkt']; 133 | 134 | return '\\Geocoder\\Dumper\\' . $dumper; 135 | } 136 | 137 | /** 138 | * Returns the list of available dumpers sorted by alphabetical order. 139 | * 140 | * @return string The list of available dumpers comma separated. 141 | */ 142 | protected function getDumpers() 143 | { 144 | ksort($this->dumpers); 145 | 146 | return implode(', ', array_keys($this->dumpers)); 147 | } 148 | 149 | /** 150 | * Make a string lowercase. 151 | * 152 | * @param string $string A string to lowercase. 153 | * 154 | * @return string The lowercased string. 155 | */ 156 | private function lowerize($string) 157 | { 158 | return extension_loaded('mbstring') ? mb_strtolower($string, 'UTF-8') : strtolower($string); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Batch/BatchGeocoded.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Batch; 13 | 14 | use Geocoder\Location; 15 | use Geocoder\Model\Address; 16 | use Geocoder\Model\Coordinates; 17 | 18 | /** 19 | * BatchGeocoded class 20 | * 21 | * @author Antoine Corcy 22 | */ 23 | class BatchGeocoded 24 | { 25 | /** 26 | * The name of the provider. 27 | * 28 | * @var string 29 | */ 30 | protected $providerName; 31 | 32 | /** 33 | * The query. 34 | * 35 | * @var string 36 | */ 37 | protected $query; 38 | 39 | /** 40 | * The exception message. 41 | * 42 | * @var string 43 | */ 44 | protected $exception; 45 | 46 | /** 47 | * The Location object. 48 | * 49 | * @var Location 50 | */ 51 | protected $address; 52 | 53 | /** 54 | * Get the name of the provider. 55 | * 56 | * @return string The name of the provider. 57 | */ 58 | public function getProviderName() 59 | { 60 | return $this->providerName; 61 | } 62 | 63 | /** 64 | * Set the name of the provider. 65 | * 66 | * @param string $providerName The name of the provider. 67 | */ 68 | public function setProviderName($providerName) 69 | { 70 | $this->providerName = $providerName; 71 | } 72 | 73 | /** 74 | * Get the query. 75 | * 76 | * @return string The query. 77 | */ 78 | public function getQuery() 79 | { 80 | return $this->query; 81 | } 82 | 83 | /** 84 | * Set the query. 85 | * 86 | * @param string $query The query. 87 | */ 88 | public function setQuery($query) 89 | { 90 | $this->query = $query; 91 | } 92 | 93 | /** 94 | * Get the exception message. 95 | * 96 | * @return string The exception message. 97 | */ 98 | public function getExceptionMessage() 99 | { 100 | return $this->exception; 101 | } 102 | 103 | /** 104 | * Set the exception message. 105 | * 106 | * @param string $exception The exception message. 107 | */ 108 | public function setExceptionMessage($exception) 109 | { 110 | $this->exception = $exception; 111 | } 112 | 113 | /** 114 | * Get the address 115 | * 116 | * @return Location 117 | */ 118 | public function getAddress() 119 | { 120 | return $this->address; 121 | } 122 | 123 | /** 124 | * Set the address 125 | * 126 | * @param Location $address 127 | */ 128 | public function setAddress($address) 129 | { 130 | $this->address = $address; 131 | } 132 | 133 | /** 134 | * Returns an array of coordinates (latitude, longitude). 135 | * 136 | * @return Coordinates|null 137 | */ 138 | public function getCoordinates() 139 | { 140 | if (null === $this->address) { 141 | return null; 142 | } 143 | 144 | return $this->address->getCoordinates(); 145 | } 146 | 147 | /** 148 | * Returns the latitude value. 149 | * 150 | * @return double 151 | */ 152 | public function getLatitude() 153 | { 154 | if (null === $coordinates = $this->getCoordinates()) { 155 | return null; 156 | } 157 | 158 | return $coordinates->getLatitude(); 159 | } 160 | 161 | /** 162 | * Returns the longitude value. 163 | * 164 | * @return double 165 | */ 166 | public function getLongitude() 167 | { 168 | if (null === $coordinates = $this->getCoordinates()) { 169 | return null; 170 | } 171 | 172 | return $coordinates->getLongitude(); 173 | } 174 | 175 | /** 176 | * Create an instance from an array, used from cache libraries. 177 | * 178 | * @param array $data 179 | */ 180 | public function fromArray(array $data = []) 181 | { 182 | if (isset($data['providerName'])) { 183 | $this->providerName = $data['providerName']; 184 | } 185 | if (isset($data['query'])) { 186 | $this->query = $data['query']; 187 | } 188 | if (isset($data['exception'])) { 189 | $this->exception = $data['exception']; 190 | } 191 | 192 | //GeoCoder Address::createFromArray expects longitude/latitude keys 193 | $data['address']['longitude'] = $data['address']['coordinates']['longitude'] ?? null; 194 | $data['address']['latitude'] = $data['address']['coordinates']['latitude'] ?? null; 195 | 196 | // Shortcut to create the address and set it in this class 197 | $this->setAddress(Address::createFromArray($data['address'])); 198 | } 199 | 200 | /** 201 | * Router all other methods call directly to our address object 202 | * 203 | * @param $method 204 | * @param $args 205 | * @return mixed 206 | */ 207 | public function __call($method, $args) 208 | { 209 | if (null === $this->address) { 210 | return null; 211 | } 212 | 213 | return call_user_func_array([$this->address, $method], $args); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/GeometryCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools; 13 | 14 | use League\Geotools\BoundingBox\BoundingBox; 15 | use League\Geotools\BoundingBox\BoundingBoxInterface; 16 | use League\Geotools\Coordinate\Coordinate; 17 | use League\Geotools\Coordinate\CoordinateCollection; 18 | use League\Geotools\Coordinate\Ellipsoid; 19 | use League\Geotools\Exception\InvalidArgumentException; 20 | 21 | /** 22 | * @author Rémi San 23 | */ 24 | abstract class GeometryCollection extends ArrayCollection implements GeometryInterface 25 | { 26 | /** 27 | * @var Ellipsoid 28 | */ 29 | private $ellipsoid; 30 | 31 | /** 32 | * @var integer 33 | */ 34 | private $precision; 35 | 36 | /** 37 | * CoordinateCollection constructor. 38 | * 39 | * @param GeometryInterface[] $geometries 40 | * @param ?Ellipsoid $ellipsoid 41 | */ 42 | public function __construct(array $geometries = array(), ?Ellipsoid $ellipsoid = null) 43 | { 44 | $this->precision = -1; 45 | 46 | $this->ellipsoid = $ellipsoid ? : null; 47 | 48 | $this->checkGeometriesArray($geometries); 49 | 50 | parent::__construct($geometries); 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | abstract public function getGeometryType(); 57 | 58 | /** 59 | * @return integer 60 | */ 61 | public function getPrecision() 62 | { 63 | return $this->precision; 64 | } 65 | 66 | /** 67 | * @return Coordinate 68 | */ 69 | public function getCoordinate() 70 | { 71 | if ($this->isEmpty()) { 72 | return null; 73 | } 74 | 75 | return $this->offsetGet(0)->getCoordinate(); 76 | } 77 | 78 | /** 79 | * @return CoordinateCollection 80 | */ 81 | public function getCoordinates() 82 | { 83 | $coordinates = new CoordinateCollection(array(), $this->ellipsoid); 84 | 85 | /** @var GeometryInterface $element */ 86 | foreach ($this->elements as $element) { 87 | $coordinates = $coordinates->merge($element->getCoordinates()); 88 | } 89 | 90 | return $coordinates; 91 | } 92 | 93 | /** 94 | * @return boolean 95 | */ 96 | public function isEmpty() 97 | { 98 | return count($this->elements) === 0 ; 99 | } 100 | 101 | /** 102 | * @return BoundingBoxInterface 103 | */ 104 | public function getBoundingBox() 105 | { 106 | $boundingBox = new BoundingBox(); 107 | 108 | /** @var GeometryInterface $element */ 109 | foreach ($this->elements as $element) { 110 | $boundingBox = $boundingBox->merge($element->getBoundingBox()); 111 | } 112 | 113 | return $boundingBox; 114 | } 115 | 116 | /** 117 | * @param string $key 118 | * 119 | * @param GeometryInterface $value 120 | */ 121 | public function set($key, $value) 122 | { 123 | $this->checkEllipsoid($value); 124 | $this->elements[$key] = $value; 125 | } 126 | 127 | /** 128 | * @param GeometryInterface $value 129 | * 130 | * @return bool 131 | */ 132 | public function add($value) 133 | { 134 | $this->checkEllipsoid($value); 135 | $this->elements[] = $value; 136 | 137 | return true; 138 | } 139 | 140 | /** 141 | * @return Ellipsoid 142 | */ 143 | public function getEllipsoid() 144 | { 145 | return $this->ellipsoid; 146 | } 147 | 148 | /** 149 | * @param array GeometryInterface[] $geometries 150 | */ 151 | private function checkGeometriesArray(array $geometries) 152 | { 153 | foreach ($geometries as $geometry) { 154 | if (!$geometry instanceof GeometryInterface) { 155 | throw new InvalidArgumentException("You didn't provide a geometry!"); 156 | } 157 | $this->checkEllipsoid($geometry); 158 | } 159 | } 160 | 161 | /** 162 | * @param GeometryInterface $geometry 163 | * 164 | * @throws InvalidArgumentException 165 | */ 166 | private function checkEllipsoid(GeometryInterface $geometry) 167 | { 168 | if (bccomp($geometry->getPrecision(), $this->precision) === 1) { 169 | $this->precision = $geometry->getPrecision(); 170 | } 171 | 172 | if ($this->ellipsoid === null) { 173 | $this->ellipsoid = $geometry->getEllipsoid(); 174 | } elseif ($geometry->isEmpty() || $geometry->getEllipsoid() != $this->ellipsoid) { 175 | throw new InvalidArgumentException("Geometry is invalid"); 176 | } 177 | } 178 | 179 | /** 180 | * @param ArrayCollection $collection 181 | * 182 | * @return ArrayCollection 183 | */ 184 | public function merge(ArrayCollection $collection) 185 | { 186 | if (!$collection instanceof GeometryCollection || $collection->getGeometryType() !== $this->getGeometryType()) { 187 | throw new InvalidArgumentException("Collections types don't match, you can't merge."); 188 | } 189 | 190 | return parent::merge($collection); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/CLI/Command/Geocoder/Reverse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Geocoder; 13 | 14 | use Geocoder\Formatter\StringFormatter; 15 | use Geocoder\ProviderAggregator; 16 | use Http\Discovery\HttpClientDiscovery; 17 | use League\Geotools\Batch\Batch; 18 | use League\Geotools\Coordinate\Coordinate; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | 24 | /** 25 | * Command-line geocoder:reverse class 26 | * 27 | * @author Antoine Corcy 28 | */ 29 | class Reverse extends Command 30 | { 31 | protected function configure() 32 | { 33 | $this 34 | ->setName('geocoder:reverse') 35 | ->setDescription('Reverse geocode street address, IPv4 or IPv6 against a provider with an adapter') 36 | ->addArgument('coordinate', InputArgument::REQUIRED, 'The coordinate to reverse') 37 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 38 | 'If set, the name of the provider to use, Google Maps by default', 'google_maps') 39 | ->addOption('cache', null, InputOption::VALUE_REQUIRED, 40 | 'If set, the name of the cache to use, Redis by default') 41 | ->addOption('raw', null, InputOption::VALUE_NONE, 42 | 'If set, the raw format of the reverse geocoding result') 43 | ->addOption('json', null, InputOption::VALUE_NONE, 44 | 'If set, the json format of the reverse geocoding result') 45 | ->addOption('args', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 46 | 'If set, the provider constructor arguments like api key, locale, region, ssl, toponym and service') 47 | ->addOption('format', null, InputOption::VALUE_REQUIRED, 48 | 'If set, the format of the reverse geocoding result', '%S %n, %z %L') 49 | ->setHelp(<<Available providers: {$this->getProviders()} (some providers need arguments) 51 | Available dumpers: {$this->getDumpers()} 52 | 53 | Use the default provider with the socket adapter and format the output: 54 | 55 | %command.full_name% "48.8631507, 2.388911" --format="%L, %R, %C" --adapter=socket 56 | 57 | Use the OpenStreetMap provider with the default adapter: 58 | 59 | %command.full_name% "48.8631507, 2.388911" --provider=openstreetmap 60 | EOT 61 | ); 62 | } 63 | 64 | protected function execute(InputInterface $input, OutputInterface $output): int 65 | { 66 | $coordinate = new Coordinate($input->getArgument('coordinate')); 67 | 68 | $geocoder = new ProviderAggregator; 69 | $httpClient = HttpClientDiscovery::find(); 70 | $provider = $this->getProvider($input->getOption('provider')); 71 | 72 | if ($input->getOption('args')) { 73 | $args = is_array($input->getOption('args')) 74 | ? implode(',', $input->getOption('args')) 75 | : $input->getOption('args'); 76 | $geocoder->registerProvider(new $provider($httpClient, $args)); 77 | } else { 78 | $geocoder->registerProvider(new $provider($httpClient)); 79 | } 80 | 81 | $batch = new Batch($geocoder); 82 | if ($input->getOption('cache')) { 83 | $batch->setCache( $this->getCache($input->getOption('cache'))); 84 | } 85 | 86 | $reversed = $batch->reverse($coordinate)->parallel(); 87 | $address = $reversed[0]->first(); 88 | 89 | if ($input->getOption('raw')) { 90 | $result = array(); 91 | $result[] = sprintf(': %s', get_class($httpClient)); 92 | $result[] = sprintf(': %s', $provider); 93 | $result[] = sprintf(': %s', isset($cache) ? $cache : 'None'); 94 | if ($input->getOption('args')) { 95 | $result[] = sprintf(': %s', $args); 96 | } 97 | $result[] = '---'; 98 | $coordinates = $address->getCoordinates(); 99 | $result[] = sprintf(': %s', null !== $coordinates ? $coordinates->getLatitude() : ''); 100 | $result[] = sprintf(': %s', null !== $coordinates ? $coordinates->getLongitude() : ''); 101 | if ($address->getBounds()) { 102 | $bounds = $address->getBounds()->toArray(); 103 | $result[] = ''; 104 | $result[] = sprintf(' - : %s', $bounds['south']); 105 | $result[] = sprintf(' - : %s', $bounds['west']); 106 | $result[] = sprintf(' - : %s', $bounds['north']); 107 | $result[] = sprintf(' - : %s', $bounds['east']); 108 | } 109 | $result[] = sprintf(': %s', $address->getStreetNumber()); 110 | $result[] = sprintf(': %s', $address->getStreetName()); 111 | $result[] = sprintf(': %s', $address->getPostalCode()); 112 | $result[] = sprintf(': %s', $address->getLocality()); 113 | $result[] = sprintf(': %s', $address->getSubLocality()); 114 | if ( NULL !== $adminLevels = $address->getAdminLevels() ) { 115 | $result[] = ''; 116 | foreach ($adminLevels as $adminLevel) { 117 | $result[] = sprintf(' - : %s', $adminLevel->getCode(), $adminLevel->getName()); 118 | } 119 | } 120 | $country = $address->getCountry(); 121 | $result[] = sprintf(': %s', null !== $country ? $country->getName() : ''); 122 | $result[] = sprintf(': %s', null !== $country ? $country->getCode() : ''); 123 | $result[] = sprintf(': %s', $address->getTimezone()); 124 | } elseif ($input->getOption('json')) { 125 | $result = sprintf('%s', json_encode($address->toArray())); 126 | } else { 127 | $result = (new StringFormatter)->format($address, $input->getOption('format')); 128 | } 129 | 130 | $output->writeln($result); 131 | return 0; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/CLI/Command/Geocoder/Geocode.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\CLI\Command\Geocoder; 13 | 14 | use Geocoder\ProviderAggregator; 15 | use Http\Discovery\HttpClientDiscovery; 16 | use League\Geotools\Batch\Batch; 17 | use Symfony\Component\Console\Input\InputArgument; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * Command-line geocoder:geocode class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Geocode extends Command 28 | { 29 | protected function configure() 30 | { 31 | $this 32 | ->setName('geocoder:geocode') 33 | ->setDescription('Geocode a street-address, IPv4 or IPv6 against a provider with an adapter') 34 | ->addArgument('value', InputArgument::REQUIRED, 'The street-address, IPv4 or IPv6 to geocode') 35 | ->addOption('provider', null, InputOption::VALUE_REQUIRED, 36 | 'If set, the name of the provider to use, Google Maps by default', 'google_maps') 37 | ->addOption('cache', null, InputOption::VALUE_REQUIRED, 38 | 'If set, the name of a factory method that will create a PSR-6 cache. "Example\Acme::create"') 39 | ->addOption('raw', null, InputOption::VALUE_NONE, 40 | 'If set, the raw format of the reverse geocoding result') 41 | ->addOption('json', null, InputOption::VALUE_NONE, 42 | 'If set, the json format of the reverse geocoding result') 43 | ->addOption('dumper', null, InputOption::VALUE_REQUIRED, 44 | 'If set, the name of the dumper to use, no dumper by default') 45 | ->addOption('args', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 46 | 'If set, the provider constructor arguments like api key, locale, region, ssl, toponym and service') 47 | ->setHelp(<<Available providers: {$this->getProviders()} (some providers need arguments) 49 | Available dumpers: {$this->getDumpers()} 50 | 51 | Use the default provider with the socket adapter and dump the output in WKT standard: 52 | 53 | %command.full_name% paris --adapter=socket --dumper=wkt 54 | 55 | Use the OpenStreetMap provider with the default adapter: 56 | 57 | %command.full_name% paris --provider=openstreetmap 58 | 59 | Use the FreeGeoIp provider with the socket adapter 60 | 61 | %command.full_name% 74.200.247.59 --provider="free_geo_ip" --adapter="socket" 62 | 63 | Use the default provider with the french locale and region via SSL: 64 | 65 | %command.full_name% "Tagensvej 47, Copenhagen" --args=da_DK --args=Denmark --args="true" 66 | EOT 67 | ); 68 | } 69 | 70 | protected function execute(InputInterface $input, OutputInterface $output): int 71 | { 72 | $geocoder = new ProviderAggregator; 73 | $httpClient = HttpClientDiscovery::find(); 74 | $provider = $this->getProvider($input->getOption('provider')); 75 | 76 | if ($input->getOption('args')) { 77 | $args = is_array($input->getOption('args')) 78 | ? implode(',', $input->getOption('args')) 79 | : $input->getOption('args'); 80 | $geocoder->registerProvider(new $provider($httpClient, $args)); 81 | } else { 82 | $geocoder->registerProvider(new $provider($httpClient)); 83 | } 84 | 85 | $batch = new Batch($geocoder); 86 | if ($input->getOption('cache')) { 87 | $batch->setCache($this->getCache($input->getOption('cache'))); 88 | } 89 | 90 | $geocoded = $batch->geocode($input->getArgument('value'))->parallel(); 91 | $address = $geocoded[0]->first(); 92 | 93 | if ($input->getOption('raw')) { 94 | $result = array(); 95 | $result[] = sprintf(': %s', get_class($httpClient)); 96 | $result[] = sprintf(': %s', $provider); 97 | $result[] = sprintf(': %s', isset($cache) ? $cache : 'None'); 98 | if ($input->getOption('args')) { 99 | $result[] = sprintf(': %s', $args); 100 | } 101 | $result[] = '---'; 102 | $coordinates = $address->getCoordinates(); 103 | $result[] = sprintf(': %s', null !== $coordinates ? $coordinates->getLatitude() : ''); 104 | $result[] = sprintf(': %s', null !== $coordinates ? $coordinates->getLongitude() : ''); 105 | if ($address->getBounds()) { 106 | $bounds = $address->getBounds()->toArray(); 107 | $result[] = ''; 108 | $result[] = sprintf(' - : %s', $bounds['south']); 109 | $result[] = sprintf(' - : %s', $bounds['west']); 110 | $result[] = sprintf(' - : %s', $bounds['north']); 111 | $result[] = sprintf(' - : %s', $bounds['east']); 112 | } 113 | $result[] = sprintf(': %s', $address->getStreetNumber()); 114 | $result[] = sprintf(': %s', $address->getStreetName()); 115 | $result[] = sprintf(': %s', $address->getPostalCode()); 116 | $result[] = sprintf(': %s', $address->getLocality()); 117 | $result[] = sprintf(': %s', $address->getSublocality()); 118 | if (null !== $adminLevels = $address->getAdminLevels()) { 119 | $result[] = ''; 120 | foreach ($adminLevels as $adminLevel) { 121 | $result[] = sprintf(' - : %s', $adminLevel->getCode(), $adminLevel->getName()); 122 | } 123 | } 124 | $country = $address->getCountry(); 125 | $result[] = sprintf(': %s', null !== $country ? $country->getName() : ''); 126 | $result[] = sprintf(': %s', null !== $country ? $country->getCode() : ''); 127 | $result[] = sprintf(': %s', $address->getTimezone()); 128 | } elseif ($input->getOption('json')) { 129 | $result = sprintf('%s', json_encode($address->toArray())); 130 | } elseif ($input->getOption('dumper')) { 131 | $dumper = $this->getDumper($input->getOption('dumper')); 132 | $dumper = new $dumper; 133 | $result = sprintf('%s', $dumper->dump($address)); 134 | } else { 135 | $coordinates = $address->getCoordinates(); 136 | $result = 'null, null'; 137 | if (null !== $coordinates) { 138 | $result = sprintf('%s, %s', $coordinates->getLatitude(), $coordinates->getLongitude()); 139 | } 140 | } 141 | 142 | $output->writeln($result); 143 | return 0; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/BoundingBox/BoundingBox.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\BoundingBox; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\CoordinateCollection; 16 | use League\Geotools\Coordinate\CoordinateInterface; 17 | use League\Geotools\Coordinate\Ellipsoid; 18 | use League\Geotools\Exception\InvalidArgumentException; 19 | use League\Geotools\Polygon\Polygon; 20 | use League\Geotools\Polygon\PolygonInterface; 21 | 22 | /** 23 | * @author Gabriel Bull 24 | */ 25 | class BoundingBox implements BoundingBoxInterface 26 | { 27 | /** 28 | * The latitude of the north coordinate 29 | * 30 | * @var float|string|integer 31 | */ 32 | private $north; 33 | 34 | /** 35 | * The longitude of the east coordinate 36 | * 37 | * @var float|string|integer 38 | */ 39 | private $east; 40 | 41 | /** 42 | * The latitude of the south coordinate 43 | * 44 | * @var float|string|integer 45 | */ 46 | private $south; 47 | 48 | /** 49 | * The longitude of the west coordinate 50 | * 51 | * @var float|string|integer 52 | */ 53 | private $west; 54 | 55 | /** 56 | * @var boolean 57 | */ 58 | private $hasCoordinate = false; 59 | 60 | /** 61 | * @var Ellipsoid 62 | */ 63 | private $ellipsoid; 64 | 65 | /** 66 | * @var integer 67 | */ 68 | private $precision = 8; 69 | 70 | /** 71 | * @param PolygonInterface|CoordinateInterface $object 72 | */ 73 | public function __construct($object = null) 74 | { 75 | if ($object instanceof PolygonInterface) { 76 | $this->setPolygon($object); 77 | } elseif ($object instanceof CoordinateInterface) { 78 | $this->addCoordinate($object); 79 | } elseif (null !== $object) { 80 | throw new \InvalidArgumentException; 81 | } 82 | } 83 | 84 | private function createBoundingBoxForPolygon(PolygonInterface $polygon) 85 | { 86 | $this->hasCoordinate = false; 87 | $this->west = $this->east = $this->north = $this->south = null; 88 | foreach ($polygon->getCoordinates() as $coordinate) { 89 | $this->addCoordinate($coordinate); 90 | } 91 | } 92 | 93 | /** 94 | * @param CoordinateInterface $coordinate 95 | */ 96 | private function addCoordinate(CoordinateInterface $coordinate) 97 | { 98 | if ($this->ellipsoid === null) { 99 | $this->ellipsoid = $coordinate->getEllipsoid(); 100 | } elseif ($this->ellipsoid != $coordinate->getEllipsoid()) { 101 | throw new InvalidArgumentException("Ellipsoids don't match"); 102 | } 103 | 104 | $latitude = $coordinate->getLatitude(); 105 | $longitude = $coordinate->getLongitude(); 106 | 107 | if (!$this->hasCoordinate) { 108 | $this->setNorth($latitude); 109 | $this->setSouth($latitude); 110 | $this->setEast($longitude); 111 | $this->setWest($longitude); 112 | } else { 113 | if (bccomp($latitude, $this->getSouth(), $this->getPrecision()) === -1) { 114 | $this->setSouth($latitude); 115 | } 116 | if (bccomp($latitude, $this->getNorth(), $this->getPrecision()) === 1) { 117 | $this->setNorth($latitude); 118 | } 119 | if (bccomp($longitude, $this->getEast(), $this->getPrecision()) === 1) { 120 | $this->setEast($longitude); 121 | } 122 | if (bccomp($longitude, $this->getWest(), $this->getPrecision()) === -1) { 123 | $this->setWest($longitude); 124 | } 125 | } 126 | 127 | $this->hasCoordinate = true; 128 | } 129 | 130 | /** 131 | * @param CoordinateInterface $coordinate 132 | * @return bool 133 | */ 134 | public function pointInBoundingBox(CoordinateInterface $coordinate) 135 | { 136 | if (bccomp($coordinate->getLatitude(), $this->getSouth(), $this->getPrecision()) === -1 || 137 | bccomp($coordinate->getLatitude(), $this->getNorth(), $this->getPrecision()) === 1 || 138 | bccomp($coordinate->getLongitude(), $this->getEast(), $this->getPrecision()) === 1 || 139 | bccomp($coordinate->getLongitude(), $this->getWest(), $this->getPrecision()) === -1 140 | ) { 141 | return false; 142 | } 143 | 144 | return true; 145 | } 146 | 147 | /** 148 | * @return PolygonInterface 149 | */ 150 | public function getAsPolygon() 151 | { 152 | if (!$this->hasCoordinate) { 153 | return null; 154 | } 155 | 156 | $nw = new Coordinate(array($this->north, $this->west), $this->ellipsoid); 157 | 158 | return new Polygon( 159 | new CoordinateCollection( 160 | array( 161 | $nw, 162 | new Coordinate(array($this->north, $this->east), $this->ellipsoid), 163 | new Coordinate(array($this->south, $this->east), $this->ellipsoid), 164 | new Coordinate(array($this->south, $this->west), $this->ellipsoid), 165 | $nw 166 | ), 167 | $this->ellipsoid 168 | ) 169 | ); 170 | } 171 | 172 | /** 173 | * @param PolygonInterface $polygon 174 | * @return $this 175 | */ 176 | public function setPolygon(PolygonInterface $polygon) 177 | { 178 | $this->createBoundingBoxForPolygon($polygon); 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * {@inheritDoc} 185 | */ 186 | public function getNorth() 187 | { 188 | return $this->north; 189 | } 190 | 191 | /** 192 | * @param float|string|integer $north 193 | * @return $this 194 | */ 195 | public function setNorth($north) 196 | { 197 | $this->north = $north; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * {@inheritDoc} 204 | */ 205 | public function getEast() 206 | { 207 | return $this->east; 208 | } 209 | 210 | /** 211 | * @param float|string|integer $east 212 | * @return $this 213 | */ 214 | public function setEast($east) 215 | { 216 | $this->east = $east; 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * {@inheritDoc} 223 | */ 224 | public function getSouth() 225 | { 226 | return $this->south; 227 | } 228 | 229 | /** 230 | * @param float|string|integer $south 231 | * @return $this 232 | */ 233 | public function setSouth($south) 234 | { 235 | $this->south = $south; 236 | 237 | return $this; 238 | } 239 | 240 | /** 241 | * {@inheritDoc} 242 | */ 243 | public function getWest() 244 | { 245 | return $this->west; 246 | } 247 | 248 | /** 249 | * @param float|string|integer $west 250 | * @return $this 251 | */ 252 | public function setWest($west) 253 | { 254 | $this->west = $west; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * @return integer 261 | */ 262 | public function getPrecision() 263 | { 264 | return $this->precision; 265 | } 266 | 267 | /** 268 | * @param integer $precision 269 | * @return $this 270 | */ 271 | public function setPrecision($precision) 272 | { 273 | $this->precision = $precision; 274 | 275 | return $this; 276 | } 277 | 278 | /** 279 | * @param BoundingBoxInterface $boundingBox 280 | * @return BoundingBoxInterface 281 | */ 282 | public function merge(BoundingBoxInterface $boundingBox) 283 | { 284 | $bbox = clone $this; 285 | 286 | $newCoordinates = $boundingBox->getAsPolygon()->getCoordinates(); 287 | foreach ($newCoordinates as $coordinate) { 288 | $bbox->addCoordinate($coordinate); 289 | } 290 | 291 | return $bbox; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Distance/Distance.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Distance; 13 | 14 | use League\Geotools\CoordinateCouple; 15 | use League\Geotools\Exception\NotConvergingException; 16 | use League\Geotools\Coordinate\CoordinateInterface; 17 | use League\Geotools\Coordinate\Ellipsoid; 18 | use League\Geotools\GeotoolsInterface; 19 | 20 | /** 21 | * Distance class 22 | * 23 | * @author Antoine Corcy 24 | */ 25 | class Distance implements DistanceInterface 26 | { 27 | use CoordinateCouple; 28 | 29 | /** 30 | * The user unit. 31 | * 32 | * @var string 33 | */ 34 | protected $unit; 35 | 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | public function setFrom(CoordinateInterface $from) 41 | { 42 | $this->from = $from; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function getFrom() 51 | { 52 | return $this->from; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | */ 58 | public function setTo(CoordinateInterface $to) 59 | { 60 | $this->to = $to; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function getTo() 69 | { 70 | return $this->to; 71 | } 72 | 73 | /** 74 | * {@inheritDoc} 75 | */ 76 | public function in($unit) 77 | { 78 | $this->unit = $unit; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Returns the approximate flat distance between two coordinates 85 | * using Pythagoras’ theorem which is not very accurate. 86 | * @see http://en.wikipedia.org/wiki/Pythagorean_theorem 87 | * @see http://en.wikipedia.org/wiki/Equirectangular_projection 88 | * 89 | * @return double The distance in meters 90 | */ 91 | public function flat() 92 | { 93 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 94 | 95 | $latA = deg2rad($this->from->getLatitude()); 96 | $lngA = deg2rad($this->from->getLongitude()); 97 | $latB = deg2rad($this->to->getLatitude()); 98 | $lngB = deg2rad($this->to->getLongitude()); 99 | 100 | $x = ($lngB - $lngA) * cos(($latA + $latB) / 2); 101 | $y = $latB - $latA; 102 | 103 | $d = sqrt(($x * $x) + ($y * $y)) * $this->from->getEllipsoid()->getA(); 104 | 105 | return $this->convertToUserUnit($d); 106 | } 107 | 108 | /** 109 | * Returns the approximate distance between two coordinates 110 | * using the spherical trigonometry called Great Circle Distance. 111 | * @see http://www.ga.gov.au/earth-monitoring/geodesy/geodetic-techniques/distance-calculation-algorithms.html#circle 112 | * @see http://en.wikipedia.org/wiki/Cosine_law 113 | * 114 | * @return double The distance in meters 115 | */ 116 | public function greatCircle() 117 | { 118 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 119 | 120 | $latA = deg2rad($this->from->getLatitude()); 121 | $lngA = deg2rad($this->from->getLongitude()); 122 | $latB = deg2rad($this->to->getLatitude()); 123 | $lngB = deg2rad($this->to->getLongitude()); 124 | 125 | $degrees = acos(sin($latA) * sin($latB) + cos($latA) * cos($latB) * cos($lngB - $lngA)); 126 | 127 | if (is_nan($degrees)) { 128 | return 0.0; 129 | } 130 | 131 | return $this->convertToUserUnit($degrees * $this->from->getEllipsoid()->getA()); 132 | } 133 | 134 | /** 135 | * Returns the approximate sea level great circle (Earth) distance between 136 | * two coordinates using the Haversine formula which is accurate to around 0.3%. 137 | * @see http://www.movable-type.co.uk/scripts/latlong.html 138 | * 139 | * @return double The distance in meters 140 | */ 141 | public function haversine() 142 | { 143 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 144 | 145 | $latA = deg2rad($this->from->getLatitude()); 146 | $lngA = deg2rad($this->from->getLongitude()); 147 | $latB = deg2rad($this->to->getLatitude()); 148 | $lngB = deg2rad($this->to->getLongitude()); 149 | 150 | $dLat = $latB - $latA; 151 | $dLon = $lngB - $lngA; 152 | 153 | $a = (sin($dLat / 2) ** 2) + cos($latA) * cos($latB) * (sin($dLon / 2) ** 2); 154 | $c = 2 * asin(sqrt($a)); 155 | 156 | return $this->convertToUserUnit($this->from->getEllipsoid()->getArithmeticMeanRadius() * $c); 157 | } 158 | 159 | /** 160 | * Returns geodetic distance between between two coordinates using Vincenty inverse 161 | * formula for ellipsoids which is accurate to within 0.5mm. 162 | * @see http://www.movable-type.co.uk/scripts/latlong-vincenty.html 163 | * 164 | * @return double The distance in meters 165 | */ 166 | public function vincenty() 167 | { 168 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 169 | 170 | $a = $this->from->getEllipsoid()->getA(); 171 | $b = $this->from->getEllipsoid()->getB(); 172 | $f = 1 / $this->from->getEllipsoid()->getInvF(); 173 | 174 | $lL = deg2rad($this->to->getLongitude() - $this->from->getLongitude()); 175 | $u1 = atan((1 - $f) * tan(deg2rad($this->from->getLatitude()))); 176 | $u2 = atan((1 - $f) * tan(deg2rad($this->to->getLatitude()))); 177 | 178 | $sinU1 = sin($u1); 179 | $cosU1 = cos($u1); 180 | $sinU2 = sin($u2); 181 | $cosU2 = cos($u2); 182 | 183 | $lambda = $lL; 184 | $iterLimit = 100; 185 | 186 | do { 187 | $sinLambda = sin($lambda); 188 | $cosLambda = cos($lambda); 189 | $sinSigma = sqrt(($cosU2 * $sinLambda) * ($cosU2 * $sinLambda) + 190 | ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda) * ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda)); 191 | 192 | if (0.0 === $sinSigma) { 193 | return 0.0; // co-incident points 194 | } 195 | 196 | $cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda; 197 | $sigma = atan2($sinSigma, $cosSigma); 198 | $sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma; 199 | $cosSqAlpha = 1 - $sinAlpha * $sinAlpha; 200 | if ($cosSqAlpha != 0.0) { 201 | $cos2SigmaM = $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha; 202 | } 203 | else { 204 | $cos2SigmaM = 0.0; 205 | } 206 | $cC = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha)); 207 | $lambdaP = $lambda; 208 | $lambda = $lL + (1 - $cC) * $f * $sinAlpha * ($sigma + $cC * $sinSigma * 209 | ($cos2SigmaM + $cC * $cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM))); 210 | } while (abs($lambda - $lambdaP) > 1e-12 && --$iterLimit > 0); 211 | 212 | // @codeCoverageIgnoreStart 213 | if (0 === $iterLimit) { 214 | throw new NotConvergingException('Vincenty formula failed to converge !'); 215 | } 216 | // @codeCoverageIgnoreEnd 217 | 218 | $uSq = $cosSqAlpha * ($a * $a - $b * $b) / ($b * $b); 219 | $aA = 1 + $uSq / 16384 * (4096 + $uSq * (-768 + $uSq * (320 - 175 * $uSq))); 220 | $bB = $uSq / 1024 * (256 + $uSq * (-128 + $uSq * (74 - 47 * $uSq))); 221 | $deltaSigma = $bB * $sinSigma * ($cos2SigmaM + $bB / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM) - 222 | $bB / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma) * (-3 + 4 * $cos2SigmaM * $cos2SigmaM))); 223 | $s = $b * $aA * ($sigma - $deltaSigma); 224 | 225 | return $this->convertToUserUnit($s); 226 | } 227 | 228 | /** 229 | * Converts results in meters to user's unit (if any). 230 | * The default returned value is in meters. 231 | * 232 | * @param double $meters 233 | * 234 | * @return double 235 | */ 236 | protected function convertToUserUnit($meters) 237 | { 238 | switch ($this->unit) { 239 | case GeotoolsInterface::KILOMETER_UNIT: 240 | return $meters / 1000; 241 | case GeotoolsInterface::MILE_UNIT: 242 | return $meters / GeotoolsInterface::METERS_PER_MILE; 243 | case GeotoolsInterface::FOOT_UNIT: 244 | return $meters / GeotoolsInterface::FEET_PER_METER; 245 | default: 246 | return $meters; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Coordinate/Coordinate.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Coordinate; 13 | 14 | use Geocoder\Location; 15 | use League\Geotools\Exception\InvalidArgumentException; 16 | 17 | /** 18 | * Coordinate class 19 | * 20 | * @author Antoine Corcy 21 | */ 22 | class Coordinate implements CoordinateInterface, \JsonSerializable 23 | { 24 | /** 25 | * The latitude of the coordinate. 26 | * 27 | * @var double 28 | */ 29 | protected $latitude; 30 | 31 | /** 32 | * The longitude of the coordinate. 33 | * 34 | * @var double 35 | */ 36 | protected $longitude; 37 | 38 | /** 39 | * The selected ellipsoid. 40 | * 41 | * @var Ellipsoid 42 | */ 43 | protected $ellipsoid; 44 | 45 | 46 | /** 47 | * The precision to use to compare big numbers 48 | * 49 | * @var integer 50 | */ 51 | private $precision = 8; 52 | 53 | /** 54 | * Set the latitude and the longitude of the coordinates into an selected ellipsoid. 55 | * 56 | * @param Location|array|string $coordinates The coordinates. 57 | * @param Ellipsoid|null $ellipsoid The selected ellipsoid (WGS84 by default). 58 | * 59 | * @throws InvalidArgumentException 60 | */ 61 | public function __construct($coordinates, ?Ellipsoid $ellipsoid = null) 62 | { 63 | if ($coordinates instanceof Location) { 64 | if (null !== $locationCoordinates = $coordinates->getCoordinates()) { 65 | $this->setLatitude($locationCoordinates->getLatitude()); 66 | $this->setLongitude($locationCoordinates->getLongitude()); 67 | } 68 | } elseif (is_array($coordinates) && 2 === count($coordinates)) { 69 | $this->setLatitude($coordinates[0]); 70 | $this->setLongitude($coordinates[1]); 71 | } elseif (is_string($coordinates)) { 72 | $this->setFromString($coordinates); 73 | } else { 74 | throw new InvalidArgumentException( 75 | 'It should be a string, an array or a class which implements Geocoder\Model\Address !' 76 | ); 77 | } 78 | 79 | $this->ellipsoid = $ellipsoid ?: Ellipsoid::createFromName(Ellipsoid::WGS84); 80 | } 81 | 82 | /** 83 | * {@inheritDoc} 84 | */ 85 | public function normalizeLatitude($latitude) 86 | { 87 | $latitude = rtrim(sprintf('%.13F', max(-90, min(90, $latitude))), 0); 88 | 89 | return '.' === substr($latitude, -1) ? $latitude . '0' : $latitude; 90 | } 91 | 92 | /** 93 | * {@inheritDoc} 94 | */ 95 | public function normalizeLongitude($longitude) 96 | { 97 | if (180 === floor($longitude) % 360) { 98 | return '180.0'; 99 | } 100 | 101 | $mod = fmod($longitude, 360); 102 | $longitude = $mod < -180 ? $mod + 360 : ($mod > 180 ? $mod - 360 : $mod); 103 | $longitude = rtrim(sprintf('%.13F', $longitude), 0); 104 | 105 | return '.' === substr($longitude, -1) ? $longitude . '0' : $longitude; 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function setLatitude($latitude) 112 | { 113 | $this->latitude = $this->normalizeLatitude($latitude); 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | */ 119 | public function getLatitude() 120 | { 121 | return $this->latitude; 122 | } 123 | 124 | /** 125 | * {@inheritDoc} 126 | */ 127 | public function setLongitude($longitude) 128 | { 129 | $this->longitude = $this->normalizeLongitude($longitude); 130 | } 131 | 132 | /** 133 | * {@inheritDoc} 134 | */ 135 | public function getLongitude() 136 | { 137 | return $this->longitude; 138 | } 139 | 140 | /** 141 | * {@inheritDoc} 142 | */ 143 | public function getEllipsoid() 144 | { 145 | return $this->ellipsoid; 146 | } 147 | 148 | /** 149 | * Creates a valid and acceptable geographic coordinates. 150 | * 151 | * @param string $coordinates 152 | * 153 | * @throws InvalidArgumentException 154 | */ 155 | public function setFromString($coordinates) 156 | { 157 | if (!is_string($coordinates)) { 158 | throw new InvalidArgumentException('The given coordinates should be a string !'); 159 | } 160 | 161 | try { 162 | $inDecimalDegree = $this->toDecimalDegrees($coordinates); 163 | $this->setLatitude($inDecimalDegree[0]); 164 | $this->setLongitude($inDecimalDegree[1]); 165 | } catch (InvalidArgumentException $e) { 166 | throw $e; 167 | } 168 | } 169 | 170 | /** 171 | * @return integer 172 | */ 173 | public function getPrecision() 174 | { 175 | return $this->precision; 176 | } 177 | 178 | /** 179 | * @param integer $precision 180 | * @return $this 181 | */ 182 | public function setPrecision($precision) 183 | { 184 | $this->precision = $precision; 185 | 186 | return $this; 187 | } 188 | 189 | 190 | /** 191 | * Converts a valid and acceptable geographic coordinates to decimal degrees coordinate. 192 | * 193 | * @param string $coordinates A valid and acceptable geographic coordinates. 194 | * 195 | * @return array An array of coordinate in decimal degree. 196 | * 197 | * @throws InvalidArgumentException 198 | * 199 | * @see http://en.wikipedia.org/wiki/Geographic_coordinate_conversion 200 | */ 201 | private function toDecimalDegrees($coordinates) 202 | { 203 | // Degrees, Decimal Minutes format (DD MM.mmm) 204 | // N 40°26.7717 E 79°56.93172 205 | // N40°26.7717E79°56.93172 206 | // N 25°59.86′, W 21°09.81′ 207 | if (preg_match('/([ns]{1})\s?([0-9]{1,2})\D+([0-9]{1,2}\.?\d*)\D*[, ]?([we]{1})\s? ?([0-9]{1,3})\D+([0-9]{1,2}\.?\d*)\D*$/i', $coordinates, $match)) { 208 | $latitude = $match[2] + $match[3] / 60; 209 | $longitude = $match[5] + $match[6] / 60; 210 | 211 | return [ 212 | 'N' === strtoupper($match[1]) ? $latitude : -$latitude, 213 | 'E' === strtoupper($match[4]) ? $longitude : -$longitude, 214 | ]; 215 | } 216 | 217 | // 40.446195, -79.948862 218 | if (preg_match('/(\-?[0-9]{1,2}\.?\d*)[, ] ?(\-?[0-9]{1,3}\.?\d*)$/', $coordinates, $match)) { 219 | return array($match[1], $match[2]); 220 | } 221 | 222 | // 40° 26.7717, -79° 56.93172 223 | if (preg_match('/(\-?[0-9]{1,2})\D+([0-9]{1,2}\.?\d*)[, ] ?(\-?[0-9]{1,3})\D+([0-9]{1,2}\.?\d*)$/i', 224 | $coordinates, $match)) { 225 | return array( 226 | $match[1] < 0 227 | ? $match[1] - $match[2] / 60 228 | : $match[1] + $match[2] / 60, 229 | $match[3] < 0 230 | ? $match[3] - $match[4] / 60 231 | : $match[3] + $match[4] / 60 232 | ); 233 | } 234 | 235 | // 40.446195N 79.948862W 236 | if (preg_match('/([0-9]{1,2}\.?\d*)\D*([ns]{1})[, ] ?([0-9]{1,3}\.?\d*)\D*([we]{1})$/i', $coordinates, $match)) { 237 | return array( 238 | 'N' === strtoupper($match[2]) ? $match[1] : -$match[1], 239 | 'E' === strtoupper($match[4]) ? $match[3] : -$match[3] 240 | ); 241 | } 242 | 243 | // 40°26.7717S 79°56.93172E 244 | // 25°59.86′N,21°09.81′W 245 | if (preg_match('/([0-9]{1,2})\D+([0-9]{1,2}\.?\d*)\D*([ns]{1})[, ] ?([0-9]{1,3})\D+([0-9]{1,2}\.?\d*)\D*([we]{1})$/i', 246 | $coordinates, $match)) { 247 | $latitude = $match[1] + $match[2] / 60; 248 | $longitude = $match[4] + $match[5] / 60; 249 | 250 | return array( 251 | 'N' === strtoupper($match[3]) ? $latitude : -$latitude, 252 | 'E' === strtoupper($match[6]) ? $longitude : -$longitude 253 | ); 254 | } 255 | 256 | // 40:26:46N, 079:56:55W 257 | // 40:26:46.302N 079:56:55.903W 258 | // 40°26′47″N 079°58′36″W 259 | // 40d 26′ 47″ N 079d 58′ 36″ W 260 | if (preg_match('/([0-9]{1,2})\D+([0-9]{1,2})\D+([0-9]{1,2}\.?\d*)\D*([ns]{1})[, ] ?([0-9]{1,3})\D+([0-9]{1,2})\D+([0-9]{1,2}\.?\d*)\D*([we]{1})$/i', 261 | $coordinates, $match)) { 262 | $latitude = $match[1] + ($match[2] * 60 + $match[3]) / 3600; 263 | $longitude = $match[5] + ($match[6] * 60 + $match[7]) / 3600; 264 | return array( 265 | 'N' === strtoupper($match[4]) ? $latitude : -$latitude, 266 | 'E' === strtoupper($match[8]) ? $longitude : -$longitude 267 | ); 268 | } 269 | 270 | throw new InvalidArgumentException( 271 | 'It should be a valid and acceptable ways to write geographic coordinates !' 272 | ); 273 | } 274 | 275 | /** 276 | * {@inheritDoc} 277 | */ 278 | #[\ReturnTypeWillChange] 279 | public function jsonSerialize() 280 | { 281 | return [$this->latitude, $this->longitude]; 282 | } 283 | 284 | /** 285 | * Returns a boolean determining coordinates equality 286 | * @param Coordinate $coordinate 287 | * @return boolean 288 | */ 289 | public function isEqual(Coordinate $coordinate) { 290 | return bccomp($this->latitude, $coordinate->getLatitude(), $this->getPrecision()) === 0 && bccomp($this->longitude, $coordinate->getLongitude(), $this->getPrecision()) === 0; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Vertex/Vertex.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Vertex; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\CoordinateInterface; 16 | use League\Geotools\Coordinate\Ellipsoid; 17 | use League\Geotools\CoordinateCouple; 18 | use League\Geotools\Geotools; 19 | 20 | /** 21 | * Vertex class 22 | * 23 | * @author Antoine Corcy 24 | */ 25 | class Vertex implements VertexInterface 26 | { 27 | use CoordinateCouple; 28 | 29 | /** 30 | * @var double 31 | */ 32 | protected $gradient; 33 | 34 | /** 35 | * @var double 36 | */ 37 | protected $ordinateIntercept; 38 | 39 | /** 40 | * @var integer 41 | */ 42 | private $precision = 8; 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function setFrom(CoordinateInterface $from) 48 | { 49 | $this->from = $from; 50 | 51 | if (empty($this->to) || ($this->to->getLatitude() - $this->from->getLatitude() === 0)) { 52 | return $this; 53 | } 54 | 55 | if ($this->to->getLatitude() !== $this->from->getLatitude()) { 56 | $this->gradient = ($this->to->getLongitude() - $this->from->getLongitude()) / ($this->to->getLatitude() - $this->from->getLatitude()); 57 | $this->ordinateIntercept = $this->from->getLongitude() - $this->from->getLatitude() * $this->gradient; 58 | } else { 59 | $this->gradient = null; 60 | $this->ordinateIntercept = null; 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function getFrom() 70 | { 71 | return $this->from; 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | public function setTo(CoordinateInterface $to) 78 | { 79 | $this->to = $to; 80 | 81 | if (empty($this->from) || ($this->to->getLatitude() - $this->from->getLatitude() === 0)) { 82 | return $this; 83 | } 84 | 85 | if ($this->to->getLatitude() !== $this->from->getLatitude()) { 86 | $this->gradient = ($this->to->getLongitude() - $this->from->getLongitude()) / ($this->to->getLatitude() - $this->from->getLatitude()); 87 | $this->ordinateIntercept = $this->from->getLongitude() - $this->from->getLatitude() * $this->gradient; 88 | } else { 89 | $this->gradient = null; 90 | $this->ordinateIntercept = null; 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * {@inheritDoc} 98 | */ 99 | public function getTo() 100 | { 101 | return $this->to; 102 | } 103 | 104 | /** 105 | * {@inheritDoc} 106 | */ 107 | public function getGradient() 108 | { 109 | return $this->gradient; 110 | } 111 | 112 | /** 113 | * {@inheritDoc} 114 | */ 115 | public function getOrdinateIntercept() 116 | { 117 | return $this->ordinateIntercept; 118 | } 119 | 120 | /** 121 | * @return integer 122 | */ 123 | public function getPrecision() 124 | { 125 | return $this->precision; 126 | } 127 | 128 | /** 129 | * @param integer $precision 130 | * @return $this 131 | */ 132 | public function setPrecision($precision) 133 | { 134 | $this->precision = $precision; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Returns the initial bearing from the origin coordinate 141 | * to the destination coordinate in degrees. 142 | * 143 | * @return float The initial bearing in degrees 144 | */ 145 | public function initialBearing() 146 | { 147 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 148 | 149 | $latA = deg2rad($this->from->getLatitude()); 150 | $latB = deg2rad($this->to->getLatitude()); 151 | $dLng = deg2rad($this->to->getLongitude() - $this->from->getLongitude()); 152 | 153 | $y = sin($dLng) * cos($latB); 154 | $x = cos($latA) * sin($latB) - sin($latA) * cos($latB) * cos($dLng); 155 | 156 | return fmod(rad2deg(atan2($y, $x)) + 360, 360); 157 | } 158 | 159 | /** 160 | * Returns the final bearing from the origin coordinate 161 | * to the destination coordinate in degrees. 162 | * 163 | * @return float The final bearing in degrees 164 | */ 165 | public function finalBearing() 166 | { 167 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 168 | 169 | $latA = deg2rad($this->to->getLatitude()); 170 | $latB = deg2rad($this->from->getLatitude()); 171 | $dLng = deg2rad($this->from->getLongitude() - $this->to->getLongitude()); 172 | 173 | $y = sin($dLng) * cos($latB); 174 | $x = cos($latA) * sin($latB) - sin($latA) * cos($latB) * cos($dLng); 175 | 176 | return fmod(fmod(rad2deg(atan2($y, $x)) + 360, 360) + 180, 360); 177 | } 178 | 179 | /** 180 | * Returns the initial cardinal point / direction from the origin coordinate to 181 | * the destination coordinate. 182 | * @see http://en.wikipedia.org/wiki/Cardinal_direction 183 | * 184 | * @return string The initial cardinal point / direction 185 | */ 186 | public function initialCardinal() 187 | { 188 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 189 | 190 | return Geotools::$cardinalPoints[(integer) round($this->initialBearing() / 22.5)]; 191 | } 192 | 193 | /** 194 | * Returns the final cardinal point / direction from the origin coordinate to 195 | * the destination coordinate. 196 | * @see http://en.wikipedia.org/wiki/Cardinal_direction 197 | * 198 | * @return string The final cardinal point / direction 199 | */ 200 | public function finalCardinal() 201 | { 202 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 203 | 204 | return Geotools::$cardinalPoints[(integer) round($this->finalBearing() / 22.5)]; 205 | } 206 | 207 | /** 208 | * Returns the half-way point / coordinate along a great circle 209 | * path between the origin and the destination coordinates. 210 | * 211 | * @return CoordinateInterface 212 | */ 213 | public function middle() 214 | { 215 | Ellipsoid::checkCoordinatesEllipsoid($this->from, $this->to); 216 | 217 | $latA = deg2rad($this->from->getLatitude()); 218 | $lngA = deg2rad($this->from->getLongitude()); 219 | $latB = deg2rad($this->to->getLatitude()); 220 | $lngB = deg2rad($this->to->getLongitude()); 221 | 222 | $bx = cos($latB) * cos($lngB - $lngA); 223 | $by = cos($latB) * sin($lngB - $lngA); 224 | 225 | $lat3 = rad2deg(atan2(sin($latA) + sin($latB), sqrt((cos($latA) + $bx) * (cos($latA) + $bx) + $by * $by))); 226 | $lng3 = rad2deg($lngA + atan2($by, cos($latA) + $bx)); 227 | 228 | return new Coordinate([$lat3, $lng3], $this->from->getEllipsoid()); 229 | } 230 | 231 | /** 232 | * Returns the destination point with a given bearing in degrees travelling along a 233 | * (shortest distance) great circle arc and a distance in meters. 234 | * 235 | * @param integer $bearing The bearing of the origin in degrees. 236 | * @param integer $distance The distance from the origin in meters. 237 | * 238 | * @return CoordinateInterface 239 | */ 240 | public function destination($bearing, $distance) 241 | { 242 | $lat = deg2rad($this->from->getLatitude()); 243 | $lng = deg2rad($this->from->getLongitude()); 244 | 245 | $bearing = deg2rad($bearing); 246 | 247 | $endLat = asin(sin($lat) * cos($distance / $this->from->getEllipsoid()->getA()) + cos($lat) * 248 | sin($distance / $this->from->getEllipsoid()->getA()) * cos($bearing)); 249 | $endLon = $lng + atan2(sin($bearing) * sin($distance / $this->from->getEllipsoid()->getA()) * cos($lat), 250 | cos($distance / $this->from->getEllipsoid()->getA()) - sin($lat) * sin($endLat)); 251 | 252 | return new Coordinate([rad2deg($endLat), rad2deg($endLon)], $this->from->getEllipsoid()); 253 | } 254 | 255 | /** 256 | * Returns true if the vertex passed on argument is on the same line as this object 257 | * 258 | * @param Vertex $vertex The vertex to compare 259 | * @return boolean 260 | */ 261 | public function isOnSameLine(Vertex $vertex) { 262 | if (is_null($this->getGradient()) && is_null($vertex->getGradient()) && $this->from->getLongitude() == $vertex->getFrom()->getLongitude()) { 263 | return true; 264 | } elseif (!is_null($this->getGradient()) && !is_null($vertex->getGradient())) { 265 | return ( 266 | bccomp($this->getGradient(), $vertex->getGradient(), $this->getPrecision()) === 0 267 | && 268 | bccomp($this->getOrdinateIntercept(), $vertex->getOrdinateIntercept(), $this->getPrecision()) ===0 269 | ); 270 | } else { 271 | return false; 272 | } 273 | } 274 | 275 | /** 276 | * Returns the other coordinate who is not the coordinate passed on argument 277 | * @param CoordinateInterface $coordinate 278 | * @return null|Coordinate 279 | */ 280 | public function getOtherCoordinate(CoordinateInterface $coordinate) { 281 | if ($coordinate->isEqual($this->from)) { 282 | return $this->to; 283 | } else if ($coordinate->isEqual($this->to)) { 284 | return $this->from; 285 | } 286 | return null; 287 | } 288 | 289 | /** 290 | * Returns the determinant value between $this (vertex) and another vertex. 291 | * 292 | * @param Vertex $vertex [description] 293 | * @return [type] [description] 294 | */ 295 | public function getDeterminant(Vertex $vertex) { 296 | $abscissaVertexOne = $this->to->getLatitude() - $this->from->getLatitude(); 297 | $ordinateVertexOne = $this->to->getLongitude() - $this->from->getLongitude(); 298 | $abscissaVertexSecond = $vertex->getTo()->getLatitude() - $vertex->getFrom()->getLatitude(); 299 | $ordinateVertexSecond = $vertex->getTo()->getLongitude() - $vertex->getFrom()->getLongitude(); 300 | 301 | return bcsub( 302 | bcmul($abscissaVertexOne, $ordinateVertexSecond, $this->precision), 303 | bcmul($abscissaVertexSecond, $ordinateVertexOne, $this->precision), 304 | $this->precision 305 | ); 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 1.3.0 5 | ----- 6 | ### Fixed 7 | 8 | - PHP 8.4 support by @batyrmastyr in #194 9 | 10 | 1.2.0 11 | ----- 12 | ### Fixed 13 | 14 | - PHP 8.2 CI support by @phpfui in #184 15 | - Add support for Symfony 7/Laravel 11 by @dwightwatson in #188 16 | - OpenStreetMap typo fixed #187. by @Surfoo in #189 17 | - Ability to receive codes of neighboring points by @OleksandrWebLab in #185 18 | 19 | 1.1.0 20 | ----- 21 | ### Fixed 22 | 23 | - Suppressed of deprecation warnings 24 | - increased precision in calculation 25 | - Compatility with different locales 26 | 27 | 1.0.0 28 | ----- 29 | ### Fixed 30 | 31 | - greatCircle calculator returned NAN #159 32 | - Suppressed deprecation warning for JsonSerializable in PHP 8.1 #167 33 | - Added floor on implicit float to in conversion #166 34 | - Haversine distance calculation #158 35 | 36 | ### Added 37 | 38 | - New coordinate format "Degree Decimal Minutes" available with `toDegreeDecimalMinutes()` [BC break] 39 | - Support PHP 8.1 40 | - Support Symfony 6, removed Symfony 3.4 41 | 42 | ### Change 43 | 44 | - Increased precision of `initialBearing()` and `finalBearing()` 45 | 46 | 0.8.3 47 | ----- 48 | 49 | ### Fixed 50 | 51 | - Fix incompatibility with PHP >= 7.4 by saving longitude and latitude as a string 52 | 53 | ### Changed 54 | 55 | - Use react/event-loop: 1.0 56 | 57 | 0.8.2 58 | ----- 59 | 60 | ### Fixed 61 | 62 | - Fix namespace issue when creating provider class name 63 | - PHPUnit deprecations 64 | 65 | ### Changed 66 | 67 | - Supported PHP versions >= 7.3 68 | - PHPUnit 8.5 69 | 70 | 0.8.1 71 | ----- 72 | 73 | ### Fixed 74 | 75 | - Symfony 5 support. 76 | 77 | 0.8.0 (2018-02-22) 78 | ------------------ 79 | 80 | ### Added 81 | 82 | - We use `willdurand/geocoder` 4. 83 | - Add a method to vertex to compute the determinant with another vertex 84 | - `ArrayCollection::merge` 85 | - `BoundingBox::merge` 86 | - `BoundingBoxInterface::merge` 87 | - `BoundingBoxInterface::getAsPolygon` 88 | - Added abstract class `GeometryCollection` 89 | - Added `GeometryInterface` 90 | - Added `MultiPolygon` 91 | 92 | ### Changes 93 | 94 | - Renamed `BoundingBox::getPolygon` to `BoundingBox::getAsPolygon` 95 | - `PolygonInterface` extends `GeometryInterface` 96 | 97 | ### Fixed 98 | 99 | - Decimal-Degrees parser from Decimal-Minutes 100 | 101 | ### Removed 102 | 103 | - Removed `AbstractGeotools` class in favor of `CoordinateCouple`. Also added constants to `GeotoolsInterface`. 104 | - Our HTTP layer in favor of HTTPlug 105 | - Our cache layer in favor of PSR-6 106 | 107 | 0.7.0 (2016-02-03) 108 | ------------------ 109 | 110 | * Updated: `Point` is now `Vertex` [BC break] 111 | * Updated: use `Predis` 1.0 version 112 | * Updated: tests against PHP7 113 | * Updated: documentation and badges 114 | * Added: allow Symfony console, property-access and serializer ~3.0 115 | 116 | 0.6.0 (2015-10-11) 117 | ------------------ 118 | 119 | * Fixed: cache layer: Redis, Memcached and MongoDB 120 | * Added: cache possibility in CLI 121 | * Added: 10:10 algorithm 122 | * Updated: symfony console, serializer and property-access to ~2.7 123 | 124 | 0.5.0 (2015-10-10) 125 | ------------------ 126 | 127 | * Updated: use Geocoder 3.2.x 128 | * Added: Polygon class 129 | * Added: Bounding box class 130 | * Fixed: division by zero in vincenty algorithm 131 | * Dropped: PHP 5.3 and stub to JsonSerializable 132 | * Updated: switch from async to promise 133 | * Updated: documentation 134 | * Added: code of conduct 135 | 136 | 0.4.0 (2014-07-30) 137 | ------------------ 138 | 139 | * Uses: PSR-4 140 | * Removed: not relevant autoloads 141 | * Fixed: tests 142 | * Fixed: typos 143 | 144 | 0.3.3 (2014-05-16) 145 | ------------------ 146 | 147 | * Fixed: HHVM compatible tested on `HipHop VM 3.1.0-dev+2014.05.15` 148 | * Added: falling tests in Distance with same coordinates (@kornrunner) 149 | * Fixed: division by zero in computing distance between 2 identical coordinates (@kornrunner) 150 | * Added: `setFromString` method to create and modify coordinate + doc - fix #31 151 | * Fixed: coordinate parsing issue 152 | 153 | 0.3.2 (2014-03-15) 154 | ------------------ 155 | 156 | * Updated: geotools CLI moved in bin folder 157 | * Updated: use Geocoder 2.4.x 158 | * Added: great circle formula and CLI + tests 159 | * Added: test against php 5.6 160 | * Updated: repo name 161 | * Added: coverage and scrutinizer-ci badges 162 | * Updated: organisation name 163 | * Added: test against HHVM 164 | 165 | 0.3.1 (2013-11-16) 166 | ------------------ 167 | 168 | * Updated: use Geocoder 2.3.x 169 | * Updated: use SensioLabs Insight 170 | * Updated: documentation 171 | * Fixed: travis, packagist and sensiolabs insight badges 172 | * Fixed: tests 173 | 174 | 0.3.0 (2013-07-19) 175 | ------------------ 176 | 177 | * Updated: loep (The League of Extraordinary Packages) is now owner 178 | * Updated: use Geocoder 2.0.0 179 | 180 | 0.2.4 (2013-05-03) 181 | ------------------ 182 | 183 | * Updated: made it working with Geocoder 1.5.0 184 | * Updated: integration with frameworks in features list 185 | * Added: integration with Silex 186 | * Added: integration with frameworks 187 | * Updated: Contribution doc 188 | * Added: memcached and mongo extensions to travis-ci 189 | * Added: mongodb service to travis-ci 190 | * Added: expire to Memcached cache - fix #26 191 | * Added: expire to Redis cache + test - fix #26 192 | 193 | 0.2.3 (2013-03-29) 194 | ------------------ 195 | 196 | * Updated: MongoDB test coverage 197 | * Added: Memcached cache test - fix #22 198 | * Refactored: Redis and MongoDB caches tests 199 | * Added: MongoDB cache test - fix #22 200 | * Added: Redis cache test - fix #22 201 | * Added: Memcached cache - fix #24 202 | 203 | 0.2.2 (2013-03-26) 204 | ------------------ 205 | 206 | * Added: Redis cache - fix #23 207 | * Updated: MongoDB cache search by key 208 | * Fixed: MangoDB cache 209 | * Updated: doc with try .. catch bloc 210 | * Fixed: Batch test for php 5.3 211 | * Added: Cache interface + mongoDB - fix #2 212 | * Refactored: Point test 213 | 214 | 0.2.1 (2013-03-16) 215 | ------------------ 216 | 217 | * Added: arcgis_online provider 218 | * Merge branch 'BatchImproved' 219 | * Fixed: batched result object embed provider's name, query and exception string - fix #6 220 | * Added: Geocoder dev-master as require-dev 221 | * Fixed: CLI tests 222 | 223 | 0.2.0 (2013-03-12) 224 | ------------------ 225 | 226 | * Fixed: empty ellipsoid name throws an exception now 227 | * Added: Ellipsoid support to Point, Convert and Distance CLI - fix #7 228 | * Added: Ellipsoid support to Point CLI 229 | * Added: Ellipsoid support to Distance CLI 230 | * Added: Ellipsoid support to Convert CLI 231 | * Added: TomTom provier to CLI - fix #14 232 | * Changed: mile parameter to mi in Distance to be more consistent [BC break] 233 | 234 | 0.1.12 (2013-03-08) 235 | ------------------- 236 | 237 | * Added: command exemples and refactoring 238 | * Added: help to geocoding and reverse geocoding CLI 239 | * Fixed: homepage in composer.json 240 | * Updated: doc and composer.json 241 | * Fixed: php warning in CLI on wrong providers constuction arguments 242 | * Updated: list of contributors 243 | * Updated: geohash doc 244 | 245 | 0.1.11 (2013-03-05) 246 | ------------------- 247 | 248 | * Added: international feet unit to CLI + test - fix #10 249 | * Updated: relative links to absolute ones 250 | * Added: ip_geo_base and baidu as CLI providers - fix #8 251 | * Fixed: feet unit + test 252 | * Added International Feet as a unit 253 | * Added a bunch of tests. 254 | 255 | 0.1.10 (2013-02-27) 256 | ------------------- 257 | 258 | * Added: support of different ellipsoid + doc + tests - fix #5 259 | * Refactored: Doc + CLI commands + tests 260 | * Improved: geocoder:geocode and geocoder:reverse CLI + tests 261 | * Added: lowerize() method using mbstring extension 262 | 263 | 0.1.9 (2013-02-24) 264 | ------------------ 265 | 266 | * Added: dumper option to geocoder:geocode CLI + test 267 | * Fixed: composer.json 268 | * Added: cURL requirement for tests and CLI 269 | * Removed: old files 270 | * Fixed: finalCardinal() into CLI + test 271 | 272 | 0.1.8 (2013-02-21) 273 | ------------------ 274 | 275 | * Refactored: getAdapter and getProvider in CLI 276 | * Added: CLI for Geocoder class + tests 277 | * Added: CLI for Geocoder class + tests 278 | * Updated: composer installation info 279 | * Added: logo to CLI 280 | * Fixed: travis-ci config 281 | * Added: finalBearing() to CLI + test 282 | * Added: finalCardinal() method + test 283 | * Updated: cardinal() method to initialCardinal() [BC break] 284 | * Added: finalBearing() + test 285 | * Renamed: bearing() method to initialBearing() [BC break] 286 | 287 | 0.1.7 (2013-02-20) 288 | ------------------ 289 | 290 | * Added: CLI for Convert class + tests 291 | * Added: CLI for Geohash class + tests 292 | * Updated: doc with internal links 293 | * Fixed: CLI include paths 294 | 295 | 0.1.6 (2013-02-20) 296 | ------------------ 297 | 298 | * Added: CLI for Distance and Point classes + tests 299 | * Updated: phpunit bootstrap 300 | * Updated: composer installation info 301 | * Updated: Convert UTM zone exceptions are covered 302 | * Updated: Point and Distance chainable logic and refactoring [BC break] 303 | * Added: UTM conversion + tests + doc 304 | * Updated: geodetic datum into doc 305 | 306 | 0.1.5 (2013-02-13) 307 | ------------------ 308 | 309 | * Added: Convert class, tests and doc 310 | * Updated: doc about Coordinate class 311 | * Updated: method visibility in Coordinate class 312 | * Added: Coordinate class support different DMS coordinates 313 | * Fixed: thrown message on invalid coordinate 314 | * Fixed: typo calculate to compute 315 | 316 | 0.1.4 (2013-02-10) 317 | ------------------ 318 | 319 | * Updated: Batch class test 320 | * Added: test to AbstractGeotools class 321 | * Refactored: Batch tests 322 | * Updated: doc with a better batch exemple 323 | * Added: batch a set of values/coordinates againt a set of providers + tests 324 | * Fixed: changelog list 325 | 326 | 0.1.3 (2013-02-09) 327 | ------------------ 328 | 329 | * Added: geohash ref to the doc 330 | * Refactored: tests 331 | * Added: Geohash class, tests and doc 332 | * Added: normalize methods to Coordinate class 333 | * Updated: Coordinate support string in its constructor 334 | * Updated: Testcase's expects methods 335 | * Updated: test to Batch class 336 | * Refactored: Batch test class 337 | * Updated: TestCase stub clases 338 | 339 | 0.1.2 (2013-02-08) 340 | ------------------ 341 | 342 | * Fixed: test to be compatible with PHP 5.3.x 343 | * Added: test to Distance class 344 | * Added: test to Batch class 345 | * Added: test to Point class 346 | * Updated: test to Getools class with a CoordinateInterface stub 347 | * Updated: Contributing doc 348 | * Updated: test to Geotools class 349 | * Added: test to Coordinate class 350 | * Added: test to Geotools class 351 | 352 | 0.1.1 (2013-02-06) 353 | ------------------ 354 | 355 | * Fixed: the minimum-stability of React/Async 356 | 357 | 0.1.0 (2013-02-06) 358 | ------------------ 359 | 360 | * Added: Contributing doc 361 | * Added: Travis-ci to the doc 362 | * Added: stillmaintained.com to the doc 363 | * Initial import 364 | -------------------------------------------------------------------------------- /src/Batch/Batch.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Batch; 13 | 14 | use Geocoder\Geocoder; 15 | use Geocoder\ProviderAggregator; 16 | use League\Geotools\Coordinate\CoordinateInterface; 17 | use League\Geotools\Exception\InvalidArgumentException; 18 | use Psr\Cache\CacheItemPoolInterface; 19 | use React\EventLoop\Factory as EventLoopFactory; 20 | use React\Promise\Deferred; 21 | 22 | /** 23 | * Batch class 24 | * 25 | * @author Antoine Corcy 26 | */ 27 | class Batch implements BatchInterface 28 | { 29 | /** 30 | * The Geocoder instance to use. 31 | * 32 | * @var ProviderAggregator 33 | */ 34 | protected $geocoder; 35 | 36 | /** 37 | * An array of closures. 38 | * 39 | * @var array 40 | */ 41 | protected $tasks; 42 | 43 | /** 44 | * The cache instance to use. 45 | * 46 | * @var CacheItemPoolInterface 47 | */ 48 | protected $cache; 49 | 50 | /** 51 | * Set the Geocoder instance to use. 52 | * 53 | * @param ProviderAggregator $geocoder The Geocoder instance to use. 54 | */ 55 | public function __construct(ProviderAggregator $geocoder) 56 | { 57 | $this->geocoder = $geocoder; 58 | } 59 | 60 | /** 61 | * Check against the cache instance if any. 62 | * 63 | * @param string $providerName The name of the provider. 64 | * @param string $query The query string. 65 | * 66 | * @return boolean|BatchGeocoded The BatchGeocoded object from the query or the cache instance. 67 | */ 68 | public function isCached($providerName, $query) 69 | { 70 | if (null === $this->cache) { 71 | return false; 72 | } 73 | 74 | $item = $this->cache->getItem($this->getCacheKey($providerName, $query)); 75 | 76 | if ($item->isHit()) { 77 | return $item->get(); 78 | } 79 | 80 | return false; 81 | } 82 | 83 | 84 | /** 85 | * Cache the BatchGeocoded object. 86 | * 87 | * @param BatchGeocoded $geocoded The BatchGeocoded object to cache. 88 | * 89 | * @return BatchGeocoded The BatchGeocoded object. 90 | */ 91 | public function cache(BatchGeocoded $geocoded) 92 | { 93 | if (isset($this->cache)) { 94 | $key = $this->getCacheKey($geocoded->getProviderName(), $geocoded->getQuery()); 95 | $item = $this->cache->getItem($key); 96 | $item->set($geocoded); 97 | $this->cache->save($item); 98 | } 99 | 100 | return $geocoded; 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | public function geocode($values) 107 | { 108 | $geocoder = $this->geocoder; 109 | $cache = $this; 110 | 111 | foreach ($geocoder->getProviders() as $provider) { 112 | if (is_array($values) && count($values) > 0) { 113 | foreach ($values as $value) { 114 | $this->tasks[] = function () use ($geocoder, $provider, $value, $cache) { 115 | $deferred = new Deferred; 116 | 117 | try { 118 | if ($cached = $cache->isCached($provider->getName(), $value)) { 119 | $deferred->resolve($cached); 120 | } else { 121 | $batchResult = new BatchResult($provider->getName(), $value); 122 | $address = $geocoder->using($provider->getName())->geocode($value)->first(); 123 | $deferred->resolve($cache->cache($batchResult->createFromAddress($address))); 124 | } 125 | } catch (\Exception $e) { 126 | $batchGeocoded = new BatchResult($provider->getName(), $value, $e->getMessage()); 127 | $deferred->resolve($batchGeocoded->newInstance()); 128 | } 129 | 130 | return $deferred->promise(); 131 | }; 132 | } 133 | } elseif (is_string($values) && '' !== trim($values)) { 134 | $this->tasks[] = function () use ($geocoder, $provider, $values, $cache) { 135 | $deferred = new Deferred; 136 | 137 | try { 138 | if ($cached = $cache->isCached($provider->getName(), $values)) { 139 | $deferred->resolve($cached); 140 | } else { 141 | $batchResult = new BatchResult($provider->getName(), $values); 142 | $address = $geocoder->using($provider->getName())->geocode($values)->first(); 143 | $deferred->resolve($cache->cache($batchResult->createFromAddress($address))); 144 | } 145 | } catch (\Exception $e) { 146 | $batchGeocoded = new BatchResult($provider->getName(), $values, $e->getMessage()); 147 | $deferred->resolve($batchGeocoded->newInstance()); 148 | } 149 | 150 | return $deferred->promise(); 151 | }; 152 | } else { 153 | throw new InvalidArgumentException( 154 | 'The argument should be a string or an array of strings to geocode.' 155 | ); 156 | } 157 | } 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * {@inheritDoc} 164 | */ 165 | public function reverse($coordinates) 166 | { 167 | $geocoder = $this->geocoder; 168 | $cache = $this; 169 | 170 | foreach ($geocoder->getProviders() as $provider) { 171 | if (is_array($coordinates) && count($coordinates) > 0) { 172 | foreach ($coordinates as $coordinate) { 173 | $this->tasks[] = function () use ($geocoder, $provider, $coordinate, $cache) { 174 | $deferred = new Deferred(); 175 | 176 | $valueCoordinates = sprintf('%s, %s', $coordinate->getLatitude(), $coordinate->getLongitude()); 177 | try { 178 | if ($cached = $cache->isCached($provider->getName(), $valueCoordinates)) { 179 | $deferred->resolve($cached); 180 | } else { 181 | $batchResult = new BatchResult($provider->getName(), $valueCoordinates); 182 | $address = $geocoder->using($provider->getName())->reverse( 183 | $coordinate->getLatitude(), 184 | $coordinate->getLongitude() 185 | )->first(); 186 | 187 | $deferred->resolve($cache->cache($batchResult->createFromAddress($address))); 188 | } 189 | } catch (\Exception $e) { 190 | $batchGeocoded = new BatchResult($provider->getName(), $valueCoordinates, $e->getMessage()); 191 | $deferred->resolve($batchGeocoded->newInstance()); 192 | } 193 | 194 | return $deferred->promise(); 195 | }; 196 | } 197 | } elseif ($coordinates instanceOf CoordinateInterface) { 198 | $this->tasks[] = function () use ($geocoder, $provider, $coordinates, $cache) { 199 | $deferred = new Deferred(); 200 | 201 | $valueCoordinates = sprintf('%s, %s', $coordinates->getLatitude(), $coordinates->getLongitude()); 202 | try { 203 | if ($cached = $cache->isCached($provider->getName(), $valueCoordinates)) { 204 | $deferred->resolve($cached); 205 | } else { 206 | $batchResult = new BatchResult($provider->getName(), $valueCoordinates); 207 | $address = $geocoder->using($provider->getName())->reverse( 208 | $coordinates->getLatitude(), 209 | $coordinates->getLongitude() 210 | )->first(); 211 | $deferred->resolve($cache->cache($batchResult->createFromAddress($address))); 212 | } 213 | } catch (\Exception $e) { 214 | $batchGeocoded = new BatchResult($provider->getName(), $valueCoordinates, $e->getMessage()); 215 | $deferred->resolve($batchGeocoded->newInstance()); 216 | } 217 | 218 | return $deferred->promise(); 219 | }; 220 | } else { 221 | throw new InvalidArgumentException( 222 | 'The argument should be a Coordinate instance or an array of Coordinate instances to reverse.' 223 | ); 224 | } 225 | } 226 | 227 | return $this; 228 | } 229 | 230 | /** 231 | * {@inheritDoc} 232 | * 233 | * $this cannot be used in anonymous function in PHP 5.3.x 234 | * @see http://php.net/manual/en/functions.anonymous.php 235 | */ 236 | public function serie() 237 | { 238 | $computedInSerie = array(); 239 | 240 | foreach ($this->tasks as $task) { 241 | $task()->then(function($result) use (&$computedInSerie) { 242 | $computedInSerie[] = $result; 243 | }, function ($emptyResult) use (&$computedInSerie) { 244 | $computedInSerie[] = $emptyResult; 245 | }); 246 | } 247 | 248 | return $computedInSerie; 249 | } 250 | 251 | /** 252 | * {@inheritDoc} 253 | * 254 | * $this cannot be used in anonymous function in PHP 5.3.x 255 | * @see http://php.net/manual/en/functions.anonymous.php 256 | */ 257 | public function parallel() 258 | { 259 | $loop = EventLoopFactory::create(); 260 | $computedInParallel = array(); 261 | 262 | foreach ($this->tasks as $task) { 263 | $loop->futureTick(function () use ($task, &$computedInParallel) { 264 | $task()->then(function($result) use (&$computedInParallel) { 265 | $computedInParallel[] = $result; 266 | }, function ($emptyResult) use (&$computedInParallel) { 267 | $computedInParallel[] = $emptyResult; 268 | }); 269 | }); 270 | } 271 | 272 | $loop->run(); 273 | 274 | return $computedInParallel; 275 | } 276 | 277 | /** 278 | * {@inheritDoc} 279 | */ 280 | public function setCache(CacheItemPoolInterface $cache) 281 | { 282 | $this->cache = $cache; 283 | 284 | return $this; 285 | } 286 | 287 | private function getCacheKey(string $providerName, string $query): string 288 | { 289 | return sha1($providerName.'-'.$query); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/Coordinate/Ellipsoid.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Coordinate; 13 | 14 | use League\Geotools\Exception\InvalidArgumentException; 15 | use League\Geotools\Exception\NotMatchingEllipsoidException; 16 | 17 | /** 18 | * Ellipsoid class 19 | * 20 | * @author Antoine Corcy 21 | * 22 | * @see http://en.wikipedia.org/wiki/Reference_ellipsoid 23 | * @see http://www.colorado.edu/geography/gcraft/notes/datum/gif/ellipse.gif 24 | */ 25 | class Ellipsoid 26 | { 27 | /** 28 | * List of selected reference ellipsoids. 29 | * 30 | * @var string 31 | */ 32 | const AIRY = 'AIRY'; 33 | const AUSTRALIAN_NATIONAL = 'AUSTRALIAN_NATIONAL'; 34 | const BESSEL_1841 = 'BESSEL_1841'; 35 | const BESSEL_1841_NAMBIA = 'BESSEL_1841_NAMBIA'; 36 | const CLARKE_1866 = 'CLARKE_1866'; 37 | const CLARKE_1880 = 'CLARKE_1880'; 38 | const EVEREST = 'EVEREST'; 39 | const FISCHER_1960_MERCURY = 'FISCHER_1960_MERCURY'; 40 | const FISCHER_1968 = 'FISCHER_1968'; 41 | const GRS_1967 = 'GRS_1967'; 42 | const GRS_1980 = 'GRS_1980'; 43 | const HELMERT_1906 = 'HELMERT_1906'; 44 | const HOUGH = 'HOUGH'; 45 | const INTERNATIONAL = 'INTERNATIONAL'; 46 | const KRASSOVSKY = 'KRASSOVSKY'; 47 | const MODIFIED_AIRY = 'MODIFIED_AIRY'; 48 | const MODIFIED_EVEREST = 'MODIFIED_EVEREST'; 49 | const MODIFIED_FISCHER_1960 = 'MODIFIED_FISCHER_1960'; 50 | const SOUTH_AMERICAN_1969 = 'SOUTH_AMERICAN_1969'; 51 | const WGS60 = 'WGS60'; 52 | const WGS66 = 'WGS66'; 53 | const WGS72 = 'WGS72'; 54 | const WGS84 = 'WGS84'; 55 | 56 | /** 57 | * The name of the Ellipsoid. 58 | * 59 | * @var string 60 | */ 61 | protected $name; 62 | 63 | /** 64 | * The semi-major axis (equatorial radius) in meters. 65 | * @see http://en.wikipedia.org/wiki/Earth_radius 66 | * @see http://home.online.no/~sigurdhu/WGS84_Eng.html 67 | * 68 | * @var double 69 | */ 70 | protected $a; 71 | 72 | /** 73 | * The inverse flattening. 74 | * @see http://home.online.no/~sigurdhu/WGS84_Eng.html 75 | * 76 | * @var double 77 | */ 78 | protected $invF; 79 | 80 | /** 81 | * Selected reference ellipsoids. 82 | * Source: Defense Mapping Agency. 1987b. Washington, DC: Defense Mapping Agency 83 | * DMA Technical Report: Supplement to Department of Defense World Geodetic System 1984 Technical Report. 84 | * @see http://en.wikipedia.org/wiki/Geodetic_datum 85 | * @see http://www.colorado.edu/geography/gcraft/notes/datum/gif/refellip.gif 86 | * 87 | * @var array 88 | */ 89 | protected static $referenceEllipsoids = array( 90 | self::AIRY => array( 91 | 'name' => 'Airy', 92 | 'a' => 6377563.396, 93 | 'invF' => 299.3249646, 94 | ), 95 | self::AUSTRALIAN_NATIONAL => array( 96 | 'name' => 'Australian National', 97 | 'a' => 6378160.0, 98 | 'invF' => 298.25, 99 | ), 100 | self::BESSEL_1841 => array( 101 | 'name' => 'Bessel 1841', 102 | 'a' => 6377397.155, 103 | 'invF' => 299.1528128, 104 | ), 105 | self::BESSEL_1841_NAMBIA => array( 106 | 'name' => 'Bessel 1841 (Nambia)', 107 | 'a' => 6377483.865, 108 | 'invF' => 299.1528128, 109 | ), 110 | self::CLARKE_1866 => array( 111 | 'name' => 'Clarke 1866', 112 | 'a' => 6378206.4, 113 | 'invF' => 294.9786982, 114 | ), 115 | self::CLARKE_1880 => array( 116 | 'name' => 'Clarke 1880', 117 | 'a' => 6378249.145, 118 | 'invF' => 293.465, 119 | ), 120 | self::EVEREST => array( 121 | 'name' => 'Everest', 122 | 'a' => 6377276.345, 123 | 'invF' => 300.8017, 124 | ), 125 | self::FISCHER_1960_MERCURY => array( 126 | 'name' => 'Fischer 1960 (Mercury)', 127 | 'a' => 6378166.0, 128 | 'invF' => 298.3, 129 | ), 130 | self::FISCHER_1968 => array( 131 | 'name' => 'Fischer 1968', 132 | 'a' => 6378150.0, 133 | 'invF' => 298.3, 134 | ), 135 | self::GRS_1967 => array( 136 | 'name' => 'GRS 1967', 137 | 'a' => 6378160.0, 138 | 'invF' => 298.247167427, 139 | ), 140 | self::GRS_1980 => array( 141 | 'name' => 'GRS 1980', 142 | 'a' => 6378137, 143 | 'invF' => 298.257222101, 144 | ), 145 | self::HELMERT_1906 => array( 146 | 'name' => 'Helmert 1906', 147 | 'a' => 6378200.0, 148 | 'invF' => 298.3, 149 | ), 150 | self::HOUGH => array( 151 | 'name' => 'Hough', 152 | 'a' => 6378270.0, 153 | 'invF' => 297.0, 154 | ), 155 | self::INTERNATIONAL => array( 156 | 'name' => 'International', 157 | 'a' => 6378388.0, 158 | 'invF' => 297.0, 159 | ), 160 | self::KRASSOVSKY => array( 161 | 'name' => 'Krassovsky', 162 | 'a' => 6378245.0, 163 | 'invF' => 298.3, 164 | ), 165 | self::MODIFIED_AIRY => array( 166 | 'name' => 'Modified Airy', 167 | 'a' => 6377340.189, 168 | 'invF' => 299.3249646, 169 | ), 170 | self::MODIFIED_EVEREST => array( 171 | 'name' => 'Modified Everest', 172 | 'a' => 6377304.063, 173 | 'invF' => 300.8017, 174 | ), 175 | self::MODIFIED_FISCHER_1960 => array( 176 | 'name' => 'Modified Fischer 1960', 177 | 'a' => 6378155.0, 178 | 'invF' => 298.3, 179 | ), 180 | self::SOUTH_AMERICAN_1969 => array( 181 | 'name' => 'South American 1969', 182 | 'a' => 6378160.0, 183 | 'invF' => 298.25, 184 | ), 185 | self::WGS60 => array( 186 | 'name' => 'WGS 60', 187 | 'a' => 6378165.0, 188 | 'invF' => 298.3, 189 | ), 190 | self::WGS66 => array( 191 | 'name' => 'WGS 66', 192 | 'a' => 6378145.0, 193 | 'invF' => 298.25, 194 | ), 195 | self::WGS72 => array( 196 | 'name' => 'WGS 72', 197 | 'a' => 6378135.0, 198 | 'invF' => 298.26, 199 | ), 200 | self::WGS84 => array( 201 | 'name' => 'WGS 84', 202 | 'a' => 6378137.0, 203 | 'invF' => 298.257223563, 204 | ), 205 | ); 206 | 207 | 208 | /** 209 | * Create a new ellipsoid. 210 | * 211 | * @param string $name The name of the ellipsoid to create. 212 | * @param double $a The semi-major axis (equatorial radius) in meters. 213 | * @param double $invF The inverse flattening. 214 | * 215 | * @throws InvalidArgumentException 216 | */ 217 | public function __construct($name, $a, $invF) 218 | { 219 | if (0.0 >= (double) $invF) { 220 | throw new InvalidArgumentException('The inverse flattening cannot be negative or equal to zero !'); 221 | } 222 | 223 | $this->name = $name; 224 | $this->a = $a; 225 | $this->invF = $invF; 226 | } 227 | 228 | /** 229 | * Create the ellipsoid chosen by its name. 230 | * 231 | * @param string $name The name of the ellipsoid to create (optional). 232 | * 233 | * @return Ellipsoid 234 | */ 235 | public static function createFromName($name = self::WGS84) 236 | { 237 | $name = trim($name); 238 | 239 | if (empty($name)) { 240 | throw new InvalidArgumentException('Please provide an ellipsoid name !'); 241 | } 242 | 243 | if (!array_key_exists($name, self::$referenceEllipsoids)) { 244 | throw new InvalidArgumentException( 245 | sprintf('%s ellipsoid does not exist in selected reference ellipsoids !', $name) 246 | ); 247 | } 248 | 249 | return self::createFromArray(self::$referenceEllipsoids[$name]); 250 | } 251 | 252 | /** 253 | * Create an ellipsoid from an array. 254 | * 255 | * @param array $newEllipsoid The ellipsoid's parameters to create. 256 | * 257 | * @return Ellipsoid 258 | */ 259 | public static function createFromArray(array $newEllipsoid) 260 | { 261 | if (!isset($newEllipsoid['name']) || !isset($newEllipsoid['a']) || !isset($newEllipsoid['invF']) 262 | || 3 !== count($newEllipsoid)) { 263 | throw new InvalidArgumentException('Ellipsoid arrays should contain `name`, `a` and `invF` keys !'); 264 | } 265 | 266 | return new self($newEllipsoid['name'], $newEllipsoid['a'], $newEllipsoid['invF']); 267 | } 268 | 269 | /** 270 | * Check if coordinates have the same ellipsoid. 271 | * 272 | * @param CoordinateInterface $a A coordinate. 273 | * @param CoordinateInterface $b A coordinate. 274 | * 275 | * @throws NotMatchingEllipsoidException 276 | */ 277 | public static function checkCoordinatesEllipsoid(CoordinateInterface $a, CoordinateInterface $b) 278 | { 279 | if ($a->getEllipsoid() != $b->getEllipsoid()) { 280 | throw new NotMatchingEllipsoidException('The ellipsoids for both coordinates must match !'); 281 | } 282 | } 283 | 284 | /** 285 | * Returns the ellipsoid's name. 286 | * 287 | * @return string 288 | */ 289 | public function getName() 290 | { 291 | return $this->name; 292 | } 293 | 294 | /** 295 | * Returns the semi-major axis (equatorial radius) in meters. 296 | * 297 | * @return double 298 | */ 299 | public function getA() 300 | { 301 | return (double) $this->a; 302 | } 303 | 304 | /** 305 | * Computes and returns the semi-minor axis (polar distance) in meters. 306 | * @see http://home.online.no/~sigurdhu/WGS84_Eng.html 307 | * 308 | * @return double 309 | */ 310 | public function getB() 311 | { 312 | return (double) $this->a * (1 - 1 / $this->invF); 313 | } 314 | 315 | /** 316 | * Returns the inverse flattening. 317 | * 318 | * @return double 319 | */ 320 | public function getInvF() 321 | { 322 | return (double) $this->invF; 323 | } 324 | 325 | /** 326 | * Computes and returns the arithmetic mean radius in meters. 327 | * @see http://home.online.no/~sigurdhu/WGS84_Eng.html 328 | * 329 | * @return double 330 | */ 331 | public function getArithmeticMeanRadius() 332 | { 333 | return (double) $this->a * (1 - 1 / $this->invF / 3); 334 | } 335 | 336 | /** 337 | * Returns the list of available ellipsoids sorted by alphabetical order. 338 | * 339 | * @return string The list of available ellipsoids comma separated. 340 | */ 341 | public static function getAvailableEllipsoidNames() 342 | { 343 | ksort(self::$referenceEllipsoids); 344 | 345 | return implode(', ', array_keys(self::$referenceEllipsoids)); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Convert/Convert.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Convert; 13 | 14 | use League\Geotools\Coordinate\CoordinateInterface; 15 | use League\Geotools\Geotools; 16 | use League\Geotools\GeotoolsInterface; 17 | 18 | /** 19 | * Convert class 20 | * 21 | * @author Antoine Corcy 22 | */ 23 | class Convert implements ConvertInterface 24 | { 25 | /** 26 | * The coordinate to convert. 27 | * 28 | * @var CoordinateInterface 29 | */ 30 | protected $coordinates; 31 | 32 | 33 | /** 34 | * Set the coordinate to convert. 35 | * 36 | * @param CoordinateInterface $coordinates The coordinate to convert. 37 | */ 38 | public function __construct(CoordinateInterface $coordinates) 39 | { 40 | $this->coordinates = $coordinates; 41 | } 42 | 43 | /** 44 | * Parse decimal degrees coordinate to degrees minutes seconds and decimal minutes coordinate. 45 | * 46 | * @param string $coordinate The coordinate to parse. 47 | * 48 | * @return array The replace pairs values. 49 | */ 50 | private function parseCoordinate($coordinate) 51 | { 52 | list($degrees) = explode('.', abs($coordinate)); 53 | list($minutes) = explode('.', (abs($coordinate) - $degrees) * 60); 54 | 55 | return [ 56 | 'positive' => $coordinate >= 0, 57 | 'degrees' => (string) $degrees, 58 | 'decimalMinutes' => (string) round((abs($coordinate) - $degrees) * 60, 59 | ConvertInterface::DECIMAL_MINUTES_PRECISION, 60 | ConvertInterface::DECIMAL_MINUTES_MODE), 61 | 'minutes' => (string) $minutes, 62 | 'seconds' => (string) round(((abs($coordinate) - $degrees) * 60 - $minutes) * 60), 63 | ]; 64 | } 65 | 66 | /** 67 | * {@inheritDoc} 68 | */ 69 | public function toDegreesMinutesSeconds($format = ConvertInterface::DEFAULT_DMS_FORMAT) 70 | { 71 | $latitude = $this->parseCoordinate($this->coordinates->getLatitude()); 72 | $longitude = $this->parseCoordinate($this->coordinates->getLongitude()); 73 | 74 | return strtr($format, [ 75 | ConvertInterface::LATITUDE_SIGN => $latitude['positive'] ? '' : '-', 76 | ConvertInterface::LATITUDE_DIRECTION => $latitude['positive'] ? 'N' : 'S', 77 | ConvertInterface::LATITUDE_DEGREES => $latitude['degrees'], 78 | ConvertInterface::LATITUDE_MINUTES => $latitude['minutes'], 79 | ConvertInterface::LATITUDE_SECONDS => $latitude['seconds'], 80 | ConvertInterface::LONGITUDE_SIGN => $longitude['positive'] ? '' : '-', 81 | ConvertInterface::LONGITUDE_DIRECTION => $longitude['positive'] ? 'E' : 'W', 82 | ConvertInterface::LONGITUDE_DEGREES => $longitude['degrees'], 83 | ConvertInterface::LONGITUDE_MINUTES => $longitude['minutes'], 84 | ConvertInterface::LONGITUDE_SECONDS => $longitude['seconds'], 85 | ]); 86 | } 87 | 88 | /** 89 | * Alias of toDegreesMinutesSeconds function. 90 | * 91 | * @param string $format The way to format the DMS coordinate. 92 | * 93 | * @deprecated This alias is deprecated, use toDegreesMinutesSeconds() 94 | * 95 | * @return string Converted and formatted string. 96 | */ 97 | public function toDMS($format = ConvertInterface::DEFAULT_DMS_FORMAT) 98 | { 99 | return $this->toDegreesMinutesSeconds($format); 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | public function toDecimalMinutes($format = ConvertInterface::DEFAULT_DM_FORMAT) 106 | { 107 | $latitude = $this->parseCoordinate($this->coordinates->getLatitude()); 108 | $longitude = $this->parseCoordinate($this->coordinates->getLongitude()); 109 | 110 | return strtr($format, [ 111 | ConvertInterface::LATITUDE_SIGN => $latitude['positive'] ? '' : '-', 112 | ConvertInterface::LATITUDE_DIRECTION => $latitude['positive'] ? 'N' : 'S', 113 | ConvertInterface::LATITUDE_DEGREES => $latitude['degrees'], 114 | ConvertInterface::LATITUDE_DECIMAL_MINUTES => $latitude['decimalMinutes'], 115 | ConvertInterface::LONGITUDE_SIGN => $longitude['positive'] ? '' : '-', 116 | ConvertInterface::LONGITUDE_DIRECTION => $longitude['positive'] ? 'E' : 'W', 117 | ConvertInterface::LONGITUDE_DEGREES => $longitude['degrees'], 118 | ConvertInterface::LONGITUDE_DECIMAL_MINUTES => $longitude['decimalMinutes'], 119 | ]); 120 | } 121 | 122 | /** 123 | * Alias of toDecimalMinutes function. 124 | * 125 | * @param string $format The way to format the DMS coordinate. 126 | * 127 | * @deprecated This alias is deprecated, use toDecimalMinutes() 128 | * 129 | * @return string Converted and formatted string. 130 | */ 131 | public function toDM($format = ConvertInterface::DEFAULT_DM_FORMAT) 132 | { 133 | return $this->toDecimalMinutes($format); 134 | } 135 | 136 | /** 137 | * {@inheritDoc} 138 | */ 139 | public function toDegreeDecimalMinutes($format = ConvertInterface::DEFAULT_DDM_FORMAT) 140 | { 141 | $latitude = $this->parseCoordinate($this->coordinates->getLatitude()); 142 | $longitude = $this->parseCoordinate($this->coordinates->getLongitude()); 143 | 144 | $decimalPrecisionFormat = sprintf('%%0.%df', ConvertInterface::DEGREE_DECIMAL_MINUTES_PRECISION); 145 | $latitude['decimalMinutes'] = sprintf($decimalPrecisionFormat, $latitude['decimalMinutes']); 146 | $longitude['decimalMinutes'] = sprintf($decimalPrecisionFormat, $longitude['decimalMinutes']); 147 | 148 | return strtr($format, [ 149 | ConvertInterface::LATITUDE_SIGN => $latitude['positive'] ? '' : '-', 150 | ConvertInterface::LATITUDE_DIRECTION => $latitude['positive'] ? 'N' : 'S', 151 | ConvertInterface::LATITUDE_DEGREES => $latitude['degrees'], 152 | ConvertInterface::LATITUDE_DECIMAL_MINUTES => $latitude['decimalMinutes'], 153 | ConvertInterface::LONGITUDE_SIGN => $longitude['positive'] ? '' : '-', 154 | ConvertInterface::LONGITUDE_DIRECTION => $longitude['positive'] ? 'E' : 'W', 155 | ConvertInterface::LONGITUDE_DEGREES => $longitude['degrees'], 156 | ConvertInterface::LONGITUDE_DECIMAL_MINUTES => $longitude['decimalMinutes'], 157 | ]); 158 | } 159 | 160 | /** 161 | * {@inheritDoc} 162 | */ 163 | public function toUniversalTransverseMercator() 164 | { 165 | // Convert decimal degrees coordinates to radian. 166 | $phi = deg2rad($this->coordinates->getLatitude()); 167 | $lambda = deg2rad($this->coordinates->getLongitude()); 168 | 169 | // Compute the zone UTM zone. 170 | $zone = (int) (($this->coordinates->getLongitude() + 180.0) / 6) + 1; 171 | 172 | // Special zone for South Norway. 173 | // On the southwest coast of Norway, grid zone 32V (9° of longitude in width) is extended further west, 174 | // and grid zone 31V (3° of longitude in width) is correspondingly shrunk to cover only open water. 175 | if ($this->coordinates->getLatitude() >= 56.0 && $this->coordinates->getLatitude() < 64.0 176 | && $this->coordinates->getLongitude() >= 3.0 && $this->coordinates->getLongitude() < 12.0) { 177 | $zone = 32; 178 | } 179 | 180 | // Special zone for Svalbard. 181 | // In the region around Svalbard, the four grid zones 31X (9° of longitude in width), 182 | // 33X (12° of longitude in width), 35X (12° of longitude in width), and 37X (9° of longitude in width) 183 | // are extended to cover what would otherwise have been covered by the seven grid zones 31X to 37X. 184 | // The three grid zones 32X, 34X and 36X are not used. 185 | if ($this->coordinates->getLatitude() >= 72.0 && $this->coordinates->getLatitude() < 84.0) { 186 | if ($this->coordinates->getLongitude() >= 0.0 && $this->coordinates->getLongitude() < 9.0) { 187 | $zone = 31; 188 | } elseif ($this->coordinates->getLongitude() >= 9.0 && $this->coordinates->getLongitude() < 21.0) { 189 | $zone = 33; 190 | } elseif ($this->coordinates->getLongitude() >= 21.0 && $this->coordinates->getLongitude() < 33.0) { 191 | $zone = 35; 192 | } elseif ($this->coordinates->getLongitude() >= 33.0 && $this->coordinates->getLongitude() < 42.0) { 193 | $zone = 37; 194 | } 195 | } 196 | 197 | // Determines the central meridian for the given UTM zone. 198 | $lambda0 = deg2rad(-183.0 + ($zone * 6.0)); 199 | 200 | $ep2 = (pow($this->coordinates->getEllipsoid()->getA(), 2.0) - 201 | pow($this->coordinates->getEllipsoid()->getB(), 2.0)) / pow($this->coordinates->getEllipsoid()->getB(), 2.0); 202 | $nu2 = $ep2 * pow(cos($phi), 2.0); 203 | $nN = pow($this->coordinates->getEllipsoid()->getA(), 2.0) / 204 | ($this->coordinates->getEllipsoid()->getB() * sqrt(1 + $nu2)); 205 | $t = tan($phi); 206 | $t2 = $t * $t; 207 | $l = $lambda - $lambda0; 208 | 209 | $l3coef = 1.0 - $t2 + $nu2; 210 | $l4coef = 5.0 - $t2 + 9 * $nu2 + 4.0 * ($nu2 * $nu2); 211 | $l5coef = 5.0 - 18.0 * $t2 + ($t2 * $t2) + 14.0 * $nu2 - 58.0 * $t2 * $nu2; 212 | $l6coef = 61.0 - 58.0 * $t2 + ($t2 * $t2) + 270.0 * $nu2 - 330.0 * $t2 * $nu2; 213 | $l7coef = 61.0 - 479.0 * $t2 + 179.0 * ($t2 * $t2) - ($t2 * $t2 * $t2); 214 | $l8coef = 1385.0 - 3111.0 * $t2 + 543.0 * ($t2 * $t2) - ($t2 * $t2 * $t2); 215 | 216 | // Calculate easting. 217 | $easting = $nN * cos($phi) * $l 218 | + ($nN / 6.0 * pow(cos($phi), 3.0) * $l3coef * pow($l, 3.0)) 219 | + ($nN / 120.0 * pow(cos($phi), 5.0) * $l5coef * pow($l, 5.0)) 220 | + ($nN / 5040.0 * pow(cos($phi), 7.0) * $l7coef * pow($l, 7.0)); 221 | 222 | // Calculate northing. 223 | $n = ($this->coordinates->getEllipsoid()->getA() - $this->coordinates->getEllipsoid()->getB()) / 224 | ($this->coordinates->getEllipsoid()->getA() + $this->coordinates->getEllipsoid()->getB()); 225 | $alpha = (($this->coordinates->getEllipsoid()->getA() + $this->coordinates->getEllipsoid()->getB()) / 2.0) * 226 | (1.0 + (pow($n, 2.0) / 4.0) + (pow($n, 4.0) / 64.0)); 227 | $beta = (-3.0 * $n / 2.0) + (9.0 * pow($n, 3.0) / 16.0) + (-3.0 * pow($n, 5.0) / 32.0); 228 | $gamma = (15.0 * pow($n, 2.0) / 16.0) + (-15.0 * pow($n, 4.0) / 32.0); 229 | $delta = (-35.0 * pow($n, 3.0) / 48.0) + (105.0 * pow($n, 5.0) / 256.0); 230 | $epsilon = (315.0 * pow($n, 4.0) / 512.0); 231 | $northing = $alpha 232 | * ($phi + ($beta * sin(2.0 * $phi)) 233 | + ($gamma * sin(4.0 * $phi)) 234 | + ($delta * sin(6.0 * $phi)) 235 | + ($epsilon * sin(8.0 * $phi))) 236 | + ($t / 2.0 * $nN * pow(cos($phi), 2.0) * pow($l, 2.0)) 237 | + ($t / 24.0 * $nN * pow(cos($phi), 4.0) * $l4coef * pow($l, 4.0)) 238 | + ($t / 720.0 * $nN * pow(cos($phi), 6.0) * $l6coef * pow($l, 6.0)) 239 | + ($t / 40320.0 * $nN * pow(cos($phi), 8.0) * $l8coef * pow($l, 8.0)); 240 | 241 | // Adjust easting and northing for UTM system. 242 | $easting = $easting * GeotoolsInterface::UTM_SCALE_FACTOR + 500000.0; 243 | $northing = $northing * GeotoolsInterface::UTM_SCALE_FACTOR; 244 | if ($northing < 0.0) { 245 | $northing += 10000000.0; 246 | } 247 | 248 | return sprintf('%d%s %d %d', 249 | $zone, Geotools::$latitudeBands[(int) (($this->coordinates->getLatitude() + 80) / 8)], $easting, $northing 250 | ); 251 | } 252 | 253 | /** 254 | * Alias of toUniversalTransverseMercator function. 255 | * 256 | * @deprecated This alias is deprecated, use toUniversalTransverseMercator() 257 | * 258 | * @return string The converted UTM coordinate in meters 259 | */ 260 | public function toUTM() 261 | { 262 | return $this->toUniversalTransverseMercator(); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Geohash/Geohash.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace League\Geotools\Geohash; 13 | 14 | use League\Geotools\Coordinate\Coordinate; 15 | use League\Geotools\Coordinate\CoordinateInterface; 16 | use League\Geotools\Exception\InvalidArgumentException; 17 | use League\Geotools\Exception\RuntimeException; 18 | 19 | /** 20 | * Geohash class 21 | * 22 | * @author Antoine Corcy 23 | */ 24 | class Geohash implements GeohashInterface 25 | { 26 | /** 27 | * The minimum length of the geo hash. 28 | * 29 | * @var integer 30 | */ 31 | public const MIN_LENGTH = 1; 32 | 33 | /** 34 | * The maximum length of the geo hash. 35 | * 36 | * @var integer 37 | */ 38 | public const MAX_LENGTH = 12; 39 | 40 | public const DIRECTION_NORTH = 'north'; 41 | public const DIRECTION_SOUTH = 'south'; 42 | public const DIRECTION_WEST = 'west'; 43 | public const DIRECTION_EAST = 'east'; 44 | 45 | public const DIRECTION_NORTH_WEST = 'north_west'; 46 | public const DIRECTION_NORTH_EAST = 'north_east'; 47 | public const DIRECTION_SOUTH_WEST = 'south_west'; 48 | public const DIRECTION_SOUTH_EAST = 'south_east'; 49 | 50 | /** 51 | * The geo hash. 52 | * 53 | * @var string 54 | */ 55 | protected $geohash = ''; 56 | 57 | /** 58 | * The interval of latitudes in degrees. 59 | * 60 | * @var array 61 | */ 62 | protected $latitudeInterval = array(-90.0, 90.0); 63 | 64 | /** 65 | * The interval of longitudes in degrees. 66 | * 67 | * @var array 68 | */ 69 | protected $longitudeInterval = array(-180.0, 180.0); 70 | 71 | /** 72 | * The interval of bits. 73 | * 74 | * @var array 75 | */ 76 | protected $bits = array(16, 8, 4, 2, 1); 77 | 78 | /** 79 | * The array of chars in base 32. 80 | * 81 | * @var array 82 | */ 83 | protected $base32Chars = array( 84 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 85 | 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' 86 | ); 87 | 88 | private $neighbors = array( 89 | 'north' => array( 90 | 'even' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 91 | 'odd' => 'bc01fg45238967deuvhjyznpkmstqrwx', 92 | ), 93 | 'south' => array( 94 | 'even' => '14365h7k9dcfesgujnmqp0r2twvyx8zb', 95 | 'odd' => '238967debc01fg45kmstqrwxuvhjyznp', 96 | ), 97 | 'west' => array( 98 | 'even' => '238967debc01fg45kmstqrwxuvhjyznp', 99 | 'odd' => '14365h7k9dcfesgujnmqp0r2twvyx8zb', 100 | ), 101 | 'east' => array( 102 | 'even' => 'bc01fg45238967deuvhjyznpkmstqrwx', 103 | 'odd' => 'p0r21436x8zb9dcf5h7kjnmqesgutwvy', 104 | ), 105 | ); 106 | 107 | private $borders = array( 108 | 'north' => array( 109 | 'even' => 'prxz', 110 | 'odd' => 'bcfguvyz', 111 | ), 112 | 'south' => array( 113 | 'even' => '028b', 114 | 'odd' => '0145hjnp', 115 | ), 116 | 'west' => array( 117 | 'even' => '0145hjnp', 118 | 'odd' => '028b', 119 | ), 120 | 'east' => array( 121 | 'even' => 'bcfguvyz', 122 | 'odd' => 'prxz', 123 | ), 124 | ); 125 | 126 | 127 | /** 128 | * Returns the geo hash. 129 | * 130 | * @return string 131 | */ 132 | public function getGeohash(): string 133 | { 134 | return $this->geohash; 135 | } 136 | 137 | /** 138 | * Returns the decoded coordinate (The center of the bounding box). 139 | * 140 | * @return CoordinateInterface 141 | */ 142 | public function getCoordinate() 143 | { 144 | return new Coordinate(array( 145 | ($this->latitudeInterval[0] + $this->latitudeInterval[1]) / 2, 146 | ($this->longitudeInterval[0] + $this->longitudeInterval[1]) / 2 147 | )); 148 | } 149 | 150 | /** 151 | * Returns the bounding box which is an array of coordinates (SouthWest & NorthEast). 152 | * 153 | * @return CoordinateInterface[] 154 | */ 155 | public function getBoundingBox(): array 156 | { 157 | return array( 158 | new Coordinate(array( 159 | $this->latitudeInterval[0], 160 | $this->longitudeInterval[0] 161 | )), 162 | new Coordinate(array( 163 | $this->latitudeInterval[1], 164 | $this->longitudeInterval[1] 165 | )) 166 | ); 167 | } 168 | 169 | /** 170 | * Returns the code of the adjacent area 171 | * 172 | * @param string $direction 173 | * 174 | * @return string 175 | */ 176 | public function getNeighbor(string $direction): string 177 | { 178 | $geohash = $this->getGeohash(); 179 | 180 | if (in_array($direction, [self::DIRECTION_NORTH_WEST, self::DIRECTION_NORTH_EAST])) { 181 | $geohash = $this->calculateAdjacent($geohash, self::DIRECTION_NORTH); 182 | } 183 | 184 | if (in_array($direction, [self::DIRECTION_SOUTH_WEST, self::DIRECTION_SOUTH_EAST])) { 185 | $geohash = $this->calculateAdjacent($geohash, self::DIRECTION_SOUTH); 186 | } 187 | 188 | if (in_array($direction, [self::DIRECTION_NORTH_WEST, self::DIRECTION_SOUTH_WEST])) { 189 | $direction = self::DIRECTION_WEST; 190 | } 191 | 192 | if (in_array($direction, [self::DIRECTION_NORTH_EAST, self::DIRECTION_SOUTH_EAST])) { 193 | $direction = self::DIRECTION_EAST; 194 | } 195 | 196 | return $this->calculateAdjacent($geohash, $direction); 197 | } 198 | 199 | /** 200 | * Returns neighboring area codes 201 | * 202 | * @param bool $includingCornerNeighbors 203 | * 204 | * @return array 205 | */ 206 | public function getNeighbors(bool $includingCornerNeighbors = false): array 207 | { 208 | $geohash = $this->getGeohash(); 209 | 210 | $north = $this->calculateAdjacent($geohash, self::DIRECTION_NORTH); 211 | $south = $this->calculateAdjacent($geohash, self::DIRECTION_SOUTH); 212 | $west = $this->calculateAdjacent($geohash, self::DIRECTION_WEST); 213 | $east = $this->calculateAdjacent($geohash, self::DIRECTION_EAST); 214 | 215 | $neighbors = array( 216 | self::DIRECTION_NORTH => $north, 217 | self::DIRECTION_SOUTH => $south, 218 | self::DIRECTION_WEST => $west, 219 | self::DIRECTION_EAST => $east, 220 | ); 221 | 222 | if ($includingCornerNeighbors) { 223 | $neighbors = array_merge($neighbors, array( 224 | self::DIRECTION_NORTH_WEST => $this->calculateAdjacent($north, self::DIRECTION_WEST), 225 | self::DIRECTION_NORTH_EAST => $this->calculateAdjacent($north, self::DIRECTION_EAST), 226 | self::DIRECTION_SOUTH_WEST => $this->calculateAdjacent($south, self::DIRECTION_WEST), 227 | self::DIRECTION_SOUTH_EAST => $this->calculateAdjacent($south, self::DIRECTION_EAST), 228 | )); 229 | } 230 | 231 | return $neighbors; 232 | } 233 | 234 | /** 235 | * {@inheritDoc} 236 | * 237 | * @see http://en.wikipedia.org/wiki/Geohash 238 | * @see http://geohash.org/ 239 | */ 240 | public function encode(CoordinateInterface $coordinate, $length = self::MAX_LENGTH): GeohashInterface 241 | { 242 | $length = (int) $length; 243 | if ($length < self::MIN_LENGTH || $length > self::MAX_LENGTH) { 244 | throw new InvalidArgumentException('The length should be between 1 and 12.'); 245 | } 246 | 247 | $latitudeInterval = $this->latitudeInterval; 248 | $longitudeInterval = $this->longitudeInterval; 249 | $isEven = true; 250 | $bit = 0; 251 | $charIndex = 0; 252 | 253 | while (strlen($this->geohash) < $length) { 254 | if ($isEven) { 255 | $middle = ($longitudeInterval[0] + $longitudeInterval[1]) / 2; 256 | if ($coordinate->getLongitude() > $middle) { 257 | $charIndex |= $this->bits[$bit]; 258 | $longitudeInterval[0] = $middle; 259 | } else { 260 | $longitudeInterval[1] = $middle; 261 | } 262 | } else { 263 | $middle = ($latitudeInterval[0] + $latitudeInterval[1]) / 2; 264 | if ($coordinate->getLatitude() > $middle) { 265 | $charIndex |= $this->bits[$bit]; 266 | $latitudeInterval[0] = $middle; 267 | } else { 268 | $latitudeInterval[1] = $middle; 269 | } 270 | } 271 | 272 | if ($bit < 4) { 273 | $bit++; 274 | } else { 275 | $this->geohash .= $this->base32Chars[$charIndex]; 276 | $bit = 0; 277 | $charIndex = 0; 278 | } 279 | 280 | $isEven = !$isEven; 281 | } 282 | 283 | $this->latitudeInterval = $latitudeInterval; 284 | $this->longitudeInterval = $longitudeInterval; 285 | 286 | return $this; 287 | } 288 | 289 | /** 290 | * {@inheritDoc} 291 | */ 292 | public function decode($geohash): GeohashInterface 293 | { 294 | if (!is_string($geohash)) { 295 | throw new InvalidArgumentException('The geo hash should be a string.'); 296 | } 297 | 298 | if (strlen($geohash) < self::MIN_LENGTH || strlen($geohash) > self::MAX_LENGTH) { 299 | throw new InvalidArgumentException('The length of the geo hash should be between 1 and 12.'); 300 | } 301 | 302 | $base32DecodeMap = array(); 303 | $base32CharsTotal = count($this->base32Chars); 304 | for ($i = 0; $i < $base32CharsTotal; $i++) { 305 | $base32DecodeMap[$this->base32Chars[$i]] = $i; 306 | } 307 | 308 | $latitudeInterval = $this->latitudeInterval; 309 | $longitudeInterval = $this->longitudeInterval; 310 | $isEven = true; 311 | 312 | $geohashLength = strlen($geohash); 313 | for ($i = 0; $i < $geohashLength; $i++) { 314 | 315 | if (!isset($base32DecodeMap[$geohash[$i]])) { 316 | throw new RuntimeException('This geo hash is invalid.'); 317 | } 318 | 319 | $currentChar = $base32DecodeMap[$geohash[$i]]; 320 | 321 | $bitsTotal = count($this->bits); 322 | for ($j = 0; $j < $bitsTotal; $j++) { 323 | $mask = $this->bits[$j]; 324 | 325 | if ($isEven) { 326 | if (($currentChar & $mask) !== 0) { 327 | $longitudeInterval[0] = ($longitudeInterval[0] + $longitudeInterval[1]) / 2; 328 | } else { 329 | $longitudeInterval[1] = ($longitudeInterval[0] + $longitudeInterval[1]) / 2; 330 | } 331 | } else { 332 | if (($currentChar & $mask) !== 0) { 333 | $latitudeInterval[0] = ($latitudeInterval[0] + $latitudeInterval[1]) / 2; 334 | } else { 335 | $latitudeInterval[1] = ($latitudeInterval[0] + $latitudeInterval[1]) / 2; 336 | } 337 | } 338 | 339 | $isEven = !$isEven; 340 | } 341 | } 342 | 343 | $this->geohash = $geohash; 344 | 345 | $this->latitudeInterval = $latitudeInterval; 346 | $this->longitudeInterval = $longitudeInterval; 347 | 348 | return $this; 349 | } 350 | 351 | protected function calculateAdjacent(string $geohash, string $direction): string 352 | { 353 | $geohash = strtolower($geohash); 354 | $lastChr = $geohash[strlen($geohash) - 1]; 355 | $type = (strlen($geohash) % 2) ? 'odd' : 'even'; 356 | $base = substr($geohash, 0, strlen($geohash) - 1); 357 | 358 | if (!empty($base) && strpos($this->borders[$direction][$type], $lastChr) !== false) { 359 | $base = $this->calculateAdjacent($base, $direction); 360 | } 361 | 362 | return $base.$this->base32Chars[strpos($this->neighbors[$direction][$type], $lastChr)]; 363 | } 364 | } 365 | --------------------------------------------------------------------------------