Full Code of jsor/geokit for AI

main 67b44ef6f3e8 cached
27 files
72.1 KB
19.8k tokens
161 symbols
1 requests
Download .txt
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.

[![Build Status](https://github.com/jsor/geokit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jsor/geokit/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/jsor/geokit/badge.svg?branch=main&service=github)](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
{
}
Download .txt
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
Download .txt
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.

Copied to clipboard!