Repository: jsor/geokit
Branch: main
Commit: 67b44ef6f3e8
Files: 27
Total size: 72.1 KB
Directory structure:
gitextract_40il9h5a/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── static.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── psalm.xml
├── src/
│ ├── BoundingBox.php
│ ├── Distance.php
│ ├── Earth.php
│ ├── Exception/
│ │ ├── Exception.php
│ │ ├── InvalidArgumentException.php
│ │ ├── LogicException.php
│ │ ├── MissingCoordinateException.php
│ │ └── RuntimeException.php
│ ├── Polygon.php
│ ├── Position.php
│ ├── functions.php
│ └── functions_include.php
└── tests/
├── BoundingBoxTest.php
├── DistanceTest.php
├── FunctionsTest.php
├── PolygonTest.php
├── PositionTest.php
└── TestCase.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
pull_request:
jobs:
tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- php: '7.4'
- php: '8.0'
code-coverage: 'yes'
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: pcov
- name: Install dependencies
run: composer update --no-interaction --no-progress --prefer-dist
- name: Run tests
if: matrix.code-coverage != 'yes'
run: vendor/bin/phpunit --coverage-text
- name: Run tests with code coverage
if: matrix.code-coverage == 'yes'
run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml
- name: Upload coverage results to Coveralls
if: matrix.code-coverage == 'yes'
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
composer global require php-coveralls/php-coveralls
php-coveralls -v
================================================
FILE: .github/workflows/static.yml
================================================
name: Static analysis
on:
push:
branches:
- master
pull_request:
jobs:
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: none
extensions: mbstring
tools: "cs2pr"
- name: Install dependencies
run: composer update --no-interaction --no-progress --prefer-dist
- name: Run PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --format=checkstyle | cs2pr
psalm:
name: Psalm
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: none
extensions: mbstring, intl
- name: Install dependencies
run: composer update --no-interaction --no-progress --prefer-dist
- name: Run Psalm
run: vendor/bin/psalm --no-progress --output-format=github
================================================
FILE: .gitignore
================================================
build/
vendor/
.php-cs-fixer.php
.php-cs-fixer.cache
.phpunit.result.cache
composer.lock
composer.phar
phpunit.xml
================================================
FILE: .php-cs-fixer.dist.php
================================================
<?php
$finder = (new PhpCsFixer\Finder())
->in(__DIR__);
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'array_syntax' => [
'syntax' => 'short',
],
'concat_space' => [
'spacing' => 'one',
],
'global_namespace_import' => [
'import_classes' => true,
'import_constants' => null,
'import_functions' => true,
],
'native_constant_invocation' => [
'fix_built_in' => false,
],
'ordered_imports' => [
'imports_order' => [
'class',
'function',
'const',
],
],
'single_line_throw' => false,
])
->setFinder($finder);
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2011-2022 Jan Sorgalla
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
Geokit
======
Geokit is a PHP toolkit to solve geo-related tasks like:
* Distance calculations.
* Heading, midpoint and endpoint calculations.
* Rectangular bounding box calculations.
[](https://github.com/jsor/geokit/actions/workflows/ci.yml)
[](https://coveralls.io/github/jsor/geokit?branch=main)
* [Installation](#installation)
* [Reference](#reference)
* [Distance](#distance)
* [Position](#position)
* [BoundingBox](#boundingbox)
* [Polygon](#polygon)
* [Functions](#functions)
* [Distance calculations](#distance-calculations)
* [Transformations](#transformations)
* [Other calculations](#other-calculations)
* [License](#license)
Installation
------------
Install the latest version with [Composer](https://getcomposer.org).
```bash
composer require geokit/geokit
```
Check the [Packagist page](https://packagist.org/packages/geokit/geokit) for all
available versions.
Reference
---------
### Distance
A Distance instance allows for a convenient representation of a distance unit of
measure.
```php
use Geokit\Distance;
$distance = new Distance(1000); // Defaults to meters
// or
$distance = new Distance(1, Distance::UNIT_KILOMETERS);
$meters = $distance->meters();
$kilometers = $distance->kilometers();
$miles = $distance->miles();
$yards = $distance->yards();
$feet = $distance->feet();
$inches = $distance->inches();
$nauticalMiles = $distance->nautical();
```
A Distance can also be created from a string with an optional unit.
```php
use Geokit\Distance;
$distance = Distance::fromString('1000'); // Defaults to meters
$distance = Distance::fromString('1000m');
$distance = Distance::fromString('1km');
$distance = Distance::fromString('100 miles');
$distance = Distance::fromString('100 yards');
$distance = Distance::fromString('1 foot');
$distance = Distance::fromString('1 inch');
$distance = Distance::fromString('234nm');
```
### Position
A `Position` is a fundamental construct representing a geographical position in
`x` (or `longitude`) and `y` (or `latitude`) coordinates.
Note, that `x`/`y` coordinates are kept as is, while `longitude`/`latitude` are
normalized.
* Longitudes range between -180 and 180 degrees, inclusive. Longitudes above 180
or below -180 are normalized. For example, 480, 840 and 1200 will all be
normalized to 120 degrees.
* Latitudes range between -90 and 90 degrees, inclusive. Latitudes above 90 or
below -90 are normalized. For example, 100 will be normalized to 80 degrees.
```php
use Geokit\Position;
$position = new Position(181, 91);
$x = $position->x(); // Returns 181.0
$y = $position->y(); // Returns 91.0
$longitude = $position->longitude(); // Returns -179.0, normalized
$latitude = $position->latitude(); // Returns 89.0, normalized
```
### BoundingBox
A BoundingBox instance represents a rectangle in geographical coordinates,
including one that crosses the 180 degrees longitudinal meridian.
It is constructed from its left-bottom (south-west) and right-top (north-east)
corner points.
```php
use Geokit\BoundingBox;
use Geokit\Position;
$southWest = Position::fromXY(2, 1);
$northEast = Position::fromXY(2, 1);
$boundingBox = BoundingBox::fromCornerPositions($southWest, $northEast);
$southWestPosition = $boundingBox->southWest();
$northEastPosition = $boundingBox->northEast();
$center = $boundingBox->center();
$span = $boundingBox->span();
$boolean = $boundingBox->contains($position);
$newBoundingBox = $boundingBox->extend($position);
$newBoundingBox = $boundingBox->union($otherBoundingBox);
```
With the `expand()` and `shrink()` methods, you can expand or shrink a
BoundingBox instance by a distance.
```php
use Geokit\Distance;
$expandedBoundingBox = $boundingBox->expand(
Distance::fromString('10km')
);
$shrinkedBoundingBox = $boundingBox->shrink(
Distance::fromString('10km')
);
```
The `toPolygon()` method converts the BoundingBox to an equivalent Polygon
instance.
```php
$polygon = $boundingBox->toPolygon();
```
### Polygon
A Polygon instance represents a two-dimensional shape of connected line segments
and may either be closed (the first and last point are the same) or open.
```php
use Geokit\BoundingBox;
use Geokit\Polygon;
use Geokit\Position;
$polygon = Polygon::fromPositions(
Position::fromXY(0, 0),
Position::fromXY(1, 0),
Position::fromXY(1, 1)
);
$closedPolygon = $polygon->close();
/** @var Position $position */
foreach ($polygon as $position) {
}
$polygon->contains(Position::fromXY(0.5, 0.5)); // true
/** @var BoundingBox $boundingBox */
$boundingBox = $polygon->toBoundingBox();
```
### Functions
Geokit provides several functions to perform geographic calculations.
#### Distance calculations
* `distanceHaversine(Position $from, Position $to)`:
Calculates the approximate sea level great circle (Earth) distance between two
points using the Haversine formula.
* `distanceVincenty(Position $from, Position $to)`:
Calculates the geodetic distance between two points using the Vincenty inverse
formula for ellipsoids.
```php
use function Geokit\distanceHaversine;
use function Geokit\distanceVincenty;
$distance1 = distanceHaversine($from, $to);
$distance2 = distanceVincenty($from, $to);
```
Both functions return a [Distance](#distance) instance.
#### Transformations
The `circle()` function calculates a closed circle Polygon given a center,
radius and steps for precision.
```php
use Geokit\Distance;
use Geokit\Position;
use function Geokit\circle;
$circlePolygon = circle(
Position::fromXY(8.50207515, 49.50042565),
Distance::fromString('5km'),
32
);
```
#### Other calculations
Other useful functions are:
* `heading(Position $from, Position $to)`: Calculates the (initial) heading from
the first point to the second point in degrees.
* `midpoint(Position $from, Position $to)`: Calculates an intermediate point on
the geodesic between the two given points.
* `endpoint(Position $start, float $heading, Geokit\Distance $distance)`:
Calculates the destination point along a geodesic, given an initial heading
and distance, from the given start point.
License
-------
Copyright (c) 2011-2022 Jan Sorgalla.
Released under the [MIT License](LICENSE).
================================================
FILE: composer.json
================================================
{
"name": "geokit/geokit",
"description": "Geo-Toolkit for PHP",
"keywords": ["geo", "geometry", "geography"],
"homepage": "https://github.com/jsor/geokit",
"license": "MIT",
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com",
"homepage": "http://sorgalla.com"
}
],
"require": {
"php": "^7.3 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"vimeo/psalm": "^4.9",
"friendsofphp/php-cs-fixer": "^3.1"
},
"autoload": {
"psr-4": {
"Geokit\\": "src/"
},
"files": ["src/functions_include.php"]
},
"autoload-dev": {
"psr-4": {
"Geokit\\": "tests/"
}
}
}
================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Geokit Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>./src/</directory>
</include>
<exclude>
<file>./src/functions_include.php</file>
</exclude>
</coverage>
</phpunit>
================================================
FILE: psalm.xml
================================================
<?xml version="1.0"?>
<psalm
xmlns="https://getpsalm.org/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
totallyTyped="true"
>
<projectFiles>
<directory name="src"/>
<directory name="tests"/>
</projectFiles>
<issueHandlers>
<PropertyNotSetInConstructor>
<errorLevel type="suppress">
<directory name="tests"/>
</errorLevel>
</PropertyNotSetInConstructor>
</issueHandlers>
</psalm>
================================================
FILE: src/BoundingBox.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use Geokit\Exception\MissingCoordinateException;
use JsonSerializable;
use function array_key_exists;
use function asin;
use function cos;
use function deg2rad;
use function max;
use function min;
use function rad2deg;
use function sin;
final class BoundingBox implements JsonSerializable
{
/** @var Position */
private $southWest;
/** @var Position */
private $northEast;
private function __construct(Position $southWest, Position $northEast)
{
$this->southWest = $southWest;
$this->northEast = $northEast;
if ($this->southWest->latitude() > $this->northEast->latitude()) {
throw new Exception\LogicException(
'Bounding Box south-west coordinate cannot be north of the north-east coordinate'
);
}
}
public static function fromCornerPositions(
Position $southWest,
Position $northEast
): self {
return new self($southWest, $northEast);
}
/**
* @param iterable<float> $iterable
*/
public static function fromCoordinates(iterable $iterable): self
{
$array = [];
foreach ($iterable as $coordinate) {
$array[] = $coordinate;
if (isset($array[3])) {
break;
}
}
if (!array_key_exists(0, $array)) {
throw MissingCoordinateException::create('west', 0);
}
if (!array_key_exists(1, $array)) {
throw MissingCoordinateException::create('south', 1);
}
if (!array_key_exists(2, $array)) {
throw MissingCoordinateException::create('east', 0);
}
if (!array_key_exists(3, $array)) {
throw MissingCoordinateException::create('north', 1);
}
return new self(
Position::fromXY($array[0], $array[1]),
Position::fromXY($array[2], $array[3])
);
}
/**
* @return iterable<float>
*/
public function toCoordinates(): iterable
{
return [
$this->southWest->x(),
$this->southWest->y(),
$this->northEast->x(),
$this->northEast->y(),
];
}
/**
* @return array<float>
*/
public function jsonSerialize(): array
{
return [
$this->southWest->x(),
$this->southWest->y(),
$this->northEast->x(),
$this->northEast->y(),
];
}
public function southWest(): Position
{
return $this->southWest;
}
public function northEast(): Position
{
return $this->northEast;
}
public function center(): Position
{
if ($this->crossesAntimeridian()) {
$span = $this->lngSpan(
$this->southWest->longitude(),
$this->northEast->longitude()
);
$lng = $this->southWest->longitude() + $span / 2;
} else {
$lng = ($this->southWest->longitude() + $this->northEast->longitude()) / 2;
}
return Position::fromXY(
$lng,
($this->southWest->latitude() + $this->northEast->latitude()) / 2
);
}
public function span(): Position
{
return Position::fromXY(
$this->lngSpan($this->southWest->longitude(), $this->northEast->longitude()),
$this->northEast->latitude() - $this->southWest->latitude()
);
}
public function crossesAntimeridian(): bool
{
return $this->southWest->longitude() > $this->northEast->longitude();
}
public function contains(Position $position): bool
{
$lat = $position->latitude();
// check latitude
if ($this->southWest->latitude() > $lat ||
$lat > $this->northEast->latitude()
) {
return false;
}
// check longitude
return $this->containsLng($position->longitude());
}
public function extend(Position $position): self
{
$newSouth = min($this->southWest->latitude(), $position->latitude());
$newNorth = max($this->northEast->latitude(), $position->latitude());
$newWest = $this->southWest->longitude();
$newEast = $this->northEast->longitude();
if (!$this->containsLng($position->longitude())) {
// try extending east and try extending west, and use the one that
// has the smaller longitudinal span
$extendEastLngSpan = $this->lngSpan($newWest, $position->longitude());
$extendWestLngSpan = $this->lngSpan($position->longitude(), $newEast);
if ($extendEastLngSpan <= $extendWestLngSpan) {
$newEast = $position->longitude();
} else {
$newWest = $position->longitude();
}
}
return new self(Position::fromXY($newWest, $newSouth), Position::fromXY($newEast, $newNorth));
}
public function union(self $bbox): self
{
$newBbox = $this->extend($bbox->southWest());
return $newBbox->extend($bbox->northEast());
}
public function expand(Distance $distance): self
{
return self::transformBoundingBox($this, $distance->meters());
}
public function shrink(Distance $distance): self
{
return self::transformBoundingBox($this, -$distance->meters());
}
public function toPolygon(): Polygon
{
return Polygon::fromPositions(
Position::fromXY($this->southWest->x(), $this->southWest->y()),
Position::fromXY($this->northEast->x(), $this->southWest->y()),
Position::fromXY($this->northEast->x(), $this->northEast->y()),
Position::fromXY($this->southWest->x(), $this->northEast->y()),
Position::fromXY($this->southWest->x(), $this->southWest->y())
);
}
/**
* @see http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates
*/
private static function transformBoundingBox(self $bbox, float $distanceInMeters): self
{
$latSW = deg2rad($bbox->southWest()->latitude());
$lngSW = deg2rad($bbox->southWest()->longitude());
$latNE = deg2rad($bbox->northEast()->latitude());
$lngNE = deg2rad($bbox->northEast()->longitude());
$angularDistance = $distanceInMeters / Earth::RADIUS;
$minLat = $latSW - $angularDistance;
$maxLat = $latNE + $angularDistance;
$deltaLonSW = asin(sin($angularDistance) / cos($latSW));
$deltaLonNE = asin(sin($angularDistance) / cos($latNE));
$minLon = $lngSW - $deltaLonSW;
$maxLon = $lngNE + $deltaLonNE;
$positionSW = Position::fromXY(rad2deg($minLon), rad2deg($minLat));
$positionNE = Position::fromXY(rad2deg($maxLon), rad2deg($maxLat));
// Check if we're shrinking too much
if ($positionSW->latitude() > $positionNE->latitude()) {
$center = $bbox->center();
return self::fromCornerPositions($center, $center);
}
return self::fromCornerPositions($positionSW, $positionNE);
}
private function containsLng(float $lng): bool
{
if ($this->crossesAntimeridian()) {
return $lng <= $this->northEast->longitude() ||
$lng >= $this->southWest->longitude();
}
return $this->southWest->longitude() <= $lng &&
$lng <= $this->northEast->longitude();
}
private function lngSpan(float $west, float $east): float
{
return $west > $east ? ($east + 360 - $west) : ($east - $west);
}
}
================================================
FILE: src/Distance.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use function json_encode;
use function preg_match;
use function sprintf;
/**
* Inspired by GeoPy's distance class (https://github.com/geopy/geopy).
*/
final class Distance
{
public const UNIT_METERS = 'meters';
public const UNIT_KILOMETERS = 'kilometers';
public const UNIT_MILES = 'miles';
public const UNIT_YARDS = 'yards';
public const UNIT_FEET = 'feet';
public const UNIT_INCHES = 'inches';
public const UNIT_NAUTICAL = 'nautical';
public const DEFAULT_UNIT = self::UNIT_METERS;
/** @var array<float> */
private static $units = [
self::UNIT_METERS => 1.0,
self::UNIT_KILOMETERS => 1000.0,
self::UNIT_MILES => 1609.344,
self::UNIT_YARDS => 0.9144,
self::UNIT_FEET => 0.3048,
self::UNIT_INCHES => 0.0254,
self::UNIT_NAUTICAL => 1852.0,
];
/** @var array<string> */
private static $aliases = [
'meter' => self::UNIT_METERS,
'metre' => self::UNIT_METERS,
'metres' => self::UNIT_METERS,
'm' => self::UNIT_METERS,
'kilometer' => self::UNIT_KILOMETERS,
'kilometre' => self::UNIT_KILOMETERS,
'kilometres' => self::UNIT_KILOMETERS,
'km' => self::UNIT_KILOMETERS,
'mile' => self::UNIT_MILES,
'mi' => self::UNIT_MILES,
'yard' => self::UNIT_YARDS,
'yd' => self::UNIT_YARDS,
'foot' => self::UNIT_FEET,
'ft' => self::UNIT_FEET,
'nm' => self::UNIT_NAUTICAL,
'inch' => self::UNIT_INCHES,
'in' => self::UNIT_INCHES,
'″' => self::UNIT_INCHES,
'nauticalmile' => self::UNIT_NAUTICAL,
'nauticalmiles' => self::UNIT_NAUTICAL,
];
/** @var float */
private $value;
public function __construct(float $value, string $unit = self::DEFAULT_UNIT)
{
if (!isset(self::$units[$unit])) {
throw new Exception\InvalidArgumentException(
sprintf(
'Invalid unit %s.',
json_encode($unit)
)
);
}
if ($value < 0) {
throw new Exception\InvalidArgumentException(
sprintf(
'The distance must be a positive number, got "%s".',
$value
)
);
}
$this->value = $value * self::$units[$unit];
}
public static function fromString(string $input): self
{
if ((bool) preg_match('/(\-?\d+\.?\d*)\s*((kilo)?met[er]+s?|m|km|miles?|mi|yards?|yd|feet|foot|ft|in(ch(es)?)?|″|nautical(mile)?s?|nm)?$/u', $input, $match)) {
$unit = self::DEFAULT_UNIT;
if (isset($match[2])) {
$unit = $match[2];
if (!isset(self::$units[$unit])) {
$unit = self::$aliases[$unit];
}
}
return new self((float) $match[1], $unit);
}
throw new Exception\InvalidArgumentException(
sprintf(
'Cannot create Distance from string %s.',
json_encode($input)
)
);
}
public function meters(): float
{
return $this->value / self::$units[self::UNIT_METERS];
}
public function m(): float
{
return $this->meters();
}
public function kilometers(): float
{
return $this->value / self::$units[self::UNIT_KILOMETERS];
}
public function km(): float
{
return $this->kilometers();
}
public function miles(): float
{
return $this->value / self::$units[self::UNIT_MILES];
}
public function mi(): float
{
return $this->miles();
}
public function yards(): float
{
return $this->value / self::$units[self::UNIT_YARDS];
}
public function yd(): float
{
return $this->yards();
}
public function feet(): float
{
return $this->value / self::$units[self::UNIT_FEET];
}
public function ft(): float
{
return $this->feet();
}
public function inches(): float
{
return $this->value / self::$units[self::UNIT_INCHES];
}
public function in(): float
{
return $this->inches();
}
public function nautical(): float
{
return $this->value / self::$units[self::UNIT_NAUTICAL];
}
public function nm(): float
{
return $this->nautical();
}
}
================================================
FILE: src/Earth.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
/**
* @see http://en.wikipedia.org/wiki/World_Geodetic_System
* @see https://en.wikipedia.org/wiki/Earth_radius
*/
final class Earth
{
public const SEMI_MAJOR_AXIS = 6378137.0;
public const INVERSE_FLATTENING = 298.257223563;
/**
* SEMI_MAJOR_AXIS - SEMI_MAJOR_AXIS / INVERSE_FLATTENING.
*/
public const SEMI_MINOR_AXIS = 6356752.3142;
/**
* 1 / INVERSE_FLATTENING.
*/
public const FLATTENING = 0.0033528106647475;
/**
* Mean earth radius.
*
* (2 * SEMI_MAJOR_AXIS + SEMI_MINOR_AXIS) / 3
*
* @see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius
*/
public const RADIUS = 6371008.8;
}
================================================
FILE: src/Exception/Exception.php
================================================
<?php
declare(strict_types=1);
namespace Geokit\Exception;
interface Exception
{
}
================================================
FILE: src/Exception/InvalidArgumentException.php
================================================
<?php
declare(strict_types=1);
namespace Geokit\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
}
================================================
FILE: src/Exception/LogicException.php
================================================
<?php
declare(strict_types=1);
namespace Geokit\Exception;
class LogicException extends \LogicException implements Exception
{
}
================================================
FILE: src/Exception/MissingCoordinateException.php
================================================
<?php
declare(strict_types=1);
namespace Geokit\Exception;
use function sprintf;
final class MissingCoordinateException extends InvalidArgumentException
{
public static function create(
string $coordinate,
int $position
): self {
return new self(
sprintf(
'Missing %s-coordinate at position %d.',
$coordinate,
$position
)
);
}
}
================================================
FILE: src/Exception/RuntimeException.php
================================================
<?php
declare(strict_types=1);
namespace Geokit\Exception;
class RuntimeException extends \RuntimeException implements Exception
{
}
================================================
FILE: src/Polygon.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use Countable;
use Generator;
use IteratorAggregate;
use JsonSerializable;
use function array_shift;
use function count;
use function end;
use function reset;
final class Polygon implements Countable, IteratorAggregate, JsonSerializable
{
/** @var array<Position> */
private $positions;
private function __construct(Position ...$positions)
{
$this->positions = $positions;
}
public static function fromPositions(Position ...$positions): self
{
return new self(...$positions);
}
/**
* @param iterable<iterable<float>> $iterable
*/
public static function fromCoordinates(iterable $iterable): self
{
$positions = [];
foreach ($iterable as $position) {
$positions[] = Position::fromCoordinates($position);
}
return new self(...$positions);
}
public function close(): self
{
if (0 === count($this->positions)) {
return new self();
}
$positions = $this->positions;
$lastPosition = end($positions);
$firstPosition = reset($positions);
$isClosed = (
$lastPosition->latitude() === $firstPosition->latitude() &&
$lastPosition->longitude() === $firstPosition->longitude()
);
if (!$isClosed) {
$positions[] = clone reset($this->positions);
}
return new self(...$positions);
}
/**
* @see https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
*/
public function contains(Position $position): bool
{
if (0 === count($this->positions)) {
return false;
}
$positions = $this->positions;
$x = $position->longitude();
$y = $position->latitude();
$p = end($positions);
$x0 = $p->longitude();
$y0 = $p->latitude();
$inside = false;
foreach ($positions as $pos) {
$x1 = $pos->longitude();
$y1 = $pos->latitude();
if (($y1 > $y) !== ($y0 > $y) &&
($x < ($x0 - $x1) * ($y - $y1) / ($y0 - $y1) + $x1)
) {
$inside = !$inside;
}
$x0 = $x1;
$y0 = $y1;
}
return $inside;
}
public function toBoundingBox(): BoundingBox
{
if (0 === count($this->positions)) {
throw new Exception\LogicException('Cannot create a BoundingBox from empty Polygon.');
}
$positions = $this->positions;
$start = array_shift($positions);
$bbox = BoundingBox::fromCornerPositions($start, $start);
foreach ($positions as $position) {
$bbox = $bbox->extend($position);
}
return $bbox;
}
public function count(): int
{
return count($this->positions);
}
public function getIterator(): Generator
{
yield from $this->positions;
}
/**
* @return iterable<iterable<float>>
*/
public function toCoordinates(): iterable
{
foreach ($this->positions as $position) {
yield $position->toCoordinates();
}
}
/**
* @return array<array<float>>
*/
public function jsonSerialize(): array
{
$coordinates = [];
foreach ($this->positions as $position) {
$coordinates[] = $position->jsonSerialize();
}
return $coordinates;
}
}
================================================
FILE: src/Position.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use Geokit\Exception\MissingCoordinateException;
use JsonSerializable;
use function array_key_exists;
final class Position implements JsonSerializable
{
/** @var float */
private $x;
/** @var float */
private $y;
private function __construct(float $x, float $y)
{
$this->x = $x;
$this->y = $y;
}
public static function fromXY(float $x, float $y): self
{
return new self($x, $y);
}
/**
* @param iterable<float> $iterable
*/
public static function fromCoordinates(iterable $iterable): self
{
$array = [];
foreach ($iterable as $coordinate) {
$array[] = $coordinate;
if (isset($array[1])) {
break;
}
}
if (!array_key_exists(0, $array)) {
throw MissingCoordinateException::create('x', 0);
}
if (!array_key_exists(1, $array)) {
throw MissingCoordinateException::create('y', 1);
}
return new self($array[0], $array[1]);
}
public function x(): float
{
return $this->x;
}
public function y(): float
{
return $this->y;
}
public function longitude(): float
{
return normalizeLongitude($this->x);
}
public function latitude(): float
{
return normalizeLatitude($this->y);
}
/**
* @return iterable<float>
*/
public function toCoordinates(): iterable
{
return [$this->x, $this->y];
}
/**
* @return array<float>
*/
public function jsonSerialize(): array
{
return [$this->x, $this->y];
}
}
================================================
FILE: src/functions.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use function abs;
use function asin;
use function atan;
use function atan2;
use function cos;
use function deg2rad;
use function fmod;
use function rad2deg;
use function sin;
use function sqrt;
use function tan;
/**
* Calculates the approximate sea level great circle (Earth) distance
* between two points using the Haversine formula.
*
* @see http://en.wikipedia.org/wiki/Haversine_formula
* @see http://www.movable-type.co.uk/scripts/latlong.html
*/
function distanceHaversine(Position $from, Position $to): Distance
{
$lat1 = deg2rad($from->latitude());
$lng1 = deg2rad($from->longitude());
$lat2 = deg2rad($to->latitude());
$lng2 = deg2rad($to->longitude());
$dLat = $lat2 - $lat1;
$dLon = $lng2 - $lng1;
$a = sin($dLat / 2) * sin($dLat / 2) +
cos($lat1) * cos($lat2) *
sin($dLon / 2) * sin($dLon / 2);
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
return new Distance(Earth::RADIUS * $c);
}
/**
* Calculates the geodetic distance between two points using the
* Vincenty inverse formula for ellipsoids.
*
* @see http://en.wikipedia.org/wiki/Vincenty%27s_formulae
* @see http://www.movable-type.co.uk/scripts/latlong-vincenty.html
*/
function distanceVincenty(Position $from, Position $to): Distance
{
$lat1 = $from->latitude();
$lng1 = $from->longitude();
$lat2 = $to->latitude();
$lng2 = $to->longitude();
$a = Earth::SEMI_MAJOR_AXIS;
$b = Earth::SEMI_MINOR_AXIS;
$f = Earth::FLATTENING;
$L = deg2rad($lng2 - $lng1);
$U1 = atan((1 - $f) * tan(deg2rad($lat1)));
$U2 = atan((1 - $f) * tan(deg2rad($lat2)));
$sinU1 = sin($U1);
$cosU1 = cos($U1);
$sinU2 = sin($U2);
$cosU2 = cos($U2);
$lambda = $L;
$iterLimit = 100;
do {
$sinLambda = sin($lambda);
$cosLambda = cos($lambda);
$sinSigma = sqrt(($cosU2 * $sinLambda) * ($cosU2 * $sinLambda) +
($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda) *
($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda));
if (0.0 === $sinSigma) {
return new Distance(0); // co-incident points
}
$cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda;
$sigma = atan2($sinSigma, $cosSigma);
$sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma;
$cosSqAlpha = (float) 1 - $sinAlpha * $sinAlpha;
if (0.0 !== $cosSqAlpha) {
$cos2SigmaM = $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha;
} else {
$cos2SigmaM = 0.0; // Equatorial line
}
$C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
$lambdaP = $lambda;
$lambda = $L + (1 - $C) * $f * $sinAlpha *
($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM)));
} while (--$iterLimit > 0 && abs($lambda - $lambdaP) > 1e-12);
if ($iterLimit <= 0) {
throw new Exception\RuntimeException('Vincenty formula failed to converge.');
}
$uSq = $cosSqAlpha * ($a * $a - $b * $b) / ($b * $b);
$A = 1 + $uSq / 16384 * (4096 + $uSq * (-768 + $uSq * (320 - 175 * $uSq)));
$B = $uSq / 1024 * (256 + $uSq * (-128 + $uSq * (74 - 47 * $uSq)));
$deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM) -
$B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma) * (-3 + 4 * $cos2SigmaM * $cos2SigmaM)));
$s = $b * $A * ($sigma - $deltaSigma);
return new Distance($s);
}
/**
* Calculates the (initial) heading from the first point to the second point
* in degrees.
*/
function heading(Position $from, Position $to): float
{
$lat1 = $from->latitude();
$lng1 = $from->longitude();
$lat2 = $to->latitude();
$lng2 = $to->longitude();
$lat1 = deg2rad($lat1);
$lat2 = deg2rad($lat2);
$dLon = deg2rad($lng2 - $lng1);
$y = sin($dLon) * cos($lat2);
$x = cos($lat1) * sin($lat2) -
sin($lat1) * cos($lat2) * cos($dLon);
$heading = atan2($y, $x);
return fmod(rad2deg($heading) + 360, 360);
}
/**
* Calculates an intermediate point on the geodesic between the two given
* points.
*
* @see http://www.movable-type.co.uk/scripts/latlong.html
*/
function midpoint(Position $from, Position $to): Position
{
$lat1 = $from->latitude();
$lng1 = $from->longitude();
$lat2 = $to->latitude();
$lng2 = $to->longitude();
$lat1 = deg2rad($lat1);
$lat2 = deg2rad($lat2);
$dLon = deg2rad($lng2 - $lng1);
$Bx = cos($lat2) * cos($dLon);
$By = cos($lat2) * sin($dLon);
$lat3 = atan2(
sin($lat1) + sin($lat2),
sqrt((cos($lat1) + $Bx) * (cos($lat1) + $Bx) + $By * $By)
);
$lon3 = deg2rad($lng1) + atan2($By, cos($lat1) + $Bx);
return Position::fromXY(rad2deg($lon3), rad2deg($lat3));
}
/**
* Calculates the destination point along a geodesic, given an initial
* heading and distance, from the given start point.
*
* @see http://www.movable-type.co.uk/scripts/latlong.html
*/
function endpoint(Position $start, float $heading, Distance $distance): Position
{
$lat = deg2rad($start->latitude());
$lng = deg2rad($start->longitude());
$angularDistance = $distance->meters() / Earth::RADIUS;
$heading = deg2rad($heading);
$lat2 = asin(
sin($lat) * cos($angularDistance) +
cos($lat) * sin($angularDistance) * cos($heading)
);
$lon2 = $lng + atan2(
sin($heading) * sin($angularDistance) * cos($lat),
cos($angularDistance) - sin($lat) * sin($lat2)
);
return Position::fromXY(rad2deg($lon2), rad2deg($lat2));
}
function circle(Position $center, Distance $radius, int $steps): Polygon
{
$points = [];
for ($i = 0; $i <= $steps; ++$i) {
$points[] = endpoint(
$center,
$i * (-360 / $steps),
$radius
);
}
return Polygon::fromPositions(...$points);
}
/**
* Normalize a latitude into the range (-90, 90) (upper and lower bound
* included).
*/
function normalizeLatitude(float $lat): float
{
$mod = fmod($lat, 360);
if ($mod < -90) {
$mod = -180 - $mod;
}
if ($mod > 90) {
$mod = 180 - $mod;
}
return $mod;
}
/**
* Normalize a longitude into the range (-180, 180) (lower bound excluded,
* upper bound included).
*/
function normalizeLongitude(float $lng): float
{
$mod = fmod($lng, 360);
if ($mod <= -180) {
$mod += 360;
}
if ($mod > 180) {
$mod -= 360;
}
return $mod;
}
================================================
FILE: src/functions_include.php
================================================
<?php
declare(strict_types=1);
if (!function_exists('Geokit\distanceHaversine')) {
require __DIR__ . '/functions.php';
}
================================================
FILE: tests/BoundingBoxTest.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use ArrayIterator;
use Generator;
use function iterator_to_array;
use function json_encode;
class BoundingBoxTest extends TestCase
{
protected function assertBoundingBox(BoundingBox $b, float $s, float $w, float $n, float $e): void
{
self::assertEquals($s, $b->southWest()->latitude());
self::assertEquals($w, $b->southWest()->longitude());
self::assertEquals($n, $b->northEast()->latitude());
self::assertEquals($e, $b->northEast()->longitude());
}
public function testConstructorShouldAcceptPositionsAsFirstAndSecondArgument(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1234, 2.5678), Position::fromXY(3.1234, 4.5678));
self::assertEquals(1.1234, $bbox->southWest()->longitude());
self::assertEquals(2.5678, $bbox->southWest()->latitude());
self::assertEquals(3.1234, $bbox->northEast()->longitude());
self::assertEquals(4.5678, $bbox->northEast()->latitude());
}
public function testConstructorShouldThrowExceptionForInvalidSouthCoordinate(): void
{
$this->expectException(Exception\LogicException::class);
BoundingBox::fromCornerPositions(Position::fromXY(90, 1), Position::fromXY(90, 0));
}
public function testFromCoordinatesWithArray(): void
{
$bbox = BoundingBox::fromCoordinates([1, 2, 3, 4]);
self::assertSame(1.0, $bbox->southWest()->longitude());
self::assertSame(2.0, $bbox->southWest()->latitude());
self::assertSame(1.0, $bbox->southWest()->longitude());
self::assertSame(2.0, $bbox->southWest()->latitude());
self::assertSame(3.0, $bbox->northEast()->longitude());
self::assertSame(4.0, $bbox->northEast()->latitude());
self::assertSame(3.0, $bbox->northEast()->longitude());
self::assertSame(4.0, $bbox->northEast()->latitude());
}
public function testFromCoordinatesWithIterator(): void
{
$bbox = BoundingBox::fromCoordinates(new ArrayIterator([1, 2, 3, 4]));
self::assertSame(1.0, $bbox->southWest()->longitude());
self::assertSame(2.0, $bbox->southWest()->latitude());
self::assertSame(1.0, $bbox->southWest()->longitude());
self::assertSame(2.0, $bbox->southWest()->latitude());
self::assertSame(3.0, $bbox->northEast()->longitude());
self::assertSame(4.0, $bbox->northEast()->latitude());
self::assertSame(3.0, $bbox->northEast()->longitude());
self::assertSame(4.0, $bbox->northEast()->latitude());
}
public function testFromCoordinatesWithGenerator(): void
{
$bbox = BoundingBox::fromCoordinates((/** @return Generator<float> */ static function (): Generator {
yield 1;
yield 2;
yield 3;
yield 4;
})());
self::assertSame(1.0, $bbox->southWest()->longitude());
self::assertSame(2.0, $bbox->southWest()->latitude());
self::assertSame(1.0, $bbox->southWest()->longitude());
self::assertSame(2.0, $bbox->southWest()->latitude());
self::assertSame(3.0, $bbox->northEast()->longitude());
self::assertSame(4.0, $bbox->northEast()->latitude());
self::assertSame(3.0, $bbox->northEast()->longitude());
self::assertSame(4.0, $bbox->northEast()->latitude());
}
public function testFromCoordinatesThrowsExceptionForMissingSouthWestXCoordinate(): void
{
$this->expectException(Exception\MissingCoordinateException::class);
BoundingBox::fromCoordinates([]);
}
public function testFromCoordinatesThrowsExceptionForMissingSouthWestYCoordinate(): void
{
$this->expectException(Exception\MissingCoordinateException::class);
BoundingBox::fromCoordinates([1]);
}
public function testFromCoordinatesThrowsExceptionForMissingNorthEastXCoordinate(): void
{
$this->expectException(Exception\MissingCoordinateException::class);
BoundingBox::fromCoordinates([1, 2]);
}
public function testFromCoordinatesThrowsExceptionForMissingNorthEastYCoordinate(): void
{
$this->expectException(Exception\MissingCoordinateException::class);
BoundingBox::fromCoordinates([1, 2, 3]);
}
public function testToCoordinates(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(1, 2), Position::fromXY(3, 4));
self::assertSame([1.0, 2.0, 3.0, 4.0], $bbox->toCoordinates());
}
public function testJsonSerialize(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1, 2), Position::fromXY(3.3, 4));
self::assertSame('[1.1,2,3.3,4]', json_encode($bbox));
}
public function testGetCenterShouldReturnAPositionObject(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1234, 2.5678), Position::fromXY(3.1234, 4.5678));
$center = Position::fromXY(2.1234, 3.5678);
self::assertEquals($center, $bbox->center());
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(179, -45), Position::fromXY(-179, 45));
$center = Position::fromXY(180, 0);
self::assertEquals($center, $bbox->center());
}
public function testGetSpanShouldReturnAPositionObject(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1234, 2.5678), Position::fromXY(3.1234, 4.5678));
$span = Position::fromXY(2, 2);
self::assertEquals($span, $bbox->span());
}
public function testContains(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(37, -122), Position::fromXY(38, -123));
self::assertTrue($bbox->contains(Position::fromXY(37, -122)));
self::assertTrue($bbox->contains(Position::fromXY(38, -123)));
self::assertFalse($bbox->contains(Position::fromXY(-12, -70)));
}
public function testExtendInACircle(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(0, 0), Position::fromXY(0, 0));
$bbox = $bbox->extend(Position::fromXY(1, 0));
$bbox = $bbox->extend(Position::fromXY(0, 1));
$bbox = $bbox->extend(Position::fromXY(-1, 0));
$bbox = $bbox->extend(Position::fromXY(0, -1));
self::assertBoundingBox($bbox, -1, -1, 1, 1);
}
public function testUnion(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(-122, 37), Position::fromXY(-122, 37));
$bbox = $bbox->union(BoundingBox::fromCornerPositions(Position::fromXY(123, -38), Position::fromXY(-123, 38)));
self::assertBoundingBox($bbox, -38, 123, 38, -122);
}
public function testCrossesAntimeridian(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(179, -45), Position::fromXY(-179, 45));
self::assertTrue($bbox->crossesAntimeridian());
self::assertEquals(90, $bbox->span()->latitude());
self::assertEquals(2, $bbox->span()->longitude());
}
public function testCrossesAntimeridianViaExtend(): void
{
$bbox = BoundingBox::fromCornerPositions(Position::fromXY(179, -45), Position::fromXY(-179, 45));
$bbox = $bbox->extend(Position::fromXY(-180, 90));
self::assertTrue($bbox->crossesAntimeridian());
self::assertEquals(45, $bbox->span()->latitude());
self::assertEquals(2, $bbox->span()->longitude());
}
public function testExpand(): void
{
$bbox = BoundingBox::fromCornerPositions(
Position::fromXY(179, -45),
Position::fromXY(-179, 45)
);
$expandedBbox = $bbox->expand(
new Distance(100, Distance::UNIT_KILOMETERS)
);
self::assertEquals(
-45.89932036372454,
$expandedBbox->southWest()->latitude()
);
self::assertEquals(
177.72811671076983,
$expandedBbox->southWest()->longitude()
);
self::assertEquals(
45.89932036372454,
$expandedBbox->northEast()->latitude()
);
self::assertEquals(
-177.72811671076983,
$expandedBbox->northEast()->longitude()
);
}
public function testShrink(): void
{
$bbox = BoundingBox::fromCornerPositions(
Position::fromXY(178.99872959034192, -45.000898315284132),
Position::fromXY(-178.99872959034192, 45.000898315284132)
);
$shrinkedBbox = $bbox->shrink(
new Distance(100, Distance::UNIT_KILOMETERS)
);
self::assertEquals(
-44.10157795155959,
$shrinkedBbox->southWest()->latitude()
);
self::assertEquals(
-179.7293671753848,
$shrinkedBbox->southWest()->longitude()
);
self::assertEquals(
44.10157795155959,
$shrinkedBbox->northEast()->latitude()
);
self::assertEquals(
179.7293671753848,
$shrinkedBbox->northEast()->longitude()
);
}
public function testShrinkTooMuch(): void
{
$bbox = BoundingBox::fromCornerPositions(
Position::fromXY(1, 1),
Position::fromXY(1, 1)
);
$shrinkedBbox = $bbox->shrink(
new Distance(100)
);
self::assertEquals(
1,
$shrinkedBbox->southWest()->latitude()
);
self::assertEquals(
1,
$shrinkedBbox->southWest()->longitude()
);
self::assertEquals(
1,
$shrinkedBbox->northEast()->latitude()
);
self::assertEquals(
1,
$shrinkedBbox->northEast()->longitude()
);
}
public function testToPolygon(): void
{
$bbox = BoundingBox::fromCornerPositions(
Position::fromXY(0, 0),
Position::fromXY(10, 10)
);
$polygon = $bbox->toPolygon();
self::assertCount(5, $polygon);
/** @var Position[] $array */
$array = iterator_to_array($polygon);
self::assertEquals(
0,
$array[0]->latitude()
);
self::assertEquals(
0,
$array[0]->longitude()
);
self::assertEquals(
0,
$array[1]->latitude()
);
self::assertEquals(
10,
$array[1]->longitude()
);
self::assertEquals(
10,
$array[2]->latitude()
);
self::assertEquals(
10,
$array[2]->longitude()
);
self::assertEquals(
10,
$array[3]->latitude()
);
self::assertEquals(
0,
$array[3]->longitude()
);
}
}
================================================
FILE: tests/DistanceTest.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use Generator;
use function sprintf;
class DistanceTest extends TestCase
{
public function testShouldConvertToMeters(): void
{
$distance = new Distance(1000);
self::assertSame(1000.0, $distance->meters());
}
public function testShouldConvertToMetersWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(1000.0, $distance->m());
}
public function testShouldConvertToKilometers(): void
{
$distance = new Distance(1000);
self::assertSame(1.0, $distance->kilometers());
}
public function testShouldConvertToKilometersWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(1.0, $distance->km());
}
public function testShouldConvertToMiles(): void
{
$distance = new Distance(1000);
self::assertSame(0.62137119223733395, $distance->miles());
}
public function testShouldConvertToMilesWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(0.62137119223733395, $distance->mi());
}
public function testShouldConvertToYards(): void
{
$distance = new Distance(1000);
self::assertSame(1093.6132983377079, $distance->yards());
}
public function testShouldConvertToYardsWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(1093.6132983377079, $distance->yd());
}
public function testShouldConvertToFeet(): void
{
$distance = new Distance(1000);
self::assertSame(3280.8398950131232, $distance->feet());
}
public function testShouldConvertToFeetWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(3280.8398950131232, $distance->ft());
}
public function testShouldConvertToInches(): void
{
$distance = new Distance(1000);
self::assertSame(39370.078740157485, $distance->inches());
}
public function testShouldConvertToInchesWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(39370.078740157485, $distance->in());
}
public function testShouldConvertToNauticalMiles(): void
{
$distance = new Distance(1000);
self::assertSame(0.5399568034557235, $distance->nautical());
}
public function testShouldConvertToNauticalWithAlias(): void
{
$distance = new Distance(1000);
self::assertSame(0.5399568034557235, $distance->nm());
}
public function testShouldThrowExceptionForInvalidUnit(): void
{
$this->expectException(Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid unit "foo".');
new Distance(1000, 'foo');
}
public function testShouldThrowExceptionForNegativeValue(): void
{
$this->expectException(Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('The distance must be a positive number, got "-1000".');
new Distance(-1000);
}
/**
* @dataProvider fromStringDataProvider
*/
public function testFromString(float $value, string $unit): void
{
self::assertEquals(1000, Distance::fromString(sprintf('%.15F%s', $value, $unit))->meters());
self::assertEquals(1000, Distance::fromString(sprintf('%.15F %s', $value, $unit))->meters(), 'With space');
}
public function fromStringDataProvider(): Generator
{
yield [
1000,
'',
];
yield [
1000,
'm',
];
yield [
1000,
'meter',
];
yield [
1000,
'meters',
];
yield [
1000,
'metre',
];
yield [
1000,
'metres',
];
yield [
1,
'km',
];
yield [
1,
'kilometer',
];
yield [
1,
'kilometers',
];
yield [
1,
'kilometre',
];
yield [
1,
'kilometres',
];
yield [
0.62137119223733,
'mi',
];
yield [
0.62137119223733,
'mile',
];
yield [
0.62137119223733,
'miles',
];
yield [
1093.6132983377079,
'yd',
];
yield [
1093.6132983377079,
'yard',
];
yield [
1093.6132983377079,
'yards',
];
yield [
3280.83989501312336,
'ft',
];
yield [
3280.83989501312336,
'foot',
];
yield [
3280.83989501312336,
'feet',
];
yield [
39370.078740157485,
'″',
];
yield [
39370.0787401574856,
'in',
];
yield [
39370.0787401574856,
'inch',
];
yield [
39370.078740157485,
'inches',
];
yield [
0.53995680345572,
'nm',
];
yield [
0.53995680345572,
'nautical',
];
yield [
0.53995680345572,
'nauticalmile',
];
yield [
0.53995680345572,
'nauticalmiles',
];
}
public function testFromStringThrowsExceptionForInvalidUnit(): void
{
$this->expectException(Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot create Distance from string "1000foo".');
Distance::fromString('1000foo');
}
public function testFromStringThrowsExceptionForNegativeValue(): void
{
$this->expectException(Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('The distance must be a positive number, got "-1000.5".');
Distance::fromString('-1000.5m');
}
}
================================================
FILE: tests/FunctionsTest.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use Generator;
class FunctionsTest extends TestCase
{
/**
* @dataProvider distanceHaversineDataProvider
*/
public function testDistanceHaversine(Position $pos1, Position $pos2, float $distance): void
{
self::assertEqualsWithDelta(
$distance,
distanceHaversine($pos1, $pos2)->meters(),
0.0001
);
}
public static function distanceHaversineDataProvider(): Generator
{
yield [
Position::fromXY(60.463472083210945, 44.65105198323727),
Position::fromXY(83.73959356918931, -35.21140778437257),
9185291.4233,
];
yield [
Position::fromXY(-100.69272816181183, 85.67559066228569),
Position::fromXY(-169.56520546227694, 8.659202512353659),
8873933.2562,
];
yield [
Position::fromXY(86.67973218485713, -61.20406142435968),
Position::fromXY(-112.75070607662201, -46.86954100616276),
7871751.1082,
];
yield [
Position::fromXY(114.92620809003711, 19.441748214885592),
Position::fromXY(5.652987342327833, 82.39083864726126),
8141668.5354,
];
yield [
Position::fromXY(71.53828611597419, -15.120142288506031),
Position::fromXY(176.72984121367335, -28.01164012402296),
10651065.6175,
];
yield [
Position::fromXY(69.48629681020975, -30.777964973822236),
Position::fromXY(-13.63121923059225, 8.096220837906003),
9817278.1798,
];
yield [
Position::fromXY(-144.45135304704309, -69.95015325956047),
Position::fromXY(-115.67441381514072, 23.054808229207993),
10590559.7265,
];
}
/**
* @dataProvider distanceVincentyDataProvider
*/
public function testDistanceVincenty(Position $pos1, Position $pos2, float $distance): void
{
self::assertEqualsWithDelta(
$distance,
distanceVincenty($pos1, $pos2)->meters(),
0.0001
);
}
public static function distanceVincentyDataProvider(): Generator
{
yield [
Position::fromXY(60.463472083210945, 44.65105198323727),
Position::fromXY(83.73959356918931, -35.21140778437257),
9151350.5841,
];
yield [
Position::fromXY(-100.69272816181183, 85.67559066228569),
Position::fromXY(-169.56520546227694, 8.659202512353659),
8872957.8831,
];
yield [
Position::fromXY(86.67973218485713, -61.20406142435968),
Position::fromXY(-112.75070607662201, -46.86954100616276),
7896462.2245,
];
yield [
Position::fromXY(114.92620809003711, 19.441748214885592),
Position::fromXY(5.652987342327833, 82.39083864726126),
8148846.7071,
];
yield [
Position::fromXY(71.53828611597419, -15.120142288506031),
Position::fromXY(176.72984121367335, -28.01164012402296),
10666157.5230,
];
yield [
Position::fromXY(69.48629681020975, -30.777964973822236),
Position::fromXY(-13.63121923059225, 8.096220837906003),
9818690.27471,
];
yield [
Position::fromXY(-144.45135304704309, -69.95015325956047),
Position::fromXY(-115.67441381514072, 23.054808229207993),
10564410.1591,
];
}
public function testDistanceHaversineCoIncidentPositions(): void
{
self::assertEquals(
0,
distanceVincenty(Position::fromXY(90, 90), Position::fromXY(90, 90))->meters()
);
}
public function testDistanceHaversineShouldNotConvergeForHalfTripAroundEquator(): void
{
$this->expectException(Exception\RuntimeException::class);
$this->expectExceptionMessage('Vincenty formula failed to converge.');
distanceVincenty(Position::fromXY(0, 0), Position::fromXY(180, 0));
}
public function testHeading(): void
{
self::assertEquals(90, heading(Position::fromXY(0, 0), Position::fromXY(1, 0)));
self::assertEquals(0, heading(Position::fromXY(0, 0), Position::fromXY(0, 1)));
self::assertEquals(270, heading(Position::fromXY(0, 0), Position::fromXY(-1, 0)));
self::assertEquals(180, heading(Position::fromXY(0, 0), Position::fromXY(0, -1)));
}
public function testMidpoint(): void
{
$midpoint = midpoint(
Position::fromXY(-96.958444, 32.918593),
Position::fromXY(-96.990159, 32.969527)
);
self::assertEquals(
32.94406100147102,
$midpoint->latitude()
);
self::assertEquals(
-96.974296932499726,
$midpoint->longitude()
);
}
public function testEndpoint(): void
{
$endpoint = endpoint(
Position::fromXY(-96.958444, 32.918593),
332,
new Distance(6389.09568)
);
self::assertEquals(
32.96932167481445,
$endpoint->latitude()
);
self::assertEquals(
-96.99059694331415,
$endpoint->longitude()
);
}
public function testCircle(): void
{
$center = Position::fromXY(-75.343, 39.984);
$distance = Distance::fromString('50km');
$circle = circle(
$center,
$distance,
32
);
self::assertCount(33, $circle);
self::assertTrue($circle->contains($center));
/** @var Position $point */
foreach ($circle as $point) {
self::assertEqualsWithDelta(
$distance->meters(),
distanceHaversine($center, $point)->meters(),
0.001
);
}
}
/**
* @dataProvider normalizeLatDataProvider
*/
public function testNormalizeLat(float $a, float $b): void
{
self::assertEquals($b, normalizeLatitude($a));
}
public function normalizeLatDataProvider(): Generator
{
yield [-365, -5];
yield [-185, 5];
yield [-95, -85];
yield [-90, -90];
yield [5, 5];
yield [90, 90];
yield [100, 80];
yield [185, -5];
yield [365, 5];
}
/**
* @dataProvider normalizeLngDataProvider
*/
public function testNormalizeLng(float $a, float $b): void
{
self::assertEquals($b, normalizeLongitude($a));
}
public function normalizeLngDataProvider(): Generator
{
yield [-545, 175];
yield [-365, -5];
yield [-360, 0];
yield [-185, 175];
yield [-180, 180];
yield [5, 5];
yield [180, 180];
yield [215, -145];
yield [360, 0];
yield [395, 35];
yield [540, 180];
}
}
================================================
FILE: tests/PolygonTest.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use ArrayIterator;
use Generator;
use function count;
use function iterator_to_array;
use function json_encode;
class PolygonTest extends TestCase
{
public function testConstructorAcceptsPositions(): void
{
$points = [
Position::fromXY(0, 0),
Position::fromXY(1, 1),
Position::fromXY(0, 1),
];
$polygon = Polygon::fromPositions(...$points);
/** @var Position[] $array */
$array = iterator_to_array($polygon);
self::assertEquals($points[0], $array[0]);
self::assertEquals(0, $array[0]->latitude());
self::assertEquals(1, $array[1]->latitude());
self::assertEquals(1, $array[2]->latitude());
}
public function testFromCoordinatesWithArray(): void
{
$polygon = Polygon::fromCoordinates([[1, 2], [2, 3]]);
self::assertCount(2, $polygon);
}
public function testFromCoordinatesWithIterator(): void
{
$polygon = Polygon::fromCoordinates(new ArrayIterator([[1, 2], [2, 3]]));
self::assertCount(2, $polygon);
}
public function testFromCoordinatesWithGenerator(): void
{
$polygon = Polygon::fromCoordinates((/** @return Generator<array<float>> */ static function (): Generator {
yield [1, 2];
yield [2, 3];
})());
self::assertCount(2, $polygon);
}
public function testCloseOpenPolygon(): void
{
$polygon = Polygon::fromPositions(
Position::fromXY(0, 0),
Position::fromXY(1, 0),
Position::fromXY(1, 1),
Position::fromXY(1, 0)
);
$closedPolygon = $polygon->close();
$array = iterator_to_array($closedPolygon);
self::assertEquals(Position::fromXY(0, 0), $array[count($closedPolygon) - 1]);
}
public function testCloseEmptyPolygon(): void
{
$polygon = Polygon::fromPositions();
$closedPolygon = $polygon->close();
self::assertCount(0, $closedPolygon);
}
public function testCloseAlreadyClosedPolygon(): void
{
$polygon = Polygon::fromPositions(
Position::fromXY(0, 0),
Position::fromXY(1, 0),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0)
);
$closedPolygon = $polygon->close();
$array = iterator_to_array($closedPolygon);
self::assertEquals(Position::fromXY(0, 0), $array[count($closedPolygon) - 1]);
}
/**
* @param array<Position> $polygonPositions
*
* @dataProvider containsDataProvider
*/
public function testContains(array $polygonPositions, Position $position, bool $expected): void
{
$polygon = Polygon::fromPositions(...$polygonPositions);
self::assertEquals($expected, $polygon->contains($position));
}
public function containsDataProvider(): Generator
{
// Closed counterclockwise polygons
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0),
],
Position::fromXY(0.5, 0.5),
true,
];
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0),
],
Position::fromXY(1.5, 0.5),
false,
];
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0),
],
Position::fromXY(-0.5, 0.5),
false,
];
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0),
],
Position::fromXY(0.5, 1.5),
false,
];
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0),
],
Position::fromXY(0.5, -0.5),
false,
];
// Closed clockwise polygons
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
Position::fromXY(0, 0),
],
Position::fromXY(0.5, 0.5),
true,
];
yield [
[
Position::fromXY(1, 1),
Position::fromXY(3, 2),
Position::fromXY(2, 3),
Position::fromXY(1, 1),
],
Position::fromXY(1.5, 1.5),
true,
];
// Open counterclockwise polygons
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
],
Position::fromXY(0.5, 0.5),
true,
];
// Open clockwise polygons
yield [
[
Position::fromXY(0, 0),
Position::fromXY(0, 1),
Position::fromXY(1, 1),
Position::fromXY(1, 0),
],
Position::fromXY(0.5, 0.5),
true,
];
yield [
[
Position::fromXY(1, 1),
Position::fromXY(3, 2),
Position::fromXY(2, 3),
],
Position::fromXY(1.5, 1.5),
true,
];
// Empty polygon
yield [
[],
Position::fromXY(0.5, 0.5),
false,
];
}
public function testToBoundingBox(): void
{
$polygon = Polygon::fromPositions(
Position::fromXY(0, 0),
Position::fromXY(1, 0),
Position::fromXY(1, 1),
Position::fromXY(1, 0)
);
$bbox = $polygon->toBoundingBox();
self::assertEquals(0, $bbox->southWest()->latitude());
self::assertEquals(0, $bbox->southWest()->longitude());
self::assertEquals(1, $bbox->northEast()->latitude());
self::assertEquals(1, $bbox->northEast()->longitude());
}
public function testToBoundingBoxThrowsExceptionForEmptyPolygon(): void
{
$this->expectException(Exception\LogicException::class);
$this->expectExceptionMessage('Cannot create a BoundingBox from empty Polygon.');
$polygon = Polygon::fromPositions();
$polygon->toBoundingBox();
}
public function testCountable(): void
{
$polygon = Polygon::fromPositions(
Position::fromXY(0, 0)
);
self::assertCount(1, $polygon);
}
public function testIterable(): void
{
self::assertIsIterable(Polygon::fromPositions());
}
public function testToCoordinates(): void
{
$polygon = Polygon::fromPositions(
Position::fromXY(1, 2)
);
$coordinates = $polygon->toCoordinates();
foreach ($coordinates as $positionCoordinates) {
self::assertSame([1.0, 2.0], $positionCoordinates);
}
}
public function testJsonSerialize(): void
{
$polygon = Polygon::fromPositions(
Position::fromXY(1.1, 2)
);
self::assertSame('[[1.1,2]]', json_encode($polygon));
}
}
================================================
FILE: tests/PositionTest.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use ArrayIterator;
use Generator;
use PHPUnit\Framework\TestCase;
use function json_encode;
class PositionTest extends TestCase
{
public function testConstructor(): void
{
$position = Position::fromXY(1.0, 2.0);
self::assertSame(1.0, $position->x());
self::assertSame(2.0, $position->y());
self::assertSame(1.0, $position->longitude());
self::assertSame(2.0, $position->latitude());
}
public function testConstructorWithNormalization(): void
{
$position = Position::fromXY(181, 91);
self::assertSame(181.0, $position->x());
self::assertSame(91.0, $position->y());
self::assertSame(-179.0, $position->longitude());
self::assertSame(89.0, $position->latitude());
}
public function testConstructorWithInts(): void
{
$position = Position::fromXY(1, 2);
self::assertSame(1.0, $position->x());
self::assertSame(2.0, $position->y());
self::assertSame(1.0, $position->longitude());
self::assertSame(2.0, $position->latitude());
}
public function testFromCoordinatesWithArray(): void
{
$position = Position::fromCoordinates([1, 2]);
self::assertSame(1.0, $position->x());
self::assertSame(2.0, $position->y());
self::assertSame(1.0, $position->longitude());
self::assertSame(2.0, $position->latitude());
}
public function testFromCoordinatesWithIterator(): void
{
$position = Position::fromCoordinates(new ArrayIterator([1, 2]));
self::assertSame(1.0, $position->x());
self::assertSame(2.0, $position->y());
self::assertSame(1.0, $position->longitude());
self::assertSame(2.0, $position->latitude());
}
public function testFromCoordinatesWithGenerator(): void
{
$position = Position::fromCoordinates((/** @return Generator<float> */ static function (): Generator {
yield 1;
yield 2;
})());
self::assertSame(1.0, $position->x());
self::assertSame(2.0, $position->y());
self::assertSame(1.0, $position->longitude());
self::assertSame(2.0, $position->latitude());
}
public function testFromCoordinatesThrowsExceptionForMissingXCoordinate(): void
{
$this->expectException(Exception\MissingCoordinateException::class);
Position::fromCoordinates([]);
}
public function testFromCoordinatesThrowsExceptionForMissingYCoordinate(): void
{
$this->expectException(Exception\MissingCoordinateException::class);
Position::fromCoordinates([1]);
}
public function testToCoordinates(): void
{
$position = Position::fromXY(1, 2);
self::assertSame([1.0, 2.0], $position->toCoordinates());
}
public function testJsonSerialize(): void
{
$position = Position::fromXY(1.1, 2);
self::assertSame('[1.1,2]', json_encode($position));
}
}
================================================
FILE: tests/TestCase.php
================================================
<?php
declare(strict_types=1);
namespace Geokit;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
}
gitextract_40il9h5a/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── static.yml
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── psalm.xml
├── src/
│ ├── BoundingBox.php
│ ├── Distance.php
│ ├── Earth.php
│ ├── Exception/
│ │ ├── Exception.php
│ │ ├── InvalidArgumentException.php
│ │ ├── LogicException.php
│ │ ├── MissingCoordinateException.php
│ │ └── RuntimeException.php
│ ├── Polygon.php
│ ├── Position.php
│ ├── functions.php
│ └── functions_include.php
└── tests/
├── BoundingBoxTest.php
├── DistanceTest.php
├── FunctionsTest.php
├── PolygonTest.php
├── PositionTest.php
└── TestCase.php
SYMBOL INDEX (161 symbols across 17 files)
FILE: src/BoundingBox.php
class BoundingBox (line 18) | final class BoundingBox implements JsonSerializable
method __construct (line 26) | private function __construct(Position $southWest, Position $northEast)
method fromCornerPositions (line 38) | public static function fromCornerPositions(
method fromCoordinates (line 48) | public static function fromCoordinates(iterable $iterable): self
method toCoordinates (line 85) | public function toCoordinates(): iterable
method jsonSerialize (line 98) | public function jsonSerialize(): array
method southWest (line 108) | public function southWest(): Position
method northEast (line 113) | public function northEast(): Position
method center (line 118) | public function center(): Position
method span (line 136) | public function span(): Position
method crossesAntimeridian (line 144) | public function crossesAntimeridian(): bool
method contains (line 149) | public function contains(Position $position): bool
method extend (line 164) | public function extend(Position $position): self
method union (line 188) | public function union(self $bbox): self
method expand (line 195) | public function expand(Distance $distance): self
method shrink (line 200) | public function shrink(Distance $distance): self
method toPolygon (line 205) | public function toPolygon(): Polygon
method transformBoundingBox (line 219) | private static function transformBoundingBox(self $bbox, float $distan...
method containsLng (line 251) | private function containsLng(float $lng): bool
method lngSpan (line 262) | private function lngSpan(float $west, float $east): float
FILE: src/Distance.php
class Distance (line 14) | final class Distance
method __construct (line 64) | public function __construct(float $value, string $unit = self::DEFAULT...
method fromString (line 87) | public static function fromString(string $input): self
method meters (line 111) | public function meters(): float
method m (line 116) | public function m(): float
method kilometers (line 121) | public function kilometers(): float
method km (line 126) | public function km(): float
method miles (line 131) | public function miles(): float
method mi (line 136) | public function mi(): float
method yards (line 141) | public function yards(): float
method yd (line 146) | public function yd(): float
method feet (line 151) | public function feet(): float
method ft (line 156) | public function ft(): float
method inches (line 161) | public function inches(): float
method in (line 166) | public function in(): float
method nautical (line 171) | public function nautical(): float
method nm (line 176) | public function nm(): float
FILE: src/Earth.php
class Earth (line 11) | final class Earth
FILE: src/Exception/Exception.php
type Exception (line 7) | interface Exception
FILE: src/Exception/InvalidArgumentException.php
class InvalidArgumentException (line 7) | class InvalidArgumentException extends \InvalidArgumentException impleme...
FILE: src/Exception/LogicException.php
class LogicException (line 7) | class LogicException extends \LogicException implements Exception
FILE: src/Exception/MissingCoordinateException.php
class MissingCoordinateException (line 9) | final class MissingCoordinateException extends InvalidArgumentException
method create (line 11) | public static function create(
FILE: src/Exception/RuntimeException.php
class RuntimeException (line 7) | class RuntimeException extends \RuntimeException implements Exception
FILE: src/Polygon.php
class Polygon (line 16) | final class Polygon implements Countable, IteratorAggregate, JsonSeriali...
method __construct (line 21) | private function __construct(Position ...$positions)
method fromPositions (line 26) | public static function fromPositions(Position ...$positions): self
method fromCoordinates (line 34) | public static function fromCoordinates(iterable $iterable): self
method close (line 45) | public function close(): self
method contains (line 71) | public function contains(Position $position): bool
method toBoundingBox (line 106) | public function toBoundingBox(): BoundingBox
method count (line 125) | public function count(): int
method getIterator (line 130) | public function getIterator(): Generator
method toCoordinates (line 138) | public function toCoordinates(): iterable
method jsonSerialize (line 148) | public function jsonSerialize(): array
FILE: src/Position.php
class Position (line 11) | final class Position implements JsonSerializable
method __construct (line 19) | private function __construct(float $x, float $y)
method fromXY (line 25) | public static function fromXY(float $x, float $y): self
method fromCoordinates (line 33) | public static function fromCoordinates(iterable $iterable): self
method x (line 56) | public function x(): float
method y (line 61) | public function y(): float
method longitude (line 66) | public function longitude(): float
method latitude (line 71) | public function latitude(): float
method toCoordinates (line 79) | public function toCoordinates(): iterable
method jsonSerialize (line 87) | public function jsonSerialize(): array
FILE: src/functions.php
function distanceHaversine (line 26) | function distanceHaversine(Position $from, Position $to): Distance
function distanceVincenty (line 52) | function distanceVincenty(Position $from, Position $to): Distance
function heading (line 121) | function heading(Position $from, Position $to): float
function midpoint (line 147) | function midpoint(Position $from, Position $to): Position
function endpoint (line 176) | function endpoint(Position $start, float $heading, Distance $distance): ...
function circle (line 196) | function circle(Position $center, Distance $radius, int $steps): Polygon
function normalizeLatitude (line 215) | function normalizeLatitude(float $lat): float
function normalizeLongitude (line 234) | function normalizeLongitude(float $lng): float
FILE: tests/BoundingBoxTest.php
class BoundingBoxTest (line 12) | class BoundingBoxTest extends TestCase
method assertBoundingBox (line 14) | protected function assertBoundingBox(BoundingBox $b, float $s, float $...
method testConstructorShouldAcceptPositionsAsFirstAndSecondArgument (line 22) | public function testConstructorShouldAcceptPositionsAsFirstAndSecondAr...
method testConstructorShouldThrowExceptionForInvalidSouthCoordinate (line 33) | public function testConstructorShouldThrowExceptionForInvalidSouthCoor...
method testFromCoordinatesWithArray (line 39) | public function testFromCoordinatesWithArray(): void
method testFromCoordinatesWithIterator (line 54) | public function testFromCoordinatesWithIterator(): void
method testFromCoordinatesWithGenerator (line 69) | public function testFromCoordinatesWithGenerator(): void
method testFromCoordinatesThrowsExceptionForMissingSouthWestXCoordinate (line 89) | public function testFromCoordinatesThrowsExceptionForMissingSouthWestX...
method testFromCoordinatesThrowsExceptionForMissingSouthWestYCoordinate (line 96) | public function testFromCoordinatesThrowsExceptionForMissingSouthWestY...
method testFromCoordinatesThrowsExceptionForMissingNorthEastXCoordinate (line 103) | public function testFromCoordinatesThrowsExceptionForMissingNorthEastX...
method testFromCoordinatesThrowsExceptionForMissingNorthEastYCoordinate (line 110) | public function testFromCoordinatesThrowsExceptionForMissingNorthEastY...
method testToCoordinates (line 117) | public function testToCoordinates(): void
method testJsonSerialize (line 124) | public function testJsonSerialize(): void
method testGetCenterShouldReturnAPositionObject (line 131) | public function testGetCenterShouldReturnAPositionObject(): void
method testGetSpanShouldReturnAPositionObject (line 144) | public function testGetSpanShouldReturnAPositionObject(): void
method testContains (line 153) | public function testContains(): void
method testExtendInACircle (line 163) | public function testExtendInACircle(): void
method testUnion (line 173) | public function testUnion(): void
method testCrossesAntimeridian (line 181) | public function testCrossesAntimeridian(): void
method testCrossesAntimeridianViaExtend (line 190) | public function testCrossesAntimeridianViaExtend(): void
method testExpand (line 201) | public function testExpand(): void
method testShrink (line 230) | public function testShrink(): void
method testShrinkTooMuch (line 259) | public function testShrinkTooMuch(): void
method testToPolygon (line 288) | public function testToPolygon(): void
FILE: tests/DistanceTest.php
class DistanceTest (line 10) | class DistanceTest extends TestCase
method testShouldConvertToMeters (line 12) | public function testShouldConvertToMeters(): void
method testShouldConvertToMetersWithAlias (line 18) | public function testShouldConvertToMetersWithAlias(): void
method testShouldConvertToKilometers (line 24) | public function testShouldConvertToKilometers(): void
method testShouldConvertToKilometersWithAlias (line 30) | public function testShouldConvertToKilometersWithAlias(): void
method testShouldConvertToMiles (line 36) | public function testShouldConvertToMiles(): void
method testShouldConvertToMilesWithAlias (line 42) | public function testShouldConvertToMilesWithAlias(): void
method testShouldConvertToYards (line 48) | public function testShouldConvertToYards(): void
method testShouldConvertToYardsWithAlias (line 54) | public function testShouldConvertToYardsWithAlias(): void
method testShouldConvertToFeet (line 60) | public function testShouldConvertToFeet(): void
method testShouldConvertToFeetWithAlias (line 66) | public function testShouldConvertToFeetWithAlias(): void
method testShouldConvertToInches (line 72) | public function testShouldConvertToInches(): void
method testShouldConvertToInchesWithAlias (line 78) | public function testShouldConvertToInchesWithAlias(): void
method testShouldConvertToNauticalMiles (line 84) | public function testShouldConvertToNauticalMiles(): void
method testShouldConvertToNauticalWithAlias (line 90) | public function testShouldConvertToNauticalWithAlias(): void
method testShouldThrowExceptionForInvalidUnit (line 96) | public function testShouldThrowExceptionForInvalidUnit(): void
method testShouldThrowExceptionForNegativeValue (line 103) | public function testShouldThrowExceptionForNegativeValue(): void
method testFromString (line 113) | public function testFromString(float $value, string $unit): void
method fromStringDataProvider (line 119) | public function fromStringDataProvider(): Generator
method testFromStringThrowsExceptionForInvalidUnit (line 262) | public function testFromStringThrowsExceptionForInvalidUnit(): void
method testFromStringThrowsExceptionForNegativeValue (line 269) | public function testFromStringThrowsExceptionForNegativeValue(): void
FILE: tests/FunctionsTest.php
class FunctionsTest (line 9) | class FunctionsTest extends TestCase
method testDistanceHaversine (line 14) | public function testDistanceHaversine(Position $pos1, Position $pos2, ...
method distanceHaversineDataProvider (line 23) | public static function distanceHaversineDataProvider(): Generator
method testDistanceVincenty (line 71) | public function testDistanceVincenty(Position $pos1, Position $pos2, f...
method distanceVincentyDataProvider (line 80) | public static function distanceVincentyDataProvider(): Generator
method testDistanceHaversineCoIncidentPositions (line 125) | public function testDistanceHaversineCoIncidentPositions(): void
method testDistanceHaversineShouldNotConvergeForHalfTripAroundEquator (line 133) | public function testDistanceHaversineShouldNotConvergeForHalfTripAroun...
method testHeading (line 141) | public function testHeading(): void
method testMidpoint (line 149) | public function testMidpoint(): void
method testEndpoint (line 166) | public function testEndpoint(): void
method testCircle (line 184) | public function testCircle(): void
method testNormalizeLat (line 212) | public function testNormalizeLat(float $a, float $b): void
method normalizeLatDataProvider (line 217) | public function normalizeLatDataProvider(): Generator
method testNormalizeLng (line 233) | public function testNormalizeLng(float $a, float $b): void
method normalizeLngDataProvider (line 238) | public function normalizeLngDataProvider(): Generator
FILE: tests/PolygonTest.php
class PolygonTest (line 13) | class PolygonTest extends TestCase
method testConstructorAcceptsPositions (line 15) | public function testConstructorAcceptsPositions(): void
method testFromCoordinatesWithArray (line 34) | public function testFromCoordinatesWithArray(): void
method testFromCoordinatesWithIterator (line 41) | public function testFromCoordinatesWithIterator(): void
method testFromCoordinatesWithGenerator (line 48) | public function testFromCoordinatesWithGenerator(): void
method testCloseOpenPolygon (line 58) | public function testCloseOpenPolygon(): void
method testCloseEmptyPolygon (line 74) | public function testCloseEmptyPolygon(): void
method testCloseAlreadyClosedPolygon (line 83) | public function testCloseAlreadyClosedPolygon(): void
method testContains (line 105) | public function testContains(array $polygonPositions, Position $positi...
method containsDataProvider (line 112) | public function containsDataProvider(): Generator
method testToBoundingBox (line 241) | public function testToBoundingBox(): void
method testToBoundingBoxThrowsExceptionForEmptyPolygon (line 258) | public function testToBoundingBoxThrowsExceptionForEmptyPolygon(): void
method testCountable (line 268) | public function testCountable(): void
method testIterable (line 277) | public function testIterable(): void
method testToCoordinates (line 282) | public function testToCoordinates(): void
method testJsonSerialize (line 295) | public function testJsonSerialize(): void
FILE: tests/PositionTest.php
class PositionTest (line 12) | class PositionTest extends TestCase
method testConstructor (line 14) | public function testConstructor(): void
method testConstructorWithNormalization (line 24) | public function testConstructorWithNormalization(): void
method testConstructorWithInts (line 34) | public function testConstructorWithInts(): void
method testFromCoordinatesWithArray (line 44) | public function testFromCoordinatesWithArray(): void
method testFromCoordinatesWithIterator (line 54) | public function testFromCoordinatesWithIterator(): void
method testFromCoordinatesWithGenerator (line 64) | public function testFromCoordinatesWithGenerator(): void
method testFromCoordinatesThrowsExceptionForMissingXCoordinate (line 77) | public function testFromCoordinatesThrowsExceptionForMissingXCoordinat...
method testFromCoordinatesThrowsExceptionForMissingYCoordinate (line 84) | public function testFromCoordinatesThrowsExceptionForMissingYCoordinat...
method testToCoordinates (line 91) | public function testToCoordinates(): void
method testJsonSerialize (line 98) | public function testJsonSerialize(): void
FILE: tests/TestCase.php
class TestCase (line 9) | abstract class TestCase extends BaseTestCase
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (78K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 1117,
"preview": "name: CI\n\non:\n push:\n pull_request:\n\njobs:\n tests:\n name: Tests (PHP ${{ matrix.php }})\n runs-on: ubuntu-latest"
},
{
"path": ".github/workflows/static.yml",
"chars": 1136,
"preview": "name: Static analysis\n\non:\n push:\n branches:\n - master\n pull_request:\n\njobs:\n php-cs-fixer:\n name: PHP-CS-"
},
{
"path": ".gitignore",
"chars": 115,
"preview": "build/\nvendor/\n.php-cs-fixer.php\n.php-cs-fixer.cache\n.phpunit.result.cache\ncomposer.lock\ncomposer.phar\nphpunit.xml\n"
},
{
"path": ".php-cs-fixer.dist.php",
"chars": 843,
"preview": "<?php\n\n$finder = (new PhpCsFixer\\Finder())\n ->in(__DIR__);\n\nreturn (new PhpCsFixer\\Config())\n ->setRiskyAllowed(tr"
},
{
"path": "LICENSE",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2011-2022 Jan Sorgalla\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 6408,
"preview": "Geokit\n======\n\nGeokit is a PHP toolkit to solve geo-related tasks like:\n\n* Distance calculations.\n* Heading, midpoint an"
},
{
"path": "composer.json",
"chars": 807,
"preview": "{\n \"name\": \"geokit/geokit\",\n \"description\": \"Geo-Toolkit for PHP\",\n \"keywords\": [\"geo\", \"geometry\", \"geography\""
},
{
"path": "phpunit.xml.dist",
"chars": 589,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noName"
},
{
"path": "psalm.xml",
"chars": 592,
"preview": "<?xml version=\"1.0\"?>\n<psalm\n xmlns=\"https://getpsalm.org/schema/config\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSch"
},
{
"path": "src/BoundingBox.php",
"chars": 7659,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Geokit\\Exception\\MissingCoordinateException;\nuse JsonSerializabl"
},
{
"path": "src/Distance.php",
"chars": 4512,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse function json_encode;\nuse function preg_match;\nuse function spri"
},
{
"path": "src/Earth.php",
"chars": 731,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\n/**\n * @see http://en.wikipedia.org/wiki/World_Geodetic_System\n * @s"
},
{
"path": "src/Exception/Exception.php",
"chars": 86,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\ninterface Exception\n{\n}\n"
},
{
"path": "src/Exception/InvalidArgumentException.php",
"chars": 152,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nclass InvalidArgumentException extends \\InvalidArgumentExc"
},
{
"path": "src/Exception/LogicException.php",
"chars": 132,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nclass LogicException extends \\LogicException implements Ex"
},
{
"path": "src/Exception/MissingCoordinateException.php",
"chars": 449,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nuse function sprintf;\n\nfinal class MissingCoordinateExcept"
},
{
"path": "src/Exception/RuntimeException.php",
"chars": 136,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nclass RuntimeException extends \\RuntimeException implement"
},
{
"path": "src/Polygon.php",
"chars": 3506,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Countable;\nuse Generator;\nuse IteratorAggregate;\nuse JsonSeriali"
},
{
"path": "src/Position.php",
"chars": 1720,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Geokit\\Exception\\MissingCoordinateException;\nuse JsonSerializabl"
},
{
"path": "src/functions.php",
"chars": 6594,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse function abs;\nuse function asin;\nuse function atan;\nuse function"
},
{
"path": "src/functions_include.php",
"chars": 127,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nif (!function_exists('Geokit\\distanceHaversine')) {\n require __DIR__ . '/functions.p"
},
{
"path": "tests/BoundingBoxTest.php",
"chars": 10891,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse ArrayIterator;\nuse Generator;\nuse function iterator_to_array;\nus"
},
{
"path": "tests/DistanceTest.php",
"chars": 6201,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Generator;\nuse function sprintf;\n\nclass DistanceTest extends Tes"
},
{
"path": "tests/FunctionsTest.php",
"chars": 7062,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Generator;\n\nclass FunctionsTest extends TestCase\n{\n /**\n "
},
{
"path": "tests/PolygonTest.php",
"chars": 7965,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse ArrayIterator;\nuse Generator;\nuse function count;\nuse function i"
},
{
"path": "tests/PositionTest.php",
"chars": 3030,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse ArrayIterator;\nuse Generator;\nuse PHPUnit\\Framework\\TestCase;\nus"
},
{
"path": "tests/TestCase.php",
"chars": 150,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\n\nabstract class Test"
}
]
About this extraction
This page contains the full source code of the jsor/geokit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (72.1 KB), approximately 19.8k tokens, and a symbol index with 161 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.