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\Mapzen; |
||
14 | |||
15 | use Geocoder\Collection; |
||
16 | use Geocoder\Exception\InvalidCredentials; |
||
17 | use Geocoder\Exception\QuotaExceeded; |
||
18 | use Geocoder\Exception\UnsupportedOperation; |
||
19 | use Geocoder\Model\Address; |
||
20 | use Geocoder\Model\AddressCollection; |
||
21 | use Geocoder\Query\GeocodeQuery; |
||
22 | use Geocoder\Query\ReverseQuery; |
||
23 | use Geocoder\Http\Provider\AbstractHttpProvider; |
||
24 | use Geocoder\Provider\Provider; |
||
25 | use Http\Client\HttpClient; |
||
26 | |||
27 | /** |
||
28 | * Mapzen has shut down as their APIs as of February 1, 2018. |
||
29 | * |
||
30 | * @deprecated https://github.com/geocoder-php/Geocoder/issues/808 |
||
31 | */ |
||
32 | final class Mapzen extends AbstractHttpProvider implements Provider |
||
33 | { |
||
34 | /** |
||
35 | * @var string |
||
36 | */ |
||
37 | const GEOCODE_ENDPOINT_URL = 'https://search.mapzen.com/v1/search?text=%s&api_key=%s&size=%d'; |
||
38 | |||
39 | /** |
||
40 | * @var string |
||
41 | */ |
||
42 | const REVERSE_ENDPOINT_URL = 'https://search.mapzen.com/v1/reverse?point.lat=%f&point.lon=%f&api_key=%s&size=%d'; |
||
43 | |||
44 | /** |
||
45 | * @var string |
||
46 | */ |
||
47 | private $apiKey; |
||
48 | |||
49 | /** |
||
50 | * Mapzen has shut down as their APIs as of February 1, 2018. |
||
51 | * |
||
52 | * @deprecated https://github.com/geocoder-php/Geocoder/issues/808 |
||
53 | * |
||
54 | * @param HttpClient $client an HTTP adapter |
||
55 | * @param string $apiKey an API key |
||
56 | */ |
||
57 | 26 | public function __construct(HttpClient $client, string $apiKey) |
|
58 | { |
||
59 | 26 | if (empty($apiKey)) { |
|
60 | throw new InvalidCredentials('No API key provided.'); |
||
61 | } |
||
62 | |||
63 | 26 | $this->apiKey = $apiKey; |
|
64 | 26 | parent::__construct($client); |
|
65 | 26 | } |
|
66 | |||
67 | /** |
||
68 | * {@inheritdoc} |
||
69 | */ |
||
70 | 17 | public function geocodeQuery(GeocodeQuery $query): Collection |
|
71 | { |
||
72 | 17 | $address = $query->getText(); |
|
73 | |||
74 | // This API doesn't handle IPs |
||
75 | 17 | if (filter_var($address, FILTER_VALIDATE_IP)) { |
|
76 | 4 | throw new UnsupportedOperation('The Mapzen provider does not support IP addresses, only street addresses.'); |
|
77 | } |
||
78 | |||
79 | 13 | $url = sprintf(self::GEOCODE_ENDPOINT_URL, urlencode($address), $this->apiKey, $query->getLimit()); |
|
80 | |||
81 | 13 | return $this->executeQuery($url); |
|
82 | } |
||
83 | |||
84 | /** |
||
85 | * {@inheritdoc} |
||
86 | */ |
||
87 | 8 | public function reverseQuery(ReverseQuery $query): Collection |
|
88 | { |
||
89 | 8 | $coordinates = $query->getCoordinates(); |
|
90 | 8 | $longitude = $coordinates->getLongitude(); |
|
91 | 8 | $latitude = $coordinates->getLatitude(); |
|
92 | 8 | $url = sprintf(self::REVERSE_ENDPOINT_URL, $latitude, $longitude, $this->apiKey, $query->getLimit()); |
|
93 | |||
94 | 8 | return $this->executeQuery($url); |
|
95 | } |
||
96 | |||
97 | /** |
||
98 | * {@inheritdoc} |
||
99 | */ |
||
100 | 7 | public function getName(): string |
|
101 | { |
||
102 | 7 | return 'mapzen'; |
|
103 | } |
||
104 | |||
105 | /** |
||
106 | * @param $url |
||
107 | * |
||
108 | * @return Collection |
||
109 | */ |
||
110 | 21 | private function executeQuery(string $url): AddressCollection |
|
111 | { |
||
112 | 21 | $content = $this->getUrlContents($url); |
|
113 | 11 | $json = json_decode($content, true); |
|
114 | |||
115 | // See https://mapzen.com/documentation/search/api-keys-rate-limits/ |
||
116 | 11 | if (isset($json['meta'])) { |
|
117 | 2 | switch ($json['meta']['status_code']) { |
|
118 | 2 | case 403: |
|
119 | 1 | throw new InvalidCredentials('Invalid or missing api key.'); |
|
120 | 1 | case 429: |
|
121 | 1 | throw new QuotaExceeded('Valid request but quota exceeded.'); |
|
122 | } |
||
123 | } |
||
124 | |||
125 | 9 | if (!isset($json['type']) || 'FeatureCollection' !== $json['type'] || !isset($json['features']) || 0 === count($json['features'])) { |
|
126 | 3 | return new AddressCollection([]); |
|
127 | } |
||
128 | |||
129 | 6 | $locations = $json['features']; |
|
130 | |||
131 | 6 | if (empty($locations)) { |
|
132 | return new AddressCollection([]); |
||
133 | } |
||
134 | |||
135 | 6 | $results = []; |
|
136 | 6 | foreach ($locations as $location) { |
|
137 | $bounds = [ |
||
138 | 6 | 'south' => null, |
|
139 | 'west' => null, |
||
140 | 'north' => null, |
||
141 | 'east' => null, |
||
142 | ]; |
||
143 | 6 | if (isset($location['bbox'])) { |
|
144 | $bounds = [ |
||
145 | 5 | 'south' => $location['bbox'][3], |
|
146 | 5 | 'west' => $location['bbox'][2], |
|
147 | 5 | 'north' => $location['bbox'][1], |
|
148 | 5 | 'east' => $location['bbox'][0], |
|
149 | ]; |
||
150 | } |
||
151 | |||
152 | 6 | $props = $location['properties']; |
|
153 | |||
154 | 6 | $adminLevels = []; |
|
155 | 6 | foreach (['region', 'county', 'locality', 'macroregion', 'country'] as $i => $component) { |
|
156 | 6 | if (isset($props[$component])) { |
|
157 | 6 | $adminLevels[] = ['name' => $props[$component], 'level' => $i + 1]; |
|
158 | } |
||
159 | } |
||
160 | |||
161 | 6 | $results[] = Address::createFromArray([ |
|
162 | 6 | 'providedBy' => $this->getName(), |
|
163 | 6 | 'latitude' => $location['geometry']['coordinates'][1], |
|
164 | 6 | 'longitude' => $location['geometry']['coordinates'][0], |
|
165 | 6 | 'bounds' => $bounds, |
|
166 | 6 | 'streetNumber' => isset($props['housenumber']) ? $props['housenumber'] : null, |
|
167 | 6 | 'streetName' => isset($props['street']) ? $props['street'] : null, |
|
168 | 6 | 'subLocality' => isset($props['neighbourhood']) ? $props['neighbourhood'] : null, |
|
169 | 6 | 'locality' => isset($props['locality']) ? $props['locality'] : null, |
|
170 | 6 | 'postalCode' => isset($props['postalcode']) ? $props['postalcode'] : null, |
|
171 | 6 | 'adminLevels' => $adminLevels, |
|
172 | 6 | 'country' => isset($props['country']) ? $props['country'] : null, |
|
173 | 6 | 'countryCode' => isset($props['country_a']) ? strtoupper($props['country_a']) : null, |
|
174 | ]); |
||
175 | } |
||
176 | |||
177 | 6 | return new AddressCollection($results); |
|
178 | } |
||
179 | |||
180 | /** |
||
181 | * @param array $components |
||
182 | * |
||
183 | * @return string|null |
||
184 | */ |
||
185 | protected function guessLocality(array $components) |
||
186 | { |
||
187 | $localityKeys = ['city', 'town', 'village', 'hamlet']; |
||
188 | |||
189 | return $this->guessBestComponent($components, $localityKeys); |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * @param array $components |
||
194 | * |
||
195 | * @return string|null |
||
196 | */ |
||
197 | protected function guessStreetName(array $components) |
||
198 | { |
||
199 | $streetNameKeys = ['road', 'street', 'street_name', 'residential']; |
||
200 | |||
201 | return $this->guessBestComponent($components, $streetNameKeys); |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * @param array $components |
||
206 | * |
||
207 | * @return string|null |
||
208 | */ |
||
209 | protected function guessSubLocality(array $components) |
||
210 | { |
||
211 | $subLocalityKeys = ['neighbourhood', 'city_district']; |
||
212 | |||
213 | return $this->guessBestComponent($components, $subLocalityKeys); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * @param array $components |
||
218 | * @param array $keys |
||
219 | * |
||
220 | * @return string|null |
||
221 | */ |
||
222 | protected function guessBestComponent(array $components, array $keys) |
||
223 | { |
||
224 | foreach ($keys as $key) { |
||
225 | if (isset($components[$key]) && !empty($components[$key])) { |
||
226 | return $components[$key]; |
||
227 | } |
||
228 | } |
||
229 | |||
230 | return null; |
||
231 | } |
||
232 | } |
||
233 |