class RadarWidget(QGLWidget): vcount_circle = 36 width = height = 600 viewport = (0, 0, width, height) panlat = 0.0 panlon = 0.0 zoom = 1.0 ar = 1.0 flat_earth = 1.0 wraplon = int(-999) wrapdir = int(0) max_texture_size = 0 do_text = True invalid_count = 0 def __init__(self, navdb, shareWidget=None): super(RadarWidget, self).__init__(shareWidget=shareWidget) self.setAttribute(Qt.WA_AcceptTouchEvents, True) self.grabGesture(Qt.PanGesture) self.grabGesture(Qt.PinchGesture) # self.grabGesture(Qt.SwipeGesture) # The number of aircraft in the simulation self.map_texture = 0 self.naircraft = 0 self.nwaypoints = 0 self.nairports = 0 self.route_acid = "" self.ssd_ownship = np.array([], dtype=np.uint16) self.apt_inrange = np.array([]) self.ssd_all = False self.navdb = navdb self.iactconn = 0 self.nodedata = list() # Display flags self.show_map = True self.show_coast = True self.show_traf = True self.show_pz = False self.show_lbl = True self.show_wpt = True self.show_apt = True self.initialized = False # Connect to manager's nodelist changed and activenode changed signal manager.instance.nodes_changed.connect(self.nodesChanged) manager.instance.activenode_changed.connect(self.actnodeChanged) # Load vertex data self.vbuf_asphalt, self.vbuf_concrete, self.vbuf_runways, self.vbuf_rwythr, \ self.apt_ctrlat, self.apt_ctrlon, self.apt_indices = load_aptsurface() @pyqtSlot(str, tuple, int) def nodesChanged(self, address, nodeid, connidx): # For each node we have to keep data such as the visible polygons, etc. self.nodedata.append(nodeData()) @pyqtSlot(tuple, int) def actnodeChanged(self, nodeid, connidx): self.iactconn = connidx nact = self.nodedata[connidx] if len(nact.polydata) > 0: update_buffer(self.allpolysbuf, nact.polydata) self.allpolys.set_vertex_count(len(nact.polydata) / 2) def create_objects(self): if not self.isValid(): self.invalid_count += 1 print 'Radarwidget: Context not valid in create_objects, count=%d' % self.invalid_count QTimer.singleShot(100, self.create_objects) return # Make the radarwidget context current, necessary when create_objects is not called from initializeGL self.makeCurrent() # Initialize font for radar view with specified settings self.font = Font() self.font.create_font_array(char_height=text_texture_size, font_family=font_family, font_weight=font_weight) self.font.init_shader(self.text_shader) # Load and bind world texture max_texture_size = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE) print 'Maximum supported texture size: %d' % max_texture_size for i in [16384, 8192, 4096]: if max_texture_size >= i: fname = 'data/graphics/world.%dx%d.dds' % (i, i / 2) print 'Loading texture ' + fname self.map_texture = self.bindTexture(fname) break # Create initial empty buffers for aircraft position, orientation, label, and color self.achdgbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.aclatbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.aclonbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.acaltbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.actasbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.accolorbuf = create_empty_buffer(MAX_NAIRCRAFT * 3, usage=gl.GL_STREAM_DRAW) self.aclblbuf = create_empty_buffer(MAX_NAIRCRAFT * 24, usage=gl.GL_STREAM_DRAW) self.confcpabuf = create_empty_buffer(MAX_NCONFLICTS * 16, usage=gl.GL_STREAM_DRAW) self.polyprevbuf = create_empty_buffer(MAX_POLYPREV_SEGMENTS * 8, usage=gl.GL_DYNAMIC_DRAW) self.allpolysbuf = create_empty_buffer(MAX_ALLPOLYS_SEGMENTS * 16, usage=gl.GL_DYNAMIC_DRAW) self.routebuf = create_empty_buffer(MAX_ROUTE_LENGTH * 8, usage=gl.GL_DYNAMIC_DRAW) self.routewplatbuf = create_empty_buffer(MAX_ROUTE_LENGTH * 4, usage=gl.GL_DYNAMIC_DRAW) self.routewplonbuf = create_empty_buffer(MAX_ROUTE_LENGTH * 4, usage=gl.GL_DYNAMIC_DRAW) self.routelblbuf = create_empty_buffer(MAX_ROUTE_LENGTH * 12, usage=gl.GL_DYNAMIC_DRAW) # ------- Map ------------------------------------ self.map = RenderObject(gl.GL_TRIANGLE_FAN, vertex_count=4) mapvertices = np.array([(-90.0, 540.0), (-90.0, -540.0), (90.0, -540.0), (90.0, 540.0)], dtype=np.float32) texcoords = np.array([(1, 3), (1, 0), (0, 0), (0, 3)], dtype=np.float32) self.map.bind_attrib(ATTRIB_VERTEX, 2, mapvertices) self.map.bind_attrib(ATTRIB_TEXCOORDS, 2, texcoords) # ------- Coastlines ----------------------------- self.coastlines = RenderObject(gl.GL_LINES) coastvertices, coastindices = load_coastlines() self.coastlines.bind_attrib(ATTRIB_VERTEX, 2, coastvertices) self.coastlines.bind_attrib(ATTRIB_COLOR, 3, np.array(lightblue2, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.vcount_coast = len(coastvertices) self.coastindices = coastindices del coastvertices # ------- Runways -------------------------------- self.runways = RenderObject(gl.GL_TRIANGLES) self.runways.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_runways) self.runways.bind_attrib(ATTRIB_COLOR, 3, np.array(grey, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.runways.set_vertex_count(len(self.vbuf_runways) / 2) #---------Runway Thresholds----------------------- self.thresholds = RenderObject(gl.GL_TRIANGLES) self.thresholds.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_rwythr) self.thresholds.bind_attrib(ATTRIB_COLOR, 3, np.array(white, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.thresholds.set_vertex_count(len(self.vbuf_rwythr) / 2) # ------- Taxiways ------------------------------- self.taxiways = RenderObject(gl.GL_TRIANGLES) self.taxiways.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_asphalt) self.taxiways.bind_attrib(ATTRIB_COLOR, 3, np.array(grey, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.taxiways.set_vertex_count(len(self.vbuf_asphalt) / 2) # ------- Pavement ------------------------------- self.pavement = RenderObject(gl.GL_TRIANGLES) self.pavement.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_concrete) self.pavement.bind_attrib(ATTRIB_COLOR, 3, np.array(lightgrey, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.pavement.set_vertex_count(len(self.vbuf_concrete) / 2) # Polygon preview object self.polyprev = RenderObject(gl.GL_LINE_LOOP) self.polyprev.bind_attrib(ATTRIB_VERTEX, 2, self.polyprevbuf) self.polyprev.bind_attrib(ATTRIB_COLOR, 3, np.array(lightblue, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # Fixed polygons self.allpolys = RenderObject(gl.GL_LINES) self.allpolys.bind_attrib(ATTRIB_VERTEX, 2, self.allpolysbuf) self.allpolys.bind_attrib(ATTRIB_COLOR, 3, np.array(blue, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # ------- SSD object ----------------------------- self.ssd = RenderObject(gl.GL_POINTS) self.ssd.bind_attrib(ATTRIB_LAT0, 1, self.aclatbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_LON0, 1, self.aclonbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_ALT0, 1, self.acaltbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_TAS0, 1, self.actasbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_TRK0, 1, self.achdgbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_LAT1, 1, self.aclatbuf) self.ssd.bind_attrib(ATTRIB_LON1, 1, self.aclonbuf) self.ssd.bind_attrib(ATTRIB_ALT1, 1, self.acaltbuf) self.ssd.bind_attrib(ATTRIB_TAS1, 1, self.actasbuf) self.ssd.bind_attrib(ATTRIB_TRK1, 1, self.achdgbuf) # ------- Circle --------------------------------- # Create a new VAO (Vertex Array Object) and bind it self.protectedzone = RenderObject(gl.GL_LINE_LOOP, vertex_count=self.vcount_circle) circlevertices = np.transpose( np.array( (2.5 * nm * np.cos(np.linspace(0.0, 2.0 * np.pi, self.vcount_circle)), 2.5 * nm * np.sin(np.linspace(0.0, 2.0 * np.pi, self.vcount_circle))), dtype=np.float32)) self.protectedzone.bind_attrib(ATTRIB_VERTEX, 2, circlevertices) self.protectedzone.bind_attrib(ATTRIB_LAT, 1, self.aclatbuf, instance_divisor=1) self.protectedzone.bind_attrib(ATTRIB_LON, 1, self.aclonbuf, instance_divisor=1) self.protectedzone.bind_attrib(ATTRIB_COLOR, 3, self.accolorbuf, datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # ------- A/C symbol ----------------------------- self.ac_symbol = RenderObject(gl.GL_TRIANGLE_FAN, vertex_count=4) acvertices = np.array([(0.0, 0.5 * ac_size), (-0.5 * ac_size, -0.5 * ac_size), (0.0, -0.25 * ac_size), (0.5 * ac_size, -0.5 * ac_size)], dtype=np.float32) self.ac_symbol.bind_attrib(ATTRIB_VERTEX, 2, acvertices) self.ac_symbol.bind_attrib(ATTRIB_LAT, 1, self.aclatbuf, instance_divisor=1) self.ac_symbol.bind_attrib(ATTRIB_LON, 1, self.aclonbuf, instance_divisor=1) self.ac_symbol.bind_attrib(ATTRIB_ORIENTATION, 1, self.achdgbuf, instance_divisor=1) self.ac_symbol.bind_attrib(ATTRIB_COLOR, 3, self.accolorbuf, datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.aclabels = self.font.prepare_text_instanced( self.aclblbuf, (8, 3), self.aclatbuf, self.aclonbuf, self.accolorbuf, char_size=text_size, vertex_offset=(ac_size, -0.5 * ac_size)) # ------- Conflict CPA lines --------------------- self.cpalines = RenderObject(gl.GL_LINES) self.cpalines.bind_attrib(ATTRIB_VERTEX, 2, self.confcpabuf) self.cpalines.bind_attrib(ATTRIB_COLOR, 3, np.array(amber, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # ------- Aircraft Route ------------------------- self.route = RenderObject(gl.GL_LINES) self.route.bind_attrib(ATTRIB_VERTEX, 2, self.routebuf) self.route.bind_attrib(ATTRIB_COLOR, 3, np.array(magenta, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.routelbl = self.font.prepare_text_instanced( self.routelblbuf, (12, 1), self.routewplatbuf, self.routewplonbuf, char_size=text_size, vertex_offset=(wpt_size, 0.5 * wpt_size)) # ------- Waypoints ------------------------------ self.nwaypoints = len(self.navdb.wplat) self.waypoints = RenderObject(gl.GL_LINE_LOOP, vertex_count=3, n_instances=self.nwaypoints) wptvertices = np.array([(0.0, 0.5 * wpt_size), (-0.5 * wpt_size, -0.5 * wpt_size), (0.5 * wpt_size, -0.5 * wpt_size)], dtype=np.float32) # a triangle self.waypoints.bind_attrib(ATTRIB_VERTEX, 2, wptvertices) # Sort based on id string length llid = sorted(zip(self.navdb.wpid, self.navdb.wplat, self.navdb.wplon), key=lambda i: len(i[0]) > 3) wplat = [lat for (wpid, lat, lon) in llid] wplon = [lon for (wpid, lon, lon) in llid] self.wptlatbuf = self.waypoints.bind_attrib(ATTRIB_LAT, 1, np.array(wplat, dtype=np.float32), instance_divisor=1) self.wptlonbuf = self.waypoints.bind_attrib(ATTRIB_LON, 1, np.array(wplon, dtype=np.float32), instance_divisor=1) wptids = '' self.nnavaids = 0 for wptid in llid: if len(wptid[0]) <= 3: self.nnavaids += 1 wptids += wptid[0].ljust(5) self.wptlabels = self.font.prepare_text_instanced( np.array(wptids, dtype=np.string_), (5, 1), self.wptlatbuf, self.wptlonbuf, char_size=text_size, vertex_offset=(wpt_size, 0.5 * wpt_size)) del wptids # ------- Airports ------------------------------- self.nairports = len(self.navdb.aptlat) self.airports = RenderObject(gl.GL_LINE_LOOP, vertex_count=4, n_instances=self.nairports) aptvertices = np.array([(-0.5 * apt_size, -0.5 * apt_size), (0.5 * apt_size, -0.5 * apt_size), (0.5 * apt_size, 0.5 * apt_size), (-0.5 * apt_size, 0.5 * apt_size)], dtype=np.float32) # a square self.airports.bind_attrib(ATTRIB_VERTEX, 2, aptvertices) indices = self.navdb.aptype.argsort() aplat = np.array(self.navdb.aptlat[indices], dtype=np.float32) aplon = np.array(self.navdb.aptlon[indices], dtype=np.float32) aptypes = self.navdb.aptype[indices] apnames = np.array(self.navdb.aptid) apnames = apnames[indices] # The number of large, large+med, and large+med+small airports self.nairports = [ aptypes.searchsorted(2), aptypes.searchsorted(3), self.nairports ] self.aptlatbuf = self.airports.bind_attrib(ATTRIB_LAT, 1, aplat, instance_divisor=1) self.aptlonbuf = self.airports.bind_attrib(ATTRIB_LON, 1, aplon, instance_divisor=1) aptids = '' for aptid in apnames: aptids += aptid.ljust(4) self.aptlabels = self.font.prepare_text_instanced( np.array(aptids, dtype=np.string_), (4, 1), self.aptlatbuf, self.aptlonbuf, char_size=text_size, vertex_offset=(apt_size, 0.5 * apt_size)) del aptids # Unbind VAO, VBO RenderObject.unbind_all() # Set initial values for the global uniforms self.globaldata.set_wrap(self.wraplon, self.wrapdir) self.globaldata.set_pan_and_zoom(self.panlat, self.panlon, self.zoom) # Clean up memory del self.vbuf_asphalt, self.vbuf_concrete, self.vbuf_runways, self.vbuf_rwythr self.initialized = True def initializeGL(self): """Initialize OpenGL, VBOs, upload data on the GPU, etc.""" # First check for supported GL version gl_version = float(gl.glGetString(gl.GL_VERSION)[:3]) if gl_version < 3.3: print('OpenGL context created with GL version %.1f' % gl_version) qCritical( """Your system reports that it supports OpenGL up to version %.1f. The minimum requirement for BlueSky is OpenGL 3.3. Generally, AMD/ATI/nVidia cards from 2008 and newer support OpenGL 3.3, and Intel integrated graphics from the Haswell generation and newer. If you think your graphics system should be able to support GL>=3.3 please open an issue report on the BlueSky Github page (https://github.com/ProfHoekstra/bluesky/issues)""" % gl_version) return # background color gl.glClearColor(0, 0, 0, 0) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) self.globaldata = radarUBO() try: # Compile shaders and link color shader program self.color_shader = BlueSkyProgram( 'data/graphics/shaders/radarwidget-normal.vert', 'data/graphics/shaders/radarwidget-color.frag') self.color_shader.bind_uniform_buffer('global_data', self.globaldata) # Compile shaders and link texture shader program self.texture_shader = BlueSkyProgram( 'data/graphics/shaders/radarwidget-normal.vert', 'data/graphics/shaders/radarwidget-texture.frag') self.texture_shader.bind_uniform_buffer('global_data', self.globaldata) # Compile shaders and link text shader program self.text_shader = BlueSkyProgram( 'data/graphics/shaders/radarwidget-text.vert', 'data/graphics/shaders/radarwidget-text.frag') self.text_shader.bind_uniform_buffer('global_data', self.globaldata) self.ssd_shader = BlueSkyProgram('data/graphics/shaders/ssd.vert', 'data/graphics/shaders/ssd.frag', 'data/graphics/shaders/ssd.geom') self.ssd_shader.bind_uniform_buffer('global_data', self.globaldata) self.ssd_shader.loc_vlimits = gl.glGetUniformLocation( self.ssd_shader.program, 'Vlimits') self.ssd_shader.loc_nac = gl.glGetUniformLocation( self.ssd_shader.program, 'n_ac') except RuntimeError as e: qCritical('Error compiling shaders in radarwidget: ' + e.args[0]) return # create all vertex array objects self.create_objects() def paintGL(self): """Paint the scene.""" # pass if the framebuffer isn't complete yet or if not initialized if not (gl.glCheckFramebufferStatus( gl.GL_FRAMEBUFFER) == gl.GL_FRAMEBUFFER_COMPLETE and self.initialized and self.isVisible()): return # Set the viewport and clear the framebuffer gl.glViewport(*self.viewport) gl.glClear(gl.GL_COLOR_BUFFER_BIT) # Send the (possibly) updated global uniforms to the buffer self.globaldata.set_vertex_scale_type(VERTEX_IS_LATLON) # --- DRAW THE MAP AND COASTLINES --------------------------------------------- # Map and coastlines: don't wrap around in the shader self.globaldata.enable_wrap(False) if self.show_map: # Select the texture shader self.texture_shader.use() # Draw map texture gl.glActiveTexture(gl.GL_TEXTURE0 + 0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.map_texture) self.map.draw() # Select the non-textured shader self.color_shader.use() # Draw coastlines if self.show_coast: if self.wrapdir == 0: # Normal case, no wrap around self.coastlines.draw(first_vertex=0, vertex_count=self.vcount_coast) else: self.coastlines.bind() wrapindex = np.uint32(self.coastindices[int(self.wraplon) + 180]) if self.wrapdir == 1: gl.glVertexAttrib1f(ATTRIB_LON, 360.0) self.coastlines.draw(first_vertex=0, vertex_count=wrapindex) gl.glVertexAttrib1f(ATTRIB_LON, 0.0) self.coastlines.draw(first_vertex=wrapindex, vertex_count=self.vcount_coast - wrapindex) else: gl.glVertexAttrib1f(ATTRIB_LON, -360.0) self.coastlines.draw(first_vertex=wrapindex, vertex_count=self.vcount_coast - wrapindex) gl.glVertexAttrib1f(ATTRIB_LON, 0.0) self.coastlines.draw(first_vertex=0, vertex_count=wrapindex) # --- DRAW PREVIEW SHAPE (WHEN AVAILABLE) ----------------------------- self.polyprev.draw() # --- DRAW CUSTOM SHAPES (WHEN AVAILABLE) ----------------------------- self.allpolys.draw() # --- DRAW THE SELECTED AIRCRAFT ROUTE (WHEN AVAILABLE) --------------- if self.show_traf: self.route.draw() self.cpalines.draw() # --- DRAW AIRPORT DETAILS (RUNWAYS, TAXIWAYS, PAVEMENTS) ------------- self.runways.draw() self.thresholds.draw() if self.zoom >= 1.0: for idx in self.apt_inrange: self.taxiways.draw(first_vertex=idx[0], vertex_count=idx[1]) self.pavement.draw(first_vertex=idx[2], vertex_count=idx[3]) # --- DRAW THE INSTANCED AIRCRAFT SHAPES ------------------------------ # update wrap longitude and direction for the instanced objects self.globaldata.enable_wrap(True) # PZ circles only when they are bigger than the A/C symbols if self.naircraft > 0 and self.show_traf and self.show_pz and self.zoom >= 0.15: self.globaldata.set_vertex_scale_type(VERTEX_IS_METERS) self.protectedzone.draw(n_instances=self.naircraft) self.globaldata.set_vertex_scale_type(VERTEX_IS_SCREEN) # Draw traffic symbols if self.naircraft > 0 and self.show_traf: self.ac_symbol.draw(n_instances=self.naircraft) if self.zoom >= 0.5: nairports = self.nairports[2] elif self.zoom >= 0.25: nairports = self.nairports[1] else: nairports = self.nairports[0] if self.zoom >= 3: nwaypoints = self.nwaypoints else: nwaypoints = self.nnavaids # Draw waypoint symbols if self.show_wpt: self.waypoints.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue3 + (255, ))) self.waypoints.draw(n_instances=nwaypoints) # Draw airport symbols if self.show_apt: self.airports.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue3 + (255, ))) self.airports.draw(n_instances=nairports) if self.do_text: self.text_shader.use() self.font.use() if self.show_apt: self.font.set_char_size(self.aptlabels.char_size) self.font.set_block_size(self.aptlabels.block_size) self.aptlabels.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue4 + (255, ))) self.aptlabels.draw(n_instances=nairports) if self.show_wpt: self.font.set_char_size(self.wptlabels.char_size) self.font.set_block_size(self.wptlabels.block_size) self.wptlabels.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue4 + (255, ))) self.wptlabels.draw(n_instances=nwaypoints) if self.show_traf and self.route.vertex_count > 1: gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(magenta + (255, ))) self.font.set_char_size(self.routelbl.char_size) self.font.set_block_size(self.routelbl.block_size) self.routelbl.draw() if self.naircraft > 0 and self.show_traf and self.show_lbl: self.font.set_char_size(self.aclabels.char_size) self.font.set_block_size(self.aclabels.block_size) self.aclabels.draw(n_instances=self.naircraft) # SSD if self.ssd_all or len(self.ssd_ownship) > 0: self.ssd_shader.use() gl.glUniform3f(self.ssd_shader.loc_vlimits, 4e4, 25e4, 500.0) gl.glUniform1i(self.ssd_shader.loc_nac, self.naircraft) if self.ssd_all: self.ssd.draw(first_vertex=0, vertex_count=self.naircraft, n_instances=self.naircraft) else: self.ssd.draw(first_vertex=self.ssd_ownship[-1], vertex_count=1, n_instances=self.naircraft) # Unbind everything RenderObject.unbind_all() gl.glUseProgram(0) def resizeGL(self, width, height): """Called upon window resizing: reinitialize the viewport.""" if not self.initialized: return # update the window size # Qt5 supports getting the device pixel ratio, which can be > 1 for HiDPI displays such as Mac Retina screens pixel_ratio = 1 if QT_VERSION >= 5: pixel_ratio = self.devicePixelRatio() # Calculate zoom so that the window resize doesn't affect the scale, but only enlarges or shrinks the view zoom = float(self.width) / float(width) * pixel_ratio origin = (width / 2, height / 2) # Update width, height, and aspect ratio self.width, self.height = width / pixel_ratio, height / pixel_ratio self.ar = float(width) / max(1, float(height)) self.globaldata.set_win_width_height(self.width, self.height) self.viewport = (0, 0, width, height) # Update zoom self.event(PanZoomEvent(zoom=zoom, origin=origin)) def update_route_data(self, data): self.route_acid = data.acid if data.acid != "" and len(data.wplat) > 0: nsegments = len(data.wplat) data.iactwp = min(max(0, data.iactwp), nsegments - 1) self.routelbl.n_instances = nsegments self.route.set_vertex_count(2 * nsegments) routedata = np.empty(4 * nsegments, dtype=np.float32) routedata[0:4] = [ data.aclat, data.aclon, data.wplat[data.iactwp], data.wplon[data.iactwp] ] routedata[4::4] = data.wplat[:-1] routedata[5::4] = data.wplon[:-1] routedata[6::4] = data.wplat[1:] routedata[7::4] = data.wplon[1:] update_buffer(self.routebuf, routedata) update_buffer(self.routewplatbuf, np.array(data.wplat, dtype=np.float32)) update_buffer(self.routewplonbuf, np.array(data.wplon, dtype=np.float32)) wpname = [] for wp in data.wpname: wpname += wp[:12].ljust(12) update_buffer(self.routelblbuf, np.array(wpname)) else: self.route.set_vertex_count(0) def update_aircraft_data(self, data): self.naircraft = len(data.lat) if self.naircraft == 0: self.cpalines.set_vertex_count(0) else: # Update data in GPU buffers update_buffer(self.aclatbuf, np.array(data.lat, dtype=np.float32)) update_buffer(self.aclonbuf, np.array(data.lon, dtype=np.float32)) update_buffer(self.achdgbuf, np.array(data.trk, dtype=np.float32)) update_buffer(self.acaltbuf, np.array(data.alt, dtype=np.float32)) update_buffer(self.actasbuf, np.array(data.tas, dtype=np.float32)) # CPA lines to indicate conflicts ncpalines = len(data.confcpalat) cpalines = np.zeros(4 * ncpalines, dtype=np.float32) self.cpalines.set_vertex_count(2 * ncpalines) # Labels and colors rawlabel = '' color = np.empty((self.naircraft, 3), dtype=np.uint8) for i in range(self.naircraft): if np.isnan(data.tas[i]): print 'CAS NaN in %d: %s' % (i, data.id[i]) data.cas[i] = 0.0 if np.isnan(data.alt[i]): print 'ALT NaN in %d: %s' % (i, data.id[i]) data.alt[i] = 0.0 # Make label: 3 lines of 8 characters per aircraft rawlabel += '%-8sFL%03d %-8d' % (data.id[i][:8], int(data.alt[i] / ft / 100), int(data.cas[i] / kts)) confindices = data.iconf[i] if len(confindices) > 0: color[i, :] = amber for confidx in confindices: cpalines[4 * confidx:4 * confidx + 4] = [ data.lat[i], data.lon[i], data.confcpalat[confidx], data.confcpalon[confidx] ] else: color[i, :] = green update_buffer(self.confcpabuf, cpalines) update_buffer(self.accolorbuf, color) update_buffer(self.aclblbuf, np.array(rawlabel, dtype=np.string_)) # If there is a visible route, update the start position if self.route_acid != "": if self.route_acid in data.id: idx = data.id.index(self.route_acid) update_buffer( self.routebuf, np.array([data.lat[idx], data.lon[idx]], dtype=np.float32)) def show_ssd(self, arg): if arg == 'ALL': self.ssd_all = True elif arg == 'OFF': self.ssd_all = False self.ssd_ownship = np.array([], dtype=np.uint16) else: if arg in self.ssd_ownship: self.ssd_ownship = np.delete( self.ssd_ownship, np.argmax(self.ssd_ownship == arg)) else: self.ssd_ownship = np.append(self.ssd_ownship, arg) def updatePolygon(self, name, data_in): nact = self.nodedata[manager.sender()[0]] if name in nact.polynames: # We're either updating a polygon, or deleting it. In both cases # we remove the current one. nact.polydata = np.delete(nact.polydata, range(*nact.polynames[name])) del nact.polynames[name] if data_in is not None: nact.polynames[name] = (len(nact.polydata), 2 * len(data_in)) newbuf = np.empty(2 * len(data_in), dtype=np.float32) newbuf[0::4] = data_in[0::2] # lat newbuf[1::4] = data_in[1::2] # lon newbuf[2:-2:4] = data_in[2::2] # lat newbuf[3:-3:4] = data_in[3::2] # lon newbuf[-2:] = data_in[0:2] nact.polydata = np.append(nact.polydata, newbuf) update_buffer(self.allpolysbuf, nact.polydata) self.allpolys.set_vertex_count(len(nact.polydata) / 2) def previewpoly(self, shape_type, data_in=None): if shape_type is None: self.polyprev.set_vertex_count(0) return if shape_type in ['BOX', 'AREA']: # For a box (an area is a box) we need to add two additional corners data = np.zeros(8, dtype=np.float32) data[0:2] = data_in[0:2] data[2:4] = data_in[2], data_in[1] data[4:6] = data_in[2:4] data[6:8] = data_in[0], data_in[3] else: data = data_in update_buffer(self.polyprevbuf, data) self.polyprev.set_vertex_count(len(data) / 2) def airportsInRange(self): ll_range = max(1.5 / self.zoom, 1.0) indices = np.logical_and.reduce( (self.apt_ctrlat >= self.panlat - ll_range, self.apt_ctrlat <= self.panlat + ll_range, self.apt_ctrlon >= self.panlon - ll_range, self.apt_ctrlon <= self.panlon + ll_range)) self.apt_inrange = self.apt_indices[indices] def pixelCoordsToGLxy(self, x, y): """Convert screen pixel coordinates to GL projection coordinates (x, y range -1 -- 1) """ # GL coordinates (x, y range -1 -- 1) glx = (float(2.0 * x) / self.width - 1.0) gly = -(float(2.0 * y) / self.height - 1.0) return glx, gly def pixelCoordsToLatLon(self, x, y): """Convert screen pixel coordinates to lat/lon coordinates """ glx, gly = self.pixelCoordsToGLxy(x, y) # glxy = zoom * (latlon - pan) # latlon = pan + glxy / zoom lat = self.panlat + gly / (self.zoom * self.ar) lon = self.panlon + glx / (self.zoom * self.flat_earth) return lat, lon def event(self, event): if event.type() == PanZoomEventType: if event.pan is not None: # Absolute pan operation if event.absolute: self.panlat = event.pan[0] self.panlon = event.pan[1] # Relative pan operation else: self.panlat += event.pan[0] self.panlon += event.pan[1] # Don't pan further than the poles in y-direction self.panlat = min( max(self.panlat, -90.0 + 1.0 / (self.zoom * self.ar)), 90.0 - 1.0 / (self.zoom * self.ar)) # Update flat-earth factor and possibly zoom in case of very wide windows (> 2:1) self.flat_earth = np.cos(np.deg2rad(self.panlat)) self.zoom = max(self.zoom, 1.0 / (180.0 * self.flat_earth)) if event.zoom is not None: if event.absolute: # Limit zoom extents in x-direction to [-180:180], and in y-direction to [-90:90] self.zoom = max( event.zoom, 1.0 / min(90.0 * self.ar, 180.0 * self.flat_earth)) else: prevzoom = self.zoom glx, gly = self.pixelCoordsToGLxy(event.origin[0], event.origin[1]) self.zoom *= event.zoom # Limit zoom extents in x-direction to [-180:180], and in y-direction to [-90:90] self.zoom = max( self.zoom, 1.0 / min(90.0 * self.ar, 180.0 * self.flat_earth)) # Correct pan so that zoom actions are around the mouse position, not around 0, 0 # glxy / zoom1 - pan1 = glxy / zoom2 - pan2 # pan2 = pan1 + glxy (1/zoom2 - 1/zoom1) self.panlon = self.panlon - glx * ( 1.0 / self.zoom - 1.0 / prevzoom) / self.flat_earth self.panlat = self.panlat - gly * ( 1.0 / self.zoom - 1.0 / prevzoom) / self.ar # Don't pan further than the poles in y-direction self.panlat = min( max(self.panlat, -90.0 + 1.0 / (self.zoom * self.ar)), 90.0 - 1.0 / (self.zoom * self.ar)) # Update flat-earth factor self.flat_earth = np.cos(np.deg2rad(self.panlat)) if self.zoom >= 1.0: self.airportsInRange() event.accept() # Check for necessity wrap-around in x-direction self.wraplon = -999.9 self.wrapdir = 0 if self.panlon + 1.0 / (self.zoom * self.flat_earth) < -180.0: # The left edge of the map has passed the right edge of the screen: we can just change the pan position self.panlon += 360.0 elif self.panlon - 1.0 / (self.zoom * self.flat_earth) < -180.0: # The left edge of the map has passed the left edge of the screen: we need to wrap around to the left self.wraplon = float( np.ceil(360.0 + self.panlon - 1.0 / (self.zoom * self.flat_earth))) self.wrapdir = -1 elif self.panlon - 1.0 / (self.zoom * self.flat_earth) > 180.0: # The right edge of the map has passed the left edge of the screen: we can just change the pan position self.panlon -= 360.0 elif self.panlon + 1.0 / (self.zoom * self.flat_earth) > 180.0: # The right edge of the map has passed the right edge of the screen: we need to wrap around to the right self.wraplon = float( np.floor(-360.0 + self.panlon + 1.0 / (self.zoom * self.flat_earth))) self.wrapdir = 1 self.globaldata.set_wrap(self.wraplon, self.wrapdir) # update pan and zoom on GPU for all shaders self.globaldata.set_pan_and_zoom(self.panlat, self.panlon, self.zoom) return True else: return super(RadarWidget, self).event(event)
class BlipDriver(QGLWidget): def __init__(self, parent=None): super(BlipDriver, self).__init__(parent=parent) self.initialized = False self.width = 800 self.height = 800 self.ndviewport = (100, 200, 600, 600) self.mcpviewport = (0, 0, 800, 200) self.btn_state = [False] * 14 self.resize(800, 800) self.setMouseTracking(True) self.drag_start = (0, 0) self.btn_pressed = None self.selValues = 5 * [ 0 ] # [0:courseLeftRight, 1:spd, 2:hdg, 3:alt, 4:vs] self.rate = 0.0 self.remainder = 0.0 self.updownpos = None def create_objects(self): self.mcp = RenderObject(gl.GL_TRIANGLES, vertex=np.array(rect(-1, -1, 2, 2), dtype=np.float32)) self.mcp_texture = load_texture('mcp737.png') self.btn_tex = load_texture('btn_led.png') self.lcd_tex = load_lcd_font() v_mcp_text = [] for pos in [ 0.0, 0.03, 0.06, 0.31, 0.34, 0.37, 0.4, 0.43, 0.644, 0.674, 0.704, 0.955, 0.985, 1.015, 1.045, 1.075, 1.22, 1.25, 1.28, 1.31, 1.34, 1.69, 1.72, 1.75 ]: v, t = Font.char(-0.886 + pos, 0.45, 0.03, 0.25) v_mcp_text += v self.mcp_text = RenderObject(gl.GL_TRIANGLES, vertex=np.array(v_mcp_text, dtype=np.float32)) self.lcd_charcoords = np.zeros(24 * 6, dtype=np.float32) self.lcdbuf = self.mcp_text.bind_attrib(1, 1, self.lcd_charcoords, datatype=gl.GL_FLOAT) btn_leds = [] for pos in [(-0.74, -0.75), (-0.645, -0.75), (-0.37, -0.75), (-0.232, -0.75), (-0.09, -0.75), (0.105, -0.75), (0.2, -0.75), (-0.37, 0.5), (-0.09, 0.5), (-0.09, -0.125), (0.575, 0.34), (0.575, -0.34), (0.684, 0.34), (0.684, -0.34)]: btn_leds += rect(pos[0], pos[1], 0.055, 0.075) btn_color = np.zeros(14 * 6 * 4, dtype=np.uint8) self.btn_leds = RenderObject(gl.GL_TRIANGLES, vertex=np.array(btn_leds, dtype=np.float32), color=btn_color) # Button up/down indicator w, h, offset = 0.04, 0.16, 0.04 triangles = np.array([ -w, offset, 0.0, offset + h, w, offset, -w, -offset, 0.0, -offset - h, w, -offset ], dtype=np.float32) col_triangles = np.array(6 * [255, 255, 255, 180], dtype=np.uint8) self.updown = RenderObject(gl.GL_TRIANGLES, vertex=triangles, color=col_triangles) # Use the same font as the radarwidget self.font = Font() self.font.create_font_array('../../data/graphics/font/') self.font.init_shader(self.text_shader) edge = np.zeros(120, dtype=np.float32) edge[0:120:2] = 1.4 * np.sin(np.radians(np.arange(-60, 60, 2))) edge[1:120:2] = 1.4 * np.cos(np.radians(np.arange(-60, 60, 2))) self.edge = RenderObject(gl.GL_LINE_STRIP, vertex=edge, color=white) arcs = [] for i in range(1, 4): for angle in range(-60, 60, max(2, 6 - 2 * i)): arcs.append(float(i) * 0.35 * sin(radians(angle))) arcs.append(float(i) * 0.35 * cos(radians(angle))) if i == 4: arcs.append(float(i) * 0.35 * sin(radians(angle + 2))) arcs.append(float(i) * 0.35 * cos(radians(angle + 2))) arcs = np.array(arcs, dtype=np.float32) self.arcs = RenderObject(gl.GL_LINES, vertex=arcs, color=white) mask = [] for angle in range(-60, 60, 2): mask.append(1.4 * sin(radians(angle))) mask.append(10.0) mask.append(1.4 * sin(radians(angle))) mask.append(1.4 * cos(radians(angle))) mask = np.array(mask, dtype=np.float32) self.mask = RenderObject(gl.GL_TRIANGLE_STRIP, vertex=mask, color=black) ticks = np.zeros(288, dtype=np.float32) for i in range(72): ticktop = 1.46 if i % 6 == 0 else (1.44 if i % 2 == 0 else 1.42) ticks[4 * i:4 * i + 2] = (1.4 * sin(radians(i * 5)), 1.4 * cos(radians(i * 5))) ticks[4 * i + 2:4 * i + 4] = (ticktop * sin(radians(i * 5)), ticktop * cos(radians(i * 5))) self.ticks = RenderObject(gl.GL_LINES, vertex=ticks, color=white) ticklbls = np.zeros(24 * 36, dtype=np.float32) texcoords = np.zeros(36 * 36, dtype=np.float32) for i in range(36): if i % 3 == 0: w, h, y = 0.045, 0.09, 1.48 else: w, h, y = 0.035, 0.07, 1.46 tmp = [(-w, h + y), (-w, y), (0.0, h + y), (0.0, h + y), (-w, y), (0.0, y), (0.0, h + y), (0.0, y), (w, h + y), (w, h + y), (0.0, y), (w, y)] # numerics start at ASCII 48 c1 = i / 10 + 48 c2 = i % 10 + 48 texcoords[36 * i:36 * i + 18] = [ 0, 0, c1, 0, 1, c1, 1, 0, c1, 1, 0, c1, 0, 1, c1, 1, 1, c1 ] texcoords[36 * i + 18:36 * i + 36] = [ 0, 0, c2, 0, 1, c2, 1, 0, c2, 1, 0, c2, 0, 1, c2, 1, 1, c2 ] angle = radians(10 * (36 - i)) rot = np.array([[cos(angle), -sin(angle)], [sin(angle), cos(angle)]]) for j in range(12): ticklbls[24 * i + 2 * j:24 * i + 2 * j + 2] = rot.dot(tmp[j]) self.ticklbls = RenderObject(gl.GL_TRIANGLES, vertex=ticklbls, color=white, texcoords=texcoords) vown = np.array([ 0.0, 0.0, 0.0, -0.12, 0.065, -0.03, -0.065, -0.03, 0.022, -0.1, -0.022, -0.1 ], dtype=np.float32) self.ownship = RenderObject(gl.GL_LINES, vertex=vown, color=yellow) self.spdlabel_text = self.font.prepare_text_string( 'GS TAS', 0.05, white, (-0.98, 1.6)) self.spdlabel_val = self.font.prepare_text_string( ' 000 000', 0.05, green, (-0.97, 1.6)) self.initialized = True # Unbind everything RenderObject.unbind_all() def setAircraftID(self, ac_id): self.ac_id = ac_id self.setWindowTitle(ac_id) def update_aircraft_data(self, ownid, ownlat, ownlon, owntas, ownhdg, n_aircraft): self.globaldata.set_owndata(ownid, ownlat, ownlon, ownhdg) self.n_aircraft = n_aircraft def initializeGL(self): """Initialize OpenGL, VBOs, upload data on the GPU, etc.""" # First check for supported GL version gl_version = float(gl.glGetString(gl.GL_VERSION)[:3]) if gl_version < 3.3: return # background color gl.glClearColor(0, 0, 0, 0) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) # self.mcp_data = mcpUBO() self.globaldata = ndUBO() self.mcp_col_shader = BlueSkyProgram('mcp.vert', 'color.frag') self.mcp_tex_shader = BlueSkyProgram('mcp.vert', 'texture.frag') self.mcp_txt_shader = BlueSkyProgram('mcp_text.vert', 'mcp_text.frag') self.color_shader = BlueSkyProgram('normal.vert', 'color.frag') self.text_shader = BlueSkyProgram('text.vert', 'text.frag') self.text_shader.bind_uniform_buffer('global_data', self.globaldata) self.create_objects() self.update_lcd() def resizeGL(self, width, height): pixel_ratio = 1 if QT_VERSION >= 5: pixel_ratio = self.devicePixelRatio() self.width, self.height = width / pixel_ratio, height / pixel_ratio hmcp = height / 10 * 2 # paint ND within the largest possible rectangular area in the window wnd = hnd = min(width, height - hmcp) xnd = max(0, (width - wnd) / 2) ynd = hmcp + max(0, (height - hmcp - hnd) / 2) self.mcpviewport = (0, 0, width, hmcp) self.ndviewport = (xnd, ynd, wnd, hnd) def paintGL(self): """Paint the scene.""" # pass if the framebuffer isn't complete yet or if not initialized if not (gl.glCheckFramebufferStatus( gl.GL_FRAMEBUFFER) == gl.GL_FRAMEBUFFER_COMPLETE and self.isVisible() and self.initialized): return gl.glClearColor(0, 0, 0, 0) gl.glClear(gl.GL_COLOR_BUFFER_BIT) # Set the viewport and clear the framebuffer # --- Draw the ND in its viewport ---- gl.glViewport(*self.ndviewport) self.color_shader.use() self.arcs.draw() self.ownship.draw() self.mask.draw() self.edge.draw() self.ticks.draw() # Select the text shader self.text_shader.use() self.font.use() self.font.set_block_size((0, 0)) self.globaldata.set_vertex_modifiers(VERTEX_IS_GLXY, True) self.ticklbls.draw() self.globaldata.set_vertex_modifiers(VERTEX_IS_GLXY, False) self.spdlabel_text.draw() self.spdlabel_val.draw() # --- Draw the MCP in its viewport --- gl.glViewport(*self.mcpviewport) self.mcp_tex_shader.use() gl.glActiveTexture(gl.GL_TEXTURE0 + 0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.mcp_texture) self.mcp.draw() gl.glBindTexture(gl.GL_TEXTURE_2D, self.btn_tex) self.btn_leds.draw() if self.updownpos is not None: self.mcp_col_shader.use() gl.glVertexAttrib2f(2, *self.updownpos) self.updown.draw() gl.glVertexAttrib2f(2, 0.0, 0.0) self.mcp_txt_shader.use() gl.glActiveTexture(gl.GL_TEXTURE0 + 0) gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.lcd_tex) self.mcp_text.draw() # Unbind everything RenderObject.unbind_all() gl.glUseProgram(0) def update_lcd(self): # [0:courseLeftRight, 1:spd, 2:hdg, 3:alt, 4:vs] val = self.selValues chars = '%03d' % (int(val[0]) % 360) chars += '% 5d' % max(0, val[1]) chars += '%03d' % (int(val[2]) % 360) chars += '% 5d' % min(99999, max(0, val[3])) chars += ' ' if val[4] == 0 else '% 5d' % min( 9999, max(-9999, val[4])) chars += '%03d' % (int(val[0]) % 360) indices = [] for c in chars: if c == ' ': indices += [0] * 6 elif c == '-': indices += [1] * 6 elif c == '.': indices += [2] * 6 else: indices += [int(c) + 3] * 6 update_buffer(self.lcdbuf, np.array(indices, dtype=np.float32)) def event(self, event): if not self.initialized: return super(BlipDriver, self).event(event) self.makeCurrent() if event.type() in [ QEvent.MouseMove, QEvent.MouseButtonPress, QEvent.MouseButtonRelease ]: px = 2.0 * (event.x()) / self.width - 1.0 py = 10.0 * (self.height - event.y()) / self.height - 1.0 if event.type( ) == QEvent.MouseButtonPress and event.button() & Qt.LeftButton: self.btn_pressed, _ = check_knob(px, py) if self.btn_pressed is not None: self.drag_start = event.x(), event.y() QTimer.singleShot(100, self.updateAPValues) elif event.type( ) == QEvent.MouseButtonRelease and event.button() & Qt.LeftButton: # print px, py self.btn_pressed = None col_triangles = 6 * [255, 255, 255, 180] update_buffer(self.updown.colorbuf, np.array(col_triangles, dtype=np.uint8)) # MCP button clicked? name, idx = check_btn(px, py) if name is not None: self.btn_state[idx] = not self.btn_state[idx] btn_color = [] for b in self.btn_state: btn_color += 24 * [255] if b else 24 * [0] update_buffer(self.btn_leds.colorbuf, np.array(btn_color, dtype=np.uint8)) elif event.type() == QEvent.MouseMove: if event.buttons( ) & Qt.LeftButton and self.btn_pressed is not None: self.rate = float(self.drag_start[1] - event.y()) / self.height if self.rate > 1e-2: col_triangles = 3 * [255, 140, 0, 255 ] + 3 * [255, 255, 255, 180] elif self.rate < 1e-2: col_triangles = 3 * [255, 255, 255, 180 ] + 3 * [255, 140, 0, 255] else: col_triangles = 6 * [255, 255, 255, 180] update_buffer(self.updown.colorbuf, np.array(col_triangles, dtype=np.uint8)) return True # Mouse-over for knobs name, self.updownpos = check_knob(px, py) # ismcp = float(self.height - event.y()) / self.height <= 0.2 return super(BlipDriver, self).event(event) @pyqtSlot() def updateAPValues(self): if self.btn_pressed == 'COURSE': val = self.remainder + 15 * self.rate self.selValues[0] += int(val) self.remainder = val - int(val) if self.btn_pressed == 'SPD': val = self.remainder + 20 * self.rate self.selValues[1] += int(val) self.remainder = val - int(val) if self.btn_pressed == 'HDG': val = self.remainder + 15 * self.rate self.selValues[2] += int(val) self.remainder = val - int(val) if self.btn_pressed == 'ALT': if abs(self.rate) > 0.06: increment = 1000 val = self.remainder + min(2.0, abs(self.rate) / 0.04) * np.sign( self.rate) elif abs(self.rate) > 0.02: increment = 100 val = self.remainder + self.rate / 0.04 else: increment = 10 val = self.remainder + self.rate / 0.02 self.selValues[3] += int(val) * increment self.remainder = val - int(val) if self.btn_pressed == 'VS': if abs(self.rate) > 0.2: increment = 1000 val = self.remainder + min(2.0, abs(self.rate) / 0.04) * np.sign( self.rate) elif abs(self.rate) > 0.05: increment = 100 val = self.remainder + self.rate / 0.04 else: increment = 10 val = self.remainder + self.rate / 0.02 self.selValues[4] += int(val) * increment self.remainder = val - int(val) self.update_lcd() if self.btn_pressed is not None: QTimer.singleShot(200, self.updateAPValues)
class RadarWidget(QGLWidget): vcount_circle = 36 width = height = 600 viewport = (0, 0, width, height) panlat = 0.0 panlon = 0.0 zoom = 1.0 ar = 1.0 flat_earth = 1.0 wraplon = int(-999) wrapdir = int(0) max_texture_size = 0 do_text = True invalid_count = 0 def __init__(self, navdb, shareWidget=None): super(RadarWidget, self).__init__(shareWidget=shareWidget) self.setAttribute(Qt.WA_AcceptTouchEvents, True) self.grabGesture(Qt.PanGesture) self.grabGesture(Qt.PinchGesture) # self.grabGesture(Qt.SwipeGesture) # The number of aircraft in the simulation self.map_texture = 0 self.naircraft = 0 self.nwaypoints = 0 self.nairports = 0 self.route_acid = "" self.ssd_ownship = np.array([], dtype=np.uint16) self.apt_inrange = np.array([]) self.ssd_all = False self.navdb = navdb self.iactconn = 0 self.nodedata = list() # Display flags self.show_map = True self.show_coast = True self.show_traf = True self.show_pz = False self.show_lbl = True self.show_wpt = True self.show_apt = True self.initialized = False # Connect to manager's nodelist changed and activenode changed signal manager.instance.nodes_changed.connect(self.nodesChanged) manager.instance.activenode_changed.connect(self.actnodeChanged) # Load vertex data self.vbuf_asphalt, self.vbuf_concrete, self.vbuf_runways, self.vbuf_rwythr, \ self.apt_ctrlat, self.apt_ctrlon, self.apt_indices = load_aptsurface() @pyqtSlot(str, int) def nodesChanged(self, address, nodeid, connidx): # For each node we have to keep data such as the visible polygons, etc. self.nodedata.append(nodeData()) @pyqtSlot(int) def actnodeChanged(self, nodeid, connidx): self.iactconn = connidx nact = self.nodedata[connidx] if len(nact.polydata) > 0: update_buffer(self.allpolysbuf, nact.polydata) self.allpolys.set_vertex_count(len(nact.polydata) / 2) def create_objects(self): if not self.isValid(): self.invalid_count += 1 print 'Radarwidget: Context not valid in create_objects, count=%d' % self.invalid_count QTimer.singleShot(100, self.create_objects) return # Make the radarwidget context current, necessary when create_objects is not called from initializeGL self.makeCurrent() # Initialize font for radar view with specified settings self.font = Font() self.font.create_font_array(char_height=text_texture_size, font_family=font_family, font_weight=font_weight) self.font.init_shader(self.text_shader) # Load and bind world texture max_texture_size = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE) print 'Maximum supported texture size: %d' % max_texture_size for i in [16384, 8192, 4096]: if max_texture_size >= i: fname = 'data/graphics/world.%dx%d.dds' % (i, i / 2) print 'Loading texture ' + fname self.map_texture = self.bindTexture(fname) break # Create initial empty buffers for aircraft position, orientation, label, and color self.achdgbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.aclatbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.aclonbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.acaltbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.actasbuf = create_empty_buffer(MAX_NAIRCRAFT * 4, usage=gl.GL_STREAM_DRAW) self.accolorbuf = create_empty_buffer(MAX_NAIRCRAFT * 3, usage=gl.GL_STREAM_DRAW) self.aclblbuf = create_empty_buffer(MAX_NAIRCRAFT * 24, usage=gl.GL_STREAM_DRAW) self.confcpabuf = create_empty_buffer(MAX_NCONFLICTS * 16, usage=gl.GL_STREAM_DRAW) self.polyprevbuf = create_empty_buffer(MAX_POLYPREV_SEGMENTS * 8, usage=gl.GL_DYNAMIC_DRAW) self.allpolysbuf = create_empty_buffer(MAX_ALLPOLYS_SEGMENTS * 16, usage=gl.GL_DYNAMIC_DRAW) self.routebuf = create_empty_buffer(MAX_ROUTE_LENGTH * 8, usage=gl.GL_DYNAMIC_DRAW) self.routewplatbuf = create_empty_buffer(MAX_ROUTE_LENGTH * 4, usage=gl.GL_DYNAMIC_DRAW) self.routewplonbuf = create_empty_buffer(MAX_ROUTE_LENGTH * 4, usage=gl.GL_DYNAMIC_DRAW) self.routelblbuf = create_empty_buffer(MAX_ROUTE_LENGTH * 12, usage=gl.GL_DYNAMIC_DRAW) # ------- Map ------------------------------------ self.map = RenderObject(gl.GL_TRIANGLE_FAN, vertex_count=4) mapvertices = np.array([(-90.0, 540.0), (-90.0, -540.0), (90.0, -540.0), (90.0, 540.0)], dtype=np.float32) texcoords = np.array([(1, 3), (1, 0), (0, 0), (0, 3)], dtype=np.float32) self.map.bind_attrib(ATTRIB_VERTEX, 2, mapvertices) self.map.bind_attrib(ATTRIB_TEXCOORDS, 2, texcoords) # ------- Coastlines ----------------------------- self.coastlines = RenderObject(gl.GL_LINES) coastvertices, coastindices = load_coastlines() self.coastlines.bind_attrib(ATTRIB_VERTEX, 2, coastvertices) self.coastlines.bind_attrib(ATTRIB_COLOR, 3, np.array(lightblue2, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.vcount_coast = len(coastvertices) self.coastindices = coastindices del coastvertices # ------- Runways -------------------------------- self.runways = RenderObject(gl.GL_TRIANGLES) self.runways.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_runways) self.runways.bind_attrib(ATTRIB_COLOR, 3, np.array(grey, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.runways.set_vertex_count(len(self.vbuf_runways) / 2) #---------Runway Thresholds----------------------- self.thresholds = RenderObject(gl.GL_TRIANGLES) self.thresholds.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_rwythr) self.thresholds.bind_attrib(ATTRIB_COLOR, 3, np.array(white, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.thresholds.set_vertex_count(len(self.vbuf_rwythr) / 2) # ------- Taxiways ------------------------------- self.taxiways = RenderObject(gl.GL_TRIANGLES) self.taxiways.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_asphalt) self.taxiways.bind_attrib(ATTRIB_COLOR, 3, np.array(grey, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.taxiways.set_vertex_count(len(self.vbuf_asphalt) / 2) # ------- Pavement ------------------------------- self.pavement = RenderObject(gl.GL_TRIANGLES) self.pavement.bind_attrib(ATTRIB_VERTEX, 2, self.vbuf_concrete) self.pavement.bind_attrib(ATTRIB_COLOR, 3, np.array(lightgrey, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.pavement.set_vertex_count(len(self.vbuf_concrete) / 2) # Polygon preview object self.polyprev = RenderObject(gl.GL_LINE_LOOP) self.polyprev.bind_attrib(ATTRIB_VERTEX, 2, self.polyprevbuf) self.polyprev.bind_attrib(ATTRIB_COLOR, 3, np.array(lightblue, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # Fixed polygons self.allpolys = RenderObject(gl.GL_LINES) self.allpolys.bind_attrib(ATTRIB_VERTEX, 2, self.allpolysbuf) self.allpolys.bind_attrib(ATTRIB_COLOR, 3, np.array(blue, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # ------- SSD object ----------------------------- self.ssd = RenderObject(gl.GL_POINTS) self.ssd.bind_attrib(ATTRIB_LAT0, 1, self.aclatbuf) self.ssd.bind_attrib(ATTRIB_LON0, 1, self.aclonbuf) self.ssd.bind_attrib(ATTRIB_ALT0, 1, self.acaltbuf) self.ssd.bind_attrib(ATTRIB_TAS0, 1, self.actasbuf) self.ssd.bind_attrib(ATTRIB_TRK0, 1, self.achdgbuf) self.ssd.bind_attrib(ATTRIB_LAT1, 1, self.aclatbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_LON1, 1, self.aclonbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_ALT1, 1, self.acaltbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_TAS1, 1, self.actasbuf, instance_divisor=1) self.ssd.bind_attrib(ATTRIB_TRK1, 1, self.achdgbuf, instance_divisor=1) # ------- Circle --------------------------------- # Create a new VAO (Vertex Array Object) and bind it self.protectedzone = RenderObject(gl.GL_LINE_LOOP, vertex_count=self.vcount_circle) circlevertices = np.transpose(np.array((5.0 * nm * np.cos(np.linspace(0.0, 2.0 * np.pi, self.vcount_circle)), 5.0 * nm * np.sin(np.linspace(0.0, 2.0 * np.pi, self.vcount_circle))), dtype=np.float32)) self.protectedzone.bind_attrib(ATTRIB_VERTEX, 2, circlevertices) self.protectedzone.bind_attrib(ATTRIB_LAT, 1, self.aclatbuf, instance_divisor=1) self.protectedzone.bind_attrib(ATTRIB_LON, 1, self.aclonbuf, instance_divisor=1) self.protectedzone.bind_attrib(ATTRIB_COLOR, 3, self.accolorbuf, datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # ------- A/C symbol ----------------------------- self.ac_symbol = RenderObject(gl.GL_TRIANGLE_FAN, vertex_count=4) acvertices = np.array([(0.0, 0.5 * ac_size), (-0.5 * ac_size, -0.5 * ac_size), (0.0, -0.25 * ac_size), (0.5 * ac_size, -0.5 * ac_size)], dtype=np.float32) self.ac_symbol.bind_attrib(ATTRIB_VERTEX, 2, acvertices) self.ac_symbol.bind_attrib(ATTRIB_LAT, 1, self.aclatbuf, instance_divisor=1) self.ac_symbol.bind_attrib(ATTRIB_LON, 1, self.aclonbuf, instance_divisor=1) self.ac_symbol.bind_attrib(ATTRIB_ORIENTATION, 1, self.achdgbuf, instance_divisor=1) self.ac_symbol.bind_attrib(ATTRIB_COLOR, 3, self.accolorbuf, datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.aclabels = self.font.prepare_text_instanced(self.aclblbuf, (8, 3), self.aclatbuf, self.aclonbuf, self.accolorbuf, char_size=text_size, vertex_offset=(ac_size, -0.5 * ac_size)) # ------- Conflict CPA lines --------------------- self.cpalines = RenderObject(gl.GL_LINES) self.cpalines.bind_attrib(ATTRIB_VERTEX, 2, self.confcpabuf) self.cpalines.bind_attrib(ATTRIB_COLOR, 3, np.array(amber, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) # ------- Aircraft Route ------------------------- self.route = RenderObject(gl.GL_LINES) self.route.bind_attrib(ATTRIB_VERTEX, 2, self.routebuf) self.route.bind_attrib(ATTRIB_COLOR, 3, np.array(magenta, dtype=np.uint8), datatype=gl.GL_UNSIGNED_BYTE, normalize=True, instance_divisor=1) self.routelbl = self.font.prepare_text_instanced(self.routelblbuf, (12, 1), self.routewplatbuf, self.routewplonbuf, char_size=text_size, vertex_offset=(wpt_size, 0.5 * wpt_size)) # ------- Waypoints ------------------------------ self.nwaypoints = len(self.navdb.wplat) self.waypoints = RenderObject(gl.GL_LINE_LOOP, vertex_count=3, n_instances=self.nwaypoints) wptvertices = np.array([(0.0, 0.5 * wpt_size), (-0.5 * wpt_size, -0.5 * wpt_size), (0.5 * wpt_size, -0.5 * wpt_size)], dtype=np.float32) # a triangle self.waypoints.bind_attrib(ATTRIB_VERTEX, 2, wptvertices) # Sort based on id string length llid = sorted(zip(self.navdb.wpid, self.navdb.wplat, self.navdb.wplon), key=lambda i: len(i[0]) > 3) wplat = [lat for (wpid, lat, lon) in llid] wplon = [lon for (wpid, lon, lon) in llid] self.wptlatbuf = self.waypoints.bind_attrib(ATTRIB_LAT, 1, np.array(wplat, dtype=np.float32), instance_divisor=1) self.wptlonbuf = self.waypoints.bind_attrib(ATTRIB_LON, 1, np.array(wplon, dtype=np.float32), instance_divisor=1) wptids = '' self.nnavaids = 0 for wptid in llid: if len(wptid[0]) <= 3: self.nnavaids += 1 wptids += wptid[0].ljust(5) self.wptlabels = self.font.prepare_text_instanced(np.array(wptids, dtype=np.string_), (5, 1), self.wptlatbuf, self.wptlonbuf, char_size=text_size, vertex_offset=(wpt_size, 0.5 * wpt_size)) del wptids # ------- Airports ------------------------------- self.nairports = len(self.navdb.aplat) self.airports = RenderObject(gl.GL_LINE_LOOP, vertex_count=4, n_instances=self.nairports) aptvertices = np.array([(-0.5 * apt_size, -0.5 * apt_size), (0.5 * apt_size, -0.5 * apt_size), (0.5 * apt_size, 0.5 * apt_size), (-0.5 * apt_size, 0.5 * apt_size)], dtype=np.float32) # a square self.airports.bind_attrib(ATTRIB_VERTEX, 2, aptvertices) indices = self.navdb.aptype.argsort() aplat = np.array(self.navdb.aplat[indices], dtype=np.float32) aplon = np.array(self.navdb.aplon[indices], dtype=np.float32) aptypes = self.navdb.aptype[indices] apnames = np.array(self.navdb.apid) apnames = apnames[indices] # The number of large, large+med, and large+med+small airports self.nairports = [aptypes.searchsorted(2), aptypes.searchsorted(3), self.nairports] self.aptlatbuf = self.airports.bind_attrib(ATTRIB_LAT, 1, aplat, instance_divisor=1) self.aptlonbuf = self.airports.bind_attrib(ATTRIB_LON, 1, aplon, instance_divisor=1) aptids = '' for aptid in apnames: aptids += aptid.ljust(4) self.aptlabels = self.font.prepare_text_instanced(np.array(aptids, dtype=np.string_), (4, 1), self.aptlatbuf, self.aptlonbuf, char_size=text_size, vertex_offset=(apt_size, 0.5 * apt_size)) del aptids # Unbind VAO, VBO RenderObject.unbind_all() # Set initial values for the global uniforms self.globaldata.set_wrap(self.wraplon, self.wrapdir) self.globaldata.set_pan_and_zoom(self.panlat, self.panlon, self.zoom) # Clean up memory del self.vbuf_asphalt, self.vbuf_concrete, self.vbuf_runways, self.vbuf_rwythr self.initialized = True def initializeGL(self): """Initialize OpenGL, VBOs, upload data on the GPU, etc.""" # First check for supported GL version gl_version = float(gl.glGetString(gl.GL_VERSION)[:3]) if gl_version < 3.3: print('OpenGL context created with GL version %.1f' % gl_version) qCritical("""Your system reports that it supports OpenGL up to version %.1f. The minimum requirement for BlueSky is OpenGL 3.3. Generally, AMD/ATI/nVidia cards from 2008 and newer support OpenGL 3.3, and Intel integrated graphics from the Haswell generation and newer. If you think your graphics system should be able to support GL>=3.3 please open an issue report on the BlueSky Github page (https://github.com/ProfHoekstra/bluesky/issues)""" % gl_version) return # background color gl.glClearColor(0, 0, 0, 0) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) self.globaldata = radarUBO() try: # Compile shaders and link color shader program self.color_shader = BlueSkyProgram('data/graphics/shaders/radarwidget-normal.vert', 'data/graphics/shaders/radarwidget-color.frag') self.color_shader.bind_uniform_buffer('global_data', self.globaldata) # Compile shaders and link texture shader program self.texture_shader = BlueSkyProgram('data/graphics/shaders/radarwidget-normal.vert', 'data/graphics/shaders/radarwidget-texture.frag') self.texture_shader.bind_uniform_buffer('global_data', self.globaldata) # Compile shaders and link text shader program self.text_shader = BlueSkyProgram('data/graphics/shaders/radarwidget-text.vert', 'data/graphics/shaders/radarwidget-text.frag') self.text_shader.bind_uniform_buffer('global_data', self.globaldata) self.ssd_shader = BlueSkyProgram('data/graphics/shaders/ssd.vert', 'data/graphics/shaders/ssd.frag', 'data/graphics/shaders/ssd.geom') self.ssd_shader.bind_uniform_buffer('global_data', self.globaldata) self.ssd_shader.loc_vlimits = gl.glGetUniformLocation(self.ssd_shader.program, 'Vlimits') except RuntimeError as e: qCritical('Error compiling shaders in radarwidget: ' + e.args[0]) return # create all vertex array objects self.create_objects() def paintGL(self): """Paint the scene.""" # pass if the framebuffer isn't complete yet or if not initialized if not (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == gl.GL_FRAMEBUFFER_COMPLETE and self.initialized and self.isVisible()): return # Set the viewport and clear the framebuffer gl.glViewport(*self.viewport) gl.glClear(gl.GL_COLOR_BUFFER_BIT) # Send the (possibly) updated global uniforms to the buffer self.globaldata.set_vertex_scale_type(VERTEX_IS_LATLON) # --- DRAW THE MAP AND COASTLINES --------------------------------------------- # Map and coastlines: don't wrap around in the shader self.globaldata.enable_wrap(False) if self.show_map: # Select the texture shader self.texture_shader.use() # Draw map texture gl.glActiveTexture(gl.GL_TEXTURE0 + 0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.map_texture) self.map.draw() # Select the non-textured shader self.color_shader.use() # Draw coastlines if self.show_coast: if self.wrapdir == 0: # Normal case, no wrap around self.coastlines.draw(first_vertex=0, vertex_count=self.vcount_coast) else: self.coastlines.bind() wrapindex = np.uint32(self.coastindices[int(self.wraplon) + 180]) if self.wrapdir == 1: gl.glVertexAttrib1f(ATTRIB_LON, 360.0) self.coastlines.draw(first_vertex=0, vertex_count=wrapindex) gl.glVertexAttrib1f(ATTRIB_LON, 0.0) self.coastlines.draw(first_vertex=wrapindex, vertex_count=self.vcount_coast - wrapindex) else: gl.glVertexAttrib1f(ATTRIB_LON, -360.0) self.coastlines.draw(first_vertex=wrapindex, vertex_count=self.vcount_coast - wrapindex) gl.glVertexAttrib1f(ATTRIB_LON, 0.0) self.coastlines.draw(first_vertex=0, vertex_count=wrapindex) # --- DRAW PREVIEW SHAPE (WHEN AVAILABLE) ----------------------------- self.polyprev.draw() # --- DRAW CUSTOM SHAPES (WHEN AVAILABLE) ----------------------------- self.allpolys.draw() # --- DRAW THE SELECTED AIRCRAFT ROUTE (WHEN AVAILABLE) --------------- if self.show_traf: self.route.draw() self.cpalines.draw() # --- DRAW AIRPORT DETAILS (RUNWAYS, TAXIWAYS, PAVEMENTS) ------------- self.runways.draw() self.thresholds.draw() if self.zoom >= 1.0: for idx in self.apt_inrange: self.taxiways.draw(first_vertex=idx[0], vertex_count=idx[1]) self.pavement.draw(first_vertex=idx[2], vertex_count=idx[3]) # --- DRAW THE INSTANCED AIRCRAFT SHAPES ------------------------------ # update wrap longitude and direction for the instanced objects self.globaldata.enable_wrap(True) # PZ circles only when they are bigger than the A/C symbols if self.naircraft > 0 and self.show_traf and self.show_pz and self.zoom >= 0.15: self.globaldata.set_vertex_scale_type(VERTEX_IS_METERS) self.protectedzone.draw(n_instances=self.naircraft) self.globaldata.set_vertex_scale_type(VERTEX_IS_SCREEN) # Draw traffic symbols if self.naircraft > 0 and self.show_traf: self.ac_symbol.draw(n_instances=self.naircraft) if self.zoom >= 0.5: nairports = self.nairports[2] elif self.zoom >= 0.25: nairports = self.nairports[1] else: nairports = self.nairports[0] if self.zoom >= 3: nwaypoints = self.nwaypoints else: nwaypoints = self.nnavaids # Draw waypoint symbols if self.show_wpt: self.waypoints.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue3 + (255,))) self.waypoints.draw(n_instances=nwaypoints) # Draw airport symbols if self.show_apt: self.airports.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue3 + (255,))) self.airports.draw(n_instances=nairports) if self.do_text: self.text_shader.use() self.font.use() if self.show_apt: self.font.set_char_size(self.aptlabels.char_size) self.font.set_block_size(self.aptlabels.block_size) self.aptlabels.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue4 + (255,))) self.aptlabels.draw(n_instances=nairports) if self.show_wpt: self.font.set_char_size(self.wptlabels.char_size) self.font.set_block_size(self.wptlabels.block_size) self.wptlabels.bind() gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(lightblue4 + (255,))) self.wptlabels.draw(n_instances=nwaypoints) if self.show_traf and self.route.vertex_count > 1: gl.glVertexAttrib4Nub(ATTRIB_COLOR, *(magenta + (255,))) self.font.set_char_size(self.routelbl.char_size) self.font.set_block_size(self.routelbl.block_size) self.routelbl.draw() if self.naircraft > 0 and self.show_traf and self.show_lbl: self.font.set_char_size(self.aclabels.char_size) self.font.set_block_size(self.aclabels.block_size) self.aclabels.draw(n_instances=self.naircraft) # SSD if self.ssd_all or len(self.ssd_ownship) > 0: self.ssd_shader.use() gl.glUniform3f(self.ssd_shader.loc_vlimits, 1e4, 4e4, 200.0) if self.ssd_all: self.ssd.draw(first_vertex=0, vertex_count=self.naircraft, n_instances=self.naircraft) else: self.ssd.draw(first_vertex=self.ssd_ownship[-1], vertex_count=1, n_instances=self.naircraft) # Unbind everything RenderObject.unbind_all() gl.glUseProgram(0) def resizeGL(self, width, height): """Called upon window resizing: reinitialize the viewport.""" if not self.initialized: return # update the window size # Qt5 supports getting the device pixel ratio, which can be > 1 for HiDPI displays such as Mac Retina screens pixel_ratio = 1 if QT_VERSION >= 5: pixel_ratio = self.devicePixelRatio() # Calculate zoom so that the window resize doesn't affect the scale, but only enlarges or shrinks the view zoom = float(self.width) / float(width) * pixel_ratio origin = (width / 2, height / 2) # Update width, height, and aspect ratio self.width, self.height = width / pixel_ratio, height / pixel_ratio self.ar = float(width) / max(1, float(height)) self.globaldata.set_win_width_height(self.width, self.height) self.viewport = (0, 0, width, height) # Update zoom self.event(PanZoomEvent(zoom=zoom, origin=origin)) def update_route_data(self, data): self.route_acid = data.acid if data.acid != "" and len(data.lat) > 0: nsegments = len(data.lat) data.iactwp = max(0, data.iactwp) self.routelbl.n_instances = nsegments self.route.set_vertex_count(2 * nsegments) routedata = np.empty(4 * nsegments, dtype=np.float32) routedata[0:4] = [data.aclat, data.aclon, data.lat[data.iactwp], data.lon[data.iactwp]] routedata[4::4] = data.lat[:-1] routedata[5::4] = data.lon[:-1] routedata[6::4] = data.lat[1:] routedata[7::4] = data.lon[1:] update_buffer(self.routebuf, routedata) update_buffer(self.routewplatbuf, np.array(data.lat, dtype=np.float32)) update_buffer(self.routewplonbuf, np.array(data.lon, dtype=np.float32)) wptlabels = [] for wp in data.wptlabels: wptlabels += wp[:12].ljust(12) update_buffer(self.routelblbuf, np.array(wptlabels)) else: self.route.set_vertex_count(0) def update_aircraft_data(self, data): self.naircraft = len(data.lat) if self.naircraft == 0: self.cpalines.set_vertex_count(0) else: # Update data in GPU buffers update_buffer(self.aclatbuf, np.array(data.lat, dtype=np.float32)) update_buffer(self.aclonbuf, np.array(data.lon, dtype=np.float32)) update_buffer(self.achdgbuf, np.array(data.trk, dtype=np.float32)) update_buffer(self.acaltbuf, np.array(data.alt, dtype=np.float32)) update_buffer(self.actasbuf, np.array(data.tas, dtype=np.float32)) # CPA lines to indicate conflicts ncpalines = len(data.confcpalat) cpalines = np.zeros(4 * ncpalines, dtype=np.float32) self.cpalines.set_vertex_count(2 * ncpalines) # Labels and colors rawlabel = '' color = np.empty((self.naircraft, 3), dtype=np.uint8) for i in range(self.naircraft): if np.isnan(data.tas[i]): print 'CAS NaN in %d: %s' % (i, data.id[i]) data.cas[i] = 0.0 if np.isnan(data.alt[i]): print 'ALT NaN in %d: %s' % (i, data.id[i]) data.alt[i] = 0.0 # Make label: 3 lines of 8 characters per aircraft rawlabel += '%-8sFL%03d %-8d' % (data.id[i][:8], int(data.alt[i] / ft / 100), int(data.cas[i] / kts)) confindices = data.iconf[i] if len(confindices) > 0: color[i, :] = amber for confidx in confindices: cpalines[4 * confidx : 4 * confidx + 4] = [ data.lat[i], data.lon[i], data.confcpalat[confidx], data.confcpalon[confidx]] else: color[i, :] = green update_buffer(self.confcpabuf, cpalines) update_buffer(self.accolorbuf, color) update_buffer(self.aclblbuf, np.array(rawlabel, dtype=np.string_)) # If there is a visible route, update the start position if self.route_acid != "": if self.route_acid in data.id: idx = data.id.index(self.route_acid) update_buffer(self.routebuf, np.array([data.lat[idx], data.lon[idx]], dtype=np.float32)) def show_ssd(self, arg): if arg == 'ALL': self.ssd_all = True elif arg == 'OFF': self.ssd_all = False self.ssd_ownship = np.array([], dtype=np.uint16) else: if arg in self.ssd_ownship: self.ssd_ownship = np.delete(self.ssd_ownship, np.argmax(self.ssd_ownship == arg)) else: self.ssd_ownship = np.append(self.ssd_ownship, arg) def updatePolygon(self, name, data_in): nact = self.nodedata[manager.sender()[0]] if name in nact.polynames: # We're either updating a polygon, or deleting it. In both cases # we remove the current one. nact.polydata = np.delete(nact.polydata, range(*nact.polynames[name])) del nact.polynames[name] if data_in is not None: nact.polynames[name] = (len(nact.polydata), 2*len(data_in)) newbuf = np.empty(2 * len(data_in), dtype=np.float32) newbuf[0::4] = data_in[0::2] # lat newbuf[1::4] = data_in[1::2] # lon newbuf[2:-2:4] = data_in[2::2] # lat newbuf[3:-3:4] = data_in[3::2] # lon newbuf[-2:] = data_in[0:2] nact.polydata = np.append(nact.polydata, newbuf) update_buffer(self.allpolysbuf, nact.polydata) self.allpolys.set_vertex_count(len(nact.polydata)/2) def previewpoly(self, shape_type, data_in=None): if shape_type is None: self.polyprev.set_vertex_count(0) return if shape_type in ['BOX', 'AREA']: # For a box (an area is a box) we need to add two additional corners data = np.zeros(8, dtype=np.float32) data[0:2] = data_in[0:2] data[2:4] = data_in[2], data_in[1] data[4:6] = data_in[2:4] data[6:8] = data_in[0], data_in[3] else: data = data_in update_buffer(self.polyprevbuf, data) self.polyprev.set_vertex_count(len(data)/2) def airportsInRange(self): ll_range = max(1.5 / self.zoom, 1.0) indices = np.logical_and.reduce((self.apt_ctrlat >= self.panlat - ll_range, self.apt_ctrlat <= self.panlat + ll_range, self.apt_ctrlon >= self.panlon - ll_range, self.apt_ctrlon <= self.panlon + ll_range)) self.apt_inrange = self.apt_indices[indices] def pixelCoordsToGLxy(self, x, y): """Convert screen pixel coordinates to GL projection coordinates (x, y range -1 -- 1) """ # GL coordinates (x, y range -1 -- 1) glx = (float(2.0 * x) / self.width - 1.0) gly = -(float(2.0 * y) / self.height - 1.0) return glx, gly def pixelCoordsToLatLon(self, x, y): """Convert screen pixel coordinates to lat/lon coordinates """ glx, gly = self.pixelCoordsToGLxy(x, y) # glxy = zoom * (latlon - pan) # latlon = pan + glxy / zoom lat = self.panlat + gly / (self.zoom * self.ar) lon = self.panlon + glx / (self.zoom * self.flat_earth) return lat, lon def event(self, event): if event.type() == PanZoomEventType: if event.pan is not None: # Absolute pan operation if event.absolute: self.panlat = event.pan[0] self.panlon = event.pan[1] # Relative pan operation else: self.panlat += event.pan[0] self.panlon += event.pan[1] # Don't pan further than the poles in y-direction self.panlat = min(max(self.panlat, -90.0 + 1.0 / (self.zoom * self.ar)), 90.0 - 1.0 / (self.zoom * self.ar)) # Update flat-earth factor and possibly zoom in case of very wide windows (> 2:1) self.flat_earth = np.cos(np.deg2rad(self.panlat)) self.zoom = max(self.zoom, 1.0 / (180.0 * self.flat_earth)) if event.zoom is not None: if event.absolute: # Limit zoom extents in x-direction to [-180:180], and in y-direction to [-90:90] self.zoom = max(event.zoom, 1.0 / min(90.0 * self.ar, 180.0 * self.flat_earth)) else: prevzoom = self.zoom glx, gly = self.pixelCoordsToGLxy(event.origin[0], event.origin[1]) self.zoom *= event.zoom # Limit zoom extents in x-direction to [-180:180], and in y-direction to [-90:90] self.zoom = max(self.zoom, 1.0 / min(90.0 * self.ar, 180.0 * self.flat_earth)) # Correct pan so that zoom actions are around the mouse position, not around 0, 0 # glxy / zoom1 - pan1 = glxy / zoom2 - pan2 # pan2 = pan1 + glxy (1/zoom2 - 1/zoom1) self.panlon = self.panlon - glx * (1.0 / self.zoom - 1.0 / prevzoom) / self.flat_earth self.panlat = self.panlat - gly * (1.0 / self.zoom - 1.0 / prevzoom) / self.ar # Don't pan further than the poles in y-direction self.panlat = min(max(self.panlat, -90.0 + 1.0 / (self.zoom * self.ar)), 90.0 - 1.0 / (self.zoom * self.ar)) # Update flat-earth factor self.flat_earth = np.cos(np.deg2rad(self.panlat)) if self.zoom >= 1.0: self.airportsInRange() event.accept() # Check for necessity wrap-around in x-direction self.wraplon = -999.9 self.wrapdir = 0 if self.panlon + 1.0 / (self.zoom * self.flat_earth) < -180.0: # The left edge of the map has passed the right edge of the screen: we can just change the pan position self.panlon += 360.0 elif self.panlon - 1.0 / (self.zoom * self.flat_earth) < -180.0: # The left edge of the map has passed the left edge of the screen: we need to wrap around to the left self.wraplon = float(np.ceil(360.0 + self.panlon - 1.0 / (self.zoom * self.flat_earth))) self.wrapdir = -1 elif self.panlon - 1.0 / (self.zoom * self.flat_earth) > 180.0: # The right edge of the map has passed the left edge of the screen: we can just change the pan position self.panlon -= 360.0 elif self.panlon + 1.0 / (self.zoom * self.flat_earth) > 180.0: # The right edge of the map has passed the right edge of the screen: we need to wrap around to the right self.wraplon = float(np.floor(-360.0 + self.panlon + 1.0 / (self.zoom * self.flat_earth))) self.wrapdir = 1 self.globaldata.set_wrap(self.wraplon, self.wrapdir) # update pan and zoom on GPU for all shaders self.globaldata.set_pan_and_zoom(self.panlat, self.panlon, self.zoom) return True else: return super(RadarWidget, self).event(event)
class BlipDriver(QGLWidget): def __init__(self, parent=None): super(BlipDriver, self).__init__(parent=parent) self.initialized = False self.width = 800 self.height = 800 self.ndviewport = (100, 200, 600, 600) self.mcpviewport = (0, 0, 800, 200) self.btn_state = [False] * 14 self.resize(800, 800) self.setMouseTracking(True) self.drag_start = (0, 0) self.btn_pressed = None self.selValues = 5 * [0] # [0:courseLeftRight, 1:spd, 2:hdg, 3:alt, 4:vs] self.rate = 0.0 self.remainder = 0.0 self.updownpos = None def create_objects(self): self.mcp = RenderObject(gl.GL_TRIANGLES, vertex=np.array(rect(-1, -1, 2, 2), dtype=np.float32)) self.mcp_texture = load_texture('mcp737.png') self.btn_tex = load_texture('btn_led.png') self.lcd_tex = load_lcd_font() v_mcp_text = [] for pos in [0.0, 0.03, 0.06, 0.31, 0.34, 0.37, 0.4, 0.43, 0.644, 0.674, 0.704, 0.955, 0.985, 1.015, 1.045, 1.075, 1.22, 1.25, 1.28, 1.31, 1.34, 1.69, 1.72, 1.75]: v, t = Font.char(-0.886 + pos, 0.45, 0.03, 0.25) v_mcp_text += v self.mcp_text = RenderObject(gl.GL_TRIANGLES, vertex=np.array(v_mcp_text, dtype=np.float32)) self.lcd_charcoords = np.zeros(24 * 6, dtype=np.float32) self.lcdbuf = self.mcp_text.bind_attrib(1, 1, self.lcd_charcoords, datatype=gl.GL_FLOAT) btn_leds = [] for pos in [(-0.74, -0.75), (-0.645, -0.75), (-0.37, -0.75), (-0.232, -0.75), (-0.09, -0.75), (0.105, -0.75), (0.2, -0.75), (-0.37, 0.5), (-0.09, 0.5), (-0.09, -0.125), (0.575, 0.34), (0.575, -0.34), (0.684, 0.34), (0.684, -0.34)]: btn_leds += rect(pos[0], pos[1], 0.055, 0.075) btn_color = np.zeros(14 * 6 * 4, dtype=np.uint8) self.btn_leds = RenderObject(gl.GL_TRIANGLES, vertex=np.array(btn_leds, dtype=np.float32), color=btn_color) # Button up/down indicator w, h, offset = 0.04, 0.16, 0.04 triangles = np.array([-w, offset, 0.0, offset + h, w, offset, -w, -offset, 0.0, -offset - h, w, -offset], dtype=np.float32) col_triangles = np.array(6 * [255, 255, 255, 180], dtype=np.uint8) self.updown = RenderObject(gl.GL_TRIANGLES, vertex=triangles, color=col_triangles) # Use the same font as the radarwidget self.font = Font() self.font.create_font_array('../../data/graphics/font/') self.font.init_shader(self.text_shader) edge = np.zeros(120, dtype=np.float32) edge[0:120:2] = 1.4 * np.sin(np.radians(np.arange(-60, 60, 2))) edge[1:120:2] = 1.4 * np.cos(np.radians(np.arange(-60, 60, 2))) self.edge = RenderObject(gl.GL_LINE_STRIP, vertex=edge, color=white) arcs = [] for i in range(1, 4): for angle in range(-60, 60, max(2, 6 - 2 * i)): arcs.append(float(i) * 0.35 * sin(radians(angle))) arcs.append(float(i) * 0.35 * cos(radians(angle))) if i == 4: arcs.append(float(i) * 0.35 * sin(radians(angle + 2))) arcs.append(float(i) * 0.35 * cos(radians(angle + 2))) arcs = np.array(arcs, dtype=np.float32) self.arcs = RenderObject(gl.GL_LINES, vertex=arcs, color=white) mask = [] for angle in range(-60, 60, 2): mask.append(1.4 * sin(radians(angle))) mask.append(10.0) mask.append(1.4 * sin(radians(angle))) mask.append(1.4 * cos(radians(angle))) mask = np.array(mask, dtype=np.float32) self.mask = RenderObject(gl.GL_TRIANGLE_STRIP, vertex=mask, color=black) ticks = np.zeros(288, dtype=np.float32) for i in range(72): ticktop = 1.46 if i % 6 == 0 else (1.44 if i % 2 == 0 else 1.42) ticks[4*i :4*i+2] = (1.4 * sin(radians(i * 5)), 1.4 * cos(radians(i * 5))) ticks[4*i+2:4*i+4] = (ticktop * sin(radians(i * 5)), ticktop * cos(radians(i * 5))) self.ticks = RenderObject(gl.GL_LINES, vertex=ticks, color=white) ticklbls = np.zeros(24 * 36, dtype=np.float32) texcoords = np.zeros(36 * 36, dtype=np.float32) for i in range(36): if i % 3 == 0: w, h, y = 0.045, 0.09, 1.48 else: w, h, y = 0.035, 0.07, 1.46 tmp = [(-w, h+y), (-w, y), (0.0, h+y), (0.0, h+y), (-w, y), (0.0, y), (0.0, h+y), (0.0, y), (w, h+y), (w, h+y), (0.0, y), (w, y)] # numerics start at ASCII 48 c1 = i / 10 + 48 c2 = i % 10 + 48 texcoords[36*i:36*i+18] = [0, 0, c1, 0, 1, c1, 1, 0, c1, 1, 0, c1, 0, 1, c1, 1, 1, c1] texcoords[36*i+18:36*i+36] = [0, 0, c2, 0, 1, c2, 1, 0, c2, 1, 0, c2, 0, 1, c2, 1, 1, c2] angle = radians(10 * (36 - i)) rot = np.array([[cos(angle), -sin(angle)], [sin(angle), cos(angle)]]) for j in range(12): ticklbls[24*i+2*j:24*i+2*j+2] = rot.dot(tmp[j]) self.ticklbls = RenderObject(gl.GL_TRIANGLES, vertex=ticklbls, color=white, texcoords=texcoords) vown = np.array([0.0, 0.0, 0.0, -0.12, 0.065, -0.03, -0.065, -0.03, 0.022, -0.1, -0.022, -0.1], dtype=np.float32) self.ownship = RenderObject(gl.GL_LINES, vertex=vown, color=yellow) self.spdlabel_text = self.font.prepare_text_string('GS TAS', 0.05, white, (-0.98, 1.6)) self.spdlabel_val = self.font.prepare_text_string(' 000 000', 0.05, green, (-0.97, 1.6)) self.initialized = True # Unbind everything RenderObject.unbind_all() def setAircraftID(self, ac_id): self.ac_id = ac_id self.setWindowTitle(ac_id) def update_aircraft_data(self, ownid, ownlat, ownlon, owntas, ownhdg, n_aircraft): self.globaldata.set_owndata(ownid, ownlat, ownlon, ownhdg) self.n_aircraft = n_aircraft def initializeGL(self): """Initialize OpenGL, VBOs, upload data on the GPU, etc.""" # First check for supported GL version gl_version = float(gl.glGetString(gl.GL_VERSION)[:3]) if gl_version < 3.3: return # background color gl.glClearColor(0, 0, 0, 0) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) # self.mcp_data = mcpUBO() self.globaldata = ndUBO() self.mcp_col_shader = BlueSkyProgram('mcp.vert', 'color.frag') self.mcp_tex_shader = BlueSkyProgram('mcp.vert', 'texture.frag') self.mcp_txt_shader = BlueSkyProgram('mcp_text.vert', 'mcp_text.frag') self.color_shader = BlueSkyProgram('normal.vert', 'color.frag') self.text_shader = BlueSkyProgram('text.vert', 'text.frag') self.text_shader.bind_uniform_buffer('global_data', self.globaldata) self.create_objects() self.update_lcd() def resizeGL(self, width, height): pixel_ratio = self.devicePixelRatio() self.width, self.height = width / pixel_ratio, height / pixel_ratio hmcp = height / 10 * 2 # paint ND within the largest possible rectangular area in the window wnd = hnd = min(width, height - hmcp) xnd = max(0, (width - wnd) / 2) ynd = hmcp + max(0, (height - hmcp - hnd) / 2) self.mcpviewport = (0, 0, width, hmcp) self.ndviewport = (xnd, ynd, wnd, hnd) def paintGL(self): """Paint the scene.""" # pass if the framebuffer isn't complete yet or if not initialized if not (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == gl.GL_FRAMEBUFFER_COMPLETE and self.isVisible() and self.initialized): return gl.glClearColor(0, 0, 0, 0) gl.glClear(gl.GL_COLOR_BUFFER_BIT) # Set the viewport and clear the framebuffer # --- Draw the ND in its viewport ---- gl.glViewport(*self.ndviewport) self.color_shader.use() self.arcs.draw() self.ownship.draw() self.mask.draw() self.edge.draw() self.ticks.draw() # Select the text shader self.text_shader.use() self.font.use() self.font.set_block_size((0, 0)) self.globaldata.set_vertex_modifiers(VERTEX_IS_GLXY, True) self.ticklbls.draw() self.globaldata.set_vertex_modifiers(VERTEX_IS_GLXY, False) self.spdlabel_text.draw() self.spdlabel_val.draw() # --- Draw the MCP in its viewport --- gl.glViewport(*self.mcpviewport) self.mcp_tex_shader.use() gl.glActiveTexture(gl.GL_TEXTURE0 + 0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.mcp_texture) self.mcp.draw() gl.glBindTexture(gl.GL_TEXTURE_2D, self.btn_tex) self.btn_leds.draw() if self.updownpos is not None: self.mcp_col_shader.use() gl.glVertexAttrib2f(2, *self.updownpos) self.updown.draw() gl.glVertexAttrib2f(2, 0.0, 0.0) self.mcp_txt_shader.use() gl.glActiveTexture(gl.GL_TEXTURE0 + 0) gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.lcd_tex) self.mcp_text.draw() # Unbind everything RenderObject.unbind_all() gl.glUseProgram(0) def update_lcd(self): # [0:courseLeftRight, 1:spd, 2:hdg, 3:alt, 4:vs] val = self.selValues chars = '%03d' % (int(val[0]) % 360) chars += '% 5d' % max(0, val[1]) chars += '%03d' % (int(val[2]) % 360) chars += '% 5d' % min(99999, max(0, val[3])) chars += ' ' if val[4] == 0 else '% 5d' % min(9999, max(-9999, val[4])) chars += '%03d' % (int(val[0]) % 360) indices = [] for c in chars: if c == ' ': indices += [0] * 6 elif c == '-': indices += [1] * 6 elif c == '.': indices += [2] * 6 else: indices += [int(c) + 3] * 6 update_buffer(self.lcdbuf, np.array(indices, dtype=np.float32)) def event(self, event): if not self.initialized: return super(BlipDriver, self).event(event) self.makeCurrent() if event.type() in [QEvent.MouseMove, QEvent.MouseButtonPress, QEvent.MouseButtonRelease]: px = 2.0 * (event.x()) / self.width - 1.0 py = 10.0 * (self.height - event.y()) / self.height - 1.0 if event.type() == QEvent.MouseButtonPress and event.button() & Qt.LeftButton: self.btn_pressed, _ = check_knob(px, py) if self.btn_pressed is not None: self.drag_start = event.x(), event.y() QTimer.singleShot(100, self.updateAPValues) elif event.type() == QEvent.MouseButtonRelease and event.button() & Qt.LeftButton: # print px, py self.btn_pressed = None col_triangles = 6 * [255, 255, 255, 180] update_buffer(self.updown.colorbuf, np.array(col_triangles, dtype=np.uint8)) # MCP button clicked? name, idx = check_btn(px, py) if name is not None: self.btn_state[idx] = not self.btn_state[idx] btn_color = [] for b in self.btn_state: btn_color += 24 * [255] if b else 24 * [0] update_buffer(self.btn_leds.colorbuf, np.array(btn_color, dtype=np.uint8)) elif event.type() == QEvent.MouseMove: if event.buttons() & Qt.LeftButton and self.btn_pressed is not None: self.rate = float(self.drag_start[1] - event.y()) / self.height if self.rate > 1e-2: col_triangles = 3 * [255, 140, 0, 255] + 3 * [255, 255, 255, 180] elif self.rate < 1e-2: col_triangles = 3 * [255, 255, 255, 180] + 3 * [255, 140, 0, 255] else: col_triangles = 6 * [255, 255, 255, 180] update_buffer(self.updown.colorbuf, np.array(col_triangles, dtype=np.uint8)) return True # Mouse-over for knobs name, self.updownpos = check_knob(px, py) # ismcp = float(self.height - event.y()) / self.height <= 0.2 return super(BlipDriver, self).event(event) @pyqtSlot() def updateAPValues(self): if self.btn_pressed == 'COURSE': val = self.remainder + 15 * self.rate self.selValues[0] += int(val) self.remainder = val - int(val) if self.btn_pressed == 'SPD': val = self.remainder + 20 * self.rate self.selValues[1] += int(val) self.remainder = val - int(val) if self.btn_pressed == 'HDG': val = self.remainder + 15 * self.rate self.selValues[2] += int(val) self.remainder = val - int(val) if self.btn_pressed == 'ALT': if abs(self.rate) > 0.06: increment = 1000 val = self.remainder + min(2.0, abs(self.rate) / 0.04) * np.sign(self.rate) elif abs(self.rate) > 0.02: increment = 100 val = self.remainder + self.rate / 0.04 else: increment = 10 val = self.remainder + self.rate / 0.02 self.selValues[3] += int(val) * increment self.remainder = val - int(val) if self.btn_pressed == 'VS': if abs(self.rate) > 0.2: increment = 1000 val = self.remainder + min(2.0, abs(self.rate) / 0.04) * np.sign(self.rate) elif abs(self.rate) > 0.05: increment = 100 val = self.remainder + self.rate / 0.04 else: increment = 10 val = self.remainder + self.rate / 0.02 self.selValues[4] += int(val) * increment self.remainder = val - int(val) self.update_lcd() if self.btn_pressed is not None: QTimer.singleShot(200, self.updateAPValues)