You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
10 KiB

  1. <?php
  2. namespace App\Http\Controllers;
  3. use Laravel\Lumen\Routing\Controller as BaseController;
  4. use Illuminate\Http\Request;
  5. use DB;
  6. use Quartz;
  7. use Log;
  8. use DateTime, DateTimeZone, DateInterval;
  9. use App\Jobs\TripComplete;
  10. use App\Jobs\NotifyOfNewLocations;
  11. class Api extends BaseController
  12. {
  13. public function account(Request $request) {
  14. $token = $request->input('token');
  15. if(!$token)
  16. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  17. $db = DB::table('databases')->where('write_token','=',$token)->first();
  18. if(!$db)
  19. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  20. return response(json_encode(['name' => $db->name]))->header('Content-Type', 'application/json');
  21. }
  22. public function query(Request $request) {
  23. $token = $request->input('token');
  24. if(!$token)
  25. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  26. $db = DB::table('databases')->where('read_token','=',$token)->first();
  27. if(!$db)
  28. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  29. $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'r');
  30. if($request->input('tz')) {
  31. $tz = $request->input('tz');
  32. } else {
  33. $tz = 'UTC';
  34. }
  35. if($date=$request->input('date')) {
  36. $start = DateTime::createFromFormat('Y-m-d H:i:s', $date.' 00:00:00', new DateTimeZone($tz));
  37. $end = DateTime::createFromFormat('Y-m-d H:i:s', $date.' 23:59:59', new DateTimeZone($tz));
  38. } elseif(($start=$request->input('start')) && ($end=$request->input('end'))) {
  39. $start = new DateTime($start, new DateTimeZone($tz));
  40. $end = new DateTime($end, new DateTimeZone($tz));
  41. } else {
  42. return response(json_encode(['error' => 'no date provided']))->header('Content-Type', 'application/json');
  43. }
  44. $results = $qz->queryRange($start, $end);
  45. $locations = [];
  46. $properties = [];
  47. $events = [];
  48. if($request->input('format') == 'linestring') {
  49. foreach($results as $id=>$record) {
  50. // When returning a linestring, separate out the "event" records from the "location" records
  51. if(property_exists($record->data->properties, 'action')) {
  52. $rec = $record->data;
  53. # add a unixtime property
  54. $rec->properties->unixtime = (int)$record->date->format('U');
  55. $events[] = $rec;
  56. } else {
  57. #$record->date->format('U.u');
  58. // Ignore super inaccurate locations
  59. if(!property_exists($record->data->properties, 'horizontal_accuracy')
  60. || $record->data->properties->horizontal_accuracy <= 5000) {
  61. $locations[] = $record->data;
  62. $props = $record->data->properties;
  63. $date = $record->date;
  64. $date->setTimeZone(new DateTimeZone($tz));
  65. $props->timestamp = $date->format('c');
  66. $props->unixtime = (int)$date->format('U');
  67. $properties[] = $props;
  68. }
  69. }
  70. }
  71. $linestring = array(
  72. 'type' => 'LineString',
  73. 'coordinates' => [],
  74. 'properties' => $properties
  75. );
  76. foreach($locations as $loc) {
  77. if(property_exists($loc, 'geometry'))
  78. $linestring['coordinates'][] = $loc->geometry->coordinates;
  79. else
  80. $linestring['coordinates'][] = null;
  81. }
  82. $response = array(
  83. 'linestring' => $linestring,
  84. 'events' => $events
  85. );
  86. } else {
  87. foreach($results as $id=>$record) {
  88. $locations[] = $record->data;
  89. }
  90. $response = [
  91. 'locations' => $locations
  92. ];
  93. }
  94. return response(json_encode($response))->header('Content-Type', 'application/json');
  95. }
  96. public function last(Request $request) {
  97. $token = $request->input('token');
  98. if(!$token)
  99. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  100. $db = DB::table('databases')->where('read_token','=',$token)->first();
  101. if(!$db)
  102. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  103. $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'r');
  104. if($request->input('tz')) {
  105. $tz = $request->input('tz');
  106. } else {
  107. $tz = 'UTC';
  108. }
  109. if($input=$request->input('before')) {
  110. if(preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/', $input)) {
  111. // If the input date is given in YYYY-mm-dd HH:mm:ss format, interpret it in the timezone given
  112. $date = DateTime::createFromFormat('Y-m-d H:i:s', $input, new DateTimeZone($tz));
  113. } else {
  114. // Otherwise, parse the string and use the timezone in the input
  115. $date = new DateTime($input);
  116. $date->setTimeZone(new DateTimeZone($tz));
  117. }
  118. if(!$date) {
  119. return response(json_encode(['error' => 'invalid date provided']))->header('Content-Type', 'application/json');
  120. }
  121. } else {
  122. $date = new DateTime();
  123. }
  124. /* ********************************************** */
  125. // TODO: move this logic into QuartzDB
  126. // Load the shard for the given date
  127. $shard = $qz->shardForDate($date);
  128. // If the shard doesn't exist, check one day before
  129. if(!$shard->exists()) {
  130. $date = $date->sub(new DateInterval('PT86400S'));
  131. $shard = $qz->shardForDate($date);
  132. }
  133. // Now if the shard doesn't exist, return an empty result
  134. if(!$shard->exists()) {
  135. return response(json_encode([
  136. 'data'=>null
  137. ]));
  138. }
  139. // Start iterating through the shard and look for the last line that is before the given date
  140. $shard->init();
  141. $record = false;
  142. foreach($shard as $r) {
  143. if($r->date > $date)
  144. break;
  145. $record = $r;
  146. }
  147. /* ********************************************** */
  148. if(!$record) {
  149. return response(json_encode([
  150. 'data'=>null
  151. ]));
  152. }
  153. $response = [
  154. 'data' => $record->data
  155. ];
  156. if($request->input('geocode') && property_exists($record->data, 'geometry') && property_exists($record->data->geometry, 'coordinates')) {
  157. $coords = $record->data->geometry->coordinates;
  158. $params = [
  159. 'latitude' => $coords[1],
  160. 'longitude' => $coords[0],
  161. 'date' => $record->data->properties->timestamp
  162. ];
  163. $ch = curl_init(env('ATLAS_BASE').'api/geocode?'.http_build_query($params));
  164. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  165. curl_setopt($ch, CURLOPT_TIMEOUT, 8);
  166. $geocode = json_decode(curl_exec($ch));
  167. if($geocode) {
  168. $response['geocode'] = $geocode;
  169. } else {
  170. $response['geocode'] = null;
  171. }
  172. }
  173. return response(json_encode($response))->header('Content-Type', 'application/json');;
  174. }
  175. public function input(Request $request) {
  176. $token = $request->input('token');
  177. if(!$token)
  178. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  179. $db = DB::table('databases')->where('write_token','=',$token)->first();
  180. if(!$db)
  181. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  182. if(!is_array($request->input('locations')))
  183. return response(json_encode(['error' => 'invalid input', 'error_description' => 'parameter "locations" must be an array of GeoJSON data with a "timestamp" property']))->header('Content-Type', 'application/json');
  184. $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'w');
  185. $num = 0;
  186. $trips = 0;
  187. foreach($request->input('locations') as $loc) {
  188. if(array_key_exists('properties', $loc)) {
  189. if(array_key_exists('timestamp', $loc['properties'])) {
  190. try {
  191. if(preg_match('/^\d+\.\d+$/', $loc['properties']['timestamp']))
  192. $date = DateTime::createFromFormat('U.u', $loc['properties']['timestamp']);
  193. elseif(preg_match('/^\d+$/', $loc['properties']['timestamp']))
  194. $date = DateTime::createFromFormat('U', $loc['properties']['timestamp']);
  195. else
  196. $date = new DateTime($loc['properties']['timestamp']);
  197. if($date) {
  198. $num++;
  199. $qz->add($date, $loc);
  200. if(array_key_exists('type', $loc['properties']) && $loc['properties']['type'] == 'trip') {
  201. try {
  202. $job = (new TripComplete($db->id, $loc))->onQueue('compass');
  203. $this->dispatch($job);
  204. $trips++;
  205. Log::info('Got a trip record');
  206. } catch(Exception $e) {
  207. Log::warning('Received invalid trip');
  208. }
  209. }
  210. } else {
  211. Log::warning('Received invalid date: ' . $loc['properties']['timestamp']);
  212. }
  213. } catch(Exception $e) {
  214. Log::warning('Received invalid date: ' . $loc['properties']['timestamp']);
  215. }
  216. }
  217. }
  218. }
  219. if($num > 0) {
  220. // Notify subscribers that new data is available
  221. if($db->ping_urls) {
  222. $job = (new NotifyOfNewLocations($db->id))->onQueue('compass');
  223. $this->dispatch($job);
  224. }
  225. }
  226. return response(json_encode(['result' => 'ok', 'saved' => $num, 'trips' => $trips]))->header('Content-Type', 'application/json');
  227. }
  228. public function trip_complete(Request $request) {
  229. $token = $request->input('token');
  230. if(!$token)
  231. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  232. $db = DB::table('databases')->where('write_token','=',$token)->first();
  233. if(!$db)
  234. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  235. if($request->input('tz')) {
  236. $tz = new DateTimeZone($request->input('tz'));
  237. } else {
  238. $tz = new DateTimeZone('UTC');
  239. }
  240. $start = new DateTime($request->input('start'), $tz);
  241. $end = new DateTime($request->input('end'), $tz);
  242. $loc = [
  243. 'properties' => [
  244. 'mode' => $request->input('mode'),
  245. 'start' => $start->format('c'),
  246. 'end' => $end->format('c'),
  247. ]
  248. ];
  249. try {
  250. $job = (new TripComplete($db->id, $loc))->onQueue('compass');
  251. $this->dispatch($job);
  252. Log::info('Got a manual trip record: '.$start->format('c').' '.$end->format('c'));
  253. } catch(Exception $e) {
  254. Log::warning('Received invalid trip');
  255. }
  256. return response(json_encode(['result' => 'ok']))->header('Content-Type', 'application/json');
  257. }
  258. }