├── .github └── workflows │ └── provider.yml ├── .gitignore ├── CHANGELOG.md ├── Here.php ├── LICENSE ├── Model └── HereAddress.php ├── Readme.md ├── Tests ├── .cached_responses │ ├── _bf41b7f8395cec5a4143aab0913bd065d24aa313 │ ├── geocoder.ls.hereapi.com_02569c45cb4c0fd27c70039e6a900ca95ca81908 │ ├── geocoder.ls.hereapi.com_0d5d391f86631dda232c2d8b192316b666e68c0d │ ├── geocoder.ls.hereapi.com_0e3c5e72e5557cad55805db7c9068d6a45361010 │ ├── geocoder.ls.hereapi.com_1eef69315e2bf66d0867c320442706acd8d2032c │ ├── geocoder.ls.hereapi.com_2b6ae17b8c33a87de93049279998ef7ad5b72445 │ ├── geocoder.ls.hereapi.com_323a1e4373899cad5e011261c6e0047aa231d8d1 │ ├── geocoder.ls.hereapi.com_33fa188977a0a58c3958c2cff392a55c318c44c5 │ ├── geocoder.ls.hereapi.com_46598c864a0ad1ad1b907e3493e0ef1b23da01d5 │ ├── geocoder.ls.hereapi.com_4f61c17093afd830d99ba13a63b9dc0766a284b4 │ ├── geocoder.ls.hereapi.com_7e52e04b8e7f10183998393328c606db1ae90a4e │ ├── geocoder.ls.hereapi.com_9985b3b28a3ca989ef86d61d2615fef9111625da │ ├── geocoder.ls.hereapi.com_9dff6abd84f79c49f66c561a4fd60c5a3d5bd33f │ ├── geocoder.ls.hereapi.com_ad151662406090cd705d020d806631322a383fd3 │ ├── geocoder.ls.hereapi.com_ae654eb82e2e1918d3c48004a796c7f1fec9e46c │ ├── geocoder.ls.hereapi.com_b86d9e51f223e2c686854be974ed8974ef6176f3 │ ├── geocoder.ls.hereapi.com_d70259de1d8283fc572d14144c0ac15d322b5fc0 │ ├── geocoder.ls.hereapi.com_dbcee9c26d0ac929d6758e843c9beabf646ccae4 │ ├── geocoder.ls.hereapi.com_e1534aa2e104309ef144c7214ebd19312f546a21 │ ├── geocoder.ls.hereapi.com_e388244eefeec9687d013b506e0df5b4096b96b0 │ ├── geocoder.ls.hereapi.com_f76d36ec9d68439ff8b2fdb4a742a2cdf909cfa4 │ ├── geocoder.ls.hereapi.com_f99bbfd40a7b3b121e4b403535a095b176e2f2c2 │ ├── reverse.geocoder.ls.hereapi.com_3a3c7b6e92700df820f0af128ea5455782d17fb2 │ ├── reverse.geocoder.ls.hereapi.com_4d8e57750a4b54386407508576d53bae6f55a057 │ ├── reverse.geocoder.ls.hereapi.com_5047fd1377cea483533977301ebce9861405517d │ ├── reverse.geocoder.ls.hereapi.com_deac011c73ec257fa0e2a1e4da8867dc00b2a4e9 │ └── reverse.geocoder.sit.ls.hereapi.com_1fd7796d332743f3ae617822999f20365e3acf66 ├── HereTest.php └── IntegrationTest.php ├── composer.json └── phpunit.xml.dist /.github/workflows/provider.yml: -------------------------------------------------------------------------------- 1 | name: Provider 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: PHP ${{ matrix.php-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: ['8.0', '8.1', '8.2', '8.3', '8.4'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use PHP ${{ matrix.php-version }} 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-version }} 23 | extensions: curl 24 | - name: Validate composer.json and composer.lock 25 | run: composer validate --strict 26 | - name: Install dependencies 27 | run: composer update --prefer-stable --prefer-dist --no-progress 28 | - name: Run test suite 29 | run: composer run-script test-ci 30 | - name: Upload Coverage report 31 | run: | 32 | wget https://scrutinizer-ci.com/ocular.phar 33 | php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 0.8.0 6 | 7 | ### Added 8 | 9 | - Add support for PHP Geocoder 5 10 | 11 | ## 0.7.1 12 | 13 | ### Fixed 14 | 15 | - Fix issue with missing `locationId` 16 | 17 | ## 0.7.0 18 | 19 | ### Added 20 | 21 | - Add support for PHP 8.1 22 | - Add GitHub Actions workflow 23 | 24 | ### Removed 25 | 26 | - Drop support for PHP 7.3 27 | 28 | ### Changed 29 | 30 | - Migrate from PHP-HTTP to PSR-18 client 31 | 32 | ## 0.6.0 33 | 34 | ### Added 35 | 36 | - Add support for PHP 8.0 37 | 38 | ### Removed 39 | 40 | - Drop support for PHP 7.2 41 | 42 | ### Changed 43 | 44 | - Upgrade PHPUnit to version 9 45 | 46 | ## 0.5.0 47 | 48 | ### Added 49 | 50 | - Support API key (see [documentation](https://developer.here.com/documentation/authentication/dev_guide/topics/api-key-credentials.html)) 51 | 52 | ## 0.4.0 53 | 54 | ### Removed 55 | 56 | - Drop support for PHP < 7.2 57 | 58 | ## 0.3.0 59 | 60 | ### Added 61 | 62 | - Compatibility with `AdditionalData` field (see [documentation](https://developer.here.com/documentation/geocoder/topics/resource-type-response-geocode.html#resource-type-response-geocode__address)) 63 | 64 | ## 0.2.1 65 | 66 | ### Fixed 67 | 68 | - `prox` parameter in reverse geocoding query 69 | 70 | ### Changed 71 | 72 | - Upgrade API to generation 9 (see [documentation](https://developer.here.com/documentation/geocoder/topics/backwards-compatibility.html#backwards-compatibility__api-different-generations)) 73 | 74 | ## 0.2.0 75 | 76 | ### Added 77 | 78 | - Add support for [Customer Integration Testing (CIT) environment](https://developer.here.com/documentation/places/common/request-cit-environment.html) 79 | 80 | ## 0.1.0 81 | 82 | First release of this library. 83 | -------------------------------------------------------------------------------- /Here.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | final class Here extends AbstractHttpProvider implements Provider 34 | { 35 | /** 36 | * @var string 37 | */ 38 | public const GEOCODE_ENDPOINT_URL_API_KEY = 'https://geocoder.ls.hereapi.com/6.2/geocode.json'; 39 | 40 | /** 41 | * @var string 42 | */ 43 | public const GEOCODE_ENDPOINT_URL_APP_CODE = 'https://geocoder.api.here.com/6.2/geocode.json'; 44 | 45 | /** 46 | * @var string 47 | */ 48 | public const GEOCODE_CIT_ENDPOINT_API_KEY = 'https:/geocoder.sit.ls.hereapi.com/6.2/geocode.json'; 49 | 50 | /** 51 | * @var string 52 | */ 53 | public const GEOCODE_CIT_ENDPOINT_APP_CODE = 'https://geocoder.cit.api.here.com/6.2/geocode.json'; 54 | 55 | /** 56 | * @var string 57 | */ 58 | public const REVERSE_ENDPOINT_URL_API_KEY = 'https://reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json'; 59 | 60 | /** 61 | * @var string 62 | */ 63 | public const REVERSE_ENDPOINT_URL_APP_CODE = 'https://reverse.geocoder.api.here.com/6.2/reversegeocode.json'; 64 | 65 | /** 66 | * @var string 67 | */ 68 | public const REVERSE_CIT_ENDPOINT_URL_API_KEY = 'https://reverse.geocoder.sit.ls.hereapi.com/6.2/reversegeocode.json'; 69 | 70 | /** 71 | * @var string 72 | */ 73 | public const REVERSE_CIT_ENDPOINT_URL_APP_CODE = 'https://reverse.geocoder.cit.api.here.com/6.2/reversegeocode.json'; 74 | 75 | /** 76 | * @var string[] 77 | */ 78 | public const GEOCODE_ADDITIONAL_DATA_PARAMS = [ 79 | 'CrossingStreets', 80 | 'PreserveUnitDesignators', 81 | 'Country2', 82 | 'IncludeChildPOIs', 83 | 'IncludeRoutingInformation', 84 | 'AdditionalAddressProvider', 85 | 'HouseNumberMode', 86 | 'FlexibleAdminValues', 87 | 'IntersectionSnapTolerance', 88 | 'AddressRangeSqueezeOffset', 89 | 'AddressRangeSqueezeFactor', 90 | 'AddressRangeSqueezeOffset', 91 | 'IncludeShapeLevel', 92 | 'RestrictLevel', 93 | 'SuppressStreetType', 94 | 'NormalizeNames', 95 | 'IncludeMicroPointAddresses', 96 | ]; 97 | 98 | /** 99 | * @var string 100 | */ 101 | private $appId; 102 | 103 | /** 104 | * @var string 105 | */ 106 | private $appCode; 107 | 108 | /** 109 | * @var bool 110 | */ 111 | private $useCIT; 112 | 113 | /** 114 | * @var string 115 | */ 116 | private $apiKey; 117 | 118 | /** 119 | * @param ClientInterface $client an HTTP adapter 120 | * @param string $appId an App ID 121 | * @param string $appCode an App code 122 | * @param bool $useCIT use Customer Integration Testing environment (CIT) instead of production 123 | */ 124 | public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false) 125 | { 126 | $this->appId = $appId; 127 | $this->appCode = $appCode; 128 | $this->useCIT = $useCIT; 129 | 130 | parent::__construct($client); 131 | } 132 | 133 | public static function createUsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false): self 134 | { 135 | $client = new self($client, null, null, $useCIT); 136 | $client->apiKey = $apiKey; 137 | 138 | return $client; 139 | } 140 | 141 | public function geocodeQuery(GeocodeQuery $query): Collection 142 | { 143 | // This API doesn't handle IPs 144 | if (filter_var($query->getText(), FILTER_VALIDATE_IP)) { 145 | throw new UnsupportedOperation('The Here provider does not support IP addresses, only street addresses.'); 146 | } 147 | 148 | $queryParams = $this->withApiCredentials([ 149 | 'searchtext' => $query->getText(), 150 | 'gen' => 9, 151 | 'additionaldata' => $this->getAdditionalDataParam($query), 152 | ]); 153 | 154 | if (null !== $query->getData('country')) { 155 | $queryParams['country'] = $query->getData('country'); 156 | } 157 | 158 | if (null !== $query->getData('state')) { 159 | $queryParams['state'] = $query->getData('state'); 160 | } 161 | 162 | if (null !== $query->getData('county')) { 163 | $queryParams['county'] = $query->getData('county'); 164 | } 165 | 166 | if (null !== $query->getData('city')) { 167 | $queryParams['city'] = $query->getData('city'); 168 | } 169 | 170 | if (null !== $query->getLocale()) { 171 | $queryParams['language'] = $query->getLocale(); 172 | } 173 | 174 | return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit()); 175 | } 176 | 177 | public function reverseQuery(ReverseQuery $query): Collection 178 | { 179 | $coordinates = $query->getCoordinates(); 180 | 181 | $queryParams = $this->withApiCredentials([ 182 | 'gen' => 9, 183 | 'mode' => 'retrieveAddresses', 184 | 'prox' => sprintf('%s,%s', $coordinates->getLatitude(), $coordinates->getLongitude()), 185 | 'maxresults' => $query->getLimit(), 186 | ]); 187 | 188 | return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit()); 189 | } 190 | 191 | private function executeQuery(string $url, int $limit): Collection 192 | { 193 | $content = $this->getUrlContents($url); 194 | 195 | $json = json_decode($content, true); 196 | 197 | if (isset($json['type'])) { 198 | switch ($json['type']['subtype']) { 199 | case 'InvalidInputData': 200 | throw new InvalidArgument('Input parameter validation failed.'); 201 | case 'QuotaExceeded': 202 | throw new QuotaExceeded('Valid request but quota exceeded.'); 203 | case 'InvalidCredentials': 204 | throw new InvalidCredentials('Invalid or missing api key.'); 205 | } 206 | } 207 | 208 | if (!isset($json['Response']) || empty($json['Response'])) { 209 | return new AddressCollection([]); 210 | } 211 | 212 | if (!isset($json['Response']['View'][0])) { 213 | return new AddressCollection([]); 214 | } 215 | 216 | $locations = $json['Response']['View'][0]['Result']; 217 | 218 | $results = []; 219 | 220 | foreach ($locations as $loc) { 221 | $location = $loc['Location']; 222 | $builder = new AddressBuilder($this->getName()); 223 | $coordinates = isset($location['NavigationPosition'][0]) ? $location['NavigationPosition'][0] : $location['DisplayPosition']; 224 | $builder->setCoordinates($coordinates['Latitude'], $coordinates['Longitude']); 225 | $bounds = $location['MapView']; 226 | 227 | $builder->setBounds($bounds['BottomRight']['Latitude'], $bounds['TopLeft']['Longitude'], $bounds['TopLeft']['Latitude'], $bounds['BottomRight']['Longitude']); 228 | $builder->setStreetNumber($location['Address']['HouseNumber'] ?? null); 229 | $builder->setStreetName($location['Address']['Street'] ?? null); 230 | $builder->setPostalCode($location['Address']['PostalCode'] ?? null); 231 | $builder->setLocality($location['Address']['City'] ?? null); 232 | $builder->setSubLocality($location['Address']['District'] ?? null); 233 | $builder->setCountryCode($location['Address']['Country'] ?? null); 234 | 235 | // The name of the country can be found in the AdditionalData. 236 | $additionalData = $location['Address']['AdditionalData'] ?? null; 237 | if (!empty($additionalData)) { 238 | $builder->setCountry($additionalData[array_search('CountryName', array_column($additionalData, 'key'))]['value'] ?? null); 239 | } 240 | 241 | // There may be a second AdditionalData. For example if "IncludeRoutingInformation" parameter is added 242 | $extraAdditionalData = $loc['AdditionalData'] ?? []; 243 | 244 | /** @var HereAddress $address */ 245 | $address = $builder->build(HereAddress::class); 246 | $address = $address->withLocationId($location['LocationId'] ?? null); 247 | $address = $address->withLocationType($location['LocationType']); 248 | $address = $address->withAdditionalData(array_merge($additionalData, $extraAdditionalData)); 249 | $address = $address->withShape($location['Shape'] ?? null); 250 | $results[] = $address; 251 | 252 | if (count($results) >= $limit) { 253 | break; 254 | } 255 | } 256 | 257 | return new AddressCollection($results); 258 | } 259 | 260 | public function getName(): string 261 | { 262 | return 'Here'; 263 | } 264 | 265 | /** 266 | * Get serialized additional data param. 267 | */ 268 | private function getAdditionalDataParam(GeocodeQuery $query): string 269 | { 270 | $additionalDataParams = [ 271 | 'IncludeShapeLevel' => 'country', 272 | ]; 273 | 274 | foreach (self::GEOCODE_ADDITIONAL_DATA_PARAMS as $paramKey) { 275 | if (null !== $query->getData($paramKey)) { 276 | $additionalDataParams[$paramKey] = $query->getData($paramKey); 277 | } 278 | } 279 | 280 | return $this->serializeComponents($additionalDataParams); 281 | } 282 | 283 | /** 284 | * Add API credentials to query params. 285 | * 286 | * @param array $queryParams 287 | * 288 | * @return array 289 | */ 290 | private function withApiCredentials(array $queryParams): array 291 | { 292 | if ( 293 | empty($this->apiKey) 294 | && (empty($this->appId) || empty($this->appCode)) 295 | ) { 296 | throw new InvalidCredentials('Invalid or missing api key.'); 297 | } 298 | 299 | if (null !== $this->apiKey) { 300 | $queryParams['apiKey'] = $this->apiKey; 301 | } else { 302 | $queryParams['app_id'] = $this->appId; 303 | $queryParams['app_code'] = $this->appCode; 304 | } 305 | 306 | return $queryParams; 307 | } 308 | 309 | public function getBaseUrl(Query $query): string 310 | { 311 | $usingApiKey = null !== $this->apiKey; 312 | 313 | if ($query instanceof ReverseQuery) { 314 | if ($this->useCIT) { 315 | return $usingApiKey ? self::REVERSE_CIT_ENDPOINT_URL_API_KEY : self::REVERSE_CIT_ENDPOINT_URL_APP_CODE; 316 | } 317 | 318 | return $usingApiKey ? self::REVERSE_ENDPOINT_URL_API_KEY : self::REVERSE_ENDPOINT_URL_APP_CODE; 319 | } 320 | 321 | if ($this->useCIT) { 322 | return $usingApiKey ? self::GEOCODE_CIT_ENDPOINT_API_KEY : self::GEOCODE_CIT_ENDPOINT_APP_CODE; 323 | } 324 | 325 | return $usingApiKey ? self::GEOCODE_ENDPOINT_URL_API_KEY : self::GEOCODE_ENDPOINT_URL_APP_CODE; 326 | } 327 | 328 | /** 329 | * Serialize the component query parameter. 330 | * 331 | * @param array $components 332 | */ 333 | private function serializeComponents(array $components): string 334 | { 335 | return implode(';', array_map(function ($name, $value) { 336 | return sprintf('%s,%s', $name, $value); 337 | }, array_keys($components), $components)); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 — Sébastien Barré 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Model/HereAddress.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class HereAddress extends Address 21 | { 22 | /** 23 | * @var string|null 24 | */ 25 | private $locationId; 26 | 27 | /** 28 | * @var string|null 29 | */ 30 | private $locationType; 31 | 32 | /** 33 | * @var string|null 34 | */ 35 | private $locationName; 36 | 37 | /** 38 | * @var array|null 39 | */ 40 | private $additionalData; 41 | 42 | /** 43 | * @var array|null 44 | */ 45 | private $shape; 46 | 47 | /** 48 | * @return string|null 49 | */ 50 | public function getLocationId() 51 | { 52 | return $this->locationId; 53 | } 54 | 55 | public function withLocationId(?string $locationId = null): self 56 | { 57 | $new = clone $this; 58 | $new->locationId = $locationId; 59 | 60 | return $new; 61 | } 62 | 63 | /** 64 | * @return string|null 65 | */ 66 | public function getLocationType() 67 | { 68 | return $this->locationType; 69 | } 70 | 71 | public function withLocationType(?string $locationType = null): self 72 | { 73 | $new = clone $this; 74 | $new->locationType = $locationType; 75 | 76 | return $new; 77 | } 78 | 79 | /** 80 | * @return string|null 81 | */ 82 | public function getLocationName() 83 | { 84 | return $this->locationName; 85 | } 86 | 87 | public function withLocationName(?string $locationName = null): self 88 | { 89 | $new = clone $this; 90 | $new->locationName = $locationName; 91 | 92 | return $new; 93 | } 94 | 95 | /** 96 | * @return array|null 97 | */ 98 | public function getAdditionalData() 99 | { 100 | return $this->additionalData; 101 | } 102 | 103 | /** 104 | * @param array|null $additionalData 105 | */ 106 | public function withAdditionalData(?array $additionalData = null): self 107 | { 108 | $new = clone $this; 109 | 110 | foreach ($additionalData as $data) { 111 | $new = $new->addAdditionalData($data['key'], $data['value']); 112 | } 113 | 114 | return $new; 115 | } 116 | 117 | /** 118 | * @param mixed|null $value 119 | */ 120 | public function addAdditionalData(string $name, $value = null): self 121 | { 122 | $new = clone $this; 123 | $new->additionalData[$name] = $value; 124 | 125 | return $new; 126 | } 127 | 128 | public function getAdditionalDataValue(string $name, mixed $default = null): mixed 129 | { 130 | if ($this->hasAdditionalDataValue($name)) { 131 | return $this->additionalData[$name]; 132 | } 133 | 134 | return $default; 135 | } 136 | 137 | public function hasAdditionalDataValue(string $name): bool 138 | { 139 | return array_key_exists($name, $this->additionalData); 140 | } 141 | 142 | /** 143 | * @param array|null $shape 144 | */ 145 | public function withShape(?array $shape = null): self 146 | { 147 | $new = clone $this; 148 | 149 | if (!empty($shape)) { 150 | foreach ($shape as $key => $data) { 151 | $new = $new->addShape($key, $data); 152 | } 153 | } 154 | 155 | return $new; 156 | } 157 | 158 | public function addShape(string $name, mixed $value = null): self 159 | { 160 | $new = clone $this; 161 | $new->shape[$name] = $value; 162 | 163 | return $new; 164 | } 165 | 166 | public function getShapeValue(string $name, mixed $default = null): mixed 167 | { 168 | if ($this->hasShapeValue($name)) { 169 | return $this->shape[$name]; 170 | } 171 | 172 | return $default; 173 | } 174 | 175 | public function hasShapeValue(string $name): bool 176 | { 177 | return array_key_exists($name, $this->shape); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Here Geocoder provider 2 | [![Build Status](https://travis-ci.org/geocoder-php/here-provider.svg?branch=master)](http://travis-ci.org/geocoder-php/here-provider) 3 | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/here-provider/v/stable)](https://packagist.org/packages/geocoder-php/here-provider) 4 | [![Total Downloads](https://poser.pugx.org/geocoder-php/here-provider/downloads)](https://packagist.org/packages/geocoder-php/here-provider) 5 | [![Monthly Downloads](https://poser.pugx.org/geocoder-php/here-provider/d/monthly.png)](https://packagist.org/packages/geocoder-php/here-provider) 6 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/here-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/here-provider) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/here-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/here-provider) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 9 | 10 | This is the Here provider from the PHP Geocoder. This is a **READ ONLY** repository. See the 11 | [main repo](https://github.com/geocoder-php/Geocoder) for information and documentation. 12 | 13 | You can find the [documentation for the provider here](https://developer.here.com/documentation/geocoder/dev_guide/topics/resources.html). 14 | 15 | 16 | ### Install 17 | 18 | ```bash 19 | composer require geocoder-php/here-provider 20 | ``` 21 | 22 | ## Using 23 | 24 | New applications on the Here platform use the `api_key` authentication method. 25 | 26 | ```php 27 | $httpClient = new \Http\Discovery\Psr18Client(); 28 | 29 | // You must provide an API key 30 | $provider = \Geocoder\Provider\Here\Here::createUsingApiKey($httpClient, 'your-api-key'); 31 | 32 | $result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); 33 | ``` 34 | 35 | If you're using the legacy `app_code` authentication method, use the constructor on the provider like so: 36 | 37 | ```php 38 | $httpClient = new \Http\Discovery\Psr18Client(); 39 | 40 | // You must provide both the app_id and app_code 41 | $provider = new \Geocoder\Provider\Here\Here($httpClient, 'app-id', 'app-code'); 42 | 43 | $result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); 44 | ``` 45 | 46 | ### Language parameter 47 | 48 | Define the preferred language of address elements in the result. Without a preferred language, the Here geocoder will return results in an official country language or in a regional primary language so that local people will understand. Language code must be provided according to RFC 4647 standard. 49 | 50 | ### Contribute 51 | 52 | Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or 53 | report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues). 54 | -------------------------------------------------------------------------------- /Tests/.cached_responses/geocoder.ls.hereapi.com_ae654eb82e2e1918d3c48004a796c7f1fec9e46c: -------------------------------------------------------------------------------- 1 | s:80:"{"Response":{"MetaInfo":{"Timestamp":"2020-08-08T13:00:41.999+0000"},"View":[]}}"; -------------------------------------------------------------------------------- /Tests/.cached_responses/reverse.geocoder.ls.hereapi.com_3a3c7b6e92700df820f0af128ea5455782d17fb2: -------------------------------------------------------------------------------- 1 | s:80:"{"Response":{"MetaInfo":{"Timestamp":"2020-08-08T13:26:54.282+0000"},"View":[]}}"; -------------------------------------------------------------------------------- /Tests/.cached_responses/reverse.geocoder.ls.hereapi.com_4d8e57750a4b54386407508576d53bae6f55a057: -------------------------------------------------------------------------------- 1 | s:2297:"{"Response":{"MetaInfo":{"Timestamp":"2020-08-08T13:26:53.778+0000"},"View":[{"_type":"SearchResultsViewType","ViewId":0,"Result":[{"Relevance":1.0,"Distance":-0.6,"Direction":326.1,"MatchLevel":"district","MatchQuality":{"Country":1.0,"State":1.0,"County":1.0,"City":1.0,"District":1.0,"PostalCode":1.0},"Location":{"LocationId":"NT_oGdFab4eWiYVcjO-u0pTFB","LocationType":"area","DisplayPosition":{"Latitude":38.90357,"Longitude":-77.0399},"MapView":{"TopLeft":{"Latitude":38.90939,"Longitude":-77.04974},"BottomRight":{"Latitude":38.89599,"Longitude":-77.03363}},"Address":{"Label":"Connecticut Avenue/K Street, Washington, DC, United States","Country":"USA","State":"DC","County":"District of Columbia","City":"Washington","District":"Connecticut Avenue/K Street","PostalCode":"20036","AdditionalData":[{"value":"United States","key":"CountryName"},{"value":"District of Columbia","key":"StateName"},{"value":"District of Columbia","key":"CountyName"},{"value":"N","key":"PostalCodeType"}]},"MapReference":{"ReferenceId":"1219975889","MapId":"NAAM20120","MapVersion":"Q1/2020","MapReleaseDate":"2020-07-21","SideOfStreet":"neither","CountryId":"21000001","StateId":"21022302","CountyId":"21022303","CityId":"21022306"}}},{"Relevance":1.0,"Distance":0.7,"Direction":163.6,"MatchLevel":"district","MatchQuality":{"Country":1.0,"State":1.0,"County":1.0,"City":1.0,"District":1.0,"PostalCode":1.0},"Location":{"LocationId":"NT_zeyJ5M16Ci53y9KE8RMTGD","LocationType":"area","DisplayPosition":{"Latitude":38.89142,"Longitude":-77.03367},"MapView":{"TopLeft":{"Latitude":38.90021,"Longitude":-77.05679},"BottomRight":{"Latitude":38.88508,"Longitude":-77.00202}},"Address":{"Label":"Washington Mall, Washington, DC, United States","Country":"USA","State":"DC","County":"District of Columbia","City":"Washington","District":"Washington Mall","PostalCode":"20004","AdditionalData":[{"value":"United States","key":"CountryName"},{"value":"District of Columbia","key":"StateName"},{"value":"District of Columbia","key":"CountyName"},{"value":"N","key":"PostalCodeType"}]},"MapReference":{"ReferenceId":"1114357553","MapId":"NAAM20120","MapVersion":"Q1/2020","MapReleaseDate":"2020-07-21","SideOfStreet":"neither","CountryId":"21000001","StateId":"21022302","CountyId":"21022303","CityId":"21022306"}}}]}]}}"; -------------------------------------------------------------------------------- /Tests/.cached_responses/reverse.geocoder.ls.hereapi.com_5047fd1377cea483533977301ebce9861405517d: -------------------------------------------------------------------------------- 1 | s:1267:"{"Response":{"MetaInfo":{"Timestamp":"2020-08-08T13:30:00.535+0000"},"View":[{"_type":"SearchResultsViewType","ViewId":0,"Result":[{"Relevance":1.0,"Distance":0.1,"MatchLevel":"street","MatchQuality":{"Country":1.0,"State":1.0,"County":1.0,"City":1.0,"District":1.0,"Street":[1.0],"PostalCode":1.0},"Location":{"LocationId":"NT_-nSC1VIpTK6RxGk-RZa.1D_l_1231932773_L","LocationType":"address","DisplayPosition":{"Latitude":48.8632147,"Longitude":2.3887725},"NavigationPosition":[{"Latitude":48.8632147,"Longitude":2.3887725}],"MapView":{"TopLeft":{"Latitude":48.86323,"Longitude":2.38853},"BottomRight":{"Latitude":48.86315,"Longitude":2.38883}},"Address":{"Label":"Avenue Gambetta, 75020 Paris, France","Country":"FRA","State":"Île-de-France","County":"Paris","City":"Paris","District":"20e Arrondissement","Street":"Avenue Gambetta","PostalCode":"75020","AdditionalData":[{"value":"France","key":"CountryName"},{"value":"Île-de-France","key":"StateName"},{"value":"Paris","key":"CountyName"}]},"MapReference":{"ReferenceId":"1231932773","MapId":"UWAM20121","MapVersion":"Q1/2020","MapReleaseDate":"2020-07-09","Spot":0.81,"SideOfStreet":"neither","CountryId":"20000001","StateId":"20002126","CountyId":"20002127","CityId":"20002128","DistrictId":"20002149"}}}]}]}}"; -------------------------------------------------------------------------------- /Tests/.cached_responses/reverse.geocoder.ls.hereapi.com_deac011c73ec257fa0e2a1e4da8867dc00b2a4e9: -------------------------------------------------------------------------------- 1 | s:1267:"{"Response":{"MetaInfo":{"Timestamp":"2020-08-08T13:30:43.565+0000"},"View":[{"_type":"SearchResultsViewType","ViewId":0,"Result":[{"Relevance":1.0,"Distance":0.1,"MatchLevel":"street","MatchQuality":{"Country":1.0,"State":1.0,"County":1.0,"City":1.0,"District":1.0,"Street":[1.0],"PostalCode":1.0},"Location":{"LocationId":"NT_-nSC1VIpTK6RxGk-RZa.1D_l_1231932773_L","LocationType":"address","DisplayPosition":{"Latitude":48.8632147,"Longitude":2.3887725},"NavigationPosition":[{"Latitude":48.8632147,"Longitude":2.3887725}],"MapView":{"TopLeft":{"Latitude":48.86323,"Longitude":2.38853},"BottomRight":{"Latitude":48.86315,"Longitude":2.38883}},"Address":{"Label":"Avenue Gambetta, 75020 Paris, France","Country":"FRA","State":"Île-de-France","County":"Paris","City":"Paris","District":"20e Arrondissement","Street":"Avenue Gambetta","PostalCode":"75020","AdditionalData":[{"value":"France","key":"CountryName"},{"value":"Île-de-France","key":"StateName"},{"value":"Paris","key":"CountyName"}]},"MapReference":{"ReferenceId":"1231932773","MapId":"UWAM20121","MapVersion":"Q1/2020","MapReleaseDate":"2020-07-09","Spot":0.81,"SideOfStreet":"neither","CountryId":"20000001","StateId":"20002126","CountyId":"20002127","CityId":"20002128","DistrictId":"20002149"}}}]}]}}"; -------------------------------------------------------------------------------- /Tests/.cached_responses/reverse.geocoder.sit.ls.hereapi.com_1fd7796d332743f3ae617822999f20365e3acf66: -------------------------------------------------------------------------------- 1 | s:2297:"{"Response":{"MetaInfo":{"Timestamp":"2020-08-08T13:26:54.103+0000"},"View":[{"_type":"SearchResultsViewType","ViewId":0,"Result":[{"Relevance":1.0,"Distance":-0.6,"Direction":326.1,"MatchLevel":"district","MatchQuality":{"Country":1.0,"State":1.0,"County":1.0,"City":1.0,"District":1.0,"PostalCode":1.0},"Location":{"LocationId":"NT_oGdFab4eWiYVcjO-u0pTFB","LocationType":"area","DisplayPosition":{"Latitude":38.90357,"Longitude":-77.0399},"MapView":{"TopLeft":{"Latitude":38.90939,"Longitude":-77.04974},"BottomRight":{"Latitude":38.89599,"Longitude":-77.03363}},"Address":{"Label":"Connecticut Avenue/K Street, Washington, DC, United States","Country":"USA","State":"DC","County":"District of Columbia","City":"Washington","District":"Connecticut Avenue/K Street","PostalCode":"20036","AdditionalData":[{"value":"United States","key":"CountryName"},{"value":"District of Columbia","key":"StateName"},{"value":"District of Columbia","key":"CountyName"},{"value":"N","key":"PostalCodeType"}]},"MapReference":{"ReferenceId":"1219975889","MapId":"NAAM20120","MapVersion":"Q1/2020","MapReleaseDate":"2020-07-21","SideOfStreet":"neither","CountryId":"21000001","StateId":"21022302","CountyId":"21022303","CityId":"21022306"}}},{"Relevance":1.0,"Distance":0.7,"Direction":163.6,"MatchLevel":"district","MatchQuality":{"Country":1.0,"State":1.0,"County":1.0,"City":1.0,"District":1.0,"PostalCode":1.0},"Location":{"LocationId":"NT_zeyJ5M16Ci53y9KE8RMTGD","LocationType":"area","DisplayPosition":{"Latitude":38.89142,"Longitude":-77.03367},"MapView":{"TopLeft":{"Latitude":38.90021,"Longitude":-77.05679},"BottomRight":{"Latitude":38.88508,"Longitude":-77.00202}},"Address":{"Label":"Washington Mall, Washington, DC, United States","Country":"USA","State":"DC","County":"District of Columbia","City":"Washington","District":"Washington Mall","PostalCode":"20004","AdditionalData":[{"value":"United States","key":"CountryName"},{"value":"District of Columbia","key":"StateName"},{"value":"District of Columbia","key":"CountyName"},{"value":"N","key":"PostalCodeType"}]},"MapReference":{"ReferenceId":"1114357553","MapId":"NAAM20120","MapVersion":"Q1/2020","MapReleaseDate":"2020-07-21","SideOfStreet":"neither","CountryId":"21000001","StateId":"21022302","CountyId":"21022303","CityId":"21022306"}}}]}]}}"; -------------------------------------------------------------------------------- /Tests/HereTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | 17 | namespace Geocoder\Provider\Here\Tests; 18 | 19 | use Geocoder\IntegrationTest\BaseTestCase; 20 | use Geocoder\Location; 21 | use Geocoder\Provider\Here\Here; 22 | use Geocoder\Provider\Here\Model\HereAddress; 23 | use Geocoder\Query\GeocodeQuery; 24 | use Geocoder\Query\ReverseQuery; 25 | 26 | class HereTest extends BaseTestCase 27 | { 28 | protected function getCacheDir(): ?string 29 | { 30 | if (isset($_SERVER['USE_CACHED_RESPONSES']) && true === $_SERVER['USE_CACHED_RESPONSES']) { 31 | return __DIR__.'/.cached_responses'; 32 | } 33 | 34 | return null; 35 | } 36 | 37 | public function testGeocodeWithRealAddress(): void 38 | { 39 | if (!isset($_SERVER['HERE_API_KEY'])) { 40 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 41 | } 42 | 43 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 44 | 45 | $results = $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')->withLocale('fr-FR')); 46 | 47 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results); 48 | $this->assertCount(1, $results); 49 | 50 | /** @var Location $result */ 51 | $result = $results->first(); 52 | $this->assertInstanceOf(\Geocoder\Model\Address::class, $result); 53 | $this->assertEqualsWithDelta(48.8653, $result->getCoordinates()->getLatitude(), 0.01); 54 | $this->assertEqualsWithDelta(2.39844, $result->getCoordinates()->getLongitude(), 0.01); 55 | $this->assertNotNull($result->getBounds()); 56 | $this->assertEqualsWithDelta(48.8664242, $result->getBounds()->getSouth(), 0.01); 57 | $this->assertEqualsWithDelta(2.3967311, $result->getBounds()->getWest(), 0.01); 58 | $this->assertEqualsWithDelta(48.8641758, $result->getBounds()->getNorth(), 0.01); 59 | $this->assertEqualsWithDelta(2.4001489, $result->getBounds()->getEast(), 0.01); 60 | $this->assertEquals(10, $result->getStreetNumber()); 61 | 62 | $this->assertEquals('Avenue Gambetta', $result->getStreetName()); 63 | $this->assertEquals(75020, $result->getPostalCode()); 64 | $this->assertEquals('Paris', $result->getLocality()); 65 | $this->assertEquals('France', $result->getCountry()->getName()); 66 | $this->assertEquals('FRA', $result->getCountry()->getCode()); 67 | } 68 | 69 | /** 70 | * @throws \Geocoder\Exception\Exception 71 | */ 72 | public function testGeocodeWithDefaultAdditionalData(): void 73 | { 74 | if (!isset($_SERVER['HERE_API_KEY'])) { 75 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 76 | } 77 | 78 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 79 | 80 | $results = $provider->geocodeQuery(GeocodeQuery::create('Sant Roc, Santa Coloma de Cervelló, Espanya')->withLocale('ca')); 81 | 82 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results); 83 | $this->assertCount(1, $results); 84 | 85 | /** @var HereAddress $result */ 86 | $result = $results->first(); 87 | 88 | $this->assertInstanceOf(\Geocoder\Model\Address::class, $result); 89 | $this->assertEqualsWithDelta(41.37854, $result->getCoordinates()->getLatitude(), 0.01); 90 | $this->assertEqualsWithDelta(2.01196, $result->getCoordinates()->getLongitude(), 0.01); 91 | $this->assertNotNull($result->getBounds()); 92 | $this->assertEqualsWithDelta(41.36505, $result->getBounds()->getSouth(), 0.01); 93 | $this->assertEqualsWithDelta(1.99398, $result->getBounds()->getWest(), 0.01); 94 | $this->assertEqualsWithDelta(41.39203, $result->getBounds()->getNorth(), 0.01); 95 | $this->assertEqualsWithDelta(2.02994, $result->getBounds()->getEast(), 0.01); 96 | 97 | $this->assertEquals('08690', $result->getPostalCode()); 98 | $this->assertEquals('Sant Roc', $result->getSubLocality()); 99 | $this->assertEquals('Santa Coloma de Cervelló', $result->getLocality()); 100 | $this->assertEquals('Espanya', $result->getCountry()->getName()); 101 | $this->assertEquals('ESP', $result->getCountry()->getCode()); 102 | 103 | $this->assertEquals('Espanya', $result->getAdditionalDataValue('CountryName')); 104 | $this->assertEquals('Catalunya', $result->getAdditionalDataValue('StateName')); 105 | $this->assertEquals('Barcelona', $result->getAdditionalDataValue('CountyName')); 106 | } 107 | 108 | /** 109 | * Validation of some AdditionalData filters. 110 | * https://developer.here.com/documentation/geocoder/topics/resource-params-additional.html. 111 | * 112 | * @throws \Geocoder\Exception\Exception 113 | */ 114 | public function testGeocodeWithAdditionalData(): void 115 | { 116 | if (!isset($_SERVER['HERE_API_KEY'])) { 117 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 118 | } 119 | 120 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 121 | 122 | $results = $provider->geocodeQuery(GeocodeQuery::create('Sant Roc, Santa Coloma de Cervelló, Espanya') 123 | ->withData('Country2', 'true') 124 | ->withData('IncludeShapeLevel', 'country') 125 | ->withData('IncludeRoutingInformation', 'true') 126 | ->withLocale('ca')); 127 | 128 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results); 129 | $this->assertCount(1, $results); 130 | 131 | /** @var HereAddress $result */ 132 | $result = $results->first(); 133 | $this->assertInstanceOf(\Geocoder\Model\Address::class, $result); 134 | $this->assertEqualsWithDelta(41.37854, $result->getCoordinates()->getLatitude(), 0.01); 135 | $this->assertEqualsWithDelta(2.01196, $result->getCoordinates()->getLongitude(), 0.01); 136 | $this->assertNotNull($result->getBounds()); 137 | $this->assertEqualsWithDelta(41.36505, $result->getBounds()->getSouth(), 0.01); 138 | $this->assertEqualsWithDelta(1.99398, $result->getBounds()->getWest(), 0.01); 139 | $this->assertEqualsWithDelta(41.39203, $result->getBounds()->getNorth(), 0.01); 140 | $this->assertEqualsWithDelta(2.02994, $result->getBounds()->getEast(), 0.01); 141 | 142 | $this->assertEquals('08690', $result->getPostalCode()); 143 | $this->assertEquals('Sant Roc', $result->getSubLocality()); 144 | $this->assertEquals('Santa Coloma de Cervelló', $result->getLocality()); 145 | $this->assertEquals('Espanya', $result->getCountry()->getName()); 146 | $this->assertEquals('ESP', $result->getCountry()->getCode()); 147 | 148 | $this->assertEquals('ES', $result->getAdditionalDataValue('Country2')); 149 | $this->assertEquals('Espanya', $result->getAdditionalDataValue('CountryName')); 150 | $this->assertEquals('Catalunya', $result->getAdditionalDataValue('StateName')); 151 | $this->assertEquals('Barcelona', $result->getAdditionalDataValue('CountyName')); 152 | $this->assertEquals('district', $result->getAdditionalDataValue('routing_address_matchLevel')); 153 | $this->assertEquals('NT_TzyupfxmTFN0Rh1TXEMqSA', $result->getAdditionalDataValue('routing_locationId')); 154 | $this->assertEquals('address', $result->getAdditionalDataValue('routing_result_type')); 155 | $this->assertEquals('WKTShapeType', $result->getShapeValue('_type')); 156 | $this->assertMatchesRegularExpression('/^MULTIPOLYGON/', $result->getShapeValue('Value')); 157 | } 158 | 159 | /** 160 | * Search for a specific city in a different country. 161 | * 162 | * @throws \Geocoder\Exception\Exception 163 | */ 164 | public function testGeocodeWithExtraFilterCountry(): void 165 | { 166 | if (!isset($_SERVER['HERE_API_KEY'])) { 167 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 168 | } 169 | 170 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 171 | 172 | $queryBarcelonaFromSpain = GeocodeQuery::create('Barcelona')->withData('country', 'ES')->withLocale('ca'); 173 | $queryBarcelonaFromVenezuela = GeocodeQuery::create('Barcelona')->withData('country', 'VE')->withLocale('ca'); 174 | 175 | $resultsSpain = $provider->geocodeQuery($queryBarcelonaFromSpain); 176 | $resultsVenezuela = $provider->geocodeQuery($queryBarcelonaFromVenezuela); 177 | 178 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsSpain); 179 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsVenezuela); 180 | $this->assertCount(1, $resultsSpain); 181 | $this->assertCount(1, $resultsVenezuela); 182 | 183 | $resultSpain = $resultsSpain->first(); 184 | $resultVenezuela = $resultsVenezuela->first(); 185 | 186 | $this->assertEquals('Barcelona', $resultSpain->getLocality()); 187 | $this->assertEquals('Barcelona', $resultVenezuela->getLocality()); 188 | $this->assertEquals('Espanya', $resultSpain->getCountry()->getName()); 189 | $this->assertEquals('República Bolivariana De Venezuela', $resultVenezuela->getCountry()->getName()); 190 | $this->assertEquals('ESP', $resultSpain->getCountry()->getCode()); 191 | $this->assertEquals('VEN', $resultVenezuela->getCountry()->getCode()); 192 | } 193 | 194 | /** 195 | * Search for a specific street in different towns in the same country. 196 | * 197 | * @throws \Geocoder\Exception\Exception 198 | */ 199 | public function testGeocodeWithExtraFilterCity(): void 200 | { 201 | if (!isset($_SERVER['HERE_API_KEY'])) { 202 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 203 | } 204 | 205 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 206 | 207 | $queryStreetCity1 = GeocodeQuery::create('Carrer de Barcelona')->withData('city', 'Sant Vicenç dels Horts')->withLocale('ca')->withLimit(1); 208 | $queryStreetCity2 = GeocodeQuery::create('Carrer de Barcelona')->withData('city', 'Girona')->withLocale('ca')->withLimit(1); 209 | $queryStreetCity3 = GeocodeQuery::create('Carrer de Barcelona')->withData('city', 'Pallejà')->withLocale('ca')->withLimit(1); 210 | 211 | $resultsCity1 = $provider->geocodeQuery($queryStreetCity1); 212 | $resultsCity2 = $provider->geocodeQuery($queryStreetCity2); 213 | $resultsCity3 = $provider->geocodeQuery($queryStreetCity3); 214 | 215 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsCity1); 216 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsCity2); 217 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsCity3); 218 | 219 | $resultCity1 = $resultsCity1->first(); 220 | $resultCity2 = $resultsCity2->first(); 221 | $resultCity3 = $resultsCity3->first(); 222 | 223 | $this->assertEquals('Carrer de Barcelona', $resultCity1->getStreetName()); 224 | $this->assertEquals('Carrer de Barcelona', $resultCity2->getStreetName()); 225 | $this->assertEquals('Carrer de Barcelona', $resultCity3->getStreetName()); 226 | $this->assertEquals('Sant Vicenç dels Horts', $resultCity1->getLocality()); 227 | $this->assertEquals('Girona', $resultCity2->getLocality()); 228 | $this->assertEquals('Pallejà', $resultCity3->getLocality()); 229 | $this->assertEquals('Espanya', $resultCity1->getCountry()->getName()); 230 | $this->assertEquals('Espanya', $resultCity2->getCountry()->getName()); 231 | $this->assertEquals('Espanya', $resultCity3->getCountry()->getName()); 232 | $this->assertEquals('ESP', $resultCity1->getCountry()->getCode()); 233 | $this->assertEquals('ESP', $resultCity2->getCountry()->getCode()); 234 | $this->assertEquals('ESP', $resultCity3->getCountry()->getCode()); 235 | } 236 | 237 | public function testGeocodeWithExtraFilterCounty(): void 238 | { 239 | if (!isset($_SERVER['HERE_API_KEY'])) { 240 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 241 | } 242 | 243 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 244 | 245 | $queryCityRegion1 = GeocodeQuery::create('Cabanes')->withData('county', 'Girona')->withLocale('ca')->withLimit(1); 246 | $queryCityRegion2 = GeocodeQuery::create('Cabanes')->withData('county', 'Castelló')->withLocale('ca')->withLimit(1); 247 | 248 | $resultsRegion1 = $provider->geocodeQuery($queryCityRegion1); 249 | $resultsRegion2 = $provider->geocodeQuery($queryCityRegion2); 250 | 251 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsRegion1); 252 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $resultsRegion2); 253 | 254 | /** @var HereAddress $resultRegion1 */ 255 | $resultRegion1 = $resultsRegion1->first(); 256 | /** @var HereAddress $resultRegion2 */ 257 | $resultRegion2 = $resultsRegion2->first(); 258 | 259 | $this->assertEquals('Cabanes', $resultRegion1->getLocality()); 260 | $this->assertEquals('Cabanes', $resultRegion2->getLocality()); 261 | $this->assertEquals('Girona', $resultRegion1->getAdditionalDataValue('CountyName')); 262 | $this->assertEquals('Castelló', $resultRegion2->getAdditionalDataValue('CountyName')); 263 | $this->assertEquals('Catalunya', $resultRegion1->getAdditionalDataValue('StateName')); 264 | $this->assertEquals('Comunitat Valenciana', $resultRegion2->getAdditionalDataValue('StateName')); 265 | $this->assertEquals('Espanya', $resultRegion1->getCountry()->getName()); 266 | $this->assertEquals('Espanya', $resultRegion2->getCountry()->getName()); 267 | $this->assertEquals('ESP', $resultRegion1->getCountry()->getCode()); 268 | $this->assertEquals('ESP', $resultRegion2->getCountry()->getCode()); 269 | } 270 | 271 | public function testReverseWithRealCoordinates(): void 272 | { 273 | if (!isset($_SERVER['HERE_API_KEY'])) { 274 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 275 | } 276 | 277 | $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); 278 | 279 | $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8632156, 2.3887722)); 280 | 281 | $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results); 282 | $this->assertCount(1, $results); 283 | 284 | /** @var Location $result */ 285 | $result = $results->first(); 286 | $this->assertInstanceOf(\Geocoder\Model\Address::class, $result); 287 | $this->assertEqualsWithDelta(48.8632147, $result->getCoordinates()->getLatitude(), 0.001); 288 | $this->assertEqualsWithDelta(2.3887722, $result->getCoordinates()->getLongitude(), 0.001); 289 | $this->assertNotNull($result->getBounds()); 290 | $this->assertEqualsWithDelta(48.86315, $result->getBounds()->getSouth(), 0.001); 291 | $this->assertEqualsWithDelta(2.38853, $result->getBounds()->getWest(), 0.001); 292 | $this->assertEqualsWithDelta(48.8632147, $result->getBounds()->getNorth(), 0.001); 293 | $this->assertEqualsWithDelta(2.38883, $result->getBounds()->getEast(), 0.001); 294 | $this->assertEquals('Avenue Gambetta', $result->getStreetName()); 295 | $this->assertEquals(75020, $result->getPostalCode()); 296 | $this->assertEquals('Paris', $result->getLocality()); 297 | $this->assertEquals('France', $result->getCountry()->getName()); 298 | $this->assertEquals('FRA', $result->getCountry()->getCode()); 299 | } 300 | 301 | public function testGetName(): void 302 | { 303 | $provider = new Here($this->getMockedHttpClient(), 'appId', 'appCode'); 304 | $this->assertEquals('Here', $provider->getName()); 305 | } 306 | 307 | public function testGeocodeWithInvalidData(): void 308 | { 309 | $this->expectException(\Geocoder\Exception\InvalidServerResponse::class); 310 | 311 | $provider = new Here($this->getMockedHttpClient(), 'appId', 'appCode'); 312 | $provider->geocodeQuery(GeocodeQuery::create('foobar')); 313 | } 314 | 315 | public function testGeocodeIpv4(): void 316 | { 317 | $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); 318 | $this->expectExceptionMessage('The Here provider does not support IP addresses, only street addresses.'); 319 | 320 | $provider = $this->getProvider(); 321 | $provider->geocodeQuery(GeocodeQuery::create('127.0.0.1')); 322 | } 323 | 324 | public function testGeocodeWithLocalhostIPv6(): void 325 | { 326 | $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); 327 | $this->expectExceptionMessage('The Here provider does not support IP addresses, only street addresses.'); 328 | 329 | $provider = $this->getProvider(); 330 | $provider->geocodeQuery(GeocodeQuery::create('::1')); 331 | } 332 | 333 | public function testGeocodeInvalidApiKey(): void 334 | { 335 | $this->expectException(\Geocoder\Exception\InvalidCredentials::class); 336 | $this->expectExceptionMessage('Invalid or missing api key.'); 337 | 338 | $provider = new Here( 339 | $this->getMockedHttpClient( 340 | '{ 341 | "type": { 342 | "subtype": "InvalidCredentials" 343 | } 344 | }' 345 | ), 346 | 'appId', 347 | 'appCode' 348 | ); 349 | $provider->geocodeQuery(GeocodeQuery::create('New York')); 350 | } 351 | 352 | public function testGeocodeWithRealIPv6(): void 353 | { 354 | $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); 355 | $this->expectExceptionMessage('The Here provider does not support IP addresses, only street addresses.'); 356 | 357 | $provider = $this->getProvider(); 358 | $provider->geocodeQuery(GeocodeQuery::create('::ffff:88.188.221.14')); 359 | } 360 | 361 | public function getProvider(): Here 362 | { 363 | if (!isset($_SERVER['HERE_API_KEY'])) { 364 | $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); 365 | } 366 | 367 | return Here::createUsingApiKey($this->getHttpClient(), $_SERVER['HERE_API_KEY']); 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /Tests/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class IntegrationTest extends ProviderIntegrationTest 31 | { 32 | protected bool $testIpv4 = false; 33 | 34 | protected bool $testIpv6 = false; 35 | 36 | protected function createProvider(ClientInterface $httpClient, bool $useCIT = false) 37 | { 38 | return Here::createUsingApiKey($httpClient, $this->getApiKey(), $useCIT); 39 | } 40 | 41 | protected function getCacheDir(): string 42 | { 43 | return __DIR__.'/.cached_responses'; 44 | } 45 | 46 | /** 47 | * This client will make real request if cache was not found. 48 | * 49 | * @return CachedResponseClient 50 | */ 51 | private function getCachedHttpClient() 52 | { 53 | try { 54 | $client = Psr18ClientDiscovery::find(); 55 | } catch (\Http\Discovery\Exception\NotFoundException $e) { 56 | $client = $this->getMockForAbstractClass(ClientInterface::class); 57 | 58 | $client 59 | ->expects($this->any()) 60 | ->method('sendRequest') 61 | ->willThrowException($e); 62 | } 63 | 64 | return new CachedResponseClient($client, $this->getCacheDir(), $this->getApiKey()); 65 | } 66 | 67 | protected function getApiKey(): string 68 | { 69 | return $_SERVER['HERE_APP_ID']; 70 | } 71 | 72 | protected function getAppId(): string 73 | { 74 | return $_SERVER['HERE_APP_ID']; 75 | } 76 | 77 | /** 78 | * @return string the Here AppCode or substring to be removed from cache 79 | */ 80 | protected function getAppCode(): string 81 | { 82 | return $_SERVER['HERE_APP_CODE']; 83 | } 84 | 85 | public function testGeocodeQuery(): void 86 | { 87 | if (isset($this->skippedTests[__FUNCTION__])) { 88 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 89 | } 90 | if (!$this->testAddress) { 91 | $this->markTestSkipped('Geocoding address is not supported by this provider'); 92 | } 93 | 94 | $provider = $this->createProvider($this->getCachedHttpClient()); 95 | $query = GeocodeQuery::create('10 Downing St, London, UK')->withLocale('en'); 96 | $result = $provider->geocodeQuery($query); 97 | $this->assertWellFormattedResult($result); 98 | 99 | // Check Downing Street 100 | $location = $result->first(); 101 | $this->assertEqualsWithDelta(51.5033, $location->getCoordinates()->getLatitude(), 0.1, 'Latitude should be in London'); 102 | $this->assertEqualsWithDelta(-0.1276, $location->getCoordinates()->getLongitude(), 0.1, 'Longitude should be in London'); 103 | $this->assertStringContainsString('Downing', $location->getStreetName(), 'Street name should contain "Downing St"'); 104 | 105 | if (null !== $streetNumber = $location->getStreetNumber()) { 106 | $this->assertStringContainsString('10', $streetNumber, 'Street number should contain "10"'); 107 | } 108 | } 109 | 110 | public function testGeocodeQueryCIT(): void 111 | { 112 | if (isset($this->skippedTests[__FUNCTION__])) { 113 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 114 | } 115 | if (!$this->testAddress) { 116 | $this->markTestSkipped('Geocoding address is not supported by this provider'); 117 | } 118 | 119 | $provider = $this->createProvider($this->getCachedHttpClient(), true); 120 | $query = GeocodeQuery::create('10 Downing St, London, UK')->withLocale('en'); 121 | $result = $provider->geocodeQuery($query); 122 | $this->assertWellFormattedResult($result); 123 | 124 | // Check Downing Street 125 | $location = $result->first(); 126 | $this->assertEqualsWithDelta(51.5033, $location->getCoordinates()->getLatitude(), 0.1, 'Latitude should be in London'); 127 | $this->assertEqualsWithDelta(-0.1276, $location->getCoordinates()->getLongitude(), 0.1, 'Longitude should be in London'); 128 | $this->assertStringContainsString('Downing', $location->getStreetName(), 'Street name should contain "Downing St"'); 129 | 130 | if (null !== $streetNumber = $location->getStreetNumber()) { 131 | $this->assertStringContainsString('10', $streetNumber, 'Street number should contain "10"'); 132 | } 133 | } 134 | 135 | public function testGeocodeQueryWithNoResults(): void 136 | { 137 | if (isset($this->skippedTests[__FUNCTION__])) { 138 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 139 | } 140 | if (!$this->testAddress) { 141 | $this->markTestSkipped('Geocoding address is not supported by this provider'); 142 | } 143 | 144 | $provider = $this->createProvider($this->getCachedHttpClient()); 145 | $query = GeocodeQuery::create('jsajhgsdkfjhsfkjhaldkadjaslgldasd')->withLocale('en'); 146 | $result = $provider->geocodeQuery($query); 147 | $this->assertWellFormattedResult($result); 148 | $this->assertEquals(0, $result->count()); 149 | } 150 | 151 | public function testReverseQuery(): void 152 | { 153 | if (isset($this->skippedTests[__FUNCTION__])) { 154 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 155 | } 156 | if (!$this->testReverse) { 157 | $this->markTestSkipped('Reverse geocoding address is not supported by this provider'); 158 | } 159 | 160 | $provider = $this->createProvider($this->getCachedHttpClient()); 161 | 162 | // Close to the white house 163 | $result = $provider->reverseQuery(ReverseQuery::fromCoordinates(38.900206, -77.036991)->withLocale('en')); 164 | $this->assertWellFormattedResult($result); 165 | } 166 | 167 | public function testReverseQueryCIT(): void 168 | { 169 | if (isset($this->skippedTests[__FUNCTION__])) { 170 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 171 | } 172 | if (!$this->testReverse) { 173 | $this->markTestSkipped('Reverse geocoding address is not supported by this provider'); 174 | } 175 | 176 | $provider = $this->createProvider($this->getCachedHttpClient(), true); 177 | 178 | // Close to the white house 179 | $result = $provider->reverseQuery(ReverseQuery::fromCoordinates(38.900206, -77.036991)->withLocale('en')); 180 | $this->assertWellFormattedResult($result); 181 | } 182 | 183 | public function testReverseQueryWithNoResults(): void 184 | { 185 | if (isset($this->skippedTests[__FUNCTION__])) { 186 | $this->markTestSkipped($this->skippedTests[__FUNCTION__]); 187 | } 188 | 189 | if (!$this->testReverse) { 190 | $this->markTestSkipped('Reverse geocoding address is not supported by this provider'); 191 | } 192 | 193 | $provider = $this->createProvider($this->getCachedHttpClient()); 194 | 195 | $result = $provider->reverseQuery(ReverseQuery::fromCoordinates(0, 0)); 196 | $this->assertEquals(0, $result->count()); 197 | } 198 | 199 | /** 200 | * Make sure that a result for a Geocoder is well formatted. Be aware that even 201 | * a Location with no data may be well formatted. 202 | */ 203 | private function assertWellFormattedResult(Collection $result): void 204 | { 205 | $this->assertInstanceOf( 206 | Collection::class, 207 | $result, 208 | 'The result must be an instance of a Geocoder\Collection' 209 | ); 210 | 211 | /** @var Location $location */ 212 | foreach ($result as $location) { 213 | $this->assertInstanceOf( 214 | Location::class, 215 | $location, 216 | 'All items in Geocoder\Collection must implement Geocoder\Location' 217 | ); 218 | 219 | $this->assertInstanceOf( 220 | AdminLevelCollection::class, 221 | $location->getAdminLevels(), 222 | 'Location::getAdminLevels MUST always return a AdminLevelCollection' 223 | ); 224 | $arrayData = $location->toArray(); 225 | $this->assertTrue(is_array($arrayData), 'Location::toArray MUST return an array.'); 226 | $this->assertNotEmpty($arrayData, 'Location::toArray cannot be empty.'); 227 | 228 | // Verify coordinates 229 | if (null !== $coords = $location->getCoordinates()) { 230 | $this->assertInstanceOf( 231 | Coordinates::class, 232 | $coords, 233 | 'Location::getCoordinates MUST always return a Coordinates or null' 234 | ); 235 | 236 | // Using "assertNotEmpty" means that we can not have test code where coordinates is on equator or long = 0 237 | $this->assertNotEmpty($coords->getLatitude(), 'If coordinate object exists it cannot have an empty latitude.'); 238 | $this->assertNotEmpty($coords->getLongitude(), 'If coordinate object exists it cannot have an empty longitude.'); 239 | } 240 | 241 | // Verify bounds 242 | if (null !== $bounds = $location->getBounds()) { 243 | $this->assertInstanceOf( 244 | Bounds::class, 245 | $bounds, 246 | 'Location::getBounds MUST always return a Bounds or null' 247 | ); 248 | 249 | // Using "assertNotEmpty" means that we can not have test code where coordinates is on equator or long = 0 250 | $this->assertNotEmpty($bounds->getSouth(), 'If bounds object exists it cannot have an empty values.'); 251 | $this->assertNotEmpty($bounds->getWest(), 'If bounds object exists it cannot have an empty values.'); 252 | $this->assertNotEmpty($bounds->getNorth(), 'If bounds object exists it cannot have an empty values.'); 253 | $this->assertNotEmpty($bounds->getEast(), 'If bounds object exists it cannot have an empty values.'); 254 | } 255 | 256 | // Check country 257 | if (null !== $country = $location->getCountry()) { 258 | $this->assertInstanceOf( 259 | Country::class, 260 | $country, 261 | 'Location::getCountry MUST always return a Country or null' 262 | ); 263 | $this->assertFalse(null === $country->getCode() && null === $country->getName(), 'Both code and name cannot be empty'); 264 | 265 | if (null !== $country->getCode()) { 266 | $this->assertNotEmpty( 267 | $location->getCountry()->getCode(), 268 | 'The Country should not have an empty code.' 269 | ); 270 | } 271 | 272 | if (null !== $country->getName()) { 273 | $this->assertNotEmpty( 274 | $location->getCountry()->getName(), 275 | 'The Country should not have an empty name.' 276 | ); 277 | } 278 | } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geocoder-php/here-provider", 3 | "type": "library", 4 | "description": "Geocoder here adapter", 5 | "keywords": [], 6 | "homepage": "http://geocoder-php.org/Geocoder/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "William Durand", 11 | "email": "william.durand1@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.0", 16 | "geocoder-php/common-http": "^4.0", 17 | "willdurand/geocoder": "^4.0|^5.0" 18 | }, 19 | "provide": { 20 | "geocoder-php/provider-implementation": "1.0" 21 | }, 22 | "require-dev": { 23 | "geocoder-php/provider-integration-tests": "^1.6.3", 24 | "php-http/message": "^1.0", 25 | "phpunit/phpunit": "^9.6.11" 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Geocoder\\Provider\\Here\\": "" 35 | }, 36 | "exclude-from-classmap": [ 37 | "/Tests/" 38 | ] 39 | }, 40 | "minimum-stability": "dev", 41 | "scripts": { 42 | "test": "vendor/bin/phpunit", 43 | "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./ 6 | 7 | 8 | ./Tests 9 | ./vendor 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./Tests/ 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------