--- /dev/null
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Various PyRisk class -> graphic renderers.
+
+Creates SVG files by hand. See the TemplateLibrary class for a
+description of map specification format.
+"""
+
+import operator
+import os
+import os.path
+
+from .base import ID_CmpMixin
+
+
+class Template (object):
+ """Setup regions for a particular world.
+ """
+ def __init__(self, name, regions):
+ self.name = name
+ self.regions = regions
+
+class TemplateLibrary (object):
+ """Create Templates on demand from a directory of template data.
+
+ TODO: explain template data format.
+ """
+ def __init__(self, template_dir='/usr/share/pyrisk/templates/'):
+ self.template_dir = os.path.abspath(os.path.expanduser(template_dir))
+ def get(self, name):
+ region_pointlists,route_pointlists = self._get_pointlists(name)
+ template = self._generate_template(region_pointlists, route_pointlists)
+ return template
+ def _get_pointlists(self, name):
+ dirname = os.path.join(self.template_dir, name.lower())
+ try:
+ files = os.listdir(dirname)
+ except IOError:
+ return None
+ region_pointlists = {}
+ route_pointlists = {}
+ for filename in files:
+ path = os.path.join(dirname, filename)
+ name,extension = filename.rsplit('.', 1)
+ if extension == 'reg':
+ region_pointlists[name] = self._read_file(path)
+ elif extension == 'rt':
+ route_pointlists[name] = self._read_file(path)
+ return (region_pointlists, route_pointlists)
+ def _read_file(self, filename):
+ pointlist = []
+ for line in open(filename, 'r'):
+ line = line.strip()
+ if len(line) == 0:
+ pointlist.append(None)
+ continue
+ fields = line.split('\t')
+ x = int(fields[0])
+ y = -int(fields[1])
+ if len(fields) == 3:
+ label = fields[2].strip()
+ else:
+ label = None
+ pointlist.append((x,y,label))
+ return pointlist
+ def _generate_template(self, region_pointlists, route_pointlists):
+ regions = []
+ all_boundaries = []
+ for name,pointlist in region_pointlists.items():
+ boundaries,head_to_tail = self._pointlist_to_array_of_boundaries(
+ all_boundaries, pointlist)
+ regions.append(Region(name, boundaries, head_to_tail))
+ r = regions[-1]
+ for name,pointlist in route_pointlists.items():
+ boundaries,head_to_tail = self._pointlist_to_array_of_boundaries(
+ all_boundaries, pointlist)
+ assert len(boundaries) == 1, boundaries
+ route = boundaries[0]
+ for terminal in [route[0], route[-1]]:
+ for r in regions:
+ for point in r.outline:
+ if hasattr(point, 'name') \
+ and point.name == terminal.name:
+ r.routes.append(route)
+ r.route_head_to_tail.append(
+ terminal == route[0])
+ route.regions.append(r)
+ for r in regions:
+ r.locate_routes()
+ match_counts = [b.match_count for b in all_boundaries]
+ assert min(match_counts) in [0, 1], set(match_counts)
+ assert max(match_counts) == 1, set(match_counts)
+ return Template('template', regions)
+ def _pointlist_to_array_of_boundaries(self, all_boundaries, pointlist):
+ boundaries = []
+ head_to_tail = []
+ b_points = []
+ for i,point in enumerate(pointlist):
+ if point == None:
+ boundary,reverse = self._analyze(b_points)
+ boundary = self._insert_boundary(all_boundaries, boundary)
+ boundaries.append(boundary)
+ head_to_tail.append(not reverse)
+ b_points = []
+ continue
+ b_points.append(point)
+ if len(b_points) > 0:
+ boundary,reverse = self._analyze(b_points)
+ boundary = self._insert_boundary(all_boundaries, boundary)
+ boundaries.append(boundary)
+ head_to_tail.append(not reverse)
+ return boundaries, head_to_tail
+ def _analyze(self, boundary_points):
+ start = self._vbp(boundary_points[0])
+ stop = self._vbp(boundary_points[-1])
+ if stop < start:
+ reverse = True
+ points = [self._vbp(b) for b in boundary_points]
+ reverse = start > stop
+ if reverse == True:
+ points.reverse()
+ start,stop = (stop, start)
+ boundary = Boundary([p-start for p in points])
+ for bp,p in zip(boundary, points):
+ bp.name = p.name # preserve point names
+ boundary.name = '(%s) -> (%s)' % (start.name, stop.name)
+ boundary.real_pos = start
+ return (boundary, reverse)
+ def _vbp(self, boundary_point):
+ v = Vector((boundary_point[0], boundary_point[1]))
+ v.name = boundary_point[2]
+ return v
+ def _insert_boundary(self, all_boundaries, new):
+ if new in all_boundaries:
+ return new
+ for b in all_boundaries:
+ if len(b) == len(new) and b.real_pos == new.real_pos:
+ match = True
+ for bp,np in zip(b, new):
+ if bp != np:
+ match = False
+ break
+ if match == True:
+ b.match_count += 1
+ return b
+ all_boundaries.append(new)
+ new.match_count = 0
+ return new
+
+TEMPLATE_LIBRARY = TemplateLibrary()
+
+
+class Vector (tuple):
+ """Simple vector addition and subtraction.
+
+ >>> v = Vector
+ >>> a = v((0, 0))
+ >>> b = v((1, 1))
+ >>> c = v((2, 3))
+ >>> a+b
+ (1, 1)
+ >>> a+b+c
+ (3, 4)
+ >>> b-c
+ (-1, -2)
+ >>> -c
+ (-2, -3)
+ >>> a < b
+ True
+ >>> c > b
+ True
+ """
+ def _set_name(self, new, other=None):
+ if hasattr(self, 'name'):
+ if self.name == None:
+ if hasattr(other, 'name'):
+ new.name = other.name
+ return
+ new.name = self.name
+ elif hasattr(other, 'name'):
+ new.name = other.name
+ def __neg__(self):
+ new = self.__class__(map(operator.neg, self))
+ self._set_name(new)
+ return new
+ def __add__(self, other):
+ if len(self) != len(other):
+ raise ValueError('length missmatch %s, %s' % (self, other))
+ new = self.__class__(map(operator.add, self, other))
+ self._set_name(new, other)
+ return new
+ def __sub__(self, other):
+ if len(self) != len(other):
+ raise ValueError('length missmatch %s, %s' % (self, other))
+ new = self.__class__(map(operator.sub, self, other))
+ self._set_name(new, other)
+ return new
+ def __mul__(self, other):
+ if len(self) != len(other):
+ raise ValueError('length missmatch %s, %s' % (self, other))
+ new = self.__class__(map(operator.mul, self, other))
+ self._set_name(new, other)
+ return new
+
+def nameless(vector):
+ """Return a nameless version of a given Vector.
+
+ Useful for ensuring the result of a sum / etc. has the name of the
+ *other* vector.
+ """
+ return Vector(vector)
+
+class Boundary (ID_CmpMixin, list):
+ """Contains a list of points along the boundary.
+
+ All positions are relative to the location of the first point,
+ which should therefore always be (0,0).
+ """
+ def __init__(self, points):
+ list.__init__(self)
+ ID_CmpMixin.__init__(self)
+ for p in points:
+ self.append(Vector(p))
+ self.regions = []
+ assert self[0] == (0,0), self
+ self.x_min = min([p[0] for p in self])
+ self.x_max = max([p[0] for p in self])
+ self.y_min = min([p[1] for p in self])
+ self.y_max = max([p[1] for p in self])
+
+class Region (ID_CmpMixin, list):
+ """Contains a list of boundaries and a label.
+
+ Regions can be Territories, sections of ocean, etc.
+
+ >>> r = Region('Earth',
+ ... [Boundary([(0,0), (0,1)]),
+ ... Boundary([(0,0), (1,0)]),
+ ... Boundary([(0,0), (0,1)]),
+ ... Boundary([(0,0), (1,0)])],
+ ... [True, True, False, False],
+ ... (0.5, 0.5))
+ >>> r.outline
+ [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]
+ """
+ def __init__(self, name, boundaries, head_to_tail, routes=None,
+ route_head_to_tail=None, label_offset=(0,0)):
+ list.__init__(self, boundaries)
+ ID_CmpMixin.__init__(self)
+ for boundary in self:
+ boundary.regions.append(self)
+ self.head_to_tail = head_to_tail
+ self.name = name
+ self.routes = routes
+ self.route_head_to_tail = route_head_to_tail
+ if routes == None:
+ assert route_head_to_tail == None
+ self.routes = []
+ self.route_head_to_tail = []
+ self.route_starts = [] # set by .locate_routes
+ self.label_offset = Vector(label_offset)
+ self.generate_outline() # sets .outline, .starts
+ self.x_min = min([b.x_min+s[0] for b,s in zip(self, self.starts)])
+ self.x_max = max([b.x_max+s[0] for b,s in zip(self, self.starts)])
+ self.y_min = min([b.y_min+s[1] for b,s in zip(self, self.starts)])
+ self.y_max = max([b.y_max+s[1] for b,s in zip(self, self.starts)])
+ def generate_outline(self):
+ """Return a list of boundary points surrounding the region.
+
+ The main issue here is determining the proper border
+ orientation for a CCW outline, which we do via a user-supplied
+ list head_to_tail.
+ """
+ self.starts = []
+ points = [Vector((0,0))]
+ for boundary,htt in zip(self, self.head_to_tail):
+ pos = points[-1]
+ if htt == True:
+ assert boundary[0] == (0,0), boundary
+ new = boundary[1:]
+ else:
+ pos -= nameless(boundary[-1])
+ new = reversed(boundary[:-1])
+ pos = nameless(pos)
+ self.starts.append(pos)
+ for p in new:
+ points.append(pos+p)
+ assert points[-1] == points[0], points
+ self.outline = points
+ def locate_routes(self):
+ self.route_starts = []
+ for route,htt in zip(self.routes, self.route_head_to_tail):
+ if htt:
+ anchor = route[0]
+ else:
+ anchor = route[-1]
+ for point in self.outline:
+ if hasattr(point, 'name') and point.name == anchor.name:
+ self.route_starts.append(point-nameless(anchor))
+ break
+
+class Route (ID_CmpMixin):
+ """Connect non-adjacent Regions.
+ """
+ def __init__(self, boundary):
+ ID_CmpMixin.__init__(self)
+ self.boundary = boundary
+
+
+class WorldRenderer (object):
+ def __init__(self, template_lib=None, line_width=2, buf=10, dpcm=60):
+ self.template_lib = template_lib
+ if self.template_lib == None:
+ self.template_lib = TEMPLATE_LIBRARY
+ self.buf = buf
+ self.line_width = line_width
+ self.dpcm = dpcm
+ def render(self, world):
+ template = self.template_lib.get(world.name)
+ if template == None:
+ template = self._auto_template(world)
+ return self.render_template(template)
+ def render_template(self, template):
+ region_pos,width,height = self._locate(template)
+ lines = [
+ '<?xml version="1.0" standalone="no"?>',
+ '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"',
+ ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
+ '<svg width="%.1fcm" height="%.1fcm" viewBox="0 0 %d %d"'
+ % (float(width)/self.dpcm, float(height)/self.dpcm,
+ width, height),
+ ' xmlns="http://www.w3.org/2000/svg" version="1.1">',
+ '<desc>PyRisk world: %s</desc>' % template.name,
+ ]
+ drawn_rts = {}
+ for r in template.regions:
+ lines.extend([
+ '<!-- %s -->' % r.name,
+ '<polygon fill="red" stroke="blue" stroke-width="%d"'
+ % self.line_width,
+ ' points="%s" />'
+ % ' '.join(['%d,%d' % ((region_pos[id(r)]+p)
+ *(1,-1) # svg y value increases down
+ +(0,height)) # shift back into bbox
+ for p in r.outline[:-1]])
+ ])
+ for rt,rt_start in zip(r.routes, r.route_starts):
+ if id(rt) in drawn_rts:
+ continue
+ drawn_rts[id(rt)] = rt
+ lines.extend([
+ '<polyline stroke="black" stroke-width="%d"'
+ % self.line_width,
+ ' points="%s" />'
+ % ' '.join(['%d,%d' % ((region_pos[id(r)]+rt_start+p)
+ *(1,-1) # svg y value increases down
+ +(0,height)) # shift back into bbox
+ for p in rt])
+ ])
+ lines.extend([
+ '<circle fill="black" cx="0" cy="0" r="20" />',
+ '<circle fill="green" cx="%d" cy="%d" r="20" />'
+ % (width, height)
+ ])
+ lines.extend(['</svg>', ''])
+ return '\n'.join(lines)
+ def _locate(self, template):
+ region_pos = {} # {id: absolute position, ...}
+ boundary_pos = {} # {id: absolute position, ...}
+ route_pos = {} # {id: absolute position, ...}
+ b1 = template.regions[0][0]
+ boundary_pos[id(b1)] = Vector((0,0)) # fix the first boundary point
+ stack = [r for r in b1.regions]
+ while len(stack) > 0:
+ r = stack.pop()
+ if id(r) in region_pos:
+ continue # skip duplicate entries
+ r_start = None
+ for b,rel_b_start in zip(r, r.starts):
+ if id(b) in boundary_pos:
+ b_start = boundary_pos[id(b)]
+ r_start = b_start - rel_b_start
+ break # found an anchor
+ if r_start == None:
+ for rt,rel_rt_start in zip(r.routes, r.route_starts):
+ if id(rt) in route_pos:
+ rt_start = route_pos[id(rt)]
+ r_start = rt_start - rel_rt_start
+ break # found an anchor
+ region_pos[id(r)] = r_start
+ for b,rel_b_start in zip(r, r.starts):
+ if id(b) not in boundary_pos:
+ boundary_pos[id(b)] = r_start + rel_b_start
+ for r2 in b.regions:
+ stack.append(r2)
+ for rt,rt_start in zip(r.routes, r.route_starts):
+ if id(rt) not in route_pos:
+ route_pos[id(rt)] = r_start + rt_start
+ for r2 in rt.regions:
+ stack.append(r2)
+ for r in template.regions:
+ if id(r) not in region_pos:
+ raise KeyError(r.name)
+ x_min = min([r.x_min + region_pos[id(r)][0]
+ for r in template.regions]) - self.buf
+ x_max = max([r.x_max + region_pos[id(r)][0]
+ for r in template.regions]) + self.buf
+ y_min = min([r.y_min + region_pos[id(r)][1]
+ for r in template.regions]) - self.buf
+ y_max = max([r.y_max + region_pos[id(r)][1]
+ for r in template.regions]) + self.buf
+ for key,value in region_pos.items():
+ region_pos[key] = value - Vector((x_min, y_min))
+ return (region_pos, x_max-x_min, y_max-y_min)
+ def _auto_template(self, world):
+ raise NotImplementedError
+
+def test():
+ import doctest, sys
+ failures,tests = doctest.testmod(sys.modules[__name__])
+ return failures
+
+def render_earth():
+ from .base import generate_earth
+ r = WorldRenderer()
+ print r.render(generate_earth())
+ #f = open('world.svg', 'w')
+ #f.write(r.render(generate_earth()))
+ #f.close()