class Time(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Time') self.__font__ = font font_height = font.get_height() text_half_height = int(font_height) >> 1 self.__text_y_pos__ = framebuffer_size[ 1] - text_half_height - font_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = int(framebuffer_size[0] * 0.01) self.__center_x__ = framebuffer_size[0] >> 1 def render(self, framebuffer, orientation): self.task_timer.start() time_text = str(orientation.utc_time).split( '.' )[0] + "UTC" if orientation.utc_time is not None else AhrsElement.GPS_UNAVAILABLE_TEXT texture = self.__font__.render(time_text, True, YELLOW, BLACK) width = texture.get_size()[0] framebuffer.blit(texture, (self.__center_x__ - (width >> 1), self.__text_y_pos__)) self.task_timer.stop()
class RollIndicatorText(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('RollIndicatorText') self.__roll_elements__ = {} self.__framebuffer_size__ = framebuffer_size self.__center__ = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) half_texture_height = int(font.get_height()) >> 1 self.__font__ = font self.__text_y_pos__ = self.__center__[1] - half_texture_height for reference_angle in range(-180, 181): text = font.render( "{0:3}".format(int(math.fabs(reference_angle))), True, display.WHITE, display.BLACK) size_x, size_y = text.get_size() self.__roll_elements__[reference_angle] = ( text, (size_x >> 1, size_y >> 1)) def render(self, framebuffer, orientation): self.task_timer.start() roll = int(orientation.roll) pitch = int(orientation.pitch) pitch_direction = '' if pitch > 0: pitch_direction = '+' attitude_text = "{0}{1:3} | {2:3}".format(pitch_direction, pitch, roll) roll_texture = self.__font__.render( attitude_text, True, display.BLACK, display.WHITE) texture_size = roll_texture.get_size() text_half_width, text_half_height = texture_size text_half_width = int(text_half_width / 2) framebuffer.blit( roll_texture, (self.__center__[0] - text_half_width, self.__text_y_pos__)) self.task_timer.stop()
class Altitude(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Altitude') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = center_y - text_half_height self.__rhs__ = int(framebuffer_size[0]) # was 0.9 def render(self, framebuffer, orientation): self.task_timer.start() is_altitude_valid = orientation.alt is not None and isinstance(orientation.alt, Number) altitude_text = str(int(orientation.alt)) + \ "' MSL" if is_altitude_valid else AhrsElement.INOPERATIVE_TEXT color = display.WHITE if is_altitude_valid else display.RED alt_texture = self.__font__.render( altitude_text, True, color, display.BLACK) text_width, text_height = alt_texture.get_size() framebuffer.blit( alt_texture, (self.__rhs__ - text_width, self.__text_y_pos__)) self.task_timer.stop()
class TrafficNotAvailable(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('TrafficNotAvailable') self.__font__ = font font_height = font.get_height() self.__text_y_pos__ = int(font_height * 0.7) self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = int(framebuffer_size[0] * 0.01) self.__center_x__ = framebuffer_size[0] >> 1 def render(self, framebuffer, orientation): self.task_timer.start() if not HudDataCache.IS_TRAFFIC_AVAILABLE: (texture, size) = HudDataCache.get_cached_text_texture( "TRAFFIC UNAVAILABLE", self.__font__, text_color=RED, background_color=BLACK, use_alpha=True) width = size[0] framebuffer.blit(texture, (self.__center_x__ - (width >> 1), self.__text_y_pos__)) self.task_timer.stop()
class Groundspeed(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Groundspeed') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = (text_half_height << 2) + \ center_y - text_half_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = 0 # WAS int(framebuffer_size[0] * 0.01) def render(self, framebuffer, orientation): self.task_timer.start() speed_units = configuration.CONFIGURATION.__get_config_value__( configuration.Configuration.DISTANCE_UNITS_KEY, units.STATUTE) groundspeed_text = units.get_converted_units_string( speed_units, orientation.groundspeed * units.feet_to_nm, units.SPEED) texture = self.__font__.render( groundspeed_text, True, WHITE, BLACK) framebuffer.blit( texture, (self.__left_x__, self.__text_y_pos__)) self.task_timer.stop()
class LevelReference(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('LevelReference') self.level_reference_lines = [] width = framebuffer_size[0] center = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) edge_reference_proportion = int(width * 0.05) artificial_horizon_level = [[int(width * 0.4), center[1]], [int(width * 0.6), center[1]]] left_hash = [[0, center[1]], [edge_reference_proportion, center[1]]] right_hash = [[width - edge_reference_proportion, center[1]], [width, center[1]]] self.level_reference_lines.append(artificial_horizon_level) self.level_reference_lines.append(left_hash) self.level_reference_lines.append(right_hash) def render(self, framebuffer, orientation): """ Renders a "straight and level" line to the HUD. """ self.task_timer.start() [pygame.draw.lines(framebuffer, WHITE, False, line, 6) for line in self.level_reference_lines] self.task_timer.stop()
class AhrsNotAvailable(object): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('AhrsNotAvailable') self.__not_available_lines__ = [] width, height = framebuffer_size self.__not_available_lines__.append([[0, 0], [width, height]]) self.__not_available_lines__.append([[0, height], [width, 0]]) self.__na_color__ = RED self.__na_line_width__ = 20 def render(self, framebuffer, orientation): """ Render an "X" over the screen to indicate the AHRS is not available. """ self.task_timer.start() pygame.draw.line(framebuffer, self.__na_color__, self.__not_available_lines__[0][0], self.__not_available_lines__[0][1], self.__na_line_width__) pygame.draw.line(framebuffer, self.__na_color__, self.__not_available_lines__[1][0], self.__not_available_lines__[1][1], self.__na_line_width__) self.task_timer.stop()
class Aithre(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Aithre') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = center_y - text_half_height self.__lhs__ = 0 self.__has_been_connected__ = False def render(self, framebuffer, orientation): self.task_timer.start() if aithre.sensor is not None and configuration.CONFIGURATION.aithre_enabled: co_level = aithre.sensor.get_co_level() if co_level is None or isinstance(co_level, basestring): if self.__has_been_connected__: co_color = RED co_ppm_text = "OFFLINE" else: self.task_timer.stop() return else: co_color = get_aithre_co_color(co_level) co_ppm_text = str(int(co_level)) + " PPM" self.__has_been_connected__ = True co_ppm_texture = self.__font__.render(co_ppm_text, True, co_color, BLACK) framebuffer.blit(co_ppm_texture, (self.__lhs__, self.__text_y_pos__)) self.task_timer.stop()
class Groundspeed(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Groundspeed') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = (text_half_height << 2) + \ center_y - text_half_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = 0 # WAS int(framebuffer_size[0] * 0.01) def render(self, framebuffer, orientation): self.task_timer.start() speed_units = configuration.CONFIGURATION.__get_config_value__( configuration.Configuration.DISTANCE_UNITS_KEY, units.STATUTE) groundspeed_text = units.get_converted_units_string( speed_units, orientation.groundspeed * units.feet_to_nm, unit_type=units.SPEED, decimal_places=False ) if orientation.groundspeed is not None and isinstance( orientation.groundspeed, Number) else "INOP" display_color = display.WHITE if orientation is not None and orientation.groundspeed is not None and orientation.gps_online else display.RED texture = self.__font__.render(groundspeed_text, True, display_color, display.BLACK) framebuffer.blit(texture, (self.__left_x__, self.__text_y_pos__)) self.task_timer.stop()
class Illyrian(AhrsElement): """ Screen element to support the Illyrian blood/pulse oxymeter from Aithre """ def uses_ahrs(self): """ Does this element use AHRS data to render? Returns: bool -- True if the element uses AHRS data. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Illyrian') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = center_y + (6 * text_half_height) self.__pulse_y_pos__ = center_y + (8 * text_half_height) self.__lhs__ = 0 self.__has_been_connected__ = False def render(self, framebuffer, orientation): self.task_timer.start() if AithreClient.INSTANCE is not None and configuration.CONFIGURATION.aithre_enabled: report = AithreClient.INSTANCE.get_spo2_report() spo2_level = report.spo2 heartbeat = report.heartrate heartbeat_text = "{}BPM".format(heartbeat) if spo2_level is None or isinstance(spo2_level, basestring): if self.__has_been_connected__: spo2_color = RED spo2_text = "OFFLINE" else: self.task_timer.stop() return else: spo2_color = get_illyrian_spo2_color(spo2_level) spo2_text = str(int(spo2_level)) + "% SPO" self.__has_been_connected__ = True spo2_ppm_texture = self.__font__.render(spo2_text, True, spo2_color, BLACK) heartbeat_texture = self.__font__.render(heartbeat_text, True, GREEN, BLACK) framebuffer.blit(spo2_ppm_texture, (self.__lhs__, self.__text_y_pos__)) framebuffer.blit(heartbeat_texture, (self.__lhs__, self.__pulse_y_pos__)) self.task_timer.stop()
class SystemInfo(AhrsElement): def uses_ahrs(self): """ The diagnostics page does not use AHRS. Returns: bool -- Always returns False. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Time') self.__font__ = font self.font_height = font.get_height() text_half_height = int(self.font_height) >> 1 self.__text_y_pos__ = framebuffer_size[1] - \ text_half_height - self.font_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = int(framebuffer_size[0] * 0.01) self.__center_x__ = framebuffer_size[0] >> 1 def render(self, framebuffer, orientation): self.task_timer.start() # Status lines are pushed in as a stack. # First line in the array is at the bottom. # Last line in the array is towards the top. info_lines = [ ["IP : ", get_ip_address()], ["HUD CPU : ", get_cpu_temp()], ["SOCKET : ", get_websocket_uptime()], ["DECLINATION : ", [ str(configuration.CONFIGURATION.get_declination()), BLUE]], ["OWNSHIP : ", [configuration.CONFIGURATION.ownship, BLUE]] ] render_y = self.__text_y_pos__ for line in info_lines: # Draw the label in a standard color. texture_lhs = self.__font__.render(line[0], True, BLUE, BLACK) framebuffer.blit(texture_lhs, (0, render_y)) size = texture_lhs.get_size() # Draw the value in the encoded colors. texture_rhs = self.__font__.render( line[1][0], True, line[1][1], BLACK) framebuffer.blit(texture_rhs, (size[0], render_y)) render_y = render_y - (self.font_height * 1.2) self.task_timer.stop()
class Aithre(AhrsElement): def uses_ahrs(self): """ Does this element use AHRS data to render? Returns: bool -- True if the element uses AHRS data. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Aithre') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = center_y + (10 * text_half_height) self.__lhs__ = 0 def render(self, framebuffer, orientation): self.task_timer.start() if AithreClient.INSTANCE is not None and configuration.CONFIGURATION.aithre_enabled: co_level = AithreClient.INSTANCE.get_co_report() if (co_level.co is None and co_level.has_been_connected) or isinstance( co_level, basestring): co_color = RED co_ppm_text = "OFFLINE" elif not co_level.has_been_connected: self.task_timer.stop() return else: co_color = get_aithre_co_color(co_level.co) units_text = "PPM" if co_level.is_connected else "" co_ppm_text = "{}{}".format(co_level.co, units_text) co_ppm_texture = self.__font__.render(co_ppm_text, True, co_color, BLACK) framebuffer.blit(co_ppm_texture, (self.__lhs__, self.__text_y_pos__)) self.task_timer.stop()
class SkidAndGs(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('SkidAndGs') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = (text_half_height << 2) + \ center_y - text_half_height self.__rhs__ = int(framebuffer_size[0]) # was 0.9 def render(self, framebuffer, orientation): self.task_timer.start() g_load_text = "{0:.1f}Gs".format(orientation.g_load) texture = self.__font__.render( g_load_text, True, WHITE, BLACK) text_width, text_height = texture.get_size() framebuffer.blit( texture, (self.__rhs__ - text_width, self.__text_y_pos__)) self.task_timer.stop()
class TargetCount(AhrsElement): def uses_ahrs(self): """ Does this element use AHRS data to render? Returns: bool -- True if the element uses AHRS data. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('TargetCount') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = center_y - text_half_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = 0 # WAS int(framebuffer_size[0] * 0.01) def render(self, framebuffer, orientation): self.task_timer.start() # Get the traffic, and bail out of we have none text = "NO TARGETS" try: count = len(targets.TARGET_MANAGER.targets) if count > 0: text = "{0} TARGETS".format(count) except Exception as e: text = "ERROR" + str(e) texture = self.__font__.render(text, True, WHITE, BLACK) framebuffer.blit(texture, (self.__left_x__, self.__text_y_pos__)) self.task_timer.stop()
class Groundspeed(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Groundspeed') self.__font__ = font center_y = framebuffer_size[1] >> 2 text_half_height = int(font.get_height()) >> 1 self.__text_y_pos__ = (text_half_height << 2) + \ center_y - text_half_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = 0 # WAS int(framebuffer_size[0] * 0.01) def render(self, framebuffer, orientation): self.task_timer.start() # TODO - Pass in the configuration to all elements so they can have access to the unit types. groundspeed_text = "{0:.1f}".format(orientation.groundspeed).rjust(5) + units.UNIT_LABELS[units.STATUTE][units.SPEED] texture = self.__font__.render( groundspeed_text, True, WHITE, BLACK) framebuffer.blit( texture, (self.__left_x__, self.__text_y_pos__)) self.task_timer.stop()
class AdsbOnScreenReticles(AdsbElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbOnScreenReticles') self.__listing_text_start_y__ = int(self.__font__.get_height() * 4) self.__listing_text_start_x__ = int(self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__top_border__ = int(self.__height__ * 0.1) self.__bottom_border__ = self.__height__ - self.__top_border__ def __render_on_screen_reticle__(self, framebuffer, orientation, traffic): """ Draws a single reticle on the screen. Arguments: framebuffer {Surface} -- Render target orientation {Orientation} -- The orientation of the plane. traffic {Traffic} -- The traffic to draw the reticle for. """ identifier = traffic.get_identifer() # Find where to draw the reticle.... reticle_x, reticle_y = self.__get_traffic_projection__( orientation, traffic) # Render using the Above us bug on_screen_reticle_scale = get_reticle_size(traffic.distance) reticle, reticle_size_px = self.get_onscreen_reticle( reticle_x, reticle_y, on_screen_reticle_scale) reticle_x, reticle_y = self.__rotate_reticle__( [[reticle_x, reticle_y]], orientation.roll)[0] self.__render_target_reticle__(framebuffer, identifier, (reticle_x, reticle_y), reticle, orientation.roll, reticle_size_px) def render(self, framebuffer, orientation): """ Renders all of the on-screen reticles for nearby traffic. Arguments: framebuffer {Surface} -- The render target. orientation {Orientation} -- The orientation of the plane the HUD is in. """ self.task_timer.start() # Get the traffic, and bail out of we have none traffic_reports = HudDataCache.get_reliable_traffic() traffic_reports = filter(lambda x: not x.is_on_ground(), traffic_reports) traffic_reports = traffic_reports[:max_target_bugs] [ self.__render_on_screen_reticle__(framebuffer, orientation, traffic) for traffic in traffic_reports ] self.task_timer.stop() def __render_target_reticle__(self, framebuffer, identifier, pos, reticle_lines, roll, reticle_size_px): """ Renders a targetting reticle on the screen. Assumes the X/Y projection has already been performed. """ center_x, center_y = pos border_space = int(reticle_size_px * 1.2) center_y = border_space if center_y < border_space else center_y center_y = int(self.__height__ - border_space) \ if center_y > (self.__height__ - border_space) else center_y pygame.draw.lines(framebuffer, BLACK, True, reticle_lines, 20) pygame.draw.lines(framebuffer, RED, True, reticle_lines, 10) def __rotate_reticle__(self, reticle, roll): """ Takes a series of line segments and rotates them (roll) about the screen's center Arguments: reticle {list of tuples} -- The line segments roll {float} -- The amount to rotate about the center by. Returns: list of lists -- The new list of line segments """ # Takes the roll in degrees # Example input.. # [ # [center_x, center_y - size], # [center_x + size, center_y], # [center_x, center_y + size], # [center_x - size, center_y] # ] translated_points = [] int_roll = int(-roll) cos_roll = COS_RADIANS_BY_DEGREES[int_roll] sin_roll = SIN_RADIANS_BY_DEGREES[int_roll] ox, oy = self.__center__ translated_points = [[ (ox + cos_roll * (x_y[0] - ox) - sin_roll * (x_y[1] - oy)), (oy + sin_roll * (x_y[0] - ox) + cos_roll * (x_y[1] - oy)) ] for x_y in reticle] return translated_points
class Groundspeed(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Groundspeed') self.__font__ = font self.__font_height__ = font.get_height() center_y = framebuffer_size[1] >> 2 text_half_height = int(self.__font_height__) >> 1 self.__text_y_pos__ = center_y - text_half_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = 0 # WAS int(framebuffer_size[0] * 0.01) def render(self, framebuffer, orientation): self.task_timer.start() speed_units = configuration.CONFIGURATION.__get_config_value__( configuration.Configuration.DISTANCE_UNITS_KEY, units.STATUTE) airspeed_text = None is_valid_airspeed = orientation.is_avionics_source and isinstance( orientation.airspeed, Number) is_valid_groundspeed = orientation.groundspeed is not None and isinstance( orientation.groundspeed, Number) airspeed_text = units.get_converted_units_string( speed_units, orientation.airspeed * units.feet_to_nm, unit_type=units.SPEED, decimal_places=False) if is_valid_airspeed else None groundspeed_text = units.get_converted_units_string( speed_units, (orientation.groundspeed * units.yards_to_nm), unit_type=units.SPEED, decimal_places=False ) if is_valid_groundspeed else orientation.groundspeed groundspeed_text += " GND" if airspeed_text is not None: airspeed_text += " IAS" ias_len = len(airspeed_text) gnd_len = len(groundspeed_text) max_len = gnd_len if ias_len < gnd_len else ias_len airspeed_text = airspeed_text.rjust(max_len) groundspeed_text = groundspeed_text.rjust(max_len) gs_display_color = display.WHITE if is_valid_groundspeed and orientation.gps_online else display.RED airspeed_color = display.WHITE if is_valid_airspeed else display.RED ias_texture = self.__font__.render( airspeed_text, True, airspeed_color, display.BLACK) if airspeed_text is not None else None gs_texture = self.__font__.render(groundspeed_text, True, gs_display_color, display.BLACK) gs_position_adj = self.__font_height__ if ias_texture is not None else 0 framebuffer.blit( gs_texture, (self.__left_x__, self.__text_y_pos__ + gs_position_adj)) if ias_texture is not None: framebuffer.blit(ias_texture, (self.__left_x__, self.__text_y_pos__)) self.task_timer.stop()
class AdsbTrafficListing(AdsbElement): def uses_ahrs(self): """ Does this element use AHRS data to render? Returns: bool -- False as this element does not use AHRS data. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__( self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbTargetBugs') self.__listing_text_start_y__ = int(self.__font__.get_height()) self.__listing_text_start_x__ = int( self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height()) self.__max_reports__ = int( (self.__height__ - self.__listing_text_start_y__) / self.__next_line_distance__) - 1 self.__top_border__ = int(self.__height__ * 0.2) self.__bottom_border__ = self.__height__ - int(self.__height__ * 0.1) def __get_listing__(self, report, max_string_lengths): identifier = report[0] try: bearing = str(utils.apply_declination(float(report[1]))) except: bearing = str(report[1]) distance_text = report[2] altitude = report[3] icao = report[4] # if self.__show_list__: traffic_report = "{0} {1} {2} {3}".format( identifier.ljust(max_string_lengths[0]), bearing.rjust(max_string_lengths[1]), distance_text.rjust(max_string_lengths[2]), altitude.rjust(max_string_lengths[3])) return (icao, traffic_report) def __get_padded_traffic_reports__(self, traffic_reports): pre_padded_text, max_string_lengths = self.__get_pre_padded_text_reports__( traffic_reports) out_padded_reports = [self.__get_listing__(report, max_string_lengths) for report in pre_padded_text] return out_padded_reports def __get_report_text__(self, traffic): identifier = str(traffic.get_display_name()) altitude_delta = int(traffic.altitude / 100.0) distance_text = self.__get_distance_string__(traffic.distance, True) delta_sign = '' if altitude_delta > 0: delta_sign = '+' altitude_text = "{0}{1}".format(delta_sign, altitude_delta) bearing_text = "{0:.0f}".format( utils.apply_declination(traffic.bearing)) return [identifier, bearing_text, distance_text, altitude_text, traffic.icao_address] def __get_pre_padded_text_reports__(self, traffic_reports): # We do not want to show traffic on the ground. reports_to_show = filter( lambda x: not x.is_on_ground(), traffic_reports) # The __max_reports__ value is set based on the screen size # and how much can fit on the screen reports_to_show = reports_to_show[:self.__max_reports__] pre_padded_text = [['IDENT', 'BEAR', 'DIST', 'ALT', None]] + \ [self.__get_report_text__(traffic) for traffic in reports_to_show] # An ICAO code is the worst case display length, # but add a little buffer so the columns do # not shift around. # len(max(pre_padded_text, key = lambda x: len(str(x[4])))[0]) max_identifier_length = 10 # Since the bearing length should never be any more the 3 digits max_bearing_length = 4 # len(max(pre_padded_text, key = lambda x: len(x[2]))[2]) + 1 max_distance_length = 8 # We really should never get anything more than 35k above, but same some room # len(max(pre_padded_text, key = lambda x: len(x[3]))[3]) max_altitude_length = 5 return pre_padded_text, (max_identifier_length, max_bearing_length, max_distance_length, max_altitude_length) def render(self, framebuffer, orientation): # Render a heading strip along the top self.task_timer.start() # Get the traffic, and bail out of we have none traffic_reports = HudDataCache.get_reliable_traffic() if traffic_reports is None: self.task_timer.stop() return # Render a list of traffic that we have positions # for, along with the tail number y_pos = self.__listing_text_start_y__ x_pos = self.__listing_text_start_x__ padded_traffic_reports = self.__get_padded_traffic_reports__( traffic_reports) if len(padded_traffic_reports) == 0: framebuffer.blit(HudDataCache.get_cached_text_texture("NO TRAFFIC", self.__font__)[0], (x_pos, y_pos)) for identifier, traffic_report in padded_traffic_reports: traffic_text_texture = HudDataCache.get_cached_text_texture(traffic_report, self.__font__)[0] framebuffer.blit(traffic_text_texture, (x_pos, y_pos)) y_pos += self.__next_line_distance__ self.task_timer.stop()
class ArtificialHorizon(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('ArtificialHorizon') self.__pitch_elements__ = {} self.__framebuffer_size__ = framebuffer_size self.__center__ = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) self.__long_line_width__ = self.__framebuffer_size__[0] * 0.4 self.__short_line_width__ = self.__framebuffer_size__[0] * 0.2 self.__pixels_per_degree_y__ = pixels_per_degree_y self.__height__ = framebuffer_size[1] for reference_angle in range(-degrees_of_pitch, degrees_of_pitch + 1, 10): text = font.render(str(reference_angle), True, WHITE, BLACK).convert() size_x, size_y = text.get_size() self.__pitch_elements__[reference_angle] = (text, (size_x >> 1, size_y >> 1)) def render(self, framebuffer, orientation): self.task_timer.start() for reference_angle in self.__pitch_elements__: line_coords, line_center = self.__get_line_coords__( orientation.pitch, orientation.roll, reference_angle) # Perform some trivial clipping of the lines # This also prevents early text rasterization if line_center[1] < 0 or line_center[1] > self.__height__: continue pygame.draw.lines(framebuffer, GREEN, False, line_coords, 4) text, half_size = self.__pitch_elements__[reference_angle] text = pygame.transform.rotate(text, orientation.roll) half_x, half_y = half_size center_x, center_y = line_center framebuffer.blit(text, (center_x - half_x, center_y - half_y)) self.task_timer.stop() def __get_line_coords__(self, pitch, roll, reference_angle): """ Get the coordinate for the lines for a given pitch and roll. """ if reference_angle == 0: length = self.__long_line_width__ else: length = self.__short_line_width__ pitch = int(pitch) roll = int(roll) ahrs_center_x, ahrs_center_y = self.__center__ pitch_offset = self.__pixels_per_degree_y__ * \ (-pitch + reference_angle) roll_delta = 90 - roll center_x = int((ahrs_center_x - (pitch_offset * COS_RADIANS_BY_DEGREES[roll_delta])) + 0.5) center_y = int((ahrs_center_y - (pitch_offset * SIN_RADIANS_BY_DEGREES[roll_delta])) + 0.5) x_len = int((length * COS_RADIANS_BY_DEGREES[roll]) + 0.5) y_len = int((length * SIN_RADIANS_BY_DEGREES[roll]) + 0.5) half_x_len = x_len >> 1 half_y_len = y_len >> 1 start_x = center_x - half_x_len end_x = center_x + half_x_len start_y = center_y + half_y_len end_y = center_y - half_y_len return [[int(start_x), int(start_y)], [int(end_x), int(end_y)]], (int(center_x), int(center_y))
class AdsbTargetBugsOnly(AdsbElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__( self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbTargetBugs') self.__listing_text_start_y__ = int(self.__font__.get_height() * 4) self.__listing_text_start_x__ = int( self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__top_border__ = 0 self.__bottom_border__ = self.__height__ - int(self.__height__ * 0.1) def __render_traffic_heading_bug__(self, traffic_report, heading, orientation, framebuffer): """ Render a single heading bug to the framebuffer. Arguments: traffic_report {Traffic} -- The traffic we want to render a bug for. heading {int} -- Our current heading. orientation {Orientation} -- Our plane's current orientation. framebuffer {Framebuffer} -- What we are going to draw to. """ # Render using the Above us bug # target_bug_scale = 0.04 target_bug_scale = get_reticle_size(traffic_report.distance) heading_bug_x = get_heading_bug_x( heading, traffic_report.bearing, self.__pixels_per_degree_x__) try: is_below = (orientation.alt - 100) > traffic_report.altitude reticle, reticle_edge_positon_y = self.get_below_reticle( heading_bug_x, target_bug_scale) if is_below else self.get_above_reticle(heading_bug_x, target_bug_scale) bug_color = display.BLUE if traffic_report.is_on_ground() == True else display.RED pygame.draw.polygon(framebuffer, bug_color, reticle) except: pass def render(self, framebuffer, orientation): # Render a heading strip along the top self.task_timer.start() heading = orientation.get_onscreen_projection_heading() # Get the traffic, and bail out of we have none traffic_reports = HudDataCache.get_reliable_traffic() if traffic_reports is None: self.task_timer.stop() return reports_to_show = filter(lambda x: math.fabs(x.altitude - orientation.alt) < max_altitude_delta, traffic_reports) reports_to_show = reports_to_show[:max_target_bugs] [self.__render_traffic_heading_bug__( traffic_report, heading, orientation, framebuffer) for traffic_report in reports_to_show] self.task_timer.stop()
class ArtificialHorizon(AhrsElement): def __generate_reference_angle__(self, reference_angle): """ Renders the text for the reference angle. Arguments: reference_angle {int} -- The angle that we are going to produce text for. Returns: (Surface, (int, int)) -- Tuple of the texture and the half size x & y. """ text = self.__font__.render(str(reference_angle), False, WHITE, BLACK).convert() size_x, size_y = text.get_size() return (text, (size_x >> 1, size_y >> 1)) def __generate_rotated_reference_angle__(self, reference_angle): """ Returns the text for the reference angle rotated. Arguments: reference_angle {int} -- The angle marking to generate the textures for. Returns: ({int, Surface}, (int, int)) -- A map of the textures keyed by roll angle and the half size of the texture. """ rotate = pygame.transform.rotate reference_angle, half_size = self.__generate_reference_angle__( reference_angle) rotated_angles = { roll: rotate(reference_angle, roll) for roll in range(-45, 46, 1) } # Make sure that the un-rolled version is the original # so as to not have any artifacts for later rotations # that get added. rotated_angles[0] = reference_angle return rotated_angles, half_size def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('ArtificialHorizon') self.__framebuffer_size__ = framebuffer_size self.__center__ = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) self.__long_line_width__ = self.__framebuffer_size__[0] * 0.4 self.__short_line_width__ = self.__framebuffer_size__[0] * 0.2 self.__pixels_per_degree_y__ = int(pixels_per_degree_y) self.__height__ = framebuffer_size[1] self.__font__ = font self.__reference_angles__ = range(-degrees_of_pitch, degrees_of_pitch + 1, 10) self.__pitch_elements__ = { reference_angle: self.__generate_rotated_reference_angle__(reference_angle) for reference_angle in self.__reference_angles__ } def __render_reference_line__(self, framebuffer, line_info, draw_line, roll): """ Renders a single line of the AH ladder. Arguments: framebuffer {Surface} -- The target framebuffer to draw to. line_info {triplet} -- The line coords, center, and angle. draw_line {function} -- The function to draw the line. rot_text {function} -- The function to rotate the text. roll {float} -- How much the plane is rolled. """ line_coords, line_center, reference_angle = line_info draw_line(framebuffer, GREEN, False, line_coords, 4) text, half_size = self.__pitch_elements__[reference_angle] roll = int(roll) # Since we will start with a limited number of pre-cached # pre-rotated textures (to improve boot time), # add any missing rotated textures using the upright # as the base. if roll not in text: rotated_surface = pygame.transform.rotate(text[0], roll) self.__pitch_elements__[reference_angle][0][roll] = rotated_surface text[roll] = rotated_surface text = text[roll] half_x, half_y = half_size center_x, center_y = line_center framebuffer.blit(text, (center_x - half_x, center_y - half_y)) def render(self, framebuffer, orientation): """ Renders the artifical horizon to the framebuffer Arguments: framebuffer {Surface} -- Target framebuffer to draw to. orientation {orientation} -- The airplane's orientation (roll & pitch) """ self.task_timer.start() # Creating aliases to the functions saves time... draw_line = pygame.draw.lines pitch = orientation.pitch roll = orientation.roll # Calculating the coordinates ahead of time... lines_centers_and_angles = [ self.__get_line_coords__(pitch, roll, reference_angle) for reference_angle in self.__reference_angles__ ] # ... only to use filter to throw them out saves time. # This allows for the cores to be used and removes the conditionals # from the actual render function. lines_centers_and_angles = filter( lambda center: center[1][1] >= 0 and center[1][1] <= self. __height__, lines_centers_and_angles) [ self.__render_reference_line__(framebuffer, line_info, draw_line, roll) for line_info in lines_centers_and_angles ] self.task_timer.stop() def __get_line_coords__(self, pitch, roll, reference_angle): """ Get the coordinate for the lines for a given pitch and roll. Arguments: pitch {float} -- The pitch of the plane. roll {float} -- The roll of the plane. reference_angle {int} -- The pitch angle to be marked on the AH. Returns: [tuple] -- An array[4] of the X/Y line coords. """ length = self.__long_line_width__ if reference_angle == 0 else self.__short_line_width__ roll_int = int(roll) ahrs_center_x, ahrs_center_y = self.__center__ pitch_offset = self.__pixels_per_degree_y__ * \ (-pitch + reference_angle) roll_delta = 90 - roll_int center_x = ahrs_center_x - \ (pitch_offset * COS_RADIANS_BY_DEGREES[roll_delta]) + 0.5 center_y = ahrs_center_y - \ (pitch_offset * SIN_RADIANS_BY_DEGREES[roll_delta]) + 0.5 center_x = int(center_x) center_y = int(center_y) x_len = int(length * COS_RADIANS_BY_DEGREES[roll_int] + 0.5) y_len = int(length * SIN_RADIANS_BY_DEGREES[roll_int] + 0.5) half_x_len = x_len >> 1 half_y_len = y_len >> 1 start_x = center_x - half_x_len end_x = center_x + half_x_len start_y = center_y + half_y_len end_y = center_y - half_y_len return [[start_x, start_y], [end_x, end_y]], (center_x, center_y), reference_angle
class AdsbTargetBugs(AdsbElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbTargetBugs') self.__listing_text_start_y__ = int(self.__font__.get_height() * 4) self.__listing_text_start_x__ = int(self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__max_reports__ = int( (self.__height__ - self.__listing_text_start_y__) / self.__next_line_distance__) self.__top_border__ = int(self.__height__ * 0.2) self.__bottom_border__ = self.__height__ - int(self.__height__ * 0.1) def render(self, framebuffer, orientation): # Render a heading strip along the top self.task_timer.start() heading = orientation.get_onscreen_projection_heading() # Get the traffic, and bail out of we have none traffic_reports = AdsbTrafficClient.TRAFFIC_MANAGER.get_traffic_with_position( ) if traffic_reports is None: self.task_timer.stop() return # Draw the heading bugs in reverse order so the traffic closest to # us will be the most visible traffic_bug_reports = sorted(traffic_reports, key=lambda traffic: traffic.distance, reverse=True) for traffic_report in traffic_bug_reports: if traffic_report.distance > imperial_occlude: continue try: altitude_delta = int( (traffic_report.altitude - orientation.alt) / 100.0) # TEST - Ignore stuff crazy separated if math.fabs(altitude_delta) > 50: continue finally: pass # Now find where to draw the reticle.... reticle_x, reticle_y = self.__get_traffic_projection__( orientation, traffic_report) # Render using the Above us bug # target_bug_scale = 0.04 target_bug_scale = get_reticle_size(traffic_report.distance) heading_bug_x = get_heading_bug_x(heading, traffic_report.bearing, self.__pixels_per_degree_x__) additional_info_text = self.__get_additional_target_text__( traffic_report, orientation) try: self.__render_heading_bug__( framebuffer, str(traffic_report.get_identifer()), additional_info_text, heading_bug_x, target_bug_scale, traffic_report.is_on_ground(), traffic_report.get_age()) except Exception as e: print(str(e)) self.task_timer.stop()
class CompassAndHeadingBottomElement(CompassAndHeadingTopElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): CompassAndHeadingTopElement.__init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('CompassAndHeadingBottomElement') self.__line_top__ = framebuffer_size[1] - self.line_height self.__line_bottom__ = framebuffer_size[1] self.heading_text_y = self.__line_top__ - (font.get_height() * 1.2) self.compass_text_y = framebuffer_size[1] - \ int(font.get_height() * 2) self.__border_width__ = 4 text_height = font.get_height() border_vertical_size = (text_height >> 1) + (text_height >> 2) vertical_alignment_offset = int((border_vertical_size / 2.0) + 0.5) + self.__border_width__ half_width = int(self.__heading_text__[360][1][0] * 3.5) self.__heading_text_box_lines__ = [ [ self.__center_x__ - half_width, self.compass_text_y - border_vertical_size + vertical_alignment_offset ], [ self.__center_x__ + half_width, self.compass_text_y - border_vertical_size + vertical_alignment_offset ], [ self.__center_x__ + half_width, self.compass_text_y + border_vertical_size + vertical_alignment_offset ], [ self.__center_x__ - half_width, self.compass_text_y + border_vertical_size + vertical_alignment_offset ] ] def __render_heading_mark__(self, framebuffer, x_pos, heading): pygame.draw.line(framebuffer, GREEN, [x_pos, self.__line_top__], [x_pos, self.__line_bottom__], self.__border_width__) self.__render_heading_text__(framebuffer, utils.apply_declination(heading), x_pos, self.compass_text_y) def render(self, framebuffer, orientation): """ Renders the current heading to the HUD. """ self.task_timer.start() # Render a crude compass # Render a heading strip along the top heading = orientation.get_onscreen_projection_heading() if heading < 0: heading += 360 if heading > 360: heading -= 360 [ self.__render_heading_mark__(framebuffer, heading_mark_to_render[0], heading_mark_to_render[1]) for heading_mark_to_render in self.__heading_strip__[heading] ] # Render the text that is showing our AHRS and GPS headings heading_text = "{0} | {1}".format( utils.apply_declination( orientation.get_onscreen_projection_display_heading()), utils.apply_declination(orientation.gps_heading)) rendered_text = self.__font__.render(heading_text, True, BLACK, GREEN) text_width, text_height = rendered_text.get_size() pygame.draw.polygon(framebuffer, GREEN, self.__heading_text_box_lines__) framebuffer.blit(rendered_text, (self.__center_x__ - (text_width >> 1), self.compass_text_y)) self.task_timer.stop()
class CompassAndHeadingBottomElement(CompassAndHeadingTopElement): def __init__( self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size ): CompassAndHeadingTopElement.__init__( self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('CompassAndHeadingBottomElement') self.__line_top__ = framebuffer_size[1] - self.line_height self.__line_bottom__ = framebuffer_size[1] self.heading_text_y = self.__line_top__ - (font.get_height() * 1.2) self._heading_box_y_ = framebuffer_size[1] - \ int(font.get_height() * 2.8) self.compass_text_y = framebuffer_size[1] - \ int(font.get_height()) self.__border_width__ = 4 text_height = font.get_height() border_vertical_size = (text_height >> 1) + (text_height >> 2) vertical_alignment_offset = int( (border_vertical_size / 2.0) + 0.5) + self.__border_width__ half_width = int(self.__heading_text__[360][1][0] * 3.5) self.__heading_text_box_lines__ = [ [self.__center_x__ - half_width, self._heading_box_y_ - border_vertical_size + vertical_alignment_offset], [self.__center_x__ + half_width, self._heading_box_y_ - border_vertical_size + vertical_alignment_offset], [self.__center_x__ + half_width, self._heading_box_y_ + border_vertical_size + vertical_alignment_offset], [self.__center_x__ - half_width, self._heading_box_y_ + border_vertical_size + vertical_alignment_offset]] def __render_heading_mark__(self, framebuffer, x_pos, heading): pygame.draw.line( framebuffer, GREEN, [x_pos, self.__line_top__], [x_pos, self.__line_bottom__], self.__border_width__) self.__render_heading_text__( framebuffer, heading, x_pos, self.compass_text_y) def render(self, framebuffer, orientation): """ Renders the current heading to the HUD. """ self.task_timer.start() # Render a crude compass # Render a heading strip along the top heading = orientation.get_onscreen_projection_heading() if isinstance(heading, Number): if heading < 0: heading += 360 if heading > 360: heading -= 360 [self.__render_heading_mark__(framebuffer, heading_mark_to_render[0], heading_mark_to_render[1]) for heading_mark_to_render in self.__heading_strip__[heading]] self._render_hallow_heading_box_( orientation, framebuffer, self._heading_box_y_) self.task_timer.stop()
class AdsbTargetBugs(AdsbElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__( self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbTargetBugs') self.__listing_text_start_y__ = int(self.__font__.get_height() * 4) self.__listing_text_start_x__ = int( self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__top_border__ = int(self.__height__ * 0.2) self.__bottom_border__ = self.__height__ - int(self.__height__ * 0.1) def __render_traffic_heading_bug__(self, traffic_report, heading, orientation, framebuffer): """ Render a single heading bug to the framebuffer. Arguments: traffic_report {Traffic} -- The traffic we want to render a bug for. heading {int} -- Our current heading. orientation {Orientation} -- Our plane's current orientation. framebuffer {Framebuffer} -- What we are going to draw to. """ heading_bug_x = get_heading_bug_x( heading, utils.apply_declination(traffic_report.bearing), self.__pixels_per_degree_x__) additional_info_text = self.__get_additional_target_text__( traffic_report, orientation) try: self.__render_info_card__(framebuffer, str(traffic_report.get_display_name()), additional_info_text, heading_bug_x, traffic_report.get_age()) except Exception as ex: print("EX:{}".format(ex)) pass def render(self, framebuffer, orientation): # Render a heading strip along the top self.task_timer.start() heading = orientation.get_onscreen_projection_heading() # Get the traffic, and bail out of we have none traffic_reports = HudDataCache.get_reliable_traffic() if traffic_reports is None: self.task_timer.stop() return traffic_reports = traffic_reports[:max_target_bugs] # Draw the heading bugs in reverse order so the traffic closest to # us will be the most visible traffic_reports.reverse() [self.__render_traffic_heading_bug__( traffic_report, heading, orientation, framebuffer) for traffic_report in traffic_reports] self.task_timer.stop()
class SystemInfo(AhrsElement): def uses_ahrs(self): """ The diagnostics page does not use AHRS. Returns: bool -- Always returns False. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('Time') self.__font__ = font self.font_height = font.get_height() self.__text_y_pos__ = framebuffer_size[1] - self.font_height self.__rhs__ = int(0.9 * framebuffer_size[0]) self.__left_x__ = int(framebuffer_size[0] * 0.01) self.__center_x__ = framebuffer_size[0] >> 1 self.__update_ip_timer__ = 0 self.__update_temp_timer__ = 0 self.__ip_address__ = get_ip_address() self.__cpu_temp__ = None self.__framebuffer_size__ = framebuffer_size self.__line_spacing__ = 1.01 def __get_aithre_text_and_color__(self): """ Gets the text and text color for the Aithre status. """ if AithreClient.INSTANCE is None: return (DISCONNECTED_TEXT, RED) if configuration.CONFIGURATION.aithre_enabled else ( DISABLED_TEXT, BLUE) co_report = AithreClient.INSTANCE.get_co_report() battery_text = 'UNK' battery_color = RED try: battery = co_report.battery battery_suffix = "%" if isinstance(battery, basestring): battery_suffix = "" if battery is not None: battery_color = get_aithre_battery_color(battery) battery_text = "bat:{}{}".format(battery, battery_suffix) except Exception as ex: battery_text = 'ERR' co_text = 'UNK' co_color = RED try: co_ppm = co_report.co if co_ppm is not None and OFFLINE_TEXT not in co_ppm: co_text = 'co:{}ppm'.format(co_ppm) co_color = get_aithre_co_color(co_ppm) except Exception as ex: co_text = 'ERR' color = RED if co_color is RED or battery_color is RED else \ (YELLOW if co_color is YELLOW or battery_color is YELLOW else BLUE) return ('{} {}'.format(co_text, battery_text), color) def render(self, framebuffer, orientation): self.task_timer.start() self.__update_ip_timer__ -= 1 if self.__update_ip_timer__ <= 0: self.__ip_address__ = get_ip_address() self.__update_ip_timer__ = 120 self.__update_temp_timer__ -= 1 if self.__update_temp_timer__ <= 0: self.__cpu_temp__ = get_cpu_temp() self.__update_temp_timer__ = 60 info_lines = [ ["VERSION : ", [configuration.VERSION, YELLOW]], [ "DECLINATION : ", [str(configuration.CONFIGURATION.get_declination()), BLUE] ], [ "TRAFFIC : ", [ configuration.CONFIGURATION.get_traffic_manager_address(), BLUE ] ] ] addresses = self.__ip_address__[0].split(' ') for addr in addresses: info_lines.append( ["IP : ", (addr, self.__ip_address__[1])]) info_lines.append( ["AITHRE : ", self.__get_aithre_text_and_color__()]) # Status lines are pushed in as a stack. # First line in the array is at the bottom. # Last line in the array is towards the top. info_lines.append(["HUD CPU : ", self.__cpu_temp__]) info_lines.append([ "DISPLAY RES : ", [ "{} x {}".format(self.__framebuffer_size__[0], self.__framebuffer_size__[1]), BLUE ] ]) render_y = self.__text_y_pos__ for line in info_lines: # Draw the label in a standard color. texture_lhs = self.__font__.render(line[0], True, BLUE, BLACK) framebuffer.blit(texture_lhs, (0, render_y)) size = texture_lhs.get_size() # Draw the value in the encoded colors. texture_rhs = self.__font__.render(line[1][0], True, line[1][1], BLACK) framebuffer.blit(texture_rhs, (size[0], render_y)) render_y = render_y - (self.font_height * self.__line_spacing__) self.task_timer.stop()
class HeadingTargetBugs(AdsbElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('HeadingTargetBugs') self.__listing_text_start_y__ = int(self.__font__.get_height()) self.__listing_text_start_x__ = int(self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__top_border__ = int(self.__height__ * 0.2) self.__bottom_border__ = self.__height__ - int(self.__height__ * 0.1) def __get_additional_target_text__(self, time_until_drop=0.0, altitude_delta=0.0, distance_meters=0.0): """ Returns a tuple of text to be rendered with the target card. Keyword Arguments: time_until_drop {float} -- The number of seconds until the flour bomb should be dropped. (default: {0.0}) altitude_delta {float} -- The number of feet above the target. (default: {0.0}) distance_meters {float} -- The distance (in meters) to the target. (default: {0.0}) Returns: string[] -- Tuple of strings. """ if altitude_delta < 0: altitude_delta = 0.0 if time_until_drop < 0.0: altitude_delta = 0 distance_text = self.__get_distance_string__(distance_meters) altitude_text = "{0:.1f}AGL".format(altitude_delta) time_until_drop = "{0:.1f}S".format(time_until_drop) return [altitude_text, distance_text, time_until_drop] def render(self, framebuffer, orientation): # Render a heading strip along the top self.task_timer.start() heading = orientation.get_onscreen_projection_heading() # Get the traffic, and bail out of we have none if targets.TARGET_MANAGER is None or targets.TARGET_MANAGER.targets is None: return for target_position in targets.TARGET_MANAGER.targets: ground_speed_ms = units.get_meters_per_second_from_mph( orientation.groundspeed) distance_miles = norden.get_distance(orientation.position, target_position) distance_meters = units.get_meters_from_feet( units.get_feet_from_miles(distance_miles)) time_to_target = norden.get_time_to_distance( distance_meters, ground_speed_ms) # NOTE: # Remember that the altitude off the AHRS is # in terms of MSL. That means that we need to # subtract out the altitude of the target. delta_altitude = orientation.alt - target_position[2] time_to_impact = norden.get_time_to_impact( units.get_meters_from_feet(delta_altitude)) time_until_drop = time_to_target - time_to_impact # target_altitude_for_drop = units.get_feet_from_meters( # norden.get_altitude(time_to_target)) bearing_to_target = norden.get_bearing(target_position, orientation.position) # time_to_impact_from_ideal_current_altitude = norden.get_time_to_impact( # target_altitude_for_drop) # Render using the Above us bug # target_bug_scale = 0.04 # target_bug_scale = get_reticle_size(distance_miles) heading_bug_x = get_heading_bug_x(heading, bearing_to_target, self.__pixels_per_degree_x__) additional_info_text = self.__get_additional_target_text__( time_until_drop, delta_altitude, units.get_feet_from_miles(distance_miles)) self.__render_info_card__( framebuffer, "{0:.1f}".format(utils.apply_declination(bearing_to_target)), additional_info_text, heading_bug_x, False) self.task_timer.stop()
class CompassAndHeadingTopElement(AhrsElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): self.task_timer = TaskTimer('CompassAndHeadingTopElement') self.__framebuffer_size__ = framebuffer_size self.__center__ = (framebuffer_size[0] >> 1, framebuffer_size[1] >> 1) self.__long_line_width__ = self.__framebuffer_size__[0] * 0.2 self.__short_line_width__ = self.__framebuffer_size__[0] * 0.1 self.__pixels_per_degree_y__ = pixels_per_degree_y self.heading_text_y = int(font.get_height()) self.compass_text_y = int(font.get_height()) self.pixels_per_degree_x = framebuffer_size[0] / 360.0 cardinal_direction_line_proportion = 0.2 self.line_height = int(framebuffer_size[1] * cardinal_direction_line_proportion) self.__font__ = font self.__heading_text__ = {} for heading in range(-1, 361): texture = self.__font__.render(str(heading), True, display.BLACK, display.YELLOW).convert() width, height = texture.get_size() self.__heading_text__[heading] = texture, (width >> 1, height >> 1) text_height = font.get_height() border_vertical_size = (text_height >> 1) + (text_height >> 2) half_width = int(self.__heading_text__[360][1][0] * 3.5) self.__center_x__ = self.__center__[0] self.__heading_text_box_lines__ = [ [ self.__center_x__ - half_width, self.compass_text_y + (1.5 * text_height) - border_vertical_size ], [ self.__center_x__ + half_width, self.compass_text_y + (1.5 * text_height) - border_vertical_size ], [ self.__center_x__ + half_width, self.compass_text_y + (1.5 * text_height) + border_vertical_size ], [ self.__center_x__ - half_width, self.compass_text_y + (1.5 * text_height) + border_vertical_size ] ] self.__heading_strip_offset__ = {} for heading in range(0, 181): self.__heading_strip_offset__[heading] = int( self.pixels_per_degree_x * heading) self.__heading_strip__ = {} for heading in range(0, 361): self.__heading_strip__[heading] = self.__generate_heading_strip__( heading) self.__render_heading_mark_timer__ = TaskTimer("HeadingRender") def __generate_heading_strip__(self, heading): things_to_render = [] for heading_strip in self.__heading_strip_offset__: to_the_left = (heading - heading_strip) to_the_right = (heading + heading_strip) displayed_left = utils.apply_declination(to_the_left) displayed_right = utils.apply_declination(to_the_right) if to_the_left < 0: to_the_left += 360 if to_the_right > 360: to_the_right -= 360 if (displayed_left == 0) or ((displayed_left % 90) == 0): line_x_left = self.__center_x__ - \ self.__heading_strip_offset__[heading_strip] things_to_render.append([line_x_left, to_the_left]) if to_the_left == to_the_right: continue if (displayed_right % 90) == 0: line_x_right = self.__center_x__ + \ self.__heading_strip_offset__[heading_strip] things_to_render.append([line_x_right, to_the_right]) return things_to_render def __render_heading_mark__(self, framebuffer, x_pos, heading): pygame.draw.line(framebuffer, display.GREEN, [x_pos, self.line_height], [x_pos, 0], 4) self.__render_heading_text__(framebuffer, utils.apply_declination(heading), x_pos, self.compass_text_y) def render(self, framebuffer, orientation): """ Renders the current heading to the HUD. """ self.task_timer.start() # Render a crude compass # Render a heading strip along the top heading = orientation.get_onscreen_projection_heading() [ self.__render_heading_mark__(framebuffer, heading_mark_to_render[0], heading_mark_to_render[1]) for heading_mark_to_render in self.__heading_strip__[heading] ] # Render the text that is showing our AHRS and GPS headings cover_old_rendering_spaces = " " heading_text = "{0}{1} | {2}{0}".format( cover_old_rendering_spaces, str( int( utils.apply_declination( orientation.get_onscreen_projection_display_heading())) ).rjust(3), str(int(utils.apply_declination( orientation.gps_heading))).rjust(3)) rendered_text = self.__font__.render(heading_text, True, display.GREEN, display.BLACK) text_width, text_height = rendered_text.get_size() framebuffer.blit(rendered_text, (self.__center_x__ - (text_width >> 1), text_height << 1)) pygame.draw.lines(framebuffer, display.GREEN, True, self.__heading_text_box_lines__, 2) self.task_timer.stop() def __render_heading_text__(self, framebuffer, heading, position_x, position_y): """ Renders the text with the results centered on the given position. """ rendered_text, half_size = self.__heading_text__[heading] framebuffer.blit( rendered_text, (position_x - half_size[0], position_y - half_size[1]))
class AdsbOnScreenReticles(AdsbElement): def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__( self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbOnScreenReticles') self.__listing_text_start_y__ = int(self.__font__.get_height() * 4) self.__listing_text_start_x__ = int( self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__max_reports__ = int( (self.__height__ - self.__listing_text_start_y__) / self.__next_line_distance__) self.__top_border__ = int(self.__height__ * 0.1) self.__bottom_border__ = self.__height__ - self.__top_border__ def render(self, framebuffer, orientation): self.task_timer.start() # Get the traffic, and bail out of we have none traffic_reports = AdsbTrafficClient.TRAFFIC_MANAGER.get_traffic_with_position() if traffic_reports is None: self.task_timer.stop() return for traffic in traffic_reports: # Do not render reticles for things to far away if traffic.distance > imperial_occlude: continue if traffic.is_on_ground(): continue identifier = traffic.get_identifer() # Find where to draw the reticle.... reticle_x, reticle_y = self.__get_traffic_projection__( orientation, traffic) # Render using the Above us bug on_screen_reticle_scale = get_reticle_size(traffic.distance) reticle, reticle_size_px = self.get_onscreen_reticle( reticle_x, reticle_y, on_screen_reticle_scale) if reticle_y < self.__top_border__ or reticle_y > self.__bottom_border__ or \ reticle_x < 0 or reticle_x > self.__width__: continue else: reticle_x, reticle_y = self.__rotate_reticle__( [[reticle_x, reticle_y]], orientation.roll)[0] self.__render_target_reticle__( framebuffer, identifier, reticle_x, reticle_y, reticle, orientation.roll, reticle_size_px) self.task_timer.stop() def __render_target_reticle__(self, framebuffer, identifier, center_x, center_y, reticle_lines, roll, reticle_size_px): """ Renders a targetting reticle on the screen. Assumes the X/Y projection has already been performed. """ border_space = int(reticle_size_px * 1.2) if center_y < border_space: center_y = border_space if center_y > (self.__height__ - border_space): center_y = int(self.__height__ - border_space) pygame.draw.lines(framebuffer, BLACK, True, reticle_lines, 20) pygame.draw.lines(framebuffer, RED, True, reticle_lines, 10) def __rotate_reticle__(self, reticle, roll): """ Takes a series of line segments and rotates them (roll) about the screen's center Arguments: reticle {list of tuples} -- The line segments roll {float} -- The amount to rotate about the center by. Returns: list of lists -- The new list of line segments """ # Takes the roll in degrees # Example input.. # [ # [center_x, center_y - size], # [center_x + size, center_y], # [center_x, center_y + size], # [center_x - size, center_y] # ] translated_points = [] int_roll = int(-roll) cos_roll = COS_RADIANS_BY_DEGREES[int_roll] sin_roll = SIN_RADIANS_BY_DEGREES[int_roll] ox, oy = self.__center__ for x_y in reticle: px, py = x_y qx = ox + cos_roll * (px - ox) - sin_roll * (py - oy) qy = oy + sin_roll * (px - ox) + cos_roll * (py - oy) translated_points.append([qx, qy]) return translated_points
class AdsbTrafficListing(AdsbElement): def uses_ahrs(self): """ Does this element use AHRS data to render? Returns: bool -- False as this element does not use AHRS data. """ return False def __init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size): AdsbElement.__init__(self, degrees_of_pitch, pixels_per_degree_y, font, framebuffer_size) self.task_timer = TaskTimer('AdsbTargetBugs') self.__listing_text_start_y__ = int(self.__font__.get_height() * 2) self.__listing_text_start_x__ = int(self.__framebuffer_size__[0] * 0.01) self.__next_line_distance__ = int(font.get_height() * 1.5) self.__max_reports__ = int( (self.__height__ - self.__listing_text_start_y__) / self.__next_line_distance__) self.__top_border__ = int(self.__height__ * 0.2) self.__bottom_border__ = self.__height__ - int(self.__height__ * 0.1) def __get_padded_traffic_reports__(self, traffic_reports): max_identifier_length = 0 max_bearing_length = 0 max_altitude_length = 0 max_distance_length = 0 pre_padded_text = [] max_identifier_length, max_distance_length, max_altitude_length = self.__get_pre_padded_text_reports__( traffic_reports, max_identifier_length, max_bearing_length, max_altitude_length, max_distance_length, pre_padded_text) out_padded_reports = [] for report in pre_padded_text: identifier = report[0] bearing = str(utils.apply_declination(float(report[1]))) distance_text = report[2] altitude = report[3] icao = report[4] # if self.__show_list__: traffic_report = "{0} {1} {2} {3}".format( identifier.ljust(max_identifier_length), bearing.rjust(3), distance_text.rjust(max_distance_length), altitude.rjust(max_altitude_length)) out_padded_reports.append((icao, traffic_report)) return out_padded_reports def __get_pre_padded_text_reports__(self, traffic_reports, max_identifier_length, max_bearing_length, max_altitude_length, max_distance_length, pre_padded_text): report_count = 0 for traffic in traffic_reports: # Do not list traffic too far away if report_count > self.__max_reports__ or traffic.distance > imperial_occlude or traffic.is_on_ground( ): continue report_count += 1 identifier = str(traffic.get_identifer()) altitude_delta = int(traffic.altitude / 100.0) distance_text = self.__get_distance_string__(traffic.distance) delta_sign = '' if altitude_delta > 0: delta_sign = '+' altitude_text = "{0}{1}".format(delta_sign, altitude_delta) bearing_text = "{0:.0f}".format(traffic.bearing) identifier_length = len(identifier) bearing_length = len(bearing_text) altitude_length = len(altitude_text) distance_length = len(distance_text) if identifier_length > max_identifier_length: max_identifier_length = identifier_length if bearing_length > max_bearing_length: max_bearing_length = bearing_length if altitude_length > max_altitude_length: max_altitude_length = altitude_length if distance_length > max_distance_length: max_distance_length = distance_length pre_padded_text.append([ identifier, bearing_text, distance_text, altitude_text, traffic.icao_address ]) return max_identifier_length, max_distance_length, max_altitude_length def render(self, framebuffer, orientation): # Render a heading strip along the top self.task_timer.start() # Get the traffic, and bail out of we have none traffic_reports = AdsbTrafficClient.TRAFFIC_MANAGER.get_traffic_with_position( ) if traffic_reports is None: self.task_timer.stop() return # Render a list of traffic that we have positions # for, along with the tail number y_pos = self.__listing_text_start_y__ x_pos = self.__listing_text_start_x__ padded_traffic_reports = self.__get_padded_traffic_reports__( traffic_reports) if len(padded_traffic_reports) == 0: framebuffer.blit( HudDataCache.get_cached_text_texture("NO TRAFFIC", self.__font__), (x_pos, y_pos)) for identifier, traffic_report in padded_traffic_reports: traffic_text_texture = HudDataCache.get_cached_text_texture( traffic_report, self.__font__) framebuffer.blit(traffic_text_texture, (x_pos, y_pos)) y_pos += self.__next_line_distance__ self.task_timer.stop()