messageboard-2020-07-03-1742.py
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---->