Files
dev-chiefworks 47f4fad75c Added Other AP
2022-04-26 11:30:34 -04:00

634 lines
25 KiB
PHP

<?php
class Route {
/*
savvy=> 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