Untitled

mail@pastecode.io avatar
unknown
php
a year ago
16 kB
1
Indexable
Never
<?php

namespace App\Http\Utility;

use App\DTO\LocationDto;
use App\DTO\MarkerDto;
use Geocoder\Collection;
use Geocoder\Provider\GoogleMaps\GoogleMaps;
use Geocoder\Query\ReverseQuery;
use Geocoder\StatefulGeocoder;
use Http\Client\Curl\Client;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class Geocode
{
    /**
     * Minimal distance in kilometers that 2 markers can be when checking entire route
     *
     * @var int
     */
    private static $markerMinDistance = 20;

    /**
     * Maximum distance in kilometers that 2 markers can be when checking entire route
     *
     * @var int
     */
    private static $markerMaxDistance = 50;

    /**
     * Maximum distance in kilometers for origin point and polyline segment to take segment into consideration
     *
     * @var int
     */
    private static $segmentTolerance = 20;

    /**
     * Minimum distance in kilometers between points of delivery routes and origin, where route is taken into consideration
     *
     * @var int
     */
    private static $minMarkerDistanceRadius = 8;

    /**
     * Value for detour radius that the driver designated while making his route
     *
     * @var int
     */
    public $detourRadius = 40;

    /**
     * @var LocationDto
     */
    public $origin = [];

    /**
     * @var LocationDto
     */
    public $destination = [];

    /**
     * @var array
     */
    public $provinces = [];

    /**
     * @var array
     */
    public $countries = [];

    /**
     * @param string $origin
     * @param string $destination
     * @param bool|null $avoid_tolls
     * @param array|null $vias
     * @return string
     * @throws \Exception
     */
    public function googleMapsRouteUrl (string $origin, string $destination, bool|null $avoid_tolls = false, array|null $vias = []): string
    {
        $google_maps_api_key = config('googlemaps.google_maps_api_key');

        if (empty($google_maps_api_key)) {
            throw new HttpResponseException(response()->json([
                'message' => 'Google maps key is not set up.'
            ], JsonResponse::HTTP_BAD_REQUEST));
        }

        $url = 'https://maps.googleapis.com/maps/api/directions/json?origin=' . urlencode($origin) . '&destination=' . urlencode($destination);

        if ($avoid_tolls) {
            $url .= '&avoid=tolls';
        }

        if ($vias && count($vias)) {
            $viaList = '';
            $url .= '&waypoints=';

            foreach ($vias as $location) {
                $viaList .= 'via:' . $location . '|';
            }

            $viaList = rtrim($viaList, '|');
            $url .= urlencode($viaList);
        }

        $url .= '&language=en&key=' . $google_maps_api_key;

        return $url;
    }

    /**
     * @param string $address
     * @return string
     * @throws \Exception
     */
    public function googleMapsAddressUrl (string $address)
    {
        $google_maps_api_key = config('googlemaps.google_geocode_api_key');

        if (empty($google_maps_api_key)) {
            throw new HttpResponseException(response()->json([
                'message' => 'Google maps key is not set up.'
            ], JsonResponse::HTTP_BAD_REQUEST));
        }

        return 'https://maps.google.com/maps/api/geocode/json?address=' . urlencode($address) . '&language=en&key=' . $google_maps_api_key;
    }

    /**
     * @param string $url
     * @return mixed
     * @throws \Exception
     */
    public function googleMapsFetchUrl (string $url)
    {
        $response = Http::get($url)->object();

        if ($response->status !== "OK") {
            throw new HttpResponseException(response()->json([
                'message' => trans('response.geocode.empty')
            ], JsonResponse::HTTP_BAD_REQUEST));
        }

        return $response;
    }

    /**
     * @param array $coordinates
     * @return void
     * @throws \Geocoder\Exception\Exception
     */
    public function geocodeLocation (array $coordinates)
    {
        $this->provinces = [];
        $this->countries = [];

        /*Storage::put('geocode_rmp_coords.json', \Illuminate\Support\Collection::make($coordinates)->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));*/

        $province_markers = $this->callAWS(GeocodeUrls::routeMarkerProvince(), ["markers" => $coordinates])->object()->route_marker_province;

        /*Storage::put('geocode_test.json', \Illuminate\Support\Collection::make($province_markers)->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        dump('geocode');
        exit;*/

        $province_marker_count = count($province_markers) - 1;
        foreach ($province_markers as $cnt => $marker) {
            $geocode_data = $this->geocodeQuery($marker->lat, $marker->lng);

            if ($geocode_data->isEmpty()) {
                continue;
            }

            if ($cnt === 0) {
                $this->origin = new LocationDto();
                $this->locationFiller($this->origin, $geocode_data);
            }

            if ($cnt === $province_marker_count) {
                $this->destination = new LocationDto();
                $this->locationFiller($this->destination, $geocode_data);
            }

            $this->countryFiller($geocode_data);
            $this->provinceFiller($geocode_data);
        }
    }

    /**
     * @param float $latitude
     * @param float $longitude
     * @return Collection
     * @throws \Geocoder\Exception\Exception
     */
    public function geocodeQuery (float $latitude, float $longitude): Collection
    {
        $google_geocode_api_key = config('googlemaps.google_geocode_api_key', '');
        $httpClient = new Client();
        $provider = new GoogleMaps($httpClient, null, $google_geocode_api_key);
        $geocoder = new StatefulGeocoder($provider);

        $geocoder_query = ReverseQuery::fromCoordinates($latitude, $longitude)
            ->withData('result_type', 'administrative_area_level_1|locality')->withLocale('en');

        return $geocoder->reverseQuery($geocoder_query);
    }

    /**
     * @param LocationDto $location_data
     * @param Collection $data
     * @return void
     */
    public function locationFiller (LocationDto $location_data, Collection $data): void
    {
        $location_data->town = $data->first()->getLocality();

        if ($data->first()->getCountry()) {
            $location_data->country = $data->first()->getCountry()->getName();
        }

        foreach ($data as $value) {
            if (! $value->getAdminLevels()->count()) {
                continue;
            }

            if (in_array($value->getAdminLevels()->first()->getLevel(), [1, 2])) {
                $location_data->province = $value->getAdminLevels()->first()->getName();
                break;
            }
        }
    }

    /**
     * @param Collection $data
     * @return void
     */
    private function countryFiller (Collection $data): void
    {
        if (! $data->first()->getCountry()) {
            return;
        }

        $name = $data->first()->getCountry()->getName();

        if (! in_array($name, $this->countries)) {
            $this->countries[] = $name;
        }
    }

    /**
     * @param Collection $data
     * @return void
     */
    private function provinceFiller (Collection $data): void
    {
        $name = '';
        if ($data->first()->getAdminLevels()->count()) {
            $name = $data->first()->getAdminLevels()->first()->getName();
        }

        if ($data->has(1)) {
            $name = $data->get(1)->getAdminLevels()->first()->getName();
        }

        if (! $name) {
            return;
        }

        if (! in_array($name, $this->provinces)) {
            $this->provinces[] = $name;

            $this->provinceSpecialCases($name);
        }
    }

    /**
     * @param string $province
     * @return void
     */
    private function provinceSpecialCases (string $province): void
    {
        // TODO: We'll need to extend this for easier implementation of edge cases
        $special_case = ['Zagreb County', 'City of Zagreb'];
        if ($match = array_search($province, $special_case)) {
            unset($special_case[$match]);
            $this->provinces[] = array_shift($special_case);
        }
    }

    /**
     * @param array $coordinates
     * @return Response|JsonResponse
     */
    public function polylineEncode (array $coordinates)
    {
        return $this->callAWS(GeocodeUrls::encode(), $coordinates);
    }

    /**
     * @param array $polyline
     * @return Response|JsonResponse
     */
    public function polylineDecode (string $polyline)
    {
        return $this->callAWS(GeocodeUrls::decode(), $polyline);
    }

    /**
     * Because every end_location marker is same as start_location of the next marker
     * we're only taking the end_location markers except of the first one
     *
     * @param array|null $steps
     * @return array
     */
    public function markerList (array|null $steps)
    {
        return $this->callAWS(GeocodeUrls::markerList(), ["steps" => $steps])->object()->marker_list;
    }

    /**
     * @param array $coordinates
     * @return false|string
     */
    public function routeMarkerFullPath (array $coordinates)
    {
        $polylines = $this->callAWS(GeocodeUrls::routeMarkerFullPath(), ["markers" => $coordinates])->object()->polylines;
        if (! is_string($polylines)) {
            $polylines = json_encode($polylines);
        }

        return $polylines;
    }

    /**
     * @param string $polylines
     * @param MarkerDto $targetMarker
     * @param int $detour_radius
     * @param bool $polyline_continue
     * @return array|bool
     */
    public function shipmentCheck (string $polylines, MarkerDto $targetMarker, int $detour_radius): array|bool
    {
        if (empty($polylines)) {
            return false;
        }

        $this->detourRadius = $detour_radius;

        return $this->callAWS(GeocodeUrls::shipmentCheck(), [
            "shipment_check_input" => [
                'polylines' => json_decode($polylines),
                'target_marker' => $targetMarker
            ]
        ])->object()->polylines;
    }

    /**
     * @return JsonResponse|string
     */
    private function getToken (): JsonResponse|string
    {
        $response = Http::asForm()->post(GeocodeUrls::token(),
            [
                'username' => env("FAST_API_GEO_USER"),
                'password' => env("FAST_API_GEO_PASS"),
            ],
        );

        if ($response->status() !== JsonResponse::HTTP_OK) {
            return response()->json([
                'message' => trans('response.geocode.connection')
            ], JsonResponse::HTTP_BAD_REQUEST);
        }

        $token = $response->json('access_token');
        if (! isset($token)) {
            return response()->json([
                'message' => trans('response.not_found', ['item' => 'Token'])
            ], JsonResponse::HTTP_NOT_FOUND);
        }

        if (! Cache::add('aws_auth', $token)) {
            Cache::put('aws_auth', $token);
        }

        return $token;
    }


    /**
     * @param string $url
     * @param array|string|null $payload
     * @param string $type
     * @return JsonResponse|Response
     */
    private function callAWS (string $url, array|string|null $payload, string $type = 'post')
    {
        if (! in_array($type, ['post', 'get'])) {
            return response()->json([
                'message' => trans('response.geocode.connection_type')
            ], JsonResponse::HTTP_BAD_REQUEST);
        }

        if (! is_string($payload)) {
            $payload["geo_marker_options"] = [
                "marker_min_distance" => self::$markerMinDistance,
                "marker_max_distance" => self::$markerMaxDistance,
                "segment_tolerance" => self::$segmentTolerance,
                "min_marker_distance_radius" => max($this->detourRadius, self::$minMarkerDistanceRadius)
            ];
        } else {
            $payload = [
                "encoded_polyline" => $payload,
            ];
        }

        try {
            if (! Cache::has('aws_auth')) {
                $this->getToken();
            }

            $request = Http::withToken(Cache::get('aws_auth'))->retry(1, 100)->withoutRedirecting()->acceptJson();

            if ($type === 'get') {
                $payload = null;
            }

            $response = $request->$type($url, $payload);

            if ($response->status() === JsonResponse::HTTP_UNAUTHORIZED) {
                throw new RequestException($response);
            }

            if ($response->status() !== JsonResponse::HTTP_OK) {
                throw new \Exception();
            }
        } catch (RequestException $e) {
            if ($type === 'get') {
                $payload = null;
            }

            $response = $request->withToken($this->getToken())->retry(3, 100)->acceptJson()->$type($url, $payload);
        } catch (\Exception $e) {
            error_log("URL: " . $url . PHP_EOL);
            error_log("HTTP code: " . $response->getStatusCode() . PHP_EOL);
            error_log("Response body: " . $response->getBody()->getContents() . PHP_EOL);

            return response()->json([
                'message' => trans('response.geocode.connection_error')
            ], JsonResponse::HTTP_BAD_REQUEST);
        }

        return $response;
    }
}

<?php

namespace App\Http\Utility;

enum GeocodeUrls: string
{
    case TOKEN = 'auth/token';
    case ENCODE = 'polyline-encoder/encode';
    case DECODE = 'polyline-encoder/decode';
    case CREATE_MARKER = 'geocode/create_marker';
    case MARKER_LIST = 'geocode/marker-list';
    case ROUTE_MARKER_PROVINCE = 'geocode/route-marker-province';
    case ROUTE_MARKER_FULL_PATH = 'geocode/route-marker-full-path';
    case SHIPMENT_CHECK = 'geocode/shipment-check';

    /**
     * @return string
     */
    public static function token(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::TOKEN->value;
    }

    /**
     * @return string
     */
    public static function encode(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::ENCODE->value;
    }

    /**
     * @return string
     */
    public static function decode(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::DECODE->value;
    }

    /**
     * @return string
     */
    public static function createMarker(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::CREATE_MARKER->value;
    }

    /**
     * @return string
     */
    public static function markerList(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::MARKER_LIST->value;
    }

    /**
     * @return string
     */
    public static function routeMarkerProvince(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::ROUTE_MARKER_PROVINCE->value;
    }

    /**
     * @return string
     */
    public static function routeMarkerFullPath(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::ROUTE_MARKER_FULL_PATH->value;
    }

    /**
     * @return string
     */
    public static function shipmentCheck(): string
    {
        return env("FAST_API_GEO_BASE_URL") . self::SHIPMENT_CHECK->value;
    }
}