1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """
22 Handle the plotting of profile data and saving the profile to a file.
23 """
24 import math
25 import os.path
26
27 import matplotlib
28 matplotlib.use('Agg')
29 import matplotlib.pyplot as plt
30
31 from sabx10.oxm import mile_feet
32
33 from consts import SMALL_WIDTH, SMALL_HEIGHT, LARGE_WIDTH, LARGE_HEIGHT, \
34 FONT_POINTS_PER_INCH, FONT_NAME, LABEL_FONT_SIZE, ANNO_FONT_SIZE
35
37 """
38 Generator to provide the plot fill data for a graph based on a list of
39 distances (X axis) and elevations (Y axis). Fill data is a grade-dependant
40 color and x and y co-ordinates needed to specify a polygon on the graph so
41 it can be filled in with the proper color.
42
43 @param distances: C{list} of distances for points in segment being plotted
44 @type distances: C{list} of C{float}
45 @param elevations: C{list} of elevations for point in segment being plotted
46 @type elevations: C{list} of C{float}
47
48 @return: (x1, x2), (y1, y2), color
49 @rtype: (C{float},C{float}), (C{float},C{float}), C{string}
50 """
51 colors = {0: '1.0',
52 1: '0.9',
53 2: '0.8',
54 3: '0.7',
55 4: '0.6',
56 5: '0.5',
57 6: '0.4',
58 7: '0.3',
59 8: '0.2',
60 9: '0.1',
61 10: '0.0'}
62
63 for x in range(len(distances)-1):
64 if distances[x+1]-distances[x] == 0:
65 grade = 0;
66 else:
67 grade = ((elevations[x+1]-elevations[x])/
68 ((distances[x+1]-distances[x]) * mile_feet)) * 100.0
69 grade = math.trunc(grade)
70 grade = max(0, grade)
71 grade = min(10, grade)
72
73 yield (distances[x], distances[x+1]), \
74 (elevations[x], elevations[x+1]), \
75 colors[grade]
76
78 """
79 Generates a profile file.
80
81 @ivar ride: L{Ride} to process
82 @type ride: L{Ride}
83 @ivar graph_filebase: base name for profile files
84 @type graph_filebase: C{string}
85 @ivar graph_dir: directory to write profile files into
86 @type graph_dir: C{string}
87 @ivar dpi: resolution of profile file
88 @type dpi: C{int}
89 @ivar min_anno_dist: minimum distance between annotations
90 @type min_anno_dist: C{float}
91 @ivar longest: length of longest segment
92 @type longest: C{float}
93 @ivar lowest: lowest elevation
94 @type lowest: C{float}
95 @ivar highest: highest elevation
96 @type highest: C{float}
97 """
98
100 """
101 Go through the segments for the ride and find the longest segment, the
102 lowest elevation, and the highest elevation.
103 """
104 self.longest = 0.0
105 self.lowest = 20000.0
106 self.highest = 0.0
107
108 for seg in self.ride.segs:
109 seg_lowest, seg_highest = seg.find_lowest_highest()
110 self.lowest = min(self.lowest, seg_lowest)
111 self.highest = max(self.highest, seg_highest)
112 self.longest = max(self.longest, seg.length)
113
114 - def __init__(self, ride, graph_filebase, graph_dir, dpi):
115 """
116 Save the passed-in data and generate calculated instance data.
117
118 @param ride: L{Ride} to process
119 @type ride: L{Ride}
120 @param graph_filebase: base name for profile files
121 @type graph_filebase: C{string}
122 @param graph_dir: directory to write profile files into
123 @type graph_dir: C{string}
124 @param dpi: resolution of profile file
125 @type dpi: C{int}
126 """
127 self.ride = ride
128 self.graph_filebase = graph_filebase
129 self.graph_dir = graph_dir
130 self.dpi = dpi
131 self.min_anno_dist = self.ride.distance / 50.0
132 self._calc_most_least()
133
135 """
136 Calculate and set the min_ann_dist for the plot. This is how far apart
137 annotations need to be before they overlap.
138
139 @param inch_width: width of profile, in inches
140 @type inch_width: C{float}
141 """
142 plot_inch_width = inch_width * 0.7
143 label_inch_width = (float(ANNO_FONT_SIZE) / float(FONT_POINTS_PER_INCH))
144 miles_per_inch = self.ride.distance / plot_inch_width
145 miles_per_label = miles_per_inch * label_inch_width
146 self.min_anno_dist = miles_per_label * 1.1
147
149 """
150 Normalize an elevation in relation to the lowest elevation in the ride,
151 such that the lowest elevation in the ride will be at an elevation of
152 zero.
153
154 @param elevation: elevation to normalize
155 @type elevation: C{float}
156
157 @return: normalized elevation
158 @rtype: C{float}
159 """
160 return elevation - self.lowest
161
163 """
164 Normalize all the elevations in the list.
165
166 @param elevations: C{list} of elevations to normalize
167 @type elevations: C{list} of C{float}
168 """
169 return [self._normalize_elevation(ele) for ele in elevations]
170
172 """
173 Plot the graph of the distances and elevations. Fill-in under the
174 graph based on the grade between points.
175
176 @param distances: C{list} of distances to plot
177 @type distances: C{list} of C{float}
178 @param elevations: C{list} of elevations corresponding to the distances
179 @type elevations: C{list} of C{float}
180 @param length: length of set of points being plotted
181 @type length: C{float}
182 """
183 elevations = self._normalize_elevations(elevations)
184 for x, y, color in _poly_gen(distances, elevations):
185 plt.fill_between(x, y, color=color, edgecolor=color)
186 plt.plot(distances, elevations, scalex=False, scaley=False)
187 plt.axis([0.0, max(length, 1.0), 0.0, self.highest - self.lowest])
188
190 """
191 Take the annotation list and remove annotations that are too close to
192 the ones next to it, to prevent overlap.
193
194 @param annotations: list of annotations to filter
195 @type annotations: C{list} of L{Annotation}
196 """
197 prev_dist = 0.0
198 ann_dist = 0.0
199 filtered = []
200
201 for anno in annotations:
202 ann_dist += anno.dist - prev_dist
203 prev_dist = anno.dist
204 if ann_dist >= self.min_anno_dist:
205 filtered.append(anno)
206 ann_dist = 0.0
207
208 return filtered
209
211 """
212 Normalize the elevations of the annotation points.
213
214 @param annotations: list of annotations to filter
215 @type annotations: C{list} of L{Annotation}
216
217 @return: list of filtered annotations
218 @rtype: C{list} of L{Annotation}
219 """
220 for anno in annotations:
221 anno.ele = self._normalize_elevation(anno.ele)
222 return annotations
223
225 """
226 Add the annotations to the current plot (if there are any). Filter and
227 normalize them first.
228
229 @param annotations: list of annotations to filter
230 @type annotations: C{list} of L{Annotation}
231 """
232 if annotations is None:
233 return
234
235 annotations = self._filter_annotations(annotations)
236 annotations = self._normalize_annotations(annotations)
237 for anno in annotations:
238 plt.annotate(anno.text, (anno.dist, anno.ele), xytext=(-4,20),
239 textcoords='offset points',
240 arrowprops=dict(arrowstyle="->", relpos=(0.5,0.0)),
241 rotation='vertical',
242 fontname=FONT_NAME, fontsize=ANNO_FONT_SIZE)
243
244 - def _plot_profile(self, distances, elevations, length, annotations):
245 """
246 Graph the distances, elevations, and annotations on the current plot.
247
248 @param distances: list of distances for the plot
249 @type distances: C{list} of C{float}
250 @param elevations: list of elevations for the plot
251 @type elevations: C{list} of C{float}
252 @param length: overall length of plot
253 @type length: C{float}
254 @param annotations: annotations for the plot
255 @type annotations: C{list} of L{Annotation}
256 """
257 plt.grid(False)
258 self._plot_graph(distances, elevations, length)
259 self._plot_annotations(annotations)
260
262 """
263 Save the current plot to a file.
264
265 @param seg_index: index of segment being plotted
266 @type seg_index: C{int}
267 @param size_name: size name being plotted
268 @type size_name: C{string}
269 """
270 file_name = '%s_prof_%s_%s_%s.png' % (self.graph_filebase, size_name,
271 self.ride.index, seg_index)
272 plt.savefig(os.path.join(self.graph_dir, file_name), dpi=self.dpi)
273 plt.close()
274
275 - def plot_small_profile(self, distances, elevations, length,
276 seg_index='all', annotations=None):
277 """
278 Plot a small sized profile.
279
280 @param distances: list of distances for the plot
281 @type distances: C{list} of C{float}
282 @param elevations: list of elevations for the plot
283 @type elevations: C{list} of C{float}
284 @param length: overall length of plot
285 @type length: C{float}
286 @param seg_index: index of segment being plotted
287 @type seg_index: C{int}
288 @param annotations: annotations for the plot
289 @type annotations: C{list} of L{Annotation}
290 """
291 self._set_min_anno_dist(SMALL_WIDTH)
292 plt.figure(1, figsize=(SMALL_WIDTH, SMALL_HEIGHT))
293
294 self._plot_profile(distances, elevations, length, annotations)
295 self._save_profile(seg_index, 'small')
296
297 - def plot_large_profile(self, distances, elevations, length,
298 seg_index='all', annotations=None):
299 """
300 Plot a large sized profile.
301
302 @param distances: list of distances for the plot
303 @type distances: C{list} of C{float}
304 @param elevations: list of elevations for the plot
305 @type elevations: C{list} of C{float}
306 @param length: overall length of plot
307 @type length: C{float}
308 @param seg_index: index of segment being plotted
309 @type seg_index: C{int}
310 @param annotations: annotations for the plot
311 @type annotations: C{list} of L{Annotation}
312 """
313 self._set_min_anno_dist(LARGE_WIDTH)
314 plt.figure(1, figsize=(LARGE_WIDTH, LARGE_HEIGHT))
315 plt.xlabel('Distance (miles)', fontname=FONT_NAME, fontsize=LABEL_FONT_SIZE)
316 plt.ylabel('Elevation (feet)', fontname=FONT_NAME, fontsize=LABEL_FONT_SIZE)
317 if annotations is None:
318 plt.title('Profile', fontname=FONT_NAME, fontsize=LABEL_FONT_SIZE)
319 else:
320 plt.box(False)
321 plt.axhline(y=0.001, color='black')
322 plt.axvline(color='black')
323
324 self._plot_profile(distances, elevations, length, annotations)
325 self._save_profile(seg_index, 'large')
326