class SignalStream(QObject): '''SignalStream is a file-like object that emits a text signal on writing This class is used to provide threadsafe communication of data to the GUI. A SignalStream can be used in place of sys.stdout and the instance's write_signal can be connected to a slot that processes the text to where it ought to go. Since signals and slots are threadsafe, this lets you pass text from anywhere to anywhere reasonably safely SignalStream uses some intelligent buffering to prevent the signalstorm that happened the first time I used it. Signal emit only happens when flush() is called - so an application can force a flush - but in order to make sure that happens reasonable often SignalStream can be initialized with a QTimer on an interval (default: 100ms) and the QTimer will make sure to call flush() every 100ms. ''' write_signal = pyqtSignal(str) def __init__(self, interval_ms=100): '''Create a SignalStream that emits text at least every interval_ms''' super(SignalStream, self).__init__() self.mutex = QMutex() self.data = [] self.thread = QThread() self.pbar_timer = QTimer() self.pbar_timer.moveToThread(self.thread) self.pbar_timer.setInterval(interval_ms) self.pbar_timer.timeout.connect(self.flush) self.thread.started.connect(self.pbar_timer.start) self.thread.start() def __del__(self): self.thread.quit() self.thread.wait() def write(self, m): '''Add the message in m to this stream's cache''' locker = QMutexLocker(self.mutex) self.data.append(m) @pyqtSlot() def flush(self): '''Write all data in the stream and clear the stream's cache''' locker = QMutexLocker(self.mutex) if self.data: self.write_signal.emit(''.join(self.data)) self.data = [] def set_interval(self, interval_ms): '''Alter the pbar_timer period''' self.pbar_timer.setInteval(interval_ms)
class ScreenIO(QObject): # ========================================================================= # Settings # ========================================================================= # Update rate of simulation info messages [Hz] siminfo_rate = 2 # Update rate of aircraft update messages [Hz] acupdate_rate = 5 # ========================================================================= # Slots # ========================================================================= @pyqtSlot() def send_siminfo(self): t = time.time() dt = t - self.prevtime qapp.postEvent( qapp.instance(), SimInfoEvent( (self.sim.samplecount - self.prevcount) / dt, self.sim.simdt, self.sim.simt, self.sim.traf.ntraf, self.sim.mode, ), ) self.prevtime = t self.prevcount = self.sim.samplecount @pyqtSlot() def send_aircraft_data(self): data = ACDataEvent() data.id = list(self.sim.traf.id) data.lat = np.array(self.sim.traf.lat, dtype=np.float32, copy=True) data.lon = np.array(self.sim.traf.lon, dtype=np.float32, copy=True) data.alt = np.array(self.sim.traf.alt, dtype=np.float32, copy=True) data.tas = np.array(self.sim.traf.tas, dtype=np.float32, copy=True) data.iconf = np.array(self.sim.traf.iconf, copy=True) data.confcpalat = np.array(self.sim.traf.dbconf.latowncpa, copy=True) data.confcpalon = np.array(self.sim.traf.dbconf.lonowncpa, copy=True) data.trk = np.array(self.sim.traf.trk, dtype=np.float32, copy=True) qapp.postEvent(qapp.instance(), data) # ========================================================================= # Functions # ========================================================================= def __init__(self, sim): super(ScreenIO, self).__init__() # Keep track of the important parameters of the screen state # (We receive these through events from the gui) self.ctrlat = 0.0 self.ctrlon = 0.0 self.zoom = 1.0 # Keep reference to parent simulation object for access to simulation data self.sim = sim # Timing bookkeeping counters self.prevtime = 0.0 self.prevcount = 0 # Output event timers self.siminfo_timer = QTimer() self.siminfo_timer.timeout.connect(self.send_siminfo) self.siminfo_timer.start(1000 / self.siminfo_rate) self.acupdate_timer = QTimer() self.acupdate_timer.timeout.connect(self.send_aircraft_data) self.acupdate_timer.start(1000 / self.acupdate_rate) def moveToThread(self, target_thread): self.siminfo_timer.moveToThread(target_thread) self.acupdate_timer.moveToThread(target_thread) super(ScreenIO, self).moveToThread(target_thread) def echo(self, text): qapp.postEvent(qapp.instance(), StackTextEvent(text)) def zoom(self, zoomfac, absolute=False): if absolute: self.zoom = zoomfac else: self.zoom *= zoomfac qapp.postEvent(qapp.instance(), PanZoomEvent(zoom=zoomfac, absolute=absolute)) def pan(self, pan, absolute=False): if absolute: self.ctrlat = pan[0] self.ctrlon = pan[1] else: self.ctrlat += pan[0] self.ctrlon += pan[1] qapp.postEvent(qapp.instance(), PanZoomEvent(pan=pan, absolute=absolute)) def showroute(self, acid): data = RouteDataEvent() data.acidx = self.sim.traf.id2idx(acid) if data.acidx >= 0: route = self.sim.traf.route[data.acidx] n_segments = len(route.wplat) + 1 data.lat = np.empty(n_segments, dtype=np.float32) data.lat[0] = self.sim.traf.lat[data.acidx] data.lat[1:] = route.wplat data.lon = np.empty(n_segments, dtype=np.float32) data.lon[0] = self.sim.traf.lon[data.acidx] data.lon[1:] = route.wplon qapp.postEvent(qapp.instance(), data) def showssd(self, param): if param == "ALL" or param == "OFF": qapp.postEvent(qapp.instance(), DisplayFlagEvent("SSD", param)) else: idx = self.sim.traf.id2idx(param) if idx >= 0: qapp.postEvent(qapp.instance(), DisplayFlagEvent("SSD", idx)) def show_file_dialog(self): qapp.postEvent(qapp.instance(), ShowDialogEvent()) return "" def feature(self, switch, argument=""): qapp.postEvent(qapp.instance(), DisplayFlagEvent(switch, argument)) def objappend(self, objtype, objname, data_in): if data_in is None: # This is an object delete event data = None elif objtype == 1 or objtype == 4: # LINE(1) or POLY(4) data = np.array(data_in, dtype=np.float32) elif objtype == 2: # BOX data = np.array( [data_in[0], data_in[1], data_in[0], data_in[3], data_in[2], data_in[3], data_in[2], data_in[1]], dtype=np.float32, ) elif objtype == 3: # CIRCLE pass qapp.postEvent(qapp.instance(), DisplayShapeEvent(objname, data)) def event(self, event): if event.type() == StackTextEventType: self.sim.stack.stack(event.text) elif event.type() == PanZoomEventType: self.ctrlat = event.pan[0] self.ctrlon = event.pan[1] self.zoom = event.zoom return True
class FileSystemEventsManager(QThread): """ Defines the file system events manager. """ # Custom signals definitions. fileChanged = pyqtSignal(unicode) """ This signal is emited by the :class:`FileSystemEventsManager` class when a file is changed. ( pyqtSignal ) :return: Current changed file. :rtype: unicode """ fileInvalidated = pyqtSignal(unicode) """ This signal is emited by the :class:`FileSystemEventsManager` class when a file is invalidated. ( pyqtSignal ) :return: Current invalidated file. :rtype: unicode """ directoryChanged = pyqtSignal(unicode) """ This signal is emited by the :class:`FileSystemEventsManager` class when a directory is changed. ( pyqtSignal ) :return: Current changed directory. :rtype: unicode """ directoryInvalidated = pyqtSignal(unicode) """ This signal is emited by the :class:`FileSystemEventsManager` class when a directory is invalidated. ( pyqtSignal ) :return: Current invalidated directory. :rtype: unicode """ def __init__(self, parent=None): """ Initializes the class. """ LOGGER.debug("> Initializing '{0}()' class.".format(self.__class__.__name__)) QThread.__init__(self, parent) # --- Setting class attributes. --- self.__container = parent self.__paths = {} self.__timer = None self.__timerCycleMultiplier = 5 self.__toRegisterPaths = {} self.__toUnregisterPaths = [] #****************************************************************************************************************** #*** Attributes properties. #****************************************************************************************************************** @property def container(self): """ Property for **self.__container** attribute. :return: self.__container. :rtype: QObject """ return self.__container @container.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def container(self, value): """ Setter for **self.__container** attribute. :param value: Attribute value. :type value: QObject """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "container")) @container.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def container(self): """ Deleter for **self.__container** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "container")) @property def paths(self): """ Property for **self.__paths** attribute. :return: self.__paths. :rtype: dict """ return self.__paths @paths.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def paths(self, value): """ Setter for **self.__paths** attribute. :param value: Attribute value. :type value: dict """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "paths")) @paths.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def paths(self): """ Deleter for **self.__paths** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "paths")) @property def timer(self): """ Property for **self.__timer** attribute. :return: self.__timer. :rtype: QTimer """ return self.__timer @timer.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def timer(self, value): """ Setter for **self.__timer** attribute. :param value: Attribute value. :type value: QTimer """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "timer")) @timer.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def timer(self): """ Deleter for **self.__timer** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "timer")) @property def timerCycleMultiplier(self): """ Property for **self.__timerCycleMultiplier** attribute. :return: self.__timerCycleMultiplier. :rtype: float """ return self.__timerCycleMultiplier @timerCycleMultiplier.setter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def timerCycleMultiplier(self, value): """ Setter for **self.__timerCycleMultiplier** attribute. :param value: Attribute value. :type value: float """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "timerCycleMultiplier")) @timerCycleMultiplier.deleter @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError) def timerCycleMultiplier(self): """ Deleter for **self.__timerCycleMultiplier** attribute. """ raise foundations.exceptions.ProgrammingError( "{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "timerCycleMultiplier")) #****************************************************************************************************************** #*** Class methods. #****************************************************************************************************************** def __getitem__(self, path): """ Reimplements the :meth:`object.__getitem__` method. :param path: Path name. :type path: unicode :return: Path. :rtype: Path """ return self.__paths.__getitem__(path) def __setitem__(self, path, modifiedTime): """ Reimplements the :meth:`object.__setitem__` method. :param path: Path. :type path: unicode :param modifiedTime: Modified time. :type modifiedTime: int or float """ self.registerPath(path, modifiedTime) def __iter__(self): """ Reimplements the :meth:`object.__iter__` method. :return: Paths iterator. :rtype: object """ return self.__paths.iteritems() def __contains__(self, path): """ Reimplements the :meth:`object.__contains__` method. :param path: Path name. :type path: unicode :return: Path existence. :rtype: bool """ return path in self.__paths def __len__(self): """ Reimplements the :meth:`object.__len__` method. :return: Paths count. :rtype: int """ return len(self.__paths) def get(self, path, default=None): """ Returns given path value. :param path: Path name. :type path: unicode :param default: Default value if path is not found. :type default: object :return: Action. :rtype: QAction """ try: return self.__getitem__(path) except KeyError as error: return default def run(self): """ Reimplements the :meth:`QThread.run` method. """ self.__timer = QTimer() self.__timer.moveToThread(self) self.__timer.start(Constants.defaultTimerCycle * self.__timerCycleMultiplier) self.__timer.timeout.connect(self.__watchFileSystem, Qt.DirectConnection) self.exec_() def __watchFileSystem(self): """ Watches the file system for paths that have been changed or invalidated on disk. """ for path, data in self.__paths.items(): storedModifiedTime, isFile = data try: if not foundations.common.pathExists(path): LOGGER.warning( "!> {0} | '{1}' path has been invalidated and will be unregistered!".format(self.__class__.__name__, path)) del(self.__paths[path]) if isFile: self.fileInvalidated.emit(path) else: self.directoryInvalidated.emit(path) continue except KeyError: LOGGER.debug("> {0} | '{1}' path has been unregistered while iterating!".format( self.__class__.__name__, path)) continue try: modifiedTime = self.getPathModifiedTime(path) except OSError: LOGGER.debug("> {0} | '{1}' path has been invalidated while iterating!".format( self.__class__.__name__, path)) continue if storedModifiedTime != modifiedTime: self.__paths[path] = (modifiedTime, os.path.isfile(path)) LOGGER.debug("> {0} | '{1}' path has been changed!".format(self.__class__.__name__, path)) if isFile: self.fileChanged.emit(path) else: self.directoryChanged.emit(path) def listPaths(self): """ Returns the registered paths. :return: Registered paths. :rtype: list """ return sorted(self.__paths.keys()) def isPathRegistered(self, path): """ Returns if the given path is registered. :param path: Path name. :type path: unicode :return: Is path registered. :rtype: bool """ return path in self @foundations.exceptions.handleExceptions(foundations.exceptions.PathExistsError, umbra.exceptions.PathRegistrationError) def registerPath(self, path, modifiedTime=None): """ Registers given path. :param path: Path name. :type path: unicode :param modifiedTime: Custom modified time. :type modifiedTime: int or float :return: Method success. :rtype: bool """ if not foundations.common.pathExists(path): raise foundations.exceptions.PathExistsError("{0} | '{1}' path doesn't exists!".format( self.__class__.__name__, path)) if path in self: raise umbra.exceptions.PathRegistrationError("{0} | '{1}' path is already registered!".format( self.__class__.__name__, path)) self.__paths[path] = (self.getPathModifiedTime(path) if modifiedTime is None else modifiedTime, os.path.isfile(path)) return True @foundations.exceptions.handleExceptions(umbra.exceptions.PathExistsError) def unregisterPath(self, path): """ Unregisters given path. :param path: Path name. :type path: unicode :return: Method success. :rtype: bool """ if not path in self: raise umbra.exceptions.PathExistsError("{0} | '{1}' path isn't registered!".format( self.__class__.__name__, path)) del(self.__paths[path]) return True @staticmethod def getPathModifiedTime(path): """ Returns given path modification time. :param path: Path. :type path: unicode :return: Modification time. :rtype: int """ return float(foundations.common.getFirstItem(str(os.path.getmtime(path)).split(".")))