Package sabx10 :: Package map :: Module elevations
[hide private]
[frames] | no frames]

Source Code for Module sabx10.map.elevations

  1  ############################################################################### 
  2  # 
  3  # sabx10 - an SABX file manipulation library 
  4  # Copyright (C) 2009, 2010 Jay Farrimond (jay@sabikerides.com) 
  5  # 
  6  # This file is part of sabx10. 
  7  # 
  8  # sabx10 is free software: you can redistribute it and/or modify it under the 
  9  # terms of the GNU General Public License as published by the Free Software 
 10  # Foundation, either version 3 of the License, or (at your option) any later 
 11  # version. 
 12  # 
 13  # sabx10 is distributed in the hope that it will be useful, but WITHOUT ANY 
 14  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 
 15  # A PARTICULAR PURPOSE.  See the GNU General Public License for more details. 
 16  # 
 17  # You should have received a copy of the GNU General Public License along with 
 18  # sabx10.  If not, see <http://www.gnu.org/licenses/>. 
 19  # 
 20  ############################################################################### 
 21  """ 
 22  Process elevation data for an SABX ride. 
 23  """ 
 24  import math 
 25   
 26  from sabx10.oxm import mile_feet, meter_feet 
 27   
 28  from consts import CLIMB_COUNT 
 29   
30 -def _filter_climbs(climbs, count):
31 """ 32 Takes a raw list of climbs, filters out climbs that are only downhill, then 33 sorts by length and returns the top "count" climbs. The returned list is 34 sorted by steepness. Each climb in the list is a tuple of (grade,length). 35 36 @param climbs: list of climbs to process 37 @type climbs: C{list} 38 @param count: how many climbs to return 39 @type count: C{int} 40 41 @return: top 'count' climbs 42 @rtype: C{list} 43 """ 44 climbs = [climb for climb in climbs if climb[0] > 0.0] 45 climbs.sort(key=lambda x: x[1], reverse=True) 46 climbs = climbs[:count] 47 climbs.sort(key=lambda x: x[0], reverse=True) 48 49 return climbs
50
51 -def _calc_rise_fall_grades(rise_height, rise_dist, fall_height, fall_dist):
52 """ 53 Calculate the rising and falling grades for a set of data. 54 55 @param rise_height: rising height 56 @type rise_height: C{float} 57 @param rise_dist: rising distance 58 @type rise_dist: C{float} 59 @param fall_height: falling height 60 @type fall_height: C{float} 61 @param fall_dist: falling distance 62 @type fall_dist: C{float} 63 64 @return: rising grade, falling grade 65 @rtype: (C{float},C{float}) 66 """ 67 rise_grade = 0.0 68 if rise_dist > 0.0: 69 rise_grade = (rise_height / (rise_dist * mile_feet)) * 100.0 70 fall_grade = 0.0 71 if fall_dist > 0.0: 72 fall_grade = (fall_height / (fall_dist * mile_feet)) * 100.0 73 return rise_grade, fall_grade
74 75 ######## PROCESS WAYPOINTS ######## 76
77 -class PointsProcessor(object):
78 """ 79 Encapsulates a set of points representing a line and all the operations we 80 might want to do on it. 81 82 @ivar intras: intra-point values used for all our calculations, in the form 83 of a list of (length,rise) tuples 84 @type intras: C{list} of (C{float},C{float}) 85 """ 86
87 - def __init__(self, points):
88 """ 89 Calculate and store the distance and rise between each pair of points 90 in the list. This list will be used by all the other methods on this 91 object. 92 93 @param points: list of waypoints 94 @type points: C{list} of L{Point} objects 95 """ 96 self.intras = [ 97 (points[index].calculate_distance(points[index+1]), 98 (points[index+1].ele - points[index].ele) * meter_feet) 99 for index in range(len(points)-1)]
100
101 - def calc_points_distance(self):
102 """ 103 Calculate the length of the line. This is the distance along the set 104 of waypoints. 105 106 @return: length of line 107 @rtype: C{float} 108 """ 109 return sum([length for length, rise in self.intras])
110
111 - def calc_points_rise(self):
112 """ 113 Calculate the cumulative rise for the line. This is the rise height 114 for every intra-point value that rises. 115 116 @return: rise height, rise distance 117 @rtype: (C{float},C{float}) 118 """ 119 rise_height = sum([rise for length, rise in self.intras if rise > 0.0]) 120 rise_dist = sum([length for length, rise in self.intras if rise > 0.0]) 121 return rise_height, rise_dist
122
123 - def calc_points_fall(self):
124 """ 125 Calculate the cumulative fall for the line. This is the rise height for 126 every intra-point value that falls. 127 128 @return: fall height, fall distance 129 @rtype: (C{float},C{float}) 130 """ 131 fall_height = sum([rise for length, rise in self.intras if rise <= 0.0]) 132 fall_dist = sum([length for length, rise in self.intras if rise <= 0.0]) 133 return fall_height, fall_dist
134
135 - def calc_points_climbs(self):
136 """ 137 Create a list of climbs in the line. Ignore single-point declines 138 during a climb, since this seems to give more realistic climbs than if 139 single "bumps" cause climbs to restart. 140 141 @return: list of climbs with (grade,distance) 142 @rtype: C{list} of (C{float},C{float}) 143 """ 144 climb_height = 0.0 145 climb_dist = 0.0 146 climbs = [] 147 in_climb = -1 148 149 for length, rise in self.intras: 150 if rise > 0.0: 151 in_climb = 1 152 climb_height += rise 153 climb_dist += length 154 else: 155 if in_climb == 0: 156 if climb_dist > 0.0: 157 climbs.append( ( 158 (climb_height / (climb_dist * mile_feet)) * 100.0, 159 climb_dist) ) 160 climb_height = 0.0 161 climb_dist = 0.0 162 elif in_climb > 0: 163 climb_height += rise # remember, it's negative 164 climb_dist += length 165 in_climb -= 1 166 167 return climbs
168
169 -class SegmentProcessor(PointsProcessor):
170 """ 171 Hold all the values we are interested in for a ride segment and create a 172 dictionary to present them. 173 174 @ivar seg: segment we are processing 175 @type seg: C{Segment} 176 @ivar start_dist: distance into ride for first point of segment 177 @type start_dist: C{float} 178 @ivar length: length of the segment 179 @type length: C{float} 180 @ivar rise_height: cumulative height of ascent in this segment 181 @type rise_height: C{float} 182 @ivar rise_dist: length of the part of the segment going up 183 @type rise_dist: C{float} 184 @ivar fall_height: cumulative height of descent in this segment 185 @type fall_height: C{float} 186 @ivar fall_dist: length of the part of the segment going down 187 @type fall_dist: C{float} 188 @ivar rise_grade: average climbing grade of segment 189 @type rise_grade: C{float} 190 @ivar fall_grade: average descending grade of segment 191 @type fall_grade: C{float} 192 @ivar climbs: list of "top" climbs for the segment 193 @type climbs: C{list} 194 """ 195
196 - def __init__(self, seg, accum):
197 """ 198 Initialize and calculate all values for the segment. 199 200 @param seg: segment to analyze 201 @type seg: L{Segment} 202 @param accum: segment accumulator 203 @type accum: L{SegmentAccumulator} 204 """ 205 PointsProcessor.__init__(self, seg.waypoints) 206 self.seg = seg 207 self.start_dist = accum.length 208 self.length = self.calc_points_distance() 209 self.rise_height, self.rise_dist = self.calc_points_rise() 210 self.fall_height, self.fall_dist = self.calc_points_fall() 211 self.rise_grade, self.fall_grade = _calc_rise_fall_grades( 212 self.rise_height, self.rise_dist, self.fall_height, self.fall_dist) 213 climbs = self.calc_points_climbs() 214 self.climbs = _filter_climbs(climbs, CLIMB_COUNT)
215
216 - def create_element(self):
217 """ 218 Create a dictionary that can be used by Jinja2 to present this data. 219 220 The dictionary contains the following: 221 - id: id of segment 222 - start_dist: distance in ride for starting point 223 - end_dist: distance in ride for ending point 224 - length: length of ride 225 - net_height: net change in elevation over segment 226 - rise_height: height of climbing in segment 227 - rise_dist: distance spent climbing in segment 228 - rise_grade: average grade of climbing in segment 229 - fall_height: height of descending in segment 230 - fall_dist: distance spent descending in segment 231 - fall_grade: average grade of descending in segment 232 - gradients: table containing gradients and how much time spent in them 233 - climbs: list with climbs, their grades, and their lengths in segment 234 235 @return: presentable data 236 @rtype: C{dict} 237 """ 238 return {'id': self.seg.id, 239 'start_dist': self.start_dist, 240 'end_dist': self.start_dist + self.length, 241 'length': self.length, 242 'net_height': self.rise_height - self.fall_height, 243 'rise_height': self.rise_height, 244 'rise_dist': self.rise_dist, 245 'rise_grade': self.rise_grade, 246 'fall_height': self.fall_height, 247 'fall_dist': self.fall_dist, 248 'fall_grade': self.fall_grade, 249 'climbs': self.climbs 250 }
251
252 -class SegmentAccumulator(object):
253 """ 254 Gather all the values we're interested in for a ride by extracting them 255 from the processed segments and accumulating them. 256 257 @ivar distance: length of the ride 258 @type distance: C{float} 259 @ivar rise_height: cumulative ascent in this ride 260 @type rise_height: C{float} 261 @ivar rise_dist: length of part of the ride ascending 262 @type rise_dist: C{float} 263 @ivar fall_height: cumulative descent in this ride 264 @type fall_height: C{float} 265 @ivar fall_dist: length of part of the ride descending 266 @type fall_dist: C{float} 267 @ivar climbs: list of "top" climbs for the ride 268 @type climbs: C{list} 269 """ 270
271 - def __init__(self):
272 """ 273 Initialize all values for the ride. 274 """ 275 self.length = 0.0 276 self.rise_height = 0.0 277 self.rise_dist = 0.0 278 self.fall_height = 0.0 279 self.fall_dist = 0.0 280 self.climbs = []
281
282 - def accumulate_segment(self, seg):
283 """ 284 Extract all relevant values from the given segment processor and add 285 them to the instance variables. 286 287 @param seg: segment processor to get data from 288 @type seg: L{SegmentProcessor} 289 """ 290 self.length += seg.length 291 self.rise_height += seg.rise_height 292 self.rise_dist += seg.rise_dist 293 self.fall_height += seg.fall_height 294 self.fall_dist += seg.fall_dist 295 self.climbs.extend(seg.climbs)
296
297 - def create_element(self):
298 """ 299 Create a dictionary that can be used by Jinja2 to present this data. 300 Calculate the values that haven't been accumulated. 301 302 The dictionary contains the following data: 303 - length: length of ride 304 - net_height: net change in elevation over ride 305 - rise_height: height of climbing in ride 306 - rise_dist: distance spent climbing in ride 307 - rise_grade: average grade of climbing in ride 308 - fall_height: height of descending in ride 309 - fall_dist: distance spent descending in ride 310 - fall_grade: average grade of descending in ride 311 - climbs: list with climbs, their grades, and their lengths in ride 312 313 @return: presentable data 314 @rtype: C{dict} 315 """ 316 rise_grade, fall_grade = _calc_rise_fall_grades( 317 self.rise_height, self.rise_dist, 318 self.fall_height, self.fall_dist) 319 climbs = _filter_climbs(self.climbs, CLIMB_COUNT) 320 321 return {'length': self.length, 322 'net_height': self.rise_height - self.fall_height, 323 'rise_height': self.rise_height, 324 'rise_dist': self.rise_dist, 325 'rise_grade': rise_grade, 326 'fall_height': self.fall_height, 327 'fall_dist': self.fall_dist, 328 'fall_grade': fall_grade, 329 'climbs': climbs 330 }
331 332 ######## PROCESS RIDE ######## 333
334 -def process_elevations(ride):
335 """ 336 This is the main entry point for this module. It goes through the 337 elevation data for a ride and creates lists of processed data. It returns 338 the data as a single dictionary describing the overall ride and a list of 339 dictionaries that each describe a segment of the ride. 340 341 Each dictionary element in the segment list has the following information: 342 - id: id of segment 343 - start_dist: distance in ride for starting point 344 - end_dist: distance in ride for ending point 345 - length: length of ride 346 - net_height: net change in elevation over segment 347 - rise_height: height of climbing in segment 348 - rise_dist: distance spent climbing in segment 349 - rise_grade: average grade of climbing in segment 350 - fall_height: height of descending in segment 351 - fall_dist: distance spent descending in segment 352 - fall_grade: average grade of descending in segment 353 - climbs: list with climbs, their grades, and their lengths in segment 354 355 The overall ride data is a dictionary with the following data: 356 - length: length of ride 357 - net_height: net change in elevation over ride 358 - rise_height: height of climbing in ride 359 - rise_dist: distance spent climbing in ride 360 - rise_grade: average grade of climbing in ride 361 - fall_height: height of descending in ride 362 - fall_dist: distance spent descending in ride 363 - fall_grade: average grade of descending in ride 364 - climbs: list with climbs, their grades, and their lengths in ride 365 366 @param ride: ride to process 367 @type ride: L{Ride} 368 369 @return: ride data, list of segment data 370 @rtype: C{dict}, C{list} of C{dict} 371 """ 372 ele_set = [] 373 accum = SegmentAccumulator() 374 for seg in ride.segs: 375 seg_proc = SegmentProcessor(seg, accum) 376 ele_set.append(seg_proc.create_element()) 377 accum.accumulate_segment(seg_proc) 378 379 return accum.create_element(), ele_set
380