Passed
Push — master ( 1bb8bb...31bd57 )
by Joël
01:50
created

Api::getRequestParameters()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 4
nop 1
dl 0
loc 20
ccs 12
cts 12
cp 1
crap 3
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace Defro\Google\StreetView;
4
5
use Defro\Google\StreetView\Exception\BadStatusCodeException;
6
use Defro\Google\StreetView\Exception\RequestException;
7
use Defro\Google\StreetView\Exception\UnexpectedStatusException;
8
use Defro\Google\StreetView\Exception\UnexpectedValueException;
9
use GuzzleHttp\Client;
10
use GuzzleHttp\Exception\GuzzleException;
11
12
class Api
13
{
14
    /** @var string default source */
15
    public const SOURCE_DEFAULT = 'default';
16
17
    /** @var string outdoor source */
18
    public const SOURCE_OUTDOOR = 'outdoor';
19
20
    /** @var string */
21
    private $endpointImage = 'https://maps.googleapis.com/maps/api/streetview';
22
23
    /** @var string */
24
    private $endpointMetadata = 'https://maps.googleapis.com/maps/api/streetview/metadata';
25
26
    /** @var Client */
27
    private $client;
28
29
    /** @var string */
30
    private $apiKey;
31
32
    /** @var string */
33
    private $signature;
34
35
    /** @var string */
36
    private $signingSecret;
37
38
    /** @var int */
39
    private $imageWidth = 600;
40
41
    /** @var int */
42
    private $imageHeight = 600;
43
44
    /** @var int */
45
    private $heading;
46
47
    /** @var int */
48
    private $cameraFov = 90;
49
50
    /** @var int */
51
    private $cameraPitch = 0;
52
53
    /** @var int */
54
    private $radius = 50;
55
56
    /** @var string */
57
    private $source = self::SOURCE_DEFAULT;
58
59
    /**
60
     * Api constructor.
61
     *
62
     * @param Client $client
63
     */
64 36
    public function __construct(Client $client)
65
    {
66 36
        $this->client = $client;
67 36
    }
68
69
    /**
70
     * API key from your Google console.
71
     *
72
     * @param string $apiKey
73
     *
74
     * @return $this
75
     */
76 1
    public function setApiKey(string $apiKey): self
77
    {
78 1
        $this->apiKey = $apiKey;
79
80 1
        return $this;
81
    }
82
83
    /**
84
     * Digital signature used to verify that any site generating requests.
85
     *
86
     * @param string $signature
87
     *
88
     * @return $this
89
     */
90 2
    public function setSignature(string $signature): self
91
    {
92 2
        $this->signature = $signature;
93
94 2
        return $this;
95
    }
96
97
    /**
98
     * Used in conjunction with an API key, a URL signing secret can tag API requests with a higher degree of security.
99
     * Providing a signing secret will automatically generate digital signatures for subsequent requests.
100
     *
101
     * @param string $secret Base64 encoded signing secret
102
     *
103
     * @return $this
104
     */
105 3
    public function setSigningSecret(string $secret): self
106
    {
107 3
        $this->signingSecret = $this->decodeModifiedBase64($secret);
108
109 3
        return $this;
110
    }
111
112
    /**
113
     * Determines the horizontal field of view of the image.
114
     * The field of view is expressed in degrees, with a maximum allowed value of 120.
115
     * When dealing with a fixed-size viewport, as with a Street View image of a set size,
116
     * field of view in essence represents zoom, with smaller numbers indicating a higher level of zoom.
117
     *
118
     * @param int $cameraFov
119
     *
120
     * @throws UnexpectedValueException
121
     *
122
     * @return $this
123
     */
124 2
    public function setCameraFov(int $cameraFov): self
125
    {
126 2
        if ($cameraFov > 120) {
127 1
            throw new UnexpectedValueException(
128 1
                'Camera FOV value cannot exceed 120 degrees.'
129
            );
130
        }
131
132 1
        $this->cameraFov = $cameraFov;
133
134 1
        return $this;
135
    }
136
137
    /**
138
     * Specifies the up or down angle of the camera relative to the Street View vehicle.
139
     * This is often, but not always, flat horizontal.
140
     * Positive values angle the camera up (with 90 degrees indicating straight up);
141
     * negative values angle the camera down (with -90 indicating straight down).
142
     *
143
     * @param int $cameraPitch
144
     *
145
     * @throws UnexpectedValueException
146
     *
147
     * @return $this
148
     */
149 3
    public function setCameraPitch(int $cameraPitch): self
150
    {
151 3
        if ($cameraPitch > 90) {
152 1
            throw new UnexpectedValueException(
153 1
                'Camera pitch value for Google Street View cannot exceed 90 degrees.'
154
            );
155
        }
156 2
        if ($cameraPitch < -90) {
157 1
            throw new UnexpectedValueException(
158 1
                'Camera pitch value for Google Street View cannot be inferior of -90 degrees.'
159
            );
160
        }
161
162 1
        $this->cameraPitch = $cameraPitch;
163
164 1
        return $this;
165
    }
166
167
    /**
168
     * Sets a radius, specified in meters,
169
     * in which to search for a panorama, centered on the given latitude and longitude.
170
     * Valid values are non-negative integers.
171
     *
172
     * @param int $radius
173
     *
174
     * @throws UnexpectedValueException
175
     *
176
     * @return $this
177
     */
178 2
    public function setRadius(int $radius): self
179
    {
180 2
        if ($radius < 0) {
181 1
            throw new UnexpectedValueException(
182 1
                'Radius value cannot be negative.'
183
            );
184
        }
185
186 1
        $this->radius = $radius;
187
188 1
        return $this;
189
    }
190
191
    /**
192
     * Indicates the compass heading of the camera.
193
     * Accepted values are from 0 to 360 (both values indicating North, with 90 indicating East, and 180 South).
194
     *
195
     * @param int $heading
196
     *
197
     * @throws UnexpectedValueException
198
     *
199
     * @return $this
200
     */
201 4
    public function setHeading(int $heading): self
202
    {
203 4
        if ($heading < 0) {
204 1
            throw new UnexpectedValueException(
205 1
                'Heading value cannot be inferior to zero degree.'
206
            );
207
        }
208 3
        if ($heading > 360) {
209 1
            throw new UnexpectedValueException(
210 1
                'Heading value cannot exceed 360 degrees.'
211
            );
212
        }
213
214 2
        $this->heading = $heading;
215
216 2
        return $this;
217
    }
218
219
    /**
220
     * Limits Street View searches to selected sources. Valid values are:
221
     *  - default uses the default sources for Street View; searches are not limited to specific sources.
222
     *  - outdoor limits searches to outdoor collections.
223
     *
224
     * @param string $source
225
     *
226
     * @throws UnexpectedValueException
227
     *
228
     * @return $this
229
     */
230 2
    public function setSource(string $source): self
231
    {
232 2
        if (!in_array($source, [self::SOURCE_DEFAULT, self::SOURCE_OUTDOOR], true)) {
233 1
            throw new UnexpectedValueException(sprintf(
234 1
                'Source value "%s" is unknown, only "%s" or "%s" values expected.',
235
                $source,
236 1
                self::SOURCE_DEFAULT,
237 1
                self::SOURCE_OUTDOOR
238
            ));
239
        }
240
241 1
        $this->source = $source;
242
243 1
        return $this;
244
    }
245
246
    /**
247
     * @param int $height
248
     *
249
     * @throws UnexpectedValueException
250
     *
251
     * @return $this
252
     */
253 2
    public function setImageHeight(int $height): self
254
    {
255 2
        if ($height < 1) {
256 1
            throw new UnexpectedValueException(
257 1
                'Image height value cannot be negative or equal to zero.'
258
            );
259
        }
260
261 1
        $this->imageHeight = $height;
262
263 1
        return $this;
264
    }
265
266
    /**
267
     * @param int $width
268
     *
269
     * @throws UnexpectedValueException
270
     *
271
     * @return $this
272
     */
273 2
    public function setImageWidth(int $width): self
274
    {
275 2
        if ($width < 1) {
276 1
            throw new UnexpectedValueException(
277 1
                'Image height value cannot be negative or equal to zero.'
278
            );
279
        }
280
281 1
        $this->imageWidth = $width;
282
283 1
        return $this;
284
    }
285
286
    /**
287
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
288
     *
289
     * @param string $location
290
     *
291
     * @throws BadStatusCodeException
292
     *
293
     * @return string
294
     */
295 2
    public function getImageUrlByLocation(string $location): string
296
    {
297 2
        $parameters = $this->getRequestParameters([
298 2
            'location' => $location,
299
        ]);
300
301 2
        return $this->getImageUrl($parameters);
302
    }
303
304
    /**
305
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
306
     *
307
     * @param float $latitude
308
     * @param float $longitude
309
     *
310
     * @throws BadStatusCodeException
311
     *
312
     * @return string
313
     */
314 1
    public function getImageUrlByLatitudeAndLongitude(float $latitude, float $longitude): string
315
    {
316 1
        $parameters = $this->getRequestParameters([
317 1
            'location' => $latitude.','.$longitude,
318
        ]);
319
320 1
        return $this->getImageUrl($parameters);
321
    }
322
323
    /**
324
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail.
325
     *
326
     * @param string $panoramaId
327
     *
328
     * @throws BadStatusCodeException
329
     *
330
     * @return string
331
     */
332 1
    public function getImageUrlByPanoramaId(string $panoramaId): string
333
    {
334 1
        $parameters = $this->getRequestParameters([
335 1
            'pano' => $panoramaId,
336
        ]);
337
338 1
        return $this->getImageUrl($parameters);
339
    }
340
341
    /**
342
     * Returns URL to a static (non-interactive) Street View panorama or thumbnail
343
     * The viewport is defined with URL parameters sent through a standard HTTP request, and is returned as a static image.
344
     *
345
     * @param array $parameters
346
     *
347
     * @throws BadStatusCodeException
348
     *
349
     * @return string
350
     */
351 4
    private function getImageUrl(array $parameters): string
352
    {
353 4
        if (empty($parameters['signature']) && $this->signingSecret) {
354
            $parameters['signature'] = $this->generateSignature($this->endpointImage, $parameters);
355
        }
356
357 4
        $uri = $this->endpointImage.'?'.http_build_query($parameters);
358
359 4
        $response = $this->client->get($uri);
360
361 4
        if ($response->getStatusCode() !== 200) {
362 1
            throw new BadStatusCodeException(
363 1
                'Could not connect to '.$this->endpointImage,
364 1
                $response->getStatusCode()
365
            );
366
        }
367
368 3
        return $uri;
369
    }
370
371
    /**
372
     * Requests provide data about Street View panoramas.
373
     * Using the metadata, you can find out if a Street View image is available at a given location,
374
     * as well as getting programmatic access to the latitude and longitude,
375
     * the panorama ID, the date the photo was taken, and the copyright information for the image.
376
     * Accessing this metadata allows you to customize error behavior in your application.
377
     * Street View API metadata requests are free to use. No quota is consumed when you request metadata.
378
     *
379
     * @param string $location
380
     *
381
     * @return array|null
382
     *
383
     * @throws RequestException
384
     * @throws BadStatusCodeException
385
     * @throws UnexpectedStatusException
386
     * @throws UnexpectedValueException
387
     */
388 11
    public function getMetadata(string $location): ?array
389
    {
390 11
        $location = trim($location);
391
392 11
        if (empty($location)) {
393 1
            throw new UnexpectedValueException(
394 1
                'Location argument cannot be empty to request Google Street view API Metadata.'
395
            );
396
        }
397
398 10
        $parameters = $this->getRequestParameters(compact('location'));
399
400 10
        if (empty($parameters['signature']) && $this->signingSecret) {
401
            $parameters['signature'] = $this->generateSignature($this->endpointMetadata, $parameters);
402
        }
403
404 10
        $payload = ['query' => http_build_query($parameters)];
405
406
        try {
407 10
            $response = $this->client->request('GET', $this->endpointMetadata, $payload);
408 1
        } catch (GuzzleException $e) {
409 1
            throw new RequestException(
410 1
                'Guzzle http client request failed.',
411
                $e
412
            );
413
        }
414
415 9
        if ($response->getStatusCode() !== 200) {
416 1
            throw new BadStatusCodeException(
417 1
                'Could not connect to '.$this->endpointMetadata,
418 1
                $response->getStatusCode()
419
            );
420
        }
421
422 8
        $response = json_decode($response->getBody(), false);
423
424
        // Indicates that no panorama could be found near the provided location.
425
        // Indicates that no errors occurred; a panorama is found and metadata is returned.
426 8
        if ($response->status === 'OK') {
427 1
            return $this->formatMetadataResponse($response);
428
        }
429
430 7
        $this->handleResponseStatus($response->status);
431
432
        return null;
433
    }
434
435
    /**
436
     * @param string $status
437
     */
438 7
    private function handleResponseStatus(string $status): void
439
    {
440
        // This may occur if a non-existent or invalid panorama ID is given.
441 7
        if ($status === 'ZERO_RESULTS') {
442 1
            throw new UnexpectedStatusException('Google Street view return zero results.');
443
        }
444
        // Indicates that the address string provided in the location parameter could not be found.
445
        // This may occur if a non-existent address is given.
446 6
        if ($status === 'NOT_FOUND') {
447 1
            throw new UnexpectedStatusException('No Google Street view result found.');
448
        }
449
        // Indicates that you have exceeded your daily quota or per-second quota for this API.
450 5
        if ($status === 'OVER_QUERY_LIMIT') {
451 1
            throw new UnexpectedStatusException('Google Street view API quota exceed.');
452
        }
453
        // Indicates that your request was denied.
454
        // This may occur if you did not use an API key or client ID, or
455
        // if the Street View API is not activated in the Google Cloud Platform Console project containing your API key.
456 4
        if ($status === 'REQUEST_DENIED') {
457 1
            throw new UnexpectedStatusException('Google Street view denied the request.');
458
        }
459
        // Generally indicates that the query parameters (address or latlng or components) are missing.
460 3
        if ($status === 'INVALID_REQUEST') {
461 1
            throw new UnexpectedStatusException('Google Street view request is invalid.');
462
        }
463
        // Indicates that the request could not be processed due to a server error.
464
        // This is often a temporary status. The request may succeed if you try again.
465 2
        if ($status === 'UNKNOWN_ERROR') {
466 1
            throw new UnexpectedStatusException('Google Street view unknown error occurred. Please try again.');
467
        }
468
469 1
        throw new UnexpectedStatusException(
470 1
            'Google Street view respond an unknown status response : "'.$status.'".'
471
        );
472
    }
473
474
    /**
475
     * Formatter of metadata endpoint response
476
     *
477
     * @param $response
478
     *
479
     * @return array
480
     */
481 1
    private function formatMetadataResponse($response): array
482
    {
483
        return [
484 1
            'lat'           => $response->location->lat,
485 1
            'lng'           => $response->location->lng,
486 1
            'date'          => $response->date,
487 1
            'copyright'     => $response->copyright,
488 1
            'panoramaId'    => $response->pano_id,
489
        ];
490
    }
491
492 14
    private function getRequestParameters(array $parameters): array
493
    {
494
        $defaultParameters = [
495 14
            'key'       => $this->apiKey,
496 14
            'size'      => $this->imageWidth.'x'.$this->imageHeight,
497 14
            'fov'       => $this->cameraFov,
498 14
            'pitch'     => $this->cameraPitch,
499 14
            'radius'    => $this->radius,
500 14
            'source'    => $this->source,
501
        ];
502
503
        // optional parameters which have not default value
504 14
        if ($this->heading) {
505 1
            $defaultParameters['heading'] = $this->heading;
506
        }
507 14
        if ($this->signature) {
508 1
            $defaultParameters['signature'] = $this->signature;
509
        }
510
511 14
        return array_merge($defaultParameters, $parameters);
512
    }
513
514
    /**
515
     * Encode a string to URL-safe base64.
516
     *
517
     * @param string $value
518
     *
519
     * @return string
520
     */
521 2
    private function encodeModifiedBase64(string $value): string
522
    {
523 2
        return str_replace(['+', '/'], ['-', '_'], base64_encode($value));
524
    }
525
526
    /**
527
     * Decode a string from URL-safe base64.
528
     *
529
     * @param string $value
530
     *
531
     * @return string
532
     */
533 3
    private function decodeModifiedBase64(string $value): string
534
    {
535 3
        return base64_decode(str_replace(['-', '_'], ['+', '/'], $value));
536
    }
537
538
    /**
539
     * Sign a URL with the current signing secret.
540
     *
541
     * @param string $url A valid URL that is properly URL-encoded
542
     * @param array|null $parameters Parameters to include in the URL
543
     *
544
     * @return string
545
     */
546 2
    public function generateSignature(string $url, ?array $parameters = null): string
547
    {
548 2
        $urlParsed = parse_url($url);
549
550 2
        if (!is_null($parameters)) {
551 1
            $urlParsed['query'] = http_build_query($parameters);
552
        }
553
554 2
        $url = $urlParsed['path'].'?'.$urlParsed['query'];
555
556
        // Generate binary signature
557 2
        $signature = hash_hmac('sha1', $url, $this->signingSecret, true);
558
559
        // Encode signature into base64
560 2
        return $this->encodeModifiedBase64($signature);
561
    }
562
}
563