From 7c1166eeb2d530cd073d51ca65db6ce99a048059 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 27 Mar 2010 16:51:19 -0400 Subject: [PATCH] Add preliminary graphics library with world generation --- pyrisk/graphics.py | 443 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 pyrisk/graphics.py diff --git a/pyrisk/graphics.py b/pyrisk/graphics.py new file mode 100644 index 0000000..4a758cb --- /dev/null +++ b/pyrisk/graphics.py @@ -0,0 +1,443 @@ +# Copyright (C) 2010 W. Trevor King +# +# 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 = [ + '', + '', + '', + 'PyRisk world: %s' % template.name, + ] + drawn_rts = {} + for r in template.regions: + lines.extend([ + '' % r.name, + '' + % ' '.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([ + '' + % ' '.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([ + '', + '' + % (width, height) + ]) + lines.extend(['', '']) + 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() -- 2.26.2