├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── dependabot.yml └── workflows │ ├── pest-coverage.yml │ ├── pest.yml │ ├── phpstan.yml │ ├── pint.yml │ └── update-changelog.yml ├── .gitignore ├── API.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── laravel-spatial.php ├── docker-compose.yml ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── src ├── Database │ └── Connection.php ├── Doctrine │ ├── GeometryCollectionType.php │ ├── LineStringType.php │ ├── MultiLineStringType.php │ ├── MultiPointType.php │ ├── MultiPolygonType.php │ ├── PointType.php │ └── PolygonType.php ├── Eloquent │ ├── GeometryCast.php │ └── HasSpatial.php ├── Enums │ ├── GeometryType.php │ └── Srid.php ├── Exceptions │ ├── LaravelSpatialException.php │ └── LaravelSpatialJsonException.php ├── Geometry │ ├── Geometry.php │ ├── GeometryCollection.php │ ├── GeometryFactory.php │ ├── LineString.php │ ├── MultiLineString.php │ ├── MultiPoint.php │ ├── MultiPolygon.php │ ├── Point.php │ ├── PointCollection.php │ └── Polygon.php └── LaravelSpatialServiceProvider.php └── tests ├── Custom ├── CustomConfigTest.php ├── CustomPoint.php ├── CustomPointConfig.php ├── CustomPointInvalid.php ├── CustomPointTest.php ├── CustomTestPlace.php └── CustomTestPlaceFactory.php ├── Database ├── TestFactories │ └── TestPlaceFactory.php ├── TestModels │ └── TestPlace.php └── migrations │ └── 0000_00_00_000000_create_test_places_table.php ├── Doctrine └── DoctrineTypesTest.php ├── Eloquent ├── GeometryCastTest.php └── HasSpatialTest.php ├── Geometry ├── GeometryCollectionTest.php ├── GeometryTest.php ├── LineStringTest.php ├── MultiLineStringTest.php ├── MultiPointTest.php ├── MultiPolygonTest.php ├── PointTest.php └── PolygonTest.php ├── Pest.php └── TestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [phpstan.neon] 18 | indent_size = 2 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: asanikovich 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report an Issue or Bug with the Package 3 | title: "[Bug]: " 4 | labels: [ "bug" ] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | We're sorry to hear you have a problem. Can you help us solve it by providing the following details. 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: What did you expect to happen? 15 | placeholder: I cannot currently do X thing because when I do, it breaks X thing. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: how-to-reproduce 20 | attributes: 21 | label: How to reproduce the bug 22 | description: How did this occur, please add any config values used and provide a set of reliable steps if possible. 23 | placeholder: When I do X I see Y. 24 | validations: 25 | required: true 26 | - type: input 27 | id: package-version 28 | attributes: 29 | label: Package Version 30 | description: What version of our Package are you running? Please be as specific as possible 31 | placeholder: 2.0.0 32 | validations: 33 | required: true 34 | - type: input 35 | id: php-version 36 | attributes: 37 | label: PHP Version 38 | description: What version of PHP are you running? Please be as specific as possible 39 | placeholder: 8.2.0 40 | validations: 41 | required: true 42 | - type: input 43 | id: laravel-version 44 | attributes: 45 | label: Laravel Version 46 | description: What version of Laravel are you running? Please be as specific as possible 47 | placeholder: 9.0.0 48 | validations: 49 | required: true 50 | - type: dropdown 51 | id: db 52 | attributes: 53 | label: Which database does with happen with? 54 | options: 55 | - MySql 56 | - Mariadb 57 | - type: input 58 | id: db-version 59 | attributes: 60 | label: Database Version 61 | description: What version of Database are you running? Please be as specific as possible 62 | placeholder: '8.0' 63 | validations: 64 | required: true 65 | - type: dropdown 66 | id: operating-systems 67 | attributes: 68 | label: Which operating systems does with happen with? 69 | description: You may select more than one. 70 | multiple: true 71 | options: 72 | - macOS 73 | - Windows 74 | - Linux 75 | - type: textarea 76 | id: notes 77 | attributes: 78 | label: Notes 79 | description: Use this field to provide any other notes that you feel might be relevant to the issue. 80 | validations: 81 | required: false 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/asanikovich/laravel-spatial/discussions/new?category=q-a 5 | about: Ask the community for help 6 | - name: Request a feature 7 | url: https://github.com/asanikovich/laravel-spatial/discussions/new?category=ideas 8 | about: Share ideas for new features 9 | - name: Report a security issue 10 | url: https://github.com/asanikovich/laravel-spatial/security/policy 11 | about: Learn how to notify us for sensitive bugs 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | labels: 12 | - "dependencies" 13 | -------------------------------------------------------------------------------- /.github/workflows/pest-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Tests coverage 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | name: Pest - coverage 8 | 9 | runs-on: ubuntu-latest 10 | 11 | services: 12 | db: 13 | image: mysql:8.0 14 | env: 15 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 16 | MYSQL_DATABASE: laravel 17 | ports: 18 | - 3306 19 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: 8.2 29 | coverage: xdebug 30 | 31 | - name: Install dependencies 32 | run: composer install --prefer-dist --no-interaction 33 | 34 | - name: Execute tests 35 | env: 36 | DB_PORT: ${{ job.services.db.ports['3306'] }} 37 | run: XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=100 --coverage-clover coverage.xml 38 | 39 | - name: Upload coverage reports to Codecov 40 | uses: codecov/codecov-action@v4 41 | env: 42 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/pest.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | name: Pest PHP${{ matrix.php }} Laravel ${{ matrix.laravel }} ${{ matrix.db }} ${{ matrix.dependency-version }} 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php: [ 8.2, 8.1 ] 15 | laravel: [ 10.* ] 16 | db: [ 'mysql:8.0', 'mysql:5.7', 'mariadb:10.9' ] 17 | dependency-version: [ prefer-stable ] 18 | include: 19 | - laravel: 10.* 20 | testbench: ^8.0 21 | 22 | services: 23 | db: 24 | image: ${{ matrix.db }} 25 | env: 26 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 27 | MYSQL_DATABASE: laravel 28 | ports: 29 | - 3306 30 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 31 | 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup PHP 37 | uses: shivammathur/setup-php@v2 38 | with: 39 | php-version: ${{ matrix.php }} 40 | coverage: none 41 | 42 | - name: Install dependencies 43 | run: | 44 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 45 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 46 | 47 | - name: Execute tests 48 | env: 49 | DB_PORT: ${{ job.services.db.ports['3306'] }} 50 | run: vendor/bin/pest 51 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: Static code analysis 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | phpstan: 7 | name: PHPStan 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php: [ 8.2, 8.1 ] 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | coverage: none 25 | 26 | - name: Install dependencies 27 | run: composer install --prefer-dist --no-interaction 28 | 29 | - name: Run PHPStan 30 | run: ./vendor/bin/phpstan analyse --memory-limit=2G --error-format=github 31 | -------------------------------------------------------------------------------- /.github/workflows/pint.yml: -------------------------------------------------------------------------------- 1 | name: Fix PHP code style issues 2 | 3 | on: 4 | push: 5 | paths: 6 | - "**.php" 7 | pull_request: 8 | branches: [master] 9 | 10 | jobs: 11 | php-code-styling: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | ref: ${{ github.head_ref }} 19 | 20 | - name: Fix PHP code style issues 21 | uses: aglipanci/laravel-pint-action@2.3.1 22 | 23 | - name: Commit changes 24 | uses: stefanzweifel/git-auto-commit-action@v5 25 | with: 26 | commit_message: Fix styling 27 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | name: Update Changelog 2 | 3 | on: 4 | release: 5 | types: [ released ] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | ref: master 19 | 20 | - name: Update Changelog 21 | uses: stefanzweifel/changelog-updater-action@v1 22 | with: 23 | latest-version: ${{ github.event.release.name }} 24 | release-notes: ${{ github.event.release.body }} 25 | 26 | - name: Commit updated CHANGELOG 27 | uses: stefanzweifel/git-auto-commit-action@v5 28 | with: 29 | branch: changelog-${{ github.event.release.name }} 30 | create_branch: true 31 | commit_message: Update CHANGELOG 32 | file_pattern: CHANGELOG.md 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.result.cache 3 | build 4 | composer.lock 5 | coverage 6 | vendor 7 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Available geometry classes 4 | 5 | * `Point(float $latitude, float $longitude, int $srid = 0)` - [MySQL Point](https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html) 6 | * `MultiPoint(Point[] | Collection $geometries, int $srid = 0)` - [MySQL MultiPoint](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipoint.html) 7 | * `LineString(Point[] | Collection $geometries, int $srid = 0)` - [MySQL LineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-linestring.html) 8 | * `MultiLineString(LineString[] | Collection $geometries, int $srid = 0)` - [MySQL MultiLineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multilinestring.html) 9 | * `Polygon(LineString[] | Collection $geometries, int $srid = 0)` - [MySQL Polygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html) 10 | * `MultiPolygon(Polygon[] | Collection $geometries, int $srid = 0)` - [MySQL MultiPolygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipolygon.html) 11 | * `GeometryCollection(Geometry[] | Collection $geometries, int $srid = 0)` - [MySQL GeometryCollection](https://dev.mysql.com/doc/refman/8.0/en/gis-class-geometrycollection.html) 12 | 13 | Geometry classes can be also created by these static methods: 14 | 15 | * `fromArray(array $geometry)` - Creates a geometry object from a [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) array. 16 | * `fromJson(string $geoJson, int $srid = 0)` - Creates a geometry object from a [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) string. 17 | * `fromWkt(string $wkt, int $srid = 0)` - Creates a geometry object from a [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry). 18 | * `fromWkb(string $wkb, int $srid = 0)` - Creates a geometry object from a [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary). 19 | 20 | ## Available geometry class methods 21 | 22 | * `toArray()` - Serializes the geometry object into a GeoJSON associative array. 23 | * `toJson()` - Serializes the geometry object into an GeoJSON string. 24 | * `toFeatureCollectionJson()` - Serializes the geometry object into an GeoJSON's FeatureCollection string. 25 | * `toWkt()` - Serializes the geometry object into a WKT. 26 | * `toWkb()` - Serializes the geometry object into a WKB. 27 | * `getCoordinates()` - Returns the coordinates of the geometry object. 28 | * `toSqlExpression(ConnectionInterface $connection)` - Serializes the geometry object into an SQL query. 29 | In addition, `GeometryCollection` also has these functions: 30 | 31 | * `getGeometries()` - Returns a geometry array. Can be used with `ArrayAccess` as well. 32 | 33 | ```php 34 | $geometryCollection = new GeometryCollection([ 35 | new Polygon([ 36 | new LineString([ 37 | new Point(0, 180), 38 | new Point(1, 179), 39 | new Point(2, 178), 40 | new Point(3, 177), 41 | new Point(0, 180), 42 | ]), 43 | ]), 44 | new Point(0, 180), 45 | ]), 46 | ]); 47 | 48 | echo $geometryCollection->getGeometries()[1]->latitude; // 0 49 | // or access as an array: 50 | echo $geometryCollection[1]->latitude; // 0 51 | ``` 52 | 53 | ## Available Enums 54 | 55 | Spatial reference identifiers (SRID) identify the type of coordinate system to use. 56 | 57 | An enum is provided with the following values: 58 | 59 | | Identifier | Value | Description | 60 | |----------------------|--------|-------------------------------------------------------------------------------------| 61 | | `Srid::WGS84` | `4326` | [Geographic coordinate system](https://epsg.org/crs_4326/WGS-84.html) | 62 | | `Srid::WEB_MERCATOR` | `3857` | [Mercator coordinate system](https://epsg.org/crs_3857/WGS-84-Pseudo-Mercator.html) | 63 | 64 | ## Available spatial scopes 65 | 66 | * [withDistance](#withDistance) 67 | * [whereDistance](#whereDistance) 68 | * [orderByDistance](#orderByDistance) 69 | * [withDistanceSphere](#withDistanceSphere) 70 | * [whereDistanceSphere](#whereDistanceSphere) 71 | * [orderByDistanceSphere](#orderByDistanceSphere) 72 | * [whereWithin](#whereWithin) 73 | * [whereNotWithin](#whereNotWithin) 74 | * [whereContains](#whereContains) 75 | * [whereNotContains](#whereNotContains) 76 | * [whereTouches](#whereTouches) 77 | * [whereIntersects](#whereIntersects) 78 | * [whereCrosses](#whereCrosses) 79 | * [whereDisjoint](#whereDisjoint) 80 | * [whereEquals](#whereEquals) 81 | * [whereSrid](#whereSrid) 82 | 83 | ### withDistance 84 | 85 | Retrieves the distance between 2 geometry objects. Uses [ST_Distance](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-distance). 86 | 87 | | parameter name | type | default | 88 | |---------------------|---------------------|--------------| 89 | | `$column` | `Geometry \ string` | | 90 | | `$geometryOrColumn` | `Geometry \ string` | | 91 | | `$alias` | `string` | `'distance'` | 92 | 93 |
Example 94 | 95 | ```php 96 | Place::create(['location' => new Point(0, 0, 4326)]); 97 | 98 | $placeWithDistance = Place::query() 99 | ->withDistance('location', new Point(1, 1, 4326)) 100 | ->first(); 101 | 102 | echo $placeWithDistance->distance; // 156897.79947260793 103 | 104 | // when using alias: 105 | $placeWithDistance = Place::query() 106 | ->withDistance('location', new Point(1, 1, 4326), 'distance_in_meters') 107 | ->first(); 108 | 109 | echo $placeWithDistance->distance_in_meters; // 156897.79947260793 110 | ``` 111 |
112 | 113 | ### whereDistance 114 | 115 | Filters records by distance. Uses [ST_Distance](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-distance). 116 | 117 | | parameter name | type | 118 | |---------------------|---------------------| 119 | | `$column` | `Geometry \ string` | 120 | | `$geometryOrColumn` | `Geometry \ string` | 121 | | `$operator` | `string` | 122 | | `$value` | `int \ float` | 123 | 124 |
Example 125 | 126 | ```php 127 | Place::create(['location' => new Point(0, 0, 4326)]); 128 | Place::create(['location' => new Point(50, 50, 4326)]); 129 | 130 | $placesCountWithinDistance = Place::query() 131 | ->whereDistance('location', new Point(1, 1, 4326), '<', 160000) 132 | ->count(); 133 | 134 | echo $placesCountWithinDistance; // 1 135 | ``` 136 |
137 | 138 | ### orderByDistance 139 | 140 | Orders records by distance. Uses [ST_Distance](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-distance). 141 | 142 | | parameter name | type | default | 143 | |---------------------|---------------------|---------| 144 | | `$column` | `Geometry \ string` | | 145 | | `$geometryOrColumn` | `Geometry \ string` | | 146 | | `$direction` | `string` | `'asc'` | 147 | 148 |
Example 149 | 150 | ```php 151 | Place::create([ 152 | 'name' => 'first', 153 | 'location' => new Point(0, 0, 4326), 154 | ]); 155 | Place::create([ 156 | 'name' => 'second', 157 | 'location' => new Point(50, 50, 4326), 158 | ]); 159 | 160 | $places = Place::query() 161 | ->orderByDistance('location', new Point(1, 1, 4326), 'desc') 162 | ->get(); 163 | 164 | echo $places[0]->name; // second 165 | echo $places[1]->name; // first 166 | ``` 167 |
168 | 169 | ### withDistanceSphere 170 | 171 | Retrieves the spherical distance between 2 geometry objects. Uses [ST_Distance_Sphere](https://dev.mysql.com/doc/refman/8.0/en/spatial-convenience-functions.html#function_st-distance-sphere). 172 | 173 | | parameter name | type | default | 174 | |---------------------|---------------------|--------------| 175 | | `$column` | `Geometry \ string` | | 176 | | `$geometryOrColumn` | `Geometry \ string` | | 177 | | `$alias` | `string` | `'distance'` | 178 | 179 |
Example 180 | 181 | ```php 182 | Place::create(['location' => new Point(0, 0, 4326)]); 183 | 184 | $placeWithDistance = Place::query() 185 | ->withDistanceSphere('location', new Point(1, 1, 4326)) 186 | ->first(); 187 | 188 | echo $placeWithDistance->distance; // 157249.59776850493 189 | 190 | // when using alias: 191 | $placeWithDistance = Place::query() 192 | ->withDistanceSphere('location', new Point(1, 1, 4326), 'distance_in_meters') 193 | ->first(); 194 | 195 | echo $placeWithDistance->distance_in_meters; // 157249.59776850493 196 | ``` 197 |
198 | 199 | ### whereDistanceSphere 200 | 201 | Filters records by spherical distance. Uses [ST_Distance_Sphere](https://dev.mysql.com/doc/refman/8.0/en/spatial-convenience-functions.html#function_st-distance-sphere). 202 | 203 | | parameter name | type | 204 | |---------------------|---------------------| 205 | | `$column` | `Geometry \ string` | 206 | | `$geometryOrColumn` | `Geometry \ string` | 207 | | `$operator` | `string` | 208 | | `$value` | `int \ float` | 209 | 210 |
Example 211 | 212 | ```php 213 | Place::create(['location' => new Point(0, 0, 4326)]); 214 | Place::create(['location' => new Point(50, 50, 4326)]); 215 | 216 | $placesCountWithinDistance = Place::query() 217 | ->whereDistanceSphere('location', new Point(1, 1, 4326), '<', 160000) 218 | ->count(); 219 | 220 | echo $placesCountWithinDistance; // 1 221 | ``` 222 |
223 | 224 | ### orderByDistanceSphere 225 | 226 | Orders records by spherical distance. Uses [ST_Distance_Sphere](https://dev.mysql.com/doc/refman/8.0/en/spatial-convenience-functions.html#function_st-distance-sphere). 227 | 228 | | parameter name | type | default | 229 | |---------------------|---------------------|---------| 230 | | `$column` | `Geometry \ string` | | 231 | | `$geometryOrColumn` | `Geometry \ string` | | 232 | | `$direction` | `string` | `'asc'` | 233 | 234 |
Example 235 | 236 | ```php 237 | Place::create([ 238 | 'name' => 'first', 239 | 'location' => new Point(0, 0, 4326), 240 | ]); 241 | Place::create([ 242 | 'name' => 'second', 243 | 'location' => new Point(100, 100, 4326), 244 | ]); 245 | 246 | $places = Place::query() 247 | ->orderByDistanceSphere('location', new Point(1, 1, 4326), 'desc') 248 | ->get(); 249 | 250 | echo $places[0]->name; // second 251 | echo $places[1]->name; // first 252 | ``` 253 |
254 | 255 | ### whereWithin 256 | 257 | Filters records by the [ST_Within](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-within) function. 258 | 259 | | parameter name | type | 260 | |---------------------|---------------------| 261 | | `$column` | `Geometry \ string` | 262 | | `$geometryOrColumn` | `Geometry \ string` | 263 | 264 |
Example 265 | 266 | ```php 267 | Place::create(['location' => new Point(0, 0, 4326)]); 268 | 269 | Place::query() 270 | ->whereWithin('location', Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')) 271 | ->exists(); // true 272 | ``` 273 |
274 | 275 | ### whereNotWithin 276 | 277 | Filters records by the [ST_Within](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-within) function. 278 | 279 | | parameter name | type | 280 | |---------------------|---------------------| 281 | | `$column` | `Geometry \ string` | 282 | | `$geometryOrColumn` | `Geometry \ string` | 283 | 284 |
Example 285 | 286 | ```php 287 | Place::create(['location' => new Point(0, 0, 4326)]); 288 | 289 | Place::query() 290 | ->whereNotWithin('location', Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')) 291 | ->exists(); // false 292 | ``` 293 |
294 | 295 | ### whereContains 296 | 297 | Filters records by the [ST_Contains](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-contains) function. 298 | 299 | | parameter name | type | 300 | |---------------------|---------------------| 301 | | `$column` | `Geometry \ string` | 302 | | `$geometryOrColumn` | `Geometry \ string` | 303 | 304 |
Example 305 | 306 | ```php 307 | Place::create(['area' => Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'),]); 308 | 309 | Place::query() 310 | ->whereContains('area', new Point(0, 0, 4326)) 311 | ->exists(); // true 312 | ``` 313 |
314 | 315 | ### whereNotContains 316 | 317 | Filters records by the [ST_Contains](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-contains) function. 318 | 319 | | parameter name | type | 320 | |---------------------|---------------------| 321 | | `$column` | `Geometry \ string` | 322 | | `$geometryOrColumn` | `Geometry \ string` | 323 | 324 |
Example 325 | 326 | ```php 327 | Place::create(['area' => Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'),]); 328 | 329 | Place::query() 330 | ->whereNotContains('area', new Point(0, 0, 4326)) 331 | ->exists(); // false 332 | ``` 333 |
334 | 335 | ### whereTouches 336 | 337 | Filters records by the [ST_Touches](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-touches) function. 338 | 339 | | parameter name | type | 340 | |---------------------|---------------------| 341 | | `$column` | `Geometry \ string` | 342 | | `$geometryOrColumn` | `Geometry \ string` | 343 | 344 |
Example 345 | 346 | ```php 347 | Place::create(['location' => new Point(0, 0, 4326)]); 348 | 349 | Place::query() 350 | ->whereTouches('location', Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[0,-1],[0,0],[-1,0],[-1,-1]]]}')) 351 | ->exists(); // true 352 | ``` 353 |
354 | 355 | ### whereIntersects 356 | 357 | Filters records by the [ST_Intersects](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-intersects) function. 358 | 359 | | parameter name | type | 360 | |---------------------|---------------------| 361 | | `$column` | `Geometry \ string` | 362 | | `$geometryOrColumn` | `Geometry \ string` | 363 | 364 |
Example 365 | 366 | ```php 367 | Place::create(['location' => new Point(0, 0, 4326)]); 368 | 369 | Place::query() 370 | ->whereIntersects('location', Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')) 371 | ->exists(); // true 372 | ``` 373 |
374 | 375 | ### whereCrosses 376 | 377 | Filters records by the [ST_Crosses](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-crosses) function. 378 | 379 | | parameter name | type | 380 | |---------------------|---------------------| 381 | | `$column` | `Geometry \ string` | 382 | | `$geometryOrColumn` | `Geometry \ string` | 383 | 384 |
Example 385 | 386 | ```php 387 | Place::create(['line_string' => LineString::fromJson('{"type":"LineString","coordinates":[[0,0],[2,0]]}')]); 388 | 389 | Place::query() 390 | ->whereCrosses('line_string', Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}')) 391 | ->exists(); // true 392 | ``` 393 |
394 | 395 | ### whereDisjoint 396 | 397 | Filters records by the [ST_Disjoint](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-disjoint) function. 398 | 399 | | parameter name | type | 400 | |---------------------|---------------------| 401 | | `$column` | `Geometry \ string` | 402 | | `$geometryOrColumn` | `Geometry \ string` | 403 | 404 |
Example 405 | 406 | ```php 407 | Place::create(['location' => new Point(0, 0, 4326)]); 408 | 409 | Place::query() 410 | ->whereDisjoint('location', Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[-0.5,-1],[-0.5,-0.5],[-1,-0.5],[-1,-1]]]}')) 411 | ->exists(); // true 412 | ``` 413 |
414 | 415 | ### whereEquals 416 | 417 | Filters records by the [ST_Equal](https://dev.mysql.com/doc/refman/8.0/en/spatial-relation-functions-object-shapes.html#function_st-equals) function. 418 | 419 | | parameter name | type | 420 | |---------------------|---------------------| 421 | | `$column` | `Geometry \ string` | 422 | | `$geometryOrColumn` | `Geometry \ string` | 423 | 424 |
Example 425 | 426 | ```php 427 | Place::create(['location' => new Point(0, 0, 4326)]); 428 | 429 | Place::query() 430 | ->whereEquals('location', new Point(0, 0, 4326)) 431 | ->exists(); // true 432 | ``` 433 |
434 | 435 | ### whereSrid 436 | 437 | Filters records by the [ST_Srid](https://dev.mysql.com/doc/refman/8.0/en/gis-general-property-functions.html#function_st-srid) function. 438 | 439 | | parameter name | type | 440 | |----------------|---------------------| 441 | | `$column` | `Geometry \ string` | 442 | | `$operator` | `string` | 443 | | `$value` | `int` | 444 | 445 |
Example 446 | 447 | ```php 448 | Place::create(['location' => new Point(0, 0, 4326)]); 449 | 450 | Place::query() 451 | ->whereSrid('location', '=', 4326) 452 | ->exists(); // true 453 | ``` 454 |
455 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-spatial` will be documented in this file. 4 | 5 | ## v2.0.0 - 2023-06-02 6 | 7 | Initial release for laravel 10 8 | 9 | ## v1.0.0 - 2023-06-02 10 | 11 | Initial release for laravel 8,9 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Aliaksei Sanikovich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Spatial 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/asanikovich/laravel-spatial.svg?style=flat-square)](https://packagist.org/packages/asanikovich/laravel-spatial) 4 | [![GitHub Tests Status](https://img.shields.io/github/actions/workflow/status/asanikovich/laravel-spatial/pest.yml?branch=master&label=tests&style=flat-square)](https://github.com/asanikovich/laravel-spatial/actions/workflows/pest.yml?query=branch%3Amaster) 5 | [![GitHub Tests Coverage Status](https://img.shields.io/codecov/c/github/asanikovich/laravel-spatial?token=E0703O0PPT&style=flat-square)](https://github.com/asanikovich/laravel-spatial/actions/workflows/pest-coverage.yml?query=branch%3Amaster) 6 | [![GitHub Code Style Status](https://img.shields.io/github/actions/workflow/status/asanikovich/laravel-spatial/phpstan.yml?branch=master&label=code%20style&style=flat-square)](https://github.com/asanikovich/laravel-spatial/actions/workflows/phpstan.yml?query=branch%3Amaster) 7 | [![GitHub Lint Status](https://img.shields.io/github/actions/workflow/status/asanikovich/laravel-spatial/pint.yml?branch=master&label=lint&style=flat-square)](https://github.com/asanikovich/laravel-spatial/actions/workflows/pint.yml?query=branch%3Amaster) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/asanikovich/laravel-spatial.svg?style=flat-square)](https://packagist.org/packages/asanikovich/laravel-spatial) 9 | [![Licence](https://img.shields.io/packagist/l/asanikovich/laravel-spatial.svg?style=flat-square)](https://packagist.org/packages/asanikovich/laravel-spatial) 10 | 11 | **This Laravel package allows you to easily work with spatial data types and functions.** 12 | 13 | * v2 supports Laravel 10+ and PHP 8.1+ 14 | * v1 supports Laravel 8,9 and PHP 8.1+ 15 | 16 | This package supports MySQL v8 or v5.7, and MariaDB v10. 17 | 18 | ## Getting Started 19 | 20 | ### Installing the Package 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require asanikovich/laravel-spatial 26 | ``` 27 | 28 | ### Configuration 29 | 30 | Default Configuration file includes geometry types mapping: 31 | ```php 32 | value => Geometry\Point::class, 39 | GeometryType::POLYGON->value => Geometry\Polygon::class, 40 | /// ... 41 | ]; 42 | ``` 43 | 44 | You can publish the config file with: 45 | 46 | ```bash 47 | php artisan vendor:publish --tag="laravel-spatial-config" 48 | ``` 49 | 50 | If you want you can override custom geometry types mapping: 51 | * globally by config file 52 | * by custom `$casts` in your model (top priority) 53 | 54 | ### Setting Up Your First Model 55 | 56 | 1. First, generate a new model along with a migration file by running: 57 | 58 | ```bash 59 | php artisan make:model {modelName} --migration 60 | ``` 61 | 62 | 2. Next, add some spatial columns to the migration file. For instance, to create a "places" table: 63 | 64 | ```php 65 | use Illuminate\Database\Migrations\Migration; 66 | use Illuminate\Database\Schema\Blueprint; 67 | 68 | class CreatePlacesTable extends Migration 69 | { 70 | public function up(): void 71 | { 72 | Schema::create('places', static function (Blueprint $table) { 73 | $table->id(); 74 | $table->string('name')->unique(); 75 | $table->point('location')->nullable(); 76 | $table->polygon('area')->nullable(); 77 | $table->timestamps(); 78 | }); 79 | } 80 | 81 | public function down(): void 82 | { 83 | Schema::dropIfExists('places'); 84 | } 85 | } 86 | ``` 87 | 88 | 3. Run the migration: 89 | 90 | ```bash 91 | php artisan migrate 92 | ``` 93 | 94 | 4. In your new model, fill `$casts` arrays and use the `HasSpatial` trait (fill the `$fillable` - optional): 95 | 96 | ```php 97 | namespace App\Models; 98 | 99 | use Illuminate\Database\Eloquent\Model; 100 | use ASanikovich\LaravelSpatial\Eloquent\HasSpatial; 101 | use ASanikovich\LaravelSpatial\Geometry\Point; 102 | use ASanikovich\LaravelSpatial\Geometry\Polygon; 103 | 104 | /** 105 | * @property Point $location 106 | * @property Polygon $area 107 | */ 108 | class Place extends Model 109 | { 110 | use HasSpatial; 111 | 112 | protected $fillable = [ 113 | 'name', 114 | 'location', 115 | 'area', 116 | ]; 117 | 118 | protected $casts = [ 119 | 'location' => Point::class, 120 | 'area' => Polygon::class, 121 | ]; 122 | } 123 | ``` 124 | 125 | ### Interacting with Spatial Data 126 | 127 | After setting up your model, you can now create and access spatial data. Here's an example: 128 | 129 | ```php 130 | use App\Models\Place; 131 | use ASanikovich\LaravelSpatial\Geometry\Polygon; 132 | use ASanikovich\LaravelSpatial\Geometry\LineString; 133 | use ASanikovich\LaravelSpatial\Geometry\Point; 134 | use ASanikovich\LaravelSpatial\Enums\Srid; 135 | 136 | // Create new records 137 | 138 | $londonEye = Place::create([ 139 | 'name' => 'London Eye', 140 | 'location' => new Point(51.5032973, -0.1217424), 141 | ]); 142 | 143 | $whiteHouse = Place::create([ 144 | 'name' => 'White House', 145 | 'location' => new Point(38.8976763, -77.0365298, Srid::WGS84->value), // with SRID 146 | ]); 147 | 148 | $vaticanCity = Place::create([ 149 | 'name' => 'Vatican City', 150 | 'area' => new Polygon([ 151 | new LineString([ 152 | new Point(12.455363273620605, 41.90746728266806), 153 | new Point(12.450309991836548, 41.906636872349075), 154 | new Point(12.445632219314575, 41.90197359839437), 155 | new Point(12.447413206100464, 41.90027269624499), 156 | new Point(12.457906007766724, 41.90000118654431), 157 | new Point(12.458517551422117, 41.90281205461268), 158 | new Point(12.457584142684937, 41.903107507989986), 159 | new Point(12.457734346389769, 41.905918239316286), 160 | new Point(12.45572805404663, 41.90637337450963), 161 | new Point(12.455363273620605, 41.90746728266806), 162 | ]), 163 | ]), 164 | ]) 165 | 166 | // Access the data 167 | 168 | echo $londonEye->location->latitude; // 51.5032973 169 | echo $londonEye->location->longitude; // -0.1217424 170 | 171 | echo $whiteHouse->location->srid; // 4326 172 | 173 | echo $vacationCity->area->toJson(); // {"type":"Polygon","coordinates":[[[41.90746728266806,12.455363273620605],[41.906636872349075,12.450309991836548],[41.90197359839437,12.445632219314575],[41.90027269624499,12.447413206100464],[41.90000118654431,12.457906007766724],[41.90281205461268,12.458517551422117],[41.903107507989986,12.457584142684937],[41.905918239316286,12.457734346389769],[41.90637337450963,12.45572805404663],[41.90746728266806,12.455363273620605]]]} 174 | ``` 175 | 176 | ## Further Reading 177 | 178 | For more comprehensive documentation on the API, please refer to the [API](API.md) page. 179 | 180 | Create queries only with scopes methods: 181 | ```php 182 | Place::whereDistance(...); // This is IDE-friendly 183 | ``` 184 | 185 | ## Extension 186 | 187 | You can add new methods to the `Geometry` class through macros. 188 | 189 | Here's an example of how to register a macro in your service provider's `boot` method: 190 | 191 | ```php 192 | class AppServiceProvider extends ServiceProvider 193 | { 194 | public function boot(): void 195 | { 196 | Geometry::macro('getName', function (): string { 197 | /** @var Geometry $this */ 198 | return class_basename($this); 199 | }); 200 | } 201 | } 202 | ``` 203 | 204 | Use the method in your code: 205 | 206 | ```php 207 | $londonEyePoint = new Point(51.5032973, -0.1217424); 208 | 209 | echo $londonEyePoint->getName(); // Point 210 | ``` 211 | 212 | ## Development 213 | Here are some useful commands for development 214 | 215 | Before running tests run db by docker-compose: 216 | ```bash 217 | docker-compose up -d 218 | ``` 219 | Run tests: 220 | ```bash 221 | composer run test 222 | ``` 223 | Run tests with coverage: 224 | ```bash 225 | composer run test-coverage 226 | ``` 227 | Perform type checking: 228 | ```bash 229 | composer run phpstan 230 | ``` 231 | Format your code: 232 | ```bash 233 | composer run format 234 | ``` 235 | 236 | ## Updates and Changes 237 | 238 | For details on updates and changes, please refer to our [CHANGELOG](CHANGELOG.md). 239 | 240 | ## License 241 | 242 | Laravel Spatial is released under The MIT License (MIT). For more information, please see our [License File](LICENSE.md). 243 | 244 | ## Credits 245 | 246 | Originally inspired from [MatanYadaev's laravel-eloquent-spatial package](https://github.com/MatanYadaev/laravel-eloquent-spatial). 247 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asanikovich/laravel-spatial", 3 | "description": "Laravel Eloquent spatial package", 4 | "homepage": "https://github.com/asanikovich/laravel-spatial", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Aliaksei Sanikovich", 9 | "email": "asanikovich@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.1", 14 | "ext-json": "*", 15 | "ext-pdo": "*", 16 | "laravel/framework": "^10.0", 17 | "phayes/geophp": "^1.2" 18 | }, 19 | "require-dev": { 20 | "doctrine/dbal": "^3.0", 21 | "laravel/pint": "^1.5", 22 | "nunomaduro/larastan": "^1.0|^2.4", 23 | "orchestra/testbench": "^8.0", 24 | "pestphp/pest": "^1.0|^2.6", 25 | "pestphp/pest-plugin-laravel": "^1.0|^2.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "ASanikovich\\LaravelSpatial\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "ASanikovich\\LaravelSpatial\\Tests\\": "tests" 35 | } 36 | }, 37 | "scripts": { 38 | "phpstan": "vendor/bin/phpstan analyse --memory-limit=2G", 39 | "test": "vendor/bin/pest", 40 | "test-coverage": "XDEBUG_MODE=coverage vendor/bin/pest --coverage --min=100", 41 | "format": "vendor/bin/pint" 42 | }, 43 | "config": { 44 | "sort-packages": true, 45 | "allow-plugins": { 46 | "pestphp/pest-plugin": true 47 | } 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true, 51 | "extra": { 52 | "laravel": { 53 | "providers": [ 54 | "ASanikovich\\LaravelSpatial\\LaravelSpatialServiceProvider" 55 | ] 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/laravel-spatial.php: -------------------------------------------------------------------------------- 1 | value => Geometry\GeometryCollection::class, 8 | GeometryType::LINESTRING->value => Geometry\LineString::class, 9 | GeometryType::MULTILINESTRING->value => Geometry\MultiLineString::class, 10 | GeometryType::MULTIPOINT->value => Geometry\MultiPoint::class, 11 | GeometryType::MULTIPOLYGON->value => Geometry\MultiPolygon::class, 12 | GeometryType::POINT->value => Geometry\Point::class, 13 | GeometryType::POLYGON->value => Geometry\Polygon::class, 14 | ]; 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | mysql8: 4 | image: mysql:8.0 5 | ports: 6 | - "3306:3306" 7 | environment: 8 | MYSQL_DATABASE: 'laravel' 9 | MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' 10 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | parameters: 4 | paths: 5 | - src 6 | - tests 7 | excludePaths: 8 | - src/Geometry/GeometryFactory.php 9 | level: max 10 | checkMissingIterableValueType: true 11 | checkGenericClassInNonGenericObjectType: false 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ./src 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "use_arrow_functions": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Database/Connection.php: -------------------------------------------------------------------------------- 1 | isMariaDb($connection)) { 20 | return false; 21 | } 22 | 23 | if ($this->isMySql57($connection)) { 24 | return false; 25 | } 26 | 27 | return true; 28 | } 29 | 30 | private function isMariaDb(MySqlConnection $connection): bool 31 | { 32 | return $connection->isMaria(); 33 | } 34 | 35 | private function isMySql57(MySqlConnection $connection): bool 36 | { 37 | /** @var string $version */ 38 | $version = $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); 39 | 40 | return version_compare($version, '5.8.0', '<'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Doctrine/GeometryCollectionType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::GEOMETRY_COLLECTION->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Doctrine/LineStringType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::LINESTRING->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Doctrine/MultiLineStringType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::MULTILINESTRING->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Doctrine/MultiPointType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::MULTIPOINT->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Doctrine/MultiPolygonType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::MULTIPOLYGON->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Doctrine/PointType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::POINT->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Doctrine/PolygonType.php: -------------------------------------------------------------------------------- 1 | value; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return GeometryType::POLYGON->value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Eloquent/GeometryCast.php: -------------------------------------------------------------------------------- 1 | */ 17 | private string $className; 18 | 19 | /** 20 | * @param class-string $className 21 | */ 22 | public function __construct(string $className) 23 | { 24 | $this->className = $className; 25 | } 26 | 27 | /** 28 | * @param string|Expression|null $value 29 | * @param array $attributes 30 | */ 31 | public function get(Model $model, string $key, mixed $value, array $attributes): ?Geometry 32 | { 33 | if (! $value) { 34 | return null; 35 | } 36 | 37 | if ($value instanceof Expression) { 38 | $wkt = $this->extractWktFromExpression($value, $model->getConnection()); 39 | $srid = $this->extractSridFromExpression($value, $model->getConnection()); 40 | 41 | return $this->className::fromWkt($wkt, $srid); 42 | } 43 | 44 | return $this->className::fromWkb($value); 45 | } 46 | 47 | /** 48 | * @param Geometry|mixed|null $value 49 | * @param array $attributes 50 | * 51 | * @throws LaravelSpatialException 52 | */ 53 | public function set(Model $model, string $key, mixed $value, array $attributes): Expression|null 54 | { 55 | if (! $value) { 56 | return null; 57 | } 58 | 59 | if (is_array($value)) { 60 | $value = Geometry::fromArray($value); 61 | } 62 | 63 | if ($value instanceof Expression) { 64 | return $value; 65 | } 66 | 67 | if (! ($value instanceof $this->className)) { 68 | $geometryType = is_object($value) ? $value::class : gettype($value); 69 | 70 | throw new LaravelSpatialException(sprintf('Expected %s, %s given.', static::class, $geometryType)); 71 | } 72 | 73 | return $value->toSqlExpression($model->getConnection()); 74 | } 75 | 76 | private function extractWktFromExpression(Expression $expression, Connection $connection): string 77 | { 78 | $expressionValue = $expression->getValue($connection->getQueryGrammar()); 79 | 80 | preg_match('/ST_GeomFromText\(\'(.+)\', .+(, .+)?\)/', (string) $expressionValue, $match); 81 | 82 | return $match[1]; 83 | } 84 | 85 | private function extractSridFromExpression(Expression $expression, Connection $connection): int 86 | { 87 | $expressionValue = $expression->getValue($connection->getQueryGrammar()); 88 | 89 | preg_match('/ST_GeomFromText\(\'.+\', (.+)(, .+)?\)/', (string) $expressionValue, $match); 90 | 91 | return (int) $match[1]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Eloquent/HasSpatial.php: -------------------------------------------------------------------------------- 1 | getQuery()->columns)) { 37 | $query->select(); 38 | } 39 | 40 | return $query->selectRaw(sprintf( 41 | 'ST_DISTANCE(%s, %s) AS %s', 42 | $this->toSpatialExpressionString($query, $column), 43 | $this->toSpatialExpressionString($query, $geometryOrColumn), 44 | $alias, 45 | )); 46 | } 47 | 48 | public function scopeWithDistanceSphere( 49 | Builder $query, 50 | Expression|Geometry|string $column, 51 | Expression|Geometry|string $geometryOrColumn, 52 | string $alias = 'distance' 53 | ): Builder { 54 | if (empty($query->getQuery()->columns)) { 55 | $query->select(); 56 | } 57 | 58 | return $query->selectRaw(sprintf( 59 | 'ST_DISTANCE_SPHERE(%s, %s) AS %s', 60 | $this->toSpatialExpressionString($query, $column), 61 | $this->toSpatialExpressionString($query, $geometryOrColumn), 62 | $alias, 63 | )); 64 | } 65 | 66 | public function scopeWhereDistance( 67 | Builder $query, 68 | Expression|Geometry|string $column, 69 | Expression|Geometry|string $geometryOrColumn, 70 | string $operator, 71 | int|float $value 72 | ): Builder { 73 | $query->whereRaw( 74 | sprintf( 75 | 'ST_DISTANCE(%s, %s) %s ?', 76 | $this->toSpatialExpressionString($query, $column), 77 | $this->toSpatialExpressionString($query, $geometryOrColumn), 78 | $operator, 79 | ), 80 | [$value], 81 | ); 82 | 83 | return $query; 84 | } 85 | 86 | public function scopeOrderByDistance( 87 | Builder $query, 88 | Expression|Geometry|string $column, 89 | Expression|Geometry|string $geometryOrColumn, 90 | string $direction = 'asc' 91 | ): Builder { 92 | $query->orderByRaw( 93 | sprintf( 94 | 'ST_DISTANCE(%s, %s) %s', 95 | $this->toSpatialExpressionString($query, $column), 96 | $this->toSpatialExpressionString($query, $geometryOrColumn), 97 | $direction, 98 | ) 99 | ); 100 | 101 | return $query; 102 | } 103 | 104 | public function scopeWhereDistanceSphere( 105 | Builder $query, 106 | Expression|Geometry|string $column, 107 | Expression|Geometry|string $geometryOrColumn, 108 | string $operator, 109 | int|float $value 110 | ): Builder { 111 | $query->whereRaw( 112 | sprintf( 113 | 'ST_DISTANCE_SPHERE(%s, %s) %s ?', 114 | $this->toSpatialExpressionString($query, $column), 115 | $this->toSpatialExpressionString($query, $geometryOrColumn), 116 | $operator, 117 | ), 118 | [$value], 119 | ); 120 | 121 | return $query; 122 | } 123 | 124 | public function scopeOrderByDistanceSphere( 125 | Builder $query, 126 | Expression|Geometry|string $column, 127 | Expression|Geometry|string $geometryOrColumn, 128 | string $direction = 'asc' 129 | ): Builder { 130 | $query->orderByRaw( 131 | sprintf( 132 | 'ST_DISTANCE_SPHERE(%s, %s) %s', 133 | $this->toSpatialExpressionString($query, $column), 134 | $this->toSpatialExpressionString($query, $geometryOrColumn), 135 | $direction 136 | ) 137 | ); 138 | 139 | return $query; 140 | } 141 | 142 | public function scopeWhereWithin( 143 | Builder $query, 144 | Expression|Geometry|string $column, 145 | Expression|Geometry|string $geometryOrColumn, 146 | ): Builder { 147 | $query->whereRaw( 148 | sprintf( 149 | 'ST_WITHIN(%s, %s)', 150 | $this->toSpatialExpressionString($query, $column), 151 | $this->toSpatialExpressionString($query, $geometryOrColumn), 152 | ) 153 | ); 154 | 155 | return $query; 156 | } 157 | 158 | public function scopeWhereNotWithin( 159 | Builder $query, 160 | Expression|Geometry|string $column, 161 | Expression|Geometry|string $geometryOrColumn, 162 | ): Builder { 163 | $query->whereRaw( 164 | sprintf( 165 | 'ST_WITHIN(%s, %s) = 0', 166 | $this->toSpatialExpressionString($query, $column), 167 | $this->toSpatialExpressionString($query, $geometryOrColumn), 168 | ) 169 | ); 170 | 171 | return $query; 172 | } 173 | 174 | public function scopeWhereContains( 175 | Builder $query, 176 | Expression|Geometry|string $column, 177 | Expression|Geometry|string $geometryOrColumn, 178 | ): Builder { 179 | $query->whereRaw( 180 | sprintf( 181 | 'ST_CONTAINS(%s, %s)', 182 | $this->toSpatialExpressionString($query, $column), 183 | $this->toSpatialExpressionString($query, $geometryOrColumn), 184 | ) 185 | ); 186 | 187 | return $query; 188 | } 189 | 190 | public function scopeWhereNotContains( 191 | Builder $query, 192 | Expression|Geometry|string $column, 193 | Expression|Geometry|string $geometryOrColumn, 194 | ): Builder { 195 | $query->whereRaw( 196 | sprintf( 197 | 'ST_CONTAINS(%s, %s) = 0', 198 | $this->toSpatialExpressionString($query, $column), 199 | $this->toSpatialExpressionString($query, $geometryOrColumn), 200 | ) 201 | ); 202 | 203 | return $query; 204 | } 205 | 206 | public function scopeWhereTouches( 207 | Builder $query, 208 | Expression|Geometry|string $column, 209 | Expression|Geometry|string $geometryOrColumn, 210 | ): Builder { 211 | $query->whereRaw( 212 | sprintf( 213 | 'ST_TOUCHES(%s, %s)', 214 | $this->toSpatialExpressionString($query, $column), 215 | $this->toSpatialExpressionString($query, $geometryOrColumn), 216 | ) 217 | ); 218 | 219 | return $query; 220 | } 221 | 222 | public function scopeWhereIntersects( 223 | Builder $query, 224 | Expression|Geometry|string $column, 225 | Expression|Geometry|string $geometryOrColumn, 226 | ): Builder { 227 | $query->whereRaw( 228 | sprintf( 229 | 'ST_INTERSECTS(%s, %s)', 230 | $this->toSpatialExpressionString($query, $column), 231 | $this->toSpatialExpressionString($query, $geometryOrColumn), 232 | ) 233 | ); 234 | 235 | return $query; 236 | } 237 | 238 | public function scopeWhereCrosses( 239 | Builder $query, 240 | Expression|Geometry|string $column, 241 | Expression|Geometry|string $geometryOrColumn, 242 | ): Builder { 243 | $query->whereRaw( 244 | sprintf( 245 | 'ST_CROSSES(%s, %s)', 246 | $this->toSpatialExpressionString($query, $column), 247 | $this->toSpatialExpressionString($query, $geometryOrColumn), 248 | ) 249 | ); 250 | 251 | return $query; 252 | } 253 | 254 | public function scopeWhereDisjoint( 255 | Builder $query, 256 | Expression|Geometry|string $column, 257 | Expression|Geometry|string $geometryOrColumn, 258 | ): Builder { 259 | $query->whereRaw( 260 | sprintf( 261 | 'ST_DISJOINT(%s, %s)', 262 | $this->toSpatialExpressionString($query, $column), 263 | $this->toSpatialExpressionString($query, $geometryOrColumn), 264 | ) 265 | ); 266 | 267 | return $query; 268 | } 269 | 270 | public function scopeWhereOverlaps( 271 | Builder $query, 272 | Expression|Geometry|string $column, 273 | Expression|Geometry|string $geometryOrColumn, 274 | ): Builder { 275 | $query->whereRaw( 276 | sprintf( 277 | 'ST_OVERLAPS(%s, %s)', 278 | $this->toSpatialExpressionString($query, $column), 279 | $this->toSpatialExpressionString($query, $geometryOrColumn), 280 | ) 281 | ); 282 | 283 | return $query; 284 | } 285 | 286 | public function scopeWhereEquals( 287 | Builder $query, 288 | Expression|Geometry|string $column, 289 | Expression|Geometry|string $geometryOrColumn, 290 | ): Builder { 291 | $query->whereRaw( 292 | sprintf( 293 | 'ST_EQUALS(%s, %s)', 294 | $this->toSpatialExpressionString($query, $column), 295 | $this->toSpatialExpressionString($query, $geometryOrColumn), 296 | ) 297 | ); 298 | 299 | return $query; 300 | } 301 | 302 | public function scopeWhereSrid( 303 | Builder $query, 304 | Expression|Geometry|string $column, 305 | string $operator, 306 | int|float $value 307 | ): Builder { 308 | $query->whereRaw( 309 | sprintf( 310 | 'ST_SRID(%s) %s ?', 311 | $this->toSpatialExpressionString($query, $column), 312 | $operator, 313 | ), 314 | [$value], 315 | ); 316 | 317 | return $query; 318 | } 319 | 320 | protected function toSpatialExpressionString(Builder $query, Expression|Geometry|string $value): string 321 | { 322 | $grammar = $query->getGrammar(); 323 | 324 | if ($value instanceof Expression) { 325 | $expression = $value; 326 | } elseif ($value instanceof Geometry) { 327 | $expression = $value->toSqlExpression($query->getConnection()); 328 | } else { 329 | $expression = $query->raw($grammar->wrap($value)); 330 | } 331 | 332 | return (string) $expression->getValue($grammar); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/Enums/GeometryType.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function getDoctrineClassName(): string 25 | { 26 | return match ($this) { 27 | self::GEOMETRY_COLLECTION => Doctrine\GeometryCollectionType::class, 28 | self::LINESTRING => Doctrine\LineStringType::class, 29 | self::MULTILINESTRING => Doctrine\MultiLineStringType::class, 30 | self::MULTIPOINT => Doctrine\MultiPointType::class, 31 | self::MULTIPOLYGON => Doctrine\MultiPolygonType::class, 32 | self::POINT => Doctrine\PointType::class, 33 | self::POLYGON => Doctrine\PolygonType::class, 34 | }; 35 | } 36 | 37 | /** 38 | * @return class-string 39 | */ 40 | public function getBaseGeometryClassName(): string 41 | { 42 | return match ($this) { 43 | self::GEOMETRY_COLLECTION => Geometry\GeometryCollection::class, 44 | self::LINESTRING => Geometry\LineString::class, 45 | self::MULTILINESTRING => Geometry\MultiLineString::class, 46 | self::MULTIPOINT => Geometry\MultiPoint::class, 47 | self::MULTIPOLYGON => Geometry\MultiPolygon::class, 48 | self::POINT => Geometry\Point::class, 49 | self::POLYGON => Geometry\Polygon::class, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Enums/Srid.php: -------------------------------------------------------------------------------- 1 | toWkt(); 39 | } 40 | 41 | /** 42 | * @throws LaravelSpatialJsonException 43 | */ 44 | public function toJson($options = 0): string 45 | { 46 | try { 47 | return json_encode($this, $options | JSON_THROW_ON_ERROR); 48 | } catch (JsonException $e) { // @codeCoverageIgnore 49 | throw new LaravelSpatialJsonException($e->getMessage(), previous: $e); // @codeCoverageIgnore 50 | } // @codeCoverageIgnore 51 | } 52 | 53 | /** 54 | * @throws LaravelSpatialException 55 | */ 56 | public function toWkb(): string 57 | { 58 | try { 59 | $geoPHPGeometry = geoPHP::load($this->toJson()); 60 | } catch (Throwable $e) { // @codeCoverageIgnore 61 | throw new LaravelSpatialException($e->getMessage(), previous: $e); // @codeCoverageIgnore 62 | } // @codeCoverageIgnore 63 | 64 | $sridInBinary = pack('L', $this->srid); 65 | 66 | // @phpstan-ignore-next-line 67 | $wkbWithoutSrid = (new geoPHPWkb)->write($geoPHPGeometry); 68 | 69 | return $sridInBinary.$wkbWithoutSrid; 70 | } 71 | 72 | public static function fromWkb(string $wkb): Geometry 73 | { 74 | $srid = substr($wkb, 0, 4); 75 | // @phpstan-ignore-next-line 76 | $srid = unpack('L', $srid)[1]; 77 | 78 | $wkb = substr($wkb, 4); 79 | 80 | $geometry = GeometryFactory::parse($wkb, static::class); 81 | $geometry->srid = $srid; 82 | 83 | if (! ($geometry instanceof static)) { 84 | throw new LaravelSpatialException(sprintf('Expected %s, %s given.', static::class, $geometry::class)); 85 | } 86 | 87 | return $geometry; 88 | } 89 | 90 | public static function fromWkt(string $wkt, int $srid = 0): static 91 | { 92 | $geometry = GeometryFactory::parse($wkt, static::class); 93 | $geometry->srid = $srid; 94 | 95 | if (! ($geometry instanceof static)) { 96 | throw new LaravelSpatialException(sprintf('Expected %s, %s given.', static::class, $geometry::class)); 97 | } 98 | 99 | return $geometry; 100 | } 101 | 102 | public static function fromJson(string $geoJson, int $srid = 0): static 103 | { 104 | $geometry = GeometryFactory::parse($geoJson, static::class); 105 | $geometry->srid = $srid; 106 | 107 | if (! ($geometry instanceof static)) { 108 | throw new LaravelSpatialException(sprintf('Expected %s, %s given.', static::class, $geometry::class)); 109 | } 110 | 111 | return $geometry; 112 | } 113 | 114 | /** 115 | * @param array $geometry 116 | * 117 | * @throws LaravelSpatialJsonException 118 | */ 119 | public static function fromArray(array $geometry): static 120 | { 121 | try { 122 | $geoJson = json_encode($geometry, JSON_THROW_ON_ERROR); 123 | } catch (JsonException $e) { // @codeCoverageIgnore 124 | throw new LaravelSpatialJsonException($e->getMessage(), previous: $e); // @codeCoverageIgnore 125 | } // @codeCoverageIgnore 126 | 127 | return static::fromJson($geoJson); 128 | } 129 | 130 | /** 131 | * @return array 132 | */ 133 | public function jsonSerialize(): array 134 | { 135 | return $this->toArray(); 136 | } 137 | 138 | /** 139 | * @return array{type: string, coordinates: array} 140 | */ 141 | public function toArray(): array 142 | { 143 | return [ 144 | 'type' => class_basename(static::class), 145 | 'coordinates' => $this->getCoordinates(), 146 | ]; 147 | } 148 | 149 | /** 150 | * @throws JsonException 151 | */ 152 | public function toFeatureCollectionJson(): string 153 | { 154 | if (static::class === GeometryCollection::class) { 155 | /** @var GeometryCollection $this */ 156 | $geometries = $this->geometries; 157 | } else { 158 | $geometries = collect([$this]); 159 | } 160 | 161 | $features = $geometries->map(static function (self $geometry): array { 162 | return [ 163 | 'type' => 'Feature', 164 | 'properties' => [], 165 | 'geometry' => $geometry->toArray(), 166 | ]; 167 | }); 168 | 169 | return json_encode([ 170 | 'type' => 'FeatureCollection', 171 | 'features' => $features, 172 | ], JSON_THROW_ON_ERROR); 173 | } 174 | 175 | /** 176 | * @return array 177 | */ 178 | abstract public function getCoordinates(): array; 179 | 180 | /** 181 | * @param array $arguments 182 | */ 183 | public static function castUsing(array $arguments): CastsAttributes 184 | { 185 | return new GeometryCast(static::class); 186 | } 187 | 188 | public function toSqlExpression(ConnectionInterface $connection): Expression 189 | { 190 | $wkt = $this->toWkt(); 191 | 192 | if (! (new Connection())->isSupportAxisOrder($connection)) { 193 | // @codeCoverageIgnoreStart 194 | return DB::raw(sprintf("ST_GeomFromText('%s', %d)", $wkt, $this->srid)); 195 | // @codeCoverageIgnoreEnd 196 | } 197 | 198 | return DB::raw(sprintf("ST_GeomFromText('%s', %d, 'axis-order=long-lat')", $wkt, $this->srid)); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Geometry/GeometryCollection.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected Collection $geometries; 16 | 17 | protected string $collectionOf = Geometry::class; 18 | 19 | protected int $minimumGeometries = 0; 20 | 21 | /** 22 | * @param Collection|array $geometries 23 | * 24 | * @throws LaravelSpatialException 25 | */ 26 | public function __construct(Collection|array $geometries, int $srid = 0) 27 | { 28 | if (is_array($geometries)) { 29 | $geometries = collect($geometries); 30 | } 31 | 32 | $this->geometries = $geometries; 33 | $this->srid = $srid; 34 | 35 | $this->geometries->each(fn (mixed $geometry) => $this->validateGeometriesType($geometry)); 36 | $this->validateGeometriesCount(); 37 | } 38 | 39 | public function toWkt(): string 40 | { 41 | $wktData = $this->getWktData(); 42 | 43 | return sprintf('GEOMETRYCOLLECTION(%s)', $wktData); 44 | } 45 | 46 | public function getWktData(): string 47 | { 48 | return $this->geometries 49 | ->map(static fn (Geometry $geometry): string => $geometry->toWkt()) 50 | ->join(', '); 51 | } 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function getCoordinates(): array 57 | { 58 | return $this->geometries 59 | ->map(static fn (Geometry $geometry): array => $geometry->getCoordinates()) 60 | ->all(); 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function toArray(): array 67 | { 68 | if ($this->isExtended()) { 69 | return parent::toArray(); 70 | } 71 | 72 | return [ 73 | 'type' => class_basename(static::class), 74 | 'geometries' => $this->geometries->map(static fn (Geometry $geometry): array => $geometry->toArray()), 75 | ]; 76 | } 77 | 78 | /** 79 | * @return Collection 80 | */ 81 | public function getGeometries(): Collection 82 | { 83 | return new Collection($this->geometries->all()); 84 | } 85 | 86 | /** 87 | * @param int $offset 88 | */ 89 | public function offsetExists($offset): bool 90 | { 91 | return isset($this->geometries[$offset]); 92 | } 93 | 94 | /** 95 | * @param int $offset 96 | */ 97 | public function offsetGet($offset): ?Geometry 98 | { 99 | return $this->geometries[$offset]; 100 | } 101 | 102 | /** 103 | * @param int $offset 104 | * @param Geometry $value 105 | * 106 | * @throws LaravelSpatialException 107 | */ 108 | public function offsetSet($offset, $value): void 109 | { 110 | $this->validateGeometriesType($value); 111 | $this->geometries[$offset] = $value; 112 | } 113 | 114 | /** 115 | * @param int $offset 116 | */ 117 | public function offsetUnset($offset): void 118 | { 119 | $this->geometries->splice($offset, 1); 120 | $this->validateGeometriesCount(); 121 | } 122 | 123 | /** 124 | * @throws LaravelSpatialException 125 | */ 126 | protected function validateGeometriesCount(): void 127 | { 128 | $geometriesCount = $this->geometries->count(); 129 | if ($geometriesCount < $this->minimumGeometries) { 130 | throw new LaravelSpatialException( 131 | sprintf( 132 | '%s must contain at least %s %s', 133 | static::class, 134 | $this->minimumGeometries, 135 | Str::plural('entries', $geometriesCount) 136 | ) 137 | ); 138 | } 139 | } 140 | 141 | /** 142 | * @throws LaravelSpatialException 143 | */ 144 | protected function validateGeometriesType(mixed $geometry): void 145 | { 146 | if (! is_object($geometry) || ! ($geometry instanceof $this->collectionOf)) { 147 | throw new LaravelSpatialException( 148 | sprintf('%s must be a collection of %s', static::class, $this->collectionOf) 149 | ); 150 | } 151 | } 152 | 153 | private function isExtended(): bool 154 | { 155 | return is_subclass_of(static::class, self::class); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Geometry/GeometryFactory.php: -------------------------------------------------------------------------------- 1 | getSRID()) ? $geometry->getSRID() : 0; 44 | 45 | if ($geometry instanceof geoPHPPoint) { 46 | if ($geometry->coords[0] === null || $geometry->coords[1] === null) { 47 | throw new LaravelSpatialException('Invalid spatial value'); 48 | } 49 | 50 | $class = self::getGeometryClass(GeometryType::POINT, $geometryClass); 51 | 52 | return new $class($geometry->coords[1], $geometry->coords[0], $srid); 53 | } 54 | 55 | /** @var geoPHPGeometryCollection $geometry */ 56 | $components = collect($geometry->components) 57 | ->map(static fn (geoPHPGeometry $component) => self::createFromGeometry($component, $geometryClass)); 58 | 59 | $type = match ($geometry::class) { 60 | geoPHPMultiPoint::class => GeometryType::MULTIPOINT, 61 | geoPHPLineString::class => GeometryType::LINESTRING, 62 | geoPHPPolygon::class => GeometryType::POLYGON, 63 | geoPHPMultiLineString::class => GeometryType::MULTILINESTRING, 64 | geoPHPMultiPolygon::class => GeometryType::MULTIPOLYGON, 65 | default => GeometryType::GEOMETRY_COLLECTION, 66 | }; 67 | 68 | $class = self::getGeometryClass($type, $geometryClass); 69 | 70 | return new $class($components, $srid); 71 | } 72 | 73 | /** 74 | * @param class-string $geometryClass 75 | * @return class-string 76 | */ 77 | private static function getGeometryClass(GeometryType $type, string $geometryClass): string 78 | { 79 | $classFromConfig = config('laravel-spatial.'.$type->value); 80 | $classFromBase = $type->getBaseGeometryClassName(); 81 | 82 | if (is_subclass_of($geometryClass, $classFromBase) || is_subclass_of($geometryClass, $classFromConfig)) { 83 | return $geometryClass; 84 | } 85 | 86 | return $classFromConfig; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Geometry/LineString.php: -------------------------------------------------------------------------------- 1 | getWktData(); 14 | 15 | return sprintf('LINESTRING(%s)', $wktData); 16 | } 17 | 18 | public function getWktData(): string 19 | { 20 | return $this->geometries 21 | ->map(static fn (Point $point): string => $point->getWktData()) 22 | ->join(', '); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Geometry/MultiLineString.php: -------------------------------------------------------------------------------- 1 | $geometries 12 | * 13 | * @method Collection getGeometries() 14 | * @method LineString offsetGet(int $offset) 15 | * @method void offsetSet(int $offset, LineString $value) 16 | */ 17 | class MultiLineString extends GeometryCollection 18 | { 19 | protected string $collectionOf = LineString::class; 20 | 21 | protected int $minimumGeometries = 1; 22 | 23 | /** 24 | * @param Collection|array $geometries 25 | * 26 | * @throws LaravelSpatialException 27 | */ 28 | public function __construct(Collection|array $geometries, int $srid = 0) 29 | { 30 | // @phpstan-ignore-next-line 31 | parent::__construct($geometries, $srid); 32 | } 33 | 34 | public function toWkt(): string 35 | { 36 | $wktData = $this->getWktData(); 37 | 38 | return sprintf('MULTILINESTRING(%s)', $wktData); 39 | } 40 | 41 | public function getWktData(): string 42 | { 43 | return $this->geometries 44 | ->map(static function (LineString $lineString): string { 45 | $wktData = $lineString->getWktData(); 46 | 47 | return sprintf('(%s)', $wktData); 48 | }) 49 | ->join(', '); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Geometry/MultiPoint.php: -------------------------------------------------------------------------------- 1 | getWktData(); 14 | 15 | return sprintf('MULTIPOINT(%s)', $wktData); 16 | } 17 | 18 | public function getWktData(): string 19 | { 20 | return $this->geometries 21 | ->map(static fn (Point $point): string => $point->getWktData()) 22 | ->join(', '); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Geometry/MultiPolygon.php: -------------------------------------------------------------------------------- 1 | $geometries 12 | * 13 | * @method Collection getGeometries() 14 | * @method Polygon offsetGet(int $offset) 15 | * @method void offsetSet(int $offset, Polygon $value) 16 | */ 17 | class MultiPolygon extends GeometryCollection 18 | { 19 | protected string $collectionOf = Polygon::class; 20 | 21 | protected int $minimumGeometries = 1; 22 | 23 | /** 24 | * @param Collection|array $geometries 25 | * 26 | * @throws LaravelSpatialException 27 | */ 28 | public function __construct(Collection|array $geometries, int $srid = 0) 29 | { 30 | // @phpstan-ignore-next-line 31 | parent::__construct($geometries, $srid); 32 | } 33 | 34 | public function toWkt(): string 35 | { 36 | $wktData = $this->getWktData(); 37 | 38 | return sprintf('MULTIPOLYGON(%s)', $wktData); 39 | } 40 | 41 | public function getWktData(): string 42 | { 43 | return $this->geometries 44 | ->map(static function (Polygon $polygon): string { 45 | $wktData = $polygon->getWktData(); 46 | 47 | return sprintf('(%s)', $wktData); 48 | }) 49 | ->join(', '); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Geometry/Point.php: -------------------------------------------------------------------------------- 1 | srid = $srid; 12 | } 13 | 14 | public function toWkt(): string 15 | { 16 | $wktData = $this->getWktData(); 17 | 18 | return sprintf('POINT(%s)', $wktData); 19 | } 20 | 21 | public function getWktData(): string 22 | { 23 | return sprintf('%s %s', $this->longitude, $this->latitude); 24 | } 25 | 26 | /** 27 | * @return array{0: float, 1: float} 28 | */ 29 | public function getCoordinates(): array 30 | { 31 | return [ 32 | $this->longitude, 33 | $this->latitude, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Geometry/PointCollection.php: -------------------------------------------------------------------------------- 1 | $geometries 12 | * 13 | * @method Collection getGeometries() 14 | * @method Point offsetGet(int $offset) 15 | * @method void offsetSet(int $offset, Point $value) 16 | */ 17 | abstract class PointCollection extends GeometryCollection 18 | { 19 | protected string $collectionOf = Point::class; 20 | 21 | /** 22 | * @param Collection|array $geometries 23 | * 24 | * @throws LaravelSpatialException 25 | */ 26 | public function __construct(Collection|array $geometries, int $srid = 0) 27 | { 28 | // @phpstan-ignore-next-line 29 | parent::__construct($geometries, $srid); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Geometry/Polygon.php: -------------------------------------------------------------------------------- 1 | getWktData(); 12 | 13 | return sprintf('POLYGON(%s)', $wktData); 14 | } 15 | 16 | public function getWktData(): string 17 | { 18 | return $this->geometries 19 | ->map(static function (LineString $lineString): string { 20 | $wktData = $lineString->getWktData(); 21 | 22 | return sprintf('(%s)', $wktData); 23 | }) 24 | ->join(', '); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/LaravelSpatialServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes( 23 | [__DIR__.'/../config/laravel-spatial.php' => config_path('laravel-spatial.php')], 24 | 'laravel-spatial-config' 25 | ); 26 | 27 | $this->mergeConfigFrom(__DIR__.'/../config/laravel-spatial.php', 'laravel-spatial'); 28 | 29 | $this->validateConfig(); 30 | 31 | if (DB::connection()->isDoctrineAvailable()) { 32 | $this->registerDoctrineTypes(); 33 | } 34 | } 35 | 36 | /** 37 | * @throws Throwable 38 | */ 39 | private function registerDoctrineTypes(): void 40 | { 41 | foreach (GeometryType::cases() as $type) { 42 | $this->registerDoctrineType($type->getDoctrineClassName(), $type->value); 43 | } 44 | 45 | $this->registerDoctrineType(GeometryType::GEOMETRY_COLLECTION->getDoctrineClassName(), 'geomcollection'); 46 | } 47 | 48 | /** 49 | * @param class-string $class 50 | * 51 | * @throws Throwable 52 | */ 53 | private function registerDoctrineType(string $class, string $type): void 54 | { 55 | DB::registerDoctrineType($class, $type, $type); 56 | 57 | DB::connection()->registerDoctrineType($class, $type, $type); 58 | } 59 | 60 | /** 61 | * @throws LaravelSpatialException 62 | */ 63 | private function validateConfig(): void 64 | { 65 | /** @var array>|array $config */ 66 | $config = config('laravel-spatial'); 67 | 68 | foreach (GeometryType::cases() as $type) { 69 | $configType = $config[$type->value] ?? null; 70 | if (! $configType) { 71 | throw new LaravelSpatialException( 72 | sprintf('Invalid class for geometry type "%s", please check config', $type->value) 73 | ); 74 | } 75 | 76 | $baseClass = $type->getBaseGeometryClassName(); 77 | /** @phpstan-ignore-next-line */ 78 | if ($configType !== $baseClass && ! $configType instanceof $baseClass) { 79 | throw new LaravelSpatialException(sprintf( 80 | 'Class for geometry type "%s" should be instance of "%s" ("%s" provided), please check config', 81 | $type->value, 82 | $baseClass, 83 | $configType, 84 | )); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/Custom/CustomConfigTest.php: -------------------------------------------------------------------------------- 1 | value => '']); 11 | 12 | expect(function (): void { 13 | $provider = new LaravelSpatialServiceProvider(app()); 14 | $provider->boot(); 15 | })->toThrow(LaravelSpatialException::class, 'Invalid class for geometry type "point", please check config'); 16 | }); 17 | 18 | it('throws exception when invalid class in config', function (): void { 19 | config(['laravel-spatial.'.GeometryType::POINT->value => CustomPointInvalid::class]); 20 | 21 | $error = sprintf( 22 | 'Class for geometry type "point" should be instance of "%s" ("%s" provided), please check config', 23 | Point::class, 24 | CustomPointInvalid::class 25 | ); 26 | 27 | expect(function (): void { 28 | $provider = new LaravelSpatialServiceProvider(app()); 29 | $provider->boot(); 30 | })->toThrow(LaravelSpatialException::class, $error); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/Custom/CustomPoint.php: -------------------------------------------------------------------------------- 1 | create(['point' => $point]); 17 | 18 | expect($testPlace->point)->toBeInstanceOf(CustomPoint::class) 19 | ->and($testPlace->point)->toEqual($point); 20 | 21 | /** @var CustomTestPlace $testPlace */ 22 | $testPlace = CustomTestPlace::find(1); 23 | expect($testPlace->point)->toBeInstanceOf(CustomPoint::class) 24 | ->and($testPlace->point)->toEqual($point); 25 | }); 26 | 27 | it('creates a model record with custom point based on config', function (): void { 28 | config(['laravel-spatial.'.GeometryType::POINT->value => CustomPointConfig::class]); 29 | 30 | $point = new Point(0, 180); 31 | 32 | /** @var TestPlace $testPlace */ 33 | $testPlace = TestPlace::factory()->create(['point' => $point]); 34 | 35 | expect($testPlace->point)->toBeInstanceOf(Point::class) 36 | ->and($testPlace->point)->toEqual($point); 37 | 38 | /** @var TestPlace $testPlace */ 39 | $testPlace = TestPlace::find(1); 40 | expect($testPlace->point)->toBeInstanceOf(CustomPointConfig::class); 41 | }); 42 | 43 | it('creates a model record with custom point override config', function (): void { 44 | config(['laravel-spatial.'.GeometryType::POINT->value => CustomPointConfig::class]); 45 | 46 | $point = new CustomPoint(0, 180); 47 | 48 | /** @var CustomTestPlace $testPlace */ 49 | $testPlace = CustomTestPlace::factory()->create(['point' => $point]); 50 | 51 | expect($testPlace->point)->toBeInstanceOf(CustomPoint::class) 52 | ->and($testPlace->point)->toEqual($point); 53 | 54 | /** @var CustomTestPlace $testPlace */ 55 | $testPlace = CustomTestPlace::find(1); 56 | expect($testPlace->point)->toBeInstanceOf(CustomPoint::class); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/Custom/CustomTestPlace.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected $casts = [ 36 | 'point' => CustomPoint::class, 37 | ]; 38 | 39 | protected static function newFactory(): TestPlaceFactory 40 | { 41 | return new TestPlaceFactory; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Custom/CustomTestPlaceFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CustomTestPlaceFactory extends Factory 11 | { 12 | protected $model = CustomTestPlace::class; 13 | 14 | /** 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => $this->faker->streetName, 21 | 'address' => $this->faker->address, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Database/TestFactories/TestPlaceFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TestPlaceFactory extends Factory 12 | { 13 | protected $model = TestPlace::class; 14 | 15 | /** 16 | * @return array 17 | */ 18 | public function definition(): array 19 | { 20 | return [ 21 | 'name' => $this->faker->streetName, 22 | 'address' => $this->faker->address, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Database/TestModels/TestPlace.php: -------------------------------------------------------------------------------- 1 | 51 | */ 52 | protected $casts = [ 53 | 'point' => Point::class, 54 | 'multi_point' => MultiPoint::class, 55 | 'line_string' => LineString::class, 56 | 'multi_line_string' => MultiLineString::class, 57 | 'polygon' => Polygon::class, 58 | 'multi_polygon' => MultiPolygon::class, 59 | 'geometry_collection' => GeometryCollection::class, 60 | 'point_with_line_string_cast' => LineString::class, 61 | ]; 62 | 63 | protected static function newFactory(): TestPlaceFactory 64 | { 65 | return new TestPlaceFactory; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Database/migrations/0000_00_00_000000_create_test_places_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->timestamps(); 14 | $table->string('name'); 15 | $table->string('address'); 16 | $table->point('point')->nullable(); 17 | $table->multiPoint('multi_point')->nullable(); 18 | $table->lineString('line_string')->nullable(); 19 | $table->multiLineString('multi_line_string')->nullable(); 20 | $table->polygon('polygon')->nullable(); 21 | $table->multiPolygon('multi_polygon')->nullable(); 22 | $table->geometryCollection('geometry_collection')->nullable(); 23 | $table->point('point_with_line_string_cast')->nullable(); 24 | $table->decimal('longitude')->nullable(); 25 | $table->decimal('latitude')->nullable(); 26 | }); 27 | } 28 | 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('test_places'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Doctrine/DoctrineTypesTest.php: -------------------------------------------------------------------------------- 1 | $typeClass */ 16 | $doctrineSchemaManager = DB::connection()->getDoctrineSchemaManager(); 17 | 18 | $columns = $doctrineSchemaManager->listTableColumns('test_places'); 19 | $dbPlatform = DB::getDoctrineConnection()->getDatabasePlatform(); 20 | 21 | expect($columns[$column]->getType())->toBeInstanceOf($typeClass) 22 | ->and($columns[$column]->getType()->getName())->toBe($typeName) 23 | ->and($columns[$column]->getType()->getSQLDeclaration([''], $dbPlatform))->toBe($typeName); 24 | })->with([ 25 | 'point' => ['point', PointType::class, 'point'], 26 | 'line_string' => ['line_string', LineStringType::class, 'linestring'], 27 | 'multi_point' => ['multi_point', MultiPointType::class, 'multipoint'], 28 | 'polygon' => ['polygon', PolygonType::class, 'polygon'], 29 | 'multi_line_string' => ['multi_line_string', MultiLineStringType::class, 'multilinestring'], 30 | 'multi_polygon' => ['multi_polygon', MultiPolygonType::class, 'multipolygon'], 31 | 'geometry_collection' => ['geometry_collection', GeometryCollectionType::class, 'geometrycollection'], 32 | ]); 33 | -------------------------------------------------------------------------------- /tests/Eloquent/GeometryCastTest.php: -------------------------------------------------------------------------------- 1 | create(['point' => null]); 15 | 16 | expect($testPlace->point)->toBeNull(); 17 | }); 18 | 19 | it('updates a model record', function (): void { 20 | $point = new Point(0, 180); 21 | $point2 = new Point(0, 0); 22 | /** @var TestPlace $testPlace */ 23 | $testPlace = TestPlace::factory()->create(['point' => $point]); 24 | 25 | $testPlace->update(['point' => $point2]); 26 | 27 | expect($testPlace->point)->not->toEqual($point); 28 | expect($testPlace->point)->toEqual($point2); 29 | }); 30 | 31 | it('updates a model record with expression', function (): void { 32 | $point = new Point(0, 180); 33 | /** @var TestPlace $testPlace */ 34 | $testPlace = TestPlace::factory()->create(['point' => $point]); 35 | $pointFromAttributes = $testPlace->getAttributes()['point']; 36 | 37 | expect($testPlace->update(['point' => $pointFromAttributes]))->toBeTrue(); 38 | }); 39 | 40 | it('updates a model record with null geometry', function (): void { 41 | $point = new Point(0, 180); 42 | /** @var TestPlace $testPlace */ 43 | $testPlace = TestPlace::factory()->create(['point' => $point]); 44 | 45 | $testPlace->update(['point' => null]); 46 | 47 | expect($testPlace->point)->toBeNull(); 48 | }); 49 | 50 | it('gets original geometry field', function (): void { 51 | $point = new Point(0, 180, Srid::WGS84->value); 52 | $point2 = new Point(0, 0, Srid::WGS84->value); 53 | /** @var TestPlace $testPlace */ 54 | $testPlace = TestPlace::factory()->create(['point' => $point]); 55 | 56 | $testPlace->point = $point2; 57 | 58 | expect($testPlace->getOriginal('point'))->toEqual($point); 59 | expect($testPlace->point)->not->toEqual($point); 60 | expect($testPlace->point)->toEqual($point2); 61 | }); 62 | 63 | it('serializes a model record to array with geometry', function (): void { 64 | $point = new Point(0, 180); 65 | /** @var TestPlace $testPlace */ 66 | $testPlace = TestPlace::factory()->create(['point' => $point]); 67 | 68 | $serialized = $testPlace->toArray(); 69 | 70 | $expectedArray = $point->toArray(); 71 | expect($serialized['point'])->toEqual($expectedArray); 72 | }); 73 | 74 | it('serializes a model record to json with geometry', function (): void { 75 | $point = new Point(0, 180); 76 | /** @var TestPlace $testPlace */ 77 | $testPlace = TestPlace::factory()->create(['point' => $point]); 78 | 79 | $serialized = $testPlace->toJson(); 80 | 81 | // @phpstan-ignore-next-line 82 | $json = json_encode(json_decode($serialized, true)['point']); 83 | $expectedJson = $point->toJson(); 84 | expect($json)->toBe($expectedJson); 85 | }); 86 | 87 | it('throws exception when cast serializing incorrect geometry object', function (): void { 88 | expect(function (): void { 89 | TestPlace::factory()->make([ 90 | 'point' => new LineString([ 91 | new Point(0, 180), 92 | new Point(1, 179), 93 | ]), 94 | ]); 95 | })->toThrow(LaravelSpatialException::class); 96 | }); 97 | 98 | it('throws exception when cast serializing non-geometry object', function (): void { 99 | expect(function (): void { 100 | TestPlace::factory()->make([ 101 | 'point' => 'not-a-point-object', 102 | ]); 103 | })->toThrow(LaravelSpatialException::class); 104 | }); 105 | 106 | it('throws exception when cast deserializing incorrect geometry object', function (): void { 107 | TestPlace::insert(array_merge(TestPlace::factory()->definition(), [ 108 | 'point_with_line_string_cast' => DB::raw('POINT(0, 180)'), 109 | ])); 110 | /** @var TestPlace $testPlace */ 111 | $testPlace = TestPlace::firstOrFail(); 112 | 113 | expect(function () use ($testPlace): void { 114 | $testPlace->getAttribute('point_with_line_string_cast'); 115 | })->toThrow(LaravelSpatialException::class); 116 | }); 117 | 118 | it('creates a model record with geometry from geo json array', function (): void { 119 | $point = new Point(0, 180); 120 | $pointGeoJsonArray = $point->toArray(); 121 | 122 | /** @var TestPlace $testPlace */ 123 | $testPlace = TestPlace::factory()->make(['point' => $pointGeoJsonArray]); 124 | 125 | expect($testPlace->point)->toEqual($point); 126 | }); 127 | -------------------------------------------------------------------------------- /tests/Eloquent/HasSpatialTest.php: -------------------------------------------------------------------------------- 1 | create(['point' => new Point(0, 0, Srid::WGS84->value)]); 14 | 15 | /** @var TestPlace $testPlaceWithDistance */ 16 | $testPlaceWithDistance = TestPlace::query()->select(['id'])->selectRaw('id as id_new') 17 | ->withDistance('point', new Point(1, 1, Srid::WGS84->value)) 18 | ->firstOrFail(); 19 | 20 | expect($testPlaceWithDistance->distance)->toBe(156897.79947260793) 21 | ->and($testPlaceWithDistance->id)->toBe(1) 22 | ->and($testPlaceWithDistance->id_new)->toBe(1); 23 | })->skip(fn () => ! isSupportAxisOrder()); 24 | 25 | it('calculates distance - without axis-order', function (): void { 26 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 27 | 28 | /** @var TestPlace $testPlaceWithDistance */ 29 | $testPlaceWithDistance = TestPlace::withDistance('point', new Point(1, 1, Srid::WGS84->value))->firstOrFail(); 30 | 31 | expect($testPlaceWithDistance->distance)->toBe(1.4142135623730951); 32 | })->skip(fn () => isSupportAxisOrder()); 33 | 34 | it('calculates distance with alias', function (): void { 35 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 36 | 37 | /** @var TestPlace $testPlaceWithDistance */ 38 | $testPlaceWithDistance = TestPlace::withDistance('point', new Point(1, 1, Srid::WGS84->value), 'distance_in_meters') 39 | ->firstOrFail(); 40 | 41 | expect($testPlaceWithDistance->distance_in_meters)->toBe(156897.79947260793); 42 | })->skip(fn () => ! isSupportAxisOrder()); 43 | 44 | it('calculates distance with alias - without axis-order', function (): void { 45 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 46 | 47 | /** @var TestPlace $testPlaceWithDistance */ 48 | $testPlaceWithDistance = TestPlace::withDistance('point', new Point(1, 1, Srid::WGS84->value), 'distance_in_meters') 49 | ->firstOrFail(); 50 | 51 | expect($testPlaceWithDistance->distance_in_meters)->toBe(1.4142135623730951); 52 | })->skip(fn () => isSupportAxisOrder()); 53 | 54 | it('filters by distance', function (): void { 55 | $pointWithinDistance = new Point(0, 0, Srid::WGS84->value); 56 | $pointNotWithinDistance = new Point(50, 50, Srid::WGS84->value); 57 | TestPlace::factory()->create(['point' => $pointWithinDistance]); 58 | TestPlace::factory()->create(['point' => $pointNotWithinDistance]); 59 | 60 | /** @var TestPlace[] $testPlacesWithinDistance */ 61 | $testPlacesWithinDistance = TestPlace::whereDistance('point', new Point(1, 1, Srid::WGS84->value), '<', 200_000) 62 | ->get(); 63 | 64 | expect($testPlacesWithinDistance)->toHaveCount(1) 65 | ->and($testPlacesWithinDistance[0]->point)->toEqual($pointWithinDistance); 66 | })->skip(fn () => ! isSupportAxisOrder()); 67 | 68 | it('filters by distance - without axis-order', function (): void { 69 | $pointWithinDistance = new Point(0, 0, Srid::WGS84->value); 70 | $pointNotWithinDistance = new Point(50, 50, Srid::WGS84->value); 71 | TestPlace::factory()->create(['point' => $pointWithinDistance]); 72 | TestPlace::factory()->create(['point' => $pointNotWithinDistance]); 73 | 74 | /** @var TestPlace[] $testPlacesWithinDistance */ 75 | $testPlacesWithinDistance = TestPlace::whereDistance('point', new Point(1, 1, Srid::WGS84->value), '<', 2)->get(); 76 | 77 | expect($testPlacesWithinDistance)->toHaveCount(1) 78 | ->and($testPlacesWithinDistance[0]->point)->toEqual($pointWithinDistance); 79 | })->skip(fn () => isSupportAxisOrder()); 80 | 81 | it('orders by distance ASC', function (): void { 82 | $closerTestPlace = TestPlace::factory()->create(['point' => new Point(1, 1, Srid::WGS84->value)]); 83 | $fartherTestPlace = TestPlace::factory()->create(['point' => new Point(2, 2, Srid::WGS84->value)]); 84 | 85 | /** @var TestPlace[] $testPlacesOrderedByDistance */ 86 | $testPlacesOrderedByDistance = TestPlace::orderByDistance('point', new Point(0, 0, Srid::WGS84->value))->get(); 87 | 88 | expect($testPlacesOrderedByDistance[0]->id)->toBe($closerTestPlace->id) 89 | ->and($testPlacesOrderedByDistance[1]->id)->toBe($fartherTestPlace->id); 90 | }); 91 | 92 | it('orders by distance DESC', function (): void { 93 | $closerTestPlace = TestPlace::factory()->create(['point' => new Point(1, 1, Srid::WGS84->value)]); 94 | $fartherTestPlace = TestPlace::factory()->create(['point' => new Point(2, 2, Srid::WGS84->value)]); 95 | 96 | /** @var TestPlace[] $testPlacesOrderedByDistance */ 97 | $testPlacesOrderedByDistance = TestPlace::orderByDistance('point', new Point(0, 0, Srid::WGS84->value), 'desc') 98 | ->get(); 99 | 100 | expect($testPlacesOrderedByDistance[1]->id)->toBe($closerTestPlace->id) 101 | ->and($testPlacesOrderedByDistance[0]->id)->toBe($fartherTestPlace->id); 102 | }); 103 | 104 | it('calculates distance sphere', function (): void { 105 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 106 | 107 | /** @var TestPlace $testPlaceWithDistance */ 108 | $testPlaceWithDistance = TestPlace::withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value))->firstOrFail(); 109 | 110 | expect($testPlaceWithDistance->distance)->toBe(157249.59776850493) 111 | ->and($testPlaceWithDistance->name)->not()->toBeNull(); 112 | })->skip(fn () => ! isSupportAxisOrder()); 113 | 114 | it('calculates distance sphere - without axis-order', function (): void { 115 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 116 | 117 | /** @var TestPlace $testPlaceWithDistance */ 118 | $testPlaceWithDistance = TestPlace::withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value))->firstOrFail(); 119 | 120 | expect($testPlaceWithDistance->distance)->toBe(157249.0357231545) 121 | ->and($testPlaceWithDistance->name)->not()->toBeNull(); 122 | })->skip(fn () => isSupportAxisOrder()); 123 | 124 | it('calculates distance sphere with alias', function (): void { 125 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 126 | 127 | $point = new Point(1, 1, Srid::WGS84->value); 128 | /** @var TestPlace $testPlaceWithDistance */ 129 | $testPlaceWithDistance = TestPlace::withDistanceSphere('point', $point, 'distance_in_meters')->firstOrFail(); 130 | 131 | expect($testPlaceWithDistance->distance_in_meters)->toBe(157249.59776850493); 132 | })->skip(fn () => ! isSupportAxisOrder()); 133 | 134 | it('calculates distance sphere with alias - without axis-order', function (): void { 135 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 136 | 137 | $point = new Point(1, 1, Srid::WGS84->value); 138 | /** @var TestPlace $testPlaceWithDistance */ 139 | $testPlaceWithDistance = TestPlace::withDistanceSphere('point', $point, 'distance_in_meters')->firstOrFail(); 140 | 141 | expect($testPlaceWithDistance->distance_in_meters)->toBe(157249.0357231545); 142 | })->skip(fn () => isSupportAxisOrder()); 143 | 144 | it('filters distance sphere', function (): void { 145 | $pointWithinDistance = new Point(0, 0, Srid::WGS84->value); 146 | $pointNotWithinDistance = new Point(50, 50, Srid::WGS84->value); 147 | TestPlace::factory()->create(['point' => $pointWithinDistance]); 148 | TestPlace::factory()->create(['point' => $pointNotWithinDistance]); 149 | 150 | $point = new Point(1, 1, Srid::WGS84->value); 151 | /** @var TestPlace[] $testPlacesWithinDistance */ 152 | $testPlacesWithinDistance = TestPlace::whereDistanceSphere('point', $point, '<', 200000)->get(); 153 | 154 | expect($testPlacesWithinDistance)->toHaveCount(1) 155 | ->and($testPlacesWithinDistance[0]->point)->toEqual($pointWithinDistance); 156 | }); 157 | 158 | it('orders by distance sphere ASC', function (): void { 159 | $closerTestPlace = TestPlace::factory()->create(['point' => new Point(1, 1, Srid::WGS84->value)]); 160 | $fartherTestPlace = TestPlace::factory()->create(['point' => new Point(2, 2, Srid::WGS84->value)]); 161 | 162 | /** @var TestPlace[] $testPlacesOrderedByDistance */ 163 | $testPlacesOrderedByDistance = TestPlace::orderByDistanceSphere('point', new Point(0, 0, Srid::WGS84->value)) 164 | ->get(); 165 | 166 | expect($testPlacesOrderedByDistance[0]->id)->toBe($closerTestPlace->id) 167 | ->and($testPlacesOrderedByDistance[1]->id)->toBe($fartherTestPlace->id); 168 | }); 169 | 170 | it('orders by distance sphere DESC', function (): void { 171 | $closerTestPlace = TestPlace::factory()->create(['point' => new Point(1, 1, Srid::WGS84->value)]); 172 | $fartherTestPlace = TestPlace::factory()->create(['point' => new Point(2, 2, Srid::WGS84->value)]); 173 | 174 | $point = new Point(0, 0, Srid::WGS84->value); 175 | /** @var TestPlace[] $testPlacesOrderedByDistance */ 176 | $testPlacesOrderedByDistance = TestPlace::orderByDistanceSphere('point', $point, 'desc')->get(); 177 | 178 | expect($testPlacesOrderedByDistance[1]->id)->toBe($closerTestPlace->id) 179 | ->and($testPlacesOrderedByDistance[0]->id)->toBe($fartherTestPlace->id); 180 | }); 181 | 182 | it('filters by within', function (): void { 183 | $polygon = Polygon::fromJson( 184 | '{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}', 185 | Srid::WGS84->value 186 | ); 187 | $pointWithinPolygon = new Point(0, 0, Srid::WGS84->value); 188 | $pointOutsidePolygon = new Point(50, 50, Srid::WGS84->value); 189 | TestPlace::factory()->create(['point' => $pointWithinPolygon]); 190 | TestPlace::factory()->create(['point' => $pointOutsidePolygon]); 191 | 192 | /** @var TestPlace[] $testPlacesWithinPolygon */ 193 | $testPlacesWithinPolygon = TestPlace::whereWithin('point', $polygon)->get(); 194 | 195 | expect($testPlacesWithinPolygon)->toHaveCount(1) 196 | ->and($testPlacesWithinPolygon[0]->point)->toEqual($pointWithinPolygon); 197 | }); 198 | 199 | it('filters by not within', function (): void { 200 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}', 201 | Srid::WGS84->value); 202 | $pointWithinPolygon = new Point(0, 0, Srid::WGS84->value); 203 | $pointOutsidePolygon = new Point(50, 50, Srid::WGS84->value); 204 | TestPlace::factory()->create(['point' => $pointWithinPolygon]); 205 | TestPlace::factory()->create(['point' => $pointOutsidePolygon]); 206 | 207 | /** @var TestPlace[] $testPlacesNotWithinPolygon */ 208 | $testPlacesNotWithinPolygon = TestPlace::whereNotWithin('point', $polygon)->get(); 209 | 210 | expect($testPlacesNotWithinPolygon)->toHaveCount(1) 211 | ->and($testPlacesNotWithinPolygon[0]->point)->toEqual($pointOutsidePolygon); 212 | }); 213 | 214 | it('filters by contains', function (): void { 215 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}', 216 | Srid::WGS84->value); 217 | $pointWithinPolygon = new Point(0, 0, Srid::WGS84->value); 218 | $pointOutsidePolygon = new Point(50, 50, Srid::WGS84->value); 219 | TestPlace::factory()->create(['polygon' => $polygon]); 220 | 221 | $testPlace = TestPlace::whereContains('polygon', $pointWithinPolygon)->first(); 222 | $testPlace2 = TestPlace::whereContains('polygon', $pointOutsidePolygon)->first(); 223 | 224 | expect($testPlace)->not->toBeNull() 225 | ->and($testPlace2)->toBeNull(); 226 | }); 227 | 228 | it('filters by not contains', function (): void { 229 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}', 230 | Srid::WGS84->value); 231 | $pointWithinPolygon = new Point(0, 0, Srid::WGS84->value); 232 | $pointOutsidePolygon = new Point(50, 50, Srid::WGS84->value); 233 | TestPlace::factory()->create(['polygon' => $polygon]); 234 | 235 | $testPlace = TestPlace::whereNotContains('polygon', $pointWithinPolygon)->first(); 236 | $testPlace2 = TestPlace::whereNotContains('polygon', $pointOutsidePolygon)->first(); 237 | 238 | expect($testPlace)->toBeNull() 239 | ->and($testPlace2)->not->toBeNull(); 240 | }); 241 | 242 | it('filters by touches', function (): void { 243 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[0,-1],[0,0],[-1,0],[-1,-1]]]}', 244 | Srid::WGS84->value); 245 | $pointTouchesPolygon = new Point(0, 0, Srid::WGS84->value); 246 | $pointNotTouchesPolygon = new Point(50, 50, Srid::WGS84->value); 247 | TestPlace::factory()->create(['point' => $pointTouchesPolygon]); 248 | TestPlace::factory()->create(['point' => $pointNotTouchesPolygon]); 249 | 250 | /** @var TestPlace[] $testPlacesTouchPolygon */ 251 | $testPlacesTouchPolygon = TestPlace::whereTouches('point', $polygon)->get(); 252 | 253 | expect($testPlacesTouchPolygon)->toHaveCount(1) 254 | ->and($testPlacesTouchPolygon[0]->point)->toEqual($pointTouchesPolygon); 255 | }); 256 | 257 | it('filters by intersects', function (): void { 258 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}', 259 | Srid::WGS84->value); 260 | $pointIntersectsPolygon = new Point(0, 0, Srid::WGS84->value); 261 | $pointNotIntersectsPolygon = new Point(50, 50, Srid::WGS84->value); 262 | TestPlace::factory()->create(['point' => $pointIntersectsPolygon]); 263 | TestPlace::factory()->create(['point' => $pointNotIntersectsPolygon]); 264 | 265 | /** @var TestPlace[] $testPlacesInterestPolygon */ 266 | $testPlacesInterestPolygon = TestPlace::whereIntersects('point', $polygon)->get(); 267 | 268 | expect($testPlacesInterestPolygon)->toHaveCount(1) 269 | ->and($testPlacesInterestPolygon[0]->point)->toEqual($pointIntersectsPolygon); 270 | }); 271 | 272 | it('filters by crosses', function (): void { 273 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}', 274 | Srid::WGS84->value); 275 | $lineStringCrossesPolygon = LineString::fromJson('{"type":"LineString","coordinates":[[0,0],[2,0]]}', 276 | Srid::WGS84->value); 277 | $lineStringNotCrossesPolygon = LineString::fromJson('{"type":"LineString","coordinates":[[50,50],[52,50]]}', 278 | Srid::WGS84->value); 279 | TestPlace::factory()->create(['line_string' => $lineStringCrossesPolygon]); 280 | TestPlace::factory()->create(['line_string' => $lineStringNotCrossesPolygon]); 281 | 282 | /** @var TestPlace[] $testPlacesCrossPolygon */ 283 | $testPlacesCrossPolygon = TestPlace::whereCrosses('line_string', $polygon)->get(); 284 | 285 | expect($testPlacesCrossPolygon)->toHaveCount(1) 286 | ->and($testPlacesCrossPolygon[0]->line_string)->toEqual($lineStringCrossesPolygon); 287 | }); 288 | 289 | it('filters by disjoint', function (): void { 290 | $polygon = Polygon::fromJson( 291 | '{"type":"Polygon","coordinates":[[[-1,-1],[-0.5,-1],[-0.5,-0.5],[-1,-0.5],[-1,-1]]]}', 292 | Srid::WGS84->value 293 | ); 294 | $pointDisjointsPolygon = new Point(0, 0, Srid::WGS84->value); 295 | $pointNotDisjointsPolygon = new Point(-1, -1, Srid::WGS84->value); 296 | TestPlace::factory()->create(['point' => $pointDisjointsPolygon]); 297 | TestPlace::factory()->create(['point' => $pointNotDisjointsPolygon]); 298 | 299 | /** @var TestPlace[] $testPlacesDisjointPolygon */ 300 | $testPlacesDisjointPolygon = TestPlace::whereDisjoint('point', $polygon)->get(); 301 | 302 | expect($testPlacesDisjointPolygon)->toHaveCount(1) 303 | ->and($testPlacesDisjointPolygon[0]->point)->toEqual($pointDisjointsPolygon); 304 | }); 305 | 306 | it('filters by overlaps', function (): void { 307 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-0.75,-0.75],[1,-1],[1,1],[-1,1],[-0.75,-0.75]]]}', 308 | Srid::WGS84->value); 309 | $overlappingPolygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[-0.5,-1],[-0.5,-0.5],[-1,-0.5],[-1,-1]]]}', 310 | Srid::WGS84->value); 311 | $notOverlappingPolygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-10,-10],[-5,-10],[-5,-5],[-10,-5],[-10,-10]]]}', 312 | Srid::WGS84->value); 313 | TestPlace::factory()->create(['polygon' => $overlappingPolygon]); 314 | TestPlace::factory()->create(['polygon' => $notOverlappingPolygon]); 315 | 316 | /** @var TestPlace[] $overlappingTestPlaces */ 317 | $overlappingTestPlaces = TestPlace::whereOverlaps('polygon', $polygon)->get(); 318 | 319 | expect($overlappingTestPlaces)->toHaveCount(1) 320 | ->and($overlappingTestPlaces[0]->polygon)->toEqual($overlappingPolygon); 321 | }); 322 | 323 | it('filters by equals', function (): void { 324 | $point1 = new Point(0, 0, Srid::WGS84->value); 325 | $point2 = new Point(50, 50, Srid::WGS84->value); 326 | TestPlace::factory()->create(['point' => $point1]); 327 | TestPlace::factory()->create(['point' => $point2]); 328 | 329 | /** @var TestPlace[] $testPlaces */ 330 | $testPlaces = TestPlace::whereEquals('point', $point1)->get(); 331 | 332 | expect($testPlaces)->toHaveCount(1) 333 | ->and($testPlaces[0]->point)->toEqual($point1); 334 | }); 335 | 336 | it('filters by SRID', function (): void { 337 | $point1 = new Point(0, 0, Srid::WGS84->value); 338 | $point2 = new Point(50, 50, 0); 339 | TestPlace::factory()->create(['point' => $point1]); 340 | TestPlace::factory()->create(['point' => $point2]); 341 | 342 | /** @var TestPlace[] $testPlaces */ 343 | $testPlaces = TestPlace::whereSrid('point', '=', Srid::WGS84->value)->get(); 344 | 345 | expect($testPlaces)->toHaveCount(1) 346 | ->and($testPlaces[0]->point)->toEqual($point1); 347 | }); 348 | 349 | it('uses spatial function with column', function (): void { 350 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 351 | 352 | /** @var TestPlace $testPlaceWithDistance */ 353 | $testPlaceWithDistance = TestPlace::withDistance('point', 'point')->firstOrFail(); 354 | 355 | expect($testPlaceWithDistance->distance)->toBe(0.0) 356 | ->and($testPlaceWithDistance->name)->not()->toBeNull(); 357 | }); 358 | 359 | it('uses spatial function with column that contains table name', function (): void { 360 | TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); 361 | 362 | /** @var TestPlace $testPlaceWithDistance */ 363 | $testPlaceWithDistance = TestPlace::withDistance('test_places.point', 'test_places.point')->firstOrFail(); 364 | 365 | expect($testPlaceWithDistance->distance)->toBe(0.0) 366 | ->and($testPlaceWithDistance->name)->not()->toBeNull(); 367 | }); 368 | 369 | it('uses spatial function with expression', function (): void { 370 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'); 371 | TestPlace::factory()->create([ 372 | 'polygon' => $polygon, 373 | 'longitude' => 0, 374 | 'latitude' => 0, 375 | ]); 376 | 377 | /** @var TestPlace $testPlaceWithDistance */ 378 | $testPlaceWithDistance = TestPlace::whereWithin(DB::raw('POINT(longitude, latitude)'), DB::raw('polygon')) 379 | ->firstOrFail(); 380 | 381 | expect($testPlaceWithDistance)->not()->toBeNull(); 382 | }); 383 | 384 | it('toSpatialExpressionString can handle a Expression input', function (): void { 385 | $model = new TestPlace(); 386 | $method = (new ReflectionClass(TestPlace::class))->getMethod('toSpatialExpressionString'); 387 | 388 | $result = $method->invoke($model, $model->newQuery(), DB::raw('POINT(longitude, latitude)')); 389 | 390 | expect($result)->toBe('POINT(longitude, latitude)'); 391 | }); 392 | 393 | it('toSpatialExpressionString can handle a Geometry input', function (): void { 394 | $model = new TestPlace(); 395 | $method = (new ReflectionClass(TestPlace::class))->getMethod('toSpatialExpressionString'); 396 | $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'); 397 | 398 | $result = $method->invoke($model, $model->newQuery(), $polygon); 399 | 400 | $grammar = $model->newQuery()->getGrammar(); 401 | $connection = $model->newQuery()->getConnection(); 402 | $sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar); 403 | expect($result)->toBe($sqlSerializedPolygon); 404 | }); 405 | 406 | it('toSpatialExpressionString can handle a string input', function (): void { 407 | $model = new TestPlace(); 408 | $method = (new ReflectionClass(TestPlace::class))->getMethod('toSpatialExpressionString'); 409 | 410 | $result = $method->invoke($model, $model->newQuery(), 'test_places.point'); 411 | 412 | expect($result)->toBe('`test_places`.`point`'); 413 | }); 414 | -------------------------------------------------------------------------------- /tests/Geometry/GeometryCollectionTest.php: -------------------------------------------------------------------------------- 1 | create(['geometry_collection' => $geometryCollection]); 30 | 31 | expect($testPlace->geometry_collection)->toBeInstanceOf(GeometryCollection::class); 32 | expect($testPlace->geometry_collection)->toEqual($geometryCollection); 33 | }); 34 | 35 | it('creates a model record with geometry collection with SRID', function (): void { 36 | $geometryCollection = new GeometryCollection([ 37 | new Polygon([ 38 | new LineString([ 39 | new Point(0, 180), 40 | new Point(1, 179), 41 | new Point(2, 178), 42 | new Point(3, 177), 43 | new Point(0, 180), 44 | ]), 45 | ]), 46 | new Point(0, 180), 47 | ], Srid::WGS84->value); 48 | 49 | /** @var TestPlace $testPlace */ 50 | $testPlace = TestPlace::factory()->create(['geometry_collection' => $geometryCollection]); 51 | 52 | expect($testPlace->geometry_collection->srid)->toBe(Srid::WGS84->value); 53 | }); 54 | 55 | it('creates geometry collection from JSON', function (): void { 56 | $geometryCollection = new GeometryCollection([ 57 | new Polygon([ 58 | new LineString([ 59 | new Point(0, 180), 60 | new Point(1, 179), 61 | new Point(2, 178), 62 | new Point(3, 177), 63 | new Point(0, 180), 64 | ]), 65 | ]), 66 | new Point(0, 180), 67 | ]); 68 | 69 | $geometryCollectionFromJson = GeometryCollection::fromJson('{"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]},{"type":"Point","coordinates":[180,0]}]}'); 70 | 71 | expect($geometryCollectionFromJson)->toEqual($geometryCollection); 72 | }); 73 | 74 | it('creates geometry collection with SRID from JSON', function (): void { 75 | $geometryCollection = new GeometryCollection([ 76 | new Polygon([ 77 | new LineString([ 78 | new Point(0, 180), 79 | new Point(1, 179), 80 | new Point(2, 178), 81 | new Point(3, 177), 82 | new Point(0, 180), 83 | ]), 84 | ]), 85 | new Point(0, 180), 86 | ], Srid::WGS84->value); 87 | 88 | $geometryCollectionFromJson = GeometryCollection::fromJson('{"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]},{"type":"Point","coordinates":[180,0]}]}', 89 | Srid::WGS84->value); 90 | 91 | expect($geometryCollectionFromJson)->toEqual($geometryCollection); 92 | }); 93 | 94 | it('creates geometry collection from feature collection JSON', function (): void { 95 | $geometryCollection = new GeometryCollection([ 96 | new Polygon([ 97 | new LineString([ 98 | new Point(0, 180), 99 | new Point(1, 179), 100 | new Point(2, 178), 101 | new Point(3, 177), 102 | new Point(0, 180), 103 | ]), 104 | ]), 105 | new Point(0, 180), 106 | ]); 107 | 108 | $geometryCollectionFromFeatureCollectionJson = GeometryCollection::fromJson('{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}},{"type":"Feature","properties":[],"geometry":{"type":"Point","coordinates":[180,0]}}]}'); 109 | 110 | expect($geometryCollectionFromFeatureCollectionJson)->toEqual($geometryCollection); 111 | }); 112 | 113 | it('generates geometry collection JSON', function (): void { 114 | $geometryCollection = new GeometryCollection([ 115 | new Polygon([ 116 | new LineString([ 117 | new Point(0, 180), 118 | new Point(1, 179), 119 | new Point(2, 178), 120 | new Point(3, 177), 121 | new Point(0, 180), 122 | ]), 123 | ]), 124 | new Point(0, 180), 125 | ]); 126 | 127 | $json = $geometryCollection->toJson(); 128 | 129 | $expectedJson = '{"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]},{"type":"Point","coordinates":[180,0]}]}'; 130 | expect($json)->toBe($expectedJson); 131 | }); 132 | 133 | it('generates geometry collection feature collection JSON', function (): void { 134 | $geometryCollection = new GeometryCollection([ 135 | new Polygon([ 136 | new LineString([ 137 | new Point(0, 180), 138 | new Point(1, 179), 139 | new Point(2, 178), 140 | new Point(3, 177), 141 | new Point(0, 180), 142 | ]), 143 | ]), 144 | new Point(0, 180), 145 | ]); 146 | 147 | $featureCollectionJson = $geometryCollection->toFeatureCollectionJson(); 148 | 149 | $expectedFeatureCollectionJson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}},{"type":"Feature","properties":[],"geometry":{"type":"Point","coordinates":[180,0]}}]}'; 150 | expect($featureCollectionJson)->toBe($expectedFeatureCollectionJson); 151 | }); 152 | 153 | it('creates geometry collection from WKT', function (): void { 154 | $geometryCollection = new GeometryCollection([ 155 | new Polygon([ 156 | new LineString([ 157 | new Point(0, 180), 158 | new Point(1, 179), 159 | new Point(2, 178), 160 | new Point(3, 177), 161 | new Point(0, 180), 162 | ]), 163 | ]), 164 | new Point(0, 180), 165 | ]); 166 | 167 | $geometryCollectionFromWkt = GeometryCollection::fromWkt('GEOMETRYCOLLECTION(POLYGON((180 0, 179 1, 178 2, 177 3, 180 0)), POINT(180 0))'); 168 | 169 | expect($geometryCollectionFromWkt)->toEqual($geometryCollection); 170 | }); 171 | 172 | it('creates geometry collection with SRID from WKT', function (): void { 173 | $geometryCollection = new GeometryCollection([ 174 | new Polygon([ 175 | new LineString([ 176 | new Point(0, 180), 177 | new Point(1, 179), 178 | new Point(2, 178), 179 | new Point(3, 177), 180 | new Point(0, 180), 181 | ]), 182 | ]), 183 | new Point(0, 180), 184 | ], Srid::WGS84->value); 185 | 186 | $geometryCollectionFromWkt = GeometryCollection::fromWkt('GEOMETRYCOLLECTION(POLYGON((180 0, 179 1, 178 2, 177 3, 180 0)), POINT(180 0))', 187 | Srid::WGS84->value); 188 | 189 | expect($geometryCollectionFromWkt)->toEqual($geometryCollection); 190 | }); 191 | 192 | it('generates geometry collection WKT', function (): void { 193 | $geometryCollection = new GeometryCollection([ 194 | new Polygon([ 195 | new LineString([ 196 | new Point(0, 180), 197 | new Point(1, 179), 198 | new Point(2, 178), 199 | new Point(3, 177), 200 | new Point(0, 180), 201 | ]), 202 | ]), 203 | new Point(0, 180), 204 | ]); 205 | 206 | $wkt = $geometryCollection->toWkt(); 207 | 208 | $expectedWkt = 'GEOMETRYCOLLECTION(POLYGON((180 0, 179 1, 178 2, 177 3, 180 0)), POINT(180 0))'; 209 | expect($wkt)->toBe($expectedWkt); 210 | }); 211 | 212 | it('creates geometry collection from WKB', function (): void { 213 | $geometryCollection = new GeometryCollection([ 214 | new Polygon([ 215 | new LineString([ 216 | new Point(0, 180), 217 | new Point(1, 179), 218 | new Point(2, 178), 219 | new Point(3, 177), 220 | new Point(0, 180), 221 | ]), 222 | ]), 223 | new Point(0, 180), 224 | ]); 225 | 226 | $geometryCollectionFromWkb = GeometryCollection::fromWkb($geometryCollection->toWkb()); 227 | 228 | expect($geometryCollectionFromWkb)->toEqual($geometryCollection); 229 | }); 230 | 231 | it('creates geometry collection with SRID from WKB', function (): void { 232 | $geometryCollection = new GeometryCollection([ 233 | new Polygon([ 234 | new LineString([ 235 | new Point(0, 180), 236 | new Point(1, 179), 237 | new Point(2, 178), 238 | new Point(3, 177), 239 | new Point(0, 180), 240 | ]), 241 | ]), 242 | new Point(0, 180), 243 | ], Srid::WGS84->value); 244 | 245 | $geometryCollectionFromWkb = GeometryCollection::fromWkb($geometryCollection->toWkb()); 246 | 247 | expect($geometryCollectionFromWkb)->toEqual($geometryCollection); 248 | }); 249 | 250 | it('does not throw exception when geometry collection has no geometries', function (): void { 251 | $geometryCollection = new GeometryCollection([]); 252 | 253 | expect($geometryCollection->getGeometries())->toHaveCount(0); 254 | }); 255 | 256 | it('unsets geometry collection item', function (): void { 257 | $point = new Point(0, 180); 258 | $geometryCollection = new GeometryCollection([ 259 | new Polygon([ 260 | new LineString([ 261 | new Point(0, 180), 262 | new Point(1, 179), 263 | new Point(2, 178), 264 | new Point(3, 177), 265 | new Point(0, 180), 266 | ]), 267 | ]), 268 | $point, 269 | ]); 270 | 271 | unset($geometryCollection[0]); 272 | 273 | expect($geometryCollection[0])->toBe($point); 274 | expect($geometryCollection->getGeometries())->toHaveCount(1); 275 | }); 276 | 277 | it('throws exception when unsetting geometry collection item below minimum', function (): void { 278 | $polygon = new Polygon([ 279 | new LineString([ 280 | new Point(0, 180), 281 | new Point(1, 179), 282 | new Point(2, 178), 283 | new Point(3, 177), 284 | new Point(0, 180), 285 | ]), 286 | ]); 287 | 288 | expect(function () use ($polygon): void { 289 | unset($polygon[0]); 290 | })->toThrow(LaravelSpatialException::class); 291 | }); 292 | 293 | it('checks if geometry collection item is exists', function (): void { 294 | $geometryCollection = new GeometryCollection([ 295 | new Polygon([ 296 | new LineString([ 297 | new Point(0, 180), 298 | new Point(1, 179), 299 | new Point(2, 178), 300 | new Point(3, 177), 301 | new Point(0, 180), 302 | ]), 303 | ]), 304 | new Point(0, 180), 305 | ]); 306 | 307 | $firstItemExists = isset($geometryCollection[0]); 308 | $secondItemExists = isset($geometryCollection[1]); 309 | $thirdItemExists = isset($geometryCollection[2]); 310 | 311 | expect($firstItemExists)->toBeTrue(); 312 | expect($secondItemExists)->toBeTrue(); 313 | expect($thirdItemExists)->toBeFalse(); 314 | }); 315 | 316 | it('sets item to geometry collection', function (): void { 317 | $geometryCollection = new GeometryCollection([ 318 | new Polygon([ 319 | new LineString([ 320 | new Point(0, 180), 321 | new Point(1, 179), 322 | new Point(2, 178), 323 | new Point(3, 177), 324 | new Point(0, 180), 325 | ]), 326 | ]), 327 | new Point(0, 180), 328 | ]); 329 | $lineString = new LineString([ 330 | new Point(0, 180), 331 | new Point(1, 179), 332 | ]); 333 | 334 | $geometryCollection[2] = $lineString; 335 | 336 | expect($geometryCollection[2])->toBe($lineString); 337 | }); 338 | 339 | it('throws exception when setting invalid item to geometry collection', function (): void { 340 | $polygon = new Polygon([ 341 | new LineString([ 342 | new Point(0, 180), 343 | new Point(1, 179), 344 | new Point(2, 178), 345 | new Point(3, 177), 346 | new Point(0, 180), 347 | ]), 348 | ]); 349 | 350 | expect(function () use ($polygon): void { 351 | // @phpstan-ignore-next-line 352 | $polygon[1] = new Point(0, 180); 353 | })->toThrow(LaravelSpatialException::class); 354 | }); 355 | 356 | it('casts a GeometryCollection to a string', function (): void { 357 | $geometryCollection = new GeometryCollection([ 358 | new Polygon([ 359 | new LineString([ 360 | new Point(0, 180), 361 | new Point(1, 179), 362 | new Point(2, 178), 363 | new Point(3, 177), 364 | new Point(0, 180), 365 | ]), 366 | ]), 367 | new Point(0, 180), 368 | ]); 369 | 370 | expect($geometryCollection->__toString())->toEqual('GEOMETRYCOLLECTION(POLYGON((180 0, 179 1, 178 2, 177 3, 180 0)), POINT(180 0))'); 371 | }); 372 | 373 | it('adds a macro toGeometryCollection', function (): void { 374 | Geometry::macro('getName', function (): string { 375 | /** @var Geometry $this */ 376 | // @phpstan-ignore-next-line 377 | return class_basename($this); 378 | }); 379 | 380 | $geometryCollection = new GeometryCollection([ 381 | new Polygon([ 382 | new LineString([ 383 | new Point(0, 180), 384 | new Point(1, 179), 385 | new Point(2, 178), 386 | new Point(3, 177), 387 | new Point(0, 180), 388 | ]), 389 | ]), 390 | new Point(0, 180), 391 | ]); 392 | 393 | // @phpstan-ignore-next-line 394 | expect($geometryCollection->getName())->toBe('GeometryCollection'); 395 | }); 396 | -------------------------------------------------------------------------------- /tests/Geometry/GeometryTest.php: -------------------------------------------------------------------------------- 1 | toWkb(); 15 | 16 | LineString::fromWkb($pointWkb); 17 | })->toThrow(LaravelSpatialException::class); 18 | }); 19 | 20 | it('throws exception when generating geometry with invalid latitude', function (): void { 21 | expect(function (): void { 22 | $point = (new Point(91, 0, Srid::WGS84->value)); 23 | TestPlace::factory()->create(['point' => $point]); 24 | })->toThrow(QueryException::class); 25 | })->skip(fn () => ! isSupportAxisOrder()); 26 | 27 | it('throws exception when generating geometry with invalid latitude - without axis-order', function (): void { 28 | expect(function (): void { 29 | $point = (new Point(91, 0, Srid::WGS84->value)); 30 | TestPlace::factory()->create(['point' => $point]); 31 | 32 | TestPlace::withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value))->firstOrFail(); 33 | })->toThrow(QueryException::class); 34 | })->skip(fn () => isSupportAxisOrder()); 35 | 36 | it('throws exception when generating geometry with invalid longitude', function (): void { 37 | expect(function (): void { 38 | $point = (new Point(0, 181, Srid::WGS84->value)); 39 | TestPlace::factory()->create(['point' => $point]); 40 | })->toThrow(QueryException::class); 41 | })->skip(fn () => ! isSupportAxisOrder()); 42 | 43 | it('throws exception when generating geometry with invalid longitude - without axis-order', function (): void { 44 | expect(function (): void { 45 | $point = (new Point(0, 181, Srid::WGS84->value)); 46 | TestPlace::factory()->create(['point' => $point]); 47 | 48 | TestPlace::withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value))->firstOrFail(); 49 | })->toThrow(QueryException::class); 50 | })->skip(fn () => isSupportAxisOrder()); 51 | 52 | it('throws exception when generating geometry from other geometry WKT', function (): void { 53 | expect(function (): void { 54 | $pointWkt = 'POINT(180 0)'; 55 | 56 | LineString::fromWkt($pointWkt); 57 | })->toThrow(LaravelSpatialException::class); 58 | }); 59 | 60 | it('throws exception when generating geometry from non-JSON', function (): void { 61 | expect(function (): void { 62 | Point::fromJson('invalid-value'); 63 | })->toThrow(LaravelSpatialException::class); 64 | }); 65 | 66 | it('throws exception when generating geometry from empty JSON', function (): void { 67 | expect(function (): void { 68 | Point::fromJson('{}'); 69 | })->toThrow(LaravelSpatialException::class); 70 | }); 71 | 72 | it('throws exception when generating geometry from other geometry JSON', function (): void { 73 | expect(function (): void { 74 | $pointJson = '{"type":"Point","coordinates":[0,180]}'; 75 | 76 | LineString::fromJson($pointJson); 77 | })->toThrow(LaravelSpatialException::class); 78 | }); 79 | 80 | it('creates an SQL expression from a geometry', function (): void { 81 | $point = new Point(0, 180, Srid::WGS84->value); 82 | 83 | $expression = $point->toSqlExpression(DB::connection()); 84 | 85 | $grammar = DB::getQueryGrammar(); 86 | $expressionValue = $expression->getValue($grammar); 87 | expect($expressionValue)->toEqual("ST_GeomFromText('POINT(180 0)', 4326, 'axis-order=long-lat')"); 88 | })->skip(fn () => ! isSupportAxisOrder()); 89 | 90 | it('creates an SQL expression from a geometry - without axis-order', function (): void { 91 | $point = new Point(0, 180, Srid::WGS84->value); 92 | 93 | $expression = $point->toSqlExpression(DB::connection()); 94 | 95 | $grammar = DB::getQueryGrammar(); 96 | $expressionValue = $expression->getValue($grammar); 97 | expect($expressionValue)->toEqual("ST_GeomFromText('POINT(180 0)', 4326)"); 98 | })->skip(fn () => isSupportAxisOrder()); 99 | 100 | it('creates a geometry object from a geo json array', function (): void { 101 | $point = new Point(0, 180); 102 | $pointGeoJsonArray = $point->toArray(); 103 | 104 | $geometryCollectionFromArray = Point::fromArray($pointGeoJsonArray); 105 | 106 | expect($geometryCollectionFromArray)->toEqual($point); 107 | }); 108 | 109 | it('throws exception when creating a geometry object from an invalid geo json array', function (): void { 110 | $invalidPointGeoJsonArray = [ 111 | 'type' => 'InvalidGeometryType', 112 | 'coordinates' => [0, 180], 113 | ]; 114 | 115 | expect(function () use ($invalidPointGeoJsonArray): void { 116 | Geometry::fromArray($invalidPointGeoJsonArray); 117 | })->toThrow(LaravelSpatialException::class); 118 | }); 119 | 120 | it('throws exception when creating a geometry object from another geometry geo json array', function (): void { 121 | $pointGeoJsonArray = [ 122 | 'type' => 'Point', 123 | 'coordinates' => [0, 180], 124 | ]; 125 | 126 | expect(function () use ($pointGeoJsonArray): void { 127 | LineString::fromArray($pointGeoJsonArray); 128 | })->toThrow(LaravelSpatialException::class); 129 | }); 130 | -------------------------------------------------------------------------------- /tests/Geometry/LineStringTest.php: -------------------------------------------------------------------------------- 1 | create(['line_string' => $lineString]); 21 | 22 | expect($testPlace->line_string)->toBeInstanceOf(LineString::class); 23 | expect($testPlace->line_string)->toEqual($lineString); 24 | }); 25 | 26 | it('creates a model record with line string with SRID', function (): void { 27 | $lineString = new LineString([ 28 | new Point(0, 180), 29 | new Point(1, 179), 30 | ], Srid::WGS84->value); 31 | 32 | /** @var TestPlace $testPlace */ 33 | $testPlace = TestPlace::factory()->create(['line_string' => $lineString]); 34 | 35 | expect($testPlace->line_string->srid)->toBe(Srid::WGS84->value); 36 | }); 37 | 38 | it('creates line string from JSON', function (): void { 39 | $lineString = new LineString([ 40 | new Point(0, 180), 41 | new Point(1, 179), 42 | ]); 43 | 44 | $lineStringFromJson = LineString::fromJson('{"type":"LineString","coordinates":[[180,0],[179,1]]}'); 45 | 46 | expect($lineStringFromJson)->toEqual($lineString); 47 | }); 48 | 49 | it('creates line string with SRID from JSON', function (): void { 50 | $lineString = new LineString([ 51 | new Point(0, 180), 52 | new Point(1, 179), 53 | ], Srid::WGS84->value); 54 | 55 | $lineStringFromJson = LineString::fromJson('{"type":"LineString","coordinates":[[180,0],[179,1]]}', 56 | Srid::WGS84->value); 57 | 58 | expect($lineStringFromJson)->toEqual($lineString); 59 | }); 60 | 61 | it('generates line string JSON', function (): void { 62 | $lineString = new LineString([ 63 | new Point(0, 180), 64 | new Point(1, 179), 65 | ]); 66 | 67 | $json = $lineString->toJson(); 68 | 69 | $expectedJson = '{"type":"LineString","coordinates":[[180,0],[179,1]]}'; 70 | expect($json)->toBe($expectedJson); 71 | }); 72 | 73 | it('generates line string feature collection JSON', function (): void { 74 | $lineString = new LineString([ 75 | new Point(0, 180), 76 | new Point(1, 179), 77 | ]); 78 | 79 | $featureCollectionJson = $lineString->toFeatureCollectionJson(); 80 | 81 | $expectedFeatureCollectionJson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"LineString","coordinates":[[180,0],[179,1]]}}]}'; 82 | expect($featureCollectionJson)->toBe($expectedFeatureCollectionJson); 83 | }); 84 | 85 | it('creates line string from WKT', function (): void { 86 | $lineString = new LineString([ 87 | new Point(0, 180), 88 | new Point(1, 179), 89 | ]); 90 | 91 | $lineStringFromWkt = LineString::fromWkt('LINESTRING(180 0, 179 1)'); 92 | 93 | expect($lineStringFromWkt)->toEqual($lineString); 94 | }); 95 | 96 | it('creates line string with SRID from WKT', function (): void { 97 | $lineString = new LineString([ 98 | new Point(0, 180), 99 | new Point(1, 179), 100 | ], Srid::WGS84->value); 101 | 102 | $lineStringFromWkt = LineString::fromWkt('LINESTRING(180 0, 179 1)', Srid::WGS84->value); 103 | 104 | expect($lineStringFromWkt)->toEqual($lineString); 105 | }); 106 | 107 | it('generates line string WKT', function (): void { 108 | $lineString = new LineString([ 109 | new Point(0, 180), 110 | new Point(1, 179), 111 | ]); 112 | 113 | $wkt = $lineString->toWkt(); 114 | 115 | $expectedWkt = 'LINESTRING(180 0, 179 1)'; 116 | expect($wkt)->toBe($expectedWkt); 117 | }); 118 | 119 | it('creates line string from WKB', function (): void { 120 | $lineString = new LineString([ 121 | new Point(0, 180), 122 | new Point(1, 179), 123 | ]); 124 | 125 | $lineStringFromWkb = LineString::fromWkb($lineString->toWkb()); 126 | 127 | expect($lineStringFromWkb)->toEqual($lineString); 128 | }); 129 | 130 | it('creates line string with SRID from WKB', function (): void { 131 | $lineString = new LineString([ 132 | new Point(0, 180), 133 | new Point(1, 179), 134 | ], Srid::WGS84->value); 135 | 136 | $lineStringFromWkb = LineString::fromWkb($lineString->toWkb()); 137 | 138 | expect($lineStringFromWkb)->toEqual($lineString); 139 | }); 140 | 141 | it('throws exception when line string has less than two points', function (): void { 142 | expect(function (): void { 143 | new LineString([ 144 | new Point(0, 180), 145 | ]); 146 | })->toThrow(LaravelSpatialException::class); 147 | }); 148 | 149 | it('throws exception when creating line string from incorrect geometry', function (): void { 150 | expect(function (): void { 151 | // @phpstan-ignore-next-line 152 | new LineString([ 153 | Polygon::fromJson('{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}'), 154 | ]); 155 | })->toThrow(LaravelSpatialException::class); 156 | }); 157 | 158 | it('casts a LineString to a string', function (): void { 159 | $lineString = new LineString([ 160 | new Point(0, 180), 161 | new Point(1, 179), 162 | ]); 163 | 164 | expect($lineString->__toString())->toEqual('LINESTRING(180 0, 179 1)'); 165 | }); 166 | 167 | it('adds a macro toLineString', function (): void { 168 | Geometry::macro('getName', function (): string { 169 | /** @var Geometry $this */ 170 | // @phpstan-ignore-next-line 171 | return class_basename($this); 172 | }); 173 | 174 | $lineString = new LineString([ 175 | new Point(0, 180), 176 | new Point(1, 179), 177 | ]); 178 | 179 | // @phpstan-ignore-next-line 180 | expect($lineString->getName())->toBe('LineString'); 181 | }); 182 | -------------------------------------------------------------------------------- /tests/Geometry/MultiLineStringTest.php: -------------------------------------------------------------------------------- 1 | create(['multi_line_string' => $multiLineString]); 23 | 24 | expect($testPlace->multi_line_string)->toBeInstanceOf(MultiLineString::class); 25 | expect($testPlace->multi_line_string)->toEqual($multiLineString); 26 | }); 27 | 28 | it('creates a model record with multi line string with SRID', function (): void { 29 | $multiLineString = new MultiLineString([ 30 | new LineString([ 31 | new Point(0, 180), 32 | new Point(1, 179), 33 | ]), 34 | ], Srid::WGS84->value); 35 | 36 | /** @var TestPlace $testPlace */ 37 | $testPlace = TestPlace::factory()->create(['multi_line_string' => $multiLineString]); 38 | 39 | expect($testPlace->multi_line_string->srid)->toBe(Srid::WGS84->value); 40 | }); 41 | 42 | it('creates multi line string from JSON', function (): void { 43 | $multiLineString = new MultiLineString([ 44 | new LineString([ 45 | new Point(0, 180), 46 | new Point(1, 179), 47 | ]), 48 | ]); 49 | 50 | $multiLineStringFromJson = MultiLineString::fromJson('{"type":"MultiLineString","coordinates":[[[180,0],[179,1]]]}'); 51 | 52 | expect($multiLineStringFromJson)->toEqual($multiLineString); 53 | }); 54 | 55 | it('creates multi line string with SRID from JSON', function (): void { 56 | $multiLineString = new MultiLineString([ 57 | new LineString([ 58 | new Point(0, 180), 59 | new Point(1, 179), 60 | ]), 61 | ], Srid::WGS84->value); 62 | 63 | $multiLineStringFromJson = MultiLineString::fromJson('{"type":"MultiLineString","coordinates":[[[180,0],[179,1]]]}', 64 | Srid::WGS84->value); 65 | 66 | expect($multiLineStringFromJson)->toEqual($multiLineString); 67 | }); 68 | 69 | it('generates multi line string JSON', function (): void { 70 | $multiLineString = new MultiLineString([ 71 | new LineString([ 72 | new Point(0, 180), 73 | new Point(1, 179), 74 | ]), 75 | ]); 76 | 77 | $json = $multiLineString->toJson(); 78 | 79 | $expectedJson = '{"type":"MultiLineString","coordinates":[[[180,0],[179,1]]]}'; 80 | expect($json)->toBe($expectedJson); 81 | }); 82 | 83 | it('generates multi line string feature collection JSON', function (): void { 84 | $multiLineString = new MultiLineString([ 85 | new LineString([ 86 | new Point(0, 180), 87 | new Point(1, 179), 88 | ]), 89 | ]); 90 | 91 | $featureCollectionJson = $multiLineString->toFeatureCollectionJson(); 92 | 93 | $expectedFeatureCollectionJson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"MultiLineString","coordinates":[[[180,0],[179,1]]]}}]}'; 94 | expect($featureCollectionJson)->toBe($expectedFeatureCollectionJson); 95 | }); 96 | 97 | it('creates multi line string from WKT', function (): void { 98 | $multiLineString = new MultiLineString([ 99 | new LineString([ 100 | new Point(0, 180), 101 | new Point(1, 179), 102 | ]), 103 | ]); 104 | 105 | $multiLineStringFromWkt = MultiLineString::fromWkt('MULTILINESTRING((180 0, 179 1))'); 106 | 107 | expect($multiLineStringFromWkt)->toEqual($multiLineString); 108 | }); 109 | 110 | it('creates multi line string with SRID from WKT', function (): void { 111 | $multiLineString = new MultiLineString([ 112 | new LineString([ 113 | new Point(0, 180), 114 | new Point(1, 179), 115 | ]), 116 | ], Srid::WGS84->value); 117 | 118 | $multiLineStringFromWkt = MultiLineString::fromWkt('MULTILINESTRING((180 0, 179 1))', Srid::WGS84->value); 119 | 120 | expect($multiLineStringFromWkt)->toEqual($multiLineString); 121 | }); 122 | 123 | it('generates multi line string WKT', function (): void { 124 | $multiLineString = new MultiLineString([ 125 | new LineString([ 126 | new Point(0, 180), 127 | new Point(1, 179), 128 | ]), 129 | ]); 130 | 131 | $wkt = $multiLineString->toWkt(); 132 | 133 | $expectedWkt = 'MULTILINESTRING((180 0, 179 1))'; 134 | expect($wkt)->toBe($expectedWkt); 135 | }); 136 | 137 | it('creates multi line string from WKB', function (): void { 138 | $multiLineString = new MultiLineString([ 139 | new LineString([ 140 | new Point(0, 180), 141 | new Point(1, 179), 142 | ]), 143 | ]); 144 | 145 | $multiLineStringFromWkb = MultiLineString::fromWkb($multiLineString->toWkb()); 146 | 147 | expect($multiLineStringFromWkb)->toEqual($multiLineString); 148 | }); 149 | 150 | it('creates multi line string with SRID from WKB', function (): void { 151 | $multiLineString = new MultiLineString([ 152 | new LineString([ 153 | new Point(0, 180), 154 | new Point(1, 179), 155 | ]), 156 | ], Srid::WGS84->value); 157 | 158 | $multiLineStringFromWkb = MultiLineString::fromWkb($multiLineString->toWkb()); 159 | 160 | expect($multiLineStringFromWkb)->toEqual($multiLineString); 161 | }); 162 | 163 | it('throws exception when multi line string has no line strings', function (): void { 164 | expect(function (): void { 165 | new MultiLineString([]); 166 | })->toThrow(LaravelSpatialException::class); 167 | }); 168 | 169 | it('throws exception when creating multi line string from incorrect geometry', function (): void { 170 | expect(function (): void { 171 | // @phpstan-ignore-next-line 172 | new MultiLineString([ 173 | new Point(0, 0), 174 | ]); 175 | })->toThrow(LaravelSpatialException::class); 176 | }); 177 | 178 | it('casts a MultiLineString to a string', function (): void { 179 | $multiLineString = new MultiLineString([ 180 | new LineString([ 181 | new Point(0, 180), 182 | new Point(1, 179), 183 | ]), 184 | ]); 185 | 186 | expect($multiLineString->__toString())->toEqual('MULTILINESTRING((180 0, 179 1))'); 187 | }); 188 | 189 | it('adds a macro toMultiLineString', function (): void { 190 | Geometry::macro('getName', function (): string { 191 | /** @var Geometry $this */ 192 | // @phpstan-ignore-next-line 193 | return class_basename($this); 194 | }); 195 | 196 | $multiLineString = new MultiLineString([ 197 | new LineString([ 198 | new Point(0, 180), 199 | new Point(1, 179), 200 | ]), 201 | ]); 202 | 203 | // @phpstan-ignore-next-line 204 | expect($multiLineString->getName())->toBe('MultiLineString'); 205 | }); 206 | -------------------------------------------------------------------------------- /tests/Geometry/MultiPointTest.php: -------------------------------------------------------------------------------- 1 | create(['multi_point' => $multiPoint]); 20 | 21 | expect($testPlace->multi_point)->toBeInstanceOf(MultiPoint::class); 22 | expect($testPlace->multi_point)->toEqual($multiPoint); 23 | }); 24 | 25 | it('creates a model record with multi point with SRID', function (): void { 26 | $multiPoint = new MultiPoint([ 27 | new Point(0, 180), 28 | ], Srid::WGS84->value); 29 | 30 | /** @var TestPlace $testPlace */ 31 | $testPlace = TestPlace::factory()->create(['multi_point' => $multiPoint]); 32 | 33 | expect($testPlace->multi_point->srid)->toBe(Srid::WGS84->value); 34 | }); 35 | 36 | it('creates multi point from JSON', function (): void { 37 | $multiPoint = new MultiPoint([ 38 | new Point(0, 180), 39 | ]); 40 | 41 | $multiPointFromJson = MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[180,0]]}'); 42 | 43 | expect($multiPointFromJson)->toEqual($multiPoint); 44 | }); 45 | 46 | it('creates multi point with SRID from JSON', function (): void { 47 | $multiPoint = new MultiPoint([ 48 | new Point(0, 180), 49 | ], Srid::WGS84->value); 50 | 51 | $multiPointFromJson = MultiPoint::fromJson('{"type":"MultiPoint","coordinates":[[180,0]]}', Srid::WGS84->value); 52 | 53 | expect($multiPointFromJson)->toEqual($multiPoint); 54 | }); 55 | 56 | it('generates multi point JSON', function (): void { 57 | $multiPoint = new MultiPoint([ 58 | new Point(0, 180), 59 | ]); 60 | 61 | $json = $multiPoint->toJson(); 62 | 63 | $expectedJson = '{"type":"MultiPoint","coordinates":[[180,0]]}'; 64 | expect($json)->toBe($expectedJson); 65 | }); 66 | 67 | it('generates multi point feature collection JSON', function (): void { 68 | $multiPoint = new MultiPoint([ 69 | new Point(0, 180), 70 | ]); 71 | 72 | $multiPointFeatureCollectionJson = $multiPoint->toFeatureCollectionJson(); 73 | 74 | $expectedFeatureCollectionJson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"MultiPoint","coordinates":[[180,0]]}}]}'; 75 | expect($multiPointFeatureCollectionJson)->toBe($expectedFeatureCollectionJson); 76 | }); 77 | 78 | it('creates multi point from WKT', function (): void { 79 | $multiPoint = new MultiPoint([ 80 | new Point(0, 180), 81 | ]); 82 | 83 | $multiPointFromWkt = MultiPoint::fromWkt('MULTIPOINT(180 0)'); 84 | 85 | expect($multiPointFromWkt)->toEqual($multiPoint); 86 | }); 87 | 88 | it('creates multi point with SRID from WKT', function (): void { 89 | $multiPoint = new MultiPoint([ 90 | new Point(0, 180), 91 | ], Srid::WGS84->value); 92 | 93 | $multiPointFromWkt = MultiPoint::fromWkt('MULTIPOINT(180 0)', Srid::WGS84->value); 94 | 95 | expect($multiPointFromWkt)->toEqual($multiPoint); 96 | }); 97 | 98 | it('generates multi point WKT', function (): void { 99 | $multiPoint = new MultiPoint([ 100 | new Point(0, 180), 101 | ]); 102 | 103 | $wkt = $multiPoint->toWkt(); 104 | 105 | $expectedWkt = 'MULTIPOINT(180 0)'; 106 | expect($wkt)->toBe($expectedWkt); 107 | }); 108 | 109 | it('creates multi point from WKB', function (): void { 110 | $multiPoint = new MultiPoint([ 111 | new Point(0, 180), 112 | ]); 113 | 114 | $multiPointFromWkb = MultiPoint::fromWkb($multiPoint->toWkb()); 115 | 116 | expect($multiPointFromWkb)->toEqual($multiPoint); 117 | }); 118 | 119 | it('creates multi point with SRID from WKB', function (): void { 120 | $multiPoint = new MultiPoint([ 121 | new Point(0, 180), 122 | ], Srid::WGS84->value); 123 | 124 | $multiPointFromWkb = MultiPoint::fromWkb($multiPoint->toWkb()); 125 | 126 | expect($multiPointFromWkb)->toEqual($multiPoint); 127 | }); 128 | 129 | it('throws exception when multi point has no points', function (): void { 130 | expect(function (): void { 131 | new MultiPoint([]); 132 | })->toThrow(LaravelSpatialException::class); 133 | }); 134 | 135 | it('throws exception when creating multi point from incorrect geometry', function (): void { 136 | expect(function (): void { 137 | // @phpstan-ignore-next-line 138 | new MultiPoint([ 139 | Polygon::fromJson('{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}'), 140 | ]); 141 | })->toThrow(LaravelSpatialException::class); 142 | }); 143 | 144 | it('casts a MultiPoint to a string', function (): void { 145 | $multiPoint = new MultiPoint([ 146 | new Point(0, 180), 147 | ]); 148 | 149 | expect($multiPoint->__toString())->toEqual('MULTIPOINT(180 0)'); 150 | }); 151 | 152 | it('adds a macro toMultiPoint', function (): void { 153 | Geometry::macro('getName', function (): string { 154 | /** @var Geometry $this */ 155 | // @phpstan-ignore-next-line 156 | return class_basename($this); 157 | }); 158 | 159 | $multiPoint = new MultiPoint([ 160 | new Point(0, 180), 161 | ]); 162 | 163 | // @phpstan-ignore-next-line 164 | expect($multiPoint->getName())->toBe('MultiPoint'); 165 | }); 166 | -------------------------------------------------------------------------------- /tests/Geometry/MultiPolygonTest.php: -------------------------------------------------------------------------------- 1 | create(['multi_polygon' => $multiPolygon]); 29 | 30 | expect($testPlace->multi_polygon)->toBeInstanceOf(MultiPolygon::class); 31 | expect($testPlace->multi_polygon)->toEqual($multiPolygon); 32 | }); 33 | 34 | it('creates a model record with multi polygon with SRID', function (): void { 35 | $multiPolygon = new MultiPolygon([ 36 | new Polygon([ 37 | new LineString([ 38 | new Point(0, 180), 39 | new Point(1, 179), 40 | new Point(2, 178), 41 | new Point(3, 177), 42 | new Point(0, 180), 43 | ]), 44 | ]), 45 | ], Srid::WGS84->value); 46 | 47 | /** @var TestPlace $testPlace */ 48 | $testPlace = TestPlace::factory()->create(['multi_polygon' => $multiPolygon]); 49 | 50 | expect($testPlace->multi_polygon->srid)->toBe(Srid::WGS84->value); 51 | }); 52 | 53 | it('creates multi polygon from JSON', function (): void { 54 | $multiPolygon = new MultiPolygon([ 55 | new Polygon([ 56 | new LineString([ 57 | new Point(0, 180), 58 | new Point(1, 179), 59 | new Point(2, 178), 60 | new Point(3, 177), 61 | new Point(0, 180), 62 | ]), 63 | ]), 64 | ]); 65 | 66 | $multiPolygonFromJson = MultiPolygon::fromJson('{"type":"MultiPolygon","coordinates":[[[[180,0],[179,1],[178,2],[177,3],[180,0]]]]}'); 67 | 68 | expect($multiPolygonFromJson)->toEqual($multiPolygon); 69 | }); 70 | 71 | it('creates multi polygon with SRID from JSON', function (): void { 72 | $multiPolygon = new MultiPolygon([ 73 | new Polygon([ 74 | new LineString([ 75 | new Point(0, 180), 76 | new Point(1, 179), 77 | new Point(2, 178), 78 | new Point(3, 177), 79 | new Point(0, 180), 80 | ]), 81 | ]), 82 | ], Srid::WGS84->value); 83 | 84 | $multiPolygonFromJson = MultiPolygon::fromJson('{"type":"MultiPolygon","coordinates":[[[[180,0],[179,1],[178,2],[177,3],[180,0]]]]}', 85 | Srid::WGS84->value); 86 | 87 | expect($multiPolygonFromJson)->toEqual($multiPolygon); 88 | }); 89 | 90 | it('generates multi polygon JSON', function (): void { 91 | $multiPolygon = new MultiPolygon([ 92 | new Polygon([ 93 | new LineString([ 94 | new Point(0, 180), 95 | new Point(1, 179), 96 | new Point(2, 178), 97 | new Point(3, 177), 98 | new Point(0, 180), 99 | ]), 100 | ]), 101 | ]); 102 | 103 | $json = $multiPolygon->toJson(); 104 | 105 | $expectedJson = '{"type":"MultiPolygon","coordinates":[[[[180,0],[179,1],[178,2],[177,3],[180,0]]]]}'; 106 | expect($json)->toBe($expectedJson); 107 | }); 108 | 109 | it('generates multi polygon feature collection JSON', function (): void { 110 | $multiPolygon = new MultiPolygon([ 111 | new Polygon([ 112 | new LineString([ 113 | new Point(0, 180), 114 | new Point(1, 179), 115 | new Point(2, 178), 116 | new Point(3, 177), 117 | new Point(0, 180), 118 | ]), 119 | ]), 120 | ]); 121 | 122 | $featureCollectionJson = $multiPolygon->toFeatureCollectionJson(); 123 | 124 | $expectedFeatureCollectionJson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"MultiPolygon","coordinates":[[[[180,0],[179,1],[178,2],[177,3],[180,0]]]]}}]}'; 125 | expect($featureCollectionJson)->toBe($expectedFeatureCollectionJson); 126 | }); 127 | 128 | it('creates multi polygon from WKT', function (): void { 129 | $multiPolygon = new MultiPolygon([ 130 | new Polygon([ 131 | new LineString([ 132 | new Point(0, 180), 133 | new Point(1, 179), 134 | new Point(2, 178), 135 | new Point(3, 177), 136 | new Point(0, 180), 137 | ]), 138 | ]), 139 | ]); 140 | 141 | $multiPolygonFromWkt = MultiPolygon::fromWkt('MULTIPOLYGON(((180 0, 179 1, 178 2, 177 3, 180 0)))'); 142 | 143 | expect($multiPolygonFromWkt)->toEqual($multiPolygon); 144 | }); 145 | 146 | it('creates multi polygon with SRID from WKT', function (): void { 147 | $multiPolygon = new MultiPolygon([ 148 | new Polygon([ 149 | new LineString([ 150 | new Point(0, 180), 151 | new Point(1, 179), 152 | new Point(2, 178), 153 | new Point(3, 177), 154 | new Point(0, 180), 155 | ]), 156 | ]), 157 | ], Srid::WGS84->value); 158 | 159 | $multiPolygonFromWkt = MultiPolygon::fromWkt('MULTIPOLYGON(((180 0, 179 1, 178 2, 177 3, 180 0)))', 160 | Srid::WGS84->value); 161 | 162 | expect($multiPolygonFromWkt)->toEqual($multiPolygon); 163 | }); 164 | 165 | it('generates multi polygon WKT', function (): void { 166 | $multiPolygon = new MultiPolygon([ 167 | new Polygon([ 168 | new LineString([ 169 | new Point(0, 180), 170 | new Point(1, 179), 171 | new Point(2, 178), 172 | new Point(3, 177), 173 | new Point(0, 180), 174 | ]), 175 | ]), 176 | ]); 177 | 178 | $wkt = $multiPolygon->toWkt(); 179 | 180 | $expectedWkt = 'MULTIPOLYGON(((180 0, 179 1, 178 2, 177 3, 180 0)))'; 181 | expect($wkt)->toBe($expectedWkt); 182 | }); 183 | 184 | it('creates multi polygon from WKB', function (): void { 185 | $multiPolygon = new MultiPolygon([ 186 | new Polygon([ 187 | new LineString([ 188 | new Point(0, 180), 189 | new Point(1, 179), 190 | new Point(2, 178), 191 | new Point(3, 177), 192 | new Point(0, 180), 193 | ]), 194 | ]), 195 | ]); 196 | 197 | $multiPolygonFromWkb = MultiPolygon::fromWkb($multiPolygon->toWkb()); 198 | 199 | expect($multiPolygonFromWkb)->toEqual($multiPolygon); 200 | }); 201 | 202 | it('creates multi polygon with SRID from WKB', function (): void { 203 | $multiPolygon = new MultiPolygon([ 204 | new Polygon([ 205 | new LineString([ 206 | new Point(0, 180), 207 | new Point(1, 179), 208 | new Point(2, 178), 209 | new Point(3, 177), 210 | new Point(0, 180), 211 | ]), 212 | ]), 213 | ], Srid::WGS84->value); 214 | 215 | $multiPolygonFromWkb = MultiPolygon::fromWkb($multiPolygon->toWkb()); 216 | 217 | expect($multiPolygonFromWkb)->toEqual($multiPolygon); 218 | }); 219 | 220 | it('throws exception when multi polygon has no polygons', function (): void { 221 | expect(function (): void { 222 | new MultiPolygon([]); 223 | })->toThrow(LaravelSpatialException::class); 224 | }); 225 | 226 | it('throws exception when creating multi polygon from incorrect geometry', function (): void { 227 | expect(function (): void { 228 | // @phpstan-ignore-next-line 229 | new MultiPolygon([ 230 | new Point(0, 0), 231 | ]); 232 | })->toThrow(LaravelSpatialException::class); 233 | }); 234 | 235 | it('casts a MultiPolygon to a string', function (): void { 236 | $multiPolygon = new MultiPolygon([ 237 | new Polygon([ 238 | new LineString([ 239 | new Point(0, 180), 240 | new Point(1, 179), 241 | new Point(2, 178), 242 | new Point(3, 177), 243 | new Point(0, 180), 244 | ]), 245 | ]), 246 | ]); 247 | 248 | expect($multiPolygon->__toString())->toEqual('MULTIPOLYGON(((180 0, 179 1, 178 2, 177 3, 180 0)))'); 249 | }); 250 | 251 | it('adds a macro toMultiPolygon', function (): void { 252 | Geometry::macro('getName', function (): string { 253 | /** @var Geometry $this */ 254 | // @phpstan-ignore-next-line 255 | return class_basename($this); 256 | }); 257 | 258 | $multiPolygon = new MultiPolygon([ 259 | new Polygon([ 260 | new LineString([ 261 | new Point(0, 180), 262 | new Point(1, 179), 263 | new Point(2, 178), 264 | new Point(3, 177), 265 | new Point(0, 180), 266 | ]), 267 | ]), 268 | ]); 269 | 270 | // @phpstan-ignore-next-line 271 | expect($multiPolygon->getName())->toBe('MultiPolygon'); 272 | }); 273 | -------------------------------------------------------------------------------- /tests/Geometry/PointTest.php: -------------------------------------------------------------------------------- 1 | create(['point' => $point]); 16 | 17 | expect($testPlace->point)->toBeInstanceOf(Point::class); 18 | expect($testPlace->point)->toEqual($point); 19 | }); 20 | 21 | it('creates a model record with point with SRID', function (): void { 22 | $point = new Point(0, 180, Srid::WGS84->value); 23 | 24 | /** @var TestPlace $testPlace */ 25 | $testPlace = TestPlace::factory()->create(['point' => $point]); 26 | 27 | expect($testPlace->point->srid)->toBe(Srid::WGS84->value); 28 | }); 29 | 30 | it('creates point from JSON', function (): void { 31 | $point = new Point(0, 180); 32 | 33 | $pointFromJson = Point::fromJson('{"type":"Point","coordinates":[180,0]}'); 34 | 35 | expect($pointFromJson)->toEqual($point); 36 | }); 37 | 38 | it('creates point with SRID from JSON', function (): void { 39 | $point = new Point(0, 180, Srid::WGS84->value); 40 | 41 | $pointFromJson = Point::fromJson('{"type":"Point","coordinates":[180,0]}', Srid::WGS84->value); 42 | 43 | expect($pointFromJson)->toEqual($point); 44 | }); 45 | 46 | it('generates point JSON', function (): void { 47 | $point = new Point(0, 180); 48 | 49 | $json = $point->toJson(); 50 | 51 | $expectedJson = '{"type":"Point","coordinates":[180,0]}'; 52 | expect($json)->toBe($expectedJson); 53 | }); 54 | 55 | it('throws exception when creating point from invalid JSON', function (): void { 56 | expect(function (): void { 57 | Point::fromJson('{"type":"Point","coordinates":[]}'); 58 | })->toThrow(LaravelSpatialException::class); 59 | }); 60 | 61 | it('creates point from WKT', function (): void { 62 | $point = new Point(0, 180); 63 | 64 | $pointFromWkt = Point::fromWkt('POINT(180 0)'); 65 | 66 | expect($pointFromWkt)->toEqual($point); 67 | }); 68 | 69 | it('creates point with SRID from WKT', function (): void { 70 | $point = new Point(0, 180, Srid::WGS84->value); 71 | 72 | $pointFromWkt = Point::fromWkt('POINT(180 0)', Srid::WGS84->value); 73 | 74 | expect($pointFromWkt)->toEqual($point); 75 | }); 76 | 77 | it('generates point WKT', function (): void { 78 | $point = new Point(0, 180); 79 | 80 | $wkt = $point->toWkt(); 81 | 82 | $expectedWkt = 'POINT(180 0)'; 83 | expect($wkt)->toBe($expectedWkt); 84 | }); 85 | 86 | it('creates point from WKB', function (): void { 87 | $point = new Point(0, 180); 88 | 89 | $pointFromWkb = Point::fromWkb($point->toWkb()); 90 | 91 | expect($pointFromWkb)->toEqual($point); 92 | }); 93 | 94 | it('creates point with SRID from WKB', function (): void { 95 | $point = new Point(0, 180, Srid::WGS84->value); 96 | 97 | $pointFromWkb = Point::fromWkb($point->toWkb()); 98 | 99 | expect($pointFromWkb)->toEqual($point); 100 | }); 101 | 102 | it('casts a Point to a string', function (): void { 103 | $point = new Point(0, 180, Srid::WGS84->value); 104 | 105 | expect($point->__toString())->toEqual('POINT(180 0)'); 106 | }); 107 | 108 | it('adds a macro toPoint', function (): void { 109 | Geometry::macro('getName', function (): string { 110 | /** @var Geometry $this */ 111 | // @phpstan-ignore-next-line 112 | return class_basename($this); 113 | }); 114 | 115 | $point = new Point(0, 180, Srid::WGS84->value); 116 | 117 | // @phpstan-ignore-next-line 118 | expect($point->getName())->toBe('Point'); 119 | }); 120 | -------------------------------------------------------------------------------- /tests/Geometry/PolygonTest.php: -------------------------------------------------------------------------------- 1 | create(['polygon' => $polygon]); 26 | 27 | expect($testPlace->polygon)->toBeInstanceOf(Polygon::class); 28 | expect($testPlace->polygon)->toEqual($polygon); 29 | }); 30 | 31 | it('creates a model record with polygon with SRID', function (): void { 32 | $polygon = new Polygon([ 33 | new LineString([ 34 | new Point(0, 180), 35 | new Point(1, 179), 36 | new Point(2, 178), 37 | new Point(3, 177), 38 | new Point(0, 180), 39 | ]), 40 | ], Srid::WGS84->value); 41 | 42 | /** @var TestPlace $testPlace */ 43 | $testPlace = TestPlace::factory()->create(['polygon' => $polygon]); 44 | 45 | expect($testPlace->polygon->srid)->toBe(Srid::WGS84->value); 46 | }); 47 | 48 | it('creates polygon from JSON', function (): void { 49 | $polygon = new Polygon([ 50 | new LineString([ 51 | new Point(0, 180), 52 | new Point(1, 179), 53 | new Point(2, 178), 54 | new Point(3, 177), 55 | new Point(0, 180), 56 | ]), 57 | ]); 58 | 59 | $polygonFromJson = Polygon::fromJson('{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}'); 60 | 61 | expect($polygonFromJson)->toEqual($polygon); 62 | }); 63 | 64 | it('creates polygon with SRID from JSON', function (): void { 65 | $polygon = new Polygon([ 66 | new LineString([ 67 | new Point(0, 180), 68 | new Point(1, 179), 69 | new Point(2, 178), 70 | new Point(3, 177), 71 | new Point(0, 180), 72 | ]), 73 | ], Srid::WGS84->value); 74 | 75 | $polygonFromJson = Polygon::fromJson('{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}', 76 | Srid::WGS84->value); 77 | 78 | expect($polygonFromJson)->toEqual($polygon); 79 | }); 80 | 81 | it('generates polygon JSON', function (): void { 82 | $polygon = new Polygon([ 83 | new LineString([ 84 | new Point(0, 180), 85 | new Point(1, 179), 86 | new Point(2, 178), 87 | new Point(3, 177), 88 | new Point(0, 180), 89 | ]), 90 | ]); 91 | 92 | $json = $polygon->toJson(); 93 | 94 | $expectedJson = '{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}'; 95 | expect($json)->toBe($expectedJson); 96 | }); 97 | 98 | it('generates polygon feature collection JSON', function (): void { 99 | $polygon = new Polygon([ 100 | new LineString([ 101 | new Point(0, 180), 102 | new Point(1, 179), 103 | new Point(2, 178), 104 | new Point(3, 177), 105 | new Point(0, 180), 106 | ]), 107 | ]); 108 | 109 | $featureCollectionJson = $polygon->toFeatureCollectionJson(); 110 | 111 | $expectedFeatureCollectionJson = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":[],"geometry":{"type":"Polygon","coordinates":[[[180,0],[179,1],[178,2],[177,3],[180,0]]]}}]}'; 112 | expect($featureCollectionJson)->toBe($expectedFeatureCollectionJson); 113 | }); 114 | 115 | it('creates polygon from WKT', function (): void { 116 | $polygon = new Polygon([ 117 | new LineString([ 118 | new Point(0, 180), 119 | new Point(1, 179), 120 | new Point(2, 178), 121 | new Point(3, 177), 122 | new Point(0, 180), 123 | ]), 124 | ]); 125 | 126 | $polygonFromWkt = Polygon::fromWkt('POLYGON((180 0, 179 1, 178 2, 177 3, 180 0))'); 127 | 128 | expect($polygonFromWkt)->toEqual($polygon); 129 | }); 130 | 131 | it('creates polygon with SRID from WKT', function (): void { 132 | $polygon = new Polygon([ 133 | new LineString([ 134 | new Point(0, 180), 135 | new Point(1, 179), 136 | new Point(2, 178), 137 | new Point(3, 177), 138 | new Point(0, 180), 139 | ]), 140 | ], Srid::WGS84->value); 141 | 142 | $polygonFromWkt = Polygon::fromWkt('POLYGON((180 0, 179 1, 178 2, 177 3, 180 0))', Srid::WGS84->value); 143 | 144 | expect($polygonFromWkt)->toEqual($polygon); 145 | }); 146 | 147 | it('generates polygon WKT', function (): void { 148 | $polygon = new Polygon([ 149 | new LineString([ 150 | new Point(0, 180), 151 | new Point(1, 179), 152 | new Point(2, 178), 153 | new Point(3, 177), 154 | new Point(0, 180), 155 | ]), 156 | ]); 157 | 158 | $wkt = $polygon->toWkt(); 159 | 160 | $expectedWkt = 'POLYGON((180 0, 179 1, 178 2, 177 3, 180 0))'; 161 | expect($wkt)->toBe($expectedWkt); 162 | }); 163 | 164 | it('creates polygon from WKB', function (): void { 165 | $polygon = new Polygon([ 166 | new LineString([ 167 | new Point(0, 180), 168 | new Point(1, 179), 169 | new Point(2, 178), 170 | new Point(3, 177), 171 | new Point(0, 180), 172 | ]), 173 | ]); 174 | 175 | $polygonFromWkb = Polygon::fromWkb($polygon->toWkb()); 176 | 177 | expect($polygonFromWkb)->toEqual($polygon); 178 | }); 179 | 180 | it('creates polygon with SRID from WKB', function (): void { 181 | $polygon = new Polygon([ 182 | new LineString([ 183 | new Point(0, 180), 184 | new Point(1, 179), 185 | new Point(2, 178), 186 | new Point(3, 177), 187 | new Point(0, 180), 188 | ]), 189 | ], Srid::WGS84->value); 190 | 191 | $polygonFromWkb = Polygon::fromWkb($polygon->toWkb()); 192 | 193 | expect($polygonFromWkb)->toEqual($polygon); 194 | }); 195 | 196 | it('throws exception when polygon has no line strings', function (): void { 197 | expect(function (): void { 198 | new Polygon([]); 199 | })->toThrow(LaravelSpatialException::class); 200 | }); 201 | 202 | it('throws exception when creating polygon from incorrect geometry', function (): void { 203 | expect(function (): void { 204 | // @phpstan-ignore-next-line 205 | new Polygon([new Point(0, 0)]); 206 | })->toThrow(LaravelSpatialException::class); 207 | }); 208 | 209 | it('casts a Polygon to a string', function (): void { 210 | $polygon = new Polygon([ 211 | new LineString([ 212 | new Point(0, 180), 213 | new Point(1, 179), 214 | new Point(2, 178), 215 | new Point(3, 177), 216 | new Point(0, 180), 217 | ]), 218 | ]); 219 | 220 | expect($polygon->__toString())->toEqual('POLYGON((180 0, 179 1, 178 2, 177 3, 180 0))'); 221 | }); 222 | 223 | it('adds a macro toPolygon', function (): void { 224 | Geometry::macro('getName', function (): string { 225 | /** @var Geometry $this */ 226 | // @phpstan-ignore-next-line 227 | return class_basename($this); 228 | }); 229 | 230 | $polygon = new Polygon([ 231 | new LineString([ 232 | new Point(0, 180), 233 | new Point(1, 179), 234 | new Point(2, 178), 235 | new Point(3, 177), 236 | new Point(0, 180), 237 | ]), 238 | ]); 239 | 240 | // @phpstan-ignore-next-line 241 | expect($polygon->getName())->toBe('Polygon'); 242 | }); 243 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 9 | 10 | function isSupportAxisOrder(): bool 11 | { 12 | return (new Connection())->isSupportAxisOrder(DB::connection()); 13 | } 14 | 15 | /** 16 | * @return class-string 17 | */ 18 | function getDatabaseTruncationClass(): string 19 | { 20 | return DatabaseTruncation::class; 21 | } 22 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/Database/migrations'); 16 | } 17 | 18 | /** 19 | * @return class-string[] 20 | */ 21 | protected function getPackageProviders($app): array 22 | { 23 | return [ 24 | LaravelSpatialServiceProvider::class, 25 | ]; 26 | } 27 | } 28 | --------------------------------------------------------------------------------