[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  tests:\n    name: Tests (PHP ${{ matrix.php }})\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - php: '7.4'\n          - php: '8.0'\n            code-coverage: 'yes'\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          coverage: pcov\n\n      - name: Install dependencies\n        run: composer update --no-interaction --no-progress --prefer-dist\n\n      - name: Run tests\n        if: matrix.code-coverage != 'yes'\n        run: vendor/bin/phpunit --coverage-text\n\n      - name: Run tests with code coverage\n        if: matrix.code-coverage == 'yes'\n        run: vendor/bin/phpunit --coverage-clover build/logs/clover.xml\n\n      - name: Upload coverage results to Coveralls\n        if: matrix.code-coverage == 'yes'\n        env:\n          COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          composer global require php-coveralls/php-coveralls\n          php-coveralls -v\n"
  },
  {
    "path": ".github/workflows/static.yml",
    "content": "name: Static analysis\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\njobs:\n  php-cs-fixer:\n    name: PHP-CS-Fixer\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '7.4'\n          coverage: none\n          extensions: mbstring\n          tools: \"cs2pr\"\n\n      - name: Install dependencies\n        run: composer update --no-interaction --no-progress --prefer-dist\n\n      - name: Run PHP CS Fixer\n        run: vendor/bin/php-cs-fixer fix --dry-run --format=checkstyle | cs2pr\n\n  psalm:\n    name: Psalm\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: '7.4'\n          coverage: none\n          extensions: mbstring, intl\n\n      - name: Install dependencies\n        run: composer update --no-interaction --no-progress --prefer-dist\n\n      - name: Run Psalm\n        run: vendor/bin/psalm --no-progress --output-format=github\n"
  },
  {
    "path": ".gitignore",
    "content": "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",
    "content": "<?php\n\n$finder = (new PhpCsFixer\\Finder())\n    ->in(__DIR__);\n\nreturn (new PhpCsFixer\\Config())\n    ->setRiskyAllowed(true)\n    ->setRules([\n        '@Symfony' => true,\n        '@Symfony:risky' => true,\n        'array_syntax' => [\n            'syntax' => 'short',\n        ],\n        'concat_space' => [\n            'spacing' => 'one',\n        ],\n        'global_namespace_import' => [\n            'import_classes' => true,\n            'import_constants' => null,\n            'import_functions' => true,\n        ],\n        'native_constant_invocation' => [\n            'fix_built_in' => false,\n        ],\n        'ordered_imports' => [\n            'imports_order' => [\n                'class',\n                'function',\n                'const',\n            ],\n        ],\n        'single_line_throw' => false,\n    ])\n    ->setFinder($finder);\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2011-2022 Jan Sorgalla\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "Geokit\n======\n\nGeokit is a PHP toolkit to solve geo-related tasks like:\n\n* Distance calculations.\n* Heading, midpoint and endpoint calculations.\n* Rectangular bounding box calculations.\n\n[![Build Status](https://github.com/jsor/geokit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/jsor/geokit/actions/workflows/ci.yml)\n[![Coverage Status](https://coveralls.io/repos/jsor/geokit/badge.svg?branch=main&service=github)](https://coveralls.io/github/jsor/geokit?branch=main)\n\n* [Installation](#installation)\n* [Reference](#reference)\n    * [Distance](#distance)\n    * [Position](#position)\n    * [BoundingBox](#boundingbox)\n    * [Polygon](#polygon)\n    * [Functions](#functions)\n        * [Distance calculations](#distance-calculations)\n        * [Transformations](#transformations)\n        * [Other calculations](#other-calculations)\n* [License](#license)\n\nInstallation\n------------\n\nInstall the latest version with [Composer](https://getcomposer.org).\n\n```bash\ncomposer require geokit/geokit\n```\n\nCheck the [Packagist page](https://packagist.org/packages/geokit/geokit) for all\navailable versions.\n\nReference\n---------\n\n### Distance\n\nA Distance instance allows for a convenient representation of a distance unit of\nmeasure.\n\n```php\nuse Geokit\\Distance;\n\n$distance = new Distance(1000); // Defaults to meters\n// or\n$distance = new Distance(1, Distance::UNIT_KILOMETERS);\n\n$meters = $distance->meters();\n$kilometers = $distance->kilometers();\n$miles = $distance->miles();\n$yards = $distance->yards();\n$feet = $distance->feet();\n$inches = $distance->inches();\n$nauticalMiles = $distance->nautical();\n```\n\nA Distance can also be created from a string with an optional unit.\n\n```php\nuse Geokit\\Distance;\n\n$distance = Distance::fromString('1000'); // Defaults to meters\n$distance = Distance::fromString('1000m');\n$distance = Distance::fromString('1km');\n$distance = Distance::fromString('100 miles');\n$distance = Distance::fromString('100 yards');\n$distance = Distance::fromString('1 foot');\n$distance = Distance::fromString('1 inch');\n$distance = Distance::fromString('234nm');\n```\n\n### Position\n\nA `Position` is a fundamental construct representing a geographical position in\n`x` (or `longitude`) and `y` (or `latitude`) coordinates.\n\nNote, that `x`/`y` coordinates are kept as is, while `longitude`/`latitude` are\nnormalized.\n\n* Longitudes range between -180 and 180 degrees, inclusive. Longitudes above 180\n  or below -180 are normalized. For example, 480, 840 and 1200 will all be\n  normalized to 120 degrees.\n* Latitudes range between -90 and 90 degrees, inclusive. Latitudes above 90 or\n  below -90 are normalized. For example, 100 will be normalized to 80 degrees.\n\n```php\nuse Geokit\\Position;\n\n$position = new Position(181, 91);\n\n$x = $position->x(); // Returns 181.0\n$y = $position->y(); // Returns 91.0\n$longitude = $position->longitude(); // Returns -179.0, normalized\n$latitude = $position->latitude(); // Returns 89.0, normalized\n```\n\n### BoundingBox\n\nA BoundingBox instance represents a rectangle in geographical coordinates,\nincluding one that crosses the 180 degrees longitudinal meridian.\n\nIt is constructed from its left-bottom (south-west) and right-top (north-east)\ncorner points.\n\n```php\nuse Geokit\\BoundingBox;\nuse Geokit\\Position;\n\n$southWest = Position::fromXY(2, 1);\n$northEast = Position::fromXY(2, 1);\n\n$boundingBox = BoundingBox::fromCornerPositions($southWest, $northEast);\n\n$southWestPosition = $boundingBox->southWest();\n$northEastPosition = $boundingBox->northEast();\n\n$center = $boundingBox->center();\n\n$span = $boundingBox->span();\n\n$boolean = $boundingBox->contains($position);\n\n$newBoundingBox = $boundingBox->extend($position);\n$newBoundingBox = $boundingBox->union($otherBoundingBox);\n```\n\nWith the `expand()` and `shrink()` methods, you can expand or shrink a\nBoundingBox instance by a distance.\n\n```php\nuse Geokit\\Distance;\n\n$expandedBoundingBox = $boundingBox->expand(\n    Distance::fromString('10km')\n);\n\n$shrinkedBoundingBox = $boundingBox->shrink(\n    Distance::fromString('10km')\n);\n```\n\nThe `toPolygon()` method converts the BoundingBox to an equivalent Polygon\ninstance.\n\n```php\n$polygon = $boundingBox->toPolygon();\n```\n\n### Polygon\n\nA Polygon instance represents a two-dimensional shape of connected line segments\nand may either be closed (the first and last point are the same) or open.\n\n```php\nuse Geokit\\BoundingBox;\nuse Geokit\\Polygon;\nuse Geokit\\Position;\n\n$polygon = Polygon::fromPositions(\n    Position::fromXY(0, 0),\n    Position::fromXY(1, 0),\n    Position::fromXY(1, 1)\n);\n\n$closedPolygon = $polygon->close();\n\n/** @var Position $position */\nforeach ($polygon as $position) {\n}\n\n$polygon->contains(Position::fromXY(0.5, 0.5)); // true\n\n/** @var BoundingBox $boundingBox */\n$boundingBox = $polygon->toBoundingBox();\n```\n\n### Functions\n\nGeokit provides several functions to perform geographic calculations.\n\n#### Distance calculations\n\n* `distanceHaversine(Position $from, Position $to)`:\n  Calculates the approximate sea level great circle (Earth) distance between two\n  points using the Haversine formula.\n* `distanceVincenty(Position $from, Position $to)`:\n  Calculates the geodetic distance between two points using the Vincenty inverse\n  formula for ellipsoids.\n\n```php\nuse function Geokit\\distanceHaversine;\nuse function Geokit\\distanceVincenty;\n\n$distance1 = distanceHaversine($from, $to);\n$distance2 = distanceVincenty($from, $to);\n```\n\nBoth functions return a [Distance](#distance) instance.\n\n#### Transformations\n\nThe `circle()` function calculates a closed circle Polygon given a center,\nradius and steps for precision.\n\n```php\nuse Geokit\\Distance;\nuse Geokit\\Position;\nuse function Geokit\\circle;\n\n$circlePolygon = circle(\n    Position::fromXY(8.50207515, 49.50042565), \n    Distance::fromString('5km'),\n    32\n);\n```\n\n#### Other calculations\n\nOther useful functions are:\n\n* `heading(Position $from, Position $to)`: Calculates the (initial) heading from\n  the first point to the second point in degrees.\n* `midpoint(Position $from, Position $to)`: Calculates an intermediate point on\n  the geodesic between the two given points.\n* `endpoint(Position $start, float $heading, Geokit\\Distance $distance)`:\n  Calculates the destination point along a geodesic, given an initial heading\n  and distance, from the given start point.\n\nLicense\n-------\n\nCopyright (c) 2011-2022 Jan Sorgalla.\nReleased under the [MIT License](LICENSE).\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"geokit/geokit\",\n    \"description\": \"Geo-Toolkit for PHP\",\n    \"keywords\": [\"geo\", \"geometry\", \"geography\"],\n    \"homepage\": \"https://github.com/jsor/geokit\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Jan Sorgalla\",\n            \"email\": \"jsorgalla@gmail.com\",\n            \"homepage\": \"http://sorgalla.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^7.3 || ^8.0\",\n        \"ext-json\": \"*\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^9.5\",\n        \"vimeo/psalm\": \"^4.9\",\n        \"friendsofphp/php-cs-fixer\": \"^3.1\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Geokit\\\\\": \"src/\"\n        },\n        \"files\": [\"src/functions_include.php\"]\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Geokit\\\\\": \"tests/\"\n        }\n    }\n}\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"vendor/phpunit/phpunit/phpunit.xsd\"\n    colors=\"true\"\n    bootstrap=\"vendor/autoload.php\"\n>\n    <testsuites>\n        <testsuite name=\"Geokit Test Suite\">\n            <directory>./tests/</directory>\n        </testsuite>\n    </testsuites>\n    <coverage>\n        <include>\n            <directory>./src/</directory>\n        </include>\n        <exclude>\n            <file>./src/functions_include.php</file>\n        </exclude>\n    </coverage>\n</phpunit>\n"
  },
  {
    "path": "psalm.xml",
    "content": "<?xml version=\"1.0\"?>\n<psalm\n    xmlns=\"https://getpsalm.org/schema/config\"\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:schemaLocation=\"https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd\"\n    totallyTyped=\"true\"\n>\n    <projectFiles>\n        <directory name=\"src\"/>\n        <directory name=\"tests\"/>\n    </projectFiles>\n\n    <issueHandlers>\n        <PropertyNotSetInConstructor>\n            <errorLevel type=\"suppress\">\n                <directory name=\"tests\"/>\n            </errorLevel>\n        </PropertyNotSetInConstructor>\n    </issueHandlers>\n</psalm>\n"
  },
  {
    "path": "src/BoundingBox.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Geokit\\Exception\\MissingCoordinateException;\nuse JsonSerializable;\nuse function array_key_exists;\nuse function asin;\nuse function cos;\nuse function deg2rad;\nuse function max;\nuse function min;\nuse function rad2deg;\nuse function sin;\n\nfinal class BoundingBox implements JsonSerializable\n{\n    /** @var Position */\n    private $southWest;\n\n    /** @var Position */\n    private $northEast;\n\n    private function __construct(Position $southWest, Position $northEast)\n    {\n        $this->southWest = $southWest;\n        $this->northEast = $northEast;\n\n        if ($this->southWest->latitude() > $this->northEast->latitude()) {\n            throw new Exception\\LogicException(\n                'Bounding Box south-west coordinate cannot be north of the north-east coordinate'\n            );\n        }\n    }\n\n    public static function fromCornerPositions(\n        Position $southWest,\n        Position $northEast\n    ): self {\n        return new self($southWest, $northEast);\n    }\n\n    /**\n     * @param iterable<float> $iterable\n     */\n    public static function fromCoordinates(iterable $iterable): self\n    {\n        $array = [];\n\n        foreach ($iterable as $coordinate) {\n            $array[] = $coordinate;\n\n            if (isset($array[3])) {\n                break;\n            }\n        }\n\n        if (!array_key_exists(0, $array)) {\n            throw MissingCoordinateException::create('west', 0);\n        }\n\n        if (!array_key_exists(1, $array)) {\n            throw MissingCoordinateException::create('south', 1);\n        }\n\n        if (!array_key_exists(2, $array)) {\n            throw MissingCoordinateException::create('east', 0);\n        }\n\n        if (!array_key_exists(3, $array)) {\n            throw MissingCoordinateException::create('north', 1);\n        }\n\n        return new self(\n            Position::fromXY($array[0], $array[1]),\n            Position::fromXY($array[2], $array[3])\n        );\n    }\n\n    /**\n     * @return iterable<float>\n     */\n    public function toCoordinates(): iterable\n    {\n        return [\n            $this->southWest->x(),\n            $this->southWest->y(),\n            $this->northEast->x(),\n            $this->northEast->y(),\n        ];\n    }\n\n    /**\n     * @return array<float>\n     */\n    public function jsonSerialize(): array\n    {\n        return [\n            $this->southWest->x(),\n            $this->southWest->y(),\n            $this->northEast->x(),\n            $this->northEast->y(),\n        ];\n    }\n\n    public function southWest(): Position\n    {\n        return $this->southWest;\n    }\n\n    public function northEast(): Position\n    {\n        return $this->northEast;\n    }\n\n    public function center(): Position\n    {\n        if ($this->crossesAntimeridian()) {\n            $span = $this->lngSpan(\n                $this->southWest->longitude(),\n                $this->northEast->longitude()\n            );\n            $lng = $this->southWest->longitude() + $span / 2;\n        } else {\n            $lng = ($this->southWest->longitude() + $this->northEast->longitude()) / 2;\n        }\n\n        return Position::fromXY(\n            $lng,\n            ($this->southWest->latitude() + $this->northEast->latitude()) / 2\n        );\n    }\n\n    public function span(): Position\n    {\n        return Position::fromXY(\n            $this->lngSpan($this->southWest->longitude(), $this->northEast->longitude()),\n            $this->northEast->latitude() - $this->southWest->latitude()\n        );\n    }\n\n    public function crossesAntimeridian(): bool\n    {\n        return $this->southWest->longitude() > $this->northEast->longitude();\n    }\n\n    public function contains(Position $position): bool\n    {\n        $lat = $position->latitude();\n\n        // check latitude\n        if ($this->southWest->latitude() > $lat ||\n            $lat > $this->northEast->latitude()\n        ) {\n            return false;\n        }\n\n        // check longitude\n        return $this->containsLng($position->longitude());\n    }\n\n    public function extend(Position $position): self\n    {\n        $newSouth = min($this->southWest->latitude(), $position->latitude());\n        $newNorth = max($this->northEast->latitude(), $position->latitude());\n\n        $newWest = $this->southWest->longitude();\n        $newEast = $this->northEast->longitude();\n\n        if (!$this->containsLng($position->longitude())) {\n            // try extending east and try extending west, and use the one that\n            // has the smaller longitudinal span\n            $extendEastLngSpan = $this->lngSpan($newWest, $position->longitude());\n            $extendWestLngSpan = $this->lngSpan($position->longitude(), $newEast);\n\n            if ($extendEastLngSpan <= $extendWestLngSpan) {\n                $newEast = $position->longitude();\n            } else {\n                $newWest = $position->longitude();\n            }\n        }\n\n        return new self(Position::fromXY($newWest, $newSouth), Position::fromXY($newEast, $newNorth));\n    }\n\n    public function union(self $bbox): self\n    {\n        $newBbox = $this->extend($bbox->southWest());\n\n        return $newBbox->extend($bbox->northEast());\n    }\n\n    public function expand(Distance $distance): self\n    {\n        return self::transformBoundingBox($this, $distance->meters());\n    }\n\n    public function shrink(Distance $distance): self\n    {\n        return self::transformBoundingBox($this, -$distance->meters());\n    }\n\n    public function toPolygon(): Polygon\n    {\n        return Polygon::fromPositions(\n            Position::fromXY($this->southWest->x(), $this->southWest->y()),\n            Position::fromXY($this->northEast->x(), $this->southWest->y()),\n            Position::fromXY($this->northEast->x(), $this->northEast->y()),\n            Position::fromXY($this->southWest->x(), $this->northEast->y()),\n            Position::fromXY($this->southWest->x(), $this->southWest->y())\n        );\n    }\n\n    /**\n     * @see http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates\n     */\n    private static function transformBoundingBox(self $bbox, float $distanceInMeters): self\n    {\n        $latSW = deg2rad($bbox->southWest()->latitude());\n        $lngSW = deg2rad($bbox->southWest()->longitude());\n\n        $latNE = deg2rad($bbox->northEast()->latitude());\n        $lngNE = deg2rad($bbox->northEast()->longitude());\n\n        $angularDistance = $distanceInMeters / Earth::RADIUS;\n\n        $minLat = $latSW - $angularDistance;\n        $maxLat = $latNE + $angularDistance;\n\n        $deltaLonSW = asin(sin($angularDistance) / cos($latSW));\n        $deltaLonNE = asin(sin($angularDistance) / cos($latNE));\n\n        $minLon = $lngSW - $deltaLonSW;\n        $maxLon = $lngNE + $deltaLonNE;\n\n        $positionSW = Position::fromXY(rad2deg($minLon), rad2deg($minLat));\n        $positionNE = Position::fromXY(rad2deg($maxLon), rad2deg($maxLat));\n\n        // Check if we're shrinking too much\n        if ($positionSW->latitude() > $positionNE->latitude()) {\n            $center = $bbox->center();\n\n            return self::fromCornerPositions($center, $center);\n        }\n\n        return self::fromCornerPositions($positionSW, $positionNE);\n    }\n\n    private function containsLng(float $lng): bool\n    {\n        if ($this->crossesAntimeridian()) {\n            return $lng <= $this->northEast->longitude() ||\n                $lng >= $this->southWest->longitude();\n        }\n\n        return $this->southWest->longitude() <= $lng &&\n            $lng <= $this->northEast->longitude();\n    }\n\n    private function lngSpan(float $west, float $east): float\n    {\n        return $west > $east ? ($east + 360 - $west) : ($east - $west);\n    }\n}\n"
  },
  {
    "path": "src/Distance.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse function json_encode;\nuse function preg_match;\nuse function sprintf;\n\n/**\n * Inspired by GeoPy's distance class (https://github.com/geopy/geopy).\n */\nfinal class Distance\n{\n    public const UNIT_METERS = 'meters';\n    public const UNIT_KILOMETERS = 'kilometers';\n    public const UNIT_MILES = 'miles';\n    public const UNIT_YARDS = 'yards';\n    public const UNIT_FEET = 'feet';\n    public const UNIT_INCHES = 'inches';\n    public const UNIT_NAUTICAL = 'nautical';\n\n    public const DEFAULT_UNIT = self::UNIT_METERS;\n\n    /** @var array<float> */\n    private static $units = [\n        self::UNIT_METERS => 1.0,\n        self::UNIT_KILOMETERS => 1000.0,\n        self::UNIT_MILES => 1609.344,\n        self::UNIT_YARDS => 0.9144,\n        self::UNIT_FEET => 0.3048,\n        self::UNIT_INCHES => 0.0254,\n        self::UNIT_NAUTICAL => 1852.0,\n    ];\n\n    /** @var array<string> */\n    private static $aliases = [\n        'meter' => self::UNIT_METERS,\n        'metre' => self::UNIT_METERS,\n        'metres' => self::UNIT_METERS,\n        'm' => self::UNIT_METERS,\n        'kilometer' => self::UNIT_KILOMETERS,\n        'kilometre' => self::UNIT_KILOMETERS,\n        'kilometres' => self::UNIT_KILOMETERS,\n        'km' => self::UNIT_KILOMETERS,\n        'mile' => self::UNIT_MILES,\n        'mi' => self::UNIT_MILES,\n        'yard' => self::UNIT_YARDS,\n        'yd' => self::UNIT_YARDS,\n        'foot' => self::UNIT_FEET,\n        'ft' => self::UNIT_FEET,\n        'nm' => self::UNIT_NAUTICAL,\n        'inch' => self::UNIT_INCHES,\n        'in' => self::UNIT_INCHES,\n        '″' => self::UNIT_INCHES,\n        'nauticalmile' => self::UNIT_NAUTICAL,\n        'nauticalmiles' => self::UNIT_NAUTICAL,\n    ];\n\n    /** @var float */\n    private $value;\n\n    public function __construct(float $value, string $unit = self::DEFAULT_UNIT)\n    {\n        if (!isset(self::$units[$unit])) {\n            throw new Exception\\InvalidArgumentException(\n                sprintf(\n                    'Invalid unit %s.',\n                    json_encode($unit)\n                )\n            );\n        }\n\n        if ($value < 0) {\n            throw new Exception\\InvalidArgumentException(\n                sprintf(\n                    'The distance must be a positive number, got \"%s\".',\n                    $value\n                )\n            );\n        }\n\n        $this->value = $value * self::$units[$unit];\n    }\n\n    public static function fromString(string $input): self\n    {\n        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)) {\n            $unit = self::DEFAULT_UNIT;\n\n            if (isset($match[2])) {\n                $unit = $match[2];\n\n                if (!isset(self::$units[$unit])) {\n                    $unit = self::$aliases[$unit];\n                }\n            }\n\n            return new self((float) $match[1], $unit);\n        }\n\n        throw new Exception\\InvalidArgumentException(\n            sprintf(\n                'Cannot create Distance from string %s.',\n                json_encode($input)\n            )\n        );\n    }\n\n    public function meters(): float\n    {\n        return $this->value / self::$units[self::UNIT_METERS];\n    }\n\n    public function m(): float\n    {\n        return $this->meters();\n    }\n\n    public function kilometers(): float\n    {\n        return $this->value / self::$units[self::UNIT_KILOMETERS];\n    }\n\n    public function km(): float\n    {\n        return $this->kilometers();\n    }\n\n    public function miles(): float\n    {\n        return $this->value / self::$units[self::UNIT_MILES];\n    }\n\n    public function mi(): float\n    {\n        return $this->miles();\n    }\n\n    public function yards(): float\n    {\n        return $this->value / self::$units[self::UNIT_YARDS];\n    }\n\n    public function yd(): float\n    {\n        return $this->yards();\n    }\n\n    public function feet(): float\n    {\n        return $this->value / self::$units[self::UNIT_FEET];\n    }\n\n    public function ft(): float\n    {\n        return $this->feet();\n    }\n\n    public function inches(): float\n    {\n        return $this->value / self::$units[self::UNIT_INCHES];\n    }\n\n    public function in(): float\n    {\n        return $this->inches();\n    }\n\n    public function nautical(): float\n    {\n        return $this->value / self::$units[self::UNIT_NAUTICAL];\n    }\n\n    public function nm(): float\n    {\n        return $this->nautical();\n    }\n}\n"
  },
  {
    "path": "src/Earth.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\n/**\n * @see http://en.wikipedia.org/wiki/World_Geodetic_System\n * @see https://en.wikipedia.org/wiki/Earth_radius\n */\nfinal class Earth\n{\n    public const SEMI_MAJOR_AXIS = 6378137.0;\n\n    public const INVERSE_FLATTENING = 298.257223563;\n\n    /**\n     * SEMI_MAJOR_AXIS - SEMI_MAJOR_AXIS / INVERSE_FLATTENING.\n     */\n    public const SEMI_MINOR_AXIS = 6356752.3142;\n\n    /**\n     * 1 / INVERSE_FLATTENING.\n     */\n    public const FLATTENING = 0.0033528106647475;\n\n    /**\n     * Mean earth radius.\n     *\n     * (2 * SEMI_MAJOR_AXIS + SEMI_MINOR_AXIS) / 3\n     *\n     * @see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius\n     */\n    public const RADIUS = 6371008.8;\n}\n"
  },
  {
    "path": "src/Exception/Exception.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\ninterface Exception\n{\n}\n"
  },
  {
    "path": "src/Exception/InvalidArgumentException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nclass InvalidArgumentException extends \\InvalidArgumentException implements Exception\n{\n}\n"
  },
  {
    "path": "src/Exception/LogicException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nclass LogicException extends \\LogicException implements Exception\n{\n}\n"
  },
  {
    "path": "src/Exception/MissingCoordinateException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nuse function sprintf;\n\nfinal class MissingCoordinateException extends InvalidArgumentException\n{\n    public static function create(\n        string $coordinate,\n        int $position\n    ): self {\n        return new self(\n            sprintf(\n                'Missing %s-coordinate at position %d.',\n                $coordinate,\n                $position\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/Exception/RuntimeException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit\\Exception;\n\nclass RuntimeException extends \\RuntimeException implements Exception\n{\n}\n"
  },
  {
    "path": "src/Polygon.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Countable;\nuse Generator;\nuse IteratorAggregate;\nuse JsonSerializable;\nuse function array_shift;\nuse function count;\nuse function end;\nuse function reset;\n\nfinal class Polygon implements Countable, IteratorAggregate, JsonSerializable\n{\n    /** @var array<Position> */\n    private $positions;\n\n    private function __construct(Position ...$positions)\n    {\n        $this->positions = $positions;\n    }\n\n    public static function fromPositions(Position ...$positions): self\n    {\n        return new self(...$positions);\n    }\n\n    /**\n     * @param iterable<iterable<float>> $iterable\n     */\n    public static function fromCoordinates(iterable $iterable): self\n    {\n        $positions = [];\n\n        foreach ($iterable as $position) {\n            $positions[] = Position::fromCoordinates($position);\n        }\n\n        return new self(...$positions);\n    }\n\n    public function close(): self\n    {\n        if (0 === count($this->positions)) {\n            return new self();\n        }\n\n        $positions = $this->positions;\n\n        $lastPosition = end($positions);\n        $firstPosition = reset($positions);\n\n        $isClosed = (\n            $lastPosition->latitude() === $firstPosition->latitude() &&\n            $lastPosition->longitude() === $firstPosition->longitude()\n        );\n\n        if (!$isClosed) {\n            $positions[] = clone reset($this->positions);\n        }\n\n        return new self(...$positions);\n    }\n\n    /**\n     * @see https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html\n     */\n    public function contains(Position $position): bool\n    {\n        if (0 === count($this->positions)) {\n            return false;\n        }\n\n        $positions = $this->positions;\n\n        $x = $position->longitude();\n        $y = $position->latitude();\n\n        $p = end($positions);\n\n        $x0 = $p->longitude();\n        $y0 = $p->latitude();\n\n        $inside = false;\n\n        foreach ($positions as $pos) {\n            $x1 = $pos->longitude();\n            $y1 = $pos->latitude();\n\n            if (($y1 > $y) !== ($y0 > $y) &&\n                ($x < ($x0 - $x1) * ($y - $y1) / ($y0 - $y1) + $x1)\n            ) {\n                $inside = !$inside;\n            }\n\n            $x0 = $x1;\n            $y0 = $y1;\n        }\n\n        return $inside;\n    }\n\n    public function toBoundingBox(): BoundingBox\n    {\n        if (0 === count($this->positions)) {\n            throw new Exception\\LogicException('Cannot create a BoundingBox from empty Polygon.');\n        }\n\n        $positions = $this->positions;\n\n        $start = array_shift($positions);\n\n        $bbox = BoundingBox::fromCornerPositions($start, $start);\n\n        foreach ($positions as $position) {\n            $bbox = $bbox->extend($position);\n        }\n\n        return $bbox;\n    }\n\n    public function count(): int\n    {\n        return count($this->positions);\n    }\n\n    public function getIterator(): Generator\n    {\n        yield from $this->positions;\n    }\n\n    /**\n     * @return iterable<iterable<float>>\n     */\n    public function toCoordinates(): iterable\n    {\n        foreach ($this->positions as $position) {\n            yield $position->toCoordinates();\n        }\n    }\n\n    /**\n     * @return array<array<float>>\n     */\n    public function jsonSerialize(): array\n    {\n        $coordinates = [];\n\n        foreach ($this->positions as $position) {\n            $coordinates[] = $position->jsonSerialize();\n        }\n\n        return $coordinates;\n    }\n}\n"
  },
  {
    "path": "src/Position.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Geokit\\Exception\\MissingCoordinateException;\nuse JsonSerializable;\nuse function array_key_exists;\n\nfinal class Position implements JsonSerializable\n{\n    /** @var float */\n    private $x;\n\n    /** @var float */\n    private $y;\n\n    private function __construct(float $x, float $y)\n    {\n        $this->x = $x;\n        $this->y = $y;\n    }\n\n    public static function fromXY(float $x, float $y): self\n    {\n        return new self($x, $y);\n    }\n\n    /**\n     * @param iterable<float> $iterable\n     */\n    public static function fromCoordinates(iterable $iterable): self\n    {\n        $array = [];\n\n        foreach ($iterable as $coordinate) {\n            $array[] = $coordinate;\n\n            if (isset($array[1])) {\n                break;\n            }\n        }\n\n        if (!array_key_exists(0, $array)) {\n            throw MissingCoordinateException::create('x', 0);\n        }\n\n        if (!array_key_exists(1, $array)) {\n            throw MissingCoordinateException::create('y', 1);\n        }\n\n        return new self($array[0], $array[1]);\n    }\n\n    public function x(): float\n    {\n        return $this->x;\n    }\n\n    public function y(): float\n    {\n        return $this->y;\n    }\n\n    public function longitude(): float\n    {\n        return normalizeLongitude($this->x);\n    }\n\n    public function latitude(): float\n    {\n        return normalizeLatitude($this->y);\n    }\n\n    /**\n     * @return iterable<float>\n     */\n    public function toCoordinates(): iterable\n    {\n        return [$this->x, $this->y];\n    }\n\n    /**\n     * @return array<float>\n     */\n    public function jsonSerialize(): array\n    {\n        return [$this->x, $this->y];\n    }\n}\n"
  },
  {
    "path": "src/functions.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse function abs;\nuse function asin;\nuse function atan;\nuse function atan2;\nuse function cos;\nuse function deg2rad;\nuse function fmod;\nuse function rad2deg;\nuse function sin;\nuse function sqrt;\nuse function tan;\n\n/**\n * Calculates the approximate sea level great circle (Earth) distance\n * between two points using the Haversine formula.\n *\n * @see http://en.wikipedia.org/wiki/Haversine_formula\n * @see http://www.movable-type.co.uk/scripts/latlong.html\n */\nfunction distanceHaversine(Position $from, Position $to): Distance\n{\n    $lat1 = deg2rad($from->latitude());\n    $lng1 = deg2rad($from->longitude());\n    $lat2 = deg2rad($to->latitude());\n    $lng2 = deg2rad($to->longitude());\n\n    $dLat = $lat2 - $lat1;\n    $dLon = $lng2 - $lng1;\n\n    $a = sin($dLat / 2) * sin($dLat / 2) +\n        cos($lat1) * cos($lat2) *\n        sin($dLon / 2) * sin($dLon / 2);\n\n    $c = 2 * atan2(sqrt($a), sqrt(1 - $a));\n\n    return new Distance(Earth::RADIUS * $c);\n}\n\n/**\n * Calculates the geodetic distance between two points using the\n * Vincenty inverse formula for ellipsoids.\n *\n * @see http://en.wikipedia.org/wiki/Vincenty%27s_formulae\n * @see http://www.movable-type.co.uk/scripts/latlong-vincenty.html\n */\nfunction distanceVincenty(Position $from, Position $to): Distance\n{\n    $lat1 = $from->latitude();\n    $lng1 = $from->longitude();\n    $lat2 = $to->latitude();\n    $lng2 = $to->longitude();\n\n    $a = Earth::SEMI_MAJOR_AXIS;\n    $b = Earth::SEMI_MINOR_AXIS;\n    $f = Earth::FLATTENING;\n\n    $L = deg2rad($lng2 - $lng1);\n    $U1 = atan((1 - $f) * tan(deg2rad($lat1)));\n    $U2 = atan((1 - $f) * tan(deg2rad($lat2)));\n\n    $sinU1 = sin($U1);\n    $cosU1 = cos($U1);\n    $sinU2 = sin($U2);\n    $cosU2 = cos($U2);\n\n    $lambda = $L;\n    $iterLimit = 100;\n\n    do {\n        $sinLambda = sin($lambda);\n        $cosLambda = cos($lambda);\n        $sinSigma = sqrt(($cosU2 * $sinLambda) * ($cosU2 * $sinLambda) +\n            ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda) *\n            ($cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda));\n\n        if (0.0 === $sinSigma) {\n            return new Distance(0); // co-incident points\n        }\n\n        $cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda;\n        $sigma = atan2($sinSigma, $cosSigma);\n        $sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma;\n        $cosSqAlpha = (float) 1 - $sinAlpha * $sinAlpha;\n\n        if (0.0 !== $cosSqAlpha) {\n            $cos2SigmaM = $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha;\n        } else {\n            $cos2SigmaM = 0.0; // Equatorial line\n        }\n\n        $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));\n        $lambdaP = $lambda;\n        $lambda = $L + (1 - $C) * $f * $sinAlpha *\n            ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM)));\n    } while (--$iterLimit > 0 && abs($lambda - $lambdaP) > 1e-12);\n\n    if ($iterLimit <= 0) {\n        throw new Exception\\RuntimeException('Vincenty formula failed to converge.');\n    }\n\n    $uSq = $cosSqAlpha * ($a * $a - $b * $b) / ($b * $b);\n    $A = 1 + $uSq / 16384 * (4096 + $uSq * (-768 + $uSq * (320 - 175 * $uSq)));\n    $B = $uSq / 1024 * (256 + $uSq * (-128 + $uSq * (74 - 47 * $uSq)));\n    $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM * $cos2SigmaM) -\n                $B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma * $sinSigma) * (-3 + 4 * $cos2SigmaM * $cos2SigmaM)));\n    $s = $b * $A * ($sigma - $deltaSigma);\n\n    return new Distance($s);\n}\n\n/**\n * Calculates the (initial) heading from the first point to the second point\n * in degrees.\n */\nfunction heading(Position $from, Position $to): float\n{\n    $lat1 = $from->latitude();\n    $lng1 = $from->longitude();\n    $lat2 = $to->latitude();\n    $lng2 = $to->longitude();\n\n    $lat1 = deg2rad($lat1);\n    $lat2 = deg2rad($lat2);\n    $dLon = deg2rad($lng2 - $lng1);\n\n    $y = sin($dLon) * cos($lat2);\n    $x = cos($lat1) * sin($lat2) -\n        sin($lat1) * cos($lat2) * cos($dLon);\n\n    $heading = atan2($y, $x);\n\n    return fmod(rad2deg($heading) + 360, 360);\n}\n\n/**\n * Calculates an intermediate point on the geodesic between the two given\n * points.\n *\n * @see http://www.movable-type.co.uk/scripts/latlong.html\n */\nfunction midpoint(Position $from, Position $to): Position\n{\n    $lat1 = $from->latitude();\n    $lng1 = $from->longitude();\n    $lat2 = $to->latitude();\n    $lng2 = $to->longitude();\n\n    $lat1 = deg2rad($lat1);\n    $lat2 = deg2rad($lat2);\n    $dLon = deg2rad($lng2 - $lng1);\n\n    $Bx = cos($lat2) * cos($dLon);\n    $By = cos($lat2) * sin($dLon);\n\n    $lat3 = atan2(\n        sin($lat1) + sin($lat2),\n        sqrt((cos($lat1) + $Bx) * (cos($lat1) + $Bx) + $By * $By)\n    );\n    $lon3 = deg2rad($lng1) + atan2($By, cos($lat1) + $Bx);\n\n    return Position::fromXY(rad2deg($lon3), rad2deg($lat3));\n}\n\n/**\n * Calculates the destination point along a geodesic, given an initial\n * heading and distance, from the given start point.\n *\n * @see http://www.movable-type.co.uk/scripts/latlong.html\n */\nfunction endpoint(Position $start, float $heading, Distance $distance): Position\n{\n    $lat = deg2rad($start->latitude());\n    $lng = deg2rad($start->longitude());\n\n    $angularDistance = $distance->meters() / Earth::RADIUS;\n    $heading = deg2rad($heading);\n\n    $lat2 = asin(\n        sin($lat) * cos($angularDistance) +\n        cos($lat) * sin($angularDistance) * cos($heading)\n    );\n    $lon2 = $lng + atan2(\n        sin($heading) * sin($angularDistance) * cos($lat),\n        cos($angularDistance) - sin($lat) * sin($lat2)\n    );\n\n    return Position::fromXY(rad2deg($lon2), rad2deg($lat2));\n}\n\nfunction circle(Position $center, Distance $radius, int $steps): Polygon\n{\n    $points = [];\n\n    for ($i = 0; $i <= $steps; ++$i) {\n        $points[] = endpoint(\n            $center,\n            $i * (-360 / $steps),\n            $radius\n        );\n    }\n\n    return Polygon::fromPositions(...$points);\n}\n\n/**\n * Normalize a latitude into the range (-90, 90) (upper and lower bound\n * included).\n */\nfunction normalizeLatitude(float $lat): float\n{\n    $mod = fmod($lat, 360);\n\n    if ($mod < -90) {\n        $mod = -180 - $mod;\n    }\n\n    if ($mod > 90) {\n        $mod = 180 - $mod;\n    }\n\n    return $mod;\n}\n\n/**\n * Normalize a longitude into the range (-180, 180) (lower bound excluded,\n * upper bound included).\n */\nfunction normalizeLongitude(float $lng): float\n{\n    $mod = fmod($lng, 360);\n\n    if ($mod <= -180) {\n        $mod += 360;\n    }\n\n    if ($mod > 180) {\n        $mod -= 360;\n    }\n\n    return $mod;\n}\n"
  },
  {
    "path": "src/functions_include.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nif (!function_exists('Geokit\\distanceHaversine')) {\n    require __DIR__ . '/functions.php';\n}\n"
  },
  {
    "path": "tests/BoundingBoxTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse ArrayIterator;\nuse Generator;\nuse function iterator_to_array;\nuse function json_encode;\n\nclass BoundingBoxTest extends TestCase\n{\n    protected function assertBoundingBox(BoundingBox $b, float $s, float $w, float $n, float $e): void\n    {\n        self::assertEquals($s, $b->southWest()->latitude());\n        self::assertEquals($w, $b->southWest()->longitude());\n        self::assertEquals($n, $b->northEast()->latitude());\n        self::assertEquals($e, $b->northEast()->longitude());\n    }\n\n    public function testConstructorShouldAcceptPositionsAsFirstAndSecondArgument(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1234, 2.5678), Position::fromXY(3.1234, 4.5678));\n\n        self::assertEquals(1.1234, $bbox->southWest()->longitude());\n        self::assertEquals(2.5678, $bbox->southWest()->latitude());\n\n        self::assertEquals(3.1234, $bbox->northEast()->longitude());\n        self::assertEquals(4.5678, $bbox->northEast()->latitude());\n    }\n\n    public function testConstructorShouldThrowExceptionForInvalidSouthCoordinate(): void\n    {\n        $this->expectException(Exception\\LogicException::class);\n        BoundingBox::fromCornerPositions(Position::fromXY(90, 1), Position::fromXY(90, 0));\n    }\n\n    public function testFromCoordinatesWithArray(): void\n    {\n        $bbox = BoundingBox::fromCoordinates([1, 2, 3, 4]);\n\n        self::assertSame(1.0, $bbox->southWest()->longitude());\n        self::assertSame(2.0, $bbox->southWest()->latitude());\n        self::assertSame(1.0, $bbox->southWest()->longitude());\n        self::assertSame(2.0, $bbox->southWest()->latitude());\n\n        self::assertSame(3.0, $bbox->northEast()->longitude());\n        self::assertSame(4.0, $bbox->northEast()->latitude());\n        self::assertSame(3.0, $bbox->northEast()->longitude());\n        self::assertSame(4.0, $bbox->northEast()->latitude());\n    }\n\n    public function testFromCoordinatesWithIterator(): void\n    {\n        $bbox = BoundingBox::fromCoordinates(new ArrayIterator([1, 2, 3, 4]));\n\n        self::assertSame(1.0, $bbox->southWest()->longitude());\n        self::assertSame(2.0, $bbox->southWest()->latitude());\n        self::assertSame(1.0, $bbox->southWest()->longitude());\n        self::assertSame(2.0, $bbox->southWest()->latitude());\n\n        self::assertSame(3.0, $bbox->northEast()->longitude());\n        self::assertSame(4.0, $bbox->northEast()->latitude());\n        self::assertSame(3.0, $bbox->northEast()->longitude());\n        self::assertSame(4.0, $bbox->northEast()->latitude());\n    }\n\n    public function testFromCoordinatesWithGenerator(): void\n    {\n        $bbox = BoundingBox::fromCoordinates((/** @return Generator<float> */ static function (): Generator {\n            yield 1;\n            yield 2;\n            yield 3;\n            yield 4;\n        })());\n\n        self::assertSame(1.0, $bbox->southWest()->longitude());\n        self::assertSame(2.0, $bbox->southWest()->latitude());\n        self::assertSame(1.0, $bbox->southWest()->longitude());\n        self::assertSame(2.0, $bbox->southWest()->latitude());\n\n        self::assertSame(3.0, $bbox->northEast()->longitude());\n        self::assertSame(4.0, $bbox->northEast()->latitude());\n        self::assertSame(3.0, $bbox->northEast()->longitude());\n        self::assertSame(4.0, $bbox->northEast()->latitude());\n    }\n\n    public function testFromCoordinatesThrowsExceptionForMissingSouthWestXCoordinate(): void\n    {\n        $this->expectException(Exception\\MissingCoordinateException::class);\n\n        BoundingBox::fromCoordinates([]);\n    }\n\n    public function testFromCoordinatesThrowsExceptionForMissingSouthWestYCoordinate(): void\n    {\n        $this->expectException(Exception\\MissingCoordinateException::class);\n\n        BoundingBox::fromCoordinates([1]);\n    }\n\n    public function testFromCoordinatesThrowsExceptionForMissingNorthEastXCoordinate(): void\n    {\n        $this->expectException(Exception\\MissingCoordinateException::class);\n\n        BoundingBox::fromCoordinates([1, 2]);\n    }\n\n    public function testFromCoordinatesThrowsExceptionForMissingNorthEastYCoordinate(): void\n    {\n        $this->expectException(Exception\\MissingCoordinateException::class);\n\n        BoundingBox::fromCoordinates([1, 2, 3]);\n    }\n\n    public function testToCoordinates(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(1, 2), Position::fromXY(3, 4));\n\n        self::assertSame([1.0, 2.0, 3.0, 4.0], $bbox->toCoordinates());\n    }\n\n    public function testJsonSerialize(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1, 2), Position::fromXY(3.3, 4));\n\n        self::assertSame('[1.1,2,3.3,4]', json_encode($bbox));\n    }\n\n    public function testGetCenterShouldReturnAPositionObject(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1234, 2.5678), Position::fromXY(3.1234, 4.5678));\n        $center = Position::fromXY(2.1234, 3.5678);\n\n        self::assertEquals($center, $bbox->center());\n\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(179, -45), Position::fromXY(-179, 45));\n        $center = Position::fromXY(180, 0);\n\n        self::assertEquals($center, $bbox->center());\n    }\n\n    public function testGetSpanShouldReturnAPositionObject(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(1.1234, 2.5678), Position::fromXY(3.1234, 4.5678));\n\n        $span = Position::fromXY(2, 2);\n\n        self::assertEquals($span, $bbox->span());\n    }\n\n    public function testContains(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(37, -122), Position::fromXY(38, -123));\n\n        self::assertTrue($bbox->contains(Position::fromXY(37, -122)));\n        self::assertTrue($bbox->contains(Position::fromXY(38, -123)));\n\n        self::assertFalse($bbox->contains(Position::fromXY(-12, -70)));\n    }\n\n    public function testExtendInACircle(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(0, 0), Position::fromXY(0, 0));\n        $bbox = $bbox->extend(Position::fromXY(1, 0));\n        $bbox = $bbox->extend(Position::fromXY(0, 1));\n        $bbox = $bbox->extend(Position::fromXY(-1, 0));\n        $bbox = $bbox->extend(Position::fromXY(0, -1));\n        self::assertBoundingBox($bbox, -1, -1, 1, 1);\n    }\n\n    public function testUnion(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(-122, 37), Position::fromXY(-122, 37));\n\n        $bbox = $bbox->union(BoundingBox::fromCornerPositions(Position::fromXY(123, -38), Position::fromXY(-123, 38)));\n        self::assertBoundingBox($bbox, -38, 123, 38, -122);\n    }\n\n    public function testCrossesAntimeridian(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(179, -45), Position::fromXY(-179, 45));\n\n        self::assertTrue($bbox->crossesAntimeridian());\n        self::assertEquals(90, $bbox->span()->latitude());\n        self::assertEquals(2, $bbox->span()->longitude());\n    }\n\n    public function testCrossesAntimeridianViaExtend(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(Position::fromXY(179, -45), Position::fromXY(-179, 45));\n\n        $bbox = $bbox->extend(Position::fromXY(-180, 90));\n\n        self::assertTrue($bbox->crossesAntimeridian());\n        self::assertEquals(45, $bbox->span()->latitude());\n        self::assertEquals(2, $bbox->span()->longitude());\n    }\n\n    public function testExpand(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(\n            Position::fromXY(179, -45),\n            Position::fromXY(-179, 45)\n        );\n\n        $expandedBbox = $bbox->expand(\n            new Distance(100, Distance::UNIT_KILOMETERS)\n        );\n\n        self::assertEquals(\n            -45.89932036372454,\n            $expandedBbox->southWest()->latitude()\n        );\n        self::assertEquals(\n            177.72811671076983,\n            $expandedBbox->southWest()->longitude()\n        );\n        self::assertEquals(\n            45.89932036372454,\n            $expandedBbox->northEast()->latitude()\n        );\n        self::assertEquals(\n            -177.72811671076983,\n            $expandedBbox->northEast()->longitude()\n        );\n    }\n\n    public function testShrink(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(\n            Position::fromXY(178.99872959034192, -45.000898315284132),\n            Position::fromXY(-178.99872959034192, 45.000898315284132)\n        );\n\n        $shrinkedBbox = $bbox->shrink(\n            new Distance(100, Distance::UNIT_KILOMETERS)\n        );\n\n        self::assertEquals(\n            -44.10157795155959,\n            $shrinkedBbox->southWest()->latitude()\n        );\n        self::assertEquals(\n            -179.7293671753848,\n            $shrinkedBbox->southWest()->longitude()\n        );\n        self::assertEquals(\n            44.10157795155959,\n            $shrinkedBbox->northEast()->latitude()\n        );\n        self::assertEquals(\n            179.7293671753848,\n            $shrinkedBbox->northEast()->longitude()\n        );\n    }\n\n    public function testShrinkTooMuch(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(\n            Position::fromXY(1, 1),\n            Position::fromXY(1, 1)\n        );\n\n        $shrinkedBbox = $bbox->shrink(\n            new Distance(100)\n        );\n\n        self::assertEquals(\n            1,\n            $shrinkedBbox->southWest()->latitude()\n        );\n        self::assertEquals(\n            1,\n            $shrinkedBbox->southWest()->longitude()\n        );\n        self::assertEquals(\n            1,\n            $shrinkedBbox->northEast()->latitude()\n        );\n        self::assertEquals(\n            1,\n            $shrinkedBbox->northEast()->longitude()\n        );\n    }\n\n    public function testToPolygon(): void\n    {\n        $bbox = BoundingBox::fromCornerPositions(\n            Position::fromXY(0, 0),\n            Position::fromXY(10, 10)\n        );\n\n        $polygon = $bbox->toPolygon();\n\n        self::assertCount(5, $polygon);\n\n        /** @var Position[] $array */\n        $array = iterator_to_array($polygon);\n\n        self::assertEquals(\n            0,\n            $array[0]->latitude()\n        );\n        self::assertEquals(\n            0,\n            $array[0]->longitude()\n        );\n\n        self::assertEquals(\n            0,\n            $array[1]->latitude()\n        );\n        self::assertEquals(\n            10,\n            $array[1]->longitude()\n        );\n\n        self::assertEquals(\n            10,\n            $array[2]->latitude()\n        );\n        self::assertEquals(\n            10,\n            $array[2]->longitude()\n        );\n\n        self::assertEquals(\n            10,\n            $array[3]->latitude()\n        );\n        self::assertEquals(\n            0,\n            $array[3]->longitude()\n        );\n    }\n}\n"
  },
  {
    "path": "tests/DistanceTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Generator;\nuse function sprintf;\n\nclass DistanceTest extends TestCase\n{\n    public function testShouldConvertToMeters(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(1000.0, $distance->meters());\n    }\n\n    public function testShouldConvertToMetersWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(1000.0, $distance->m());\n    }\n\n    public function testShouldConvertToKilometers(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(1.0, $distance->kilometers());\n    }\n\n    public function testShouldConvertToKilometersWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(1.0, $distance->km());\n    }\n\n    public function testShouldConvertToMiles(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(0.62137119223733395, $distance->miles());\n    }\n\n    public function testShouldConvertToMilesWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(0.62137119223733395, $distance->mi());\n    }\n\n    public function testShouldConvertToYards(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(1093.6132983377079, $distance->yards());\n    }\n\n    public function testShouldConvertToYardsWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(1093.6132983377079, $distance->yd());\n    }\n\n    public function testShouldConvertToFeet(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(3280.8398950131232, $distance->feet());\n    }\n\n    public function testShouldConvertToFeetWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(3280.8398950131232, $distance->ft());\n    }\n\n    public function testShouldConvertToInches(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(39370.078740157485, $distance->inches());\n    }\n\n    public function testShouldConvertToInchesWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(39370.078740157485, $distance->in());\n    }\n\n    public function testShouldConvertToNauticalMiles(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(0.5399568034557235, $distance->nautical());\n    }\n\n    public function testShouldConvertToNauticalWithAlias(): void\n    {\n        $distance = new Distance(1000);\n        self::assertSame(0.5399568034557235, $distance->nm());\n    }\n\n    public function testShouldThrowExceptionForInvalidUnit(): void\n    {\n        $this->expectException(Exception\\InvalidArgumentException::class);\n        $this->expectExceptionMessage('Invalid unit \"foo\".');\n        new Distance(1000, 'foo');\n    }\n\n    public function testShouldThrowExceptionForNegativeValue(): void\n    {\n        $this->expectException(Exception\\InvalidArgumentException::class);\n        $this->expectExceptionMessage('The distance must be a positive number, got \"-1000\".');\n        new Distance(-1000);\n    }\n\n    /**\n     * @dataProvider fromStringDataProvider\n     */\n    public function testFromString(float $value, string $unit): void\n    {\n        self::assertEquals(1000, Distance::fromString(sprintf('%.15F%s', $value, $unit))->meters());\n        self::assertEquals(1000, Distance::fromString(sprintf('%.15F %s', $value, $unit))->meters(), 'With space');\n    }\n\n    public function fromStringDataProvider(): Generator\n    {\n        yield [\n            1000,\n            '',\n        ];\n\n        yield [\n            1000,\n            'm',\n        ];\n\n        yield [\n            1000,\n            'meter',\n        ];\n\n        yield [\n            1000,\n            'meters',\n        ];\n\n        yield [\n            1000,\n            'metre',\n        ];\n\n        yield [\n            1000,\n            'metres',\n        ];\n\n        yield [\n            1,\n            'km',\n        ];\n\n        yield [\n            1,\n            'kilometer',\n        ];\n\n        yield [\n            1,\n            'kilometers',\n        ];\n\n        yield [\n            1,\n            'kilometre',\n        ];\n\n        yield [\n            1,\n            'kilometres',\n        ];\n\n        yield [\n            0.62137119223733,\n            'mi',\n        ];\n\n        yield [\n            0.62137119223733,\n            'mile',\n        ];\n\n        yield [\n            0.62137119223733,\n            'miles',\n        ];\n\n        yield [\n            1093.6132983377079,\n            'yd',\n        ];\n\n        yield [\n            1093.6132983377079,\n            'yard',\n        ];\n\n        yield [\n            1093.6132983377079,\n            'yards',\n        ];\n\n        yield [\n            3280.83989501312336,\n            'ft',\n        ];\n\n        yield [\n            3280.83989501312336,\n            'foot',\n        ];\n\n        yield [\n            3280.83989501312336,\n            'feet',\n        ];\n\n        yield [\n            39370.078740157485,\n            '″',\n        ];\n\n        yield [\n            39370.0787401574856,\n            'in',\n        ];\n\n        yield [\n            39370.0787401574856,\n            'inch',\n        ];\n\n        yield [\n            39370.078740157485,\n            'inches',\n        ];\n\n        yield [\n            0.53995680345572,\n            'nm',\n        ];\n\n        yield [\n            0.53995680345572,\n            'nautical',\n        ];\n\n        yield [\n            0.53995680345572,\n            'nauticalmile',\n        ];\n\n        yield [\n            0.53995680345572,\n            'nauticalmiles',\n        ];\n    }\n\n    public function testFromStringThrowsExceptionForInvalidUnit(): void\n    {\n        $this->expectException(Exception\\InvalidArgumentException::class);\n        $this->expectExceptionMessage('Cannot create Distance from string \"1000foo\".');\n        Distance::fromString('1000foo');\n    }\n\n    public function testFromStringThrowsExceptionForNegativeValue(): void\n    {\n        $this->expectException(Exception\\InvalidArgumentException::class);\n        $this->expectExceptionMessage('The distance must be a positive number, got \"-1000.5\".');\n        Distance::fromString('-1000.5m');\n    }\n}\n"
  },
  {
    "path": "tests/FunctionsTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse Generator;\n\nclass FunctionsTest extends TestCase\n{\n    /**\n     * @dataProvider distanceHaversineDataProvider\n     */\n    public function testDistanceHaversine(Position $pos1, Position $pos2, float $distance): void\n    {\n        self::assertEqualsWithDelta(\n            $distance,\n            distanceHaversine($pos1, $pos2)->meters(),\n            0.0001\n        );\n    }\n\n    public static function distanceHaversineDataProvider(): Generator\n    {\n        yield [\n            Position::fromXY(60.463472083210945, 44.65105198323727),\n            Position::fromXY(83.73959356918931, -35.21140778437257),\n            9185291.4233,\n        ];\n\n        yield [\n            Position::fromXY(-100.69272816181183, 85.67559066228569),\n            Position::fromXY(-169.56520546227694, 8.659202512353659),\n            8873933.2562,\n        ];\n\n        yield [\n            Position::fromXY(86.67973218485713, -61.20406142435968),\n            Position::fromXY(-112.75070607662201, -46.86954100616276),\n            7871751.1082,\n        ];\n\n        yield [\n            Position::fromXY(114.92620809003711, 19.441748214885592),\n            Position::fromXY(5.652987342327833, 82.39083864726126),\n            8141668.5354,\n        ];\n\n        yield [\n            Position::fromXY(71.53828611597419, -15.120142288506031),\n            Position::fromXY(176.72984121367335, -28.01164012402296),\n            10651065.6175,\n        ];\n\n        yield [\n            Position::fromXY(69.48629681020975, -30.777964973822236),\n            Position::fromXY(-13.63121923059225, 8.096220837906003),\n            9817278.1798,\n        ];\n\n        yield [\n            Position::fromXY(-144.45135304704309, -69.95015325956047),\n            Position::fromXY(-115.67441381514072, 23.054808229207993),\n            10590559.7265,\n        ];\n    }\n\n    /**\n     * @dataProvider distanceVincentyDataProvider\n     */\n    public function testDistanceVincenty(Position $pos1, Position $pos2, float $distance): void\n    {\n        self::assertEqualsWithDelta(\n            $distance,\n            distanceVincenty($pos1, $pos2)->meters(),\n            0.0001\n        );\n    }\n\n    public static function distanceVincentyDataProvider(): Generator\n    {\n        yield [\n            Position::fromXY(60.463472083210945, 44.65105198323727),\n            Position::fromXY(83.73959356918931, -35.21140778437257),\n            9151350.5841,\n        ];\n\n        yield [\n            Position::fromXY(-100.69272816181183, 85.67559066228569),\n            Position::fromXY(-169.56520546227694, 8.659202512353659),\n            8872957.8831,\n        ];\n\n        yield [\n            Position::fromXY(86.67973218485713, -61.20406142435968),\n            Position::fromXY(-112.75070607662201, -46.86954100616276),\n            7896462.2245,\n        ];\n\n        yield [\n            Position::fromXY(114.92620809003711, 19.441748214885592),\n            Position::fromXY(5.652987342327833, 82.39083864726126),\n            8148846.7071,\n        ];\n\n        yield [\n            Position::fromXY(71.53828611597419, -15.120142288506031),\n            Position::fromXY(176.72984121367335, -28.01164012402296),\n            10666157.5230,\n        ];\n\n        yield [\n            Position::fromXY(69.48629681020975, -30.777964973822236),\n            Position::fromXY(-13.63121923059225, 8.096220837906003),\n            9818690.27471,\n        ];\n\n        yield [\n            Position::fromXY(-144.45135304704309, -69.95015325956047),\n            Position::fromXY(-115.67441381514072, 23.054808229207993),\n            10564410.1591,\n        ];\n    }\n\n    public function testDistanceHaversineCoIncidentPositions(): void\n    {\n        self::assertEquals(\n            0,\n            distanceVincenty(Position::fromXY(90, 90), Position::fromXY(90, 90))->meters()\n        );\n    }\n\n    public function testDistanceHaversineShouldNotConvergeForHalfTripAroundEquator(): void\n    {\n        $this->expectException(Exception\\RuntimeException::class);\n        $this->expectExceptionMessage('Vincenty formula failed to converge.');\n\n        distanceVincenty(Position::fromXY(0, 0), Position::fromXY(180, 0));\n    }\n\n    public function testHeading(): void\n    {\n        self::assertEquals(90, heading(Position::fromXY(0, 0), Position::fromXY(1, 0)));\n        self::assertEquals(0, heading(Position::fromXY(0, 0), Position::fromXY(0, 1)));\n        self::assertEquals(270, heading(Position::fromXY(0, 0), Position::fromXY(-1, 0)));\n        self::assertEquals(180, heading(Position::fromXY(0, 0), Position::fromXY(0, -1)));\n    }\n\n    public function testMidpoint(): void\n    {\n        $midpoint = midpoint(\n            Position::fromXY(-96.958444, 32.918593),\n            Position::fromXY(-96.990159, 32.969527)\n        );\n\n        self::assertEquals(\n            32.94406100147102,\n            $midpoint->latitude()\n        );\n        self::assertEquals(\n            -96.974296932499726,\n            $midpoint->longitude()\n        );\n    }\n\n    public function testEndpoint(): void\n    {\n        $endpoint = endpoint(\n            Position::fromXY(-96.958444, 32.918593),\n            332,\n            new Distance(6389.09568)\n        );\n\n        self::assertEquals(\n            32.96932167481445,\n            $endpoint->latitude()\n        );\n        self::assertEquals(\n            -96.99059694331415,\n            $endpoint->longitude()\n        );\n    }\n\n    public function testCircle(): void\n    {\n        $center = Position::fromXY(-75.343, 39.984);\n        $distance = Distance::fromString('50km');\n\n        $circle = circle(\n            $center,\n            $distance,\n            32\n        );\n\n        self::assertCount(33, $circle);\n\n        self::assertTrue($circle->contains($center));\n\n        /** @var Position $point */\n        foreach ($circle as $point) {\n            self::assertEqualsWithDelta(\n                $distance->meters(),\n                distanceHaversine($center, $point)->meters(),\n                0.001\n            );\n        }\n    }\n\n    /**\n     * @dataProvider normalizeLatDataProvider\n     */\n    public function testNormalizeLat(float $a, float $b): void\n    {\n        self::assertEquals($b, normalizeLatitude($a));\n    }\n\n    public function normalizeLatDataProvider(): Generator\n    {\n        yield [-365, -5];\n        yield [-185, 5];\n        yield [-95, -85];\n        yield [-90, -90];\n        yield [5, 5];\n        yield [90, 90];\n        yield [100, 80];\n        yield [185, -5];\n        yield [365, 5];\n    }\n\n    /**\n     * @dataProvider normalizeLngDataProvider\n     */\n    public function testNormalizeLng(float $a, float $b): void\n    {\n        self::assertEquals($b, normalizeLongitude($a));\n    }\n\n    public function normalizeLngDataProvider(): Generator\n    {\n        yield [-545, 175];\n        yield [-365, -5];\n        yield [-360, 0];\n        yield [-185, 175];\n        yield [-180, 180];\n        yield [5, 5];\n        yield [180, 180];\n        yield [215, -145];\n        yield [360, 0];\n        yield [395, 35];\n        yield [540, 180];\n    }\n}\n"
  },
  {
    "path": "tests/PolygonTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse ArrayIterator;\nuse Generator;\nuse function count;\nuse function iterator_to_array;\nuse function json_encode;\n\nclass PolygonTest extends TestCase\n{\n    public function testConstructorAcceptsPositions(): void\n    {\n        $points = [\n            Position::fromXY(0, 0),\n            Position::fromXY(1, 1),\n            Position::fromXY(0, 1),\n        ];\n\n        $polygon = Polygon::fromPositions(...$points);\n\n        /** @var Position[] $array */\n        $array = iterator_to_array($polygon);\n\n        self::assertEquals($points[0], $array[0]);\n        self::assertEquals(0, $array[0]->latitude());\n        self::assertEquals(1, $array[1]->latitude());\n        self::assertEquals(1, $array[2]->latitude());\n    }\n\n    public function testFromCoordinatesWithArray(): void\n    {\n        $polygon = Polygon::fromCoordinates([[1, 2], [2, 3]]);\n\n        self::assertCount(2, $polygon);\n    }\n\n    public function testFromCoordinatesWithIterator(): void\n    {\n        $polygon = Polygon::fromCoordinates(new ArrayIterator([[1, 2], [2, 3]]));\n\n        self::assertCount(2, $polygon);\n    }\n\n    public function testFromCoordinatesWithGenerator(): void\n    {\n        $polygon = Polygon::fromCoordinates((/** @return Generator<array<float>> */ static function (): Generator {\n            yield [1, 2];\n            yield [2, 3];\n        })());\n\n        self::assertCount(2, $polygon);\n    }\n\n    public function testCloseOpenPolygon(): void\n    {\n        $polygon = Polygon::fromPositions(\n            Position::fromXY(0, 0),\n            Position::fromXY(1, 0),\n            Position::fromXY(1, 1),\n            Position::fromXY(1, 0)\n        );\n\n        $closedPolygon = $polygon->close();\n\n        $array = iterator_to_array($closedPolygon);\n\n        self::assertEquals(Position::fromXY(0, 0), $array[count($closedPolygon) - 1]);\n    }\n\n    public function testCloseEmptyPolygon(): void\n    {\n        $polygon = Polygon::fromPositions();\n\n        $closedPolygon = $polygon->close();\n\n        self::assertCount(0, $closedPolygon);\n    }\n\n    public function testCloseAlreadyClosedPolygon(): void\n    {\n        $polygon = Polygon::fromPositions(\n            Position::fromXY(0, 0),\n            Position::fromXY(1, 0),\n            Position::fromXY(1, 1),\n            Position::fromXY(1, 0),\n            Position::fromXY(0, 0)\n        );\n\n        $closedPolygon = $polygon->close();\n\n        $array = iterator_to_array($closedPolygon);\n\n        self::assertEquals(Position::fromXY(0, 0), $array[count($closedPolygon) - 1]);\n    }\n\n    /**\n     * @param array<Position> $polygonPositions\n     *\n     * @dataProvider containsDataProvider\n     */\n    public function testContains(array $polygonPositions, Position $position, bool $expected): void\n    {\n        $polygon = Polygon::fromPositions(...$polygonPositions);\n\n        self::assertEquals($expected, $polygon->contains($position));\n    }\n\n    public function containsDataProvider(): Generator\n    {\n        // Closed counterclockwise polygons\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n                Position::fromXY(0, 0),\n            ],\n            Position::fromXY(0.5, 0.5),\n            true,\n        ];\n\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n                Position::fromXY(0, 0),\n            ],\n            Position::fromXY(1.5, 0.5),\n            false,\n        ];\n\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n                Position::fromXY(0, 0),\n            ],\n            Position::fromXY(-0.5, 0.5),\n            false,\n        ];\n\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n                Position::fromXY(0, 0),\n            ],\n            Position::fromXY(0.5, 1.5),\n            false,\n        ];\n\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n                Position::fromXY(0, 0),\n            ],\n            Position::fromXY(0.5, -0.5),\n            false,\n        ];\n\n        // Closed clockwise polygons\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n                Position::fromXY(0, 0),\n            ],\n            Position::fromXY(0.5, 0.5),\n            true,\n        ];\n\n        yield [\n            [\n                Position::fromXY(1, 1),\n                Position::fromXY(3, 2),\n                Position::fromXY(2, 3),\n                Position::fromXY(1, 1),\n            ],\n            Position::fromXY(1.5, 1.5),\n            true,\n        ];\n\n        // Open counterclockwise polygons\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n            ],\n            Position::fromXY(0.5, 0.5),\n            true,\n        ];\n\n        // Open clockwise polygons\n        yield [\n            [\n                Position::fromXY(0, 0),\n                Position::fromXY(0, 1),\n                Position::fromXY(1, 1),\n                Position::fromXY(1, 0),\n            ],\n            Position::fromXY(0.5, 0.5),\n            true,\n        ];\n\n        yield [\n            [\n                Position::fromXY(1, 1),\n                Position::fromXY(3, 2),\n                Position::fromXY(2, 3),\n            ],\n            Position::fromXY(1.5, 1.5),\n            true,\n        ];\n\n        // Empty polygon\n        yield [\n            [],\n            Position::fromXY(0.5, 0.5),\n            false,\n        ];\n    }\n\n    public function testToBoundingBox(): void\n    {\n        $polygon = Polygon::fromPositions(\n            Position::fromXY(0, 0),\n            Position::fromXY(1, 0),\n            Position::fromXY(1, 1),\n            Position::fromXY(1, 0)\n        );\n\n        $bbox = $polygon->toBoundingBox();\n\n        self::assertEquals(0, $bbox->southWest()->latitude());\n        self::assertEquals(0, $bbox->southWest()->longitude());\n        self::assertEquals(1, $bbox->northEast()->latitude());\n        self::assertEquals(1, $bbox->northEast()->longitude());\n    }\n\n    public function testToBoundingBoxThrowsExceptionForEmptyPolygon(): void\n    {\n        $this->expectException(Exception\\LogicException::class);\n        $this->expectExceptionMessage('Cannot create a BoundingBox from empty Polygon.');\n\n        $polygon = Polygon::fromPositions();\n\n        $polygon->toBoundingBox();\n    }\n\n    public function testCountable(): void\n    {\n        $polygon = Polygon::fromPositions(\n            Position::fromXY(0, 0)\n        );\n\n        self::assertCount(1, $polygon);\n    }\n\n    public function testIterable(): void\n    {\n        self::assertIsIterable(Polygon::fromPositions());\n    }\n\n    public function testToCoordinates(): void\n    {\n        $polygon = Polygon::fromPositions(\n            Position::fromXY(1, 2)\n        );\n\n        $coordinates = $polygon->toCoordinates();\n\n        foreach ($coordinates as $positionCoordinates) {\n            self::assertSame([1.0, 2.0], $positionCoordinates);\n        }\n    }\n\n    public function testJsonSerialize(): void\n    {\n        $polygon = Polygon::fromPositions(\n            Position::fromXY(1.1, 2)\n        );\n\n        self::assertSame('[[1.1,2]]', json_encode($polygon));\n    }\n}\n"
  },
  {
    "path": "tests/PositionTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse ArrayIterator;\nuse Generator;\nuse PHPUnit\\Framework\\TestCase;\nuse function json_encode;\n\nclass PositionTest extends TestCase\n{\n    public function testConstructor(): void\n    {\n        $position = Position::fromXY(1.0, 2.0);\n\n        self::assertSame(1.0, $position->x());\n        self::assertSame(2.0, $position->y());\n        self::assertSame(1.0, $position->longitude());\n        self::assertSame(2.0, $position->latitude());\n    }\n\n    public function testConstructorWithNormalization(): void\n    {\n        $position = Position::fromXY(181, 91);\n\n        self::assertSame(181.0, $position->x());\n        self::assertSame(91.0, $position->y());\n        self::assertSame(-179.0, $position->longitude());\n        self::assertSame(89.0, $position->latitude());\n    }\n\n    public function testConstructorWithInts(): void\n    {\n        $position = Position::fromXY(1, 2);\n\n        self::assertSame(1.0, $position->x());\n        self::assertSame(2.0, $position->y());\n        self::assertSame(1.0, $position->longitude());\n        self::assertSame(2.0, $position->latitude());\n    }\n\n    public function testFromCoordinatesWithArray(): void\n    {\n        $position = Position::fromCoordinates([1, 2]);\n\n        self::assertSame(1.0, $position->x());\n        self::assertSame(2.0, $position->y());\n        self::assertSame(1.0, $position->longitude());\n        self::assertSame(2.0, $position->latitude());\n    }\n\n    public function testFromCoordinatesWithIterator(): void\n    {\n        $position = Position::fromCoordinates(new ArrayIterator([1, 2]));\n\n        self::assertSame(1.0, $position->x());\n        self::assertSame(2.0, $position->y());\n        self::assertSame(1.0, $position->longitude());\n        self::assertSame(2.0, $position->latitude());\n    }\n\n    public function testFromCoordinatesWithGenerator(): void\n    {\n        $position = Position::fromCoordinates((/** @return Generator<float> */ static function (): Generator {\n            yield 1;\n            yield 2;\n        })());\n\n        self::assertSame(1.0, $position->x());\n        self::assertSame(2.0, $position->y());\n        self::assertSame(1.0, $position->longitude());\n        self::assertSame(2.0, $position->latitude());\n    }\n\n    public function testFromCoordinatesThrowsExceptionForMissingXCoordinate(): void\n    {\n        $this->expectException(Exception\\MissingCoordinateException::class);\n\n        Position::fromCoordinates([]);\n    }\n\n    public function testFromCoordinatesThrowsExceptionForMissingYCoordinate(): void\n    {\n        $this->expectException(Exception\\MissingCoordinateException::class);\n\n        Position::fromCoordinates([1]);\n    }\n\n    public function testToCoordinates(): void\n    {\n        $position = Position::fromXY(1, 2);\n\n        self::assertSame([1.0, 2.0], $position->toCoordinates());\n    }\n\n    public function testJsonSerialize(): void\n    {\n        $position = Position::fromXY(1.1, 2);\n\n        self::assertSame('[1.1,2]', json_encode($position));\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Geokit;\n\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{\n}\n"
  }
]