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)
Example #2
0
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(".")))