1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """
22 Create the template data and process the templates so that OSM format XML files
23 can be generated. These files will later be processed by Mapnik into PDF files
24 containing ride maps.
25 """
26 import math
27 import sys
28
29 from sabx10.map import BORDER
30 from sabx10.oxm import parse_segments, parse_top_level, process_rides, \
31 parse_no_def_namespaces
32 from sabx10.templating import TemplateProcessor
33
34 from bounds import Bounds, Cluster
35 from consts import HEIGHT, WIDTH, PPI, PIX_SCALE_FACTOR
36 from legend import generate_legend
37 from utils import BaseId, NodeId
38 from zoom import find_close_item, get_zoom_items
39
40
42 """
43 Go through all the segments for the ride and add way ids. This is necessary
44 because the OSM way elements are generated from SABX segments and must have
45 unique ids.
46
47 @param ride: L{Ride} whose segments will be modified
48 @type ride: L{Ride}
49 """
50 way_id = BaseId()
51 for seg in ride.segs:
52 seg.way_id = way_id.next()
53
54
56 """
57 Go through all the turns for the ride and add node ids. This is necessary
58 because some OSM node elements are generated from SABX turns and must have
59 unique ids. Also, add a name to the turn because all nodes are expected to
60 have names.
61
62 @param ride: L{Ride} whose turns will be modified
63 @type ride: L{Ride}
64 @param node_id: L{NodeId} to generate turn node ids from
65 @type node_id: L{NodeId}
66 """
67 for turn in ride.turns:
68 if turn.index == 0:
69 turn.name = 'P'
70 else:
71 turn.name = turn.index
72 turn.node_id = node_id.next()
73
74
76 """
77 Find clusters of turns that are within "dist" distance of eachother.
78 Clusters are used to create smaller maps of turns whose icons would overlap
79 on the large map and be hard to distinguish. These are like insets on some
80 maps used to show highly detailed areas of interest.
81
82 @param turns: C{list} of L{Turn}s to process
83 @type turns: C{list} of L{Turn}
84 @param dist: distance threshold for determining clusters
85 @type dist: C{float}
86
87 @return: C{list} of L{Cluster}
88 @rtype: C{list} of L{Cluster}
89 """
90 dist /= 2.0
91 clusters = []
92 for turn in turns:
93 old_cluster = find_close_item(clusters, turn, dist)
94 if old_cluster is not None:
95 old_cluster.expand_to_point(turn.lat, turn.lon)
96 else:
97 new_cluster = Cluster(turn.lat, turn.lon)
98 clusters.append(new_cluster)
99
100 clusters = [cluster for cluster in clusters if cluster.point_count > 1]
101 for index, cluster in enumerate(clusters):
102 cluster.index = index
103
104 return clusters
105
107 """
108 Enlarge and set the pixel size for the L{Cluster} boxes.
109
110 @param clusters: C{list} of L{Cluster}s to process
111 @type clusters: C{list} of L{Cluster}
112 @param dist: distance threshold for determining clusters
113 @type dist: C{float}
114 """
115 for cluster in clusters:
116 dist *= 1.1
117 cluster.resize(dist, dist)
118 cluster.set_pixel_size()
119
121 """
122 Generate and size the clusters for a list of turns.
123
124 @param turns: C{list} of L{Turn}s to process
125 @type turns: C{list} of L{Turn}
126 @param dist: distance threshold for determining clusters
127 @type dist: C{float}
128
129 @return: C{list} of L{Cluster}
130 @rtype: C{list} of L{Cluster}
131 """
132 clusters = _find_clustered_turns(turns, dist)
133 _size_cluster_bounding_boxes(clusters, dist)
134 return clusters
135
136
138 """
139 Go through a list of stops or pois, filter out duplicates, and add node ids
140 to them.
141
142 Adding node ids is necessary because some OSM node elements are generated
143 from SABX stops and pois and must have unique ids.
144
145 Filtering out duplicates is necessary because some stops and pois can be
146 visited more than once in a ride if a ride passes through the same place
147 twice (or more). For the map, the stop or poi icon should only be
148 displayed once.
149
150 @param items: C{list} of L{Stop} or L{Poi} items
151 @type items: C{list} of L{Stop} or L{Poi}
152 @param node_id: L{NodeId} to generate stop and poi node ids from
153 @type node_id: L{NodeId}
154 """
155 item_ids = []
156 filtered_items = []
157 for item in items:
158 if item.id not in item_ids:
159 item_ids.append(item.id)
160 item.node_id = node_id.next()
161 filtered_items.append(item)
162 return filtered_items
163
164
166 """
167 Update the parking for a ride by adding a node id. This is necessary
168 because some OSM node elements are generated from SABX parking elements and
169 must have unique ids.
170
171 @param ride: L{Ride} whose parking will be modified
172 @type ride: L{Ride}
173 @param node_id: L{NodeId} to generate the parking node id from
174 @type node_id: L{NodeId}
175 """
176 ride.parking.node_id = node_id.next()
177
178
180 """
181 Go through the given ride and generate turn clusters and lists of zoomed-in
182 and zommed-out turns, stops, and pois.
183
184 @param the_ride: L{Ride} to be processed
185 @type the_ride: L{Ride}
186 @param node_id: L{NodeId} to generate the node ids from
187 @type node_id: L{NodeId}
188 """
189 icon_width = 10 * PIX_SCALE_FACTOR
190 icon_height = 10 * PIX_SCALE_FACTOR
191
192 sep_dist = the_ride.bounds.calc_sep_dist(icon_width, icon_height)
193 the_ride.zoom_out_turns = get_zoom_items(the_ride.turns, sep_dist, node_id)
194 the_ride.zoom_out_stops = get_zoom_items(the_ride.stops, sep_dist, node_id)
195 the_ride.zoom_out_pois = get_zoom_items(the_ride.pois, sep_dist, node_id)
196
197 the_ride.turn_clusters = _get_turn_clusters(the_ride.turns, sep_dist * 2.0)
198 if len(the_ride.turn_clusters) > 0:
199 sep_dist = the_ride.turn_clusters[0].calc_sep_dist(icon_width,
200 icon_height)
201 the_ride.zoom_in_turns = get_zoom_items(
202 the_ride.turns, sep_dist, node_id)
203 the_ride.zoom_in_stops = get_zoom_items(
204 the_ride.stops, sep_dist, node_id)
205 the_ride.zoom_in_pois = get_zoom_items(
206 the_ride.pois, sep_dist, node_id)
207 else:
208 the_ride.zoom_in_turns = []
209 the_ride.zoom_in_stops = []
210 the_ride.zoom_in_pois = []
211
241
243 """
244 Specialized L{TemplateProcessor} to handle SABX files and turn them into
245 map data that Mapnik can handle.
246 """
247 - def __init__(self, template_file=None, man=None):
248 """
249 Add command-line option for input file.
250
251 @param template_file: name of template file to process
252 @type template_file: C{string}
253 @param man: man page text
254 @type man: C{string}
255 """
256 TemplateProcessor.__init__(self, template_file, man)
257
258 self.parser.add_option("-i", "--infile", dest="in_file",
259 help="input sabx data from FILE",
260 metavar="FILE")
261
263 """
264 Setup for the input file.
265 """
266 TemplateProcessor.process_options(self)
267 if self.options.in_file:
268 self.in_file = open(self.options.in_file, "r")
269 else:
270 self.in_file = sys.stdin
271
273 """
274 Add all the extra data that Mapnik needs.
275 """
276 xml_tree = parse_no_def_namespaces(self.in_file)
277 self.template_data.update(parse_top_level(xml_tree))
278
279 self.template_data['seg_list'], \
280 self.template_data['seg_dict'] = parse_segments(xml_tree)
281 node_id = NodeId(self.template_data['seg_list'])
282 self.template_data['rides'], self.template_data['bounds'] = \
283 _process_all_rides(xml_tree, self.template_data['title'], node_id)
284 self.template_data['PIX_SCALE_FACTOR'] = PIX_SCALE_FACTOR
285 self.template_data['PPI'] = PPI
286 self.template_data['WIDTH'] = WIDTH
287 self.template_data['HEIGHT'] = HEIGHT
288 self.template_data['BORDER'] = BORDER
289