|
|
- <?php
-
- namespace App\Http\Controllers;
-
- use Laravel\Lumen\Routing\Controller as BaseController;
- use Illuminate\Http\Request;
- use DB, Log, Cache;
- use Quartz;
- use DateTime, DateTimeZone, DateInterval;
- use App\Jobs\TripComplete;
- use App\Jobs\NotifyOfNewLocations;
-
- class Api extends BaseController
- {
-
- public function account(Request $request) {
- $token = $request->input('token');
- if(!$token)
- return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
-
- $db = DB::table('databases')->where('write_token','=',$token)->first();
- if(!$db)
- return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
-
- return response(json_encode(['name' => $db->name]))->header('Content-Type', 'application/json');
- }
-
- public function query(Request $request) {
- $token = $request->input('token');
- if(!$token)
- return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
-
- $db = DB::table('databases')->where('read_token','=',$token)->first();
- if(!$db)
- return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
-
- $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'r');
-
- if($request->input('tz')) {
- $tz = $request->input('tz');
- } else {
- $tz = 'UTC';
- }
-
- if($date=$request->input('date')) {
- $start = DateTime::createFromFormat('Y-m-d H:i:s', $date.' 00:00:00', new DateTimeZone($tz));
- $end = DateTime::createFromFormat('Y-m-d H:i:s', $date.' 23:59:59', new DateTimeZone($tz));
- } elseif(($start=$request->input('start')) && ($end=$request->input('end'))) {
- $start = new DateTime($start, new DateTimeZone($tz));
- $end = new DateTime($end, new DateTimeZone($tz));
- } else {
- return response(json_encode(['error' => 'no date provided']))->header('Content-Type', 'application/json');
- }
-
- $results = $qz->queryRange($start, $end);
-
- $locations = [];
- $properties = [];
- $events = [];
-
- if($request->input('format') == 'linestring') {
-
- foreach($results as $id=>$record) {
- // When returning a linestring, separate out the "event" records from the "location" records
- if($record->data) {
- if(property_exists($record->data->properties, 'action')) {
- $rec = $record->data;
- # add a unixtime property
- $rec->properties->unixtime = (int)$record->date->format('U');
- $events[] = $rec;
- } else {
- #$record->date->format('U.u');
- // Ignore super inaccurate locations
- if(!property_exists($record->data->properties, 'horizontal_accuracy')
- || $record->data->properties->horizontal_accuracy <= 5000) {
-
- $locations[] = $record->data;
- $props = $record->data->properties;
- $date = $record->date;
- $date->setTimeZone(new DateTimeZone($tz));
- $props->timestamp = $date->format('c');
- $props->unixtime = (int)$date->format('U');
- $properties[] = $props;
- }
- }
- }
- }
-
- $linestring = array(
- 'type' => 'LineString',
- 'coordinates' => [],
- 'properties' => $properties
- );
- foreach($locations as $loc) {
- if(property_exists($loc, 'geometry'))
- $linestring['coordinates'][] = $loc->geometry->coordinates;
- else
- $linestring['coordinates'][] = null;
- }
-
- $response = array(
- 'linestring' => $linestring,
- 'events' => $events
- );
-
- } else {
- foreach($results as $id=>$record) {
- if($record->data) {
- $locations[] = $record->data;
- }
- }
-
- $response = [
- 'locations' => $locations
- ];
- }
-
- return response(json_encode($response))->header('Content-Type', 'application/json');
- }
-
- public function last(Request $request) {
- $token = $request->input('token');
- if(!$token)
- return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
-
- $db = DB::table('databases')->where('read_token','=',$token)->first();
- if(!$db)
- return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
-
- if($request->input('tz')) {
- $tz = $request->input('tz');
- } else {
- $tz = 'UTC';
- }
-
- if($input=$request->input('before')) {
- // If a specific time was requested, look up the data in the filesystem
-
- $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'r');
-
- if(preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/', $input)) {
- // If the input date is given in YYYY-mm-dd HH:mm:ss format, interpret it in the timezone given
- $date = DateTime::createFromFormat('Y-m-d H:i:s', $input, new DateTimeZone($tz));
- } else {
- // Otherwise, parse the string and use the timezone in the input
- $date = new DateTime($input);
- $date->setTimeZone(new DateTimeZone($tz));
- }
-
- if(!$date) {
- return response(json_encode(['error' => 'invalid date provided']))->header('Content-Type', 'application/json');
- }
-
- /* ********************************************** */
- // TODO: move this logic into QuartzDB
-
- // Load the shard for the given date
- $shard = $qz->shardForDate($date);
- // If the shard doesn't exist, check one day before
- if(!$shard->exists()) {
- $date = $date->sub(new DateInterval('PT86400S'));
- $shard = $qz->shardForDate($date);
- }
- // Now if the shard doesn't exist, return an empty result
- if(!$shard->exists()) {
- return response(json_encode([
- 'data'=>null
- ]));
- }
-
- // Start iterating through the shard and look for the last line that is before the given date
- $shard->init();
- $record = false;
- foreach($shard as $r) {
- if($r->date > $date)
- break;
- $record = $r;
- }
- /* ********************************************** */
-
- if(!$record) {
- return response(json_encode([
- 'data'=>null
- ]));
- }
-
- $response = [
- 'data' => $record->data,
- ];
- } else {
- // If no specific time was requested, use the cached location from the database
- $response = [
- 'data' => json_decode($db->last_location),
- ];
- }
-
- if($request->input('geocode') && property_exists($response['data'], 'geometry') && property_exists($response['data']->geometry, 'coordinates')) {
- $coords = $response['data']->geometry->coordinates;
- $params = [
- 'latitude' => $coords[1],
- 'longitude' => $coords[0],
- 'date' => $response['data']->properties->timestamp
- ];
- $geocode = self::geocode($params);
- if($geocode) {
- $response['geocode'] = $geocode;
- } else {
- $response['geocode'] = null;
- }
- }
-
- return response(json_encode($response))->header('Content-Type', 'application/json');;
- }
-
- public function input(Request $request) {
- $token = $request->input('token');
- if(!$token)
- return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
-
- $db = DB::table('databases')->where('write_token','=',$token)->first();
- if(!$db)
- return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
-
- if(!is_array($request->input('locations')))
- 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');
-
- $qz = new Quartz\DB(env('STORAGE_DIR').$db->name, 'w');
-
- $num = 0;
- $trips = 0;
- $last_location = false;
- foreach($request->input('locations') as $loc) {
- if(array_key_exists('properties', $loc)) {
- if(array_key_exists('timestamp', $loc['properties'])) {
- try {
- if(preg_match('/^\d+\.\d+$/', $loc['properties']['timestamp']))
- $date = DateTime::createFromFormat('U.u', $loc['properties']['timestamp']);
- elseif(preg_match('/^\d+$/', $loc['properties']['timestamp']))
- $date = DateTime::createFromFormat('U', $loc['properties']['timestamp']);
- else
- $date = new DateTime($loc['properties']['timestamp']);
-
- if($date) {
- $cacheKey = 'compass::'.$db->name.'::'.$date->format('U');
-
- // Skip adding if the timestamp is already in the cache.
- // Helps prevent writing duplicate data when the HTTP request is interrupted.
- if(!env('CACHE_DRIVER') || !Cache::has($cacheKey)) {
- $num++;
- $qz->add($date, $loc);
- if(env('CACHE_DRIVER'))
- Cache::put($cacheKey, 1, 360); // cache this for 6 hours
- $last_location = $loc;
- }
-
- if(array_key_exists('type', $loc['properties']) && $loc['properties']['type'] == 'trip') {
- try {
- $job = (new TripComplete($db->id, $loc))->onQueue('compass');
- $this->dispatch($job);
- $trips++;
- Log::info('Got a trip record');
- } catch(Exception $e) {
- Log::warning('Received invalid trip');
- }
- }
-
- } else {
- Log::warning('Received invalid date: ' . $loc['properties']['timestamp']);
- }
- } catch(Exception $e) {
- Log::warning('Received invalid date: ' . $loc['properties']['timestamp']);
- }
- }
- }
- }
-
- if($request->input('current')) {
- $last_location = $request->input('current');
- Log::info('Device sent current location');
- }
-
- $response = [
- 'result' => 'ok',
- 'saved' => $num,
- 'trips' => $trips
- ];
-
- if($last_location) {
- /*
- // 2017-08-22 Don't geocode cause it takes too long. Maybe make a separate route for this later.
- $geocode = self::geocode(['latitude'=>$last_location['geometry']['coordinates'][1], 'longitude'=>$last_location['geometry']['coordinates'][0]]);
- $response['geocode'] = [
- 'full_name' => $geocode->full_name,
- 'locality' => $geocode->locality,
- 'country' => $geocode->country
- ];
- */
- $response['geocode'] = null;
-
- // Cache the last location in the database
- if(isset($last_location['properties']['timestamp'])) {
- if(preg_match('/^\d+\.\d+$/', $last_location['properties']['timestamp']))
- $date = DateTime::createFromFormat('U.u', $last_location['properties']['timestamp']);
- elseif(preg_match('/^\d+$/', $last_location['properties']['timestamp']))
- $date = DateTime::createFromFormat('U', $last_location['properties']['timestamp']);
- else
- $date = new DateTime($last_location['properties']['timestamp']);
- $date->setTimeZone(new DateTimeZone('UTC'));
- $date = $date->format('Y-m-d H:i:s');
- } else {
- $date = null;
- }
-
- DB::table('databases')->where('id', $db->id)
- ->update([
- 'last_location' => json_encode($last_location, JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT),
- 'last_location_date' => $date,
- ]);
-
- // Notify subscribers that new data is available
- if($db->ping_urls) {
- $job = (new NotifyOfNewLocations($db->id, $last_location))->onQueue('compass');
- $this->dispatch($job);
- }
- }
-
- if($request->input('trip')) {
- DB::table('databases')->where('id', $db->id)
- ->update([
- 'current_trip' => json_encode($request->input('trip'), JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT)
- ]);
- } else {
- DB::table('databases')->where('id', $db->id)->update(['current_trip' => null]);
- }
-
- return response(json_encode($response))->header('Content-Type', 'application/json');
- }
-
- public function trip_complete(Request $request) {
- $token = $request->input('token');
- if(!$token)
- return response(json_encode(['error' => 'no token provided']))->header('Content-Type', 'application/json');
-
- $db = DB::table('databases')->where('write_token','=',$token)->first();
- if(!$db)
- return response(json_encode(['error' => 'invalid token']))->header('Content-Type', 'application/json');
-
- if($request->input('tz')) {
- $tz = new DateTimeZone($request->input('tz'));
- } else {
- $tz = new DateTimeZone('UTC');
- }
- $start = new DateTime($request->input('start'), $tz);
- $end = new DateTime($request->input('end'), $tz);
-
- $loc = [
- 'properties' => [
- 'mode' => $request->input('mode'),
- 'start' => $start->format('c'),
- 'end' => $end->format('c'),
- ]
- ];
-
- try {
- $job = (new TripComplete($db->id, $loc))->onQueue('compass');
- $this->dispatch($job);
- Log::info('Got a manual trip record: '.$start->format('c').' '.$end->format('c'));
- } catch(Exception $e) {
- Log::warning('Received invalid trip');
- }
-
- return response(json_encode(['result' => 'ok']))->header('Content-Type', 'application/json');
- }
-
- public static function geocode($params) {
- $ch = curl_init(env('ATLAS_BASE').'api/geocode?'.http_build_query($params));
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_TIMEOUT, 8);
- $response = curl_exec($ch);
- if($response) {
- return json_decode($response);
- }
- }
-
- }
|