def __init__(self, actnode_topics=b''): ctx = zmq.Context.instance() self.event_io = ctx.socket(zmq.DEALER) self.stream_in = ctx.socket(zmq.SUB) self.poller = zmq.Poller() self.host_id = b'' self.client_id = b'\x00' + os.urandom(4) self.sender_id = b'' self.servers = dict() self.act = b'' self.actroute = [] self.acttopics = actnode_topics self.discovery = None # Signals self.nodes_changed = Signal('nodes_changed') self.server_discovered = Signal('server_discovered') self.signal_quit = Signal('quit') self.event_received = Signal('event_received') self.stream_received = Signal('stream_received') # Tell bluesky that this client will manage the network I/O bluesky.net = self # If no other object is taking care of this, let this client act as screen object as well if not bluesky.scr: bluesky.scr = self
def __init__(self): super().__init__(ACTNODE_TOPICS) self.nodedata = dict() self.ref_nodedata = nodeData() self.discovery_timer = None self.timer = QTimer() self.timer.timeout.connect(self.receive) self.timer.start(20) self.subscribe(b'SIMINFO') self.subscribe(b'PLOT' + self.client_id) self.subscribe(b'ROUTEDATA' + self.client_id) # Signals self.actnodedata_changed = Signal()
def __init__(self, glsurface, tilesource='opentopomap'): super().__init__(target=glh.Texture.Target2DArray) self.threadpool = QThreadPool() tileinfo = bs.settings.tile_sources.get(tilesource) if not tileinfo: raise KeyError(f'Tile source {tilesource} not found!') max_dl = tileinfo.get('max_download_workers', bs.settings.max_download_workers) self.maxzoom = tileinfo.get('max_tile_zoom', bs.settings.max_tile_zoom) self.threadpool.setMaxThreadCount( min(bs.settings.max_download_workers, max_dl)) self.tileslot = TiledTexture.SlotHolder(self.load_tile) self.tilesource = tilesource self.tilesize = (256, 256) self.curtileext = (0, 0, 0, 0) self.curtilezoom = 1 self.curtiles = OrderedDict() self.fullscreen = False self.offsetscale = np.array([0, 0, 1], dtype=np.float32) self.bbox = list() self.glsurface = glsurface self.indextexture = glh.Texture(target=glh.Texture.Target2D) self.indexsampler_loc = 0 self.arraysampler_loc = 0 bs.net.actnodedata_changed.connect(self.actdata_changed) Signal('panzoom').connect(self.on_panzoom_changed)
def __init__(self, actnode_topics=b''): ctx = zmq.Context.instance() self.event_io = ctx.socket(zmq.DEALER) self.stream_in = ctx.socket(zmq.SUB) self.poller = zmq.Poller() self.host_id = b'' self.client_id = b'\x00' + os.urandom(4) self.sender_id = b'' self.servers = dict() self.act = b'' self.actroute = [] self.acttopics = actnode_topics self.discovery = None # Signals self.nodes_changed = Signal() self.server_discovered = Signal() self.signal_quit = Signal() self.event_received = Signal() self.stream_received = Signal() # Tell bluesky that this client will manage the network I/O bluesky.net = self
def __init__(self, parent=None): super().__init__(parent) self.prevwidth = self.prevheight = 600 self.panlat = 0.0 self.panlon = 0.0 self.zoom = 1.0 self.ar = 1.0 self.flat_earth = 1.0 self.wraplon = int(-999) self.wrapdir = int(0) self.initialized = False self.panzoomchanged = False self.mousedragged = False self.mousepos = (0, 0) self.prevmousepos = (0, 0) self.shaderset = RadarShaders(self) self.set_shaderset(self.shaderset) # Add default objects self.addobject(Map(parent=self)) self.addobject(Traffic(parent=self)) self.addobject(Navdata(parent=self)) self.addobject(Poly(parent=self)) self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.grabGesture(Qt.GestureType.PanGesture) self.grabGesture(Qt.GestureType.PinchGesture) # self.grabGesture(Qt.SwipeGesture) self.setMouseTracking(True) # Signals and slots bs.net.actnodedata_changed.connect(self.actdata_changed) self.mouse_event = Signal('radarmouse') self.panzoom_event = Signal('panzoom')
class Client: ''' Base class for (GUI) clients of a BlueSky server. ''' def __init__(self, actnode_topics=b''): ctx = zmq.Context.instance() self.event_io = ctx.socket(zmq.DEALER) self.stream_in = ctx.socket(zmq.SUB) self.poller = zmq.Poller() self.host_id = b'' self.client_id = b'\x00' + os.urandom(4) self.sender_id = b'' self.servers = dict() self.act = b'' self.actroute = [] self.acttopics = actnode_topics self.discovery = None # Signals self.nodes_changed = Signal('nodes_changed') self.server_discovered = Signal('server_discovered') self.signal_quit = Signal('quit') self.event_received = Signal('event_received') self.stream_received = Signal('stream_received') # Tell bluesky that this client will manage the network I/O bluesky.net = self # If no other object is taking care of this, let this client act as screen object as well if not bluesky.scr: bluesky.scr = self def start_discovery(self): ''' Start UDP-based discovery of available BlueSky servers. ''' if not self.discovery: self.discovery = Discovery(self.client_id) self.poller.register(self.discovery.handle, zmq.POLLIN) self.discovery.send_request() def stop_discovery(self): ''' Stop UDP-based discovery. ''' if self.discovery: self.poller.unregister(self.discovery.handle) self.discovery = None def get_hostid(self): ''' Return the id of the host that this client is connected to. ''' return self.host_id def sender(self): ''' Return the id of the sender of the most recent event. ''' return self.sender_id def event(self, name, data, sender_id): ''' Default event handler for Client. Override this function for added functionality. ''' self.event_received.emit(name, data, sender_id) def stream(self, name, data, sender_id): ''' Default stream handler for Client. Override this function for added functionality. ''' self.stream_received.emit(name, data, sender_id) def actnode_changed(self, newact): ''' Default actnode change handler for Client. Override or monkey-patch this function to implement actual actnode change handling. ''' print('Client active node changed.') def subscribe(self, streamname, node_id=b'', actonly=False): ''' Subscribe to a stream. Arguments: - streamname: The name of the stream to subscribe to - node_id: The id of the node from which to receive the stream (optional) - actonly: Set to true if you only want to receive this stream from the active node. ''' if actonly and not node_id and streamname not in self.acttopics: self.acttopics.append(streamname) node_id = self.act self.stream_in.setsockopt(zmq.SUBSCRIBE, streamname + node_id) def unsubscribe(self, streamname, node_id=b''): ''' Unsubscribe from a stream. Arguments: - streamname: The name of the stream to unsubscribe from. - node_id: ID of the specific node to unsubscribe from. This is also used when switching active nodes. ''' if not node_id and streamname in self.acttopics: self.acttopics.remove(streamname) node_id = self.act self.stream_in.setsockopt(zmq.UNSUBSCRIBE, streamname + node_id) def connect(self, hostname='localhost', event_port=0, stream_port=0, protocol='tcp'): ''' Connect client to a server. Arguments: - hostname: Network name or ip of the server to connect to - event_port: Network port to use for event communication - stream_port: Network port to use for stream communication - protocol: Network protocol to use ''' conbase = '{}://{}'.format(protocol, hostname) econ = conbase + (':{}'.format(event_port) if event_port else '') scon = conbase + (':{}'.format(stream_port) if stream_port else '') self.event_io.setsockopt(zmq.IDENTITY, self.client_id) self.event_io.connect(econ) self.send_event(b'REGISTER') self.host_id = self.event_io.recv_multipart()[0] print('Client {} connected to host {}'.format(self.client_id, self.host_id)) self.stream_in.connect(scon) self.poller.register(self.event_io, zmq.POLLIN) self.poller.register(self.stream_in, zmq.POLLIN) def echo(self, text, flags=None, sender_id=None): ''' Default client echo function. Prints to console. Overload this function to process echo text in your GUI. ''' print(text) def update(self): ''' Client periodic update function. Periodically call this function to allow client to receive and process data. ''' self.receive() # Process any waiting stacked commands process() def receive(self, timeout=0): ''' Poll for incoming data from Server, and receive if available. Arguments: timeout: The polling timeout in milliseconds. ''' try: socks = dict(self.poller.poll(timeout)) if socks.get(self.event_io) == zmq.POLLIN: msg = self.event_io.recv_multipart() # Remove send-to-all flag if present if msg[0] == b'*': msg.pop(0) route, eventname, data = msg[:-2], msg[-2], msg[-1] self.sender_id = route[0] route.reverse() pydata = msgpack.unpackb(data, object_hook=decode_ndarray, raw=False) if eventname == b'STACK': stack(pydata, sender_id=self.sender_id) elif eventname == b'ECHO': self.echo(**pydata, sender_id=self.sender_id) elif eventname == b'NODESCHANGED': self.servers.update(pydata) self.nodes_changed.emit(pydata) # If this is the first known node, select it as active node nodes_myserver = next(iter(pydata.values())).get('nodes') if not self.act and nodes_myserver: self.actnode(nodes_myserver[0]) elif eventname == b'QUIT': self.signal_quit.emit() else: self.event(eventname, pydata, self.sender_id) if socks.get(self.stream_in) == zmq.POLLIN: msg = self.stream_in.recv_multipart() strmname = msg[0][:-5] sender_id = msg[0][-5:] pydata = msgpack.unpackb(msg[1], object_hook=decode_ndarray, raw=False) self.stream(strmname, pydata, sender_id) # If we are in discovery mode, parse this message if self.discovery and socks.get(self.discovery.handle.fileno()): dmsg = self.discovery.recv_reqreply() if dmsg.conn_id != self.client_id and dmsg.is_server: self.server_discovered.emit(dmsg.conn_ip, dmsg.ports) except zmq.ZMQError: return False def _getroute(self, target): for srv in self.servers.values(): if target in srv['nodes']: return srv['route'] return None def actnode(self, newact=None): ''' Set the new active node, or return the current active node. ''' if newact: route = self._getroute(newact) if route is None: print('Error selecting active node (unknown node)') return None # Unsubscribe from previous node, subscribe to new one. if newact != self.act: for topic in self.acttopics: if self.act: self.unsubscribe(topic, self.act) self.subscribe(topic, newact) self.actroute = route self.act = newact self.actnode_changed(newact) return self.act def addnodes(self, count=1): ''' Tell the server to add 'count' nodes. ''' self.send_event(b'ADDNODES', count) def send_event(self, name, data=None, target=None): ''' Send an event to one or all simulation node(s). Arguments: - name: Name of the event - data: Data to send as payload - target: Destination of this event. Event is sent to all nodes if * is specified as target. ''' pydata = msgpack.packb(data, default=encode_ndarray, use_bin_type=True) if not target: self.event_io.send_multipart(self.actroute + [self.act, name, pydata]) elif target == b'*': self.event_io.send_multipart([target, name, pydata]) else: self.event_io.send_multipart( self._getroute(target) + [target, name, pydata])
class Client: def __init__(self, actnode_topics=b''): ctx = zmq.Context.instance() self.event_io = ctx.socket(zmq.DEALER) self.stream_in = ctx.socket(zmq.SUB) self.poller = zmq.Poller() self.host_id = b'' self.client_id = b'\x00' + os.urandom(4) self.sender_id = b'' self.servers = dict() self.act = b'' self.actroute = [] self.acttopics = actnode_topics self.discovery = None # Signals self.nodes_changed = Signal() self.server_discovered = Signal() self.signal_quit = Signal() self.event_received = Signal() self.stream_received = Signal() # Tell bluesky that this client will manage the network I/O bluesky.net = self def start_discovery(self): if not self.discovery: self.discovery = Discovery(self.client_id) self.poller.register(self.discovery.handle, zmq.POLLIN) self.discovery.send_request() def stop_discovery(self): if self.discovery: self.poller.unregister(self.discovery.handle) self.discovery = None def get_hostid(self): return self.host_id def sender(self): return self.sender_id def event(self, name, data, sender_id): ''' Default event handler for Client. Override this function for added functionality. ''' self.event_received.emit(name, data, sender_id) def stream(self, name, data, sender_id): ''' Default stream handler for Client. Override this function for added functionality. ''' self.stream_received.emit(name, data, sender_id) def actnode_changed(self, newact): ''' Default actnode change handler for Client. Override or monkey-patch this function to implement actual actnode change handling. ''' print('Client active node changed.') def subscribe(self, streamname, node_id=b''): ''' Subscribe to a stream. ''' self.stream_in.setsockopt(zmq.SUBSCRIBE, streamname + node_id) def unsubscribe(self, streamname, node_id=b''): ''' Unsubscribe from a stream. ''' self.stream_in.setsockopt(zmq.UNSUBSCRIBE, streamname + node_id) def connect(self, hostname='localhost', event_port=0, stream_port=0, protocol='tcp'): print(" Entering connect function") conbase = '{}://{}'.format(protocol, hostname) econ = conbase + (':{}'.format(event_port) if event_port else '') scon = conbase + (':{}'.format(stream_port) if stream_port else '') print(" econ: ", econ, " scon: ", scon) self.event_io.setsockopt(zmq.IDENTITY, self.client_id) print(" 2") self.event_io.connect(econ) print(" 3") self.send_event(b'REGISTER') print(" 4") self.host_id = self.event_io.recv_multipart()[0] print(" 5") print('Client {} connected to host {}'.format(self.client_id, self.host_id)) self.stream_in.connect(scon) self.poller.register(self.event_io, zmq.POLLIN) self.poller.register(self.stream_in, zmq.POLLIN) def receive(self, timeout=0): ''' Poll for incoming data from Server, and receive if available. Arguments: timeout: The polling timeout in milliseconds. ''' try: socks = dict(self.poller.poll(timeout)) if socks.get(self.event_io) == zmq.POLLIN: msg = self.event_io.recv_multipart() # Remove send-to-all flag if present if msg[0] == b'*': msg.pop(0) route, eventname, data = msg[:-2], msg[-2], msg[-1] self.sender_id = route[0] route.reverse() pydata = msgpack.unpackb(data, object_hook=decode_ndarray, raw=False) if eventname == b'NODESCHANGED': self.servers.update(pydata) self.nodes_changed.emit(pydata) # If this is the first known node, select it as active node nodes_myserver = next(iter(pydata.values())).get('nodes') if not self.act and nodes_myserver: self.actnode(nodes_myserver[0]) elif eventname == b'QUIT': self.signal_quit.emit() else: self.event(eventname, pydata, self.sender_id) if socks.get(self.stream_in) == zmq.POLLIN: msg = self.stream_in.recv_multipart() strmname = msg[0][:-5] sender_id = msg[0][-5:] pydata = msgpack.unpackb(msg[1], object_hook=decode_ndarray, raw=False) self.stream(strmname, pydata, sender_id) # If we are in discovery mode, parse this message if self.discovery and socks.get(self.discovery.handle.fileno()): dmsg = self.discovery.recv_reqreply() if dmsg.conn_id != self.client_id and dmsg.is_server: self.server_discovered.emit(dmsg.conn_ip, dmsg.ports) except zmq.ZMQError: return False def _getroute(self, target): for srv in self.servers.values(): if target in srv['nodes']: return srv['route'] return None def actnode(self, newact=None): if newact: route = self._getroute(newact) if route is None: print('Error selecting active node (unknown node)') return None # Unsubscribe from previous node, subscribe to new one. if newact != self.act: for topic in self.acttopics: if self.act: self.unsubscribe(topic, self.act) self.subscribe(topic, newact) self.actroute = route self.act = newact self.actnode_changed(newact) return self.act def addnodes(self, count=1): self.send_event(b'ADDNODES', count) def send_event(self, name, data=None, target=None): pydata = msgpack.packb(data, default=encode_ndarray, use_bin_type=True) if not target: self.event_io.send_multipart(self.actroute + [self.act, name, pydata]) elif target == b'*': self.event_io.send_multipart([target, name, pydata]) else: self.event_io.send_multipart( self._getroute(target) + [target, name, pydata])
class GuiClient(Client): def __init__(self): super().__init__(ACTNODE_TOPICS) self.nodedata = dict() self.ref_nodedata = nodeData() self.discovery_timer = None self.timer = QTimer() self.timer.timeout.connect(self.update) self.timer.start(20) self.subscribe(b'SIMINFO') self.subscribe(b'TRAILS') self.subscribe(b'PLOT' + self.client_id) self.subscribe(b'ROUTEDATA' + self.client_id) # Signals self.actnodedata_changed = Signal('actnodedata_changed') def start_discovery(self): super().start_discovery() self.discovery_timer = QTimer() self.discovery_timer.timeout.connect(self.discovery.send_request) self.discovery_timer.start(3000) def stop_discovery(self): self.discovery_timer.stop() self.discovery_timer = None super().stop_discovery() def stream(self, name, data, sender_id): ''' Guiclient stream handler. ''' changed = '' actdata = self.get_nodedata(sender_id) if name == b'ACDATA': actdata.setacdata(data) changed = name.decode('utf8') elif name.startswith(b'ROUTEDATA'): actdata.setroutedata(data) changed = 'ROUTEDATA' elif name == b'TRAILS': actdata.settrails(**data) changed = name.decode('utf8') if sender_id == self.act and changed: self.actnodedata_changed.emit(sender_id, actdata, changed) super().stream(name, data, sender_id) def echo(self, text, flags=None, sender_id=None): ''' Overloaded Client.echo function. ''' sender_data = self.get_nodedata(sender_id) sender_data.echo(text, flags) # If sender_id is None this is an echo command originating from the gui user, and therefore also meant for the active node sender_id = sender_id or self.act if sender_id == self.act: self.actnodedata_changed.emit(sender_id, sender_data, ('ECHOTEXT',)) def event(self, name, data, sender_id): sender_data = self.get_nodedata(sender_id) data_changed = [] if name == b'RESET': sender_data.clear_scen_data() data_changed = list(UPDATE_ALL) elif name == b'SHAPE': sender_data.update_poly_data(**data) data_changed.append('SHAPE') elif name == b'COLOR': sender_data.update_color_data(**data) if 'polyid' in data: data_changed.append('SHAPE') elif name == b'DEFWPT': sender_data.defwpt(**data) data_changed.append('CUSTWPT') elif name == b'DISPLAYFLAG': sender_data.setflag(**data) elif name == b'ECHO': data_changed.append('ECHOTEXT') elif name == b'PANZOOM': sender_data.panzoom(**data) data_changed.append('PANZOOM') elif name == b'SIMSTATE': sender_data.siminit(**data) data_changed = list(UPDATE_ALL) else: super().event(name, data, sender_id) if sender_id == self.act and data_changed: self.actnodedata_changed.emit(sender_id, sender_data, data_changed) def actnode_changed(self, newact): self.actnodedata_changed.emit(newact, self.get_nodedata(newact), UPDATE_ALL) def get_nodedata(self, nodeid=None): nodeid = nodeid or self.act if not nodeid: return self.ref_nodedata data = self.nodedata.get(nodeid) if not data: # If this is a node we haven't addressed yet: create dataset and # request node settings self.nodedata[nodeid] = data = nodeData() self.send_event(b'GETSIMSTATE', target=nodeid) return data
class RadarWidget(glh.RenderWidget): ''' The BlueSky radar view. ''' def __init__(self, parent=None): super().__init__(parent) self.prevwidth = self.prevheight = 600 self.panlat = 0.0 self.panlon = 0.0 self.zoom = 1.0 self.ar = 1.0 self.flat_earth = 1.0 self.wraplon = int(-999) self.wrapdir = int(0) self.initialized = False self.panzoomchanged = False self.mousedragged = False self.mousepos = (0, 0) self.prevmousepos = (0, 0) self.shaderset = RadarShaders(self) self.set_shaderset(self.shaderset) # Add default objects self.addobject(Map(parent=self)) self.addobject(Traffic(parent=self)) self.addobject(Navdata(parent=self)) self.addobject(Poly(parent=self)) self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.grabGesture(Qt.GestureType.PanGesture) self.grabGesture(Qt.GestureType.PinchGesture) # self.grabGesture(Qt.SwipeGesture) self.setMouseTracking(True) # Signals and slots bs.net.actnodedata_changed.connect(self.actdata_changed) self.mouse_event = Signal('radarmouse') self.panzoom_event = Signal('panzoom') def actdata_changed(self, nodeid, nodedata, changed_elems): ''' Update buffers when a different node is selected, or when the data of the current node is updated. ''' # Update pan/zoom if 'PANZOOM' in changed_elems: self.panzoom(pan=nodedata.pan, zoom=nodedata.zoom, absolute=True) def initializeGL(self): """Initialize OpenGL, VBOs, upload data on the GPU, etc.""" super().initializeGL() # Set initial values for the global uniforms self.shaderset.set_wrap(self.wraplon, self.wrapdir) self.shaderset.set_pan_and_zoom(self.panlat, self.panlon, self.zoom) # background color glh.gl.glClearColor(0.7, 0.7, 0.7, 0) glh.gl.glEnable(glh.gl.GL_BLEND) glh.gl.glBlendFunc(glh.gl.GL_SRC_ALPHA, glh.gl.GL_ONE_MINUS_SRC_ALPHA) self.initialized = True def resizeGL(self, width, height): """Called upon window resizing: reinitialize the viewport.""" # update the window size # Calculate zoom so that the window resize doesn't affect the scale, but only enlarges or shrinks the view zoom = float(self.prevwidth) / float(width) origin = (width / 2, height / 2) # Update width, height, and aspect ratio self.prevwidth, self.prevheight = width, height self.ar = float(width) / max(1, float(height)) self.shaderset.set_win_width_height(width, height) # Update zoom self.panzoom(zoom=zoom, origin=origin) 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.prevwidth - 1.0) gly = -(float(2.0 * y) / self.prevheight - 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 viewportlatlon(self): ''' Return the viewport bounds in lat/lon coordinates. ''' return (self.panlat + 1.0 / (self.zoom * self.ar), self.panlon - 1.0 / (self.zoom * self.flat_earth), self.panlat - 1.0 / (self.zoom * self.ar), self.panlon + 1.0 / (self.zoom * self.flat_earth)) def panzoom(self, pan=None, zoom=None, origin=None, absolute=False): if not self.initialized: return False if pan: # Absolute pan operation if absolute: self.panlat = pan[0] self.panlon = pan[1] # Relative pan operation else: self.panlat += pan[0] self.panlon += 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 zoom: if absolute: # Limit zoom extents in x-direction to [-180:180], and in y-direction to [-90:90] self.zoom = max( zoom, 1.0 / min(90.0 * self.ar, 180.0 * self.flat_earth)) else: prevzoom = self.zoom glx, gly = self.pixelCoordsToGLxy(*origin) if origin else (0, 0) self.zoom *= 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)) # 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.shaderset.set_wrap(self.wraplon, self.wrapdir) # update pan and zoom on GPU for all shaders self.shaderset.set_pan_and_zoom(self.panlat, self.panlon, self.zoom) # Update pan and zoom in centralized nodedata bs.net.get_nodedata().panzoom((self.panlat, self.panlon), self.zoom) self.panzoom_event.emit(False) return True def event(self, event): ''' Event handling for input events. ''' if event.type() == QEvent.Type.Wheel: # For mice we zoom with control/command and the scrolwheel if event.modifiers() & Qt.KeyboardModifier.ControlModifier: origin = (event.pos().x(), event.pos().y()) zoom = 1.0 try: if event.pixelDelta(): # High resolution scroll zoom *= (1.0 + 0.01 * event.pixelDelta().y()) else: # Low resolution scroll zoom *= (1.0 + 0.001 * event.angleDelta().y()) except AttributeError: zoom *= (1.0 + 0.001 * event.delta()) self.panzoomchanged = True return self.panzoom(zoom=zoom, origin=origin) # For touchpad scroll (2D) is used for panning else: try: dlat = 0.01 * event.pixelDelta().y() / (self.zoom * self.ar) dlon = -0.01 * event.pixelDelta().x() / (self.zoom * self.flat_earth) self.panzoomchanged = True return self.panzoom(pan=(dlat, dlon)) except AttributeError: pass # For touchpad, pinch gesture is used for zoom elif event.type() == QEvent.Type.Gesture: pan = zoom = None dlat = dlon = 0.0 for g in event.gestures(): if g.gestureType() == Qt.GestureType.PinchGesture: event.accept(g) zoom = g.scaleFactor() * (zoom or 1.0) if CORRECT_PINCH: zoom /= g.lastScaleFactor() elif g.gestureType() == Qt.GestureType.PanGesture: event.accept(g) if abs(g.delta().y() + g.delta().x()) > 1e-1: dlat += 0.005 * g.delta().y() / (self.zoom * self.ar) dlon -= 0.005 * g.delta().x() / (self.zoom * self.flat_earth) pan = (dlat, dlon) if pan is not None or zoom is not None: self.panzoomchanged = True return self.panzoom(pan, zoom, self.mousepos) elif event.type() == QEvent.Type.MouseButtonPress and event.button( ) & Qt.MouseButton.LeftButton: self.mousedragged = False # For mice we pan with control/command and mouse movement. # Mouse button press marks the beginning of a pan self.prevmousepos = (event.pos().x(), event.pos().y()) elif event.type() == QEvent.Type.MouseButtonRelease and \ event.button() & Qt.MouseButton.LeftButton and not self.mousedragged: lat, lon = self.pixelCoordsToLatLon(event.pos().x(), event.pos().y()) actdata = bs.net.get_nodedata() tostack, tocmdline = radarclick(console.get_cmdline(), lat, lon, actdata.acdata, actdata.routedata) console.process_cmdline((tostack + '\n' + tocmdline) if tostack else tocmdline) elif event.type() == QEvent.Type.MouseMove: self.mousedragged = True self.mousepos = (event.pos().x(), event.pos().y()) if event.buttons() & Qt.MouseButton.LeftButton: dlat = 0.003 * \ (event.pos().y() - self.prevmousepos[1]) / (self.zoom * self.ar) dlon = 0.003 * \ (self.prevmousepos[0] - event.pos().x()) / \ (self.zoom * self.flat_earth) self.prevmousepos = (event.pos().x(), event.pos().y()) self.panzoomchanged = True return self.panzoom(pan=(dlat, dlon)) elif event.type() == QEvent.Type.TouchBegin: # Accept touch start to enable reception of follow-on touch update and touch end events event.accept() # Update pan/zoom to simulation thread only when the pan/zoom gesture is finished elif (event.type() == QEvent.Type.MouseButtonRelease or event.type() == QEvent.Type.TouchEnd) and self.panzoomchanged: self.panzoomchanged = False bs.net.send_event( b'PANZOOM', dict(pan=(self.panlat, self.panlon), zoom=self.zoom, ar=self.ar, absolute=True)) self.panzoom_event.emit(True) else: return super().event(event) # If we get here, the event was a mouse/trackpad event. Emit it to interested children self.mouse_event.emit(event) # For all other events call base class event handling return True
class GuiClient(Client): def __init__(self): super().__init__(ACTNODE_TOPICS) self.nodedata = dict() self.ref_nodedata = nodeData() self.discovery_timer = None self.timer = QTimer() self.timer.timeout.connect(self.receive) self.timer.start(20) self.subscribe(b'SIMINFO') self.subscribe(b'PLOT' + self.client_id) self.subscribe(b'ROUTEDATA' + self.client_id) # Signals self.actnodedata_changed = Signal() def start_discovery(self): super().start_discovery() self.discovery_timer = QTimer() self.discovery_timer.timeout.connect(self.discovery.send_request) self.discovery_timer.start(3000) def stop_discovery(self): self.discovery_timer.stop() self.discovery_timer = None super().stop_discovery() def event(self, name, data, sender_id): sender_data = self.get_nodedata(sender_id) data_changed = [] if name == b'RESET': sender_data.clear_scen_data() data_changed = list(UPDATE_ALL) elif name == b'SHAPE': sender_data.update_poly_data(**data) data_changed.append('SHAPE') elif name == b'COLOR': sender_data.update_color_data(**data) if 'polyid' in data: data_changed.append('SHAPE') elif name == b'DEFWPT': sender_data.defwpt(**data) data_changed.append('CUSTWPT') elif name == b'DISPLAYFLAG': sender_data.setflag(**data) elif name == b'ECHO': sender_data.echo(**data) data_changed.append('ECHOTEXT') elif name == b'PANZOOM': sender_data.panzoom(**data) data_changed.append('PANZOOM') elif name == b'SIMSTATE': sender_data.siminit(**data) data_changed = list(UPDATE_ALL) else: super().event(name, data, sender_id) if sender_id == self.act and data_changed: self.actnodedata_changed.emit(sender_id, sender_data, data_changed) def actnode_changed(self, newact): self.actnodedata_changed.emit(newact, self.get_nodedata(newact), UPDATE_ALL) def get_nodedata(self, nodeid=None): nodeid = nodeid or self.act if not nodeid: return self.ref_nodedata data = self.nodedata.get(nodeid) if not data: # If this is a node we haven't addressed yet: create dataset and # request node settings self.nodedata[nodeid] = data = nodeData() self.send_event(b'GETSIMSTATE', target=nodeid) return data