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.

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