Completed
Push — master ( aabf36...cf3e1f )
by Tobias
01:12
created

ArcGISOnline::reverseQuery()   B

Complexity

Conditions 8
Paths 65

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 8.0036

Importance

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