Code

< 40 %
40-60 %
> 60 %
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\Http\Provider\AbstractHttpProvider;
21
use Geocoder\Model\Address;
22
use Geocoder\Model\AddressCollection;
23
use Geocoder\Provider\Provider;
24
use Geocoder\Query\GeocodeQuery;
25
use Geocoder\Query\ReverseQuery;
26
use Psr\Http\Client\ClientInterface;
27
28
/**
29
 * @author ALKOUM Dorian <[email protected]>
30
 */
31
final class ArcGISOnline extends AbstractHttpProvider implements Provider
32
{
33
    /**
34
     * @var string
35
     */
36
    public const ENDPOINT_URL = 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates?SingleLine=%s';
37
38
    /**
39
     * @var string
40
     */
41
    public 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
    public 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 ClientInterface $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
        ClientInterface $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 ClientInterface $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(ClientInterface $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
    }
94
95 16
    public function geocodeQuery(GeocodeQuery $query): Collection
96
    {
97 16
        $address = $query->getText();
98 16
        if (filter_var($address, FILTER_VALIDATE_IP)) {
99 4
            throw new UnsupportedOperation('The ArcGISOnline provider does not support IP addresses, only street addresses.');
100
        }
101
102
        // Save a request if no valid address entered
103 12
        if (empty($address)) {
104
            throw new InvalidArgument('Address cannot be empty.');
105
        }
106
107 12
        if (is_null($this->token)) {
108 10
            $url = sprintf(self::ENDPOINT_URL, urlencode($address));
109
        } else {
110 2
            $url = sprintf(self::TOKEN_ENDPOINT_URL, $this->token, urlencode($this->formatAddresses([$address])));
111
        }
112 12
        $json = $this->executeQuery($url, $query->getLimit());
113
114 6
        $property = is_null($this->token) ? 'candidates' : 'locations';
115
116
        // no result
117 6
        if (!property_exists($json, $property) || empty($json->{$property})) {
118 2
            return new AddressCollection([]);
119
        }
120
121 4
        $results = [];
122 4
        foreach ($json->{$property} as $location) {
123 4
            $data = $location->attributes;
124
125 4
            $coordinates = (array) $location->location;
126 4
            $streetName = !empty($data->StAddr) ? $data->StAddr : null;
127 4
            $streetNumber = !empty($data->AddNum) ? $data->AddNum : null;
128 4
            $city = !empty($data->City) ? $data->City : null;
129 4
            $zipcode = !empty($data->Postal) ? $data->Postal : null;
130 4
            $countryCode = !empty($data->Country) ? $data->Country : null;
131
132 4
            $adminLevels = [];
133 4
            foreach (['Region', 'Subregion'] as $i => $property) {
134 4
                if (!empty($data->{$property})) {
135 4
                    $adminLevels[] = ['name' => $data->{$property}, 'level' => $i + 1];
136
                }
137
            }
138
139 4
            $results[] = Address::createFromArray([
140 4
                'providedBy' => $this->getName(),
141 4
                'latitude' => $coordinates['y'],
142 4
                'longitude' => $coordinates['x'],
143 4
                'streetNumber' => $streetNumber,
144 4
                'streetName' => $streetName,
145 4
                'locality' => $city,
146 4
                'postalCode' => $zipcode,
147 4
                'adminLevels' => $adminLevels,
148 4
                'countryCode' => $countryCode,
149 4
            ]);
150
        }
151
152 4
        return new AddressCollection($results);
153
    }
154
155 7
    public function reverseQuery(ReverseQuery $query): Collection
156
    {
157 7
        $coordinates = $query->getCoordinates();
158 7
        $longitude = $coordinates->getLongitude();
159 7
        $latitude = $coordinates->getLatitude();
160
161 7
        $url = sprintf(self::REVERSE_ENDPOINT_URL, $longitude, $latitude);
162 7
        $json = $this->executeQuery($url, $query->getLimit());
163
164 2
        if (property_exists($json, 'error')) {
165
            return new AddressCollection([]);
166
        }
167
168 2
        $data = $json->address;
169
170 2
        $streetName = !empty($data->Address) ? $data->Address : null;
171 2
        $city = !empty($data->City) ? $data->City : null;
172 2
        $zipcode = !empty($data->Postal) ? $data->Postal : null;
173 2
        $region = !empty($data->Region) ? $data->Region : null;
174 2
        $county = !empty($data->Subregion) ? $data->Subregion : null;
175 2
        $countryCode = !empty($data->CountryCode) ? $data->CountryCode : null;
176
177 2
        return new AddressCollection([
178 2
            Address::createFromArray([
179 2
                'providedBy' => $this->getName(),
180 2
                'latitude' => $latitude,
181 2
                'longitude' => $longitude,
182 2
                'streetName' => $streetName,
183 2
                'locality' => $city,
184 2
                'postalCode' => $zipcode,
185 2
                'region' => $region,
186 2
                'countryCode' => $countryCode,
187 2
                'county' => $county,
188 2
            ]),
189 2
        ]);
190
    }
191
192 7
    public function getName(): string
193
    {
194 7
        return 'arcgis_online';
195
    }
196
197 19
    private function buildQuery(string $query, int $limit): string
198
    {
199 19
        if (null !== $this->sourceCountry) {
200 1
            $query = sprintf('%s&sourceCountry=%s', $query, $this->sourceCountry);
201
        }
202 19
        if (is_null($this->token)) {
203 17
            $query = sprintf('%s&maxLocations=%d&outFields=*', $query, $limit);
204
        }
205
206 19
        return sprintf('%s&f=%s', $query, 'json');
207
    }
208
209 19
    private function executeQuery(string $url, int $limit): \stdClass
210
    {
211 19
        $url = $this->buildQuery($url, $limit);
212 19
        $content = $this->getUrlContents($url);
213 9
        $json = json_decode($content);
214
215
        // API error
216 9
        if (!isset($json)) {
217
            throw InvalidServerResponse::create($url);
218
        }
219 9
        if (property_exists($json, 'error') && property_exists($json->error, 'message')) {
220 1
            if ('Invalid Token' == $json->error->message) {
221 1
                throw new InvalidCredentials(sprintf('Invalid token %s', $this->token));
222
            }
223
        }
224
225 8
        return $json;
226
    }
227
228
    /**
229
     * Formatter for 1..n addresses, for the geocodeAddresses endpoint.
230
     *
231
     * @param array $array an array of SingleLine addresses
232
     *
233
     * @return string an Array formatted as a JSON string
234
     */
235 2
    private function formatAddresses(array $array): string
236
    {
237
        // Just in case, get rid of any custom, non-numeric indices.
238 2
        $array = array_values($array);
239
240 2
        $addresses = [
241 2
            'records' => [],
242 2
        ];
243 2
        foreach ($array as $i => $address) {
244 2
            $addresses['records'][] = [
245 2
                'attributes' => [
246 2
                    'OBJECTID' => $i + 1,
247 2
                    'SingleLine' => $address,
248 2
                ],
249 2
            ];
250
        }
251
252 2
        return json_encode($addresses);
253
    }
254
}
255