01234567890123456789012345678901234567890123456789012345678901234567890123456789
184185186187188189190191192193194195196197198199200201202203 204205206207208209210211212213214215216217218219220221222223224 1693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720 1721172217231724172517261727 172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751 24962497249824992500250125022503250425052506250725082509251025112512251325142515 25162517251825192520252125222523252425252526252725282529253025312532253325342535 36753676367736783679368036813682368336843685368636873688368936903691369236933694 36953696369736983699370037013702370337043705370637073708370937103711371237133714 40384039404040414042404340444045404640474048404940504051405240534054405540564057 405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093 40944095409640974098409941004101410241034104410541064107410841094110411141124113 41544155415641574158415941604161416241634164416541664167416841694170417141724173 41744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214 42154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249 42504251425242534254425542564257425842594260426142624263426442654266426742684269 43844385438643874388438943904391439243934394439543964397439843994400440144024403 44044405440644074408440944104411441244134414441544164417441844194420442144224423 56885689569056915692569356945695569656975698569957005701570257035704570557065707 57085709571057115712571357145715571657175718571957205721572257235724572557265727 | <----SKIPPED LINES----> FLAG_INSIGHT_DIFF_AIRCRAFT = 1 FLAG_INSIGHT_NTH_FLIGHT = 2 FLAG_INSIGHT_GROUNDSPEED = 3 FLAG_INSIGHT_ALTITUDE = 4 FLAG_INSIGHT_VERTRATE = 5 FLAG_INSIGHT_FIRST_DEST = 6 FLAG_INSIGHT_FIRST_ORIGIN = 7 FLAG_INSIGHT_FIRST_AIRLINE = 8 FLAG_INSIGHT_FIRST_AIRCRAFT = 9 FLAG_INSIGHT_LONGEST_DELAY = 10 FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11 FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12 FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13 FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14 FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15 FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16 FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17 FLAG_INSIGHT_HOUR_DELAY_TIME = 18 FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19 FLAG_INSIGHT_DATE_DELAY_TIME = 20 INSIGHT_TYPES = 21 TEMP_FAN_TURN_ON_CELSIUS = 65 TEMP_FAN_TURN_OFF_CELSIUS = 55 # GPIO relay connections # format: (GPIO pin, true message, false message, relay number, # description, initial_state) GPIO_ERROR_VESTABOARD_CONNECTION = ( 22, 'ERROR: Vestaboard unavailable', 'SUCCESS: Vestaboard available', 1, 'Vestaboard connected', False) GPIO_ERROR_FLIGHT_AWARE_CONNECTION = ( 23, 'ERROR: FlightAware not available', 'SUCCESS: FlightAware available', 2, 'FlightAware connected', False) GPIO_ERROR_ARDUINO_SERVO_CONNECTION = ( 24, 'ERROR: Servos not running or lost connection', <----SKIPPED LINES----> simplified_aircraft['lon'] = lon altitude = aircraft.get('altitude', aircraft.get('alt_baro')) if isinstance(altitude, numbers.Number): simplified_aircraft['altitude'] = altitude speed = aircraft.get('speed', aircraft.get('gs')) if speed is not None: simplified_aircraft['speed'] = speed vert_rate = aircraft.get('vert_rate', aircraft.get('baro_rate')) if vert_rate is not None: simplified_aircraft['vert_rate'] = vert_rate track = aircraft.get('track') if isinstance(track, numbers.Number): min_meters = MinMetersToHome((lat, lon), track) simplified_aircraft['track'] = track simplified_aircraft['min_feet'] = min_meters * FEET_IN_METER # TODO: describe why we want to base this off haversine distance ( # i.e.: the actual distance from home) vs. MinMetersToHome (i.e.: # forecasted min distance from home); it seems like the latter would # give us more time to respond? - maybe because there might be other # closer flights even though a far away flight might look like it's # going to come nearby? haversine_distance_meters = HaversineDistanceMeters(HOME, (lat, lon)) simplified_aircraft['distance'] = haversine_distance_meters if haversine_distance_meters < MIN_METERS: #nearby_aircraft[id_to_use]['distance'] = haversine_distance_meters nearby_aircraft[id_to_use] = simplified_aircraft if flight_number: nearby_aircraft[id_to_use]['flight_number'] = flight_number if squawk: nearby_aircraft[id_to_use]['squawk'] = squawk # aircraft classification: # https://github.com/wiedehopf/adsb-wiki/wiki/ # ADS-B-aircraft-categories category = aircraft.get('category') if category is not None: nearby_aircraft[id_to_use]['category'] = category # keep all that track info - once we start reporting on a nearby # flight, it will become part of the flight's persistent record. Also, # note that as we are building a list of tracks for each flight, and we # are later assigning the flight dictionary to point to the list, we # just simply need to continue updating this list to keep the # dictionary up to date (i.e.: we don't need to directly touch the # flights dictionary in main). (last_seen, current_path) = persistent_path.get(id_to_use, (None, [])) if ( # flight position has been updated with this radio signal not current_path or simplified_aircraft.get('lat') != current_path[-1].get('lat') or simplified_aircraft.get('lon') != current_path[-1].get('lon')): current_path.append(simplified_aircraft) persistent_path[id_to_use] = (now, current_path) # if the flight was last seen too far in the past, remove the track info <----SKIPPED LINES----> days_ago: the minimum time difference for which a message should be generated - i.e.: many flights are daily, and so we are not necessarily interested to see about every daily flight that it was seen yesterday. However, more infrequent flights might be of interest. Returns: Printable string message; if no message or insights to generate, then an empty string. """ message = '' this_flight = flights[-1] this_flight_number = DisplayFlightNumber(this_flight) this_timestamp = flights[-1]['now'] last_seen = [ f for f in flights[:-1] if DisplayFlightNumber(f) == this_flight_number] if last_seen and 'flight_number' in this_flight: last_timestamp = last_seen[-1]['now'] if this_timestamp - last_timestamp > days_ago*SECONDS_IN_DAY: message = '%s was last seen %s ago' % ( this_flight_number, SecondsToDdHh(this_timestamp - last_timestamp)) return message def FlightInsightDifferentAircraft(flights, percent_size_difference=0.1): """Generates string indicating changes in aircraft for the most recent flight. Generates text of the following form for the "focus" flight in the data. - Last time ASA1964 was seen on Mar 16, it was with a much larger plane (Airbus A320 (twin-jet) @ 123ft vs. Airbus A319 (twin-jet) @ 111ft) - Last time ASA743 was seen on Mar 19, it was with a different type of airpline (Boeing 737-900 (twin-jet) vs. Boeing 737-800 (twin-jet)) Args: flights: the list of the raw data from which the insights will be generated, where the flights are listed in order of observation - i.e.: flights[0] was the earliest seen, and flights[-1] is the most recent flight for which we are attempting to generate an insight. percent_size_difference: the minimum size (i.e.: length) difference for the insight to warrant including the size details. <----SKIPPED LINES----> messages = [] def AppendMessageType(message_type, message): if message: messages.append((message_type, message)) # This flight number was last seen x days ago AppendMessageType( FLAG_INSIGHT_LAST_SEEN, FlightInsightLastSeen(flights, days_ago=2)) # Yesterday this same flight flew a materially different type of aircraft AppendMessageType( FLAG_INSIGHT_DIFF_AIRCRAFT, FlightInsightDifferentAircraft(flights, percent_size_difference=0.1)) # This is the 3rd flight to the same destination in the last hour AppendMessageType( FLAG_INSIGHT_NTH_FLIGHT, FlightInsightNthFlight(flights, hours=1, min_multiple_flights=2)) # This is the [lowest / highest] [speed / altitude / climbrate] # in the last 24 hours AppendMessageType(FLAG_INSIGHT_GROUNDSPEED, FlightInsightSuperlativeAttribute( flights, 'speed', 'groundspeed', SPEED_UNITS, ['slowest', 'fastest'], hours=HOURS_IN_DAY)) AppendMessageType(FLAG_INSIGHT_ALTITUDE, FlightInsightSuperlativeAttribute( flights, 'altitude', 'altitude', DISTANCE_UNITS, ['lowest', 'highest'], hours=HOURS_IN_DAY)) AppendMessageType( FLAG_INSIGHT_VERTRATE, FlightInsightSuperlativeVertrate(flights)) # First instances: destination, first aircraft, etc. <----SKIPPED LINES----> title += ' (%+d)' % (round(sum(values) - sum(last_values))) ax.set_title(title) ax.set_ylabel('Average Observed Flights') if comparison: ax.legend() matplotlib.pyplot.xticks( x, keys, rotation='vertical', wrap=True, horizontalalignment='right', verticalalignment='center') matplotlib.pyplot.savefig(filename) matplotlib.pyplot.close() def GenerateHistogramData( data, keyfunction, sort_type, truncate=float('inf'), hours=float('inf'), max_distance_feet=float('inf'), max_altitude_feet=float('inf'), normalize_factor=0, exhaustive=False): """Generates sorted data for a histogram from a description of the flights. Given an iterable describing the flights, this function generates the label (or key), and the frequency (or value) from which a histogram can be rendered. Args: data: the iterable of the raw data from which the histogram will be generated; each element of the iterable is a dictionary, that contains at least the key 'now', and depending on other parameters, also potentially 'min_feet' amongst others. keyfunction: the function that determines how the key or label of the histogram should be generated; it is called for each element of the data iterable. For instance, to simply generate a histogram on the attribute 'heading', keyfunction would be lambda a: a['heading']. sort_type: determines how the keys (and the corresponding values) are sorted: 'key': the keys are sorted by a simple comparison operator between them, which sorts strings alphabetically and numbers numerically. 'value': the keys are sorted by a comparison between the values, which means that more frequency-occurring keys are listed first. list: if instead of the strings a list is passed, the keys are then sorted in the sequence enumerated in the list. This is useful for, say, ensuring that the days of the week (Tues, Wed, Thur, ...) are listed in sequence. Keys that are generated by keyfunction but that are not in the given list are sorted last (and then amongst those, alphabetically). truncate: integer indicating the maximum number of keys to return; if set to 0, or if set to a value larger than the number of keys, no truncation occurs. But if set to a value less than the number of keys, then the keys with the lowest frequency are combined into one key named OTHER_STRING so that the number of keys in the resulting histogram (together with OTHER_STRING) is equal to truncate. hours: integer indicating the number of hours of history to include. Flights with a calcd_display_time more than this many hours in the past are excluded from the histogram generation. Note that this is timezone aware, so that if the histogram data is generated on a machine with a different timezone than that that recorded the original data, the correct number of hours is still honored. max_distance_feet: number indicating the geo fence outside of which flights should be ignored for the purposes of including the flight data in the histogram. max_altitude_feet: number indicating the maximum altitude outside of which flights should be ignored for the purposes of including the flight data in the histogram. normalize_factor: divisor to apply to all the values, so that we can easily renormalize the histogram to display on a percentage or daily basis; if zero, no renormalization is applied. exhaustive: boolean only relevant if sort_type is a list, in which case, this ensures that the returned set of keys (and matching values) contains all the elements in the list, including potentially those with a frequency of zero, within therestrictions of truncate. <----SKIPPED LINES----> missing_values = [0 for unused_k in missing_keys] keys.extend(missing_keys) values.extend(missing_values) if keys: # filters could potentially have removed all data if not truncate or len(keys) <= truncate: if sort_by_enumerated_list: (values, keys) = SortByDefinedList(values, keys, sort_type) elif sort_type == 'value': (values, keys) = SortByValues(values, keys) else: (values, keys) = SortByKeys(values, keys) else: #Unknown might fall in the middle, and so shouldn't be truncated (values, keys) = SortByValues( values, keys, ignore_sort_at_end_strings=True) truncated_values = list(values[:truncate-1]) truncated_keys = list(keys[:truncate-1]) other_value = sum(values[truncate-1:]) truncated_values.append(other_value) truncated_keys.append(OTHER_STRING) if sort_by_enumerated_list: (values, keys) = SortByDefinedList( truncated_values, truncated_keys, sort_type) elif sort_type == 'value': (values, keys) = SortByValues( truncated_values, truncated_keys, ignore_sort_at_end_strings=False) else: (values, keys) = SortByKeys(truncated_values, truncated_keys) else: values = [] keys = [] return (values, keys, filtered_data) def SortByValues(values, keys, ignore_sort_at_end_strings=False): """Sorts list of values in desc sequence, applying same resorting to keys. Given a list of keys and values representing a histogram, returns two new lists that are sorted so that the values occur in descending sequence and the keys are moved around in the same way. This allows the printing of a histogram with the largest keys listed first - i.e.: top five airlines. Keys identified by SORT_AT_END_STRINGS - such as, perhaps, 'Other' - will optionally be placed at the end of the sequence. And where values are identical, the secondary sort is based on the keys. Args: values: list of values for the histogram to be used as the primary sort key. keys: list of keys for the histogram that will be moved in the same way as the values. ignore_sort_at_end_strings: boolean indicating whether specially-defined keys will be sorted at the end. Returns: 2-tuple of (values, keys) lists sorted as described above """ if ignore_sort_at_end_strings: sort_at_end_strings = [] else: sort_at_end_strings = SORT_AT_END_STRINGS return SortZipped( values, keys, True, lambda a: ( False, False, a[1]) if a[1] in sort_at_end_strings else (True, a[0], a[1])) def SortByKeys(values, keys, ignore_sort_at_end_strings=False): """Sorts list of keys in asc sequence, applying same resorting to values. Given a list of keys and values representing a histogram, returns two new lists that are sorted so that the keys occur in ascending alpha sequence and the values are moved around in the same way. This allows the printing of a histogram with the first keys alphabetically listed first - i.e.: 7am, 8am, 9am. Keys identified by SORT_AT_END_STRINGS - such as, perhaps, 'Other' - will optionally be placed at the end of the sequence. Args: values: list of values for the histogram that will be moved in the same way as the keys. keys: list of keys for the histogram to be used as the primary sort key. ignore_sort_at_end_strings: boolean indicating whether specially-defined keys will be sorted at the end. Returns: 2-tuple of (values, keys) lists sorted as described above """ if ignore_sort_at_end_strings: sort_at_end_strings = [] else: sort_at_end_strings = SORT_AT_END_STRINGS return SortZipped( values, keys, False, lambda a: (True, a[1]) if a[1] in sort_at_end_strings else (False, a[1])) def SortByDefinedList(values, keys, sort_sequence): """Sorts keys in user-enumerated sequence, applying same resorting to values. Given a list of keys and values representing a histogram, returns two new lists that are sorted so that the keys occur in the specific sequence identified in the list sort_sequence, while the values are moved around in the same way. This allows the printing of a histogram with the keys occurring in a canonical order - i.e.: Tuesday, Wednesday, Thursday. Keys present in keys but not existing in sort_sequence are then sorted at the end, but amongst them, sorted based on the value. Args: values: list of values for the histogram that will be moved in the same way as the keys. <----SKIPPED LINES----> should be ignored for the purposes of including the flight data in the histogram. max_altitude_feet: number indicating the maximum altitude outside of which flights should be ignored for the purposes of including the flight data in the histogram. normalize_factor: divisor to apply to all the values, so that we can easily renormalize the histogram to display on a percentage or daily basis; if zero, no renormalization is applied. exhaustive: boolean only relevant if sort_type is a list, in which case, this ensures that the returned set of keys (and matching values) contains all the elements in the list, including potentially those with a frequency of zero, within the restrictions of truncate. figsize_inches: a 2-tuple of width, height indicating the size of the histogram. """ (values, keys, filtered_data) = GenerateHistogramData( data, keyfunction, sort_type, truncate=truncate, hours=hours, max_distance_feet=max_distance_feet, max_altitude_feet=max_altitude_feet, normalize_factor=normalize_factor, exhaustive=exhaustive) if position: matplotlib.pyplot.subplot(*position) matplotlib.pyplot.figure(figsize=figsize_inches) values_coordinates = numpy.arange(len(keys)) matplotlib.pyplot.bar(values_coordinates, values) # The filtering may have removed any flight data, # or there may be none to start if not filtered_data: return earliest_flight_time = int(filtered_data[0]['now']) last_flight_time = int(filtered_data[-1]['now']) date_range_string = ' %d flights over last %s hours' % ( sum(values), SecondsToDdHh(last_flight_time - earliest_flight_time)) <----SKIPPED LINES----> return None if p is None or not p.is_alive(): if p is None: Log('Process for %s starting for first time' % str(start_function)) elif VERBOSE: Log('Process (%s) for %s died; restarting' % (str(p), str(start_function))) args[2].value = 0 # (re)set shutdown flag to allow function to run p = multiprocessing.Process(target=start_function, args=args) p.daemon = True p.start() return p def LastFlightAvailable(flights, screen_history): """Returns True if splitflap display not displaying last flight message.""" if not screen_history: return False last_message_tuple = screen_history[-1] last_message_type = last_message_tuple[0] if last_message_type == FLAG_MSG_FLIGHT: last_message_flight = last_message_tuple[2] if SameFlight(last_message_flight, flights[-1]): return False # already displaying the last flight! return True def EnqueueArduinos( flights, json_desc_dict, configuration, to_remote_q, to_servo_q, screen_history): """Send latest data to arduinos via their shared-memory queues. Args: flights: List of all flights. json_desc_dict: Dictionary of additional attributes about radio. configuration: Dictionary of configuration settings. to_remote_q: Multi-processing messaging queue for one-way comm from <----SKIPPED LINES----> |
01234567890123456789012345678901234567890123456789012345678901234567890123456789
184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 16941695169616971698169917001701170217031704170517061707170817091710171117121713 17141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742 17431744174517461747174817491750175117521753175417551756175717581759176017611762 25072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577 37173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760 408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164 42054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326 44414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481 574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787 | <----SKIPPED LINES----> FLAG_INSIGHT_DIFF_AIRCRAFT = 1 FLAG_INSIGHT_NTH_FLIGHT = 2 FLAG_INSIGHT_GROUNDSPEED = 3 FLAG_INSIGHT_ALTITUDE = 4 FLAG_INSIGHT_VERTRATE = 5 FLAG_INSIGHT_FIRST_DEST = 6 FLAG_INSIGHT_FIRST_ORIGIN = 7 FLAG_INSIGHT_FIRST_AIRLINE = 8 FLAG_INSIGHT_FIRST_AIRCRAFT = 9 FLAG_INSIGHT_LONGEST_DELAY = 10 FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY = 11 FLAG_INSIGHT_FLIGHT_DELAY_TIME = 12 FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY = 13 FLAG_INSIGHT_AIRLINE_DELAY_TIME = 14 FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY = 15 FLAG_INSIGHT_DESTINATION_DELAY_TIME = 16 FLAG_INSIGHT_HOUR_DELAY_FREQUENCY = 17 FLAG_INSIGHT_HOUR_DELAY_TIME = 18 FLAG_INSIGHT_DATE_DELAY_FREQUENCY = 19 FLAG_INSIGHT_DATE_DELAY_TIME = 20 FLAG_INSIGHT_HELICOPTER = 21 INSIGHT_TYPES = 22 TEMP_FAN_TURN_ON_CELSIUS = 65 TEMP_FAN_TURN_OFF_CELSIUS = 55 # GPIO relay connections # format: (GPIO pin, true message, false message, relay number, # description, initial_state) GPIO_ERROR_VESTABOARD_CONNECTION = ( 22, 'ERROR: Vestaboard unavailable', 'SUCCESS: Vestaboard available', 1, 'Vestaboard connected', False) GPIO_ERROR_FLIGHT_AWARE_CONNECTION = ( 23, 'ERROR: FlightAware not available', 'SUCCESS: FlightAware available', 2, 'FlightAware connected', False) GPIO_ERROR_ARDUINO_SERVO_CONNECTION = ( 24, 'ERROR: Servos not running or lost connection', <----SKIPPED LINES----> simplified_aircraft['lon'] = lon altitude = aircraft.get('altitude', aircraft.get('alt_baro')) if isinstance(altitude, numbers.Number): simplified_aircraft['altitude'] = altitude speed = aircraft.get('speed', aircraft.get('gs')) if speed is not None: simplified_aircraft['speed'] = speed vert_rate = aircraft.get('vert_rate', aircraft.get('baro_rate')) if vert_rate is not None: simplified_aircraft['vert_rate'] = vert_rate track = aircraft.get('track') if isinstance(track, numbers.Number): min_meters = MinMetersToHome((lat, lon), track) simplified_aircraft['track'] = track simplified_aircraft['min_feet'] = min_meters * FEET_IN_METER haversine_distance_meters = HaversineDistanceMeters(HOME, (lat, lon)) simplified_aircraft['distance'] = haversine_distance_meters # We only hit FlightAware for flights within this distance from the # house. We don't want too large a perimeter, or else we might get # too many hits on FA, and potentially raise suspicion and get cut # off; we don't want too small a perimeter, or else we won't have # enough time to process & get a message on the splitflap display # before the flight is heard overhead. Note that this is separate # from the sensitivity setting controlled by the user; the sensitivity # can be set lower, and this method will still "detail" the full # set of flights. This is so that we can do posthoc analysis, # including analysis of how changing the sensitivity may increase # or decrease the frequency of messageboard messages. # # The actual filtering of which flights are displayed to the # splitflap display, based on user-set sensitivity, is done in # FlightMeetsDisplayCriteria. if haversine_distance_meters < MIN_METERS: #nearby_aircraft[id_to_use]['distance'] = haversine_distance_meters nearby_aircraft[id_to_use] = simplified_aircraft if flight_number: nearby_aircraft[id_to_use]['flight_number'] = flight_number if squawk: nearby_aircraft[id_to_use]['squawk'] = squawk # aircraft classification: # https://github.com/wiedehopf/adsb-wiki/wiki/ # ADS-B-aircraft-categories category = aircraft.get('category') if category is not None: nearby_aircraft[id_to_use]['category'] = category # keep all that track info - once we start reporting on a nearby # flight, it will become part of the flight's persistent record. Also, # note that as we are building a list of tracks for each flight, and we # are later assigning the flight dictionary to point to the list, we # just simply need to continue updating this list to keep the # dictionary up to date (i.e.: we don't need to directly touch the # flights dictionary in main). (last_seen, current_path) = persistent_path.get(id_to_use, (None, [])) if ( # flight position has been updated with this radio signal not current_path or simplified_aircraft.get('lat') != current_path[-1].get('lat') or simplified_aircraft.get('lon') != current_path[-1].get('lon')): current_path.append(simplified_aircraft) persistent_path[id_to_use] = (now, current_path) # if the flight was last seen too far in the past, remove the track info <----SKIPPED LINES----> days_ago: the minimum time difference for which a message should be generated - i.e.: many flights are daily, and so we are not necessarily interested to see about every daily flight that it was seen yesterday. However, more infrequent flights might be of interest. Returns: Printable string message; if no message or insights to generate, then an empty string. """ message = '' this_flight = flights[-1] this_flight_number = DisplayFlightNumber(this_flight) this_timestamp = flights[-1]['now'] last_seen = [ f for f in flights[:-1] if DisplayFlightNumber(f) == this_flight_number] if last_seen and 'flight_number' in this_flight: last_timestamp = last_seen[-1]['now'] if this_timestamp - last_timestamp > days_ago*SECONDS_IN_DAY: message = '%s was last seen %s ago' % ( this_flight_number, SecondsToDdHh(this_timestamp - last_timestamp)) return message def FlightInsightHelicopter(flights): """Generates string indicating a helicopter flying overhead. Generates text of the following form for the "focus" flight in the data. - N376PH with a Eurocopter EC-635 (twin-turboshaft) is the 4th helicopter seen in the last 30 days. Args: flights: the list of the raw data from which the insights will be generated, where the flights are listed in order of observation - i.e.: flights[0] was the earliest seen, and flights[-1] is the most recent flight for which we are attempting to generate an insight. Returns: Printable string message; if no message or insights to generate, then an empty string. """ flight = flights[-1] category = flight.get('category') if category != 'A7': return '' flight_number = DisplayFlightNumber(flight) aircraft = DisplayAircraft(flight) time_string = SecondsToDdHh(flight['now'] - flights[0]['now']) helicopter_count = sum([1 for f in flights if f.get('category') == 'A7']) helicopter_count_ordinal = Ordinal(helicopter_count) message = '%s with a %s is the %s helicopter seen in the last %s' % ( flight_number, aircraft, helicopter_count_ordinal, time_string) return message def FlightInsightDifferentAircraft(flights, percent_size_difference=0.1): """Generates string indicating changes in aircraft for the most recent flight. Generates text of the following form for the "focus" flight in the data. - Last time ASA1964 was seen on Mar 16, it was with a much larger plane (Airbus A320 (twin-jet) @ 123ft vs. Airbus A319 (twin-jet) @ 111ft) - Last time ASA743 was seen on Mar 19, it was with a different type of airpline (Boeing 737-900 (twin-jet) vs. Boeing 737-800 (twin-jet)) Args: flights: the list of the raw data from which the insights will be generated, where the flights are listed in order of observation - i.e.: flights[0] was the earliest seen, and flights[-1] is the most recent flight for which we are attempting to generate an insight. percent_size_difference: the minimum size (i.e.: length) difference for the insight to warrant including the size details. <----SKIPPED LINES----> messages = [] def AppendMessageType(message_type, message): if message: messages.append((message_type, message)) # This flight number was last seen x days ago AppendMessageType( FLAG_INSIGHT_LAST_SEEN, FlightInsightLastSeen(flights, days_ago=2)) # Yesterday this same flight flew a materially different type of aircraft AppendMessageType( FLAG_INSIGHT_DIFF_AIRCRAFT, FlightInsightDifferentAircraft(flights, percent_size_difference=0.1)) # This is the 3rd flight to the same destination in the last hour AppendMessageType( FLAG_INSIGHT_NTH_FLIGHT, FlightInsightNthFlight(flights, hours=1, min_multiple_flights=2)) # N376PH flying Eurocopter EC-635 (twin-turboshaft) is the 4th helicopter # seen in the last 30 days. AppendMessageType(FLAG_INSIGHT_HELICOPTER, FlightInsightHelicopter(flights)) # This is the [lowest / highest] [speed / altitude / climbrate] # in the last 24 hours AppendMessageType(FLAG_INSIGHT_GROUNDSPEED, FlightInsightSuperlativeAttribute( flights, 'speed', 'groundspeed', SPEED_UNITS, ['slowest', 'fastest'], hours=HOURS_IN_DAY)) AppendMessageType(FLAG_INSIGHT_ALTITUDE, FlightInsightSuperlativeAttribute( flights, 'altitude', 'altitude', DISTANCE_UNITS, ['lowest', 'highest'], hours=HOURS_IN_DAY)) AppendMessageType( FLAG_INSIGHT_VERTRATE, FlightInsightSuperlativeVertrate(flights)) # First instances: destination, first aircraft, etc. <----SKIPPED LINES----> title += ' (%+d)' % (round(sum(values) - sum(last_values))) ax.set_title(title) ax.set_ylabel('Average Observed Flights') if comparison: ax.legend() matplotlib.pyplot.xticks( x, keys, rotation='vertical', wrap=True, horizontalalignment='right', verticalalignment='center') matplotlib.pyplot.savefig(filename) matplotlib.pyplot.close() def GenerateHistogramData( data, keyfunction, sort_type, truncate=float('inf'), other_string_key_count=False, hours=float('inf'), max_distance_feet=float('inf'), max_altitude_feet=float('inf'), normalize_factor=0, exhaustive=False): """Generates sorted data for a histogram from a description of the flights. Given an iterable describing the flights, this function generates the label (or key), and the frequency (or value) from which a histogram can be rendered. Args: data: the iterable of the raw data from which the histogram will be generated; each element of the iterable is a dictionary, that contains at least the key 'now', and depending on other parameters, also potentially 'min_feet' amongst others. keyfunction: the function that determines how the key or label of the histogram should be generated; it is called for each element of the data iterable. For instance, to simply generate a histogram on the attribute 'heading', keyfunction would be lambda a: a['heading']. sort_type: determines how the keys (and the corresponding values) are sorted: 'key': the keys are sorted by a simple comparison operator between them, which sorts strings alphabetically and numbers numerically. 'value': the keys are sorted by a comparison between the values, which means that more frequency-occurring keys are listed first. list: if instead of the strings a list is passed, the keys are then sorted in the sequence enumerated in the list. This is useful for, say, ensuring that the days of the week (Tues, Wed, Thur, ...) are listed in sequence. Keys that are generated by keyfunction but that are not in the given list are sorted last (and then amongst those, alphabetically). truncate: integer indicating the maximum number of keys to return; if set to 0, or if set to a value larger than the number of keys, no truncation occurs. But if set to a value less than the number of keys, then the keys with the lowest frequency are combined into one key named OTHER_STRING so that the number of keys in the resulting histogram (together with OTHER_STRING) is equal to truncate. other_string_key_count: boolean indicating whether OTHER_STRING, if included, will be augmented with a count of how many keys are grouped into this aggregate. i.e.: If false, the label might be Other; if true, the label might be Other (17). hours: integer indicating the number of hours of history to include. Flights with a calcd_display_time more than this many hours in the past are excluded from the histogram generation. Note that this is timezone aware, so that if the histogram data is generated on a machine with a different timezone than that that recorded the original data, the correct number of hours is still honored. max_distance_feet: number indicating the geo fence outside of which flights should be ignored for the purposes of including the flight data in the histogram. max_altitude_feet: number indicating the maximum altitude outside of which flights should be ignored for the purposes of including the flight data in the histogram. normalize_factor: divisor to apply to all the values, so that we can easily renormalize the histogram to display on a percentage or daily basis; if zero, no renormalization is applied. exhaustive: boolean only relevant if sort_type is a list, in which case, this ensures that the returned set of keys (and matching values) contains all the elements in the list, including potentially those with a frequency of zero, within therestrictions of truncate. <----SKIPPED LINES----> missing_values = [0 for unused_k in missing_keys] keys.extend(missing_keys) values.extend(missing_values) if keys: # filters could potentially have removed all data if not truncate or len(keys) <= truncate: if sort_by_enumerated_list: (values, keys) = SortByDefinedList(values, keys, sort_type) elif sort_type == 'value': (values, keys) = SortByValues(values, keys) else: (values, keys) = SortByKeys(values, keys) else: #Unknown might fall in the middle, and so shouldn't be truncated (values, keys) = SortByValues( values, keys, ignore_sort_at_end_strings=True) truncated_values = list(values[:truncate-1]) truncated_keys = list(keys[:truncate-1]) other_value = sum(values[truncate-1:]) truncated_values.append(other_value) if other_string_key_count and other_value: truncated_keys.append('%s (%d)' % ( OTHER_STRING, sum([1 for e in values[truncate-1:] if e]))) elif other_value: truncated_keys.append(OTHER_STRING) if sort_by_enumerated_list: (values, keys) = SortByDefinedList( truncated_values, truncated_keys, sort_type) elif sort_type == 'value': (values, keys) = SortByValues( truncated_values, truncated_keys, ignore_sort_at_end_strings=False) else: (values, keys) = SortByKeys(truncated_values, truncated_keys) else: values = [] keys = [] return (values, keys, filtered_data) def SortByValues(values, keys, ignore_sort_at_end_strings=False): """Sorts list of values in desc sequence, applying same resorting to keys. Given a list of keys and values representing a histogram, returns two new lists that are sorted so that the values occur in descending sequence and the keys are moved around in the same way. This allows the printing of a histogram with the largest keys listed first - i.e.: top five airlines. Keys identified by SORT_AT_END_STRINGS - such as, perhaps, 'Other' - will optionally be placed at the end of the sequence. And where values are identical, the secondary sort is based on the keys. Args: values: list of values for the histogram to be used as the primary sort key. keys: list of keys for the histogram that will be moved in the same way as the values. ignore_sort_at_end_strings: boolean indicating whether specially-defined keys will be sorted at the end. Returns: 2-tuple of (values, keys) lists sorted as described above """ if ignore_sort_at_end_strings: sort_at_end_strings = [] else: sort_at_end_strings = [s for s in keys if s.startswith(OTHER_STRING)] sort_at_end_strings.append(KEY_NOT_PRESENT_STRING) return SortZipped( values, keys, True, lambda a: ( False, False, a[1]) if a[1] in sort_at_end_strings else (True, a[0], a[1])) def SortByKeys(values, keys, ignore_sort_at_end_strings=False): """Sorts list of keys in asc sequence, applying same resorting to values. Given a list of keys and values representing a histogram, returns two new lists that are sorted so that the keys occur in ascending alpha sequence and the values are moved around in the same way. This allows the printing of a histogram with the first keys alphabetically listed first - i.e.: 7am, 8am, 9am. Keys identified by SORT_AT_END_STRINGS - such as, perhaps, 'Other' - will optionally be placed at the end of the sequence. Args: values: list of values for the histogram that will be moved in the same way as the keys. keys: list of keys for the histogram to be used as the primary sort key. ignore_sort_at_end_strings: boolean indicating whether specially-defined keys will be sorted at the end. Returns: 2-tuple of (values, keys) lists sorted as described above """ if ignore_sort_at_end_strings: sort_at_end_strings = [] else: sort_at_end_strings = [s for s in keys if s.startswith(OTHER_STRING)] sort_at_end_strings.append(KEY_NOT_PRESENT_STRING) return SortZipped( values, keys, False, lambda a: (True, a[1]) if a[1] in sort_at_end_strings else (False, a[1])) def SortByDefinedList(values, keys, sort_sequence): """Sorts keys in user-enumerated sequence, applying same resorting to values. Given a list of keys and values representing a histogram, returns two new lists that are sorted so that the keys occur in the specific sequence identified in the list sort_sequence, while the values are moved around in the same way. This allows the printing of a histogram with the keys occurring in a canonical order - i.e.: Tuesday, Wednesday, Thursday. Keys present in keys but not existing in sort_sequence are then sorted at the end, but amongst them, sorted based on the value. Args: values: list of values for the histogram that will be moved in the same way as the keys. <----SKIPPED LINES----> should be ignored for the purposes of including the flight data in the histogram. max_altitude_feet: number indicating the maximum altitude outside of which flights should be ignored for the purposes of including the flight data in the histogram. normalize_factor: divisor to apply to all the values, so that we can easily renormalize the histogram to display on a percentage or daily basis; if zero, no renormalization is applied. exhaustive: boolean only relevant if sort_type is a list, in which case, this ensures that the returned set of keys (and matching values) contains all the elements in the list, including potentially those with a frequency of zero, within the restrictions of truncate. figsize_inches: a 2-tuple of width, height indicating the size of the histogram. """ (values, keys, filtered_data) = GenerateHistogramData( data, keyfunction, sort_type, truncate=truncate, other_string_key_count=True, hours=hours, max_distance_feet=max_distance_feet, max_altitude_feet=max_altitude_feet, normalize_factor=normalize_factor, exhaustive=exhaustive) if position: matplotlib.pyplot.subplot(*position) matplotlib.pyplot.figure(figsize=figsize_inches) values_coordinates = numpy.arange(len(keys)) matplotlib.pyplot.bar(values_coordinates, values) # The filtering may have removed any flight data, # or there may be none to start if not filtered_data: return earliest_flight_time = int(filtered_data[0]['now']) last_flight_time = int(filtered_data[-1]['now']) date_range_string = ' %d flights over last %s hours' % ( sum(values), SecondsToDdHh(last_flight_time - earliest_flight_time)) <----SKIPPED LINES----> return None if p is None or not p.is_alive(): if p is None: Log('Process for %s starting for first time' % str(start_function)) elif VERBOSE: Log('Process (%s) for %s died; restarting' % (str(p), str(start_function))) args[2].value = 0 # (re)set shutdown flag to allow function to run p = multiprocessing.Process(target=start_function, args=args) p.daemon = True p.start() return p def LastFlightAvailable(flights, screen_history): """Returns True if splitflap display not displaying last flight message.""" if not screen_history: return False if not flights: return True last_message_tuple = screen_history[-1] last_message_type = last_message_tuple[0] if last_message_type == FLAG_MSG_FLIGHT: last_message_flight = last_message_tuple[2] if SameFlight(last_message_flight, flights[-1]): return False # already displaying the last flight! return True def EnqueueArduinos( flights, json_desc_dict, configuration, to_remote_q, to_servo_q, screen_history): """Send latest data to arduinos via their shared-memory queues. Args: flights: List of all flights. json_desc_dict: Dictionary of additional attributes about radio. configuration: Dictionary of configuration settings. to_remote_q: Multi-processing messaging queue for one-way comm from <----SKIPPED LINES----> |