1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 """Various PyRisk class -> graphic renderers.
19 Creates SVG files by hand. See the TemplateLibrary class for a
20 description of map specification format.
27 from .base import ID_CmpMixin
30 class Template (object):
31 """Setup regions for a particular world.
33 def __init__(self, name, regions):
35 self.regions = regions
37 class TemplateLibrary (object):
38 """Create Templates on demand from a directory of template data.
40 TODO: explain template data format.
42 def __init__(self, template_dir='/usr/share/pyrisk/templates/'):
43 self.template_dir = os.path.abspath(os.path.expanduser(template_dir))
45 region_pointlists,route_pointlists = self._get_pointlists(name)
46 template = self._generate_template(region_pointlists, route_pointlists)
48 def _get_pointlists(self, name):
49 dirname = os.path.join(self.template_dir, name.lower())
51 files = os.listdir(dirname)
54 region_pointlists = {}
56 for filename in files:
57 path = os.path.join(dirname, filename)
58 name,extension = filename.rsplit('.', 1)
59 if extension == 'reg':
60 region_pointlists[name] = self._read_file(path)
61 elif extension == 'rt':
62 route_pointlists[name] = self._read_file(path)
63 return (region_pointlists, route_pointlists)
64 def _read_file(self, filename):
66 for line in open(filename, 'r'):
69 pointlist.append(None)
71 fields = line.split('\t')
75 label = fields[2].strip()
78 pointlist.append((x,y,label))
80 def _generate_template(self, region_pointlists, route_pointlists):
83 for name,pointlist in region_pointlists.items():
84 boundaries,head_to_tail = self._pointlist_to_array_of_boundaries(
85 all_boundaries, pointlist)
86 regions.append(Region(name, boundaries, head_to_tail))
88 for name,pointlist in route_pointlists.items():
89 boundaries,head_to_tail = self._pointlist_to_array_of_boundaries(
90 all_boundaries, pointlist)
91 assert len(boundaries) == 1, boundaries
93 for terminal in [route[0], route[-1]]:
95 for point in r.outline:
96 if hasattr(point, 'name') \
97 and point.name == terminal.name:
98 r.routes.append(route)
99 r.route_head_to_tail.append(
100 terminal == route[0])
101 route.regions.append(r)
104 match_counts = [b.match_count for b in all_boundaries]
105 assert min(match_counts) in [0, 1], set(match_counts)
106 assert max(match_counts) == 1, set(match_counts)
107 return Template('template', regions)
108 def _pointlist_to_array_of_boundaries(self, all_boundaries, pointlist):
112 for i,point in enumerate(pointlist):
114 boundary,reverse = self._analyze(b_points)
115 boundary = self._insert_boundary(all_boundaries, boundary)
116 boundaries.append(boundary)
117 head_to_tail.append(not reverse)
120 b_points.append(point)
121 if len(b_points) > 0:
122 boundary,reverse = self._analyze(b_points)
123 boundary = self._insert_boundary(all_boundaries, boundary)
124 boundaries.append(boundary)
125 head_to_tail.append(not reverse)
126 return boundaries, head_to_tail
127 def _analyze(self, boundary_points):
128 start = self._vbp(boundary_points[0])
129 stop = self._vbp(boundary_points[-1])
132 points = [self._vbp(b) for b in boundary_points]
133 reverse = start > stop
136 start,stop = (stop, start)
137 boundary = Boundary([p-start for p in points])
138 for bp,p in zip(boundary, points):
139 bp.name = p.name # preserve point names
140 boundary.name = '(%s) -> (%s)' % (start.name, stop.name)
141 boundary.real_pos = start
142 return (boundary, reverse)
143 def _vbp(self, boundary_point):
144 v = Vector((boundary_point[0], boundary_point[1]))
145 v.name = boundary_point[2]
147 def _insert_boundary(self, all_boundaries, new):
148 if new in all_boundaries:
150 for b in all_boundaries:
151 if len(b) == len(new) and b.real_pos == new.real_pos:
153 for bp,np in zip(b, new):
160 all_boundaries.append(new)
164 TEMPLATE_LIBRARY = TemplateLibrary()
167 class Vector (tuple):
168 """Simple vector addition and subtraction.
187 def _set_name(self, new, other=None):
188 if hasattr(self, 'name'):
189 if self.name == None:
190 if hasattr(other, 'name'):
191 new.name = other.name
194 elif hasattr(other, 'name'):
195 new.name = other.name
197 new = self.__class__(map(operator.neg, self))
200 def __add__(self, other):
201 if len(self) != len(other):
202 raise ValueError('length missmatch %s, %s' % (self, other))
203 new = self.__class__(map(operator.add, self, other))
204 self._set_name(new, other)
206 def __sub__(self, other):
207 if len(self) != len(other):
208 raise ValueError('length missmatch %s, %s' % (self, other))
209 new = self.__class__(map(operator.sub, self, other))
210 self._set_name(new, other)
212 def __mul__(self, other):
213 if len(self) != len(other):
214 raise ValueError('length missmatch %s, %s' % (self, other))
215 new = self.__class__(map(operator.mul, self, other))
216 self._set_name(new, other)
219 def nameless(vector):
220 """Return a nameless version of a given Vector.
222 Useful for ensuring the result of a sum / etc. has the name of the
225 return Vector(vector)
227 class Boundary (ID_CmpMixin, list):
228 """Contains a list of points along the boundary.
230 All positions are relative to the location of the first point,
231 which should therefore always be (0,0).
233 def __init__(self, points):
235 ID_CmpMixin.__init__(self)
237 self.append(Vector(p))
239 assert self[0] == (0,0), self
240 self.x_min = min([p[0] for p in self])
241 self.x_max = max([p[0] for p in self])
242 self.y_min = min([p[1] for p in self])
243 self.y_max = max([p[1] for p in self])
245 class Region (ID_CmpMixin, list):
246 """Contains a list of boundaries and a label.
248 Regions can be Territories, sections of ocean, etc.
250 >>> r = Region('Earth',
251 ... [Boundary([(0,0), (0,1)]),
252 ... Boundary([(0,0), (1,0)]),
253 ... Boundary([(0,0), (0,1)]),
254 ... Boundary([(0,0), (1,0)])],
255 ... [True, True, False, False],
258 [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]
260 def __init__(self, name, boundaries, head_to_tail, routes=None,
261 route_head_to_tail=None, label_offset=(0,0)):
262 list.__init__(self, boundaries)
263 ID_CmpMixin.__init__(self)
264 for boundary in self:
265 boundary.regions.append(self)
266 self.head_to_tail = head_to_tail
269 self.route_head_to_tail = route_head_to_tail
271 assert route_head_to_tail == None
273 self.route_head_to_tail = []
274 self.route_starts = [] # set by .locate_routes
275 self.label_offset = Vector(label_offset)
276 self.generate_outline() # sets .outline, .starts
277 self.x_min = min([b.x_min+s[0] for b,s in zip(self, self.starts)])
278 self.x_max = max([b.x_max+s[0] for b,s in zip(self, self.starts)])
279 self.y_min = min([b.y_min+s[1] for b,s in zip(self, self.starts)])
280 self.y_max = max([b.y_max+s[1] for b,s in zip(self, self.starts)])
281 def generate_outline(self):
282 """Return a list of boundary points surrounding the region.
284 The main issue here is determining the proper border
285 orientation for a CCW outline, which we do via a user-supplied
289 points = [Vector((0,0))]
290 for boundary,htt in zip(self, self.head_to_tail):
293 assert boundary[0] == (0,0), boundary
296 pos -= nameless(boundary[-1])
297 new = reversed(boundary[:-1])
299 self.starts.append(pos)
302 assert points[-1] == points[0], points
303 self.outline = points
304 def locate_routes(self):
305 self.route_starts = []
306 for route,htt in zip(self.routes, self.route_head_to_tail):
311 for point in self.outline:
312 if hasattr(point, 'name') and point.name == anchor.name:
313 self.route_starts.append(point-nameless(anchor))
316 class Route (ID_CmpMixin):
317 """Connect non-adjacent Regions.
319 def __init__(self, boundary):
320 ID_CmpMixin.__init__(self)
321 self.boundary = boundary
324 class WorldRenderer (object):
325 def __init__(self, template_lib=None, line_width=2, buf=10, dpcm=60):
326 self.template_lib = template_lib
327 if self.template_lib == None:
328 self.template_lib = TEMPLATE_LIBRARY
330 self.line_width = line_width
332 def render(self, world):
333 template = self.template_lib.get(world.name)
335 template = self._auto_template(world)
336 return self.render_template(template)
337 def render_template(self, template):
338 region_pos,width,height = self._locate(template)
340 '<?xml version="1.0" standalone="no"?>',
341 '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"',
342 ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
343 '<svg width="%.1fcm" height="%.1fcm" viewBox="0 0 %d %d"'
344 % (float(width)/self.dpcm, float(height)/self.dpcm,
346 ' xmlns="http://www.w3.org/2000/svg" version="1.1">',
347 '<desc>PyRisk world: %s</desc>' % template.name,
350 for r in template.regions:
352 '<!-- %s -->' % r.name,
353 '<polygon fill="red" stroke="blue" stroke-width="%d"'
356 % ' '.join(['%d,%d' % ((region_pos[id(r)]+p)
357 *(1,-1) # svg y value increases down
358 +(0,height)) # shift back into bbox
359 for p in r.outline[:-1]])
361 for rt,rt_start in zip(r.routes, r.route_starts):
362 if id(rt) in drawn_rts:
364 drawn_rts[id(rt)] = rt
366 '<polyline stroke="black" stroke-width="%d"'
369 % ' '.join(['%d,%d' % ((region_pos[id(r)]+rt_start+p)
370 *(1,-1) # svg y value increases down
371 +(0,height)) # shift back into bbox
375 '<circle fill="black" cx="0" cy="0" r="20" />',
376 '<circle fill="green" cx="%d" cy="%d" r="20" />'
379 lines.extend(['</svg>', ''])
380 return '\n'.join(lines)
381 def _locate(self, template):
382 region_pos = {} # {id: absolute position, ...}
383 boundary_pos = {} # {id: absolute position, ...}
384 route_pos = {} # {id: absolute position, ...}
385 b1 = template.regions[0][0]
386 boundary_pos[id(b1)] = Vector((0,0)) # fix the first boundary point
387 stack = [r for r in b1.regions]
388 while len(stack) > 0:
390 if id(r) in region_pos:
391 continue # skip duplicate entries
393 for b,rel_b_start in zip(r, r.starts):
394 if id(b) in boundary_pos:
395 b_start = boundary_pos[id(b)]
396 r_start = b_start - rel_b_start
397 break # found an anchor
399 for rt,rel_rt_start in zip(r.routes, r.route_starts):
400 if id(rt) in route_pos:
401 rt_start = route_pos[id(rt)]
402 r_start = rt_start - rel_rt_start
403 break # found an anchor
404 region_pos[id(r)] = r_start
405 for b,rel_b_start in zip(r, r.starts):
406 if id(b) not in boundary_pos:
407 boundary_pos[id(b)] = r_start + rel_b_start
410 for rt,rt_start in zip(r.routes, r.route_starts):
411 if id(rt) not in route_pos:
412 route_pos[id(rt)] = r_start + rt_start
413 for r2 in rt.regions:
415 for r in template.regions:
416 if id(r) not in region_pos:
417 raise KeyError(r.name)
418 x_min = min([r.x_min + region_pos[id(r)][0]
419 for r in template.regions]) - self.buf
420 x_max = max([r.x_max + region_pos[id(r)][0]
421 for r in template.regions]) + self.buf
422 y_min = min([r.y_min + region_pos[id(r)][1]
423 for r in template.regions]) - self.buf
424 y_max = max([r.y_max + region_pos[id(r)][1]
425 for r in template.regions]) + self.buf
426 for key,value in region_pos.items():
427 region_pos[key] = value - Vector((x_min, y_min))
428 return (region_pos, x_max-x_min, y_max-y_min)
429 def _auto_template(self, world):
430 raise NotImplementedError
434 failures,tests = doctest.testmod(sys.modules[__name__])
438 from .base import generate_earth
440 print r.render(generate_earth())
441 #f = open('world.svg', 'w')
442 #f.write(r.render(generate_earth()))