diff --git a/LapsToGoCount.py b/LapsToGoCount.py index d5772f79..7cb98643 100644 --- a/LapsToGoCount.py +++ b/LapsToGoCount.py @@ -129,25 +129,45 @@ def __exit__(self, *args): else: getattr( self.dc, funcName )( vNew ) +@Model.memoize def LapsToGoCount( t=None ): ltgc = {} # dict indexed by category with a list of (lapsToGo, count). sc = {} # dict index by category with counts of each status. race = Model.race - if not race or race.isUnstarted() or race.isFinished(): - return ltgc + if not race or race.isUnstarted(): + return ltgc, sc + isTimeTrial = race.isTimeTrial if not t: - t = race.curRaceTime() + t = race.curRaceTime() if race.isRunning() else float('inf') Finisher = Model.Rider.Finisher + NP = Model.Rider.NP + lapsToGoCountCategory = defaultdict( int ) for category in race.getCategories(): - statusCategory = defaultdict( int ) + categoryLaps = category.getNumLaps() + statusCategory = defaultdict( int ) for rr in GetResults(category): statusCategory[rr.status] += 1 - if rr.status != Finisher or not rr.raceTimes: + + if rr.status != Finisher: + if isTimeTrial and rr.status == NP and categoryLaps is not None: + # Record TT riders who have started with the full laps. + rider = race.riders[rr.num] + # print( rider.num, rider.firstTime, t, rr.status, categoryLaps ) + if rider.firstTime is not None and t >= rider.firstTime: + lapsToGoCountCategory[categoryLaps] += 1 + # Reclassify NP to Finisher as we know the rider is on course. + statusCategory[NP] -= 1 + statusCategory[Finisher] += 1 + continue + + if not rr.raceTimes: + if isTimeTrial and categoryLaps is not None: + lapsToGoCountCategory[categoryLaps] += 1 continue try: @@ -155,10 +175,11 @@ def LapsToGoCount( t=None ): except KeyError: continue - if rr.raceTimes[-1] <= tSearch or not (lap := bisect_right(rr.raceTimes, tSearch) ): + if not (lap := bisect_right(rr.raceTimes, tSearch) ): continue + lap -= 1 - lapsToGoCountCategory[len(rr.raceTimes) - lap] += 1 + lapsToGoCountCategory[len(rr.raceTimes) - lap - 1] += 1 ltgc[category] = sorted( lapsToGoCountCategory.items(), reverse=True ) lapsToGoCountCategory.clear() @@ -174,7 +195,7 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, super().__init__(parent, id, pos, size, style, validator, name) - self.barBrushes = [wx.Brush(wx.Colour( int(c[:2],16), int(c[2:4],16), int(c[4:],16)), wx.SOLID) for c in ('D8E6AD', 'E6ADD8', 'ADD8E6')] + self.barBrushes = [wx.Brush(wx.Colour( int(c[:2],16), int(c[2:4],16), int(c[4:],16)), wx.SOLID) for c in ('A0A0A0', 'D3D3D3', 'E5E4E2')] self.statusKeys = sorted( (k for k in Model.Rider.statusSortSeq.keys() if isinstance(k, int)), key=lambda k: Model.Rider.statusSortSeq[k] ) self.SetBackgroundColour(wx.WHITE) @@ -199,7 +220,7 @@ def SetBackgroundColour(self, colour): def ShouldInheritColours(self): return True - def OnPaint(self, event ): + def OnPaint(self, event): dc = wx.PaintDC(self) self.Draw(dc) @@ -231,94 +252,118 @@ def Draw( self, dc ): race = Model.race categories = race.getCategories() - yTop = xLeft = int( min( height * 0.03, width * 0.03 ) ) + catLabelFontHeight = min( 12, int(height * 0.1) ) + catLabelHeight = int( catLabelFontHeight * 2.5 ) + yTop = catLabelHeight + xLeft = 0 xRight = width - xLeft - yBottom = height - yTop + yBottom = height catHeight = int( (yBottom-yTop) / len(categories) ) - catLabelFontHeight = min( 12, int(catHeight * 0.1) ) - catLabelHeight = int( catLabelFontHeight * 2.5 ) catFieldHeight = catHeight - catLabelHeight + barFieldHeight = catFieldHeight - catLabelHeight catLabelFont = wx.Font( wx.FontInfo(catLabelFontHeight).FaceName('Helvetica') ) catLabelFontBold = wx.Font( wx.FontInfo(catLabelFontHeight).FaceName('Helvetica').Bold() ) catLabelMargin = int( (catLabelHeight - catLabelFontHeight) / 2 ) - def statusCountStr( sc ): + Finisher = Model.Rider.Finisher + def statusCountStr( sc, lap0Count ): statusNames = Model.Rider.statusNames translate = _ t = [] + onCourseCount = 0 for status in self.statusKeys: if count := sc.get(status, 0): + if status == Finisher: + onCourseCount = count - lap0Count sName = translate(statusNames[status].replace('Finisher','Competing')) t.append( f'{count}={sName}' ) - return ' | '.join( t ) + return f"{onCourseCount}={_('OnCourse')}" + (" | " if t else '') + ' | '.join( t ) titleStyle = DCStyle( dc, Font=catLabelFontBold ) + chartLineStyle = DCStyle( dc, Pen=greyPen, Brush=wx.TRANSPARENT_BRUSH ) + lap0Total = finisherTotal = 0 yCur = yTop for cat in categories: - # Draw the lines. - dc.SetPen( greyPen ) - dc.DrawLine( xLeft, yCur + catFieldHeight, xLeft, yCur ) - dc.DrawLine( xRight, yCur + catFieldHeight, xRight, yCur ) - dc.DrawLine( xLeft, yCur + catFieldHeight, xRight, yCur + catFieldHeight ) - dc.DrawLine( xLeft, yCur, xRight, yCur ) + if barFieldHeight >= catLabelFontHeight: + # Draw the chart lines. + with chartLineStyle: + dc.DrawRectangle( xLeft, yCur, xRight-xLeft, catFieldHeight ) # Draw the lap bars. ltg = lapsToGoCount[cat] barCount = 1 if ltg: barCount = ltg[0][0] - ltg[-1][0] + 1 - + + finisherTotal += statusCount[cat].get( Finisher, 0 ) + # Compute the barwidths so they take the entire horizontal line. barWidth = (xRight - xLeft) / barCount barX = [round( xLeft + i * barWidth ) for i in range(barCount)] barX.append( xRight ) + barTextWidth = barWidth - 2 # Draw the bars and labels. countTotal = sum( count for lap, count in ltg ) dc.SetPen( greyPen ) dc.SetFont( catLabelFont ) + lap0Count = 0 for lap, count in ltg: - barHeight = round( catFieldHeight * count / countTotal ) - i = ltg[0][0] - lap - dc.SetBrush( self.barBrushes[lap%len(self.barBrushes)] ) - dc.DrawRectangle( barX[i], yCur + catFieldHeight - barHeight, barX[i+1] - barX[i], barHeight ) - if lap: - s = f'{count} @ {lap} {_("to go")}' + s = f'{lap} {_("to go")}' tWidth = dc.GetTextExtent( s ).width - if tWidth >= barWidth - 2: - s = f'{count} @ {lap}' + if tWidth >= barTextWidth: + s = f'{lap}' tWidth = dc.GetTextExtent( s ).width - if tWidth >= barWidth - 2: - s = f'{count}@{lap}' - tWidth = dc.GetTextExtent( s ).width else: - s = f'{count} {_("Finished")}' + lap0Count = count + lap0Total += count + s = f'{_("Finished")}' tWidth = dc.GetTextExtent( s ).width - if tWidth >= barWidth - 2: - s = f'{count} @ {_("Fin")}' + if tWidth >= barTextWidth: + s = f'{_("Fin")}' tWidth = dc.GetTextExtent( s ).width - if tWidth >= barWidth - 2: - s = f'{count}@{_("Fin")}' - tWidth = dc.GetTextExtent( s ).width - y = yCur + catHeight - catLabelMargin - catLabelFontHeight - x = barX[i] + (barX[i+1] - barX[i] - tWidth) // 2 - dc.DrawText( s, x, y ) + if barFieldHeight < catLabelFontHeight: + continue + + barHeight = round( barFieldHeight * count / countTotal ) + + i = ltg[0][0] - lap + dc.SetBrush( self.barBrushes[lap%len(self.barBrushes)] ) + dc.DrawRectangle( barX[i], yCur + catFieldHeight - barHeight, barX[i+1] - barX[i], barHeight ) + + if tWidth < barTextWidth: + y = yCur + catHeight - catLabelMargin - catLabelFontHeight + x = barX[i] + (barX[i+1] - barX[i] - tWidth) // 2 + dc.DrawText( s, x, y ) + + s = f'{count}' + tWidth = dc.GetTextExtent( s ).width + if tWidth < barTextWidth: + y = min( yCur + catFieldHeight - catLabelFontHeight- catLabelFontHeight//4, yCur + catFieldHeight - barHeight + catLabelFontHeight//2 ) + x = barX[i] + (barX[i+1] - barX[i] - tWidth) // 2 + dc.DrawText( s, x, y ) - # Draw the category label with the on course total. - #onCourse = countTotal - (ltg[-1][1] if ltg and ltg[-1][0] == 0 else 0) - #finished = countTotal - onCourse + # Draw the category label with the status totals. with titleStyle: - dc.DrawText( f'{cat.fullname}', xLeft + catLabelMargin, yCur + catLabelMargin ) - dc.DrawText( f'{statusCountStr(statusCount[cat])}', xLeft + catLabelMargin*4, int(yCur + catLabelMargin + catLabelFontHeight*1.75) ) + catText = f'{cat.fullname}' + titleTextWidth = dc.GetTextExtent(catText).width + dc.DrawText( catText, xLeft + catLabelMargin, yCur + catLabelMargin ) + dc.DrawText( f'{statusCountStr(statusCount[cat], lap0Count)}', xLeft + titleTextWidth + catLabelMargin*2, int(yCur + catLabelMargin) ) + + yCur += catHeight - yCur += catHeight + with titleStyle: + catText = f'{_("All")}' + titleTextWidth = dc.GetTextExtent(catText).width + dc.DrawText( catText, xLeft + catLabelMargin, catLabelFontHeight//2 ) + dc.DrawText( f'{finisherTotal-lap0Total}={_("OnCourse")}', xLeft + titleTextWidth + catLabelMargin*2, catLabelFontHeight//2 ) def OnEraseBackground(self, event): # This is intentionally empty. diff --git a/MainWin.py b/MainWin.py index 7425526e..9edbd278 100644 --- a/MainWin.py +++ b/MainWin.py @@ -2686,7 +2686,7 @@ def onCloseWindow( self, event ): self.doCleanup() wx.Exit() - @logCall + #@logCall def writeRace( self, doCommit = True ): if doCommit: self.commit() @@ -4065,6 +4065,9 @@ def refresh( self ): self.updateRaceClock() def refreshTTStart( self ): + if Model.race: + # If a rider started the TT, force the results to be re-computed if necessary. + Model.race.setChanged() if self.notebook.GetSelection() in (self.iHistoryPage, self.iRecordPage): self.refreshCurrentPage() diff --git a/NumKeypad.py b/NumKeypad.py index fb7b769c..21d5724b 100644 --- a/NumKeypad.py +++ b/NumKeypad.py @@ -20,6 +20,7 @@ from NonBusyCall import NonBusyCall from SetLaps import SetLaps from InputUtils import enterCodes, validKeyCodes, clearCodes, actionCodes, getRiderNumsFromText, MakeKeypadButton +from LapsToGoCount import LapsToGoCountGraph SplitterMinPos = 390 SplitterMaxPos = 530 @@ -419,15 +420,8 @@ def __init__( self, parent, id = wx.ID_ANY ): #------------------------------------------------------------------------------ # Rider Lap Count. rcVertical = wx.BoxSizer( wx.VERTICAL ) - rcVertical.AddSpacer( 32 ) - - self.categoryStatsList = wx.ListCtrl( panel, wx.ID_ANY, style = wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_HRULES|wx.BORDER_NONE ) - self.categoryStatsList.SetFont( wx.Font(int(fontSize*0.9), wx.DEFAULT, wx.NORMAL, wx.NORMAL) ) - self.categoryStatsList.AppendColumn( _('Category'), wx.LIST_FORMAT_LEFT, 140 ) - self.categoryStatsList.AppendColumn( _('Composition'), wx.LIST_FORMAT_LEFT, 130 ) - self.categoryStatsList.SetColumnWidth( 0, wx.LIST_AUTOSIZE_USEHEADER ) - self.categoryStatsList.SetColumnWidth( 1, wx.LIST_AUTOSIZE_USEHEADER ) - rcVertical.Add( self.categoryStatsList, 1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border = 4 ) + self.lapsToGoCountGraph = LapsToGoCountGraph( panel ) + rcVertical.Add( self.lapsToGoCountGraph, 1, flag=wx.EXPAND|wx.TOP|wx.RIGHT, border = 4 ) horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) self.horizontalMainSizer = horizontalMainSizer @@ -663,36 +657,7 @@ def refreshLaps( self ): wx.CallAfter( self.refreshRaceHUD ) def refreshRiderCategoryStatsList( self ): - self.categoryStatsList.DeleteAllItems() - race = Model.race - if not race: - return - - def appendListRow( row = tuple(), colour = None, bold = None ): - r = self.categoryStatsList.InsertItem( self.categoryStatsList.GetItemCount(), '{}'.format(row[0]) if row else '' ) - for c in range(1, len(row)): - self.categoryStatsList.SetItem( r, c, '{}'.format(row[c]) ) - if colour is not None: - item = self.categoryStatsList.GetItem( r ) - item.SetTextColour( colour ) - self.categoryStatsList.SetItem( item ) - if bold is not None: - item = self.categoryStatsList.GetItem( r ) - font = self.categoryStatsList.GetFont() - font.SetWeight( wx.FONTWEIGHT_BOLD ) - item.SetFont( font ) - self.categoryStatsList.SetItem( item ) - return r - - for catStat in getCategoryStats(): - if catStat[0] == _('All'): - colour, bold = wx.BLUE, None - else: - colour = bold = None - appendListRow( catStat, colour, bold ) - - self.categoryStatsList.SetColumnWidth( 0, wx.LIST_AUTOSIZE_USEHEADER ) - self.categoryStatsList.SetColumnWidth( 1, wx.LIST_AUTOSIZE_USEHEADER ) + self.lapsToGoCountGraph.Refresh() def refreshLastRiderOnCourse( self ): race = Model.race diff --git a/Utils.py b/Utils.py index 34490428..d8600991 100644 --- a/Utils.py +++ b/Utils.py @@ -629,8 +629,10 @@ def refresh(): mainWin.refresh() def refreshForecastHistory(): - if mainWin is not None: + try: mainWin.forecastHistory.refresh() + except AttributeError: + pass def updateUndoStatus(): if mainWin is not None: diff --git a/Version.py b/Version.py index 4f4be93f..92a277a4 100644 --- a/Version.py +++ b/Version.py @@ -1 +1 @@ -AppVerName="CrossMgr 3.1.61-private" +AppVerName="CrossMgr 3.1.62-private"