1   
  2  """ 
  3  KhipuDraw.py generates eps, svg, and/or pdf drawings of khipu. 
  4   
  5  Some examples of its use:: 
  6   
  7   KhipuDraw.py --help 
  8   KhipuDraw.py UR001  ## generate out/UR001.eps and out/UR001.pdf 
  9   KhipuDraw.py -f eps UR001  ## generate out/UR001.eps 
 10   KhipuDraw.py -f svg UR001  ## generate out/UR001.svg 
 11   KhipuDraw.py -o foo UR001  ## generate foo/UR001.{eps,pdf} 
 12   KhipuDraw.py all  ## generate out/*.{eps,pdf} 
 13   KhipuDraw.py -d yourdatabase ...  ## use custom database 
 14   
 15  """ 
 16  from __future__ import division, with_statement 
 17  import collections, itertools, math, os, os.path, traceback, warnings 
 18  import KhipuDB 
 19  import Drawing 
 20   
 21   
 22  ps_centimeter = 28.3464567 
 23   
 24   
 25   
 26   
 27   
 28   
 29   
 30  special_strings = { 
 31    'S9 E': (0,1,1), 'S8 L9': (0,1,1), 'S1 S4 L7': (0,1,1), 'S1 S4 L6': (0,1,1), 
 32    'S7 L6': (0,1,0), 'S5 L3': (0,1,0), 'S5 L4': (0,1,0), 'S6 L6': (0,1,0), 'S4 L8': (0,1,0), 'S1 S9 L2': (0,1,0), 'S6 L3': (0,1,0), 'S1 S4 L3': (0,1,0), 'S4 L7': (0,1,0), 'S8 L7': (0,1,0), 'S3 L8': (0,1,0), 
 33    'S2 L6': (0,0,1), 'S1 S3 L6': (0,0,1), 'S3 L6': (0,0,1), 'S2 E': (0,0,1), 'S6 L5': (0,0,1), 'S1 S2 L3': (0,0,1), 'S3 L9': (0,0,1), 'S2 L3': (0,0,1), 'S8 L4': (0,0,1), 'S7 E': (0,0,1), 
 34    'S1 L9': (1,0,0), 'S1 L8': (1,0,0), 'S1 L3': (1,0,0), 'S1 L2': (1,0,0), 'S1 L5': (1,0,0), 'S1 L4': (1,0,0), 'S1 L7': (1,0,0), 'S2 L7': (1,0,0), 'S2 L4': (1,0,0), 'S2 L2': (1,0,0), 'S2 L5': (1,0,0), 'S1 L6': (1,0,0), 'S1 S1 L2': (1,0,0), 'S3 L7': (1,0,0), 'S3 L5': (1,0,0), 'S3 L4': (1,0,0), 'S4 E': (1,0,0), 'S3 L2': (1,0,0), 'S2 L8': (1,0,0), 'S4 L5': (1,0,0), 'S4 L2': (1,0,0), 'S6 E': (1,0,0), 'S2 L9': (1,0,0), 'S1 E': (1,0,0), 'S5 L5': (1,0,0), 'S5 L2': (1,0,0), 'S4 L6': (1,0,0), 'S4 L4': (1,0,0), 'S3 E': (1,0,0), 'S5 L9': (1,0,0), 'S4 L3': (1,0,0), 
 35  } 
 36   
 37   
 39    """Draw primary cord with accurate horizontal spacing.""" 
 40    height = 5 
 41    default_width = 0.1 
 42     
 44      self.primary = (0,0) 
 45      self.cords = [] 
 46      self.bottoms = [] 
 47      self.width = self.default_width 
  48    @classmethod 
 49 -  def make (Class, khipu, colormap, labelmap): 
  76 -  def postscript (self, ps, y): 
  77      if not self.cords: return 
 78      ps.comment ('ACCURATE PRIMARY CORD') 
 79      ps.setgray (0) 
 80      ps.setlinecap ('round') 
 81      ps.setlinewidth ((self.width or self.default_width) * ps_centimeter) 
 82      with ps.group(classes=('c'+str(self.primary[0])+' cord_upper')): 
 83        with ps.stroke() as path: 
 84          path.moveto (self.primary[1] * ps_centimeter, y) 
 85          path.lineto (self.primary[2] * ps_centimeter, y) 
 86       
 87       
 88      ps.comment ('Attached cords') 
 89      for id, coord, bottom, width, up, color in self.cords: 
 90        cn = 'c'+str(id) 
 91        with ps.group(classes=(cn+' cord_upper'), link='#'+cn): 
 92          ps.setgrayorrgb (color) 
 93          ps.setlinewidth ((width or self.default_width) * ps_centimeter) 
 94          with ps.stroke() as path: 
 95            path.moveto (coord * ps_centimeter, y) 
 96            if up: 
 97              path.lineto(bottom * ps_centimeter, y + self.height * ps_centimeter) 
 98            else: 
 99              path.lineto(bottom * ps_centimeter, y - self.height * ps_centimeter) 
  100           
101           
102           
103           
104           
105           
106           
107   
109    """Hierarchical drawing of a khipu with accurate vertical spacing. 
110   
111    This class does not attempt to draw knots; it merely renders them as circles. 
112    However, the `make` method could be used for drawing knots however you like 
113    in a subclass (and `KhipuKnotTreeAccurateVertical` does exactly that). 
114    """ 
115    relative_each_width = 4 
116    relative_descent = 4 
117    default_width = 0.1 
118    relative_knot_radius = 1.5 
119    relative_font_size = 3 
120    relative_text_space = 2 
121    text_angle = 80 
125    @classmethod 
126 -  def make (Class, khipu, colormap, labelmap): 
 127      self = Class () 
128      minwidth = khipu.min_nonzero_thickness () 
129      if minwidth is not None: self.default_width = float(minwidth) / 2 
130      primary_cord = khipu.primary_cord () 
131      def recurse (cord, x, y): 
132        width = float(cord.thickness or self.default_width) 
133        x += self.relative_each_width * width/2 
134        self.cords.append ((cord.cord_id, 
135                            (x, y), (x, y - float(cord.cord_length)), width, 
136                            colormap[cord], labelmap[cord], cord.termination)) 
137         
138         
139         
140         
141         
142         
143         
144        for knot in cord.knots (): 
145          self.knots.append (((x, y - float(knot.position ())), width, 
146                              knot.type_code, knot.num_turns, knot.direction)) 
147        newx = x 
148        newx += self.relative_each_width * width/2 
149        for subcord in reversed (list (cord.cords ())): 
150          subwidth = float(subcord.thickness or self.default_width) 
151          thisy = y - float(subcord.position ()) 
152          self.cords.append ((subcord.cord_id, (x, thisy), 
153                              (newx + self.relative_each_width * subwidth/2, 
154                               thisy - self.relative_descent * subwidth), 
155                              subwidth, colormap[subcord], None, None)) 
156          newx = recurse (subcord, newx, thisy - self.relative_descent * subwidth) 
157        return newx 
 158      x = 0 
159      for cord in primary_cord.cords (): 
160        width = float(cord.thickness or self.default_width) 
161        if cord.cluster_ordinal == 1: 
162          x += self.relative_each_width * width 
163        x = recurse (cord, x, 0) 
164      self.cords.append ((primary_cord.pcord_id, (0, 0), (x, 0), 
165                          float(primary_cord.thickness or self.default_width), 0, None, 
166                          primary_cord.termination)) 
167      return self 
 168 -  def postscript (self, ps, y): 
 169      ps.comment ('KHIPU TREE, ACCURATE VERTICAL') 
170      ys = [cord[1][1] for cord in self.cords] + \ 
171           [cord[2][1] for cord in self.cords] + \ 
172           [knot[0][1] for knot in self.knots] 
173      if not ys: return 
174      yoff = y - min (ys) * ps_centimeter 
175      ps.comment ('Cords') 
176      ps.setfont ('Times-Roman', self.relative_font_size * width * ps_centimeter) 
177      for cord_id, (x1, y1), (x2, y2), width, color, label, termination in self.cords: 
178        if not width: width = self.default_width 
179        ps.setlinewidth (width * ps_centimeter) 
180        ps.setgrayorrgb (color) 
181        with ps.stroke() as path: 
182          path.moveto (x1 * ps_centimeter, y1 * ps_centimeter + yoff) 
183          path.lineto (x2 * ps_centimeter, y2 * ps_centimeter + yoff) 
184   
185        if termination == 'B': 
186          draw_X (ps, x2 * ps_centimeter, y2 * ps_centimeter + yoff, 
187                  width * ps_centimeter * self.relative_knot_radius) 
188        if label: 
189          ps.setgray (0) 
190          ps.text(x1 * ps_centimeter, (y1 + width) * ps_centimeter + yoff, 
191                  label) 
192      ps.comment ('Knots') 
193      last = (None, None) 
194      clusteroff = 0 
195      for (x, y), width, type, turns, direction in self.knots: 
196        if (x, y) == last: 
197          clusteroff += 2 * (width or self.default_width) * \ 
198                        self.relative_knot_radius * ps_centimeter 
199        else: 
200          clusteroff = 0 
201          last = (x, y) 
202        with ps.fill() as path: 
203          path.circle (x * ps_centimeter, y * ps_centimeter + yoff - clusteroff, 
204                       (width or self.default_width) * self.relative_knot_radius * \ 
205                       ps_centimeter) 
 206       
207   
208 -def draw_X (ps, x, y, radius): 
 209    with ps.stroke() as path: 
210      path.moveto (x - radius, y - radius) 
211      path.lineto (x + radius, y + radius) 
212      path.moveto (x - radius, y + radius) 
213      path.lineto (x + radius, y - radius) 
 214   
216    scale = 1.5 
217    def draw (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 
218       
219      ps.setgrayorrgb (fill_color) 
220      with ps.fill() as path: 
221        path.circle (x, y - scale / 2, outer_width * scale) 
222      ps.setgrayorrgb (stroke_color) 
223      ps.setlinewidth ((1-inner_ratio)/2 * outer_width) 
224      with ps.stroke() as path: 
225        path.circle (x, y - scale / 2, outer_width * scale) 
226      return outer_width * scale 
 227    return draw 
228   
229 -def simple_knot_S (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 
 230     
231     
232     
233    scale = outer_width / 195 
234    ps.comment ('Simple knot') 
235    ps.setgrayorrgb (fill_color) 
236    with ps.fill() as path: 
237      path.circle (x-3*scale, y-271*scale, 183 * scale) 
238    with ps.fill () as path: 
239      path.circle (x+90*scale, y-131*scale, 131 * scale) 
240    ps.setgrayorrgb (stroke_color) 
241     
242    ps.setlinewidth ((1-inner_ratio)/2 * outer_width) 
243    ps.setlinecap ('round') 
244    with ps.stroke() as path: 
245      path.arcn (x-3.1*scale, y-272.2*scale, 184.9*scale, 14.6123, 120.2152) 
246    with ps.stroke() as path: 
247      path.arc (x-130.8*scale, y-9.9*scale, 222.5*scale, -59.1121, -2.8157) 
248    with ps.stroke() as path: 
249      path.arc (x+58.7*scale, y-223.3*scale, 76.5*scale, 164.3158, 259.6954) 
250    with ps.stroke() as path: 
251      path.arcn (x+26.5*scale, y-103.2*scale, 196.1*scale, 354.5894, 274.4478) 
252    with ps.stroke() as path: 
253      path.arcn (x-131.6*scale, y-210.2*scale, 103.7*scale, 71.3697, 5.6789) 
254    with ps.stroke() as path: 
255      path.arcn (x+116.3*scale, y-111.7*scale, 105.9*scale, 94.1480, -12.7264) 
256    return 454*scale 
 257   
258 -def reflect_knot (ps, knot, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 
 259    with ps.translate(x,y).scale(-1,1): 
260      return cached_knot (ps, knot, knot_key, (0, 0, outer_width, inner_ratio, stroke_color, fill_color)) 
 261   
265   
309   
313   
314 -def long_2_knot_S (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 
 315     
316     
317    scale = outer_width / 195  
318    ps.setgrayorrgb (fill_color) 
319    with ps.fill () as path: 
320      path.circle (x-2*scale, y-169*scale, 173*scale) 
321    with ps.fill () as path: 
322      path.circle (x-8*scale, y-361*scale, 173*scale) 
323    with ps.fill () as path: 
324      path.circle (x+108*scale, y-347*scale, 91*scale) 
325    with ps.fill () as path: 
326      path.circle (x-38*scale, y-211*scale, 173*scale) 
327    with ps.fill () as path: 
328      path.circle (x-51*scale, y-286*scale, 173*scale) 
329    ps.setgrayorrgb (stroke_color) 
330     
331    ps.setlinewidth ((1-inner_ratio)/2 * outer_width) 
332    ps.setlinecap ('round') 
333    with ps.stroke () as path: 
334      path.arcn (x+21.3*scale, y-247.5*scale, 133.7*scale, -146.6297, 140.8330) 
335    with ps.stroke () as path: 
336      path.arc (x-2.3*scale, y-167.6*scale, 174.5*scale, -119.8184, 60.3693) 
337    with ps.stroke () as path: 
338      path.arc (x-37.7*scale, y-49.9*scale, 122.9*scale, -110.6191, 13.1398) 
339    with ps.stroke () as path: 
340      path.arc (x+89.2*scale, y-261.1*scale, 312.0*scale, 122.5411, -137.7146) 
341    with ps.stroke () as path: 
342      path.arc (x+153.7*scale, y-227.4*scale, 259.6*scale, -158.3845, -125.3882) 
343    with ps.stroke () as path: 
344      path.arc (x+75.8*scale, y-344.9*scale, 118.6*scale, -127.5088, 47.0929) 
345    with ps.stroke () as path: 
346      path.arc (x-6.4*scale, y-355.8*scale, 177.8*scale, -138.7502, -31.6226) 
347    with ps.stroke () as path: 
348      path.moveto (x-81*scale, y-161*scale) 
349      path.lineto (x-81*scale, y-0*scale) 
350    return 540*scale 
 351   
353    if k == 2: return long_2_knot_S 
354    def long_k_knot_S (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 
355       
356       
357       
358      scale = outer_width / 195 * 0.8   
359      up = (9-k)*140*scale 
360      if k == 3: up -= 30*scale 
361      ps.setgrayorrgb (fill_color) 
362      with ps.fill () as path: 
363        path.circle (x+75*scale, y-124*scale, 124*scale)  
364      with ps.fill () as path: 
365        path.circle (x-21*scale, y-241*scale, 240*scale)  
366      if k >= 4: 
367        with ps.fill () as path: 
368          path.circle (x+117*scale, y-361*scale, 112*scale)  
369      if k >= 5: 
370        with ps.fill () as path: 
371          path.circle (x+114*scale, y-508*scale, 112*scale)  
372      if k >= 6: 
373        with ps.fill () as path: 
374          path.circle (x+114*scale, y-643*scale, 112*scale)  
375      if k >= 7: 
376        with ps.fill () as path: 
377          path.circle (x+114*scale, y-784*scale, 112*scale)  
378      if k >= 8: 
379        with ps.fill () as path: 
380          path.circle (x+114*scale, y-925*scale, 112*scale)  
381      if k >= 9: 
382        with ps.fill () as path: 
383          path.circle (x+111*scale, y-1072*scale, 112*scale)  
384      with ps.fill () as path: 
385        path.circle (x-93*scale, y-1144*scale+up, 171*scale)  
386      with ps.fill () as path: 
387        path.circle (x+82*scale, y-1167*scale+up, 112*scale)  
388      with ps.fill () as path: 
389        path.circle (x-69*scale, y-1255*scale+up, 112*scale)  
390      with ps.fill () as path: 
391        path.circle (x+24*scale, y-1288*scale+up, 112*scale)  
392      with ps.fill () as path: 
393        path.moveto (x-264*scale, y-226*scale) 
394        path.lineto (x+159*scale, y-226*scale) 
395        path.lineto (x+159*scale, y-1126*scale+up) 
396        path.lineto (x-264*scale, y-1126*scale+up) 
397      ps.setgrayorrgb (stroke_color) 
398       
399      ps.setlinewidth ((1-inner_ratio)/2 * outer_width) 
400      ps.setlinecap ('round') 
401      if k >= 5: 
402        with ps.stroke () as path: 
403          path.arcn (x-29.2*scale, y-32.4*scale, 366.5*scale, -51.6882, -103.3708) 
404      if k >= 5: 
405        with ps.stroke () as path: 
406          path.arcn (x+40.6*scale, y-480.8*scale, 158.9*scale, -164.9553, 146.4629) 
407      if k >= 6: 
408        with ps.stroke () as path: 
409          path.arcn (x-29.2*scale, y-165.4*scale, 366.5*scale, -51.6882, -103.3708) 
410      if k >= 5: 
411        with ps.stroke () as path: 
412          path.arcn (x+123.7*scale, y-374.2*scale, 108.7*scale, 36.2225, -48.0347) 
413      if k >= 6: 
414        with ps.stroke () as path: 
415          path.arcn (x+40.6*scale, y-621.8*scale, 158.9*scale, -164.9553, 146.4629) 
416      if k >= 7: 
417        with ps.stroke () as path: 
418          path.arcn (x-29.2*scale, y-306.4*scale, 366.5*scale, -51.6882, -103.3708) 
419      if k >= 6: 
420        with ps.stroke () as path: 
421          path.arcn (x+123.7*scale, y-515.2*scale, 108.7*scale, 36.2225, -48.0347) 
422      if k >= 7: 
423        with ps.stroke () as path: 
424          path.arcn (x+40.6*scale, y-759.8*scale, 158.9*scale, -164.9553, 146.4629) 
425      if k >= 8: 
426        with ps.stroke () as path: 
427          path.arcn (x-29.2*scale, y-444.4*scale, 366.5*scale, -51.6882, -103.3708) 
428      if k >= 7: 
429        with ps.stroke () as path: 
430          path.arcn (x+123.7*scale, y-653.2*scale, 108.7*scale, 36.2225, -48.0347) 
431      if k >= 8: 
432        with ps.stroke () as path: 
433          path.arcn (x+40.6*scale, y-894.8*scale, 158.9*scale, -164.9553, 146.4629) 
434      if k >= 9: 
435        with ps.stroke () as path: 
436          path.arcn (x-29.2*scale, y-579.4*scale, 366.5*scale, -51.6882, -103.3708) 
437      if k >= 8: 
438        with ps.stroke () as path: 
439          path.arcn (x+123.7*scale, y-788.2*scale, 108.7*scale, 36.2225, -48.0347) 
440      if k >= 9: 
441        with ps.stroke () as path: 
442          path.arcn (x+40.6*scale, y-1035.8*scale, 158.9*scale, -164.9553, 146.4629) 
443      if k >= 4: 
444        with ps.stroke () as path: 
445          path.arcn (x-29.2*scale, y-720.4*scale+up, 366.5*scale, -51.6882, -103.3708) 
446      if k >= 9: 
447        with ps.stroke () as path: 
448          path.arcn (x+123.7*scale, y-929.2*scale, 108.7*scale, 36.2225, -48.0347) 
449      with ps.stroke () as path: 
450        path.arc (x-30.1*scale, y-7.4*scale, 123.2*scale, -119.0830, 1.5613) 
451      with ps.stroke () as path: 
452        path.arcn (x+58.6*scale, y-138.8*scale, 145.2*scale, 75.7951, -14.4403) 
453      with ps.stroke () as path: 
454        path.arcn (x+1.1*scale, y-23.5*scale, 248.6*scale, -37.5364, -117.5226) 
455      with ps.stroke () as path: 
456        path.arcn (x+40.6*scale, y-202.8*scale, 158.9*scale, -164.9553, 146.4629) 
457      if k >= 4: 
458        with ps.stroke () as path: 
459          path.arcn (x+40.6*scale, y-347.8*scale, 158.9*scale, -164.9553, 146.4629) 
460      with ps.stroke () as path: 
461        path.arcn (x+123.9*scale, y-241.1*scale, 108.9*scale, 45.8136, -47.9755) 
462      if k >= 4: 
463        with ps.stroke () as path: 
464          path.arcn (x+143.6*scale, y-1072.4*scale+up, 86.4*scale, 47.2854, -59.6924) 
465      with ps.stroke () as path: 
466        path.arcn (x+23.9*scale, y-818.7*scale+up, 366.9*scale, -63.4701, -102.4539) 
467      with ps.stroke () as path: 
468        path.arc (x+218.9*scale, y-985.6*scale+up, 343.5*scale, -162.8295, -117.2286) 
469      with ps.stroke () as path: 
470        path.arc (x+67.3*scale, y-1163.3*scale+up, 130.7*scale, -91.6330, 11.1605) 
471      with ps.stroke () as path: 
472        path.arcn (x+53.6*scale, y-1085.0*scale+up, 315.3*scale, -99.5645, -173.6356) 
473      with ps.stroke () as path: 
474        path.arc (x+44.8*scale, y-275.6*scale, 306.6*scale, 116.3868, 172.9506) 
475      with ps.stroke () as path: 
476        path.arcn (x+32.7*scale, y-1297.8*scale+up, 103.1*scale, 12.2140, -107.6668) 
477      with ps.stroke () as path: 
478        path.moveto (x-93*scale, y-115*scale) 
479        path.lineto (x-93*scale, y-1*scale) 
480        path.moveto (x-261*scale, y-1123*scale+up) 
481        path.lineto (x-261*scale, y-241*scale) 
482      return 1400*scale+up 
 483    return long_k_knot_S 
484   
486     
487    knot = long_knot_S(k) 
488     
489    return lambda ps, (type, turns, direction), params: \ 
490      reflect_knot(ps, knot, (type, turns, 'S'), params) 
 491   
492  knotCache = {} 
493 -def cached_knot (ps, knot, (type, turns, direction), 
494                   (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 
 510    name = uniqid(type + str(turns) + direction) 
511    if name is None:  
512      return knot(ps, (type, turns, direction), (x, y, outer_width, inner_ratio, stroke_color, fill_color)) 
513     
514    with ps.defineSymbol(name): 
515      height= knot(ps, (type, turns, direction), (0, 0, outer_width, inner_ratio, stroke_color, fill_color)) 
516    knotCache[knotid] = (name, height) 
517    ps.refSymbol(name, x, y) 
518    return height 
519   
521    fill_color = 0.5 
522    stroke_color = 0 
523    inner_ratio = 0.5 
524 -  def postscript (self, ps, y): 
 525      ps.comment ('KHIPU KNOT TREE, ACCURATE VERTICAL') 
526      ys = [cord[1][1] for cord in self.cords] + \ 
527           [cord[2][1] for cord in self.cords] + \ 
528           [knot[0][1] for knot in self.knots] 
529      if not ys: return 
530      yoff = y - min (ys) * ps_centimeter 
531      ps.setlinecap ('round') 
532      bb = {}  
533      ps.comment ('Cord borders') 
534      with ps.group(classes="cord_border"): 
535        for id, (x1, y1), (x2, y2), width, color, label, termination in self.cords: 
536          ps.setgrayorrgb (color) 
537          ps.setlinewidth (width * ps_centimeter) 
538          with ps.group(classes=('c'+str(id))): 
539            with ps.stroke () as path: 
540              path.moveto (x1 * ps_centimeter, y1 * ps_centimeter + yoff) 
541              path.lineto (x2 * ps_centimeter, y2 * ps_centimeter + yoff) 
542            bb.setdefault(id, Drawing.BBox()).updateBBox(ps.bbox()) 
543      ps.comment ('Cord innards') 
544      with ps.group(classes="cord_innard"): 
545        for id, (x1, y1), (x2, y2), width, color, label, termination in self.cords: 
546          if isinstance (color, tuple): 
547            ps.setgrayorrgb (((color[0] + 1)/2, (color[1] + 1)/2, (color[2] + 1)/2)) 
548          else: 
549            ps.setgrayorrgb ((color + 1)/2) 
550          ps.setlinewidth (self.inner_ratio * width * ps_centimeter) 
551          with ps.group(classes=('c'+str(id))): 
552            with ps.stroke () as path: 
553              path.moveto (x1 * ps_centimeter, y1 * ps_centimeter + yoff) 
554              path.lineto (x2 * ps_centimeter, y2 * ps_centimeter + yoff) 
555      ps.comment ('Labels') 
556      ps.setgray (0) 
557      for id, (x1, y1), (x2, y2), width, color, label, termination in self.cords: 
558        ps.setfont ('Times-Roman', self.relative_font_size * width * ps_centimeter) 
559        if label: 
560           
561          y = 0 
562          with ps.translate(x1 * ps_centimeter + width * ps_centimeter, 
563            (y + self.relative_text_space * width) * ps_centimeter + yoff)\ 
564            .rotate(self.text_angle): 
565            ps.text (0, 0, label, id=('label_'+str(id))) 
566             
567             
568      ps.comment ('Broken cords') 
569      with ps.group(classes="cord_broken"): 
570        for id, (x1, y1), (x2, y2), width, color, label, termination in self.cords: 
571          if termination == 'B': 
572            ps.setgrayorrgb (color) 
573            ps.setlinewidth (width * ps_centimeter) 
574            with ps.group(classes=('c'+str(id))): 
575              draw_X (ps, x2 * ps_centimeter, y2 * ps_centimeter + yoff, 
576                      width * ps_centimeter * self.relative_knot_radius) 
577              bb[id].updateBBox(ps.bbox()) 
578      ps.comment ('Cord views') 
579      for id, bbox in bb.iteritems(): 
580        ps.view(bbox, id=('c'+str(id)), viewTarget='label_'+str(id)) 
581      ps.comment ('Knots') 
582      last = (None, None) 
583      clusteroff = 0 
584      for (x, y), width, type, turns, direction in self.knots: 
585        if (x, y) == last: 
586          clusteroff += last_height 
587        else: 
588          clusteroff = 0 
589          last = (x, y) 
590        if type == 'S': 
591          if direction == 'Z': 
592            knot = simple_knot_Z 
593          else:  
594            knot = simple_knot_S 
595            direction = 'S'  
596          if turns not in (0, 1): 
597            warnings.warn ('S knot has %d turns; not rendering' % turns) 
598        elif type == 'E': 
599          if direction == 'Z': 
600            knot = figure_eight_knot_Z 
601          else: 
602            knot = figure_eight_knot_S 
603            direction = 'S'  
604          if turns not in (0, 1): 
605            warnings.warn ('E knot has %d turns; not rendering' % turns) 
606        elif type == 'L': 
607          if 2 <= turns <= 9: 
608            if direction == 'Z': 
609              knot = long_knot_Z (turns) 
610            else: 
611              knot = long_knot_S (turns) 
612              direction = 'S'  
613          else: 
614            warnings.warn ('L knot has %d turns; rendering as dot' % turns) 
615            knot = dot_knot (type, turns) 
616        else: 
617          warnings.warn ('Unknown knot type %r; rendering as dot' % type) 
618          knot = dot_knot (type, turns) 
619        last_height = cached_knot (ps, knot, (type, turns, direction), 
620                                   (x * ps_centimeter, 
621                                    y * ps_centimeter + yoff - clusteroff, 
622                                    width * ps_centimeter, self.inner_ratio, 
623                                    self.stroke_color, self.fill_color)) 
624      return max (ys) + yoff 
  625   
634     
635     
636   
643   
645    colors = [(1,0,0),(0,1,0),(0,0,1), 
646              (0,1,1),(1,0,1), 
647              (0.5,0,0),(0,0.5,0),(0,0,0.5),(0.5,0.5,0), 
648              (0,0.5,0.5),(0.5,0,0.5),(0.5,0.5,0.5), 
649              (1,0.5,0),(0.5,1,0),(1,0,0.5),(0.5,0,1), 
650              (0,1,0.5),(0,0.5,1), 
651              (1,0.5,0.5),(0.5,1,0.5),(0.5,0.5,1), 
652               
653              (1,0.5,1),(0.5,1,1), 
654              ] 
 669   
676   
677 -class Labelmap (collections.defaultdict): 
 681      if isinstance (key, KhipuDB.Cord): 
682        return self[key.cord_id] 
 703    name = khipu.investigator_num.encode ('utf8').replace ('/', '_') 
704    outdir = options.get('outdir') 
705    if outdir is not None: 
706      name = os.path.join(outdir, name) 
707    return name 
 708   
714     
717     
719    out = options.get ('out') 
720    if out is None: 
721      out = default_filename (khipu, **options) + '.' + ext 
722    if isinstance (out, str): 
723      dirpart = os.path.dirname(out) 
724      if dirpart != '' and not os.path.exists(dirpart): 
725        os.makedirs(dirpart) 
726      out = open (out, 'w') 
727      try: 
728        options['out'] = out 
729        return render_vector (khipu, ps, ext, **options) 
730      finally: 
731        out.close () 
732        print 'Wrote', out.name 
733     
734     
735    colormap = PopularCordColormap (khipu) 
736     
737    if options.get ('labelmap'): 
738      labelmap = Labelmap (khipu) 
739      labelmapOpt = options['labelmap'] 
740      if isinstance (labelmapOpt, str): 
741         
742        labelmapOpt = eval (open (labelmapOpt, 'r').read ()) 
743      else: 
744        labelmap = Labelmap (khipu) 
745      for k,v in labelmapOpt.iteritems(): 
746        labelmap[int(k)] = v 
747    else: 
748       
749       
750      labelmap = NumberLabelmap (khipu) 
751     
752    global knotCache 
753    knotCache = {} 
754     
755    KhipuKnotTreeAccurateVertical.make (khipu, colormap, labelmap).postscript (ps, 0) 
756    top = ps.bbox()[3] 
757    ps.comment ('Khipu name') 
758    link = "http://projects.csail.mit.edu/khipu/draw/%s.svg" % khipu.investigator_num 
759    with ps.group(link=link, classes='khipu_title'):  
760      ps.setgray (0) 
761      ps.setfont ('Times-Roman', 0.5 * AccuratePrimaryCord.height * ps_centimeter) 
762      ps.text (0, top + 0.25 * AccuratePrimaryCord.height * ps_centimeter, 
763               khipu.investigator_num) 
764    AccuratePrimaryCord.make (khipu, colormap, labelmap) \ 
765      .postscript (ps, top + 2 * AccuratePrimaryCord.height * ps_centimeter) 
766    out.write (str (ps)) 
 767   
769    out = options.get ('out') 
770    if out is None: 
771      out = default_filename (khipu, **options) 
772    if out.endswith ('.pdf'): 
773      epsout = out[:-4] + '.eps' 
774      pdfout = out 
775    else: 
776      epsout = out + '.eps' 
777      pdfout = out + '.pdf' 
778    options['out'] = epsout 
779    render_ps (khipu, **options) 
780    os.system ('perl /usr/bin/epstopdf ' + epsout) 
781    print 'Wrote', pdfout 
782     
783     
784     
785     
786     
787    return pdfout 
 788   
789 -def render (khipu, **options): 
 797   
799    import optparse, sys 
800    optparser = optparse.OptionParser (usage='%prog [options] (all | investigator_num)') 
801    optparser.add_option ('-l', '--labelmap', dest='labelmap', 
802                          help='specify a mapping of cords to labels') 
803    optparser.add_option ('-f', '--format', dest='format', default='pdf', 
804                          help='which output to generate: svg, eps, or pdf') 
805    optparser.add_option ('-o', '--outdir', dest='outdir', default='out', 
806                          help='the directory in which to put the output') 
807    optparser.add_option ('-d', '--db', dest='dbfile', default='khipu.db', 
808                          help='the name of the database file to use (or "sql")') 
809    optparser.add_option ('-s', '--script', dest='script', 
810                          help='generate a reference in SVG output to the javascript file with the given name/URL.') 
811    options, args = optparser.parse_args () 
812    if not args: 
813      optparser.error ('specify khipu name or "all"') 
814    if options.dbfile == 'sql': 
815      options.dbfile = None 
816    db = KhipuDB.KhipuDB (options.dbfile) 
817    if args[0] == 'all': 
818      if options.format is None: 
819        options.format = 'pdf' 
820      for khipu in db: 
821         
822        print '***', khipu.investigator_num 
823        sys.stdout.flush () 
824        try: 
825          render (khipu, **options.__dict__) 
826        except KeyboardInterrupt: 
827          return 
828        except: 
829          traceback.print_exc () 
830    else: 
831      render (db.khipu_with_investigator_num (args[0]), **options.__dict__) 
832    db.save_cache () 
 833   
834  if __name__ == '__main__': main () 
835