select id,name from transport_providers; id | name ----+--------------- 1 | Uber 2 | Lyft 3 | Grab 4 | ComfortDelGro 5 | GOJEK 6 | Turo 7 | Getaround 10 | GrabWheels 11 | BART 12 | Muni 13 | LTA (11 rows) */ public static function getTransportRoute($db, $origin, $destination, $option, $trip) { $route = []; switch ($option["transport_provider_id"]) { case 1: case 2: case 3: case 4: case 5: case 8: list($option, $trip, $route) = Route::drivingDirections($db, $origin, $destination, $option, $trip); break; case 11: // BART list($option, $trip, $route) = Route::bartDirections($db, $origin, $destination, $option, $trip); break; case 12: // Muni list($option, $trip, $route) = Route::muniDirections($db, $origin, $destination, $option, $trip); break; case 13: // LTA list($option, $trip, $route) = Route::ltaDirections($db, $origin, $destination, $option, $trip); break; case 14: // MARTA list($option, $trip, $route) = Route::martaDirections($db, $origin, $destination, $option, $trip); break; } error_log(json_encode($trip)); return [$option, $trip, $route]; } public static function drivingDirections($db, $origin, $destination, $option, $trip) { list($option, $trip, $route) = Route::processRideshareOption($db, $option, $origin, $destination, $trip); $option["travel_time"] = (int)$trip["duration"]; $option["travel_distance"] = (int)$trip["distance"]; $trip["source"] = strtolower($option["name"]); return [$option, $trip, $route]; } public static function drivingDirectionsOriginal($db, $origin, $destination, $option, $trip) { $options = []; $trip = []; $option["travel_time"] = 0; $option["travel_distance"] = 0; $fromLat = $origin['lat']; $fromLng = $origin['lng']; $toLat = $destination['lat']; $toLng = $destination['lng']; // This is paranoid redudant check, but since the function is "public"... if ($fromLat==null || $fromLng==null || $toLat==null || $toLng==null || ($fromLat==0 && $fromLng==0) || ($toLat==0 && $toLng==0)) { throw new Exception('Invalid origin and/or destination coordinates'); } list($res, $err) = Route::serviceRoute($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 [$option, $trip, $options]; } $options["route_overlay"] = []; $travel_time = PHP_INT_MAX; $travel_distance = PHP_INT_MAX; foreach ($res["routes"] as $route) { $r = Route::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; $option["travel_time"] = (int)$options["travel_time"]; $option["travel_distance"] = (int)$options["travel_distance"]; //https://developers.google.com/maps/documentation/utilities/polylinealgorithm //overview_polyline //bounds contains the viewport bounding box of the overview_polyline. return [$option, $trip, $options]; } public static function bartDirections($db, $origin, $destination, $option, $trip) { $route = []; $option["travel_time"] = 0; $option["travel_distance"] = 0; $route["travel_time"] = 0; $route["travel_distance"] = 0; $route["route_overlay"] = []; $country = $origin["country"]; $member_id = $option["member_id"]; // New directions $tripService = Route::tripService($trip["id"], $country, 'BART'); //error_log(json_encode($tripService)); if (is_array($tripService) && isset($tripService["travel_date"])) { $tripService["member_id"] = $member_id; // Pass along $tripService["country"] = $country; $tripService = MultiModalFilter::byDuration($tripService); //$tripService = MultiModalFilter::byCost($tripService); $trip = $tripService; $option["travel_time"] = (int)$trip["options"]["legs"][0]["duration"]; $option["travel_distance"] = (int)$trip["options"]["legs"][0]["distance"]; $route = Route::routeFromTrip($db, $origin, $destination, $option, $trip); } return [$option, $trip, $route]; } public static function muniDirections($db, $origin, $destination, $option, $trip) { $route = []; $option["travel_time"] = 0; $option["travel_distance"] = 0; $country = $origin["country"]; $member_id = $option["member_id"]; // New directions $tripService = Route::tripService($trip["id"], $country, 'MUNI'); //error_log(json_encode($tripService)); if (is_array($tripService) && isset($tripService["travel_date"])) { $tripService["member_id"] = $member_id; // Pass along $tripService["country"] = $country; $tripService = MultiModalFilter::byDuration($tripService); //$tripService = MultiModalFilter::byCost($tripService); $trip = $tripService; $option["travel_time"] = (int)$trip["options"]["legs"][0]["duration"]; $option["travel_distance"] = (int)$trip["options"]["legs"][0]["distance"]; $route = Route::routeFromTrip($db, $origin, $destination, $option, $trip); } return [$option, $trip, $route]; } public static function ltaDirections($db, $origin, $destination, $option, $trip) { $route = []; $option["travel_time"] = 0; $option["travel_distance"] = 0; $country = $origin["country"]; $member_id = $option["member_id"]; // New directions $tripService = Route::tripService($trip["id"], $country, 'LTA'); //error_log(json_encode($tripService)); if (is_array($tripService) && isset($tripService["travel_date"])) { $tripService["member_id"] = $member_id; // Pass along $tripService["country"] = $country; $tripService = MultiModalFilter::byDuration($tripService); //$tripService = MultiModalFilter::byCost($tripService); $trip = $tripService; $option["travel_time"] = (int)$trip["options"]["legs"][0]["duration"]; $option["travel_distance"] = (int)$trip["options"]["legs"][0]["distance"]; $route = Route::routeFromTrip($db, $origin, $destination, $option, $trip); } return [$option, $trip, $route]; } public static function martaDirections($db, $origin, $destination, $option, $trip) { $route = []; $option["travel_time"] = 0; $option["travel_distance"] = 0; $country = $origin["country"]; $member_id = $option["member_id"]; // New directions $tripService = Route::tripService($trip["id"], $country, 'MARTA'); //error_log(json_encode($tripService)); if (is_array($tripService) && isset($tripService["travel_date"])) { $tripService["member_id"] = $member_id; // Pass along $tripService["country"] = $country; $tripService = MultiModalFilter::byDuration($tripService); $trip = $tripService; $option["travel_time"] = (int)$trip["options"]["legs"][0]["duration"]; $option["travel_distance"] = (int)$trip["options"]["legs"][0]["distance"]; $route = Route::routeFromTrip($db, $origin, $destination, $option, $trip); } return [$option, $trip, $route]; } public static function routeFromTrip($db, $origin, $destination, $option, $trip) { $leg = [ "distance" => 0, "duration" => 0, "polyline" => NULL ]; // Select leg foreach ($trip["options"]["legs"] as $leg) { break; } $route["travel_time"] = (int)$leg["duration"]; $route["travel_distance"] = (int)$leg["distance"]; // TODO: BART fix polyline! $route["route_overlay"][] = [ "polyline" => [], "distance" => $leg["distance"], "duration" => $leg["duration"], "summary" => "Generated option", "overview_polyline" => [ "points" => Route::processPolyline($leg["polyline"]) ], "bounds" => [ "northeast" => [ "lat" => $origin["lat"], "lng" => $origin["lng"] ], "southwest" => [ "lat" => $destination["lat"], "lng" => $destination["lng"] ] ] ]; return $route; } public static function checkValidRoute($db, $route) { // Basic input sanity check: route if (!is_array($route) || !array_key_exists('travel_time',$route) || !is_numeric($route['travel_time']) || !array_key_exists('travel_distance',$route) || !is_numeric($route['travel_distance']) || !array_key_exists('route_overlay',$route) || !is_array($route['route_overlay'])) { throw new Exception('Invalid transport route'); } } public static function serviceRoute($db, $fromLat, $fromLng, $toLat, $toLng, $mode='driving',$waypoints=[]) { global $savvyext; $httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token'); $oauth2_url = $savvyext->cfgReadChar('system.oauth2_url'); error_log("Route::serviceRoute(\$db, $fromLat, $fromLng, $toLat, $toLng, $mode, \$waypoints=[])"); // 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); $geocoded = json_decode($body,true); if (is_array($geocoded) && is_array($geocoded["data"]) && !isset($geocoded["error"])) { 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 static 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"] ]; $distance = 0; $duration = 0; foreach ($route["legs"] as $leg) { $distance += $leg["distance"]["value"]; $duration += $leg["duration"]["value"]; foreach ($leg["steps"] as $step) { $points = Polyline::decode($step["polyline"]["points"]); $result['polyline'] = array_merge($result['polyline'],Polyline::pair($points)); } } $result['distance'] = $distance; $result['duration'] = $duration; return $result; } public static function processRideshareOption($db, $option, $origin, $destination, $trip) { error_log("Options::processRideshareOption(\$db, \$option, \$origin, \$destination, \$trip)"); $route = []; $route["travel_time"] = 0; $route["travel_distance"] = 0; $route["route_overlay"] = []; $member_id = $option["member_id"]; $country = $origin["country"]; $rideshare = $option["name"]; $transport_provider_id = $option["transport_provider_id"]; if (array_key_exists("timeZoneId",$origin) && $origin["timeZoneId"]!="") { $tz = $origin["timeZoneId"]; } else if (array_key_exists("timeZoneId",$destination) && $destination["timeZoneId"]!="") { $tz = $destination["timeZoneId"]; } else { $tz = Options::getTimezoneById($db, $origin["timezone"], "Asia/Singapore"); } //"trips"=>$trips; if (!is_array($trip) && count($trip)<1) { syslog(LOG_WARNING,"Invalid trip"); return [$option, $trip, $route]; // No idea how to handle it } /*if (!array_key_exists('trip',$trip) || !is_array($trip['trip']) || count($trip['trip'])<10) { syslog(LOG_WARNING,"Invalid trip[0]!"); return $trips; // No idea how to handle it } //*/ $oldTrip = $trip; //['trip']; $origin['latitude'] = $origin['lat']; $origin['longitude'] = $origin['lng']; $destination['latitude'] = $destination['lat']; $destination['longitude'] = $destination['lng']; list($res, $err) = Route::serviceRoute($db, $origin['lat'], $origin['lng'], $destination['lat'], $destination['lng'], 'driving' ); if ($err!=NULL || !is_array($res) || !isset($res["routes"]) || count($res["routes"])<1) { error_log('No available routes!'); error_log($err); return [$option, $trip, $route]; // We cannot route driving! } $route_overlay = []; $travel_time = PHP_INT_MAX; $travel_distance = PHP_INT_MAX; foreach ($res["routes"] as $item) { $r = Route::processRoute($item); if ($travel_time>$r['duration']) { $travel_time = $r['duration']; $travel_distance = $r['distance']; } $route_overlay[] = $r; } $oldTrip["duration"] = $travel_time==PHP_INT_MAX ? NULL : $travel_time; $oldTrip["distance"] = $travel_distance==PHP_INT_MAX ? NULL : $travel_distance; $route["travel_time"] = (int)$oldTrip["duration"]; $route["travel_distance"] = (int)$oldTrip["distance"]; $route["route_overlay"] = $route_overlay; /* $result = [ 'polyline' => array(), 'distance' => NULL, 'duration' => NULL, 'summary' => $route["summary"], 'overview_polyline' => $route["overview_polyline"], 'bounds' => $route["bounds"] ]; */ //syslog(LOG_WARNING,json_encode($res)); $route_0 = $res["routes"][0]; $route_leg = $route_0["legs"][0]; $route_leg_steps = $route_leg["steps"]; $overview_polyline = Route::processPolyline($route_0["overview_polyline"]["points"]); $leg_steps = []; $distance = 0; $duration = 0; $location_start_lat = 0; $location_start_lng = 0; $location_end_lat = 0; $location_end_lng = 0; $polyline = []; $i = 0; $j=1; $n = count($route_leg_steps); for (; $i<$n; $i++) { $step = $route_leg_steps[$i]; if ($step["travel_mode"] == "DRIVING") { $location_start_lat = $step["start_location"]["lat"]; $location_start_lng = $step["start_location"]["lng"]; break; } $leg_steps[] = [ 'distance' => $step["distance"]["value"], 'duration' => $step["duration"]["value"], 'html_instructions' => $step["html_instructions"], 'location_end_lat' => $step["end_location"]["lat"], 'location_end_lng' => $step["end_location"]["lng"], 'location_start_lat' => $step["start_location"]["lat"], 'location_start_lng' => $step["start_location"]["lng"], 'polyline' => $step["polyline"]["points"], /*"yz|Fm_nxRi@e@_@P",*/ 'sid' => $j++, 'travel_mode' => $step["travel_mode"] ]; } for (;$i<$n; $i++) { $step = $route_leg_steps[$i]; if ($step["travel_mode"] != "DRIVING") { break; } $distance += $step["distance"]["value"]; $duration += $step["duration"]["value"]; $location_end_lat = $step["end_location"]["lat"]; $location_end_lng = $step["end_location"]["lng"]; $polyline[] = $step["polyline"]["points"]; } $leg_steps[] = [ 'distance' => $distance, 'duration' => $duration, 'fare_quote' => null, 'fare_raw' => null, 'html_instructions' => "Take ".$rideshare." to ".$destination["address"], 'location_end_lat' => $location_end_lat, 'location_end_lng' => $location_end_lng, 'location_start_lat' => $location_start_lat, 'location_start_lng' => $location_start_lng, 'polyline' => Route::processPolyline($polyline), 'quote_group_id' => null, 'sid' => $j++, 'travel_mode' => strtoupper($rideshare) ]; for (;$i<$n; $i++) { $step = $route_leg_steps[$i]; $leg_steps[] = [ 'distance' => $step["distance"]["value"], 'duration' => $step["duration"]["value"], 'html_instructions' => $step["html_instructions"], 'location_end_lat' => $step["end_location"]["lat"], 'location_end_lng' => $step["end_location"]["lng"], 'location_start_lat' => $step["start_location"]["lat"], 'location_start_lng' => $step["start_location"]["lng"], 'polyline' => $step["polyline"]["points"], /*"yz|Fm_nxRi@e@_@P",*/ 'sid' => $j++, 'travel_mode' => $step["travel_mode"] ]; } $cost = null; $rideshare = [ 'cost' => $cost, 'cost_raw' => $cost, 'count' => 1, 'country' => $country, 'distance' => $oldTrip['distance'], 'dup_id' => null, 'duration' => $oldTrip['duration'], 'id' => $oldTrip['id'], 'location_end' => $destination['address'], 'location_end_id' => $oldTrip['location_end_id'], 'location_end_lat' => $destination['latitude'], 'location_end_lng' => $destination['longitude'], 'location_end_tz' => $destination['timezone'], 'location_start' => $origin['address'], 'location_start_id' => $oldTrip['location_start_id'], 'location_start_lat' => $origin['latitude'], 'location_start_lng' => $origin['longitude'], 'location_start_tz' => $origin['timezone'], 'member_id' => $member_id, 'options' => [], 'parsedemail_item_id' => null, /* $trip['id'] ??? */ 'private' => 't', 'scheduled' => null, 'trackedemail_item_id' => null, 'transport_provider_id' => $transport_provider_id, 'travel_date' => $oldTrip['travel_date'], 'travel_date_end' => $oldTrip['travel_date_end'], 'updated' => $oldTrip['updated'] ]; // Create trip advice $rideshare["source"] = strtolower($option["name"]); $parsedemail_item_advice_google_id = Options::saveTripAdvice($db, $rideshare); error_log(json_encode($parsedemail_item_advice_google_id)); $leg_fare = $cost; // Get departure and arrival times... $date = new DateTime("now", new DateTimeZone($tz)); $date->add(new DateInterval('PT15M')); $departure_time = $date->getTimestamp(); $duration = Route::getDuration(array_key_exists('duration',$oldTrip)?$oldTrip['duration']:900); $date->add(new DateInterval('PT'.$duration.'M')); $arrival_time = $date->getTimestamp(); $leg = [ 'id' => NULL, 'parsedemail_item_advice_google_id' => $parsedemail_item_advice_google_id, 'arrival_time' => $arrival_time, 'arrival_time_zone' => $destination['timezone'], 'arrival_timezone' => $tz, 'departure_time' => $departure_time, 'departure_time_zone' => $origin['timezone'], 'departure_timezone' => $tz, 'distance' => $oldTrip['distance'], 'duration' => $oldTrip['duration'], 'fare_raw' => $cost, 'polyline' => $overview_polyline, 'quote' => $cost, 'steps' => count($leg_steps), ]; // Create leg $google_directions_leg_id = Options::saveTripAdviceLeg($db, $rideshare, $leg); $leg['id'] = $google_directions_leg_id; foreach ($leg_steps as $i=>$step) { $step['google_directions_leg_id'] = $google_directions_leg_id; $google_directions_leg_step_id = Options::saveTripAdviceLegStep($db, $rideshare, $leg, $step); $step['id'] = $google_directions_leg_step_id; $leg_steps[$i] = $step; } $options = [ 'gid' => $google_directions_leg_id, /* ??? */ 'leg_fare' => [$google_directions_leg_id => $leg_fare], 'leg_steps' => [$google_directions_leg_id => $leg_steps], 'legs' => [$leg], 'routes' => 1 ]; $rideshare['options'] = $options; return [$option, $rideshare, $route]; } protected static function getDuration($duration=900) { if ($duration==NULL) { return 15; } if ($duration>60) { return (int)($duration/60); } return 1; } public static function processPolyline($data) { if (!is_array($data)) { return $data; } require_once('../common/Polyline.php'); $points = []; foreach ($data as $item) { $points = array_merge($points, Polyline::decode($item)); //$points = array_merge(Polyline::decode($item),$points); break; } return Polyline::encode($points); } public static function tripService($id, $country, $mode) { global $savvyext; $httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token'); $oauth2_url = $savvyext->cfgReadChar('system.oauth2_url'); error_log("Route::tripService($id)"); if ($country!='SG' && $country!='US') { throw new Exception('Unsupported country: '.$country); } // curl -d 'id=44254' -d 'mode=BART' -d 'country=US' -H 'Authorization: Server-Token 99dfe35fcb7de1ee' -X POST {$oauth2_url}modetrips $postdata = http_build_query( array( 'id'=> $id, 'mode' => $mode, 'country' => $country ) ); $url = $oauth2_url."modetrips"; $opts = array( 'http' => array( 'method' => "POST", '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", 'content' => $postdata ), "ssl" => array( "verify_peer"=>false, "verify_peer_name"=>false, ) ); $context = stream_context_create($opts); error_log("started trips service call"); $t = time(); $body = file_get_contents($url, false, $context); error_log("complete trips service call: ".(time()-$t)." seconds"); $trip = json_decode($body,true); return $trip; } } /* ["id"]=> string(1) "5" ["city_id"]=> string(1) "2" ["name"]=> string(4) "Uber" ["transport_provider_id"]=> string(1) "1" ["custom"]=> NULL */ // vi:ts=2