Skip to content

Commit

Permalink
Release v3.1.60-20240811101149
Browse files Browse the repository at this point in the history
  • Loading branch information
esitarski committed Aug 11, 2024
2 parents 2699e8a + d50f420 commit 22fd252
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ class Categories( wx.Panel ):
DistanceTypeChoices = [_('Lap'),_('Race')]

def __init__( self, parent, id = wx.ID_ANY ):
wx.Panel.__init__(self, parent, id)
super().__init__( parent, id )

self.state = RaceInputState()

Expand Down
2 changes: 1 addition & 1 deletion DNSManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, parent, ID = wx.ID_ANY, pos=wx.DefaultPosition,

class DNSManager( wx.Panel, listmix.ColumnSorterMixin ):
def __init__( self, parent, id = wx.ID_ANY ):
wx.Panel.__init__(self, parent, id)
super().__init__( parent, id )

self.category = None

Expand Down
4 changes: 2 additions & 2 deletions GanttChartPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ def DoGetBestSize(self):
return wx.Size(128, 100)

def SetForegroundColour(self, colour):
wx.Panel.SetForegroundColour(self, colour)
super().SetForegroundColour( colour )
self.Refresh()

def SetBackgroundColour(self, colour):
wx.Panel.SetBackgroundColour(self, colour)
super().SetBackgroundColour( colour )
self.Refresh()

def GetDefaultAttributes(self):
Expand Down
4 changes: 2 additions & 2 deletions GapChartPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ def DoGetBestSize(self):
return wx.Size(128, 100)

def SetForegroundColour(self, colour):
wx.Panel.SetForegroundColour(self, colour)
super().SetForegroundColour( colour )
self.Refresh()

def SetBackgroundColour(self, colour):
wx.Panel.SetBackgroundColour(self, colour)
super().SetBackgroundColour( colour )
self.Refresh()

def GetDefaultAttributes(self):
Expand Down
2 changes: 1 addition & 1 deletion Histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Histogram(wx.Control):

def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator,
name="LineGraph"):
name="HistogramGraph"):

super().__init__(parent, id, pos, size, style, validator, name)
self.SetBackgroundColour('white')
Expand Down
335 changes: 335 additions & 0 deletions LapsToGoCount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
import wx
from collections import defaultdict
from bisect import bisect_right

import Model
from GetResults import GetResults
import Utils

from collections.abc import Iterable

class DCStyle:
# Class to support setting wxPython dc elements, and restoring them from a "with" statement.
#
# Example:
#
# ...
# dc.SetPen( wx.RED_PEN )
# dc.SetBrush( wx.WHITE_BRUSH )
# ...
#
# with DCStyle( dc, Pen=wx.BLACK_PEN, Brush=wx.TRANSPARENT_BRUSH ):
# ... do some drawing in the DC using the black Pen and and transparent Brush.
# ... other settings of the dc will remain unchanged.
# ... do not use SetPen or SetBrush in the "with" block.
#
# ... Pen and Brush are automatically restored to their previous values (red, white).
#
# Explanation: The Pen and Brush are set to the new values inside the "with".
# After the "with" block, the Pen and Brush are restored to the previous values.
#
# It is imperative that you do not modify elements of the dc state inside the "with" block
# (eg. SetPen, SetBrush). Otherwise, they may not be restored.
#
# If you with to use Set within the block, you can force them to be restored by specifying None
# as the value. For example:
#
# with DCStyle( dc, Pen=None, Brush=None ):
# dc.SetPen( wx.BLACK_PEN )
# dc.SetBrush( wx.TRANSPARENT_BRUSH )
# ...
# dc.SetBrush( wx.RED_BRUSH )
# .. OK to call SetPen and SetBrush in the block as we indicated None as the values.
#
# When DCStyle is used without any keyword arguments, it restores the full state of the dc after the "with" block.
# This allows you to change any state in the dc conventionally:
#
# Example:
#
# with DCStyle( dc ):
# dc.SetPen( wx.BLACK_PEN )
# dc.SetBrush( wx.TRANSPARENT_BRUSH )
# dc.SetBackground( wx.RED )
# ...
#
# The dc's full state will be restored after the "with".
# Note: this is slower (and less clear, IMHO) than the previous examples.
#
# Finally, you can create DCStyle contexts ahead of time and reuse them. For example:
#
# titleStyle = DCStyle( dc, Font=wx.Font(wx.FontInfo(24).Bold() )
# linkStyle = DCStyle( dc, Font=wx.Font(wx.FontInfo(12).Italic().Underline()), TextForeground=wx.Colour(0,0,200) )
#
# Now you can do:
#
# with titleStyle:
# # draw a title
# dc.DrawText( "A Title', x, y )
#
# with linkStyle:
# # draw a link
# dc.DrawText( "link_to_cool_site', x, y )
#
# This make it easier to maintain formatting styles from a common location.

# Accepted arguments
valid_kw = {
'AxisOrientation',
'Background',
'BackgroundMode',
'Brush',
'ClippingRegion',
'DeviceClippingRegion',
'DeviceOrigin',
'Font',
'LayoutDirection',
'LogicalFunction',
'LogicalOrigin',
'LogicalScale',
'MapMode',
'Palette',
'Pen',
'TextBackground',
'TextForeground',
'TransformMatrix',
'UserScale',
}

def __init__(self, dc, /, **kwargs):
self.dc = dc
if kwargs:
# Check for invalid state parameters.
if not all( k in self.valid_kw for k in kwargs.keys() ):
for k in kwargs.keys():
if k not in self.valid_kw:
raise ValueError( f'Invalid argument: "{k}"' )
self.contextNew = kwargs.copy()

def __enter__(self):
if not hasattr(self, 'contextNew'):
# Cache all current state values to restore on __exit__.
self.settersRestore = [('Set'+k, getattr(self.dc, 'Get'+k)()) for k in self.valid_kw]
else:
# Only cache what is different from the current dc.
self.settersRestore = []
for k, vNew in self.contextNew.items():
if (vCur := getattr(self.dc, 'Get'+k)()) != vNew:
funcName = 'Set' + k
self.settersRestore.append( (funcName, vCur) )
if isinstance(vNew, Iterable):
getattr( self.dc, funcName )( *vNew )
elif vNew is not None:
getattr( self.dc, funcName )( vNew )
return self

def __exit__(self, *args):
for funcName, vNew in self.settersRestore:
if isinstance(vNew, Iterable):
getattr( self.dc, funcName )( *vNew )
else:
getattr( self.dc, funcName )( vNew )

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 t:
t = race.curRaceTime()

Finisher = Model.Rider.Finisher
lapsToGoCountCategory = defaultdict( int )
for category in race.getCategories():
statusCategory = defaultdict( int )

for rr in GetResults(category):
statusCategory[rr.status] += 1
if rr.status != Finisher or not rr.raceTimes:
continue

try:
tSearch = race.riders[rr.num].raceTimeToRiderTime( t )
except KeyError:
continue

if rr.raceTimes[-1] <= tSearch or not (lap := bisect_right(rr.raceTimes, tSearch) ):
continue

lapsToGoCountCategory[len(rr.raceTimes) - lap] += 1

ltgc[category] = sorted( lapsToGoCountCategory.items(), reverse=True )
lapsToGoCountCategory.clear()

sc[category] = statusCategory

return ltgc, sc

class LapsToGoCountGraph( wx.Control ):
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator,
name="LapsToGoGraph"):

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.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)
self.SetBackgroundStyle( wx.BG_STYLE_CUSTOM )

# Bind the events related to our control: first of all, we use a
# combination of wx.BufferedPaintDC and an empty handler for
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_SIZE, self.OnSize)

def DoGetBestSize(self):
return wx.Size(64, 128)

def SetForegroundColour(self, colour):
super().SetForegroundColour( colour )
self.Refresh()

def SetBackgroundColour(self, colour):
super().SetBackgroundColour( colour )
self.Refresh()

def ShouldInheritColours(self):
return True

def OnPaint(self, event ):
dc = wx.PaintDC(self)
self.Draw(dc)

def OnSize(self, event):
self.Refresh()
event.Skip()

def Draw( self, dc ):
size = self.GetClientSize()
width, height = size.width, size.height

backColour = self.GetBackgroundColour()
greyColour = wx.Colour( 196, 196, 196 )

greyPen = wx.Pen( greyColour )

backBrush = wx.Brush(backColour, wx.SOLID)
greyBrush = wx.Brush( greyColour, wx.SOLID )

lightGreyBrush = wx.Brush( wx.Colour(220,220,220), wx.SOLID )

dc.SetBackground(backBrush)
dc.Clear()

lapsToGoCount, statusCount = LapsToGoCount()
if not lapsToGoCount or width < 100 or height < 64:
return

race = Model.race
categories = race.getCategories()

yTop = xLeft = int( min( height * 0.03, width * 0.03 ) )

xRight = width - xLeft
yBottom = height - yTop

catHeight = int( (yBottom-yTop) / len(categories) )
catLabelFontHeight = min( 12, int(catHeight * 0.1) )
catLabelHeight = int( catLabelFontHeight * 2.5 )

catFieldHeight = catHeight - 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 ):
statusNames = Model.Rider.statusNames
translate = _
t = []
for status in self.statusKeys:
if count := sc.get(status, 0):
sName = translate(statusNames[status].replace('Finisher','Competing'))
t.append( f'{count}={sName}' )
return ' | '.join( t )

titleStyle = DCStyle( dc, Font=catLabelFontBold )

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 )

# Draw the lap bars.
ltg = lapsToGoCount[cat]
barCount = 1
if ltg:
barCount = ltg[0][0] - ltg[-1][0] + 1

# 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 )

# Draw the bars and labels.
countTotal = sum( count for lap, count in ltg )
dc.SetPen( greyPen )
dc.SetFont( catLabelFont )
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")}'
tWidth = dc.GetTextExtent( s ).width
if tWidth >= barWidth - 2:
s = f'{count} @ {lap}'
tWidth = dc.GetTextExtent( s ).width
if tWidth >= barWidth - 2:
s = f'{count}@{lap}'
tWidth = dc.GetTextExtent( s ).width
else:
s = f'{count} {_("Finished")}'
tWidth = dc.GetTextExtent( s ).width
if tWidth >= barWidth - 2:
s = f'{count} @ {_("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 )

# 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
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) )

yCur += catHeight

def OnEraseBackground(self, event):
# This is intentionally empty.
pass

if __name__ == '__main__':
Utils.disable_stdout_buffering()
app = wx.App(False)
mainWin = wx.Frame(None,title="CrossMan", size=(600,400))
Model.setRace( Model.Race() )
Model.getRace()._populate()
ltg = LapsToGoCountGraph( mainWin )
mainWin.Show()
app.MainLoop()
Loading

0 comments on commit 22fd252

Please sign in to comment.