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.

407 lines
14 KiB

7 years ago
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Laravel\Lumen\Routing\Controller as BaseController;
  4. use Illuminate\Http\Request;
  5. use DB, Log, Cache;
  6. use Quartz;
  7. use DateTime, DateTimeZone, DateInterval;
  8. use App\Jobs\TripComplete;
  9. use App\Jobs\NotifyOfNewLocations;
  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($record->data) {
  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. }
  72. $linestring = array(
  73. 'type' => 'LineString',
  74. 'coordinates' => [],
  75. 'properties' => $properties
  76. );
  77. foreach($locations as $loc) {
  78. if(property_exists($loc, 'geometry'))
  79. $linestring['coordinates'][] = $loc->geometry->coordinates;
  80. else
  81. $linestring['coordinates'][] = null;
  82. }
  83. $response = array(
  84. 'linestring' => $linestring,
  85. 'events' => $events
  86. );
  87. } else {
  88. foreach($results as $id=>$record) {
  89. if($record->data) {
  90. $locations[] = $record->data;
  91. }
  92. }
  93. $response = [
  94. 'locations' => $locations
  95. ];
  96. }
  97. return response(json_encode($response))->header('Content-Type', 'application/json');
  98. }
  99. public function last(Request $request) {
  100. $token = $request->input('token');
  101. if(!$token)
  102. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  103. $db = DB::table('databases')->where('read_token','=',$token)->first();
  104. if(!$db)
  105. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  106. if($request->input('tz')) {
  107. $tz = $request->input('tz');
  108. } else {
  109. $tz = 'UTC';
  110. }
  111. if($input=$request->input('before')) {
  112. // If a specific time was requested, look up the data in the filesystem
  113. $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'r');
  114. if(preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/', $input)) {
  115. // If the input date is given in YYYY-mm-dd HH:mm:ss format, interpret it in the timezone given
  116. $date = DateTime::createFromFormat('Y-m-d H:i:s', $input, new DateTimeZone($tz));
  117. } else {
  118. // Otherwise, parse the string and use the timezone in the input
  119. $date = new DateTime($input);
  120. $date->setTimeZone(new DateTimeZone($tz));
  121. }
  122. if(!$date) {
  123. return response(json_encode(['error' => 'invalid date provided']))->header('Content-Type', 'application/json');
  124. }
  125. /* ********************************************** */
  126. // TODO: move this logic into QuartzDB
  127. // Load the shard for the given date
  128. $shard = $qz->shardForDate($date);
  129. // If the shard doesn't exist, check one day before
  130. if(!$shard->exists()) {
  131. $date = $date->sub(new DateInterval('PT86400S'));
  132. $shard = $qz->shardForDate($date);
  133. }
  134. // Now if the shard doesn't exist, return an empty result
  135. if(!$shard->exists()) {
  136. return response(json_encode([
  137. 'data'=>null
  138. ]));
  139. }
  140. // Start iterating through the shard and look for the last line that is before the given date
  141. $shard->init();
  142. $record = false;
  143. foreach($shard as $r) {
  144. if($r->date > $date)
  145. break;
  146. $record = $r;
  147. }
  148. /* ********************************************** */
  149. if(!$record) {
  150. return response(json_encode([
  151. 'data'=>null
  152. ]));
  153. }
  154. $response = [
  155. 'data' => $record->data,
  156. ];
  157. } else {
  158. // If no specific time was requested, use the cached location from the database
  159. $response = [
  160. 'data' => json_decode($db->last_location),
  161. ];
  162. }
  163. if($request->input('geocode') && property_exists($response['data'], 'geometry') && property_exists($response['data']->geometry, 'coordinates')) {
  164. $coords = $response['data']->geometry->coordinates;
  165. $params = [
  166. 'latitude' => $coords[1],
  167. 'longitude' => $coords[0],
  168. 'date' => $response['data']->properties->timestamp
  169. ];
  170. $geocode = self::geocode($params);
  171. if($geocode) {
  172. $response['geocode'] = $geocode;
  173. } else {
  174. $response['geocode'] = null;
  175. }
  176. }
  177. return response(json_encode($response))->header('Content-Type', 'application/json');
  178. }
  179. public function trip(Request $request) {
  180. $token = $request->input('token');
  181. if(!$token)
  182. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  183. $db = DB::table('databases')->where('read_token','=',$token)->first();
  184. if(!$db)
  185. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  186. if($db->current_trip) {
  187. $response = [
  188. 'trip' => json_decode($db->current_trip)
  189. ];
  190. } else {
  191. $response = [
  192. 'trip' => null
  193. ];
  194. }
  195. return response(json_encode($response))->header('Content-Type', 'application/json');
  196. }
  197. public function input(Request $request) {
  198. $token = $request->input('token');
  199. if(!$token)
  200. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  201. $db = DB::table('databases')->where('write_token','=',$token)->first();
  202. if(!$db)
  203. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  204. if(!is_array($request->input('locations')))
  205. 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');
  206. $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'w');
  207. $num = 0;
  208. $trips = 0;
  209. $last_location = false;
  210. foreach($request->input('locations') as $loc) {
  211. if(array_key_exists('properties', $loc)) {
  212. if(array_key_exists('timestamp', $loc['properties'])) {
  213. try {
  214. if(preg_match('/^\d+\.\d+$/', $loc['properties']['timestamp']))
  215. $date = DateTime::createFromFormat('U.u', $loc['properties']['timestamp']);
  216. elseif(preg_match('/^\d+$/', $loc['properties']['timestamp']))
  217. $date = DateTime::createFromFormat('U', $loc['properties']['timestamp']);
  218. else
  219. $date = new DateTime($loc['properties']['timestamp']);
  220. if($date) {
  221. $cacheKey = 'compass::'.$db->name.'::'.$date->format('U');
  222. // Skip adding if the timestamp is already in the cache.
  223. // Helps prevent writing duplicate data when the HTTP request is interrupted.
  224. if(!env('CACHE_DRIVER') || !Cache::has($cacheKey)) {
  225. $num++;
  226. $qz->add($date, $loc);
  227. if(env('CACHE_DRIVER'))
  228. Cache::put($cacheKey, 1, 360); // cache this for 6 hours
  229. $last_location = $loc;
  230. }
  231. if(array_key_exists('type', $loc['properties']) && $loc['properties']['type'] == 'trip') {
  232. try {
  233. $job = (new TripComplete($db->id, $loc))->onQueue('compass');
  234. $this->dispatch($job);
  235. $trips++;
  236. Log::info('Got a trip record');
  237. } catch(Exception $e) {
  238. Log::warning('Received invalid trip');
  239. }
  240. }
  241. } else {
  242. Log::warning('Received invalid date: ' . $loc['properties']['timestamp']);
  243. }
  244. } catch(Exception $e) {
  245. Log::warning('Received invalid date: ' . $loc['properties']['timestamp']);
  246. }
  247. }
  248. }
  249. }
  250. if($request->input('current')) {
  251. $last_location = $request->input('current');
  252. Log::info('Device sent current location');
  253. }
  254. $response = [
  255. 'result' => 'ok',
  256. 'saved' => $num,
  257. 'trips' => $trips
  258. ];
  259. if($last_location) {
  260. /*
  261. // 2017-08-22 Don't geocode cause it takes too long. Maybe make a separate route for this later.
  262. $geocode = self::geocode(['latitude'=>$last_location['geometry']['coordinates'][1], 'longitude'=>$last_location['geometry']['coordinates'][0]]);
  263. $response['geocode'] = [
  264. 'full_name' => $geocode->full_name,
  265. 'locality' => $geocode->locality,
  266. 'country' => $geocode->country
  267. ];
  268. */
  269. $response['geocode'] = null;
  270. // Cache the last location in the database
  271. if(isset($last_location['properties']['timestamp'])) {
  272. if(preg_match('/^\d+\.\d+$/', $last_location['properties']['timestamp']))
  273. $date = DateTime::createFromFormat('U.u', $last_location['properties']['timestamp']);
  274. elseif(preg_match('/^\d+$/', $last_location['properties']['timestamp']))
  275. $date = DateTime::createFromFormat('U', $last_location['properties']['timestamp']);
  276. else
  277. $date = new DateTime($last_location['properties']['timestamp']);
  278. $date->setTimeZone(new DateTimeZone('UTC'));
  279. $date = $date->format('Y-m-d H:i:s');
  280. } else {
  281. $date = null;
  282. }
  283. DB::table('databases')->where('id', $db->id)
  284. ->update([
  285. 'last_location' => json_encode($last_location, JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT),
  286. 'last_location_date' => $date,
  287. ]);
  288. // Notify subscribers that new data is available
  289. if($db->ping_urls) {
  290. $job = (new NotifyOfNewLocations($db->id, $last_location))->onQueue('compass');
  291. $this->dispatch($job);
  292. }
  293. }
  294. if($request->input('trip')) {
  295. DB::table('databases')->where('id', $db->id)
  296. ->update([
  297. 'current_trip' => json_encode($request->input('trip'), JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT)
  298. ]);
  299. } else {
  300. DB::table('databases')->where('id', $db->id)->update(['current_trip' => null]);
  301. }
  302. return response(json_encode($response))->header('Content-Type', 'application/json');
  303. }
  304. public function trip_complete(Request $request) {
  305. $token = $request->input('token');
  306. if(!$token)
  307. return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
  308. $db = DB::table('databases')->where('write_token','=',$token)->first();
  309. if(!$db)
  310. return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
  311. if($request->input('tz')) {
  312. $tz = new DateTimeZone($request->input('tz'));
  313. } else {
  314. $tz = new DateTimeZone('UTC');
  315. }
  316. $start = new DateTime($request->input('start'), $tz);
  317. $end = new DateTime($request->input('end'), $tz);
  318. $loc = [
  319. 'properties' => [
  320. 'mode' => $request->input('mode'),
  321. 'start' => $start->format('c'),
  322. 'end' => $end->format('c'),
  323. ]
  324. ];
  325. try {
  326. $job = (new TripComplete($db->id, $loc))->onQueue('compass');
  327. $this->dispatch($job);
  328. Log::info('Got a manual trip record: '.$start->format('c').' '.$end->format('c'));
  329. } catch(Exception $e) {
  330. Log::warning('Received invalid trip');
  331. }
  332. return response(json_encode(['result' => 'ok']))->header('Content-Type', 'application/json');
  333. }
  334. public static function geocode($params) {
  335. $ch = curl_init(env('ATLAS_BASE').'api/geocode?'.http_build_query($params));
  336. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  337. curl_setopt($ch, CURLOPT_TIMEOUT, 8);
  338. $response = curl_exec($ch);
  339. if($response) {
  340. return json_decode($response);
  341. }
  342. }
  343. }