Esempio n. 1
0
	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()
Esempio n. 2
0
	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()
Esempio n. 3
0
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 )
Esempio n. 4
0
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 )
Esempio n. 5
0
	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()
Esempio n. 6
0
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 )
Esempio n. 7
0
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)
Esempio n. 8
0
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 )