2019-01-01 19:13:08 +00:00
from datetime import timedelta
2016-07-02 18:07:22 +00:00
2019-01-01 19:13:08 +00:00
from sqlalchemy import and_ , or_ , insert , between , exists
2016-11-01 07:30:54 +00:00
from sqlalchemy . sql import func , null
2016-07-02 18:07:22 +00:00
from sqlalchemy . sql . expression import case
2019-02-10 17:39:06 +00:00
from ogn_python . model import AircraftBeacon , TakeoffLanding , Airport
2016-07-02 18:07:22 +00:00
2019-03-10 14:58:10 +00:00
from ogn_python import app
2016-07-02 18:07:22 +00:00
2019-03-11 22:24:55 +00:00
def update_entries ( session , start , end , logger = None ) :
2017-12-16 21:18:04 +00:00
""" Compute takeoffs and landings. """
2019-01-01 19:13:08 +00:00
2019-03-10 14:58:10 +00:00
if logger is None :
logger = app . logger
2016-07-02 18:07:22 +00:00
2019-03-10 14:58:10 +00:00
logger . info ( " Compute takeoffs and landings. " )
2016-07-02 18:07:22 +00:00
2019-03-11 22:24:55 +00:00
# considered time interval should not exceed a complete day
if end - start > timedelta ( days = 1 ) :
2019-03-31 08:58:55 +00:00
abort_message = " TakeoffLanding: timeinterval start= ' {} ' and end= ' {} ' is too big. " . format ( start , end )
logger . warn ( abort_message )
return abort_message
2019-03-11 22:24:55 +00:00
2016-11-01 07:30:54 +00:00
# check if we have any airport
2018-10-21 15:34:03 +00:00
airports_query = session . query ( Airport ) . limit ( 1 )
2016-11-01 07:30:54 +00:00
if not airports_query . all ( ) :
2019-03-31 08:58:55 +00:00
abort_message = " TakeoffLanding: Cannot calculate takeoff and landings without any airport! Please import airports first. "
logger . warn ( abort_message )
return abort_message
2016-11-01 07:30:54 +00:00
2019-01-01 19:13:08 +00:00
# takeoff / landing detection is based on 3 consecutive points all below a certain altitude AGL
2016-07-02 18:07:22 +00:00
takeoff_speed = 55 # takeoff detection: 1st point below, 2nd and 3rd above this limit
landing_speed = 40 # landing detection: 1st point above, 2nd and 3rd below this limit
duration = 100 # the points must not exceed this duration
2017-12-12 02:47:28 +00:00
radius = 5000 # the points must not exceed this radius around the 2nd point
2019-03-31 08:58:55 +00:00
max_agl = 200 # takeoff / landing must not exceed this altitude AGL
2016-07-02 18:07:22 +00:00
2019-03-31 08:58:55 +00:00
# get beacons for selected time range, one per device_id and timestamp
2019-01-01 19:13:08 +00:00
sq = session . query ( AircraftBeacon ) \
. distinct ( AircraftBeacon . device_id , AircraftBeacon . timestamp ) \
. order_by ( AircraftBeacon . device_id , AircraftBeacon . timestamp , AircraftBeacon . error_count ) \
. filter ( AircraftBeacon . agl < max_agl ) \
2019-03-31 08:58:55 +00:00
. filter ( between ( AircraftBeacon . timestamp , start , end ) ) \
2018-10-21 15:34:03 +00:00
. subquery ( )
2019-01-01 19:13:08 +00:00
2016-07-02 18:07:22 +00:00
# make a query with current, previous and next position
2019-01-01 19:13:08 +00:00
sq2 = session . query (
sq . c . device_id ,
func . lag ( sq . c . device_id ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' device_id_prev ' ) ,
func . lead ( sq . c . device_id ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' device_id_next ' ) ,
sq . c . timestamp ,
func . lag ( sq . c . timestamp ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' timestamp_prev ' ) ,
func . lead ( sq . c . timestamp ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' timestamp_next ' ) ,
sq . c . location ,
func . lag ( sq . c . location ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' location_wkt_prev ' ) ,
func . lead ( sq . c . location ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' location_wkt_next ' ) ,
sq . c . track ,
func . lag ( sq . c . track ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' track_prev ' ) ,
func . lead ( sq . c . track ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' track_next ' ) ,
sq . c . ground_speed ,
func . lag ( sq . c . ground_speed ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' ground_speed_prev ' ) ,
func . lead ( sq . c . ground_speed ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' ground_speed_next ' ) ,
sq . c . altitude ,
func . lag ( sq . c . altitude ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' altitude_prev ' ) ,
func . lead ( sq . c . altitude ) . over ( partition_by = sq . c . device_id , order_by = sq . c . timestamp ) . label ( ' altitude_next ' ) ) \
. subquery ( )
# consider only positions with predecessor and successor and limit distance and duration between points
sq3 = session . query ( sq2 ) \
. filter ( and_ ( sq2 . c . device_id_prev != null ( ) ,
sq2 . c . device_id_next != null ( ) ) ) \
. filter ( and_ ( func . ST_DistanceSphere ( sq2 . c . location , sq2 . c . location_wkt_prev ) < radius ,
func . ST_DistanceSphere ( sq2 . c . location , sq2 . c . location_wkt_next ) < radius ) ) \
. filter ( sq2 . c . timestamp_next - sq2 . c . timestamp_prev < timedelta ( seconds = duration ) ) \
2016-07-02 18:07:22 +00:00
. subquery ( )
2019-01-01 19:13:08 +00:00
2017-12-11 18:16:03 +00:00
# find possible takeoffs and landings
2019-01-01 19:13:08 +00:00
sq4 = session . query (
sq3 . c . timestamp ,
case ( [ ( sq3 . c . ground_speed > takeoff_speed , sq3 . c . location_wkt_prev ) , # on takeoff we take the location from the previous fix because it is nearer to the airport
( sq3 . c . ground_speed < = takeoff_speed , sq3 . c . location ) ] ) . label ( ' location ' ) ,
case ( [ ( sq3 . c . ground_speed > landing_speed , sq3 . c . track ) ,
( sq3 . c . ground_speed < = landing_speed , sq3 . c . track_prev ) ] ) . label ( ' track ' ) , # on landing we take the track from the previous fix because gliders tend to leave the runway quickly
sq3 . c . ground_speed ,
sq3 . c . altitude ,
case ( [ ( sq3 . c . ground_speed > takeoff_speed , True ) ,
( sq3 . c . ground_speed < landing_speed , False ) ] ) . label ( ' is_takeoff ' ) ,
sq3 . c . device_id ) \
. filter ( or_ ( and_ ( sq3 . c . ground_speed_prev < takeoff_speed , # takeoff
sq3 . c . ground_speed > takeoff_speed ,
sq3 . c . ground_speed_next > takeoff_speed ) ,
and_ ( sq3 . c . ground_speed_prev > landing_speed , # landing
sq3 . c . ground_speed < landing_speed ,
sq3 . c . ground_speed_next < landing_speed ) ) ) \
. subquery ( )
# consider them if the are near airports ...
2018-10-21 15:34:03 +00:00
sq5 = session . query (
sq4 . c . timestamp ,
2019-01-01 19:13:08 +00:00
sq4 . c . track ,
sq4 . c . is_takeoff ,
sq4 . c . device_id ,
Airport . id . label ( ' airport_id ' ) ,
func . ST_DistanceSphere ( sq4 . c . location , Airport . location_wkt ) . label ( ' airport_distance ' ) ) \
. filter ( and_ ( func . ST_Within ( sq4 . c . location , Airport . border ) ,
between ( Airport . style , 2 , 5 ) ) ) \
2016-11-01 07:30:54 +00:00
. subquery ( )
2019-01-01 19:13:08 +00:00
# ... and take the nearest airport
sq6 = session . query ( sq5 . c . timestamp , sq5 . c . track , sq5 . c . is_takeoff , sq5 . c . device_id , sq5 . c . airport_id ) \
2019-03-31 08:58:55 +00:00
. distinct ( sq5 . c . timestamp , sq5 . c . track , sq5 . c . is_takeoff , sq5 . c . device_id ) \
. order_by ( sq5 . c . timestamp , sq5 . c . track , sq5 . c . is_takeoff , sq5 . c . device_id , sq5 . c . airport_distance ) \
2016-07-17 08:25:56 +00:00
. subquery ( )
2019-01-01 19:13:08 +00:00
2016-07-17 08:25:56 +00:00
# consider them only if they are not already existing in db
2018-10-21 15:34:03 +00:00
takeoff_landing_query = session . query ( sq6 ) \
2016-07-17 08:25:56 +00:00
. filter ( ~ exists ( ) . where (
2018-10-21 15:34:03 +00:00
and_ ( TakeoffLanding . timestamp == sq6 . c . timestamp ,
TakeoffLanding . device_id == sq6 . c . device_id ,
TakeoffLanding . airport_id == sq6 . c . airport_id ) ) )
2019-01-01 19:13:08 +00:00
2016-07-02 18:07:22 +00:00
# ... and save them
ins = insert ( TakeoffLanding ) . from_select ( ( TakeoffLanding . timestamp ,
TakeoffLanding . track ,
TakeoffLanding . is_takeoff ,
TakeoffLanding . device_id ,
TakeoffLanding . airport_id ) ,
takeoff_landing_query )
2018-10-21 15:34:03 +00:00
2016-07-02 18:07:22 +00:00
result = session . execute ( ins )
2018-10-21 15:34:03 +00:00
session . commit ( )
2018-01-21 20:06:27 +00:00
insert_counter = result . rowcount
2019-03-31 08:58:55 +00:00
finish_message = " TakeoffLandings: {} inserted " . format ( insert_counter )
2019-03-30 16:50:29 +00:00
logger . info ( finish_message )
return finish_message