Added Other AP

This commit is contained in:
dev-chiefworks
2022-04-26 11:30:34 -04:00
parent 5e006a6a21
commit 47f4fad75c
251 changed files with 29298 additions and 4 deletions
+27
View File
@@ -0,0 +1,27 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /SAVVY/trips/
#RewriteBase /
#Checks to
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?/$1 [L]
</IfModule>
<IfModule !mod_rewrite.c>
# If we don't have mod_rewrite installed, all 404's
# can be sent to index.php, and everything works as normal.
# Submitted by: ElliotHaughin
ErrorDocument 404 /index.php
</IfModule>
#Header add Access-Control-Allow-Origin "*"
#Header add Access-Control-Expose-Headers "Access-Control-Allow-Origin"
#Header add Access-Control-Allow-Headers "Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With"
#Header add Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS"
#Header add Content-type "application/json"
+240
View File
@@ -0,0 +1,240 @@
<?php
class Activity {
const TIME_DELTA = 180; // min (3 hrs)
const TIME_LIMIT = 1262322000; // 2010-01-01
const DEFAULT_COUNTRY = 'SG';
const DEFAULT_OPTIONS = 3;
const CITY_CURVATURE = 1.25;
const CITY_SPEED = 0.667; // km/min (0.0667 = 40 km/h)
const MIN_ACTIVITY_DISTANCE = 0.05; // 50m
const MAX_ACTIVITY_DISTANCE = 50.0; // 50km
const ACTIVITY_RADIUS = 30000; // 30km
public function getActivity($db, $gpsdb, $member_id, $lat, $lng, $radius, $time, $time_delta=self::TIME_DELTA, $country=self::DEFAULT_COUNTRY) {
syslog(LOG_WARNING,"Activity::getActivity(\$db, \$gpsdb, $member_id, $lat, $lng, $radius, $time, $time_delta, $country)");
$all_time_days = self::getAllTimeDays();
list($res, $err) = self::activityByRangeAndTimeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'start', $time, $time_delta);
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndTimeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'end', $time, $time_delta);
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'start');
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'end');
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
if ($country=='SG') {
// For US it may produce the results too far apart
list($res, $err) = self::activityByRangeAndTime($db, $member_id, 7, $time, $time_delta, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRangeAndTime($db, $member_id, 30, $time, $time_delta, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRangeAndTime($db, $member_id, $all_time_days, $time, $time_delta, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRange($db, $member_id, 7, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRange($db, $member_id, 30, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRange($db, $member_id, $all_time_days, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
}
//*/
return [NULL, "We could not find any recent activity in this area"];
}
public function getAddressActivity($db, $address, $member_id, $time, $time_delta=self::TIME_DELTA) {
// TODO: radius search
}
public function activityByRange($db, $member_id, $days, $country=self::DEFAULT_COUNTRY) {
$db_days = date("w",$db_time) + (int)$days;
$db_country = pg_escape_string($country);
$q = "SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm ";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days') ";
$q.= " AND (c.country='${db_country}' OR d.country='${db_country}') ";
$q.= " ORDER BY b.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public static function activityByRangeAndRadius($gpsdb, $db, $member_id, $days, $lat, $lng, $radius, $what) {
syslog(LOG_WARNING,"Activity::activityByRangeAndRadius(\$gpsdb, \$db, $member_id, $days, $lat, $lng, $radius, $what)");
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$db_days = (int)$days;
$q = "SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm, ";
$q.= " c.geometry AS location_start_geometry, d.geometry AS location_end_geometry ";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days') ";
//$q.= " AND (b.location_start_id IN (%d,%d) OR b.location_end_id IN (%d,%d)) ";
$q.= " AND (";
$q.= "ST_DWithin(c.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " OR ";
$q.= "ST_DWithin(d.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= ") ORDER BY b.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$cache_from = [];
$cache_to = [];
$result = [];
while ($f = pg_fetch_assoc($r)) {
if (array_key_exists($f["location_start_geometry"],$cache_from) && $f["location_end_geometry"]==$cache_from[$f["location_start_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
if (array_key_exists($f["location_end_geometry"],$cache_to) && $f["location_start_geometry"]==$cache_to[$f["location_end_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
$result[] = $f;
$cache_from[$f["location_start_geometry"]] = $f["location_end_geometry"];
$cache_to[$f["location_end_geometry"]] = $f["location_start_geometry"];
}
unset($cache_from); // clear
unset($cache_to); // clear
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public function activityByRangeAndTimeAndRadius($gpsdb, $db, $member_id, $days, $lat, $lng, $radius, $what, $time, $time_delta=self::TIME_DELTA) {
syslog(LOG_WARNING,"Activity::activityByRangeAndTimeAndRadius(\$gpsdb, \$db, $member_id, $days, $lat, $lng, $radius, $what, $time, $time_delta)");
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$db_time = strtotime($time);
$db_days = date("w",$db_time) + (int)$days;
$db_time_delta = (int)$time_delta;
syslog(LOG_WARNING,"${db_time}=".date("Y-m-d H:i",$db_time));
// Ranges
$dh = 60*date("G",$db_time)+date("i",$db_time);
$d1 = $dh - $db_time_delta;
$d2 = $dh + $db_time_delta;
syslog(LOG_WARNING,"${d1} <= x <= ${d2}");
$q = "SELECT e.* FROM (SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm, ";
$q.= " c.geometry AS location_start_geometry, d.geometry AS location_end_geometry ";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days') ";
$q.= " AND (";
$q.= "ST_DWithin(c.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " OR ";
$q.= "ST_DWithin(d.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= ")) AS e ";
$q.= " WHERE e.dm>=${d1} AND e.dm<=${d2} ";
$q.= " ORDER BY e.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
syslog(LOG_WARNING,"parsedemail_item(s) to process: ".pg_num_rows($r));
$cache_from = [];
$cache_to = [];
$result = [];
while ($f=pg_fetch_assoc($r)) {
if (array_key_exists($f["location_start_geometry"],$cache_from) && $f["location_end_geometry"]==$cache_from[$f["location_start_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
if (array_key_exists($f["location_end_geometry"],$cache_to) && $f["location_start_geometry"]==$cache_to[$f["location_end_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
$result[] = $f;
}
unset($cache_from); // clear
unset($cache_to); // clear
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public function activityByRangeAndTime($db, $member_id, $days, $time, $time_delta=self::TIME_DELTA, $country=self::DEFAULT_COUNTRY) {
syslog(LOG_WARNING,"Activity::activityByRangeAndTime(\$db, $member_id, $days, $time, $time_delta");
$db_time = strtotime($time);
$db_days = date("w",$db_time) + (int)$days;
$db_time_delta = (int)$time_delta;
$db_country = pg_escape_string($country);
syslog(LOG_WARNING,"${db_time}=".date("Y-m-d H:i",$db_time));
// Ranges
$dh = 60*date("G",$db_time)+date("i",$db_time);
$d1 = $dh - $db_time_delta;
$d2 = $dh + $db_time_delta;
syslog(LOG_WARNING,"${d1} <= x <= ${d2}");
$q = "SELECT e.* FROM (SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm ";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND (c.country='${db_country}' OR d.country='${db_country}') ";
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days')) AS e ";
$q.= " WHERE e.dm>=${d1} AND e.dm<=${d2} ";
$q.= " ORDER BY e.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
protected function getAllTimeDays() {
$datediff = time() - self::TIME_LIMIT;
return round($datediff / (60 * 60 * 24));
}
}
// vi:ts=2
File diff suppressed because it is too large Load Diff
+280
View File
@@ -0,0 +1,280 @@
<?php
class ActivityDirect {
const TIME_DELTA = 180; // min (3 hrs)
const TIME_LIMIT = 1262322000; // 2010-01-01
const DEFAULT_COUNTRY = 'SG';
const DEFAULT_OPTIONS = 3;
const CITY_CURVATURE = 1.25;
const CITY_SPEED = 0.667; // km/min (0.0667 = 40 km/h)
const ACTIVITY_RADIUS = 30000; // 30km
public function getActivity($db, $gpsdb, $member_id, $lat, $lng, $radius, $time, $time_delta=self::TIME_DELTA, $country=self::DEFAULT_COUNTRY) {
syslog(LOG_WARNING,"ActivityDirect::getActivity(\$db, \$gpsdb, $member_id, $lat, $lng, $radius, $time, $time_delta, $country)");
$all_time_days = self::getAllTimeDays();
list($res, $err) = self::activityByRangeAndTimeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'start', $time, $time_delta);
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndTimeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'end', $time, $time_delta);
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'start');
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndRadius(
$gpsdb, $db, $member_id, $all_time_days, $lat, $lng, $radius, 'end');
if ($res && count($res)>0) {
return [$res, NULL];
}
syslog(LOG_WARNING,$err);
list($res, $err) = self::activityByRangeAndTime($db, $member_id, 7, $time, $time_delta, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRangeAndTime($db, $member_id, 30, $time, $time_delta, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRangeAndTime($db, $member_id, $all_time_days, $time, $time_delta, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRange($db, $member_id, 7, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRange($db, $member_id, 30, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}
list($res, $err) = self::activityByRange($db, $member_id, $all_time_days, $country);
if ($res && count($res)>0) {
return [$res, NULL];
}//*/
return [NULL, "No activity found"];
}
public function getAddressActivity($db, $address, $member_id, $time, $time_delta=self::TIME_DELTA) {
// TODO: radius search
}
public function activityByRange($db, $member_id, $days, $country=self::DEFAULT_COUNTRY) {
$db_days = date("w",$db_time) + (int)$days;
$db_country = pg_escape_string($country);
$q = "SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm ";
$q.= " c.geometry AS location_start_geometry, d.geometry AS location_end_geometry ";
$q.= ",c.address AS location_start,c.latitude AS location_start_lat,c.longitude AS location_start_lng";
$q.= ",c.timezone AS location_start_tz,c.postal AS location_start_postal,c.country AS location_start_country";
$q.= ",c.description AS location_start_description";
$q.= ",d.address AS location_end,d.latitude AS location_end_lat,d.longitude AS location_end_lng";
$q.= ",d.timezone AS location_end_tz,d.postal AS location_end_postal,d.country AS location_end_country";
$q.= ",d.description AS location_end_description";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days') ";
$q.= " AND (c.country='${db_country}' OR d.country='${db_country}') ";
$q.= " ORDER BY b.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public static function activityByRangeAndRadius($gpsdb, $db, $member_id, $days, $lat, $lng, $radius, $what) {
syslog(LOG_WARNING,"ActivityDirect::activityByRangeAndRadius(\$gpsdb, \$db, $member_id, $days, $lat, $lng, $radius, $what)");
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$db_days = (int)$days;
$q = "SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm, ";
$q.= " c.geometry AS location_start_geometry, d.geometry AS location_end_geometry ";
$q.= ",c.address AS location_start,c.latitude AS location_start_lat,c.longitude AS location_start_lng";
$q.= ",c.timezone AS location_start_tz,c.postal AS location_start_postal,c.country AS location_start_country";
$q.= ",c.description AS location_start_description";
$q.= ",d.address AS location_end,d.latitude AS location_end_lat,d.longitude AS location_end_lng";
$q.= ",d.timezone AS location_end_tz,d.postal AS location_end_postal,d.country AS location_end_country";
$q.= ",d.description AS location_end_description";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days') ";
//$q.= " AND (b.location_start_id IN (%d,%d) OR b.location_end_id IN (%d,%d)) ";
$q.= " AND (";
$q.= "ST_DWithin(c.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " OR ";
$q.= "ST_DWithin(d.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= ") ORDER BY b.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$cache_from = [];
$cache_to = [];
$result = [];
while ($f = pg_fetch_assoc($r)) {
if (array_key_exists($f["location_start_geometry"],$cache_from) && $f["location_end_geometry"]==$cache_from[$f["location_start_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
if (array_key_exists($f["location_end_geometry"],$cache_to) && $f["location_start_geometry"]==$cache_to[$f["location_end_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
$cache_from[$f["location_start_geometry"]] = $f["location_end_geometry"];
$cache_to[$f["location_end_geometry"]] = $f["location_start_geometry"];
}
unset($cache_from); // clear
unset($cache_to); // clear
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public function activityByRangeAndTimeAndRadius($gpsdb, $db, $member_id, $days, $lat, $lng, $radius, $what, $time, $time_delta=self::TIME_DELTA) {
syslog(LOG_WARNING,"ActivityDirect::activityByRangeAndTimeAndRadius(\$gpsdb, \$db, $member_id, $days, $lat, $lng, $radius, $what, $time, $time_delta)");
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$db_time = strtotime($time);
$db_days = date("w",$db_time) + (int)$days;
$db_time_delta = (int)$time_delta;
syslog(LOG_WARNING,"${db_time}=".date("Y-m-d H:i",$db_time));
// Ranges
$dh = 60*date("G",$db_time)+date("i",$db_time);
$d1 = $dh - $db_time_delta;
$d2 = $dh + $db_time_delta;
syslog(LOG_WARNING,"${d1} <= x <= ${d2}");
$q = "SELECT e.* FROM (SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm, ";
$q.= " c.geometry AS location_start_geometry, d.geometry AS location_end_geometry ";
$q.= ",c.address AS location_start,c.latitude AS location_start_lat,c.longitude AS location_start_lng";
$q.= ",c.timezone AS location_start_tz,c.postal AS location_start_postal,c.country AS location_start_country";
$q.= ",c.description AS location_start_description";
$q.= ",d.address AS location_end,d.latitude AS location_end_lat,d.longitude AS location_end_lng";
$q.= ",d.timezone AS location_end_tz,d.postal AS location_end_postal,d.country AS location_end_country";
$q.= ",d.description AS location_end_description";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days') ";
$q.= " AND (";
$q.= "ST_DWithin(c.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " OR ";
$q.= "ST_DWithin(d.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= ")) AS e ";
$q.= " WHERE e.dm>=${d1} AND e.dm<=${d2} ";
$q.= " ORDER BY e.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
syslog(LOG_WARNING,"parsedemail_item(s) to process: ".pg_num_rows($r));
$cache_from = [];
$cache_to = [];
$result = [];
while ($f=pg_fetch_assoc($r)) {
if (array_key_exists($f["location_start_geometry"],$cache_from) && $f["location_end_geometry"]==$cache_from[$f["location_start_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
if (array_key_exists($f["location_end_geometry"],$cache_to) && $f["location_start_geometry"]==$cache_to[$f["location_end_geometry"]]) {
continue; // We have similar trip in result set - let's skip it
}
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
unset($cache_from); // clear
unset($cache_to); // clear
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public function activityByRangeAndTime($db, $member_id, $days, $time, $time_delta=self::TIME_DELTA, $country=self::DEFAULT_COUNTRY) {
syslog(LOG_WARNING,"ActivityDirect::activityByRangeAndTime(\$db, $member_id, $days, $time, $time_delta");
$db_time = strtotime($time);
$db_days = date("w",$db_time) + (int)$days;
$db_time_delta = (int)$time_delta;
$db_country = pg_escape_string($country);
syslog(LOG_WARNING,"${db_time}=".date("Y-m-d H:i",$db_time));
// Ranges
$dh = 60*date("G",$db_time)+date("i",$db_time);
$d1 = $dh - $db_time_delta;
$d2 = $dh + $db_time_delta;
syslog(LOG_WARNING,"${d1} <= x <= ${d2}");
$q = "SELECT e.* FROM (SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm ";
$q.= " c.geometry AS location_start_geometry, d.geometry AS location_end_geometry ";
$q.= ",c.address AS location_start,c.latitude AS location_start_lat,c.longitude AS location_start_lng";
$q.= ",c.timezone AS location_start_tz,c.postal AS location_start_postal,c.country AS location_start_country";
$q.= ",c.description AS location_start_description";
$q.= ",d.address AS location_end,d.latitude AS location_end_lat,d.longitude AS location_end_lng";
$q.= ",d.timezone AS location_end_tz,d.postal AS location_end_postal,d.country AS location_end_country";
$q.= ",d.description AS location_end_description";
$q.= " FROM trackedemail_item a, parsedemail_item b ";
$q.= " LEFT JOIN address c ON (c.id=b.location_start_id) ";
$q.= " LEFT JOIN address d ON (d.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".((int)$member_id);
$q.= " AND (c.country='${db_country}' OR d.country='${db_country}') ";
$q.= " AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days')) AS e ";
$q.= " WHERE e.dm>=${d1} AND e.dm<=${d2} ";
$q.= " ORDER BY e.travel_date DESC";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
protected function getAllTimeDays() {
$datediff = time() - self::TIME_LIMIT;
return round($datediff / (60 * 60 * 24));
}
}
// vi:ts=2
@@ -0,0 +1,367 @@
<?php
class ActivityDirectApi extends Api
{
public $apiName = 'activitydirect';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
syslog(LOG_WARNING,'ActivityDirectApi::createAction()');
$message = 'No recent activity found';
$code = 404;
$time = $this->requestParams['time'] ?? '';
$location = $this->requestParams['location'] ?? [];
$country = $this->requestParams['country'] ?? ActivityDirect::DEFAULT_COUNTRY;
$member_id = $this->requestParams['member_id'] ?? 0;
try {
syslog(LOG_WARNING,'country='.$country);
$db = new Db();
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
if ($country!='US' && $country!='SG') {
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 ($member_id<1) {
throw new RuntimeException('Invalid member ID',500);
}
if ($time=='' || strtotime($time)<ActivityDirect::TIME_LIMIT) {
throw new RuntimeException('Invalid time',500);
}
$address = NULL;
$street_address = "";
if (is_array($location) && isset($location["lat"]) && isset($location["lng"])) {
// DEBUG
list($location["lat"],$location["lng"]) = Geocode::mockGPSLocation(
$db->getConnect(), $member_id, $location["lat"], $location["lng"], 'default');
syslog(LOG_WARNING,"lat=".$location["lat"].",lng=".$location["lng"]);
list($street_address,$err) = GeocodeApi::reverseGeocode($db, $location["lat"], $location["lng"]);
if ($street_address=="") {
syslog(LOG_WARNING,'Reverse geocoder failed for ('.$location["lat"].', '.$location["lng"].')');
throw new RuntimeException($err?"Geocoder failed: $err":'Cannot geocode your GSP location',500);
}
syslog(LOG_WARNING,"street_address=".json_encode($street_address));
list($address,$err) = Geocode::checkLatLngByAddress($db->getConnect(), $street_address, $country);
if (!is_array($address) || !isset($address["address"])) {
syslog(LOG_WARNING,'Geocoder failed for "'.$street_address.'"');
throw new RuntimeException($err?"Geocoder failed: $err":'Cannot get address for your GPS location',500);
}
syslog(LOG_WARNING,json_encode($address));
}
// Step 1. Get activities
list($res,$err) = ActivityDirect::getActivity(
$db->getConnect(), $db->getConnectGPS(), $member_id,
$location["lat"], $location["lng"], ActivityDirect::ACTIVITY_RADIUS,
$time, ActivityDirect::TIME_DELTA, $country);
if (!$res || count($res)<1) {
if ($err!="") {
throw new RuntimeException($err, 500);
}
throw new RuntimeException('No recent activity found', 404);
}
syslog(LOG_WARNING,'Recent activity has '.count($res).' trips!');
//ActivityDirectApi::debugTrips($res,'0');
$destination_cache = [];
$result = [];
$i = 0;
foreach ($res as $trip) {
//ActivityDirectApi::debugTrips([$trip],'1');
$trip['mode'] = 'rideshare';
$trip['steps'] = [];
$advice = [
"trip"=>$trip,
"scooter"=>[],
"multimodal"=>[],
"rideshare"=>$trip,
"faster"=>NULL,
"cheaper"=>NULL
];
//ActivityDirectApi::debugTrips([$advice],'2');
if ($advice && count($advice)>0) {
$result[] = $advice;
$i++;
}
if ($i>=ActivityDirect::DEFAULT_OPTIONS) {
break;
}
}
//syslog(LOG_WARNING,json_encode($result));
//ActivityDirectApi::debugTrips($result,'3');
//$result = self::processQuotesTrips($db, $member_id, $country, $result);
//ActivityDirectApi::debugTrips($result,'4');
//$result = self::processGojekOption($db, $member_id, $country, $result);
//ActivityDirectApi::debugTrips($result,'5');
//syslog(LOG_WARNING,json_encode($result));
return $this->response(
array(
"trips" => $result,
"street_address" => html_entity_decode($street_address)
), 200);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$code = $e->getCode();
syslog(LOG_WARNING,$message);
}
return $this->response(
array(
"error" => $message
), $code);
}
public static function debugTrips($trips,$what='0') {
return; /*
foreach ($trips as $trip) {
$leg_fare = $trip['multimodal']['options']['leg_fare'];
syslog(LOG_WARNING,'>>>>>>> '.$what.' >>>>>>> '.json_encode($leg_fare));
}*/
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
protected function checkCache($trip, $cache) {
syslog(LOG_WARNING,'ActivityDirectApi::checkCache()');
foreach ($cache as $item) {
if ($item["id"]>0 && $item["id"]==$trip["id"]) {
return [false, $cache];
}
if ($item["location_end_id"]==$trip["location_end_id"]) {
return [false, $cache];
}
}
$cache[] = $trip;
return [true, $cache];
}
public static function processGojekOption($db, $member_id, $country, $trips) {
syslog(LOG_WARNING,"ActivityDirectApi::processGojekOption(\$db, $member_id, $country, \$trips)");
//"trips"=>$trips;
if (!is_array($trips) && count($trips)<1) {
syslog(LOG_WARNING,"Invalid trips!");
return $trips; // No idea how to handle it
}
$trip = $trips[0];
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'];
$newTrip = [
'cheaper' => [],
'faster' => [],
'multimodal' => [],
'rideshare' => [],
'scooter' => [],
'trip' => $trip['trip']
];
$origin = Address::getAddressById($db->getConnect(), $oldTrip['location_start_id']);
$destination = Address::getAddressById($db->getConnect(), $oldTrip['location_end_id']);
list($res, $err) = GeocodeApi::route($db,
$origin['latitude'], $origin['longitude'],
$destination['latitude'], $destination['longitude'], 'driving'
);
if ($err!=NULL || !is_array($res) || !isset($res["routes"]) || count($res["routes"])<1) {
syslog(LOG_WARNING,'No available routes!');
syslog(LOG_WARNING,$err);
return $trips; // We cannot route driving!
}
$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'];
}
$route_overlay[] = $r;
}
$oldTrip["duration"] = $travel_time==PHP_INT_MAX ? NULL : $travel_time;
$oldTrip["distance"] = $travel_distance==PHP_INT_MAX ? NULL : $travel_distance;
/*
$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 = $res["routes"][0];
$route_leg = $route["legs"][0];
$route_leg_steps = $route_leg["steps"];
$overview_polyline = $route["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 Gojek 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' => MultiModal::processPolyline($polyline),
'quote_group_id' => null,
'sid' => $j++,
'travel_mode' => "GOJEK"
];
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"]
];
}
$rideshare = [
'cost' => 1,
'cost_raw' => 1,
'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' => null,
'travel_date' => $oldTrip['travel_date'],
'travel_date_end' => $oldTrip['travel_date_end'],
'updated' => $oldTrip['updated']
];
$leg_id = rand(1,10000);
$leg_fare = 1;
// TODO!
$leg = [
'id' => $leg_id,
'arrival_time' => "1569311499",
'arrival_time_zone' => $destination['timezone'],
'arrival_timezone' => "Asia/Singapore",
'departure_time' => "1569310392",
'departure_time_zone' => $origin['timezone'],
'departure_timezone' => "Asia/Singapore",
'distance' => $oldTrip['distance'],
'duration' => $oldTrip['duration'],
'fare_raw' => 1,
'polyline' => $overview_polyline,
'quote' => 1,
'steps' => count($leg_steps),
];
$options = [
'gid' => $leg_id, /* ??? */
'leg_fare' => [$leg_id => $leg_fare],
'leg_steps' => [$leg_id => $leg_steps],
'legs' => [$leg],
'routes' => 1
];
$rideshare['options'] = $options;
$newTrip['rideshare'] = $rideshare;
if (count($trips)>2) {
array_shift($trips); // Remove the first element
}
$trips[] = $newTrip;
return $trips;
}
}
+358
View File
@@ -0,0 +1,358 @@
<?php
class Address
{
public function getAddressById($db, $id)
{
syslog(LOG_WARNING, "Address::getAddressById(\$db, $id)");
$result = array();
$q = "SELECT a.*,b.timezone AS \"timeZoneId\" FROM address a LEFT JOIN address_timezone b ON (b.id=a.timezone) ";
$q .= " WHERE a.id=" . ((int) $id);
syslog(LOG_WARNING, $q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result = $f;
}
return $result;
}
public function getAddress($db, $data, $prefix = "location_start")
{
syslog(LOG_WARNING, "Address::getAddress(\$db, \$data, $prefix)");
$db_address = pg_escape_string(trim(html_entity_decode($data[$prefix],ENT_QUOTES|ENT_HTML5,"UTF-8")));
error_log(json_encode($data));
$db_lat = (float) $data["${prefix}_lat"];
$db_lng = (float) $data["${prefix}_lng"];
$q = "SELECT id FROM address WHERE address='${db_address}' AND latitude=${db_lat} AND longitude=${db_lng}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f["id"];
}
$q = "SELECT id FROM address WHERE address='${db_address}'";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f["id"];
}
$db_timezone = (int) $data["${prefix}_tz"];
if ($db_timezone == 0) {
$db_timezone = 1;
}
// Asia/Singapore
$db_postal = pg_escape_string($data["${prefix}_postal"]);
if ($db_postal == "") {
if (($pos = strrpos($db_address, " ")) !== false) {
$db_postal = trim(substr($db_address, $pos));
}
}
$db_country = ""; // SG?
if (array_key_exists("${prefix}_country", $data)) {
$db_country = pg_escape_string($data["${prefix}_country"]);
}
$q = "INSERT INTO address (address,latitude,longitude,timezone,geocoding_date,postal,country,geometry) VALUES(";
$q .= "'${db_address}',${db_lat},${db_lng},${db_timezone},'" . date("Y-m-d") . "','${db_postal}','${db_country}',ST_SetSRID(ST_MakePoint(${db_lng},${db_lat}), 4326)";
$q .= ") RETURNING id";
syslog(LOG_WARNING, $q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f["id"];
}
return null;
}
public function save($db, $result)
{
// Check the address exists
$db_address = pg_escape_string(html_entity_decode($result["address"],ENT_QUOTES|ENT_HTML5,"UTF-8"));
$db_address_input = isset($result["address_input"])?pg_escape_string(html_entity_decode($result["address_input"],ENT_QUOTES|ENT_HTML5,"UTF-8")):"";
$q = "SELECT id, address, latitude as lat, longitude as lng, postal, country, timezone, plus_code FROM address WHERE lower(address)=lower('${db_address}') ";
$q .= "AND latitude<>0 AND longitude<>0 AND geocoding_date IS NOT NULL AND postal IS NOT NULL ";
$q .= "ORDER BY geocoding_date DESC LIMIT 1";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f;
}
syslog(LOG_WARNING, 'Address::save($db, $result)');
$db_lat = floatval($result["lat"]);
$db_lng = floatval($result["lng"]);
$db_postal = pg_escape_string($result["postal"]);
$db_plus_code = pg_escape_string($result["plus_code"]);
$db_city = pg_escape_string($result["city"]);
$db_country = pg_escape_string($result["country"]);
$db_tz = Geocode::getTimezone($db, $result["timeZoneId"]);
$db_city_lat = floatval($result["city_lat"]);
$db_city_lng = floatval($result["city_lng"]);
$city_id = Address::getCity($db, $db_city, $db_country,$db_city_lat, $db_city_lng);
if (empty($city_id)) {
$city_id = Address::insertCity($db, $db_city, $db_country, $db_city_lat, $db_city_lng);
}
if (empty($db_postal)) {
$db_postal = Address::getClosestPostalCode($db_lat, $db_lng, $db_country);
}
$q = "INSERT INTO address (address,latitude,longitude,timezone,geocoding_date,postal,country,geometry, city_id, plus_code) VALUES (";
$q .= "'${db_address}',${db_lat},${db_lng}," . ($db_tz == null ? "NULL" : $db_tz) . ",now(),'${db_postal}','${db_country}',ST_SetSRID(ST_MakePoint(${db_lng},${db_lat}), 4326),'${city_id}','${db_plus_code}')";
$q .= " RETURNING id, address, latitude as lat, longitude as lng, postal, country, timezone, city_id, plus_code";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
if(!empty($db_address_input) && $db_plus_code!=$db_address_input){
Address::saveAliasAddress($db, $f['id'], $db_address_input);
}
return $f;
}
return null;
}
public function saveAliasAddress($db, $address_id, $address, $source='Google'){
$q = "SELECT id, address_id, name FROM address_alias WHERE lower(name)=lower('${address}') ";
$q .= "ORDER BY geocoding_date DESC LIMIT 1";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f;
}
$q = "INSERT INTO address_alias (address_id,name,geocoding_date,source) VALUES (";
$q .= "${address_id},'${address}', now() ,'${source}')";
$q .= " RETURNING address_id, name";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f;
}
return null;
}
public function update($db, $condition, $data) {
syslog(LOG_WARNING,'Address::update(): '.json_encode($data));
$q = "select column_name, data_type, character_maximum_length from INFORMATION_SCHEMA.COLUMNS where table_name = 'address'";
$r = pg_query($db, $q);
while ($f = pg_fetch_assoc($r)) {
$columns[$f['column_name']] = $f['data_type'];
}
if(count($data)<1) return null;
$q = "UPDATE address SET id=id ";
foreach ($data as $key => $val) {
if ($key == "id") {
continue;
}
if (array_key_exists($key, $data) && !in_array($columns[$key], ['character varying', 'date', 'timestamp with time zone', 'timestamp without time zone'])) {
$q .= ", ${key} = " . ($val == "" ? "NULL" : $val);
} else {
$q .= ", ${key} = '" . pg_escape_string($val) . "'";
}
}
$q .= " WHERE " . $condition . "";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return $f;
}
return array(NULL, pg_last_error($db));
}
public function searchInView($db, $country, $address) {
$db_country = pg_escape_string($country); // is not used for now...
$db_address = pg_escape_string(html_entity_decode($address,ENT_QUOTES|ENT_HTML5,"UTF-8"));
$words = preg_split("/[\s,]+/", $db_address);
$tsquery = implode(':* & ', $words). ':* & '.$db_country;
$q = "SELECT * FROM address_view a
WHERE address_search @@ to_tsquery('simple', '${tsquery}')
ORDER BY ts_rank(address_search,to_tsquery('simple', '${tsquery}')) desc
LIMIT 10";
$r = pg_query($db, $q);
$results = [];
if ($r && pg_num_rows($r)) {
while ($f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$results[] = $f;
}
if (count($results) > 9) {
return $results;
}
}
return $results;
}
public function search($db, $country, $address)
{
//syslog(LOG_WARNING,"Address::search(\$db, $country, $address)");
$db_country = pg_escape_string($country); // is not used for now...
$db_address = pg_escape_string(html_entity_decode($address,ENT_QUOTES|ENT_HTML5,"UTF-8"));
$q = "SELECT a.*, b.timezone AS \"timeZoneId\" FROM address a LEFT JOIN address_timezone b ON (b.id=a.timezone) ";
$q .= " WHERE UPPER(a.address) ILIKE ('%${db_address}%') AND a.country='${db_country}' LIMIT 10";
//error_log($q);
$r = pg_query($db, $q);
$results = [];
if ($r && pg_num_rows($r)) {
while ($f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$results[] = $f;
}
if (count($results) > 9) {
return $results;
}
}
if ($country == 'SG') {
$q = "SELECT id as singapore_buildings_id, address, latitude, longitude, 1 AS timezone, NOW() as geocoding_date, postal, 'SG' AS country, building AS description, 'Asia/Singapore' AS \"timeZoneId\" ";
$q .= " FROM singapore_buildings WHERE UPPER(address) ILIKE ('%${db_address}%') LIMIT 10";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
while ($f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$results[] = $f;
}
return $results;
}
}
/*
savvy=> \d singapore_buildings
Table "public.singapore_buildings"
Column | Type | Modifiers
-----------+------------------------+------------------------------------------------------------------
id | integer | not null default nextval('singapore_buildings_id_seq'::regclass)
address | character varying(150) | not null
blk_no | character varying(10) |
building | character varying(100) |
latitude | numeric |
longitude | numeric |
postal | character varying(6) | not null
road_name | character varying(40) | not null
searchval | character varying(100) | not null
x | numeric |
y | numeric |
Indexes:
"singapore_buildings_pkey" PRIMARY KEY, btree (id)
savvy=> \d address
Table "public.address"
Column | Type | Modifiers
----------------+------------------------+------------------------------------------------------
id | bigint | not null default nextval('address_id_seq'::regclass)
address | character varying(200) |
latitude | numeric |
longitude | numeric |
timezone | integer |
geocoding_date | date |
postal | character varying(40) |
country | character varying(2) | default 'SG'::character varying
geometry | geography(Point,4326) |
description | character varying(100) |
*/
return $results;
}
public function recentTrips($db, $member_id, $country, $address)
{
$db_country = pg_escape_string($country); // is not used for now...
$db_address = pg_escape_string(html_entity_decode($address,ENT_QUOTES|ENT_HTML5,"UTF-8"));
$q = "SELECT a.location_start_id, a.location_end_id ";
$q .= " FROM trackedemail_item b, parsedemail_item a ";
$q .= " LEFT JOIN address c ON (c.id=a.location_start_id) LEFT JOIN address d ON (d.id=a.location_end_id) ";
$q .= " WHERE b.id=a.trackedemail_item_id AND b.member_id=" . ((int) $member_id);
$q .= " AND (c.country='${db_country}' OR d.country='${db_country}')";
$q .= " ORDER BY a.travel_date DESC LIMIT 10";
//error_log($q);
$r = pg_query($db, $q);
$results = [];
$address = [];
if ($r && pg_num_rows($r)) {
while ($f = pg_fetch_assoc($r)) {
if (!array_key_exists($f["location_start_id"], $address)) {
$results[] = self::getAddressById($db, $f["location_start_id"]);
$address[$f["location_start_id"]] = 1;
}
if (!array_key_exists($f["location_end_id"], $address)) {
$results[] = self::getAddressById($db, $f["location_end_id"]);
$address[$f["location_end_id"]] = 1;
}
}
return $results;
}
return null;
}
public function favourites($db, $member_id, $country, $address)
{
// Not implemented
return null;
}
public static function getTzById($db, $id)
{
syslog(LOG_WARNING, "Address::getTzById(\$db, $id)");
$q = "SELECT timezone FROM address_timezone WHERE id=" . ((int) $id);
//syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
return $f[0];
}
return 'America/Los_Angeles'; // 2
}
public static function getTzByAddressId($db, $id)
{
syslog(LOG_WARNING, "Address::getTzByAddressId(\$db, $id)");
$q = "SELECT timezone FROM address_timezone WHERE id=(SELECT timezone FROM address WHERE id=" . ((int) $id) . ")";
//syslog(LOG_WARNING,"ActivityApi: ".$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
//syslog(LOG_WARNING,"ActivityApi: ".$f[0]);
return $f[0];
} else {
//syslog(LOG_WARNING,"ActivityApi: ".pg_last_error());
}
return 'America/Los_Angeles'; // 2
}
public static function getCity($db, $city, $country_code, $latitude='', $longitude='')
{
$q = "SELECT id FROM geofence_area_city WHERE (LOWER(city) ILIKE '" . strtolower($city) . "' AND country='" . $country_code . "') OR (latitude = '${latitude}' AND longitude ='${longitude}') LIMIT 1";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f['id'];
} else {
return null;
}
}
public static function insertCity($db, $city, $country_code, $latitude, $longitude)
{
$i = "INSERT INTO geofence_area_city (city,country,latitude,longitude,location) VALUES ('${city}','${country_code}',${latitude},${longitude},ST_SetSRID(ST_MakePoint(${longitude},${latitude}), 4326)) RETURNING id";
$city_id=null;
$r = pg_query($db, $i);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
$city_id = $f[0];
}
return $city_id;
}
public function getClosestPostalCode($latitude, $longitude, $country, $distance = 50000)
{
$q = "SELECT postal,
ST_DistanceSphere(geometry::geometry, ST_SetSRID(ST_MakePoint(" . $longitude . ", " . $latitude . "),4326)) AS distance
FROM geoname_postal_code
WHERE ST_DistanceSphere(geometry::geometry, ST_SetSRID(ST_MakePoint(" . $longitude . ", " . $latitude . "),4326)) BETWEEN 0 AND " . $distance . " AND country='" . $country . "'
ORDER BY distance
LIMIT 1;";
$r = pg_query($q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return $f['postal'];
}
return null;
}
}
// vi:ts=2
+199
View File
@@ -0,0 +1,199 @@
<?php
class AddressApi extends Api
{
public $apiName = 'address';
const SEARCH_RADIUS = 100000; // 100km
const SEARCH_RESULTS = 3;
public function indexAction()
{
//syslog(LOG_WARNING,'AddressApi::indexAction()');
$country = trim($this->requestParams["country"] ?? "");
$address = trim($this->requestParams["address"] ?? "");
$location = $this->requestParams['location'] ?? [];
$member_id = $this->requestParams["member_id"] ?? 0;
if ($country!='US') $country = 'SG';
//error_log('country='.$country.', member_id='.$member_id);
if (strlen($address)>2) {
$db = new Db();
// DEBUG
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
$results = Address::search($db->getConnect(), $country, $address);
//error_log('results='.count($results).", strlen=".strlen($address));
if ((!is_array($results) || count($results)<1) && strlen($address)>7) {
$results = AddressApi::searchGooglePlaces($db, $address, $country, $location, $member_id);
}
if(is_array($results) && count($results)>0){
return $this->response(
array(
'country' => $country,
'address' => $address,
'count' => count($results),
'addresses' => $results
), 200);
}
}
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/address/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /address/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
$db = new Db();
$address = Address::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()
{
$country = trim($this->requestParams["country"] ?? "");
$address = trim($this->requestParams["address"] ?? "");
$member_id = (int)($this->requestParams["member_id"] ?? 0);
$location = $this->requestParams['location'] ?? [];
$autocomplete = (int)($this->requestParams["autocomplete"] ?? 0);
if ($country!='US') $country = 'SG';
if (strlen($address)>2) {
$db = new Db();
$benchmarks['function'] = 'createaddress';
Utilities::startBenchmark($benchmarks);
// DEBUG
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
$recentTrips = [];
$favourites = [];
if ($autocomplete) {
$results = Address::searchInView($db->getConnect(), $country, $address);
} else {
if ($member_id>0) {
$recentTrips = Address::recentTrips($db->getConnect(), $member_id, $country, $address);
$favourites = Address::favourites($db->getConnect(), $member_id, $country, $address);
}
$results = Address::search($db->getConnect(), $country, $address);
}
// if ((!is_array($results) || count($results)<1) && strlen($address)>7) {
if ((!is_array($results) || count($results)<3) && strlen($address)>7) {
if (!is_array($results)) $results = [];
$results = array_merge(
$results,
AddressApi::searchGooglePlaces($db, $address, $country, $location, $member_id)
);
}
Utilities::finishBenchmark($benchmarks);
if ((is_array($results) && count($results)>0)
|| (is_array($recentTrips) && count($recentTrips)>0)
|| (is_array($favourites) && count($favourites)>0)) {
return $this->response(
array(
'country' => $country,
'address' => $address,
'count' => is_array($results) ? count($results) : 0,
'addresses' => $results,
'recent' => $recentTrips,
'favourites' => $favourites,
'location' => $location
), 200);
}
}
return $this->response(
array(
"error" => "Data not found"
), 404);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public function searchGooglePlaces($db, $address, $country, $location, $member_id) {
syslog(LOG_WARNING,"AddressApi::searchGooglePlaces(\$db, $address, $country, \$location, $member_id)");
global $savvyext;
$result = [];
$has_location = true;
if (!is_array($location) || count($location)!=2
|| !array_key_exists('lat',$location) || !array_key_exists('lng',$location)
|| ($location['lat']==0 && $location['lng']==0)) {
$has_location = false;
}
syslog(LOG_WARNING,"has_location=".($has_location?"TRUE":"FALSE"));
require_once('../common/GooglePlaces.php');
$key = $savvyext->cfgReadChar('google.api_key');
$places = new GooglePlaces($key);
if ($has_location) {
syslog(LOG_WARNING,"location => (".$location['lat'].",".$location['lng'].") => ".AddressApi::SEARCH_RADIUS);
$places->location = [$location['lat'], $location['lng']]; //array(33.9212128,-84.4787968);
$places->radius = AddressApi::SEARCH_RADIUS; // 2600 Bentley
} else {
syslog(LOG_WARNING,"No location!");
}
$places->types = 'geocode'; // 'address';
$places->input = $address;
$benchmarks = [];
$benchmarks['function'] = 'google places';
Utilities::startBenchmark($benchmarks);
$results = $places->autoComplete();
Utilities::finishBenchmark($benchmarks);
syslog(LOG_WARNING,json_encode($results));
if (is_array($results) && array_key_exists('predictions',$results) && count($results['predictions'])>0) {
$predictions = [];
foreach ($results['predictions'] as $item) {
if (in_array("street_address",$item["types"]) || in_array("premise",$item["types"])) {
$predictions[] = $item["description"]; // 2600 Bentley Road Southeast, Marietta, GA, USA
$result[]['address'] = $item["description"];
if (count($result)>=AddressApi::SEARCH_RESULTS) break;
}
}
/*$i = 0;
foreach ($predictions as $address) {
list($res, $err) = GeocodeApi::geocode($db, $address, $country);
if (is_array($res) && array_key_exists('id',$res) && $res['id']>0) {
$result[] = $res;
$i++;
} else {
syslog(LOG_WARNING,$err!=""?$err:'Geocoding error!');
}
if ($i>=AddressApi::SEARCH_RESULTS) break;
}*/
}
return $result;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
class ByCategory {
const CATEGORIES = [
'carshare' => 1, /* no */
'scooterrental' => 1, /* n/a */
'publictransport' => 1, /* n/a */
'taxi' => 1, /* yes */
'parking' => 1, /* n/a */
'tollsfees' => 1, /* n/a */
'rideshare' => 1, /* yes */
'gas' => 1, /* n/a */
'carrental' => 1 /* n/a */
];
public function andMemberId($db, $category, $member_id) {
$db_category = pg_escape_string(strtolower($category));
$q = "SELECT a.*,
b.address AS location_start, b.latitude AS location_start_lat, b.longitude AS location_start_lng,
c.address AS location_end, c.latitude AS location_end_lat, c.longitude AS location_end_lng,
a.id AS parsedemail_item_id
FROM trackedemail_item t, parsedemail_item a
LEFT JOIN address b ON (b.id=a.location_start_id)
LEFT JOIN address c ON (c.id=a.location_end_id)
LEFT JOIN transport_providers d ON (d.id=a.transport_provider_id)
WHERE t.id=a.trackedemail_item_id AND lower(d.category) = '${db_category}' AND t.member_id=".((int)$member_id)."
AND a.dup_id IS NULL ORDER BY a.travel_date_end DESC";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public function andMemberIdForDaysTotal($db, $category, $member_id, $days) {
$result = [
"category"=>$category,
"member_id"=>$member_id,
"days"=>$days,
"count"=>0,
"amount"=>0,
"errors"=>[]
];
$db_category = pg_escape_string(strtolower($category));
$q = "SELECT count(*) AS count, sum(b.cost) AS amount
FROM trackedemail_item a, parsedemail_item b
LEFT JOIN transport_providers c ON (c.id=b.transport_provider_id)
WHERE a.id=b.trackedemail_item_id AND lower(c.category) = '${db_category}' AND a.member_id=".((int)$member_id)."
AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${days} days')";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$result["count"] += $f["count"];
$result["amount"] += $f["amount"];
} else {
$result["errors"]["receipts"] = pg_last_error($db);
}
$q = "SELECT count(*) AS count, sum(amount)/100 AS amount FROM members_bankimport ";
$q.= " WHERE member_id=${member_id} AND time > (now() - interval '${days} days') ";
$q.= " AND lower(category) = '${db_category}'";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$result["count"] += $f["count"];
$result["amount"] += $f["amount"];
} else {
$result["errors"]["bank"] = pg_last_error($db);
}
return $result;
}
public function averageForDaysTotal($db, $category, $member_id, $days) {
$result = [
"category"=>$category,
"member_id"=>$member_id,
"days"=>$days,
"count"=>0,
"amount"=>0,
"errors"=>[]
];
$db_category = pg_escape_string(strtolower($category));
$q = "SELECT count(*) AS count, sum(d.amount) AS amount FROM
(SELECT a.member_id, count(*) AS count, sum(b.cost) AS amount
FROM trackedemail_item a, parsedemail_item b
LEFT JOIN transport_providers c ON (c.id=b.transport_provider_id)
WHERE a.id=b.trackedemail_item_id AND lower(c.category) = '${db_category}' AND a.member_id<>".((int)$member_id)."
AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${days} days')
GROUP BY a.member_id) AS d";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$result["count"] += $f["count"];
$result["amount"] += $f["amount"]/$f["count"];
} else {
$result["errors"]["receipts"] = pg_last_error($db);
}
$q = "SELECT count(*) AS count, sum(a.amount) AS amount FROM ";
$q.= " (SELECT member_id, count(*) AS count, sum(amount)/100 AS amount FROM members_bankimport ";
$q.= " WHERE member_id<>${member_id} AND time > (now() - interval '${days} days') ";
$q.= " AND lower(category) = '${db_category}' GROUP BY member_id) AS a";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$result["count"] += $f["count"];
$result["amount"] += $f["amount"]/$f["count"];
} else {
$result["errors"]["bank"] = pg_last_error($db);
}
return $result;
}
}
// vi:ts=2
@@ -0,0 +1,97 @@
<?php
class ByCategoryApi extends Api
{
public $apiName = 'bycategory';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
$db = new Db();
$tripOptions = Trips::getOptionsById($db->getConnect(), (int)$id);
if(is_array($tripOptions) && count($tripOptions)>0){
return $this->response(
array(
'id' => $id,
'count' => count($tripOptions),
'options' => $tripOptions
), 200);
}
}
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
$message = 'Data not found';
$code = 404;
$category = strtolower($this->requestParams['category'] ?? '');
$member_id = $this->requestParams['member_id'] ?? 0;
try {
if ($member_id<1) {
throw new RuntimeException('Invalid member ID',500);
}
if ($category=='' || !array_key_exists($category,ByCategory::CATEGORIES)) {
throw new RuntimeException('Invalid category',500);
}
if (ByCategory::CATEGORIES[$category]!=1) {
throw new RuntimeException('Data not found',404);
}
$db = new Db();
list($res,$err) = ByCategory::andMemberId($db->getConnect(), $category, $member_id);
if (!$res) {
if ($err!="") {
throw new RuntimeException($err, 500);
}
throw new RuntimeException('Data not found', 404);
}
return $this->response(
array(
"trips" => $res
), 200);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$code = $e->getCode();
}
return $this->response(
array(
"error" => $message
), $code);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
class Compare {
public function youVsOthersOnCategory($db, $gpsdb, $data) {
error_log('Compare::youVsOthersOnCategory($db, $gpsdb, $data) ');
$db_country = pg_escape_string($data['country']); // is not used for now...
$db_address = pg_escape_string($data['address']);
$member_id = (int)$data['member_id'];
$category = $data['category']; // see ByCategory::CATEGORIES
$days = (int)$data['days'];
//$member_id = 3; // DEBUG
if ($member_id<1) {
error_log("Invalid member_id='${member_id}'");
return NULL;
}
if ($category=='' || !array_key_exists($category,ByCategory::CATEGORIES)
|| ByCategory::CATEGORIES[$category]!=1) {
error_log("Invalid category='${category}'");
return NULL;
}
if ($days<7) $daya = 7;
$you = ByCategory::andMemberIdForDaysTotal($db, $category, $member_id, $days);
$avg = ByCategory::averageForDaysTotal($db, $category, $member_id, $days);
return [
"member_id" => $member_id,
"category" => $category,
"days" => $days,
"you" => $you,
"average" => $avg,
"compare" => self::compareYouVsAverage($you,$avg)
];
}
public function compareYouVsAverage($you,$avg) {
/*
"category"=>$category,
"member_id"=>$member_id,
"days"=>$days,
"count"=>0,
"amount"=>0,
"errors"=>[]
*/
if ($you["amount"]>0 || $avg["amount"]>0) {
if ($you["amount"]==0) return 100;
if ($avg["amount"]==0) return -100;
if ($you["amount"]>$avg["amount"]) {
return sprintf("%0.02f",-(100*(1-$avg["amount"]/$you["amount"])));
}
return sprintf("%0.02f",100*(1-$you["amount"]/$avg["amount"]));
}
return 0;
}
}
// vi:ts=2
+85
View File
@@ -0,0 +1,85 @@
<?php
class CompareApi extends Api
{
public $apiName = 'compare';
const COMPARISONS = array(
"youVsOthersOnCategory" => 1
);
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/address/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /address/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
/*$db = new Db();
$address = Address::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()
{
$message = "Data not found";
$type = (string)$this->requestParams["type"] ?? "";
$data = $this->requestParams["data"] ?? array();
if ($type!='' && is_array($data) && self::COMPARISONS[$type]==1) {
$db = new Db();
$results = call_user_func("Compare::${type}",
$db->getConnect(), $db->getConnectGPS(), $data);
if($results!=NULL && is_array($results) && count($results)>0){
return $this->response(
array(
'type' => $type,
'data' => $data,
'comparison' => $results
), 200);
} else {
$message = "Comparison data is not found";
}
} else {
$message = "Invalid input";
}
return $this->response(
array(
"error" => $message
), 404);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
+326
View File
@@ -0,0 +1,326 @@
<?php
class Geocode
{
const GPS_BOX_DECIMAL = 10;
const COUNTRY_LIMITS = [
'SG' => null, /* no limit! */
'US' => [
[
'name' => 'San Francisco',
'latitude' => 37.7126152,
'longitude' => -122.1754642,
'radius' => 60000, /* in meters */
],
],
];
public function getAddressById($db, $id)
{
$result = array();
$q = "SELECT a.*,b.timezone AS \"timeZoneId\" FROM address a LEFT JOIN address_timezone b ON (b.id=a.timezone) ";
$q.= " WHERE a.id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result = $f;
}
return $result;
}
public function save($db, $result)
{
return Address::save($db, $result);
}
public function checkLatLngByAddress($db, $address, $country = GeocodeApi::DEFAULT_COUNTRY_CODE)
{
syslog(LOG_WARNING, "Geocode::checkLatLngByAddress(\$db,'${address}',$country)");
$db_address = pg_escape_string(strtolower($address));
$db_country = pg_escape_string($country);
$q = "SELECT a.address, a.latitude AS lat, a.longitude AS lng, b.timezone AS location_start_tz, a.postal, a.timezone, a.id, a.country, b.timezone AS \"timeZoneId\", plus_code ";
$q .= " FROM address a LEFT JOIN address_timezone b ON b.id=a.timezone LEFT JOIN address_alias c ON a.id=c.address_id ";
$q .= " WHERE (lower(a.address) = '${db_address}' OR lower(a.plus_code) = '${db_address}' OR lower(c.name) = '${db_address}') ";
$q .= " AND a.country='${db_country}' AND a.geocoding_date IS NOT NULL ORDER BY a.geocoding_date DESC LIMIT 1";
$r = pg_query($db, $q);
syslog(LOG_WARNING, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["timeZoneId"] = $f["location_start_tz"]; // Case is lost on pg_query()
unset($f["location_start_tz"]);
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
return [$f, null];
}
return [null, pg_last_error($db)];
}
public function getTimezone($db, $timezone)
{
if (is_int($timezone) && $timezone > 0) {
return $timezone;
}
$db_timezone = pg_escape_string($timezone);
$q = "SELECT id FROM address_timezone WHERE timezone='${db_timezone}'";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
return $f[0];
}
$q = "INSERT INTO address_timezone (timezone) VALUES('${db_timezone}') RETURNING id";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
return $f[0];
}
return null;
}
public function checkDistanceCache($db, $fromLat, $fromLng, $toLat, $toLng)
{
$db_fromLat = (float) $fromLat;
$db_fromLng = (float) $fromLng;
$db_toLat = (float) $toLat;
$db_toLng = (float) $toLng;
$q = "SELECT distance,duration FROM address_distance_cache WHERE ";
$q .= "start_lat=${db_fromLat} AND start_lng=${db_fromLng} AND end_lat=${db_toLat} AND end_lng=${db_toLng}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return array($f, null);
}
return array(null, pg_last_error($db));
}
public function saveDistanceCache($db, $fromLat, $fromLng, $toLat, $toLng, $data)
{
$db_fromLat = (float) $fromLat;
$db_fromLng = (float) $fromLng;
$db_toLat = (float) $toLat;
$db_toLng = (float) $toLng;
$distance = $data["distance"];
$duration = $data["duration"];
$q = "INSERT INTO address_distance_cache (start_lat,start_lng,end_lat,end_lng,distance,duration) VALUES (";
$q .= "${db_fromLat},${db_fromLng},${db_toLat},${db_toLng},${distance},${duration}) RETURNING id";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
return array($f, null);
}
return array(null, pg_last_error($db));
}
public function getTransportProvidersByCountry($db, $country)
{
$db_country = pg_escape_string($country);
$q = "SELECT a.id as app_id,a.transport_provider_id,b.name as transport_provider_name,a.country,a.ios_app_id,a.android_app_id,b.timeout,b.retries
FROM transport_provider_apps a, transport_providers b
WHERE a.country='${db_country}' AND b.id=a.transport_provider_id AND b.active=1";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f = pg_fetch_assoc($r)) {
$result[] = $f;
}
return array($result, null);
}
return array(null, pg_last_error($db));
}
public function reverseGeocodeCache($db, $address, $lat, $lng, $postal = null, $country = "SG", $timezone = null,$plus_code="")
{
syslog(LOG_WARNING, "Geocode::reverseGeocodeCache(\$db, '$address', $lat, $lng, $postal, $country, $timezone,$plus_code)");
$result = [
"lat" => $lat,
"lng" => $lng,
"address" => $address,
"postal" => $postal,
"country" => $country,
"timeZoneId" => $timezone,
"plus_code" => $plus_code,
];
$address = Address::save($db, $result);
if (!empty($address)) {
syslog(LOG_WARNING, 'Address cached!');
} else {
syslog(LOG_WARNING, 'Failed to cache address!');
}
}
public function reverseGeocode($db, $lat, $lng)
{
syslog(LOG_WARNING, "Geocode::reverseGeocode(\$db, $lat, $lng)");
$db_lat = floatval($lat);
$db_lng = floatval($lng);
// Check our local address table with precisions 5 to 3
for ($i = 5; $i > 2; $i--) {
$q = "SELECT * FROM address WHERE round(latitude,${i})=round(${db_lat},${i}) AND round(longitude,${i})=round(${db_lng},${i}) ";
$q .= " AND latitude<>0 AND longitude<>0 ORDER BY geocoding_date DESC LIMIT 1";
syslog(LOG_WARNING, $q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
return [$f, null];
}
}
// Check our local Singapore table with precisions 5 to 3
for ($i = 5; $i > 2; $i--) {
$q = "SELECT * FROM singapore_buildings WHERE round(latitude,${i})=round(${db_lat},${i}) AND round(longitude,${i})=round(${db_lng},${i})";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$tz = 1; // Asia/Singapore - since this is "singapore_buildings" table
self::reverseGeocodeCache($db, $f['address'], $db_lat, $db_lng, $f['postal'], 'SG', $tz);
return [$f, null];
}
}
return [null, pg_last_error($db)];
}
public function reverseGeocodeGps($db, $gpsdb, $lat, $lng, $radius = 100)
{
$db_lat = floatval($lat);
$db_lng = floatval($lng);
// Check our gps address table with radius
$r = pg_query_params($gpsdb, self::SELECT_ADDRESS_IN_RADIUS, [
$db_lng,
$db_lat,
$radius,
]);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
return [$f, null];
}
// Check our gps Singapore table with radius
$r = pg_query_params($gpsdb, self::SELECT_SINGAPORE_BUILDINGS_IN_RADIUS, [
$db_lng,
$db_lat,
$radius,
]);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$q = "SELECT * FROM singapore_buildings WHERE id=" . $f["id"];
$r = pg_query($db, $q);
$d = pg_fetch_assoc($r);
$tz = 1; // Asia/Singapore - since this is "singapore_buildings" table
self::reverseGeocodeCache($db, $f['address'], $db_lat, $db_lng, $d['postal'], 'SG', $tz);
return [$f, null];
}
return [null, pg_last_error($gpsdb)];
}
public function mockGPSLocation($db, $member_id, $lat, $lng, $location = 'default')
{
syslog(LOG_WARNING, "Geocode::mockGPSLocation(\$db, $member_id, $lat, $lng, $location)");
$db_member_id = (int) $member_id;
$db_location = pg_escape_string($location);
$q = "SELECT lat,lng FROM mock_gps_location WHERE member_id=${db_member_id} AND location='${db_location}'";
syslog(LOG_WARNING, $q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
$lat = $f[0];
$lng = $f[1];
} else if ($lat == 0 && $lng == 0) {
$lat = 1.2966426;
$lng = 103.7763939;
}
return [$lat, $lng];
}
public function mockGPSCountry($db, $member_id, $country, $location = 'default')
{
syslog(LOG_WARNING, "Geocode::mockGPSCountry(\$db, $member_id, $country, $location)");
$db_member_id = (int) $member_id;
$db_location = pg_escape_string($location);
$q = "SELECT country FROM mock_gps_location WHERE member_id=${db_member_id} AND location='${db_location}'";
syslog(LOG_WARNING, $q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
$country = $f[0];
} else if ($country == '') {
$country = 'SG';
}
return $country;
}
public function getCountry($db, $country)
{
$db_country = pg_escape_string(strtolower($country));
$q = "SELECT * FROM country WHERE lower(code)=lower('${db_country}') OR lower(country)=lower('${db_country}') OR lower(short_name)=lower('${db_country}')";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_array($r)) {
return $f;
}
return null;
}
public function getCountryByGPS($db, $location, $country, $defaultCountry) {
syslog(LOG_WARNING,"Geocode::getCountryByGPS(\$db, \$location, $country, $defaultCountry)");
if ($country!=null && strlen(trim($country))==2) {
return $country;
}
if (is_array($location) && array_key_exists('lat',$location) && array_key_exists('lng',$location)
&& $location['lat']!=0 && $location['lat']!=0) {
$fromLat = $location['lat'] - Geocode::GPS_BOX_DECIMAL;
$fromLng = $location['lng'] - Geocode::GPS_BOX_DECIMAL;
$toLat = $location['lat'] + Geocode::GPS_BOX_DECIMAL;
$toLng = $location['lng'] + Geocode::GPS_BOX_DECIMAL;
// First we try address
$baseQuery = "SELECT country FROM %s WHERE ";
$baseQuery.= "(latitude BETWEEN ${fromLat} AND ${toLat}) AND (longitude BETWEEN ${fromLng} AND ${toLng})";
$baseQuery.= " ORDER BY ST_DistanceSphere(%s::geometry, ST_SetSRID(ST_MakePoint(".$location["lng"].",".$location["lat"]."),4326))";
$q = sprintf($baseQuery, "address", "geometry");
$q.= " LIMIT 1";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$country = $f[0];
} else {
// Then we try geoname
$q = sprintf($baseQuery, "geoname", "location");
$q.= " LIMIT 1";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$country = $f[0];
}
}
} else {
syslog(LOG_WARNING,"No GPS coordinates supplied!");
}
if ($country==null || strlen(trim($country))!=2) {
return $defaultCountry;
}
return $country;
}
public static function saveGeocodingLog($db, $address, $latitude, $longitude, $address_output,$duration, $client_ip, $client_id)
{
$i = "INSERT INTO geocoding_logs (address_input,latitude,longitude,call_dated,address_output, duration, client_ip,client_id) VALUES ('${address}'," . (!empty($latitude)?$latitude: "NULL") . "," . (!empty($longitude)?$longitude: "NULL") . ",NOW(),'${address_output}',${duration},'${client_ip}','${client_id}') RETURNING id";
$r = pg_query($db, $i);
syslog(LOG_WARNING,$i);
if ($r && pg_num_rows($r) && $f = pg_fetch_row($r)) {
$log_id = $f[0];
}
return $log_id;
}
const SELECT_ADDRESS_IN_RADIUS = "SELECT id,
address,
latitude,
longitude,
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1, $2),4326)) AS distance
FROM address
WHERE
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1, $2),4326)) BETWEEN 0 AND $3
ORDER BY distance";
const SELECT_SINGAPORE_BUILDINGS_IN_RADIUS = "SELECT id,
address,
latitude,
longitude,
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1, $2),4326)) AS distance
FROM singapore_buildings
WHERE
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1, $2),4326)) BETWEEN 0 AND $3
ORDER BY distance";
}
// vi:ts=2
+736
View File
@@ -0,0 +1,736 @@
<?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);
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
class Geofence {
public static function getAnchor($db, $lat, $lng) {
error_log("Geofence::getAnchor(\$db, $lat, $lng)");
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
// Step 0: Find out city
$city_id = 2; // 1096; // TODO!!!!!!!!!!!
// Step 1: Find out area
$q = "SELECT * FROM geofence_area WHERE city_id=${city_id}";
$r = pg_query($db, $q);
if (!$r || pg_num_rows($r)<1) {
// TODO: load closest city address
return [NULL, "This city has no areas available"];
}
$area = NULL;
while ($f=pg_fetch_assoc($r)) {
// TODO: Remember area coordinates
// TODO: Calculate distance to area coordinates
if ($f["type"]=='polygon') {
$data = json_decode($f["boundaries"],true);
if (is_array($data) && array_key_exists('polygon',$data) && is_array($data["polygon"]) && count($data["polygon"])>0) {
$polygon = $data["polygon"];
$polygonCoords = [];
foreach ($polygon as $coord) {
$polygonCoords[] = "$coord[0] $coord[1]";
}
$polygonCoordsString = implode(",", $polygonCoords);
$q = "SELECT ST_Contains(ST_GeomFromText('POLYGON((${polygonCoordsString}))', 4326), ST_SetSRID(ST_Point (" . $lng . ", " . $lat . "), 4326)) as iscorrect";
$i = pg_query($db, $q);
if ($i && pg_num_rows($i) && $j = pg_fetch_assoc($i)) {
if ($j["iscorrect"] == 't') {
$area = $f;
break;
} // Is not within the polygon
} // Polyfon data processing failed
} // Polygon data is missing
} // Not polygon
}
// Step 2: Find out anchor
if (is_array($area) && array_key_exists('id',$area) && $area["id"]>0) {
$q = "SELECT a.title,b.*,ST_DistanceSphere(b.geometry::geometry, ST_SetSRID(ST_MakePoint(" . $lng . ", " . $lat . "),4326)) AS distance ";
$q.= " FROM geofence_area_anchor a, address b WHERE a.geofence_area_id=" . $area["id"] . " AND b.id=a.address_id";
$r = pg_query($db, $q);
if (!$r || pg_num_rows($r)<1) {
// TODO: other?
return [NULL, "Geofence area has no anchors"];
}
$distance = PHP_INT_MAX;
$address = NULL;
while ($f=pg_fetch_assoc($r)) {
// TODOL We might need direction or another parameter
if ($distance>$f["distance"]) {
$distance = $f["distance"];
$address = $f;
}
}
if (is_array($address) && array_key_exists('address',$address) && $address["address"]!="") {
return [$address, NULL];
}
}
// No area, try GPS coordinates?
return [NULL, "Anchor address not found"];
}
public function youVsOthersOnCategory($db, $gpsdb, $data) {
error_log('Compare::youVsOthersOnCategory($db, $gpsdb, $data) ');
$db_country = pg_escape_string($data['country']); // is not used for now...
$db_address = pg_escape_string($data['address']);
$member_id = (int)$data['member_id'];
$category = $data['category']; // see ByCategory::CATEGORIES
$days = (int)$data['days'];
//$member_id = 3; // DEBUG
if ($member_id<1) {
error_log("Invalid member_id='${member_id}'");
return NULL;
}
if ($category=='' || !array_key_exists($category,ByCategory::CATEGORIES)
|| ByCategory::CATEGORIES[$category]!=1) {
error_log("Invalid category='${category}'");
return NULL;
}
if ($days<7) $daya = 7;
$you = ByCategory::andMemberIdForDaysTotal($db, $category, $member_id, $days);
$avg = ByCategory::averageForDaysTotal($db, $category, $member_id, $days);
return [
"member_id" => $member_id,
"category" => $category,
"days" => $days,
"you" => $you,
"average" => $avg,
"compare" => self::compareYouVsAverage($you,$avg)
];
}
}
// vi:ts=2
+76
View File
@@ -0,0 +1,76 @@
<?php
class GeofenceApi extends Api
{
public $apiName = 'geofence';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
$db = new Db();
$tripOptions = NULL; //Trips::getOptionsById($db->getConnect(), (int)$id);
if(is_array($tripOptions) && count($tripOptions)>0){
return $this->response(
array(
'id' => $id,
'count' => count($tripOptions),
'options' => $tripOptions
), 200);
}
}
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
return $this->response(
array(
"error" => "Saving error"
), 500);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public static function getAnchor($db, $lat, $lng)
{
if ($lat===NULL || $lng===NULL || ($lat===0 && $lng===0)) {
return [NULL, "Invalid GPS coordinates"];
}
return Geofence::getAnchor($db->getConnect(), $lat, $lng);
//return [NULL, "Not implemented"];
}
}
+554
View File
@@ -0,0 +1,554 @@
<?php
class MultiModal {
public function getLegById($db, $id) {
syslog(LOG_WARNING,"MultiModal::getLegById(\$db, $id)");
list($departure_time_zone,$arrival_time_zone) = self::getLegTimezones($db, $id);
$q = "SELECT a.*,b.timezone AS departure_timezone,c.timezone AS arrival_timezone FROM google_directions_legs a ";
$q.= " LEFT JOIN address_timezone b ON b.id=${departure_time_zone} ";
$q.= " LEFT JOIN address_timezone c ON c.id=${arrival_time_zone} ";
$q.= " WHERE a.id=".((int)$id);
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [$f, NULL];
}
return [NULL, pg_last_error($db)];
}
public function getAdviceLegs($db, $advice_id) {
syslog(LOG_WARNING,"MultiModal::getAdviceLegs(\$db, $advice_id)");
list($departure_time_zone,$arrival_time_zone) = self::getLegTimezones($db, $advice_id);
$q = "SELECT a.*,b.timezone AS departure_timezone,c.timezone AS arrival_timezone FROM google_directions_legs a ";
$q.= " LEFT JOIN address_timezone b ON b.id=${departure_time_zone} ";
$q.= " LEFT JOIN address_timezone c ON c.id=${arrival_time_zone} ";
$q.= " WHERE a.parsedemail_item_advice_google_id=".((int)$advice_id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[$f['id']] = $f;
}
return [$result, NULL];
}
return [NULL, pg_last_error($db)];
}
public function getLegSteps($db, $id) {
$result = NULL;
$q = "SELECT a.id AS sid, a.*,b.*,c.* FROM google_directions_leg_steps a ";
$q.= " LEFT JOIN google_directions_leg_step_details b ON (b.google_directions_leg_step_id=a.id) ";
$q.= " LEFT JOIN leg_step_quote c ON (c.google_directions_leg_step_id=a.id) ";
$q.= " WHERE a.google_directions_leg_id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
while ($f=pg_fetch_assoc($r)) {
if ($f["polyline"]=="") {
$f["polyline"] = self::getPolyline($db, $f);
}
$result[] = $f;
}
return [$result, NULL];
}
return [NULL, pg_last_error($db)];
}
public function getStepQuote($db, $id) {
$q = "SELECT * FROM leg_step_quote WHERE google_directions_leg_step_id=${id} ORDER BY id DESC LIMIT 1";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [$f, NULL];
}
return [NULL, pg_last_error($db)];
}
public function getLegAdviceGoogleById($db, $id) {
syslog(LOG_WARNING,"MultiModal::getLegAdviceGoogleById(\$db, $id)");
$q = "SELECT * FROM parsedemail_item_advice_google WHERE id=${id}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [$f, NULL];
}
return [NULL, pg_last_error($db)];
}
public function getLastGoogleAdviceByTrip($db, $id) {
$q = "SELECT *,EXTRACT(EPOCH FROM (NOW()-updated)) AS elapsed FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id} ORDER BY updated DESC LIMIT 1";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [$f, NULL];
}
return [NULL, pg_last_error($db)];
}
public function getTimezoneById($db, $id) {
$q = "SELECT timezone FROM address_timezone WHERE id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return $f;
}
return $id;
}
public function create($db, $trip) {
list ($id, $err) = self::saveParsedemailItemAdviceGoogle($db, [
'parsedemail_item_id' => $trip['id'],
'routes' => count($trip['options']['legs'])
]);
if ($id && $id>0) {
$trip['options']['gid'] = $id;
error_log('New parsedemail_item_advice_google.id='.$id);
} else {
return [NULL, 'Failed to create advice record'];
}
$timezones = [];
$n = count($trip['options']['legs']);
for ($i=0; $i<$n; $i++) {
$leg = $trip['options']['legs'][$i];
$leg_id = $leg['id'];
$leg_steps = $trip['options']['leg_steps'][$leg_id];
$leg_fare = $trip['options']['leg_fare'][$leg_id];
// Timezones
foreach(array("departure","arrival") as $tzid) {
$key1 = "${tzid}_time_zone";
$key2 = "${tzid}_timezone";
if (array_key_exists($leg[$key1],$timezones)) {
$leg[$key2] = $timezones[$leg[$key1]];
} else {
$tz = self::getTimezoneById($db, $leg[$key1]);
$timezones[$leg[$key1]] = $tz;
$leg[$key2] = $tz;
}
}
//error_log(json_encode($leg));
list ($id, $err) = self::saveGoogleDirectionsLeg($db, [
'parsedemail_item_advice_google_id' => $trip['options']['gid'],
'arrival_time' => $leg["arrival_time"],
'arrival_time_zone' => $leg["arrival_time_zone"],
'departure_time' => $leg["departure_time"],
'departure_time_zone' => $leg["departure_time_zone"],
'distance' => $leg["distance"],
'duration' => $leg["duration"],
'steps' => $leg["steps"],
'fare_raw' => $leg["fare_raw"],
'polyline' => self::processPolyline($leg["polyline"])
]);
if ($id && $id>0) {
$old_leg_id = $leg_id;
$leg_id = $id;
$leg['parsedemail_item_advice_google_id'] = $trip['options']['gid'];
// Legs
$leg['id'] = $id;
$trip['options']['legs'][$i] = $leg;
// Leg steps
unset($trip["options"]["leg_steps"][$old_leg_id]);
$trip['options']['leg_steps'][$leg_id] = $leg_steps;
// Leg fare
unset($trip['options']['leg_fare'][$old_leg_id]);
$trip['options']['leg_fare'][$leg_id] = $leg_fare;
// ...
error_log('New google_directions_legs.id='.$id);
} else {
self::deleteAdviceGoogle($db, $trip['options']['gid']);
return [NULL, 'Failed to create leg record: '.$err];
}
$m = count($leg_steps);
for ($j=0; $j<$m; $j++) {
$step = $leg_steps[$j];
$data = [
'google_directions_leg_id' => $leg_id,
'distance' => $step["distance"],
'duration' => $step["duration"],
'travel_mode' => $step["travel_mode"],
'location_start_lat' => $step["location_start_lat"],
'location_start_lng' => $step["location_start_lng"],
'location_end_lat' => $step["location_end_lat"],
'location_end_lng' => $step["location_end_lng"],
'html_instructions' => $step["html_instructions"],
'polyline' => self::processPolyline($step["polyline"])
];
list($id, $err) = self::saveGoogleDirectionsLegStep($db, $data);
if ($id && $id>0) {
$sid = $id;
$step['sid'] = $id;
$leg_steps[$j] = $step;
if ($data['polyline']=="") { // We do not have a polyline - try get one...
$data['id'] = $id;
$data['sid'] = $sid;
$polyline = self::getPolyline($db, $data);
}
error_log('New google_directions_leg_steps.id='.$sid);
} else {
self::deleteAdviceGoogle($db, $trip['options']['gid']);
return [NULL, 'Failed to create leg step record'];
}
if (count($step)>20) {
list($id, $err) = self::saveGoogleDirectionsLegStepDetail($db, [
'google_directions_leg_step_id' => $sid,
'num_stops' => $step["num_stops"],
'line' => $step["line"],
'vehicle' => $step["vehicle"],
'departure_stop' => $step["departure_stop"],
'departure_stop_lat' => $step["departure_stop_lat"],
'departure_stop_lng' => $step["departure_stop_lng"],
'arrival_stop' => $step["arrival_stop"],
'arrival_stop_lat' => $step["arrival_stop_lat"],
'arrival_stop_lng' => $step["arrival_stop_lng"],
'headsign' => $step["headsign"],
'headway' => $step["headway"]
]);
if ($id && $id>0) {
$step['id'] = $id;
$leg_steps[$j] = $step;
error_log('New google_directions_leg_step_details.id='.$id);
error_log(json_encode($step));
if (array_key_exists("fare_raw",$step)) {
self::saveLegStepQuote($db, [
'google_directions_leg_step_id' => $sid,
'name' => $step["line"],
'service' => $step["agency"],
'board' => $step["departure_stop"],
'alight' => $step["arrival_stop"],
'distance' => sprintf("%0.02f",$step["distance"]/1000),
'fare' => $step["fare"],
'fare_raw' => $step["fare_raw"],
'distance_raw' => $step["distance"]
]);
}
} else {
self::deleteAdviceGoogle($db, $trip['options']['gid']);
return [NULL, 'Failed to create leg step detail record'];
}
} else {
error_log('Step #'.$sid.' does not have details');
}
} // for
$trip['options']['leg_steps'][$leg_id] = $leg_steps;
} // for
return [$trip, NULL];
}
public function getLegStepQuotes($db, $id) {
$q = "SELECT * FROM leg_step_quote WHERE google_directions_leg_step_id ";
$q.= " IN (SELECT id FROM google_directions_leg_steps WHERE google_directions_leg_id=${id})";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$quotes = [];
while ($f=pg_fetch_assoc($r)) {
$quotes[] = $f;
}
return [$quotes, NULL];
}
return [NULL, pg_last_error($db)];
}
public function delete($db, $id) {
$result = 0;
$q = "DELETE FROM leg_step_quote WHERE google_directions_leg_step_id ";
$q.= " IN (SELECT id FROM google_directions_leg_steps WHERE google_directions_leg_id ";
$q.= " IN (SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id ";
$q.= " IN (SELECT id FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id})))";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
//error_log(pg_last_error());
$q = "DELETE FROM google_directions_leg_step_details WHERE google_directions_leg_step_id ";
$q.= " IN (SELECT id FROM google_directions_leg_steps WHERE google_directions_leg_id ";
$q.= " IN (SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id ";
$q.= " IN (SELECT id FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id})))";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
//error_log(pg_last_error());
$q = "DELETE FROM google_directions_leg_steps WHERE google_directions_leg_id ";
$q.= " IN (SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id ";
$q.= " IN (SELECT id FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id}))";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
//error_log(pg_last_error());
$q = "DELETE FROM google_directions_legs WHERE parsedemail_item_advice_google_id ";
$q.= " IN (SELECT id FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id})";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
//error_log(pg_last_error());
$q = "DELETE FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id}";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
//error_log(pg_last_error());
$q = "DELETE FROM parsedemail_item WHERE id=${id}";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
//error_log(pg_last_error());
return $result;
}
public function deleteAdviceGoogle($db, $id) {
$result = 0;
$q = "DELETE FROM leg_step_quote WHERE google_directions_leg_step_id ";
$q.= " IN (SELECT id FROM google_directions_leg_steps WHERE google_directions_leg_id ";
$q.= " IN (SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id ";
$q.= " IN (SELECT id FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id})))";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
$q = "DELETE FROM google_directions_leg_step_details WHERE google_directions_leg_step_id
IN (SELECT id FROM google_directions_leg_steps WHERE google_directions_leg_id
IN (SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id
IN (SELECT id FROM parsedemail_item_advice_google WHERE id=${id})))";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
$q = "DELETE FROM google_directions_leg_steps WHERE google_directions_leg_id
IN (SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id
IN (SELECT id FROM parsedemail_item_advice_google WHERE id=${id}))";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
$q = "DELETE FROM google_directions_legs WHERE parsedemail_item_advice_google_id
IN (SELECT id FROM parsedemail_item_advice_google WHERE id=${id})";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
$q = "DELETE FROM parsedemail_item_advice_google WHERE id=${id}";
$r = pg_query($db, $q);
if ($r && pg_affected_rows($r)) {
$result++;
}
return $result;
}
public function saveParsedemailItem($db, $data) {
$fields = array(
'travel_date' => false,
'duration' => false,
'cost_raw' => false,
'trackedemail_item_id' => false,
'cost' => false,
'updated' => false,
'distance' => false,
'transport_provider_id' => false,
'scheduled' => false,
'travel_date_end' => false,
'dup_id' => false,
'location_start_id' => true,
'location_end_id' => true,
'private' => false
);
return self::saveGeneric($db, $fields, $data, 'parsedemail_item');
}
public function saveParsedemailItemAdviceGoogle($db, $data) {
$fields = array(
'parsedemail_item_id' => true,
'routes' => true
);
return self::saveGeneric($db, $fields, $data, 'parsedemail_item_advice_google');
}
public function saveGoogleDirectionsLeg($db, $data) {
$fields = array(
'parsedemail_item_advice_google_id' => true,
'arrival_time' => false,
'arrival_time_zone' => false,
'departure_time' => false,
'departure_time_zone' => false,
'distance' => false,
'duration' => false,
'steps' => false,
'fare_raw' => false,
'polyline' => false
);
return self::saveGeneric($db, $fields, $data, 'google_directions_legs');
}
public function saveGoogleDirectionsLegStep($db, $data) {
$fields = array(
'google_directions_leg_id' => true,
'distance' => false,
'duration' => false,
'travel_mode' => false,
'location_start_lat' => false,
'location_start_lng' => false,
'location_end_lat' => false,
'location_end_lng' => false,
'html_instructions' => false,
'polyline' => false
);
return self::saveGeneric($db, $fields, $data, 'google_directions_leg_steps');
}
public function saveGoogleDirectionsLegStepDetail($db, $data) {
$fields = array(
'google_directions_leg_step_id' => true,
'num_stops' => false,
'line' => false,
'vehicle' => false,
'departure_stop' => false,
'departure_stop_lat' => false,
'departure_stop_lng' => false,
'arrival_stop' => false,
'arrival_stop_lat' => false,
'arrival_stop_lng' => false,
'headsign' => false,
'headway' => false
);
return self::saveGeneric($db, $fields, $data, 'google_directions_leg_step_details');
}
public function saveLegStepQuote($db, $data) {
error_log('public function saveLegStepQuote($db, $data)');
$fields = array(
'google_directions_leg_step_id' => true,
'name' => false,
'service' => false,
'board' => false,
'alight' => false,
'distance' => false,
'fare' => false,
'fare_raw' => true,
'distance_raw' => false
);
return self::saveGeneric($db, $fields, $data, 'leg_step_quote');
}
private function saveGeneric($db, $fields, $data, $table) {
$q = "select column_name, data_type, character_maximum_length from INFORMATION_SCHEMA.COLUMNS where table_name = '${table}'";
$r = pg_query($db, $q);
$columns = [];
while ($f=pg_fetch_assoc($r)) {
$columns[$f['column_name']] = $f;
}
$field_key_list = [];
$field_val_list = [];
foreach ($fields as $key=>$required) {
if ($required && !isset($data[$key])) {
return [NULL, "Missing required field '${key}'"];
}
if (!isset($data[$key])) continue;
$field_key_list[] = "${key}";
if (isset($columns[$key]) && ($columns[$key]["data_type"]=='integer' || $columns[$key]["data_type"]=='bigint')) {
$field_val_list[] = ($data[$key]==NULL || strlen($data[$key])==0)?"NULL":((int)$data[$key]);
} else {
$field_val_list[] = (($data[$key]==NULL || $data[$key]=="")?"NULL":"'".pg_escape_string($data[$key])."'");
}
}
$q = "INSERT INTO ${table} (".implode(",",$field_key_list).") VALUES(".implode(",",$field_val_list).") RETURNING id";
//error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$f = pg_fetch_row($r);
return [$f[0], NULL];
}
return [NULL, pg_last_error($db)];
}
public function processPolyline($data) {
if (!is_array($data)) {
return $data;
}
$points = [];
foreach ($data as $item) {
$points = array_merge($points, Polyline::decode($item));
}
return Polyline::encode($points);
}
public function getPolyline($db, $f) {
$q = "SELECT polyline FROM google_directions_leg_steps WHERE ";
$q.= " location_start_lat='".$f["location_start_lat"]."' AND ";
$q.= " location_start_lng='".$f["location_start_lng"]."' AND ";
$q.= " location_end_lat='".$f["location_end_lat"]."' AND ";
$q.= " location_end_lng='".$f["location_end_lng"]."' AND ";
$q.= " polyline<>'' ORDER BY ID desc LIMIT 1";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $polyline=pg_fetch_row($r)) {
$f["polyline"] = $polyline[0];
$q = "UPDATE google_directions_leg_steps SET polyline='".pg_escape_string($f["polyline"])."' WHERE id=".$f['sid']." RETURNING *";
$r = pg_query($db, $q); // Do not quite care...
return $f["polyline"];
}
$res = MultiModalApi::getPolyline($f["sid"]);
if (is_array($res) && isset($res["code"]) && $res["code"]==0) {
if (isset($res["data"]) && isset($res["data"]["polyline"])) {
return $res["data"]["polyline"]; // No need to update
}
}
return $f["polyline"];
}
public function getDate($country) {
$tz = 'Asia/Singapore';
if ($country=='US') {
$tz = 'America/Los_Angeles';
}
$timestamp = time();
$dt = new DateTime("now", new DateTimeZone($tz)); //first argument "must" be a string
$dt->setTimestamp($timestamp); //adjust the object to correct timestamp
return $dt->format('Y-m-d, H:i:s');
}
public function getLegTimezones($db, $id) {
$departure_time_zone = 1; // Asia/Singapore
$q = "SELECT departure_time_zone FROM google_directions_legs WHERE id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
if (is_integer($f[0]) && $f[0]>0) {
$departure_time_zone = $f[0];
} else {
$q = "SELECT * FROM address_timezone WHERE lower(timezone)=lower('".$f[0]."')";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$departure_time_zone = $f[0];
}
}
}
$arrival_time_zone = 1; // Asia/Singapore
$q = "SELECT arrival_time_zone FROM google_directions_legs WHERE id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
if (is_integer($f[0]) && $f[0]>0) {
$arrival_time_zone = $f[0];
} else {
$q = "SELECT * FROM address_timezone WHERE lower(timezone)=lower('".$f[0]."')";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$arrival_time_zone = $f[0];
}
}
}
return [$departure_time_zone, $arrival_time_zone];
}
public function getLegTimezonesForAdvice($db, $id) {
$result = [];
$q = "SELECT id FROM google_directions_legs WHERE parsedemail_item_advice_google_id=".((int)$id);
$r = pg_query($db, $q);
while ($f=pg_fetch_row($r)) {
$result[$f[0]] = self::getLegTimezones($db, $f[0]);
}
return $result;
}
}
// vi:ts=2
+828
View File
@@ -0,0 +1,828 @@
<?php
class MultiModalApi extends Api
{
public $apiName = 'multimodal';
const CACHE_TIMEOUT = 1800; // 30 minutes
public function indexAction() {
syslog(LOG_WARNING,'MultiModalApi::indexAction()');
// 16279
$message = "Unexpected error";
$leg_id = $this->requestParams["leg_id"] ?? 0;
$country = $this->requestParams["country"] ?? 'SG';
$member_id = $this->requestParams["member_id"] ?? 0;
$advice_id = 0;
try {
if ($leg_id<1) {
throw new Exception('Invalid trip leg ID');
}
$db = new Db();
list ($leg, $err) = MultiModal::getLegById($db->getConnect(), (int)$leg_id);
if (!$leg || !isset($leg['id']) || $leg['id']<1) {
throw new Exception($err!=""?$err:'No trip leg found');
}
syslog(LOG_WARNING,'111111111');
list ($advice, $err) = MultiModal::getLegAdviceGoogleById($db->getConnect(), (int)$leg['parsedemail_item_advice_google_id']);
if ($advice && $advice['id']>0) {
$advice_id = $advice['id'];
} else {
throw new Exception($err!=""?$err:'No google advice found');
}
// DEBUG
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
list ($legs, $err) = MultiModal::getAdviceLegs($db->getConnect(), $advice_id);
return MultiModalApi::processLegs($db, $legs, $advice_id, $country);
} catch (Exception $e) {
syslog(LOG_WARNING,json_encode($e));
$message = $e->getMessage();
}
return $this->response(
array(
'error' => $message
), 500);
}
protected function processSteps($db, $leg, $ok) {
list ($steps, $err) = MultiModal::getLegSteps($db->getConnect(), (int)$leg['id']);
if ($steps || count($steps)>0) {
$total = 0;
$overview = [];
foreach ($steps as $key=>$step) {
$leg['step_fares'][$step['sid']] = NULL;
// We support BUS, SUBWAY, TAXI only (and Singapore...)
if ($step['travel_mode']=='TAXI' || $step['travel_mode']=='SCOOTER' ||
($step['travel_mode']=='TRANSIT' && isset($step['vehicle']) &&
($step['vehicle']='BUS' || $step['vehicle']=='SUBWAY'))) {
if (isset($step['fare_raw']) && $step['fare_raw']!="") {
// OK
$total += (int)$step['fare_raw'];
$leg['step_fares'][$step['sid']] = (int)$step['fare_raw'];
} else {
// Not OK
$ok = false;
error_log('No quote for step_id='.$step['sid'].' traveling by '.(isset($step['vehicle'])?$step['vehicle']:"NO VEHICLE"));
}
} else {
error_log('Unsupported travel mode: '.$step['travel_mode']."/".(isset($step['vehicle'])?$step['vehicle']:"NO VEHICLE"));
}
if (array_key_exists("polyline",$step)) {
$overview[] = $step["polyline"];
}
if (!array_key_exists('duration',$step) || $step['duration']<60) {
$step['duration'] = 60;
}
$steps[$key] = $step;
}
if ($leg['fare_raw']!=$total && $total>0) {
error_log('Leg #'.$leg['id'].' total ('.$leg['fare_raw'].') != calculated ('.$total.')');
$leg['fare_raw'] = $total;
}
if (count($overview)>0) {
require_once('../common/Polyline.php');
$leg["polyline"] = MultiModal::processPolyline($overview);
}
} else {
error_log('No steps found for leg #'.$leg['id']);
}
return [$leg, $ok, $steps];
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/multimodal/1
* @return string
*/
public function viewAction() {
//id must be the first parameter after /multimodal/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
$db = new Db();
$tripOptions = Trips::getOptionsById($db->getConnect(), (int)$id);
if(is_array($tripOptions) && count($tripOptions)>0) {
/*$tripOptions = ActivityApi::processQuotesTripMultimodalOptions(
$db, $member_id, $country, $tripOptions);*/
return $this->response(
array(
'id' => $id,
'count' => count($tripOptions),
'options' => $tripOptions
), 200);
}
}
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction() {
syslog(LOG_WARNING,'MultiModalApi::createAction()');
$message = "Failed to run multi-modal router";
$origin = $this->requestParams["origin"] ?? 0;
$destination = $this->requestParams["destination"] ?? 0;
$country = $this->requestParams["country"] ?? 'SG';
$no_cache = $this->requestParams["no_cache"] ?? false;
$member_id = $this->requestParams["member_id"] ?? 0;
$cache_timeout = $this->requestParams["cache_timeout"] ?? MultiModalApi::CACHE_TIMEOUT;
$distance = $this->requestParams["distance"] ?? 0;
$duration = $this->requestParams["duration"] ?? 0;
try {
$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);
}
// Check input
$addrOrigin = Address::getAddressById($db->getConnect(), (int)$origin);
$addrDestination = Address::getAddressById($db->getConnect(), (int)$destination);
if ($addrOrigin==NULL || $addrDestination==NULL) {
throw new Exception('Invalid origin and/or destination address');
}
$checkOptions = [
[
"type"=>1,
"geocode" => [
"lat" => $addrOrigin["latitude"],
"lng" => $addrOrigin["longitude"]
]
]
];
syslog(LOG_WARNING,'MultiModalApi: '.json_encode($checkOptions));
$withinTheServiceArea = GeocodeApi::checkWithinTheServiceArea($country, $checkOptions, false);
if ($withinTheServiceArea) {
syslog(LOG_WARNING,'MultiModalApi: WITHIN SERVICE AREA!');
$trip = MultiModalApi::multimodal(
$db, $addrOrigin, $addrDestination, $country, $no_cache, $member_id, $cache_timeout, $distance, $duration
);
} else {
// Trip format
syslog(LOG_WARNING,'MultiModalApi: IS NOT WITHIN SERVICE AREA!');
$trip = ActivityApi::createTrip($db, $country, $duration, $distance, $addrOrigin["id"], $addrDestination["id"]);
}
/******* Start GOJEK ******/
//$trip = MultiModalApi::processGojekOption($db, $member_id, $country, $trip);
/******* End GOJEK ******/
if ($country=='US') {
$trip = MultiModalApi::processRidesharesOption($db, $member_id, $country, $trip);
}
if ($trip && count($trip)) {
$trip = MultiModalFilter::sortTrip($trip);
return $this->response($trip, 200);
} else {
$message = "Failed to process multimodal trip";
}
} catch (Exception $e) {
error_log(json_encode($e));
$message = $e->getMessage();
}
return $this->response(
array(
"error" => $message
), 500);
}
public static function processGojekOption($db, $member_id, $country, $trip) {
$trips = ActivityApi::processGojekOption($db, $member_id, $country, [
[
'trip' => $trip
]
]);
$n = count($trips) - 1;
if ($n>0 && array_key_exists('rideshare',$trips[$n]) && is_array($trips[$n]['rideshare']) && count($trips[$n]['rideshare'])>10) {
$gojek = $trips[$n]['rideshare'];
$options = $gojek['options'];
$leg = $options['legs'][0];
$leg_fare = $options['leg_fare'][$leg['id']];
$leg_steps = $options['leg_steps'][$leg['id']];
$trip['options']['legs'][] = $leg; // Append GOJEK
$trip['options']['leg_fare'][$leg['id']] = $leg_fare;
$trip['options']['leg_steps'][$leg['id']] = $leg_steps;
$trip['options']['routes'] = count($trip['options']['legs']);
}
return $trip;
}
public static function processRidesharesOption($db, $member_id, $country, $trip) {
syslog(LOG_WARNING,"MultiModalApi::processRidesharesOption(\$db, $member_id, $country, \$trip)");
$trips = ActivityApi::processRidesharesOption($db, $member_id, $country, [
[
'trip' => $trip
]
]);
foreach ($trips as $item) {
if (is_array($item) && array_key_exists('rideshare',$item) && is_array($item['rideshare']) && count($item['rideshare'])>10) {
$rideshare = $item['rideshare'];
$options = $rideshare['options'];
$leg = $options['legs'][0];
$leg_fare = $options['leg_fare'][$leg['id']];
$leg_steps = $options['leg_steps'][$leg['id']];
$trip['options']['legs'][] = $leg; // Append Rideshare
$trip['options']['leg_fare'][$leg['id']] = $leg_fare;
$trip['options']['leg_steps'][$leg['id']] = $leg_steps;
$trip['options']['routes'] = count($trip['options']['legs']);
} else {
syslog(LOG_WARNING,"Invalid rideshare entry!");
}
}
/*
$n = count($trips) - 1;
if ($n>0 && array_key_exists('rideshare',$trips[$n]) && is_array($trips[$n]['rideshare']) && count($trips[$n]['rideshare'])>10) {
$rideshare = $trips[$n]['rideshare'];
$options = $rideshare['options'];
$leg = $options['legs'][0];
$leg_fare = $options['leg_fare'][$leg['id']];
$leg_steps = $options['leg_steps'][$leg['id']];
$trip['options']['legs'][] = $leg; // Append Rideshare
$trip['options']['leg_fare'][$leg['id']] = $leg_fare;
$trip['options']['leg_steps'][$leg['id']] = $leg_steps;
$trip['options']['routes'] = count($trip['options']['legs']);
}*/
return $trip;
}
public function multimodal($db, $addrOrigin, $addrDestination, $country, $no_cache, $member_id, $cache_timeout, $distance, $duration) {
error_log('MultiModalApi::multimodal()');
$trip = NULL;
try {
// id,address,latitude,longitude,timezone,geocoding_date,postal
// 3,'97 Meyer Road, Singapore', 1.2833754, 103.8607264, 1, 2019-05-18, 018956
if (!$no_cache) { // we can force to skip the cache...
$trip = Trips::getByCoordinates(
$db->getConnect(),
['lat' => $addrOrigin['latitude'], 'lng' => $addrOrigin['longitude']],
['lat' => $addrDestination['latitude'], 'lng' => $addrDestination['longitude']],
1
);
if ($trip && $trip["id"]>0 && MultiModalApi::checkCache($db, $trip, $cache_timeout)) {
// Trip options
$trip["member_id"] = $member_id; // Pass along
$tripOptions = Trips::getOptionsById($db->getConnect(), $trip["id"]);
if(is_array($tripOptions) && count($tripOptions)>0) {
ActivityApi::debugTrips([['multimodal'=>['options'=>$tripOptions]]],'11111');
$tripOptions = ActivityApi::processQuotesTripMultimodalOptions($db, $member_id, $country, $tripOptions);
$trip = array_merge($trip, array(
'count' => count($tripOptions),
'options' => $tripOptions,
'from_cache' => true
));
ActivityApi::debugTrips([['multimodal'=>$trip]],'11112');
$trip['country'] = $country;
$trip = MultiModalApi::processTrip($db, $trip, $country);
ActivityApi::debugTrips([['multimodal'=>$trip]],'11113');
throw new Exception('OK');
} else {
// Will need new trip options!
error_log('Trip parsedemail_item.id='.$trip["id"].' does not have options!');
}
} else if ($trip && $trip["id"]>0) {
$trip = NULL; // cache miss - we will need a new trip!
} // else trip was not found
}
// Create a new trip entry
if ($trip==NULL || !isset($trip["id"])) {
$data = [
'travel_date' => MultiModal::getDate($country),
'duration' => (int)$duration,
'cost_raw' => NULL,
'trackedemail_item_id' => NULL,
'cost' => NULL,
'distance' => $distance,
'transport_provider_id' => NULL,
'scheduled' => NULL,
'travel_date_end' => MultiModal::getDate($country),
'dup_id' => NULL,
'location_start_id' => $addrOrigin['id'],
'location_end_id' => $addrDestination['id'],
'private' => 't' /* this is a private entry without the source e-mail */
];
list($id, $err) = Trips::create($db->getConnect(), $data);
if ($id && $id>0) {
$trip = Trips::getById($db->getConnect(), $id);
}
if ($trip==NULL || !isset($trip["id"])) {
throw new Exception('Failed to create new trip record'.($err?": ${err}":""));
}
}
// New directions
$tripService = MultiModalApi::tripService($trip["id"], $country);
//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);
ActivityApi::debugTrips([['multimodal'=>$tripService]],'11114');
$tripService = MultiModalApi::processTrip($db, $tripService, $country);
ActivityApi::debugTrips([['multimodal'=>$tripService]],'11115');
$tripOptions = $tripService["options"];
ActivityApi::debugTrips([['multimodal'=>$tripService]],'111');
$tripOptions = ActivityApi::processQuotesTripMultimodalOptions(
$db, $member_id, $country, $tripOptions);
$tripService["options"] = $tripOptions;
$trip = $tripService;
ActivityApi::debugTrips([['multimodal'=>$trip]],'112');
$trip = MultiModalFilter::byCost($trip);
}
} catch (Exception $e) {
error_log($e->getMessage());
}
return $trip;
}
protected function checkCache($db, $trip, $cache_timeout) {
error_log('MultiModalApi::checkCache()');
$advice = NULL;
if (!is_array($trip) || $trip['id']<1) {
error_log('No trip data to analyse => no cache!');
return false;
}
list ($res, $err) = MultiModal::getLastGoogleAdviceByTrip($db->getConnect(),$trip['id']);
error_log(json_encode($res));
if ($res && $res['id']>0) {
$advice = $res;
} else {
error_log('No parsedemail_item_advice_google record => no cache!');
return false;
}
if ($advice['elapsed'] > $cache_timeout) {
error_log('Cache expired! We will need a new advice!');
return false;
}
return true;
}
protected function checkTaxiQuote($db, $trip, $country) {
syslog(LOG_WARNING,"MultiModalApi::checkTaxiQuote(\$db, \$trip, $country)");
try {
// Do we have trip options?
if (!isset($trip['options']) || !is_array($trip['options']) || !isset($trip['options']['legs'])) {
error_log('Trip parsedemail_item.id='.$trip['id'].' has no options!');
return $trip;
}
// Do we have a member_id?
$member_id = 0;
if (isset($trip["member_id"])) {
$member_id = $trip["member_id"];
}
// Do we have transport providers?
$transport_providers = [];
list ($result, $error) = Geocode::getTransportProvidersByCountry(
$db->getConnect(), $trip["country"]);
if ($result==NULL || count($result)<1) {
syslog(LOG_WARNING,"Failed to get transport providers: ".$error);
return $trip;
}
foreach ($result as $f) {
if ($trip["country"]!='SG' || $f["transport_provider_id"]==4) { // ComfortDelGro
$transport_providers[] = $f["transport_provider_id"];
}
}
// Check leg steps for the taxi
$n = count($trip['options']['legs']);
for ($i=0; $i<$n; $i++) {
$leg = $trip['options']['legs'][$i];
$leg_id = $leg['id'];
$leg_steps = $trip['options']['leg_steps'][$leg_id];
$leg_fare = $trip['options']['leg_fare'][$leg_id];
$m = count($leg_steps);
$total = 0;
for ($j=0; $j<$m; $j++) {
$step = $leg_steps[$j];
if ($step['travel_mode']=="TAXI") {
// Reverse geocode
list($origin, $err1) = GeocodeApi::reverseGeocode($db, $step["location_start_lat"], $step["location_start_lng"]);
list($destination, $err2) = GeocodeApi::reverseGeocode($db, $step["location_end_lat"], $step["location_end_lng"]);
if ($origin=='' || $destination=='') {
syslog(LOG_WARNING,'MultiModalApi::checkTaxiQuote() Reverse geocoding failed! From "'.$origin.'"('.$step["location_start_lat"].','.$step["location_start_lng"].') to "'.$destination.'"('.$step["location_end_lat"].','.$step["location_end_lng"].')');
continue;
}
syslog(LOG_WARNING,"origin='${origin}'");
syslog(LOG_WARNING,"destination='${destination}");
list($message,$code,$action,$data) = QuoteGroupApi::createReal(
$db, $origin, $destination, $member_id, $transport_providers, $country
);
if (is_array($data) && isset($data["id"]) && $data["id"]>0) {
syslog(LOG_WARNING,"QuoteGroup ID: ".$data["id"]);
$quoteGroupId = $data["id"];
// Check quote
$step["quote_group_id"] = $quoteGroupId;
sleep(3);
list($message,$code,$action,$data) = QuoteGroupApi::viewReal($db, $quoteGroupId);
if ($code==200 && isset($data["cost"]) && $data["cost"]>0) {
// TODO: double-check!
$step["fare_quote"] = ((int)(100*$data["cost"]));
if (!array_key_exists('fare_raw',$step) || $step['fare_raw']<$step["fare_quote"]) {
$step['fare_raw'] = $step["fare_quote"];
}
if ($leg_fare==NULL) $leg_fare = $step["fare_quote"];
else $leg_fare += $step["fare_quote"];
} else {
syslog(LOG_WARNING,"ERROR: ".$message);
}
} else {
syslog(LOG_WARNING,"ERROR: ".$message);
}
}
if (array_key_exists('fare_raw',$step) && $step['fare_raw']>0) {
$total += $step['fare_raw'];
}
if (!array_key_exists('duration',$step) || $step['duration']<60) {
$step['duration'] = 60;
}
$leg_steps[$j] = $step;
}
if (!array_key_exists('fare_raw',$leg) || $leg['fare_raw']<$leg_fare || $leg['fare_raw']<$total) {
$leg['fare_raw'] = $leg_fare>$total ? $leg_fare : $total;
}
$trip['options']['leg_fare'][$leg_id] = $leg_fare>$total ? $leg_fare : $total;
$trip['options']['leg_steps'][$leg_id] = $leg_steps;
$trip['options']['legs'][$i] = $leg;
}
//*/
} catch (Exception $e) {
syslog(LOG_WARNING,'MultiModalApi::checkTaxiQuote() EXCEPTION: '.$e->getMessage());
}
return $trip;
}
protected function processTrip($db, $trip, $country) {
syslog(LOG_WARNING,'MultiModalApi::processTrip()');
require_once('../common/Polyline.php');
//error_log(json_encode($trip));
ActivityApi::debugTrips([['multimodal'=>$trip]],'1111');
if (array_key_exists('options',$trip) && is_array($trip['options']) && isset($trip['options']['legs'])) {
if ($trip['options']['gid']<1) {
// Save options...
list($res, $err) = MultiModal::create($db->getConnect(), $trip);
error_log($err);
if ($res && $res['id']>0) {
$trip = $res;
// TODO: fix!
//unset($trip['error']);
} else {
$message = $err!='' ? $err : 'Failed to save multi-modal option';
error_log($message);
$trip['error'] = $message;
$trip['options']['routes'] = count($trip['options']['legs']);
$trip = MultiModalApi::checkTaxiQuote($db, $trip, $country);
return $trip;
}
} else {
error_log('DB record for options already exists');
}
// Schedule quote
$trip = MultiModalApi::checkTaxiQuote($db, $trip, $country);
$n = count($trip['options']['legs']);
for ($i=0; $i<$n; $i++) {
$leg = $trip['options']['legs'][$i];
$leg_id = $leg['id'];
$leg_steps = $trip['options']['leg_steps'][$leg_id];
$leg_fare = $trip['options']['leg_fare'][$leg_id];
// We must schedule leg fare quote...
$m = count($leg_steps);
$overview = [];
for ($j=0; $j<$m; $j++) {
$step = $leg_steps[$j];
//"travel_mode": "TRANSIT"
//"vehicle": "BUS",
//"line": "", <------------- WTF?
if ($step['travel_mode']=="SCOOTER") {
$polyline = [];
// Get directions
$input = [
1 => [
"coordinates" => [
"lat" => $trip["location_start_lat"],
"lng" => $trip["location_start_lng"]
]
],
2 => [
"coordinates" => [
"lat" => $trip["location_end_lat"],
"lng" => $trip["location_end_lng"]
]
]
];
$result = [
1 => [
"data" => [
"location_start_lat" => $step["location_start_lat"],
"location_start_lng" => $step["location_start_lng"],
"location_end_lat" => $step["location_end_lat"],
"location_end_lng" => $step["location_end_lng"]
]
]
];
$options = ScooterApi::directions($db, $input, $result, []);
$step["route_overlay"] = isset($options["route_overlay"]) ? $options["route_overlay"][0] : [];
$step["polyline"] = $options["route_overlay"][0]["polyline"];
$polyline = $step["polyline"]; // TODO: merge with walking...
// Get quote
$scooter_time = $step['duration'];
$country = 'SG'; // Singapore only!
list($res,$err) = ScooterApi::quote($db, $scooter_time, $country);
if ($res && count($res)>0) {
$fare_raw = PHP_INT_MAX;
foreach ($res as $quote) {
if ($quote['pricing']['dropoff_anywhere']!=null) {
$fare_raw = $quote['quote'] + $quote['pricing']['dropoff_anywhere'];
break; // with this option we have Neuron only and that be it
}
if ($fare_raw > $quote['quote']) {
$fare_raw = $quote['quote'];
}
}
if ($fare_raw == PHP_INT_MAX) {
$fare_raw = NULL; // we did not get any quote?
} else {
$fare_raw = (int)($fare_raw);
}
$trip['options']['leg_fare'][$leg_id] = $fare_raw;
$leg['fare_raw'] = $fare_raw;
$step['fare_raw'] = $fare_raw;
}
$leg['polyline'] = $polyline;
$step['polyline'] = $polyline;
}
//error_log(json_encode($step));
$leg_steps[$j] = $step;
// TODO: FIX!!!
if (array_key_exists("polyline",$step)) {
$overview[] = $step["polyline"];
}
}
if (count($overview)>0) {
$leg["polyline"] = MultiModal::processPolyline($overview);
}
//syslog(LOG_WARNING,'$leg["polyline"] = '.$leg["polyline"]);
$trip['options']['leg_steps'][$leg_id] = $leg_steps;
$trip['options']['legs'][$i] = $leg;
}
$trip['options']['routes'] = count($trip['options']['legs']);
} else {
syslog(LOG_WARNING,'Trip parsedemail_item.id='.$trip['id'].' has no options!');
}
return $trip;
}
public function updateAction() {
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction() {
$res = 0;
$message = "Delete error";
$id = (int)array_shift($this->requestUri);
if ($id>0) {
$db = new Db();
$res = MultiModal::delete($db->getConnect(), $id);
if ($res>0) {
return $this->response(array(
'id' => $id,
'result' => $res
), 200);
} else {
$message = "Delete failed";
}
} else {
$message = "Invalid error";
}
return $this->response(
array(
"error" => $message,
"result" => $res
), 500);
}
public function tripService($id, $country) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
error_log("MultiModalApi::tripService($id)");
if ($country!='SG' && $country!='US') {
throw new Exception('Unsupported country: '.$country);
}
// curl -d 'id=8907' -X POST {$oauth2_url}trips/
$postdata = http_build_query(
array(
'id'=> $id,
'country' => $country
)
);
$url = $oauth2_url."trips";
$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");
file_put_contents('/home/savvy/FloatWeb/adminsavvy/DEBUG/trips'.time().'.json',$body);
$trip = json_decode($body,true);
return $trip;
}
public function mytansportsgService($id) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
error_log("public function mytransportsgService($id)");
// curl -d 'id=8907' -X POST $oauth2_url.'mytransportsg'/
$postdata = http_build_query(
array(
'advice_id'=> $id
)
);
$url = $oauth2_url."mytransportsg";
$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);
$body = file_get_contents($url, false, $context);
$advice = json_decode($body,true);
return $advice;
}
public function mytansportsgServiceLeg($id) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
error_log("public function mytransportsgServiceLeg($id)");
$url = $oauth2_url."mytransportsg/" . $id;
$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);
$advice = json_decode($body,true);
return $advice;
}
public function bartService($db, $legs, $id) {
$quotes = []; // We do not call the service as the data is presented after the initial call
foreach ($legs as $leg_id=>$leg) {
$total = 0;
list ($res, $err) = MultiModal::getLegStepQuotes($db->getConnect(), $leg_id);
if ($res && count($res)>0) {
foreach ($res as $quote) {
$total += ((int)$f["fare_raw"]);
}
}
$quotes[$leg_id] = [
"total" => $total,
];
}
$result = [
"code" => 0,
"data" => $quotes
];
return $result;
}
protected function processLegs($db, $legs, $advice_id, $country) {
syslog(LOG_WARNING,"MultiModalApi::processLegs(\$db, \$legs, $advice_id, $country)");
$ok = true; // All the steps on all the legs have the quotes?
$leg_steps = [];
foreach ($legs as $leg_id=>$leg) {
list ($leg, $ok, $steps) = MultiModalApi::processSteps($db, $leg, $ok);
$legs[$leg_id] = $leg;
$leg_steps[$leg_id] = $leg_steps;
}
if ($ok) {
return $this->response($legs, 200);
}
// Get quote
$options = [
"legs" => $legs,
"leg_steps" => $leg_steps
];
$options = ActivityApi::processQuotesTripMultimodalOptions(
$db, 0, $country, $options); /* NOTE: $member_id = 0; */
$legs = $options["legs"];
/*
if (is_array($result) && isset($result["code"]) && $result["code"]==0 && isset($result["data"])) {
$data = $result["data"];
error_log(json_encode($data));
foreach ($data as $leg_id=>$quote) {
$leg = $legs[$leg_id];
if (isset($quote['total']) && $quote['total']>0) {
$leg['fare_raw'] = $quote['total'];
}
$ok = true;
list ($leg, $ok, $steps) = MultiModalApi::processSteps($db, $leg, $ok);
if ($ok) {
$legs[$leg_id] = $leg;
} else {
error_log('Failed to process steps for leg #'.$leg_id);
}
}
return $this->response($legs, 200);
}
*/
return $this->response($legs, 200);
/*
return $this->response(
array(
'error' => isset($result["error"])?$result["error"]:"MyTrasnport.sg service call failed"
), 500);*/
}
public static function getPolyline($id) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
$url = $oauth2_url."polyline/" . $id;
$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);
//var_dump($http_response_header);
//var_dump($body);
return $geocoded;
}
}
@@ -0,0 +1,246 @@
<?php
class MultiModalFilter {
const NO_FILTER_COUNT = 3;
// https://www.php.net/manual/en/function.uasort.php
public static function sortTrip($trip) {
if (is_array($trip) && array_key_exists('options',$trip) && array_key_exists('legs',$trip['options']) && is_array($trip['options']['legs'])) {
$legs = $trip['options']['legs'];
$sorted_legs = [];
$sorted_fare = [];
foreach ($legs as $leg) {
$sorted_legs[$leg['id']] = $leg;
$sorted_fare[$leg['id']] = $leg['fare_raw'];
}
asort($sorted_fare);
$legs = [];
foreach ($sorted_fare as $leg_id=>$fare_raw) {
$legs[] = $sorted_legs[$leg_id];
}
$trip['options']['legs'] = $legs;
}
return $trip;
}
public static function byDuration($trip) {
syslog(LOG_WARNING,'MultiModalFilter::byDuration()');
if (MultiModalFilter::sanityCheck($trip)) {
syslog(LOG_WARNING,'Sanity check failed!');
return $trip; // I do not know how to handle it or not many options
}
// Taxi
$taxies = MultiModalFilter::getTravelModeRoutes($trip,"TAXI",["NUS_TRANSIT"]);
if (is_array($taxies) && count($taxies)>1) {
list($good,$bad) = MultiModalFilter::getFastest($taxies);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// NUS
$nus = MultiModalFilter::getTravelModeRoutes($trip,"NUS_TRANSIT",["TAXI"]);
if (is_array($nus) && count($nus)>1) {
list($good,$bad) = MultiModalFilter::getFastest($nus);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// Scooter
$scooters = MultiModalFilter::getTravelModeRoutes($trip,"SCOOTER");
if (is_array($scooters) && count($scooters)>1) {
list($good,$bad) = MultiModalFilter::getFastest($scooters);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// bikes
$bikes = MultiModalFilter::getTravelModeRoutes($trip,"BIKE");
if (is_array($bikes) && count($bikes)>1) {
list($good,$bad) = MultiModalFilter::getFastest($bikes);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// Public
$transit = MultiModalFilter::getTravelModeRoutes($trip,"TRANSIT",["TAXI","NUS_TRANSIT","SCOOTER","BIKE"]);
if (is_array($transit) && count($transit)>1) {
list($good,$bad) = MultiModalFilter::getFastest($transit);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
$trip['options']['routes'] = count($trip['options']['legs']);
return $trip;
}
public static function byCost($trip) {
syslog(LOG_WARNING,'MultiModalFilter::byCost()');
if (MultiModalFilter::sanityCheck($trip)) {
syslog(LOG_WARNING,'Sanity check failed!');
return $trip; // I do not know how to handle it or not many options
}
// Taxi
$taxies = MultiModalFilter::getTravelModeRoutes($trip,"TAXI",["NUS_TRANSIT"]);
if (is_array($taxies) && count($taxies)>1) {
list($good,$bad) = MultiModalFilter::getCheapest($trip,$taxies);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// NUS
$nus = MultiModalFilter::getTravelModeRoutes($trip,"NUS_TRANSIT",["TAXI"]);
if (is_array($nus) && count($nus)>1) {
list($good,$bad) = MultiModalFilter::getCheapest($trip,$nus);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// Scooter
$scooters = MultiModalFilter::getTravelModeRoutes($trip,"SCOOTER");
if (is_array($scooters) && count($scooters)>1) {
list($good,$bad) = MultiModalFilter::getCheapest($trip,$scooters);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// Scooter
$bikes = MultiModalFilter::getTravelModeRoutes($trip,"BIKE");
if (is_array($bikes) && count($bikes)>1) {
list($good,$bad) = MultiModalFilter::getCheapest($trip,$bikes);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
// Public
$transit = MultiModalFilter::getTravelModeRoutes($trip,"TRANSIT",["TAXI","NUS_TRANSIT","SCOOTER","BIKE"]);
if (is_array($transit) && count($transit)>1) {
list($good,$bad) = MultiModalFilter::getCheapest($trip,$transit);
if (is_array($good)) {
$trip = MultiModalFilter::filterOut($trip, $bad);
}
}
$trip['options']['routes'] = count($trip['options']['legs']);
$trip = MultiModalFilter::durations($trip);
return $trip;
}
protected static function filterOut($trip, $bad) {
$badLegs = [];
foreach ($bad as $item) {
$leg_id = $item["id"];
unset($trip['options']['leg_fare'][$leg_id]);
unset($trip['options']['leg_steps'][$leg_id]);
$badLegs[] = $leg_id;
}
$newLegs = [];
$legs = $trip['options']['legs'];
foreach ($legs as $leg) {
if (in_array($leg['id'],$badLegs)) continue;
$newLegs[] = $leg;
}
$trip['options']['legs'] = $newLegs;
return $trip;
}
protected static function getCheapest($trip,$what) {
$cheapest = PHP_INT_MAX;
$good = NULL;
$bad = [];
foreach ($what as $item) {
$leg_id = $item['id'];
$fare = $trip['options']['leg_fare'][$leg_id];
if ($fare!=null && $fare>0 && $fare<$cheapest) {
$good = $item;
$cheapest = $fare;
}
// Do we do extra checks?
}
if (!is_array($good) || !array_key_exists('id',$good)) {
return [NULL,$what]; // All entries are BAD
}
foreach ($what as $item) {
if ($item['id']!=$good['id']) {
$bad[] = $item;
}
}
return [$good, $bad];
}
protected static function getFastest($what) {
$fastest = PHP_INT_MAX;
$good = NULL;
$bad = [];
foreach ($what as $item) {
if ($item['duration']<$fastest) {
$good = $item;
$fastest = $item['duration'];
}
}
if (!is_array($good) || !array_key_exists('id',$good)) {
return [NULL,$what]; // All entries are BAD
}
foreach ($what as $item) {
if ($item['id']!=$good['id']) {
$bad[] = $item;
}
}
return [$good, $bad];
}
protected static function getLegById($trip,$leg_id) {
foreach ($trip['options']['legs'] as $leg) {
if ($leg['id']==$leg_id) {
return $leg;
}
}
return NULL;
}
protected static function getTravelModeRoutes($trip,$travel_mode,$exclude=[]) {
$result = [];
$excludes = count($exclude);
$leg_steps = $trip['options']['leg_steps'];
foreach ($leg_steps as $leg_id=>$steps) {
foreach ($steps as $step) {
if ($step['travel_mode']==$travel_mode) {
$result[$leg_id] = MultiModalFilter::getLegById($trip,$leg_id);
if ($excludes<1) {
break;
}
}
if ($excludes>0 && in_array($step['travel_mode'],$exclude)) {
if (array_key_exists($leg_id,$result)) {
unset($result[$leg_id]);
}
break;
}
}
}
return $result;
}
protected static function sanityCheck($trip) {
return !is_array($trip) || !array_key_exists('options',$trip) || !is_array($trip['options'])
|| !array_key_exists('legs',$trip['options']) || !is_array($trip['options']['legs'])
|| count($trip['options']['legs']) <= MultiModalFilter::NO_FILTER_COUNT;
}
protected static function durations($trip) {
foreach ($trip['options']['leg_steps'] as $leg_id=>$steps) {
$dirty = false;
foreach ($steps as $i=>$step) {
if (!array_key_exists('duration',$step) || $step['duration']<60) {
$step['duration'] = 60;
$steps[$i] = $step;
$dirty = true;
}
}
if ($dirty) {
$trip['options']['leg_steps'][$leg_id] = $steps;
}
}
return $trip;
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
class Popular {
const TIME_DELTA = 180; // min (3 hrs)
const TIME_LIMIT = 1262322000; // 2010-01-01
const DEFAULT_COUNTRY = 'SG';
const DEFAULT_OPTIONS = 3;
const CITY_CURVATURE = 1.25;
const CITY_SPEED = 0.667; // km/min (0.0667 = 40 km/h)
const POPULAR_RADIUS = 5000; // 5km
public function getPopular($db, $gpsdb, $member_id, $lat, $lng, $time, $time_delta=self::TIME_DELTA, $radius=self::POPULAR_RADIUS, $country=self::DEFAULT_COUNTRY) {
error_log('Popular::getPopular()');
if ($member_id<1) {
$loc = self::popularRadiusLocationsMock();
} else {
list($loc, $err) = self::popularRadiusLocations($gpsdb, $lat, $lng, $radius, 'start');
}
error_log('popularRadiusLocations='.($loc==NULL?"NULL":count($loc)));
if (!is_array($loc) || count($loc)<1) {
return [NULL, $err];
}
$result = [];
foreach ($loc as $f) { // location_end, location_start
$id = self::getPopularTripsID($gpsdb, $f["location_start"], $f["location_end"]);
if ($id<1) continue;
$q = "SELECT * FROM parsedemail_item WHERE id=(SELECT dup_id FROM parsedemail_item WHERE id=${id}) UNION SELECT * FROM parsedemail_item WHERE id=${id} ORDER BY dup_id DESC";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$result[] = $f;
}
}
if (count($result)) {
return [$result, NULL];
}
return [NULL, "No popular trips nearby found"];
}
public function getPopularTripsID($db, $location_start, $location_end) {
$q = "SELECT id FROM parsedemail_item WHERE location_start='${location_start}' AND location_end='${location_end}' ORDER BY id DESC LIMIT 1";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
return $f[0];
}
return 0;
}
public static function popularRadiusLocations($db, $lat, $lng, $radius, $what) {
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$q = "SELECT location_end, location_start, count(*) AS num FROM parsedemail_item WHERE ";
$q.= "ST_DWithin(location_${what},ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " GROUP BY location_end, location_start ORDER BY count(*) DESC";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
/*
public function popularByRange($db, $member_id, $days, $ids) {
$db_days = date("w",$db_time) + (int)$days;
$q = "SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm
FROM trackedemail_item a, parsedemail_item b
WHERE a.id=b.trackedemail_item_id AND a.member_id<>".((int)$member_id)."
AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days')
AND b.id IN (".implode(",",$ids).")
ORDER BY b.travel_date DESC";
file_put_contents("/home/savvy/tmp/popularByRange-${member_id}-${days}.sql",$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public function popularByRangeAndTime($db, $member_id, $days, $ids, $time, $time_delta=self::TIME_DELTA) {
$db_time = strtotime($time);
$db_days = date("w",$db_time) + (int)$days;
$db_time_delta = (int)$time_delta;
// Ranges
$dh = 60*date("G")+date("i");
$d1 = $dh - $db_time_delta;
$d2 = $dh + $db_time_delta;
$q = "SELECT c.* FROM (SELECT b.*, 60*date_part('hour', b.travel_date)+date_part('minute',b.travel_date) AS dm
FROM trackedemail_item a, parsedemail_item b
WHERE a.id=b.trackedemail_item_id AND a.member_id<>".((int)$member_id)."
AND b.dup_id IS NULL AND b.travel_date_end > (now() - interval '${db_days} days')
AND b.id IN (".implode(",",$ids).")) AS c
WHERE c.dm>=${d1} AND c.dm<=${d2} ORDER BY c.travel_date DESC";
file_put_contents("/home/savvy/tmp/popularByRangeAndTime-${member_id}-${days}.sql",$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
*/
protected function popularRadiusLocationsMock() {
return [
[
"location_end" => "0101000020E6100000892650C4A2985EC0C029070EC3CE4240",
"location_start" => "0101000020E61000004A5B012A419E5EC066A4390DACE34240",
"num" => 100
],
[
"location_end" => "0101000020E61000007F3CAA5013995EC0A6D590B8C7CE4240",
"location_start" => "0101000020E61000004A5B012A419E5EC066A4390DACE34240",
"num" => 50
],
[
"location_end" => "0101000020E610000023707F89D3985EC098A02BB6EECE4240",
"location_start" => "0101000020E61000008B02D8DB419E5EC03360DA8184E34240",
"num" => 10
]
];
}
}
// vi:ts=2
+141
View File
@@ -0,0 +1,141 @@
<?php
class PopularApi extends Api
{
public $apiName = 'popular';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
syslog(LOG_WARNING,'PopularApi::createAction()');
$message = 'Data not found';
$code = 404;
$time = $this->requestParams['time'] ?? '';
$location = $this->requestParams['location'] ?? [];
$country = $this->requestParams['country'] ?? Popular::DEFAULT_COUNTRY;
$member_id = $this->requestParams['member_id'] ?? 0;
try {
$db = new Db();
// DEUG
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
if ($country!='US' && $country!='SG') {
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 ($member_id<1) {
throw new RuntimeException('Invalid member ID',500);
}
/* if ($time=='' || strtotime($time)<Popular::TIME_LIMIT) {
throw new RuntimeException('Invalid time',500);
} */
if (!is_array($location) || !array_key_exists('lat',$location) || !array_key_exists('lng',$location)) {
throw new RuntimeException('You did not provide you GPS location',500);
}
if ($location["lat"]==NULL || $location["lng"]==NULL || ($location["lat"]==0 && $location["lng"]==0)) {
throw new RuntimeException('Invalid GPS location provided',500);
}
$address = NULL;
$street_address = "";
// DEBUG
list($location["lat"],$location["lng"]) = Geocode::mockGPSLocation(
$db->getConnect(), $member_id, $location["lat"], $location["lng"], 'default');
syslog(LOG_WARNING,"lat=".$location["lat"].",lng=".$location["lng"]);
list($street_address,$err) = GeocodeApi::reverseGeocode($db, $location["lat"], $location["lng"]);
if ($street_address=="") {
throw new RuntimeException($err?"Geocoder failed: $err":'Cannot geocode your GSP location',500);
}
syslog(LOG_WARNING,"street_address=".json_encode($street_address));
list($address,$err) = Geocode::checkLatLngByAddress($db->getConnect(), $street_address, $country);
if (!is_array($address) || !isset($address["address"])) {
throw new RuntimeException($err?"Geocoder failed: $err":'Cannot get address for your GPS location',500);
}
syslog(LOG_WARNING,json_encode($address));
// Step 1. Get activities
list($res,$err) = Popular::getPopular(
$db->getConnect(), $db->getConnectGPS(),
$member_id, $location["lat"], $location["lng"],
$time, Popular::TIME_DELTA, /* last 2 parameters are not used at the moment */
Popular::POPULAR_RADIUS, $country
);
if (!$res || count($res)<1) {
if ($err!="") {
throw new RuntimeException($err, 500);
}
throw new RuntimeException('No popular trips nearby found', 404);
}
syslog(LOG_WARNING,'Popular trips has '.count($res).' trips!');
$destination_cache = [];
$result = [];
$i = 0;
foreach ($res as $trip) {
list($trip, $destination_cache) = ActivityApi::processAddressTrip($db, $country, $address, $trip, $destination_cache);
if ($trip==NULL) continue;
$advice = ActivityApi::processAdvice($db, $member_id, $country, $trip);
if ($advice && count($advice)>0) {
$result[] = $advice;
$i++;
}
if ($i>=Popular::DEFAULT_OPTIONS) {
break;
}
}
$result = ActivityApi::processQuotesTrips($db, $member_id, $country, $result);
//$result = ActivityApi::processGojekOption($db, $member_id, $country, $result);
return $this->response(
array(
"trips" => $result,
"street_address" => html_entity_decode($street_address)
), 200);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$code = $e->getCode();
syslog(LOG_WARNING,$message);
}
return $this->response(
array(
"error" => $message
), $code);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
@@ -0,0 +1,47 @@
<?php
class Populardirect {
const TIME_DELTA = 180; // min (3 hrs)
const TIME_LIMIT = 1262322000; // 2010-01-01
const DEFAULT_COUNTRY = 'SG';
const DEFAULT_OPTIONS = 3;
const CITY_CURVATURE = 1.25;
const CITY_SPEED = 0.667; // km/min (0.0667 = 40 km/h)
const POPULAR_RADIUS = 100000; // 100km
public function getPopularDirect($db, $gpsdb, $member_id, $lat, $lng, $time, $time_delta = self::TIME_DELTA, $radius = self::POPULAR_RADIUS, $country = self::DEFAULT_COUNTRY) {
error_log('Populardirect::getPopular()');
$result = [];
$limit_lat = (float) $lat;
$limit_lng = (float) $lng;
$limit_rad = $radius;
//$q = "SELECT a.id,a.attraction,b.address,b.latitude,b.longitude FROM tourist_attraction a, address b WHERE b.id=a.address_id and b.country='US' and b.postal like '30%' ";
$q = "SELECT a.id,a.attraction,b.address,b.latitude,b.longitude "
. " FROM tourist_attraction a, address b "
. " WHERE b.id=a.address_id "
. " AND ST_DWithin(b.geometry,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})"
. " ORDER BY ST_DistanceSphere(b.geometry::geometry, ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326))";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
while ($f = pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
if (count($result)) {
return [$result, NULL];
}
return [NULL, "No popular trips nearby found"];
}
}
// vi:ts=2
@@ -0,0 +1,109 @@
<?php
class PopulardirectApi extends Api
{
public $apiName = 'populardirect';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
syslog(LOG_WARNING,'PopulardirectApi::createAction()');
$message = 'Data not found';
$code = 404;
$time = $this->requestParams['time'] ?? '';
$location = $this->requestParams['location'] ?? [];
$country = $this->requestParams['country'] ?? '';
$member_id = $this->requestParams['member_id'] ?? 0;
try {
$db = new Db();
$country = Geocode::getCountryByGPS($db->getConnect(), $location, $country, Activity::DEFAULT_COUNTRY);
syslog(LOG_WARNING,'country='.$country);
// DEBUG
//$country ='US';
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
list($location["lat"],$location["lng"]) = Geocode::mockGPSLocation(
$db->getConnect(), $member_id, $location["lat"], $location["lng"], 'default');
syslog(LOG_WARNING,"lat=".$location["lat"].",lng=".$location["lng"]);
if ($country!='US' && $country!='SG') {
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 ($member_id<1) {
throw new RuntimeException('Invalid member ID',500);
}
if (!is_array($location) || !array_key_exists('lat',$location) || !array_key_exists('lng',$location)) {
throw new RuntimeException('You did not provide you GPS location',500);
}
if ($location["lat"]==NULL || $location["lng"]==NULL || ($location["lat"]==0 && $location["lng"]==0)) {
throw new RuntimeException('Invalid GPS location provided',500);
}
$address = NULL;
$street_address = "";
// Step 1. Get activities
list($res,$err) = Populardirect::getPopularDirect(
$db->getConnect(), $db->getConnectGPS(),
$member_id, $location["lat"], $location["lng"],
$time, Populardirect::TIME_DELTA, /* last 2 parameters are not used at the moment */
Populardirect::POPULAR_RADIUS, $country
);
return $this->response(
array(
"trips" => $res,
"street_address" => '1'
), 200);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$code = $e->getCode();
syslog(LOG_WARNING,$message);
}
return $this->response(
array(
"error" => $message
), $code);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
+544
View File
@@ -0,0 +1,544 @@
<?php
class QuoteApi extends Api
{
public $apiName = 'quote';
const CACHE_TIMEOUT = 300; // 5 minutes
const UBER_DISTANCE_RESTRICTION = 320;
const LYFT_DISTANCE_RESTRICTION = 499;
const SINGAPORE_DISTANCE_RESTRICTION = 48;
const AUTOCAB_DISTANCE_RESTRICTION = 320; // TODO: Check and update late
public function __construct($requestUri, $encryption=true) {
// $this->cacheWhitelist = [
// "viewAction" => ['ttl' => 300], // 300 sec. = 5 min.
// "createAction" => ['ttl' => 300] // 300 sec. = 5 min.
// ];
parent::__construct($requestUri, $encryption);
}
public function indexAction() {
$origin = $this->requestParams["origin"] ?? "";
$destination = $this->requestParams["destination"] ?? "";
$transport_provider_id = $this->requestParams["transport_provider_id"] ?? "";
$limit = 10;
$offset = 0;
if ($destination && $origin) {
$db =new Db();
list($total, $quotes) = Quotes::getByOriginDestinationId(
$db->getConnect(),
$origin, $destination, $transport_provider_id,
$limit, $offset
);
if(is_array($quotes) && count($quotes)>0) {
return $this->response(array(
'origin' => $origin,
'destination' => $destination,
'count' => count($quotes),
'limit' => $limit,
'offset' => $offset,
'total' => $total,
'quotes' => $quotes
), 200);
}
}
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/quote/1
* @return string
*/
public function viewAction() {
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
$message = 'Data not found';
$code = 404;
try {
if($id && (int)$id>0) {
$db = new Db();
$quote = QuoteApi::viewReal($db, $id);
if (is_array($quote)) {
return $this->response($quote, 200);
}
}
} catch (Exception $e) {
$code = 500;
$messge = $e->getMessage();
}
return $this->response(
array(
'error'=> $message
), $code);
}
public static function viewReal($db, $id) {
$quote = Quotes::getById($db->getConnect(), (int)$id);
if(is_array($quote) && count($quote)>0) {
$log = [
'message' => 'viewReal quote',
'data' => $quote
];
Logger::debug($log);
$quote["deeplink"] = Quotes::deeplink($db->getConnect(), $quote);
// Check "completed"
if ($quote["completed"]!="" && $quote['travel_date']!="" && (int)$quote["cost"]!=0) {
/* $log = [
'message' => 'completed quote',
'data' => $quote
];
Logger::debug($log); */
return $quote; // OK!
}
// Call service if needed
if ($quote["automation_id"]>0) {
list($quoteService, $err) = QuoteAPI::checkQuote($quote["automation_id"]);
// Update
if (is_array($quoteService) && $quoteService["complete"]!="") {
$quoteService["automation_id"] = $quoteService["id"];
$quoteService["id"] = (int)$id;
$quoteService["completed"] = "now()";
$timezone = Address::getTzByAddressId($db->getConnect(),$quote["location_start_id"]);
$quoteService["travel_date"] = Utilities::convertUtcToLocal($quoteService["complete"],$timezone);
$quoteService["created"] = $quote["created"];
$quoteService["location_start_id"] = $quote["location_start_id"];
$quoteService["location_end_id"] = $quote["location_end_id"];
$quoteService["member_id"] = $quote["member_id"];
$quoteService["deeplink"] = $quote["deeplink"];
$log = [
'message' => 'QuoteApi::viewReal update',
'data' =>$quoteService
];
Logger::debug($log);
list ($res, $err) = Quotes::update($db->getConnect(), $quoteService);
/* $log = [
'message' => 'QuoteApi::viewReal update response',
'data' =>$res,
'error' =>$err
];
Logger::debug($log); */
if ($quoteService["cost"]==0 &&
$quoteService["message"]=="android_automation_job_detail") {
// We must take the details
$quoteService = QuoteApi::quoteDetails($db, $quoteService);
return $quoteService;
}
return $quoteService;
}
}
/* $log = [
'message' => 'can not checkquote from android_automation',
'data' =>$quote
];
Logger::debug($log); */
return $quote;
}
return null;
}
public function quoteDetails($db, $quoteService) {
list($details,$err) = self::getQuoteDetails($quoteService["automation_id"]);
if ($details==NULL) {
error_log($err);
$quoteService['error'] = $err;
}
//$quoteService['details'] = $details;
$low_estimate = $quoteService['cost'];
foreach ($details as $item) {
$c_low_estimate = sprintf("%0.02f", 0.01*$item["low_estimate"]);
// We show GrabShare only for now!
if ($c_low_estimate==0 || $item["name"]!='JustGrab') continue;
if ($low_estimate==0 || $low_estimate>$c_low_estimate) {
error_log("quoteDetails: ".$item["name"]." => ".$c_low_estimate);
$low_estimate = $c_low_estimate;
}
}
$quoteService['cost'] = $low_estimate;
// Update cost
$log = [
'message' => 'get automation detail and update quotes',
'data' =>$quoteService
];
Logger::debug($log);
list ($res, $err) = Quotes::update($db->getConnect(), $quoteService);
/* $log = [
'message' => 'QuoteApi::getQuoteDetails update response',
'data' =>$res,
'error' =>$err
];
Logger::debug($log); */
return $quoteService;
}
public static function createReal($db, $origin, $destination, $member_id, $transport_provider_id, $quote_group_id, $trackedemail_item_id, $country=GeocodeApi::DEFAULT_COUNTRY_CODE, $prefill='f',$pool=1) {
$originStr = is_array($origin) ? json_encode($origin) : $origin;
$destinationStr = is_array($destination) ? json_encode($destination) : $destination;
syslog(LOG_WARNING,"QuoteApi::createReal(\$db, $originStr, $destinationStr, $member_id, $transport_provider_id, $quote_group_id, $trackedemail_item_id, $country, $prefill, $pool");
$message = "Failed to schedule quote";
$code = 500;
$action = "";
$data = [];
try {
if (!in_array($transport_provider_id, [1,2,3,4,5,8])) {
throw new Exception('Unsupported transport provider ID');
}
if (!$destination || !$origin) {
throw new Exception('Invalid origin and/or destination address');
}
$originData= NULL;
$destinationData = NULL;
if (is_array($origin)) {
$address = $origin["address"];
if (0 === count(array_diff(['lat', 'lng', 'address', 'postal', 'timeZoneId', 'country'], array_keys($origin)))) {
$originData = $origin;
}
$origin = $address;
}
if (is_array($destination)) {
$address = $destination["address"];
if (0 === count(array_diff(['lat', 'lng', 'address', 'postal', 'timeZoneId', 'country'], array_keys($destination)))) {
$destinationData = $destination;
}
$destination = $address;
}
// Check if exists
if (isset($originData) && isset($destinationData)) {
$quoteUpdate = Quotes::getLastQuoteByOriginDestinationCoordinates($db->getConnect(),
$originData, $destinationData, $transport_provider_id,SELF::CACHE_TIMEOUT);
} else {
$quoteUpdate = Quotes::getLastQuoteByOriginDestinationId($db->getConnect(),
$origin, $destination, $transport_provider_id,SELF::CACHE_TIMEOUT);
}
/* $log = [
'message' => 'QuoteApi::createReal getLastQuoteByOriginDestinationId',
'quote' => $quoteUpdate
];
Logger::debug($log); */
if(isset($quoteUpdate)) {
$id = $quoteUpdate["id"];
$completed = $quoteUpdate["completed"];
if(empty($quoteUpdate['travel_date']) && $completed != "") {
if (isset($originData)) {
$timezone = $originData['timeZoneId'];
} else {
$timezone = Address::getTzByAddressId($db->getConnect(),$quoteUpdate["location_start_id"]);
}
$quoteUpdate["travel_date"] = Utilities::convertUtcToLocal($completed,$timezone);
list ($res, $err) = Quotes::update($db->getConnect(), $quoteUpdate);
/* $log = [
'message' => 'QuoteApi::createReal getQuoteFromCache update response',
'data' => $res,
'errror' => $err
];
Logger::debug($log); */
}
return ["Quote from cache",200,"view",["id"=>$id]];
}
// Geocode origin and destination
$headers = getallheaders();
$client_id = isset($headers['client_id'])?$headers['client_id']:"Unknown";
if (!isset($originData)) {
list($originData, $err) = GeocodeAPI::geocode($db, $origin, $country,$client_id);
/* $log = [
'message' => 'QuoteApi::geocode origin',
'data' => ['client_id'=>$client_id, 'address_input'=>$origin],
'errror' => $err
];
Logger::debug($log); */
if ($err || !isset($originData["lat"])) throw new Exception("Origin geocoding failed: ".$err);
}
if (!isset($destinationData)) {
list($destinationData, $err) = GeocodeAPI::geocode($db, $destination, $country,$client_id);
/* $log = [
'message' => 'QuoteApi::geocode destination',
'data' => ['client_id'=>$client_id, 'address_input'=>$destination],
'errror' => $err
];
Logger::debug($log); */
if ($err || !isset($destinationData["lat"])) throw new Exception("Destination geocoding failed: ".$err);
}
$availableTransportProviders = Gis::getCityServicesAvailableForCoordinates($db->getConnect(), $originData["lat"], $originData["lng"],[$transport_provider_id]);
if (count($availableTransportProviders) == 0) {
$availableTransportProviders = Gis::getCountryServicesAvailableForCoordinates( $db->getConnect(), $originData["lat"], $originData["lng"],[$transport_provider_id]);
}
if (count($availableTransportProviders) == 0) {
$message = "No transport providers available for origin coordinates (".$originData["lat"].",".$originData["lng"].").";
throw new Exception($message);
}
$distance = Gis::cosinesDistanceBetweenTwoGpsCoordinates($originData["lat"], $originData["lng"], $destinationData["lat"], $destinationData["lng"], "K");
$isDistanceRestricted = false;
switch ($transport_provider_id) {
case Quotes::VENDOR_UBER:
$isDistanceRestricted = $distance > QuoteApi::UBER_DISTANCE_RESTRICTION;
break;
case Quotes::VENDOR_LYFT:
$isDistanceRestricted = $distance > QuoteApi::LYFT_DISTANCE_RESTRICTION;
break;
case Quotes::VENDOR_GRAB: case Quotes::VENDOR_GOJEK: case Quotes::VENDOR_COMFORTDELGRO:
$isDistanceRestricted = $distance > QuoteApi::SINGAPORE_DISTANCE_RESTRICTION;
break;
case Quotes::VENDOR_AUTOCAB:
$isDistanceRestricted = $distance > QuoteApi::AUTOCAB_DISTANCE_RESTRICTION;
break;
}
if ($isDistanceRestricted) {
$message = "Distance restriction. Could not get quote for (".$originData["lat"].",".$originData["lng"].") and (".$destinationData["lat"].",".$destinationData["lng"]."). The distance (".$distance." km) too long.";
throw new Exception($message);
}
$isOriginWithin = false;
$isDestinationWithin = false;
// Check if we are within Singapore: for Grab, Gojek, CDG
if ($transport_provider_id>2 && $transport_provider_id<6) {
$city = 'Singapore';
$sgLat = 1.352083;
$sgLng = 103.819836;
$radius = 25; // km
$isOriginWithin = GeocodeAPI::withinCity(
$originData["address"],$originData["lat"],$originData["lng"],
$city,$sgLat,$sgLng,$radius);
$isDestinationWithin = GeocodeAPI::withinCity(
$destinationData["address"],$destinationData["lat"],$destinationData["lng"],
$city,$sgLat,$sgLng,$radius);
}
// Uber, Lyft and Autocab: no check required
if (in_array($transport_provider_id, [1,2,8])) {
$isOriginWithin = true;
$isDestinationWithin = true;
}
if ($isOriginWithin && $isDestinationWithin) {
syslog(LOG_WARNING,"'".$originData["address"]."' and '".$destinationData["address"]."' are within Singapore");
} else {
throw new Exception("Origin and/or destination is not within service area!");
}
// Schedule the quote
$requestData = array(
"location_start" => $originData["address"] ?? $origin,
"location_end" => $destinationData["address"] ?? $destination,
"trackedemail_item_id" => $trackedemail_item_id,
"transport_provider_id" => $transport_provider_id,
"location_start_lat" => $originData["lat"],
"location_start_lng" => $originData["lng"],
"location_start_country" => $originData["country"],
"location_end_lat" => $destinationData["lat"],
"location_end_lng" => $destinationData["lng"],
"location_end_country" => $destinationData["country"],
"pool" => $pool
);
list($res, $err) = QuoteAPI::scheduleQuote($requestData);
if ($res==NULL || !isset($res["id"])) {
throw new Exception("Quote schedule failed: " . ($err ?? $message));
}
// Save to DB
$requestData["location_start_tz"] = $originData["timeZoneId"];
$requestData["location_start_postal"] = $originData["postal"];
$requestData["location_start_country"] = $originData["country"];
$requestData["location_end_tz"] = $destinationData["timeZoneId"];
$requestData["location_end_postal"] = $destinationData["postal"];
$requestData["location_end_country"] = $destinationData["country"];
$requestData["automation_id"] = $res["id"];
$requestData["member_id"] = (int)$member_id;
$requestData["quote_group_id"] = (int)$quote_group_id;
$requestData["prefill"] = $prefill;
$requestData["pool"] = $pool;
error_log('quote_group_id='.((int)$quote_group_id));
unset($requestData["trackedemail_item_id"]);
$log = [
'message' => 'createReal quote',
'data' =>$requestData
];
Logger::debug($log);
list($quote,$err) = Quotes::create($db->getConnect(), $requestData);
/* $log = [
'message' => 'createReal Quotes::create response',
'quote' => $quote
];
Logger::debug($log); */
if ($quote!=NULL && is_array($quote)) {
$quote["deeplink"] = Quotes::deeplink($db->getConnect(), $quote, $requestData);
return ["Quote scheduled",200,"response",$quote];
} else {
$message = "Failed to create quote record: ".$err;
}
} catch (Exception $e) {
syslog(LOG_WARNING,json_encode($e));
$message = $e->getMessage();
}
syslog(LOG_WARNING,$message);
return [$message,$code,$action,$data];
}
public function createAction() {
syslog(LOG_WARNING,"QuoteApi::createAction()");
$origin = $this->requestParams["origin"] ?? "";
$destination = $this->requestParams["destination"] ?? "";
$member_id = $this->requestParams["member_id"] ?? 0;
$country = $this->requestParams["country"] ?? GeocodeApi::DEFAULT_COUNTRY_CODE;
$transport_provider_id = $this->requestParams["transport_provider_id"] ?? 0;
$group_quote_id = $this->requestParams["group_quote_id"] ?? 0;
$trackedemail_item_id = $this->requestParams["trackedemail_item_id"] ?? 0;
$prefill = $this->requestParams["prefill"] ?? 'f';
$pool = $this->requestParams["pool"] ?? 1;
if ($prefill!='t') $prefill = 'f';
// if ($pool!=2) $pool = 1; // 1 - main pool, 2 - dedicated pool
$db = new Db();
// DEBUG
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
list($message,$code,$action,$data) = QuoteApi::createReal(
$db, $origin, $destination, $member_id, $transport_provider_id,
$group_quote_id, $trackedemail_item_id, $country, $prefill, $pool
);
syslog(LOG_WARNING,"message=${message}");
syslog(LOG_WARNING,"code=${code}");
syslog(LOG_WARNING,"action=${action}");
syslog(LOG_WARNING,"data=".json_encode($data));
if ($code==200) {
if ($action=="view") {
$id = $data["id"];
array_unshift($this->requestUri, $id);
return $this->viewAction();
} else if ($action=="response") {
return $this->response($data, 200);
}
}
return $this->response(
array(
"error" => $message
), $code);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public static function getRideshareQuote($db, $origin, $destination, $member_id, $transport_provider_id, $country) {
$group_quote_id = 0;
$trackedemail_item_id = 0;
$prefill = 'f';
$id = 0;
list($message,$code,$action,$data) = QuoteApi::createReal(
$db, $origin, $destination, $member_id, $transport_provider_id,
$group_quote_id, $trackedemail_item_id, $country, $prefill
);
if ($code==200) {
// Do we have a cost? Most likely not now...
if (is_array($data) && array_key_exists("cost",$data)) {
return [$data["cost"],$data["id"]];
}
if ($action=="view") {
// "View" quote
sleep(5);
$quote = QuoteApi::viewReal($db, $data["id"]);
if (is_array($quote) && array_key_exists("cost",$quote)) {
return [$quote["cost"],$data["id"]];
}
}
if (is_array($data) && array_key_exists("id",$data) && $data["id"]>0) {
$id = $data["id"]; // No id - no way to check what is going on
sleep(5); // wait a second...
// Try to "view" quote
$quote = QuoteApi::viewReal($db, $data["id"]);
if (is_array($quote) && array_key_exists("cost",$quote)) {
return [$quote["cost"],$data["id"]];
}
}
}
return [0,$id];
}
public function checkQuote($id) {
return Quotes::checkQuote($id);
}
public function getQuoteDetails($id) {
return Quotes::getQuoteDetails($id);
}
/*
curl -d '{
"location_start":"Marina Bay Sands, 10 Bayfront Ave, Singapore 018956",
"location_end":"97 Meyer Road, Singapore",
"trackedemail_item_id":"15549",
"transport_provider_id":"3",
"location_start_lat":"1.2833754",
"location_start_lng":"103.8607264",
"location_end_lat":"1.2967624",
"location_end_lng":"103.8927854"
}' \
-H "Authorization: Server-Token 99dfe35fcb7de1ee" -H "Content-Type: application/json" \
-X POST http://automation.float.sg/api/automation-job
*/
public function scheduleQuote($data) {
syslog(LOG_WARNING,'QuoteApi::scheduleQuote($data)');
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.automation_api_token');
$automation_api_url = $savvyext->cfgReadChar('system.automation_api_url');
$url = $automation_api_url."automation-job/";
$opts = array(
'http' => array(
'method' => "POST",
'header' =>
"Content-Type: application/json\r\n" .
"Accept: application/json\r\n" .
"Authorization: Server-Token ${httpAuthToken}\r\n",
'content' => json_encode($data)
),
"ssl" => array(
"verify_peer"=>false,
"verify_peer_name"=>false,
)
);
$context = stream_context_create($opts);
$body = file_get_contents($url, false, $context);
$schedule = json_decode($body,true);
if (is_array($schedule) && isset($schedule["id"])) {
return array($schedule, NULL);
} else if (is_array($schedule) && isset($schedule["message"])) {
$body = $schedule["message"];
syslog(LOG_WARNING,isset($schedule["error"]) ? json_encode($schedule["error"]) : $schedule["message"]);
}
syslog(LOG_WARNING,$body);
return array(NULL, $body);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
class QuoteGroup {
const BASE_SQL_DATA = "SELECT a.*,ls.address AS location_start,ls.latitude AS location_start_lat,ls.longitude AS location_start_lng,ls.timezone AS location_start_tz,ls.geocoding_date AS location_geocoding_date,le.address AS location_end,le.latitude AS location_end_lat,le.longitude AS location_end_lng ";
const BASE_SQL_FROM = " FROM quote_group a, address ls, address le WHERE ls.id=a.location_start_id AND le.id=a.location_end_id";
public function getById($db, $id) {
$q = self::BASE_SQL_DATA . self::BASE_SQL_FROM . " AND a.id=${id} ";
$r = pg_query($db, $q);
error_log($q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
return $f;
}
error_log("ERROR? ".pg_last_error($db));
return [];
}
public function getByOriginDestinationId($db, $origin, $destination, $tpid, $limit, $offset) {
$results = [];
$total = 0;
$db_origin = pg_escape_string(strtolower($origin));
$db_destination = pg_escape_string(strtolower($destination));
$q = "SELECT count(*) ".self::BASE_SQL_FROM." AND lower(ls.address)='${db_origin}'
AND lower(le.address)='${db_destination}'";
if ($tpid>0) {
$q.= " AND a.transport_provider_id=${tpid}";
}
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$total = $f[0];
}
if ($total>0 && $offset<$total) {
$q = self::BASE_SQL_DATA . self::BASE_SQL_FROM . " AND lower(ls.address)='${db_origin}'";
$q.= " AND lower(le.address)='${db_destination}' ".($tpid>0?" AND a.transport_provider_id=${tpid}":"");
$q.= " ORDER BY a.created DESC LIMIT ${limit} OFFSET ${offset}";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
while ($f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$results[] = $f;
}
}
}
return array($total, $results);
}
public function create($db, $data) {
syslog(LOG_WARNING,'QuoteGroup::create()');
$location_start_id = Address::getAddress($db, $data, "location_start");
$location_end_id = Address::getAddress($db, $data, "location_end");
if ($location_start_id==NULL || $location_end_id==NULL) {
syslog(LOG_WARNING,json_encode($data));
return array(NULL,"Failed to save address");
}
$transport_providers = count($data["transport_providers"]);
$q = "INSERT INTO quote_group (location_start_id,location_end_id,transport_providers,processed)
VALUES(${location_start_id},${location_end_id},${transport_providers},0
) RETURNING id";
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return array(self::getById($db, $f["id"]),NULL);
}
return array(NULL,pg_last_error($db));
}
public function update($db, $data) {
error_log('QuoteGroup::update()');
// transport_providers='".$data["transport_providers"]."',
// created='".$data["created"]."',
$q = "UPDATE quote_group SET cost=".($data["cost"]==""?"NULL":((float)$data["cost"])).",";
$q.= " transport_provider_id=".($data["transport_provider_id"]==""?"NULL":((int)$data["transport_provider_id"])).",";
$q.= " quote_id=".($data["quote_id"]==""?"NULL":((int)$data["quote_id"])).",";
$q.= " completed=".($data["completed"]==""?"NULL":("'".$data["completed"]."'")).",";
$q.= " location_start_id=".$data["location_start_id"].",";
$q.= " location_end_id=".$data["location_end_id"].",";
$q.= " processed=".((int)$data["processed"]);
$q.= " WHERE id='".$data["id"]."' RETURNING id";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$res = self::getById($db, $f["id"]);
error_log('res count => '.count($res));
if (is_array($res) && count($res)) {
return [$res,NULL];
}
return [$data, NULL]; // fallback...
}
return [NULL, pg_last_error($db)];
}
}
// vi:ts=2
+375
View File
@@ -0,0 +1,375 @@
<?php
class QuoteGroupApi extends Api
{
public $apiName = 'quotegroup';
const CACHE_TIMEOUT = 300; // 5 minutes
public function indexAction() {
$message = 'Unhandled exception';
$result = [];
$data = $this->requestParams["data"] ?? [];
error_log(json_encode( $this->requestParams));
try {
if (!is_array($data) || count($data)<1) {
throw new Exception('Data not found');
}
$db = new Db();
foreach ($data as $item) {
$id = $item['quote_group_id'];
if ($id<1) {
$item['quote_group_error'] = 'Missing or invalid quote group ID';
$item['quote_group'] = NULL;
$data[$key] = $item;
continue;
}
list($message,$code,$action,$data) = self::viewReal($db, $id);
if ($code==200) {
$item['quote_group_error'] = NULL;
$item['quote_group'] = $data;
} else {
$item['quote_group_error'] = $message;
$item['quote_group'] = NULL;
}
$result[] = $item;
}
return $this->response($result, 200);
} catch (Exception $e) {
$message = $e->getMessage();
error_log('EXCEPTION: '.$message);
}
return $this->response(
array(
'error' => $message
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/quote/1
* @return string
*/
public function viewAction() {
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
$db = new Db();
list($message,$code,$action,$data) = self::viewReal($db, $id);
if ($code==200) {
return $this->response($data, 200);
}
return $this->response(
array(
'error'=> $message
), $code);
}
public static function viewReal($db, $id) {
error_log('QuoteGroupApi::viewReal()');
$message = 'Data not found';
$code = 404;
$action = '';
$data = [];
try {
if(!$id || (int)$id<1) {
throw new Exception('Invalid quote group ID');
}
$quoteGroup = QuoteGroup::getById($db->getConnect(), (int)$id);
if(!is_array($quoteGroup) || count($quoteGroup)<1) {
throw new Exception('Quote group was not found');
}
// Check "completed"
if ($quoteGroup["completed"]!="" && (int)$quoteGroup["cost"]!=0) {
error_log('Quote group #'.$id.' was completed with the cost '.$quoteGroup["cost"]);
list($quotes,$err) = Quotes::getByQuoteGroupId($db->getConnect(), (int)$id);
if ((!is_array($quotes) || count($quotes)<1) && $quoteGroup["quote_id"]>0) {
$quotes = [];
array_push($quotes, Quotes::getById($db->getConnect(), $quoteGroup["quote_id"]));
}
$quoteGroup["quotes"] = $quotes; // Does not matter much if we have "cost"
$quoteGroup["deeplink"] = Quotes::deeplink($db->getConnect(), $quoteGroup);
return ["OK",200,"",$quoteGroup]; // OK!
} else {
error_log('Quote group #'.$id.' was completed yet...');
}
// Load quotes
list($quotes,$err) = Quotes::getByQuoteGroupId($db->getConnect(), (int)$id);
if (!is_array($quotes) || count($quotes)<1) {
if ($quoteGroup["quote_id"]>0) {
$quotes = [];
array_push($quotes, Quotes::getById($db->getConnect(), $quoteGroup["quote_id"]));
error_log(json_encode($quotes));
} else {
throw new Exception ($err!=''?$err:'Failed to load quotes');
}
} else {
error_log(json_encode($quotes));
}
// Check each quote
$completed = 0;
$lowestQuote = NULL;
$cost = PHP_INT_MAX;
foreach ($quotes as $quote) {
for ($i=0; $i<3; $i++) {
if ($quote["completed"]=="" && $quote["travel_date"]=="" && $quote["automation_id"]>0) {
if ($i>0) {
sleep(3); // wait...
}
// Call service if needed
list($quoteService, $err) = QuoteAPI::checkQuote($quote["automation_id"]);
// Update
if (is_array($quoteService) && $quoteService["complete"]!="") {
$quoteService["automation_id"] = $quoteService["id"];
$quoteService["id"] = (int)$quote["id"];
$quoteService["completed"] = $quoteService["complete"];
$timezone = Address::getTzByAddressId($db->getConnect(),$quote["location_start_id"]);
$quoteService["travel_date"] = Utilities::convertUtcToLocal($quoteService["complete"],$timezone);
$quoteService["created"] = $quote["created"];
$quoteService["location_start_id"] = $quote["location_start_id"];
$quoteService["location_end_id"] = $quote["location_end_id"];
$quoteService["member_id"] = $quote["member_id"];
$quoteService["deeplink"] = Quotes::deeplink($db->getConnect(), $quote);
list ($res, $err) = Quotes::update($db->getConnect(), $quoteService);
if ($quoteService["cost"]==0 &&
$quoteService["message"]=="android_automation_job_detail") {
// We must take the details
$quoteService = QuoteApi::quoteDetails($db, $quoteService);
}
$quote = $quoteService;
}
} else {
break;
}
}
// Check "completed"
if ($quote["completed"]!="" && $quote["travel_date"]!="" && (int)$quote["cost"]!=0) {
error_log('Completed!');
$completed++;
if ($quote["cost"] < $cost) {
$cost = $quote["cost"];
$lowestQuote = $quote;
}
}
}
$quoteGroup["cost"] = $cost==PHP_INT_MAX ? NULL : $cost;
$quoteGroup["transport_provider_id"] = is_array($lowestQuote) ? $lowestQuote["transport_provider_id"] : NULL;
$quoteGroup["quote_id"] = is_array($lowestQuote) ? $lowestQuote["id"] : NULL;
$quoteGroup["processed"] = $completed;
// All completed?
if ($completed>=$quoteGroup["transport_providers"]) {
// All quotes completed - hurray!
$quoteGroup["completed"] = date("Y-m-d H:i:s");
} else if (is_array($lowestQuote)) {
// We will update since we have at least one quote completed
$quoteGroup["completed"] = NULL;
} else {
// Do not update, as nothing has completed
$quoteGroup["quotes"] = $quotes;
return ["OK",200,"",$quoteGroup];
}
list($res,$err) = QuoteGroup::update($db->getConnect(), $quoteGroup);
if ($res==NULL || !array_key_exists("id",$res)) {
error_log('QuoteGroup::update() failed?');
error_log(json_encode($res));
$res = $quoteGroup;
}
$res["quotes"] = $quotes;
if (isset($res["transport_provider_id"]) && $res["transport_provider_id"]>0) {
$res["deeplink"] = Quotes::deeplink($db->getConnect(), $res);
}
error_log(json_encode($res));
return ["OK",200,"",$res];
} catch (Exception $e) {
$code = 500;
$message = $e->getMessage();
error_log('EXCEPTION: '.$message);
}
return [$message,$code,$action,$data];
}
public function createAction() {
$origin = $this->requestParams["origin"] ?? "";
$destination = $this->requestParams["destination"] ?? "";
$member_id = $this->requestParams["member_id"] ?? 0;
$country = $this->requestParams["country"] ?? GeocodeApi::DEFAULT_COUNTRY_CODE;
$transport_providers = $this->requestParams["transport_providers"] ?? [];
$db = new Db();
// DEBUG
$country = Geocode::mockGPSCountry($db->getConnect(), $member_id, $country, 'default');
list($message,$code,$action,$data) = QuoteGroupApi::createReal(
$db, $origin, $destination, $member_id, $transport_providers, $country
);
if ($code==200) {
if ($action=="view") {
$id = $data["id"];
array_unshift($this->requestUri, $id);
return $this->viewAction();
} else if ($action=="response") {
return $this->response($data, 200);
}
}
return $this->response(
array(
"error" => $message
), $code);
}
public static function createReal($db, $origin, $destination, $member_id, $transport_providers, $country=GeocodeApi::DEFAULT_COUNTRY_CODE) {
syslog(LOG_WARNING,"QuoteGroupApi::createReal(\$db, $origin, $destination, $member_id, \$transport_providers, $country)");
$message = "Failed to schedule quotes";
$code = 500;
$action = "";
$data = [];
$city = '';
try {
foreach ($transport_providers as $key=>$transport_provider_id) {
if (!in_array($transport_provider_id, [1,2,3,4,5,8])) {
unset($transport_providers[$key]);
}
if (in_array($transport_provider_id, [3,4,5])) {
$city = 'Singapore';
}
if (in_array($transport_provider_id, [1,2,8])) {
$city = 'Other';
}
}
if (count($transport_providers)<1) {
throw new Exception('Unsupported transport provider IDs');
}
if (!$destination || !$origin) {
throw new Exception('Invalid origin and/or destination address');
}
// Check if exists
list($total, $quoteGroups) = QuoteGroup::getByOriginDestinationId(
$db->getConnect(),
$origin, $destination, 0 /* transport_provider_id */,
1, 0); // LIMIT # OFFSET #
if (is_array($quoteGroups) && count($quoteGroups)>0) {
$quoteGroup = $quoteGroups[0];
$id = $quoteGroup["id"]; // Will return the last quote
$completed = $quoteGroup["completed"]; // Completed with the SELF::CACHE_TIMEOUT
if (time()-strtotime($completed)<SELF::CACHE_TIMEOUT) {
return ["Quote from cache",200,"view",["id"=>$id]];
}
}
// Geocode origin and destination
$headers = getallheaders();
$client_id = isset($headers['client_id'])?$headers['client_id']:"Unknown";
list($originData, $err) = GeocodeAPI::geocode($db, $origin, $country,$client_id);
$log = [
'message' => 'QuoteGroupApi::geocode origin',
'data' => ['client_id'=>$client_id, 'address_input'=>$origin],
'errror' => $err
];
Logger::debug($log);
if ($err || !isset($originData["lat"])) throw new Exception("Origin geocoding failed: ".$err);
list($destinationData, $err) = GeocodeAPI::geocode($db, $destination, $country, $client_id);
$log = [
'message' => 'QuoteGroupApi::geocode destination',
'data' => ['client_id'=>$client_id, 'address_input'=>$destination],
'errror' => $err
];
Logger::debug($log);
if ($err || !isset($destinationData["lat"])) throw new Exception("Destination geocoding failed: ".$err);
$isOriginWithin = false;
$isDestinationWithin = false;
// Check if we are within Singapore: for Grab, Gojek, CDG
if ($city=='Singapore') {
$sgLat = 1.352083;
$sgLng = 103.819836;
$radius = 25; // km
$isOriginWithin = GeocodeAPI::withinCity(
$originData["address"],$originData["lat"],$originData["lng"],
$city,$sgLat,$sgLng,$radius);
$isDestinationWithin = GeocodeAPI::withinCity(
$destinationData["address"],$destinationData["lat"],$destinationData["lng"],
$city,$sgLat,$sgLng,$radius);
}
// Uber & Lyft: no check required
if ($city=='Other') {
$isOriginWithin = true;
$isDestinationWithin = true;
}
if ($isOriginWithin && $isDestinationWithin) {
syslog(LOG_WARNING,"'".$originData["address"]."' and '".$destinationData["address"]."' are within Singapore");
} else {
throw new Exception("Origin and/or destination is not within the service area!");
}
// Create quote group
$requestData = [
"location_start_lat" => $originData["lat"],
"location_start_lng" => $originData["lng"],
"location_start" => $originData["address"] ?? $origin,
"location_start_country" => $originData["country"],
"location_end_lat" => $destinationData["lat"],
"location_end_lng" => $destinationData["lng"],
"location_end" => $destinationData["address"] ?? $destination,
"location_end_country" => $destinationData["country"],
"transport_providers" => $transport_providers
];
list($quoteGroup,$err) = QuoteGroup::create($db->getConnect(), $requestData);
if ($quoteGroup==NULL || !isset($quoteGroup["id"])) {
throw new Exception("Quote group creation failed: " . ($err ?? $message));
}
$id = $quoteGroup["id"];
syslog(LOG_WARNING,'New QuoteGroup ID: '.$id);
// Schedule the quotes
//$quoteApi = new QuoteApi($this->requestUri, false /* no encryption for internal call */);
$quotes = [];
foreach ($transport_providers as $transport_provider_id) {
//$quoteApi->requestParams["transport_provider_id"] = $transport_provider_id;
list($message,$code,$action,$data) = QuoteApi::createReal(
$db, $origin, $destination, $member_id, $transport_provider_id, $id, 0, $country, 'f'
);
$quote_id = 0;
if ($code==200 && ($action=="view" || $action=="response")) {
$quote_id = $data["id"];
$quotes[] = $quote_id;
} else {
syslog(LOG_WARNING,'ERROR: '.$message);
}
syslog(LOG_WARNING,'QUOTE: '.json_encode($data));
}
$quoteGroup["cost"] = NULL;
$quoteGroup["transport_provider_id"] = count($quotes)==1 ? $transport_provider_id : NULL;
$quoteGroup["quote_id"] = count($quotes)==1 ? $quotes[0] : NULL;
$quoteGroup["completed"] = NULL;
if (count($quotes)<1) {
$quoteGroup["completed"] = date("Y-m-d H:i:s");
$quoteGroup["processed"] = 0;
}
list($res,$err) = QuoteGroup::update($db->getConnect(), $quoteGroup);
if ($res==NULL || !isset($res["id"])) {
$res = $quoteGroup;
}
$res["quotes"] = $quotes;
return ["Group quote scheduled",200,"response",$res];
} catch (Exception $e) {
syslog(LOG_WARNING,json_encode($e));
$message = $e->getMessage();
syslog(LOG_WARNING,'EXCEPTION: '.$message);
}
return [$message,$code,$action,$data];
}
public function updateAction() {
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction() {
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
+305
View File
@@ -0,0 +1,305 @@
<?php
class Quotes {
const BASE_SQL_DATA = "SELECT a.*,ls.address AS location_start,ls.latitude AS location_start_lat,ls.longitude AS location_start_lng,ls.timezone AS location_start_tz,ls.geocoding_date AS location_geocoding_date,le.address AS location_end,le.latitude AS location_end_lat,le.longitude AS location_end_lng ";
const BASE_SQL_FROM = " FROM quotes a, address ls, address le WHERE ls.id=a.location_start_id AND le.id=a.location_end_id";
const VENDOR_UBER = 1;
const VENDOR_LYFT = 2;
const VENDOR_GRAB = 3;
const VENDOR_COMFORTDELGRO = 4; // ComfortDelGro
const VENDOR_GOJEK = 5;
const VENDOR_TURO = 6;
const VENDOR_GETAROUND = 7;
const VENDOR_AUTOCAB = 8;
public static function getById($db, $id) {
syslog(LOG_WARNING,"Quote::getById(\$db, $id)");
Logger::debug("Quote::getById(\$db, $id)");
$result = [];
$q = Quotes::BASE_SQL_DATA . Quotes::BASE_SQL_FROM . " AND a.id=${id} ";
$r = pg_query($db, $q);
//syslog(LOG_WARNING,$q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result = $f;
}
return $result;
}
public static function getByOriginDestinationId($db, $origin, $destination, $tpid, $limit, $offset) {
syslog(LOG_WARNING,"Quote::getByOriginDestinationId(\$db, $origin, $destination, $tpid, $limit, $offset)");
$results = [];
$total = 0;
$db_origin = pg_escape_string(strtolower($origin));
$db_destination = pg_escape_string(strtolower($destination));
$q = "SELECT count(*) ".Quotes::BASE_SQL_FROM." AND lower(ls.address)='${db_origin}'
AND lower(le.address)='${db_destination}'";
if ($tpid>0) {
$q.= " AND a.transport_provider_id=${tpid}";
}
$q.= " AND a.cost>0";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$total = $f[0];
}
if ($total>0 && $offset<$total) {
$q = Quotes::BASE_SQL_DATA . Quotes::BASE_SQL_FROM . " AND lower(ls.address)='${db_origin}' ";
$q.= " AND lower(le.address)='${db_destination}' ".($tpid>0?" AND a.transport_provider_id=${tpid}":"");
$q.= " AND a.cost>0";
$q.= " ORDER BY a.created DESC LIMIT ${limit} OFFSET ${offset}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
while ($f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$results[] = $f;
}
}
}
return array($total, $results);
}
public static function getLastQuoteByOriginDestinationId($db, $origin, $destination, $tpid, $cacheTimeout = 300) {
$db_origin = pg_escape_string(strtolower($origin));
$db_destination = pg_escape_string(strtolower($destination));
$q = Quotes::BASE_SQL_DATA . Quotes::BASE_SQL_FROM;
$q.= " AND a.completed > now() - ( ${cacheTimeout} ||' seconds')::interval ";
$q.= " AND lower(ls.address)='${db_origin}' ";
$q.= " AND lower(le.address)='${db_destination}' ".($tpid>0?" AND a.transport_provider_id=${tpid}":"");
$q.= " AND a.cost>0";
$q.= " ORDER BY a.created DESC LIMIT 1 OFFSET 0";
$result = NULL;
$r = pg_query($db, $q);
if ( $r && pg_num_rows( $r ) && $f = pg_fetch_assoc( $r ) ) {
$result = $f;
}
return $result;
}
public static function getLastQuoteByOriginDestinationCoordinates($db, $origin, $destination, $tpId, $cacheTimeout = 300) {
if (!is_array($origin) || !is_array($destination)
|| !isset($origin['lat'])|| !isset($origin['lng'])
|| !isset($destination['lat'])|| !isset($destination['lng'])
) {
return NULL;
}
$origin_lat = $origin['lat'];
$origin_lng = $origin['lng'];
$destination_lat = $destination['lat'];
$destination_lng = $destination['lng'];
$q = Quotes::BASE_SQL_DATA . Quotes::BASE_SQL_FROM;
$q.= " AND a.completed > now() - ( ${cacheTimeout} ||' seconds')::interval ";
$q.= " AND ls.geometry = ST_SetSRID(ST_MakePoint( ${origin_lng}, ${origin_lat}), 4326)::geography ";
$q.= " AND le.geometry = ST_SetSRID(ST_MakePoint( ${destination_lng}, ${destination_lat}), 4326)::geography";
$q.= ($tpId>0?" AND a.transport_provider_id=${tpId}":"");
$q.= " AND a.cost>0";
$q.= " ORDER BY a.created DESC LIMIT 1 OFFSET 0";
$result = NULL;
$r = pg_query($db, $q);
if ( $r && pg_num_rows( $r ) && $f = pg_fetch_assoc( $r ) ) {
$result = $f;
}
return $result;
}
public static function getByQuoteGroupId($db, $id) {
$q = Quotes::BASE_SQL_DATA . Quotes::BASE_SQL_FROM . " AND quote_group_id>0 AND quote_group_id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$results = [];
while ($f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$results[] = $f;
}
return array($results, NULL);
}
return array(NULL,pg_last_error($db));
}
public static function create($db, $data) {
syslog(LOG_WARNING,'Quote::create($db, $data)');
$location_start_id = Address::getAddress($db, $data, "location_start");
$location_end_id = Address::getAddress($db, $data, "location_end");
if ($location_start_id==NULL || $location_end_id==NULL) {
return array(NULL,"Failed to save address");
}
$q = "INSERT INTO quotes (transport_provider_id,automation_id,member_id,location_start_id,location_end_id, quote_group_id, prefill, pool) ";
$q.= " VALUES(".((int)$data["transport_provider_id"]).",".((int)$data["automation_id"]).",".((int)$data["member_id"]).",";
$q.= "${location_start_id},${location_end_id},".((int)$data["quote_group_id"]).",".($data["prefill"]!='t'?"'f'":"'t'");
$q.= ",".($data["pool"]!=2?"1":"2");
$q.= ") RETURNING id";
$log = [
'message' => 'insert quote',
'data' =>$data,
'query' =>$q
];
Logger::debug($log);
syslog(LOG_WARNING,'***************************************************************************');
syslog(LOG_WARNING,$q);
syslog(LOG_WARNING,'***************************************************************************');
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [Quotes::getById($db, $f["id"]),NULL];
}
syslog(LOG_WARNING,pg_last_error($db));
return [NULL,pg_last_error($db)];
}
public static function update($db, $data) {
syslog(LOG_WARNING,'Quotes::update(): '.json_encode($data));
// cost_raw='".$data["cost_raw"]."',
// created='".$data["created"]."',
// member_id='".$data["member_id"]."'
$q = "UPDATE quotes SET
transport_provider_id=".((int)$data["transport_provider_id"]).",
automation_id=".((int)$data["automation_id"]).",
cost=".((float)$data["cost"]).",
completed='".$data["completed"]."',
travel_date='".$data["travel_date"]."',
location_start_id=".$data["location_start_id"].",
location_end_id=".$data["location_end_id"]."
WHERE id='".$data["id"]."'";
//error_log($q);
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
/* $log = [
'affected_row' => pg_affected_rows($r),
'message' => 'update quote',
'data' =>$data,
'query' =>$q
];
Logger::debug($log); */
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return array(self::getById($db, $data["id"]),NULL);
}
return array(NULL, pg_last_error($db));
}
public static function getLegStepQuoteBySID($db, $sid) {
$q = "SELECT * FROM leg_step_quote WHERE google_directions_leg_step_id=".((int)$sid);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [$f, NULL];
}
return [NULL, pg_last_error($db)];
}
public static function deeplink($db, $quote) {
syslog(LOG_WARNING,"Quote::deeplink(\$db, \$quote)");
Logger::debug("Quote::deeplink(\$db, \$quote)");
$url = "";
switch ($quote["transport_provider_id"]) {
case Quotes::VENDOR_UBER: // Uber
// https://developer.uber.com/docs/riders/ride-requests/tutorials/deep-links/introduction
$url = sprintf("uber://?client_id=IrcAu0gxX7EsPKWg3QJe8reR5Q8w1jDm&action=setPickup&pickup[latitude]=%f&pickup[longitude]=%f&dropoff[latitude]=%f&dropoff[longitude]=%f",
$quote["location_start_lat"], $quote["location_start_lng"],
$quote["location_end_lat"], $quote["location_end_lng"]
);
break;
case Quotes::VENDOR_LYFT: // Lyft
// https://developer.lyft.com/docs/deeplinking
// &partner=GZbtPgh56RQb
// &credits=<my_custom_promo_code>
$url = sprintf("lyft://ridetype?id=lyft&pickup[latitude]=%f&pickup[longitude]=%f&destination[latitude]=%f&destination[longitude]=%f",
$quote["location_start_lat"], $quote["location_start_lng"],
$quote["location_end_lat"], $quote["location_end_lng"]
);
break;
case Quotes::VENDOR_GRAB: // Grab
// TODO: rotate the sources!
$url = sprintf("grab://open?screenType=BOOKING&sourceId=raZbevC7RB&sourceAppName=SingaporeAir&dropOffLatitude=%f&dropOffLongitude=%f&pickUpLatitude=%f&pickUpLongitude=%f",
$quote["location_end_lat"], $quote["location_end_lng"],
$quote["location_start_lat"], $quote["location_start_lng"]
);
break;
case Quotes::VENDOR_COMFORTDELGRO: // ComfortDelGro
$url = sprintf("ComfortDelGroTaxi:///?action=setBooking&endingLat=%f&endingLong=%f&startingLat=%f&startingLong=%f",
$quote["location_end_lat"], $quote["location_end_lng"],
$quote["location_start_lat"], $quote["location_start_lng"]
);
break;
case Quotes::VENDOR_GOJEK: // Gojek
$url = sprintf("gojek://sggocar/gocar?product_id=car&pickup_latitude=%f&pickup_longitude=%f&dropoff_latitude=%f&dropoff_longitude=%f",
$quote["location_start_lat"], $quote["location_start_lng"],
$quote["location_end_lat"], $quote["location_end_lng"]
);
break;
}
//syslog(LOG_WARNING,"url=${url}");
return $url;
}
public static function checkQuote($id) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.automation_api_token');
$automation_api_url = $savvyext->cfgReadChar('system.automation_api_url');
$url = $automation_api_url."automation-job/" . $id;
$opts = array(
'http' => array(
'method' => "GET",
'header' =>
"Content-Type: application/json\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);
$quote = json_decode($body,true);
if (is_array($quote) && isset($quote["id"])) {
return array($quote, NULL);
} else if (is_array($quote) && isset($quote["message"])) {
$body = $quote["message"];
error_log(isset($quote["error"]) ? json_encode($quote["error"]) : $quote["message"]);
}
return array(NULL, $body);
}
public static function getQuoteDetails($id) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.automation_api_token');
$automation_api_url = $savvyext->cfgReadChar('system.automation_api_url');
$url = $automation_api_url."automation-job-detail/" . $id;
$opts = array(
'http' => array(
'method' => "GET",
'header' =>
"Content-Type: application/json\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);
$details = json_decode($body,true);
if (is_array($details) && count($details)>0 && isset($details[0]["id"])) {
return array($details, NULL);
} else if (is_array($details) && isset($details["message"])) {
$body = $details["message"];
error_log(isset($details["error"]) ? json_encode($details["error"]) : $details["message"]);
}
return array(NULL, $body);
}
}
// vi:ts=2
+65
View File
@@ -0,0 +1,65 @@
<?php
class Radius {
public static function outboundTrips($db, $lat, $lng, $radius) {
return self::radiusTrips($db, $lat, $lng, $radius, 'start');
}
public static function inboundTrips($db, $lat, $lng, $radius) {
return self::radiusTrips($db, $lat, $lng, $radius, 'end');
}
public static function radiusTrips($db, $lat, $lng, $radius, $what) {
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$q = "SELECT * FROM parsedemail_item WHERE ";
$q.= "ST_DWithin(location_${what},ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " ORDER BY id DESC";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public static function anyTrips($db, $lat, $lng, $radius) {
$limit_lat = (float)$lat;
$limit_lng = (float)$lng;
$limit_rad = $radius;
$q = "SELECT * FROM parsedemail_item WHERE ";
$q.= "ST_DWithin(location_start,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " OR ST_DWithin(location_end,ST_SetSRID(ST_MakePoint(${limit_lng},${limit_lat}),4326)::geography,${limit_rad})";
$q.= " ORDER BY id DESC";
error_log($q);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
public static function limitTrips($db, $ids, $limit, $what='travel_date', $how='DESC') {
$q = "SELECT id FROM parsedemail_item WHERE id IN (".implode(",",$ids).") ORDER BY ${what} ${how} LIMIT ${limit}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$result = [];
while ($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
return [$result, NULL];
}
return [NULL,pg_last_error()];
}
}
// vi:ts=2
+127
View File
@@ -0,0 +1,127 @@
<?php
class RadiusApi extends Api
{
public $apiName = 'radius';
const RADIUS_LIMIT = 10000;
const DEFAULT_DIR = 'any';
const DEFAULT_LIMIT = 10;
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/radius/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /radius/x
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
$message = 'Data not found';
$code = 404;
$lat = $this->requestParams['lat'] ?? NULL;
$lng = $this->requestParams['lng'] ?? NULL;
$rad = $this->requestParams['radius'] ?? NULL;
$typ = $this->requestParams['type'] ?? self::DEFAULT_DIR;
$lim = $this->requestParams['limit'] ?? self::DEFAULT_LIMIT;
try {
if ($lat==NULL || $lng==NULL) {
throw new RuntimeException('Invalid GPS coordinates',500);
}
if ($rad<10 || $rad>self::RADIUS_LIMIT) {
throw new RuntimeException('Invalid radius',500);
}
if ($typ!='in' && $typ!='out') {
$typ = self::DEFAULT_DIR;
}
if ($limit<1 || $limit>self::DEFAULT_LIMIT) {
$limit = self::DEFAULT_LIMIT;
}
$db = new Db();
$res = NULL;
$err = NULL;
if ($typ=='in') {
list($res,$err) = Radius::inboundTrips($db->getConnectGPS(), $lat, $lng, $rad);
} else if ($typ=='out') {
list($res,$err) = Radius::outboundTrips($db->getConnectGPS(), $lat, $lng, $rad);
} else {
list($res,$err) = Radius::anyTrips($db->getConnectGPS(), $lat, $lng, $rad);
}
if (!$res) {
if ($err!="") {
throw new RuntimeException($err, 500);
}
throw new RuntimeException('Data not found', 404);
}
$ids = [];
foreach ($res as $f) {
$ids[] = $f["id"];
}
// Get $lim latest trips
list($res,$err) = Radius::limitTrips($db->getConnect(), $ids, $lim, 'travel_date', 'DESC');
if (!$res) {
if ($err!="") {
throw new RuntimeException($err, 500);
}
throw new RuntimeException('Data not found', 404);
}
$trips = [];
foreach ($res as $f) {
$id = $f["id"];
$trip = Trips::getById($db->getConnect(), (int)$id);
if(is_array($trip) && count($trip)>0){
$trips[] = $trip;
}
}
return $this->response(
array(
"lat" => $lat,
"lng" => $lng,
"radius" => $rad,
"type" => $typ,
"limit" => $lim,
"trips" => $trips
), 200);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$code = $e->getCode();
}
return $this->response(
array(
"error" => $message
), $code);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
<?php
class ReportApi extends Api
{
public $apiName = 'report';
const REPORTS = array(
"weeklySpending" => 1,
"spendingByCategoryAllTime" => 1,
"categoryItems" => 1,
"spendingByCategoryLastSevenDays" => 1,
"spendingByWeekdayLastSevenDays" => 1,
"travelTimeBreakdownVsAverage" => 1,
"emissionsByDays" => 1,
"travelTimeByDays" => 1,
);
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/address/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /address/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
/*$db = new Db();
$address = Address::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()
{
$message = "Data not found";
$type = (string)$this->requestParams["type"] ?? "";
$data = $this->requestParams["data"] ?? array();
if ($type!='' && is_array($data) && self::REPORTS[$type]==1) {
$db = new Db();
list ($results, $options) = call_user_func("Report::${type}",
$db->getConnect(), $db->getConnectGPS(), $data);
if($results!=NULL && is_array($results) && count($results)>0){
return $this->response(
array(
'type' => $type,
'data' => $data,
'report' => $results,
'options' => $options
), 200);
} else {
$message = "Report data is not found";
}
} else {
$message = "Invalid input";
}
return $this->response(
array(
"error" => $message
), 404);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
class ReportMock {
public function weeklySpending($db, $data) {
error_log('ReportMock::weeklySpending($db, $data) ');
return [[
[
'dow' => 0,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 1,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 2,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 3,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 4,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 5,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 6,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
],[]];
}
public function spendingByCategoryLastSevenDays($db, $data) {
error_log('ReportMock::spendingByCategoryLastSevenDays($db, $data) ');
return [[
[
'type' => 'RideShare',
'spent' => rand(20,100).'.'.rand(0,99)
],
[
'type' => 'Taxi',
'spent' => rand(20,100).'.'.rand(0,99)
],
[
'type' => 'Scooter',
'spent' => rand(0,20).'.'.rand(0,99)
],
[
'type' => 'Bus',
'spent' => rand(0,50).'.'.rand(0,99)
],
[
'type' => 'Subway',
'spent' => rand(0,50).'.'.rand(0,99)
]
],[]];
}
public function spendingByWeekdayLastSevenDays($db, $data) {
error_log('ReportMock::spendingByWeekdayLastSevenDays($db, $data)');
return [[
[
'dow' => 0,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 1,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 2,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 3,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 4,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 5,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
[
'dow' => 6,
'spent' => sprintf("%0.02f",rand(0,5000)/100.0)
],
],[]];
}
public function travelTimeBreakdownVsAverage($db, $data) {
error_log('ReportMock::travelTimeBreakdownVsAverage($db, $data)');
return [[
[// average
["axis" => "Walk","value" => 0.60],
["axis" => "Bicycle","value" => 0.9],
["axis" => "Scooter","value" => 0.47],
["axis" => "Taxi","value" => 0.60],
["axis" => "My Car","value" => 0.7],
["axis" => "Bus","value" => 0.4],
["axis" => "Train","value" => 0.40],
["axis" => "Plane","value" => 0.60]
],
[// current user
["axis" => "Walk","value" => 0.85],
["axis" => "Bicycle","value" => 0.7],
["axis" => "Scooter","value" => 0.50],
["axis" => "Taxi","value" => 0.75],
["axis" => "My Car","value" => 0.5],
["axis" => "Bus","value" => 0.37],
["axis" => "Train","value" => 0.50],
["axis" => "Plane","value" => 0.40]
]
],[]];
}
}
// vi:ts=2
+149
View File
@@ -0,0 +1,149 @@
<?php
class RouteApi extends Api
{
public $apiName = 'route';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/remove/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /remove/x
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
$message = "Unexpected route error";
$from = $this->requestParams["from"] ?? ",";
$to = $this->requestParams["to"] ?? ",";
$mode = $this->requestParams["mode"] ?? 'DRIVING';
try {
if(!is_array($from) || !is_array($to)){
return $this->response(
array(
"error" => $message
), 500);
}
$fromCoords = implode(',',$from);
$toCoords = implode(',',$to);
list($res, $err) = RouteApi::route($fromCoords, $toCoords, $mode);
if ($err==NULL && is_array($res) && isset($res["routes"]) && count($res["routes"])>0) {
//$result["route_overlay"] = [];
$result = [];
foreach ($res["routes"] as $route) {
$result[] = RouteApi::processRoute($route);
}
return $this->response(array(
'routes' => $result,
'error' =>$err
)
);
} else {
throw new Exception("Failed to get route");
}
} catch (Exception $e) {
$message = $e->getMessage();
}
return $this->response(
array(
"error" => $message
), 500);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public function route($fromCoords, $toCoords, $mode='driving', $waypoints=[]) {
syslog(LOG_WARNING,"Trip RouteApi::route($fromCoords, $toCoords, $mode, \$waypoints=[])");
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
$data = http_build_query(
array(
'gps' => implode(",",array($fromCoords, $toCoords)),
'mode' => $mode,
'waypoints' => implode(",",$waypoints)
)
);
//$data = "gps=".implode(",",array($fromCoords, $toCoords))."&mode=DRIVING";
$url = $oauth2_url."route?" . $data;
$opts = array(
'http' => array(
'method' => "GET",
'timeout' => 60, /* 1 minute */
'header' =>
"Content-Type: text/plain\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);
$route = json_decode($body,true);
//var_dump($route["data"]);exit;
if (is_array($route) && is_array($route["data"]) && !isset($route["error"])) {
return array($route["data"], NULL);
} else if (is_array($route) && isset($route["error"])) {
$body = $route["error"];
}
return array(NULL, "Routing service call error: ".$body);
}
public function processRoute($route) {
require_once('../common/Polyline.php');
$result = [
'distance' => NULL,
'duration' => NULL,
'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"];
}
$result['distance'] = $distance;
$result['duration'] = $duration;
return $result;
}
}
+276
View File
@@ -0,0 +1,276 @@
<?php
class sgbikeApi extends Api
{
public $apiName = 'sgbike';
public function indexAction()
{
$messge = 'Data not found';
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()
{
$message = "Failed to run sgbike directions";
$addresses = $this->requestParams["addresses"] ?? array();
$options = $this->requestParams["options"] ?? array();
try {
throw new Exception("We are sorry but sgbikes are not yet launched in your country.  Please check back again soon!");
if ($addresses==NULL || !is_array($addresses) || count($addresses)<1 || !isset($addresses[0]["address"])) {
throw new Exception("Invalid input");
}
$db = new Db();
$result = [];
$input = [];
foreach ($addresses as $item) {
if (isset($item["type"]) && $item["type"]>0 && $item["type"]<3) {
$input[$item["type"]] = $item;
}
}
if (isset($input[1]) && isset($input[2]) &&
isset($input[1]["address"]) && isset($input[2]["address"])) {
list($result, $err) = self::sgbike($db, $input);
if (!is_array($result) || count($result)<1) {
throw new Exception($err!=''? $err : 'No sgbike option available');
}
} else {
throw new Exception("Invalid address");
}
if ((isset($options["travel_time"]) && $options["travel_time"]) ||
(isset($options["quote"]) && $options["quote"]) ||
(isset($options["route_overlay"]) && $options["route_overlay"])) {
$options = self::options($db, $input, $result, $options);
}
return $this->response(
array(
'data'=>$result,
'options'=>$options), 200);
} catch (Exception $e) {
error_log(json_encode($e));
$message = $e->getMessage();
}
return $this->response(
array(
"error" => $message
), 500);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction() {
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public function sgbike($db, $input) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
// Call sgbike service
$gps1 = $input[1]["coordinates"];
$gps2 = $input[2]["coordinates"];
$data = http_build_query(
array(
'gps' => sprintf("%s,%s,%s,%s",$gps1["lat"],$gps1["lng"],$gps2["lat"],$gps2["lng"]),
'from' => $input[1]["address"],
'to' => $input[2]["address"]
)
);
$url = $oauth2_url."sgbike?" . $data;
//error_log($url);
$opts = array(
'http' => array(
'method' => "GET",
'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);
$sgbike = json_decode($body,true);
if (is_array($sgbike) && is_array($sgbike["data"]) && !isset($sgbike["error"])) {
// Cache the result in DB
//$sgbike["data"]["id"] = sgbike::save($db->getConnect(), $sgbike["data"]);
return array($sgbike["data"], NULL);
} else if (is_array($sgbike) && isset($sgbike["error"])) {
$body = $sgbike["error"];
}
return array(NULL, "sgbike service call error: ".$body);
}
public function options($db, $input, $result, $options) {
if (isset($options["travel_time"]) && $options["travel_time"]) {
$travel_time = 0;
foreach ($result as $step) {
$travel_time += $step["duration"];
}
$options["travel_time"] = (int)$travel_time;
}
if (isset($options["quote"]) && $options["quote"] &&
is_array($result) && count($result)>1 &&
isset($result[1]["duration"]) && $result[1]["duration"]>0) {
$sgbike_time = $result[1]["duration"]; // 2nd entry is the sgbike step
list($res,$err) = self::quote($db, $sgbike_time, 'SG');
if ($err!=NULL) {
$options["error"] = $err;
} else {
$options["quote"] = $res;
}
}
if (isset($options["route_overlay"]) && $options["route_overlay"]) {
$options = self::directions($db, $input, $result, $options);
}
return $options;
}
public function quote($db, $seconds, $country='SG',$step_details=[]) {
global $savvyext;
$sgbike_token = $savvyext->cfgReadChar('system.scooter_token');
// Check local DB cache
/* list ($res, $err) = sgbike::checksgbikeQuote($db->getConnect(), $minutes, $country);
if (is_array($res) && isset($res[0]["quote"])) {
return array($res, NULL);
} */
// Call sgbike quote service
$data = http_build_query(
array(
'minutes' => (int)($seconds/60),
'country' => $country
)
);
$url = $savvyext->cfgReadChar('microservices.catalog') . "/api/v1/bikes/price-quote/?" . $data;
//echo $url;exit;
// https://float.catalog.float.sg/api/v1/bikes/price-quote/?minutes=49&country=SG
// curl -X GET "https://float.catalog.float.sg/api/v1/bikes/price-quote/?minutes=60&country=SG" -H "accept: application/json" -H "Authorization: Server-Token MNHEzG4uEDy47NhZqBnBKM9kvKVGXzC8"
$opts = array(
'http' => array(
'method' => "GET",
'header' =>
"Content-Type: application/x-www-form-urlencoded\r\n" .
"Accept: application/json\r\n" .
"Authorization: Server-Token $sgbike_token\r\n",
),
"ssl" => array(
"verify_peer"=>false,
"verify_peer_name"=>false,
)
);
$context = stream_context_create($opts);
$body = file_get_contents($url, false, $context);
$data = json_decode($body,true);
if (is_array($data) && count($data) > 0 && !isset($data["error"])) {
// Cache the result in DB
$extra_fee_default = 5.0; // default extra fee fall-back
$apply_extra_fee = false;
$duration = ceil($duration/60);//
if (is_array($step_details) && $step_details["line"] == null && $step_details["headsign"] != null) {
$apply_extra_fee = true;
if ($vendor == null) {
$vendor = $step_details["headsign"]; /* The provider will be in the "headsign"
and this means we drop off anywhere */
}
}
if (is_array($data) && count($data) > 0 && !isset($data["error"])) {
if ($vendor != null) {
foreach ($data as $d) {
if (isset($d['provider']) && $d['provider'] == $vendor) {
return ($d["quote"]+ ($apply_extra_fee ? ($d["pricing"]["dropoff_anywhere"] != null ? $d["pricing"]["dropoff_anywhere"] : $extra_fee_default) : 0))*100;
}
}
}
$result = 0.0; // If we do not know the vendor - get the most expensive
foreach ($data as $d) {
$value = $d["quote"];
if ($value > $result) {
$result = $value;
$extra_fee = $d["pricing"]["dropoff_anywhere"] != null ? $d["pricing"]["dropoff_anywhere"] : $extra_fee_default;
}
}
$result += ($apply_extra_fee ? $extra_fee : 0);
}
return array($result*100,null);
} else if (is_array($sgbike) && isset($sgbike["error"])) {
$body = $sgbike["error"];
}
return array(NULL, "sgbike quote service call error: ".$body);
}
public function directions($db, $input, $result, $options) {
// Walking with waypoints or 3 routes
$mode='walking';
$waypoints=[
$result[1]["data"]["location_start_lat"], $result[1]["data"]["location_start_lng"],
$result[1]["data"]["location_end_lat"], $result[1]["data"]["location_end_lng"]
];
$fromLat = $input[1]["coordinates"]["lat"];
$fromLng = $input[1]["coordinates"]["lng"];
$toLat = $input[2]["coordinates"]["lat"];
$toLng = $input[2]["coordinates"]["lng"];
list($res, $err) = GeocodeApi::route(
$db, $fromLat, $fromLng, $toLat, $toLng, $mode, $waypoints);
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;
}
// ["route_overlay"]
return $options;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
class Scooter {
public function getAddressById($db, $id) {
$result = array();
$q = "SELECT * FROM address WHERE id=".((int)$id);
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$f["address"] = html_entity_decode ($f["address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result = $f;
}
return $result;
}
public function save($db, $result) {
return Address::save($db, $result);
}
public function getTimezone($db, $timezone) {
$db_timezone = pg_escape_string($timezone);
$q = "SELECT id FROM address_timezone WHERE timezone='${db_timezone}'";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
return $f[0];
}
$q = "INSERT INTO address_timezone (timezone) VALUES('${db_timezone}') RETURNING id";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
return $f[0];
}
return NULL;
}
public function checkDistanceCache($db, $fromLat, $fromLng, $toLat, $toLng) {
$db_fromLat = (float)$fromLat;
$db_fromLng = (float)$fromLng;
$db_toLat = (float)$toLat;
$db_toLng = (float)$toLng;
$q = "SELECT distance,duration FROM address_distance_cache WHERE ";
$q.= "start_lat=${db_fromLat} AND start_lng=${db_fromLng} AND end_lat=${db_toLat} AND end_lng=${db_toLng}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return array($f, NULL);
}
return array(NULL,pg_last_error($db));
}
public function saveDistanceCache($db, $fromLat, $fromLng, $toLat, $toLng, $data) {
$db_fromLat = (float)$fromLat;
$db_fromLng = (float)$fromLng;
$db_toLat = (float)$toLat;
$db_toLng = (float)$toLng;
$distance = $data["distance"];
$duration = $data["duration"];
$q = "INSERT INTO address_distance_cache (start_lat,start_lng,end_lat,end_lng,distance,duration) VALUES (";
$q.= "${db_fromLat},${db_fromLng},${db_toLat},${db_toLng},${distance},${duration}) RETURNING id";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return array($f, NULL);
}
return array(NULL,pg_last_error($db));
}
}
// vi:ts=2
+243
View File
@@ -0,0 +1,243 @@
<?php
class ScooterApi extends Api
{
public $apiName = 'scooter';
public function indexAction()
{
$messge = 'Data not found';
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()
{
$message = "Failed to run scooter directions";
$addresses = $this->requestParams["addresses"] ?? array();
$options = $this->requestParams["options"] ?? array();
try {
throw new Exception("We are sorry but scooters are not yet launched in your country.  Please check back again soon!");
if ($addresses==NULL || !is_array($addresses) || count($addresses)<1 || !isset($addresses[0]["address"])) {
throw new Exception("Invalid input");
}
$db = new Db();
$result = [];
$input = [];
foreach ($addresses as $item) {
if (isset($item["type"]) && $item["type"]>0 && $item["type"]<3) {
$input[$item["type"]] = $item;
}
}
if (isset($input[1]) && isset($input[2]) &&
isset($input[1]["address"]) && isset($input[2]["address"])) {
list($result, $err) = self::scooter($db, $input);
if (!is_array($result) || count($result)<1) {
throw new Exception($err!=''? $err : 'No scooter option available');
}
} else {
throw new Exception("Invalid address");
}
if ((isset($options["travel_time"]) && $options["travel_time"]) ||
(isset($options["quote"]) && $options["quote"]) ||
(isset($options["route_overlay"]) && $options["route_overlay"])) {
$options = self::options($db, $input, $result, $options);
}
return $this->response(
array(
'data'=>$result,
'options'=>$options), 200);
} catch (Exception $e) {
error_log(json_encode($e));
$message = $e->getMessage();
}
return $this->response(
array(
"error" => $message
), 500);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction() {
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public function scooter($db, $input) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
// Call scooter service
$gps1 = $input[1]["coordinates"];
$gps2 = $input[2]["coordinates"];
$data = http_build_query(
array(
'gps' => sprintf("%s,%s,%s,%s",$gps1["lat"],$gps1["lng"],$gps2["lat"],$gps2["lng"]),
'from' => $input[1]["address"],
'to' => $input[2]["address"]
)
);
$url = $oauth2_url."scooter?" . $data;
//error_log($url);
$opts = array(
'http' => array(
'method' => "GET",
'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);
$scooter = json_decode($body,true);
if (is_array($scooter) && is_array($scooter["data"]) && !isset($scooter["error"])) {
// Cache the result in DB
//$scooter["data"]["id"] = Scooter::save($db->getConnect(), $scooter["data"]);
return array($scooter["data"], NULL);
} else if (is_array($scooter) && isset($scooter["error"])) {
$body = $scooter["error"];
}
return array(NULL, "Scooter service call error: ".$body);
}
public function options($db, $input, $result, $options) {
if (isset($options["travel_time"]) && $options["travel_time"]) {
$travel_time = 0;
foreach ($result as $step) {
$travel_time += $step["duration"];
}
$options["travel_time"] = (int)$travel_time;
}
if (isset($options["quote"]) && $options["quote"] &&
is_array($result) && count($result)>1 &&
isset($result[1]["duration"]) && $result[1]["duration"]>0) {
$scooter_time = $result[1]["duration"]; // 2nd entry is the scooter step
list($res,$err) = self::quote($db, $scooter_time, 'SG');
if ($err!=NULL) {
$options["error"] = $err;
} else {
$options["quote"] = $res;
}
}
if (isset($options["route_overlay"]) && $options["route_overlay"]) {
$options = self::directions($db, $input, $result, $options);
}
return $options;
}
public function quote($db, $seconds, $country='SG') {
global $scooterAuthToken, $savvyext;
// Check local DB cache
/* list ($res, $err) = Scooter::checkScooterQuote($db->getConnect(), $minutes, $country);
if (is_array($res) && isset($res[0]["quote"])) {
return array($res, NULL);
} */
// Call scooter quote service
$data = http_build_query(
array(
'minutes' => (int)($seconds/60),
'country' => $country
)
);
$url = $savvyext->cfgReadChar('microservices.catalog') . "/api/v1/price-quote/?" . $data;
$opts = array(
'http' => array(
'method' => "GET",
'header' =>
"Content-Type: application/x-www-form-urlencoded\r\n" .
"Accept: application/json\r\n" .
"Authorization: Server-Token ${scooterAuthToken}\r\n",
),
"ssl" => array(
"verify_peer"=>false,
"verify_peer_name"=>false,
)
);
$context = stream_context_create($opts);
$body = file_get_contents($url, false, $context);
$scooter = json_decode($body,true);
if (is_array($scooter) && count($scooter)>0 && isset($scooter[0]["quote"])) {
// Cache the result in DB
//$scooter["data"]["id"] = Scooter::saveQuote($db->getConnect(), $scooter);
return array($scooter, NULL);
} else if (is_array($scooter) && isset($scooter["error"])) {
$body = $scooter["error"];
}
return array(NULL, "Scooter quote service call error: ".$body);
}
public function directions($db, $input, $result, $options) {
// Walking with waypoints or 3 routes
$mode='walking';
$waypoints=[
$result[1]["data"]["location_start_lat"], $result[1]["data"]["location_start_lng"],
$result[1]["data"]["location_end_lat"], $result[1]["data"]["location_end_lng"]
];
$fromLat = $input[1]["coordinates"]["lat"];
$fromLng = $input[1]["coordinates"]["lng"];
$toLat = $input[2]["coordinates"]["lat"];
$toLng = $input[2]["coordinates"]["lng"];
list($res, $err) = GeocodeApi::route(
$db, $fromLat, $fromLng, $toLat, $toLng, $mode, $waypoints);
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;
}
// ["route_overlay"]
return $options;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
class TripInsights {
public function cheaperAlternatives($db, $gpsdb, $data) {
error_log('TripInsights::cheaperAlternatives($db, $gpsdb, $data) ');
$err = "";
$member_id = (int)$data['member_id'];
$trackedemail_item_id = (int)$data['trackedemail_item_id'];
//$member_id = 3; // DEBUG
if ($member_id<1) {
error_log("Invalid member_id='${member_id}'");
return NULL;
}
$q.= "SELECT b.*,c.name AS transport_provder_name,c.category,d.flag,f.root_id,";
$q.= " CASE WHEN e.id IS NULL THEN '0' ELSE '1' END AS surge_price, ";
$q.= " CASE WHEN (f.id IS NOT NULL AND f.cost>f.average) THEN ROUND(f.average/f.cost,2) ELSE 0 END AS cheaper_alternative, ";
$q.= " (SELECT min(cost) FROM trip_price_comparison WHERE root_id=(SELECT root_id FROM trip_price_comparison WHERE data_source_id=b.id AND data_source=1)) AS min_cost ";
$q.= ", g.address AS location_start_address,g.description AS location_start_description ";
$q.= ", h.address AS location_end_address,h.description AS location_end_description ";
$q.= " FROM trackedemail_item a, transport_providers c, parsedemail_item b ";
$q.= " LEFT JOIN parsedemail_item_advice_result d ON (d.parsedemail_item_id=b.id) ";
$q.= " LEFT JOIN trip_surge_price e ON (e.data_source_id=b.id AND e.data_source=1) ";
$q.= " LEFT JOIN trip_price_comparison f ON (f.data_source_id=b.id AND e.data_source=1) ";
$q.= " LEFT JOIN address g ON (g.id=b.location_start_id) ";
$q.= " LEFT JOIN address h ON (h.id=b.location_end_id) ";
$q.= " WHERE a.id=b.trackedemail_item_id AND a.member_id=".$member_id." AND c.id=b.transport_provider_id ";
//$q.= " AND b.dup_id IS NULL ";
$q.= " AND a.id=${trackedemail_item_id} ";
//$q.= " ORDER BY b.travel_date DESC"; // LIMIT ?
/*
address | character varying(200) |
latitude | numeric |
longitude | numeric |
timezone | integer |
geocoding_date | date |
postal | character varying(40) |
country | character varying(2) | default 'SG'::character varying
geometry | geography(Point,4326) |
description | character varying(100) |
*/
error_log($q);
$trip = [];
$alternatives = [];
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $trip=pg_fetch_assoc($r)) {
$f["location_start_address"] = html_entity_decode ($f["location_start_address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end_address"] = html_entity_decode ($f["location_end_address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
// Cheaper alternatives
error_log("root_id=".$trip["root_id"]);
//SELECT min(f.cost),b.transport_provider_id FROM trip_price_comparison f, parsedemail_item b WHERE f.root_id=92 AND f.cost<13.00 AND b.id=f.parsedemail_item_id GROUP BY b.transport_provider_id;
$q = "SELECT min(f.cost) AS min_cost,b.transport_provider_id FROM trip_price_comparison f, parsedemail_item b ";
$q.= " WHERE f.root_id=".$trip["root_id"]." AND f.cost<".$trip["cost"]." AND b.id=f.parsedemail_item_id";
$q.= " GROUP BY b.transport_provider_id";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
while ($f=pg_fetch_assoc($r)) {
// SELECT f.root_id,b.*,c.name AS transport_provder_name,c.category FROM trip_price_comparison f, parsedemail_item b LEFT JOIN transport_providers c ON (c.id=b.transport_provider_id) WHERE f.root_id=92 AND f.cost=9.00 AND b.id=f.parsedemail_item_id ORDER BY b.travel_date DESC LIMIT 1;
$q0 = "SELECT f.root_id,b.*,c.name AS transport_provder_name,c.category ";
$q0.= ", g.address AS location_start_address,g.description AS location_start_description ";
$q0.= ", h.address AS location_end_address,h.description AS location_end_description ";
$q0.= " FROM trip_price_comparison f, parsedemail_item b ";
$q0.= " LEFT JOIN transport_providers c ON (c.id=b.transport_provider_id) ";
$q0.= " LEFT JOIN address g ON (g.id=b.location_start_id) ";
$q0.= " LEFT JOIN address h ON (h.id=b.location_end_id) ";
$q0.= " WHERE f.root_id=".$trip["root_id"]." AND f.cost=".$f["min_cost"];
$q0.= " AND b.id=f.parsedemail_item_id AND b.cost=".$f["min_cost"];
$q0.= " ORDER BY b.travel_date DESC LIMIT 1";
error_log($q0);
$r0 = pg_query($db, $q0);
$f0 = pg_fetch_assoc($r0);
$f["location_start_address"] = html_entity_decode ($f["location_start_address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end_address"] = html_entity_decode ($f["location_end_address"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$alternatives[] = $f0;
}
}
} else {
$err = pg_last_error($db);
}
$res = [
"name" => "Cheaper Alternatives",
"member_id" => $member_id,
"trackedemail_item_id" => $trackedemail_item_id,
"trip" => $trip,
"alternatives" => $alternatives
];
return [$res, $err];
}
}
// vi:ts=2
@@ -0,0 +1,99 @@
<?php
class TripInsightsApi extends Api
{
public $apiName = 'tripinsights';
const INSIGHTS = array(
"cheaperAlternatives" => 1
);
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
}
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
$message = "Data not found";
$member_id = $this->requestParams["member_id"] ?? 0;
$trackedemail_item_id = $this->requestParams["trackedemail_item_id"] ?? 0;
$result = [
"insights" => [],
"count" => 0,
"messages" => []
];
if ($member_id>0 && $trackedemail_item_id>0) {
$db = new Db();
$data = [
"member_id" => $member_id,
"trackedemail_item_id" => $trackedemail_item_id
];
foreach (TripInsightsApi::INSIGHTS as $type=>$enabled) {
if ($type!="" && $enabled==1) {
list ($res, $err) = call_user_func("TripInsights::${type}",
$db->getConnect(), $db->getConnectGPS(), $data);
if($res!=NULL && is_array($res) && count($res)>0){
$result["insights"][] = $res;
$result["count"]++;
} else {
$result["messages"][] = $err!="" ? $err : "Empty result for ${type}";
}
}
}
if ($result["count"]>0) {
return $this->response($result, 200);
}
} else {
$message = "Invalid input";
}
return $this->response(
array(
"error" => $message,
"messages" => $result["messages"]
), 500);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
@@ -0,0 +1,67 @@
<?php
class TripOptionsApi extends Api
{
public $apiName = 'tripoptions';
public function indexAction()
{
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
$db = new Db();
$tripOptions = Trips::getOptionsById($db->getConnect(), (int)$id);
if(is_array($tripOptions) && count($tripOptions)>0){
return $this->response(
array(
'id' => $id,
'count' => count($tripOptions),
'options' => $tripOptions
), 200);
}
}
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
public function createAction()
{
return $this->response(
array(
"error" => "Saving error"
), 500);
}
public function updateAction()
{
return $this->response(
array(
"error" => "Update error"
), 400);
}
public function deleteAction()
{
return $this->response(
array(
"error" => "Delete error"
), 500);
}
}
+237
View File
@@ -0,0 +1,237 @@
<?php
class Trips {
public function getAll($db, $members_id, $date1, $date2, $limit, $offset) {
$result = array();
$total = 0;
$q = "SELECT count(*) FROM trackedemail_item, parsedemail_item ";
$q.= " WHERE trackedemail_item.id=parsedemail_item.trackedemail_item_id AND trackedemail_item.member_id IN (${members_id})";
$q.= " AND ((parsedemail_item.travel_date BETWEEN '${date1}' AND '${date2}') OR (parsedemail_item.travel_date_end BETWEEN '${date1}' AND '${date2}')) ";
//$q.= " AND parsedemail_item.location_start_tz='Asia/Singapore';";
$q.= " AND parsedemail_item.dup_id IS NULL";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_row($r)) {
$total = $f[0];
}
if ($total==0) {
return array(0,null);
}
$q = "SELECT parsedemail_item.*,parsedemail_item_advice_google.id AS gid,parsedemail_item_advice_google.routes,trackedemail_item.id as tid,trackedemail_item.member_id ";
$q.= " FROM trackedemail_item, parsedemail_item ";
$q.= " LEFT JOIN parsedemail_item_advice_google ON parsedemail_item.id=parsedemail_item_advice_google.parsedemail_item_id ";
$q.= " WHERE trackedemail_item.id=parsedemail_item.trackedemail_item_id AND trackedemail_item.member_id IN (${members_id})";
$q.= " AND ((parsedemail_item.travel_date BETWEEN '${date1}' AND '${date2}') OR (parsedemail_item.travel_date_end BETWEEN '${date1}' AND '${date2}')) ";
//$q.= " AND parsedemail_item.location_start_tz='Asia/Singapore';";
$q.= " AND parsedemail_item.dup_id IS NULL";
$q.= " ORDER BY parsedemail_item.travel_date DESC, parsedemail_item.travel_date_end DESC";
$q.= " LIMIT ${limit} OFFSET ${offset}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
while($f=pg_fetch_assoc($r)) {
$result[] = $f;
}
}
return array($total, $result);
}
public function getById($db, $id) {
$result = array();
$q = "SELECT a.*,
b.address AS location_start, b.latitude AS location_start_lat, b.longitude AS location_start_lng,
c.address AS location_end, c.latitude AS location_end_lat, c.longitude AS location_end_lng,
a.id AS parsedemail_item_id
FROM parsedemail_item a
LEFT JOIN address b ON (b.id=a.location_start_id)
LEFT JOIN address c ON (c.id=a.location_end_id)
WHERE a.id=${id}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result = $f;
}
return $result;
}
public function getByLocations($db, $origin, $destination, $limit=1) {
$result = array();
$db_lat = is_numeric($origin["lat"]) ? $origin["lat"] : 0;
$db_lng = is_numeric($origin["lng"]) ? $origin["lng"] : 0;
$db_destination = pg_escape_string(strtolower($destination));
$q = "SELECT a.*,
b.address AS location_start, b.latitude AS location_start_lat, b.longitude AS location_start_lng,
c.address AS location_end, c.latitude AS location_end_lat, c.longitude AS location_end_lng,
a.id AS parsedemail_item_id
FROM parsedemail_item a
LEFT JOIN address b ON (b.id=a.location_start_id)
LEFT JOIN address c ON (c.id=a.location_end_id)
WHERE ROUND(b.latitude,3)=ROUND(${db_lat},3) AND ROUND(b.longitude,3)=ROUND(${db_lng},3)
AND LOWER(c.address) LIKE '%${db_destination}%' ORDER BY a.travel_date_end DESC LIMIT ${limit}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
if ($limit==1) {
$f = pg_fetch_assoc($r);
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
return $f;
}
$result = [];
while ($f = pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
}
return $result;
}
public function getByCoordinates($db, $origin, $destination, $limit=1) {
$result = array();
$dbo_lat = is_numeric($origin["lat"]) ? $origin["lat"] : 0;
$dbo_lng = is_numeric($origin["lng"]) ? $origin["lng"] : 0;
$dbd_lat = is_numeric($destination["lat"]) ? $destination["lat"] : 0;
$dbd_lng = is_numeric($destination["lng"]) ? $destination["lng"] : 0;
$q = "SELECT a.*,
b.address AS location_start, b.latitude AS location_start_lat, b.longitude AS location_start_lng,
c.address AS location_end, c.latitude AS location_end_lat, c.longitude AS location_end_lng,
a.id AS parsedemail_item_id
FROM parsedemail_item a
LEFT JOIN address b ON (b.id=a.location_start_id)
LEFT JOIN address c ON (c.id=a.location_end_id)
WHERE ROUND(b.latitude,3)=ROUND(${dbo_lat},3) AND ROUND(b.longitude,3)=ROUND(${dbo_lng},3)
AND ROUND(c.latitude,3)=ROUND(${dbd_lat},3) AND ROUND(c.longitude,3)=ROUND(${dbd_lng},3)
ORDER BY a.travel_date_end DESC LIMIT ${limit}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
if ($limit==1) {
$f = pg_fetch_assoc($r);
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
return $f;
}
$result = [];
while ($f = pg_fetch_assoc($r)) {
$f["location_start"] = html_entity_decode ($f["location_start"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$f["location_end"] = html_entity_decode ($f["location_end"],ENT_QUOTES|ENT_HTML5,"UTF-8");
$result[] = $f;
}
}
return $result;
}
public function getOptionsById($db, $id) {
syslog(LOG_WARNING,"Trips::getOptionsById(\$db, $id)");
$result = array("routes" => 0, "gid" => 0);
$q = "SELECT id AS gid, routes FROM parsedemail_item_advice_google WHERE parsedemail_item_id=${id}";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
$result = $f;
}
if ($result["routes"]>0 && $result["gid"]>0) {
$result["legs"] = array();
} else {
return NULL; // Data not found
}
$timezones = MultiModal::getLegTimezonesForAdvice($db, $result["gid"]);
/* $q = "SELECT a.*,b.timezone AS departure_timezone,c.timezone AS arrival_timezone FROM google_directions_legs a ";
$q.= " LEFT JOIN address_timezone b ON b.id=CAST(a.departure_time_zone AS INTEGER) ";
$q.= " LEFT JOIN address_timezone c ON c.id=CAST(a.arrival_time_zone AS INTEGER) ";
$q.= " WHERE a.parsedemail_item_advice_google_id=".$result["gid"]; */
$q = "SELECT * FROM google_directions_legs WHERE parsedemail_item_advice_google_id=".$result["gid"];
syslog(LOG_WARNING,$q);
$r = pg_query($db, $q);
while ($f=pg_fetch_assoc($r)) {
unset($f["parsedemail_item_advice_google_id"]); // We have it as $result["gid"]
if (is_array($timezones) && array_key_exists($f["id"],$timezones)) {
$f["departure_timezone"] = $timezones[$f["id"]][0];
$f["arrival_timezone"] = $timezones[$f["id"]][1];
}
$result["legs"][] = $f;
$q1 = "SELECT a.id AS sid,a.*,b.* FROM google_directions_leg_steps a LEFT JOIN google_directions_leg_step_details b ON a.id=b.google_directions_leg_step_id ";
$q1.= " WHERE a.google_directions_leg_id=".$f["id"];
$r1 = pg_query($db, $q1);
if ($r1 && pg_num_rows($r1)) {
$result["leg_steps"][$f["id"]] = array();
$result["leg_fare"][$f["id"]] = 0;
}
$fare = 0; $i = 0;
while ($f1=pg_fetch_assoc($r1)) {
if (array_key_exists("polyline",$f1) && $f1["polyline"]=="") {
$f1["polyline"] = MultiModal::getPolyline($db, $f1);
}
unset($f1["google_directions_leg_id"]);
unset($f1["google_directions_leg_step_id"]);
foreach ($f1 as $key=>$val) {
if ($val===NULL) unset($f1[$key]);
}
$q2 = "SELECT * FROM leg_step_quote WHERE google_directions_leg_step_id=".$f1["sid"];
$r2 = pg_query($db, $q2);
if ($r2 && pg_num_rows($r2) && $f2 = pg_fetch_assoc($r2)) {
syslog(LOG_WARNING,$q2);
if (is_array($f2) && isset($f2["fare"]) && $f2["fare"]!="") {
$fare += $f2["fare_raw"];
}
$i++;
}
//$f1["q"] = $q2;
$result["leg_steps"][$f["id"]][] = $f1;
}
if ($fare==0) $fare = $f["fare_raw"];
$result["leg_fare"][$f["id"]] = $fare;
}
ActivityApi::debugTrips([['multimodal'=>['options'=>$result]]],'22222222');
return $result;
}
public function create($db, $data) {
$fields = array(
'travel_date' => false,
'duration' => false,
'cost_raw' => false,
'trackedemail_item_id' => false,
'cost' => false,
'updated' => false,
'distance' => false,
'transport_provider_id' => false,
'scheduled' => false,
'travel_date_end' => false,
'dup_id' => false,
'location_start_id' => true,
'location_end_id' => true,
'private' => false
);
$field_key_list = [];
$field_val_list = [];
foreach ($fields as $key=>$required) {
if ($required && !isset($data[$key])) {
return [NULL, "Missing required field '${key}'"];
}
if (!isset($data[$key])) continue;
$field_key_list[] = "${key}";
$field_val_list[] = (($data[$key]==NULL || $data[$key]=="")?"NULL":"'".pg_escape_string($data[$key])."'");
}
$q = "INSERT INTO parsedemail_item (".implode(",",$field_key_list).")
VALUES(".implode(",",$field_val_list).") RETURNING id";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r)) {
$f = pg_fetch_row($r);
return [$f[0], NULL];
}
return [NULL, pg_last_error($db)];
}
public function tripByLocations($db, $location_start_id, $location_end_id) {
$q = "SELECT * FROM parsedemail_item WHERE location_start_id=${location_start_id} AND location_end_id=${location_end_id}";
$q.= " AND dup_id IS NULL ORDER BY travel_date DESC LIMIT 1";
$r = pg_query($db, $q);
if ($r && pg_num_rows($r) && $f=pg_fetch_assoc($r)) {
return [$f, NULL];
}
return [NULL, pg_last_error()];
}
}
// vi:ts=2
+221
View File
@@ -0,0 +1,221 @@
<?php
class TripsApi extends Api
{
public $apiName = 'trips';
/**
* Method GET
* Get all records
* http://DOMAIN/trips
* @return string
*/
public function indexAction()
{
// Get the parameters
$members_id = (int)($this->requestParams['members_id'] ?? 22);
$date1 = date('Y-m-d', strtotime($this->requestParams['date1'] ?? date("Y-m-1", strtotime('first day of last month'))));
$date2 = date('Y-m-d', strtotime($this->requestParams['date2'] ?? date("Y-m-d")));
$limit = (int)($this->requestParams['limit'] ?? 1000);
$offset = (int)($this->requestParams['offset'] ?? 0);
$db = new Db();
list ($total, $trips) = Trips::getAll(
$db->getConnect(),
$members_id,
$date1, $date2,
$limit, $offset);
if($total>0){
return $this->response(
array(
'members_id' => $members_id,
'date1' => $date1,
'date2' => $date2,
'limit' => $limit,
'offset' => $offset,
'count' => count($trips),
'total' => (int)$total,
'trips' => $trips
), 200);
}
return $this->response(
array(
'error' => 'Data not found'
), 404);
}
/**
* Method GET
* Get single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function viewAction()
{
//id must be the first parameter after /trips/x
$id = array_shift($this->requestUri);
if($id && (int)$id>0){
$db = new Db();
$trip = Trips::getById($db->getConnect(), (int)$id);
if(is_array($trip) && count($trip)>0){
return $this->response($trip, 200);
}
}
return $this->response(
array(
'error'=> 'Data not found'
), 404);
}
/**
* Method POST
* Create new record
* http://DOMAIN/trips + request parameters name, email
* @return string
*/
/*
curl -d '{"origin": {"lat":1.2833754,"lng":103.8607264}, "destination":"97 Meyer Road, Singapore"}' \
-H "Content-Type: application/json" \
-X POST https://adminsavvy.sworks.float.sg/SAVVY/trips/api/trips
curl -d '{"encrypted_payload": "ba3ea1f5f4032cad1ea80f087bcc05354fa1ecfb7e81187c4f8deeb92b6f5ac742c8ce10a1f56f5e3bd5f18931d067e092c840e6b6ba30b433b5e63dd27fc34a67fa1e22011f74efdf3d22a3755a45687ecb45511bf882400b"}' \
-H "Content-Type: application/json" \
-X POST https://adminsavvy.sworks.float.sg/SAVVY/trips/api/trips
curl -d '{"origin": {"lat":1.2833754,"lng":103.8607264}, "destination":"11 Bayfront Ave, Singapore 018956"}' \
-X POST https://adminsavvy.sworks.float.sg/SAVVY/trips/api/trips
*/
public function createAction()
{
global $httpAuthToken;
$message = "Unknown error";
$origin = $this->requestParams["origin"] ?? array();
$destination = $this->requestParams["destination"] ?? "";
if ($destination && count($origin)==2) {
$db = new Db();
$trip = Trips::getByLocations(
$db->getConnect(),
$origin,
$destination);
if ($trip && $trip["id"]>0) {
$tripOptions = Trips::getOptionsById($db->getConnect(), $trip["id"]);
if(is_array($tripOptions) && count($tripOptions)>0){
$trip = array_merge($trip, array(
'count' => count($tripOptions),
'options' => $tripOptions
));
return $this->response($trip, 200);
} else {
$message = "Trip options not found!";
}
} else {
// New trip
$trip = self::tripService($origin,$destination);
if (is_array($trip) && isset($trip["travel_date"])) {
return $this->response($trip, 200);
}
$message = "Failed to call service";
}
//var_dump($message);
return $this->response(
array(
'message' => 'Not implemented: ' . $message
), 500);
}
return $this->response(
array(
"error" => "Invalid request"
), 500);
}
/**
* Method PUT
* Update single record (by id)
* http://DOMAIN/trips/1 + request parameters name, email
* @return string
*/
public function updateAction()
{/*
$parse_url = parse_url($this->requestUri[0]);
$userId = $parse_url['path'] ?? null;
$db = (new Db())->getConnect();
if(!$userId || !Trips::getById($db, $userId)){
return $this->response("Trip with id=$userId not found", 404);
}
$name = $this->requestParams['name'] ?? '';
$email = $this->requestParams['email'] ?? '';
if($name && $email){
if($user = Trips::update($db, $userId, $name, $email)){
return $this->response('Data updated.', 200);
}
}*/
return $this->response(
array(
"error" => "Update error"
), 400);
}
/**
* Method DELETE
* Delete single record (by id)
* http://DOMAIN/trips/1
* @return string
*/
public function deleteAction()
{/*
$parse_url = parse_url($this->requestUri[0]);
$userId = $parse_url['path'] ?? null;
$db = (new Db())->getConnect();
if(!$userId || !Trips::getById($db, $userId)){
return $this->response("Trip with id=$userId not found", 404);
}
if(Trips::deleteById($db, $userId)){
return $this->response('Data deleted.', 200);
}*/
return $this->response(
array(
"error" => "Delete error"
), 500);
}
public function tripService($origin,$destination) {
global $savvyext;
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$oauth2_url = $savvyext->cfgReadChar('system.oauth2_url');
// curl -d 'origin_lat=1.2833754&origin_lng=103.8607264&destination=11 Bayfront Ave, Singapore 018956' -X POST $oauth2_url.'trips'/
$postdata = http_build_query(
array(
'origin_lat' => $origin["lat"],
'origin_lng' => $origin["lng"],
'destination'=> $destination
)
);
$url = $oauth2_url."trips";
$opts = array(
'http' => array(
'method' => "POST",
'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);
$body = file_get_contents($url, false, $context);
$trip = json_decode($body,true);
return $trip;
}
}
+182
View File
@@ -0,0 +1,182 @@
<?php
ob_start();
$profile = getenv("SAVVY_PROFILE");
if ($profile) {
require_once("../../core/backend.${profile}.php");
} else {
require_once('../../core/backend.php');
}
include_once '../config.php';
require_once('../constants.php');
require_once('../common/vendor/autoload.php');
require_once('../common/Api.php');
require_once('../common/Db.php');
require_once('../common/Gis.php');
require_once('../common/Utilities.php');
require_once('../common/Logger.php');
require_once('Activity.php');
require_once('ActivityApi.php');
require_once('ActivityDirect.php');
require_once('ActivityDirectApi.php');
require_once('Address.php');
require_once('AddressApi.php');
require_once('ByCategory.php');
require_once('ByCategoryApi.php');
require_once('Compare.php');
require_once('CompareApi.php');
require_once('Geocode.php');
require_once('GeocodeApi.php');
require_once('Geofence.php');
require_once('GeofenceApi.php');
require_once('MultiModal.php');
require_once('MultiModalFilter.php');
require_once('MultiModalApi.php');
require_once('Populardirect.php');
require_once('PopulardirectApi.php');
require_once('Popular.php');
require_once('PopularApi.php');
require_once('Quotes.php');
require_once('QuoteAPI.php');
require_once('QuoteGroup.php');
require_once('QuoteGroupApi.php');
require_once('Radius.php');
require_once('RadiusApi.php');
require_once('Report.php');
require_once('ReportApi.php');
require_once('Scooter.php');
require_once('ScooterApi.php');
require_once('TripInsights.php');
require_once('TripInsightsApi.php');
require_once('Trips.php');
require_once('TripsApi.php');
require_once('TripOptionsApi.php');
require_once('RouteApi.php');
$httpAuthToken = $savvyext->cfgReadChar('system.oauth2_token');
$scooterAuthToken = $savvyext->cfgReadChar('system.scooter_token');
ob_end_clean();
header("Access-Control-Allow-Origin: *");
header("Access-Control-Expose-Headers: Access-Control-Allow-Origin");
header("Access-Control-Allow-Headers: Cache-Control, Pragma, Origin, Authorization, Content-Type, X-Requested-With, client_id");
header("Access-Control-Allow-Methods: POST, GET, PUT, DELETE, OPTIONS");
header('Content-type: application/json');
if ("OPTIONS" === $_SERVER['REQUEST_METHOD']) {
exit();
}
$headers = []; foreach (getallheaders() as $key=>$val) { $headers[strtolower($key)] = $val; }
if ((!isset($headers["Authorization"]) || substr($headers["Authorization"],-strlen($httpAuthToken))!=$httpAuthToken) &&
(!isset($headers["authorization"]) || substr($headers["authorization"],-strlen($httpAuthToken))!=$httpAuthToken)) {
header('HTTP/1.1 401 Unauthorized');
header('Status: 401 Unauthorized');
echo "{\"status\":\"Missing authorization\"}";
exit();
}
openlog("TripsApi", LOG_PID | LOG_PERROR, LOG_LOCAL0);
ob_start();
try {
if (strpos($_SERVER['REQUEST_URI'],'/api/')===false) {
throw new Exception("Invalid API request");
}
$requestUri = explode('/', trim($_SERVER['REQUEST_URI'],'/'));
while (array_shift($requestUri) !== 'api') {
};
if ($requestUri[0]=='activity') {
$api = new ActivityApi($requestUri);
}
else if ($requestUri[0]=='activitydirect') {
$api = new ActivityDirectApi($requestUri);
}
else if ($requestUri[0]=='address') {
$api = new AddressApi($requestUri);
}
else if ($requestUri[0]=='bycategory') {
$api = new ByCategoryApi($requestUri);
}
else if ($requestUri[0]=='compare') {
$api = new CompareApi($requestUri);
}
else if ($requestUri[0]=='geofence') {
$api = new GeofenceApi($requestUri);
}
else if ($requestUri[0]=='geocode') {
$api = new GeocodeApi($requestUri);
}
else if ($requestUri[0]=='multimodal') {
$api = new MultiModalApi($requestUri);
}
else if ($requestUri[0]=='popular') {
$api = new PopularApi($requestUri);
}
else if ($requestUri[0]=='populardirect') {
$api = new PopulardirectApi($requestUri);
}
else if ($requestUri[0]=='quote') {
$api = new QuoteApi($requestUri);
}
else if ($requestUri[0]=='quotegroup') {
$api = new QuoteGroupApi($requestUri);
}
else if ($requestUri[0]=='radius') {
$api = new RadiusApi($requestUri);
}
else if ($requestUri[0]=='report') {
$api = new ReportApi($requestUri);
}
else if ($requestUri[0]=='scooter') {
$api = new ScooterApi($requestUri);
}
else if ($requestUri[0]=='tripinsights') {
$api = new TripInsightsApi($requestUri);
}
else if ($requestUri[0]=='trips') {
$api = new TripsApi($requestUri);
}
else if ($requestUri[0]=='tripoptions') {
$api = new TripOptionsApi($requestUri);
}
else if ($requestUri[0]=='route') {
$api = new RouteApi($requestUri);
}
else {
ob_end_clean();
echo json_encode(Array('error' => 'Invalid API request'));
exit();
}
$result = $api->run();
ob_end_clean();
echo $result;
}
catch (Exception $e) {
ob_end_clean();
echo json_encode(Array('error' => $e->getMessage()));
}
closelog();
Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

@@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="swagger-ui.css" >
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="swagger-ui-bundle.js"> </script>
<script src="swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "../swagger.php",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
<?php
require('../../../adminsavvy/vendor/autoload.php');
$openapi = \OpenApi\scan('.');
header('Content-Type: application/json');
echo $openapi->toJson();
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
class ReportApi
{
public $apiName = 'report';
const REPORTS = array(
"weeklySpending" => 1
);
public function test($code) {
$chck = self::REPORTS;
error_log('-----');
error_log($code=="weeklySpending"?"TRUE":"FALSE");
error_log(array_key_exists(trim($code),$chck)?"TRUE":"FALSE");
error_log($chck["weeklySpending"]==1?"TRUE":"FALSE");
}
}
var_dump(ReportApi::test("weeklySpending"));
+8
View File
@@ -0,0 +1,8 @@
<?php
function a($lat1, $lon1, $lat2, $lon2, $unit) {
if($lat1==$lat2 && $lon1==$lon2) {
return 0;
}
}