Example #1
0
    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
Example #2
0
    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()
Example #3
0
 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)
Example #4
0
    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
Example #5
0
    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')
Example #6
0
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])
Example #7
0
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])
Example #8
0
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
Example #9
0
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
Example #10
0
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