<?php

class GeocodeApi extends Api
{
    public $apiName = 'geocode';
    const DEFAULT_COUNTRY_CODE = 'SG';

    public function indexAction() {
        $message = 'Data not found';
        $address = $this->requestParams["address"] ?? "";
        $member_id = $this->requestParams["member_id"] ?? 0;
        $country = $this->requestParams["country"] ?? self::DEFAULT_COUNTRY_CODE;
        try {
            if ($address==NULL || $address=="") {
                throw new Exception("Invalid input");
            }
            $db = new Db();

            // DEBUG
            $country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');

            $result = [];
            $result["address"] = $address;
            list($result["geocode"], $result["error"]) = GeocodeApi::geocode($db, $address, $country);
            return $this->response($result, 200);
        } catch (Exception $e) {
            error_log(json_encode($e));
            $message = $e->getMessage();
        }
        return $this->response(
            array(
                'error' => $message
            ), 404);
    }

    /**
     * Method GET
     * Get single record (by id)
     * http://DOMAIN/geocode/1
     * @return string
     */
    public function viewAction() {
        //id must be the first parameter after /geocode/x
        $id = array_shift($this->requestUri);

        if($id && (int)$id>0){
            $db = new Db();
            $address = Geocode::getAddressById($db->getConnect(), (int)$id);
            if(is_array($address) && count($address)>0){
                return $this->response($address, 200);
            }
        }
        return $this->response(
            array(
                'error'=> 'Data not found'
            ), 404);
    }

    public function createAction() {
        error_log('GeocodeApi::createAction()');
        $message = "";
        $addresses = $this->requestParams["addresses"] ?? array();
        $options = $this->requestParams["options"] ?? array();
        $gps_country_code = $this->requestParams["gps_country_code"] ?? null;
        $country = $this->requestParams["country"] ?? SELF::DEFAULT_COUNTRY_CODE;
        $member_id = $this->requestParams["member_id"] ?? 0;
        $address_data = $this->requestParams["address_data"] ?? null;
        try {
            if ($addresses==NULL || !is_array($addresses) || count($addresses)<1 || !isset($addresses[0]["address"])) {
                throw new Exception("Invalid input");
            }

            $db = new Db();

            // DEBUG
            $country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
 
            // Check country
            if ($country!='SG' && $country!='US') {
                throw new RuntimeException('Trips has not yet launched in your country. You can still track your travel activity and access exclusive deals. Start exploring!',500);
            }
            if ($gps_country_code==null) {
                $gps_country_code = $country;
            }
           
            $result = array();
            foreach ($addresses as $item) {
                if (isset($item["type"]) && $item["type"]>0 && is_array($address_data)) {
                    if ($item["type"]==1 
                    && array_key_exists('location_start',$address_data)
                    && is_array($address_data['location_start']) 
                    && array_key_exists('id',$address_data['location_start'])
                    && $address_data['location_start']['id']>0) {
                        $item["geocode"] = GeocodeApi::addressDataToGeocode($address_data['location_start']);
                        $item["error"] = NULL;
                        $result[] = $item;
                        continue;
                    }
                    if ($item["type"]==2 
                    && array_key_exists('location_end',$address_data)
                    && is_array($address_data['location_end']) 
                    && array_key_exists('id',$address_data['location_end'])
                    && $address_data['location_end']['id']>0
                    ) {
                        $item["geocode"] = GeocodeApi::addressDataToGeocode($address_data['location_end']);
                        $item["error"] = NULL;
                        $result[] = $item;
                        continue;
                    }
                }
                if (isset($item["address"]) && $item["address"]!="") {
                    list($item["geocode"], $item["error"]) = GeocodeApi::geocode($db, $item["address"], $country);
                } else {
                    $item["geocode"] = NULL;
                    $item["error"] = "Invalid address"; 
                }
                $result[] = $item;
            }
            if (count($result)<1) throw new Exception('Sorry, but we cannot find your address');

            $withinTheServiceArea = GeocodeApi::checkWithinTheServiceArea($country,$result, false);

            if ((isset($options["travel_time"]) && $options["travel_time"]) ||
                (isset($options["route_overlay"]) && $options["route_overlay"])) {
                $options = GeocodeApi::options($db, $result, $options);
            }
            
            list($providers,$country,$service_enabled, $currency_code) = GeocodeApi::getProviders(
                $db, $gps_country_code, $country, $options, $member_id);
            return $this->response(array(
                'addresses' => $result,
                'options'   => $options,
                'providers' => $providers,
                'country'   => $country,
                'service'   => $service_enabled,
                'currency'  => $currency_code), 200);
        } catch (Exception $e) {
            error_log(json_encode($e));
            $message = $e->getMessage();
        }
        return $this->response(
            array(
                "error" => $message==""?"Failed to run geocoder":$message
            ), 500);
    }

    protected function addressDataToGeocode($address_data) {
        // TODO: check the timeZoneId?
        return [
            "address" =>  $address_data["address"],
            "lat" => array_key_exists("latitude",$address_data) ? $address_data["latitude"] : $address_data["lat"],
            "lng" => array_key_exists("longitude",$address_data) ? $address_data["longitude"] : $address_data["lng"],
            "postal" => $address_data["postal"],
            "timezone" => $address_data["timezone"],
            "id" => $address_data["id"],
            "country" => $address_data["country"],
            "timeZoneId" => $address_data["timeZoneId"]
        ];
        /*
            id: "12400"
            address: "2600 Bentley Rd SE, Marietta, GA 30067, USA"
            latitude: "33.9212128"
            longitude: "-84.4766081"
            timezone: "3"
            geocoding_date: "2019-12-31"
            postal: "30067"
            country: "US"
            geometry: "0101000020E61000008FA042BF801E55C00B54104DEAF54040"
            description: null
            timeZoneId: "America/New_York"
        */
    }

    public function updateAction() {
        $message = 'Data not found';
        $latitude = $this->requestParams["latitude"] ?? 0;
        $longitude = $this->requestParams["longitude"] ?? 0;
        $country = $this->requestParams["country"] ?? GeocodeApi::DEFAULT_COUNTRY_CODE;
        $internal = $this->requestParams["internal"] ?? false;
        try {
            if ($latitude===NULL || $longitude===NULL || ($latitude==0 && $longitude==0)) {
                $message = "Invalid input: $latitude,$longitude,$country";
                $message.= json_encode($this->requestParams);
                throw new Exception($message);
                //throw new Exception("Invalid input");
            }
            if ($internal) {
                $this->encryption = false;
            }
            $db = new Db();
            list($address, $err) = GeocodeApi::reverseGeocode(
                $db, $latitude, $longitude);
            if ($address!=NULL && strlen($address)>5) {
                return $this->response([
                    "address" => $address,
                    "latitude" => $latitude,
                    "longitude" => $longitude,
                    "country" => $country
                ], 200);
            }
            if ($err && $err!="") {
                $message = $err;
            }
        } catch (Exception $e) {
            error_log(json_encode($e));
            $message = $e->getMessage();
        }
        return $this->response(
            array(
                'error' => $message
            ), 404);
    }

    public function deleteAction() {
        return $this->response(
            array(
                "error" => "Delete error"
            ), 500);
    }

    public static function processAddressCountry($db, $address, $country) {
        syslog(LOG_WARNING,"GeocodeApi::processAddressCountry(\$db, $address, $country)");
        $code = $country;
        $name = "";
        $short_name = "";
        $res = Geocode::getCountry($db->getConnect(),$country);
        if (is_array($res) && array_key_exists('country',$res) && $res['country']!='') {
            $name = $res['country'];
            $short_name = $res['short_name'];
            $code = $res['code'];
        }
        // if it has the country name in address?
        if ($name!="" && strripos($address,$name)!==false) {
            return $address;
        }
        
        $length = strlen($code); // if it ends with the country code?
        $lengthcode = strlen($short_name); // if it ends with the country code?
        if ((strripos($address," ${code}")!==false || strrpos($address,",${short_name}")!==false)) {
            return $address; 
        }
        $address.= ", ".($name!=""?$name:$code); // append country name or code to address
        return $address;
    }

    public static function geocode($db, $address, $country, $client_id="Unknown") {
        
        syslog(LOG_WARNING,"GeocodeApi::geocode(\$db,'${address}',$country,'${client_id}')");
        global $savvyext;
        $httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
        $oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
        // Check for broken clients and assume default country for now
        if ($country==null || $country=="") {
            $country = self::DEFAULT_COUNTRY_CODE;
        }
        $address_input = $address;
        // Check local DB cache
        list ($res, $err) = Geocode::checkLatLngByAddress($db->getConnect(), $address, $country);
        if (is_array($res) && isset($res["timeZoneId"]) && !empty($res["postal"])) {
            /* if(empty($res['plus_code']) || $res['address'] == $res['plus_code']){
                $reverse_info = self::reverseGeocodeService($db, $res['lat'],$res['lng']);
                $log = [
                    'message' => 'GeocodeApi::reverseGeocodeService',
                    'data' => ['reverse_info'=>$reverse_info]
                ];
                syslog(LOG_WARNING, "GeocodeApi::reverseGeocodeService => ".json_encode($log));
                Logger::debug($log);
                if(!empty($reverse_info[0]['plus_code'])){
                    $dataUpdate = [
                        'plus_code' => $reverse_info[0]['plus_code'],
                        'address' => $reverse_info[0]['address'],
                    ];
                    $condition = "id = '".$res['id']."'";
                    Address::update($db->getConnect(),$condition, $dataUpdate );
                    $res['plus_code'] = $reverse_info[0]['plus_code'];
                    $res['address'] = $reverse_info[0]['address'];
                }
            } */
            return [$res, NULL];
        } else {
            syslog(LOG_WARNING, "GeocodeApi::geocode => ".json_encode($res));
        }
        // Massage address
        $address = GeocodeApi::processAddressCountry($db, $address, $country);

        // Call geocoding service
        $data = http_build_query(
            array(
                'address' => $address,
            )
        ); 
        $headers = getallheaders();
        $client_id = isset($headers['client_id'])?$headers['client_id']:$client_id;
        syslog(LOG_WARNING,"GeocodeApi::oauth2_geocode(\$db,'${address}',$country,'${client_id}')");
        $url = $oauth2_url."geocode?" . $data;
        $opts = array(
            'http' => array(
                'method' => "GET",
                'timeout' => 60, /* 1 minute */
                'header'  =>
                    //"Content-Type: application/x-www-form-urlencoded\r\n" .
                    //"Accept: application/json\r\n" .
                    "Authorization: Server-Token ${httpAuthToken}\r\n".
                    "client_id: ${client_id}\r\n",
            ),
            "ssl" => array(
              "verify_peer"=>false,
              "verify_peer_name"=>false,
            )
        );
        $benchmarks['function'] = 'GeocodeApi::geocode';
        Utilities::startBenchmark($benchmarks);
        $context = stream_context_create($opts);
        $body = file_get_contents($url, false, $context);
        $geocoded = json_decode($body,true);
        $log = [
            'message' => 'GeocodeApi::geocode call',
            'data' => ['address_input'=>$address_input],
            'errror' => $err
        ];
        Logger::debug($log);
        Utilities::finishBenchmark($benchmarks);
       
        if (is_array($geocoded) && is_array($geocoded["data"]) && !isset($geocoded["error"])) {
            // Cache the result in DB
            syslog(LOG_WARNING, "GeocodeApi::geocode success => ".json_encode($geocoded));
            $address_output = pg_escape_string(html_entity_decode($geocoded["data"]["address"],ENT_QUOTES|ENT_HTML5,"UTF-8"));
            $geocoded["data"]['address_input'] = $address_input;
            $geocoded["data"]['country'] = $country;
            /* if(empty($geocoded["data"]['plus_code'])){
                $benchmarks['function'] = 'google geocode';
                
                $reverse_info = self::reverseGeocodeService($db,$geocoded["data"]['lat'],$geocoded["data"]['lng']);
                
                if(!empty($reverse_info[0]['plus_code'])){
                    $geocoded["data"]['plus_code'] = $reverse_info[0]['plus_code'];
                }
            } */
            
            $address = Geocode::save($db->getConnect(), $geocoded["data"]);
            if(isset($address['id'])){
                if($address_input!=$address["address"]){
                    Address::saveAliasAddress($db->getConnect(), $address['id'], $address_input,"UserInput");
                }
                $geocoded["data"] = array_merge($geocoded["data"], $address);
            }
            return array($geocoded["data"], NULL);
        } else if (is_array($geocoded) && isset($geocoded["error"])) {
            $body = $geocoded["error"];
        }
        syslog(LOG_WARNING, "GeocodeApi::geocode => ".$body);
        return array(NULL, "Geocoding service call error: ".$body);
    }

    public static function reverseGeocode($db, $lat, $lng) {
        syslog(LOG_WARNING,"GeocodeApi::reverseGeocode(${lat}, ${lng})");
        syslog(LOG_WARNING,"lat=${lat},lng=${lng}");
        if (($lat==NULL && $lng==NULL) || ($lat==0 && $lng==0)) {
            return [NULL, "Invalid latitude and longitude"];
        }
        // Reverse geocode using local DB
        list($res,$err) = Geocode::reverseGeocode($db->getConnect(), $lat, $lng);
        if (is_array($res) && isset($res["address"])) {
            return [$res["address"],NULL];
        }

        // Revers geocode using gps DB
	    list($res,$err) = Geocode::reverseGeocodeGps(
            $db->getConnect(), $db->getConnectGPS(), $lat, $lng);
	    if (is_array($res) && isset($res["address"])) {
		    return [$res["address"],NULL];
	    }

        // Reverse geocode service using Google Maps API
        list($res,$err) = GeocodeApi::reverseGeocodeService($db, $lat, $lng);
        if (is_array($res) && isset($res["address"]) && $res["address"]!="") {
            // Cache!
            Geocode::reverseGeocodeCache(
                $db->getConnect(), $res["address"], $lat, $lng, $res["postal"], $res["country"], $res["timeZoneId"], $res['plus_code']
            );
            return [$res["address"],NULL];
        } else {
            syslog(LOG_WARNING,"reverseGeocodeService() failed: ${err}");
        }
        return [NULL, $err];
    }

    public static function reverseGeocodeService($db, $lat, $lng,$client_id="Unknown") {
        syslog(LOG_WARNING,"GeocodeApi::reverseGeocodeService(${lat},${lng})");
        global $savvyext;
        $httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
        $oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
        $data = http_build_query(
            array(
                'lat' => $lat,
                'lng' => $lng
            )
        );
        $url = $oauth2_url."reverse?" . $data;
        $opts = array(
            'http' => array(
                'method' => "GET",
                'timeout' => 60, /* 1 minute */
                'header'  => 
                    "Content-Type: application/x-www-form-urlencoded\r\n" .
                    "Accept: application/json\r\n" .
                    "Authorization: Server-Token ${httpAuthToken}\r\n",
            ),
            "ssl" => array(
              "verify_peer"=>false,
              "verify_peer_name"=>false,
            )
        );
        $context = stream_context_create($opts);
        $body = file_get_contents($url, false, $context);
        $headers = getallheaders();
        $client_id = isset($headers['client_id'])?$headers['client_id']:$client_id;
        //syslog(LOG_WARNING,"BODY: ".$body);
        $log = [
            'message' => 'GeocodeApi::reverse call',
            'data' => ['client_id'=>$client_id, 'lat'=>$lat, 'lng'=>$lng]
        ];
        Logger::debug($log);
        $geocoded = json_decode($body,true);
        if (is_array($geocoded) && is_array($geocoded["data"]) && !isset($geocoded["error"])) {
            //syslog(LOG_WARNING,json_encode($geocoded));
            // Cache the result in DB
            /*Geocode::saveDistanceCache(
                $db->getConnect(), $fromLat, $fromLng, $toLat, $toLng, $geocoded["data"]);
            */
            $address_output = pg_escape_string(html_entity_decode($geocoded["data"]["address"],ENT_QUOTES|ENT_HTML5,"UTF-8"));
            return [$geocoded["data"], NULL];
        } else if (is_array($geocoded) && isset($geocoded["error"])) {
            $body = $geocoded["error"];
        }
        return [NULL, "Reverse geocoding service call error: ".$body];        
    }

    public function checkWithinTheServiceArea($country,$result,$ex=true) {
        error_log("GeocodeApi::checkWithinTheServiceArea($country,\$result,".($ex?"TRUE":"FALSE").")");
        $limits = Geocode::COUNTRY_LIMITS;
        if (!is_array($limits) || !array_key_exists($country,$limits) || !is_array($limits[$country])) {
            return true; // Error, unhandled country or no limits
        }
        $fromLat = $fromLng = NULL;
        foreach ($result as $element) {
            if ($element["type"]==1) {
                $fromLat = $element["geocode"]["lat"];
                $fromLng = $element["geocode"]["lng"];
                
            }
        }
        if ($fromLat==NULL || $fromLng==NULL) return true; // Do not know how to handle it - throw exception?
        $within = false;
        foreach ($limits[$country] as $limit) {
            $radius = $limit['radius']/1000.0;
            $d = GeocodeApi::distanceBetweenTwoGpsCoordinates(
                $fromLat,$fromLng,$limit['latitude'],$limit['longitude'],'K');
            //error_log("$d <= $radius");
            if ($d <= $radius) {
                $within = true;
                break;
            }
        }
        if ($within) return true; // OK
        if ($ex) { // soft or hard fail?
            throw new RuntimeException('Origin address is out of the service area');
        }
        return false;
    }

    private function options($db, $result, $options) {
        error_log('GeocodeApi::options()');
        $fromLat = $fromLng = $toLat = $toLng = NULL;
        foreach ($result as $element) {
            //error_log(json_encode($element));
            if ($element["type"]==1) {
                $fromLat = $element["geocode"]["lat"];
                $fromLng = $element["geocode"]["lng"];
            }
            if ($element["type"]==2) {
                $toLat = $element["geocode"]["lat"];
                $toLng = $element["geocode"]["lng"];
            }
        }
        //error_log(json_encode($result));
        if ($fromLat==NULL || $toLat==NULL) {
            $options['error'] = 'No coordinates available';
            return $options;
        }
        if ($options["travel_time"] && !$options["route_overlay"]) {
            //error_log('*** Google distance matrix ***');
            // Use distance matrix
            list($res, $err) = GeocodeApi::distancegps($db, $fromLat, $fromLng, $toLat, $toLng);
            //error_log(json_encode($res));
            if ($err!=NULL) {
                $options['error'] = $err;
                return $options;
            }
            $options["travel_time"] = $res["duration"];
            $options["travel_distance"] = $res["distance"];
            $options["route_overlay"] = NULL;
        } else {
            //error_log('*** Google directions ***');
            // Use directions
            list($res, $err) = GeocodeApi::route($db, $fromLat, $fromLng, $toLat, $toLng);
            if ($err!=NULL || !is_array($res) || !isset($res["routes"]) || count($res["routes"])<1) {
                $options['error'] = $err!=NULL ? $err : 'No available routes';
                return $options;
            }
            $options["route_overlay"] = [];
            $travel_time = PHP_INT_MAX;
            $travel_distance = PHP_INT_MAX;
            foreach ($res["routes"] as $route) {
                $r = GeocodeApi::processRoute($route);
                if ($travel_time>$r['duration']) {
                    $travel_time = $r['duration'];
                    $travel_distance = $r['distance'];
                } 
                $options["route_overlay"][] = $r;
            }
            $options["travel_time"] = $travel_time==PHP_INT_MAX ? NULL : $travel_time;
            $options["travel_distance"] = $travel_distance==PHP_INT_MAX ? NULL : $travel_distance;
        }
        //https://developers.google.com/maps/documentation/utilities/polylinealgorithm
        //overview_polyline
        //bounds contains the viewport bounding box of the overview_polyline.
        return $options;
    }

    public function distancegps($db, $fromLat, $fromLng, $toLat, $toLng) {
        global $savvyext;
        $httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
        $oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
        // Check local DB cache
        /*
        list ($res, $err) = Geocode::checkDistanceCache(
            $db->getConnect(), $fromLat, $fromLng, $toLat, $toLng);
        if (is_array($res) && isset($res["duration"])) {
            return array($res, NULL);
        }
        */
        // Call geocoding service
        $data = http_build_query(
            array(
                'gps' => implode(",",array($fromLat, $fromLng, $toLat, $toLng)),
            )
        );
        $url = $oauth2_url."distancegps?" . $data;
        $opts = array(
            'http' => array(
                'method' => "GET",
                'timeout' => 60, /* 1 minute */
                'header'  => 
                    "Content-Type: application/x-www-form-urlencoded\r\n" .
                    "Accept: application/json\r\n" .
                    "Authorization: Server-Token ${httpAuthToken}\r\n",
            ),
            "ssl" => array(
              "verify_peer"=>false,
              "verify_peer_name"=>false,
            )
        );
        $context = stream_context_create($opts);
        $body = file_get_contents($url, false, $context);
        $geocoded = json_decode($body,true);
        if (is_array($geocoded) && is_array($geocoded["data"]) && !isset($geocoded["error"])) {
            // Cache the result in DB
            Geocode::saveDistanceCache(
                $db->getConnect(), $fromLat, $fromLng, $toLat, $toLng, $geocoded["data"]);
            return array($geocoded["data"], NULL);
        } else if (is_array($geocoded) && isset($geocoded["error"])) {
            $body = $geocoded["error"];
        }
        return array(NULL, "Geocoding service call error: ".$body);
    }

    public function route($db, $fromLat, $fromLng, $toLat, $toLng, $mode='driving',$waypoints=[]) {
        syslog(LOG_WARNING,"GeocodeApi::route(\$db, $fromLat, $fromLng, $toLat, $toLng, $mode, \$waypoints=[])");
        global $savvyext;
        $httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
        $oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
        // Check local DB cache
        /*list ($res, $err) = Geocode::checkDistanceCache(
            $db->getConnect(), $fromLat, $fromLng, $toLat, $toLng);
        if (is_array($res) && isset($res["duration"])) {
            return array($res, NULL);
        }*/
        // Call geocoding service
        $data = http_build_query(
            array(
                'gps' => implode(",",array($fromLat, $fromLng, $toLat, $toLng)),
                'mode' => $mode,
                'waypoints' => implode(",",$waypoints)
            )
        );
        $url = $oauth2_url."route?" . $data;
        $opts = array(
            'http' => array(
                'method' => "GET",
                'timeout' => 60, /* 1 minute */
                'header'  => 
                    "Content-Type: application/x-www-form-urlencoded\r\n" .
                    "Accept: application/json\r\n" .
                    "Authorization: Server-Token ${httpAuthToken}\r\n",
            ),
            "ssl" => array(
              "verify_peer"=>false,
              "verify_peer_name"=>false,
            )
        );
        $context = stream_context_create($opts);
        $body = file_get_contents($url, false, $context);
        file_put_contents('/home/savvy/FloatWeb/adminsavvy/routes.json', $body);
        //file_put_contents('/home/savvy/FloatWeb/routes.json',json_encode(array('test'=>false)));
        $geocoded = json_decode($body,true);
        if (is_array($geocoded) && is_array($geocoded["data"]) && !isset($geocoded["error"])) {
            // Cache the result in DB
            /*Geocode::saveDistanceCache(
                $db->getConnect(), $fromLat, $fromLng, $toLat, $toLng, $geocoded["data"]);
            */
            return array($geocoded["data"], NULL);
        } else if (is_array($geocoded) && isset($geocoded["error"])) {
            $body = $geocoded["error"];
        }
        return array(NULL, "Routing service call error: ".$body);
    }

    public function processRoute($route) {
        require_once('../common/Polyline.php');
        $result = [
            'polyline' => array(), 
            'distance' => NULL, 
            'duration' => NULL, 
            'summary' => $route["summary"], 
            'overview_polyline' => $route["overview_polyline"],
            'bounds' => $route["bounds"]
        ];
        // ["bounds"]=> ["northeast"] => (["lat"],["lng"])
        // ["bounds"]=> ["southwest"] => (["lat"],["lng"])
        $distance = 0;
        $duration = 0;
        foreach ($route["legs"] as $leg) {
            $distance += $leg["distance"]["value"];
            $duration += $leg["duration"]["value"];
            // ["start_address"]=> "10 Bayfront Ave, Singapore 018956"
            // ["end_address"] => "97 Meyer Rd, Singapore 437918"
            // ["start_location"] => (["lat"],["lng"])
            // ["end_location"]=> (["lat"],["lng"])
            foreach ($leg["steps"] as $step) {
                // ["distance"]=> ["value"]
                // ["duration"]=> ["value"]
                // ["start_location"] => (["lat"],["lng"])
                // ["end_location"]=> (["lat"],["lng"])
                // ["html_instructions"]=> "Head <b>north</b> on <b>Sheares Ave</b> toward <b>Exit 15</b>"
                // ["polyline"]=> array(1) { ["points"] } string(45) "osyFek|xR}ASyCY}@Ga@Eo@CsDCaBAaB?u@As@?q@?q@?"
                // ["travel_mode"]=> string(7) "DRIVING"
                $points = Polyline::decode($step["polyline"]["points"]);
                $result['polyline'] = array_merge($result['polyline'],Polyline::pair($points));
            }
        }
        //=> array(1) { ["points"]=> string(429) "osyFek|xR}ASyCY_BMcFGcEAmEAw@NwBBsB@aADeDBoC@cEHoCNiALuARa@?O?g@HeCf@_Bn@UPOL{@b@{@p@iAnAwA~AeAfAcC`CeDtC{@r@aCrB{BpBOPMK{BmB}GyFiBgBcBeBeD}DcDaEkEaFaBiBeCkCeAkA{BaCc@m@Q_@Om@WwA_@oC_BqKi@aCi@}BSsAaAiFW{@Ui@[i@kE}FsA}Bw@{A]u@e@iB_@aBAGvBU`AMzBc@hB_@dEu@|Di@b@K`@W^a@\_AZ_AHi@Ro@x@mDr@{BvAoFt@}BZw@l@gAbBcC`@q@fA}An@u@hAmAnAmAZ[hCuA~@MjAQ~AElBGbAEZEb@MTOR]P_@HWBo@N}FLiCBu@Ly@JYp@_EdGz@hALz@FdDd@Fa@TiBh@aE^oCCe@yAeHj@GN@NRPRN?r@O"}
        //=> string(14) "Mountbatten Rd"
        $result['distance'] = $distance;
        $result['duration'] = $duration;
        return $result;
    }

    public function withinCity($address,$addrLat,$addrLng,$city,$cityLat,$cityLng,$radius=25) {
        //error_log("public function withinCity($address,$addrLat,$addrLng,$city,$cityLat,$cityLng,$radius)");
        if (stripos($address,$city)!==false) {
            //error_log("'${address}' is within the '${city}'");
            return true;
        }
        $distance = GeocodeApi::distanceBetweenTwoGpsCoordinates($addrLat,$addrLng,$cityLat,$cityLng,'K');
        if ($distance!=0 && $distance<$radius) {
            //error_log("${addrLat},${addrLng} is less(${distance}) than ${radius}km from ${cityLat},${cityLng}");
            return true;
        }
        return false;
    }

    public function distanceBetweenTwoGpsCoordinates($lat1,$lon1,$lat2,$lon2,$unit) {
        //error_log("public function distanceBetweenTwoGpsCoordinates($lat1,$lon1,$lat2,$lon2,$unit)");
        if (($lat1 == $lat2) && ($lon1 == $lon2)) {
            return 0;
        }
        $theta = $lon1 - $lon2;
        $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) +  cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
        $dist = acos($dist);
        $dist = rad2deg($dist);
        $miles = $dist * 60 * 1.1515;
        $unit = strtoupper($unit);
        if ($unit == "K") {
            return ($miles * 1.609344);
        }
        if ($unit == "N") {
            ($miles * 0.8684);
        }
        return $miles;
    }


    private function getProviders($db, $gps_country_code, $country, $options, $member_id) {
        error_log("GeocodeApi::getProviders(\$db, $gps_country_code, $country, \$options, $member_id");
        $service_enabled = true;
        $currency_code = 'SGD';
        error_log('gps_country_code='.$gps_country_code);
        error_log('country='.$country);
        if ($gps_country_code=='UA') { // DEBUG
            $service_enabled = true;
            $gps_country_code = 'US';
            $country = 'US';
        }
        // For not we support US & SG only!
        if ($gps_country_code!='US' && $gps_country_code!='SG') { 
            // We will show Singapore, but disable service
            $service_enabled = false;
            $gps_country_code = 'SG';
            $country = 'SG';
        }
        // TODO: change to select from "country" table
        if ($gps_country_code=='US') {
            $currency_code = 'USD';
        } else {
            $currency_code = 'SGD';
        }
        list ($result, $error) = Geocode::getTransportProvidersByCountry(
            $db->getConnect(), $gps_country_code);
        if ($result==NULL) {
            error_log($error);
        }
        //error_log("gps_country_code=$gps_country_code, service_enabled=$service_enabled, currency_code=$currency_code");
        return array($result, $gps_country_code, $service_enabled, $currency_code);
    }
}
