messageboard-2020-05-01-0135.py
01234567890123456789012345678901234567890123456789012345678901234567890123456789









3637383940414243444546474849505152535455565758596061626364656667686970717273747576








117118119120121122123124125126127128129130131132133134135136    137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183   184185186187188189190191192193194195196197198199200201202203








241242243244245246247248249250251252253254255256257258259260 261262263264265266267268269270271 272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332








655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695








734735736737738739740741742743744745746747748749750751752753  754755756757758759760761762763764765766767768769770771772773








892893894895896897898899900901902903904905906907908909910911  912913914915916917918919920921922923924925926927928929930931








10681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108








11331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173








12171218121912201221122212231224122512261227122812291230123112321233123412351236  1237   1238   1239  124012411242124312441245124612471248124912501251125212531254125512561257125812591260








127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314








13731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413








16581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701     17021703170417051706     17071708170917101711171217131714171517161717     1718           1719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767








1930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984








21242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164








2225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258    22592260226122622263226422652266226722682269227022712272227322742275227622772278








2286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313 231423152316231723182319232023212322232323242325 232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364    236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388 238923902391239223932394239523962397239823992400 24012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426        2427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454   24552456245724582459246024612462246324642465246624672468246924702471247224732474                        24752476247724782479248024812482248324842485248624872488248924902491249224932494








27552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795








302130223023302430253026302730283029303030313032303330343035303630373038303930403041     30423043     30443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069 3070307130723073307430753076307730783079308030813082308330843085308630873088308930903091 3092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173       317431753176317731783179  3180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228








34213422342334243425342634273428342934303431343234333434343534363437343834393440      344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467








371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782 37833784378537863787378837893790  37913792379337943795379637973798379938003801380238033804380538063807380838093810








38123813381438153816381738183819382038213822382338243825382638273828382938303831     38323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866








38843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924








39873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027








41274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167








44804481448244834484448544864487448844894490449144924493449444954496449744984499 450045014502  45034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525








45614562456345644565456645674568456945704571457245734574457545764577457845794580 45814582458345844585458645874588458945904591459245934594459545964597459845994600








46244625462646274628462946304631463246334634463546364637463846394640464146424643                   4644464546464647464846494650    46514652465346544655465646574658  46594660466146624663466446654666466746684669467046714672467346744675467646774678








4691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724 472547264727472847294730473147324733         4734  47354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772 47734774477547764777477847794780478147824783478447854786478747884789479047914792








479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845 4846484748484849485048514852485348544855485648574858485948604861



                            <----SKIPPED LINES---->





HOME_LAT = 37.64406
HOME_LON = -122.43463
HOME = (HOME_LAT, HOME_LON) # lat / lon tuple of antenna
HOME_ALT = 29  #altitude in meters
RADIUS = 6371.0e3  # radius of earth in meters

FEET_IN_METER = 3.28084
FEET_IN_MILE = 5280
METERS_PER_SECOND_IN_KNOTS = 0.514444

MIN_METERS = 5000/FEET_IN_METER # only planes within this distance will be detailed
# planes not seen within MIN_METERS in PERSISTENCE_SECONDS seconds will be dropped from
# the nearby list
PERSISTENCE_SECONDS = 10
TRUNCATE = 50  # max number of keys to include in a histogram image file
# number of seconds to pause between each radio poll / command processing loop
LOOP_DELAY_SECONDS = 1

# This file is where the radio drops its json file
DUMP_JSON_FILE = '/run/dump1090-mutability/aircraft.json'

# This is the directory that stores all the ancillary messageboard configuration files
# that do not need to be exposed via the webserver
MESSAGEBOARD_PATH = '/home/pi/splitflap/'
# This is the directory of the webserver; files placed here are available at
# http://adsbx-custom.local/; files placed in this directly are visible via a browser
WEBSERVER_PATH = '/var/www/html/'

# At the time a flight is first identified as being of interest (in that it falls
# within MIN_METERS meters of HOME), it - and core attributes derived from FlightAware,
# if any - is appended to the end of this pickle file. However, since this file is
# cached in working memory, flights older than 30 days are flushed from this periodically.
PICKLE_FLIGHTS_30D = 'flights_30d.pk' #pickled list of up to about 30d of flights
# This is the same concept as the 30d pickle file, except it is not auto-flushed, and
# so will grow indefinitely.
PICKLE_FLIGHTS_ARCHIVE = 'flights_archive.pk' #pickled list of all flights

CACHED_ELEMENT_PREFIX = 'cached_'

# This web-exposed file is used for non-error messages that might highlight data or




                            <----SKIPPED LINES---->




# alignment with the html page that displays it.
HOURLY_IMAGE_FILE = 'hours.png'

# Communication with the asynchronously-running Arduino interface is done thru files;
# this file includes the superset of key-value pairs that could potentially be sent
# directly, or after some manipulation, to the Arduino.
ARDUINO_FILE = 'arduino.txt'
# One potential command from the Arduino is to "display the last flight"; this request
# is communicated to the Arduino by the presence of this file. After that request is
# processed, this file is deleted. The contents of the file are not used in any way.
LAST_FLIGHT_FILE = 'last_flight.txt'

# This is all messages that have been sent to the board since the last time the
# file was manually cleared. Newest messages are at the bottom. It is visible at the
# webserver.
ALL_MESSAGE_FILE = 'all_messages.txt'  #enumeration of all messages sent to board
# This shows the most recent n messages sent to the board. Newest messages are at the
# top for easier viewing of "what did I miss".
ROLLING_MESSAGE_FILE = 'rolling_messages.txt'





FLAG_MSG_FLIGHT = 1  # basic flight details
FLAG_MSG_INTERESTING = 2  # random tidbit about a flight
FLAG_MSG_HISTOGRAM = 3 # histogram message

FLAG_INSIGHT_LAST_SEEN = 0
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 = 17
FLAG_INSIGHT_DATE_DELAY_TIME = 18
INSIGHT_TYPES = 21

#if running on raspberry, then need to prepend path to file names
if psutil.sys.platform.title() == 'Linux':
  PICKLE_FLIGHTS_30D = MESSAGEBOARD_PATH + PICKLE_FLIGHTS_30D
  PICKLE_FLIGHTS_ARCHIVE = MESSAGEBOARD_PATH + PICKLE_FLIGHTS_ARCHIVE
  LOGFILE = MESSAGEBOARD_PATH + LOGFILE
  PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE
  PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE
  LAST_FLIGHT_FILE = MESSAGEBOARD_PATH + LAST_FLIGHT_FILE
  ARDUINO_FILE = MESSAGEBOARD_PATH + ARDUINO_FILE

  HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE
  CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE
  HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HOURLY_IMAGE_FILE
  ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE
  HISTOGRAM_IMAGE_PREFIX = (
      WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_IMAGE_PREFIX)
  HISTOGRAM_EMPTY_IMAGE_FILE = (
      WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_EMPTY_IMAGE_FILE)
  ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE
  ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE




TIMEZONE = 'US/Pacific' # timezone of display
TZ = pytz.timezone(TIMEZONE)

KNOWN_AIRPORTS = ('SJC', 'SFO', 'OAK')  # iata codes that we don't need to expand

SPLITFLAP_CHARS_PER_LINE = 22
SPLITFLAP_LINE_COUNT = 6

DIRECTIONS_4 = ['N', 'E', 'S', 'W']
DIRECTIONS_8 = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
DIRECTIONS_16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
                 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']

HOURS = ['12a', ' 1a', ' 2a', ' 3a', ' 4a', ' 5a', ' 6a', ' 7a',
         ' 8a', ' 9a', '10a', '11a', '12p', ' 1p', ' 2p', ' 3p',
         ' 4p', ' 5p', ' 6p', ' 7p', ' 8p', ' 9p', '10p', '11p']

SECONDS_IN_MINUTE = 60
MINUTES_IN_HOUR = 60




                            <----SKIPPED LINES---->




aircraft_length['Boeing 737-700 (twin-jet)'] = 33.63
aircraft_length['Boeing 737-800 (twin-jet)'] = 39.47
aircraft_length['Boeing 737-900 (twin-jet)'] = 42.11
aircraft_length['Boeing 747-400 (quad-jet)'] = 36.4
aircraft_length['Boeing 747-8 (quad-jet)'] = 76.25
aircraft_length['Boeing 757-200 (twin-jet)'] = 47.3
aircraft_length['Boeing 757-300 (twin-jet)'] = 54.4
aircraft_length['Boeing 767-200 (twin-jet)'] = 48.51
aircraft_length['Boeing 767-300 (twin-jet)'] = 54.94
aircraft_length['Boeing 777-200 (twin-jet)'] = 63.73
aircraft_length['Boeing 777-200LR/F (twin-jet)'] = 63.73
aircraft_length['Boeing 777-300ER (twin-jet)'] = 73.86
aircraft_length['Boeing 787-10 (twin-jet)'] = 68.28
aircraft_length['Boeing 787-8 (twin-jet)'] = 56.72
aircraft_length['Boeing 787-9 (twin-jet)'] = 62.81
aircraft_length['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
aircraft_length['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
aircraft_length['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
aircraft_length['Canadair Challenger 350 (twin-jet)'] = 20.9
aircraft_length['Bombardier Challenger 300 (twin-jet)'] = 20.92

aircraft_length['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
aircraft_length['Embraer ERJ-135 (twin-jet)'] = 26.33
aircraft_length['Cessna Caravan (single-turboprop)'] = 11.46
aircraft_length['Cessna Citation II (twin-jet)'] = 14.54
aircraft_length['Cessna Citation V (twin-jet)'] = 14.91
aircraft_length['Cessna Skyhawk (piston-single)'] = 8.28
aircraft_length['Cessna Skylane (piston-single)'] = 8.84
aircraft_length['Cessna Citation Sovereign (twin-jet)'] = 19.35
aircraft_length['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
aircraft_length['Beechcraft Bonanza (33) (piston-single)'] = 7.65
aircraft_length['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31

aircraft_length['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
aircraft_length['Pilatus PC-12 (single-turboprop)'] = 14.4


def CheckIfProcessRunning():
  """Returns proc id if process with identically-named python file running; else None."""
  this_process_id = os.getpid()
  this_process_name = os.path.basename(sys.argv[0])
  for proc in psutil.process_iter():
    try:
      # Check if process name contains this_process_name.
      commands = proc.as_dict(attrs=['cmdline', 'pid'])
      if commands['cmdline']:
        command_running = any(
            [this_process_name in s for s in commands['cmdline']])
        if command_running and commands['pid'] != this_process_id:
          return commands['pid']
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
      pass
  return None


def LogMessage(message, file=None):
  """Write a message to a logfile along with a timestamp.

  Args:
      message: string message to write
      file: string representing file name and, if needed, path to the file to write to
  """
  # can't define as a default parameter because LOGFILE name is potentially
  # modified based on SIMULATION flag
  if not file:
    file = LOGFILE

  try:
    with open(file, 'a') as f:
      if not SIMULATION:  # by excluding the timestamp, file diffs become easy between runs
        f.write('='*80+'\n')
        f.write(str(datetime.datetime.now(TZ))+'\n')
        f.write('\n')
      f.write(message+'\n')
  except IOError:
    LogMessage('Unable to append to ' + file)

  existing_log_lines = ReadFile(LOGFILE).splitlines()
  with open(ROLLING_LOGFILE, 'w') as f:
    f.write('\n'.join(existing_log_lines[-1000:]))


def MaintainRollingWebLog(message, max_count, filename=None):
  """Maintains a rolling text file of at most max_count printed messages.

  Newest data at top and oldest data at the end, of at most max_count messages,
  where the delimiter between each message is identified by a special fixed string.

  Args:
    message: text message to prepend to the file.
    max_count: maximum number of messages to keep in the file; the max_count+1st message
        is deleted.
    filename: the file to update.
  """




                            <----SKIPPED LINES---->




  radians = math.radians
  degrees = math.degrees

  if not all([isinstance(x, numbers.Number) for x in (
      *pos1, altitude1, *pos2, altitude2)]):
    return None

  distance = HaversineDistanceMeters(pos1, pos2)  # from home to plumb line of plane
  lat1, lon1, lat2, lon2 = [radians(x) for x in (*pos1, *pos2)]
  d_lon = lon2 - lon1
  # azimuth calc from https://www.omnicalculator.com/other/azimuth
  az = atan2((sin(d_lon)*cos(lat2)), (cos(lat1)*sin(lat2)-sin(lat1)*cos(lat2)*cos(d_lon)))
  az_degrees = degrees(az)
  altitude = altitude2 - altitude1
  alt = atan(altitude / distance)
  alt_degrees = degrees(alt)
  crow_distance = sqrt(altitude**2 + distance**2)  # from home to the plane

  return {'azimuth_degrees': az_degrees, 'altitude_degrees': alt_degrees,
          'ground_distance_feet': distance, 'crow_distance_feet': crow_distance}
  #### NEED TO WORK OUT THESE UNITS - feet or meters? DEBUG


def TrajectoryLatLon(pos, distance, track):
  """Calculates lat/lon a plane will be given its starting point and direction / speed.

  Args:
    pos: a 2-tuple of lat-lon for the flight, in degrees.
    distance: the distance, in meters, the flight is traveling from its current lat/lon.
    track: the track or bearing of the plane, in degrees.

  Returns:
    Updated lat/lon for the given trajectory.
  """
  #distance in meters
  #track in degrees
  sin = math.sin
  cos = math.cos
  atan2 = math.atan2
  asin = math.asin
  radians = math.radians




                            <----SKIPPED LINES---->




  potential_distance1 = HaversineDistanceMeters(potential_intersection1, HOME)
  potential_distance2 = HaversineDistanceMeters(potential_intersection2, HOME)

  # Since one of those two potential intersection points (i.e.: +90 or -90 degrees) will
  # create an irrational result, and given the strong locality to HOME that is expected
  # from the initial position, the "correct" result is identified by simply taking the
  # minimum distance of the two candidate.
  return min(potential_distance1, potential_distance2)


def SecondsToHhMm(seconds, colon=False):
  """Converts integer number of seconds to xhym string (i.e.: 7h17m) or to 7:17.

  Args:
    seconds: number of seconds
    colon: controls format; if False, format is 7h17m; if True, format is 7:17.

  Returns:
    String representation of hours and minutes.
  """


  minutes = int(abs(seconds) / SECONDS_IN_MINUTE)
  if minutes > MINUTES_IN_HOUR:
    hours = int(minutes / MINUTES_IN_HOUR)
    minutes = minutes % MINUTES_IN_HOUR
    if colon:
      text = str(hours) + ':' + str(minutes)
    else:
      text = str(hours) + 'h' + str(minutes) + 'm'
  else:
    if colon:
      text = ':' + str(minutes)
    else:
      text = str(minutes) + 'm'
  return text


def SecondsToHours(seconds):
  """Converts integer number of seconds to xh string (i.e.: 7h).

  Args:




                            <----SKIPPED LINES---->




    last_modified = os.path.getmtime(filename)
    if last_modified > last_read_time:
      setting_str = ReadFile(filename)
      settings = ParseSettings(setting_str)
      CACHED_FILES[filename] = (last_modified, settings)
    return settings

  # File does not - or at least no longer - exists; so remove the cache
  if filename in CACHED_FILES:
    CACHED_FILES.pop(filename)

  return {}


def BuildSettings(d):
  """Converts a dict to a string of form key1=value1;...;keyn=valuen; keys alpha sorted."""
  kv_pairs = []
  for key in sorted(list(d.keys())):
    kv_pairs.append('%s=%s' % (key, d[key]))
  s = ';'.join(kv_pairs)


  return s


def ParseSettings(settings):
  """Parse delimited string of settings in file to a dict of key value pairs.

  Parses a string like 'distance=1426;altitude=32559;on=23;off=24;delay=15;insights=all;'
  into key value pairs.

  Args:
    settings: semicolon-delimited sequence of equal-sign delimited key-value pairs, i.e.:
      key1=value1;key2=value2;....;keyn=valuen.

  Returns:
    Dict of key value pairs contained in the setting file; empty dict if file not
    available or if delimiters missing.
  """
  settings_dict = {}
  for setting in settings.split(';'):
    if '=' in setting:




                            <----SKIPPED LINES---->





def ScanForNewFlights(persistent_nearby_aircraft, persistent_path):
  """Determines if there are any new aircraft in the radio message.

  The radio is continuously dumping new json messages to the Raspberry pi with all the
  flights currently observed. This function picks up the latest radio json, and for
  any new nearby flights - there should generally be at most one new flight on each
  pass through - gets additional flight data from FlightAware and augments the flight
  definition with the relevant fields to keep.

  Args:
    persistent_nearby_aircraft: dictionary where keys are flight numbers, and the values
      are the time the flight was last seen.
    persistent_path: dictionary where keys are flight numbers, and the values are a
      sequential list of the location-attributes in the json file; allows for tracking
      the flight path over time.

  Returns:
    A tuple:
    - updated persistent_nearby_aircraft
    - current_nearby_aircraft
    - (possibly empty) dictionary of flight attributes of the new flight upon its
      first observation.
    - the time of the radio observation if present; None if no radio dump
    - a dictionary of attributes about the dump itself (i.e.: # of flights; furthest
      observed flight, etc.)
    - persistent_path, a data structure containing past details of a flight's location
      as described in ParseDumpJson
  """
  flight_details = {}
  now = time.time()
  if SIMULATION:
    (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER]
  else:
    dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True)

  json_desc_dict = {}
  current_nearby_aircraft = {}
  if dump_json:
    (current_nearby_aircraft,
     now,




                            <----SKIPPED LINES---->




      elif flight_number:
        flight_aware_json = GetFlightAwareJson(flight_number)
        if not flight_aware_json:
          LogMessage('No json returned from Flightaware for flight: %s' % flight_number)

      flight_details = {}
      if flight_aware_json:
        flight_details = ParseFlightAwareJson(flight_aware_json)

      if not SIMULATION:
        PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE)

      # Augment FlightAware details with radio / radio-derived details
      flight_details.update(current_nearby_aircraft[flight_number])

      # Augment with the past location data
      flight_details['persistent_path'] = persistent_path[flight_number][1]

  return (
      persistent_nearby_aircraft,
      current_nearby_aircraft,
      flight_details,
      now,
      json_desc_dict,
      persistent_path)


def DescribeDumpJson(parsed):
  """Generates a dictionary with descriptive attributes about the dump json file.

  Args:
    parsed: The parsed json file.

  Returns:
    Dictionary with attributes about radio range, number of flights seen, etc.
  """
  json_desc_dict = {}
  json_desc_dict['now'] = parsed['now']

  aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS]
  json_desc_dict['radio_range_flights'] = len(aircraft)




                            <----SKIPPED LINES---->





  for aircraft in parsed['aircraft']:
    simplified_aircraft = {}

    simplified_aircraft['now'] = now

    # flight_number
    flight_number = aircraft.get('flight')
    if flight_number:
      flight_number = flight_number.strip()
      if flight_number:
        simplified_aircraft['flight_number'] = flight_number

    if 'lat' in aircraft and 'lon' in aircraft:
      lat = aircraft['lat']
      lon = aircraft['lon']
      if isinstance(lat, numbers.Number) and isinstance(lon, numbers.Number):
        simplified_aircraft['lat'] = lat
        simplified_aircraft['lon'] = lon



        simplified_aircraft['altitude'] = aircraft.get('altitude')



        simplified_aircraft['speed'] = aircraft.get('speed')



        simplified_aircraft['vert_rate'] = aircraft.get('vert_rate')


        simplified_aircraft['squawk'] = aircraft.get('squawk')

        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

        if HaversineDistanceMeters(HOME, (lat, lon)) < MIN_METERS:
          nearby_aircraft[flight_number] = simplified_aircraft

        # 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(flight_number, (None, []))
        if simplified_aircraft not in current_path:
          current_path.append(simplified_aircraft)
        persistent_path[flight_number] = (now, current_path)




                            <----SKIPPED LINES---->





  Given a flight number, loads the corresponding FlightAware webpage for that flight and
  extracts the relevant script that contains all the flight details from that page.

  Args:
    flight_number: text flight number (i.e.: SWA1234)

  Returns:
    Text representation of the json message from FlightAware.
  """
  url = 'https://flightaware.com/live/flight/' + flight_number
  try:
    response = requests.get(url)
  except requests.exceptions.RequestException as e:
    LogMessage('Unable to query FA for URL due to %s: %s' % (e, url))
    return ''
  soup = bs4.BeautifulSoup(response.text, "html.parser")
  l = soup.findAll('script')
  flight_script = None
  for script in l:
    if "trackpollBootstrap" in script.text:
      flight_script = script.text
      break
  if not flight_script:
    LogMessage('Unable to find trackpollBootstrap script in page: ' + response.text)
    return ''
  first_open_curly_brace = flight_script.find('{')
  flight_json = flight_script[first_open_curly_brace:-1]
  return flight_json


def Unidecode(s):
  """Convert a special unicode characters to closest ASCII representation."""
  if s is not None:
    s = unidecode.unidecode(s)
  return s


def ParseFlightAwareJson(flight_json):
  """Strips relevant data about the flight from FlightAware feed.

  The FlightAware json has hundreds of fields about a flight, only a fraction of which




                            <----SKIPPED LINES---->




    flight['airline_full_name'] = Unidecode(airline.get('fullName'))

  if len(parsed_json['flights'].keys()) > 1:
    LogMessage('There are multiple flights in the FlightAware json: ' + parsed_json)

  return flight


def EpochDisplayTime(epoch, format_string='%Y-%m-%d %H:%M:%S.%f%z'):
  """Converts epoch in seconds to formatted time string."""
  return datetime.datetime.fromtimestamp(epoch, TZ).strftime(format_string)


def DisplayTime(flight, format_string='%Y-%m-%d %H:%M:%S.%f%z'):
  """Converts flight 'now' to formatted time string, caching results on flight."""
  cached_key = CACHED_ELEMENT_PREFIX + 'now-' + format_string
  cached_time = flight.get(cached_key)
  if cached_time:
    return cached_time

  epoch_display_time = EpochDisplayTime(flight['now'], format_string=format_string)
  flight[cached_key] = epoch_display_time
  return epoch_display_time


def DisplayAirline(flight):
  """Augments flight details with display-ready airline attributes.

  Args:
    flight: dictionary with key-value attributes about the flight.

  Returns:
    String identifying either the airline, or Unknown if not available.
  """
  airline = flight.get('airline_short_name', flight.get('airline_full_name'))

  # Some names are very similar to others and so appear identical on splitflap
  replacement_names = (
      ('Delta Private Jets', 'DPJ'),
      ('United Parcel Service', 'UPS'))
  for (old, new) in replacement_names:




                            <----SKIPPED LINES---->




    flight: dictionary with key-value attributes about the flight.

  Returns:
    Seconds, if the remaining time is calculable; None otherwise.
  """
  arrival = flight.get('estimated_arrival_time')
  if not arrival:
    arrival = flight.get('estimated_landing_time')
  if not arrival:
    arrival = flight.get('scheduled_arrival_time')
  if not arrival:
    arrival = flight.get('scheduled_landing_time')

  if arrival:
    remaining_seconds = flight['now'] - arrival
  else:
    remaining_seconds = None
  return remaining_seconds


def FlightMeetsDisplayCriteria(flight, configuration, display_all_hours=False):
  """Returns boolean indicating whether the screen is currently accepting new flight data.

  Based on the configuration file, determines whether the flight data should be displayed.
  Specifically, the configuration:
  - may include 'enabled' indicating whether screen should be driven at all
  - should include 'on' & 'off' parameters indicating minute (from midnight) of operation
  - should include altitude & elevation parameters indicating max values of interest

  Args:
    flight: dictionary of flight attributes.
    configuration: dictionary of configuration attributes.
    display_all_hours: a boolean indicating whether we should ignore whether the
      screen is turned off (either via the enabling, or via the hour settings)

  Returns:
    Boolean as described.
  """
  flight_altitude = flight.get('altitude', float('inf'))
  config_max_altitude = configuration['setting_max_altitude']

  flight_meets_criteria = True
  if flight_altitude > config_max_altitude:
    flight_meets_criteria = False





  else:
    flight_distance = flight.get('min_feet', float('inf'))
    config_max_distance = configuration['setting_max_distance']
    if flight_distance > config_max_distance:
      flight_meets_criteria = False






  if not display_all_hours and flight_meets_criteria:
    flight_timestamp = flight['now']
    dt = datetime.datetime.fromtimestamp(flight_timestamp, TZ)
    minute_of_day = dt.hour * MINUTES_IN_HOUR + dt.minute
    if (
        minute_of_day < configuration['setting_on_time'] or
        minute_of_day > configuration['setting_off_time']):
      flight_meets_criteria = False
    if configuration.get('setting_screen_enabled', 'off') == 'off':
      print(configuration.get('setting_screen_enabled'))





      flight_meets_criteria = False












  return flight_meets_criteria


def IdentifyFlightDisplayed(flights, configuration, display_all_hours=False):
  """Finds the most recent flight in flights that meet the display criteria.

  Args:
    flights: list of flight dictionaries.
    configuration: dictionary of settings.
    display_all_hours: boolean indicating whether we should ignore the time constraints
      (i.e.: whether the screen is enabled, and its turn-on or turn-off times) in
      identifying the most recent flight. That is, if False, then this will only return
      flights that would have been displayed in the ordinarily usage, vs. if True,
      a flight irrespective of the time it would be displayed.

  Returns:
    A flight dictionary if one can be found; None otherwise.
  """
  for n in range(len(flights)-1, -1, -1):  # traverse the flights in reverse
    if FlightMeetsDisplayCriteria(
        flights[n], configuration, display_all_hours=display_all_hours):
      return n
  return None


def FlightMessageTestHarness(
    flights=None,
    display=True):
  """Simulates what flight messages might be displayed by replaying past flights."""
  if flights is None:
    flights = UnpickleObjectFromFile(PICKLE_FLIGHTS_30D)

  messages = []
  for flight in flights:

    flight_message = Screenify(CreateMessageAboutFlight(flight), False)
    if display:
      print(flight_message)
    messages.append(flight_message)

  return messages


def CreateMessageAboutFlight(flight):
  """Creates a message to describe interesting attributes about a single flight.

  Generates a multi-line description of a flight. A typical message might look like:
  UAL300 - UNITED        <- Flight number and airline




                            <----SKIPPED LINES---->




    lines.append(divider)

  return append_character.join(lines)


def FlightInsightsTestHarness(
    flights=None,
    display=True,
    flight_insights_enabled_string='all'):
  """Simulates what insightful messages might be displayed by replaying past flights."""
  if flights is None:
    flights = UnpickleObjectFromFile(PICKLE_FLIGHTS_30D)

  distribution = {}
  messages = []
  for (n, flight) in enumerate(flights):

    flight_message = CreateMessageAboutFlight(flight)
    if display:
      print('='*25)
      print(flight_message)
    messages.append(flight_message)

    insights = CreateFlightInsights(
        flights[:n+1], flight_insights_enabled_string, distribution)

    #DEBUG
    FlightInsightNextFlight(
        flights[:n+1])



    if display:
      for insight in insights:
        print(insight)
    messages.extend(insights)

  return messages


def FlightInsightLastSeen(flights, days_ago=2):
  """Generates string indicating when flight was last seen.

  Generates text of the following form.
  - KAL214 was last seen 2d0h ago

  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.
    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.




                            <----SKIPPED LINES---->




      elif same_airline:
        message += ', all with %s' % this_airline
      else:
        similar_flights_airlines.append(this_airline)
        similar_flights_airlines.sort()
        message += ', served by %s and %s' % (
            ', '.join(similar_flights_airlines[:-1]),
            similar_flights_airlines[-1])

  return message


def FlightInsightSuperlativeAttribute(
    flights,
    key,
    label,
    units,
    absolute_list,
    insight_min=True,
    insight_max=True,
    hours=24):
  """Generates string about a numeric attribute of the flight being an extreme value.

  Generates text of the following form for the "focus" flight in the data.
  - N5286C has the slowest groundspeed (113mph vs. 163mph) in last 24 hours
  - CKS828 has the highest altitude (40000ft vs. 16575ft) in last 24 hours

  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.
    key: the key of the attribute of interest - i.e.: 'speed'.
    label: the human-readable string that should be displayed in the message - i.e.:
        'groundspeed'.
    units: the string units that should be used to label the value of the key - i.e.:
        'MPH'.
    absolute_list: a 2-tuple of strings that is used to label the min and the max - i.e.:
        ('lowest', 'highest'), or ('slowest', 'fastest').
    insight_min: boolean indicating whether to generate an insight about the min value.
    insight_max: boolean indicating whether to generate an insight about the max value.




                            <----SKIPPED LINES---->




  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 because not enough history, then an
    empty string.
  """
  msg = ''
  # m = min of day of this flight
  # find minute of day of prior flights st
  # -- that flight not seen in last 12 hrs
  # -- that min of day >= this
  this_flight = flights[-1]
  this_hour = int(DisplayTime(this_flight, '%-H'))
  this_minute = int(DisplayTime(this_flight, '%-M'))
  this_date = DisplayTime(this_flight, '%x')

  #DEBUG: need to further filter based on flightcriteria

  # Flights that we've already seen in the last few hours we do not expect to see
  # again for another few hours, so let's exclude them from the calculation
  exclude_flights_hours = 12
  flight_numbers_seen_in_last_n_hours = [
      f['flight_number'] for f in flights
      if f['now'] > this_flight['now'] - exclude_flights_hours*SECONDS_IN_HOUR
      and 'flight_number' in f]
  still_to_come_flights = [
      f for f in flights[:-1]
      if f.get('flight_number') not in flight_numbers_seen_in_last_n_hours
      and this_date != DisplayTime(f, '%x')]





  minimum_minutes_next_flight = {}  # min minutes to next flight by day
  for flight in still_to_come_flights:
    date = DisplayTime(flight, '%x')
    hour = int(DisplayTime(flight, '%-H'))
    minutes = int(DisplayTime(flight, '%-M'))
    minutes_after = (hour - this_hour) * MINUTES_IN_HOUR +(minutes - this_minute)
    if minutes_after < 0:
      minutes_after += MINUTES_IN_DAY
    minimum_minutes_next_flight[date] = min(
        minimum_minutes_next_flight.get(date, minutes_after), minutes_after)

  minutes = list(minimum_minutes_next_flight.values())
  if len(minutes) > 1:
    average_seconds = (sum(minutes) / len(minutes)) * SECONDS_IN_MINUTE
    max_seconds = max(minutes) * SECONDS_IN_MINUTE

    median_seconds = statistics.median(minutes) * SECONDS_IN_MINUTE
    minimum_percent_diff = 0.5
    median_different = (
        median_seconds > average_seconds * (1 + minimum_percent_diff) or




                            <----SKIPPED LINES---->




               DisplayTime(this_flight, '%-I:%M%p'), SecondsToHhMm(average_seconds),
               median_text, SecondsToHhMm(max_seconds), len(minutes)))

  return msg


def PercentileScore(scores, value):
  """Returns the percentile that a particular value is in a list of numbers.

  Roughly inverts numpy.percentile. That is, numpy.percentile(scores_list, percentile)
  to get the value of the list that is at that percentile;
  PercentileScore(scores_list, value) will yield back approximately that percentile.

  If the value matches identical elements in the list, this function takes the average
  position of those identical values to compute a percentile. Thus, for some lists
  (i.e.: where there are lots of flights that have a 0 second delay, or a 100% delay
  frequency), you may not get a percentile of 0 or 100 even with values equal to the
  min or max element in the list.

  Args:
    scores: the list of numbers.
    value: the value for which we want to determine the percentile.

  Returns:
    Returns an integer percentile in the range [0, 100] inclusive.
  """
  scores = sorted(list(scores))
  count_values_below_score = len([1 for s in scores if s < value])

  count_values_at_score = len([1 for s in scores if s == value])
  percentile = (count_values_below_score + count_values_at_score / 2) / len(scores)
  return round(percentile*100)


def FlightInsightGroupPercentile(
    flights,
    group_function,
    value_function,
    value_string_function,
    group_label,
    value_label,

    min_days=1,
    lookback_days=30,
    min_this_group_size=0,
    min_comparison_group_size=0,
    min_group_qty=0,
    percentile_low=float('-inf'),
    percentile_high=float('inf')):
  """Generates a string about extreme values of groups of flights.

  Generates text of the following form for the "focus" flight in the data.
  - flight SIA31 (n=7) has a delay frequency in the 95th %tile, with 100% of flights
    delayed an average of 6m over the last 4d1h
  - flight UAL300 (n=5) has a delay time in the 1st %tile, with an average delay of 0m
  over the last 4d5h

  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.
    group_function: function that, when called with a flight, returns the grouping key.
        That is, for example, group_function(flight) = 'B739'
    value_function: function that, when called with a list of flights, returns the
        value to be used for the comparison to identify min / max. Typically, the count,
        but could also be a sum, standard deviation, etc. - for perhaps the greatest
        range in flight altitude. If the group does not have a valid value and so
        should be excluded from comparison - i.e.: average delay of a group of flights
        which did not have a calculable_delay on any flight, this function should
        return None.
    value_string_function: function that, when called with the two parameters flights
        and value, returns a string (inclusive of units and label) that should be
        displayed to describe the quantity. For instance, if value_function returns
        seconds, value_string_function could convert that to a string '3h5m'. Or if
        value_function returns an altitude range, value_string_function could return
        a string 'altitude range of 900ft (1100ft - 2000ft)'.
    group_label: string to identify the group type - i.e.: 'aircraft' or 'flight' in
        the examples above.
    value_label: string to identify the value - i.e.: 'flights' in the examples above,
        but might also be i.e.: longest *delay*, or other quantity descriptor.




    min_days: the minimum amount of history required to start generating insights
        about delays.
    lookback_days: the maximum amount of history which will be considered in generating
        insights about delays.
    min_this_group_size: even if this group has, say, the maximum average delay, if its
        a group of size 1, that is not necessarily very interesting. This sets the
        minimum group size for the focus flight.
    min_comparison_group_size: similarly, comparing the focus group to groups of size
        one does not necessarily produce a meaningful comparison; this sets to minimum
        size for the other groups.
    min_group_qty: when generating a percentile, if there are only 3 or 4 groups among
        which to generate a percentile (i.e.: only a handful of destinations have been
        seen so far, etc.) then it is not necessarily very interesting to generate a
        message; this sets the minimum quantity of groups necessary (including the
        focus group) to generate a message.
    percentile_low: number [0, 100] inclusive that indicates the percentile that the focus
        flight group must equal or be less than for the focus group to trigger an insight.
    percentile_high: number [0, 100] inclusive that indicates the percentile that the
        focus flight group must equal or be greater than for the focus group to trigger an
        insight.

  Returns:
    Printable string message; if no message or insights to generate, then an empty string.
  """

  message = ''
  this_flight = flights[-1]
  first_timestamp = flights[0]['now']
  last_timestamp = this_flight['now']
  included_seconds = last_timestamp - first_timestamp

  if (included_seconds > SECONDS_IN_DAY * min_days and
      group_function(this_flight) != KEY_NOT_PRESENT_STRING):

    relevant_flights = [
        f for f in flights if
        last_timestamp - f['now'] < SECONDS_IN_DAY * lookback_days]


    grouped_flights = {}
    for flight in relevant_flights:
      group = group_function(flight)
      grouping = grouped_flights.get(group, [])
      grouping.append(flight)
      grouped_flights[group] = grouping
    # we will exclude "UNKNOWN" since that is not a coherent group
    if KEY_NOT_PRESENT_STRING in grouped_flights:
      grouped_flights.pop(KEY_NOT_PRESENT_STRING)

    grouped_values = {g: value_function(grouped_flights[g]) for g in grouped_flights}
    this_group = group_function(relevant_flights[-1])
    this_value = grouped_values[this_group]
    this_group_size = len(grouped_flights[this_group])

    # we will exclude groups that are not big enough
    grouped_flights = {
        k: grouped_flights[k] for k in grouped_flights
        if len(grouped_flights[k]) > min_comparison_group_size or k == this_group}

    # Remove those for which no value could be calculated or which are too small
    grouped_values = {
        g: grouped_values[g] for g in grouped_values
        if grouped_values[g] is not None and g in grouped_flights}









    if this_value and len(grouped_values) > min_group_qty:

      time_horizon_string = ' over the last %s' % SecondsToDdHh(
          last_timestamp - relevant_flights[0]['now'])
      min_comparison_group_size_string = ''
      if min_comparison_group_size > 1:
        min_comparison_group_size_string = ' amongst those with >%d flights' % (
            min_comparison_group_size - 1)

      # FLIGHT X (n=7) is has the Xth percentile of DELAYS, with an average delay of
      # 80 MINUTES
      this_percentile = PercentileScore(grouped_values.values(), this_value)
      if this_group_size > min_this_group_size and (
          this_percentile <= percentile_low or this_percentile >= percentile_high):

        if False:  #debug comparison cohorts
          print('Comparison cohorts for %s (%s)' % (group_label, str(this_group)))
          print('This percentile: %f; min: %f; max: %f' % (
              this_percentile, percentile_low, percentile_high))
          keys = list(grouped_values.keys())
          values = [grouped_values[k] for k in keys]
          print(keys)
          print(values)
          (values, keys) = SortByValues(values, keys)
          for n, value in enumerate(values):
            print('%s: %f (group size: %d)' % (
                keys[n], value, len(grouped_flights[keys[n]])))




        def TrialMessage():
          message = '%s %s (n=%d) has a %s in the %s %%tile, with %s%s%s' % (
              group_label,
              this_group,
              this_group_size,
              value_label,
              Ordinal(this_percentile),
              value_string_function(grouped_flights[this_group], this_value),
              time_horizon_string,
              min_comparison_group_size_string)
          line_count = len(textwrap.wrap(message, width=SPLITFLAP_CHARS_PER_LINE))
          return (line_count, message)

        (line_count, message) = TrialMessage()
        if line_count > SPLITFLAP_LINE_COUNT:
          min_comparison_group_size_string = ''
          (line_count, message) = TrialMessage()
          if line_count > SPLITFLAP_LINE_COUNT:
            time_horizon_string = ''
            (line_count, message) = TrialMessage()

























  return message


def FlightInsightSuperlativeGroup(
    flights,
    group_function,
    value_function,
    value_string_function,
    group_label,
    value_label,
    absolute_list,
    min_days=1,
    lookback_days=30,
    min_this_group_size=0,
    min_comparison_group_size=0,
    insight_min=True,
    insight_max=True):
  """Generates a string about extreme values of groups of flights.





                            <----SKIPPED LINES---->




    if matching:
      last_potential_observation_sec = last_timestamp - matching[-1]['now']

    if this_instance and last_potential_observation_sec > SECONDS_IN_DAY * days:
      additional_descriptor = ''
      if additional_descriptor_fcn:
        additional_descriptor = ' (%s)' % additional_descriptor_fcn(this_flight)
      last_potential_observation_string = SecondsToDdHh(last_potential_observation_sec)
      if matching:
        message = '%s is the first time %s %s%s has been seen since %s ago' % (
            this_flight_number, label, this_instance, additional_descriptor,
            last_potential_observation_string)
      else:
        message = '%s is the first time %s %s%s has been seen since at least %s ago' % (
            this_flight_number, label, this_instance, additional_descriptor,
            last_potential_observation_string)

  return message


def FlightInsightSuperlativeVertrate(flights, hours=24):
  """Generates string about the climb rate of the flight being an extreme value.

  Generates text of the following form for the "focus" flight in the data.
  - UAL631   has the fastest ascent rate (5248fpm, 64fpm faster than next fastest) in
    last 24 hours
  - CKS1820 has the fastest descent rate (-1152fpm, -1088fpm faster than next fastest)
    in last 24 hours

  While this is conceptually similar to the more generic FlightInsightSuperlativeVertrate
  function, vert_rate - because it can be either positive or negative, with different
  signs requiring different labeling and comparisons - it needs its own special handling.

  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.
    hours: the time horizon over which to look for superlative flights.

  Returns:




                            <----SKIPPED LINES---->





  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=24))





  AppendMessageType(FLAG_INSIGHT_ALTITUDE, FlightInsightSuperlativeAttribute(
      flights, 'altitude', 'altitude', DISTANCE_UNITS, ['lowest', 'highest'], hours=24))





  AppendMessageType(FLAG_INSIGHT_VERTRATE, FlightInsightSuperlativeVertrate(flights))

  # First instances: destination, first aircraft, etc.
  AppendMessageType(FLAG_INSIGHT_FIRST_DEST, FlightInsightFirstInstance(
      flights, 'destination_iata', 'destination', days=7,
      additional_descriptor_fcn=lambda f: f['destination_friendly']))
  AppendMessageType(FLAG_INSIGHT_FIRST_ORIGIN, FlightInsightFirstInstance(
      flights, 'origin_iata', 'origin', days=7,
      additional_descriptor_fcn=lambda f: f['origin_friendly']))
  AppendMessageType(FLAG_INSIGHT_FIRST_AIRLINE, FlightInsightFirstInstance(
      flights, 'airline_short_name', 'airline', days=7))
  AppendMessageType(FLAG_INSIGHT_FIRST_AIRCRAFT, FlightInsightFirstInstance(
      flights, 'aircraft_type_code', 'aircraft', days=7,
      additional_descriptor_fcn=lambda f: f['aircraft_type_friendly']))

  # This is the longest / shortest delay this flight has seen in the last 30 days at
  # 2h5m; including today, this flight has been delayed x of the last y times.
  AppendMessageType(FLAG_INSIGHT_LONGEST_DELAY, FlightInsightDelays(
      flights, min_late_percentage=0.75,
      min_this_delay_minutes=0,
      min_average_delay_minutes=0))

  def DelayTimeAndFrequencyMessage(
      types_tuple,
      group_function,
      group_label,

      min_days=1,
      lookback_days=30,
      min_this_group_size=0,
      min_comparison_group_size=0,
      min_group_qty=0,
      percentile_low=float('-inf'),
      percentile_high=float('inf')):
    value_function_tuple = (PercentDelay, AverageDelay)
    value_string_function_tuple = (
        lambda flights, value: '%d%% of flights delayed an average of %s' % (
            round(value*100), SecondsToHhMm(AverageDelay(flights))),
        lambda flights, value: 'average delay of %s' % SecondsToHhMm(value))
    value_label_tuple = ('delay frequency', 'delay time')
    for n in range(2):
      if types_tuple[n]:
        AppendMessageType(types_tuple[n], FlightInsightGroupPercentile(
            flights,
            group_function=group_function,
            value_function=value_function_tuple[n],
            value_string_function=value_string_function_tuple[n],
            group_label=group_label,
            value_label=value_label_tuple[n],

            min_days=min_days,
            min_this_group_size=min_this_group_size,
            min_comparison_group_size=min_comparison_group_size,
            min_group_qty=min_group_qty,
            lookback_days=lookback_days,
            percentile_low=percentile_low,
            percentile_high=percentile_high))

  # flight UAL1 (n=5) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY, FLAG_INSIGHT_FLIGHT_DELAY_TIME),
      group_function=lambda flight: flight.get('flight_number', KEY_NOT_PRESENT_STRING),
      group_label='flight',
      min_days=1,
      min_this_group_size=4,
      min_comparison_group_size=0,
      min_group_qty=0,
      lookback_days=30,
      percentile_low=10,
      percentile_high=90)

  # Airline United (n=5) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY, FLAG_INSIGHT_AIRLINE_DELAY_TIME),
      group_function=DisplayAirline,
      group_label='airline',
      min_days=1,
      min_this_group_size=10,
      min_comparison_group_size=5,
      min_group_qty=5,
      lookback_days=30,
      percentile_low=10,
      percentile_high=80)

  # Destination LAX (n=5) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY, FLAG_INSIGHT_DESTINATION_DELAY_TIME),
      group_function=DisplayDestinationFriendly,
      group_label='destination',
      min_days=1,
      min_this_group_size=10,
      min_comparison_group_size=5,
      min_group_qty=5,
      lookback_days=30,
      percentile_low=10,
      percentile_high=90)

  # we only want to do this if we're already at ~75% of the number of flights we'd
  # expect to see for the hour
  flight_hours = {}
  for flight in flights:
    if flights[-1]['now'] - flight['now'] < 8.5 * SECONDS_IN_DAY and DisplayTime(
        flight, format_string='%-I%p') == DisplayTime(flights[-1], format_string='%-I%p'):
      flight_hours[DisplayTime(flight, format_string='%-d')] = flight_hours.get(
          DisplayTime(flight, format_string='%-d'), 0) + 1
  min_this_hour_flights = max(5, 0.75 * max(flight_hours.values()))

  # Once we've commented on the insights for an hour or day, we don't want to do it again
  hour_delay_frequency_flag = FLAG_INSIGHT_HOUR_DELAY_FREQUENCY
  hour_delay_time_flag = FLAG_INSIGHT_HOUR_DELAY_TIME
  date_delay_frequency_flag = FLAG_INSIGHT_DATE_DELAY_FREQUENCY
  date_delay_time_flag = FLAG_INSIGHT_DATE_DELAY_TIME
  for flight in flights[:-1]:
    insights = flight['insight_types']
    this_hour = DisplayTime(flights[-1], format_string='%x %-I%p')
    this_day = DisplayTime(flights[-1], format_string='%x')
    if (this_hour == DisplayTime(flight, format_string='%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_FREQUENCY in insights):
      hour_delay_frequency_flag = None
    if (this_hour == DisplayTime(flight, format_string='%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_TIME in insights):
      hour_delay_time_flag = None
    if (this_day == DisplayTime(flight, format_string='%x') and
        FLAG_INSIGHT_DATE_DELAY_FREQUENCY in insights):
      date_delay_frequency_flag = None
    if (this_day == DisplayTime(flight, format_string='%x') and
        FLAG_INSIGHT_DATE_DELAY_TIME in insights):
      date_delay_time_flag = None








  # 7a flights have a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (hour_delay_frequency_flag, hour_delay_time_flag),
      group_function=lambda f: DisplayTime(f, format_string='%-I%p') + ' hour',
      group_label='The',


      min_days=3,
      min_this_group_size=min_this_hour_flights,
      min_comparison_group_size=10,
      min_group_qty=10,
      lookback_days=7,
      percentile_low=10,
      percentile_high=90)

  # we only want to do this if we're already at ~75% of the number of flights we'd
  # expect to see for the day
  flight_days = {}
  for flight in flights:
    if flights[-1]['now'] - flight['now'] < 8.5 * SECONDS_IN_DAY:
      flight_days[DisplayTime(flight, format_string='%-d')] = flight_days.get(
          DisplayTime(flight, format_string='%-d'), 0) + 1
  min_this_day_flights = max(40, 0.75 * max(flight_days.values()))

  # Today (31st) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (date_delay_frequency_flag, date_delay_time_flag),
      group_function=lambda f:
      '(' + Ordinal(int(DisplayTime(f, format_string='%-d'))) + ')',
      group_label='Today',
      min_days=7,
      min_this_group_size=min_this_day_flights,
      min_comparison_group_size=40,
      min_group_qty=7,
      lookback_days=28,
      percentile_low=10,
      percentile_high=90)

  messages = [
      (t, textwrap.wrap(m, width=SPLITFLAP_CHARS_PER_LINE))
      for (t, m) in messages]

  return messages


def CreateFlightInsights(
    flights, flight_insights_enabled_string, insight_message_distribution):
  """Returns the desired quantity of flight insight messages.

  Though the function FlightInsights generates all possible insight messages about a
  flight, the user may have only wanted one. Depending on the setting of
  flight_insights_enabled_string, this function reduces the set of all insights by
  selecting the least-frequently reported type of insight message.

  In order to choose the least-frequently reported type, we need to keep track of what




                            <----SKIPPED LINES---->




    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 the
        restrictions of truncate.

  Returns:
    2-tuple of lists cut and sorted as indicated by parameters above:
    - list of values (or frequency) of the histogram elements
    - list of keys (or labels) of the histogram elements
  """
  histogram_dict = {}
  filtered_data = []







  # get timezone & now so that we can generate a timestamp for comparison just once
  if hours:
    now = datetime.datetime.now(TZ)
  for element in data:
    if (
        element.get('min_feet', float('inf')) <= max_distance_feet and
        element.get('altitude', float('inf')) <= max_altitude_feet and
        HoursSinceFlight(now, element['now']) <= hours):
      filtered_data.append(element)
      key = keyfunction(element)
      if key is None or key == '':
        key = KEY_NOT_PRESENT_STRING
      if key in histogram_dict:
        histogram_dict[key] += 1
      else:
        histogram_dict[key] = 1
  values = list(histogram_dict.values())
  keys = list(histogram_dict.keys())

  if normalize_factor:
    values = [v / normalize_factor for v in values]

  sort_by_enumerated_list = isinstance(sort_type, list)
  if exhaustive and sort_by_enumerated_list:
    missing_keys = set(sort_type).difference(set(keys))
    missing_values = [0 for unused_k in missing_keys]
    keys.extend(missing_keys)




                            <----SKIPPED LINES---->




  matplotlib.pyplot.subplots_adjust(bottom=0.15, left=0.09, right=0.99, top=0.92)

  matplotlib.pyplot.xticks(
      values_coordinates, keys, rotation='vertical', wrap=True,
      horizontalalignment='right',
      verticalalignment='center')


def HistogramSettingsHours(how_much_history):
  """Extracts the desired history (in hours) from the histogram configuration string.

  Args:
    how_much_history: string from the histogram config file.

  Returns:
    Number of hours of history to include in the histogram.
  """
  if how_much_history == 'today':
    hours = HoursSinceMidnight()
  elif how_much_history == '24h':
    hours = 24
  elif how_much_history == '7d':
    hours = 7 * HOURS_IN_DAY
  elif how_much_history == '30d':
    hours = 30 * HOURS_IN_DAY
  else:
    LogMessage(
        'Histogram form has invalid value for how_much_history: %s' % how_much_history)
    hours = 7 * HOURS_IN_DAY
  return hours


def HistogramSettingsScreens(max_screens):
  """Extracts the desired number of text screens from the histogram configuration string.

  Args:
    max_screens: string from the histogram config file.

  Returns:
    Number of maximum number of screens to display for a splitflap histogram.
  """
  if max_screens == '_1':
    screen_limit = 1
  elif max_screens == '_2':
    screen_limit = 2
  elif max_screens == '_5':
    screen_limit = 5
  elif max_screens == 'all':
    screen_limit = 0  # no limit on screens
  else:
    LogMessage('Histogram form has invalid value for max_screens: %s' % max_screens)
    screen_limit = 1
  return screen_limit


def HistogramSettingsKeySortTitle(which, max_altitude=45000):
  """Provides the arguments necessary to generate a histogram from the config string.

  The same parameters are used to generate either a splitflap text or web-rendered
  histogram in terms of the histogram title, the keyfunction, and how to sort the keys.
  For a given histogram name (based on the names defined in the histogram config file),
  this provides those parameters.

  Args:
    which: string from the histogram config file indicating the histogram to provide
        settings for.

    max_altitude: indicates the maximum altitude that should be included on the
        altitude labels.

  Returns:
    A 3-tuple of the parameters used by either CreateSingleHistogramChart or
    MessageboardHistogram, of the keyfunction, sort, and title.
  """
  def DivideAndFormat(dividend, divisor):


    if isinstance(dividend, numbers.Number):
      return '%2d' % round(dividend / divisor)
    return dividend[:2]

  if which == 'destination':
    key = lambda k: k.get('destination_iata', KEY_NOT_PRESENT_STRING)
    sort = 'value'
    title = 'Destination'
  elif which == 'origin':
    key = lambda k: k.get('origin_iata', KEY_NOT_PRESENT_STRING)
    sort = 'value'
    title = 'Origin'
  elif which == 'hour':
    key = lambda k: DisplayTime(k, '%H')
    sort = 'key'
    title = 'Hour'
  elif which == 'airline':
    key = DisplayAirline
    sort = 'value'
    title = 'Airline'




                            <----SKIPPED LINES---->




    key = lambda k: k.get('aircraft_type_code', KEY_NOT_PRESENT_STRING)
    sort = 'value'
    title = 'Aircraft'
  elif which == 'altitude':
    key = lambda k: DivideAndFormat(k.get('altitude', KEY_NOT_PRESENT_STRING), 1000)
    sort = ['%2d'%x for x in range(0, round((max_altitude+1)/1000))]
    title = 'Altitude (1000ft)'
  elif which == 'bearing':
    key = lambda k: ConvertBearingToCompassDirection(
        k.get('track', KEY_NOT_PRESENT_STRING), pad=True, length=3)
    sort = [d.rjust(3) for d in DIRECTIONS_16]
    title = 'Bearing'
  elif which == 'distance':
    key = lambda k: DivideAndFormat(k.get('min_feet', KEY_NOT_PRESENT_STRING), 100)
    sort = ['%2d'%x for x in range(0, round((MIN_METERS*FEET_IN_METER)/100)+1)]
    title = 'Min Dist (100ft)'
  elif which == 'day_of_week':
    key = lambda k: DisplayTime(k, '%a')
    sort = DAYS_OF_WEEK
    title = 'Day of Week'





  elif which == 'day_of_month':
    key = lambda k: DisplayTime(k, '%-d').rjust(2)
    today_day = datetime.datetime.now(TZ).day
    days = list(range(today_day, 0, -1))  # today down to the first of the month
    days.extend(range(31, today_day, -1))  # 31st of the month down to day after today
    days = [str(d).rjust(2) for d in days]
    sort = days
    title = 'Day of Month'
  else:
    LogMessage(
        'Histogram form has invalid value for which_histograms: %s' % which)
    return HistogramSettingsKeySortTitle(
        'destination', max_altitude=max_altitude)

  return (key, sort, title)


def ImageHistograms(
    flights,
    which_histograms,
    how_much_history,
    filename_prefix=HISTOGRAM_IMAGE_PREFIX,
    filename_suffix=HISTOGRAM_IMAGE_SUFFIX):
  """Generates multiple split histogram images.

  Args:
    flights: 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.
    which_histograms: string paramater indicating which histogram(s) to generate, which
        can be either the special string 'all', or a string linked to a specific
        histogram.
    how_much_history: string parameter taking a value among ['today', '24h', '7d', '30d].
    filename_prefix: this string indicates the file path and name prefix for the images




                            <----SKIPPED LINES---->




    histograms_to_generate.append({'generate': 'hour'})
  if which_histograms in ['airline', 'all']:
    histograms_to_generate.append({'generate': 'airline', 'truncate': int(TRUNCATE/2)})
  if which_histograms in ['aircraft', 'all']:
    histograms_to_generate.append({'generate': 'aircraft'})
  if which_histograms in ['altitude', 'all']:
    histograms_to_generate.append({'generate': 'altitude', 'exhaustive': True})
  if which_histograms in ['bearing', 'all']:
    histograms_to_generate.append({'generate': 'bearing'})
  if which_histograms in ['distance', 'all']:
    histograms_to_generate.append({'generate': 'distance', 'exhaustive': True})
  if which_histograms in ['day_of_week', 'all']:
    histograms_to_generate.append({'generate': 'day_of_week'})
  if which_histograms in ['day_of_month', 'all']:
    histograms_to_generate.append({'generate': 'day_of_month'})

  for histogram in histograms_to_generate:
    this_histogram = which_histograms
    if this_histogram == 'all':
      this_histogram = histogram['generate']
    (key, sort, title) = HistogramSettingsKeySortTitle(this_histogram)

    CreateSingleHistogramChart(
        flights,
        key,
        sort,
        title,
        truncate=histogram.get('truncate', TRUNCATE),
        hours=hours,
        exhaustive=histogram.get('exhaustive', False))
    filename = filename_prefix + histogram['generate'] + '.' + filename_suffix
    matplotlib.pyplot.savefig(filename)
    matplotlib.pyplot.close()

  histograms_generated = [h['generate'] for h in histograms_to_generate]
  return histograms_generated


def MessageboardHistograms(
    flights,
    which_histograms,




                            <----SKIPPED LINES---->




        'columns': 3})
  if ((which_histograms == 'all' and how_much_history == '7d')
      or which_histograms == 'day_of_week'):
    histograms_to_generate.append({
        'generate': 'day_of_week',
        'columns': 3,
        'absolute': True})
  if ((which_histograms == 'all' and how_much_history == '30d')
      or which_histograms == 'day_of_month'):
    histograms_to_generate.append({
        'generate': 'day_of_month',
        'columns': 3,
        'suppress_percent_sign': True,
        'column_divider': '|',
        'absolute': True})

  for histogram in histograms_to_generate:
    this_histogram = which_histograms
    if this_histogram == 'all':
      this_histogram = histogram['generate']
    (key, sort, title) = HistogramSettingsKeySortTitle(this_histogram)

    histogram = MessageboardHistogram(
        flights,
        key,
        sort,
        title,
        screen_limit=screen_limit,
        columns=histogram.get('columns', 2),
        suppress_percent_sign=histogram.get('suppress_percent_sign', False),
        column_divider=histogram.get('column_divider', ' '),
        data_summary=data_summary,
        hours=hours,
        absolute=histogram.get('absolute', False))
    messages.extend(histogram)

  return messages


def MessageboardHistogram(
    data,




                            <----SKIPPED LINES---->




    start_index = screen*available_entries_per_screen
    end_index = min((screen+1)*available_entries_per_screen-1, len(keys)-1)
    number_of_entries = end_index - start_index + 1
    number_of_lines = math.ceil(number_of_entries / columns)

    lines = []

    lines.append(screen_title.upper())
    if data_summary:
      lines.append(summary_text.upper())
    for line_index in range(number_of_lines):
      key_value = []
      for column_index in range(columns):
        index = start_index + column_index*number_of_lines + line_index
        if index <= end_index:
          if absolute:
            value_string = format_string % values[index]
          else:
            # If the % is >=1%, display right-justified 2 digit percent, i.e. ' 5%'
            # Otherwise, if it rounds to at least 0.1%, display i.e. '.5%'
            if round(values[index]/total*100) >= 1:
              value_string = '%2d' % round(values[index]/total*100)
            elif round(values[index]/total*1000)/10 >= 0.1:
              value_string = ('%.1f' % (round(values[index]/total*1000)/10))[1:]
            else:
              value_string = ' 0'
          key_value.append('%s %s%s' % (
              str(keys[index])[:column_key_width].ljust(column_key_width),
              value_string,
              printed_percent_sign))

      line = (column_divider.join(key_value)).upper()
      lines.append(line)

    split_flap_boards.append(lines)

  return split_flap_boards


def TriggerHistograms(flights, histogram_settings):
  """Triggers the text-based or web-based histograms.




                            <----SKIPPED LINES---->




    d['now'] = flight['now']

    requested_time = time.time()
    requested_elapsed_seconds = requested_time - flight['now']
    updated_loc = ClosestKnownLocation(flight, requested_elapsed_seconds)
    actual_elapsed_seconds = requested_elapsed_seconds - updated_loc[1]
    d['speed'] = updated_loc[0]['speed']
    d['lat'] = updated_loc[0]['lat']
    d['lon'] = updated_loc[0]['lon']
    d['track'] = updated_loc[0]['track']
    d['altitude'] = updated_loc[0]['altitude']
    d['vertrate'] = updated_loc[0]['vertrate']
    d['flight_loc_now'] = flight['now'] + actual_elapsed_seconds

  today = datetime.datetime.now(TZ).strftime('%x')
  flight_count_today = len([1 for f in flights if DisplayTime(f, '%x') == today])

  d['flight_count_today'] = flight_count_today

  settings_string = BuildSettings(d)

  if os.path.exists(ARDUINO_FILE):
    existing_data = ReadAndParseSettings(ARDUINO_FILE)
    if d != existing_data:


      WriteFile(ARDUINO_FILE, settings_string)
  else:
    WriteFile(ARDUINO_FILE, settings_string)

  return settings_string


def PublishMessage(
    s,
    subscription_id='12fd73cd-75ef-4cae-bbbf-29b2678692c1',
    key='c5f62d44-e30d-4c43-a43e-d4f65f4eb399',
    secret='b00aeb24-72f3-467c-aad2-82ba5e5266ca',
    timeout=3):
  """Publishes a text string to a Vestaboard.

  The message is pushed to the vestaboard splitflap display by way of its web services;
  see https://docs.vestaboard.com/introduction for more details.

  Args:
    s: String to publish.
    subscription_id: string subscription id from Vestaboard.
    key: string key from Vestaboard.
    secret: string secret from Vestaboard.




                            <----SKIPPED LINES---->




    status_code = curl.getinfo(pycurl.RESPONSE_CODE)
    if status_code != 200:
      LogMessage('Server returned HTTP status code %d for message %s' % (status_code, s))

  curl.close()


def ManageMessageQueue(message_queue, next_message_time, configuration):
  """Check time & if appropriate, display next message from queue.

  Args:
    message_queue: FIFO list of message tuples of (message type, message string).
    next_message_time: epoch at which next message should be displayed
    configuration: dictionary of configuration attributes.

  Returns:
    Next_message_time, potentially updated if a message has been displayed, or unchanged
    if no message was displayed.
  """
  if message_queue and (time.time() >= next_message_time or SIMULATION):

    if SIMULATION:  # drain the queue because the messages come so fast
      messages_to_display = list(message_queue)
      # passed by reference, so clear it out since we drained it to the display
      del message_queue[:]
    else:  # display only one message, being mindful of the display timing
      messages_to_display = [message_queue.pop(0)]

    for message in messages_to_display:
      message_text = message[1]
      if isinstance(message_text, str):
        message_text = textwrap.wrap(message_text, width=SPLITFLAP_CHARS_PER_LINE)
      display_message = Screenify(message_text, False)
      LogMessage(display_message, file=ALL_MESSAGE_FILE)
      MaintainRollingWebLog(display_message, 25)
      if not SIMULATION:
        splitflap_message = Screenify(message_text, True)
        PublishMessage(splitflap_message)

    next_message_time = time.time() + configuration['setting_delay']
  return next_message_time




                            <----SKIPPED LINES---->




    tmp_filename = filename + 'tmp'

    if os.path.exists(tmp_filename):
      os.remove(tmp_filename)

    if os.path.exists(filename):
      mtime = os.path.getmtime(filename)
      flights = UnpickleObjectFromFile(filename)
      for (n, flight) in enumerate(flights):
        if n/25 == int(n/25):
          print(' - %d' % n)
        CreateFlightInsights(flights[:n+1], configuration.get('insights', 'hide'), {})
        PickleObjectToFile(flight, tmp_filename)

      if mtime == os.path.getmtime(filename):
        shutil.move(tmp_filename, filename)
      else:
        print('Failed to bootstrap %s: file changed while in process' % filename)





















def main():
  """Traffic cop between incoming radio flight messages, configuration, and messageboard.

  This is the main logic, checking for new flights, augmenting the radio signal with
  additional web-scraped data, and generating messages in a form presentable to the
  messageboard.
  """




  LogMessage('Starting up')
  if '-s' in sys.argv:
    global SIMULATION_COUNTER
    SimulationSetup()

  already_running_id = CheckIfProcessRunning()
  if already_running_id:
    os.kill(already_running_id, signal.SIGKILL)



  configuration = ReadAndParseSettings(CONFIG_FILE)
  last_distance = configuration.get('distance')
  last_altitude = configuration.get('altitude')
  startup_time = time.time()
  json_desc_dict = {}

  # If we're displaying just a single insight message, we want it to be something
  # unique, to the extent possible; this dict holds a count of the diff types of messages
  # displayed so far
  insight_message_distribution = {}

  flights = []
  if os.path.exists(PICKLE_FLIGHTS_30D):
    flights = TruncatePickledDictionaries(PICKLE_FLIGHTS_30D)

    # Clear the loaded flight of any cached data since code fixes may change
    # the values for some of those cached elements
    for flight in flights:
      for key in list(flight.keys()):




                            <----SKIPPED LINES---->




        CreateFlightInsights(
            flights[:n+1],
            configuration.get('insights', 'hide'),
            insight_message_distribution)

  # used in simulation to print the hour of simulation once per simulated hour
  prev_simulated_hour = ''

  persistent_nearby_aircraft = {} # key = flight number; value = last seen
  persistent_path = {}
  histogram = {}

  # Next up to print is element 0; this is a list of tuples:
  # Element#1: flag indicating the type of message that this is
  # Element#2: the message itself
  message_queue = []
  next_message_time = time.time()

  # We repeat the loop every x seconds; this ensures that if the processing time is long,
  # we don't wait another x seconds after processing completes
  next_loop_time = time.time()

  # These files are read only if the version on disk has been modified more recently
  # than the last time it was read
  last_dump_json_timestamp = 0

  LogMessage('Finishing initialization; starting radio polling loop')
  while not SIMULATION or SIMULATION_COUNTER < len(DUMP_JSONS):

    new_configuration = ReadAndParseSettings(CONFIG_FILE)
    if (new_configuration.get('setting_max_distance')
        != configuration.get('setting_max_distance') or
        new_configuration.get('setting_max_altitude')
        != configuration.get('setting_max_altitude')):

      last_distance = configuration.get('setting_max_distance')
      last_altitude = configuration.get('setting_max_altitude')
      FlightCriteriaHistogramPng(
          flights,
          new_configuration['setting_max_distance'],
          new_configuration['setting_max_altitude'],
          7,
          last_max_distance_feet=last_distance,
          last_max_altitude_feet=last_altitude)









    configuration = new_configuration



    # if this is a SIMULATION, then process every diff dump. But if it isn't a simulation,
    # then only read & do related processing for the next dump if the last-modified
    # timestamp indicates the file has been updated since it was last read.
    tmp_timestamp = 0
    if not SIMULATION:
      tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE)
    if (SIMULATION and DumpJsonChanges()) or (
        not SIMULATION and tmp_timestamp > last_dump_json_timestamp):
      last_dump_json_timestamp = tmp_timestamp

      # a command might request info about flight to be (re)displayed, irrespective of
      # whether the screen is on; if so, let's put that message at the front of the message
      # queue, and delete any subsequent messages in queue because presumably the button
      # was pushed either a) when the screen was off (so no messages in queue), or b)
      # because the screen was on, but the last flight details got lost after other screens
      if os.path.exists(LAST_FLIGHT_FILE):
        messageboard_flight_index = IdentifyFlightDisplayed(
            flights, configuration, display_all_hours=True)
        if messageboard_flight_index is not None:
          message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]
          flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index])
          message_queue = [FLAG_MSG_FLIGHT, flight_message]
          next_message_time = time.time()
        os.remove(LAST_FLIGHT_FILE)

      (persistent_nearby_aircraft,
       unused_current_nearby_aircraft,
       flight, now,
       json_desc_dict,
       persistent_path) = ScanForNewFlights(persistent_nearby_aircraft, persistent_path)

      if flight:
        flights.append(flight)
        UpdateArduinoFile(
            flights, json_desc_dict)

        flight_meets_display_criteria = FlightMeetsDisplayCriteria(flight, configuration)

        if flight_meets_display_criteria:
          flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight))

          # display the next message about this flight now!
          next_message_time = time.time()
          message_queue.insert(0, flight_message)
          # and delete any queued insight messages about other flights that have
          # not yet displayed, since a newer flight has taken precedence
          messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INTERESTING]
          if messages_to_delete:
            LogMessage(
                'Deleting messages from queue due to new-found plane: %s' %
                messages_to_delete)
          message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]

          # Though we also manage the message queue outside this conditional as well,
          # because it can take a half second to generate the flight insights, this allows
          # this message to start displaying on the board immediately, so it's up there
          # when it's most relevant
          next_message_time = ManageMessageQueue(




                            <----SKIPPED LINES---->




            next_flight_text = FlightInsightNextFlight(flights)
            if next_flight_text:
              insight_messages.insert(0, next_flight_text)

          insight_messages = [(FLAG_MSG_INTERESTING, m) for m in insight_messages]

          for insight_message in insight_messages:
            message_queue.insert(0, insight_message)

        else:  # flight didn't meet display criteria
          flight['insight_types'] = []

        PickleObjectToFile(flight, PICKLE_FLIGHTS_30D)
        PickleObjectToFile(flight, PICKLE_FLIGHTS_ARCHIVE)

      else:
        UpdateArduinoFile(flights, json_desc_dict)

    if SIMULATION:
      if now:
        simulated_hour = EpochDisplayTime(now, format_string='%Y-%m-%d %H:00%z')
      if simulated_hour != prev_simulated_hour:
        print(simulated_hour)
        prev_simulated_hour = simulated_hour

    histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE)
    if os.path.exists(HISTOGRAM_CONFIG_FILE):
      os.remove(HISTOGRAM_CONFIG_FILE)

    # We also need to make sure there are flights on which to generate a histogram! Why
    # might there not be any flights? Primarily during a simulation, if there's a
    # lingering histogram file at the time of history restart.
    if histogram and flights:
      histogram_messages = TriggerHistograms(flights, histogram)
      histogram_messages = [(FLAG_MSG_HISTOGRAM, m) for m in histogram_messages]
      message_queue.extend(histogram_messages)

    # check time & if appropriate, display next message from queue
    next_message_time = ManageMessageQueue(message_queue, next_message_time, configuration)

    # if we've been running a long time, and everything else is quiet, reboot
    running_hours = (time.time() - startup_time) / SECONDS_IN_HOUR
    if (
        running_hours >= HOURS_IN_DAY and
        not message_queue and
        not json_desc_dict.get('radio_range_flights')):
      LogMessage('About to reboot after running for %.2f hours' % running_hours)
      os.system('sudo reboot')


    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1

  if SIMULATION:
    SimulationEnd(message_queue, flights)


if __name__ == "__main__":
  if '-i' in sys.argv:
    BootstrapInsightList()
  else:
    main()

01234567890123456789012345678901234567890123456789012345678901234567890123456789









3637383940414243444546474849505152535455565758596061626364656667686970717273747576








117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210








248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341








664665666667668669670671672673674675676677678679680681682683 684685686687688689690691692693694695696697698699700701702703








742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783








902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943








10801081108210831084108510861087108810891090109110921093109410951096109710981099 11001101110211031104110511061107110811091110111111121113111411151116111711181119








11441145114611471148114911501151115211531154115511561157115811591160116111621163 11641165116611671168116911701171117211731174117511761177117811791180118111821183








122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280








129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334








13931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433








1678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741 1742 17431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789  17901791179217931794179517961797179817991800180118021803180418051806180718081809








19721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997 1998   199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022








21622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202








22632264226522662267226822692270227122722273227422752276227722782279228022812282  228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318








23262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351 235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576








28372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877








31033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203 3204320532063207320832093210321132123213321432153216 3217321832193220322132223223322432253226322732283229 323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283 32843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327








35203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572








3822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918








392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979








39973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037








41004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140








42404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280








459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619  46204621462246234624462546264627462846294630463146324633463446354636463746384639








46754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715








47394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818








48314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913 4914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944








49504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014



                            <----SKIPPED LINES---->





HOME_LAT = 37.64406
HOME_LON = -122.43463
HOME = (HOME_LAT, HOME_LON) # lat / lon tuple of antenna
HOME_ALT = 29  #altitude in meters
RADIUS = 6371.0e3  # radius of earth in meters

FEET_IN_METER = 3.28084
FEET_IN_MILE = 5280
METERS_PER_SECOND_IN_KNOTS = 0.514444

MIN_METERS = 5000/FEET_IN_METER # only planes within this distance will be detailed
# planes not seen within MIN_METERS in PERSISTENCE_SECONDS seconds will be dropped from
# the nearby list
PERSISTENCE_SECONDS = 10
TRUNCATE = 50  # max number of keys to include in a histogram image file
# number of seconds to pause between each radio poll / command processing loop
LOOP_DELAY_SECONDS = 1

# This file is where the radio drops its json file
DUMP_JSON_FILE = '/run/readsb/aircraft.json'

# This is the directory that stores all the ancillary messageboard configuration files
# that do not need to be exposed via the webserver
MESSAGEBOARD_PATH = '/home/pi/splitflap/'
# This is the directory of the webserver; files placed here are available at
# http://adsbx-custom.local/; files placed in this directly are visible via a browser
WEBSERVER_PATH = '/var/www/html/'

# At the time a flight is first identified as being of interest (in that it falls
# within MIN_METERS meters of HOME), it - and core attributes derived from FlightAware,
# if any - is appended to the end of this pickle file. However, since this file is
# cached in working memory, flights older than 30 days are flushed from this periodically.
PICKLE_FLIGHTS_30D = 'flights_30d.pk' #pickled list of up to about 30d of flights
# This is the same concept as the 30d pickle file, except it is not auto-flushed, and
# so will grow indefinitely.
PICKLE_FLIGHTS_ARCHIVE = 'flights_archive.pk' #pickled list of all flights

CACHED_ELEMENT_PREFIX = 'cached_'

# This web-exposed file is used for non-error messages that might highlight data or




                            <----SKIPPED LINES---->




# alignment with the html page that displays it.
HOURLY_IMAGE_FILE = 'hours.png'

# Communication with the asynchronously-running Arduino interface is done thru files;
# this file includes the superset of key-value pairs that could potentially be sent
# directly, or after some manipulation, to the Arduino.
ARDUINO_FILE = 'arduino.txt'
# One potential command from the Arduino is to "display the last flight"; this request
# is communicated to the Arduino by the presence of this file. After that request is
# processed, this file is deleted. The contents of the file are not used in any way.
LAST_FLIGHT_FILE = 'last_flight.txt'

# This is all messages that have been sent to the board since the last time the
# file was manually cleared. Newest messages are at the bottom. It is visible at the
# webserver.
ALL_MESSAGE_FILE = 'all_messages.txt'  #enumeration of all messages sent to board
# This shows the most recent n messages sent to the board. Newest messages are at the
# top for easier viewing of "what did I miss".
ROLLING_MESSAGE_FILE = 'rolling_messages.txt'

STDERR_FILE = 'stderr.txt'
BACKUP_FILE = 'backup.txt'
SERVICE_VERIFICATION_FILE = 'service-verification.txt'

FLAG_MSG_FLIGHT = 1  # basic flight details
FLAG_MSG_INTERESTING = 2  # random tidbit about a flight
FLAG_MSG_HISTOGRAM = 3 # histogram message

FLAG_INSIGHT_LAST_SEEN = 0
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

#if running on raspberry, then need to prepend path to file names
if psutil.sys.platform.title() == 'Linux':
  PICKLE_FLIGHTS_30D = MESSAGEBOARD_PATH + PICKLE_FLIGHTS_30D
  PICKLE_FLIGHTS_ARCHIVE = MESSAGEBOARD_PATH + PICKLE_FLIGHTS_ARCHIVE
  LOGFILE = MESSAGEBOARD_PATH + LOGFILE
  PICKLE_DUMP_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_DUMP_JSON_FILE
  PICKLE_FA_JSON_FILE = MESSAGEBOARD_PATH + PICKLE_FA_JSON_FILE
  LAST_FLIGHT_FILE = MESSAGEBOARD_PATH + LAST_FLIGHT_FILE
  ARDUINO_FILE = MESSAGEBOARD_PATH + ARDUINO_FILE

  HISTOGRAM_CONFIG_FILE = WEBSERVER_PATH + HISTOGRAM_CONFIG_FILE
  CONFIG_FILE = WEBSERVER_PATH + CONFIG_FILE
  HOURLY_IMAGE_FILE = WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HOURLY_IMAGE_FILE
  ROLLING_MESSAGE_FILE = WEBSERVER_PATH + ROLLING_MESSAGE_FILE
  HISTOGRAM_IMAGE_PREFIX = (
      WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_IMAGE_PREFIX)
  HISTOGRAM_EMPTY_IMAGE_FILE = (
      WEBSERVER_PATH + WEBSERVER_IMAGE_FOLDER + HISTOGRAM_EMPTY_IMAGE_FILE)
  ALL_MESSAGE_FILE = WEBSERVER_PATH + ALL_MESSAGE_FILE
  ROLLING_LOGFILE = WEBSERVER_PATH + ROLLING_LOGFILE
  STDERR_FILE = WEBSERVER_PATH + STDERR_FILE
  BACKUP_FILE = WEBSERVER_PATH + BACKUP_FILE
  SERVICE_VERIFICATION_FILE = WEBSERVER_PATH + SERVICE_VERIFICATION_FILE

TIMEZONE = 'US/Pacific' # timezone of display
TZ = pytz.timezone(TIMEZONE)

KNOWN_AIRPORTS = ('SJC', 'SFO', 'OAK')  # iata codes that we don't need to expand

SPLITFLAP_CHARS_PER_LINE = 22
SPLITFLAP_LINE_COUNT = 6

DIRECTIONS_4 = ['N', 'E', 'S', 'W']
DIRECTIONS_8 = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
DIRECTIONS_16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
                 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']

HOURS = ['12a', ' 1a', ' 2a', ' 3a', ' 4a', ' 5a', ' 6a', ' 7a',
         ' 8a', ' 9a', '10a', '11a', '12p', ' 1p', ' 2p', ' 3p',
         ' 4p', ' 5p', ' 6p', ' 7p', ' 8p', ' 9p', '10p', '11p']

SECONDS_IN_MINUTE = 60
MINUTES_IN_HOUR = 60




                            <----SKIPPED LINES---->




aircraft_length['Boeing 737-700 (twin-jet)'] = 33.63
aircraft_length['Boeing 737-800 (twin-jet)'] = 39.47
aircraft_length['Boeing 737-900 (twin-jet)'] = 42.11
aircraft_length['Boeing 747-400 (quad-jet)'] = 36.4
aircraft_length['Boeing 747-8 (quad-jet)'] = 76.25
aircraft_length['Boeing 757-200 (twin-jet)'] = 47.3
aircraft_length['Boeing 757-300 (twin-jet)'] = 54.4
aircraft_length['Boeing 767-200 (twin-jet)'] = 48.51
aircraft_length['Boeing 767-300 (twin-jet)'] = 54.94
aircraft_length['Boeing 777-200 (twin-jet)'] = 63.73
aircraft_length['Boeing 777-200LR/F (twin-jet)'] = 63.73
aircraft_length['Boeing 777-300ER (twin-jet)'] = 73.86
aircraft_length['Boeing 787-10 (twin-jet)'] = 68.28
aircraft_length['Boeing 787-8 (twin-jet)'] = 56.72
aircraft_length['Boeing 787-9 (twin-jet)'] = 62.81
aircraft_length['Canadair Regional Jet CRJ-200 (twin-jet)'] = 26.77
aircraft_length['Canadair Regional Jet CRJ-700 (twin-jet)'] = 32.3
aircraft_length['Canadair Regional Jet CRJ-900 (twin-jet)'] = 36.2
aircraft_length['Canadair Challenger 350 (twin-jet)'] = 20.9
aircraft_length['Bombardier Challenger 300 (twin-jet)'] = 20.92
aircraft_length['Embraer 170/175 (twin-jet)'] = (29.90 + 31.68) / 2
aircraft_length['EMBRAER 175 (long wing) (twin-jet)'] = 31.68
aircraft_length['Embraer ERJ-135 (twin-jet)'] = 26.33
aircraft_length['Cessna Caravan (single-turboprop)'] = 11.46
aircraft_length['Cessna Citation II (twin-jet)'] = 14.54
aircraft_length['Cessna Citation V (twin-jet)'] = 14.91
aircraft_length['Cessna Skyhawk (piston-single)'] = 8.28
aircraft_length['Cessna Skylane (piston-single)'] = 8.84
aircraft_length['Cessna Citation Sovereign (twin-jet)'] = 19.35
aircraft_length['Cessna T206 Turbo Stationair (piston-single)'] = 8.61
aircraft_length['Beechcraft Bonanza (33) (piston-single)'] = 7.65
aircraft_length['Beechcraft Super King Air 200 (twin-turboprop)'] = 13.31
aircraft_length['Beechcraft Super King Air 350 (twin-turboprop)'] = 14.22
aircraft_length['Beechcraft King Air 90 (twin-turboprop)'] = 10.82
aircraft_length['Pilatus PC-12 (single-turboprop)'] = 14.4


def CheckIfProcessRunning():
  """Returns proc id if process with identically-named python file running; else None."""
  this_process_id = os.getpid()
  this_process_name = os.path.basename(sys.argv[0])
  for proc in psutil.process_iter():
    try:
      # Check if process name contains this_process_name.
      commands = proc.as_dict(attrs=['cmdline', 'pid'])
      if commands['cmdline']:
        command_running = any(
            [this_process_name in s for s in commands['cmdline']])
        if command_running and commands['pid'] != this_process_id:
          return commands['pid']
    except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
      pass
  return None


def LogMessage(message, file=None):
  """Write a message to a logfile along with a timestamp.

  Args:
      message: string message to write
      file: string representing file name and, if needed, path to the file to write to
  """
  # can't define as a default parameter because LOGFILE name is potentially
  # modified based on SIMULATION flag
  if not file:
    file = LOGFILE

  try:
    with open(file, 'a') as f:
      if not SIMULATION:  # by excluding the timestamp, file diffs become easy between runs
        f.write('='*80+'\n')
        f.write(str(datetime.datetime.now(TZ))+'\n')
        f.write('\n')
      f.write(str(message)+'\n')
  except IOError:
    LogMessage('Unable to append to ' + file)

  existing_log_lines = ReadFile(LOGFILE).splitlines()
  with open(ROLLING_LOGFILE, 'w') as f:
    f.write('\n'.join(existing_log_lines[-1000:]))


def MaintainRollingWebLog(message, max_count, filename=None):
  """Maintains a rolling text file of at most max_count printed messages.

  Newest data at top and oldest data at the end, of at most max_count messages,
  where the delimiter between each message is identified by a special fixed string.

  Args:
    message: text message to prepend to the file.
    max_count: maximum number of messages to keep in the file; the max_count+1st message
        is deleted.
    filename: the file to update.
  """




                            <----SKIPPED LINES---->




  radians = math.radians
  degrees = math.degrees

  if not all([isinstance(x, numbers.Number) for x in (
      *pos1, altitude1, *pos2, altitude2)]):
    return None

  distance = HaversineDistanceMeters(pos1, pos2)  # from home to plumb line of plane
  lat1, lon1, lat2, lon2 = [radians(x) for x in (*pos1, *pos2)]
  d_lon = lon2 - lon1
  # azimuth calc from https://www.omnicalculator.com/other/azimuth
  az = atan2((sin(d_lon)*cos(lat2)), (cos(lat1)*sin(lat2)-sin(lat1)*cos(lat2)*cos(d_lon)))
  az_degrees = degrees(az)
  altitude = altitude2 - altitude1
  alt = atan(altitude / distance)
  alt_degrees = degrees(alt)
  crow_distance = sqrt(altitude**2 + distance**2)  # from home to the plane

  return {'azimuth_degrees': az_degrees, 'altitude_degrees': alt_degrees,
          'ground_distance_feet': distance, 'crow_distance_feet': crow_distance}



def TrajectoryLatLon(pos, distance, track):
  """Calculates lat/lon a plane will be given its starting point and direction / speed.

  Args:
    pos: a 2-tuple of lat-lon for the flight, in degrees.
    distance: the distance, in meters, the flight is traveling from its current lat/lon.
    track: the track or bearing of the plane, in degrees.

  Returns:
    Updated lat/lon for the given trajectory.
  """
  #distance in meters
  #track in degrees
  sin = math.sin
  cos = math.cos
  atan2 = math.atan2
  asin = math.asin
  radians = math.radians




                            <----SKIPPED LINES---->




  potential_distance1 = HaversineDistanceMeters(potential_intersection1, HOME)
  potential_distance2 = HaversineDistanceMeters(potential_intersection2, HOME)

  # Since one of those two potential intersection points (i.e.: +90 or -90 degrees) will
  # create an irrational result, and given the strong locality to HOME that is expected
  # from the initial position, the "correct" result is identified by simply taking the
  # minimum distance of the two candidate.
  return min(potential_distance1, potential_distance2)


def SecondsToHhMm(seconds, colon=False):
  """Converts integer number of seconds to xhym string (i.e.: 7h17m) or to 7:17.

  Args:
    seconds: number of seconds
    colon: controls format; if False, format is 7h17m; if True, format is 7:17.

  Returns:
    String representation of hours and minutes.
  """
  if seconds is None:
    return KEY_NOT_PRESENT_STRING[:3]
  minutes = int(abs(seconds) / SECONDS_IN_MINUTE)
  if minutes > MINUTES_IN_HOUR:
    hours = int(minutes / MINUTES_IN_HOUR)
    minutes = minutes % MINUTES_IN_HOUR
    if colon:
      text = str(hours) + ':' + str(minutes)
    else:
      text = str(hours) + 'h' + str(minutes) + 'm'
  else:
    if colon:
      text = ':' + str(minutes)
    else:
      text = str(minutes) + 'm'
  return text


def SecondsToHours(seconds):
  """Converts integer number of seconds to xh string (i.e.: 7h).

  Args:




                            <----SKIPPED LINES---->




    last_modified = os.path.getmtime(filename)
    if last_modified > last_read_time:
      setting_str = ReadFile(filename)
      settings = ParseSettings(setting_str)
      CACHED_FILES[filename] = (last_modified, settings)
    return settings

  # File does not - or at least no longer - exists; so remove the cache
  if filename in CACHED_FILES:
    CACHED_FILES.pop(filename)

  return {}


def BuildSettings(d):
  """Converts a dict to a string of form key1=value1;...;keyn=valuen; keys alpha sorted."""
  kv_pairs = []
  for key in sorted(list(d.keys())):
    kv_pairs.append('%s=%s' % (key, d[key]))
  s = ';'.join(kv_pairs)
  if s:  # add terminating semicolon
    s += ';'
  return s


def ParseSettings(settings):
  """Parse delimited string of settings in file to a dict of key value pairs.

  Parses a string like 'distance=1426;altitude=32559;on=23;off=24;delay=15;insights=all;'
  into key value pairs.

  Args:
    settings: semicolon-delimited sequence of equal-sign delimited key-value pairs, i.e.:
      key1=value1;key2=value2;....;keyn=valuen.

  Returns:
    Dict of key value pairs contained in the setting file; empty dict if file not
    available or if delimiters missing.
  """
  settings_dict = {}
  for setting in settings.split(';'):
    if '=' in setting:




                            <----SKIPPED LINES---->





def ScanForNewFlights(persistent_nearby_aircraft, persistent_path):
  """Determines if there are any new aircraft in the radio message.

  The radio is continuously dumping new json messages to the Raspberry pi with all the
  flights currently observed. This function picks up the latest radio json, and for
  any new nearby flights - there should generally be at most one new flight on each
  pass through - gets additional flight data from FlightAware and augments the flight
  definition with the relevant fields to keep.

  Args:
    persistent_nearby_aircraft: dictionary where keys are flight numbers, and the values
      are the time the flight was last seen.
    persistent_path: dictionary where keys are flight numbers, and the values are a
      sequential list of the location-attributes in the json file; allows for tracking
      the flight path over time.

  Returns:
    A tuple:
    - updated persistent_nearby_aircraft

    - (possibly empty) dictionary of flight attributes of the new flight upon its
      first observation.
    - the time of the radio observation if present; None if no radio dump
    - a dictionary of attributes about the dump itself (i.e.: # of flights; furthest
      observed flight, etc.)
    - persistent_path, a data structure containing past details of a flight's location
      as described in ParseDumpJson
  """
  flight_details = {}
  now = time.time()
  if SIMULATION:
    (dump_json, json_time) = DUMP_JSONS[SIMULATION_COUNTER]
  else:
    dump_json = ReadFile(DUMP_JSON_FILE, log_exception=True)

  json_desc_dict = {}
  current_nearby_aircraft = {}
  if dump_json:
    (current_nearby_aircraft,
     now,




                            <----SKIPPED LINES---->




      elif flight_number:
        flight_aware_json = GetFlightAwareJson(flight_number)
        if not flight_aware_json:
          LogMessage('No json returned from Flightaware for flight: %s' % flight_number)

      flight_details = {}
      if flight_aware_json:
        flight_details = ParseFlightAwareJson(flight_aware_json)

      if not SIMULATION:
        PickleObjectToFile((flight_aware_json, now), PICKLE_FA_JSON_FILE)

      # Augment FlightAware details with radio / radio-derived details
      flight_details.update(current_nearby_aircraft[flight_number])

      # Augment with the past location data
      flight_details['persistent_path'] = persistent_path[flight_number][1]

  return (
      persistent_nearby_aircraft,

      flight_details,
      now,
      json_desc_dict,
      persistent_path)


def DescribeDumpJson(parsed):
  """Generates a dictionary with descriptive attributes about the dump json file.

  Args:
    parsed: The parsed json file.

  Returns:
    Dictionary with attributes about radio range, number of flights seen, etc.
  """
  json_desc_dict = {}
  json_desc_dict['now'] = parsed['now']

  aircraft = [a for a in parsed['aircraft'] if a['seen'] < PERSISTENCE_SECONDS]
  json_desc_dict['radio_range_flights'] = len(aircraft)




                            <----SKIPPED LINES---->





  for aircraft in parsed['aircraft']:
    simplified_aircraft = {}

    simplified_aircraft['now'] = now

    # flight_number
    flight_number = aircraft.get('flight')
    if flight_number:
      flight_number = flight_number.strip()
      if flight_number:
        simplified_aircraft['flight_number'] = flight_number

    if 'lat' in aircraft and 'lon' in aircraft:
      lat = aircraft['lat']
      lon = aircraft['lon']
      if isinstance(lat, numbers.Number) and isinstance(lon, numbers.Number):
        simplified_aircraft['lat'] = lat
        simplified_aircraft['lon'] = lon

        altitude = aircraft.get('altitude', aircraft.get('alt_baro'))
        if altitude is not None:
          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

        if aircraft.get('squawk') is not None:
          simplified_aircraft['squawk'] = aircraft.get('squawk')

        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

        if HaversineDistanceMeters(HOME, (lat, lon)) < MIN_METERS:
          nearby_aircraft[flight_number] = simplified_aircraft

        # 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(flight_number, (None, []))
        if simplified_aircraft not in current_path:
          current_path.append(simplified_aircraft)
        persistent_path[flight_number] = (now, current_path)




                            <----SKIPPED LINES---->





  Given a flight number, loads the corresponding FlightAware webpage for that flight and
  extracts the relevant script that contains all the flight details from that page.

  Args:
    flight_number: text flight number (i.e.: SWA1234)

  Returns:
    Text representation of the json message from FlightAware.
  """
  url = 'https://flightaware.com/live/flight/' + flight_number
  try:
    response = requests.get(url)
  except requests.exceptions.RequestException as e:
    LogMessage('Unable to query FA for URL due to %s: %s' % (e, url))
    return ''
  soup = bs4.BeautifulSoup(response.text, "html.parser")
  l = soup.findAll('script')
  flight_script = None
  for script in l:
    if "trackpollBootstrap" in script:
      flight_script = script
      break
  if not flight_script:
    LogMessage('Unable to find trackpollBootstrap script in page: ' + response.text)
    return ''
  first_open_curly_brace = flight_script.find('{')
  flight_json = flight_script[first_open_curly_brace:-1]
  return flight_json


def Unidecode(s):
  """Convert a special unicode characters to closest ASCII representation."""
  if s is not None:
    s = unidecode.unidecode(s)
  return s


def ParseFlightAwareJson(flight_json):
  """Strips relevant data about the flight from FlightAware feed.

  The FlightAware json has hundreds of fields about a flight, only a fraction of which




                            <----SKIPPED LINES---->




    flight['airline_full_name'] = Unidecode(airline.get('fullName'))

  if len(parsed_json['flights'].keys()) > 1:
    LogMessage('There are multiple flights in the FlightAware json: ' + parsed_json)

  return flight


def EpochDisplayTime(epoch, format_string='%Y-%m-%d %H:%M:%S.%f%z'):
  """Converts epoch in seconds to formatted time string."""
  return datetime.datetime.fromtimestamp(epoch, TZ).strftime(format_string)


def DisplayTime(flight, format_string='%Y-%m-%d %H:%M:%S.%f%z'):
  """Converts flight 'now' to formatted time string, caching results on flight."""
  cached_key = CACHED_ELEMENT_PREFIX + 'now-' + format_string
  cached_time = flight.get(cached_key)
  if cached_time:
    return cached_time

  epoch_display_time = EpochDisplayTime(flight['now'], format_string)
  flight[cached_key] = epoch_display_time
  return epoch_display_time


def DisplayAirline(flight):
  """Augments flight details with display-ready airline attributes.

  Args:
    flight: dictionary with key-value attributes about the flight.

  Returns:
    String identifying either the airline, or Unknown if not available.
  """
  airline = flight.get('airline_short_name', flight.get('airline_full_name'))

  # Some names are very similar to others and so appear identical on splitflap
  replacement_names = (
      ('Delta Private Jets', 'DPJ'),
      ('United Parcel Service', 'UPS'))
  for (old, new) in replacement_names:




                            <----SKIPPED LINES---->




    flight: dictionary with key-value attributes about the flight.

  Returns:
    Seconds, if the remaining time is calculable; None otherwise.
  """
  arrival = flight.get('estimated_arrival_time')
  if not arrival:
    arrival = flight.get('estimated_landing_time')
  if not arrival:
    arrival = flight.get('scheduled_arrival_time')
  if not arrival:
    arrival = flight.get('scheduled_landing_time')

  if arrival:
    remaining_seconds = flight['now'] - arrival
  else:
    remaining_seconds = None
  return remaining_seconds


def FlightMeetsDisplayCriteria(flight, configuration, display_all_hours=False, log=False):
  """Returns boolean indicating whether the screen is currently accepting new flight data.

  Based on the configuration file, determines whether the flight data should be displayed.
  Specifically, the configuration:
  - may include 'enabled' indicating whether screen should be driven at all
  - should include 'on' & 'off' parameters indicating minute (from midnight) of operation
  - should include altitude & elevation parameters indicating max values of interest

  Args:
    flight: dictionary of flight attributes.
    configuration: dictionary of configuration attributes.
    display_all_hours: a boolean indicating whether we should ignore whether the
      screen is turned off (either via the enabling, or via the hour settings)

  Returns:
    Boolean as described.
  """
  flight_altitude = flight.get('altitude', float('inf'))
  config_max_altitude = configuration['setting_max_altitude']

  flight_meets_criteria = True
  if flight_altitude > config_max_altitude:
    flight_meets_criteria = False
    if log:
      LogMessage(
          '%s not displayed because it fails altitude criteria - flight altitude: '
          '%.0f; required altitude: %.0f' % (
              DisplayFlightNumber(flight), flight_altitude, config_max_altitude))
  else:
    flight_distance = flight.get('min_feet', float('inf'))
    config_max_distance = configuration['setting_max_distance']
    if flight_distance > config_max_distance:
      flight_meets_criteria = False
      if log:
        LogMessage(
            '%s not displayed because it fails distance criteria - flight distance: '
            '%.0f; required distance: %.0f' % (
                DisplayFlightNumber(flight), flight_distance, config_max_distance))

  if not display_all_hours and flight_meets_criteria:
    flight_timestamp = flight['now']
    dt = datetime.datetime.fromtimestamp(flight_timestamp, TZ)
    minute_of_day = dt.hour * MINUTES_IN_HOUR + dt.minute

    if minute_of_day <= configuration['setting_on_time']:

      flight_meets_criteria = False
      if log:
        LogMessage(
            '%s not displayed because it occurs too early - minute_of_day: '
            '%d; setting_on_time: %d' % (
                DisplayFlightNumber(flight), minute_of_day,
                configuration['setting_on_time']))
    elif minute_of_day > configuration['setting_off_time'] + 1:
      flight_meets_criteria = False
      if log:
        LogMessage(
            '%s not displayed because it occurs too late - minute_of_day: '
            '%d; setting_off_time: %d' % (
                DisplayFlightNumber(flight), minute_of_day,
                configuration['setting_off_time']))
    elif configuration.get('setting_screen_enabled', 'off') == 'off':
      flight_meets_criteria = False
      if log:
        LogMessage(
            '%s not displayed because screen disabled' % DisplayFlightNumber(flight))

  return flight_meets_criteria


def IdentifyFlightDisplayed(flights, configuration, display_all_hours=False):
  """Finds the most recent flight in flights that meet the display criteria.

  Args:
    flights: list of flight dictionaries.
    configuration: dictionary of settings.
    display_all_hours: boolean indicating whether we should ignore the time constraints
      (i.e.: whether the screen is enabled, and its turn-on or turn-off times) in
      identifying the most recent flight. That is, if False, then this will only return
      flights that would have been displayed in the ordinarily usage, vs. if True,
      a flight irrespective of the time it would be displayed.

  Returns:
    A flight dictionary if one can be found; None otherwise.
  """
  for n in range(len(flights)-1, -1, -1):  # traverse the flights in reverse
    if FlightMeetsDisplayCriteria(
        flights[n], configuration, display_all_hours=display_all_hours):
      return n
  return None


def FlightMessageTestHarness(flights=None, display=True):


  """Simulates what flight messages might be displayed by replaying past flights."""
  if flights is None:
    flights = UnpickleObjectFromFile(PICKLE_FLIGHTS_30D)

  messages = []
  for flight in flights:

    flight_message = Screenify(CreateMessageAboutFlight(flight), False)
    if display:
      print(flight_message)
    messages.append(flight_message)

  return messages


def CreateMessageAboutFlight(flight):
  """Creates a message to describe interesting attributes about a single flight.

  Generates a multi-line description of a flight. A typical message might look like:
  UAL300 - UNITED        <- Flight number and airline




                            <----SKIPPED LINES---->




    lines.append(divider)

  return append_character.join(lines)


def FlightInsightsTestHarness(
    flights=None,
    display=True,
    flight_insights_enabled_string='all'):
  """Simulates what insightful messages might be displayed by replaying past flights."""
  if flights is None:
    flights = UnpickleObjectFromFile(PICKLE_FLIGHTS_30D)

  distribution = {}
  messages = []
  for (n, flight) in enumerate(flights):

    flight_message = CreateMessageAboutFlight(flight)
    if display:
      print('='*25)
      print(Screenify(flight_message, False))
    messages.append(flight_message)

    insights = CreateFlightInsights(
        flights[:n+1], flight_insights_enabled_string, distribution)


    FlightInsightNextFlight(flights[:n+1])




    if display:
      for insight in insights:
        print(Screenify(insight, False))
    messages.extend(insights)

  return messages


def FlightInsightLastSeen(flights, days_ago=2):
  """Generates string indicating when flight was last seen.

  Generates text of the following form.
  - KAL214 was last seen 2d0h ago

  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.
    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.




                            <----SKIPPED LINES---->




      elif same_airline:
        message += ', all with %s' % this_airline
      else:
        similar_flights_airlines.append(this_airline)
        similar_flights_airlines.sort()
        message += ', served by %s and %s' % (
            ', '.join(similar_flights_airlines[:-1]),
            similar_flights_airlines[-1])

  return message


def FlightInsightSuperlativeAttribute(
    flights,
    key,
    label,
    units,
    absolute_list,
    insight_min=True,
    insight_max=True,
    hours=HOURS_IN_DAY):
  """Generates string about a numeric attribute of the flight being an extreme value.

  Generates text of the following form for the "focus" flight in the data.
  - N5286C has the slowest groundspeed (113mph vs. 163mph) in last 24 hours
  - CKS828 has the highest altitude (40000ft vs. 16575ft) in last 24 hours

  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.
    key: the key of the attribute of interest - i.e.: 'speed'.
    label: the human-readable string that should be displayed in the message - i.e.:
        'groundspeed'.
    units: the string units that should be used to label the value of the key - i.e.:
        'MPH'.
    absolute_list: a 2-tuple of strings that is used to label the min and the max - i.e.:
        ('lowest', 'highest'), or ('slowest', 'fastest').
    insight_min: boolean indicating whether to generate an insight about the min value.
    insight_max: boolean indicating whether to generate an insight about the max value.




                            <----SKIPPED LINES---->




  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 because not enough history, then an
    empty string.
  """
  msg = ''
  # m = min of day of this flight
  # find minute of day of prior flights st
  # -- that flight not seen in last 12 hrs
  # -- that min of day >= this
  this_flight = flights[-1]
  this_hour = int(DisplayTime(this_flight, '%-H'))
  this_minute = int(DisplayTime(this_flight, '%-M'))
  this_date = DisplayTime(this_flight, '%x')



  # Flights that we've already seen in the last few hours we do not expect to see
  # again for another few hours, so let's exclude them from the calculation
  exclude_flights_hours = 12
  flight_numbers_seen_in_last_n_hours = [
      f['flight_number'] for f in flights
      if f['now'] > this_flight['now'] - exclude_flights_hours*SECONDS_IN_HOUR
      and 'flight_number' in f]
  still_to_come_flights = [
      f for f in flights[:-1]
      if f.get('flight_number') not in flight_numbers_seen_in_last_n_hours
      and this_date != DisplayTime(f, '%x')]

  # exclude flights that would be filtered out
  configuration = ReadAndParseSettings(CONFIG_FILE)
  still_to_come_flights = [
      f for f in still_to_come_flights if FlightMeetsDisplayCriteria(f, configuration)]
  minimum_minutes_next_flight = {}  # min minutes to next flight by day
  for flight in still_to_come_flights:
    date = DisplayTime(flight, '%x')
    hour = int(DisplayTime(flight, '%-H'))
    minutes = int(DisplayTime(flight, '%-M'))
    minutes_after = (hour - this_hour) * MINUTES_IN_HOUR +(minutes - this_minute)
    if minutes_after < 0:
      minutes_after += MINUTES_IN_DAY
    minimum_minutes_next_flight[date] = min(
        minimum_minutes_next_flight.get(date, minutes_after), minutes_after)

  minutes = list(minimum_minutes_next_flight.values())
  if len(minutes) > 1:
    average_seconds = (sum(minutes) / len(minutes)) * SECONDS_IN_MINUTE
    max_seconds = max(minutes) * SECONDS_IN_MINUTE

    median_seconds = statistics.median(minutes) * SECONDS_IN_MINUTE
    minimum_percent_diff = 0.5
    median_different = (
        median_seconds > average_seconds * (1 + minimum_percent_diff) or




                            <----SKIPPED LINES---->




               DisplayTime(this_flight, '%-I:%M%p'), SecondsToHhMm(average_seconds),
               median_text, SecondsToHhMm(max_seconds), len(minutes)))

  return msg


def PercentileScore(scores, value):
  """Returns the percentile that a particular value is in a list of numbers.

  Roughly inverts numpy.percentile. That is, numpy.percentile(scores_list, percentile)
  to get the value of the list that is at that percentile;
  PercentileScore(scores_list, value) will yield back approximately that percentile.

  If the value matches identical elements in the list, this function takes the average
  position of those identical values to compute a percentile. Thus, for some lists
  (i.e.: where there are lots of flights that have a 0 second delay, or a 100% delay
  frequency), you may not get a percentile of 0 or 100 even with values equal to the
  min or max element in the list.

  Args:
    scores: the list of numbers, including value.
    value: the value for which we want to determine the percentile.

  Returns:
    Returns an integer percentile in the range [0, 100] inclusive.
  """

  count_values_below_score = len([1 for s in scores if s < value])
  # -1 is because value is already in scores
  count_values_at_score = len([1 for s in scores if s == value]) - 1
  percentile = (count_values_below_score + count_values_at_score / 2) / len(scores)
  return round(percentile*100)


def FlightInsightGroupPercentile(
    flights,
    group_function,
    value_function,
    value_string_function,
    group_label,
    value_label,
    filter_function=lambda this, other: True,
    min_days=1,
    lookback_days=30,
    min_this_group_size=0,
    min_comparison_group_size=0,
    min_group_qty=0,
    percentile_low=float('-inf'),
    percentile_high=float('inf')):
  """Generates a string about extreme values of groups of flights.

  Generates text of the following form for the "focus" flight in the data.
  - flight SIA31 (n=7) has a delay frequency in the 95th %tile, with 100% of flights
    delayed an average of 6m over the last 4d1h
  - flight UAL300 (n=5) has a delay time in the 1st %tile, with an average delay of 0m
  over the last 4d5h

  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.
    group_function: function that, when called with a flight, returns the grouping key.
        That is, for example, group_function(flight) = 'B739'
    value_function: function that, when called with a list of flights, returns the
        value to be used for the comparison to identify min / max. Typically, the count,
        but could also be a sum, standard deviation, etc. - for perhaps the greatest
        range in flight altitude. If the group does not have a valid value and so
        should be excluded from comparison - i.e.: average delay of a group of flights
        which did not have a calculable_delay on any flight, this function should
        return None.
    value_string_function: function that, when called with the two parameters flights
        and value, returns a string (inclusive of units and label) that should be
        displayed to describe the quantity. For instance, if value_function returns
        seconds, value_string_function could convert that to a string '3h5m'. Or if
        value_function returns an altitude range, value_string_function could return
        a string 'altitude range of 900ft (1100ft - 2000ft)'.
    group_label: string to identify the group type - i.e.: 'aircraft' or 'flight' in
        the examples above.
    value_label: string to identify the value - i.e.: 'flights' in the examples above,
        but might also be i.e.: longest *delay*, or other quantity descriptor.
    filter_function: an optional function that, when called with the most recent flight
        and another flight filter_function(flights[-1], flight[n]), returns a value
        interpreted as a boolean indicating whether flight n should be included in
        determining the percentile.
    min_days: the minimum amount of history required to start generating insights
        about delays.
    lookback_days: the maximum amount of history which will be considered in generating
        insights about delays.
    min_this_group_size: even if this group has, say, the maximum average delay, if its
        a group of size 1, that is not necessarily very interesting. This sets the
        minimum group size for the focus flight.
    min_comparison_group_size: similarly, comparing the focus group to groups of size
        one does not necessarily produce a meaningful comparison; this sets to minimum
        size for the other groups.
    min_group_qty: when generating a percentile, if there are only 3 or 4 groups among
        which to generate a percentile (i.e.: only a handful of destinations have been
        seen so far, etc.) then it is not necessarily very interesting to generate a
        message; this sets the minimum quantity of groups necessary (including the
        focus group) to generate a message.
    percentile_low: number [0, 100] inclusive that indicates the percentile that the focus
        flight group must equal or be less than for the focus group to trigger an insight.
    percentile_high: number [0, 100] inclusive that indicates the percentile that the
        focus flight group must equal or be greater than for the focus group to trigger an
        insight.

  Returns:
    Printable string message; if no message or insights to generate, then an empty string.
  """
  debug = False
  message = ''
  this_flight = flights[-1]
  first_timestamp = flights[0]['now']
  last_timestamp = this_flight['now']
  included_seconds = last_timestamp - first_timestamp

  if (included_seconds > SECONDS_IN_DAY * min_days and
      group_function(this_flight) != KEY_NOT_PRESENT_STRING):

    relevant_flights = [
        f for f in flights if
        last_timestamp - f['now'] < SECONDS_IN_DAY * lookback_days and
        filter_function(this_flight, f)]

    grouped_flights = {}
    for flight in relevant_flights:
      group = group_function(flight)
      grouping = grouped_flights.get(group, [])
      grouping.append(flight)
      grouped_flights[group] = grouping
    # we will exclude "UNKNOWN" since that is not a coherent group
    if KEY_NOT_PRESENT_STRING in grouped_flights:
      grouped_flights.pop(KEY_NOT_PRESENT_STRING)

    grouped_values = {g: value_function(grouped_flights[g]) for g in grouped_flights}
    this_group = group_function(relevant_flights[-1])
    this_value = grouped_values[this_group]
    this_group_size = len(grouped_flights[this_group])

    # we will exclude groups that are not big enough
    grouped_flights = {
        k: grouped_flights[k] for k in grouped_flights
        if len(grouped_flights[k]) >= min_comparison_group_size or k == this_group}

    # Remove those for which no value could be calculated or which are too small
    grouped_values = {
        g: grouped_values[g] for g in grouped_values
        if grouped_values[g] is not None and g in grouped_flights}

    if debug:
      print()
      print('len(relevant_flights): %d' % len(relevant_flights))
      print('len(grouped_flights): %d' % len(grouped_flights))
      print('grouped_flights.keys(): %s' % sorted(list(grouped_flights.keys())))
      for key in sorted(list(grouped_flights.keys())):
        print('  len(grouped_flights[%s]) = %d' % (key, len(grouped_flights[key])))

    if this_value is not None and len(grouped_values) >= min_group_qty:

      time_horizon_string = ' over the last %s' % SecondsToDdHh(
          last_timestamp - relevant_flights[0]['now'])
      min_comparison_group_size_string = ''
      if min_comparison_group_size > 1:
        min_comparison_group_size_string = ' amongst those with >%d flights' % (
            min_comparison_group_size - 1)

      # FLIGHT X (n=7) is has the Xth percentile of DELAYS, with an average delay of
      # 80 MINUTES
      this_percentile = PercentileScore(grouped_values.values(), this_value)
      if this_group_size >= min_this_group_size and (
          this_percentile <= percentile_low or this_percentile >= percentile_high):

        if debug:
          print('Comparison cohorts for %s (%s)' % (group_label, str(this_group)))
          print('This percentile: %f; min: %f; max: %f' % (
              this_percentile, percentile_low, percentile_high))
          keys = list(grouped_values.keys())
          values = [grouped_values[k] for k in keys]
          print(keys)
          print(values)
          (values, keys) = SortByValues(values, keys)
          for n, value in enumerate(values):
            print('%s: %f (group size: %d)' % (
                keys[n], value, len(grouped_flights[keys[n]])))

        if group_label:
          group_label += ' '

        def TrialMessage():
          message = '%s%s (n=%d) has a %s in the %s %%tile, with %s%s%s' % (
              group_label,
              this_group,
              this_group_size,
              value_label,
              Ordinal(this_percentile),
              value_string_function(grouped_flights[this_group], this_value),
              time_horizon_string,
              min_comparison_group_size_string)
          line_count = len(textwrap.wrap(message, width=SPLITFLAP_CHARS_PER_LINE))
          return (line_count, message)

        (line_count, message) = TrialMessage()
        if line_count > SPLITFLAP_LINE_COUNT:
          min_comparison_group_size_string = ''
          (line_count, message) = TrialMessage()
          if line_count > SPLITFLAP_LINE_COUNT:
            time_horizon_string = ''
            (line_count, message) = TrialMessage()

      elif debug:
        print('Not an outlying group because A and either B or C needed to be true:')
        if not this_group_size >= min_this_group_size:
          print('A this_group_size %d >= min_this_group_size %d' % (
              this_group_size, min_this_group_size))
        else:
          print('A passed')
          if not this_percentile <= percentile_low:
            print('B this_percentile %d <= percentile_low %d' % (
                this_percentile, percentile_low))
          if not this_percentile >= percentile_high:
            print('C this_percentile %d >= percentile_high %d' % (
                this_percentile, percentile_high))

    elif debug:
      print('Not an outlying group because A or B failed:')
      if this_value is None:
        print('A this_value %s' % str(this_value))
      elif len(grouped_values) < min_group_qty:
        print('A passed')
        print('B len(grouped_values) %d >= min_group_qty %d' % (
            len(grouped_values), min_group_qty))
      print('grouped_values: %s' % grouped_values)

  return message


def FlightInsightSuperlativeGroup(
    flights,
    group_function,
    value_function,
    value_string_function,
    group_label,
    value_label,
    absolute_list,
    min_days=1,
    lookback_days=30,
    min_this_group_size=0,
    min_comparison_group_size=0,
    insight_min=True,
    insight_max=True):
  """Generates a string about extreme values of groups of flights.





                            <----SKIPPED LINES---->




    if matching:
      last_potential_observation_sec = last_timestamp - matching[-1]['now']

    if this_instance and last_potential_observation_sec > SECONDS_IN_DAY * days:
      additional_descriptor = ''
      if additional_descriptor_fcn:
        additional_descriptor = ' (%s)' % additional_descriptor_fcn(this_flight)
      last_potential_observation_string = SecondsToDdHh(last_potential_observation_sec)
      if matching:
        message = '%s is the first time %s %s%s has been seen since %s ago' % (
            this_flight_number, label, this_instance, additional_descriptor,
            last_potential_observation_string)
      else:
        message = '%s is the first time %s %s%s has been seen since at least %s ago' % (
            this_flight_number, label, this_instance, additional_descriptor,
            last_potential_observation_string)

  return message


def FlightInsightSuperlativeVertrate(flights, hours=HOURS_IN_DAY):
  """Generates string about the climb rate of the flight being an extreme value.

  Generates text of the following form for the "focus" flight in the data.
  - UAL631   has the fastest ascent rate (5248fpm, 64fpm faster than next fastest) in
    last 24 hours
  - CKS1820 has the fastest descent rate (-1152fpm, -1088fpm faster than next fastest)
    in last 24 hours

  While this is conceptually similar to the more generic FlightInsightSuperlativeVertrate
  function, vert_rate - because it can be either positive or negative, with different
  signs requiring different labeling and comparisons - it needs its own special handling.

  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.
    hours: the time horizon over which to look for superlative flights.

  Returns:




                            <----SKIPPED LINES---->





  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.
  AppendMessageType(FLAG_INSIGHT_FIRST_DEST, FlightInsightFirstInstance(
      flights, 'destination_iata', 'destination', days=7,
      additional_descriptor_fcn=lambda f: f['destination_friendly']))
  AppendMessageType(FLAG_INSIGHT_FIRST_ORIGIN, FlightInsightFirstInstance(
      flights, 'origin_iata', 'origin', days=7,
      additional_descriptor_fcn=lambda f: f['origin_friendly']))
  AppendMessageType(FLAG_INSIGHT_FIRST_AIRLINE, FlightInsightFirstInstance(
      flights, 'airline_short_name', 'airline', days=7))
  AppendMessageType(FLAG_INSIGHT_FIRST_AIRCRAFT, FlightInsightFirstInstance(
      flights, 'aircraft_type_code', 'aircraft', days=7,
      additional_descriptor_fcn=lambda f: f['aircraft_type_friendly']))

  # This is the longest / shortest delay this flight has seen in the last 30 days at
  # 2h5m; including today, this flight has been delayed x of the last y times.
  AppendMessageType(FLAG_INSIGHT_LONGEST_DELAY, FlightInsightDelays(
      flights, min_late_percentage=0.75,
      min_this_delay_minutes=0,
      min_average_delay_minutes=0))

  def DelayTimeAndFrequencyMessage(
      types_tuple,
      group_function,
      group_label,
      filter_function=lambda this, other: True,
      min_days=1,
      lookback_days=30,
      min_this_group_size=0,
      min_comparison_group_size=0,
      min_group_qty=0,
      percentile_low=float('-inf'),
      percentile_high=float('inf')):
    value_function_tuple = (PercentDelay, AverageDelay)
    value_string_function_tuple = (
        lambda flights, value: '%d%% of flights delayed an average of %s' % (
            round(value*100), SecondsToHhMm(AverageDelay(flights))),
        lambda flights, value: 'average delay of %s' % SecondsToHhMm(value))
    value_label_tuple = ('delay frequency', 'delay time')
    for n in range(2):
      if types_tuple[n]:
        AppendMessageType(types_tuple[n], FlightInsightGroupPercentile(
            flights,
            group_function=group_function,
            value_function=value_function_tuple[n],
            value_string_function=value_string_function_tuple[n],
            group_label=group_label,
            value_label=value_label_tuple[n],
            filter_function=filter_function,
            min_days=min_days,
            min_this_group_size=min_this_group_size,
            min_comparison_group_size=min_comparison_group_size,
            min_group_qty=min_group_qty,
            lookback_days=lookback_days,
            percentile_low=percentile_low,
            percentile_high=percentile_high))

  # flight UAL1 (n=5) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (FLAG_INSIGHT_FLIGHT_DELAY_FREQUENCY, FLAG_INSIGHT_FLIGHT_DELAY_TIME),
      group_function=lambda flight: flight.get('flight_number', KEY_NOT_PRESENT_STRING),
      group_label='flight',
      min_days=1,
      min_this_group_size=4,
      min_comparison_group_size=0,
      min_group_qty=0,

      percentile_low=10,
      percentile_high=90)

  # Airline United (n=5) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (FLAG_INSIGHT_AIRLINE_DELAY_FREQUENCY, FLAG_INSIGHT_AIRLINE_DELAY_TIME),
      group_function=DisplayAirline,
      group_label='airline',
      min_days=1,
      min_this_group_size=10,
      min_comparison_group_size=5,
      min_group_qty=5,

      percentile_low=10,
      percentile_high=80)

  # Destination LAX (n=5) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (FLAG_INSIGHT_DESTINATION_DELAY_FREQUENCY, FLAG_INSIGHT_DESTINATION_DELAY_TIME),
      group_function=DisplayDestinationFriendly,
      group_label='destination',
      min_days=1,
      min_this_group_size=10,
      min_comparison_group_size=5,
      min_group_qty=5,

      percentile_low=10,
      percentile_high=90)

  # we only want to do this if we're already at ~75% of the number of flights we'd
  # expect to see for the hour
  flight_hours = {}
  for flight in flights:
    if flights[-1]['now'] - flight['now'] < 8.5 * SECONDS_IN_DAY and DisplayTime(
        flight, '%-I%p') == DisplayTime(flights[-1], '%-I%p'):
      flight_hours[DisplayTime(flight, '%-d')] = flight_hours.get(
          DisplayTime(flight, '%-d'), 0) + 1
  min_this_hour_flights = max(3, 0.75 * max(flight_hours.values()))

  # Once we've commented on the insights for an hour or day, we don't want to do it again
  hour_delay_frequency_flag = FLAG_INSIGHT_HOUR_DELAY_FREQUENCY
  hour_delay_time_flag = FLAG_INSIGHT_HOUR_DELAY_TIME
  date_delay_frequency_flag = FLAG_INSIGHT_DATE_DELAY_FREQUENCY
  date_delay_time_flag = FLAG_INSIGHT_DATE_DELAY_TIME
  for flight in flights[:-1]:
    insights = flight['insight_types']
    this_hour = DisplayTime(flights[-1], '%x %-I%p')
    this_day = DisplayTime(flights[-1], '%x')
    if (this_hour == DisplayTime(flight, '%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_FREQUENCY in insights):
      hour_delay_frequency_flag = None
    if (this_hour == DisplayTime(flight, '%x %-I%p') and
        FLAG_INSIGHT_HOUR_DELAY_TIME in insights):
      hour_delay_time_flag = None
    if (this_day == DisplayTime(flight, '%x') and
        FLAG_INSIGHT_DATE_DELAY_FREQUENCY in insights):
      date_delay_frequency_flag = None
    if (this_day == DisplayTime(flight, '%x') and
        FLAG_INSIGHT_DATE_DELAY_TIME in insights):
      date_delay_time_flag = None

  def TodaysHour(f):
    f_date = DisplayTime(f, '%x')
    f_hour = DisplayTime(f, '%-I%p')
    if f_date == DisplayTime(flights[-1], '%x'):
      return '%s flights today' % f_hour
    return '%s %s' % (f_date, f_hour)

  # Today's 7a flights have a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (hour_delay_frequency_flag, hour_delay_time_flag),
      group_function=TodaysHour,
      group_label='',
      filter_function=lambda this, other:
      DisplayTime(this, '%-I%p') == DisplayTime(other, '%-I%p'),
      min_days=3,
      min_this_group_size=min_this_hour_flights,
      min_comparison_group_size=min_this_hour_flights,
      min_group_qty=5,

      percentile_low=10,
      percentile_high=90)

  # we only want to do this if we're already at ~75% of the number of flights we'd
  # expect to see for the day
  flight_days = {}
  for flight in flights:
    if flights[-1]['now'] - flight['now'] < 8.5 * SECONDS_IN_DAY:
      flight_days[DisplayTime(flight, '%-d')] = flight_days.get(
          DisplayTime(flight, '%-d'), 0) + 1
  min_this_day_flights = max(40, 0.75 * max(flight_days.values()))

  # Today (31st) has a delay frequency in the 72nd %tile, with 100% of flights
  # delayed an average of 44m over the last 4d13h
  DelayTimeAndFrequencyMessage(
      (date_delay_frequency_flag, date_delay_time_flag),
      group_function=lambda f:
      '(' + Ordinal(int(DisplayTime(f, '%-d'))) + ')',
      group_label='Today',
      min_days=7,
      min_this_group_size=min_this_day_flights,
      min_comparison_group_size=min_this_day_flights,
      min_group_qty=7,
      lookback_days=28,  # Otherwise, there might be two 1st's of the month to compare
      percentile_low=10,
      percentile_high=90)

  messages = [
      (t, textwrap.wrap(m, width=SPLITFLAP_CHARS_PER_LINE))
      for (t, m) in messages]

  return messages


def CreateFlightInsights(
    flights, flight_insights_enabled_string, insight_message_distribution):
  """Returns the desired quantity of flight insight messages.

  Though the function FlightInsights generates all possible insight messages about a
  flight, the user may have only wanted one. Depending on the setting of
  flight_insights_enabled_string, this function reduces the set of all insights by
  selecting the least-frequently reported type of insight message.

  In order to choose the least-frequently reported type, we need to keep track of what




                            <----SKIPPED LINES---->




    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 the
        restrictions of truncate.

  Returns:
    2-tuple of lists cut and sorted as indicated by parameters above:
    - list of values (or frequency) of the histogram elements
    - list of keys (or labels) of the histogram elements
  """
  histogram_dict = {}
  filtered_data = []

  def IfNoneReturnInf(f, key):
    value = f.get(key)
    if not value:
      value = float('inf')
    return value

  # get timezone & now so that we can generate a timestamp for comparison just once
  if hours:
    now = datetime.datetime.now(TZ)
  for element in data:
    if (
        IfNoneReturnInf(element, 'min_feet') <= max_distance_feet and
        IfNoneReturnInf(element, 'altitude') <= max_altitude_feet and
        HoursSinceFlight(now, element['now']) <= hours):
      filtered_data.append(element)
      key = keyfunction(element)
      if key is None or key == '':
        key = KEY_NOT_PRESENT_STRING
      if key in histogram_dict:
        histogram_dict[key] += 1
      else:
        histogram_dict[key] = 1
  values = list(histogram_dict.values())
  keys = list(histogram_dict.keys())

  if normalize_factor:
    values = [v / normalize_factor for v in values]

  sort_by_enumerated_list = isinstance(sort_type, list)
  if exhaustive and sort_by_enumerated_list:
    missing_keys = set(sort_type).difference(set(keys))
    missing_values = [0 for unused_k in missing_keys]
    keys.extend(missing_keys)




                            <----SKIPPED LINES---->




  matplotlib.pyplot.subplots_adjust(bottom=0.15, left=0.09, right=0.99, top=0.92)

  matplotlib.pyplot.xticks(
      values_coordinates, keys, rotation='vertical', wrap=True,
      horizontalalignment='right',
      verticalalignment='center')


def HistogramSettingsHours(how_much_history):
  """Extracts the desired history (in hours) from the histogram configuration string.

  Args:
    how_much_history: string from the histogram config file.

  Returns:
    Number of hours of history to include in the histogram.
  """
  if how_much_history == 'today':
    hours = HoursSinceMidnight()
  elif how_much_history == '24h':
    hours = HOURS_IN_DAY
  elif how_much_history == '7d':
    hours = 7 * HOURS_IN_DAY
  elif how_much_history == '30d':
    hours = 30 * HOURS_IN_DAY
  else:
    LogMessage(
        'Histogram form has invalid value for how_much_history: %s' % how_much_history)
    hours = 7 * HOURS_IN_DAY
  return hours


def HistogramSettingsScreens(max_screens):
  """Extracts the desired number of text screens from the histogram configuration string.

  Args:
    max_screens: string from the histogram config file.

  Returns:
    Number of maximum number of screens to display for a splitflap histogram.
  """
  if max_screens == '_1':
    screen_limit = 1
  elif max_screens == '_2':
    screen_limit = 2
  elif max_screens == '_5':
    screen_limit = 5
  elif max_screens == 'all':
    screen_limit = 0  # no limit on screens
  else:
    LogMessage('Histogram form has invalid value for max_screens: %s' % max_screens)
    screen_limit = 1
  return screen_limit


def HistogramSettingsKeySortTitle(which, hours, max_altitude=45000):
  """Provides the arguments necessary to generate a histogram from the config string.

  The same parameters are used to generate either a splitflap text or web-rendered
  histogram in terms of the histogram title, the keyfunction, and how to sort the keys.
  For a given histogram name (based on the names defined in the histogram config file),
  this provides those parameters.

  Args:
    which: string from the histogram config file indicating the histogram to provide
        settings for.
    hours: how many hours of histogram data have been requested.
    max_altitude: indicates the maximum altitude that should be included on the
        altitude labels.

  Returns:
    A 4-tuple of the parameters used by either CreateSingleHistogramChart or
    MessageboardHistogram, of the keyfunction, sort, title, and hours.
  """
  def DivideAndFormat(dividend, divisor):
    if dividend is None:
      return KEY_NOT_PRESENT_STRING
    if isinstance(dividend, numbers.Number):
      return '%2d' % round(dividend / divisor)
    return dividend[:2]

  if which == 'destination':
    key = lambda k: k.get('destination_iata', KEY_NOT_PRESENT_STRING)
    sort = 'value'
    title = 'Destination'
  elif which == 'origin':
    key = lambda k: k.get('origin_iata', KEY_NOT_PRESENT_STRING)
    sort = 'value'
    title = 'Origin'
  elif which == 'hour':
    key = lambda k: DisplayTime(k, '%H')
    sort = 'key'
    title = 'Hour'
  elif which == 'airline':
    key = DisplayAirline
    sort = 'value'
    title = 'Airline'




                            <----SKIPPED LINES---->




    key = lambda k: k.get('aircraft_type_code', KEY_NOT_PRESENT_STRING)
    sort = 'value'
    title = 'Aircraft'
  elif which == 'altitude':
    key = lambda k: DivideAndFormat(k.get('altitude', KEY_NOT_PRESENT_STRING), 1000)
    sort = ['%2d'%x for x in range(0, round((max_altitude+1)/1000))]
    title = 'Altitude (1000ft)'
  elif which == 'bearing':
    key = lambda k: ConvertBearingToCompassDirection(
        k.get('track', KEY_NOT_PRESENT_STRING), pad=True, length=3)
    sort = [d.rjust(3) for d in DIRECTIONS_16]
    title = 'Bearing'
  elif which == 'distance':
    key = lambda k: DivideAndFormat(k.get('min_feet', KEY_NOT_PRESENT_STRING), 100)
    sort = ['%2d'%x for x in range(0, round((MIN_METERS*FEET_IN_METER)/100)+1)]
    title = 'Min Dist (100ft)'
  elif which == 'day_of_week':
    key = lambda k: DisplayTime(k, '%a')
    sort = DAYS_OF_WEEK
    title = 'Day of Week'
    # if less than one week, as requested; if more than one week, in full week multiples
    hours_in_week = 7 * HOURS_IN_DAY
    weeks = hours / hours_in_week
    if weeks > 1:
      hours = hours_in_week * int(hours / hours_in_week)
  elif which == 'day_of_month':
    key = lambda k: DisplayTime(k, '%-d').rjust(2)
    today_day = datetime.datetime.now(TZ).day
    days = list(range(today_day, 0, -1))  # today down to the first of the month
    days.extend(range(31, today_day, -1))  # 31st of the month down to day after today
    days = [str(d).rjust(2) for d in days]
    sort = days
    title = 'Day of Month'
  else:
    LogMessage(
        'Histogram form has invalid value for which_histograms: %s' % which)
    return HistogramSettingsKeySortTitle(
        'destination', hours, max_altitude=max_altitude)

  return (key, sort, title, hours)


def ImageHistograms(
    flights,
    which_histograms,
    how_much_history,
    filename_prefix=HISTOGRAM_IMAGE_PREFIX,
    filename_suffix=HISTOGRAM_IMAGE_SUFFIX):
  """Generates multiple split histogram images.

  Args:
    flights: 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.
    which_histograms: string paramater indicating which histogram(s) to generate, which
        can be either the special string 'all', or a string linked to a specific
        histogram.
    how_much_history: string parameter taking a value among ['today', '24h', '7d', '30d].
    filename_prefix: this string indicates the file path and name prefix for the images




                            <----SKIPPED LINES---->




    histograms_to_generate.append({'generate': 'hour'})
  if which_histograms in ['airline', 'all']:
    histograms_to_generate.append({'generate': 'airline', 'truncate': int(TRUNCATE/2)})
  if which_histograms in ['aircraft', 'all']:
    histograms_to_generate.append({'generate': 'aircraft'})
  if which_histograms in ['altitude', 'all']:
    histograms_to_generate.append({'generate': 'altitude', 'exhaustive': True})
  if which_histograms in ['bearing', 'all']:
    histograms_to_generate.append({'generate': 'bearing'})
  if which_histograms in ['distance', 'all']:
    histograms_to_generate.append({'generate': 'distance', 'exhaustive': True})
  if which_histograms in ['day_of_week', 'all']:
    histograms_to_generate.append({'generate': 'day_of_week'})
  if which_histograms in ['day_of_month', 'all']:
    histograms_to_generate.append({'generate': 'day_of_month'})

  for histogram in histograms_to_generate:
    this_histogram = which_histograms
    if this_histogram == 'all':
      this_histogram = histogram['generate']
    (key, sort, title, hours) = HistogramSettingsKeySortTitle(this_histogram, hours)

    CreateSingleHistogramChart(
        flights,
        key,
        sort,
        title,
        truncate=histogram.get('truncate', TRUNCATE),
        hours=hours,
        exhaustive=histogram.get('exhaustive', False))
    filename = filename_prefix + histogram['generate'] + '.' + filename_suffix
    matplotlib.pyplot.savefig(filename)
    matplotlib.pyplot.close()

  histograms_generated = [h['generate'] for h in histograms_to_generate]
  return histograms_generated


def MessageboardHistograms(
    flights,
    which_histograms,




                            <----SKIPPED LINES---->




        'columns': 3})
  if ((which_histograms == 'all' and how_much_history == '7d')
      or which_histograms == 'day_of_week'):
    histograms_to_generate.append({
        'generate': 'day_of_week',
        'columns': 3,
        'absolute': True})
  if ((which_histograms == 'all' and how_much_history == '30d')
      or which_histograms == 'day_of_month'):
    histograms_to_generate.append({
        'generate': 'day_of_month',
        'columns': 3,
        'suppress_percent_sign': True,
        'column_divider': '|',
        'absolute': True})

  for histogram in histograms_to_generate:
    this_histogram = which_histograms
    if this_histogram == 'all':
      this_histogram = histogram['generate']
    (key, sort, title, hours) = HistogramSettingsKeySortTitle(this_histogram, hours)

    histogram = MessageboardHistogram(
        flights,
        key,
        sort,
        title,
        screen_limit=screen_limit,
        columns=histogram.get('columns', 2),
        suppress_percent_sign=histogram.get('suppress_percent_sign', False),
        column_divider=histogram.get('column_divider', ' '),
        data_summary=data_summary,
        hours=hours,
        absolute=histogram.get('absolute', False))
    messages.extend(histogram)

  return messages


def MessageboardHistogram(
    data,




                            <----SKIPPED LINES---->




    start_index = screen*available_entries_per_screen
    end_index = min((screen+1)*available_entries_per_screen-1, len(keys)-1)
    number_of_entries = end_index - start_index + 1
    number_of_lines = math.ceil(number_of_entries / columns)

    lines = []

    lines.append(screen_title.upper())
    if data_summary:
      lines.append(summary_text.upper())
    for line_index in range(number_of_lines):
      key_value = []
      for column_index in range(columns):
        index = start_index + column_index*number_of_lines + line_index
        if index <= end_index:
          if absolute:
            value_string = format_string % values[index]
          else:
            # If the % is >=1%, display right-justified 2 digit percent, i.e. ' 5%'
            # Otherwise, if it rounds to at least 0.1%, display i.e. '.5%'
            if values[index]/total*100 >= 0.95:
              value_string = '%2d' % round(values[index]/total*100)
            elif round(values[index]/total*1000)/10 >= 0.1:
              value_string = ('%.1f' % (round(values[index]/total*1000)/10))[1:]
            else:
              value_string = ' 0'
          key_value.append('%s %s%s' % (
              str(keys[index])[:column_key_width].ljust(column_key_width),
              value_string,
              printed_percent_sign))

      line = (column_divider.join(key_value)).upper()
      lines.append(line)

    split_flap_boards.append(lines)

  return split_flap_boards


def TriggerHistograms(flights, histogram_settings):
  """Triggers the text-based or web-based histograms.




                            <----SKIPPED LINES---->




    d['now'] = flight['now']

    requested_time = time.time()
    requested_elapsed_seconds = requested_time - flight['now']
    updated_loc = ClosestKnownLocation(flight, requested_elapsed_seconds)
    actual_elapsed_seconds = requested_elapsed_seconds - updated_loc[1]
    d['speed'] = updated_loc[0]['speed']
    d['lat'] = updated_loc[0]['lat']
    d['lon'] = updated_loc[0]['lon']
    d['track'] = updated_loc[0]['track']
    d['altitude'] = updated_loc[0]['altitude']
    d['vertrate'] = updated_loc[0]['vertrate']
    d['flight_loc_now'] = flight['now'] + actual_elapsed_seconds

  today = datetime.datetime.now(TZ).strftime('%x')
  flight_count_today = len([1 for f in flights if DisplayTime(f, '%x') == today])

  d['flight_count_today'] = flight_count_today

  settings_string = BuildSettings(d)
  if not SIMULATION:
    if os.path.exists(ARDUINO_FILE):
      existing_data = ReadAndParseSettings(ARDUINO_FILE)
      if d != existing_data:
        WriteFile(ARDUINO_FILE, settings_string)
    else:
      WriteFile(ARDUINO_FILE, settings_string)



  return settings_string


def PublishMessage(
    s,
    subscription_id='12fd73cd-75ef-4cae-bbbf-29b2678692c1',
    key='c5f62d44-e30d-4c43-a43e-d4f65f4eb399',
    secret='b00aeb24-72f3-467c-aad2-82ba5e5266ca',
    timeout=3):
  """Publishes a text string to a Vestaboard.

  The message is pushed to the vestaboard splitflap display by way of its web services;
  see https://docs.vestaboard.com/introduction for more details.

  Args:
    s: String to publish.
    subscription_id: string subscription id from Vestaboard.
    key: string key from Vestaboard.
    secret: string secret from Vestaboard.




                            <----SKIPPED LINES---->




    status_code = curl.getinfo(pycurl.RESPONSE_CODE)
    if status_code != 200:
      LogMessage('Server returned HTTP status code %d for message %s' % (status_code, s))

  curl.close()


def ManageMessageQueue(message_queue, next_message_time, configuration):
  """Check time & if appropriate, display next message from queue.

  Args:
    message_queue: FIFO list of message tuples of (message type, message string).
    next_message_time: epoch at which next message should be displayed
    configuration: dictionary of configuration attributes.

  Returns:
    Next_message_time, potentially updated if a message has been displayed, or unchanged
    if no message was displayed.
  """
  if message_queue and (time.time() >= next_message_time or SIMULATION):

    if SIMULATION:  # drain the queue because the messages come so fast
      messages_to_display = list(message_queue)
      # passed by reference, so clear it out since we drained it to the display
      del message_queue[:]
    else:  # display only one message, being mindful of the display timing
      messages_to_display = [message_queue.pop(0)]

    for message in messages_to_display:
      message_text = message[1]
      if isinstance(message_text, str):
        message_text = textwrap.wrap(message_text, width=SPLITFLAP_CHARS_PER_LINE)
      display_message = Screenify(message_text, False)
      LogMessage(display_message, file=ALL_MESSAGE_FILE)
      MaintainRollingWebLog(display_message, 25)
      if not SIMULATION:
        splitflap_message = Screenify(message_text, True)
        PublishMessage(splitflap_message)

    next_message_time = time.time() + configuration['setting_delay']
  return next_message_time




                            <----SKIPPED LINES---->




    tmp_filename = filename + 'tmp'

    if os.path.exists(tmp_filename):
      os.remove(tmp_filename)

    if os.path.exists(filename):
      mtime = os.path.getmtime(filename)
      flights = UnpickleObjectFromFile(filename)
      for (n, flight) in enumerate(flights):
        if n/25 == int(n/25):
          print(' - %d' % n)
        CreateFlightInsights(flights[:n+1], configuration.get('insights', 'hide'), {})
        PickleObjectToFile(flight, tmp_filename)

      if mtime == os.path.getmtime(filename):
        shutil.move(tmp_filename, filename)
      else:
        print('Failed to bootstrap %s: file changed while in process' % filename)


def ResetLogs(config):
  """Clears the non-scrolling logs if reset_logs in config."""
  if 'reset_logs' in config:
    LogMessage('Reset logs')
    if os.path.exists(STDERR_FILE):
      os.remove(STDERR_FILE)
      LogMessage('', STDERR_FILE)
    if os.path.exists(BACKUP_FILE):
      os.remove(BACKUP_FILE)
      open(BACKUP_FILE, 'a').close()
    if os.path.exists(SERVICE_VERIFICATION_FILE):
      os.remove(SERVICE_VERIFICATION_FILE)
      open(SERVICE_VERIFICATION_FILE, 'a').close()
    config.pop('reset_logs')
    config = BuildSettings(config)
    WriteFile(CONFIG_FILE, config)
  return config


def main():
  """Traffic cop between incoming radio flight messages, configuration, and messageboard.

  This is the main logic, checking for new flights, augmenting the radio signal with
  additional web-scraped data, and generating messages in a form presentable to the
  messageboard.
  """
  already_running_id = CheckIfProcessRunning()
  if already_running_id:
    os.kill(already_running_id, signal.SIGKILL)

  LogMessage('Starting up')
  if '-s' in sys.argv:
    global SIMULATION_COUNTER
    SimulationSetup()

  # Redirect any errors to a log file instead of the screen, and add a datestamp
  if not SIMULATION:
    sys.stderr = open(STDERR_FILE, 'a')
    sys.stdout = open(STDERR_FILE, 'a')
    LogMessage('', STDERR_FILE)

  configuration = ReadAndParseSettings(CONFIG_FILE)
  last_distance = configuration.get('distance')
  last_altitude = configuration.get('altitude')
  startup_time = time.time()
  json_desc_dict = {}

  # If we're displaying just a single insight message, we want it to be something
  # unique, to the extent possible; this dict holds a count of the diff types of messages
  # displayed so far
  insight_message_distribution = {}

  flights = []
  if os.path.exists(PICKLE_FLIGHTS_30D):
    flights = TruncatePickledDictionaries(PICKLE_FLIGHTS_30D)

    # Clear the loaded flight of any cached data since code fixes may change
    # the values for some of those cached elements
    for flight in flights:
      for key in list(flight.keys()):




                            <----SKIPPED LINES---->




        CreateFlightInsights(
            flights[:n+1],
            configuration.get('insights', 'hide'),
            insight_message_distribution)

  # used in simulation to print the hour of simulation once per simulated hour
  prev_simulated_hour = ''

  persistent_nearby_aircraft = {} # key = flight number; value = last seen
  persistent_path = {}
  histogram = {}

  # Next up to print is element 0; this is a list of tuples:
  # Element#1: flag indicating the type of message that this is
  # Element#2: the message itself
  message_queue = []
  next_message_time = time.time()

  # We repeat the loop every x seconds; this ensures that if the processing time is long,
  # we don't wait another x seconds after processing completes
  next_loop_time = time.time() + LOOP_DELAY_SECONDS

  # These files are read only if the version on disk has been modified more recently
  # than the last time it was read
  last_dump_json_timestamp = 0

  LogMessage('Finishing initialization; starting radio polling loop')
  while not SIMULATION or SIMULATION_COUNTER < len(DUMP_JSONS):

    new_configuration = ReadAndParseSettings(CONFIG_FILE)

    n_c = new_configuration
    c = configuration
    if (n_c.get('setting_max_distance') != c.get('setting_max_distance') or
        n_c.get('setting_max_altitude') != c.get('setting_max_altitude')):
      last_distance = configuration.get('setting_max_distance')
      last_altitude = configuration.get('setting_max_altitude')
      FlightCriteriaHistogramPng(
          flights,
          new_configuration['setting_max_distance'],
          new_configuration['setting_max_altitude'],
          7,
          last_max_distance_feet=last_distance,
          last_max_altitude_feet=last_altitude)

    if (n_c.get('setting_max_distance') != c.get('setting_max_distance') or
        n_c.get('setting_max_altitude') != c.get('setting_max_altitude') or
        n_c.get('setting_off_time') != c.get('setting_off_time') or
        n_c.get('setting_on_time') != c.get('setting_on_time')):
      next_flight_message = FlightInsightNextFlight(flights)
      if next_flight_message:
        message_queue.append((FLAG_MSG_INTERESTING, next_flight_message))

    configuration = new_configuration

    ResetLogs(configuration)  # clear the logs if requested

    # if this is a SIMULATION, then process every diff dump. But if it isn't a simulation,
    # then only read & do related processing for the next dump if the last-modified
    # timestamp indicates the file has been updated since it was last read.
    tmp_timestamp = 0
    if not SIMULATION:
      tmp_timestamp = os.path.getmtime(DUMP_JSON_FILE)
    if (SIMULATION and DumpJsonChanges()) or (
        not SIMULATION and tmp_timestamp > last_dump_json_timestamp):
      last_dump_json_timestamp = tmp_timestamp

      # a command might request info about flight to be (re)displayed, irrespective of
      # whether the screen is on; if so, let's put that message at the front of the message
      # queue, and delete any subsequent messages in queue because presumably the button
      # was pushed either a) when the screen was off (so no messages in queue), or b)
      # because the screen was on, but the last flight details got lost after other screens
      if os.path.exists(LAST_FLIGHT_FILE):
        messageboard_flight_index = IdentifyFlightDisplayed(
            flights, configuration, display_all_hours=True)
        if messageboard_flight_index is not None:
          message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]
          flight_message = CreateMessageAboutFlight(flights[messageboard_flight_index])
          message_queue = [FLAG_MSG_FLIGHT, flight_message]
          next_message_time = time.time()
        os.remove(LAST_FLIGHT_FILE)

      (persistent_nearby_aircraft,

       flight, now,
       json_desc_dict,
       persistent_path) = ScanForNewFlights(persistent_nearby_aircraft, persistent_path)

      if flight:
        flights.append(flight)
        UpdateArduinoFile(
            flights, json_desc_dict)

        flight_meets_display_criteria = FlightMeetsDisplayCriteria(
            flight, configuration, log=True)
        if flight_meets_display_criteria:
          flight_message = (FLAG_MSG_FLIGHT, CreateMessageAboutFlight(flight))

          # display the next message about this flight now!
          next_message_time = time.time()
          message_queue.insert(0, flight_message)
          # and delete any queued insight messages about other flights that have
          # not yet displayed, since a newer flight has taken precedence
          messages_to_delete = [m for m in message_queue if m[0] == FLAG_MSG_INTERESTING]
          if messages_to_delete:
            LogMessage(
                'Deleting messages from queue due to new-found plane: %s' %
                messages_to_delete)
          message_queue = [m for m in message_queue if m[0] != FLAG_MSG_INTERESTING]

          # Though we also manage the message queue outside this conditional as well,
          # because it can take a half second to generate the flight insights, this allows
          # this message to start displaying on the board immediately, so it's up there
          # when it's most relevant
          next_message_time = ManageMessageQueue(




                            <----SKIPPED LINES---->




            next_flight_text = FlightInsightNextFlight(flights)
            if next_flight_text:
              insight_messages.insert(0, next_flight_text)

          insight_messages = [(FLAG_MSG_INTERESTING, m) for m in insight_messages]

          for insight_message in insight_messages:
            message_queue.insert(0, insight_message)

        else:  # flight didn't meet display criteria
          flight['insight_types'] = []

        PickleObjectToFile(flight, PICKLE_FLIGHTS_30D)
        PickleObjectToFile(flight, PICKLE_FLIGHTS_ARCHIVE)

      else:
        UpdateArduinoFile(flights, json_desc_dict)

    if SIMULATION:
      if now:
        simulated_hour = EpochDisplayTime(now, '%Y-%m-%d %H:00%z')
      if simulated_hour != prev_simulated_hour:
        print(simulated_hour)
        prev_simulated_hour = simulated_hour

    histogram = ReadAndParseSettings(HISTOGRAM_CONFIG_FILE)
    if os.path.exists(HISTOGRAM_CONFIG_FILE):
      os.remove(HISTOGRAM_CONFIG_FILE)

    # We also need to make sure there are flights on which to generate a histogram! Why
    # might there not be any flights? Primarily during a simulation, if there's a
    # lingering histogram file at the time of history restart.
    if histogram and flights:
      histogram_messages = TriggerHistograms(flights, histogram)
      histogram_messages = [(FLAG_MSG_HISTOGRAM, m) for m in histogram_messages]
      message_queue.extend(histogram_messages)

    # check time & if appropriate, display next message from queue
    next_message_time = ManageMessageQueue(message_queue, next_message_time, configuration)

    # if we've been running a long time, and everything else is quiet, reboot
    running_hours = (time.time() - startup_time) / SECONDS_IN_HOUR
    if (
        running_hours >= HOURS_IN_DAY and
        not message_queue and
        not json_desc_dict.get('radio_range_flights')):
      LogMessage('About to reboot after running for %.2f hours' % running_hours)
      os.system('sudo reboot')
      return  # exit execution

    if not SIMULATION:
      time.sleep(max(0, next_loop_time - time.time()))
      next_loop_time = time.time() + LOOP_DELAY_SECONDS
    else:
      SIMULATION_COUNTER += 1

  if SIMULATION:
    SimulationEnd(message_queue, flights)


if __name__ == "__main__":
  if '-i' in sys.argv:
    BootstrapInsightList()
  else:
    main()