<filename>mw4/gui/mainWmixin/tabSatTrack.py
############################################################
# -*- coding: utf-8 -*-
#
#       #   #  #   #   #    #
#      ##  ##  #  ##  #    #
#     # # # #  # # # #    #  #
#    #  ##  #  ##  ##    ######
#   #   #   #  #   #       #
#
# Python-based Tool for interaction with the 10micron mounts
# GUI with PyQT5 for python
#
# written in python3, (c) 2019-2021 by mworion
# Licence APL2.0
#
###########################################################
# standard libraries
import logging

# external packages
from PyQt5.QtCore import pyqtSignal, QObject
import numpy as np
from sgp4.exporter import export_tle
from skyfield import almanac

# local import
from mountcontrol.convert import valueToInt


class SatControlWidget(QObject):
    """
    """

    log = logging.getLogger(__name__)
    valueChanged = pyqtSignal(object)

    def __init__(self, ui, minVal, maxVal):
        super().__init__()
        self.ui = ui
        self.minVal = minVal
        self.maxVal = maxVal
        self.ui[0].setMinimum(minVal)
        self.ui[0].setMaximum(maxVal)
        self.ui[0].sliderReleased.connect(self.updateSatOff)
        self.ui[1].editingFinished.connect(self.updateSatOffVal)
        self.ui[2].clicked.connect(self.updateSatOffButton)
        self.ui[3].clicked.connect(self.updateSatOffButton)
        self.ui[4].clicked.connect(self.updateSatOffButton)
        self.ui[5].clicked.connect(self.updateSatOffButton)
        self.ui[6].clicked.connect(self.updateSatOffButton)
        self.ui[7].clicked.connect(self.updateSatOffButton)

    def updateSatOff(self):
        """
        :return:
        """
        val = self.ui[0].value()
        self.ui[1].setText(str(val))
        self.valueChanged.emit(val)
        return True

    def updateSatOffButton(self):
        """
        :return:
        """
        senders = {self.ui[2]: 100,
                   self.ui[3]: 10,
                   self.ui[4]: 1,
                   self.ui[5]: -100,
                   self.ui[6]: -10,
                   self.ui[7]: -1,
                   }
        delta = senders[self.sender()]
        val = self.ui[0].value()
        val += delta
        val = min(self.maxVal, val)
        val = max(self.minVal, val)
        self.ui[0].setValue(val)
        self.ui[1].setText(str(val))
        self.valueChanged.emit(val)
        return True

    def updateSatOffVal(self):
        """
        :return:
        """
        val = valueToInt(self.ui[1].text())
        if val is None:
            val = self.ui[0].value()
            self.ui[1].setText(str(val))
        else:
            val = min(self.maxVal, val)
            val = max(self.minVal, val)
            self.ui[0].setValue(val)
            self.valueChanged.emit(val)
        return True


class SatTrack(object):
    """
    """

    def __init__(self):
        self.satOrbits = None
        self.satellitesRawTLE = {}
        self.nextSatPass = [None, None, None]
        self.lastMeridianLimit = None

        self.passUI = {
            0: {'rise': self.ui.satRise_1,
                'culminate': self.ui.satCulminate_1,
                'settle': self.ui.satSettle_1,
                'flip': self.ui.satFlip_1,
                'date': self.ui.satDate_1},
            1: {'rise': self.ui.satRise_2,
                'culminate': self.ui.satCulminate_2,
                'settle': self.ui.satSettle_2,
                'flip': self.ui.satFlip_2,
                'date': self.ui.satDate_2},
            2: {'rise': self.ui.satRise_3,
                'culminate': self.ui.satCulminate_3,
                'settle': self.ui.satSettle_3,
                'flip': self.ui.satFlip_3,
                'date': self.ui.satDate_3}
        }
        self.satOrbits = dict()

        msig = self.app.mount.signals
        msig.calcTLEdone.connect(self.updateSatelliteTrackGui)
        msig.calcTrajectoryDone.connect(self.updateSatelliteTrackGui)
        msig.getTLEdone.connect(self.getSatelliteDataFromDatabase)
        msig.trajectoryProgress.connect(self.trajectoryProgress)
        msig.pointDone.connect(self.followMount)
        msig.settingDone.connect(self.updatePasses)
        msig.pointDone.connect(self.toggleTrackingOffset)

        self.ui.startSatelliteTracking.clicked.connect(self.startTrack)
        self.ui.stopSatelliteTracking.clicked.connect(self.stopTrack)
        self.app.sendSatelliteData.connect(self.sendSatelliteData)
        self.ui.satAfterFlip.clicked.connect(self.showSatPasses)
        self.ui.satBeforeFlip.clicked.connect(self.showSatPasses)
        self.ui.avoidHorizon.clicked.connect(self.showSatPasses)
        self.ui.useInternalSatCalc.clicked.connect(self.showSatPasses)
        self.ui.useInternalSatCalc.clicked.connect(self.enableGuiFunctions)
        self.ui.progTrajectory.clicked.connect(self.startProg)

        self.app.update1s.connect(self.updateOrbit)
        self.satOffTime = SatControlWidget([self.ui.satOffTime,
                                            self.ui.satOffTimeVal,
                                            self.ui.satOffTimeP100,
                                            self.ui.satOffTimeP10,
                                            self.ui.satOffTimeP1,
                                            self.ui.satOffTimeM100,
                                            self.ui.satOffTimeM10,
                                            self.ui.satOffTimeM1,
                                            ], -1000, 1000)
        self.satOffRa = SatControlWidget([self.ui.satOffRa,
                                          self.ui.satOffRaVal,
                                          self.ui.satOffRaP100,
                                          self.ui.satOffRaP10,
                                          self.ui.satOffRaP1,
                                          self.ui.satOffRaM100,
                                          self.ui.satOffRaM10,
                                          self.ui.satOffRaM1,
                                          ], -1800, 1800)
        self.satOffDec = SatControlWidget([self.ui.satOffDec,
                                           self.ui.satOffDecVal,
                                           self.ui.satOffDecP100,
                                           self.ui.satOffDecP10,
                                           self.ui.satOffDecP1,
                                           self.ui.satOffDecM100,
                                           self.ui.satOffDecM10,
                                           self.ui.satOffDecM1,
                                           ], -1800, 1800)

        self.satOffTime.valueChanged.connect(self.setTrackingOffsets)
        self.satOffRa.valueChanged.connect(self.setTrackingOffsets)
        self.satOffDec.valueChanged.connect(self.setTrackingOffsets)

    def initConfig(self):
        """
        :return: True for test purpose
        """
        config = self.app.config['mainW']

        self.ui.domeAutoFollowSat.setChecked(config.get('domeAutoFollowSat',
                                                        False))
        self.ui.useInternalSatCalc.setChecked(config.get('useInternalSatCalc',
                                                         False))
        self.ui.satBeforeFlip.setChecked(config.get('satBeforeFlip', True))
        self.ui.satAfterFlip.setChecked(config.get('satAfterFlip', True))
        self.ui.avoidHorizon.setChecked(config.get('avoidHorizon', False))
        self.ui.trackingSim.setChecked(config.get('trackingSim', False))

        if not self.app.automation:
            self.installPath = self.app.mwGlob['dataDir']
        elif self.app.automation.installPath:
            self.installPath = self.app.automation.installPath
        else:
            self.installPath = self.app.mwGlob['dataDir']
        return True

    def storeConfig(self):
        """
        :return:
        """
        config = self.app.config['mainW']
        config['domeAutoFollowSat'] = self.ui.domeAutoFollowSat.isChecked()
        config['useInternalSatCalc'] = self.ui.useInternalSatCalc.isChecked()
        config['satBeforeFlip'] = self.ui.satBeforeFlip.isChecked()
        config['satAfterFlip'] = self.ui.satAfterFlip.isChecked()
        config['avoidHorizon'] = self.ui.avoidHorizon.isChecked()
        config['trackingSim'] = self.ui.trackingSim.isChecked()
        return True

    def calcPassEvents(self, obsSite):
        """
        :return:
        """
        minAlt = self.app.mount.setting.horizonLimitLow
        if minAlt is None:
            minAlt = 5
        if minAlt < 5:
            minAlt = 5

        loc = obsSite.location
        orbitCycleTime = np.pi / self.satellite.model.no_kozai / 12 / 60

        t0 = obsSite.ts.tt_jd(obsSite.timeJD.tt - orbitCycleTime)
        t1 = obsSite.ts.tt_jd(obsSite.timeJD.tt + 5)
        times, events = self.satellite.find_events(loc, t0, t1,
                                                   altitude_degrees=minAlt)
        return times, events

    def extractOrbits(self, timeNow, times, events):
        """
        :param timeNow:
        :param times:
        :param events:
        :return:
        """
        counter = 0
        self.satOrbits = []

        for ti, event in zip(times, events):
            if event == 0:
                self.satOrbits.append({'rise': ti})

            elif event == 1:
                if counter >= len(self.satOrbits):
                    continue
                self.satOrbits[counter]['culminate'] = ti

            elif event == 2:
                if counter >= len(self.satOrbits):
                    continue
                self.satOrbits[counter]['settle'] = ti

                if ti.tt < timeNow.tt:
                    del self.satOrbits[counter]
                    continue
                counter += 1

            if counter > 2:
                break

        if not self.satOrbits:
            return False
        if 'settle' not in self.satOrbits[-1]:
            del self.satOrbits[counter]
            return False

        return True

    @staticmethod
    def calcSatelliteMeridianTransit(satellite, location, tolerance):
        """
        :param satellite:
        :param location:
        :param tolerance:
        :return:
        """
        difference = satellite - location

        def west_of_meridian_at(t):
            alt, az, _ = difference.at(t).altaz()
            delta = (az.degrees + tolerance + 360) % 360 - 180
            return delta < 0

        west_of_meridian_at.step_days = 0.4
        return west_of_meridian_at

    @staticmethod
    def sortFlipEvents(satOrbit, t0, t1, t2):
        """
        :param satOrbit:
        :param t0:
        :param t1:
        :param t2:
        :return:
        """
        settle = satOrbit['settle']
        rise = satOrbit['rise']
        if t0:
            satOrbit['flip'] = t0[0]
        if t1 and t2:
            if t1[0].tt > t2[0].tt:
                satOrbit['flipEarly'] = t2[0]
                satOrbit['flipLate'] = t1[0]
            else:
                satOrbit['flipEarly'] = t1[0]
                satOrbit['flipLate'] = t2[0]
        if t1 and not t2:
            if abs(rise.tt - t1[0].tt) > abs(settle.tt - t1[0].tt):
                satOrbit['flipLate'] = t1[0]
            else:
                satOrbit['flipEarly'] = t1[0]
        if not t1 and t2:
            if abs(rise.tt - t2[0].tt) > abs(settle.tt - t2[0].tt):
                satOrbit['flipLate'] = t2[0]
            else:
                satOrbit['flipEarly'] = t2[0]

        return True

    def addMeridianTransit(self, location):
        """
        :param location:
        :return:
        """
        limit = self.app.mount.setting.meridianLimitTrack
        if limit is None:
            limit = 0
        limit = limit * 0.95

        f0 = self.calcSatelliteMeridianTransit(self.satellite, location, 0)
        f1 = self.calcSatelliteMeridianTransit(self.satellite, location, limit)
        f2 = self.calcSatelliteMeridianTransit(self.satellite, location, -limit)
        for satOrbit in self.satOrbits:
            t0, y0 = almanac.find_discrete(satOrbit['rise'],
                                           satOrbit['settle'], f0)
            t1, y1 = almanac.find_discrete(satOrbit['rise'],
                                           satOrbit['settle'], f1)
            t2, y2 = almanac.find_discrete(satOrbit['rise'],
                                           satOrbit['settle'], f2)

            self.sortFlipEvents(satOrbit, t0, t1, t2)
        return True

    def sendSatelliteData(self, alt=[], az=[]):
        """
        :param alt:
        :param az:
        :return:
        """
        if not self.satellite:
            return False

        winObj = self.app.uiWindows['showSatelliteW']
        if not winObj['classObj']:
            return False

        winObj['classObj'].signals.show.emit(self.satellite,
                                             self.satOrbits,
                                             alt,
                                             az)
        return True

    def clearTrackingParameters(self):
        """
        :return:
        """
        self.ui.satTrajectoryStart.setText('-')
        self.ui.satTrajectoryEnd.setText('-')
        self.ui.satTrajectoryFlip.setText('-')
        self.ui.trajectoryProgress.setValue(0)
        self.ui.stopSatelliteTracking.setEnabled(False)
        self.ui.startSatelliteTracking.setEnabled(False)
        self.ui.startSatelliteTracking.setText('Start satellite tracking')
        self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', 'false')
        return True

    def updatePasses(self):
        """
        :return:
        """
        actMeridianLimit = self.app.mount.setting.meridianLimitTrack
        if actMeridianLimit is None:
            return False

        if actMeridianLimit != self.lastMeridianLimit:
            self.showSatPasses()
            self.lastMeridianLimit = actMeridianLimit
        return True

    def showSatPasses(self):
        """
        :return: True for test purpose
        """
        if not self.satellite:
            return False
        self.clearTrackingParameters()
        obsSite = self.app.mount.obsSite
        times, events = self.calcPassEvents(obsSite)

        timeNow = obsSite.timeJD
        self.extractOrbits(timeNow, times, events)
        self.addMeridianTransit(obsSite.location)

        for i in range(0, 3):
            self.passUI[i]['rise'].setText('-')
            self.passUI[i]['culminate'].setText('-')
            self.passUI[i]['settle'].setText('-')
            self.passUI[i]['flip'].setText('-')
            self.passUI[i]['date'].setText('-')

        fString = "%H:%M:%S"
        fStringDate = "%d %b"
        for i, satOrbit in enumerate(self.satOrbits):
            riseT = satOrbit.get('rise', None)
            if riseT is not None:
                riseStr = riseT.utc_strftime(fString)
                dateStr = riseT.utc_strftime(fStringDate)
            else:
                riseStr = 'unknown'
                dateStr = '---'
            culminateT = satOrbit.get('culminate', None)
            if culminateT is not None:
                culminateStr = culminateT.utc_strftime(fString)
            else:
                culminateStr = 'unknown'
            settleT = satOrbit.get('settle', None)
            if settleT is not None:
                settleStr = settleT.utc_strftime(fString)
            else:
                settleStr = 'unknown'
            flipT = satOrbit.get('flip', None)
            if flipT is not None:
                flipStr = flipT.utc_strftime(fString)
            else:
                flipStr = 'no flip'

            self.passUI[i]['rise'].setText(riseStr)
            self.passUI[i]['culminate'].setText(culminateStr)
            self.passUI[i]['settle'].setText(settleStr)
            self.passUI[i]['flip'].setText(flipStr)
            self.passUI[i]['date'].setText(dateStr)

        self.progTrajectoryToMount()
        return True

    def extractSatelliteData(self, satName=''):
        """
        :param satName: additional parameter for calling this method
        :return: success
        """
        if not self.satTableBaseValid:
            return False
        satTab = self.ui.listSatelliteNames
        if satName not in self.satellites:
            return False

        self.positionCursorInSatTable(satTab, satName)
        self.satellite = self.satellites[satName]
        self.ui.satelliteName.setText(self.satellite.name)
        epochText = self.satellite.epoch.utc_strftime('%Y-%m-%d, %H:%M')
        self.ui.satelliteEpoch.setText(epochText)

        now = self.app.mount.obsSite.ts.now()
        days = now - self.satellite.epoch
        self.ui.satelliteDataAge.setText(f'{days:2.2f}')
        self.app.message.emit(f'Actually using satellite data for: [{satName}]', 0)

        if days > 10:
            self.changeStyleDynamic(self.ui.satelliteDataAge, 'color', 'red')
        elif 3 < days < 10:
            self.changeStyleDynamic(self.ui.satelliteDataAge, 'color', 'yellow')
        else:
            self.changeStyleDynamic(self.ui.satelliteDataAge, 'color', '')

        self.ui.satelliteNumber.setText(f'{self.satellite.model.satnum:5d}')
        return True

    def programDataToMount(self, satName=''):
        """
        :return: success
        """
        if not satName:
            return False
        if satName not in self.satellites:
            return False

        satellite = self.app.mount.satellite
        self.app.message.emit(f'Programming [{satName}] to mount', 0)
        line1, line2 = export_tle(self.satellites[satName].model)
        suc = satellite.setTLE(line0=satName,
                               line1=line1,
                               line2=line2)
        if not suc:
            self.app.message.emit('Error program TLE', 2)
            return False
        self.app.mount.getTLE()
        return True

    def getSatelliteDataFromDatabase(self, tleParams=None):
        """
        :param tleParams:
        :return: True for test purpose
        """
        if tleParams is None:
            return False

        self.extractSatelliteData(satName=tleParams.name)
        self.showSatPasses()
        return True

    def updateOrbit(self):
        """
        :return: success
        """
        if not self.satSourceValid:
            return False

        if self.satellite is None:
            self.ui.startSatelliteTracking.setEnabled(False)
            self.ui.stopSatelliteTracking.setEnabled(False)
            self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', 'false')
            return False

        now = self.app.mount.obsSite.ts.now()
        winObj = self.app.uiWindows['showSatelliteW']
        if not winObj.get('classObj'):
            return False

        location = self.app.mount.obsSite.location
        winObj['classObj'].signals.update.emit(now, location)
        return True

    def calcTrajectoryData(self, start, end):
        """
        :param start:
        :param end:
        :return:
        """
        duration = min(end - start, 900 / 86400)
        if duration < 1 / 86400:
            return [], []

        m = self.app.mount
        temp = m.setting.refractionTemp
        press = m.setting.refractionPress
        timeSeries = start + np.arange(0, duration, 1 / 86400)
        timeVec = m.obsSite.ts.tt_jd(timeSeries)

        earth = self.app.ephemeris['earth']
        ssb_sat = earth + self.satellite
        ssb_loc = earth + m.obsSite.location
        topocentric = ssb_loc.at(timeVec).observe(ssb_sat).apparent()
        alt, az, _ = topocentric.altaz(pressure_mbar=press, temperature_C=temp)
        return alt.degrees, az.degrees

    def filterHorizon(self, start, end, alt, az):
        """
        Filter horizon runs from starts on both sides of the track and tries to
        determine, when a track is hidden behind horizon line. As a satellite
        track has to be in one piece, the resulting vectors might have a shorter
        length and a different start and end time.

        :param start:
        :param end:
        :param alt:
        :param az:
        :return:
        """
        useHorizon = self.ui.avoidHorizon.isChecked()
        if not useHorizon:
            return start, end, alt, az

        timeDelayStart = 0
        for altitude, azimuth in list(zip(alt, az)):
            if self.app.data.isAboveHorizon((altitude, azimuth)):
                break
            timeDelayStart += 1
            alt = np.delete(alt, 0)
            az = np.delete(az, 0)

        timeDelayEnd = 0
        for altitude, azimuth in reversed(list(zip(alt, az))):
            if self.app.data.isAboveHorizon((altitude, azimuth)):
                break
            timeDelayEnd += 1
            alt = np.delete(alt, -1)
            az = np.delete(az, -1)

        start += timeDelayStart / 86400
        end -= timeDelayEnd / 86400

        return start, end, alt, az

    def selectStartEnd(self):
        """
        :return:
        """
        if not self.satOrbits:
            return 0, 0
        if 'rise' not in self.satOrbits[0]:
            return 0, 0
        if 'settle' not in self.satOrbits[0]:
            return 0, 0

        isBefore = self.ui.satBeforeFlip.isChecked()
        isAfter = self.ui.satAfterFlip.isChecked()
        start = self.satOrbits[0]['rise'].tt
        end = self.satOrbits[0]['settle'].tt

        if isBefore and isAfter:
            pass
        elif isBefore and not isAfter:
            if 'flipLate' in self.satOrbits[0]:
                end = self.satOrbits[0]['flipLate'].tt
        elif not isBefore and isAfter:
            if 'flipEarly' in self.satOrbits[0]:
                start = self.satOrbits[0]['flipEarly'].tt
        else:
            return 0, 0

        return start, end

    def progTrajectoryToMount(self):
        """
        :return:
        """
        start, end = self.selectStartEnd()
        if not start or not end:
            return False

        useInternal = self.ui.useInternalSatCalc.isChecked()
        if useInternal:
            alt, az = self.calcTrajectoryData(start, end)
            start, end, alt, az = self.filterHorizon(start, end, alt, az)
        else:
            alt = []
            az = []
        self.sendSatelliteData(alt=alt, az=az)
        if self.app.deviceStat['mount'] and not useInternal:
            self.app.mount.calcTLE(start)

        return True

    def startProg(self):
        """
        :return:
        """
        self.clearTrackingParameters()
        isSim = self.ui.trackingSim.isChecked()
        t = ('for simulation' if isSim else '')
        self.app.message.emit('Programming satellite track data ' + t, 0)
        start, end = self.selectStartEnd()
        if not start or not end:
            return False
        alt, az = self.calcTrajectoryData(start, end)
        start, end, alt, az = self.filterHorizon(start, end, alt, az)
        self.changeStyleDynamic(self.ui.progTrajectory, 'running', True)
        self.app.mount.progTrajectory(start, alt=alt, az=az, sim=isSim)
        return True

    def trajectoryProgress(self, value):
        """
        :param value:
        :return:
        """
        self.ui.trajectoryProgress.setValue(value)
        if value == 100:
            self.changeStyleDynamic(self.ui.progTrajectory, 'running', False)
        return True

    def updateSatelliteTrackGui(self, params=None):
        """
        :return: success for test purpose
        """
        if params is None:
            return False
        self.ui.trajectoryProgress.setValue(0)

        if params.jdStart is not None and self.satOrbits:
            t = params.jdStart.utc_strftime('%d %b  %H:%M:%S')
            self.ui.satTrajectoryStart.setText(t)
        else:
            self.ui.satTrajectoryStart.setText('No transit')

        if params.jdEnd is not None and self.satOrbits:
            t = params.jdEnd.utc_strftime('%d %b  %H:%M:%S')
            self.ui.satTrajectoryEnd.setText(t)
        else:
            self.ui.satTrajectoryEnd.setText('No transit')

        if params.flip and self.satOrbits:
            self.ui.satTrajectoryFlip.setText('YES')
        else:
            self.ui.satTrajectoryFlip.setText('NO')

        if params.message is not None:
            self.app.message.emit(params.message, 0)

        if params.jdStart is not None and self.satOrbits:
            self.ui.stopSatelliteTracking.setEnabled(True)
            self.ui.startSatelliteTracking.setEnabled(True)

        else:
            self.ui.stopSatelliteTracking.setEnabled(False)
            self.ui.startSatelliteTracking.setEnabled(False)

        return True

    def startTrack(self):
        """
        :return: success
        """
        if not self.app.deviceStat['mount']:
            self.app.message.emit('Mount is not online', 2)
            return False

        if self.app.mount.obsSite.status == 5:
            suc = self.app.mount.obsSite.unpark()
            if suc:
                self.app.message.emit('Mount unparked', 0)
            else:
                self.app.message.emit('Cannot unpark mount', 2)

        self.app.dome.avoidFirstOvershoot()
        suc, message = self.app.mount.satellite.slewTLE()
        if not suc:
            self.app.message.emit(message, 2)
            return False

        self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', True)
        self.app.message.emit(message, 0)
        suc = self.app.mount.satellite.clearTrackingOffsets()
        if suc:
            self.app.message.emit('Cleared tracking offsets', 0)

        return True

    def stopTrack(self):
        """
        :return: success
        """
        if not self.app.deviceStat['mount']:
            self.app.message.emit('Mount is not online', 2)
            return False

        suc = self.app.mount.obsSite.startTracking()
        if not suc:
            self.app.message.emit('Cannot stop tracking', 2)
            return False

        self.ui.startSatelliteTracking.setText('Start satellite tracking')
        self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', 'false')
        self.app.message.emit('Stopped tracking', 0)
        return suc

    def toggleTrackingOffset(self, obs):
        """
        :param obs:
        :return:
        """
        if not self.app.mount.firmware.checkNewer(21699):
            return False

        if obs.status == 10:
            self.ui.satOffGroupTime.setEnabled(True)
            self.ui.satOffGroupRa.setEnabled(True)
            self.ui.satOffGroupDec.setEnabled(True)
        else:
            self.ui.satOffGroupTime.setEnabled(False)
            self.ui.satOffGroupRa.setEnabled(False)
            self.ui.satOffGroupDec.setEnabled(False)
        return True

    def followMount(self, obs):
        """
        :return:
        """
        TLESCK = {
            'V': 'is slewing to transit start',
            'P': 'is waiting for satellite',
            'S': 'is catching satellite',
            'T': 'is tracking satellite',
        }

        status = obs.status
        if status != 10:
            return False
        if not self.ui.domeAutoFollowSat.isChecked():
            return False
        if not self.app.deviceStat['dome']:
            return False

        stat = obs.statusSat
        statText = (TLESCK[stat] if stat in TLESCK else '')
        self.ui.startSatelliteTracking.setText(statText)

        azimuth = obs.Az.degrees
        altitude = obs.Alt.degrees
        self.app.dome.slewDome(altitude=altitude, azimuth=azimuth, follow=True)
        return True

    def setTrackingOffsets(self):
        """
        :return:
        """
        valT = self.ui.satOffTime.value()
        valR = self.ui.satOffRa.value()
        valD = self.ui.satOffDec.value()
        suc = self.app.mount.satellite.setTrackingOffsets(Time=valT,
                                                          RA=valR,
                                                          DECcorr=valD)
        if not suc:
            self.app.message.emit('Cannot change offset', 2)
        return True

<filename>mw4/gui/mainWmixin/tabSatTrack.py
############################################################
# -*- coding: utf-8 -*-
#
#       #   #  #   #   #    #
#      ##  ##  #  ##  #    #
#     # # # #  # # # #    #  #
#    #  ##  #  ##  ##    ######
#   #   #   #  #   #       #
#
# Python-based Tool for interaction with the 10micron mounts
# GUI with PyQT5 for python
#
# written in python3, (c) 2019-2021 by mworion
# Licence APL2.0
#
###########################################################
# standard libraries
import logging

# external packages
from PyQt5.QtCore import pyqtSignal, QObject
import numpy as np
from sgp4.exporter import export_tle
from skyfield import almanac

# local import
from mountcontrol.convert import valueToInt


class SatControlWidget(QObject):
    """
    """

    log = logging.getLogger(__name__)
    valueChanged = pyqtSignal(object)

    def __init__(self, ui, minVal, maxVal):
        super().__init__()
        self.ui = ui
        self.minVal = minVal
        self.maxVal = maxVal
        self.ui[0].setMinimum(minVal)
        self.ui[0].setMaximum(maxVal)
        self.ui[0].sliderReleased.connect(self.updateSatOff)
        self.ui[1].editingFinished.connect(self.updateSatOffVal)
        self.ui[2].clicked.connect(self.updateSatOffButton)
        self.ui[3].clicked.connect(self.updateSatOffButton)
        self.ui[4].clicked.connect(self.updateSatOffButton)
        self.ui[5].clicked.connect(self.updateSatOffButton)
        self.ui[6].clicked.connect(self.updateSatOffButton)
        self.ui[7].clicked.connect(self.updateSatOffButton)

    def updateSatOff(self):
        """
        :return:
        """
        val = self.ui[0].value()
        self.ui[1].setText(str(val))
        self.valueChanged.emit(val)
        return True

    def updateSatOffButton(self):
        """
        :return:
        """
        senders = {self.ui[2]: 100,
                   self.ui[3]: 10,
                   self.ui[4]: 1,
                   self.ui[5]: -100,
                   self.ui[6]: -10,
                   self.ui[7]: -1,
                   }
        delta = senders[self.sender()]
        val = self.ui[0].value()
        val += delta
        val = min(self.maxVal, val)
        val = max(self.minVal, val)
        self.ui[0].setValue(val)
        self.ui[1].setText(str(val))
        self.valueChanged.emit(val)
        return True

    def updateSatOffVal(self):
        """
        :return:
        """
        val = valueToInt(self.ui[1].text())
        if val is None:
            val = self.ui[0].value()
            self.ui[1].setText(str(val))
        else:
            val = min(self.maxVal, val)
            val = max(self.minVal, val)
            self.ui[0].setValue(val)
            self.valueChanged.emit(val)
        return True


class SatTrack(object):
    """
    """

    def __init__(self):
        self.satOrbits = None
        self.satellitesRawTLE = {}
        self.nextSatPass = [None, None, None]
        self.lastMeridianLimit = None

        self.passUI = {
            0: {'rise': self.ui.satRise_1,
                'culminate': self.ui.satCulminate_1,
                'settle': self.ui.satSettle_1,
                'flip': self.ui.satFlip_1,
                'date': self.ui.satDate_1},
            1: {'rise': self.ui.satRise_2,
                'culminate': self.ui.satCulminate_2,
                'settle': self.ui.satSettle_2,
                'flip': self.ui.satFlip_2,
                'date': self.ui.satDate_2},
            2: {'rise': self.ui.satRise_3,
                'culminate': self.ui.satCulminate_3,
                'settle': self.ui.satSettle_3,
                'flip': self.ui.satFlip_3,
                'date': self.ui.satDate_3}
        }
        self.satOrbits = dict()

        msig = self.app.mount.signals
        msig.calcTLEdone.connect(self.updateSatelliteTrackGui)
        msig.calcTrajectoryDone.connect(self.updateSatelliteTrackGui)
        msig.getTLEdone.connect(self.getSatelliteDataFromDatabase)
        msig.trajectoryProgress.connect(self.trajectoryProgress)
        msig.pointDone.connect(self.followMount)
        msig.settingDone.connect(self.updatePasses)
        msig.pointDone.connect(self.toggleTrackingOffset)

        self.ui.startSatelliteTracking.clicked.connect(self.startTrack)
        self.ui.stopSatelliteTracking.clicked.connect(self.stopTrack)
        self.app.sendSatelliteData.connect(self.sendSatelliteData)
        self.ui.satAfterFlip.clicked.connect(self.showSatPasses)
        self.ui.satBeforeFlip.clicked.connect(self.showSatPasses)
        self.ui.avoidHorizon.clicked.connect(self.showSatPasses)
        self.ui.useInternalSatCalc.clicked.connect(self.showSatPasses)
        self.ui.useInternalSatCalc.clicked.connect(self.enableGuiFunctions)
        self.ui.progTrajectory.clicked.connect(self.startProg)

        self.app.update1s.connect(self.updateOrbit)
        self.satOffTime = SatControlWidget([self.ui.satOffTime,
                                            self.ui.satOffTimeVal,
                                            self.ui.satOffTimeP100,
                                            self.ui.satOffTimeP10,
                                            self.ui.satOffTimeP1,
                                            self.ui.satOffTimeM100,
                                            self.ui.satOffTimeM10,
                                            self.ui.satOffTimeM1,
                                            ], -1000, 1000)
        self.satOffRa = SatControlWidget([self.ui.satOffRa,
                                          self.ui.satOffRaVal,
                                          self.ui.satOffRaP100,
                                          self.ui.satOffRaP10,
                                          self.ui.satOffRaP1,
                                          self.ui.satOffRaM100,
                                          self.ui.satOffRaM10,
                                          self.ui.satOffRaM1,
                                          ], -1800, 1800)
        self.satOffDec = SatControlWidget([self.ui.satOffDec,
                                           self.ui.satOffDecVal,
                                           self.ui.satOffDecP100,
                                           self.ui.satOffDecP10,
                                           self.ui.satOffDecP1,
                                           self.ui.satOffDecM100,
                                           self.ui.satOffDecM10,
                                           self.ui.satOffDecM1,
                                           ], -1800, 1800)

        self.satOffTime.valueChanged.connect(self.setTrackingOffsets)
        self.satOffRa.valueChanged.connect(self.setTrackingOffsets)
        self.satOffDec.valueChanged.connect(self.setTrackingOffsets)

    def initConfig(self):
        """
        :return: True for test purpose
        """
        config = self.app.config['mainW']

        self.ui.domeAutoFollowSat.setChecked(config.get('domeAutoFollowSat',
                                                        False))
        self.ui.useInternalSatCalc.setChecked(config.get('useInternalSatCalc',
                                                         False))
        self.ui.satBeforeFlip.setChecked(config.get('satBeforeFlip', True))
        self.ui.satAfterFlip.setChecked(config.get('satAfterFlip', True))
        self.ui.avoidHorizon.setChecked(config.get('avoidHorizon', False))
        self.ui.trackingSim.setChecked(config.get('trackingSim', False))

        if not self.app.automation:
            self.installPath = self.app.mwGlob['dataDir']
        elif self.app.automation.installPath:
            self.installPath = self.app.automation.installPath
        else:
            self.installPath = self.app.mwGlob['dataDir']
        return True

    def storeConfig(self):
        """
        :return:
        """
        config = self.app.config['mainW']
        config['domeAutoFollowSat'] = self.ui.domeAutoFollowSat.isChecked()
        config['useInternalSatCalc'] = self.ui.useInternalSatCalc.isChecked()
        config['satBeforeFlip'] = self.ui.satBeforeFlip.isChecked()
        config['satAfterFlip'] = self.ui.satAfterFlip.isChecked()
        config['avoidHorizon'] = self.ui.avoidHorizon.isChecked()
        config['trackingSim'] = self.ui.trackingSim.isChecked()
        return True

    def calcPassEvents(self, obsSite):
        """
        :return:
        """
        minAlt = self.app.mount.setting.horizonLimitLow
        if minAlt is None:
            minAlt = 5
        if minAlt < 5:
            minAlt = 5

        loc = obsSite.location
        orbitCycleTime = np.pi / self.satellite.model.no_kozai / 12 / 60

        t0 = obsSite.ts.tt_jd(obsSite.timeJD.tt - orbitCycleTime)
        t1 = obsSite.ts.tt_jd(obsSite.timeJD.tt + 5)
        times, events = self.satellite.find_events(loc, t0, t1,
                                                   altitude_degrees=minAlt)
        return times, events

    def extractOrbits(self, timeNow, times, events):
        """
        :param timeNow:
        :param times:
        :param events:
        :return:
        """
        counter = 0
        self.satOrbits = []

        for ti, event in zip(times, events):
            if event == 0:
                self.satOrbits.append({'rise': ti})

            elif event == 1:
                if counter >= len(self.satOrbits):
                    continue
                self.satOrbits[counter]['culminate'] = ti

            elif event == 2:
                if counter >= len(self.satOrbits):
                    continue
                self.satOrbits[counter]['settle'] = ti

                if ti.tt < timeNow.tt:
                    del self.satOrbits[counter]
                    continue
                counter += 1

            if counter > 2:
                break

        if not self.satOrbits:
            return False
        if 'settle' not in self.satOrbits[-1]:
            del self.satOrbits[counter]
            return False

        return True

    @staticmethod
    def calcSatelliteMeridianTransit(satellite, location, tolerance):
        """
        :param satellite:
        :param location:
        :param tolerance:
        :return:
        """
        difference = satellite - location

        def west_of_meridian_at(t):
            alt, az, _ = difference.at(t).altaz()
            delta = (az.degrees + tolerance + 360) % 360 - 180
            return delta < 0

        west_of_meridian_at.step_days = 0.4
        return west_of_meridian_at

    @staticmethod
    def sortFlipEvents(satOrbit, t0, t1, t2):
        """
        :param satOrbit:
        :param t0:
        :param t1:
        :param t2:
        :return:
        """
        settle = satOrbit['settle']
        rise = satOrbit['rise']
        if t0:
            satOrbit['flip'] = t0[0]
        if t1 and t2:
            if t1[0].tt > t2[0].tt:
                satOrbit['flipEarly'] = t2[0]
                satOrbit['flipLate'] = t1[0]
            else:
                satOrbit['flipEarly'] = t1[0]
                satOrbit['flipLate'] = t2[0]
        if t1 and not t2:
            if abs(rise.tt - t1[0].tt) > abs(settle.tt - t1[0].tt):
                satOrbit['flipLate'] = t1[0]
            else:
                satOrbit['flipEarly'] = t1[0]
        if not t1 and t2:
            if abs(rise.tt - t2[0].tt) > abs(settle.tt - t2[0].tt):
                satOrbit['flipLate'] = t2[0]
            else:
                satOrbit['flipEarly'] = t2[0]

        return True

    def addMeridianTransit(self, location):
        """
        :param location:
        :return:
        """
        limit = self.app.mount.setting.meridianLimitTrack
        if limit is None:
            limit = 0
        limit = limit * 0.95

        f0 = self.calcSatelliteMeridianTransit(self.satellite, location, 0)
        f1 = self.calcSatelliteMeridianTransit(self.satellite, location, limit)
        f2 = self.calcSatelliteMeridianTransit(self.satellite, location, -limit)
        for satOrbit in self.satOrbits:
            t0, y0 = almanac.find_discrete(satOrbit['rise'],
                                           satOrbit['settle'], f0)
            t1, y1 = almanac.find_discrete(satOrbit['rise'],
                                           satOrbit['settle'], f1)
            t2, y2 = almanac.find_discrete(satOrbit['rise'],
                                           satOrbit['settle'], f2)

            self.sortFlipEvents(satOrbit, t0, t1, t2)
        return True

    def sendSatelliteData(self, alt=[], az=[]):
        """
        :param alt:
        :param az:
        :return:
        """
        if not self.satellite:
            return False

        winObj = self.app.uiWindows['showSatelliteW']
        if not winObj['classObj']:
            return False

        winObj['classObj'].signals.show.emit(self.satellite,
                                             self.satOrbits,
                                             alt,
                                             az)
        return True

    def clearTrackingParameters(self):
        """
        :return:
        """
        self.ui.satTrajectoryStart.setText('-')
        self.ui.satTrajectoryEnd.setText('-')
        self.ui.satTrajectoryFlip.setText('-')
        self.ui.trajectoryProgress.setValue(0)
        self.ui.stopSatelliteTracking.setEnabled(False)
        self.ui.startSatelliteTracking.setEnabled(False)
        self.ui.startSatelliteTracking.setText('Start satellite tracking')
        self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', 'false')
        return True

    def updatePasses(self):
        """
        :return:
        """
        actMeridianLimit = self.app.mount.setting.meridianLimitTrack
        if actMeridianLimit is None:
            return False

        if actMeridianLimit != self.lastMeridianLimit:
            self.showSatPasses()
            self.lastMeridianLimit = actMeridianLimit
        return True

    def showSatPasses(self):
        """
        :return: True for test purpose
        """
        if not self.satellite:
            return False
        self.clearTrackingParameters()
        obsSite = self.app.mount.obsSite
        times, events = self.calcPassEvents(obsSite)

        timeNow = obsSite.timeJD
        self.extractOrbits(timeNow, times, events)
        self.addMeridianTransit(obsSite.location)

        for i in range(0, 3):
            self.passUI[i]['rise'].setText('-')
            self.passUI[i]['culminate'].setText('-')
            self.passUI[i]['settle'].setText('-')
            self.passUI[i]['flip'].setText('-')
            self.passUI[i]['date'].setText('-')

        fString = "%H:%M:%S"
        fStringDate = "%d %b"
        for i, satOrbit in enumerate(self.satOrbits):
            riseT = satOrbit.get('rise', None)
            if riseT is not None:
                riseStr = riseT.utc_strftime(fString)
                dateStr = riseT.utc_strftime(fStringDate)
            else:
                riseStr = 'unknown'
                dateStr = '---'
            culminateT = satOrbit.get('culminate', None)
            if culminateT is not None:
                culminateStr = culminateT.utc_strftime(fString)
            else:
                culminateStr = 'unknown'
            settleT = satOrbit.get('settle', None)
            if settleT is not None:
                settleStr = settleT.utc_strftime(fString)
            else:
                settleStr = 'unknown'
            flipT = satOrbit.get('flip', None)
            if flipT is not None:
                flipStr = flipT.utc_strftime(fString)
            else:
                flipStr = 'no flip'

            self.passUI[i]['rise'].setText(riseStr)
            self.passUI[i]['culminate'].setText(culminateStr)
            self.passUI[i]['settle'].setText(settleStr)
            self.passUI[i]['flip'].setText(flipStr)
            self.passUI[i]['date'].setText(dateStr)

        self.progTrajectoryToMount()
        return True

    def extractSatelliteData(self, satName=''):
        """
        :param satName: additional parameter for calling this method
        :return: success
        """
        if not self.satTableBaseValid:
            return False
        satTab = self.ui.listSatelliteNames
        if satName not in self.satellites:
            return False

        self.positionCursorInSatTable(satTab, satName)
        self.satellite = self.satellites[satName]
        self.ui.satelliteName.setText(self.satellite.name)
        epochText = self.satellite.epoch.utc_strftime('%Y-%m-%d, %H:%M')
        self.ui.satelliteEpoch.setText(epochText)

        now = self.app.mount.obsSite.ts.now()
        days = now - self.satellite.epoch
        self.ui.satelliteDataAge.setText(f'{days:2.2f}')
        self.app.message.emit(f'Actually using satellite data for: [{satName}]', 0)

        if days > 10:
            self.changeStyleDynamic(self.ui.satelliteDataAge, 'color', 'red')
        elif 3 < days < 10:
            self.changeStyleDynamic(self.ui.satelliteDataAge, 'color', 'yellow')
        else:
            self.changeStyleDynamic(self.ui.satelliteDataAge, 'color', '')

        self.ui.satelliteNumber.setText(f'{self.satellite.model.satnum:5d}')
        return True

    def programDataToMount(self, satName=''):
        """
        :return: success
        """
        if not satName:
            return False
        if satName not in self.satellites:
            return False

        satellite = self.app.mount.satellite
        self.app.message.emit(f'Programming [{satName}] to mount', 0)
        line1, line2 = export_tle(self.satellites[satName].model)
        suc = satellite.setTLE(line0=satName,
                               line1=line1,
                               line2=line2)
        if not suc:
            self.app.message.emit('Error program TLE', 2)
            return False
        self.app.mount.getTLE()
        return True

    def getSatelliteDataFromDatabase(self, tleParams=None):
        """
        :param tleParams:
        :return: True for test purpose
        """
        if tleParams is None:
            return False

        self.extractSatelliteData(satName=tleParams.name)
        self.showSatPasses()
        return True

    def updateOrbit(self):
        """
        :return: success
        """
        if not self.satSourceValid:
            return False

        if self.satellite is None:
            self.ui.startSatelliteTracking.setEnabled(False)
            self.ui.stopSatelliteTracking.setEnabled(False)
            self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', 'false')
            return False

        now = self.app.mount.obsSite.ts.now()
        winObj = self.app.uiWindows['showSatelliteW']
        if not winObj.get('classObj'):
            return False

        location = self.app.mount.obsSite.location
        winObj['classObj'].signals.update.emit(now, location)
        return True

    def calcTrajectoryData(self, start, end):
        """
        :param start:
        :param end:
        :return:
        """
        duration = min(end - start, 900 / 86400)
        if duration < 1 / 86400:
            return [], []

        m = self.app.mount
        temp = m.setting.refractionTemp
        press = m.setting.refractionPress
        timeSeries = start + np.arange(0, duration, 1 / 86400)
        timeVec = m.obsSite.ts.tt_jd(timeSeries)

        earth = self.app.ephemeris['earth']
        ssb_sat = earth + self.satellite
        ssb_loc = earth + m.obsSite.location
        topocentric = ssb_loc.at(timeVec).observe(ssb_sat).apparent()
        alt, az, _ = topocentric.altaz(pressure_mbar=press, temperature_C=temp)
        return alt.degrees, az.degrees

    def filterHorizon(self, start, end, alt, az):
        """
        Filter horizon runs from starts on both sides of the track and tries to
        determine, when a track is hidden behind horizon line. As a satellite
        track has to be in one piece, the resulting vectors might have a shorter
        length and a different start and end time.

        :param start:
        :param end:
        :param alt:
        :param az:
        :return:
        """
        useHorizon = self.ui.avoidHorizon.isChecked()
        if not useHorizon:
            return start, end, alt, az

        timeDelayStart = 0
        for altitude, azimuth in list(zip(alt, az)):
            if self.app.data.isAboveHorizon((altitude, azimuth)):
                break
            timeDelayStart += 1
            alt = np.delete(alt, 0)
            az = np.delete(az, 0)

        timeDelayEnd = 0
        for altitude, azimuth in reversed(list(zip(alt, az))):
            if self.app.data.isAboveHorizon((altitude, azimuth)):
                break
            timeDelayEnd += 1
            alt = np.delete(alt, -1)
            az = np.delete(az, -1)

        start += timeDelayStart / 86400
        end -= timeDelayEnd / 86400

        return start, end, alt, az

    def selectStartEnd(self):
        """
        :return:
        """
        if not self.satOrbits:
            return 0, 0
        if 'rise' not in self.satOrbits[0]:
            return 0, 0
        if 'settle' not in self.satOrbits[0]:
            return 0, 0

        isBefore = self.ui.satBeforeFlip.isChecked()
        isAfter = self.ui.satAfterFlip.isChecked()
        start = self.satOrbits[0]['rise'].tt
        end = self.satOrbits[0]['settle'].tt

        if isBefore and isAfter:
            pass
        elif isBefore and not isAfter:
            if 'flipLate' in self.satOrbits[0]:
                end = self.satOrbits[0]['flipLate'].tt
        elif not isBefore and isAfter:
            if 'flipEarly' in self.satOrbits[0]:
                start = self.satOrbits[0]['flipEarly'].tt
        else:
            return 0, 0

        return start, end

    def progTrajectoryToMount(self):
        """
        :return:
        """
        start, end = self.selectStartEnd()
        if not start or not end:
            return False

        useInternal = self.ui.useInternalSatCalc.isChecked()
        if useInternal:
            alt, az = self.calcTrajectoryData(start, end)
            start, end, alt, az = self.filterHorizon(start, end, alt, az)
        else:
            alt = []
            az = []
        self.sendSatelliteData(alt=alt, az=az)
        if self.app.deviceStat['mount'] and not useInternal:
            self.app.mount.calcTLE(start)

        return True

    def startProg(self):
        """
        :return:
        """
        self.clearTrackingParameters()
        isSim = self.ui.trackingSim.isChecked()
        t = ('for simulation' if isSim else '')
        self.app.message.emit('Programming satellite track data ' + t, 0)
        start, end = self.selectStartEnd()
        if not start or not end:
            return False
        alt, az = self.calcTrajectoryData(start, end)
        start, end, alt, az = self.filterHorizon(start, end, alt, az)
        self.changeStyleDynamic(self.ui.progTrajectory, 'running', True)
        self.app.mount.progTrajectory(start, alt=alt, az=az, sim=isSim)
        return True

    def trajectoryProgress(self, value):
        """
        :param value:
        :return:
        """
        self.ui.trajectoryProgress.setValue(value)
        if value == 100:
            self.changeStyleDynamic(self.ui.progTrajectory, 'running', False)
        return True

    def updateSatelliteTrackGui(self, params=None):
        """
        :return: success for test purpose
        """
        if params is None:
            return False
        self.ui.trajectoryProgress.setValue(0)

        if params.jdStart is not None and self.satOrbits:
            t = params.jdStart.utc_strftime('%d %b  %H:%M:%S')
            self.ui.satTrajectoryStart.setText(t)
        else:
            self.ui.satTrajectoryStart.setText('No transit')

        if params.jdEnd is not None and self.satOrbits:
            t = params.jdEnd.utc_strftime('%d %b  %H:%M:%S')
            self.ui.satTrajectoryEnd.setText(t)
        else:
            self.ui.satTrajectoryEnd.setText('No transit')

        if params.flip and self.satOrbits:
            self.ui.satTrajectoryFlip.setText('YES')
        else:
            self.ui.satTrajectoryFlip.setText('NO')

        if params.message is not None:
            self.app.message.emit(params.message, 0)

        if params.jdStart is not None and self.satOrbits:
            self.ui.stopSatelliteTracking.setEnabled(True)
            self.ui.startSatelliteTracking.setEnabled(True)

        else:
            self.ui.stopSatelliteTracking.setEnabled(False)
            self.ui.startSatelliteTracking.setEnabled(False)

        return True

    def startTrack(self):
        """
        :return: success
        """
        if not self.app.deviceStat['mount']:
            self.app.message.emit('Mount is not online', 2)
            return False

        if self.app.mount.obsSite.status == 5:
            suc = self.app.mount.obsSite.unpark()
            if suc:
                self.app.message.emit('Mount unparked', 0)
            else:
                self.app.message.emit('Cannot unpark mount', 2)

        self.app.dome.avoidFirstOvershoot()
        suc, message = self.app.mount.satellite.slewTLE()
        if not suc:
            self.app.message.emit(message, 2)
            return False

        self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', True)
        self.app.message.emit(message, 0)
        suc = self.app.mount.satellite.clearTrackingOffsets()
        if suc:
            self.app.message.emit('Cleared tracking offsets', 0)

        return True

    def stopTrack(self):
        """
        :return: success
        """
        if not self.app.deviceStat['mount']:
            self.app.message.emit('Mount is not online', 2)
            return False

        suc = self.app.mount.obsSite.startTracking()
        if not suc:
            self.app.message.emit('Cannot stop tracking', 2)
            return False

        self.ui.startSatelliteTracking.setText('Start satellite tracking')
        self.changeStyleDynamic(self.ui.startSatelliteTracking, 'running', 'false')
        self.app.message.emit('Stopped tracking', 0)
        return suc

    def toggleTrackingOffset(self, obs):
        """
        :param obs:
        :return:
        """
        if not self.app.mount.firmware.checkNewer(21699):
            return False

        if obs.status == 10:
            self.ui.satOffGroupTime.setEnabled(True)
            self.ui.satOffGroupRa.setEnabled(True)
            self.ui.satOffGroupDec.setEnabled(True)
        else:
            self.ui.satOffGroupTime.setEnabled(False)
            self.ui.satOffGroupRa.setEnabled(False)
            self.ui.satOffGroupDec.setEnabled(False)
        return True

    def followMount(self, obs):
        """
        :return:
        """
        TLESCK = {
            'V': 'is slewing to transit start',
            'P': 'is waiting for satellite',
            'S': 'is catching satellite',
            'T': 'is tracking satellite',
        }

        status = obs.status
        if status != 10:
            return False
        if not self.ui.domeAutoFollowSat.isChecked():
            return False
        if not self.app.deviceStat['dome']:
            return False

        stat = obs.statusSat
        statText = (TLESCK[stat] if stat in TLESCK else '')
        self.ui.startSatelliteTracking.setText(statText)

        azimuth = obs.Az.degrees
        altitude = obs.Alt.degrees
        self.app.dome.slewDome(altitude=altitude, azimuth=azimuth, follow=True)
        return True

    def setTrackingOffsets(self):
        """
        :return:
        """
        valT = self.ui.satOffTime.value()
        valR = self.ui.satOffRa.value()
        valD = self.ui.satOffDec.value()
        suc = self.app.mount.satellite.setTrackingOffsets(Time=valT,
                                                          RA=valR,
                                                          DECcorr=valD)
        if not suc:
            self.app.message.emit('Cannot change offset', 2)
        return True

############################################################ # -*- coding: utf-8 -*- # #       #   #  #   #   #    # #      ##  ##  #  ##  #    # #     # # # #  # # # #    #  # #    #  ##  #  ##  ##    ###### #   #   #   #  #   #       # # # Python-based Tool for interaction with the 10micron mounts # GUI with PyQT5 for python # # written in python3, (c) 2019-2021 by mworion # Licence APL2.0 # ########################################################### # standard libraries # external packages # local import :return: :return: :return: :return: True for test purpose :return: :return: :param timeNow:         :param times:         :param events:         :return: :param satellite:         :param location:         :param tolerance:         :return: :param satOrbit:         :param t0:         :param t1:         :param t2:         :return: :param location:         :return: :param alt:         :param az:         :return: :return: :return: :return: True for test purpose :param satName: additional parameter for calling this method         :return: success :return: success :param tleParams:         :return: True for test purpose :return: success :param start:         :param end:         :return: Filter horizon runs from starts on both sides of the track and tries to         determine, when a track is hidden behind horizon line. As a satellite         track has to be in one piece, the resulting vectors might have a shorter         length and a different start and end time.          :param start:         :param end:         :param alt:         :param az:         :return: :return: :return: :return: :param value:         :return: :return: success for test purpose :return: success :return: success :param obs:         :return: :return: :return: