├── .github └── workflows │ ├── php-cs-fixer.yml │ └── run-tests.yml ├── .gitignore ├── .php-cs-fixer.php ├── CLA.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── changelog.md ├── composer.json ├── docs ├── installation.md ├── supported-files.md ├── tests.md └── usage.md ├── phpunit.xml.dist ├── src ├── Auth │ ├── Authentication.php │ └── NullAuthentication.php ├── Contracts │ ├── Authentication.php │ ├── FileReader.php │ └── Model.php ├── Exception │ ├── DeserializationException.php │ ├── ErrorResponseException.php │ ├── GeoServerClientException.php │ ├── SerializationException.php │ ├── StoreNotFoundException.php │ ├── StyleAlreadyExistsException.php │ ├── StyleNotFoundException.php │ └── UnsupportedFileException.php ├── GeoFile.php ├── GeoFormat.php ├── GeoServer.php ├── GeoType.php ├── Http │ ├── InteractsWithHttp.php │ ├── ResponseHelper.php │ ├── Responses │ │ ├── CoverageResponse.php │ │ ├── CoverageStoreResponse.php │ │ ├── CoverageStoresResponse.php │ │ ├── DataStoreResponse.php │ │ ├── FeatureResponse.php │ │ ├── StylesResponse.php │ │ └── WorkspaceResponse.php │ └── Routes.php ├── Models │ ├── BoundingBox.php │ ├── Coverage.php │ ├── CoverageStore.php │ ├── DataStore.php │ ├── Feature.php │ ├── Resource.php │ ├── Store.php │ ├── Style.php │ └── Workspace.php ├── Options.php ├── Serializer │ ├── DeserializeBoundingBoxSubscriber.php │ ├── DeserializeCoverageStoreResponseSubscriber.php │ ├── DeserializeDataStoreResponseSubscriber.php │ └── DeserializeStyleResponseSubscriber.php ├── StyleFile.php └── Support │ ├── BinaryReader.php │ ├── ImageResponse.php │ ├── TextReader.php │ ├── TypeResolver.php │ ├── WmsOptions.php │ └── ZipReader.php └── tests ├── Concern ├── GeneratesData.php └── SetupIntegrationTest.php ├── Integration ├── GeoServerCoverageStoresTest.php ├── GeoServerDataStoresTest.php ├── GeoServerStylesTest.php ├── GeoServerVersionRetrievalTest.php ├── GeoServerWmsTest.php └── GeoServerWorkspaceTest.php ├── Support └── ImageDifference.php ├── TestCase.php ├── Unit ├── AuthenticationTest.php ├── GeoFileTest.php ├── GeoServerClientInstantiationTest.php ├── OptionsTest.php ├── ResponseHelperTest.php ├── StyleFileTest.php └── WmsOptionsTest.php ├── docker-compose.yml ├── fixtures ├── buildings.zip ├── empty.gpkg ├── geojson-in-plain-json.json ├── geojson.geojson ├── geotiff.tiff ├── geotiff_thumbnail.png ├── gpx.gpx ├── kml.kml ├── kmz.kmz ├── plain.json ├── plain.zip ├── rivers.gpkg ├── shapefile.shp ├── shapefile.zip ├── shapefile_thumbnail.png ├── some_shapefile_with_cyrillicйфячыцус.zip ├── style.sld └── tiff.tif ├── geoserver.env ├── helpers.php └── wait.php /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - name: Run PHP CS Fixer 16 | uses: docker://oskarstark/php-cs-fixer-ga 17 | with: 18 | args: --config=.php-cs-fixer.php --allow-risky=yes 19 | 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Fix styling -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest] 16 | php: [7.4, 8.0, 8.1] 17 | geoserver: ["2.20.4", "2.21.0"] 18 | exclude: 19 | - geoserver: "2.20.4" 20 | php: 7.4 21 | 22 | name: P${{ matrix.php }} - G${{ matrix.geoserver }} 23 | 24 | env: 25 | GEOSERVER_TAG: ${{ matrix.geoserver }} 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v2 30 | 31 | - name: Setup PHP 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php }} 35 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 36 | coverage: none 37 | 38 | - name: Setup problem matchers 39 | run: | 40 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 41 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 42 | 43 | - name: Install dependencies 44 | run: | 45 | composer update --prefer-dist --no-interaction 46 | 47 | - name: Create Environment 48 | run: | 49 | docker-compose -f ./tests/docker-compose.yml up -d 50 | php ./tests/wait.php 51 | 52 | - name: Execute tests 53 | env: 54 | GEOSERVER_URL: "http://127.0.0.1:8600/geoserver/" 55 | GEOSERVER_USER: "GEOSERVER_ADMIN_USER" 56 | GEOSERVER_PASSWORD: "GEOSERVER_ADMIN_PASSWORD" 57 | run: vendor/bin/phpunit 58 | 59 | - name: Stop Environment 60 | if: always() 61 | run: | 62 | docker-compose -f ./tests/docker-compose.yml down -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor/ 3 | coverage 4 | phpunit.xml 5 | composer.lock 6 | .php_cs.cache 7 | .phpunit.result.cache -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | true, 5 | 'blank_line_after_opening_tag' => true, 6 | 'braces' => true, 7 | 'concat_space' => ['spacing' => 'none'], 8 | 'no_multiline_whitespace_around_double_arrow' => true, 9 | 'elseif' => true, 10 | 'encoding' => true, 11 | 'single_blank_line_at_eof' => true, 12 | 'no_extra_blank_lines' => true, 13 | 'include' => true, 14 | 'blank_line_after_namespace' => true, 15 | 'not_operator_with_successor_space' => true, 16 | 'constant_case' => true, 17 | 'lowercase_keywords' => true, 18 | 'array_syntax' => ['syntax' => 'short'], 19 | 'no_unused_imports' => true 20 | ]; 21 | 22 | $finder = PhpCsFixer\Finder::create() 23 | ->exclude('vendor') 24 | ->in(__DIR__); 25 | 26 | return (new PhpCsFixer\Config()) 27 | ->setFinder($finder) 28 | ->setRules($fixers) 29 | ->setUsingCache(false); -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | # Contributor License Agreement 2 | 3 | If you are an employee and have created the contribution as part of your employment, 4 | you need to have your employer approve this agreement. 5 | If you do not own the copyright in the entire work of authorship, 6 | any other author of the contribution also needs to sign this. 7 | 8 | ## Copyright License 9 | 10 | You hereby grant to Oneoff-tech UG, the maintainer of GeoServer PHP Client, 11 | a worldwide, royalty-free, non-exclusive, perpetual and irrevocable license, 12 | with the right to transfer an unlimited number of non-exclusive licenses 13 | or to grant sublicenses to third parties, under the copyright covering the contribution 14 | to use the contribution by all means, including, but not limited to: 15 | 16 | * publish the contribution, 17 | * modify the contribution, 18 | * prepare derivative works based upon or containing the contribution 19 | and/or to combine the contribution with other materials, 20 | * reproduce the contribution in original or modified form, 21 | * distribute, to make the contribution available to the public, display 22 | and publicly perform the contribution in original or modified form. 23 | 24 | ## Free Software Pledge 25 | 26 | We agree to irrevocably (sub)license the contribution 27 | or any materials containing, based on or derived from your contribution 28 | under the terms of any licenses 29 | the Free Software Foundation classifies as [Free Software licenses](https://www.gnu.org/licenses/license-list.html) 30 | and which are approved by the Open Source Initiative as [Open Source licenses](http://opensource.org/licenses). 31 | See the [LICENSE](/LICENSE.txt) file, for the current license used. 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to GeoServer PHP Client 2 | 3 | This library is Free and Open Source Software. All Contributions to this project are most welcome, and can take many forms such as detailed bug reports, documentation, tests, features, and patches. Note that all contributions are managed by [OneOffTech](https://www.oneofftech.xyz). 4 | 5 | ## Bugs and Help 6 | 7 | GitHub [issues should only be created](https://github.com/OneOffTech/geoserver-client-php/issues/new) to log bugs or ask for help. Before posting to Github, please also search the existing issues to see if the bug has already been reported, and add any further details to the existing issue. 8 | 9 | ## Development 10 | 11 | Contributions are most easily managed via GitHub [pull requests](https://github.com/oneofftech/geoserver-client-php/pulls). To prepare one, [fork the GeoServer PHP Client](https://github.com/oneofftech/geoserver-client-php/fork) into your own GitHub repository. Then you'll be able to commit your work and submit pull requests. Once you are done, Github allows you to create a pull request and propose your changes to the original repository. Make sure you target your pull request to the `master` branch. 12 | 13 | The project follows the PSR-2 coding standard. Please run `./vendor/bin/php-cs-fixer fix` before wrapping everything up. 14 | 15 | ## Documentation 16 | 17 | All documentation is stored in the ["docs" directory of this repository](https://github.com/OneOffTech/geoserver-client-php/tree/master/docs). Any contribution on improving the documentation is highly appreciated and a good way to become a welcomed contributor. 18 | 19 | ## Contributor License Agreement 20 | 21 | The [Contributor License Agreement](./CLA.md) specifies the way how copyright of your contribution is handled. Please include in the comment on your pull request a statement like the following: 22 | 23 | > I'd like to contribute `feature X|bugfix Y|docs|something else` to GeoServer PHP Client. I confirm that my contributions to GeoServer PHP Client will be compatible with the GeoServer PHP Client Contributor License Agreement at the time of contribution. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![run-tests](https://github.com/OneOffTech/geoserver-client-php/actions/workflows/run-tests.yml/badge.svg)](https://github.com/OneOffTech/geoserver-client-php/actions/workflows/run-tests.yml) 2 | ![Packagist](https://img.shields.io/packagist/v/oneofftech/geoserver-client-php.svg) 3 | 4 | # GeoServer PHP Client 5 | 6 | This PHP library provides programmatic functions to access a [GeoServer](http://geoserver.org/). 7 | 8 | It is Free and Open Source Software. All contributions are most welcome. Learn more about [how to contribute](./CONTRIBUTING.md). 9 | 10 | #### Features 11 | 12 | * Obtain the [version](./docs/usage.md#get-the-geoserver-version) of the connected GeoServer instance 13 | * [Create workspace](./docs/usage.md#create-the-workspace) or retrieve existing workspace details 14 | * [Create datastores](./docs/usage.md#data-stores) and listing them 15 | * [Create coveragestores](./docs/usage.md#coverage-stores) and listing them 16 | * [Upload files](./docs/usage.md#uploading-geographic-files) in various [formats](#supported-file-formats) 17 | * [Manage styles](./docs/usage.md#styles) in SLD format 18 | 19 | For detailed information of each of the provided functions check out the [documentation on the usage](./docs/usage.md). 20 | 21 | #### Requirements 22 | 23 | * [PHP 7.4](http://www.php.net/) or above. 24 | * [GeoServer](http://geoserver.org/) 2.15.0 or above 25 | 26 | ## Installation 27 | 28 | The GeoServer PHP Client uses [Composer](http://getcomposer.org/) to manage its dependencies. 29 | 30 | ```bash 31 | composer require php-http/guzzle7-adapter guzzlehttp/psr7 http-interop/http-factory-guzzle oneofftech/geoserver-client-php 32 | ``` 33 | 34 | For more information, please review the [documentation on the installation process](./docs/installation.md). 35 | 36 | ## Supported file formats 37 | 38 | The library handles: 39 | 40 | * `Shapefile` 41 | * `Shapefile` inside `zip` archive 42 | * `GeoTIFF` 43 | * `GeoPackage` format version 1.2 (can contain both vector and raster data, but will be reported as vector) 44 | * Styled Layer Descriptor `SLD` files for layer styles in XML format 45 | 46 | Read more on [supported file formats and encoding](./docs/supported-files.md). 47 | 48 | ## License 49 | 50 | This project is Free and Open Source Software, licensed under the [AGPL v3 license](./LICENSE.txt). 51 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # GeoServer client changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | This project adhere to Semantic Versioning. 8 | 9 | ## Unreleased 10 | 11 | ### Added 12 | ### Changed 13 | ### Deprecated 14 | ### Removed 15 | ### Fixed 16 | ### Security 17 | 18 | ## [0.4.1] - 2022-08-07 19 | 20 | ### Added 21 | 22 | - Automated testing against GeoServer 2.20.x and 2.21.x 23 | 24 | ### Changed 25 | 26 | - Ignore already existing errors while creating a workspace 27 | 28 | ## [0.4.0] - 2022-08-06 29 | 30 | ### Added 31 | 32 | - Support for PHP 8.0 and 8.1 33 | - Support for GeoServer 2.17.x 34 | 35 | ### Removed 36 | 37 | - Support for PHP 7.1, 7.2 and 7.3 38 | 39 | ## [0.3.0] - 2020-01-06 40 | 41 | ### Added 42 | 43 | - Support for PHP 7.4 44 | - Support for GeoServer 2.15.x 45 | 46 | ### Changed 47 | 48 | - Style upload original filename is not preserved anymore. The file name will be the same as the given name via `StyleFile->name()` 49 | (if not specified the default value is equal to the filename without the extension) 50 | - Style files are directly uploaded in the workspace without a first request to create an empty style 51 | 52 | ## [0.2.1] - 2020-01-02 53 | 54 | ### Fixed 55 | 56 | - Style format identification on PHP 7.2 and 7.3 57 | 58 | ## [0.2.0] - 2018-10-23 59 | 60 | ### Changed 61 | 62 | - File uploads to GeoServer now imposes a UTF-8 charset 63 | 64 | ## [0.1.0] - 2018-10-16 65 | 66 | ### Added 67 | 68 | - Connection to a Geoserver instance, with authentication support 69 | - Ability to identify Shapefile, GeoTiff and GeoPackage formats 70 | - Create workspace or retrieve existing workspace details 71 | - Create coverage stores and listing them 72 | - Create data stores and listing them 73 | - Upload files in various formats 74 | - Manage styles in SLD format 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oneofftech/geoserver-client-php", 3 | "description": "A GeoServer API client", 4 | "type": "library", 5 | "homepage": "https://github.com/oneofftech/geoserver-client-php", 6 | "license": "AGPL-3.0-only", 7 | "authors": [ 8 | { 9 | "name": "Alessio Vertemati", 10 | "email": "alessio@oneofftech.xyz" 11 | } 12 | ], 13 | 14 | "support": { 15 | "issues": "https://github.com/oneofftech/geoserver-client-php/issues", 16 | "source": "https://github.com/oneofftech/geoserver-client-php" 17 | }, 18 | 19 | "autoload": { 20 | "psr-4": { 21 | "OneOffTech\\GeoServer\\": "src/" 22 | } 23 | }, 24 | 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tests\\": "tests/" 28 | }, 29 | 30 | "files": [ 31 | "tests/helpers.php" 32 | ] 33 | }, 34 | 35 | "require": { 36 | "php": ">=7.4", 37 | "ext-fileinfo": "*", 38 | "ext-zip": "*", 39 | "jms/serializer": "^3.4", 40 | "php-http/client-implementation": "^1.0|^2.0", 41 | "php-http/message": "^1.6", 42 | "php-http/discovery": "^1.3", 43 | "doctrine/annotations": "^1.4", 44 | "doctrine/cache": "^1.6", 45 | "php-http/client-common": "^2.0" 46 | }, 47 | "require-dev": { 48 | "phpunit/phpunit": "^9.5", 49 | "php-http/curl-client": "^2.0", 50 | "php-http/mock-client": "^1.0", 51 | "friendsofphp/php-cs-fixer": "^3.9", 52 | "guzzlehttp/psr7": "^1.6", 53 | "http-interop/http-factory-guzzle": "^1.0" 54 | }, 55 | "extra": { 56 | "branch-alias" : { 57 | "dev-master": "0.4.0-dev" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The GeoServer client uses [Composer](http://getcomposer.org/) to manage its dependencies. 4 | 5 | ```bash 6 | composer require php-http/guzzle7-adapter guzzlehttp/psr7 http-interop/http-factory-guzzle oneofftech/geoserver-client-php 7 | ``` 8 | 9 | The GeoServer client is not hard coupled to [Guzzle](https://github.com/guzzle/guzzle) or any other library that sends HTTP messages. It uses an abstraction called [HTTPlug](http://httplug.io/). This will give you the flexibilty to choose what PSR-7 implementation and HTTP client to use. 10 | 11 | ## Why requiring so many packages? 12 | 13 | GeoServer client has a dependency on the virtual package 14 | [php-http/client-implementation](https://packagist.org/providers/php-http/client-implementation) which requires to you install **an** adapter, but we do not care which one. That is an implementation detail in your application. We also need **a** PSR-7 implementation and **a** message factory. 15 | 16 | You do not have to use the `php-http/guzzle7-adapter` if you do not want to. You may use the `php-http/curl-client`. Read more about the virtual packages, why this is a good idea and about the flexibility it brings at the [HTTPlug docs](http://docs.php-http.org/en/latest/httplug/users.html). 17 | -------------------------------------------------------------------------------- /docs/supported-files.md: -------------------------------------------------------------------------------- 1 | # Supported file formats 2 | 3 | The library is able to recognize: 4 | 5 | * `Shapefile` 6 | * `Shapefile` inside `zip` archive 7 | * `GeoTIFF` 8 | * `GeoPackage` format version 1.2 (can contain both vector and raster data, but will be reported as vector) 9 | * Styled Layer Descriptor `SLD` files for layer styles in XML format 10 | 11 | You can check if a file is supported using 12 | 13 | ```php 14 | use OneOffTech\GeoServer\GeoFile; 15 | use OneOffTech\GeoServer\StyleFile; 16 | 17 | $isSupported = GeoFile::isSupported($path); 18 | // => true/false 19 | 20 | 21 | // For style files, the support is available with the StyleFile class 22 | $isSupported = StyleFile::isSupported($path); 23 | // => true/false 24 | ``` 25 | 26 | > The library supports only the file formats that can be uploaded to a GeoServer. 27 | > For example `Geojson`, `KML` and `GPX` are not supported out-of-the-box by GeoServer, 28 | > although plugins might be available for doing that 29 | 30 | # File Character Encoding 31 | 32 | GeoServer can deal with character encoding of layers/features, but tests highlighted 33 | a limited support when using UTF-8 for data/coverage store names. 34 | 35 | To prevent issues with UTF-8 features attributes, the client set the character encoding 36 | to UTF-8 when uploading a file. In this way GeoServer will consider UTF-8 as the 37 | character encoding for features/attributes contained in the file. This do not affect 38 | layer or store names. 39 | 40 | As by design decision the store name is the filename, or the assigned name when 41 | performing the upload, we highly reccomend to use ASCII characters for it. 42 | Use of UTF-8 encoded filenames (or store name) might prevent the client to retrieve the 43 | store/layer corresponding to the uploaded file. -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | The library is covered with unit and integration tests. 4 | 5 | ``` 6 | vendor/bin/phpunit 7 | ``` 8 | 9 | By default integration tests are not executed unless in the phpunit.xml file a GeoServer instance is specified. 10 | 11 | The `phpunit.xml.dist` define the `GEOSERVER_URL`, `GEOSERVER_USER`, 12 | `GEOSERVER_PASSWORD` for that purpose. 13 | If you want you can copy `phpunit.xml.dist` to `phpunit.xml` and edit those variables in place 14 | or define them in your environment variables. 15 | 16 | ```xml 17 | 18 | 19 | 20 | ``` 21 | 22 | If you don't have a GeoServer instance to trash there is a `docker-compose.yml` file that, with 23 | the help of Docker and the [kartoza/geoserver image](https://hub.docker.com/r/kartoza/geoserver/), 24 | creates a running GeoServer instance on port 8600. 25 | 26 | > Be aware that the Kartoza GeoServer image requires [4GB of RAM](https://github.com/kartoza/docker-geoserver/blob/master/Dockerfile#L23-L25) to run 27 | 28 | ```bash 29 | docker-compose -f ./tests/docker-compose.yml up -d 30 | # here better to wait for the full startup of the geoserver 31 | vendor/bin/phpunit 32 | ``` 33 | 34 | **Notes on testing files** 35 | 36 | - The GeoPackage testing file was copied from [github.com/ngageoint/geopackage-js](https://github.com/ngageoint/geopackage-js/blob/master/test/fixtures/rivers.gpkg) 37 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | 2 | # Usage 3 | 4 | ## Creating a client 5 | 6 | To create a GeoServer client use the `GeoServer::build` method. 7 | It will give you back a configured client instance. 8 | 9 | As of now the client can handle 1 workspace at time by design. 10 | 11 | ```php 12 | use OneOffTech\GeoServer\GeoServer; 13 | use OneOffTech\GeoServer\Auth\Authentication; 14 | 15 | /** 16 | * Geoserver URL 17 | */ 18 | $url = 'http://localhost:8600/geoserver/'; 19 | 20 | /** 21 | * Define a workspace to use 22 | */ 23 | $workspace = 'your-workspace'; 24 | 25 | $authentication = new Authentication('geoserver_user', 'geoserver_password'); 26 | 27 | $geoserver = GeoServer::build($url, $workspace, $authentication); 28 | ``` 29 | 30 | ## Get the GeoServer version 31 | 32 | Once you have a client instance, you can verify the GeoServer version number using `version()` 33 | 34 | ```php 35 | // assuming to have a GeoServer instance in the $geoserver variable 36 | $version = $geoserver->version(); 37 | // => 2.13.0 38 | ``` 39 | 40 | ## Create the workspace 41 | 42 | The client can create the configured workspace if not available. 43 | To do so call `createWorkspace()`: 44 | 45 | ```php 46 | $workspace = $geoserver->createWorkspace(); 47 | // => \OneOffTech\GeoServer\Models\Workspace 48 | ``` 49 | 50 | In case the creation goes well or the workspace already exists, a `Workspace` instance is returned. 51 | 52 | ## Retrieve the workspace details 53 | 54 | The `workspace()` method retrieve the details of the configured workspace 55 | 56 | ```php 57 | $workspace = $geoserver->workspace(); 58 | // => \OneOffTech\GeoServer\Models\Workspace 59 | ``` 60 | 61 | 62 | ## Data stores 63 | 64 | A datastore is a container of vector data. A workspace can have multiple data stores. 65 | 66 | You can retrieve all defined datastores using the `datastores()` method: 67 | 68 | ```php 69 | $datastores = $geoserver->datastores(); 70 | // => array of \OneOffTech\GeoServer\Models\DataStore 71 | ``` 72 | 73 | Or retrieve a datastores by name: 74 | 75 | ```php 76 | $datastore = $geoserver->datastore($name); 77 | // => \OneOffTech\GeoServer\Models\DataStore 78 | ``` 79 | 80 | You cal also delete a data store via: 81 | 82 | ```php 83 | $result = $geoserver->deleteDatastore($name); 84 | // => true || false 85 | ``` 86 | 87 | ## Coverage stores 88 | 89 | A coveragestore is a container of raster data. A workspace can have multiple coverage stores. 90 | 91 | You can retrieve all defined coverage stores using the `coveragestores()` method: 92 | 93 | ```php 94 | $coveragestores = $geoserver->coveragestores(); 95 | // => array of \OneOffTech\GeoServer\Models\CoverageStore 96 | ``` 97 | 98 | Or retrieve a coveragestores by name: 99 | 100 | ```php 101 | $coveragestore = $geoserver->coveragestore($name); 102 | // => \OneOffTech\GeoServer\Models\CoverageStore 103 | ``` 104 | 105 | You cal also delete a coverage store via: 106 | 107 | ```php 108 | $result = $geoserver->deleteCoveragestore($name); 109 | // => true || false 110 | ``` 111 | 112 | ## Upload and Delete geographic files 113 | 114 | ### Uploading geographic files 115 | 116 | Uploading a file to a GeoServer instance is done via the `upload` method. 117 | 118 | The client recognizes the format and create a correct store type, e.g. shapefiles lead to a 119 | datastore creation. To do so the file path must be wrapped in a `GeoFile` object. 120 | 121 | > See [Supported files](./supported-files.md) for knowing what the library can handle 122 | 123 | ```php 124 | use OneOffTech\GeoServer\GeoFile; 125 | 126 | $file = GeoFile::load('path/to/shapefile.shp'); 127 | // => OneOffTech\GeoServer\GeoFile{ 128 | // + mimeType 129 | // + extension 130 | // + format 131 | // + type 132 | // + name 133 | // + originalName 134 | // } 135 | // it will throw OneOffTech\GeoServer\Exception\UnsupportedFileException in case the file cannot be recognized 136 | ``` 137 | 138 | From a GeoFile instance the file `mimeType`, `format` and `type` (`vector` or `raster`, `OneOffTech\GeoServer\GeoType`) can be discovered. 139 | 140 | The `format` is used to specify the content of the file, as in some cases Geographic files do not have a standard mime type. 141 | For example a Shapefile mime type is `application/octet-stream`, which means a binary file. 142 | 143 | The `originalName` attribute contains the original filename. By default `originalName` and `name` are equals, 144 | but the name can be changed, by using the `name($value)` method. The `name` will be used as the store name inside GeoServer. 145 | 146 | Once obtained a `GeoFile` instance, the method `upload()` can be used to really upload the file to the GeoServer: 147 | 148 | ```php 149 | use OneOffTech\GeoServer\GeoFile; 150 | 151 | $file = GeoFile::load('path/to/shapefile.shp'); 152 | 153 | $feature = $geoserver->upload($file); 154 | // OneOffTech\GeoServer\Models\Resource 155 | ``` 156 | 157 | > For file character encoding please refer to [File Character Encoding](./supported-files.md#file-character-encoding) 158 | 159 | Once uploaded, the return value will be an instance of the `OneOffTech\GeoServer\Models\Resource`. 160 | It contains the details extracted by the GeoServer, like the bounding box. 161 | 162 | `OneOffTech\GeoServer\Models\Resource` has two sub-classes: 163 | 164 | - `OneOffTech\GeoServer\Models\Feature` A feature type is a vector based spatial resource or data set that originates from a data store 165 | - `OneOffTech\GeoServer\Models\Coverage` A coverage is a raster data set which originates from a coverage store. 166 | 167 | ### Verify if a geographic files exists 168 | 169 | As a helper method, given a `GeoFile` instance, is possible to verify that a corresponding Feature or Coverage is present. 170 | 171 | ```php 172 | use OneOffTech\GeoServer\GeoFile; 173 | 174 | $file = GeoFile::load('path/to/shapefile.shp'); 175 | 176 | $exists = $geoserver->exist($file); 177 | // true || false 178 | ``` 179 | 180 | > The identification currently uses the name assigned to the GeoFile 181 | 182 | ### Delete a geographic file 183 | 184 | As a helper method, given a `GeoFile` instance, is possible to delete the corresponding Feature or Coverage in the Geoserver. 185 | 186 | ```php 187 | use OneOffTech\GeoServer\GeoFile; 188 | 189 | $file = GeoFile::load('path/to/shapefile.shp'); 190 | 191 | $removed = $geoserver->remove($file); 192 | // true || false 193 | ``` 194 | 195 | > The identification currently uses the name assigned to the GeoFile 196 | 197 | ## Styles 198 | 199 | The client can upload, retrieve and delete styles defined within the configured workspace 200 | 201 | ### Upload style file 202 | 203 | To upload a style, a `StyleFile` instance representing the file on disk is required. 204 | Once the instance is obtained use the `uploadStyle` method on a GeoServer client instance. 205 | 206 | The style will be uploaded as part of the workspace styles. 207 | 208 | ```php 209 | use OneOffTech\GeoServer\StyleFile; 210 | 211 | $file = StyleFile::from('/path/to/style.sld'); 212 | // => OneOffTech\GeoServer\StyleFile{ 213 | // + name 214 | // + originalName 215 | // + mimeType 216 | // + extension 217 | // } 218 | // it will throw OneOffTech\GeoServer\Exception\UnsupportedFileException in case the file cannot be recognized 219 | 220 | // You can change the style name before uploading it to avoid collision. By default the filename will be used. 221 | $file->name('my_custom_style'); 222 | 223 | $style = $geoserver->uploadStyle($file); 224 | // => OneOffTech\GeoServer\Models\Style 225 | ``` 226 | 227 | ### Retrieve a style 228 | 229 | The client let you retrieve a style by its name 230 | 231 | ```php 232 | $style = $geoserver->style('style_name'); 233 | // => OneOffTech\GeoServer\Models\Style 234 | ``` 235 | 236 | > The name must be equal to the one given for the upload. 237 | > It might not be the file name 238 | 239 | ### Retrieve all styles 240 | 241 | You can also retrieve all styles defined in the workspace 242 | 243 | ```php 244 | $styles = $geoserver->styles(); 245 | // => array of OneOffTech\GeoServer\Models\Style 246 | ``` 247 | 248 | ### Remove a style 249 | 250 | Style removal is performed by giving the style name to the `removeStyle` method. 251 | The method will return the details of the deleted style. 252 | 253 | ```php 254 | $style = $geoserver->removeStyle('style_name'); 255 | // => OneOffTech\GeoServer\Models\Style 256 | ``` 257 | 258 | > the `$style->exists` attribute will be set to `false` after deletion 259 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | 9 | 10 | ./tests/Integration 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Auth/Authentication.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Auth; 23 | 24 | use Psr\Http\Message\RequestInterface; 25 | use Http\Message\Authentication\BasicAuth; 26 | use OneOffTech\GeoServer\Contracts\Authentication as AuthenticationContract; 27 | 28 | final class Authentication implements AuthenticationContract 29 | { 30 | /** 31 | * @var Http\Message\Authentication\BasicAuth 32 | */ 33 | private $auth; 34 | 35 | /** 36 | * @param string $app_secret 37 | * @param string $app_url 38 | */ 39 | public function __construct($username, $password) 40 | { 41 | $this->auth = new BasicAuth($username, $password); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function authenticate(RequestInterface $request) 48 | { 49 | return $this->auth->authenticate($request); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Auth/NullAuthentication.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Auth; 23 | 24 | use Psr\Http\Message\RequestInterface; 25 | use OneOffTech\GeoServer\Contracts\Authentication as AuthenticationContract; 26 | 27 | final class NullAuthentication implements AuthenticationContract 28 | { 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function authenticate(RequestInterface $request) 34 | { 35 | return $request; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Contracts/Authentication.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Contracts; 23 | 24 | use Http\Message\Authentication as AuthenticationContract; 25 | 26 | interface Authentication extends AuthenticationContract 27 | { 28 | } 29 | -------------------------------------------------------------------------------- /src/Contracts/FileReader.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Contracts; 23 | 24 | use Exception; 25 | 26 | abstract class FileReader 27 | { 28 | protected static function openFile($path) 29 | { 30 | if (! (is_readable($path) && is_file($path))) { 31 | throw new Exception("File [$path] not readable"); 32 | } 33 | $handle = fopen($path, 'r'); 34 | if (! $handle) { 35 | throw new Exception("Unable to read [$path] as binary file"); 36 | } 37 | 38 | return $handle; 39 | } 40 | 41 | protected static function openFileBinary($path, $position = 0) 42 | { 43 | if (! (is_readable($path) && is_file($path))) { 44 | throw new Exception("File [$path] not readable"); 45 | } 46 | $handle = fopen($path, 'rb'); 47 | if (! $handle) { 48 | throw new Exception("Unable to read [$path] as binary file"); 49 | } 50 | 51 | if ($position > 0) { 52 | fseek($handle, $position, SEEK_SET); 53 | } 54 | return $handle; 55 | } 56 | 57 | protected static function closeFile($handle) 58 | { 59 | if ($handle) { 60 | fclose($handle); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Contracts/Model.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Contracts; 23 | 24 | abstract class Model 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/DeserializationException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | /** 25 | * Deserialization exception 26 | * 27 | * Thrown when the JSON response from the server could not be deserialized into an object 28 | */ 29 | class DeserializationException extends GeoServerClientException 30 | { 31 | /** 32 | * 33 | * @param string $message 34 | * @param string $json the original JSON that raised the deserialization error 35 | */ 36 | public function __construct($message, $json) 37 | { 38 | parent::__construct("$message. $json"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Exception/ErrorResponseException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | /** 25 | * Error response Exception 26 | * 27 | * Thrown when the server respond with a failure 28 | */ 29 | class ErrorResponseException extends GeoServerClientException 30 | { 31 | /** 32 | * @var mixed 33 | */ 34 | private $data; 35 | 36 | /** 37 | * ErrorResponseException constructor. 38 | * 39 | * @param string $message 40 | * @param int $code 41 | * @param mixed $data 42 | */ 43 | public function __construct($message, $code, $data) 44 | { 45 | parent::__construct($message, $code); 46 | $this->data = $data; 47 | } 48 | 49 | /** 50 | * @return mixed 51 | */ 52 | public function getData() 53 | { 54 | return $this->data; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Exception/GeoServerClientException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | use Exception; 25 | 26 | class GeoServerClientException extends Exception 27 | { 28 | } 29 | -------------------------------------------------------------------------------- /src/Exception/SerializationException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | /** 25 | * Serialization exception 26 | * 27 | * Thrown when an object cannot be serialized into JSON 28 | */ 29 | class SerializationException extends GeoServerClientException 30 | { 31 | public function __construct($message) 32 | { 33 | parent::__construct($message); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exception/StoreNotFoundException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | /** 25 | * Raised when a store cannot be found on the specific geoserver instance 26 | */ 27 | class StoreNotFoundException extends GeoServerClientException 28 | { 29 | /** 30 | * 31 | * @param string $message 32 | */ 33 | public function __construct($message) 34 | { 35 | parent::__construct($message, 404); 36 | } 37 | 38 | /** 39 | * Create a store not found exception for a data store 40 | * 41 | * @param string $name the data store name 42 | * @return StoreNotFoundException 43 | */ 44 | public static function datastore($name) 45 | { 46 | return new self("Data store [$name] not found."); 47 | } 48 | 49 | /** 50 | * Create a store not found exception for a coverage store 51 | * 52 | * @param string $name the coverage store name 53 | * @return StoreNotFoundException 54 | */ 55 | public static function coveragestore($name) 56 | { 57 | return new self("Coverage store [$name] not found."); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exception/StyleAlreadyExistsException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | /** 25 | * Raised when a style cannot be uploaded as a previous style with the same name already exists 26 | */ 27 | class StyleAlreadyExistsException extends GeoServerClientException 28 | { 29 | /** 30 | * 31 | * @param string $message 32 | */ 33 | public function __construct($message) 34 | { 35 | parent::__construct($message, 409); 36 | } 37 | 38 | /** 39 | * Create a style already exists exception for a given style 40 | * 41 | * @param string $name the style name 42 | * @param string $workspace the workspace that contains the style 43 | * @return StyleAlreadyExistsException 44 | */ 45 | public static function style($name, $workspace) 46 | { 47 | return new self("A style named [$name] already exists in [$workspace]."); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/StyleNotFoundException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | /** 25 | * Raised when a style cannot be found on the specific geoserver instance 26 | */ 27 | class StyleNotFoundException extends GeoServerClientException 28 | { 29 | /** 30 | * 31 | * @param string $message 32 | */ 33 | public function __construct($message) 34 | { 35 | parent::__construct($message, 404); 36 | } 37 | 38 | /** 39 | * Create a style not found exception for a given style 40 | * 41 | * @param string $name the style name 42 | * @param string $workspace the workspace that was expecting to contain the style 43 | * @return StyleNotFoundException 44 | */ 45 | public static function style($name, $workspace) 46 | { 47 | return new self("The style [$name] cannot be found in [$workspace]."); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/UnsupportedFileException.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Exception; 23 | 24 | class UnsupportedFileException extends GeoServerClientException 25 | { 26 | /** 27 | * @param string $path 28 | * @param string $format 29 | * @param string $supportedFormats 30 | */ 31 | public function __construct($path, $format, $supportedFormats) 32 | { 33 | parent::__construct("The given file [$path] is not supported. Found [$format] expected [$supportedFormats]"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/GeoFile.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer; 23 | 24 | use SplFileInfo; 25 | 26 | use OneOffTech\GeoServer\Support\TypeResolver; 27 | use OneOffTech\GeoServer\Exception\UnsupportedFileException; 28 | 29 | /** 30 | * 31 | */ 32 | class GeoFile 33 | { 34 | protected $file; 35 | 36 | protected $name; 37 | 38 | protected $originalName; 39 | 40 | protected $mimeType; 41 | 42 | /** 43 | * The geodata format 44 | */ 45 | protected $format; 46 | 47 | protected $extension; 48 | 49 | /** 50 | * The extension as required by GeoServer 51 | * 52 | * e.g. for a geo tiff file the extension must be .geotiff 53 | */ 54 | protected $normalizedExtension; 55 | 56 | /** 57 | * The mime type as required by GeoServer 58 | * 59 | * e.g. for a geo tiff file the mime type appears to be "geotif/geotiff", 60 | * as found in https://gis.stackexchange.com/questions/218162/creating-coveragestore-geotiff-using-rest-api 61 | */ 62 | protected $normalizedMimeType; 63 | 64 | /** 65 | * The type of the geodata (vector or raster) 66 | */ 67 | protected $type; 68 | 69 | public function __construct($path) 70 | { 71 | $this->file = new SplFileInfo($path); 72 | 73 | list($format, $type, $mimeType) = TypeResolver::identify($path); 74 | 75 | if (! in_array($format, TypeResolver::supportedFormats())) { 76 | throw new UnsupportedFileException($path, $format, join(', ', TypeResolver::supportedFormats())); 77 | } 78 | 79 | $this->mimeType = $mimeType; 80 | 81 | $this->extension = $this->file->getExtension(); 82 | 83 | $this->normalizedExtension = TypeResolver::normalizedExtensionFromFormat($format) ?? $this->extension; 84 | 85 | $this->normalizedMimeType = TypeResolver::normalizedMimeTypeFromFormat($format) ?? $mimeType; 86 | 87 | $this->format = $format; 88 | 89 | $this->type = $type; 90 | 91 | $this->name = $this->originalName = $this->file->getFileName(); 92 | } 93 | 94 | /** 95 | * Set the data name. 96 | * It will be used for store name 97 | * 98 | * @param string $value 99 | * @return Data 100 | */ 101 | public function name($value) 102 | { 103 | $this->name = $value; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Tell if the name attribute is different from the original filename 110 | * 111 | * @return bool 112 | */ 113 | public function wasRenamed() 114 | { 115 | return $this->name !== $this->originalName; 116 | } 117 | 118 | /** 119 | * Get the path to the file 120 | * 121 | * @return string 122 | */ 123 | public function path() 124 | { 125 | return $this->file->getRealPath(); 126 | } 127 | 128 | public function content() 129 | { 130 | return file_get_contents($this->file->getRealPath()); 131 | } 132 | 133 | public function __get($property) 134 | { 135 | return $this->$property; 136 | } 137 | 138 | /** 139 | * Copy the GeoFile content into a temporary folder and return the new GeoFile instance 140 | * 141 | * Please note that the temporary file is not disposed automatically 142 | * 143 | * @param string $temporaryFolder 144 | * @return GeoFile 145 | */ 146 | public function copy($temporaryFolder = null) 147 | { 148 | $tmpfilename = tempnam($temporaryFolder ?? sys_get_temp_dir(), $this->name); 149 | $handle = fopen($tmpfilename, "w+b"); 150 | fwrite($handle, $this->content()); 151 | fclose($handle); 152 | 153 | return GeoFile::from($tmpfilename)->name($this->name); 154 | } 155 | 156 | /** 157 | * Create a Geo file instance from a given file path 158 | * 159 | * @param string $path 160 | * @return Data 161 | * @throws UnsupportedFileException if file is not supported 162 | */ 163 | public static function from($path) 164 | { 165 | return new static($path); 166 | } 167 | public static function load($path) 168 | { 169 | return static::from($path); 170 | } 171 | 172 | /** 173 | * Check if the specified file is a valid geographical file 174 | * 175 | * @param string $path 176 | * @return bool 177 | */ 178 | public static function isSupported(string $path) 179 | { 180 | list($format, $type, $mimeType) = TypeResolver::identify($path); 181 | return in_array($format, TypeResolver::supportedFormats()); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/GeoFormat.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer; 23 | 24 | /** 25 | * Formats of geographical files supported by GeoServer 26 | */ 27 | final class GeoFormat 28 | { 29 | const SHAPEFILE = "shapefile"; 30 | const SHAPEFILE_ZIP = "shapefile_zip"; 31 | const GEOTIFF = "geotiff"; 32 | const SLD = "SLD"; 33 | const GEOPACKAGE = "geopackage"; 34 | } 35 | -------------------------------------------------------------------------------- /src/GeoType.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer; 23 | 24 | /** 25 | * The geographical data type: vector or raster 26 | */ 27 | final class GeoType 28 | { 29 | /** 30 | * Vector data 31 | */ 32 | const VECTOR = "vector"; 33 | 34 | /** 35 | * Raster data 36 | */ 37 | const RASTER = "raster"; 38 | 39 | /** 40 | * Get the GeoServer store for the specified type 41 | * 42 | * @param string $type 43 | * @return string 44 | */ 45 | public static function storeFor($type) 46 | { 47 | return $type === 'vector' ? 'datastores' : 'coveragestores'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/InteractsWithHttp.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http; 23 | 24 | use Exception; 25 | use Throwable; 26 | use Http\Client\HttpClient; 27 | use Http\Message\MessageFactory; 28 | use Psr\Http\Message\ResponseInterface; 29 | use OneOffTech\GeoServer\Support\ImageResponse; 30 | use OneOffTech\GeoServer\Exception\ErrorResponseException; 31 | use OneOffTech\GeoServer\Exception\SerializationException; 32 | use OneOffTech\GeoServer\Exception\DeserializationException; 33 | 34 | trait InteractsWithHttp 35 | { 36 | 37 | /** 38 | * @var HttpClient 39 | */ 40 | private $httpClient; 41 | 42 | /** 43 | * @var MessageFactory 44 | */ 45 | private $messageFactory; 46 | 47 | /** 48 | * @var Serializer 49 | */ 50 | private $serializer; 51 | 52 | /** 53 | * @param ResponseInterface $response 54 | * @throws ErrorResponseException 55 | */ 56 | private function checkResponseError(ResponseInterface $response) 57 | { 58 | $responseBody = $response->getBody(); 59 | $contentTypeHeader = $response->getHeader('Content-Type'); 60 | $contentType = ! empty($contentTypeHeader) ? $contentTypeHeader[0] : ''; 61 | if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 201 && $response->getStatusCode() !== 204) { 62 | if ($response->getStatusCode() === 500 && strpos($contentType, 'text/html')!== false) { 63 | $reason = substr((string)$responseBody, 0, 500); 64 | 65 | throw new ErrorResponseException($reason, $response->getStatusCode(), (string)$responseBody); 66 | } 67 | throw new ErrorResponseException(! empty($response->getReasonPhrase()) ? $response->getReasonPhrase() : 'There was a problem in fulfilling your request.', $response->getStatusCode(), (string)$responseBody); 68 | } 69 | } 70 | 71 | /** 72 | * Deserialize a JSON string into the given class instance 73 | * 74 | * @param string $json the JSON string to deserialized 75 | * @param string $class the fully qualified class name 76 | * @return object instance of $class 77 | * @throws DeserializationException if an error occurs during the deserialization 78 | */ 79 | protected function deserialize($response, $class = null) 80 | { 81 | if (is_null($class)) { 82 | return json_decode($response->getBody()); 83 | } 84 | 85 | try { 86 | return $this->serializer->deserialize($response->getBody(), $class, 'json'); 87 | } catch (JMSException $ex) { 88 | throw new DeserializationException($ex->getMessage(), (string)$response->getBody()); 89 | } 90 | } 91 | 92 | protected function serialize($object) 93 | { 94 | try { 95 | return $this->serializer->serialize($object, 'json'); 96 | } catch (Throwable $ex) { 97 | throw new SerializationException($ex->getMessage()); 98 | } catch (Exception $ex) { 99 | throw new SerializationException($ex->getMessage()); 100 | } 101 | } 102 | 103 | /** 104 | * Handle and send the request to the given route. 105 | * 106 | * @param RPCRequest $request The request data 107 | * @param string $route The API route 108 | * 109 | * @return ResponseInterface 110 | * @throws SerializationException 111 | */ 112 | private function handleRequest($request) 113 | { 114 | $response = $this->httpClient->sendRequest($request); 115 | 116 | $this->checkResponseError($response); 117 | 118 | return $response; 119 | } 120 | 121 | protected function get($route, $class = null) 122 | { 123 | $request = $this->messageFactory->createRequest('GET', $route, []); 124 | 125 | $response = $this->handleRequest($request); 126 | 127 | return $this->deserialize($response, $class); 128 | } 129 | 130 | protected function post($route, $data, $class = null) 131 | { 132 | $request = $this->messageFactory->createRequest('POST', $route, [], $this->serialize($data)); 133 | 134 | $response = $this->handleRequest($request); 135 | 136 | return $this->deserialize($response, $class); 137 | } 138 | 139 | protected function put($route, $data, $class = null) 140 | { 141 | $request = $this->messageFactory->createRequest('PUT', $route, [], $this->serialize($data)); 142 | 143 | $response = $this->handleRequest($request); 144 | 145 | return $this->deserialize($response, $class); 146 | } 147 | 148 | protected function putFile($route, $data, $class = null) 149 | { 150 | $request = $this->messageFactory->createRequest('PUT', $route, ['Content-Type' => $data->normalizedMimeType], $data->content()); 151 | 152 | $response = $this->handleRequest($request); 153 | 154 | return $this->deserialize($response, $class); 155 | } 156 | 157 | protected function postFile($route, $data, $class = null) 158 | { 159 | $request = $this->messageFactory->createRequest('POST', $route, ['Content-Type' => $data->normalizedMimeType], $data->content()); 160 | 161 | $response = $this->handleRequest($request); 162 | 163 | return $this->deserialize($response, $class); 164 | } 165 | 166 | protected function delete($route, $class = null) 167 | { 168 | $request = $this->messageFactory->createRequest('DELETE', $route, []); 169 | 170 | $response = $this->handleRequest($request); 171 | 172 | return $this->deserialize($response, $class); 173 | } 174 | 175 | protected function getImage($route) 176 | { 177 | $request = $this->messageFactory->createRequest('GET', $route, []); 178 | 179 | $response = $this->handleRequest($request); 180 | 181 | $contentTypeHeader = $response->getHeader('Content-Type'); 182 | $contentType = ! empty($contentTypeHeader) ? $contentTypeHeader[0] : ''; 183 | 184 | if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 201 && $response->getStatusCode() !== 204) { 185 | throw new ErrorResponseException(! empty($response->getReasonPhrase()) ? $response->getReasonPhrase() : 'There was a problem in fulfilling your request.', $response->getStatusCode(), (string)$responseBody); 186 | } 187 | 188 | if (strpos($contentType, 'image') === false) { 189 | throw new ErrorResponseException("Expected image response, but got [$contentType]", $response->getStatusCode(), (string)$response->getBody()); 190 | } 191 | 192 | return ImageResponse::from($response); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Http/ResponseHelper.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http; 23 | 24 | class ResponseHelper 25 | { 26 | /** 27 | * Check if an array is associative or index based. 28 | * 29 | * An array is considered associative if all keys are strings. 30 | * Empty array or null value are not considered associative 31 | * 32 | * @param array $array the array to check 33 | * @return true if array is associative 34 | */ 35 | public static function isAssociativeArray($array) 36 | { 37 | if (empty($array) || ! is_array($array)) { 38 | return false; 39 | } 40 | 41 | $keys = array_keys($array); 42 | foreach ($keys as $key) { 43 | if (is_int($key)) { 44 | return false; 45 | } 46 | } 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/Responses/CoverageResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | use OneOffTech\GeoServer\Models\Coverage; 26 | 27 | class CoverageResponse 28 | { 29 | /** 30 | * @var \OneOffTech\GeoServer\Models\Coverage 31 | * @JMS\Type("OneOffTech\GeoServer\Models\Coverage") 32 | * @JMS\SerializedName("coverage") 33 | */ 34 | public $coverage; 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Responses/CoverageStoreResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class CoverageStoreResponse 27 | { 28 | /** 29 | * @var \OneOffTech\GeoServer\Models\CoverageStore 30 | * @JMS\Type("OneOffTech\GeoServer\Models\CoverageStore") 31 | * @JMS\SerializedName("coverageStore") 32 | */ 33 | public $store; 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Responses/CoverageStoresResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class CoverageStoresResponse 27 | { 28 | /** 29 | * @var \OneOffTech\GeoServer\Models\CoverageStore[] 30 | * @JMS\Type("array") 31 | * @JMS\SerializedName("coverageStores") 32 | */ 33 | public $stores; 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Responses/DataStoreResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class DataStoreResponse 27 | { 28 | /** 29 | * @var \OneOffTech\GeoServer\Models\DataStore 30 | * @JMS\Type("array") 31 | * @JMS\SerializedName("dataStores") 32 | */ 33 | public $dataStores; 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Responses/FeatureResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class FeatureResponse 27 | { 28 | /** 29 | * @var \OneOffTech\GeoServer\Models\Feature 30 | * @JMS\Type("OneOffTech\GeoServer\Models\Feature") 31 | * @JMS\SerializedName("featureType") 32 | */ 33 | public $feature; 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Responses/StylesResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class StylesResponse 27 | { 28 | /** 29 | * @var \OneOffTech\GeoServer\Models\Style[] 30 | * @JMS\Type("array") 31 | * @JMS\SerializedName("styles") 32 | */ 33 | public $styles; 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Responses/WorkspaceResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http\Responses; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class WorkspaceResponse 27 | { 28 | /** 29 | * @var \OneOffTech\GeoServer\Models\Workspace 30 | * @JMS\Type("OneOffTech\GeoServer\Models\Workspace") 31 | */ 32 | public $workspace; 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Routes.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Http; 23 | 24 | use OneOffTech\GeoServer\Support\WmsOptions; 25 | 26 | /** 27 | * Helper class for managing URL creation 28 | * 29 | * @internal 30 | */ 31 | final class Routes 32 | { 33 | /** @var string */ 34 | private $baseUrl; 35 | 36 | /** 37 | * @param string $baseUrl 38 | */ 39 | public function __construct($baseUrl) 40 | { 41 | $this->baseUrl = trim(trim($baseUrl), '/'); 42 | } 43 | 44 | /** 45 | * Helper for creating GeoServer Rest URLs 46 | * 47 | * @param string $endpoint the endpoint to attach to the base URL 48 | * @return string 49 | */ 50 | public function url($endpoint) 51 | { 52 | return sprintf("%s/rest/%s", $this->baseUrl, $endpoint); 53 | } 54 | 55 | /** 56 | * Web Map Service (WMS) service url helper 57 | * 58 | * Create the URL to the WMS service based on the specified options 59 | * 60 | * @param string $workspace The workspace the URL will refer to 61 | * @param \OneOffTech\GeoServer\Support\WmsOptions $options The WMS service options 62 | * @return string 63 | */ 64 | public function wms($workspace, WmsOptions $options) 65 | { 66 | return sprintf( 67 | "%s/%s/wms?service=WMS&%s", 68 | $this->baseUrl, 69 | $workspace, 70 | $options->toUrlParameters() 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Models/BoundingBox.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class BoundingBox 27 | { 28 | /** 29 | * 30 | * @var float 31 | * @JMS\Type("float") 32 | * @JMS\SerializedName("minx") 33 | */ 34 | public $minX; 35 | 36 | /** 37 | * 38 | * @var float 39 | * @JMS\Type("float") 40 | * @JMS\SerializedName("miny") 41 | */ 42 | public $minY; 43 | 44 | /** 45 | * 46 | * @var float 47 | * @JMS\Type("float") 48 | * @JMS\SerializedName("maxx") 49 | */ 50 | public $maxX; 51 | 52 | /** 53 | * 54 | * @var float 55 | * @JMS\Type("float") 56 | * @JMS\SerializedName("maxy") 57 | */ 58 | public $maxY; 59 | 60 | /** 61 | * The coordinate system in which the bounding box values are expressed 62 | * 63 | * @var string 64 | * @JMS\Type("string") 65 | */ 66 | public $crs = null; 67 | 68 | public function toArray() 69 | { 70 | return [$this->minX, $this->minY, $this->maxX, $this->maxY]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Models/Coverage.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use OneOffTech\GeoServer\GeoType; 25 | use JMS\Serializer\Annotation as JMS; 26 | 27 | /** 28 | * A coverage is a raster data set which originates from a coverage store. 29 | */ 30 | final class Coverage extends Resource 31 | { 32 | 33 | /** 34 | * 35 | * @var string 36 | * @JMS\Type("string") 37 | * @JMS\SerializedName("nativeFormat") 38 | */ 39 | public $nativeFormat; 40 | 41 | /** 42 | * @var array 43 | * @JMS\Type("array") 44 | * @JMS\SerializedName("interpolationMethods") 45 | */ 46 | public $interpolationMethods; 47 | 48 | /** 49 | * @var array 50 | * @JMS\Type("array") 51 | */ 52 | public $nativeCRS; 53 | 54 | /** 55 | * 56 | * @var array 57 | * @JMS\Type("array") 58 | */ 59 | public $dimensions; 60 | 61 | /** 62 | * Contains information about how to translate from the raster plan to a coordinate reference system 63 | * @var array 64 | * @JMS\Type("array") 65 | */ 66 | public $grid; 67 | 68 | public function type() 69 | { 70 | return GeoType::RASTER; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Models/CoverageStore.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class CoverageStore extends Store 27 | { 28 | /** 29 | * Type of coverage store 30 | * 31 | * @var string 32 | * @JMS\Type("string") 33 | */ 34 | public $type; 35 | 36 | /** 37 | * Location of the raster data source (often, but not necessarily, a file). 38 | * Can be relative to the data directory. 39 | * 40 | * @var string 41 | * @JMS\Type("string") 42 | */ 43 | public $url; 44 | 45 | /** 46 | * The link to the coverages contained in this store 47 | * 48 | * @var array 49 | * @JMS\Type("array") 50 | */ 51 | public $coverages; 52 | } 53 | -------------------------------------------------------------------------------- /src/Models/DataStore.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | 26 | class DataStore extends Store 27 | { 28 | /** 29 | * 30 | * @var string 31 | * @JMS\Type("string") 32 | * @JMS\SerializedName("featureTypes") 33 | */ 34 | public $featureTypes; 35 | 36 | /** 37 | * 38 | * @var string 39 | * @JMS\Type("array") 40 | * @JMS\SerializedName("connectionParameters") 41 | */ 42 | public $connectionParameters; 43 | } 44 | -------------------------------------------------------------------------------- /src/Models/Feature.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use OneOffTech\GeoServer\GeoType; 25 | use JMS\Serializer\Annotation as JMS; 26 | 27 | /** 28 | * A feature type is a vector based spatial resource or data set that originates from a data store 29 | */ 30 | final class Feature extends Resource 31 | { 32 | /** 33 | * The identifier of coordinate reference system of the resource. 34 | * @var string 35 | * @JMS\Type("string") 36 | */ 37 | public $srs; 38 | 39 | /** 40 | * 41 | * @var bool 42 | * @JMS\Type("boolean") 43 | * @JMS\SerializedName("overridingServiceSRS") 44 | */ 45 | public $overridingServiceSRS = false; 46 | 47 | public function type() 48 | { 49 | return GeoType::VECTOR; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Models/Resource.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | use OneOffTech\GeoServer\Contracts\Model; 26 | 27 | /** 28 | * A resource that describe both a Coverage or a Feature. 29 | */ 30 | abstract class Resource extends Model 31 | { 32 | /** 33 | * The name of the resource. 34 | * 35 | * @var string 36 | * @JMS\Type("string") 37 | */ 38 | public $name; 39 | 40 | /** 41 | * The native name of the resource. 42 | * 43 | * This name corresponds to the physical resource that is 44 | * derived from -- a shapefile name, a database table,... 45 | * 46 | * @var string 47 | * @JMS\Type("string") 48 | * @JMS\SerializedName("nativeName") 49 | */ 50 | public $nativeName; 51 | 52 | /** 53 | * The title of the resource. 54 | * This is usually something that is meant to be displayed in a user interface. 55 | * 56 | * @var string 57 | * @JMS\Type("string") 58 | */ 59 | public $title; 60 | 61 | /** 62 | * A description of the resource. This is usually something that is meant to be displayed in a user interface. 63 | * @var string 64 | * @JMS\Type("string") 65 | */ 66 | public $abstract; 67 | 68 | /** 69 | * The store the resource is a part of. 70 | * @var array 71 | * @JMS\Type("array") 72 | */ 73 | public $store; 74 | 75 | /** 76 | * 77 | * @var bool 78 | * @JMS\Type("boolean") 79 | * @JMS\SerializedName("enabled") 80 | */ 81 | public $enabled = true; 82 | 83 | /** 84 | * True if this feature type info is overriding the counting of numberMatched 85 | * @var bool 86 | * @JMS\Type("boolean") 87 | * @JMS\SerializedName("skipNumberMatched") 88 | */ 89 | public $skipNumberMatched = false; 90 | /** 91 | * 92 | * @var bool 93 | * @JMS\Type("boolean") 94 | * @JMS\SerializedName("circularArcPresent") 95 | */ 96 | public $circularArcPresent = false; 97 | 98 | /** 99 | * A collection of keywords associated with the resource. 100 | * @var array 101 | * @JMS\Type("array") 102 | */ 103 | public $keywords; 104 | 105 | /** 106 | * Returns the bounds of the resource in its declared CRS. 107 | * 108 | * @var \OneOffTech\GeoServer\Models\BoundingBox 109 | * @JMS\Type("OneOffTech\GeoServer\Models\BoundingBox") 110 | * @JMS\SerializedName("nativeBoundingBox") 111 | */ 112 | public $nativeBoundingBox; 113 | 114 | /** 115 | * The bounds of the resource in lat / lon. This value represents a "fixed value" and is not calculated on the underlying dataset. 116 | * 117 | * @var \OneOffTech\GeoServer\Models\BoundingBox 118 | * @JMS\Type("OneOffTech\GeoServer\Models\BoundingBox") 119 | * @JMS\SerializedName("latLonBoundingBox") 120 | */ 121 | public $boundingBox; 122 | 123 | /** 124 | * Wrapper for the derived set of attributes for the feature type 125 | * 126 | * @var array 127 | * @JMS\Type("array") 128 | */ 129 | public $attributes; 130 | 131 | /** 132 | * 133 | * @var string 134 | * @JMS\Type("string") 135 | * @JMS\SerializedName("projectionPolicy") 136 | */ 137 | public $projectionPolicy; 138 | 139 | /** 140 | * 141 | * @var float 142 | * @JMS\Type("float") 143 | * @JMS\SerializedName("maxFeatures") 144 | */ 145 | public $maxFeatures; 146 | 147 | /** 148 | * 149 | * @var float 150 | * @JMS\Type("float") 151 | * @JMS\SerializedName("numDecimals") 152 | */ 153 | public $numDecimals; 154 | 155 | /** 156 | * @var array 157 | * @JMS\Type("array") 158 | */ 159 | public $namespace; 160 | 161 | /** 162 | * Return the type of the resource 163 | * 164 | * @return string The @see GeoType of the resource 165 | */ 166 | abstract public function type(); 167 | } 168 | -------------------------------------------------------------------------------- /src/Models/Store.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | use OneOffTech\GeoServer\Contracts\Model; 26 | 27 | /** 28 | * A generic store, can be a data store or a coverage store 29 | */ 30 | class Store extends Model 31 | { 32 | /** 33 | * Store name 34 | * 35 | * @var string 36 | * @JMS\Type("string") 37 | */ 38 | public $name; 39 | 40 | /** 41 | * The API URL to the store details 42 | * 43 | * @var string 44 | * @JMS\Type("string") 45 | */ 46 | public $href; 47 | 48 | /** 49 | * If the store is enabled 50 | * 51 | * @var bool 52 | * @JMS\Type("boolean") 53 | */ 54 | public $enabled; 55 | 56 | /** 57 | * If the store is the default one 58 | * 59 | * @var bool 60 | * @JMS\Type("boolean") 61 | * @JMS\SerializedName("_default") 62 | */ 63 | public $default = false; 64 | 65 | /** 66 | * The workspace in which the store is located 67 | * 68 | * @var string 69 | * @JMS\Type("string") 70 | */ 71 | public $workspace; 72 | 73 | /** 74 | * Indicates if the store exists. 75 | * 76 | * It is used to indicate the deletion status. 77 | * The $exists value is set to false after succesful deletion. 78 | * 79 | * @var bool 80 | * @JMS\Exclude 81 | */ 82 | public $exists = true; 83 | } 84 | -------------------------------------------------------------------------------- /src/Models/Style.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | use OneOffTech\GeoServer\Contracts\Model; 26 | 27 | /** 28 | * A style describes how a resource is symbolized or rendered by the Web Map Service. 29 | */ 30 | class Style extends Model 31 | { 32 | /** 33 | * The style name 34 | * 35 | * @var string 36 | * @JMS\Type("string") 37 | */ 38 | public $name; 39 | 40 | /** 41 | * The workspace in which the style is located 42 | * 43 | * @var string 44 | * @JMS\Type("string") 45 | */ 46 | public $workspace; 47 | 48 | /** 49 | * The style format 50 | * 51 | * @var string 52 | * @JMS\Type("string") 53 | */ 54 | public $format; 55 | 56 | /** 57 | * The style version 58 | * 59 | * @var string 60 | * @JMS\Type("string") 61 | * @JMS\SerializedName("languageVersion") 62 | */ 63 | public $version; 64 | 65 | /** 66 | * The original style file name 67 | * 68 | * @var string 69 | * @JMS\Type("string") 70 | * @JMS\SerializedName("filename") 71 | */ 72 | public $filename; 73 | 74 | /** 75 | * Indicates if the style exists. 76 | * 77 | * It is used to indicate the deletion status. 78 | * The $exists value is set to false after succesful deletion. 79 | * 80 | * @var bool 81 | * @JMS\Exclude 82 | */ 83 | public $exists = true; 84 | } 85 | -------------------------------------------------------------------------------- /src/Models/Workspace.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Models; 23 | 24 | use JMS\Serializer\Annotation as JMS; 25 | use OneOffTech\GeoServer\Contracts\Model; 26 | 27 | class Workspace extends Model 28 | { 29 | 30 | /** 31 | * Name of workspace 32 | * @var string 33 | * @JMS\Type("string") 34 | */ 35 | public $name; 36 | 37 | /** 38 | * The API URL to the workspace details 39 | * @var string 40 | * @JMS\Type("string") 41 | */ 42 | public $href; 43 | 44 | /** 45 | * If the workspace is isolated 46 | * @var bool 47 | * @JMS\Type("boolean") 48 | */ 49 | public $isolated; 50 | 51 | /** 52 | * URL to Datas tores in this workspace 53 | * @var string 54 | * @JMS\Type("string") 55 | * @JMS\SerializedName("dataStores") 56 | */ 57 | public $dataStores; 58 | 59 | /** 60 | * URL to Coverage stores in this workspace 61 | * @var string 62 | * @JMS\Type("string") 63 | * @JMS\SerializedName("coverageStores") 64 | */ 65 | public $coverageStores; 66 | 67 | /** 68 | * URL to WMS stores in this workspace 69 | * @var string 70 | * @JMS\Type("string") 71 | * @JMS\SerializedName("wmsStores") 72 | */ 73 | public $wmsStores; 74 | 75 | /** 76 | * URL to WMS stores in this workspace 77 | * @var string 78 | * @JMS\Type("string") 79 | * @JMS\SerializedName("wmtsStores") 80 | */ 81 | public $wmtsStores; 82 | } 83 | -------------------------------------------------------------------------------- /src/Options.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer; 23 | 24 | use Http\Client\HttpClient; 25 | use JMS\Serializer\Serializer; 26 | use Http\Message\MessageFactory; 27 | use Http\Client\Common\PluginClient; 28 | use JMS\Serializer\SerializerBuilder; 29 | use Http\Discovery\HttpClientDiscovery; 30 | use Http\Discovery\MessageFactoryDiscovery; 31 | use OneOffTech\GeoServer\Contracts\Authentication; 32 | use Doctrine\Common\Annotations\AnnotationRegistry; 33 | use Http\Client\Common\Plugin\AuthenticationPlugin; 34 | use Http\Client\Common\Plugin\HeaderDefaultsPlugin; 35 | use JMS\Serializer\EventDispatcher\EventDispatcher; 36 | use OneOffTech\GeoServer\Serializer\DeserializeBoundingBoxSubscriber; 37 | use OneOffTech\GeoServer\Serializer\DeserializeStyleResponseSubscriber; 38 | use OneOffTech\GeoServer\Serializer\DeserializeDataStoreResponseSubscriber; 39 | use OneOffTech\GeoServer\Serializer\DeserializeCoverageStoreResponseSubscriber; 40 | 41 | final class Options 42 | { 43 | public $authentication; 44 | 45 | /** 46 | * @var HttpClient 47 | */ 48 | public $httpClient; 49 | 50 | /** 51 | * @var MessageFactory 52 | */ 53 | public $messageFactory; 54 | 55 | /** 56 | * @var Serializer 57 | */ 58 | public $serializer; 59 | 60 | const FORMAT_JSON = "application/json"; 61 | 62 | /** 63 | * ... 64 | */ 65 | public function __construct(Authentication $authentication) 66 | { 67 | $this->authentication = $authentication; 68 | AnnotationRegistry::registerLoader('class_exists'); 69 | 70 | // registering a PluginClient as the authentication and 71 | // some headers should be added to all requests 72 | $this->httpClient = new PluginClient( 73 | HttpClientDiscovery::find(), 74 | [ 75 | new AuthenticationPlugin($this->authentication), 76 | new HeaderDefaultsPlugin([ 77 | 'User-Agent' => 'OneOffTech GeoServer Client', 78 | 'Content-Type' => self::FORMAT_JSON, 79 | 'Accept' => self::FORMAT_JSON 80 | ]), 81 | ] 82 | ); 83 | 84 | $this->messageFactory = MessageFactoryDiscovery::find(); 85 | $this->serializer = SerializerBuilder::create() 86 | ->configureListeners(function (EventDispatcher $dispatcher) { 87 | $dispatcher->addSubscriber(new DeserializeDataStoreResponseSubscriber()); 88 | $dispatcher->addSubscriber(new DeserializeBoundingBoxSubscriber()); 89 | $dispatcher->addSubscriber(new DeserializeCoverageStoreResponseSubscriber()); 90 | $dispatcher->addSubscriber(new DeserializeStyleResponseSubscriber()); 91 | }) 92 | ->build(); 93 | ; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Serializer/DeserializeBoundingBoxSubscriber.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Serializer; 23 | 24 | use JMS\Serializer\EventDispatcher\PreDeserializeEvent; 25 | use JMS\Serializer\EventDispatcher\EventSubscriberInterface; 26 | 27 | /** 28 | * Pre-Deserialize event for @see \OneOffTech\GeoServer\Models\BoundingBox 29 | * 30 | * It make sure that data before deserialization into the BoundingBox class 31 | * is in the expected format 32 | */ 33 | class DeserializeBoundingBoxSubscriber implements EventSubscriberInterface 34 | { 35 | public static function getSubscribedEvents() 36 | { 37 | return [ 38 | [ 39 | 'event' => 'serializer.pre_deserialize', 40 | 'method' => 'onPreDeserialize', 41 | 'class' => 'OneOffTech\\GeoServer\\Models\\BoundingBox', 42 | 'format' => 'json', 43 | 'priority' => 0, 44 | ], 45 | ]; 46 | } 47 | 48 | public function onPreDeserialize(PreDeserializeEvent $event) 49 | { 50 | $data = $event->getData(); 51 | 52 | // Convert a projected CSR response to string 53 | 54 | if (isset($data['crs']) && ! is_string($data['crs'])) { 55 | $crs = $data['crs']; 56 | 57 | if (isset($crs['@class']) && $crs['@class'] === 'projected') { 58 | $data['crs'] = $crs['$']; 59 | } 60 | } 61 | 62 | $event->setData($data); 63 | 64 | return true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Serializer/DeserializeCoverageStoreResponseSubscriber.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Serializer; 23 | 24 | use JMS\Serializer\EventDispatcher\PreDeserializeEvent; 25 | use JMS\Serializer\EventDispatcher\EventSubscriberInterface; 26 | 27 | /** 28 | * Pre-Deserialize event for @see \OneOffTech\GeoServer\Models\CoverageStore 29 | * 30 | * It make sure that data before deserialization into the CoverageStore class 31 | * is in the expected format 32 | */ 33 | class DeserializeCoverageStoreResponseSubscriber implements EventSubscriberInterface 34 | { 35 | public static function getSubscribedEvents() 36 | { 37 | return [ 38 | [ 39 | 'event' => 'serializer.pre_deserialize', 40 | 'method' => 'onPreDeserialize', 41 | 'class' => 'OneOffTech\\GeoServer\\Http\\Responses\\CoverageStoresResponse', 42 | 'format' => 'json', 43 | 'priority' => 1, 44 | ], 45 | [ 46 | 'event' => 'serializer.pre_deserialize', 47 | 'method' => 'onPreDeserialize', 48 | 'class' => 'OneOffTech\\GeoServer\\Models\\CoverageStore', 49 | 'format' => 'json', 50 | 'priority' => 0, 51 | ], 52 | ]; 53 | } 54 | 55 | public function onPreDeserialize(PreDeserializeEvent $event) 56 | { 57 | $data = $event->getData(); 58 | 59 | // The CoverageStoreResponse has an annoying multi-level 60 | // array. This aims at flatten the array to 61 | // a single level 62 | if (isset($data['coverageStores']) && is_array($data['coverageStores'])) { 63 | $data['coverageStores'] = array_map(function ($a) { 64 | return isset($a[0]) ? $a[0] : $a; 65 | }, array_values($data['coverageStores'])); 66 | } 67 | 68 | // The CoverageStore contain a reference to the workspace by 69 | // name and url. For the purpose of keeping the object 70 | // simple we transform the complex object into string 71 | if (isset($data['workspace']) && is_array($data['workspace'])) { 72 | $data['workspace'] = $data['workspace']['name']; 73 | } 74 | 75 | if (isset($data['coverages']) && is_string($data['coverages'])) { 76 | $data['coverages'] = [$data['coverages']]; 77 | } 78 | 79 | $event->setData($data); 80 | 81 | return true; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Serializer/DeserializeDataStoreResponseSubscriber.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Serializer; 23 | 24 | use JMS\Serializer\EventDispatcher\PreDeserializeEvent; 25 | use JMS\Serializer\EventDispatcher\EventSubscriberInterface; 26 | 27 | /** 28 | * Pre-Deserialize event for @see \OneOffTech\GeoServer\Models\DataStore 29 | * 30 | * It make sure that data before deserialization into the DataStore class 31 | * is in the expected format 32 | */ 33 | class DeserializeDataStoreResponseSubscriber implements EventSubscriberInterface 34 | { 35 | public static function getSubscribedEvents() 36 | { 37 | return [ 38 | [ 39 | 'event' => 'serializer.pre_deserialize', 40 | 'method' => 'onPreDeserialize', 41 | 'class' => 'OneOffTech\\GeoServer\\Models\\DataStore', 42 | 'format' => 'json', 43 | 'priority' => 0, 44 | ], 45 | [ 46 | 'event' => 'serializer.pre_deserialize', 47 | 'method' => 'onPreDeserialize', 48 | 'class' => 'OneOffTech\\GeoServer\\Http\\Responses\\DataStoreResponse', 49 | 'format' => 'json', 50 | 'priority' => 0, 51 | ], 52 | ]; 53 | } 54 | 55 | public function onPreDeserialize(PreDeserializeEvent $event) 56 | { 57 | $data = $event->getData(); 58 | 59 | // The DataStoreResponse has an annoying multi-level 60 | // array. This aims at flatten the array to 61 | // a single level 62 | if (isset($data['dataStores']) && is_array($data['dataStores'])) { 63 | $data['dataStores'] = array_map(function ($a) { 64 | return isset($a[0]) ? $a[0] : $a; 65 | }, array_values($data['dataStores'])); 66 | } 67 | 68 | // The DataStore has an annoying dataStore key that contain 69 | // the details. This aims at remove that key when 70 | // deserializing a Models\DataStore instance 71 | if (isset($data['dataStore']) && is_array($data['dataStore'])) { 72 | $data = $data['dataStore']; 73 | } 74 | 75 | // The DataStore contain a reference to the workspace by 76 | // name and url. For the purpose of keeping the object 77 | // simple we transform the complex object into string 78 | if (isset($data['workspace']) && is_array($data['workspace'])) { 79 | $data['workspace'] = $data['workspace']['name']; 80 | } 81 | 82 | $event->setData($data); 83 | 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Serializer/DeserializeStyleResponseSubscriber.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Serializer; 23 | 24 | use JMS\Serializer\EventDispatcher\PreDeserializeEvent; 25 | use JMS\Serializer\EventDispatcher\EventSubscriberInterface; 26 | 27 | /** 28 | * Pre-Deserialize event for @see \OneOffTech\GeoServer\Models\Style 29 | * 30 | * It make sure that data before deserialization into the Style class 31 | * is in the expected format 32 | */ 33 | class DeserializeStyleResponseSubscriber implements EventSubscriberInterface 34 | { 35 | public static function getSubscribedEvents() 36 | { 37 | return [ 38 | [ 39 | 'event' => 'serializer.pre_deserialize', 40 | 'method' => 'onPreDeserialize', 41 | 'class' => 'OneOffTech\\GeoServer\\Models\\Style', 42 | 'format' => 'json', 43 | 'priority' => 0, 44 | ], 45 | [ 46 | 'event' => 'serializer.pre_deserialize', 47 | 'method' => 'onPreDeserialize', 48 | 'class' => 'OneOffTech\\GeoServer\\Http\\Responses\\StylesResponse', 49 | 'format' => 'json', 50 | 'priority' => 0, 51 | ], 52 | ]; 53 | } 54 | 55 | public function onPreDeserialize(PreDeserializeEvent $event) 56 | { 57 | $data = $event->getData(); 58 | 59 | // The StyleResponse has an annoying multi-level 60 | // array. This aims at flatten the array to 61 | // a single level 62 | if (isset($data['styles']) && is_array($data['styles'])) { 63 | $data['styles'] = array_map(function ($a) { 64 | return isset($a[0]) ? $a[0] : $a; 65 | }, array_values($data['styles'])); 66 | } 67 | 68 | // The Style has an annoying style key that contain 69 | // the details. This aims at remove that key when 70 | // deserializing a Models\Style instance 71 | if (isset($data['style']) && is_array($data['style'])) { 72 | $data = $data['style']; 73 | } 74 | 75 | // The Style contain a reference to the workspace by 76 | // name. For the purpose of keeping the object simple 77 | // we transform the complex object into string 78 | if (isset($data['workspace']) && is_array($data['workspace'])) { 79 | $data['workspace'] = $data['workspace']['name'] ?? null; 80 | } 81 | 82 | // The Style object contains the version number in a 83 | // sub-object. We want it to be on the first level 84 | // for easy access 85 | if (isset($data['languageVersion']) && is_array($data['languageVersion'])) { 86 | $data['languageVersion'] = $data['languageVersion']['version'] ?? null; 87 | } 88 | 89 | $event->setData($data); 90 | 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/StyleFile.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer; 23 | 24 | use SplFileInfo; 25 | use OneOffTech\GeoServer\Support\TypeResolver; 26 | use OneOffTech\GeoServer\Exception\UnsupportedFileException; 27 | 28 | /** 29 | * A file that contains rendering style for a Web Map Service 30 | */ 31 | class StyleFile 32 | { 33 | const MIME_TYPE = "application/vnd.ogc.sld+xml"; 34 | 35 | protected $file; 36 | 37 | protected $name; 38 | 39 | protected $originalName; 40 | 41 | protected $mimeType; 42 | 43 | protected $extension; 44 | 45 | /** 46 | * The mime type as required by GeoServer 47 | * 48 | * e.g. for a geo tiff file the mime type appears to be "geotif/geotiff", 49 | * as found in https://gis.stackexchange.com/questions/218162/creating-coveragestore-geotiff-using-rest-api 50 | */ 51 | protected $normalizedMimeType; 52 | 53 | public function __construct($path) 54 | { 55 | $this->file = new SplFileInfo($path); 56 | 57 | list($format, $type, $mimeType) = TypeResolver::identify($path); 58 | 59 | if (! in_array($format, TypeResolver::supportedFormats())) { 60 | throw new UnsupportedFileException($path, $format, join(', ', TypeResolver::supportedFormats())); 61 | } 62 | 63 | $this->mimeType = $mimeType; 64 | 65 | $this->extension = $this->file->getExtension(); 66 | 67 | $this->normalizedMimeType = $mimeType; 68 | 69 | $this->originalName = $this->file->getFileName(); 70 | $this->name = str_replace('.sld', '', $this->originalName); 71 | } 72 | 73 | /** 74 | * Set the style name. 75 | * It will be used when creating the style in the GeoServer 76 | * 77 | * @param string $value 78 | * @return StyleFile 79 | */ 80 | public function name($value) 81 | { 82 | $this->name = $value; 83 | 84 | return $this; 85 | } 86 | 87 | public function content() 88 | { 89 | return file_get_contents($this->file->getRealPath()); 90 | } 91 | 92 | public function __get($property) 93 | { 94 | return $this->$property; 95 | } 96 | 97 | /** 98 | * Create a StyleFile instance from a given file path 99 | * 100 | * @param string $path 101 | * @return Data 102 | * @throws UnsupportedFileException if file is not supported 103 | */ 104 | public static function from($path) 105 | { 106 | return new static($path); 107 | } 108 | public static function load($path) 109 | { 110 | return static::from($path); 111 | } 112 | 113 | /** 114 | * Check if the specified file is a valid style file 115 | * 116 | * @param string $path 117 | * @return bool 118 | */ 119 | public static function isSupported(string $path) 120 | { 121 | list($format, $type, $mimeType) = TypeResolver::identify($path); 122 | return $mimeType === self::MIME_TYPE; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Support/BinaryReader.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Support; 23 | 24 | use Exception; 25 | use OneOffTech\GeoServer\Contracts\FileReader; 26 | 27 | final class BinaryReader extends FileReader 28 | { 29 | private static function isBigEndianMachine() 30 | { 31 | return current(unpack('v', pack('S', 0xff))) !== 0xff; 32 | } 33 | 34 | private static function readData($path, $type, $length, $position = 0, $invert_endianness = false) 35 | { 36 | $handle = self::openFileBinary($path, $position); 37 | $data = fread($handle, $length); 38 | self::closeFile($handle); 39 | 40 | if ($data === false) { 41 | return null; 42 | } 43 | if ($invert_endianness) { 44 | $data = strrev($data); 45 | } 46 | 47 | return current(unpack($type, $data)); 48 | } 49 | 50 | /** 51 | * Read a 32 bit integer from the beginning of a file 52 | * 53 | * @param string $path the file path to read from 54 | * @param bool $big_endian if the integer is in big endian notation. Default true 55 | * @return integer 56 | */ 57 | public static function readInt32($path, $position = 0, $big_endian = true) 58 | { 59 | return self::readData($path, $big_endian ? 'N' : 'V', 4, $position); 60 | } 61 | 62 | public static function readShort($path, $position = 0, $big_endian = true) 63 | { 64 | return self::readData($path, 's', 2, $position); 65 | } 66 | 67 | public static function isGeoTiff($path) 68 | { 69 | $handle = self::openFileBinary($path); 70 | 71 | // https://www.awaresystems.be/imaging/tiff/specification/TIFF6.pdf 72 | // 8 bytes header: 73 | // - 2 bytes for the byte order 74 | // - 2 bytes for the TIFF header 75 | // - 4 bytes for the offset to the first IFD. 76 | $tiffHeader = fread($handle, 8); 77 | 78 | if ($tiffHeader === false) { 79 | self::closeFile($handle); 80 | return false; 81 | } 82 | 83 | $byteOrder = current(unpack('a', $tiffHeader)).current(unpack('a', $tiffHeader, 1)); 84 | 85 | if (! in_array($byteOrder, ['MM', 'II'])) { 86 | // unknown byte order 87 | self::closeFile($handle); 88 | return false; 89 | } 90 | 91 | $big_endian = $byteOrder === 'MM' ? true : false; 92 | $tiffCode = current(unpack('s', $tiffHeader, 2)); 93 | 94 | if ($tiffCode !== 42) { 95 | // tiff code not found 96 | self::closeFile($handle); 97 | return false; 98 | } 99 | 100 | $byteOffset = self::getBytes($tiffHeader, 4, 4, $big_endian); 101 | 102 | fseek($handle, $byteOffset); 103 | 104 | $numDirData = fread($handle, 2); 105 | 106 | $numDirEntries = self::getBytes($numDirData, 2, 0, $big_endian); 107 | fseek($handle, $byteOffset+2); 108 | 109 | $imageFileDirectoriesData = fread($handle, (12 * $numDirEntries)+12); 110 | 111 | // from the Image File Directories record in the TIFF file I need the GeoKeyDirectory 112 | // and the values in the GeoKeyDirectory, which has field code 34735 113 | // https://www.geospatialworld.net/article/geotiff-a-standard-image-file-format-for-gis-applications/ 114 | 115 | // Even if from https://github.com/xlhomme/GeotiffParser.js/blob/master/js/GeotiffParser.js 116 | // seems that the GeoKeyDirectory field should have 4 values to be a valid GeoTiff 117 | // we consider the presence of the tag a valid indicator 118 | 119 | $hasGeoKeyDirectory = false; 120 | for ($i = 0, $entryCount = 0; $entryCount < $numDirEntries; $i += 12, $entryCount++) { 121 | $fieldTag = self::getBytes($imageFileDirectoriesData, 2, $i, $big_endian); 122 | 123 | if ($fieldTag === 34735) { // GeoKeyDirectory field 124 | $hasGeoKeyDirectory = true; 125 | break; 126 | } 127 | } 128 | 129 | self::closeFile($handle); 130 | 131 | return $hasGeoKeyDirectory; 132 | } 133 | 134 | public static function isGeoPackage($path) 135 | { 136 | $handle = self::openFileBinary($path); 137 | $sqliteMagic = self::getString(fread($handle, 16), 15); 138 | fseek($handle, 68); 139 | $gpkgMagic = self::getString(fread($handle, 4), 4); 140 | self::closeFile($handle); 141 | 142 | if ($sqliteMagic !== 'SQLite format 3') { 143 | return false; 144 | } 145 | 146 | if ($gpkgMagic !== 'GPKG') { 147 | return false; 148 | } 149 | 150 | return true; 151 | } 152 | 153 | private static function getBytes($data, $length, $offset = 0, $big_endian = true) 154 | { 155 | if ($length <= 2) { 156 | return current(unpack($big_endian ? 'n' : 'v', $data, $offset)); 157 | } elseif ($length <= 4) { 158 | return current(unpack($big_endian ? 'N' : 'V', $data, $offset)); 159 | } 160 | // unsigned short 16bit current(unpack($big_endian ? 'n' : 'v', $tiffHeader, 4)); 161 | // unsigned long 32bit current(unpack($big_endian ? 'N' : 'V', $tiffHeader, 4)); 162 | } 163 | 164 | private static function getString($data, $length, $offset = 0) 165 | { 166 | try { 167 | $chars = []; 168 | 169 | for ($i=$offset; $i < $length; $i++) { 170 | $chars[] = current(unpack('a', $data, $i)); 171 | } 172 | 173 | return implode('', $chars); 174 | } catch (Exception $ex) { 175 | return ''; 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Support/ImageResponse.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Support; 23 | 24 | use Psr\Http\Message\ResponseInterface; 25 | 26 | final class ImageResponse 27 | { 28 | private $response = null; 29 | 30 | public function __construct(ResponseInterface $response) 31 | { 32 | $this->response = $response; 33 | } 34 | 35 | public function mimeType() 36 | { 37 | $contentTypeHeader = $this->response->getHeader('Content-Type'); 38 | return $contentTypeHeader[0] ?? 'application/octet-stream'; 39 | } 40 | 41 | public function asString() 42 | { 43 | return (string)$this->response->getBody(); 44 | } 45 | 46 | public static function from(ResponseInterface $response) 47 | { 48 | return new static($response); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Support/TextReader.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Support; 23 | 24 | use OneOffTech\GeoServer\Contracts\FileReader; 25 | 26 | final class TextReader extends FileReader 27 | { 28 | 29 | /** 30 | * Read a line from file 31 | * 32 | * @param string $path the file path to read from 33 | * @param integer $lines the number of lines to read 34 | * @return array 35 | */ 36 | public static function readLines($path, $lines = 1) 37 | { 38 | $data = []; 39 | 40 | $handle = self::openFile($path); 41 | for ($i=0; $i < $lines; $i++) { 42 | $data[] = fgets($handle); 43 | } 44 | self::closeFile($handle); 45 | 46 | return $data; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Support/TypeResolver.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Support; 23 | 24 | use OneOffTech\GeoServer\GeoType; 25 | use OneOffTech\GeoServer\GeoFormat; 26 | 27 | final class TypeResolver 28 | { 29 | protected static $mimeTypes = [ 30 | GeoFormat::SHAPEFILE => 'application/octet-stream', // shapefile 31 | GeoFormat::SHAPEFILE_ZIP => 'application/zip', // shapefile in ZIP container 32 | GeoFormat::GEOTIFF => 'image/tiff', // geotiff 33 | GeoFormat::SLD => 'application/vnd.ogc.sld+xml', 34 | GeoFormat::GEOPACKAGE => 'application/geopackage+sqlite3', 35 | ]; 36 | 37 | protected static $mimeTypeToFormat = []; 38 | 39 | protected static $typesMap = [ 40 | GeoFormat::SHAPEFILE => GeoType::VECTOR, 41 | GeoFormat::SHAPEFILE_ZIP => GeoType::VECTOR, 42 | GeoFormat::GEOTIFF => GeoType::RASTER, 43 | GeoFormat::GEOPACKAGE => GeoType::VECTOR, 44 | 45 | GeoType::VECTOR => [ 46 | GeoFormat::SHAPEFILE, 47 | GeoFormat::SHAPEFILE_ZIP, 48 | GeoFormat::GEOPACKAGE, 49 | ], 50 | GeoType::RASTER => [ 51 | GeoFormat::GEOTIFF, 52 | ] 53 | ]; 54 | 55 | /** 56 | * The file extension, given the file format, as accepted by GeoServer 57 | */ 58 | protected static $normalizedFormatFileExtensions = [ 59 | GeoFormat::SHAPEFILE => 'shp', 60 | GeoFormat::SHAPEFILE_ZIP => 'shp', 61 | GeoFormat::GEOTIFF => 'geotiff', 62 | GeoFormat::GEOPACKAGE => 'gpkg', 63 | ]; 64 | 65 | /** 66 | * The file mime type, given the file format, as accepted by GeoServer 67 | */ 68 | protected static $normalizedMimeTypeFileFormat = [ 69 | GeoFormat::GEOTIFF => 'geotif/geotiff', // as found on https://gis.stackexchange.com/questions/218162/creating-coveragestore-geotiff-using-rest-api 70 | GeoFormat::SHAPEFILE_ZIP => 'application/zip', 71 | ]; 72 | 73 | public static function identify($path) 74 | { 75 | $mimeType = mime_content_type($path); 76 | 77 | // try to recognize the format from the mime type. 78 | // this works for files that have a specific mime type 79 | $format = isset(self::$mimeTypeToFormat[$mimeType]) ? self::$mimeTypeToFormat[$mimeType] : null; 80 | 81 | // for some files the mime type is too generic 82 | // so additional checks are required 83 | if ($mimeType === self::$mimeTypes[GeoFormat::SHAPEFILE]) { 84 | // According to https://www.esri.com/library/whitepapers/pdfs/shapefile.pdf 85 | // the first 4 bytes of a shapefile are always the number 9994 86 | $code = BinaryReader::readInt32($path); 87 | 88 | if ($code === 9994) { 89 | $format = GeoFormat::SHAPEFILE; 90 | } 91 | } 92 | 93 | if ($mimeType === 'application/zip') { 94 | 95 | // could be a compressed shapefile 96 | // Check if the zip file contains at least 1 shapefile 97 | $containsShp = ZipReader::containsFile($path, '.shp'); 98 | 99 | if ($containsShp) { 100 | $format = GeoFormat::SHAPEFILE_ZIP; 101 | $mimeType = self::$mimeTypes[GeoFormat::SHAPEFILE_ZIP]; 102 | } 103 | } elseif ($mimeType === 'image/tiff' && BinaryReader::isGeoTiff($path)) { 104 | $format = GeoFormat::GEOTIFF; 105 | } elseif ($mimeType === 'application/xml' || $mimeType === 'text/xml') { 106 | 107 | // check if Style tag is present 108 | $data = join('', TextReader::readLines($path, 2)); 109 | if (strpos($data, '. 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Support; 23 | 24 | use LogicException; 25 | use ReflectionClass; 26 | use InvalidArgumentException; 27 | use OneOffTech\GeoServer\Models\BoundingBox; 28 | 29 | /** 30 | * Options for the Web Map Service (WMS) 31 | * 32 | * Helper class to create the parameters for the WMS service call 33 | */ 34 | final class WmsOptions 35 | { 36 | /** 37 | * Output format for PNG image 38 | */ 39 | const OUTPUT_PNG = "image/png"; 40 | 41 | /** 42 | * Same as PNG, but computes an optimal 256 color (8 bit) palette, so the image size is usually smaller 43 | */ 44 | const OUTPUT_PNG8 = "image/png8"; 45 | 46 | /** 47 | * 48 | */ 49 | const OUTPUT_JPEG = "image/jpeg"; 50 | 51 | /** 52 | * A custom format that will decide dynamically, based on the image contents, if it’s best to use a JPEG or PNG compression. The images are returned in JPEG format if fully opaque and not paletted. In order to use this format in a meaningful way the GetMap must include a “&transparent=TRUE” parameter, as without it GeoServer generates opaque images with the default/requested background color, making this format always return JPEG images (or always PNG, if they are paletted). When using the layer preview to test this format, remember to add “&transparent=TRUE” to the preview URL, as normally the preview generates non transparent images. 53 | */ 54 | const OUTPUT_JPEG_PNG = "image/vnd.jpeg-png"; 55 | 56 | /** 57 | * 58 | */ 59 | const OUTPUT_GIF = "image/gif"; 60 | 61 | /** 62 | * 63 | */ 64 | const OUTPUT_TIFF = "image/tiff"; 65 | 66 | /** 67 | * Same as TIFF, but computes an optimal 256 color (8 bit) palette, so the image size is usually smaller 68 | */ 69 | const OUTPUT_TIFF8 = "image/tiff8"; 70 | 71 | /** 72 | * Same as TIFF, but includes extra GeoTIFF metadata 73 | */ 74 | const OUTPUT_GEOTIFF = "image/geotiff"; 75 | 76 | /** 77 | * Same as TIFF, but includes extra GeoTIFF metadata and computes an optimal 256 color (8 bit) palette, so the image size is usually smaller 78 | */ 79 | const OUTPUT_GEOTIFF8 = "image/geotiff8"; 80 | 81 | /** 82 | * 83 | */ 84 | const OUTPUT_SVG = "image/svg"; 85 | 86 | /** 87 | * 88 | */ 89 | const OUTPUT_PDF = "application/pdf"; 90 | 91 | /** 92 | * 93 | */ 94 | const OUTPUT_GEORSS = "rss"; 95 | 96 | /** 97 | * 98 | */ 99 | const OUTPUT_KML = "kml"; 100 | 101 | /** 102 | * 103 | */ 104 | const OUTPUT_KMZ = "kmz"; 105 | 106 | /** 107 | * Generates an OpenLayers HTML application. 108 | */ 109 | const OUTPUT_OPENLAYERS = "application/openlayers"; 110 | 111 | /** 112 | * Generates an UTFGrid 1.3 JSON response. Requires vector output, either from a vector layer, or from a raster layer turned into vectors by a rendering transformation. 113 | */ 114 | const OUTPUT_UTFGRID = "application/json;type=utfgrid"; 115 | 116 | private $format = self::OUTPUT_PNG; 117 | 118 | private $bbox = null; 119 | 120 | private $layers = null; 121 | 122 | private $styles = null; 123 | 124 | private $width = 640; 125 | 126 | private $height = 480; 127 | 128 | private $srs = "EPSG:4326"; 129 | 130 | private $version = "1.1.0"; 131 | 132 | private $request = "GetMap"; 133 | 134 | private function isFormatValid($format) 135 | { 136 | return in_array($format, $this->supportedFormats()); 137 | } 138 | 139 | public function supportedFormats() 140 | { 141 | $constants = (new ReflectionClass(get_called_class()))->getConstants(); 142 | 143 | return array_values($constants); 144 | } 145 | 146 | public function format($format) 147 | { 148 | if (! $this->isFormatValid($format)) { 149 | throw new InvalidArgumentException("Unrecognized format [$format] Expected one of [".join(",", $this->supportedFormats())."]"); 150 | } 151 | 152 | $this->format = $format; 153 | return $this; 154 | } 155 | 156 | public function srs($srs) 157 | { 158 | $this->srs = $srs; 159 | return $this; 160 | } 161 | 162 | public function layers($layers) 163 | { 164 | $this->layers = is_array($layers) ? $layers : [$layers]; 165 | return $this; 166 | } 167 | 168 | public function styles($styles) 169 | { 170 | $this->styles = is_array($styles) ? $styles : [$styles]; 171 | return $this; 172 | } 173 | 174 | public function size($width, $height) 175 | { 176 | $this->width = $width; 177 | $this->height = $height; 178 | return $this; 179 | } 180 | 181 | public function boundingBox(BoundingBox $boundingBox) 182 | { 183 | $this->bbox = $boundingBox; 184 | return $this; 185 | } 186 | 187 | public function toArray() 188 | { 189 | if (empty($this->layers)) { 190 | throw new LogicException("Layers cannot be null or empty"); 191 | } 192 | 193 | if (is_null($this->bbox)) { 194 | throw new LogicException("Bounding box cannot be null"); 195 | } 196 | 197 | return [ 198 | 'request' => $this->request, 199 | 'version' => $this->version, 200 | 'format' => $this->format, 201 | 'layers' => $this->layers, 202 | 'bbox' => $this->bbox->toArray(), 203 | 'srs' => $this->srs, 204 | 'width' => $this->width, 205 | 'height' => $this->height, 206 | 'styles' => $this->styles ?? [], 207 | ]; 208 | } 209 | 210 | public function toUrlParameters() 211 | { 212 | $params = [ 213 | 'version' => $this->version, 214 | 'request' => $this->request, 215 | 'layers' => join(',', $this->layers), 216 | 'bbox' => join(',', $this->bbox->toArray()), 217 | 'styles' => join(',', $this->styles ?? []), 218 | 'width' => $this->width, 219 | 'height' => $this->height, 220 | 'srs' => $this->srs, 221 | 'format' => urlencode($this->format), 222 | ]; 223 | 224 | $collapsedParams = array_map(function ($key, $value) { 225 | return "$key=$value"; 226 | }, array_keys($params), $params); 227 | 228 | return join("&", $collapsedParams); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Support/ZipReader.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace OneOffTech\GeoServer\Support; 23 | 24 | use Exception; 25 | use ZipArchive; 26 | use OneOffTech\GeoServer\Contracts\FileReader; 27 | 28 | final class ZipReader extends FileReader 29 | { 30 | 31 | /** 32 | * Read a 32 bit integer from the beginning of a file 33 | * 34 | * @param string $path the file path to read from 35 | * @param bool $big_endian if the integer is in big endian notation. Default true 36 | * @return integer 37 | */ 38 | public static function contentList($path) 39 | { 40 | $entries = []; 41 | 42 | $za = new ZipArchive; 43 | $za->open($path); 44 | 45 | for ($i=0; $i < $za->numFiles; $i++) { 46 | $entry = $za->statIndex($i); 47 | $entries[] = $entry['name']; 48 | } 49 | 50 | $za->close(); 51 | 52 | return $entries; 53 | } 54 | 55 | public static function containsFile($path, $name) 56 | { 57 | $entries = []; 58 | 59 | $za = new ZipArchive; 60 | $za->open($path); 61 | 62 | for ($i=0; $i < $za->numFiles; $i++) { 63 | $entry = $za->statIndex($i); 64 | if (strpos($entry['name'], $name) !== false) { 65 | $entries[] = $entry['name']; 66 | } 67 | } 68 | 69 | $za->close(); 70 | 71 | return count($entries) > 0; 72 | } 73 | 74 | /** 75 | * Tap into the Zip Archive 76 | * 77 | * After the callback is executed the ZIP archive is closed and saved 78 | * 79 | * @param string The zip file path 80 | * @param callable The function to execute when the zip file is opened. This function receive a ZipArchive instance as argument 81 | * @return string The zip file path 82 | */ 83 | public static function tap($path, $callback) 84 | { 85 | $za = new ZipArchive; 86 | $za->open($path); 87 | 88 | try { 89 | $callback($za); 90 | } catch (Exception $ex) { 91 | throw $ex; 92 | } finally { 93 | $za->close(); 94 | } 95 | 96 | return $path; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Concern/GeneratesData.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Concern; 23 | 24 | trait GeneratesData 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /tests/Concern/SetupIntegrationTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Concern; 23 | 24 | use OneOffTech\GeoServer\GeoServer; 25 | use OneOffTech\GeoServer\Auth\Authentication; 26 | 27 | trait SetupIntegrationTest 28 | { 29 | /** 30 | * @var \OneOffTech\GeoServer\GeoServer 31 | */ 32 | protected $geoserver = null; 33 | 34 | protected function setUp(): void 35 | { 36 | parent::setUp(); 37 | 38 | $url = getenv('GEOSERVER_URL'); 39 | $workspace = getenv('GEOSERVER_WORKSPACE'); 40 | 41 | if (empty($url)) { 42 | $this->markTestSkipped('The GEOSERVER_URL is not configured.'); 43 | } 44 | 45 | $auth = new Authentication(getenv('GEOSERVER_USER'), getenv('GEOSERVER_PASSWORD')); 46 | 47 | $this->geoserver = GeoServer::build($url, $workspace, $auth); 48 | 49 | $this->geoserver->createWorkspace(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Integration/GeoServerCoverageStoresTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Integration; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\GeoFile; 26 | use OneOffTech\GeoServer\GeoType; 27 | use Tests\Concern\SetupIntegrationTest; 28 | use OneOffTech\GeoServer\Models\Coverage; 29 | use OneOffTech\GeoServer\Models\CoverageStore; 30 | use OneOffTech\GeoServer\Exception\StoreNotFoundException; 31 | 32 | class GeoServerCoverageStoresTest extends TestCase 33 | { 34 | use SetupIntegrationTest; 35 | 36 | public function test_geotiff_can_be_uploaded() 37 | { 38 | $storeName = 'geotiff_test'; 39 | $data = GeoFile::from(__DIR__.'/../fixtures/geotiff.tiff')->name($storeName); 40 | 41 | $coverage = $this->geoserver->upload($data); 42 | 43 | $this->assertInstanceOf(Coverage::class, $coverage); 44 | $this->assertEquals(GeoType::RASTER, $coverage->type()); 45 | $this->assertEquals("geotiff_test", $coverage->name); 46 | $this->assertEquals("geotiff_test", $coverage->title); 47 | $this->assertEquals("geotiff_test", $coverage->nativeName); 48 | $this->assertEquals("GeoTIFF", $coverage->nativeFormat); 49 | $this->assertFalse($coverage->skipNumberMatched); 50 | $this->assertFalse($coverage->circularArcPresent); 51 | $this->assertNotNull($coverage->store); 52 | $this->assertNotNull($coverage->keywords); 53 | $this->assertNotNull($coverage->nativeBoundingBox); 54 | $this->assertNotNull($coverage->boundingBox); 55 | $this->assertNotEmpty($coverage->interpolationMethods); 56 | $this->assertEquals(78999, $coverage->nativeBoundingBox->minX); 57 | $this->assertEquals(1412948.0000000002, $coverage->nativeBoundingBox->minY); 58 | $this->assertEquals(101839, $coverage->nativeBoundingBox->maxX); 59 | $this->assertEquals(1439268.0000000002, $coverage->nativeBoundingBox->maxY); 60 | $this->assertEquals(-83.64980947326015, $coverage->boundingBox->minX); 61 | $this->assertEquals(42.724764597615966, $coverage->boundingBox->minY); 62 | $this->assertEquals(-83.36533095896407, $coverage->boundingBox->maxX); 63 | $this->assertEquals(42.96491963803106, $coverage->boundingBox->maxY); 64 | $this->assertEquals("EPSG:4326", $coverage->boundingBox->crs); 65 | 66 | return $storeName; 67 | } 68 | 69 | /** 70 | * @depends test_geotiff_can_be_uploaded 71 | */ 72 | public function test_coveragestore_can_be_retrieved_by_name($coveragestoreName) 73 | { 74 | $coveragestore = $this->geoserver->coveragestore($coveragestoreName); 75 | 76 | $this->assertInstanceOf(CoverageStore::class, $coveragestore); 77 | $this->assertEquals(getenv('GEOSERVER_WORKSPACE'), $coveragestore->workspace); 78 | $this->assertEmpty($coveragestore->href); 79 | $this->assertEquals('file:data/test/geotiff_test/geotiff_test.geotiff', $coveragestore->url); 80 | $this->assertEquals('GeoTIFF', $coveragestore->type); 81 | $this->assertTrue($coveragestore->enabled); 82 | $this->assertTrue($coveragestore->exists); 83 | $this->assertCount(1, $coveragestore->coverages); 84 | 85 | return $coveragestoreName; 86 | } 87 | 88 | /** 89 | * @depends test_coveragestore_can_be_retrieved_by_name 90 | */ 91 | public function test_coveragestores_are_retrieved($coveragestoreName) 92 | { 93 | $coveragestores = $this->geoserver->coveragestores(); 94 | 95 | $this->assertContainsOnlyInstancesOf(CoverageStore::class, $coveragestores); 96 | 97 | return $coveragestoreName; 98 | } 99 | 100 | /** 101 | * @depends test_coveragestores_are_retrieved 102 | */ 103 | public function test_coveragestore_can_be_deleted($coveragestoreName) 104 | { 105 | $coveragestore = $this->geoserver->deleteCoveragestore($coveragestoreName); 106 | 107 | $this->assertInstanceOf(CoverageStore::class, $coveragestore); 108 | $this->assertEquals(getenv('GEOSERVER_WORKSPACE'), $coveragestore->workspace); 109 | $this->assertTrue($coveragestore->enabled); 110 | $this->assertFalse($coveragestore->exists); 111 | 112 | return $coveragestoreName; 113 | } 114 | 115 | public function test_non_existing_coveragestore_cannot_be_retrieved() 116 | { 117 | $this->expectException(StoreNotFoundException::class); 118 | 119 | $coveragestore = $this->geoserver->coveragestore('some_name'); 120 | } 121 | 122 | public function test_geotiff_upload_and_deleted() 123 | { 124 | $storeName = 'geotiff_test'; 125 | $data = GeoFile::from(__DIR__.'/../fixtures/geotiff.tiff')->name($storeName); 126 | 127 | $feature = $this->geoserver->upload($data); 128 | 129 | $this->assertInstanceOf(Coverage::class, $feature); 130 | 131 | $this->assertTrue($this->geoserver->exist($data), "Data not existing after upload"); 132 | 133 | $deleteResult = $this->geoserver->remove($data); 134 | 135 | $this->assertTrue($deleteResult, "GeoFile not deleted"); 136 | 137 | $this->assertFalse($this->geoserver->exist($data), "Data still exists after remove"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/Integration/GeoServerStylesTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Integration; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\StyleFile; 26 | use OneOffTech\GeoServer\Models\Style; 27 | use Tests\Concern\SetupIntegrationTest; 28 | use OneOffTech\GeoServer\Exception\StyleNotFoundException; 29 | 30 | class GeoServerStylesTest extends TestCase 31 | { 32 | use SetupIntegrationTest; 33 | 34 | public function test_styles_can_be_uploaded() 35 | { 36 | $styleName = 'style_test'; 37 | $data = StyleFile::from(__DIR__.'/../fixtures/style.sld')->name($styleName); 38 | 39 | $style = $this->geoserver->uploadStyle($data); 40 | 41 | $this->assertInstanceOf(Style::class, $style); 42 | $this->assertEquals($styleName, $style->name); 43 | $this->assertEquals(getenv('GEOSERVER_WORKSPACE'), $style->workspace); 44 | $this->assertEquals('style_test.sld', $style->filename); 45 | $this->assertEquals('sld', $style->format); 46 | $this->assertEquals('1.0.0', $style->version); 47 | $this->assertTrue($style->exists, "Style not existing"); 48 | 49 | return $styleName; 50 | } 51 | 52 | /** 53 | * @depends test_styles_can_be_uploaded 54 | */ 55 | public function test_style_can_be_retrieved_by_name($styleName = 'style_test') 56 | { 57 | $style = $this->geoserver->style($styleName); 58 | 59 | $this->assertInstanceOf(Style::class, $style); 60 | $this->assertEquals($styleName, $style->name); 61 | $this->assertEquals(getenv('GEOSERVER_WORKSPACE'), $style->workspace); 62 | $this->assertEquals('style_test.sld', $style->filename); 63 | $this->assertEquals('sld', $style->format); 64 | $this->assertEquals('1.0.0', $style->version); 65 | $this->assertTrue($style->exists, "Style not existing"); 66 | 67 | return $styleName; 68 | } 69 | 70 | /** 71 | * @depends test_style_can_be_retrieved_by_name 72 | */ 73 | public function test_styles_are_retrieved($datastoreName) 74 | { 75 | $styles = $this->geoserver->styles(); 76 | 77 | $this->assertContainsOnlyInstancesOf(Style::class, $styles); 78 | 79 | return $datastoreName; 80 | } 81 | 82 | /** 83 | * @depends test_styles_are_retrieved 84 | */ 85 | public function test_style_can_be_deleted($styleName) 86 | { 87 | $style = $this->geoserver->removeStyle($styleName); 88 | 89 | $this->assertInstanceOf(Style::class, $style); 90 | $this->assertEquals($styleName, $style->name); 91 | $this->assertEquals(getenv('GEOSERVER_WORKSPACE'), $style->workspace); 92 | $this->assertEquals('style_test.sld', $style->filename); 93 | $this->assertEquals('sld', $style->format); 94 | $this->assertEquals('1.0.0', $style->version); 95 | $this->assertFalse($style->exists, "Style still exists after deletion"); 96 | } 97 | 98 | public function test_non_existing_style_cannot_be_retrieved() 99 | { 100 | $this->expectException(StyleNotFoundException::class); 101 | 102 | $style = $this->geoserver->style('some_name'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Integration/GeoServerVersionRetrievalTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Integration; 23 | 24 | use Tests\TestCase; 25 | use Tests\Concern\SetupIntegrationTest; 26 | 27 | class GeoServerVersionRetrievalTest extends TestCase 28 | { 29 | use SetupIntegrationTest; 30 | 31 | public function test_geoserver_version_is_retrieved() 32 | { 33 | $version = $this->geoserver->version(); 34 | 35 | $this->assertNotEmpty($version); 36 | $this->assertTrue(is_string($version)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Integration/GeoServerWmsTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Integration; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\GeoFile; 26 | use Tests\Concern\SetupIntegrationTest; 27 | use OneOffTech\GeoServer\Support\ImageResponse; 28 | use Tests\Support\ImageDifference; 29 | 30 | class GeoServerWmsTest extends TestCase 31 | { 32 | use SetupIntegrationTest; 33 | 34 | public function test_wms_url_is_generated_for_shapefile() 35 | { 36 | $datastoreName = 'shapefile_test'; 37 | $file = GeoFile::from(__DIR__.'/../fixtures/shapefile.shp')->name($datastoreName); 38 | 39 | $resource = $this->geoserver->upload($file); 40 | 41 | $expectedParams = "layers=".getenv('GEOSERVER_WORKSPACE').":shapefile_test&bbox=314618.446,5536155.822,315358.647,5536652.114&styles=&width=640&height=480&srs=EPSG:4326&format=image%2Fpng"; 42 | $expectedMapUrl = sprintf("%s%s/wms?service=WMS&version=1.1.0&request=GetMap&%s", getenv('GEOSERVER_URL'), getenv('GEOSERVER_WORKSPACE'), $expectedParams); 43 | 44 | $mapUrlGeoFile = $this->geoserver->wmsMapUrl($file); 45 | $mapUrlResource = $this->geoserver->wmsMapUrl($resource); 46 | 47 | $this->assertEquals($expectedMapUrl, $mapUrlGeoFile); 48 | $this->assertEquals($mapUrlGeoFile, $mapUrlResource); 49 | 50 | $deleteResult = $this->geoserver->remove($file); 51 | } 52 | 53 | public function test_wms_url_is_generated_for_geotiff() 54 | { 55 | $datastoreName = 'geotiff_test'; 56 | $file = GeoFile::from(__DIR__.'/../fixtures/geotiff.tiff')->name($datastoreName); 57 | 58 | $resource = $this->geoserver->upload($file); 59 | 60 | $expectedParams = "layers=".getenv('GEOSERVER_WORKSPACE').":geotiff_test&bbox=-83.64980947326,42.724764597616,-83.365330958964,42.964919638031&styles=&width=640&height=480&srs=EPSG:4326&format=image%2Fpng"; 61 | $expectedMapUrl = sprintf("%s%s/wms?service=WMS&version=1.1.0&request=GetMap&%s", getenv('GEOSERVER_URL'), getenv('GEOSERVER_WORKSPACE'), $expectedParams); 62 | 63 | $mapUrlGeoFile = $this->geoserver->wmsMapUrl($file); 64 | $mapUrlResource = $this->geoserver->wmsMapUrl($resource); 65 | 66 | $this->assertEquals($expectedMapUrl, $mapUrlGeoFile); 67 | $this->assertEquals($mapUrlGeoFile, $mapUrlResource); 68 | 69 | $deleteResult = $this->geoserver->remove($file); 70 | } 71 | 72 | public function test_shapefile_thumbnail() 73 | { 74 | $datastoreName = 'shapefile_test'; 75 | $file = GeoFile::from(__DIR__.'/../fixtures/shapefile.shp')->name($datastoreName); 76 | 77 | $resource = $this->geoserver->upload($file); 78 | 79 | $thumbnail = $this->geoserver->thumbnail($resource); 80 | 81 | $this->assertInstanceOf(ImageResponse::class, $thumbnail); 82 | $this->assertEquals('image/png', $thumbnail->mimeType()); 83 | 84 | $imageAsString = $thumbnail->asString(); 85 | 86 | list($width, $height) = getimagesizefromstring($imageAsString); 87 | 88 | $this->assertEquals(300, $width); 89 | $this->assertEquals(300, $height); 90 | 91 | // compare the image difference against a reference thumbnail 92 | 93 | file_put_contents(__DIR__.'/../fixtures/shapefile_thumbnail_from_geoserver.png', $thumbnail->asString()); 94 | 95 | $differencePercentage = ImageDifference::calculate( 96 | __DIR__.'/../fixtures/shapefile_thumbnail.png', 97 | __DIR__.'/../fixtures/shapefile_thumbnail_from_geoserver.png' 98 | ); 99 | 100 | unlink(__DIR__.'/../fixtures/shapefile_thumbnail_from_geoserver.png'); 101 | $deleteResult = $this->geoserver->remove($file); 102 | 103 | // considering a 20% difference as acceptable 104 | $this->assertTrue($differencePercentage < 20); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Integration/GeoServerWorkspaceTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Integration; 23 | 24 | use Tests\TestCase; 25 | use Tests\Concern\SetupIntegrationTest; 26 | use OneOffTech\GeoServer\Models\Workspace; 27 | 28 | class GeoServerWorkspaceTest extends TestCase 29 | { 30 | use SetupIntegrationTest; 31 | 32 | public function test_workspace_details_are_retrieved() 33 | { 34 | $workspace = $this->geoserver->workspace(); 35 | 36 | $this->assertInstanceOf(Workspace::class, $workspace); 37 | $this->assertEquals(getenv('GEOSERVER_WORKSPACE'), $workspace->name); 38 | $this->assertNotEmpty($workspace->dataStores); 39 | $this->assertNotEmpty($workspace->coverageStores); 40 | $this->assertNotEmpty($workspace->wmsStores); 41 | $this->assertNotEmpty($workspace->wmtsStores); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Support/ImageDifference.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Support; 23 | 24 | use InvalidArgumentException; 25 | 26 | class ImageDifference 27 | { 28 | 29 | /** 30 | * Calculate the difference between two images 31 | * 32 | * @param string $expected The path of the image ground truth 33 | * @param string $actual The actual image to compare 34 | * @return float the percentage of difference between the two images 35 | */ 36 | public static function calculate($expected, $actual) 37 | { 38 | list($expectedImage, $expectedImagewidth, $expectedImageHeight) = static::imageFromFile($expected); 39 | 40 | list($actualImage) = static::imageFromFile($actual); 41 | 42 | $differenceBitmap = static::calculateDifference( 43 | $expectedImage, 44 | $actualImage, 45 | $expectedImagewidth, 46 | $expectedImageHeight 47 | ); 48 | 49 | return static::calculateDifferencePercentage($differenceBitmap, $expectedImagewidth, $expectedImageHeight); 50 | } 51 | 52 | /** 53 | * Load a bitmap array from image path. 54 | * 55 | * @param string $path 56 | * 57 | * @return array 58 | * 59 | * @throws InvalidArgumentException 60 | */ 61 | private static function imageFromFile($path) 62 | { 63 | $info = getimagesize($path); 64 | $type = $info[2]; 65 | 66 | $image = null; 67 | 68 | if ($type == IMAGETYPE_JPEG) { 69 | $image = imagecreatefromjpeg($path); 70 | } 71 | if ($type == IMAGETYPE_GIF) { 72 | $image = imagecreatefromgif($path); 73 | } 74 | if ($type == IMAGETYPE_PNG) { 75 | $image = imagecreatefrompng($path); 76 | } 77 | 78 | if (! $image) { 79 | throw new InvalidArgumentException("invalid image [{$path}]"); 80 | } 81 | 82 | $width = imagesx($image); 83 | $height = imagesy($image); 84 | 85 | $bitmap = []; 86 | 87 | for ($y = 0; $y < $height; $y++) { 88 | $bitmap[$y] = []; 89 | 90 | for ($x = 0; $x < $width; $x++) { 91 | $color = imagecolorat($image, $x, $y); 92 | 93 | $bitmap[$y][$x] = [ 94 | "r" => ($color >> 16) & 0xFF, 95 | "g" => ($color >> 8) & 0xFF, 96 | "b" => $color & 0xFF 97 | ]; 98 | } 99 | } 100 | 101 | return [$bitmap, $width, $height]; 102 | } 103 | 104 | /** 105 | * Difference between all pixels of two images. 106 | * 107 | * @param array $bitmap1 108 | * @param array $bitmap2 109 | * @param int $width 110 | * @param int $height 111 | * 112 | * @return array 113 | */ 114 | private static function calculateDifference(array $bitmap1, array $bitmap2, $width, $height) 115 | { 116 | $new = []; 117 | 118 | for ($y = 0; $y < $height; $y++) { 119 | $new[$y] = []; 120 | 121 | for ($x = 0; $x < $width; $x++) { 122 | $new[$y][$x] = static::euclideanDistance( 123 | $bitmap1[$y][$x], 124 | $bitmap2[$y][$x] 125 | ); 126 | } 127 | } 128 | 129 | return $new; 130 | } 131 | 132 | /** 133 | * RGB color distance for the same pixel in two images. 134 | * 135 | * @link https://en.wikipedia.org/wiki/Euclidean_distance 136 | * 137 | * @param array $p 138 | * @param array $q 139 | * 140 | * @return float 141 | */ 142 | private static function euclideanDistance(array $p, array $q) 143 | { 144 | $r = $p["r"] - $q["r"]; 145 | $r *= $r; 146 | 147 | $g = $p["g"] - $q["g"]; 148 | $g *= $g; 149 | 150 | $b = $p["b"] - $q["b"]; 151 | $b *= $b; 152 | 153 | return (float) sqrt($r + $g + $b); 154 | } 155 | 156 | /** 157 | * Percentage of different pixels in the bitmap. 158 | * 159 | * @param array $bitmap 160 | * @param int $width 161 | * @param int $height 162 | * 163 | * @return float 164 | */ 165 | private static function calculateDifferencePercentage(array $bitmap, $width, $height) 166 | { 167 | $total = 0; 168 | $different = 0; 169 | 170 | for ($y = 0; $y < $height; $y++) { 171 | for ($x = 0; $x < $width; $x++) { 172 | $total++; 173 | 174 | if ($bitmap[$y][$x] > 0) { 175 | $different++; 176 | } 177 | } 178 | } 179 | 180 | return (float) (($different / $total) * 100); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests; 23 | 24 | use PHPUnit\Framework\TestCase as BaseTestCase; 25 | 26 | abstract class TestCase extends BaseTestCase 27 | { 28 | use Concern\GeneratesData; 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use Tests\TestCase; 25 | use GuzzleHttp\Psr7\Request; 26 | use Psr\Http\Message\RequestInterface; 27 | use OneOffTech\GeoServer\Auth\Authentication; 28 | use OneOffTech\GeoServer\Auth\NullAuthentication; 29 | 30 | class AuthenticationTest extends TestCase 31 | { 32 | public function test_authentication_appends_authorization_header() 33 | { 34 | $auth = new Authentication('username', 'password'); 35 | 36 | $request = new Request('GET', 'http://geoserver.local'); 37 | 38 | $request_with_authentication = $auth->authenticate($request); 39 | 40 | $this->assertInstanceOf(RequestInterface::class, $request_with_authentication); 41 | $this->assertEquals([sprintf('Basic %s', base64_encode("username:password"))], $request_with_authentication->getHeader('Authorization')); 42 | } 43 | 44 | public function test_null_authentication_do_not_append_authorization_header() 45 | { 46 | $auth = new NullAuthentication(); 47 | 48 | $request = new Request('GET', 'http://geoserver.local'); 49 | 50 | $request_without_authentication = $auth->authenticate($request); 51 | 52 | $this->assertInstanceOf(RequestInterface::class, $request_without_authentication); 53 | $this->assertEquals(['Host' => ['geoserver.local']], $request_without_authentication->getHeaders()); 54 | $this->assertEmpty($request_without_authentication->getHeader('Authorization')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/GeoFileTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\GeoFile; 26 | use OneOffTech\GeoServer\GeoType; 27 | use OneOffTech\GeoServer\GeoFormat; 28 | 29 | class GeoFileTest extends TestCase 30 | { 31 | public function supported_files() 32 | { 33 | return [ 34 | [__DIR__.'/../fixtures/shapefile.shp'], 35 | [__DIR__.'/../fixtures/shapefile.zip'], 36 | [__DIR__.'/../fixtures/geotiff.tiff'], 37 | [__DIR__.'/../fixtures/empty.gpkg'], 38 | ]; 39 | } 40 | 41 | public function unsupported_files() 42 | { 43 | return [ 44 | [__DIR__.'/../fixtures/plain.json'], 45 | [__DIR__.'/../fixtures/plain.zip'], 46 | [__DIR__.'/../fixtures/tiff.tif'], 47 | [__DIR__.'/../fixtures/geojson.geojson'], 48 | [__DIR__.'/../fixtures/geojson-in-plain-json.json'], 49 | [__DIR__.'/../fixtures/kml.kml'], 50 | [__DIR__.'/../fixtures/kmz.kmz'], 51 | [__DIR__.'/../fixtures/gpx.gpx'], 52 | ]; 53 | } 54 | 55 | /** 56 | * @dataProvider supported_files 57 | */ 58 | public function test_supported_function_identifies_supported_files($file) 59 | { 60 | $this->assertTrue(GeoFile::isSupported($file)); 61 | } 62 | 63 | /** 64 | * @dataProvider unsupported_files 65 | */ 66 | public function test_supported_function_reject_unsupported_files($file) 67 | { 68 | $this->assertFalse(GeoFile::isSupported($file)); 69 | } 70 | 71 | public function test_shapefile_is_recognized() 72 | { 73 | $file = GeoFile::from(__DIR__.'/../fixtures/shapefile.shp'); 74 | 75 | $this->assertInstanceOf(GeoFile::class, $file); 76 | $this->assertEquals(GeoFormat::SHAPEFILE, $file->format); 77 | $this->assertEquals(GeoType::VECTOR, $file->type); 78 | $this->assertEquals('application/octet-stream', $file->mimeType); 79 | $this->assertEquals('shp', $file->extension); 80 | $this->assertEquals('shapefile.shp', $file->name); 81 | $this->assertEquals($file->originalName, $file->name); 82 | } 83 | 84 | public function test_shapefile_packed_in_zip_is_recognized() 85 | { 86 | $file = GeoFile::from(__DIR__.'/../fixtures/shapefile.zip'); 87 | 88 | $this->assertInstanceOf(GeoFile::class, $file); 89 | $this->assertEquals(GeoFormat::SHAPEFILE_ZIP, $file->format); 90 | $this->assertEquals(GeoType::VECTOR, $file->type); 91 | $this->assertEquals('application/zip', $file->mimeType); 92 | $this->assertEquals('zip', $file->extension); 93 | $this->assertEquals('shapefile.zip', $file->name); 94 | $this->assertEquals($file->originalName, $file->name); 95 | } 96 | 97 | public function test_geotiff_is_recognized() 98 | { 99 | $file = GeoFile::from(__DIR__.'/../fixtures/geotiff.tiff'); 100 | 101 | $this->assertInstanceOf(GeoFile::class, $file); 102 | $this->assertEquals(GeoFormat::GEOTIFF, $file->format); 103 | $this->assertEquals(GeoType::RASTER, $file->type); 104 | $this->assertEquals('image/tiff', $file->mimeType); 105 | $this->assertEquals('tiff', $file->extension); 106 | $this->assertEquals('geotiff.tiff', $file->name); 107 | $this->assertEquals($file->originalName, $file->name); 108 | } 109 | 110 | public function test_geopackage_is_recognized() 111 | { 112 | $file = GeoFile::from(__DIR__.'/../fixtures/empty.gpkg'); 113 | 114 | $this->assertInstanceOf(GeoFile::class, $file); 115 | $this->assertEquals(GeoFormat::GEOPACKAGE, $file->format); 116 | $this->assertEquals(GeoType::VECTOR, $file->type); 117 | $this->assertEquals('application/geopackage+sqlite3', $file->mimeType); 118 | $this->assertEquals('gpkg', $file->extension); 119 | $this->assertEquals('empty.gpkg', $file->name); 120 | $this->assertEquals($file->originalName, $file->name); 121 | } 122 | 123 | public function test_copy_to_temporary() 124 | { 125 | $file = GeoFile::from(__DIR__.'/../fixtures/buildings.zip'); 126 | 127 | $this->assertInstanceOf(GeoFile::class, $file); 128 | $this->assertEquals(GeoFormat::SHAPEFILE_ZIP, $file->format); 129 | $this->assertEquals(GeoType::VECTOR, $file->type); 130 | $this->assertEquals('application/zip', $file->mimeType); 131 | $this->assertEquals('zip', $file->extension); 132 | $this->assertEquals('buildings.zip', $file->name); 133 | $this->assertEquals($file->originalName, $file->name); 134 | 135 | $copy = $file->copy(); 136 | 137 | $this->assertInstanceOf(GeoFile::class, $copy); 138 | $this->assertEquals(GeoFormat::SHAPEFILE_ZIP, $copy->format); 139 | $this->assertEquals(GeoType::VECTOR, $copy->type); 140 | $this->assertEquals('application/zip', $copy->mimeType); 141 | $this->assertNotEquals($copy->originalName, $copy->name); 142 | $this->assertEquals($file->name, $copy->name); 143 | $this->assertEquals($file->content(), $copy->content()); 144 | 145 | unlink($copy->path()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/Unit/GeoServerClientInstantiationTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\GeoServer; 26 | use OneOffTech\GeoServer\Auth\Authentication; 27 | 28 | class GeoServerClientInstantiationTest extends TestCase 29 | { 30 | public function test_client_can_be_created_with_authentication() 31 | { 32 | $auth = new Authentication('username', 'password'); 33 | 34 | $url = 'https://geoserver.local/'; 35 | $workspace = 'default'; 36 | 37 | $client = GeoServer::build($url, $workspace, $auth); 38 | 39 | $this->assertInstanceOf(GeoServer::class, $client); 40 | } 41 | 42 | public function test_client_can_be_created_with_no_authentication() 43 | { 44 | $url = 'https://geoserver.local/'; 45 | $workspace = 'default'; 46 | 47 | $client = GeoServer::build($url, $workspace); 48 | 49 | $this->assertInstanceOf(GeoServer::class, $client); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/OptionsTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use Tests\TestCase; 25 | use Http\Client\HttpClient; 26 | use JMS\Serializer\Serializer; 27 | use Http\Message\MessageFactory; 28 | use OneOffTech\GeoServer\Options; 29 | use Http\Client\Common\PluginClient; 30 | use OneOffTech\GeoServer\Auth\NullAuthentication; 31 | 32 | class OptionsTest extends TestCase 33 | { 34 | public function test_options_contains_expected_configuration() 35 | { 36 | $auth = new NullAuthentication(); 37 | 38 | $options = new Options($auth); 39 | 40 | $this->assertInstanceOf(PluginClient::class, $options->httpClient, "http client is not instantiated"); 41 | $this->assertInstanceOf(HttpClient::class, $options->httpClient, "http client is not instantiated"); 42 | $this->assertEquals($auth, $options->authentication, "authentication not set"); 43 | $this->assertInstanceOf(MessageFactory::class, $options->messageFactory, "message factory is not instantiated"); 44 | $this->assertInstanceOf(Serializer::class, $options->serializer, "serializer is not instantiated"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/ResponseHelperTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\Http\ResponseHelper; 26 | 27 | class ResponseHelperTest extends TestCase 28 | { 29 | public function testAssociativeArrayIsRecognized() 30 | { 31 | $array = [ 32 | 'hello' => 'value', 33 | 'key' => 'value', 34 | ]; 35 | 36 | $this->assertTrue(ResponseHelper::isAssociativeArray($array)); 37 | } 38 | 39 | public function testMixedArrayIsNotRecognizedAsAssociative() 40 | { 41 | $array = [ 42 | 'zero' => 'value', 43 | 0 => 'value', 44 | 'key' => 'value', 45 | ]; 46 | 47 | $this->assertFalse(ResponseHelper::isAssociativeArray($array)); 48 | } 49 | 50 | public function testIndexArrayIsNotRecognizedAsAssociative() 51 | { 52 | $array = [ 53 | 'value1', 54 | 'value2', 55 | ]; 56 | 57 | $this->assertFalse(ResponseHelper::isAssociativeArray($array)); 58 | } 59 | 60 | public function testNullAndEmptyAreNotRecognizedAsAssociative() 61 | { 62 | $this->assertFalse(ResponseHelper::isAssociativeArray(null)); 63 | $this->assertFalse(ResponseHelper::isAssociativeArray([])); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Unit/StyleFileTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use Tests\TestCase; 25 | use OneOffTech\GeoServer\StyleFile; 26 | 27 | class StyleFileTest extends TestCase 28 | { 29 | public function test_style_is_supported() 30 | { 31 | $supported = StyleFile::isSupported(__DIR__.'/../fixtures/style.sld'); 32 | $this->assertTrue($supported); 33 | } 34 | 35 | public function test_style_file_load() 36 | { 37 | $styleName = 'style_test'; 38 | $file = StyleFile::from(__DIR__.'/../fixtures/style.sld')->name($styleName); 39 | 40 | $this->assertInstanceOf(StyleFile::class, $file); 41 | $this->assertEquals('application/vnd.ogc.sld+xml', $file->mimeType); 42 | $this->assertEquals('sld', $file->extension); 43 | $this->assertEquals('style_test', $file->name); 44 | $this->assertEquals('style.sld', $file->originalName); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/WmsOptionsTest.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Tests\Unit; 23 | 24 | use LogicException; 25 | use Tests\TestCase; 26 | use InvalidArgumentException; 27 | use OneOffTech\GeoServer\Models\BoundingBox; 28 | use OneOffTech\GeoServer\Support\WmsOptions; 29 | 30 | class WmsOptionsTest extends TestCase 31 | { 32 | public function test_default_values_are_used() 33 | { 34 | $bbox = new BoundingBox(); 35 | $bbox->minX = -83.64980947326015; 36 | $bbox->minY = 42.724764597615966; 37 | $bbox->maxX = -83.36533095896407; 38 | $bbox->maxY = 42.96491963803106; 39 | 40 | $options = (new WmsOptions())->layers('workspace:layer')->boundingBox($bbox); 41 | 42 | $this->assertInstanceOf(WmsOptions::class, $options); 43 | 44 | $this->assertEquals([ 45 | 'request' => "GetMap", 46 | 'version' => "1.1.0", 47 | 'format' => "image/png", 48 | 'layers' => ["workspace:layer"], 49 | 'bbox' => [-83.64980947326015, 42.724764597615966, -83.36533095896407, 42.96491963803106], 50 | 'srs' => "EPSG:4326", 51 | 'width' => 640, 52 | 'height' => 480, 53 | 'styles' => [], 54 | ], $options->toArray()); 55 | } 56 | 57 | public function test_not_setting_layer_generate_exception_when_serializing() 58 | { 59 | $options = (new WmsOptions()); 60 | 61 | $this->expectException(LogicException::class); 62 | 63 | $options->toArray(); 64 | } 65 | 66 | public function test_not_setting_bounding_box_generate_exception_when_serializing() 67 | { 68 | $options = (new WmsOptions())->layers('workspace:layer'); 69 | 70 | $this->expectException(LogicException::class); 71 | 72 | $options->toArray(); 73 | } 74 | 75 | public function test_setting_wrong_format_raises_exception() 76 | { 77 | $this->expectException(InvalidArgumentException::class); 78 | 79 | $options = (new WmsOptions())->format('workspace:layer'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | ## Test environment 2 | ## This docker compose starts the necessary services to run integration tests 3 | 4 | version: "2.1" 5 | 6 | networks: 7 | internal: 8 | 9 | services: 10 | geoserver: 11 | image: "kartoza/geoserver:${GEOSERVER_TAG:-2.21.0}" 12 | env_file: 13 | - geoserver.env 14 | ports: 15 | - "8600:8080" 16 | networks: 17 | - internal 18 | -------------------------------------------------------------------------------- /tests/fixtures/buildings.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/buildings.zip -------------------------------------------------------------------------------- /tests/fixtures/empty.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/empty.gpkg -------------------------------------------------------------------------------- /tests/fixtures/geojson-in-plain-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "Point", 8 | "coordinates": [ 9 | -80.870885, 10 | 35.215151 11 | ] 12 | }, 13 | "properties": { 14 | "name": "ABBOTT NEIGHBORHOOD PARK", 15 | "address": "1300 SPRUCE ST" 16 | } 17 | }, 18 | { 19 | "type": "Feature", 20 | "geometry": { 21 | "type": "Point", 22 | "coordinates": [ 23 | -80.837753, 24 | 35.249801 25 | ] 26 | }, 27 | "properties": { 28 | "name": "DOUBLE OAKS CENTER", 29 | "address": "1326 WOODWARD AV" 30 | } 31 | }, 32 | { 33 | "type": "Feature", 34 | "geometry": { 35 | "type": "Point", 36 | "coordinates": [ 37 | -80.83827, 38 | 35.256747 39 | ] 40 | }, 41 | "properties": { 42 | "name": "DOUBLE OAKS NEIGHBORHOOD PARK", 43 | "address": "2605 DOUBLE OAKS RD" 44 | } 45 | }, 46 | { 47 | "type": "Feature", 48 | "geometry": { 49 | "type": "Point", 50 | "coordinates": [ 51 | -80.836977, 52 | 35.257517 53 | ] 54 | }, 55 | "properties": { 56 | "name": "DOUBLE OAKS POOL", 57 | "address": "1200 NEWLAND RD" 58 | } 59 | }, 60 | { 61 | "type": "Feature", 62 | "geometry": { 63 | "type": "Point", 64 | "coordinates": [ 65 | -80.816476, 66 | 35.401487 67 | ] 68 | }, 69 | "properties": { 70 | "name": "DAVID B. WAYMER FLYING REGIONAL PARK", 71 | "address": "15401 HOLBROOKS RD" 72 | } 73 | }, 74 | { 75 | "type": "Feature", 76 | "geometry": { 77 | "type": "Point", 78 | "coordinates": [ 79 | -80.835564, 80 | 35.399172 81 | ] 82 | }, 83 | "properties": { 84 | "name": "DAVID B. WAYMER COMMUNITY PARK", 85 | "address": "302 HOLBROOKS RD" 86 | } 87 | }, 88 | { 89 | "type": "Feature", 90 | "geometry": { 91 | "type": "Polygon", 92 | "coordinates": [ 93 | [ 94 | [ 95 | -80.724878, 96 | 35.265454 97 | ], 98 | [ 99 | -80.722646, 100 | 35.260338 101 | ], 102 | [ 103 | -80.720329, 104 | 35.260618 105 | ], 106 | [ 107 | -80.718698, 108 | 35.260267 109 | ], 110 | [ 111 | -80.715093, 112 | 35.260548 113 | ], 114 | [ 115 | -80.71681, 116 | 35.255361 117 | ], 118 | [ 119 | -80.710887, 120 | 35.255361 121 | ], 122 | [ 123 | -80.703248, 124 | 35.265033 125 | ], 126 | [ 127 | -80.704793, 128 | 35.268397 129 | ], 130 | [ 131 | -80.70857, 132 | 35.268257 133 | ], 134 | [ 135 | -80.712518, 136 | 35.270359 137 | ], 138 | [ 139 | -80.715179, 140 | 35.267696 141 | ], 142 | [ 143 | -80.721359, 144 | 35.267276 145 | ], 146 | [ 147 | -80.724878, 148 | 35.265454 149 | ] 150 | ] 151 | ] 152 | }, 153 | "properties": { 154 | "name": "Plaza Road Park" 155 | } 156 | } 157 | ] 158 | } -------------------------------------------------------------------------------- /tests/fixtures/geojson.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "Point", 8 | "coordinates": [ 9 | -80.870885, 10 | 35.215151 11 | ] 12 | }, 13 | "properties": { 14 | "name": "ABBOTT NEIGHBORHOOD PARK", 15 | "address": "1300 SPRUCE ST" 16 | } 17 | }, 18 | { 19 | "type": "Feature", 20 | "geometry": { 21 | "type": "Point", 22 | "coordinates": [ 23 | -80.837753, 24 | 35.249801 25 | ] 26 | }, 27 | "properties": { 28 | "name": "DOUBLE OAKS CENTER", 29 | "address": "1326 WOODWARD AV" 30 | } 31 | }, 32 | { 33 | "type": "Feature", 34 | "geometry": { 35 | "type": "Point", 36 | "coordinates": [ 37 | -80.83827, 38 | 35.256747 39 | ] 40 | }, 41 | "properties": { 42 | "name": "DOUBLE OAKS NEIGHBORHOOD PARK", 43 | "address": "2605 DOUBLE OAKS RD" 44 | } 45 | }, 46 | { 47 | "type": "Feature", 48 | "geometry": { 49 | "type": "Point", 50 | "coordinates": [ 51 | -80.836977, 52 | 35.257517 53 | ] 54 | }, 55 | "properties": { 56 | "name": "DOUBLE OAKS POOL", 57 | "address": "1200 NEWLAND RD" 58 | } 59 | }, 60 | { 61 | "type": "Feature", 62 | "geometry": { 63 | "type": "Point", 64 | "coordinates": [ 65 | -80.816476, 66 | 35.401487 67 | ] 68 | }, 69 | "properties": { 70 | "name": "DAVID B. WAYMER FLYING REGIONAL PARK", 71 | "address": "15401 HOLBROOKS RD" 72 | } 73 | }, 74 | { 75 | "type": "Feature", 76 | "geometry": { 77 | "type": "Point", 78 | "coordinates": [ 79 | -80.835564, 80 | 35.399172 81 | ] 82 | }, 83 | "properties": { 84 | "name": "DAVID B. WAYMER COMMUNITY PARK", 85 | "address": "302 HOLBROOKS RD" 86 | } 87 | }, 88 | { 89 | "type": "Feature", 90 | "geometry": { 91 | "type": "Polygon", 92 | "coordinates": [ 93 | [ 94 | [ 95 | -80.724878, 96 | 35.265454 97 | ], 98 | [ 99 | -80.722646, 100 | 35.260338 101 | ], 102 | [ 103 | -80.720329, 104 | 35.260618 105 | ], 106 | [ 107 | -80.718698, 108 | 35.260267 109 | ], 110 | [ 111 | -80.715093, 112 | 35.260548 113 | ], 114 | [ 115 | -80.71681, 116 | 35.255361 117 | ], 118 | [ 119 | -80.710887, 120 | 35.255361 121 | ], 122 | [ 123 | -80.703248, 124 | 35.265033 125 | ], 126 | [ 127 | -80.704793, 128 | 35.268397 129 | ], 130 | [ 131 | -80.70857, 132 | 35.268257 133 | ], 134 | [ 135 | -80.712518, 136 | 35.270359 137 | ], 138 | [ 139 | -80.715179, 140 | 35.267696 141 | ], 142 | [ 143 | -80.721359, 144 | 35.267276 145 | ], 146 | [ 147 | -80.724878, 148 | 35.265454 149 | ] 150 | ] 151 | ] 152 | }, 153 | "properties": { 154 | "name": "Plaza Road Park" 155 | } 156 | } 157 | ] 158 | } -------------------------------------------------------------------------------- /tests/fixtures/geotiff.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/geotiff.tiff -------------------------------------------------------------------------------- /tests/fixtures/geotiff_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/geotiff_thumbnail.png -------------------------------------------------------------------------------- /tests/fixtures/gpx.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GPS Exchange Format Sample from Wikipedia 6 | 7 | 8 | 9 | 10 | Example GPX Document 11 | 12 | 13 | 4.46 14 | 15 | 16 | 17 | 4.94 18 | 19 | 20 | 21 | 6.87 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/kml.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | KML Samples 5 | 1 6 | Unleash your creativity with the help of these examples! 7 | 14 | 24 | 33 | 42 | 48 | 56 | 64 | 72 | 80 | 88 | 96 | 105 | 106 | Placemarks 107 | These are just some of the different kinds of placemarks with 108 | which you can mark your favorite places 109 | 110 | -122.0839597145766 111 | 37.42222904525232 112 | 0 113 | -148.4122922628044 114 | 40.5575073395506 115 | 500.6566641072245 116 | 117 | 118 | Simple placemark 119 | Attached to the ground. Intelligently places itself at the 120 | height of the underlying terrain. 121 | 122 | -122.0822035425683,37.42228990140251,0 123 | 124 | 125 | 126 | 127 | Styles and Markup 128 | 0 129 | With KML it is easy to create rich, descriptive markup to 130 | annotate and enrich your placemarks 131 | 132 | -122.0845787422371 133 | 37.42215078726837 134 | 0 135 | -148.4126777488172 136 | 40.55750733930874 137 | 365.2646826292919 138 | 139 | #noDrivingDirections 140 | 141 | Highlighted Icon 142 | 0 143 | Place your mouse over the icon to see it display the new 144 | icon 145 | 146 | -122.0856552124024 147 | 37.4224281311035 148 | 0 149 | 0 150 | 0 151 | 265.8520424250024 152 | 153 | 160 | 167 | 168 | 169 | normal 170 | #normalPlacemark 171 | 172 | 173 | highlight 174 | #highlightPlacemark 175 | 176 | 177 | 178 | Roll over this icon 179 | 0 180 | #exampleStyleMap 181 | 182 | -122.0856545755255,37.42243077405461,0 183 | 184 | 185 | 186 | 187 | Descriptive HTML 188 | 0 189 |
190 | Placemark descriptions can be enriched by using many standard HTML tags.
191 | For example: 192 |
193 | Styles:
194 | Italics, 195 | Bold]]>
196 |
197 |
198 |
199 |
200 | -------------------------------------------------------------------------------- /tests/fixtures/kmz.kmz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/kmz.kmz -------------------------------------------------------------------------------- /tests/fixtures/plain.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection" 3 | } -------------------------------------------------------------------------------- /tests/fixtures/plain.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/plain.zip -------------------------------------------------------------------------------- /tests/fixtures/rivers.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/rivers.gpkg -------------------------------------------------------------------------------- /tests/fixtures/shapefile.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/shapefile.shp -------------------------------------------------------------------------------- /tests/fixtures/shapefile.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/shapefile.zip -------------------------------------------------------------------------------- /tests/fixtures/shapefile_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/shapefile_thumbnail.png -------------------------------------------------------------------------------- /tests/fixtures/some_shapefile_with_cyrillicйфячыцус.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/some_shapefile_with_cyrillicйфячыцус.zip -------------------------------------------------------------------------------- /tests/fixtures/style.sld: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | generic 10 | 11 | Generic 12 | Generic style 13 | 14 | 15 | raster 16 | raster 17 | 18 | 19 | 20 | true 21 | 22 | 23 | 24 | 1.0 25 | 26 | 27 | 28 | Polygon 29 | Polygon 30 | 31 | 32 | 33 | 34 | 35 | 2 36 | 37 | 38 | 39 | 40 | #AAAAAA 41 | 42 | 43 | #000000 44 | 1 45 | 46 | 47 | 48 | 49 | Line 50 | Line 51 | 52 | 53 | 54 | 55 | 56 | 1 57 | 58 | 59 | 60 | 61 | #0000FF 62 | 1 63 | 64 | 65 | 66 | 67 | point 68 | Point 69 | 70 | 71 | 72 | 73 | square 74 | 75 | #FF0000 76 | 77 | 78 | 6 79 | 80 | 81 | 82 | first 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /tests/fixtures/tiff.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OneOffTech/geoserver-client-php/317a105fce41b7272d985332df614f28197e0eb8/tests/fixtures/tiff.tif -------------------------------------------------------------------------------- /tests/geoserver.env: -------------------------------------------------------------------------------- 1 | GEOSERVER_DATA_DIR=/opt/geoserver/data_dir 2 | ENABLE_JSONP=true 3 | MAX_FILTER_RULES=20 4 | OPTIMIZE_LINE_WIDTH=false 5 | FOOTPRINTS_DATA_DIR=/opt/footprints_dir 6 | GEOWEBCACHE_CACHE_DIR=/opt/geoserver/data_dir/gwc -------------------------------------------------------------------------------- /tests/helpers.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | if (! function_exists('tap')) { 23 | 24 | /** 25 | * Call the given Closure with the given value then return the value. 26 | * 27 | * Borrowed from Laravel Framework https://github.com/laravel/framework/blob/407b7b085223604a82711fedb16e2ea50ac5e807/src/Illuminate/Support/helpers.php#L1029 28 | * 29 | * @param mixed $value 30 | * @param callable $callback 31 | * @return mixed 32 | */ 33 | function tap($value, $callback) 34 | { 35 | $callback($value); 36 | return $value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/wait.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | require __DIR__.'/../vendor/autoload.php'; 23 | 24 | use Http\Discovery\HttpClientDiscovery; 25 | use Http\Discovery\MessageFactoryDiscovery; 26 | use Http\Client\Exception\RequestException; 27 | 28 | $messageFactory = MessageFactoryDiscovery::find(); 29 | $httpClient = HttpClientDiscovery::find(); 30 | 31 | $route = 'http://127.0.0.1:8600/geoserver/rest/about/version.html'; 32 | 33 | $request = $messageFactory->createRequest('GET', $route, []); 34 | 35 | $start = time(); 36 | 37 | while (true) { 38 | try { 39 | $response = $httpClient->sendRequest($request); 40 | 41 | if ($response->getStatusCode() === 200 || $response->getStatusCode() === 401) { 42 | fwrite(STDOUT, 'Docker container started!'.PHP_EOL); 43 | exit(0); 44 | } 45 | } catch (RequestException $exception) { 46 | $elapsed = time() - $start; 47 | 48 | if ($elapsed > 30) { 49 | fwrite(STDERR, 'Docker container did not start in time...'.PHP_EOL); 50 | exit(1); 51 | } 52 | 53 | fwrite(STDOUT, 'Waiting for container to start...'.PHP_EOL); 54 | sleep(1); 55 | } 56 | } 57 | --------------------------------------------------------------------------------