Package bosco :: Module course
[hide private]
[frames] | no frames]

Source Code for Module bosco.course

  1  #/usr/bin/env python 
  2  # 
  3  #    Copyright (C) 2008  Gaudenz Steinlin <gaudenz@soziologie.ch> 
  4  # 
  5  #    This program is free software: you can redistribute it and/or modify 
  6  #    it under the terms of the GNU General Public License as published by 
  7  #    the Free Software Foundation, either version 3 of the License, or 
  8  #    (at your option) any later version. 
  9  # 
 10  #    This program is distributed in the hope that it will be useful, 
 11  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  #    GNU General Public License for more details. 
 14  # 
 15  #    You should have received a copy of the GNU General Public License 
 16  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 17   
 18  """ 
 19  course.py - Classes for orienteering courses. Everything here is 
 20              static during an event. Dynamic data (e.g. runs) is 
 21              handled by the classes in run.py 
 22  """ 
 23   
 24  from storm.locals import * 
 25   
 26  from datetime import timedelta 
 27   
 28  from ranking import Rankable, ValidationError, UnscoreableException 
 29  from base import MyStorm 
 30   
31 -class SIStation(Storm):
32 """SI Control Station. Each si station bleongs to a control, but each 33 control can have more than one si station (eg. if a station fails 34 during the event).""" 35 36 START = 1 37 FINISH = 2 38 CLEAR = 3 39 CHECK = 4 40 SPECIAL_MAX = 4 41 42 __storm_table__ = 'sistation' 43 44 id = Int(primary=True) 45 _control_id = Int(name='control') 46 control = Reference(_control_id, 'Control.id') 47
48 - def __init__(self, id):
49 self.id = id
50
51 -class Control(MyStorm):
52 """A control point. Control points are part of one or several courses. 53 The possible orders of the control points in a course is defined 54 with the ControlSequence relation.""" 55 56 __storm_table__ = 'control' 57 58 id = Int(primary=True) 59 code = Unicode() 60 override = Bool() 61 sistations = ReferenceSet(id, 'SIStation._control_id') 62
63 - def __init__(self, code, sistation = None, store = None):
64 """ 65 @param code: Code for this control. 66 @type code: unicode 67 @param sistation: SI-Station for this control. If this is an integer, 68 a corresponding SIStation object is created if necessary. 69 If sistation is None a SIStation with id int(code) ist added 70 if possible. 71 @type sistation: SIStation object or int 72 @param store: Storm store for the sistation. A store is needed if 73 sistation is given as int. The newly created object is 74 automatically added to this store. 75 """ 76 77 self.code = code 78 if store is not None: 79 self._store = store 80 81 if sistation is None: 82 try: 83 sistation = int(code) 84 except ValueError: 85 pass 86 87 if sistation is not None: 88 self.add_sistation(sistation)
89
90 - def add_sistation(self, sistation):
91 """Add an SIStation to this Control. 92 93 @param sistation: SI-Station for this control. If this is an integer, 94 a corresponding SIStation object is created if necessary. 95 @type sistation: SIStation object or int 96 """ 97 98 if type(sistation) == int: 99 station_nr = sistation 100 sistation = self._store.get(SIStation, sistation) 101 if sistation is None: 102 sistation = SIStation(station_nr) 103 104 self.sistations.add(sistation)
105
106 -class ControlSequence(Storm):
107 """Connects controls and courses. The sequence_number defines the 108 correct sequence of the controls. It's possible the have several 109 controls with the same sequence_number on the same course. The 110 interpretation of the sequence number is up to the validate method 111 of the course. The sequence number may be None.""" 112 113 __storm_table__ = 'controlsequence' 114 115 id = Int(primary=True) 116 length = Int() 117 climb = Int() 118 _course_id = Int(name='course') 119 course = Reference(_course_id, 'Course.id') 120 _control_id = Int(name='control') 121 control = Reference(_control_id, 'Control.id') 122 sequence_number = Int() 123
124 - def __init__(self, control, sequence_number = None, 125 length = None, climb = None):
126 self.control = control 127 self.sequence_number = sequence_number 128 self.length = length 129 self.climb = climb
130 131
132 -class BaseCourse(Rankable):
133 """Common base class for Course and CombinedCourse."""
134 135
136 -class Course(MyStorm, BaseCourse):
137 """Base class for all kinds of courses. Special kinds of courses should 138 be derived from this class. Derived class must at least override the 139 append or validate methods. This class implements an unordered set 140 of controls as a course without any validation. 141 142 The distance and altitude attributes are in meters and may be None.""" 143 144 __storm_table__ = 'course' 145 146 id = Int(primary=True) 147 code = Unicode() 148 length = Int() 149 climb = Int() 150 members = ReferenceSet(id, 'Run._course_id') 151 controls = ReferenceSet(id, ControlSequence._course_id, 152 ControlSequence._control_id, 153 Control.id, 154 order_by=ControlSequence.sequence_number) 155 sequence = ReferenceSet(id, 'ControlSequence._course_id', 156 order_by=ControlSequence.sequence_number) 157
158 - def __init__(self, code, length = None, climb = None, validator=None, scoreing=None):
159 """ 160 @param code: Descriptive code for this course. Usually 3 characters long. For 161 'normal' events this corresponds to the category name. 162 @type code: unicode 163 @param length: Length of the course in meters 164 @type length: int 165 @param climb: Altitude differences in meters 166 @type climb: int 167 """ 168 169 self.code = code 170 self.length = length 171 self.climb = climb 172 self._validator = validator 173 self._scoreing = scoreing
174
175 - def __max_index(self):
176 max = 0 177 for control in self.sequence: 178 # increase max if sequence_number is bigger 179 max = max > control.sequence_number and max or control.sequence_number 180 return max
181
182 - def __has_index(self, index):
183 for control in self.sequence: 184 if control.sequence_number == index: 185 return True 186 return False
187
188 - def append(self, control, length = None, climb = None):
189 """Append a single control to the course. 190 191 @param control: next control. The control is automatically created 192 if it does not yet exist. 193 @type control: Control object or unicode 194 """ 195 if type(control) is unicode: 196 controlcode = control 197 control = self._store.find(Control, Control.code == controlcode).one() 198 if control is None: 199 control = Control(controlcode, store = self._store) 200 201 202 self.sequence.add(ControlSequence(control, self.__max_index() + 1, 203 length, climb))
204
205 - def insert(self, control, index, length = None, climb = None):
206 """Insert an additional control into the course at an arbitrary 207 postition.""" 208 raise Exception('Not yet implemented')
209
210 - def extend(self, control_list):
211 """Extend the course with the controls from control_list.""" 212 for c in control_list: 213 self.append(c)
214
215 - def lkm(self):
216 """ 217 @return: 'Leistungskilometer': length/1000.0+climb/100.0 218 """ 219 return self.length/1000.0 + self.climb/100.0
220
221 - def expected_time(self, speed):
222 """Returns the expected time for this course. 223 @param speed: expected speed in minutes per kilometer 224 """ 225 try: 226 return timedelta(minutes=self.lkm()*speed) 227 except TypeError: 228 return None
229
230 - def controlcount(self):
231 return self.controls.count()
232
233 - def controllist(self):
234 """ 235 @return list of controls in this course with sistations that are not overriden. 236 """ 237 return [ c for c in 238 self.controls 239 if (c.sistations.count() > 0 and 240 c.override is not True) ]
241
242 - def validate(self, run):
243 """Validate a run according to this course. 244 @param run: Run to be validated. 245 @return: Validation status 246 @see: bosco.ranking.Validator 247 """ 248 if self._validator is not None: 249 return self._validator.validate(run) 250 else: 251 raise ValidationError("Can't validate a run without a validation strategy.")
252
253 - def score(self, run):
254 """Score a run according to this course. 255 @param run: Run to be scored. 256 @return: Scoreing result 257 @see: bosco.ranking.AbstractScoreing 258 """ 259 if self._scoreing is not None: 260 return self._scoreing.score(run) 261 else: 262 raise UnscoreableException("Can't score a run without a scoreing strategy.")
263
264 - def __str__(self):
265 return unicode(self).encode('utf-8')
266
267 - def __unicode__(self):
268 return self.code
269 270
271 -class CombinedCourse(BaseCourse):
272 """ 273 This class combines several courses to generate a joint ranking of all runns of 274 all the combined courses. This is primarily usefull for rankings of relay legs with 275 different variants. This class is not derived from Course and this is not a Storm object 276 and not stored in the database. 277 """ 278
279 - def __init__(self, course_list, code, store=None):
280 """ 281 @param course_list: List of courses to combine 282 @type course_list: list of either instances of Course or unicode course codes 283 @param code: Code of this course. This is only for display purposes. 284 @type code: Unicode 285 @param store: Storm store which contains the courses referenced by course 286 codes in the course list. May be None if the course list only 287 contains Course objects. 288 """ 289 self._code = code 290 self.course_list = [] 291 for c in course_list: 292 if type(c) == Course: 293 self.course_list.append(c) 294 else: 295 if store is None: 296 raise CombinedCourseException("Can't add course '%s' without a store." % c) 297 298 course = store.find(Course, Course.code == c).one() 299 if course is None: 300 raise CombinedCourseException("Can't find course with code '%s'." % c) 301 self.course_list.append(course) 302 303 self.length = self.course_list[0].length 304 self.climb = self.course_list[0].climb 305 self._controlcount = self.course_list[0].controls.count()
306
307 - def _get_members(self):
308 """Get all runs of all the courses in self.course_list.""" 309 310 runs = [] 311 for c in self.course_list: 312 runs.extend([r for r in c.members]) 313 return runs
314 members = property(_get_members) 315
316 - def controlcount(self):
317 return self._controlcount
318
319 - def __str__(self):
320 return unicode(self).encode('utf-8')
321
322 - def __unicode__(self):
323 return self._code
324
325 -class CombinedCourseException(Exception):
326 pass
327