def __init__( self, parent, id = wx.ID_ANY ): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour( wx.WHITE ) self.refreshInputUpdateNonBusy = NonBusyCall( self.refreshInputUpdate, min_millis=1000, max_millis=3000 ) fontPixels = 50 font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) splitter.Bind( wx.EVT_PAINT, self.onPaint ) panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) panel.SetDoubleBuffered( True ) panel.SetSizer( horizontalMainSizer ) panel.SetBackgroundColour( wx.WHITE ) #------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.notebook = wx.Notebook( panel, style=wx.NB_BOTTOM ) self.notebook.SetBackgroundColour( wx.WHITE ) self.keypad = Keypad( self.notebook, self ) self.timeTrialRecord = TimeTrialRecord( self.notebook, self ) self.notebook.AddPage( self.keypad, _("Bib"), select=True ) self.notebook.AddPage( self.timeTrialRecord, _("TimeTrial") ) horizontalMainSizer.Add( self.notebook, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) self.horizontalMainSizer = horizontalMainSizer #------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText( panel, label = u'0:00') self.raceTime.SetFont( font ) self.raceTime.SetDoubleBuffered(True) verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer.Add( verticalSubSizer ) hs = wx.BoxSizer( wx.HORIZONTAL ) hs.Add( self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100-40-8 ) verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border = 2 ) #------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) fontBold = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) rowCur = 0 colCur = 0 rowCur += 1 label = wx.StaticText( panel, label = _("Manual Start")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.raceStartMessage = label self.raceStartTime = wx.StaticText( panel ) self.raceStartTime.SetFont( font ) gbs.Add( self.raceStartTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) gbs.Add( line, pos=(rowCur, colCur), span=(1,2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = u'{}:'.format(_("Est. Last Rider")) ) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTime = wx.StaticText( panel ) self.lastRiderOnCourseTime.SetFont( font ) gbs.Add( self.lastRiderOnCourseTime, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseName = wx.StaticText( panel ) self.lastRiderOnCourseName.SetFont( fontBold ) gbs.Add( self.lastRiderOnCourseName, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTeam = wx.StaticText( panel ) self.lastRiderOnCourseTeam.SetFont( font ) gbs.Add( self.lastRiderOnCourseTeam, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseCategory = wx.StaticText( panel ) self.lastRiderOnCourseCategory.SetFont( font ) gbs.Add( self.lastRiderOnCourseCategory, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) self.photoCount = wx.StaticText( panel, label = u"000004" ) self.photoCount.SetFont( font ) self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT|wx.ALIGN_RIGHT, border = 6 ) self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton( panel, bitmap = self.camera_bitmap ) self.camera_tooltip = wx.ToolTip( _('Show Last Photos...') ) self.photoButton.SetToolTip( self.camera_tooltip ) self.photoButton.Bind( wx.EVT_BUTTON, self.onPhotoButton ) self.hbClockPhoto.Add( self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) rowCur += 1 self.clock = ClockDigital( panel, size=(100,24), checkFunc=self.doClockUpdate ) self.clock.SetBackgroundColour( wx.WHITE ) gbs.Add( self.clock, pos=(rowCur, 0), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT ) rowCur += 1 verticalSubSizer.Add( gbs, flag=wx.LEFT|wx.TOP, border = 8 ) #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) rcVertical.AddSpacer( 32 ) title = wx.StaticText( panel, label = _('Riders on Course:') ) title.SetFont( wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) rcVertical.Add( title, flag=wx.ALL, border = 4 ) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) self.lapCountList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) self.lapCountList.InsertColumn( 0, _('Category'), wx.LIST_FORMAT_LEFT, 140 ) self.lapCountList.InsertColumn( 1, _('Count'), wx.LIST_FORMAT_RIGHT, 70 ) self.lapCountList.InsertColumn( 2, u'', wx.LIST_FORMAT_LEFT, 90 ) rcVertical.Add( self.lapCountList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) #---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN, lapInfoFunc=getLapInfo ) splitter.SetMinimumPaneSize( 20 ) splitter.SplitHorizontally( panel, self.raceHUD, -100 ) verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) self.SetSizer( verticalMainSizer ) self.isEnabled = True self.splitter = splitter self.firstTimeDraw = True self.refreshRaceTime()
class NumKeypad( wx.Panel ): def __init__( self, parent, id = wx.ID_ANY ): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour( wx.WHITE ) self.refreshInputUpdateNonBusy = NonBusyCall( self.refreshInputUpdate, min_millis=1000, max_millis=3000 ) fontPixels = 50 font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) splitter.Bind( wx.EVT_PAINT, self.onPaint ) panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) panel.SetDoubleBuffered( True ) panel.SetSizer( horizontalMainSizer ) panel.SetBackgroundColour( wx.WHITE ) #------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.notebook = wx.Notebook( panel, style=wx.NB_BOTTOM ) self.notebook.SetBackgroundColour( wx.WHITE ) self.keypad = Keypad( self.notebook, self ) self.timeTrialRecord = TimeTrialRecord( self.notebook, self ) self.notebook.AddPage( self.keypad, _("Bib"), select=True ) self.notebook.AddPage( self.timeTrialRecord, _("TimeTrial") ) horizontalMainSizer.Add( self.notebook, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) self.horizontalMainSizer = horizontalMainSizer #------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText( panel, label = u'0:00') self.raceTime.SetFont( font ) self.raceTime.SetDoubleBuffered(True) verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer.Add( verticalSubSizer ) hs = wx.BoxSizer( wx.HORIZONTAL ) hs.Add( self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100-40-8 ) verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border = 2 ) #------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) fontBold = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) rowCur = 0 colCur = 0 rowCur += 1 label = wx.StaticText( panel, label = _("Manual Start")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.raceStartMessage = label self.raceStartTime = wx.StaticText( panel ) self.raceStartTime.SetFont( font ) gbs.Add( self.raceStartTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) gbs.Add( line, pos=(rowCur, colCur), span=(1,2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = u'{}:'.format(_("Est. Last Rider")) ) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTime = wx.StaticText( panel ) self.lastRiderOnCourseTime.SetFont( font ) gbs.Add( self.lastRiderOnCourseTime, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseName = wx.StaticText( panel ) self.lastRiderOnCourseName.SetFont( fontBold ) gbs.Add( self.lastRiderOnCourseName, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTeam = wx.StaticText( panel ) self.lastRiderOnCourseTeam.SetFont( font ) gbs.Add( self.lastRiderOnCourseTeam, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseCategory = wx.StaticText( panel ) self.lastRiderOnCourseCategory.SetFont( font ) gbs.Add( self.lastRiderOnCourseCategory, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) self.photoCount = wx.StaticText( panel, label = u"000004" ) self.photoCount.SetFont( font ) self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT|wx.ALIGN_RIGHT, border = 6 ) self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton( panel, bitmap = self.camera_bitmap ) self.camera_tooltip = wx.ToolTip( _('Show Last Photos...') ) self.photoButton.SetToolTip( self.camera_tooltip ) self.photoButton.Bind( wx.EVT_BUTTON, self.onPhotoButton ) self.hbClockPhoto.Add( self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) rowCur += 1 self.clock = ClockDigital( panel, size=(100,24), checkFunc=self.doClockUpdate ) self.clock.SetBackgroundColour( wx.WHITE ) gbs.Add( self.clock, pos=(rowCur, 0), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT ) rowCur += 1 verticalSubSizer.Add( gbs, flag=wx.LEFT|wx.TOP, border = 8 ) #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) rcVertical.AddSpacer( 32 ) title = wx.StaticText( panel, label = _('Riders on Course:') ) title.SetFont( wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) rcVertical.Add( title, flag=wx.ALL, border = 4 ) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) self.lapCountList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) self.lapCountList.InsertColumn( 0, _('Category'), wx.LIST_FORMAT_LEFT, 140 ) self.lapCountList.InsertColumn( 1, _('Count'), wx.LIST_FORMAT_RIGHT, 70 ) self.lapCountList.InsertColumn( 2, u'', wx.LIST_FORMAT_LEFT, 90 ) rcVertical.Add( self.lapCountList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) #---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN, lapInfoFunc=getLapInfo ) splitter.SetMinimumPaneSize( 20 ) splitter.SplitHorizontally( panel, self.raceHUD, -100 ) verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) self.SetSizer( verticalMainSizer ) self.isEnabled = True self.splitter = splitter self.firstTimeDraw = True self.refreshRaceTime() def doClockUpdate( self ): mainWin = Utils.getMainWin() return not mainWin or mainWin.isShowingPage(self) def isKeypadInputMode( self ): return self.notebook.GetSelection() == 0 def isTimeTrialInputMode( self ): return self.notebook.GetSelection() == 1 def setTimeTrialInput( self, isTimeTrial=True ): page = 1 if isTimeTrial else 0 if self.notebook.GetSelection() != page: self.notebook.SetSelection( page ) self.timeTrialRecord.refresh() def swapKeypadTimeTrialRecord( self ): self.notebook.SetSelection( 1 - self.notebook.GetSelection() ) def refreshRaceHUD( self ): race = Model.race if not race or race.isTimeTrial: self.raceHUD.SetData() if Utils.mainWin: Utils.mainWin.updateLapCounter() return categories = race.getCategories( startWaveOnly=True ) noLap = u'' tCur = race.curRaceTime() if race.isRunning() else None def getNoDataCategoryLap( category ): offset = race.categoryStartOffset(category) tLapStart = offset if tCur and tCur >= offset else None cn = race.getNumLapsFromCategory( category ) if cn and tCur and tCur > offset + 30.0: cn -= 1 return (u'{}'.format(cn) if cn else noLap, False, tLapStart) lapCounter = [getNoDataCategoryLap(category) for category in categories] categoryToLapCounterIndex = {category:i for i, category in enumerate(categories)} results = GetResults( None ) if tCur is None or not results: self.raceHUD.SetData() if Utils.mainWin: Utils.mainWin.updateLapCounter(lapCounter) return Finisher = Model.Rider.Finisher raceTimes = [] leader = [] categoryRaceTimes = {} categories_seen = set() getCategory = race.getCategory leaderCategory = None secondsBeforeLeaderToFlipLapCounter = race.secondsBeforeLeaderToFlipLapCounter + 1.0 def setLapCounter( leaderCategory, category, lapCur, lapMax, tLeaderArrival=sys.float_info.max, tLapStart=None ): if not category: return if not(category == leaderCategory or race.getNumLapsFromCategory(category)): return lapsToGo = max( 0, lapMax - lapCur ) if secondsBeforeLeaderToFlipLapCounter < tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter+5.0: v = (u'{}'.format(lapsToGo), True, tLapStart) # Flash current lap (about to be flipped). elif 0.0 <= tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter: v = (u'{}'.format(max(0,lapsToGo-1)), False, tLapStart) # Flip lap counter before leader. else: v = (u'{}'.format(lapsToGo), False, tLapStart) # Show current lap. try: lapCounter[categoryToLapCounterIndex[category]] = v except (KeyError, IndexError): pass for rr in results: if rr.status != Finisher or not rr.raceTimes: continue category = getCategory( rr.num ) if category in categories_seen: # This is not the leader if we have seen this category before. # Update the red lantern time. newRaceTimes = categoryRaceTimes[category] if rr.raceTimes[-1] > newRaceTimes[-1]: newRaceTimes[-1] = rr.raceTimes[-1] continue if not leaderCategory: leaderCategory = category categories_seen.add( category ) leader.append( u'{} {}'.format(category.fullname if category else u'<{}>'.format(_('Missing')), rr.num) ) # Add a copy of the race times. Append the leader's last time as the current red lantern. raceTimes.append( rr.raceTimes + [rr.raceTimes[-1]] ) categoryRaceTimes[category] = raceTimes[-1] # Find the next expected lap arrival. try: lapCur = bisect.bisect_left( rr.raceTimes, tCur ) # Time before leader's arrival. tLeaderArrival = rr.raceTimes[lapCur] - tCur except IndexError: # At the end of the race, use the leader's race time. # Make sure it is a recorded time, not a projected time. try: tLapStart = rr.raceTimes[-2] if rr.interp[-1] else rr.raceTimes[-1] except: tLapStart = None setLapCounter( leaderCategory, category, len(rr.raceTimes)-1, len(rr.raceTimes)-1, tLapStart = tLapStart ) continue if lapCur <= 1: tLapStart = race.categoryStartOffset(category) else: lapPrev = lapCur-1 # Make sure we use an actual recorded time - not a projected time. # A projected time is possible if the leader has a slow lap. if rr.interp[lapPrev]: lapPrev -= 1 try: tLapStart = rr.raceTimes[lapPrev] if lapPrev else race.categoryStartOffset(category) except IndexError: tLapStart = None setLapCounter( leaderCategory, category, lapCur, len(rr.raceTimes), tLeaderArrival, tLapStart ) if tLeaderArrival is not None: if 0.0 <= tLeaderArrival <= 3.0: if category not in self.lapReminder: self.lapReminder[category] = Utils.PlaySound( 'reminder.wav' ) elif category in self.lapReminder: del self.lapReminder[category] self.raceHUD.SetData( raceTimes, leader, tCur if race.isRunning() else None ) if Utils.mainWin: Utils.mainWin.updateLapCounter( lapCounter ) def refreshRaceTime( self ): race = Model.race if race is not None: tRace = race.lastRaceTime() tStr = Utils.formatTime( tRace ) if tStr.startswith('0'): tStr = tStr[1:] self.refreshRaceHUD() if race.enableUSBCamera: self.photoButton.Show( True ) self.photoCount.SetLabel( u'{}'.format(race.photoCount) ) if Utils.cameraError: self.photoButton.SetBitmapLabel( self.camera_broken_bitmap ) self.photoButton.SetToolTip( wx.ToolTip(Utils.cameraError) ) else: self.photoButton.SetBitmapLabel( self.camera_bitmap ) self.photoButton.SetToolTip( self.camera_tooltip ) else: self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) else: tStr = '' tRace = None self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) self.raceTime.SetLabel( ' ' + tStr ) self.hbClockPhoto.Layout() mainWin = Utils.mainWin if mainWin is not None: try: mainWin.refreshRaceAnimation() except: pass def onPhotoButton( self, event ): if not Utils.mainWin: return Utils.mainWin.photoDialog.Show( True ) Utils.mainWin.photoDialog.refresh( Utils.mainWin.photoDialog.ShowAllPhotos ) raceMessage = { 0:_("Finishers Arriving"), 1:_("Ring Bell"), 2:_("Prepare Bell") } def refreshLaps( self ): wx.CallAfter( self.refreshRaceHUD ) def refreshRiderLapCountList( self ): self.lapCountList.DeleteAllItems() race = Model.race if not race or not race.isRunning(): return Finisher = Model.Rider.Finisher NP = Model.Rider.NP getCategory = race.getCategory t = race.curRaceTime() results = GetResults( None ) if race.isTimeTrial: # Add TT riders who have started but not recoreded a lap yet. results = copy.deepcopy(list(results)) for rr in results: a = race.riders[rr.num] if rr.status == NP and a.firstTime is not None and a.firstTime <= t: rr.status = Finisher elif race.enableJChipIntegration and race.resetStartClockOnFirstTag and len(results) != len(race.riders): # Add rider entries who have been read by RFID but have not completed the first lap. results = list(results) resultNums = set( rr.num for rr in results ) for a in race.riders.values(): if a.status == Finisher and a.num not in resultNums and a.firstTime is not None: category = getCategory( a.num ) if category and t >= a.firstTime and t >= race.getStartOffset(a.num): results.append( # num, status, lastTime, raceCat, lapTimes, raceTimes, interp RiderResult( a.num, Finisher, a.firstTime, category.fullname, [], [], [] ) ) results = [rr for rr in results if rr.status == Finisher] if not results: return catLapCount = defaultdict(int) catCount = defaultdict(int) catRaceCount = defaultdict(int) catLapsMax = defaultdict(int) for rr in results: category = getCategory( rr.num ) catLapsMax[category] = max( catLapsMax[category], race.getNumLapsFromCategory(category) or 1, len(rr.raceTimes)-1 ) for rr in results: category = getCategory( rr.num ) catCount[getCategory(rr.num)] += 1 numLaps = catLapsMax[category] tSearch = t if race.isTimeTrial: try: tSearch -= race.riders[rr.num].firstTime except: pass lap = max( 1, bisect.bisect_left(rr.raceTimes, tSearch) ) if lap <= numLaps: # Rider is still on course. key = (category, lap) catLapCount[key] += 1 catRaceCount[category] += 1 if not catLapCount: return catLapList = [(category, lap, count) for (category, lap), count in catLapCount.items()] catLapList.sort( key=lambda x: (x[0].getStartOffsetSecs(), x[0].fullname, -x[1]) ) def appendListRow( row = tuple(), colour = None, bold = None ): r = self.lapCountList.InsertItem( 999999, u'{}'.format(row[0]) if row else u'' ) for c in range(1, len(row)): self.lapCountList.SetItem( r, c, u'{}'.format(row[c]) ) if colour is not None: item = self.lapCountList.GetItem( r ) item.SetTextColour( colour ) self.lapCountList.SetItem( item ) if bold is not None: item = self.lapCountList.GetItem( r ) font = self.lapCountList.GetFont() font.SetWeight( wx.FONTWEIGHT_BOLD ) item.SetFont( font ) self.lapCountList.SetItem( item ) return r appendListRow( ( _('Total'), u'{}/{}'.format(sum(count for count in catLapCount.values()), sum(count for count in catCount.values())) ), colour=wx.BLUE, bold=True ) lastCategory = None for category, lap, count in catLapList: categoryLaps = catLapsMax[category] if category != lastCategory: appendListRow( (category.fullname, u'{}/{}'.format(catRaceCount[category], catCount[category]), (u'({} {})'.format(categoryLaps if categoryLaps < 1000 else u'', _('laps') if categoryLaps > 1 else _('lap'))) ), bold = True ) appendListRow( (u'', count, u'{} {}'.format( _('on lap'), lap ) ) ) lastCategory = category def refreshLastRiderOnCourse( self ): race = Model.race lastRiderOnCourse = GetLastRider( None ) changed = False if lastRiderOnCourse: maxLength = 24 rider = race.riders[lastRiderOnCourse.num] short_name = lastRiderOnCourse.short_name(maxLength) if short_name: lastRiderOnCourseName = u'{}: {}'.format(lastRiderOnCourse.num, lastRiderOnCourse.short_name()) else: lastRiderOnCourseName = u'{}'.format(lastRiderOnCourse.num) lastRiderOnCourseTeam = u'{}'.format( getattr(lastRiderOnCourse, 'Team', u'') ) if len(lastRiderOnCourseTeam) > maxLength: lastRiderOnCourseTeam = lastRiderOnCourseTeam[:maxLength].strip() + u'...' category = race.getCategory( lastRiderOnCourse.num ) lastRiderOnCourseCategory = category.fullname t = (lastRiderOnCourse._lastTimeOrig or 0.0) + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) tFinish = race.startTime + datetime.timedelta( seconds=t ) lastRiderOnCourseTime = u'{} {}'.format(_('Finishing at'), tFinish.strftime('%H:%M:%S') ) else: lastRiderOnCourseName = u'' lastRiderOnCourseTeam = u'' lastRiderOnCourseCategory = u'' lastRiderOnCourseTime = u'' changed |= SetLabel( self.lastRiderOnCourseName, lastRiderOnCourseName ) changed |= SetLabel( self.lastRiderOnCourseTeam, lastRiderOnCourseTeam ) changed |= SetLabel( self.lastRiderOnCourseCategory, lastRiderOnCourseCategory ) changed |= SetLabel( self.lastRiderOnCourseTime, lastRiderOnCourseTime ) if changed: Utils.LayoutChildResize( self.raceStartTime ) def refreshAll( self ): self.refreshRaceTime() self.refreshLaps() def commit( self ): pass def onPaint( self, event ): if self.firstTimeDraw: self.firstTimeDraw = False self.splitter.SetSashPosition( SplitterMinPos ) event.Skip() def refreshInputUpdate( self ): self.refreshLaps() self.refreshRiderLapCountList() self.refreshLastRiderOnCourse() def refresh( self ): self.clock.Start() race = Model.race enable = bool(race and race.isRunning()) if self.isEnabled != enable: self.isEnabled = enable if not enable and self.isKeypadInputMode(): self.keypad.numEdit.SetValue( '' ) self.photoCount.Show( bool(race and race.enableUSBCamera) ) self.photoButton.Show( bool(race and race.enableUSBCamera) ) # Refresh the race start time. changed = False rst, rstSource = '', '' if race and race.startTime: st = race.startTime if race.enableJChipIntegration and race.resetStartClockOnFirstTag: if race.firstRecordedTime: rstSource = _('Chip Start') else: rstSource = _('Waiting...') else: rstSource = _('Manual Start') rst = '{:02d}:{:02d}:{:02d}.{:02d}'.format(st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) changed |= SetLabel( self.raceStartMessage, rstSource ) changed |= SetLabel( self.raceStartTime, rst ) self.refreshInputUpdateNonBusy() if self.isKeypadInputMode(): wx.CallLater( 100, self.keypad.numEdit.SetFocus )
class NumKeypad( wx.Panel ): def __init__( self, parent, id = wx.ID_ANY ): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour( wx.WHITE ) self.refreshInputUpdateNonBusy = NonBusyCall( self.refreshInputUpdate, min_millis=1000, max_millis=3000 ) fontPixels = 50 font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) splitter.Bind( wx.EVT_PAINT, self.onPaint ) panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) panel.SetDoubleBuffered( True ) panel.SetSizer( horizontalMainSizer ) panel.SetBackgroundColour( wx.WHITE ) #------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.notebook = wx.Notebook( panel, style=wx.NB_BOTTOM ) self.notebook.SetBackgroundColour( wx.WHITE ) self.keypad = Keypad( self.notebook, self ) self.timeTrialRecord = TimeTrialRecord( self.notebook, self ) self.notebook.AddPage( self.keypad, _("Bib"), select=True ) self.notebook.AddPage( self.timeTrialRecord, _("TimeTrial") ) horizontalMainSizer.Add( self.notebook, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) self.horizontalMainSizer = horizontalMainSizer #------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText( panel, label = u'0:00') self.raceTime.SetFont( font ) self.raceTime.SetDoubleBuffered(True) verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer.Add( verticalSubSizer ) hs = wx.BoxSizer( wx.HORIZONTAL ) hs.Add( self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100-40-8 ) verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border = 2 ) #------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) fontBold = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) rowCur = 0 colCur = 0 rowCur += 1 label = wx.StaticText( panel, label = _("Manual Start")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.raceStartMessage = label self.raceStartTime = wx.StaticText( panel ) self.raceStartTime.SetFont( font ) gbs.Add( self.raceStartTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) gbs.Add( line, pos=(rowCur, colCur), span=(1,2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = u'{}:'.format(_("Est. Last Rider")) ) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTime = wx.StaticText( panel ) self.lastRiderOnCourseTime.SetFont( font ) gbs.Add( self.lastRiderOnCourseTime, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseName = wx.StaticText( panel ) self.lastRiderOnCourseName.SetFont( fontBold ) gbs.Add( self.lastRiderOnCourseName, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTeam = wx.StaticText( panel ) self.lastRiderOnCourseTeam.SetFont( font ) gbs.Add( self.lastRiderOnCourseTeam, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseCategory = wx.StaticText( panel ) self.lastRiderOnCourseCategory.SetFont( font ) gbs.Add( self.lastRiderOnCourseCategory, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) self.photoCount = wx.StaticText( panel, label = u"000004" ) self.photoCount.SetFont( font ) self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT|wx.ALIGN_RIGHT, border = 6 ) self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton( panel, bitmap = self.camera_bitmap ) self.camera_tooltip = wx.ToolTip( _('Show Last Photos...') ) self.photoButton.SetToolTip( self.camera_tooltip ) self.photoButton.Bind( wx.EVT_BUTTON, self.onPhotoButton ) self.hbClockPhoto.Add( self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) rowCur += 1 self.clock = ClockDigital( panel, size=(100,24), checkFunc=self.doClockUpdate ) self.clock.SetBackgroundColour( wx.WHITE ) gbs.Add( self.clock, pos=(rowCur, 0), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT ) rowCur += 1 verticalSubSizer.Add( gbs, flag=wx.LEFT|wx.TOP, border = 8 ) #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) rcVertical.AddSpacer( 32 ) title = wx.StaticText( panel, label = _('Riders on Course:') ) title.SetFont( wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) rcVertical.Add( title, flag=wx.ALL, border = 4 ) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) self.lapCountList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) self.lapCountList.InsertColumn( 0, _('Category'), wx.LIST_FORMAT_LEFT, 140 ) self.lapCountList.InsertColumn( 1, _('Count'), wx.LIST_FORMAT_RIGHT, 70 ) self.lapCountList.InsertColumn( 2, u'', wx.LIST_FORMAT_LEFT, 90 ) rcVertical.Add( self.lapCountList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) #---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN, lapInfoFunc=getLapInfo ) splitter.SetMinimumPaneSize( 20 ) splitter.SplitHorizontally( panel, self.raceHUD, -100 ) verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) self.SetSizer( verticalMainSizer ) self.isEnabled = True self.splitter = splitter self.firstTimeDraw = True self.refreshRaceTime() def doClockUpdate( self ): mainWin = Utils.getMainWin() return not mainWin or mainWin.isShowingPage(self) def isKeypadInputMode( self ): return self.notebook.GetSelection() == 0 def isTimeTrialInputMode( self ): return self.notebook.GetSelection() == 1 def setTimeTrialInput( self, isTimeTrial=True ): page = 1 if isTimeTrial else 0 if self.notebook.GetSelection() != page: self.notebook.SetSelection( page ) self.timeTrialRecord.refresh() def swapKeypadTimeTrialRecord( self ): self.notebook.SetSelection( 1 - self.notebook.GetSelection() ) def refreshRaceHUD( self ): race = Model.race if not race or race.isTimeTrial: self.raceHUD.SetData() if Utils.mainWin: Utils.mainWin.updateLapCounter() return categories = race.getCategories( startWaveOnly=True ) noLap = u'' tCur = race.curRaceTime() if race.isRunning() else None def getNoDataCategoryLap( category ): offset = race.categoryStartOffset(category) tLapStart = offset if tCur and tCur >= offset else None cn = race.getNumLapsFromCategory( category ) if cn and tCur and tCur > offset + 30.0: cn -= 1 return (u'{}'.format(cn) if cn else noLap, False, tLapStart) lapCounter = [getNoDataCategoryLap(category) for category in categories] categoryToLapCounterIndex = {category:i for i, category in enumerate(categories)} results = GetResults( None ) if tCur is None or not results: self.raceHUD.SetData() if Utils.mainWin: Utils.mainWin.updateLapCounter(lapCounter) return Finisher = Model.Rider.Finisher raceTimes = [] leader = [] categoryRaceTimes = {} categories_seen = set() getCategory = race.getCategory leaderCategory = None secondsBeforeLeaderToFlipLapCounter = race.secondsBeforeLeaderToFlipLapCounter + 1.0 def setLapCounter( leaderCategory, category, lapCur, lapMax, tLeaderArrival=sys.float_info.max, tLapStart=None ): if not category: return if not(category == leaderCategory or race.getNumLapsFromCategory(category)): return lapsToGo = max( 0, lapMax - lapCur ) if secondsBeforeLeaderToFlipLapCounter < tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter+5.0: v = (u'{}'.format(lapsToGo), True, tLapStart) # Flash current lap (about to be flipped). elif 0.0 <= tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter: v = (u'{}'.format(max(0,lapsToGo-1)), False, tLapStart) # Flip lap counter before leader. else: v = (u'{}'.format(lapsToGo), False, tLapStart) # Show current lap. try: lapCounter[categoryToLapCounterIndex[category]] = v except (KeyError, IndexError): pass for rr in results: if rr.status != Finisher or not rr.raceTimes: continue category = getCategory( rr.num ) if category in categories_seen: # This is not the leader if we have seen this category before. # Update the red lantern time. newRaceTimes = categoryRaceTimes[category] if rr.raceTimes[-1] > newRaceTimes[-1]: newRaceTimes[-1] = rr.raceTimes[-1] continue if not leaderCategory: leaderCategory = category categories_seen.add( category ) leader.append( u'{} {}'.format(category.fullname if category else u'<{}>'.format(_('Missing')), rr.num) ) # Add a copy of the race times. Append the leader's last time as the current red lantern. raceTimes.append( rr.raceTimes + [rr.raceTimes[-1]] ) categoryRaceTimes[category] = raceTimes[-1] # Find the next expected lap arrival. try: lapCur = bisect.bisect_left( rr.raceTimes, tCur ) # Time before leader's arrival. tLeaderArrival = rr.raceTimes[lapCur] - tCur except IndexError: # At the end of the race, use the leader's race time. # Make sure it is a recorded time, not a projected time. try: tLapStart = rr.raceTimes[-2] if rr.interp[-1] else rr.raceTimes[-1] except: tLapStart = None setLapCounter( leaderCategory, category, len(rr.raceTimes)-1, len(rr.raceTimes)-1, tLapStart = tLapStart ) continue if lapCur <= 1: tLapStart = race.categoryStartOffset(category) else: lapPrev = lapCur-1 # Make sure we use an actual recorded time - not a projected time. # A projected time is possible if the leader has a slow lap. if rr.interp[lapPrev]: lapPrev -= 1 try: tLapStart = rr.raceTimes[lapPrev] if lapPrev else race.categoryStartOffset(category) except IndexError: tLapStart = None setLapCounter( leaderCategory, category, lapCur, len(rr.raceTimes), tLeaderArrival, tLapStart ) if tLeaderArrival is not None: if 0.0 <= tLeaderArrival <= 3.0: if category not in self.lapReminder: self.lapReminder[category] = Utils.PlaySound( 'reminder.wav' ) elif category in self.lapReminder: del self.lapReminder[category] self.raceHUD.SetData( raceTimes, leader, tCur if race.isRunning() else None ) if Utils.mainWin: Utils.mainWin.updateLapCounter( lapCounter ) def refreshRaceTime( self ): race = Model.race if race is not None: tRace = race.lastRaceTime() tStr = Utils.formatTime( tRace ) if tStr.startswith('0'): tStr = tStr[1:] self.refreshRaceHUD() if race.enableUSBCamera: self.photoButton.Show( True ) self.photoCount.SetLabel( u'{}'.format(race.photoCount) ) if Utils.cameraError: self.photoButton.SetBitmapLabel( self.camera_broken_bitmap ) self.photoButton.SetToolTip( wx.ToolTip(Utils.cameraError) ) else: self.photoButton.SetBitmapLabel( self.camera_bitmap ) self.photoButton.SetToolTip( self.camera_tooltip ) else: self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) else: tStr = '' tRace = None self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) self.raceTime.SetLabel( ' ' + tStr ) self.hbClockPhoto.Layout() mainWin = Utils.mainWin if mainWin is not None: try: mainWin.refreshRaceAnimation() except: pass def onPhotoButton( self, event ): if not Utils.mainWin: return Utils.mainWin.photoDialog.Show( True ) Utils.mainWin.photoDialog.refresh( Utils.mainWin.photoDialog.ShowAllPhotos ) raceMessage = { 0:_("Finishers Arriving"), 1:_("Ring Bell"), 2:_("Prepare Bell") } def refreshLaps( self ): wx.CallAfter( self.refreshRaceHUD ) def refreshRiderLapCountList( self ): self.lapCountList.DeleteAllItems() race = Model.race if not race or not race.isRunning(): return Finisher = Model.Rider.Finisher NP = Model.Rider.NP getCategory = race.getCategory t = race.curRaceTime() results = GetResults( None ) if race.isTimeTrial: # Add TT riders who have started but not recoreded a lap yet. results = copy.deepcopy(list(results)) for rr in results: a = race.riders[rr.num] if rr.status == NP and a.firstTime is not None and a.firstTime <= t: rr.status = Finisher elif race.enableJChipIntegration and race.resetStartClockOnFirstTag and len(results) != len(race.riders): # Add rider entries who have been read by RFID but have not completed the first lap. results = list(results) resultNums = set( rr.num for rr in results ) for a in six.itervalues(race.riders): if a.status == Finisher and a.num not in resultNums and a.firstTime is not None: category = getCategory( a.num ) if category and t >= a.firstTime and t >= race.getStartOffset(a.num): results.append( # num, status, lastTime, raceCat, lapTimes, raceTimes, interp RiderResult( a.num, Finisher, a.firstTime, category.fullname, [], [], [] ) ) results = [rr for rr in results if rr.status == Finisher] if not results: return catLapCount = defaultdict(int) catCount = defaultdict(int) catRaceCount = defaultdict(int) catLapsMax = defaultdict(int) for rr in results: category = getCategory( rr.num ) catLapsMax[category] = max( catLapsMax[category], race.getNumLapsFromCategory(category) or 1, len(rr.raceTimes)-1 ) for rr in results: category = getCategory( rr.num ) catCount[getCategory(rr.num)] += 1 numLaps = catLapsMax[category] tSearch = t if race.isTimeTrial: try: tSearch -= race.riders[rr.num].firstTime except: pass lap = max( 1, bisect.bisect_left(rr.raceTimes, tSearch) ) if lap <= numLaps: # Rider is still on course. key = (category, lap) catLapCount[key] += 1 catRaceCount[category] += 1 if not catLapCount: return catLapList = [(category, lap, count) for (category, lap), count in six.iteritems(catLapCount)] catLapList.sort( key=lambda x: (x[0].getStartOffsetSecs(), x[0].fullname, -x[1]) ) def appendListRow( row = tuple(), colour = None, bold = None ): r = self.lapCountList.InsertItem( 999999, u'{}'.format(row[0]) if row else u'' ) for c in six.moves.range(1, len(row)): self.lapCountList.SetItem( r, c, u'{}'.format(row[c]) ) if colour is not None: item = self.lapCountList.GetItem( r ) item.SetTextColour( colour ) self.lapCountList.SetItem( item ) if bold is not None: item = self.lapCountList.GetItem( r ) font = self.lapCountList.GetFont() font.SetWeight( wx.FONTWEIGHT_BOLD ) item.SetFont( font ) self.lapCountList.SetItem( item ) return r appendListRow( ( _('Total'), u'{}/{}'.format(sum(count for count in six.itervalues(catLapCount)), sum(count for count in six.itervalues(catCount))) ), colour=wx.BLUE, bold=True ) lastCategory = None for category, lap, count in catLapList: categoryLaps = catLapsMax[category] if category != lastCategory: appendListRow( (category.fullname, u'{}/{}'.format(catRaceCount[category], catCount[category]), (u'({} {})'.format(categoryLaps if categoryLaps < 1000 else u'', _('laps') if categoryLaps > 1 else _('lap'))) ), bold = True ) appendListRow( (u'', count, u'{} {}'.format( _('on lap'), lap ) ) ) lastCategory = category def refreshLastRiderOnCourse( self ): race = Model.race lastRiderOnCourse = GetLastRider( None ) changed = False if lastRiderOnCourse: maxLength = 24 rider = race.riders[lastRiderOnCourse.num] short_name = lastRiderOnCourse.short_name(maxLength) if short_name: lastRiderOnCourseName = u'{}: {}'.format(lastRiderOnCourse.num, lastRiderOnCourse.short_name()) else: lastRiderOnCourseName = u'{}'.format(lastRiderOnCourse.num) lastRiderOnCourseTeam = u'{}'.format( getattr(lastRiderOnCourse, 'Team', u'') ) if len(lastRiderOnCourseTeam) > maxLength: lastRiderOnCourseTeam = lastRiderOnCourseTeam[:maxLength].strip() + u'...' category = race.getCategory( lastRiderOnCourse.num ) lastRiderOnCourseCategory = category.fullname t = (lastRiderOnCourse._lastTimeOrig or 0.0) + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) tFinish = race.startTime + datetime.timedelta( seconds=t ) lastRiderOnCourseTime = u'{} {}'.format(_('Finishing at'), tFinish.strftime('%H:%M:%S') ) else: lastRiderOnCourseName = u'' lastRiderOnCourseTeam = u'' lastRiderOnCourseCategory = u'' lastRiderOnCourseTime = u'' changed |= SetLabel( self.lastRiderOnCourseName, lastRiderOnCourseName ) changed |= SetLabel( self.lastRiderOnCourseTeam, lastRiderOnCourseTeam ) changed |= SetLabel( self.lastRiderOnCourseCategory, lastRiderOnCourseCategory ) changed |= SetLabel( self.lastRiderOnCourseTime, lastRiderOnCourseTime ) if changed: Utils.LayoutChildResize( self.raceStartTime ) def refreshAll( self ): self.refreshRaceTime() self.refreshLaps() def commit( self ): pass def onPaint( self, event ): if self.firstTimeDraw: self.firstTimeDraw = False self.splitter.SetSashPosition( SplitterMinPos ) event.Skip() def refreshInputUpdate( self ): self.refreshLaps() self.refreshRiderLapCountList() self.refreshLastRiderOnCourse() def refresh( self ): self.clock.Start() race = Model.race enable = bool(race and race.isRunning()) if self.isEnabled != enable: self.isEnabled = enable if not enable and self.isKeypadInputMode(): self.keypad.numEdit.SetValue( '' ) self.photoCount.Show( bool(race and race.enableUSBCamera) ) self.photoButton.Show( bool(race and race.enableUSBCamera) ) # Refresh the race start time. changed = False rst, rstSource = '', '' if race and race.startTime: st = race.startTime if race.enableJChipIntegration and race.resetStartClockOnFirstTag: if race.firstRecordedTime: rstSource = _('Chip Start') else: rstSource = _('Waiting...') else: rstSource = _('Manual Start') rst = '{:02d}:{:02d}:{:02d}.{:02d}'.format(st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) changed |= SetLabel( self.raceStartMessage, rstSource ) changed |= SetLabel( self.raceStartTime, rst ) self.refreshInputUpdateNonBusy() if self.isKeypadInputMode(): wx.CallLater( 100, self.keypad.numEdit.SetFocus )
def __init__( self, parent, id = wx.ID_ANY ): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour( wx.WHITE ) fontPixels = 43 font = wx.FontFromPixelSize(wx.Size(0,fontPixels), wx.DEFAULT, wx.NORMAL, wx.NORMAL) verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) splitter.Bind( wx.EVT_PAINT, self.onPaint ) panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) panel.SetDoubleBuffered( True ) panel.SetSizer( horizontalMainSizer ) panel.SetBackgroundColour( wx.WHITE ) #------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.keypad = Keypad( panel, self ) horizontalMainSizer.Add( self.keypad, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) self.timeTrialRecord = TimeTrialRecord( panel, self ) self.timeTrialRecord.Show( False ) self.horizontalMainSizer = horizontalMainSizer #------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText( panel, label = u'00:00') self.raceTime.SetFont( font ) self.raceTime.SetDoubleBuffered(True) self.keypadBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'keypad.png'), wx.BITMAP_TYPE_PNG ) self.ttRecordBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'stopwatch.png'), wx.BITMAP_TYPE_PNG ) self.keypadTimeTrialToggleButton = wx.BitmapButton( panel, bitmap = self.ttRecordBitmap ) self.keypadTimeTrialToggleButton.Bind( wx.EVT_BUTTON, self.swapKeypadTimeTrialRecord ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer.Add( verticalSubSizer ) hs = wx.BoxSizer( wx.HORIZONTAL ) hs.Add( self.keypadTimeTrialToggleButton, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border = 8 ) hs.Add( self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100-40-8 ) verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border = 2 ) #------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) rowCur = 0 colCur = 0 rowCur += 1 label = wx.StaticText( panel, label = _('Max Laps')) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.numLaps = wx.StaticText( panel, label=u'', size=(64,-1) ) self.numLaps.SetFont( font ) gbs.Add( self.numLaps, pos=(rowCur, colCur+1), span=(1,1) ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Leader Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.leaderFinishTime = wx.StaticText( panel, label = u"") self.leaderFinishTime.SetFont( font ) gbs.Add( self.leaderFinishTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Last Rider Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lastRiderFinishTime = wx.StaticText( panel ) self.lastRiderFinishTime.SetFont( font ) gbs.Add( self.lastRiderFinishTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Avg Lap Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.leadersLapTime = wx.StaticText( panel ) self.leadersLapTime.SetFont( font ) gbs.Add( self.leadersLapTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Completing Lap")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lapCompleting = wx.StaticText( panel ) self.lapCompleting.SetFont( font ) gbs.Add( self.lapCompleting, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Show Laps to Go")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lapsToGo = wx.StaticText( panel ) self.lapsToGo.SetFont( font ) gbs.Add( self.lapsToGo, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 rowCur += 1 label = wx.StaticText( panel, label = _("Manual Start")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.raceStartMessage = label self.raceStartTime = wx.StaticText( panel ) self.raceStartTime.SetFont( font ) gbs.Add( self.raceStartTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Leader Finish")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.estLeaderTime = wx.StaticText( panel ) self.estLeaderTime.SetFont( font ) gbs.Add( self.estLeaderTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Last Rider Finish")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.estLastRiderTime = wx.StaticText( panel ) self.estLastRiderTime.SetFont( font ) gbs.Add( self.estLastRiderTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) self.photoCount = wx.StaticText( panel, label = u"000004" ) self.photoCount.SetFont( font ) self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT|wx.ALIGN_RIGHT, border = 6 ) self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton( panel, bitmap = self.camera_bitmap ) self.camera_tooltip = wx.ToolTip( _('Show Last Photos...') ) self.photoButton.SetToolTip( self.camera_tooltip ) self.photoButton.Bind( wx.EVT_BUTTON, self.onPhotoButton ) self.hbClockPhoto.Add( self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) label = wx.StaticText( panel, label = _("Clock") ) label.SetFont( font ) self.hbClockPhoto.Add( label, flag=wx.ALIGN_CENTRE_VERTICAL ) gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.clockTime = wx.StaticText( panel ) self.clockTime.SetFont( font ) gbs.Add( self.clockTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 rowCur += 1 self.message = wx.StaticText( panel ) self.message.SetFont( font ) self.message.SetDoubleBuffered( True ) gbs.Add( self.message, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE ) rowCur += 1 verticalSubSizer.Add( gbs, flag=wx.LEFT|wx.TOP, border = 8 ) #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) rcVertical.AddSpacer( 32 ) title = wx.StaticText( panel, label = _('Riders on Course:') ) title.SetFont( wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) rcVertical.Add( title, flag=wx.ALL, border = 4 ) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) self.lapCountList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) self.lapCountList.InsertColumn( 0, _('Category'), wx.LIST_FORMAT_LEFT, 140 ) self.lapCountList.InsertColumn( 1, _('Count'), wx.LIST_FORMAT_RIGHT, 70 ) self.lapCountList.InsertColumn( 2, u'', wx.LIST_FORMAT_LEFT, 90 ) rcVertical.Add( self.lapCountList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) #---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN ) splitter.SetMinimumPaneSize( 20 ) splitter.SplitHorizontally( panel, self.raceHUD, -100 ) verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) self.SetSizer( verticalMainSizer ) self.isEnabled = True self.splitter = splitter self.firstTimeDraw = True self.refreshRaceTime()
class NumKeypad( wx.Panel ): SwitchToTimeTrialEntryMessage = _('Switch to Time Trial Entry') SwitchToNumberEntryMessage = _('Switch to Regular Number Entry') def __init__( self, parent, id = wx.ID_ANY ): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour( wx.WHITE ) fontPixels = 43 font = wx.FontFromPixelSize(wx.Size(0,fontPixels), wx.DEFAULT, wx.NORMAL, wx.NORMAL) verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) splitter.Bind( wx.EVT_PAINT, self.onPaint ) panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) panel.SetDoubleBuffered( True ) panel.SetSizer( horizontalMainSizer ) panel.SetBackgroundColour( wx.WHITE ) #------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.keypad = Keypad( panel, self ) horizontalMainSizer.Add( self.keypad, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) self.timeTrialRecord = TimeTrialRecord( panel, self ) self.timeTrialRecord.Show( False ) self.horizontalMainSizer = horizontalMainSizer #------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText( panel, label = u'00:00') self.raceTime.SetFont( font ) self.raceTime.SetDoubleBuffered(True) self.keypadBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'keypad.png'), wx.BITMAP_TYPE_PNG ) self.ttRecordBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'stopwatch.png'), wx.BITMAP_TYPE_PNG ) self.keypadTimeTrialToggleButton = wx.BitmapButton( panel, bitmap = self.ttRecordBitmap ) self.keypadTimeTrialToggleButton.Bind( wx.EVT_BUTTON, self.swapKeypadTimeTrialRecord ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer.Add( verticalSubSizer ) hs = wx.BoxSizer( wx.HORIZONTAL ) hs.Add( self.keypadTimeTrialToggleButton, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border = 8 ) hs.Add( self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100-40-8 ) verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border = 2 ) #------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) rowCur = 0 colCur = 0 rowCur += 1 label = wx.StaticText( panel, label = _('Max Laps')) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.numLaps = wx.StaticText( panel, label=u'', size=(64,-1) ) self.numLaps.SetFont( font ) gbs.Add( self.numLaps, pos=(rowCur, colCur+1), span=(1,1) ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Leader Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.leaderFinishTime = wx.StaticText( panel, label = u"") self.leaderFinishTime.SetFont( font ) gbs.Add( self.leaderFinishTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Last Rider Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lastRiderFinishTime = wx.StaticText( panel ) self.lastRiderFinishTime.SetFont( font ) gbs.Add( self.lastRiderFinishTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Avg Lap Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.leadersLapTime = wx.StaticText( panel ) self.leadersLapTime.SetFont( font ) gbs.Add( self.leadersLapTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Completing Lap")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lapCompleting = wx.StaticText( panel ) self.lapCompleting.SetFont( font ) gbs.Add( self.lapCompleting, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Show Laps to Go")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lapsToGo = wx.StaticText( panel ) self.lapsToGo.SetFont( font ) gbs.Add( self.lapsToGo, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 rowCur += 1 label = wx.StaticText( panel, label = _("Manual Start")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.raceStartMessage = label self.raceStartTime = wx.StaticText( panel ) self.raceStartTime.SetFont( font ) gbs.Add( self.raceStartTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Leader Finish")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.estLeaderTime = wx.StaticText( panel ) self.estLeaderTime.SetFont( font ) gbs.Add( self.estLeaderTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Last Rider Finish")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.estLastRiderTime = wx.StaticText( panel ) self.estLastRiderTime.SetFont( font ) gbs.Add( self.estLastRiderTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) self.photoCount = wx.StaticText( panel, label = u"000004" ) self.photoCount.SetFont( font ) self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT|wx.ALIGN_RIGHT, border = 6 ) self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton( panel, bitmap = self.camera_bitmap ) self.camera_tooltip = wx.ToolTip( _('Show Last Photos...') ) self.photoButton.SetToolTip( self.camera_tooltip ) self.photoButton.Bind( wx.EVT_BUTTON, self.onPhotoButton ) self.hbClockPhoto.Add( self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) label = wx.StaticText( panel, label = _("Clock") ) label.SetFont( font ) self.hbClockPhoto.Add( label, flag=wx.ALIGN_CENTRE_VERTICAL ) gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.clockTime = wx.StaticText( panel ) self.clockTime.SetFont( font ) gbs.Add( self.clockTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 rowCur += 1 self.message = wx.StaticText( panel ) self.message.SetFont( font ) self.message.SetDoubleBuffered( True ) gbs.Add( self.message, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE ) rowCur += 1 verticalSubSizer.Add( gbs, flag=wx.LEFT|wx.TOP, border = 8 ) #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) rcVertical.AddSpacer( 32 ) title = wx.StaticText( panel, label = _('Riders on Course:') ) title.SetFont( wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) rcVertical.Add( title, flag=wx.ALL, border = 4 ) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) self.lapCountList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) self.lapCountList.InsertColumn( 0, _('Category'), wx.LIST_FORMAT_LEFT, 140 ) self.lapCountList.InsertColumn( 1, _('Count'), wx.LIST_FORMAT_RIGHT, 70 ) self.lapCountList.InsertColumn( 2, u'', wx.LIST_FORMAT_LEFT, 90 ) rcVertical.Add( self.lapCountList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) #---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN ) splitter.SetMinimumPaneSize( 20 ) splitter.SplitHorizontally( panel, self.raceHUD, -100 ) verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) self.SetSizer( verticalMainSizer ) self.isEnabled = True self.splitter = splitter self.firstTimeDraw = True self.refreshRaceTime() def isKeypadInputMode( self ): return self.keypadTimeTrialToggleButton.GetBitmapLabel() == self.ttRecordBitmap def isTimeTrialInputMode( self ): return not self.isKeypadInputMode() def swapKeypadTimeTrialRecord( self, event = None ): if self.isKeypadInputMode(): self.keypad.Show( False ) self.timeTrialRecord.Show( True ) self.timeTrialRecord.refresh() self.horizontalMainSizer.Replace( self.keypad, self.timeTrialRecord ) self.keypadTimeTrialToggleButton.SetBitmapLabel( self.keypadBitmap ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToNumberEntryMessage)) wx.CallAfter( self.timeTrialRecord.Refresh ) wx.CallLater( 100, self.timeTrialRecord.grid.SetFocus ) else: self.keypad.Show( True ) self.timeTrialRecord.Show( False ) self.horizontalMainSizer.Replace( self.timeTrialRecord, self.keypad ) self.keypadTimeTrialToggleButton.SetBitmapLabel( self.ttRecordBitmap ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) wx.CallAfter( self.keypad.Refresh ) wx.CallLater( 100, self.keypad.numEdit.SetFocus ) self.horizontalMainSizer.Layout() self.GetSizer().Layout() wx.CallAfter( self.Refresh ) def setKeypadInput( self, b = True ): if b: if not self.isKeypadInputMode(): self.swapKeypadTimeTrialRecord() else: if not self.isTimeTrialInputMode(): self.swapKeypadTimeTrialRecord() def setTimeTrialInput( self, b = True ): self.setKeypadInput( not b ) def refreshRaceHUD( self ): # Assumes Model is locked. race = Model.race if not race: self.raceHUD.SetData() if Utils.mainWin: Utils.mainWin.updateLapCounter() return results = GetResults( None, False ) if not results: self.raceHUD.SetData() if Utils.mainWin: lapCounter = [] if all( category.getNumLaps() for category in race.getCategories(startWaveOnly=True) ): lapCounter = [(u'{}'.format(category.getNumLaps()),False) for category in race.getCategories(startWaveOnly=True)] else: lapCounter = [(u'{} min'.format(race.minutes),False)] + [(u'{}'.format(category.getNumLaps()),False) for category in race.getCategories(startWaveOnly=True) if category.getNumLaps()] Utils.mainWin.updateLapCounter(lapCounter) return Finisher = Model.Rider.Finisher tCur = race.curRaceTime() raceTimes = [] leader = [] categoryRaceTimes = {} categories_seen = set() getCategory = race.getCategory leaderCategory = None lapCounter = [] secondsBeforeLeaderToFlipLapCounter = race.secondsBeforeLeaderToFlipLapCounter + 1.0 def appendLapCounter( leaderCategory, category, lapCur, lapMax, tLeader=sys.float_info.max ): if race.isTimeTrial or not(category == leaderCategory or category.getNumLaps()): return lapsToGo = max( 0, lapMax - lapCur ) if secondsBeforeLeaderToFlipLapCounter < tLeader <= secondsBeforeLeaderToFlipLapCounter+5.0: lapCounter.append( ('{}'.format(lapsToGo), True) ) elif 0.0 <= tLeader <= secondsBeforeLeaderToFlipLapCounter: lapCounter.append( ('{}'.format(max(0,lapsToGo-1)), False) ) else: lapCounter.append( ('{}'.format(lapsToGo), False) ) for rr in results: if rr.status != Finisher or not rr.raceTimes: continue category = getCategory( rr.num ) if category in categories_seen: # If we have not seen this category, this is not the leader. # Make sure we update the red lantern time. newRaceTimes = categoryRaceTimes[category] if rr.raceTimes[-1] > newRaceTimes[-1]: newRaceTimes[-1] = rr.raceTimes[-1] continue if leaderCategory is None: leaderCategory = category categories_seen.add( category ) leader.append( u'%s %d' % (category.fullname if category else u'<{}>'.format(_('Missing')), rr.num) ) # Add a copy of the race times. Append the leader's last time as the current red lantern. raceTimes.append( rr.raceTimes + [rr.raceTimes[-1]] ) categoryRaceTimes[category] = raceTimes[-1] try: lapCur = bisect.bisect_left( rr.raceTimes, tCur ) tLeader = rr.raceTimes[lapCur] - tCur except IndexError: appendLapCounter( leaderCategory, category, len(rr.raceTimes)-1, len(rr.raceTimes)-1 ) continue appendLapCounter( leaderCategory, category, lapCur, len(rr.raceTimes), tLeader ) if 0.0 <= tLeader <= 3.0 and not race.isTimeTrial: if category not in self.lapReminder: self.lapReminder[category] = Utils.PlaySound( 'reminder.wav' ) elif category in self.lapReminder: del self.lapReminder[category] self.raceHUD.SetData( nowTime = tCur, raceTimes = raceTimes, leader = leader ) if Utils.mainWin: Utils.mainWin.updateLapCounter( lapCounter ) def refreshRaceTime( self ): tClockStr = '' race = Model.race if race is not None: tRace = race.lastRaceTime() tStr = Utils.formatTime( tRace ) self.refreshRaceHUD() if race.enableUSBCamera: self.photoButton.Show( True ) self.photoCount.SetLabel( '{}'.format(race.photoCount) ) if Utils.cameraError: self.photoButton.SetBitmapLabel( self.camera_broken_bitmap ) self.photoButton.SetToolTip( wx.ToolTip(Utils.cameraError) ) else: self.photoButton.SetBitmapLabel( self.camera_bitmap ) self.photoButton.SetToolTip( self.camera_tooltip ) else: self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) if race.isRunning(): tNow = datetime.datetime.now() tClockStr = '%02d:%02d:%02d' % (tNow.hour, tNow.minute, tNow.second) else: tStr = '' tRace = None self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) self.raceTime.SetLabel( ' ' + tStr ) self.clockTime.SetLabel( tClockStr ) self.hbClockPhoto.Layout() mainWin = Utils.mainWin if mainWin is not None: try: mainWin.refreshRaceAnimation() except: pass mainWin.forecastHistory.updatedExpectedTimes( tRace ) def onPhotoButton( self, event ): if not Utils.mainWin: return Utils.mainWin.photoDialog.Show( True ) Utils.mainWin.photoDialog.refresh( Utils.mainWin.photoDialog.ShowAllPhotos ) def getLapInfo( self ): # Assumes Model is locked. # Returns (laps, lapsToGo, lapCompleting, # leadersExpectedLapTime, leaderNum, # raceFinishTime) return self.raceHUD.GetLapInfo() def updateEstFinishTime( self ): race = Model.race changed = False if not race or getattr(race, 'isTimeTrial', False): changed |= SetLabel( self.estLeaderTime, '' ) changed |= SetLabel( self.estLastRiderTime, '' ) return changed try: changed |= SetLabel( self.estLeaderTime, (race.startTime + datetime.timedelta(seconds = GetLeaderFinishTime())).strftime('%H:%M:%S') ) changed |= SetLabel( self.estLastRiderTime, (race.startTime + datetime.timedelta(seconds = GetLastFinisherTime())).strftime('%H:%M:%S') ) except: changed |= SetLabel( self.estLeaderTime, u'' ) changed |= SetLabel( self.estLastRiderTime, u'' ) return changed raceMessage = { 0:_("Finishers Arriving"), 1:_("Ring Bell"), 2:_("Prepare Bell") } def refreshLaps( self ): race = Model.race laps, lapsToGo, lapCompleting, leadersExpectedLapTime, leaderNum, expectedRaceFinish = self.getLapInfo() changed = False # Set the projected finish time and laps. if lapCompleting >= 1: changed |= SetLabel( self.numLaps, unicode(laps) ) changed |= SetLabel( self.leaderFinishTime, Utils.formatTime(expectedRaceFinish) ) changed |= SetLabel( self.lastRiderFinishTime, Utils.formatTime(GetLastFinisherTime()) ) changed |= SetLabel( self.leadersLapTime, Utils.formatTime(leadersExpectedLapTime) ) changed |= SetLabel( self.lapsToGo, unicode(lapsToGo) ) changed |= SetLabel( self.lapCompleting, unicode(lapCompleting) ) changed |= self.updateEstFinishTime() if lapsToGo == 2 and race.isLeaderExpected(): changed |= SetLabel( self.message, u'{}: {}'.format(_('Leader Bell Lap Alert'), leaderNum) ) elif lapsToGo == 1 and race.isLeaderExpected(): changed |= SetLabel( self.message, u'{}: {}'.format(_('Leader Finish Alert'), leaderNum) ) else: changed |= SetLabel( self.message, self.raceMessage.get(lapsToGo, '') ) if race: race.numLaps = laps else: changed |= SetLabel( self.numLaps, unicode(laps) ) changed |= SetLabel( self.leaderFinishTime, u'' ) changed |= SetLabel( self.lastRiderFinishTime, u'' ) changed |= SetLabel( self.leadersLapTime, u'' ) changed |= SetLabel( self.lapsToGo, u'' ) changed |= SetLabel( self.lapCompleting, unicode(lapCompleting) ) changed |= SetLabel( self.estLeaderTime, u'' ) changed |= SetLabel( self.estLastRiderTime, u'' ) changed |= SetLabel( self.message, _('Collecting Data') ) if race: race.numLaps = None if changed: Utils.LayoutChildResize( self.message ) wx.CallAfter( self.refreshRaceHUD ) def refreshRiderLapCountList( self ): self.lapCountList.DeleteAllItems() race = Model.race if not race or not race.isRunning(): return results = GetResults( None, False ) if not results: return categoryLapTotal = {} catLapCount = defaultdict(int) catCount = defaultdict(int) catRaceCount = defaultdict(int) getCategory = race.getCategory t = Model.race.curRaceTime() for rr in results: category = getCategory( rr.num ) catCount[category] += 1 if rr.status != Model.Rider.Finisher: continue if category not in categoryLapTotal: categoryLapTotal[category] = max(1, rr.laps) numLaps = categoryLapTotal[category] tSearch = t if race.isTimeTrial: try: tSearch -= race[rr.num].firstTime except: pass lap = max( 1, bisect.bisect_left(rr.raceTimes, tSearch) ) if lap <= numLaps: # Rider is still on course. key = (category, lap, numLaps) catLapCount[key] += 1 catRaceCount[category] += 1 if not catLapCount: return catLapList = [(category, lap, categoryLaps, count) for (category, lap, categoryLaps), count in catLapCount.iteritems()] catLapList.sort( key=lambda x: (x[0].getStartOffsetSecs(), x[0].fullname, -x[1]) ) def appendListRow( row = tuple(), colour = None, bold = None ): r = self.lapCountList.InsertStringItem( sys.maxint, u'{}'.format(row[0]) if row else '' ) for c in xrange(1, len(row)): self.lapCountList.SetStringItem( r, c, u'{}'.format(row[c]) ) if colour is not None: item = self.lapCountList.GetItem( r ) item.SetTextColour( colour ) self.lapCountList.SetItem( item ) if bold is not None: item = self.lapCountList.GetItem( r ) font = self.lapCountList.GetFont() font.SetWeight( wx.FONTWEIGHT_BOLD ) item.SetFont( font ) self.lapCountList.SetItem( item ) return r appendListRow( ( _('Total'), u'{}/{}'.format(sum(count for count in catLapCount.itervalues()), sum(count for count in catCount.itervalues())) ), colour=wx.BLUE, bold=True ) lastCategory = None for category, lap, categoryLaps, count in catLapList: if category != lastCategory: appendListRow( (category.fullname, u'{}/{}'.format(catRaceCount[category], catCount[category]), (u'({} {})'.format(categoryLaps, _('laps') if categoryLaps > 1 else _('lap'))) ), bold = True ) appendListRow( (u'', count, u'{} {}'.format( _('on lap'), lap ) ) ) lastCategory = category def commit( self ): pass def onPaint( self, event ): if self.firstTimeDraw: self.firstTimeDraw = False self.splitter.SetSashPosition( 460 ) event.Skip() def refresh( self ): race = Model.race enable = bool(race and race.isRunning()) if self.isEnabled != enable: self.keypad.Enable( enable ) self.timeTrialRecord.Enable( enable ) self.isEnabled = enable if not enable and self.isKeypadInputMode(): self.keypad.numEdit.SetValue( '' ) self.photoCount.Show( bool(race and race.enableUSBCamera) ) self.photoButton.Show( bool(race and race.enableUSBCamera) ) # Refresh the race start time. changed = False rst, rstSource = '', '' if race and race.startTime: st = race.startTime if race.enableJChipIntegration and race.resetStartClockOnFirstTag: if race.firstRecordedTime: rstSource = _('Chip Start') else: rstSource = _('Waiting...') else: rstSource = _('Manual Start') rst = '%02d:%02d:%02d.%02d' % (st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) changed |= SetLabel( self.raceStartMessage, rstSource ) changed |= SetLabel( self.raceStartTime, rst ) if changed: Utils.LayoutChildResize( self.raceStartTime ) wx.CallAfter( self.refreshLaps ) wx.CallAfter( self.refreshRiderLapCountList ) if self.isKeypadInputMode(): wx.CallLater( 100, self.keypad.numEdit.SetFocus ) if self.isTimeTrialInputMode(): wx.CallAfter( self.timeTrialRecord.refresh )
class NumKeypad(wx.Panel): SwitchToTimeTrialEntryMessage = _("Switch to Time Trial Entry") SwitchToNumberEntryMessage = _("Switch to Regular Number Entry") def __init__(self, parent, id=wx.ID_ANY): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour(wx.WHITE) self.refreshInputUpdateNonBusy = NonBusyCall(self.refreshInputUpdate, min_millis=1000, max_millis=3000) fontPixels = 50 font = wx.FontFromPixelSize(wx.Size(0, fontPixels), wx.DEFAULT, wx.NORMAL, wx.NORMAL) verticalMainSizer = wx.BoxSizer(wx.VERTICAL) horizontalMainSizer = wx.BoxSizer(wx.HORIZONTAL) splitter = wx.SplitterWindow(self, wx.ID_ANY, style=wx.SP_3DSASH) splitter.Bind(wx.EVT_PAINT, self.onPaint) panel = wx.Panel(splitter, style=wx.BORDER_SUNKEN) panel.SetDoubleBuffered(True) panel.SetSizer(horizontalMainSizer) panel.SetBackgroundColour(wx.WHITE) # ------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.keypad = Keypad(panel, self) horizontalMainSizer.Add(self.keypad, 0, flag=wx.TOP | wx.LEFT | wx.EXPAND, border=4) self.timeTrialRecord = TimeTrialRecord(panel, self) self.timeTrialRecord.Show(False) self.horizontalMainSizer = horizontalMainSizer # ------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText(panel, label=u"0:00") self.raceTime.SetFont(font) self.raceTime.SetDoubleBuffered(True) self.keypadBitmap = wx.Bitmap(os.path.join(Utils.getImageFolder(), "keypad.png"), wx.BITMAP_TYPE_PNG) self.ttRecordBitmap = wx.Bitmap(os.path.join(Utils.getImageFolder(), "stopwatch.png"), wx.BITMAP_TYPE_PNG) self.keypadTimeTrialToggleButton = wx.BitmapButton(panel, bitmap=self.ttRecordBitmap) self.keypadTimeTrialToggleButton.Bind(wx.EVT_BUTTON, self.swapKeypadTimeTrialRecord) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) verticalSubSizer = wx.BoxSizer(wx.VERTICAL) horizontalMainSizer.Add(verticalSubSizer) hs = wx.BoxSizer(wx.HORIZONTAL) hs.Add(self.keypadTimeTrialToggleButton, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=8) hs.Add(self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100 - 40 - 8) verticalSubSizer.Add(hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border=2) # ------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) fontBold = wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.BOLD) rowCur = 0 colCur = 0 rowCur += 1 label = wx.StaticText(panel, label=_("Manual Start")) label.SetFont(font) gbs.Add(label, pos=(rowCur, colCur), span=(1, 1), flag=labelAlign) self.raceStartMessage = label self.raceStartTime = wx.StaticText(panel) self.raceStartTime.SetFont(font) gbs.Add( self.raceStartTime, pos=(rowCur, colCur + 1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 line = wx.StaticLine(panel, style=wx.LI_HORIZONTAL) gbs.Add(line, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT) rowCur += 1 label = wx.StaticText(panel, label=u"{}:".format(_("Est. Last Rider"))) label.SetFont(font) gbs.Add(label, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT) rowCur += 1 self.lastRiderOnCourseTime = wx.StaticText(panel) self.lastRiderOnCourseTime.SetFont(font) gbs.Add( self.lastRiderOnCourseTime, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseName = wx.StaticText(panel) self.lastRiderOnCourseName.SetFont(fontBold) gbs.Add( self.lastRiderOnCourseName, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseTeam = wx.StaticText(panel) self.lastRiderOnCourseTeam.SetFont(font) gbs.Add( self.lastRiderOnCourseTeam, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.lastRiderOnCourseCategory = wx.StaticText(panel) self.lastRiderOnCourseCategory.SetFont(font) gbs.Add( self.lastRiderOnCourseCategory, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT, ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer(wx.HORIZONTAL) self.photoCount = wx.StaticText(panel, label=u"000004") self.photoCount.SetFont(font) self.hbClockPhoto.Add(self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL | wx.RIGHT | wx.ALIGN_RIGHT, border=6) self.camera_bitmap = wx.Bitmap(os.path.join(Utils.getImageFolder(), "camera.png"), wx.BITMAP_TYPE_PNG) self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), "camera_broken.png"), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton(panel, bitmap=self.camera_bitmap) self.camera_tooltip = wx.ToolTip(_("Show Last Photos...")) self.photoButton.SetToolTip(self.camera_tooltip) self.photoButton.Bind(wx.EVT_BUTTON, self.onPhotoButton) self.hbClockPhoto.Add(self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL | wx.RIGHT, border=18) gbs.Add(self.hbClockPhoto, pos=(rowCur, colCur), span=(1, 1), flag=labelAlign) rowCur += 1 self.clock = ClockDigital(panel, size=(100, 24), checkFunc=self.doClockUpdate) self.clock.SetBackgroundColour(wx.WHITE) gbs.Add(self.clock, pos=(rowCur, 0), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT) rowCur += 1 verticalSubSizer.Add(gbs, flag=wx.LEFT | wx.TOP, border=8) # ------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer(wx.VERTICAL) rcVertical.AddSpacer(32) title = wx.StaticText(panel, label=_("Riders on Course:")) title.SetFont(wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL)) rcVertical.Add(title, flag=wx.ALL, border=4) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_HRULES | wx.BORDER_NONE ) self.lapCountList.SetFont(wx.Font(int(fontSize * 0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL)) self.lapCountList.InsertColumn(0, _("Category"), wx.LIST_FORMAT_LEFT, 140) self.lapCountList.InsertColumn(1, _("Count"), wx.LIST_FORMAT_RIGHT, 70) self.lapCountList.InsertColumn(2, u"", wx.LIST_FORMAT_LEFT, 90) rcVertical.Add(self.lapCountList, 1, flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=4) horizontalMainSizer.Add(rcVertical, 1, flag=wx.EXPAND | wx.LEFT, border=4) # ---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD(splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN, lapInfoFunc=getLapInfo) splitter.SetMinimumPaneSize(20) splitter.SplitHorizontally(panel, self.raceHUD, -100) verticalMainSizer.Add(splitter, 1, flag=wx.EXPAND) self.SetSizer(verticalMainSizer) self.isEnabled = True self.splitter = splitter self.firstTimeDraw = True self.refreshRaceTime() def doClockUpdate(self): mainWin = Utils.getMainWin() return not mainWin or mainWin.isShowingPage(self) def isKeypadInputMode(self): return self.keypadTimeTrialToggleButton.GetBitmapLabel() == self.ttRecordBitmap def isTimeTrialInputMode(self): return not self.isKeypadInputMode() def swapKeypadTimeTrialRecord(self, event=None): if self.isKeypadInputMode(): self.keypad.Show(False) self.timeTrialRecord.Show(True) self.timeTrialRecord.refresh() self.horizontalMainSizer.Replace(self.keypad, self.timeTrialRecord) self.keypadTimeTrialToggleButton.SetBitmapLabel(self.keypadBitmap) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToNumberEntryMessage)) wx.CallAfter(self.timeTrialRecord.Refresh) wx.CallLater(100, self.timeTrialRecord.grid.SetFocus) else: self.keypad.Show(True) self.timeTrialRecord.Show(False) self.horizontalMainSizer.Replace(self.timeTrialRecord, self.keypad) self.keypadTimeTrialToggleButton.SetBitmapLabel(self.ttRecordBitmap) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) wx.CallAfter(self.keypad.Refresh) wx.CallLater(100, self.keypad.numEdit.SetFocus) self.horizontalMainSizer.Layout() self.GetSizer().Layout() wx.CallAfter(self.Refresh) def setKeypadInput(self, b=True): if b: if not self.isKeypadInputMode(): self.swapKeypadTimeTrialRecord() else: if not self.isTimeTrialInputMode(): self.swapKeypadTimeTrialRecord() def setTimeTrialInput(self, b=True): self.setKeypadInput(not b) def refreshRaceHUD(self): race = Model.race if not race: self.raceHUD.SetData() if Utils.mainWin: Utils.mainWin.updateLapCounter() return results = GetResults(None) if not results: self.raceHUD.SetData() if Utils.mainWin: lapCounter = [] if all(race.getNumLapsFromCategory(category) for category in race.getCategories(startWaveOnly=True)): lapCounter = [ (u"{}".format(race.getNumLapsFromCategory(category)), False) for category in race.getCategories(startWaveOnly=True) ] else: lapCounter = [(u"{} min".format(race.minutes), False)] + [ (u"{}".format(race.getNumLapsFromCategory(category)), False) for category in race.getCategories(startWaveOnly=True) if race.getNumLapsFromCategory(category) ] Utils.mainWin.updateLapCounter(lapCounter) return Finisher = Model.Rider.Finisher tCur = race.curRaceTime() raceTimes = [] leader = [] categoryRaceTimes = {} categories_seen = set() getCategory = race.getCategory leaderCategory = None lapCounter = [] secondsBeforeLeaderToFlipLapCounter = race.secondsBeforeLeaderToFlipLapCounter + 1.0 def appendLapCounter(leaderCategory, category, lapCur, lapMax, tLeader=sys.float_info.max): if race.isTimeTrial or not (category == leaderCategory or race.getNumLapsFromCategory(category)): return lapsToGo = max(0, lapMax - lapCur) if secondsBeforeLeaderToFlipLapCounter < tLeader <= secondsBeforeLeaderToFlipLapCounter + 5.0: lapCounter.append(("{}".format(lapsToGo), True)) elif 0.0 <= tLeader <= secondsBeforeLeaderToFlipLapCounter: lapCounter.append(("{}".format(max(0, lapsToGo - 1)), False)) else: lapCounter.append(("{}".format(lapsToGo), False)) for rr in results: if rr.status != Finisher or not rr.raceTimes: continue category = getCategory(rr.num) if category in categories_seen: # If we have not seen this category, this is not the leader. # Make sure we update the red lantern time. newRaceTimes = categoryRaceTimes[category] if rr.raceTimes[-1] > newRaceTimes[-1]: newRaceTimes[-1] = rr.raceTimes[-1] continue if leaderCategory is None: leaderCategory = category categories_seen.add(category) leader.append(u"{} {}".format(category.fullname if category else u"<{}>".format(_("Missing")), rr.num)) # Add a copy of the race times. Append the leader's last time as the current red lantern. raceTimes.append(rr.raceTimes + [rr.raceTimes[-1]]) categoryRaceTimes[category] = raceTimes[-1] try: lapCur = bisect.bisect_left(rr.raceTimes, tCur) tLeader = rr.raceTimes[lapCur] - tCur except IndexError: appendLapCounter(leaderCategory, category, len(rr.raceTimes) - 1, len(rr.raceTimes) - 1) continue appendLapCounter(leaderCategory, category, lapCur, len(rr.raceTimes), tLeader) if 0.0 <= tLeader <= 3.0 and not race.isTimeTrial: if category not in self.lapReminder: self.lapReminder[category] = Utils.PlaySound("reminder.wav") elif category in self.lapReminder: del self.lapReminder[category] self.raceHUD.SetData(raceTimes, leader, tCur if race.isRunning() else None) if Utils.mainWin: Utils.mainWin.updateLapCounter(lapCounter) def refreshRaceTime(self): race = Model.race if race is not None: tRace = race.lastRaceTime() tStr = Utils.formatTime(tRace) if tStr.startswith("0"): tStr = tStr[1:] self.refreshRaceHUD() if race.enableUSBCamera: self.photoButton.Show(True) self.photoCount.SetLabel(u"{}".format(race.photoCount)) if Utils.cameraError: self.photoButton.SetBitmapLabel(self.camera_broken_bitmap) self.photoButton.SetToolTip(wx.ToolTip(Utils.cameraError)) else: self.photoButton.SetBitmapLabel(self.camera_bitmap) self.photoButton.SetToolTip(self.camera_tooltip) else: self.photoButton.Show(False) self.photoCount.SetLabel("") else: tStr = "" tRace = None self.photoButton.Show(False) self.photoCount.SetLabel("") self.raceTime.SetLabel(" " + tStr) self.hbClockPhoto.Layout() mainWin = Utils.mainWin if mainWin is not None: try: mainWin.refreshRaceAnimation() except: pass def onPhotoButton(self, event): if not Utils.mainWin: return Utils.mainWin.photoDialog.Show(True) Utils.mainWin.photoDialog.refresh(Utils.mainWin.photoDialog.ShowAllPhotos) raceMessage = {0: _("Finishers Arriving"), 1: _("Ring Bell"), 2: _("Prepare Bell")} def refreshLaps(self): wx.CallAfter(self.refreshRaceHUD) def refreshRiderLapCountList(self): self.lapCountList.DeleteAllItems() race = Model.race if not race or not race.isRunning(): return Finisher = Model.Rider.Finisher getCategory = race.getCategory results = GetResults(None) if race.enableJChipIntegration and race.resetStartClockOnFirstTag and len(results) != len(race.riders): # Add rider entries who have been read by RFID but have not completed the first lap. results = list(results) resultNums = set(rr.num for rr in results) for a in race.riders.itervalues(): if a.num not in resultNums and a.firstTime is not None: category = getCategory(a.num) if category: results.append( # num, status, lastTime, raceCat, lapTimes, raceTimes, interp RiderResult(a.num, a.status, a.firstTime, category.fullname, [], [], []) ) if not results: return categoryLapMax = defaultdict(int) catLapCount = defaultdict(int) catCount = defaultdict(int) catRaceCount = defaultdict(int) t = race.curRaceTime() for rr in results: category = getCategory(rr.num) catCount[category] += 1 if rr.status == Finisher: categoryLapMax[category] = max(1, len(rr.raceTimes) - 1, categoryLapMax[category]) for rr in results: category = getCategory(rr.num) numLaps = categoryLapMax[category] tSearch = t if race.isTimeTrial: try: tSearch -= race.riders[rr.num].firstTime except: pass lap = max(1, bisect.bisect_left(rr.raceTimes, tSearch)) if lap <= numLaps: # Rider is still on course. key = (category, lap, numLaps) catLapCount[key] += 1 catRaceCount[category] += 1 if not catLapCount: return catLapList = [ (category, lap, categoryLaps, count) for (category, lap, categoryLaps), count in catLapCount.iteritems() ] catLapList.sort(key=lambda x: (x[0].getStartOffsetSecs(), x[0].fullname, -x[1])) def appendListRow(row=tuple(), colour=None, bold=None): r = self.lapCountList.InsertStringItem(sys.maxint, u"{}".format(row[0]) if row else "") for c in xrange(1, len(row)): self.lapCountList.SetStringItem(r, c, u"{}".format(row[c])) if colour is not None: item = self.lapCountList.GetItem(r) item.SetTextColour(colour) self.lapCountList.SetItem(item) if bold is not None: item = self.lapCountList.GetItem(r) font = self.lapCountList.GetFont() font.SetWeight(wx.FONTWEIGHT_BOLD) item.SetFont(font) self.lapCountList.SetItem(item) return r appendListRow( ( _("Total"), u"{}/{}".format( sum(count for count in catLapCount.itervalues()), sum(count for count in catCount.itervalues()) ), ), colour=wx.BLUE, bold=True, ) lastCategory = None for category, lap, categoryLaps, count in catLapList: if category != lastCategory: appendListRow( ( category.fullname, u"{}/{}".format(catRaceCount[category], catCount[category]), (u"({} {})".format(categoryLaps, _("laps") if categoryLaps > 1 else _("lap"))), ), bold=True, ) appendListRow((u"", count, u"{} {}".format(_("on lap"), lap))) lastCategory = category def refreshLastRiderOnCourse(self): race = Model.race lastRiderOnCourse = GetLastRider(None) changed = False if lastRiderOnCourse: maxLength = 24 rider = race.riders[lastRiderOnCourse.num] short_name = lastRiderOnCourse.short_name(maxLength) if short_name: lastRiderOnCourseName = u"{}: {}".format(lastRiderOnCourse.num, lastRiderOnCourse.short_name()) else: lastRiderOnCourseName = u"{}".format(lastRiderOnCourse.num) lastRiderOnCourseTeam = u"{}".format(getattr(lastRiderOnCourse, "Team", u"Independent")) if len(lastRiderOnCourseTeam) > maxLength: lastRiderOnCourseTeam = lastRiderOnCourseTeam[:maxLength].strip() + u"..." category = race.getCategory(lastRiderOnCourse.num) lastRiderOnCourseCategory = category.fullname t = (lastRiderOnCourse._lastTimeOrig or 0.0) + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) tFinish = race.startTime + datetime.timedelta(seconds=t) lastRiderOnCourseTime = u"{} {}".format(_("Finishing at"), tFinish.strftime("%H:%M:%S")) else: lastRiderOnCourseName = u"" lastRiderOnCourseTeam = u"" lastRiderOnCourseCategory = u"" lastRiderOnCourseTime = u"" changed |= SetLabel(self.lastRiderOnCourseName, lastRiderOnCourseName) changed |= SetLabel(self.lastRiderOnCourseTeam, lastRiderOnCourseTeam) changed |= SetLabel(self.lastRiderOnCourseCategory, lastRiderOnCourseCategory) changed |= SetLabel(self.lastRiderOnCourseTime, lastRiderOnCourseTime) if changed: Utils.LayoutChildResize(self.raceStartTime) def refreshAll(self): self.refreshRaceTime() self.refreshLaps() def commit(self): pass def onPaint(self, event): if self.firstTimeDraw: self.firstTimeDraw = False self.splitter.SetSashPosition(SplitterMinPos) event.Skip() def refreshInputUpdate(self): self.refreshLaps() self.refreshRiderLapCountList() self.refreshLastRiderOnCourse() def refresh(self): self.clock.Start() race = Model.race enable = bool(race and race.isRunning()) if self.isEnabled != enable: self.keypad.Enable(enable) self.timeTrialRecord.Enable(enable) self.isEnabled = enable if not enable and self.isKeypadInputMode(): self.keypad.numEdit.SetValue("") self.photoCount.Show(bool(race and race.enableUSBCamera)) self.photoButton.Show(bool(race and race.enableUSBCamera)) # Refresh the race start time. changed = False rst, rstSource = "", "" if race and race.startTime: st = race.startTime if race.enableJChipIntegration and race.resetStartClockOnFirstTag: if race.firstRecordedTime: rstSource = _("Chip Start") else: rstSource = _("Waiting...") else: rstSource = _("Manual Start") rst = "{:02d}:{:02d}:{:02d}.{:02d}".format(st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) changed |= SetLabel(self.raceStartMessage, rstSource) changed |= SetLabel(self.raceStartTime, rst) self.refreshInputUpdateNonBusy() if self.isKeypadInputMode(): wx.CallLater(100, self.keypad.numEdit.SetFocus) if self.isTimeTrialInputMode(): wx.CallAfter(self.timeTrialRecord.refresh)
class NumKeypad( wx.Panel ): SwitchToTimeTrialEntryMessage = _('Switch to Time Trial Entry') SwitchToNumberEntryMessage = _('Switch to Regular Number Entry') def __init__( self, parent, id = wx.ID_ANY ): wx.Panel.__init__(self, parent, id) self.bell = None self.lapReminder = {} self.SetBackgroundColour( wx.WHITE ) fontPixels = 43 font = wx.FontFromPixelSize(wx.Size(0,fontPixels), wx.DEFAULT, wx.NORMAL, wx.NORMAL) verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) panel.SetSizer( horizontalMainSizer ) #------------------------------------------------------------------------------- # Create the edit field, numeric keypad and buttons. self.keypad = Keypad( panel, self ) horizontalMainSizer.Add( self.keypad, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) self.timeTrialRecord = TimeTrialRecord( panel, self ) self.timeTrialRecord.Show( False ) self.horizontalMainSizer = horizontalMainSizer #------------------------------------------------------------------------------ # Race time. labelAlign = wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL self.raceTime = wx.StaticText( panel, label = u'00:00') self.raceTime.SetFont( font ) self.raceTime.SetDoubleBuffered(True) self.keypadBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'keypad.png'), wx.BITMAP_TYPE_PNG ) self.ttRecordBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'stopwatch.png'), wx.BITMAP_TYPE_PNG ) self.keypadTimeTrialToggleButton = wx.BitmapButton( panel, bitmap = self.ttRecordBitmap ) self.keypadTimeTrialToggleButton.Bind( wx.EVT_BUTTON, self.swapKeypadTimeTrialRecord ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) horizontalMainSizer.Add( verticalSubSizer ) hs = wx.BoxSizer( wx.HORIZONTAL ) hs.Add( self.keypadTimeTrialToggleButton, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border = 8 ) hs.Add( self.raceTime, flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL, border=100-40-8 ) verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALIGN_CENTRE_VERTICAL | wx.ALL, border = 2 ) #------------------------------------------------------------------------------ # Lap Management. gbs = wx.GridBagSizer(4, 12) labelAlign = wx.ALIGN_RIGHT | wx.ALIGN_CENTRE_VERTICAL fontSize = 14 font = wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) rowCur = 0 colCur = 0 self.automaticManualChoice = wx.Choice( panel, choices = [_('Automatic'), _('Manual')], size=(132,-1) ) self.Bind(wx.EVT_CHOICE, self.doChooseAutomaticManual, self.automaticManualChoice) self.automaticManualChoice.SetSelection( 0 ) self.automaticManualChoice.SetFont( font ) gbs.Add( self.automaticManualChoice, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _('Total Laps')) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.numLaps = wx.Choice( panel, choices = [''] + ['{}'.format(x) for x in xrange(2,21)], size=(64,-1) ) self.numLaps.SetSelection( 0 ) self.numLaps.SetFont( font ) self.numLaps.SetDoubleBuffered( True ) self.Bind(wx.EVT_CHOICE, self.doChangeNumLaps, self.numLaps) gbs.Add( self.numLaps, pos=(rowCur, colCur+1), span=(1,1) ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Leader Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.leaderFinishTime = wx.StaticText( panel, label = u"") self.leaderFinishTime.SetFont( font ) gbs.Add( self.leaderFinishTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Last Rider Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lastRiderFinishTime = wx.StaticText( panel ) self.lastRiderFinishTime.SetFont( font ) gbs.Add( self.lastRiderFinishTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Avg Lap Time")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.leadersLapTime = wx.StaticText( panel ) self.leadersLapTime.SetFont( font ) gbs.Add( self.leadersLapTime, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Completing Lap")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lapCompleting = wx.StaticText( panel ) self.lapCompleting.SetFont( font ) gbs.Add( self.lapCompleting, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Show Laps to Go")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.lapsToGo = wx.StaticText( panel ) self.lapsToGo.SetFont( font ) gbs.Add( self.lapsToGo, pos=(rowCur, colCur+1), span=(1,1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 rowCur += 1 label = wx.StaticText( panel, label = _("Manual Start")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.raceStartMessage = label self.raceStartTime = wx.StaticText( panel ) self.raceStartTime.SetFont( font ) gbs.Add( self.raceStartTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Leader Finish")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.estLeaderTime = wx.StaticText( panel ) self.estLeaderTime.SetFont( font ) gbs.Add( self.estLeaderTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 label = wx.StaticText( panel, label = _("Est. Last Rider Finish")) label.SetFont( font ) gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.estLastRiderTime = wx.StaticText( panel ) self.estLastRiderTime.SetFont( font ) gbs.Add( self.estLastRiderTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) self.photoCount = wx.StaticText( panel, label = u"000004" ) self.photoCount.SetFont( font ) self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT|wx.ALIGN_RIGHT, border = 6 ) bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) self.photoButton = wx.BitmapButton( panel, bitmap = bitmap ) self.photoButton.SetToolTip(wx.ToolTip(_('Show Last Photos...'))) self.photoButton.Bind( wx.EVT_BUTTON, self.onPhotoButton ) self.hbClockPhoto.Add( self.photoButton, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) if not HasPhotoFinish(): self.photoButton.Disable() label = wx.StaticText( panel, label = _("Clock") ) label.SetFont( font ) self.hbClockPhoto.Add( label, flag=wx.ALIGN_CENTRE_VERTICAL ) gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1), flag=labelAlign ) self.clockTime = wx.StaticText( panel ) self.clockTime.SetFont( font ) gbs.Add( self.clockTime, pos=(rowCur, colCur+1), span=(1, 1), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_LEFT ) rowCur += 1 rowCur += 1 self.message = wx.StaticText( panel ) self.message.SetFont( font ) self.message.SetDoubleBuffered( True ) gbs.Add( self.message, pos=(rowCur, colCur), span=(1, 2), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_CENTRE ) rowCur += 1 verticalSubSizer.Add( gbs, flag=wx.LEFT|wx.TOP, border = 8 ) #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) rcVertical.AddSpacer( 32 ) title = wx.StaticText( panel, label = _('Riders on Course:') ) title.SetFont( wx.Font(fontSize, wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) rcVertical.Add( title, flag=wx.ALL, border = 4 ) self.lapCountList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) self.lapCountList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) self.lapCountList.InsertColumn( 0, _('Category'), wx.LIST_FORMAT_LEFT, 80 ) self.lapCountList.InsertColumn( 1, _('Count'), wx.LIST_FORMAT_RIGHT, 70 ) self.lapCountList.InsertColumn( 2, '', wx.LIST_FORMAT_LEFT, 90 ) rcVertical.Add( self.lapCountList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) #---------------------------------------------------------------------------------------------- self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN ) splitter.SetMinimumPaneSize( 20 ) splitter.SplitHorizontally( panel, self.raceHUD, -100 ) verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) self.SetSizer( verticalMainSizer ) self.isEnabled = True self.splitter = splitter self.notDrawnYet = True self.refreshRaceTime() def isKeypadInputMode( self ): return self.keypadTimeTrialToggleButton.GetBitmapLabel() == self.ttRecordBitmap def isTimeTrialInputMode( self ): return not self.isKeypadInputMode() def swapKeypadTimeTrialRecord( self, event = None ): if self.isKeypadInputMode(): self.keypad.Show( False ) self.timeTrialRecord.Show( True ) self.timeTrialRecord.refresh() self.horizontalMainSizer.Replace( self.keypad, self.timeTrialRecord ) self.keypadTimeTrialToggleButton.SetBitmapLabel( self.keypadBitmap ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToNumberEntryMessage)) wx.CallAfter( self.timeTrialRecord.Refresh ) wx.CallAfter( self.timeTrialRecord.grid.SetFocus ) else: self.keypad.Show( True ) self.timeTrialRecord.Show( False ) self.horizontalMainSizer.Replace( self.timeTrialRecord, self.keypad ) self.keypadTimeTrialToggleButton.SetBitmapLabel( self.ttRecordBitmap ) self.keypadTimeTrialToggleButton.SetToolTip(wx.ToolTip(self.SwitchToTimeTrialEntryMessage)) wx.CallAfter( self.keypad.Refresh ) wx.CallAfter( self.keypad.numEdit.SetFocus ) self.horizontalMainSizer.Layout() self.GetSizer().Layout() wx.CallAfter( self.Refresh ) def setKeypadInput( self, b = True ): if b: if not self.isKeypadInputMode(): self.swapKeypadTimeTrialRecord() else: if not self.isTimeTrialInputMode(): self.swapKeypadTimeTrialRecord() def setTimeTrialInput( self, b = True ): self.setKeypadInput( not b ) def refreshRaceHUD( self ): # Assumes Model is locked. race = Model.race if not race: self.raceHUD.SetData() return results = GetResults( None, False ) if not results: self.raceHUD.SetData() return tCur = race.curRaceTime() raceTimes = [] leader = [] categoryRaceTimes = {} categories_seen = set() for rr in results: if rr.status != Model.Rider.Finisher or not rr.raceTimes: continue category = race.getCategory( rr.num ) if category in categories_seen: # If we have not seen this category, this is not the leader. # Make sure we update the red lantern time. newRaceTimes = categoryRaceTimes[category] if rr.raceTimes[-1] > newRaceTimes[-1]: newRaceTimes[-1] = rr.raceTimes[-1] continue categories_seen.add( category ) leader.append( '%s %d' % (category.fullname if category else _('<Missing>'), rr.num) ) # Add a copy of the race times. Append the leader's last time as the current red lantern. raceTimes.append( rr.raceTimes + [rr.raceTimes[-1]] ) categoryRaceTimes[category] = raceTimes[-1] try: tLeader = rr.raceTimes[bisect.bisect_left( rr.raceTimes, tCur )] - tCur except IndexError: continue if 0.0 <= tLeader <= 3.0 and not getattr(race, 'isTimeTrial', False): if category not in self.lapReminder: self.lapReminder[category] = Utils.PlaySound( 'reminder.wav' ) elif category in self.lapReminder: del self.lapReminder[category] self.raceHUD.SetData( nowTime = tCur, raceTimes = raceTimes, leader = leader ) def refreshRaceTime( self ): tClockStr = '' with Model.LockRace() as race: if race is not None: tRace = race.lastRaceTime() tStr = Utils.formatTime( tRace ) self.refreshRaceHUD() if getattr(race, 'enableUSBCamera', False) and HasPhotoFinish(): self.photoButton.Show( True ) self.photoCount.SetLabel( '{}'.format(getattr(race, 'photoCount', '')) ) else: self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) if race.isRunning(): tNow = datetime.datetime.now() tClockStr = '%02d:%02d:%02d' % (tNow.hour, tNow.minute, tNow.second) else: tStr = '' tRace = None self.photoButton.Show( False ) self.photoCount.SetLabel( '' ) self.raceTime.SetLabel( ' ' + tStr ) self.clockTime.SetLabel( tClockStr ) self.hbClockPhoto.Layout() mainWin = Utils.mainWin if mainWin is not None: try: mainWin.refreshRaceAnimation() except: pass mainWin.forecastHistory.updatedExpectedTimes( tRace ) def onPhotoButton( self, event ): if not Utils.mainWin or not HasPhotoFinish(): return Utils.mainWin.photoDialog.Show( True ) Utils.mainWin.photoDialog.refresh( Utils.mainWin.photoDialog.ShowAllPhotos ) def doChangeNumLaps( self, event ): with Model.LockRace() as race: if race and race.isFinished(): try: numLaps = int(self.numLaps.GetString(self.numLaps.GetSelection())) if race.numLaps != numLaps: race.numLaps = numLaps race.setChanged() except ValueError: pass self.refreshLaps() def doChooseAutomaticManual( self, event ): with Model.LockRace() as race: if race is not None: race.automaticManual = self.automaticManualChoice.GetSelection() self.refreshLaps() def resetLaps( self, enable = False ): # Assumes Model is locked. infoFields = [ self.leaderFinishTime, self.lastRiderFinishTime, self.leadersLapTime, self.lapCompleting, self.lapsToGo, self.estLeaderTime, self.estLastRiderTime, self.message ] for f in infoFields: f.Enable( enable ) changed = False race = Model.race if race is None or not race.isFinished() or race.numLaps is None: if race is not None and self.automaticManualChoice.GetSelection() != 0: self.numLaps.SetItems( ['{}'.format(x) for x in xrange(1, 16)] ) self.numLaps.SetSelection( 4 ) else: self.numLaps.SetItems( [''] ) self.numLaps.SetSelection( 0 ) for f in infoFields: SetLabel( f, '' ) changed |= SetLabel( self.lapCompleting, '1' ) changed |= SetLabel( self.message, _('Collecting Data') ) else: self.numLaps.SetItems( [ '{}'.format(race.numLaps) ] ) self.numLaps.SetSelection( 0 ) changed |= SetLabel( self.leaderFinishTime, Utils.formatTime(GetLeaderFinishTime()) ) changed |= SetLabel( self.lastRiderFinishTime, Utils.formatTime(GetLastFinisherTime()) ) changed |= SetLabel( self.leadersLapTime, Utils.formatTime(race.getLeaderLapTime()) ) changed |= SetLabel( self.lapCompleting, '{}'.format(race.numLaps if race.numLaps is not None else 0) ) changed |= SetLabel( self.lapsToGo, '0' ) changed |= SetLabel( self.message, '' ) changed |= self.updateEstFinishTime() if changed: Utils.LayoutChildResize( self.message ) self.refreshRaceHUD() def getLapInfo( self ): # Assumes Model is locked. # Returns (laps, lapsToGo, lapCompleting, # leadersExpectedLapTime, leaderNum, # raceFinishTime, isAutomatic) race = Model.race lapCompleting = 1 if not race: return (None, None, lapCompleting, None, None, None, False) leaderTimes, leaderNums = race.getLeaderTimesNums() if not leaderTimes: return (None, None, lapCompleting, None, None, None, False) t = race.curRaceTime() lapCompleting = bisect.bisect_right( leaderTimes, t ) - 1 if lapCompleting < 2: return (None, None, lapCompleting, None, None, None, False) raceTime = race.minutes * 60.0 leadersExpectedLapTime = race.getLeaderLapTime() isAutomatic = False if self.automaticManualChoice.GetSelection() == 0: laps = bisect.bisect_left( leaderTimes, raceTime, hi=len(leaderTimes)-1 ) if leaderTimes[laps] - raceTime > leadersExpectedLapTime * 0.5: laps -= 1 isAutomatic = True else: try: laps = int(self.numLaps.GetString(self.numLaps.GetSelection())) except ValueError: laps = max(0, len(leaderTimes)-1) laps = max( laps, 0 ) raceFinishTime = leaderTimes[laps] if laps < len(leaderTimes) else None leaderNum = leaderNums[laps] if laps < len(leaderNums) else None lapsToGo = max(0, laps - lapCompleting) return (laps, lapsToGo, lapCompleting, leadersExpectedLapTime, leaderNum, raceFinishTime, isAutomatic) def getMinMaxLapsRange( self ): race = Model.race curLaps = race.numLaps if race.numLaps is not None else 1 minLaps = max(1, curLaps-5) maxLaps = minLaps + 10 return minLaps, maxLaps def updateEstFinishTime( self ): race = Model.race changed = False if not race or getattr(race, 'isTimeTrial', False): changed |= SetLabel( self.estLeaderTime, '' ) changed |= SetLabel( self.estLastRiderTime, '' ) return changed try: changed |= SetLabel( self.estLeaderTime, (race.startTime + datetime.timedelta(seconds = GetLeaderFinishTime())).strftime('%H:%M:%S') ) changed |= SetLabel( self.estLastRiderTime, (race.startTime + datetime.timedelta(seconds = GetLastFinisherTime())).strftime('%H:%M:%S') ) except: changed |= SetLabel( self.estLeaderTime, '' ) changed |= SetLabel( self.estLastRiderTime, '' ) return changed def refreshLaps( self ): with Model.LockRace() as race: enable = bool(race and race.isRunning()) allCategoriesHaveRaceLapsDefined = bool(race and getattr(race, 'allCategoriesHaveRaceLapsDefined', False)) if allCategoriesHaveRaceLapsDefined: self.automaticManualChoice.Enable( False ) self.automaticManualChoice.SetSelection( 0 ) # Default to Automatic and do not allow edit. self.numLaps.SetItems( ['{}'.format(getattr(race, 'categoryLapsMax', 0))] ) self.numLaps.Enable( False ) else: self.automaticManualChoice.Enable( enable ) self.automaticManualChoice.SetSelection( getattr(race, 'automaticManual', 0) ) # Allow the number of laps to be changed after the race is finished. numLapsEnable = bool(not allCategoriesHaveRaceLapsDefined and race and (race.isRunning() or race.isFinished())) self.numLaps.Enable( numLapsEnable ) if numLapsEnable != enable: self.resetLaps( enable ) minLaps, maxLaps = self.getMinMaxLapsRange() self.numLaps.SetItems( ['{}'.format(x) for x in xrange(minLaps, maxLaps)] ) if race.numLaps is not None: for i, v in enumerate(self.numLaps.GetItems()): if int(v) == race.numLaps: self.numLaps.SetSelection( i ) break if not enable: return if not enable: self.resetLaps() return laps, lapsToGo, lapCompleting, leadersExpectedLapTime, leaderNum, expectedRaceFinish, isAutomatic = self.getLapInfo() if laps is None: self.resetLaps( True ) SetLabel( self.lapCompleting, '{}'.format(lapCompleting) if lapCompleting is not None else '1' ) return if isAutomatic and lapCompleting >= 2: if self.numLaps.GetString(0) != '{}'.format(lapCompleting): self.numLaps.SetItems( ['{}'.format(x) for x in xrange(lapCompleting, laps+5)] ) self.numLaps.SetSelection( 0 ) if self.numLaps.GetString(self.numLaps.GetSelection()) != '{}'.format(laps): for i, v in enumerate(self.numLaps.GetItems()): if int(v) == laps: self.numLaps.SetSelection( i ) break raceMessage = { 0:_('Finishers Arriving'), 1:_('Ring Bell'), 2:_('Prepare Bell') } changed = False # Set the projected finish time and laps. if lapCompleting >= 1 or not isAutomatic: changed |= SetLabel( self.leaderFinishTime, Utils.formatTime(expectedRaceFinish) ) changed |= SetLabel( self.lastRiderFinishTime, Utils.formatTime(GetLastFinisherTime()) ) changed |= SetLabel( self.leadersLapTime, Utils.formatTime(leadersExpectedLapTime) ) changed |= SetLabel( self.lapsToGo, '{}'.format(lapsToGo) ) changed |= SetLabel( self.lapCompleting, '{}'.format(lapCompleting) ) changed |= self.updateEstFinishTime() if lapsToGo == 2 and race.isLeaderExpected(): changed |= SetLabel( self.message, _('{}: Leader Bell Lap Alert').format(leaderNum) ) elif lapsToGo == 1 and race.isLeaderExpected(): changed |= SetLabel( self.message, _('{}: Leader Finish Alert').format(leaderNum) ) else: changed |= SetLabel( self.message, raceMessage.get(lapsToGo, '') ) if race.numLaps != laps: race.numLaps = laps race.setChanged() if race.allRidersFinished(): race.finishRaceNow() else: if self.numLaps.GetSelection() != 0: self.numLaps.SetSelection( 0 ) changed |= SetLabel( self.leaderFinishTime, '' ) changed |= SetLabel( self.lastRiderFinishTime, '' ) changed |= SetLabel( self.leadersLapTime, '' ) changed |= SetLabel( self.lapsToGo, '' ) changed |= SetLabel( self.lapCompleting, '{}'.format(lapCompleting) ) changed |= SetLabel( self.estLeaderTime, '' ) changed |= SetLabel( self.estLastRiderTime, '' ) changed |= SetLabel( self.message, _('Collecting Data') ) if race.numLaps is not None: race.numLaps = None race.setChanged() if changed: Utils.LayoutChildResize( self.message ) wx.CallAfter( self.refreshRaceHUD ) def refreshRiderLapCountList( self ): self.lapCountList.DeleteAllItems() with Model.LockRace() as race: if not race or not race.isRunning(): return results = GetResults( None, False ) if not results: return catLapCount = defaultdict(int) catCount = defaultdict(int) catRaceCount = defaultdict(int) with Model.LockRace() as race: t = Model.race.curRaceTime() for rr in results: category = race.getCategory( rr.num ) catCount[category] += 1 if rr.status != Model.Rider.Finisher: continue numLaps = race.getCategoryBestLaps( category ) numLaps = (numLaps if numLaps else 1) tSearch = t if race.isTimeTrial: try: tSearch -= race[rr.num].firstTime except: pass lap = max( 1, bisect.bisect_left(rr.raceTimes, tSearch) ) if lap <= numLaps: # Rider is still on course. key = (category, lap, numLaps) catLapCount[key] += 1 catRaceCount[category] += 1 if not catLapCount: return catLapList = [(category, lap, categoryLaps, count) for (category, lap, categoryLaps), count in catLapCount.iteritems()] catLapList.sort( key=lambda x: (x[0].getStartOffsetSecs(), x[0].fullname, -x[1]) ) def appendListRow( row = tuple(), colour = None, bold = None ): r = self.lapCountList.InsertStringItem( sys.maxint, '{}'.format(row[0]) if row else '' ) for c in xrange(1, len(row)): self.lapCountList.SetStringItem( r, c, '{}'.format(row[c]) ) if colour is not None: item = self.lapCountList.GetItem( r ) item.SetTextColour( colour ) self.lapCountList.SetItem( item ) if bold is not None: item = self.lapCountList.GetItem( r ) font = self.lapCountList.GetFont() font.SetWeight( wx.FONTWEIGHT_BOLD ) item.SetFont( font ) self.lapCountList.SetItem( item ) return r appendListRow( ('Total', '%d/%d' % ( sum(count for count in catLapCount.itervalues()), sum(count for count in catCount.itervalues())) ), colour=wx.BLUE, bold=True ) lastCategory = None for category, lap, categoryLaps, count in catLapList: if category != lastCategory: appendListRow( (category.fullname, '%d/%d' % (catRaceCount[category], catCount[category]), ('(%d lap%s)' % (categoryLaps, 's' if categoryLaps > 1 else '')) ), bold = True ) appendListRow( ('', count, _('on lap {}').format(lap)) ) lastCategory = category def refresh( self ): if self.notDrawnYet: self.notDrawnYet = False self.splitter.SetSashPosition( 446 ) if self.isKeypadInputMode(): wx.CallAfter( self.keypad.numEdit.SetFocus ) if self.isTimeTrialInputMode(): wx.CallAfter( self.timeTrialRecord.refresh ) with Model.LockRace() as race: enable = bool(race and race.isRunning()) if self.isEnabled != enable: self.keypad.Enable( enable ) self.timeTrialRecord.Enable( enable ) self.isEnabled = enable if not enable and self.isKeypadInputMode(): self.keypad.numEdit.SetValue( '' ) # Refresh the race start time. changed = False rst, rstSource = '', '' if race and race.startTime: st = race.startTime if getattr(race, 'enableJChipIntegration', False) and \ getattr(race, 'resetStartClockOnFirstTag', False): if getattr(race, 'firstRecordedTime', None): rstSource = _('Chip Start') else: rstSource = _('Waiting...') else: rstSource = _('Manual Start') rst = '%02d:%02d:%02d.%02d' % (st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) changed |= SetLabel( self.raceStartMessage, rstSource ) changed |= SetLabel( self.raceStartTime, rst ) if changed: Utils.LayoutChildResize( self.raceStartTime ) wx.CallAfter( self.refreshLaps ) wx.CallAfter( self.refreshRiderLapCountList )