1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """
22 Main code for generating interactive Google Maps representations of SABX data
23 files. It takes an SABX file and parses it and processes its rides using the
24 L{sabx10.oxm} package. It then adds additional data to the processed rides.
25
26 @var zoom_levels: distances between 32x32 map icons in Google Maps for each zoom
27 level, with the zoom level being the index of the distance in the list
28 @type zoom_levels: C{list}
29 """
30 import os.path
31 import sys
32
33 import sabx10.oxm as oxm
34 from sabx10.templating import TemplateProcessor
35
36 from consts import BORDER
37 from elevations import process_elevations
38 import glineenc
39
40
41 zoom_levels = [2690, 1354, 680, 342, 168, 86, 42, 21, 11,
42 5.3, 2.6, 1.32, 0.66, 0.33, 0.17, 0.083,
43 0.041, 0.021, 0.01, 0.005]
44
46 """
47 Determine how far away, in statute miles, the closest point to C{pt} is in
48 C{points}.
49
50 @param points: C{list} of points to check against
51 @type points: C{list} of L{Point} objects
52 @param pt: L{Point} object to check for
53 @type pt: L{Point} object
54
55 @return: distance to closest point
56 @rtype: C{float}
57 """
58 min_dist = 3000
59 for point in points:
60 min_dist = min(min_dist, pt.calculate_distance(point))
61 return min_dist
62
64 """
65 Determine the minimum Google maps zoom level to show C{pt} so that it won't
66 overlap with other points in the C{points} list at higher zooms. This
67 assumes lower zoom levels show bigger areas, such that the highest zoom
68 level would show a tick on a rat's back, while the lowest zoom level would
69 show the continents of the world. Lowest is 0, while highest is 19. These
70 are determined so that 32x32 icons won't overlap in the area of the map
71 surrounding San Antonio, TX. For other parts of the world (closer or
72 farther from the equator) these values may not be all that good.
73
74 @param points: C{list} of points to check against
75 @type points: C{list} of L{Point} objects
76 @param pt: L{Point} object to check for
77 @type pt: L{Point} object
78
79 @return: zoom level
80 @rtype: C{int}
81 """
82 dist = _min_distance(points, pt)
83 index = 0
84 for zoom in zoom_levels:
85 if dist > zoom:
86 return index
87 index += 1
88 return index
89
90
92 """
93 For each segment in a ride, give it a type, Google Map encode its points,
94 and add filenames for its profiles.
95
96 Set the type to "seg", so that Jinja2 can tell what type it is, since
97 Jinja2 doesn't natively support object introspection.
98
99 Add encoded points, meaning points that have been encoded into Google Maps'
100 compact format for storing segment points.
101
102 Add filenames for the profiles for the segments.
103
104 @param ride_segs: list of segments in the ride
105 @type ride_segs: C{list} of L{Segment} objects
106 @param graph_filebase: base name for profile files
107 @type graph_filebase: C{string}
108 @param ride_index: index of ride being processed
109 @type ride_index: C{string}
110 """
111 for seg in ride_segs:
112
113 seg.pts = [(pt.lat, pt.lon) for pt in seg.waypoints]
114
115
116 seg.item_type = 'seg'
117
118
119 seg.profile_small = '%s_prof_small_%s_%s.png' % \
120 (graph_filebase, ride_index, seg.index)
121 seg.profile_large = '%s_prof_large_%s_%s.png' % \
122 (graph_filebase, ride_index, seg.index)
123
125 """
126 For each turn in a ride, set its type and calculate a zoom level.
127
128 Calculate the minimum Google Maps zoom level to display this turn.
129
130 Set the type to "turn" so that Jinja2 can tell what type it is, since
131 Jinja2 doesn't natively support object introspection.
132
133 @param ride_turns: list of turns for this ride
134 @type ride_turns: C{list} of L{Turn} objects
135 """
136 turn_points = []
137 for turn in ride_turns:
138
139 turn_point = oxm.Point(turn.lat, turn.lon)
140 turn.min_zoom = _min_zoom_level(turn_points, turn_point)
141 turn_points.append(turn_point)
142
143
144 turn.item_type = 'turn'
145
146
148 """
149 For each stop in a ride, set its type.
150
151 Set the type to "stop" so that Jinja2 can tell what type it is, since
152 Jinja2 doesn't natively support object introspection.
153
154 @param ride_stops: list of stops to process
155 @type ride_stops: C{list} of L{Stop} objects
156 """
157 for stop in ride_stops:
158 stop.item_type = 'stop'
159
161 """
162 For each poi in a ride, set its type.
163
164 Set all the pois to have type "poi" so that Jinja2 can tell what type it
165 is, since Jinja2 doesn't natively support object introspection.
166
167 @param ride_pois: list of pois to process
168 @type ride_pois: C{list} of L{Poi} objects
169 """
170 for poi in ride_pois:
171 poi.item_type = 'poi'
172
174 """
175 Calculate the ride distance and off-course distance for each poi or stop in
176 a ride. This function works on stop or poi lists, and expects one of these
177 to be passed in.
178
179 Go through the list of stops/pois for a ride and correlate these with the
180 stops/pois defined for the rideset. For each occurrence of a stop/poi in a
181 ride, save the distance into the ride and the distance off course for the
182 stop/poi. You want to do this because a single stop/poi can be used several
183 times in a ride. When summarizing the stop/poi, you want to be able to
184 list all the distances for the stop/poi. This is used when generating the
185 stop/poi info dialog when clicking on the stop/poi icon in the Google Map.
186
187 @param item_list: list of items to process
188 @type item_list: C{list} of L{Poi} or L{Stop} objects
189
190 @return: distances dictionary
191 @rtype: C{dictionary}
192 """
193 item_dists = {}
194 for item in item_list:
195 if item.id in item_dists:
196 item_dists[item.id].append((item.distance,item.off_route))
197 else:
198 item_dists[item.id] = [(item.distance,item.off_route)]
199 return item_dists
200
201
203 """
204 Go through all the rides in the rideset and update them with the extra
205 information necessary to correctly render maps and their data from the
206 templates.
207
208 @param xml_tree: C{ElementTree} representation of a rideset
209 @type xml_tree: C{ElementTree} stuff
210 @param filebase: base upon which to build file names
211 @type filebase: C{string}
212
213 @return: modified ride data
214 @rtype: C{list} of L{Ride}
215 """
216 ride_data, bounds = oxm.process_rides(xml_tree)
217 for ride in ride_data:
218
219 _process_segments(ride.segs, filebase, ride.index)
220 _process_turns(ride.turns)
221 _process_stops(ride.stops)
222 _process_pois(ride.pois)
223
224
225 ride.stop_dists = _process_stop_poi_dists(ride.stops)
226 ride.poi_dists = _process_stop_poi_dists(ride.pois)
227
228
229 ride.ele, ride.ele_segs = process_elevations(ride)
230
231
232 ride.profile_small = \
233 '%s_prof_small_%s_all.png' % (filebase, ride.index)
234 ride.profile_large = \
235 '%s_prof_large_%s_all.png' % (filebase, ride.index)
236 ride.map_large = '%s_map_%s.png' % (filebase, ride.index)
237 ride.map_small_base = '%s_map_%s_' % (filebase, ride.index)
238
239 return ride_data
240
242 """
243 Specialized L{TemplateProcessor} to handle SABX files and turn them into
244 Google Maps displayable maps.
245 """
246
247 - def __init__(self, template_file=None, man=None):
248 """
249 Add command-line options to set the input file and various directories
250 the templates need to know about.
251
252 @param template_file: name of template file to process
253 @type template_file: C{string}
254 @param man: man page text
255 @type man: C{string}
256 """
257 TemplateProcessor.__init__(self, template_file, man)
258
259
260 self.parser.add_option("-i", "--infile", dest="in_file",
261 help="input sabx data from FILE",
262 metavar="FILE")
263
264
265 self.parser.add_option("-r", "--marker-dir", dest="marker_dir",
266 help="relative web directory for marker files",
267 metavar="MARKERDIR", default=".")
268 self.parser.add_option("-g", "--image-dir", dest="image_dir",
269 help="relative web directory for image files",
270 metavar="IMAGEDIR", default=".")
271 self.parser.add_option("-c", "--css-dir", dest="css_dir",
272 help="relative web directory for css files",
273 metavar="CSSDIR", default=".")
274 self.parser.add_option("-j", "--js-dir", dest="js_dir",
275 help="relative web directory for javascript files",
276 metavar="JSDIR", default=".")
277 self.parser.add_option("-b", "--base-dir", dest="base_dir",
278 help="relative base web directory",
279 metavar="BASEDIR", default=".")
280 self.parser.add_option("-a", "--analytics-key", dest="analytics",
281 help="Google analytics key",
282 metavar="ANALYTICS", default="")
283
285 """
286 Save the dirs and setup for the input and output files.
287 """
288 def _save_options(self):
289 self.template_data['marker_dir'] = self.options.marker_dir
290 self.template_data['image_dir'] = self.options.image_dir
291 self.template_data['css_dir'] = self.options.css_dir
292 self.template_data['js_dir'] = self.options.js_dir
293 self.template_data['base_dir'] = self.options.base_dir
294 self.template_data['analytics'] = self.options.analytics
295
296 def _setup_input_file(self):
297 if self.options.in_file:
298 self.in_file = open(self.options.in_file, "r")
299 else:
300 self.in_file = sys.stdin
301
302 def _setup_output_file(self):
303 if self.options.out_file:
304 self.filebase = \
305 os.path.splitext(os.path.basename(self.options.out_file))[0]
306 self.filebase = self.filebase.replace("-print", "")
307 else:
308 self.filebase = "stdout"
309
310 TemplateProcessor.process_options(self)
311 _save_options(self)
312 _setup_input_file(self)
313 _setup_output_file(self)
314
328