Completed
Push — master ( fe61a1...b02243 )
by Tobias
24:51
created

ArcGISOnline::token()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
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\ArcGISOnline;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidCredentials;
18
use Geocoder\Exception\InvalidServerResponse;
19
use Geocoder\Exception\UnsupportedOperation;
20
use Geocoder\Model\Address;
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 Http\Client\HttpClient;
27
28
/**
29
 * @author ALKOUM Dorian <[email protected]>
30
 */
31
final class ArcGISOnline extends AbstractHttpProvider implements Provider
32
{
33
    /**
34
     * @var string
35
     */
36
    const ENDPOINT_URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates?SingleLine=%s';
37
38
    /**
39
     * @var string
40
     */
41
    const TOKEN_ENDPOINT_URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/geocodeAddresses?token=%s&addresses=%s';
42
43
    /**
44
     * @var string
45
     */
46
    const REVERSE_ENDPOINT_URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode?location=%F,%F';
47
48
    /**
49
     * @var string
50
     */
51 23
    private $sourceCountry;
52
53 23
    /**
54
     * @var string
55 23
     *
56 23
     * Currently valid ArcGIS World Geocoding Service token.
57
     * https://developers.arcgis.com/rest/geocode/api-reference/geocoding-authenticate-a-request.htm
58
     */
59
    private $token;
60
61 14
    /**
62
     * ArcGIS World Geocoding Service.
63 14
     * https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm.
64 14
     *
65 4
     * @param HttpClient $client        An HTTP adapter
66
     * @param string     $token         Your authentication token
67
     * @param string     $sourceCountry Country biasing (optional)
68
     *
69 10
     * @return ArcGISOnline
70
     */
71
    public static function token(
72
        HttpClient $client,
73 10
        string $token,
74 10
        string $sourceCountry = null
75
    ) {
76
        $provider = new self($client, $sourceCountry, $token);
77 5
78 2
        return $provider;
79
    }
80
81 3
    /**
82 3
     * @param HttpClient $client        An HTTP adapter
83 3
     * @param string     $sourceCountry Country biasing (optional)
84
     * @param string     $token         ArcGIS World Geocoding Service token
85 3
     *                                  Required for the geocodeAddresses endpoint
86 3
     */
87 3
    public function __construct(HttpClient $client, string $sourceCountry = null, string $token = null)
88 3
    {
89 3
        parent::__construct($client);
90 3
91
        $this->sourceCountry = $sourceCountry;
92 3
        $this->token = $token;
93 3
    }
94 3
95 3
    /**
96
     * {@inheritdoc}
97
     */
98
    public function geocodeQuery(GeocodeQuery $query): Collection
99 3
    {
100 3
        $address = $query->getText();
101 3
        if (filter_var($address, FILTER_VALIDATE_IP)) {
102 3
            throw new UnsupportedOperation('The ArcGISOnline provider does not support IP addresses, only street addresses.');
103 3
        }
104 3
105 3
        // Save a request if no valid address entered
106 3
        if (empty($address)) {
107 3
            throw new InvalidArgument('Address cannot be empty.');
108 3
        }
109
110
        if (is_null($this->token)) {
111
            $url = sprintf(self::ENDPOINT_URL, urlencode($address));
112 3
        } else {
113
            $url = sprintf(self::TOKEN_ENDPOINT_URL, $this->token, urlencode($this->formatAddresses([$address])));
114
        }
115
        $json = $this->executeQuery($url, $query->getLimit());
116
117
        $property = is_null($this->token) ? 'candidates' : 'locations';
118 8
119
        // no result
120 8
        if (!property_exists($json, $property) || empty($json->{$property})) {
121 8
            return new AddressCollection([]);
122 8
        }
123
124 8
        $results = [];
125 8
        foreach ($json->{$property} as $location) {
126
            $data = $location->attributes;
127 3
128 1
            $coordinates = (array) $location->location;
129
            $streetName = !empty($data->StAddr) ? $data->StAddr : null;
130
            $streetNumber = !empty($data->AddNum) ? $data->AddNum : null;
131 2
            $city = !empty($data->City) ? $data->City : null;
132
            $zipcode = !empty($data->Postal) ? $data->Postal : null;
133 2
            $countryCode = !empty($data->Country) ? $data->Country : null;
134 2
135 2
            $adminLevels = [];
136 2
            foreach (['Region', 'Subregion'] as $i => $property) {
137 2
                if (!empty($data->{$property})) {
138 2
                    $adminLevels[] = ['name' => $data->{$property}, 'level' => $i + 1];
139
                }
140 2
            }
141 2
142 2
            $results[] = Address::createFromArray([
143 2
                'providedBy' => $this->getName(),
144 2
                'latitude' => $coordinates['y'],
145 2
                'longitude' => $coordinates['x'],
146 2
                'streetNumber' => $streetNumber,
147 2
                'streetName' => $streetName,
148 2
                'locality' => $city,
149 2
                'postalCode' => $zipcode,
150 2
                'adminLevels' => $adminLevels,
151
                'countryCode' => $countryCode,
152
            ]);
153
        }
154
155
        return new AddressCollection($results);
156
    }
157
158 6
    /**
159
     * {@inheritdoc}
160 6
     */
161
    public function reverseQuery(ReverseQuery $query): Collection
162
    {
163
        $coordinates = $query->getCoordinates();
164
        $longitude = $coordinates->getLongitude();
165
        $latitude = $coordinates->getLatitude();
166
167
        $url = sprintf(self::REVERSE_ENDPOINT_URL, $longitude, $latitude);
168
        $json = $this->executeQuery($url, $query->getLimit());
169 18
170
        if (property_exists($json, 'error')) {
171 18
            return new AddressCollection([]);
172 1
        }
173
174
        $data = $json->address;
175 18
176
        $streetName = !empty($data->Address) ? $data->Address : null;
177
        $city = !empty($data->City) ? $data->City : null;
178
        $zipcode = !empty($data->Postal) ? $data->Postal : null;
179
        $region = !empty($data->Region) ? $data->Region : null;
180
        $county = !empty($data->Subregion) ? $data->Subregion : null;
181
        $countryCode = !empty($data->CountryCode) ? $data->CountryCode : null;
182
183
        return new AddressCollection([
184 18
            Address::createFromArray([
185
                'providedBy' => $this->getName(),
186 18
                'latitude' => $latitude,
187 18
                'longitude' => $longitude,
188 8
                'streetName' => $streetName,
189
                'locality' => $city,
190
                'postalCode' => $zipcode,
191 8
                'region' => $region,
192
                'countryCode' => $countryCode,
193
                'county' => $county,
194
            ]),
195 8
        ]);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function getName(): string
202
    {
203
        return 'arcgis_online';
204
    }
205
206
    /**
207
     * @param string $query
208
     * @param int    $limit
209
     *
210
     * @return string
211
     */
212
    private function buildQuery(string $query, int $limit): string
213
    {
214
        if (null !== $this->sourceCountry) {
215
            $query = sprintf('%s&sourceCountry=%s', $query, $this->sourceCountry);
216
        }
217
        if (is_null($this->token)) {
218
            $query = sprintf('%s&maxLocations=%d&outFields=*', $query, $limit);
219
        }
220
221
        return sprintf('%s&f=%s', $query, 'json');
222
    }
223
224
    /**
225
     * @param string $url
226
     * @param int    $limit
227
     *
228
     * @return \stdClass
229
     */
230
    private function executeQuery(string $url, int $limit): \stdClass
231
    {
232
        $url = $this->buildQuery($url, $limit);
233
        $content = $this->getUrlContents($url);
234
        $json = json_decode($content);
235
236
        // API error
237
        if (!isset($json)) {
238
            throw InvalidServerResponse::create($url);
239
        }
240
        if (property_exists($json, 'error') && property_exists($json->error, 'message')) {
241
            if ('Invalid Token' == $json->error->message) {
242
                throw new InvalidCredentials(sprintf('Invalid token %s', $this->token));
243
            }
244
        }
245
246
        return $json;
247
    }
248
249
    /**
250
     * Formatter for 1..n addresses, for the geocodeAddresses endpoint.
251
     *
252
     * @param array $array an array of SingleLine addresses
253
     *
254
     * @return string an Array formatted as a JSON string
255
     */
256
    private function formatAddresses(array $array): string
257
    {
258
        // Just in case, get rid of any custom, non-numeric indices.
259
        $array = array_values($array);
260
261
        $addresses = [
262
            'records' => [],
263
        ];
264
        foreach ($array as $i => $address) {
265
            $addresses['records'][] = [
266
                'attributes' => [
267
                    'OBJECTID' => $i + 1,
268
                    'SingleLine' => $address,
269
                ],
270
            ];
271
        }
272
273
        return json_encode($addresses);
274
    }
275
}
276