Completed
Push — master ( a95cf3...a01099 )
by Tobias
01:35
created

Here.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Geocoder package.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT License
11
 */
12
13
namespace Geocoder\Provider\Here;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidCredentials;
18
use Geocoder\Exception\QuotaExceeded;
19
use Geocoder\Exception\UnsupportedOperation;
20
use Geocoder\Model\AddressBuilder;
21
use Geocoder\Model\AddressCollection;
22
use Geocoder\Query\GeocodeQuery;
23
use Geocoder\Query\ReverseQuery;
24
use Geocoder\Http\Provider\AbstractHttpProvider;
25
use Geocoder\Provider\Provider;
26
use Geocoder\Provider\Here\Model\HereAddress;
27
use Http\Client\HttpClient;
28
29
/**
30
 * @author Sébastien Barré <[email protected]>
31
 */
32
final class Here extends AbstractHttpProvider implements Provider
33
{
34
    /**
35
     * @var string
36
     */
37
    const GEOCODE_ENDPOINT_URL = 'https://geocoder.api.here.com/6.2/geocode.json?app_id=%s&app_code=%s&searchtext=%s&gen=9';
38
39
    /**
40
     * @var string
41
     */
42
    const REVERSE_ENDPOINT_URL = 'https://reverse.geocoder.api.here.com/6.2/reversegeocode.json?prox=%F,%F,250&app_id=%s&app_code=%s&mode=retrieveAddresses&gen=9&maxresults=%d';
43
44
    /**
45
     * @var string
46
     */
47
    const GEOCODE_CIT_ENDPOINT_URL = 'https://geocoder.cit.api.here.com/6.2/geocode.json?app_id=%s&app_code=%s&searchtext=%s&gen=9';
48
49
    /**
50
     * @var string
51
     */
52
    const REVERSE_CIT_ENDPOINT_URL = 'https://reverse.geocoder.cit.api.here.com/6.2/reversegeocode.json?prox=%F,%F,250&app_id=%s&app_code=%s&mode=retrieveAddresses&gen=9&maxresults=%d';
53
54
    /**
55
     * @var string
56
     */
57
    private $appId;
58
59
    /**
60
     * @var string
61
     */
62
    private $appCode;
63
64
    /**
65
     * @var bool
66
     */
67
    private $useCIT;
68
69
    /**
70
     * @param HttpClient $client  an HTTP adapter
71
     * @param string     $appId   an App ID
72
     * @param string     $appCode an App code
73
     * @param bool       $useCIT  use Customer Integration Testing environment (CIT) instead of production
74
     */
75
    public function __construct(HttpClient $client, string $appId, string $appCode, bool $useCIT = false)
76
    {
77
        if (empty($appId) || empty($appCode)) {
78
            throw new InvalidCredentials('Invalid or missing api key.');
79
        }
80
        $this->appId = $appId;
81
        $this->appCode = $appCode;
82
        $this->useCIT = $useCIT;
83
84
        parent::__construct($client);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function geocodeQuery(GeocodeQuery $query): Collection
91
    {
92
        // This API doesn't handle IPs
93
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
94
            throw new UnsupportedOperation('The Here provider does not support IP addresses, only street addresses.');
95
        }
96
97
        $url = sprintf($this->useCIT ? self::GEOCODE_CIT_ENDPOINT_URL : self::GEOCODE_ENDPOINT_URL, $this->appId, $this->appCode, rawurlencode($query->getText()));
98
99
        if (null !== $query->getData('country')) {
100
            $url = sprintf('%s&country=%s', $url, rawurlencode($query->getData('country')));
101
        }
102
103
        if (null !== $query->getData('state')) {
104
            $url = sprintf('%s&state=%s', $url, rawurlencode($query->getData('state')));
105
        }
106
107
        if (null !== $query->getData('county')) {
108
            $url = sprintf('%s&county=%s', $url, rawurlencode($query->getData('county')));
109
        }
110
111
        if (null !== $query->getData('city')) {
112
            $url = sprintf('%s&city=%s', $url, rawurlencode($query->getData('city')));
113
        }
114
115
        if (null !== $query->getLocale()) {
116
            $url = sprintf('%s&language=%s', $url, $query->getLocale());
117
        }
118
119
        $additionalDataParam = [];
120
        if (null !== $query->getData('CrossingStreets')) {
121
            $additionalDataParam['CrossingStreets'] = $query->getData('CrossingStreets');
122
        }
123
124
        if (null !== $query->getData('PreserveUnitDesignators')) {
125
            $additionalDataParam['PreserveUnitDesignators'] = $query->getData('PreserveUnitDesignators');
126
        }
127
128
        if (null !== $query->getData('Country2')) {
129
            $additionalDataParam['Country2'] = $query->getData('Country2');
130
        }
131
132
        if (null !== $query->getData('IncludeChildPOIs')) {
133
            $additionalDataParam['IncludeChildPOIs'] = $query->getData('IncludeChildPOIs');
134
        }
135
136
        if (null !== $query->getData('IncludeRoutingInformation')) {
137
            $additionalDataParam['IncludeRoutingInformation'] = $query->getData('IncludeRoutingInformation');
138
        }
139
140
        if (null !== $query->getData('AdditionalAddressProvider')) {
141
            $additionalDataParam['AdditionalAddressProvider'] = $query->getData('AdditionalAddressProvider');
142
        }
143
144
        if (null !== $query->getData('HouseNumberMode')) {
145
            $additionalDataParam['HouseNumberMode'] = $query->getData('HouseNumberMode');
146
        }
147
148
        if (null !== $query->getData('FlexibleAdminValues')) {
149
            $additionalDataParam['FlexibleAdminValues'] = $query->getData('FlexibleAdminValues');
150
        }
151
152
        if (null !== $query->getData('IntersectionSnapTolerance')) {
153
            $additionalDataParam['IntersectionSnapTolerance'] = $query->getData('IntersectionSnapTolerance');
154
        }
155
156
        if (null !== $query->getData('AddressRangeSqueezeOffset')) {
157
            $additionalDataParam['AddressRangeSqueezeOffset'] = $query->getData('AddressRangeSqueezeOffset');
158
        }
159
160
        if (null !== $query->getData('AddressRangeSqueezeFactor')) {
161
            $additionalDataParam['AddressRangeSqueezeFactor'] = $query->getData('AddressRangeSqueezeFactor');
162
        }
163
164
        if (null !== $query->getData('IncludeShapeLevel')) {
165
            $additionalDataParam['IncludeShapeLevel'] = $query->getData('IncludeShapeLevel');
166
        }
167
168
        if (null !== $query->getData('RestrictLevel')) {
169
            $additionalDataParam['RestrictLevel'] = $query->getData('RestrictLevel');
170
        }
171
172
        if (null !== $query->getData('SuppressStreetType')) {
173
            $additionalDataParam['SuppressStreetType'] = $query->getData('SuppressStreetType');
174
        }
175
176
        if (null !== $query->getData('NormalizeNames')) {
177
            $additionalDataParam['NormalizeNames'] = $query->getData('NormalizeNames');
178
        }
179
180
        if (null !== $query->getData('IncludeMicroPointAddresses')) {
181
            $additionalDataParam['IncludeMicroPointAddresses'] = $query->getData('IncludeMicroPointAddresses');
182
        }
183
184
        $additionalDataParam['IncludeShapeLevel'] = 'country';
185
186
        if (!empty($additionalDataParam)) {
187
            $url = sprintf('%s&additionaldata=%s', $url, $this->serializeComponents($additionalDataParam));
188
        }
189
190
        return $this->executeQuery($url, $query->getLimit());
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function reverseQuery(ReverseQuery $query): Collection
197
    {
198
        $coordinates = $query->getCoordinates();
199
        $url = sprintf($this->useCIT ? self::REVERSE_CIT_ENDPOINT_URL : self::REVERSE_ENDPOINT_URL, $coordinates->getLatitude(), $coordinates->getLongitude(), $this->appId, $this->appCode, $query->getLimit());
200
201
        return $this->executeQuery($url, $query->getLimit());
202
    }
203
204
    /**
205
     * @param string $url
206
     * @param int    $limit
207
     *
208
     * @return Collection
209
     */
210
    private function executeQuery(string $url, int $limit): Collection
211
    {
212
        $content = $this->getUrlContents($url);
213
        $json = json_decode($content, true);
214
215
        if (isset($json['type'])) {
216
            switch ($json['type']['subtype']) {
217
                case 'InvalidInputData':
218
                    throw new InvalidArgument('Input parameter validation failed.');
219
                case 'QuotaExceeded':
220
                    throw new QuotaExceeded('Valid request but quota exceeded.');
221
                case 'InvalidCredentials':
222
                    throw new InvalidCredentials('Invalid or missing api key.');
223
            }
224
        }
225
226
        if (!isset($json['Response']) || empty($json['Response'])) {
227
            return new AddressCollection([]);
228
        }
229
230
        if (!isset($json['Response']['View'][0])) {
231
            return new AddressCollection([]);
232
        }
233
234
        $locations = $json['Response']['View'][0]['Result'];
235
236
        foreach ($locations as $loc) {
237
            $location = $loc['Location'];
238
            $builder = new AddressBuilder($this->getName());
239
            $coordinates = isset($location['NavigationPosition'][0]) ? $location['NavigationPosition'][0] : $location['DisplayPosition'];
240
            $builder->setCoordinates($coordinates['Latitude'], $coordinates['Longitude']);
241
            $bounds = $location['MapView'];
242
243
            $builder->setBounds($bounds['BottomRight']['Latitude'], $bounds['TopLeft']['Longitude'], $bounds['TopLeft']['Latitude'], $bounds['BottomRight']['Longitude']);
244
            $builder->setStreetNumber($location['Address']['HouseNumber'] ?? null);
245
            $builder->setStreetName($location['Address']['Street'] ?? null);
246
            $builder->setPostalCode($location['Address']['PostalCode'] ?? null);
247
            $builder->setLocality($location['Address']['City'] ?? null);
248
            $builder->setSubLocality($location['Address']['District'] ?? null);
249
            $builder->setCountryCode($location['Address']['Country'] ?? null);
250
251
            // The name of the country can be found in the AdditionalData.
252
            $additionalData = $location['Address']['AdditionalData'] ?? null;
253
            if (!empty($additionalData)) {
254
                $builder->setCountry($additionalData[array_search('CountryName', array_column($additionalData, 'key'))]['value'] ?? null);
255
            }
256
257
            // There may be a second AdditionalData. For example if "IncludeRoutingInformation" parameter is added
258
            $extraAdditionalData = $loc['AdditionalData'] ?? [];
259
260
            /** @var HereAddress $address */
261
            $address = $builder->build(HereAddress::class);
262
            $address = $address->withLocationId($location['LocationId']);
263
            $address = $address->withLocationType($location['LocationType']);
264
            $address = $address->withAdditionalData(array_merge($additionalData, $extraAdditionalData));
265
            $address = $address->withShape($location['Shape'] ?? null);
266
            $results[] = $address;
267
268
            if (count($results) >= $limit) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $results seems to be defined later in this foreach loop on line 266. Are you sure it is defined here?
Loading history...
269
                break;
270
            }
271
        }
272
273
        return new AddressCollection($results);
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function getName(): string
280
    {
281
        return 'Here';
282
    }
283
284
    /**
285
     * Serialize the component query parameter.
286
     *
287
     * @param array $components
288
     *
289
     * @return string
290
     */
291
    private function serializeComponents(array $components): string
292
    {
293
        return implode(';', array_map(function ($name, $value) {
294
            return sprintf('%s,%s', $name, $value);
295
        }, array_keys($components), $components));
296
    }
297
}
298