Esempio n. 1
0
    def openservice(self, club_id):
    #----------------------------------------------------------------------
        '''
        initialize service
        recommended that the overriding method save service instance in `self.service`

        must be overridden when ResultsCollect is instantiated

        :param club_id: club.id for club this service is operating on
        '''
        # create location server
        self.locsvr = LocationServer()

        # remember club id we're working on
        self.club_id = club_id

        # debug file for races saved
        # set debugrace to False if not debugging
        debugrace = True
        if debugrace:
            clubslug = Club.query.filter_by(id=club_id).first().shname
            self.racefile = '{}/{}-athlinks-race.csv'.format(app.config['MEMBERSHIP_DIR'], clubslug)
        else:
            self.racefile = None

        if self.racefile:
            self._RACE = open(self.racefile, 'wb')
            self.racefields = 'id,name,date,distmiles,status,runner'.split(',')
            self.RACE = csv.DictWriter(self._RACE, self.racefields)
            self.RACE.writeheader()

        # open service
        key = ApiCredentials.query.filter_by(name=self.servicename).first().key
        self.service = athlinks.Athlinks(debug=True, key=key)
Esempio n. 2
0
def summarize(thistask, club_id, sources, status, summaryfile, detailfile, resultsurl, minage=12, minagegrade=20, minraces=3 , mintrend=2, numyears=3, begindate=None, enddate=None):
#----------------------------------------------------------------------
    '''
    render collected results

    :param thistask: this is required for task thistask.update_state()
    :param club_id: identifies club for which results are to be stored
    :param sources: list of sources / services we're keeping status for
    :param summaryfile: summary file name (.csv)
    :param detailfile: detail file name (.csv)
    :param resultsurl: base url to send results to, for link in summary table
    :param minage: minimum age to keep track of stats
    :param minagegrade: minimum age grade
    :param minraces: minimum races in the same year as enddate
    :param mintrend: minimum races over the full period for trendline
    :param begindate: render races between begindate and enddate, datetime
    :param enddate: render races between begindate and enddate, datetime
    '''
    
    # get club slug and location for later
    club = Club.query.filter_by(id=club_id).first()
    clubslug = club.shname
    locsvr = LocationServer()
    clublocation = locsvr.getlocation(club.location)

    # get maxdistance by service
    services = RaceResultService.query.filter_by(club_id=club_id).join(ApiCredentials).all()
    maxdistance = {}
    for service in services:
        attrs = ServiceAttributes(club_id, service.apicredentials.name)
        # app.logger.debug('service {} attrs {}'.format(service, attrs.__dict__))
        if attrs.maxdistance:
            maxdistance[service.apicredentials.name] = attrs.maxdistance
        else:
            maxdistance[service.apicredentials.name] = None
    maxdistance[productname] = None

    # set up date range. begindate and enddate take precedence, else use numyears from today
    if not (begindate and enddate):
        etoday = time.time()
        today = timeu.epoch2dt(etoday)
        begindate = datetime(today.year-numyears+1,1,1)
        enddate = datetime(today.year,12,31)

    firstyear = begindate.year
    lastyear = enddate.year
    yearrange = range(firstyear,lastyear+1)
    
    # get all the requested result data from the database and save in a data structure indexed by runner
    ## first get the data from the database
    results = RaceResult.query.join(Race).join(Runner).filter(RaceResult.club_id==club_id, 
                Race.date.between(ftime.dt2asc(begindate), ftime.dt2asc(enddate)), Runner.member==True, Runner.active==True).order_by(Runner.lname, Runner.fname).all()

    ## then set up our status and pass to the front end
    for source in sources:
        status[source]['status'] = 'summarizing'
        status[source]['lastname'] = ''
        status[source]['processed'] = 0
        status[source]['total'] = sum([1 for result in results if result.source==source])
    thistask.update_state(state='PROGRESS', meta={'progress':status})
    
    ## prepare to save detail file, for debugging
    detlfields = 'runnername,runnerid,dob,gender,resultid,racename,racedate,series,distmiles,distkm,time,timesecs,agpercent,source,sourceid'.split(',')
    detailfname = detailfile
    _DETL = open(detailfname,'wb')
    DETL = csv.DictWriter(_DETL,detlfields)
    DETL.writeheader()

    ## then fill in data structure to hold AnalyzeAgeGrade objects
    ## use OrderedDict to force aag to be in same order as DETL file, for debugging
    aag = collections.OrderedDict()
    for result in results:
        # skip results which are too far away, if a maxdistance is defined for this source
        if maxdistance[result.source]:
            locationid = result.race.locationid
            if not locationid: continue
            racelocation = Location.query.filter_by(id=locationid).first()
            distance = get_distance(clublocation, racelocation)
            if distance == None or distance > maxdistance[result.source]: continue

        thisname = (result.runner.name.lower(), result.runner.dateofbirth)
        initaagrunner(aag, thisname, result.runner.fname, result.runner.lname, result.runner.gender, ftime.asc2dt(result.runner.dateofbirth), result.runner.id)
        
        # determine location name. any error gets null string
        locationname = ''
        if result.race.locationid:
            location = Location.query.filter_by(id=result.race.locationid).first()
            if location: 
                locationname = location.name

        thisstat = aag[thisname].add_stat(ftime.asc2dt(result.race.date), result.race.distance*METERSPERMILE, result.time, race=result.race.name,
                               loc=locationname, fuzzyage=result.fuzzyage,
                               source=result.source, priority=priority[result.source])

        ### TODO: store result's agpercent, in AgeGrade.crunch() skip agegrade calculation if already present
        DETL.writerow(dict(
                runnername = result.runner.name,
                runnerid = result.runner.id,
                dob = result.runner.dateofbirth,
                gender = result.runner.gender,
                resultid = result.id,
                racename = result.race.name,
                racedate = result.race.date,
                series = result.series.name if result.seriesid else None,
                distmiles = result.race.distance,
                distkm = result.race.distance*(METERSPERMILE/1000),
                timesecs = result.time,
                time = rendertime(result.time,0),
                agpercent = result.agpercent,
                source = result.source,
                sourceid = result.sourceid,
            ))

    ## close detail file
    _DETL.close()

    # initialize summary file
    summfields = ['name', 'lname', 'fname', 'age', 'gender']
    datafields = copy(summfields)
    distcategories = ['overall'] + [TRENDLIMITS[tlimit][0] for tlimit in TRENDLIMITS]
    datacategories = ['overall'] + [TRENDLIMITS[tlimit][1] for tlimit in TRENDLIMITS]
    stattypes = ['1yr agegrade','avg agegrade','trend','numraces','stderr','r-squared','pvalue']
    statdatatypes = ['1yr-agegrade','avg-agegrade','trend','numraces','stderr','r-squared','pvalue']
    for stattype, statdatatype in zip(stattypes, statdatatypes):
        for distcategory, datacategory in zip(distcategories, datacategories):
            summfields.append('{}\n{}'.format(stattype, distcategory))
            datafields.append('{}-{}'.format(statdatatype, datacategory))
        if stattype == 'numraces':
            for year in yearrange:
                summfields.append('{}\n{}'.format(stattype, year))
                datafields.append('{}-{}'.format(statdatatype, lastyear-year))

    # save summary file columns for resultsanalysissummary
    dtcolumns = json.dumps([{ 'data':d, 'name':d, 'label':l } for d,l in zip(datafields, summfields)])
    columnsfilename = summaryfile + '.cols'
    with open(columnsfilename, 'w') as cols:
        cols.write(dtcolumns)

    # set up summary file
    summaryfname = summaryfile
    _SUMM = open(summaryfname,'wb')
    SUMM = csv.DictWriter(_SUMM,summfields)
    SUMM.writeheader()
    
    # loop through each member we've recorded information about
    for thisname in aag:
        fullname, fname, lname, gender, dob, runnerid = aag[thisname].get_runner()
        rendername = fullname.title()
        
        # check stats before deduplicating
        statcount = {}
        stats = aag[thisname].get_stats()
        for source in sources:
            statcount[source] = sum([1 for s in stats if s.source == source])

        # remove duplicate entries
        aag[thisname].deduplicate()   
        
        # crunch the numbers
        aag[thisname].crunch()    # calculate age grade for each result
        stats = aag[thisname].get_stats()
        
        jan1 = ftime.asc2dt('{}-1-1'.format(lastyear))
        runnerage = timeu.age(jan1, dob)
        
        # filter out runners younger than allowed
        if runnerage < minage: continue

        # filter out runners who have not run enough races
        stats = aag[thisname].get_stats()
        if enddate:
            lastyear = enddate.year
        else:
            lastyear = timeu.epoch2dt(time.time()).year
        lastyearstats = [s for s in stats if s.date.year==lastyear]
        if len(lastyearstats) < minraces: continue
        
        # fill in row for summary output
        summout = {}

        # get link for this runner's results chart
        # see http://stackoverflow.com/questions/2506379/add-params-to-given-url-in-python
        url_parts = list(urlparse(resultsurl))
        query = dict(parse_qsl(url_parts[4]))
        query.update({'club': clubslug, 'runnerid': runnerid, 'begindate': ftime.dt2asc(begindate), 'enddate': ftime.dt2asc(enddate)})
        url_parts[4] = urlencode(query)
        resultslink = urlunparse(url_parts)

        summout['name'] = '<a href={} target=_blank>{}</a>'.format(resultslink, rendername)
        summout['fname'] = fname
        summout['lname'] = lname
        summout['age'] = runnerage
        summout['gender'] = gender
        
        # set up to collect averages
        avg = collections.OrderedDict()

        # draw trendlines, write output
        allstats = aag[thisname].get_stats()
        if len(allstats) > 0:
            avg['overall'] = mean([s.ag for s in allstats])
        trend = aag[thisname].get_trendline()

        oneyrstats = [s.ag for s in allstats if s.date.year == lastyear]
        if len(oneyrstats) > 0:
            summout['1yr agegrade\noverall'] = mean(oneyrstats)
        if len(allstats) > 0:
            summout['avg agegrade\noverall'] = avg['overall']
        if len(allstats) >= mintrend and allstats[0].date != allstats[-1].date:
            summout['trend\noverall'] = trend.improvement
            summout['stderr\noverall'] = trend.stderr
            summout['r-squared\noverall'] = trend.r2**2
            summout['pvalue\noverall'] = trend.pvalue
        summout['numraces\noverall'] = len(allstats)
        for year in yearrange:
            summout['numraces\n{}'.format(year)] = len([s for s in allstats if s.date.year==year])
        for tlimit in TRENDLIMITS:
            distcategory,distcolor = TRENDLIMITS[tlimit]
            tstats = [s for s in allstats if s.dist >= tlimit[0] and s.dist < tlimit[1]]
            if len(tstats) > 0:
                avg[distcategory] = mean([s.ag for s in tstats])
                summout['avg agegrade\n{}'.format(distcategory)] = avg[distcategory]
            summout['numraces\n{}'.format(distcategory)] = len(tstats)
            oneyrcategory = [s.ag for s in tstats if s.date.year == lastyear]
            if len(oneyrcategory) > 0:
                summout['1yr agegrade\n{}'.format(distcategory)] = mean(oneyrcategory)
            if len(tstats) >= mintrend and tstats[0].date != tstats[-1].date:
                try:
                    trend = aag[thisname].get_trendline(thesestats=tstats)
                except ZeroDivisionError:
                    app.logger.debug('ZeroDivisionError - processing {}'.format(rendername))
                    trend = None
                # ignore trends which can't be calculated
                if trend:
                    summout['trend\n{}'.format(distcategory)] = trend.improvement
                    summout['stderr\n{}'.format(distcategory)] = trend.stderr
                    summout['r-squared\n{}'.format(distcategory)] = trend.r2
                    summout['pvalue\n{}'.format(distcategory)] = trend.pvalue
        SUMM.writerow(summout)

        # update status
        for source in sources:
            status[source]['processed'] += statcount[source]
            status[source]['lastname'] = rendername
        thistask.update_state(state='PROGRESS', meta={'progress':status})

        
    _SUMM.close()
Esempio n. 3
0
class FrameMain(wx.Frame):
    def __init__(self, title, pool):

        self.pool = pool
        self.lock = threading.Lock()

        self.sdr = None
        self.threadScan = None
        self.threadUpdate = None
        self.threadLocation = None

        self.queueScan = Queue.Queue()

        self.serverLocation = None

        self.isNewScan = True
        self.isScanning = False

        self.stopAtEnd = False
        self.stopScan = False

        self.dlgCal = None
        self.dlgSats = None
        self.dlgLog = None

        self.menuMain = None
        self.menuPopup = None

        self.graph = None
        self.toolbar = None
        self.canvas = None

        self.buttonStart = None
        self.buttonStop = None
        self.controlGain = None
        self.choiceMode = None
        self.choiceDwell = None
        self.choiceNfft = None
        self.spinCtrlStart = None
        self.spinCtrlStop = None
        self.choiceDisplay = None

        self.spectrum = OrderedDict()
        self.scanInfo = ScanInfo()
        self.locations = OrderedDict()
        self.lastLocation = [None] * 4

        self.isSaved = True

        self.settings = Settings()
        self.devicesRtl = get_devices_rtl(self.settings.devicesRtl)
        self.settings.indexRtl = limit(self.settings.indexRtl,
                                       0, len(self.devicesRtl) - 1)
        self.filename = ""
        self.oldCal = 0

        self.remoteControl = None

        self.log = Log()

        self.pageConfig = wx.PageSetupDialogData()
        self.pageConfig.GetPrintData().SetOrientation(wx.LANDSCAPE)
        self.pageConfig.SetMarginTopLeft((20, 20))
        self.pageConfig.SetMarginBottomRight((20, 20))
        self.printConfig = wx.PrintDialogData(self.pageConfig.GetPrintData())
        self.printConfig.EnableSelection(False)
        self.printConfig.EnablePageNumbers(False)

        wx.Frame.__init__(self, None, title=title)

        self.timerGpsRetry = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.__on_gps_retry, self.timerGpsRetry)

        self.Bind(wx.EVT_CLOSE, self.__on_exit)

        self.status = Statusbar(self, self.log)
        self.status.set_info(title)
        self.SetStatusBar(self.status)

        add_colours()
        self.__create_widgets()
        self.__create_menu()
        self.__create_popup_menu()
        self.__set_control_state(True)
        self.Show()

        displaySize = wx.DisplaySize()
        toolbarSize = self.toolbar.GetBestSize()
        self.SetClientSize((toolbarSize[0] + 10, displaySize[1] / 2))
        self.SetMinSize((displaySize[0] / 4, displaySize[1] / 4))

        self.Connect(-1, -1, EVENT_THREAD, self.__on_event)

        self.SetDropTarget(DropTarget(self))

        self.SetIcon(load_icon('rtlsdr_scan'))

        self.steps = 0
        self.stepsTotal = 0

        self.__start_gps()
        self.__start_location_server()

    def __create_widgets(self):
        self.remoteControl = RemoteControl()

        self.graph = PanelGraph(self, self,
                                self.settings, self.status,
                                self.remoteControl)
        self.toolbar = wx.Panel(self)

        self.buttonStart = MultiButton(self.toolbar,
                                       ['Start', 'Continue'],
                                       ['Start new scan', 'Continue scanning'])
        self.buttonStart.SetSelected(self.settings.startOption)
        self.buttonStop = MultiButton(self.toolbar,
                                      ['Stop', 'Stop at end'],
                                      ['Stop scan', 'Stop scan at end'])
        self.buttonStop.SetSelected(self.settings.stopOption)
        self.Bind(wx.EVT_BUTTON, self.__on_start, self.buttonStart)
        self.Bind(wx.EVT_BUTTON, self.__on_stop, self.buttonStop)

        textRange = wx.StaticText(self.toolbar, label="Range (MHz)",
                                  style=wx.ALIGN_CENTER)
        textStart = wx.StaticText(self.toolbar, label="Start")
        textStop = wx.StaticText(self.toolbar, label="Stop")

        self.spinCtrlStart = wx.SpinCtrl(self.toolbar)
        self.spinCtrlStop = wx.SpinCtrl(self.toolbar)
        self.spinCtrlStart.SetToolTipString('Start frequency')
        self.spinCtrlStop.SetToolTipString('Stop frequency')
        self.spinCtrlStart.SetRange(F_MIN, F_MAX - 1)
        self.spinCtrlStop.SetRange(F_MIN + 1, F_MAX)
        self.Bind(wx.EVT_SPINCTRL, self.__on_spin, self.spinCtrlStart)
        self.Bind(wx.EVT_SPINCTRL, self.__on_spin, self.spinCtrlStop)

        textGain = wx.StaticText(self.toolbar, label="Gain (dB)")
        self.controlGain = wx.Choice(self.toolbar, choices=[''])

        textMode = wx.StaticText(self.toolbar, label="Mode")
        self.choiceMode = wx.Choice(self.toolbar, choices=MODE[::2])
        self.choiceMode.SetToolTipString('Scanning mode')

        textDwell = wx.StaticText(self.toolbar, label="Dwell")
        self.choiceDwell = wx.Choice(self.toolbar, choices=DWELL[::2])
        self.choiceDwell.SetToolTipString('Scan time per step')

        textNfft = wx.StaticText(self.toolbar, label="FFT size")
        self.choiceNfft = wx.Choice(self.toolbar, choices=map(str, NFFT))
        self.choiceNfft.SetToolTipString('Higher values for greater'
                                         'precision')

        textDisplay = wx.StaticText(self.toolbar, label="Display")
        self.choiceDisplay = wx.Choice(self.toolbar, choices=DISPLAY[::2])
        self.Bind(wx.EVT_CHOICE, self.__on_choice, self.choiceDisplay)
        self.choiceDisplay.SetToolTipString('Spectrogram available in'
                                            'continuous mode')

        grid = wx.GridBagSizer(5, 5)
        grid.Add(self.buttonStart, pos=(0, 0), span=(3, 1),
                 flag=wx.ALIGN_CENTER)
        grid.Add(self.buttonStop, pos=(0, 1), span=(3, 1),
                 flag=wx.ALIGN_CENTER)
        grid.Add((20, 1), pos=(0, 2))
        grid.Add(textRange, pos=(0, 3), span=(1, 4), flag=wx.ALIGN_CENTER)
        grid.Add(textStart, pos=(1, 3), flag=wx.ALIGN_CENTER)
        grid.Add(self.spinCtrlStart, pos=(1, 4))
        grid.Add(textStop, pos=(1, 5), flag=wx.ALIGN_CENTER)
        grid.Add(self.spinCtrlStop, pos=(1, 6))
        grid.Add(textGain, pos=(0, 7), flag=wx.ALIGN_CENTER)
        grid.Add(self.controlGain, pos=(1, 7), flag=wx.ALIGN_CENTER)
        grid.Add((20, 1), pos=(0, 8))
        grid.Add(textMode, pos=(0, 9), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceMode, pos=(1, 9), flag=wx.ALIGN_CENTER)
        grid.Add(textDwell, pos=(0, 10), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceDwell, pos=(1, 10), flag=wx.ALIGN_CENTER)
        grid.Add(textNfft, pos=(0, 11), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceNfft, pos=(1, 11), flag=wx.ALIGN_CENTER)
        grid.Add((20, 1), pos=(0, 12))
        grid.Add(textDisplay, pos=(0, 13), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceDisplay, pos=(1, 13), flag=wx.ALIGN_CENTER)

        self.__set_controls()
        self.__set_gain_control()

        self.toolbar.SetSizer(grid)
        self.toolbar.Layout()

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.graph, 1, wx.EXPAND)
        sizer.Add(self.toolbar, 0, wx.EXPAND)
        self.SetSizer(sizer)
        self.Layout()

    def __create_menu(self):
        self.menuMain = MenuMain(self, self.settings)

        self.Bind(wx.EVT_MENU, self.__on_new, self.menuMain.new)
        self.Bind(wx.EVT_MENU, self.__on_open, self.menuMain.open)
        self.Bind(wx.EVT_MENU, self.__on_merge, self.menuMain.merge)
        self.Bind(wx.EVT_MENU_RANGE, self.__on_file_history,
                  id=wx.ID_FILE1, id2=wx.ID_FILE9)
        self.Bind(wx.EVT_MENU, self.__on_save, self.menuMain.save)
        self.Bind(wx.EVT_MENU, self.__on_export_scan, self.menuMain.exportScan)
        self.Bind(wx.EVT_MENU, self.__on_export_image, self.menuMain.exportImage)
        self.Bind(wx.EVT_MENU, self.__on_export_image_seq, self.menuMain.exportSeq)
        self.Bind(wx.EVT_MENU, self.__on_export_geo, self.menuMain.exportGeo)
        self.Bind(wx.EVT_MENU, self.__on_export_track, self.menuMain.exportTrack)
        self.Bind(wx.EVT_MENU, self.__on_page, self.menuMain.page)
        self.Bind(wx.EVT_MENU, self.__on_preview, self.menuMain.preview)
        self.Bind(wx.EVT_MENU, self.__on_print, self.menuMain.printer)
        self.Bind(wx.EVT_MENU, self.__on_properties, self.menuMain.properties)
        self.Bind(wx.EVT_MENU, self.__on_exit, self.menuMain.close)
        self.Bind(wx.EVT_MENU, self.__on_pref, self.menuMain.pref)
        self.Bind(wx.EVT_MENU, self.__on_adv_pref, self.menuMain.advPref)
        self.Bind(wx.EVT_MENU, self.__on_formatting, self.menuMain.formatting)
        self.Bind(wx.EVT_MENU, self.__on_devices_rtl, self.menuMain.devicesRtl)
        self.Bind(wx.EVT_MENU, self.__on_devices_gps, self.menuMain.devicesGps)
        self.Bind(wx.EVT_MENU, self.__on_reset, self.menuMain.reset)
        self.Bind(wx.EVT_MENU, self.__on_clear_select, self.menuMain.clearSelect)
        self.Bind(wx.EVT_MENU, self.__on_show_measure, self.menuMain.showMeasure)
        self.Bind(wx.EVT_MENU, self.__on_start, self.menuMain.start)
        self.Bind(wx.EVT_MENU, self.__on_continue, self.menuMain.cont)
        self.Bind(wx.EVT_MENU, self.__on_stop, self.menuMain.stop)
        self.Bind(wx.EVT_MENU, self.__on_stop_end, self.menuMain.stopEnd)
        self.Bind(wx.EVT_MENU, self.__on_compare, self.menuMain.compare)
        self.Bind(wx.EVT_MENU, self.__on_smooth, self.menuMain.smooth)
        self.Bind(wx.EVT_MENU, self.__on_cal, self.menuMain.cal)
        self.Bind(wx.EVT_MENU, self.__on_gearth, self.menuMain.gearth)
        self.Bind(wx.EVT_MENU, self.__on_gmaps, self.menuMain.gmaps)
        self.Bind(wx.EVT_MENU, self.__on_sats, self.menuMain.sats)
        self.Bind(wx.EVT_MENU, self.__on_loc_clear, self.menuMain.locClear)
        self.Bind(wx.EVT_MENU, self.__on_log, self.menuMain.log)
        self.Bind(wx.EVT_MENU, self.__on_help, self.menuMain.helpLink)
        self.Bind(wx.EVT_MENU, self.__on_update, self.menuMain.update)
        self.Bind(wx.EVT_MENU, self.__on_sys_info, self.menuMain.sys)
        self.Bind(wx.EVT_MENU, self.__on_about, self.menuMain.about)

        idF1 = wx.wx.NewId()
        self.Bind(wx.EVT_MENU, self.__on_help, id=idF1)
        accelTable = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_F1, idF1)])
        self.SetAcceleratorTable(accelTable)

        self.Bind(wx.EVT_MENU_HIGHLIGHT, self.__on_menu_highlight)

        self.SetMenuBar(self.menuMain.menuBar)

    def __create_popup_menu(self):
        self.menuPopup = PopMenuMain(self.settings)

        self.Bind(wx.EVT_MENU, self.__on_start, self.menuPopup.start)
        self.Bind(wx.EVT_MENU, self.__on_continue, self.menuPopup.cont)
        self.Bind(wx.EVT_MENU, self.__on_stop, self.menuPopup.stop)
        self.Bind(wx.EVT_MENU, self.__on_stop_end, self.menuPopup.stopEnd)
        self.Bind(wx.EVT_MENU, self.__on_range_lim, self.menuPopup.rangeLim)
        self.Bind(wx.EVT_MENU, self.__on_points_lim, self.menuPopup.pointsLim)
        self.Bind(wx.EVT_MENU, self.__on_clear_select, self.menuPopup.clearSelect)
        self.Bind(wx.EVT_MENU, self.__on_show_measure, self.menuPopup.showMeasure)

        self.Bind(wx.EVT_CONTEXT_MENU, self.__on_popup_menu)

    def __on_menu_highlight(self, event):
        item = self.GetMenuBar().FindItemById(event.GetId())
        if item is not None:
            help = item.GetHelp()
        else:
            help = ''

        self.status.set_general(help, level=None)

    def __on_popup_menu(self, event):
        if not isinstance(event.GetEventObject(), NavigationToolbar):
            pos = event.GetPosition()
            pos = self.ScreenToClient(pos)
            self.PopupMenu(self.menuPopup.menu, pos)

    def __on_new(self, _event):
        if self.__save_warn(Warn.NEW):
            return True
        self.spectrum.clear()
        self.locations.clear()
        self.__saved(True)
        self.__set_plot(self.spectrum, False)
        self.graph.clear_selection()
        self.__set_control_state(True)
        return False

    def __on_open(self, _event):
        if self.__save_warn(Warn.OPEN):
            return

        dlg = wx.FileDialog(self, "Open a scan", self.settings.dirScans,
                            self.filename,
                            File.get_type_filters(File.Types.SAVE),
                            wx.OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.open(dlg.GetDirectory(), dlg.GetFilename())
        dlg.Destroy()

    def __on_merge(self, _event):
        if self.__save_warn(Warn.MERGE):
            return

        dlg = wx.FileDialog(self, "Merge a scan", self.settings.dirScans,
                            self.filename,
                            File.get_type_filters(File.Types.SAVE),
                            wx.OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.__merge(dlg.GetDirectory(), dlg.GetFilename())
        dlg.Destroy()

    def __on_file_history(self, event):
        selection = event.GetId() - wx.ID_FILE1
        path = self.settings.fileHistory.GetHistoryFile(selection)
        self.settings.fileHistory.AddFileToHistory(path)
        dirname, filename = os.path.split(path)
        self.open(dirname, filename)

    def __on_save(self, _event):
        dlg = wx.FileDialog(self, "Save a scan", self.settings.dirScans,
                            self.filename,
                            File.get_type_filters(File.Types.SAVE),
                            wx.SAVE | wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            self.status.set_general("Saving...")
            fileName = dlg.GetFilename()
            dirName = dlg.GetDirectory()
            self.filename = os.path.splitext(fileName)[0]
            self.settings.dirScans = dirName
            fileName = extension_add(fileName, dlg.GetFilterIndex(),
                                     File.Types.SAVE)
            fullName = os.path.join(dirName, fileName)
            save_plot(fullName, self.scanInfo, self.spectrum, self.locations)
            self.__saved(True)
            self.status.set_general("Finished")
            self.settings.fileHistory.AddFileToHistory(fullName)
        dlg.Destroy()

    def __on_export_scan(self, _event):
        dlg = wx.FileDialog(self, "Export a scan", self.settings.dirExport,
                            self.filename, File.get_type_filters(),
                            wx.SAVE | wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            self.status.set_general("Exporting...")
            fileName = dlg.GetFilename()
            dirName = dlg.GetDirectory()
            self.settings.dirExport = dirName
            fileName = extension_add(fileName, dlg.GetFilterIndex(),
                                     File.Types.PLOT)
            fullName = os.path.join(dirName, fileName)
            export_plot(fullName, dlg.GetFilterIndex(), self.spectrum)
            self.status.set_general("Finished")
        dlg.Destroy()

    def __on_export_image(self, _event):
        dlgFile = wx.FileDialog(self, "Export image to file",
                                self.settings.dirExport,
                                self.filename,
                                File.get_type_filters(File.Types.IMAGE),
                                wx.SAVE | wx.OVERWRITE_PROMPT)
        dlgFile.SetFilterIndex(File.ImageType.PNG)
        if dlgFile.ShowModal() == wx.ID_OK:
            dlgImg = DialogImageSize(self, self.settings)
            if dlgImg.ShowModal() != wx.ID_OK:
                dlgFile.Destroy()
                return

            self.status.set_general("Exporting...")
            fileName = dlgFile.GetFilename()
            dirName = dlgFile.GetDirectory()
            self.settings.dirExport = dirName
            fileName = extension_add(fileName, dlgFile.GetFilterIndex(),
                                     File.Types.IMAGE)
            fullName = os.path.join(dirName, fileName)
            exportType = dlgFile.GetFilterIndex()
            export_image(fullName, exportType,
                         self.graph.get_figure(),
                         self.settings)
            self.status.set_general("Finished")
        dlgFile.Destroy()

    def __on_export_image_seq(self, _event):
        dlgSeq = DialogExportSeq(self, self.spectrum, self.settings)
        dlgSeq.ShowModal()
        dlgSeq.Destroy()

    def __on_export_geo(self, _event):
        dlgGeo = DialogExportGeo(self, self.spectrum, self.locations, self.settings)
        if dlgGeo.ShowModal() == wx.ID_OK:
            self.status.set_general("Exporting...")
            extent = dlgGeo.get_extent()
            dlgFile = wx.FileDialog(self, "Export map to file",
                                    self.settings.dirExport,
                                    self.filename,
                                    File.get_type_filters(File.Types.GEO),
                                    wx.SAVE | wx.OVERWRITE_PROMPT)
            dlgFile.SetFilterIndex(File.GeoType.KMZ)
            if dlgFile.ShowModal() == wx.ID_OK:
                fileName = dlgFile.GetFilename()
                dirName = dlgFile.GetDirectory()
                self.settings.dirExport = dirName
                fileName = extension_add(fileName, dlgFile.GetFilterIndex(),
                                         File.Types.GEO)
                fullName = os.path.join(dirName, fileName)
                exportType = dlgFile.GetFilterIndex()
                image = None
                xyz = None
                if exportType == File.GeoType.CSV:
                    xyz = dlgGeo.get_xyz()
                else:
                    image = dlgGeo.get_image()
                export_map(fullName, exportType, extent, image, xyz)
            self.status.set_general("Finished")
            dlgFile.Destroy()
        dlgGeo.Destroy()

    def __on_export_track(self, _event):
        dlg = wx.FileDialog(self, "Export GPS to file",
                            self.settings.dirExport,
                            self.filename,
                            File.get_type_filters(File.Types.TRACK),
                            wx.SAVE | wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            self.status.set_general("Exporting...")
            fileName = dlg.GetFilename()
            dirName = dlg.GetDirectory()
            self.settings.dirExport = dirName
            fileName = extension_add(fileName, dlg.GetFilterIndex(),
                                     File.Types.TRACK)
            fullName = os.path.join(dirName, fileName)
            export_gpx(fullName, self.locations, self.GetName())
            self.status.set_general("Finished")
        dlg.Destroy()

    def __on_page(self, _event):
        dlg = wx.PageSetupDialog(self, self.pageConfig)
        if dlg.ShowModal() == wx.ID_OK:
            self.pageConfig = wx.PageSetupDialogData(dlg.GetPageSetupDialogData())
            self.printConfig.SetPrintData(self.pageConfig.GetPrintData())
        dlg.Destroy()

    def __on_preview(self, _event):
        printout = PrintOut(self.graph, self.filename, self.pageConfig)
        printoutPrinting = PrintOut(self.graph, self.filename, self.pageConfig)
        preview = wx.PrintPreview(printout, printoutPrinting, self.printConfig)
        frame = wx.PreviewFrame(preview, self, 'Print Preview')
        frame.Initialize()
        frame.SetSize(self.GetSize())
        frame.Show(True)

    def __on_print(self, _event):
        printer = wx.Printer(self.printConfig)
        printout = PrintOut(self.graph, self.filename, self.pageConfig)
        if printer.Print(self, printout, True):
            self.printConfig = wx.PrintDialogData(printer.GetPrintDialogData())
            self.pageConfig.SetPrintData(self.printConfig.GetPrintData())

    def __on_properties(self, _event):
        if len(self.spectrum) > 0:
            self.scanInfo.timeFirst = min(self.spectrum)
            self.scanInfo.timeLast = max(self.spectrum)

        dlg = DialogProperties(self, self.scanInfo)
        dlg.ShowModal()
        dlg.Destroy()

    def __on_exit(self, _event):
        self.Unbind(wx.EVT_CLOSE)
        if self.__save_warn(Warn.EXIT):
            self.Bind(wx.EVT_CLOSE, self.__on_exit)
            return
        self.__scan_stop(False)
        self.__stop_gps(False)
        self.__stop_location_server()
        self.__get_controls()
        self.settings.devicesRtl = self.devicesRtl
        self.settings.save()
        self.graph.close()
        self.Destroy()

    def __on_pref(self, _event):
        self.__get_controls()
        dlg = DialogPrefs(self, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.graph.create_plot()
            self.__set_control_state(True)
            self.__set_controls()
        dlg.Destroy()

    def __on_adv_pref(self, _event):
        dlg = DialogAdvPrefs(self, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.__set_control_state(True)
        dlg.Destroy()

    def __on_formatting(self, _event):
        dlg = DialogFormatting(self, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.__set_control_state(True)
            self.graph.update_measure()
            self.graph.redraw_plot()
        dlg.Destroy()

    def __on_devices_rtl(self, _event):
        self.__get_controls()
        self.devicesRtl = self.__refresh_devices()
        dlg = DialogDevicesRTL(self, self.devicesRtl, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.devicesRtl = dlg.get_devices()
            self.settings.indexRtl = dlg.get_index()
            self.__set_gain_control()
            self.__set_control_state(True)
            self.__set_controls()
        dlg.Destroy()

    def __on_devices_gps(self, _event):
        self.__stop_gps()
        self.status.set_gps('GPS Stopped')
        dlg = DialogDevicesGPS(self, self.settings)
        dlg.ShowModal()
        dlg.Destroy()
        self.__start_gps()

    def __on_reset(self, _event):
        dlg = wx.MessageDialog(self,
                               'Reset all settings to the default values\n'
                               '(cannot be undone)?',
                               'Reset Settings',
                               wx.YES_NO | wx.ICON_QUESTION)
        if dlg.ShowModal() == wx.ID_YES:
            self.devicesRtl = []
            self.settings.reset()
            self.__set_controls()
            self.graph.create_plot()
        dlg.Destroy()

    def __on_compare(self, _event):
        dlg = DialogCompare(self, self.settings, self.filename)
        dlg.Show()

    def __on_smooth(self, _event):
        dlg = DialogSmooth(self, self.spectrum, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            saved = self.isSaved
            self.isSaved = False
            if not self.__on_new(None):
                self.spectrum.clear()
                spectrum = dlg.get_spectrum()
                self.spectrum.update(OrderedDict(sorted(spectrum.items())))
                self.__set_plot(self.spectrum, False)
                self.graph.update_measure()
                self.graph.redraw_plot()
                self.__saved(False)
            else:
                self.__saved(saved)

    def __on_clear_select(self, _event):
        self.graph.clear_selection()

    def __on_show_measure(self, event):
        show = event.Checked()
        self.menuMain.showMeasure.Check(show)
        self.menuPopup.showMeasure.Check(show)
        self.settings.showMeasure = show
        self.graph.show_measure_table(show)
        self.Layout()

    def __on_cal(self, _event):
        self.dlgCal = DialogAutoCal(self, self.settings.calFreq, self.__auto_cal)
        self.dlgCal.ShowModal()

    def __on_gearth(self, _event):
        tempPath = tempfile.mkdtemp()
        tempFile = os.path.join(tempPath, 'RTLSDRScannerLink.kml')
        handle = open(tempFile, 'wb')
        create_gearth(handle)
        handle.close()

        if not run_file(tempFile):
            wx.MessageBox('Error starting Google Earth', 'Error',
                          wx.OK | wx.ICON_ERROR)

    def __on_gmaps(self, _event):
        url = 'http://localhost:{}/rtlsdr_scan.html'.format(LOCATION_PORT)
        webbrowser.open_new(url)

    def __on_sats(self, _event):
        if self.dlgSats is None:
            self.dlgSats = DialogSats(self)
            self.dlgSats.Show()

    def __on_loc_clear(self, _event):
        result = wx.MessageBox('Remove {} locations from scan?'.format(len(self.locations)),
                               'Clear location data',
                               wx.YES_NO, self)
        if result == wx.YES:
            self.locations.clear()
            self.__set_control_state(True)

    def __on_log(self, _event):
        if self.dlgLog is None:
            self.dlgLog = DialogLog(self, self.log)
            self.dlgLog.Show()

    def __on_help(self, _event):
        webbrowser.open("http://eartoearoak.com/software/rtlsdr-scanner")

    def __on_update(self, _event):
        if self.threadUpdate is None:
            self.status.set_general("Checking for updates", level=None)
            self.threadUpdate = Thread(target=self.__update_check)
            self.threadUpdate.start()

    def __on_sys_info(self, _event):
        dlg = DialogSysInfo(self)
        dlg.ShowModal()
        dlg.Destroy()

    def __on_about(self, _event):
        dlg = DialogAbout(self)
        dlg.ShowModal()
        dlg.Destroy()

    def __on_spin(self, event):
        control = event.GetEventObject()
        if control == self.spinCtrlStart:
            self.spinCtrlStop.SetRange(self.spinCtrlStart.GetValue() + 1,
                                       F_MAX)

    def __on_choice(self, _event):
        self.__get_controls()
        self.graph.create_plot()

    def __on_start(self, event):
        self.__get_controls()

        if self.settings.start >= self.settings.stop:
            wx.MessageBox('Stop frequency must be greater that start',
                          'Warning', wx.OK | wx.ICON_WARNING)
            return

        self.devicesRtl = self.__refresh_devices()
        if len(self.devicesRtl) == 0:
            wx.MessageBox('No devices found',
                          'Error', wx.OK | wx.ICON_ERROR)
        else:
            if event.GetInt() == 0:
                self.isNewScan = True
            else:
                self.isNewScan = False
            self.__scan_start()
            if not self.settings.retainScans:
                self.status.set_info('Warning: Averaging is enabled in preferences',
                                     level=Log.WARN)

    def __on_continue(self, event):
        event.SetInt(1)
        self.__on_start(event)

    def __on_stop(self, event):
        if event.GetInt() == 0:
            self.stopScan = True
            self.stopAtEnd = False
            self.__scan_stop()
        else:
            self.stopScan = False
            self.stopAtEnd = True

    def __on_stop_end(self, _event):
        self.stopAtEnd = True

    def __on_range_lim(self, _event):
        xmin, xmax = self.graph.get_axes().get_xlim()
        xmin = int(xmin)
        xmax = math.ceil(xmax)
        if xmax < xmin + 1:
            xmax = xmin + 1
        self.settings.start = xmin
        self.settings.stop = xmax
        self.__set_controls()

    def __on_points_lim(self, _event):
        self.settings.pointsLimit = self.menuPopup.pointsLim.IsChecked()
        self.__set_plot(self.spectrum, self.settings.annotate)

    def __on_gps_retry(self, _event):
        self.timerGpsRetry.Stop()
        self.__stop_gps()
        self.__start_gps()

    def __on_event(self, event):
        status = event.data.get_status()
        arg1 = event.data.get_arg1()
        arg2 = event.data.get_arg2()
        if status == Event.STARTING:
            self.status.set_general("Starting")
            self.isScanning = True
        elif status == Event.STEPS:
            self.stepsTotal = (arg1 + 1) * 2
            self.steps = self.stepsTotal
            self.status.set_progress(0)
            self.status.show_progress()
        elif status == Event.CAL:
            self.__auto_cal(Cal.DONE)
        elif status == Event.INFO:
            if self.threadScan is not None:
                self.sdr = self.threadScan.get_sdr()
                if arg2 is not None:
                    self.devicesRtl[self.settings.indexRtl].tuner = arg2
                    self.scanInfo.tuner = arg2
        elif status == Event.DATA:
            self.__saved(False)
            cal = self.devicesRtl[self.settings.indexRtl].calibration
            freq, scan = self.queueScan.get()
            self.pool.apply_async(anaylse_data,
                                  (freq, scan, cal,
                                   self.settings.nfft,
                                   self.settings.overlap,
                                   self.settings.winFunc),
                                  callback=self.__on_process_done)
            self.__progress()
        elif status == Event.STOPPED:
            self.__cleanup()
            self.status.set_general("Stopped")
        elif status == Event.FINISHED:
            self.threadScan = None
        elif status == Event.ERROR:
            self.__cleanup()
            self.status.set_general("Error: {}".format(arg2), level=Log.ERROR)
            if self.dlgCal is not None:
                self.dlgCal.Destroy()
                self.dlgCal = None
        elif status == Event.PROCESSED:
            offset = self.settings.devicesRtl[self.settings.indexRtl].offset
            if self.settings.alert:
                alert = self.settings.alertLevel
            else:
                alert = None
            Thread(target=update_spectrum, name='Update',
                   args=(self, self.lock, self.settings.start,
                         self.settings.stop, arg1,
                         arg2, offset, self.spectrum,
                         not self.settings.retainScans,
                         alert)).start()
        elif status == Event.LEVEL:
            wx.Bell()
        elif status == Event.UPDATED:
            if arg2 and self.settings.liveUpdate:
                self.__set_plot(self.spectrum,
                                self.settings.annotate and
                                self.settings.retainScans and
                                self.settings.mode == Mode.CONTIN)
            self.__progress()
        elif status == Event.DRAW:
            self.graph.draw()
        elif status == Event.VER_UPD:
            self.__update_checked(True, arg1, arg2)
        elif status == Event.VER_NOUPD:
            self.__update_checked(False)
        elif status == Event.VER_UPDFAIL:
            self.__update_checked(failed=True)
        elif status == Event.LOC_WARN:
            self.status.set_gps("{}".format(arg2), level=Log.WARN)
            self.status.warn_gps()
        elif status == Event.LOC_ERR:
            self.status.set_gps("{}".format(arg2), level=Log.ERROR)
            self.status.error_gps()
            self.threadLocation = None
            if self.settings.gpsRetry:
                if not self.timerGpsRetry.IsRunning():
                    self.timerGpsRetry.Start(20000, True)
        elif status == Event.LOC:
            self.__update_location(arg2)
        elif status == Event.LOC_SAT:
            if self.dlgSats is not None:
                self.dlgSats.set_sats(arg2)

        wx.YieldIfNeeded()

    def __on_process_done(self, data):
        timeStamp, freq, scan = data
        post_event(self, EventThread(Event.PROCESSED, freq,
                                     (timeStamp, scan)))

    def __auto_cal(self, status):
        freq = self.dlgCal.get_arg1()
        if self.dlgCal is not None:
            if status == Cal.START:
                self.spinCtrlStart.SetValue(int(freq))
                self.spinCtrlStop.SetValue(math.ceil(freq))
                self.oldCal = self.devicesRtl[self.settings.indexRtl].calibration
                self.devicesRtl[self.settings.indexRtl].calibration = 0
                self.__get_controls()
                self.spectrum.clear()
                self.locations.clear()
                if not self.__scan_start(isCal=True):
                    self.dlgCal.reset_cal()
            elif status == Cal.DONE:
                ppm = self.__calc_ppm(freq)
                self.dlgCal.set_cal(ppm)
                self.__set_control_state(True)
            elif status == Cal.OK:
                self.devicesRtl[self.settings.indexRtl].calibration = self.dlgCal.get_cal()
                self.settings.calFreq = freq
                self.dlgCal = None
            elif status == Cal.CANCEL:
                self.dlgCal = None
                if len(self.devicesRtl) > 0:
                    self.devicesRtl[self.settings.indexRtl].calibration = self.oldCal

    def __calc_ppm(self, freq):
        with self.lock:
            timeStamp = max(self.spectrum)
            spectrum = self.spectrum[timeStamp].copy()

            for x, y in spectrum.iteritems():
                spectrum[x] = (((x - freq) * (x - freq)) + 1) * y
                peak = max(spectrum, key=spectrum.get)

        return ((freq - peak) / freq) * 1e6

    def __scan_start(self, isCal=False):
        if self.isNewScan and self.__save_warn(Warn.SCAN):
            return False

        if not self.threadScan:
            self.__set_control_state(False)
            samples = calc_samples(self.settings.dwell)
            if self.isNewScan:
                self.spectrum.clear()
                self.locations.clear()
                self.graph.clear_plots()

                self.isNewScan = False
                self.status.set_info('', level=None)
                self.scanInfo.set_from_settings(self.settings)
                self.scanInfo.time = format_iso_time(time.time())
                self.scanInfo.lat = None
                self.scanInfo.lon = None
                self.scanInfo.desc = ''

            self.stopAtEnd = False
            self.stopScan = False
            self.threadScan = ThreadScan(self, self.queueScan, self.sdr, self.settings,
                                         self.settings.indexRtl, samples, isCal)
            self.filename = "Scan {0:.1f}-{1:.1f}MHz".format(self.settings.start,
                                                             self.settings.stop)
            self.graph.set_plot_title()

            self.__start_gps()

            return True

    def __scan_stop(self, join=True):
        if self.threadScan:
            self.status.set_general("Stopping")
            self.threadScan.abort()
            if join:
                self.threadScan.join()
        self.threadScan = None
        if self.sdr is not None:
            self.sdr.close()
        self.__set_control_state(True)

    def __progress(self):
        if self.steps == self.stepsTotal:
            self.status.set_general("Scanning ({} sweeps)".format(len(self.spectrum)))
        self.steps -= 1
        if self.steps > 0 and not self.stopScan:
            self.status.set_progress((self.stepsTotal - self.steps) * 100.0
                                     / (self.stepsTotal - 1))
            self.status.show_progress()
        else:
            self.status.hide_progress()
            self.__set_plot(self.spectrum, self.settings.annotate)
            if self.stopScan:
                self.status.set_general("Stopped")
                self.__cleanup()
            elif self.settings.mode == Mode.SINGLE:
                self.status.set_general("Finished")
                self.__cleanup()
            else:
                if self.settings.mode == Mode.CONTIN:
                    if self.dlgCal is None and not self.stopAtEnd:
                        self.__limit_spectrum()
                        self.__scan_start()
                    else:
                        self.status.set_general("Stopped")
                        self.__cleanup()

    def __cleanup(self):
        if self.sdr is not None:
            self.sdr.close()
            self.sdr = None

        self.status.hide_progress()
        self.steps = 0
        self.threadScan = None
        self.__set_control_state(True)
        self.stopAtEnd = False
        self.stopScan = True
        self.isScanning = False

    def __remove_last(self, data):
        while len(data) >= self.settings.retainMax:
            timeStamp = min(data)
            del data[timeStamp]

    def __limit_spectrum(self):
        with self.lock:
            self.__remove_last(self.spectrum)
            self.__remove_last(self.locations)

    def __start_gps(self):
        if self.settings.gps and len(self.settings.devicesGps):
            self.status.enable_gps()
            if self.threadLocation is None:
                device = self.settings.devicesGps[self.settings.indexGps]
                self.threadLocation = ThreadLocation(self, device)
        else:
            self.status.disable_gps()

    def __stop_gps(self, join=True):
        if self.threadLocation and self.threadLocation.isAlive():
            self.threadLocation.stop()
            if join:
                self.threadLocation.join()
        self.threadLocation = None

    def __start_location_server(self):
        self.serverLocation = LocationServer(self.locations, self.lastLocation,
                                             self.lock, self.log)

    def __stop_location_server(self):
        if self.serverLocation:
            self.serverLocation.close()

    def __update_location(self, data):
        i = 0
        for loc in data:
            self.lastLocation[i] = loc
            i += 1
        self.status.pulse_gps()
        if data[2] is None:
            gpsStatus = '{:.5f}, {:.5f}, {:.1f}'.format(data[0], data[1])
        else:
            gpsStatus = '{:.5f}, {:.5f}, {:.1f}m'.format(data[0], data[1], data[2])

        self.status.set_gps(gpsStatus, level=None)

        if not self.isScanning:
            return

        if self.scanInfo is not None:
            if data[0] and data[1]:
                self.scanInfo.lat = str(data[0])
                self.scanInfo.lon = str(data[1])

        with self.lock:
            if len(self.spectrum) > 0:
                self.locations[max(self.spectrum)] = (data[0],
                                                      data[1],
                                                      data[2])

    def __saved(self, isSaved):
        self.isSaved = isSaved
        title = APP_NAME + " - " + self.filename
        if not isSaved:
            title += "*"
        self.SetTitle(title)

    def __set_plot(self, spectrum, annotate):
        if len(spectrum) > 0:
            total = count_points(spectrum)
            if total > 0:
                spectrum = sort_spectrum(spectrum)
                extent = Extent(spectrum)
                self.graph.set_plot(spectrum,
                                    self.settings.pointsLimit,
                                    self.settings.pointsMax,
                                    extent, annotate)
        else:
            self.graph.clear_plots()

    def __set_control_state(self, state):
        hasDevices = len(self.devicesRtl) > 0

        self.spinCtrlStart.Enable(state)
        self.spinCtrlStop.Enable(state)
        self.controlGain.Enable(state)
        self.choiceMode.Enable(state)
        self.choiceDwell.Enable(state)
        self.choiceNfft.Enable(state)
        self.buttonStart.Enable(state and hasDevices)
        self.buttonStop.Enable(not state and hasDevices)

        self.menuMain.set_state(state, self.spectrum, self.locations)
        self.menuPopup.set_state(state, self.spectrum)

    def __set_controls(self):
        self.spinCtrlStart.SetValue(self.settings.start)
        self.spinCtrlStop.SetValue(self.settings.stop)
        self.choiceMode.SetSelection(MODE[1::2].index(self.settings.mode))
        dwell = calc_real_dwell(self.settings.dwell)
        try:
            sel = DWELL[1::2].index(dwell)
        except ValueError:
            sel = DWELL[1::2][len(DWELL) / 4]
        self.choiceDwell.SetSelection(sel)
        self.choiceNfft.SetSelection(NFFT.index(self.settings.nfft))
        self.choiceDisplay.SetSelection(DISPLAY[1::2].index(self.settings.display))

    def __set_gain_control(self):
        grid = self.controlGain.GetContainingSizer()
        if len(self.devicesRtl) > 0:
            self.controlGain.Destroy()
            device = self.devicesRtl[self.settings.indexRtl]
            if device.isDevice:
                gains = device.get_gains_str()
                self.controlGain = wx.Choice(self.toolbar,
                                             choices=gains)
                gain = device.get_closest_gain_str(device.gain)
                self.controlGain.SetStringSelection(gain)
            else:
                self.controlGain = NumCtrl(self.toolbar, integerWidth=3,
                                           fractionWidth=1)
                font = self.controlGain.GetFont()
                dc = wx.WindowDC(self.controlGain)
                dc.SetFont(font)
                size = dc.GetTextExtent('####.#')
                self.controlGain.SetMinSize((size[0] * 1.2, -1))
                self.controlGain.SetValue(device.gain)

            grid.Add(self.controlGain, pos=(1, 7), flag=wx.ALIGN_CENTER)
            grid.Layout()

    def __get_controls(self):
        self.settings.start = self.spinCtrlStart.GetValue()
        self.settings.stop = self.spinCtrlStop.GetValue()
        self.settings.startOption = self.buttonStart.GetSelected()
        self.settings.stopOption = self.buttonStop.GetSelected()
        self.settings.mode = MODE[1::2][self.choiceMode.GetSelection()]
        self.settings.dwell = DWELL[1::2][self.choiceDwell.GetSelection()]
        self.settings.nfft = NFFT[self.choiceNfft.GetSelection()]
        self.settings.display = DISPLAY[1::2][self.choiceDisplay.GetSelection()]

        if len(self.devicesRtl) > 0:
            device = self.devicesRtl[self.settings.indexRtl]
            try:
                if device.isDevice:
                    device.gain = float(self.controlGain.GetStringSelection())
                else:
                    device.gain = self.controlGain.GetValue()
            except ValueError:
                device.gain = 0

    def __save_warn(self, warnType):
        if self.settings.saveWarn and not self.isSaved:
            dlg = DialogSaveWarn(self, warnType)
            code = dlg.ShowModal()
            if code == wx.ID_YES:
                self.__on_save(None)
                if self.isSaved:
                    return False
                else:
                    return True
            elif code == wx.ID_NO:
                return False
            else:
                return True

        return False

    def __update_check(self):
        local = get_version_timestamp(True)
        try:
            remote = get_version_timestamp_repo()
        except IOError:
            post_event(self, EventThread(Event.VER_UPDFAIL))
            return

        if remote > local:
            post_event(self, EventThread(Event.VER_UPD, local, remote))
        else:
            post_event(self, EventThread(Event.VER_NOUPD))

    def __update_checked(self, updateFound=False, local=None, remote=None,
                         failed=False):
        self.threadUpdate = None
        self.status.set_general("", level=None)
        if failed:
            icon = wx.ICON_ERROR
            message = "Update check failed"
        else:
            icon = wx.ICON_INFORMATION
            if updateFound:
                message = "Update found\n\n"
                message += "Local: " + time.strftime('%c',
                                                     time.localtime(local))
                message += "\nRemote: " + time.strftime('%c',
                                                        time.localtime(remote))
            else:
                message = "No updates found"

        dlg = wx.MessageDialog(self, message, "Update",
                               wx.OK | icon)
        dlg.ShowModal()
        dlg.Destroy()

    def __refresh_devices(self):
        self.settings.devicesRtl = get_devices_rtl(self.devicesRtl, self.status)
        self.settings.indexRtl = limit(self.settings.indexRtl,
                                       0, len(self.devicesRtl) - 1)
        self.settings.save()
        return self.settings.devicesRtl

    def __merge(self, dirname, filename):
        if not os.path.exists(os.path.join(dirname, filename)):
            wx.MessageBox('File not found',
                          'Error', wx.OK | wx.ICON_ERROR)
            return

        self.filename = os.path.splitext(filename)[0]
        self.settings.dirScans = dirname
        self.status.set_general("Merging: {}".format(filename))

        _scanInfo, spectrum, locations = open_plot(dirname, filename)

        if len(spectrum) > 0:
            spectrum.update(self.spectrum)
            locations.update(self.locations)
            self.spectrum.clear()
            self.locations.clear()
            self.spectrum.update(OrderedDict(sorted(spectrum.items())))
            self.locations.update(OrderedDict(sorted(locations.items())))
            self.__set_plot(self.spectrum, self.settings.annotate)
            self.graph.scale_plot(True)
            self.status.set_general("Finished")
            self.settings.fileHistory.AddFileToHistory(os.path.join(dirname,
                                                                    filename))
        else:
            self.status.set_general("Merge failed", level=Log.ERROR)

    def open(self, dirname, filename):
        if not os.path.exists(os.path.join(dirname, filename)):
            wx.MessageBox('File not found',
                          'Error', wx.OK | wx.ICON_ERROR)
            return

        self.__on_new(None)
        self.graph.get_canvas().draw()

        self.filename = os.path.splitext(filename)[0]
        self.settings.dirScans = dirname
        self.status.set_general("Opening: {}".format(filename))

        self.scanInfo, spectrum, location = open_plot(dirname, filename)

        if len(spectrum) > 0:
            self.scanInfo.set_to_settings(self.settings)
            self.spectrum = spectrum
            self.locations.clear()
            self.locations.update(location)
            self.__saved(True)
            self.__set_controls()
            self.__set_control_state(True)
            self.__set_plot(spectrum, self.settings.annotate)
            self.graph.scale_plot(True)
            self.status.set_general("Finished")
            self.settings.fileHistory.AddFileToHistory(os.path.join(dirname,
                                                                    filename))
        else:
            self.status.set_general("Open failed", level=Log.ERROR)
Esempio n. 4
0
 def __start_location_server(self):
     self.serverLocation = LocationServer(self.locations, self.lastLocation,
                                          self.lock, self.log)
Esempio n. 5
0
class AthlinksCollect(CollectServiceResults):
########################################################################

    #----------------------------------------------------------------------
    def __init__(self):
    #----------------------------------------------------------------------
        '''
        initialize object instance

        may be overridden when ResultsCollect is instantiated, but overriding method must call
        `super(<subclass>, self).__init__(servicename, resultfilehdr, resultattrs)`

        '''
        
        super(AthlinksCollect, self).__init__('athlinks', resultfilehdr, resultattrs)


    #----------------------------------------------------------------------
    def openservice(self, club_id):
    #----------------------------------------------------------------------
        '''
        initialize service
        recommended that the overriding method save service instance in `self.service`

        must be overridden when ResultsCollect is instantiated

        :param club_id: club.id for club this service is operating on
        '''
        # create location server
        self.locsvr = LocationServer()

        # remember club id we're working on
        self.club_id = club_id

        # debug file for races saved
        # set debugrace to False if not debugging
        debugrace = True
        if debugrace:
            clubslug = Club.query.filter_by(id=club_id).first().shname
            self.racefile = '{}/{}-athlinks-race.csv'.format(app.config['MEMBERSHIP_DIR'], clubslug)
        else:
            self.racefile = None

        if self.racefile:
            self._RACE = open(self.racefile, 'wb')
            self.racefields = 'id,name,date,distmiles,status,runner'.split(',')
            self.RACE = csv.DictWriter(self._RACE, self.racefields)
            self.RACE.writeheader()

        # open service
        key = ApiCredentials.query.filter_by(name=self.servicename).first().key
        self.service = athlinks.Athlinks(debug=True, key=key)


    #----------------------------------------------------------------------
    def getresults(self, name, fname, lname, gender, dt_dob, begindate, enddate):
    #----------------------------------------------------------------------
        '''
        retrieves a list of results for a single name

        must be overridden when ResultsCollect is instantiated

        use dt_dob to filter errant race results, based on age of runner on race day

        :param name: name of participant for which results are to be returned
        :param fname: first name of participant
        :param lname: last name of participant
        :param gender: 'M' or 'F'
        :param dt_dob: participant's date of birth, as datetime 
        :param begindate: epoch time for start of results, 00:00:00 on date to begin
        :param end: epoch time for end of results, 23:59:59 on date to finish
        :rtype: list of serviceresults, each of which can be processed by convertresult
        '''
        
        # remember participant data
        self.name = name
        self.fname = fname
        self.lname = lname
        self.gender = gender
        self.dt_dob = dt_dob
        self.dob = ftime.dt2asc(dt_dob)

        # get results for this athlete
        allresults = self.service.listathleteresults(name)

        # filter by date and by age
        filteredresults = []
        for result in allresults:
            e_racedate = athlinks.gettime(result['Race']['RaceDate'])
            
            # skip result if outside the desired time window
            if e_racedate < begindate or e_racedate > enddate: continue

            # skip result if wrong gender
            resultgen = result['Gender'][0]
            if resultgen != gender: continue

            # skip result if runner's age doesn't match the age within the result
            # sometimes athlinks stores the age group of the runner, not exact age,
            # so also check if this runner's age is within the age group, and indicate if so
            dt_racedate = timeu.epoch2dt(e_racedate)
            racedateage = timeu.age(dt_racedate,dt_dob)
            resultage = int(result['Age'])
            result['fuzzyage'] = False
            if resultage != racedateage:
                # if results are not stored as age group, skip this result
                if (resultage/5)*5 != resultage:
                    continue
                # result's age might be age group, not exact age
                else:
                    # if runner's age consistent with race age, use result, but mark "fuzzy"
                    if (racedateage/5)*5 == resultage:
                        result['fuzzyage'] = True
                    # otherwise skip result
                    else:
                        continue

            # if we reach here, the result is ok, and is added to filteredresults
            filteredresults.append(result)

        # back to caller
        return filteredresults


    #----------------------------------------------------------------------
    def convertserviceresult(self, result):
    #----------------------------------------------------------------------
        '''
        converts a single service result to dict suitable to be saved in resultfile

        result must be converted to dict with keys in `resultfilehdr` provided at instance creation

        must be overridden when ResultsCollect is instantiated

        use return value of None for cases when results could not be filtered by `:meth:getresults`

        :param fname: participant's first name
        :param lname: participant's last name
        :param result: single service result, from list retrieved through `getresults`
        :rtype: dict with keys matching `resultfilehdr`, or None if result is not to be saved
        '''

        # create output record and copy common fields
        outrec = {}

        # copy participant information
        outrec['name'] = self.name
        outrec['GivenName'] = self.fname
        outrec['FamilyName'] = self.lname
        outrec['DOB'] = self.dob
        outrec['Gender'] = self.gender


        # some debug items - assume everything is cached
        coursecached = True
        racecached = True

        # get course used for this result
        courseid = '{}/{}'.format(result['Race']['RaceID'], result['CourseID'])
        course = Course.query.filter_by(club_id=self.club_id, source='athlinks', sourceid=courseid).first()

        # cache course if not done already
        race = None
        if not course:
            coursecached = False

            coursedata = self.service.getcourse(result['Race']['RaceID'], result['CourseID'])

            distmiles = athlinks.dist2miles(coursedata['Courses'][0]['DistUnit'],coursedata['Courses'][0]['DistTypeID'])
            distkm = athlinks.dist2km(coursedata['Courses'][0]['DistUnit'],coursedata['Courses'][0]['DistTypeID'])
            if distkm < 0.050: return None # skip timed events, which seem to be recorded with 0 distance

            # skip result if not Running or Trail Running race
            thiscategory = coursedata['Courses'][0]['RaceCatID']
            if thiscategory not in race_category: return None
        
            course = Course()
            course.club_id = self.club_id
            course.source = 'athlinks'
            course.sourceid = courseid

            # strip racename and coursename here to make sure detail file matches what is stored in database
            racename = csvu.unicode2ascii(coursedata['RaceName']).strip()
            coursename = csvu.unicode2ascii(coursedata['Courses'][0]['CourseName']).strip()
            course.name = '{} / {}'.format(racename,coursename)

            # maybe truncate to FIRST part of race name
            if len(course.name) > MAX_RACENAME_LEN:
                course.name = course.name[:MAX_RACENAME_LEN]
            
            course.date = ftime.epoch2asc(athlinks.gettime(coursedata['RaceDate']))
            course.location = csvu.unicode2ascii(coursedata['Home'])
            # maybe truncate to LAST part of location name, to keep most relevant information (state, country)
            if len(course.location) > MAX_LOCATION_LEN:
                course.location = course.location[-MAX_LOCATION_LEN:]

            # TODO: adjust marathon and half marathon distances?
            course.distkm =distkm
            course.distmiles = distmiles

            course.surface = race_category[thiscategory]

            # retrieve or add race
            # flush should allow subsequent query per http://stackoverflow.com/questions/4201455/sqlalchemy-whats-the-difference-between-flush-and-commit
            # Race has uniqueconstraint for club_id/name/year/fixeddist. It's been seen that there are additional races in athlinks, 
            # but just assume the first is the correct one.
            raceyear = ftime.asc2dt(course.date).year
            race = Race.query.filter_by(club_id=self.club_id, name=course.name, year=raceyear, fixeddist=race_fixeddist(course.distmiles)).first()
            ### TODO: should the above be .all() then check for first race within epsilon distance?
            if not race:
                racecached = False
                race = Race(self.club_id, raceyear)
                race.name = course.name
                race.distance = course.distmiles
                race.fixeddist = race_fixeddist(race.distance)
                race.date = course.date
                race.active = True
                race.external = True
                race.surface = course.surface
                loc = self.locsvr.getlocation(course.location)
                race.locationid = loc.id
                db.session.add(race)
                db.session.flush()  # force id to be created

            course.raceid = race.id
            db.session.add(course)
            db.session.flush()      # force id to be created

        # maybe course was cached but location of race wasn't
        # update location of result race, if needed, and if supplied
        # this is here to clean up old database data
        if not race:
            race = Race.query.filter_by(club_id=self.club_id, name=course.name, year=ftime.asc2dt(course.date).year, fixeddist=race_fixeddist(course.distmiles)).first()
        if not race.locationid and course.location:
            # app.logger.debug('updating race with location {}'.format(course.location))
            loc = self.locsvr.getlocation(course.location)
            race.locationid = loc.id
            insert_or_update(db.session, Race, race, skipcolumns=['id'], 
                             club_id=self.club_id, name=course.name, year=ftime.asc2dt(course.date).year, fixeddist=race_fixeddist(course.distmiles))
        # else:
        #     app.logger.debug('race.locationid={} course.location="{}"'.format(race.locationid, course.location))

        # debug races
        if self.racefile:
            racestatusl = []
            if not coursecached: racestatusl.append('addcourse')
            if not racecached: racestatusl.append('addrace')
            if not racestatusl: racestatusl.append('cached')
            racestatus = '-'.join(racestatusl)
            racerow = {'status': racestatus, 'runner': self.name}

            for racefield in self.racefields:
                if racefield in ['status', 'runner']: continue
                racerow[racefield] = getattr(course,racefield)
            self.RACE.writerow(racerow)


        # fill in output record fields from result, course
        # combine name, get age
        outrec['age'] = result['Age']
        outrec['fuzzyage'] = result['fuzzyage']

        # leave athlid blank if result not from an athlink member
        athlmember = result['IsMember']
        if athlmember:
            outrec['athlid'] = result['RacerID']

        # remember the entryid, high water mark of which can be used to limit the work here
        outrec['entryid'] = result['EntryID']

        # race name, location; convert from unicode if necessary
        # TODO: make function to do unicode translation -- apply to runner name as well (or should csv just store unicode?)
        outrec['race'] = course.name
        outrec['date'] = course.date
        outrec['loc'] = course.location
        
        outrec['miles'] = course.distmiles
        outrec['km'] = course.distkm
        outrec['category'] = course.surface
        resulttime = result['TicksString']

        # strange case of TicksString = ':00'
        if resulttime[0] == ':':
            resulttime = '0'+resulttime
        while resulttime.count(':') < 2:
            resulttime = '0:'+resulttime
        outrec['time'] = resulttime
        
        # strange case of 0 time, causes ZeroDivisionError and is clearly not valid
        if timeu.timesecs(resulttime) == 0: return None

        # leave out age grade if exception occurs, skip results which have outliers
        try:
            # skip result if runner's age doesn't match the age within the result
            # sometimes athlinks stores the age group of the runner, not exact age,
            # so also check if this runner's age is within the age group, and indicate if so
            e_racedate = athlinks.gettime(result['Race']['RaceDate'])
            resultgen = result['Gender'][0]
            dt_racedate = timeu.epoch2dt(e_racedate)
            racedateage = timeu.age(dt_racedate,self.dt_dob)
            agpercent,agresult,agfactor = ag.agegrade(racedateage,resultgen,course.distmiles,timeu.timesecs(resulttime))
            outrec['ag'] = agpercent
            if agpercent < 15 or agpercent >= 100: return None # skip obvious outliers
        except:
            app.logger.warning(traceback.format_exc())
            pass

        # and we're done
        return outrec

    #----------------------------------------------------------------------
    def closeservice(self):
    #----------------------------------------------------------------------
        '''
        closes service, if necessary

        may be overridden when ResultsCollect is instantiated
        '''
        if self.racefile:
            self._RACE.close()
class FrameMain(wx.Frame):
    def __init__(self, title, pool):

        self.pool = pool
        self.lock = threading.Lock()

        self.sdr = None
        self.threadScan = None
        self.threadUpdate = None
        self.threadLocation = None

        self.serverLocation = None

        self.isNewScan = True
        self.isScanning = False

        self.stopAtEnd = False
        self.stopScan = False

        self.dlgCal = None
        self.dlgSats = None
        self.dlgLog = None

        self.menuMain = None
        self.menuPopup = None

        self.graph = None
        self.toolbar = None
        self.canvas = None

        self.buttonStart = None
        self.buttonStop = None
        self.controlGain = None
        self.choiceMode = None
        self.choiceDwell = None
        self.choiceNfft = None
        self.spinCtrlStart = None
        self.spinCtrlStop = None
        self.choiceDisplay = None

        self.spectrum = OrderedDict()
        self.scanInfo = ScanInfo()
        self.locations = OrderedDict()
        self.lastLocation = [None] * 4

        self.isSaved = True

        self.settings = Settings()
        self.devicesRtl = get_devices_rtl(self.settings.devicesRtl)
        self.settings.indexRtl = limit(self.settings.indexRtl, 0,
                                       len(self.devicesRtl) - 1)
        self.filename = ""
        self.oldCal = 0

        self.remoteControl = None

        self.log = Log()

        self.pageConfig = wx.PageSetupDialogData()
        self.pageConfig.GetPrintData().SetOrientation(wx.LANDSCAPE)
        self.pageConfig.SetMarginTopLeft((20, 20))
        self.pageConfig.SetMarginBottomRight((20, 20))
        self.printConfig = wx.PrintDialogData(self.pageConfig.GetPrintData())
        self.printConfig.EnableSelection(False)
        self.printConfig.EnablePageNumbers(False)

        wx.Frame.__init__(self, None, title=title)

        self.timerGpsRetry = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.__on_gps_retry, self.timerGpsRetry)

        self.Bind(wx.EVT_CLOSE, self.__on_exit)

        self.status = Statusbar(self, self.log)
        self.status.set_info(title)
        self.SetStatusBar(self.status)

        add_colours()
        self.__create_widgets()
        self.__create_menu()
        self.__create_popup_menu()
        self.__set_control_state(True)
        self.Show()

        displaySize = wx.DisplaySize()
        toolbarSize = self.toolbar.GetBestSize()
        self.SetClientSize((toolbarSize[0] + 10, displaySize[1] / 2))
        self.SetMinSize((displaySize[0] / 4, displaySize[1] / 4))

        self.Connect(-1, -1, EVENT_THREAD, self.__on_event)

        self.SetDropTarget(DropTarget(self))

        self.SetIcon(load_icon('rtlsdr_scan'))

        self.steps = 0
        self.stepsTotal = 0

        self.__start_gps()
        self.__start_location_server()

    def __create_widgets(self):
        self.remoteControl = RemoteControl()

        self.graph = PanelGraph(self, self, self.settings, self.status,
                                self.remoteControl)
        self.toolbar = wx.Panel(self)

        self.buttonStart = MultiButton(self.toolbar, ['Start', 'Continue'],
                                       ['Start new scan', 'Continue scanning'])
        self.buttonStart.SetSelected(self.settings.startOption)
        self.buttonStop = MultiButton(self.toolbar, ['Stop', 'Stop at end'],
                                      ['Stop scan', 'Stop scan at end'])
        self.buttonStop.SetSelected(self.settings.stopOption)
        self.Bind(wx.EVT_BUTTON, self.__on_start, self.buttonStart)
        self.Bind(wx.EVT_BUTTON, self.__on_stop, self.buttonStop)

        textRange = wx.StaticText(self.toolbar,
                                  label="Range (MHz)",
                                  style=wx.ALIGN_CENTER)
        textStart = wx.StaticText(self.toolbar, label="Start")
        textStop = wx.StaticText(self.toolbar, label="Stop")

        self.spinCtrlStart = wx.SpinCtrl(self.toolbar)
        self.spinCtrlStop = wx.SpinCtrl(self.toolbar)
        self.spinCtrlStart.SetToolTipString('Start frequency')
        self.spinCtrlStop.SetToolTipString('Stop frequency')
        self.spinCtrlStart.SetRange(F_MIN, F_MAX - 1)
        self.spinCtrlStop.SetRange(F_MIN + 1, F_MAX)
        self.Bind(wx.EVT_SPINCTRL, self.__on_spin, self.spinCtrlStart)
        self.Bind(wx.EVT_SPINCTRL, self.__on_spin, self.spinCtrlStop)

        textGain = wx.StaticText(self.toolbar, label="Gain (dB)")
        self.controlGain = wx.Choice(self.toolbar, choices=[''])

        textMode = wx.StaticText(self.toolbar, label="Mode")
        self.choiceMode = wx.Choice(self.toolbar, choices=MODE[::2])
        self.choiceMode.SetToolTipString('Scanning mode')

        textDwell = wx.StaticText(self.toolbar, label="Dwell")
        self.choiceDwell = wx.Choice(self.toolbar, choices=DWELL[::2])
        self.choiceDwell.SetToolTipString('Scan time per step')

        textNfft = wx.StaticText(self.toolbar, label="FFT size")
        self.choiceNfft = wx.Choice(self.toolbar, choices=map(str, NFFT))
        self.choiceNfft.SetToolTipString('Higher values for greater'
                                         'precision')

        textDisplay = wx.StaticText(self.toolbar, label="Display")
        self.choiceDisplay = wx.Choice(self.toolbar, choices=DISPLAY[::2])
        self.Bind(wx.EVT_CHOICE, self.__on_choice, self.choiceDisplay)
        self.choiceDisplay.SetToolTipString('Spectrogram available in'
                                            'continuous mode')

        grid = wx.GridBagSizer(5, 5)
        grid.Add(self.buttonStart,
                 pos=(0, 0),
                 span=(3, 1),
                 flag=wx.ALIGN_CENTER)
        grid.Add(self.buttonStop,
                 pos=(0, 1),
                 span=(3, 1),
                 flag=wx.ALIGN_CENTER)
        grid.Add((20, 1), pos=(0, 2))
        grid.Add(textRange, pos=(0, 3), span=(1, 4), flag=wx.ALIGN_CENTER)
        grid.Add(textStart, pos=(1, 3), flag=wx.ALIGN_CENTER)
        grid.Add(self.spinCtrlStart, pos=(1, 4))
        grid.Add(textStop, pos=(1, 5), flag=wx.ALIGN_CENTER)
        grid.Add(self.spinCtrlStop, pos=(1, 6))
        grid.Add(textGain, pos=(0, 7), flag=wx.ALIGN_CENTER)
        grid.Add(self.controlGain, pos=(1, 7), flag=wx.ALIGN_CENTER)
        grid.Add((20, 1), pos=(0, 8))
        grid.Add(textMode, pos=(0, 9), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceMode, pos=(1, 9), flag=wx.ALIGN_CENTER)
        grid.Add(textDwell, pos=(0, 10), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceDwell, pos=(1, 10), flag=wx.ALIGN_CENTER)
        grid.Add(textNfft, pos=(0, 11), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceNfft, pos=(1, 11), flag=wx.ALIGN_CENTER)
        grid.Add((20, 1), pos=(0, 12))
        grid.Add(textDisplay, pos=(0, 13), flag=wx.ALIGN_CENTER)
        grid.Add(self.choiceDisplay, pos=(1, 13), flag=wx.ALIGN_CENTER)

        self.__set_controls()
        self.__set_gain_control()

        self.toolbar.SetSizer(grid)
        self.toolbar.Layout()

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.graph, 1, wx.EXPAND)
        sizer.Add(self.toolbar, 0, wx.EXPAND)
        self.SetSizer(sizer)
        self.Layout()

    def __create_menu(self):
        self.menuMain = MenuMain(self, self.settings)

        self.Bind(wx.EVT_MENU, self.__on_new, self.menuMain.new)
        self.Bind(wx.EVT_MENU, self.__on_open, self.menuMain.open)
        self.Bind(wx.EVT_MENU, self.__on_merge, self.menuMain.merge)
        self.Bind(wx.EVT_MENU_RANGE,
                  self.__on_file_history,
                  id=wx.ID_FILE1,
                  id2=wx.ID_FILE9)
        self.Bind(wx.EVT_MENU, self.__on_save, self.menuMain.save)
        self.Bind(wx.EVT_MENU, self.__on_export_scan, self.menuMain.exportScan)
        self.Bind(wx.EVT_MENU, self.__on_export_image,
                  self.menuMain.exportImage)
        self.Bind(wx.EVT_MENU, self.__on_export_image_seq,
                  self.menuMain.exportSeq)
        self.Bind(wx.EVT_MENU, self.__on_export_geo, self.menuMain.exportGeo)
        self.Bind(wx.EVT_MENU, self.__on_export_track,
                  self.menuMain.exportTrack)
        self.Bind(wx.EVT_MENU, self.__on_page, self.menuMain.page)
        self.Bind(wx.EVT_MENU, self.__on_preview, self.menuMain.preview)
        self.Bind(wx.EVT_MENU, self.__on_print, self.menuMain.printer)
        self.Bind(wx.EVT_MENU, self.__on_properties, self.menuMain.properties)
        self.Bind(wx.EVT_MENU, self.__on_exit, self.menuMain.close)
        self.Bind(wx.EVT_MENU, self.__on_pref, self.menuMain.pref)
        self.Bind(wx.EVT_MENU, self.__on_adv_pref, self.menuMain.advPref)
        self.Bind(wx.EVT_MENU, self.__on_formatting, self.menuMain.formatting)
        self.Bind(wx.EVT_MENU, self.__on_devices_rtl, self.menuMain.devicesRtl)
        self.Bind(wx.EVT_MENU, self.__on_devices_gps, self.menuMain.devicesGps)
        self.Bind(wx.EVT_MENU, self.__on_reset, self.menuMain.reset)
        self.Bind(wx.EVT_MENU, self.__on_clear_select,
                  self.menuMain.clearSelect)
        self.Bind(wx.EVT_MENU, self.__on_show_measure,
                  self.menuMain.showMeasure)
        self.Bind(wx.EVT_MENU, self.__on_start, self.menuMain.start)
        self.Bind(wx.EVT_MENU, self.__on_continue, self.menuMain.cont)
        self.Bind(wx.EVT_MENU, self.__on_stop, self.menuMain.stop)
        self.Bind(wx.EVT_MENU, self.__on_stop_end, self.menuMain.stopEnd)
        self.Bind(wx.EVT_MENU, self.__on_compare, self.menuMain.compare)
        self.Bind(wx.EVT_MENU, self.__on_smooth, self.menuMain.smooth)
        self.Bind(wx.EVT_MENU, self.__on_cal, self.menuMain.cal)
        self.Bind(wx.EVT_MENU, self.__on_gearth, self.menuMain.gearth)
        self.Bind(wx.EVT_MENU, self.__on_gmaps, self.menuMain.gmaps)
        self.Bind(wx.EVT_MENU, self.__on_sats, self.menuMain.sats)
        self.Bind(wx.EVT_MENU, self.__on_loc_clear, self.menuMain.locClear)
        self.Bind(wx.EVT_MENU, self.__on_log, self.menuMain.log)
        self.Bind(wx.EVT_MENU, self.__on_help, self.menuMain.helpLink)
        self.Bind(wx.EVT_MENU, self.__on_update, self.menuMain.update)
        self.Bind(wx.EVT_MENU, self.__on_sys_info, self.menuMain.sys)
        self.Bind(wx.EVT_MENU, self.__on_about, self.menuMain.about)

        idF1 = wx.wx.NewId()
        self.Bind(wx.EVT_MENU, self.__on_help, id=idF1)
        accelTable = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_F1, idF1)])
        self.SetAcceleratorTable(accelTable)

        self.Bind(wx.EVT_MENU_HIGHLIGHT, self.__on_menu_highlight)

        self.SetMenuBar(self.menuMain.menuBar)

    def __create_popup_menu(self):
        self.menuPopup = PopMenuMain(self.settings)

        self.Bind(wx.EVT_MENU, self.__on_start, self.menuPopup.start)
        self.Bind(wx.EVT_MENU, self.__on_continue, self.menuPopup.cont)
        self.Bind(wx.EVT_MENU, self.__on_stop, self.menuPopup.stop)
        self.Bind(wx.EVT_MENU, self.__on_stop_end, self.menuPopup.stopEnd)
        self.Bind(wx.EVT_MENU, self.__on_range_lim, self.menuPopup.rangeLim)
        self.Bind(wx.EVT_MENU, self.__on_points_lim, self.menuPopup.pointsLim)
        self.Bind(wx.EVT_MENU, self.__on_clear_select,
                  self.menuPopup.clearSelect)
        self.Bind(wx.EVT_MENU, self.__on_show_measure,
                  self.menuPopup.showMeasure)

        self.Bind(wx.EVT_CONTEXT_MENU, self.__on_popup_menu)

    def __on_menu_highlight(self, event):
        item = self.GetMenuBar().FindItemById(event.GetId())
        if item is not None:
            help = item.GetHelp()
        else:
            help = ''

        self.status.set_general(help, level=None)

    def __on_popup_menu(self, event):
        if not isinstance(event.GetEventObject(), NavigationToolbar):
            pos = event.GetPosition()
            pos = self.ScreenToClient(pos)
            self.PopupMenu(self.menuPopup.menu, pos)

    def __on_new(self, _event):
        if self.__save_warn(Warn.NEW):
            return True
        self.spectrum.clear()
        self.locations.clear()
        self.__saved(True)
        self.__set_plot(self.spectrum, False)
        self.graph.clear_selection()
        self.__set_control_state(True)
        return False

    def __on_open(self, _event):
        if self.__save_warn(Warn.OPEN):
            return

        dlg = wx.FileDialog(self, "Open a scan", self.settings.dirScans,
                            self.filename,
                            File.get_type_filters(File.Types.SAVE), wx.OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.open(dlg.GetDirectory(), dlg.GetFilename())
        dlg.Destroy()

    def __on_merge(self, _event):
        if self.__save_warn(Warn.MERGE):
            return

        dlg = wx.FileDialog(self, "Merge a scan", self.settings.dirScans,
                            self.filename,
                            File.get_type_filters(File.Types.SAVE), wx.OPEN)
        if dlg.ShowModal() == wx.ID_OK:
            self.__merge(dlg.GetDirectory(), dlg.GetFilename())
        dlg.Destroy()

    def __on_file_history(self, event):
        selection = event.GetId() - wx.ID_FILE1
        path = self.settings.fileHistory.GetHistoryFile(selection)
        self.settings.fileHistory.AddFileToHistory(path)
        dirname, filename = os.path.split(path)
        self.open(dirname, filename)

    def __on_save(self, _event):
        dlg = wx.FileDialog(self, "Save a scan", self.settings.dirScans,
                            self.filename,
                            File.get_type_filters(File.Types.SAVE),
                            wx.SAVE | wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            self.status.set_general("Saving...")
            fileName = dlg.GetFilename()
            dirName = dlg.GetDirectory()
            self.filename = os.path.splitext(fileName)[0]
            self.settings.dirScans = dirName
            fileName = extension_add(fileName, dlg.GetFilterIndex(),
                                     File.Types.SAVE)
            fullName = os.path.join(dirName, fileName)
            save_plot(fullName, self.scanInfo, self.spectrum, self.locations)
            self.__saved(True)
            self.status.set_general("Finished")
            self.settings.fileHistory.AddFileToHistory(fullName)
        dlg.Destroy()

    def __on_export_scan(self, _event):
        dlg = wx.FileDialog(self, "Export a scan", self.settings.dirExport,
                            self.filename, File.get_type_filters(),
                            wx.SAVE | wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            self.status.set_general("Exporting...")
            fileName = dlg.GetFilename()
            dirName = dlg.GetDirectory()
            self.settings.dirExport = dirName
            fileName = extension_add(fileName, dlg.GetFilterIndex(),
                                     File.Types.PLOT)
            fullName = os.path.join(dirName, fileName)
            export_plot(fullName, dlg.GetFilterIndex(), self.spectrum)
            self.status.set_general("Finished")
        dlg.Destroy()

    def __on_export_image(self, _event):
        dlgFile = wx.FileDialog(self, "Export image to file",
                                self.settings.dirExport, self.filename,
                                File.get_type_filters(File.Types.IMAGE),
                                wx.SAVE | wx.OVERWRITE_PROMPT)
        dlgFile.SetFilterIndex(File.ImageType.PNG)
        if dlgFile.ShowModal() == wx.ID_OK:
            dlgImg = DialogImageSize(self, self.settings)
            if dlgImg.ShowModal() != wx.ID_OK:
                dlgFile.Destroy()
                return

            self.status.set_general("Exporting...")
            fileName = dlgFile.GetFilename()
            dirName = dlgFile.GetDirectory()
            self.settings.dirExport = dirName
            fileName = extension_add(fileName, dlgFile.GetFilterIndex(),
                                     File.Types.IMAGE)
            fullName = os.path.join(dirName, fileName)
            exportType = dlgFile.GetFilterIndex()
            export_image(fullName, exportType, self.graph.get_figure(),
                         self.settings)
            self.status.set_general("Finished")
        dlgFile.Destroy()

    def __on_export_image_seq(self, _event):
        dlgSeq = DialogExportSeq(self, self.spectrum, self.settings)
        dlgSeq.ShowModal()
        dlgSeq.Destroy()

    def __on_export_geo(self, _event):
        dlgGeo = DialogExportGeo(self, self.spectrum, self.locations,
                                 self.settings)
        if dlgGeo.ShowModal() == wx.ID_OK:
            self.status.set_general("Exporting...")
            extent = dlgGeo.get_extent()
            dlgFile = wx.FileDialog(self, "Export map to file",
                                    self.settings.dirExport, self.filename,
                                    File.get_type_filters(File.Types.GEO),
                                    wx.SAVE | wx.OVERWRITE_PROMPT)
            dlgFile.SetFilterIndex(File.GeoType.KMZ)
            if dlgFile.ShowModal() == wx.ID_OK:
                fileName = dlgFile.GetFilename()
                dirName = dlgFile.GetDirectory()
                self.settings.dirExport = dirName
                fileName = extension_add(fileName, dlgFile.GetFilterIndex(),
                                         File.Types.GEO)
                fullName = os.path.join(dirName, fileName)
                exportType = dlgFile.GetFilterIndex()
                image = None
                xyz = None
                if exportType == File.GeoType.CSV:
                    xyz = dlgGeo.get_xyz()
                else:
                    image = dlgGeo.get_image()
                export_map(fullName, exportType, extent, image, xyz)
            self.status.set_general("Finished")
            dlgFile.Destroy()
        dlgGeo.Destroy()

    def __on_export_track(self, _event):
        dlg = wx.FileDialog(self, "Export GPS to file",
                            self.settings.dirExport, self.filename,
                            File.get_type_filters(File.Types.TRACK),
                            wx.SAVE | wx.OVERWRITE_PROMPT)
        if dlg.ShowModal() == wx.ID_OK:
            self.status.set_general("Exporting...")
            fileName = dlg.GetFilename()
            dirName = dlg.GetDirectory()
            self.settings.dirExport = dirName
            fileName = extension_add(fileName, dlg.GetFilterIndex(),
                                     File.Types.TRACK)
            fullName = os.path.join(dirName, fileName)
            export_gpx(fullName, self.locations, self.GetName())
            self.status.set_general("Finished")
        dlg.Destroy()

    def __on_page(self, _event):
        dlg = wx.PageSetupDialog(self, self.pageConfig)
        if dlg.ShowModal() == wx.ID_OK:
            self.pageConfig = wx.PageSetupDialogData(
                dlg.GetPageSetupDialogData())
            self.printConfig.SetPrintData(self.pageConfig.GetPrintData())
        dlg.Destroy()

    def __on_preview(self, _event):
        printout = PrintOut(self.graph, self.filename, self.pageConfig)
        printoutPrinting = PrintOut(self.graph, self.filename, self.pageConfig)
        preview = wx.PrintPreview(printout, printoutPrinting, self.printConfig)
        frame = wx.PreviewFrame(preview, self, 'Print Preview')
        frame.Initialize()
        frame.SetSize(self.GetSize())
        frame.Show(True)

    def __on_print(self, _event):
        printer = wx.Printer(self.printConfig)
        printout = PrintOut(self.graph, self.filename, self.pageConfig)
        if printer.Print(self, printout, True):
            self.printConfig = wx.PrintDialogData(printer.GetPrintDialogData())
            self.pageConfig.SetPrintData(self.printConfig.GetPrintData())

    def __on_properties(self, _event):
        if len(self.spectrum) > 0:
            self.scanInfo.timeFirst = min(self.spectrum)
            self.scanInfo.timeLast = max(self.spectrum)

        dlg = DialogProperties(self, self.scanInfo)
        dlg.ShowModal()
        dlg.Destroy()

    def __on_exit(self, _event):
        self.Unbind(wx.EVT_CLOSE)
        if self.__save_warn(Warn.EXIT):
            self.Bind(wx.EVT_CLOSE, self.__on_exit)
            return
        self.__scan_stop(False)
        self.__stop_gps(False)
        self.__stop_location_server()
        self.__get_controls()
        self.settings.devicesRtl = self.devicesRtl
        self.settings.save()
        self.graph.close()
        self.Destroy()

    def __on_pref(self, _event):
        self.__get_controls()
        dlg = DialogPrefs(self, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.graph.create_plot()
            self.__set_control_state(True)
            self.__set_controls()
        dlg.Destroy()

    def __on_adv_pref(self, _event):
        dlg = DialogAdvPrefs(self, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.__set_control_state(True)
        dlg.Destroy()

    def __on_formatting(self, _event):
        dlg = DialogFormatting(self, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.__set_control_state(True)
            self.graph.update_measure()
            self.graph.redraw_plot()
        dlg.Destroy()

    def __on_devices_rtl(self, _event):
        self.__get_controls()
        self.devicesRtl = self.__refresh_devices()
        dlg = DialogDevicesRTL(self, self.devicesRtl, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            self.devicesRtl = dlg.get_devices()
            self.settings.indexRtl = dlg.get_index()
            self.__set_gain_control()
            self.__set_control_state(True)
            self.__set_controls()
        dlg.Destroy()

    def __on_devices_gps(self, _event):
        self.__stop_gps()
        self.status.set_gps('GPS Stopped')
        dlg = DialogDevicesGPS(self, self.settings)
        dlg.ShowModal()
        dlg.Destroy()
        self.__start_gps()

    def __on_reset(self, _event):
        dlg = wx.MessageDialog(
            self, 'Reset all settings to the default values\n'
            '(cannot be undone)?', 'Reset Settings',
            wx.YES_NO | wx.ICON_QUESTION)
        if dlg.ShowModal() == wx.ID_YES:
            self.devicesRtl = []
            self.settings.reset()
            self.__set_controls()
            self.graph.create_plot()
        dlg.Destroy()

    def __on_compare(self, _event):
        dlg = DialogCompare(self, self.settings, self.filename)
        dlg.Show()

    def __on_smooth(self, _event):
        dlg = DialogSmooth(self, self.spectrum, self.settings)
        if dlg.ShowModal() == wx.ID_OK:
            saved = self.isSaved
            self.isSaved = False
            if not self.__on_new(None):
                self.spectrum.clear()
                spectrum = dlg.get_spectrum()
                self.spectrum.update(OrderedDict(sorted(spectrum.items())))
                self.__set_plot(self.spectrum, False)
                self.graph.update_measure()
                self.graph.redraw_plot()
                self.__saved(False)
            else:
                self.__saved(saved)

    def __on_clear_select(self, _event):
        self.graph.clear_selection()

    def __on_show_measure(self, event):
        show = event.Checked()
        self.menuMain.showMeasure.Check(show)
        self.menuPopup.showMeasure.Check(show)
        self.settings.showMeasure = show
        self.graph.show_measure_table(show)
        self.Layout()

    def __on_cal(self, _event):
        self.dlgCal = DialogAutoCal(self, self.settings.calFreq,
                                    self.__auto_cal)
        self.dlgCal.ShowModal()

    def __on_gearth(self, _event):
        tempPath = tempfile.mkdtemp()
        tempFile = os.path.join(tempPath, 'RTLSDRScannerLink.kml')
        handle = open(tempFile, 'wb')
        create_gearth(handle)
        handle.close()

        if not run_file(tempFile):
            wx.MessageBox('Error starting Google Earth', 'Error',
                          wx.OK | wx.ICON_ERROR)

    def __on_gmaps(self, _event):
        url = 'http://localhost:{}/rtlsdr_scan.html'.format(LOCATION_PORT)
        webbrowser.open_new(url)

    def __on_sats(self, _event):
        if self.dlgSats is None:
            self.dlgSats = DialogSats(self)
            self.dlgSats.Show()

    def __on_loc_clear(self, _event):
        result = wx.MessageBox(
            'Remove {} locations from scan?'.format(len(self.locations)),
            'Clear location data', wx.YES_NO, self)
        if result == wx.YES:
            self.locations.clear()
            self.__set_control_state(True)

    def __on_log(self, _event):
        if self.dlgLog is None:
            self.dlgLog = DialogLog(self, self.log)
            self.dlgLog.Show()

    def __on_help(self, _event):
        webbrowser.open("http://eartoearoak.com/software/rtlsdr-scanner")

    def __on_update(self, _event):
        if self.threadUpdate is None:
            self.status.set_general("Checking for updates", level=None)
            self.threadUpdate = Thread(target=self.__update_check)
            self.threadUpdate.start()

    def __on_sys_info(self, _event):
        dlg = DialogSysInfo(self)
        dlg.ShowModal()
        dlg.Destroy()

    def __on_about(self, _event):
        dlg = DialogAbout(self)
        dlg.ShowModal()
        dlg.Destroy()

    def __on_spin(self, event):
        control = event.GetEventObject()
        if control == self.spinCtrlStart:
            self.spinCtrlStop.SetRange(self.spinCtrlStart.GetValue() + 1,
                                       F_MAX)

    def __on_choice(self, _event):
        self.__get_controls()
        self.graph.create_plot()

    def __on_start(self, event):
        self.__get_controls()

        if self.settings.start >= self.settings.stop:
            wx.MessageBox('Stop frequency must be greater that start',
                          'Warning', wx.OK | wx.ICON_WARNING)
            return

        self.devicesRtl = self.__refresh_devices()
        if len(self.devicesRtl) == 0:
            wx.MessageBox('No devices found', 'Error', wx.OK | wx.ICON_ERROR)
        else:
            if event.GetInt() == 0:
                self.isNewScan = True
            else:
                self.isNewScan = False
            self.__scan_start()
            if not self.settings.retainScans:
                self.status.set_info(
                    'Warning: Averaging is enabled in preferences',
                    level=Log.WARN)

    def __on_continue(self, event):
        event.SetInt(1)
        self.__on_start(event)

    def __on_stop(self, event):
        if event.GetInt() == 0:
            self.stopScan = True
            self.stopAtEnd = False
            self.__scan_stop()
        else:
            self.stopScan = False
            self.stopAtEnd = True

    def __on_stop_end(self, _event):
        self.stopAtEnd = True

    def __on_range_lim(self, _event):
        xmin, xmax = self.graph.get_axes().get_xlim()
        xmin = int(xmin)
        xmax = math.ceil(xmax)
        if xmax < xmin + 1:
            xmax = xmin + 1
        self.settings.start = xmin
        self.settings.stop = xmax
        self.__set_controls()

    def __on_points_lim(self, _event):
        self.settings.pointsLimit = self.menuPopup.pointsLim.IsChecked()
        self.__set_plot(self.spectrum, self.settings.annotate)

    def __on_gps_retry(self, _event):
        self.timerGpsRetry.Stop()
        self.__stop_gps()
        self.__start_gps()

    def __on_event(self, event):
        status = event.data.get_status()
        freq = event.data.get_arg1()
        data = event.data.get_arg2()
        if status == Event.STARTING:
            self.status.set_general("Starting")
            self.isScanning = True
        elif status == Event.STEPS:
            self.stepsTotal = (freq + 1) * 2
            self.steps = self.stepsTotal
            self.status.set_progress(0)
            self.status.show_progress()
        elif status == Event.CAL:
            self.__auto_cal(Cal.DONE)
        elif status == Event.INFO:
            if self.threadScan is not None:
                self.sdr = self.threadScan.get_sdr()
                if data is not None:
                    self.devicesRtl[self.settings.indexRtl].tuner = data
                    self.scanInfo.tuner = data
        elif status == Event.DATA:
            self.__saved(False)
            cal = self.devicesRtl[self.settings.indexRtl].calibration
            self.pool.apply_async(
                anaylse_data, (freq, data, cal, self.settings.nfft,
                               self.settings.overlap, self.settings.winFunc),
                callback=self.__on_process_done)
            self.__progress()
        elif status == Event.STOPPED:
            self.__cleanup()
            self.status.set_general("Stopped")
        elif status == Event.FINISHED:
            self.threadScan = None
        elif status == Event.ERROR:
            self.__cleanup()
            self.status.set_general("Error: {}".format(data), level=Log.ERROR)
            if self.dlgCal is not None:
                self.dlgCal.Destroy()
                self.dlgCal = None
        elif status == Event.PROCESSED:
            offset = self.settings.devicesRtl[self.settings.indexRtl].offset
            if self.settings.alert:
                alert = self.settings.alertLevel
            else:
                alert = None
            Thread(target=update_spectrum,
                   name='Update',
                   args=(self, self.lock, self.settings.start,
                         self.settings.stop, freq, data, offset, self.spectrum,
                         not self.settings.retainScans, alert)).start()
        elif status == Event.LEVEL:
            wx.Bell()
        elif status == Event.UPDATED:
            if data and self.settings.liveUpdate:
                self.__set_plot(
                    self.spectrum, self.settings.annotate
                    and self.settings.retainScans
                    and self.settings.mode == Mode.CONTIN)
            self.__progress()
        elif status == Event.DRAW:
            self.graph.draw()
        elif status == Event.VER_UPD:
            self.__update_checked(True, freq, data)
        elif status == Event.VER_NOUPD:
            self.__update_checked(False)
        elif status == Event.VER_UPDFAIL:
            self.__update_checked(failed=True)
        elif status == Event.LOC_WARN:
            self.status.set_gps("{}".format(data), level=Log.WARN)
            self.status.warn_gps()
        elif status == Event.LOC_ERR:
            self.status.set_gps("{}".format(data), level=Log.ERROR)
            self.status.error_gps()
            self.threadLocation = None
            if not self.timerGpsRetry.IsRunning():
                self.timerGpsRetry.Start(5000, True)
        elif status == Event.LOC:
            self.__update_location(data)
        elif status == Event.LOC_SAT:
            if self.dlgSats is not None:
                self.dlgSats.set_sats(data)

        wx.YieldIfNeeded()

    def __on_process_done(self, data):
        timeStamp, freq, scan = data
        post_event(self, EventThread(Event.PROCESSED, freq, (timeStamp, scan)))

    def __auto_cal(self, status):
        freq = self.dlgCal.get_arg1()
        if self.dlgCal is not None:
            if status == Cal.START:
                self.spinCtrlStart.SetValue(int(freq))
                self.spinCtrlStop.SetValue(math.ceil(freq))
                self.oldCal = self.devicesRtl[
                    self.settings.indexRtl].calibration
                self.devicesRtl[self.settings.indexRtl].calibration = 0
                self.__get_controls()
                self.spectrum.clear()
                self.locations.clear()
                if not self.__scan_start(isCal=True):
                    self.dlgCal.reset_cal()
            elif status == Cal.DONE:
                ppm = self.__calc_ppm(freq)
                self.dlgCal.set_cal(ppm)
                self.__set_control_state(True)
            elif status == Cal.OK:
                self.devicesRtl[self.settings.
                                indexRtl].calibration = self.dlgCal.get_cal()
                self.settings.calFreq = freq
                self.dlgCal = None
            elif status == Cal.CANCEL:
                self.dlgCal = None
                if len(self.devicesRtl) > 0:
                    self.devicesRtl[
                        self.settings.indexRtl].calibration = self.oldCal

    def __calc_ppm(self, freq):
        with self.lock:
            timeStamp = max(self.spectrum)
            spectrum = self.spectrum[timeStamp].copy()

            for x, y in spectrum.iteritems():
                spectrum[x] = (((x - freq) * (x - freq)) + 1) * y
                peak = max(spectrum, key=spectrum.get)

        return ((freq - peak) / freq) * 1e6

    def __scan_start(self, isCal=False):
        if self.isNewScan and self.__save_warn(Warn.SCAN):
            return False

        if not self.threadScan:
            self.__set_control_state(False)
            samples = calc_samples(self.settings.dwell)
            if self.isNewScan:
                self.spectrum.clear()
                self.locations.clear()
                self.graph.clear_plots()

                self.isNewScan = False
                self.status.set_info('', level=None)
                self.scanInfo.set_from_settings(self.settings)
                self.scanInfo.time = format_iso_time(time.time())
                self.scanInfo.lat = None
                self.scanInfo.lon = None
                self.scanInfo.desc = ''

            self.stopAtEnd = False
            self.stopScan = False
            self.threadScan = ThreadScan(self, self.sdr, self.settings,
                                         self.settings.indexRtl, samples,
                                         isCal)
            self.filename = "Scan {0:.1f}-{1:.1f}MHz".format(
                self.settings.start, self.settings.stop)
            self.graph.set_plot_title()

            self.__start_gps()

            return True

    def __scan_stop(self, join=True):
        if self.threadScan:
            self.status.set_general("Stopping")
            self.threadScan.abort()
            if join:
                self.threadScan.join()
        self.threadScan = None
        if self.sdr is not None:
            self.sdr.close()
        self.__set_control_state(True)

    def __progress(self):
        if self.steps == self.stepsTotal:
            self.status.set_general("Scanning ({} sweeps)".format(
                len(self.spectrum)))
        self.steps -= 1
        if self.steps > 0 and not self.stopScan:
            self.status.set_progress(
                (self.stepsTotal - self.steps) * 100.0 / (self.stepsTotal - 1))
            self.status.show_progress()
        else:
            self.status.hide_progress()
            self.__set_plot(self.spectrum, self.settings.annotate)
            if self.stopScan:
                self.status.set_general("Stopped")
                self.__cleanup()
            elif self.settings.mode == Mode.SINGLE:
                self.status.set_general("Finished")
                self.__cleanup()
            else:
                if self.settings.mode == Mode.CONTIN:
                    if self.dlgCal is None and not self.stopAtEnd:
                        self.__limit_spectrum()
                        self.__scan_start()
                    else:
                        self.status.set_general("Stopped")
                        self.__cleanup()

    def __cleanup(self):
        if self.sdr is not None:
            self.sdr.close()
            self.sdr = None

        self.status.hide_progress()
        self.steps = 0
        self.threadScan = None
        self.__set_control_state(True)
        self.stopAtEnd = False
        self.stopScan = True
        self.isScanning = False

    def __remove_last(self, data):
        while len(data) >= self.settings.retainMax:
            timeStamp = min(data)
            del data[timeStamp]

    def __limit_spectrum(self):
        with self.lock:
            self.__remove_last(self.spectrum)
            self.__remove_last(self.locations)

    def __start_gps(self):
        if self.settings.gps and len(self.settings.devicesGps):
            self.status.enable_gps()
            if self.threadLocation is None:
                device = self.settings.devicesGps[self.settings.indexGps]
                self.threadLocation = ThreadLocation(self, device)
        else:
            self.status.disable_gps()

    def __stop_gps(self, join=True):
        if self.threadLocation and self.threadLocation.isAlive():
            self.threadLocation.stop()
            if join:
                self.threadLocation.join()
        self.threadLocation = None

    def __start_location_server(self):
        self.serverLocation = LocationServer(self.locations, self.lastLocation,
                                             self.lock, self.log)

    def __stop_location_server(self):
        if self.serverLocation:
            self.serverLocation.close()

    def __update_location(self, data):
        i = 0
        for loc in data:
            self.lastLocation[i] = loc
            i += 1
        self.status.pulse_gps()
        if data[2] is None:
            gpsStatus = '{:.5f}, {:.5f}, {:.1f}'.format(data[0], data[1])
        else:
            gpsStatus = '{:.5f}, {:.5f}, {:.1f}m'.format(
                data[0], data[1], data[2])

        self.status.set_gps(gpsStatus, level=None)

        if not self.isScanning:
            return

        if self.scanInfo is not None:
            if data[0] and data[1]:
                self.scanInfo.lat = str(data[0])
                self.scanInfo.lon = str(data[1])

        with self.lock:
            if len(self.spectrum) > 0:
                self.locations[max(self.spectrum)] = (data[0], data[1],
                                                      data[2])

    def __saved(self, isSaved):
        self.isSaved = isSaved
        title = APP_NAME + " - " + self.filename
        if not isSaved:
            title += "*"
        self.SetTitle(title)

    def __set_plot(self, spectrum, annotate):
        if len(spectrum) > 0:
            total = count_points(spectrum)
            if total > 0:
                spectrum = sort_spectrum(spectrum)
                extent = Extent(spectrum)
                self.graph.set_plot(spectrum, self.settings.pointsLimit,
                                    self.settings.pointsMax, extent, annotate)
        else:
            self.graph.clear_plots()

    def __set_control_state(self, state):
        hasDevices = len(self.devicesRtl) > 0

        self.spinCtrlStart.Enable(state)
        self.spinCtrlStop.Enable(state)
        self.controlGain.Enable(state)
        self.choiceMode.Enable(state)
        self.choiceDwell.Enable(state)
        self.choiceNfft.Enable(state)
        self.buttonStart.Enable(state and hasDevices)
        self.buttonStop.Enable(not state and hasDevices)

        self.menuMain.set_state(state, self.spectrum, self.locations)
        self.menuPopup.set_state(state, self.spectrum)

    def __set_controls(self):
        self.spinCtrlStart.SetValue(self.settings.start)
        self.spinCtrlStop.SetValue(self.settings.stop)
        self.choiceMode.SetSelection(MODE[1::2].index(self.settings.mode))
        dwell = calc_real_dwell(self.settings.dwell)
        try:
            sel = DWELL[1::2].index(dwell)
        except ValueError:
            sel = DWELL[1::2][len(DWELL) / 4]
        self.choiceDwell.SetSelection(sel)
        self.choiceNfft.SetSelection(NFFT.index(self.settings.nfft))
        self.choiceDisplay.SetSelection(DISPLAY[1::2].index(
            self.settings.display))

    def __set_gain_control(self):
        grid = self.controlGain.GetContainingSizer()
        if len(self.devicesRtl) > 0:
            self.controlGain.Destroy()
            device = self.devicesRtl[self.settings.indexRtl]
            if device.isDevice:
                gains = device.get_gains_str()
                self.controlGain = wx.Choice(self.toolbar, choices=gains)
                gain = device.get_closest_gain_str(device.gain)
                self.controlGain.SetStringSelection(gain)
            else:
                self.controlGain = NumCtrl(self.toolbar,
                                           integerWidth=3,
                                           fractionWidth=1)
                font = self.controlGain.GetFont()
                dc = wx.WindowDC(self.controlGain)
                dc.SetFont(font)
                size = dc.GetTextExtent('####.#')
                self.controlGain.SetMinSize((size[0] * 1.2, -1))
                self.controlGain.SetValue(device.gain)

            grid.Add(self.controlGain, pos=(1, 7), flag=wx.ALIGN_CENTER)
            grid.Layout()

    def __get_controls(self):
        self.settings.start = self.spinCtrlStart.GetValue()
        self.settings.stop = self.spinCtrlStop.GetValue()
        self.settings.startOption = self.buttonStart.GetSelected()
        self.settings.stopOption = self.buttonStop.GetSelected()
        self.settings.mode = MODE[1::2][self.choiceMode.GetSelection()]
        self.settings.dwell = DWELL[1::2][self.choiceDwell.GetSelection()]
        self.settings.nfft = NFFT[self.choiceNfft.GetSelection()]
        self.settings.display = DISPLAY[1::2][
            self.choiceDisplay.GetSelection()]

        if len(self.devicesRtl) > 0:
            device = self.devicesRtl[self.settings.indexRtl]
            try:
                if device.isDevice:
                    device.gain = float(self.controlGain.GetStringSelection())
                else:
                    device.gain = self.controlGain.GetValue()
            except ValueError:
                device.gain = 0

    def __save_warn(self, warnType):
        if self.settings.saveWarn and not self.isSaved:
            dlg = DialogSaveWarn(self, warnType)
            code = dlg.ShowModal()
            if code == wx.ID_YES:
                self.__on_save(None)
                if self.isSaved:
                    return False
                else:
                    return True
            elif code == wx.ID_NO:
                return False
            else:
                return True

        return False

    def __update_check(self):
        local = get_version_timestamp(True)
        try:
            remote = get_version_timestamp_repo()
        except IOError:
            post_event(self, EventThread(Event.VER_UPDFAIL))
            return

        if remote > local:
            post_event(self, EventThread(Event.VER_UPD, local, remote))
        else:
            post_event(self, EventThread(Event.VER_NOUPD))

    def __update_checked(self,
                         updateFound=False,
                         local=None,
                         remote=None,
                         failed=False):
        self.threadUpdate = None
        self.status.set_general("", level=None)
        if failed:
            icon = wx.ICON_ERROR
            message = "Update check failed"
        else:
            icon = wx.ICON_INFORMATION
            if updateFound:
                message = "Update found\n\n"
                message += "Local: " + time.strftime('%c',
                                                     time.localtime(local))
                message += "\nRemote: " + time.strftime(
                    '%c', time.localtime(remote))
            else:
                message = "No updates found"

        dlg = wx.MessageDialog(self, message, "Update", wx.OK | icon)
        dlg.ShowModal()
        dlg.Destroy()

    def __refresh_devices(self):
        self.settings.devicesRtl = get_devices_rtl(self.devicesRtl,
                                                   self.status)
        self.settings.indexRtl = limit(self.settings.indexRtl, 0,
                                       len(self.devicesRtl) - 1)
        self.settings.save()
        return self.settings.devicesRtl

    def __merge(self, dirname, filename):
        if not os.path.exists(os.path.join(dirname, filename)):
            wx.MessageBox('File not found', 'Error', wx.OK | wx.ICON_ERROR)
            return

        self.filename = os.path.splitext(filename)[0]
        self.settings.dirScans = dirname
        self.status.set_general("Merging: {}".format(filename))

        _scanInfo, spectrum, locations = open_plot(dirname, filename)

        if len(spectrum) > 0:
            spectrum.update(self.spectrum)
            locations.update(self.locations)
            self.spectrum.clear()
            self.locations.clear()
            self.spectrum.update(OrderedDict(sorted(spectrum.items())))
            self.locations.update(OrderedDict(sorted(locations.items())))
            self.__set_plot(self.spectrum, self.settings.annotate)
            self.graph.scale_plot(True)
            self.status.set_general("Finished")
            self.settings.fileHistory.AddFileToHistory(
                os.path.join(dirname, filename))
        else:
            self.status.set_general("Merge failed", level=Log.ERROR)

    def open(self, dirname, filename):
        if not os.path.exists(os.path.join(dirname, filename)):
            wx.MessageBox('File not found', 'Error', wx.OK | wx.ICON_ERROR)
            return

        self.__on_new(None)
        self.graph.get_canvas().draw()

        self.filename = os.path.splitext(filename)[0]
        self.settings.dirScans = dirname
        self.status.set_general("Opening: {}".format(filename))

        self.scanInfo, spectrum, location = open_plot(dirname, filename)

        if len(spectrum) > 0:
            self.scanInfo.set_to_settings(self.settings)
            self.spectrum = spectrum
            self.locations.clear()
            self.locations.update(location)
            self.__saved(True)
            self.__set_controls()
            self.__set_control_state(True)
            self.__set_plot(spectrum, self.settings.annotate)
            self.graph.scale_plot(True)
            self.status.set_general("Finished")
            self.settings.fileHistory.AddFileToHistory(
                os.path.join(dirname, filename))
        else:
            self.status.set_general("Open failed", level=Log.ERROR)
 def __start_location_server(self):
     self.serverLocation = LocationServer(self.locations, self.lastLocation,
                                          self.lock, self.log)