Module KhipuDraw
[hide private]
[frames] | no frames]

Source Code for Module KhipuDraw

  1  #!/usr/bin/python2.5 
  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  #import yaml 
 21   
 22  ps_centimeter = 28.3464567 
 23   
 24  #special = set([ 
 25  #  3000214,3006753,3006758,3006763,3006768,3006773,3006778,3006783,3006788,3006809,3006814,3006819,3006824,3006839,3006844,3013877,3014974,3015324,3018435,3019317,3019732,3022953, 
 26  #  3000615,3000717,3013894,3013898,3013902,3013906,3013923,3013927,3013931,3013935,3015789, 
 27  #  3019724,3027168,3027171,3027172,3027173,3027181 ## E E E E 
 28  #]) 
 29  #special_strings = set(['S1 S3 L6', 'S2 L8', 'S1 L8', 'S2 L2', 'S2 L3', 'S2 L4', 'S2 L5', 'S2 L6', 'S1 L6', 'S1 L2', 'S1 L5', 'S3 L9', 'S1 L4', 'S6 L5', 'S8 L4', 'S1 L7', 'S2 L7', 'S1 S1 L2', 'S2 E', 'S3 L7', 'S3 L6', 'S3 L5', 'S3 L4', 'S4 E', 'S3 L2', 'S5 L9', 'S1 S2 L3', 'S7 E', 'S1 L9', 'S3 E', 'S6 E', 'S1 L3', 'S2 L9', 'S1 E', 'S5 L5', 'S5 L2', 'S4 L6', 'S4 L4', 'S4 L5', 'S4 L2', 'S4 L3']) 
 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   
38 -class AccuratePrimaryCord:
39 """Draw primary cord with accurate horizontal spacing.""" 40 height = 5 41 default_width = 0.1 42 #min_space = 0.1
43 - def __init__ (self):
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):
50 self = Class () 51 primary_cord = khipu.primary_cord () 52 self.primary = (primary_cord.pcord_id, 0, float(primary_cord.pcord_length)) 53 self.width = float(primary_cord.thickness or self.default_width) 54 last_coord = None 55 minwidth = khipu.min_nonzero_thickness () 56 if minwidth is not None: self.default_width = minwidth / 2 57 for cord in primary_cord.cords (): 58 coord = float(cord.position ()) 59 width = float(cord.thickness or self.default_width) 60 ## Minimum spacing: 61 if last_coord is not None: 62 coord = max (coord, last_coord + 2 * width) 63 #coord = (max (coord, last_coord + max (2 * width, self.min_space))) 64 bottom = coord 65 up = (cord.attachment_type == 'V') ## recto/verso? 66 up = False 67 self.cords.append ((cord.cord_id, coord, bottom, width, up, colormap[cord])) 68 #if cord.cord_id in special: 69 # self.cords.append ((cord.cord_id, coord, bottom, width, up, (1,0,0))) 70 #else: 71 # self.cords.append ((cord.cord_id, coord, bottom, width, up, 0)) 72 last_coord = coord 73 if minwidth != 1000: self.default_width = minwidth 74 #assert self.coords == sorted (self.coords) 75 return self
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 #for i, coord, width in \ 87 # zip (range (len (self.coords)), self.coords, self.widths): 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 ## Uniformly splayed out: 101 #if len (self.coords) == 1: 102 # ps.lineto (coord * ps_centimeter, y - self.height * ps_centimeter) 103 #else: 104 # ps.lineto (ps_centimeter * (coord + 105 # (i / (len (self.coords) - 1)) * (2 * self.height) - self.height), 106 # y - self.height * ps_centimeter) 107
108 -class KhipuTreeAccurateVertical:
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
122 - def __init__ (self):
123 self.cords = [] 124 self.knots = []
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 #if cord.cord_id in special: # or cord.short_string () == 'S1 L2': 138 #if not cord.numerical (): 139 # self.cords.append ((cord.cord_id, (x, y), (x, y - cord.cord_length), 140 # (cord.thickness or self.default_width)*3, (1,0,0))) 141 #else: 142 # self.cords.append ((cord.cord_id, (x, y), (x, y - cord.cord_length), 143 # (cord.thickness or self.default_width), 0)) 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 #return max (ys) + yoff 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
215 -def dot_knot (type, turns):
216 scale = 1.5 217 def draw (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)): 218 # XXX: this could be combined if we split stroke and fill color 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 #based on knot_S.eps 231 #main cord: 10559, 10415 (rough center), outer width 195, inner width 135 232 #y offset by an additional -155 to make y=0 the top 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 #ps.setlinewidth (15 * scale) 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
262 -def simple_knot_Z (ps, (type, turns, direction), params):
263 #reflection of simple_knot_S 264 return reflect_knot(ps, simple_knot_S, (type, turns, 'S'), params)
265
266 -def figure_eight_knot_Z (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)):
267 #based on knot_E.eps 268 #main cord: 10577, 10500 (top), outer width 165, inner width 105 269 scale = outer_width / 195 # thought it would have been 165 270 ps.setgrayorrgb (fill_color) 271 with ps.fill () as path: 272 path.circle (x+56*scale, y-428*scale, 176*scale) 273 with ps.fill () as path: 274 path.circle (x-56*scale, y-407*scale, 176*scale) 275 with ps.fill () as path: 276 path.circle (x-53*scale, y-116*scale, 188*scale) 277 with ps.fill () as path: 278 path.circle (x-57*scale, y-310*scale, 176*scale) 279 ps.setgrayorrgb (stroke_color) 280 #ps.setlinewidth (15 * scale) 281 ps.setlinewidth ((1-inner_ratio)/2 * outer_width) 282 ps.setlinecap ('round') 283 with ps.stroke () as path: 284 path.arcn (x+95.5*scale, y-346.2*scale, 329.4*scale, -172.4611, 130.4122) 285 with ps.stroke () as path: 286 path.arcn (x-94.7*scale, y-182.2*scale, 90.2*scale, 105.8830, -34.5724) 287 with ps.stroke () as path: 288 path.arc (x+90.8*scale, y-379.8*scale, 182.0*scale, 127.0943, -163.7889) 289 with ps.stroke () as path: 290 path.arcn (x-117.8*scale, y-570.8*scale, 216.7*scale, 83.0641, 25.1620) 291 with ps.stroke () as path: 292 path.arc (x-4.4*scale, y-429.2*scale, 197.9*scale, 61.4255, 91.8404) 293 with ps.stroke () as path: 294 path.arc (x-130.0*scale, y-132.8*scale, 259.2*scale, -29.2886, 37.3453) 295 with ps.stroke () as path: 296 path.arc (x-49.4*scale, y-85.5*scale, 166.5*scale, 41.5731, 96.3519) 297 with ps.stroke () as path: 298 path.arc (x-18.3*scale, y-131.9*scale, 216.8*scale, 103.1514, -154.9121) 299 with ps.stroke () as path: 300 path.arc (x+53.9*scale, y-429.7*scale, 177.9*scale, -80.9052, 76.9666) 301 with ps.stroke () as path: 302 path.arcn (x-47.6*scale, y-402.3*scale, 185.0*scale, -99.8301, 176.0669) 303 with ps.stroke () as path: 304 path.moveto (x-76*scale, y-445*scale) 305 path.lineto (x-76*scale, y-584*scale) 306 path.moveto (x+80*scale, y-477*scale) 307 path.lineto (x+80*scale, y-604*scale) 308 return 635*scale
309
310 -def figure_eight_knot_S (ps, (type, turns, direction), params):
311 #reflection of figure_eight_knot_Z 312 return reflect_knot(ps, figure_eight_knot_Z, (type, turns, 'Z'), params)
313
314 -def long_2_knot_S (ps, knot_key, (x, y, outer_width, inner_ratio, stroke_color, fill_color)):
315 #based on knot_2.eps 316 #main cord: -6571, 10487 (top), outer width 165, inner width 105 317 scale = outer_width / 195 # thought it would have been 165 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 #ps.setlinewidth (15 * scale) 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
352 -def long_knot_S (k):
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 #based on knot_9.eps 356 #main cord: -2025, 11923 (top), outer width 195, inner width 135 357 #to reflect: negate, subtract 1400 358 scale = outer_width / 195 * 0.8 # 0.8 just looks right... 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 #ps.setlinewidth (15 * scale) 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
485 -def long_knot_Z (k):
486 #reflection of long_knot_S 487 knot = long_knot_S(k) 488 # this is more painful than it needs to be... 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)):
495 global knotCache 496 knotid = (type, turns, direction, outer_width, inner_ratio, 497 stroke_color, fill_color) 498 if knotCache.has_key(knotid): 499 name, height = knotCache[knotid] 500 ps.refSymbol(name, x, y) 501 return height 502 def uniqid(str): 503 if not knotCache.has_key(str): 504 return str 505 for cnt in range(1000): 506 id = str + '_' + str(cnt) 507 if not knotCache.has_key(id): 508 return id 509 return None
510 name = uniqid(type + str(turns) + direction) 511 if name is None: # bail and just emit the knot code 512 return knot(ps, (type, turns, direction), (x, y, outer_width, inner_ratio, stroke_color, fill_color)) 513 # ok, let's make a new symbol. 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
520 -class KhipuKnotTreeAccurateVertical (KhipuTreeAccurateVertical):
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 = {} # bounding boxes for each cord 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 #y = y1 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 # xxx: once we support bbox w/ active transforms: 567 # bb[id].updateBBox(ps.bbox()) 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: # 'S' or 'U' 594 knot = simple_knot_S 595 direction = 'S' # for better caching. 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' # for better caching. 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' # for better caching. 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
626 -class CordColormap (collections.defaultdict):
627 - def __init__ (self, khipu):
628 self.khipu = khipu
629 - def __missing__ (self, key):
630 if isinstance (key, KhipuDB.Cord): 631 return self[key.short_string ()] 632 else: 633 return 0
634 #def default_factory (self): 635 # return 0 636
637 -class SpecialCordColormap (CordColormap):
638 - def __init__ (self, khipu):
639 for cord in khipu.cords (): 640 if cord.short_string () in special_strings: 641 #self[cord] = (1,0,0) 642 self[cord] = special_strings[cord.short_string ()]
643
644 -class PopularCordColormap (CordColormap):
645 colors = [(1,0,0),(0,1,0),(0,0,1),#(1,1,0), 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 #(1,1,0.5), 653 (1,0.5,1),(0.5,1,1), 654 ]
655 - def __init__ (self, khipu):
656 CordColormap.__init__ (self, khipu) 657 cords = [(cord.short_string (), cord) for cord in khipu.cords ()] 658 cords.sort (key = lambda x: x[0]) 659 hist = [(x, len(list(y))) 660 for x,y in itertools.groupby (cords, lambda x: x[0])] 661 hist = [(x,y) for x,y in hist if y > 1] 662 hist.sort (key = lambda x: -x[1]) 663 #print hist[:len (self.colors)] 664 for i, (cord, count) in enumerate (hist[:len (self.colors)]): 665 self[cord] = self.colors[i] 666 if len (hist) > len (self.colors): 667 warnings.warn ('Ran out of colors; wanted %d more' % 668 (len (hist) - len (self.colors)))
669
670 -class MultiletterPopularCordColormap (PopularCordColormap):
671 - def __init__ (self, khipu):
672 PopularCordColormap.__init__ (self, khipu) 673 for key in self.keys (): 674 if ' ' not in key: 675 del self[key]
676
677 -class Labelmap (collections.defaultdict):
678 - def __init__ (self, khipu):
679 self.khipu = khipu
680 - def __missing__ (self, key):
681 if isinstance (key, KhipuDB.Cord): 682 return self[key.cord_id]
683 - def default_factory (self):
684 return None
685
686 -class StringLabelmap (Labelmap):
687 - def __init__ (self, khipu):
688 Labelmap.__init__ (self, khipu) 689 for cord in khipu.cords (): 690 self[cord] = cord.short_string ()
691
692 -class NumberLabelmap (Labelmap):
693 - def __init__ (self, khipu):
694 Labelmap.__init__ (self, khipu) 695 for cord in khipu.cords (): 696 num = cord.numerify () 697 if num is None: 698 self[cord] = cord.short_string () 699 else: 700 self[cord] = str (num)
701
702 -def default_filename (khipu, **options):
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
709 -def render_svg (khipu, **options):
710 svg = Drawing.SVG(khipu.investigator_num) 711 if options.get('script') is not None: 712 svg.loadScript(options.get('script')) 713 render_vector (khipu, svg, 'svg', **options)
714
715 -def render_ps (khipu, **options):
716 render_vector (khipu, Drawing.PostScript(), 'eps', **options)
717
718 -def render_vector (khipu, ps, ext, **options):
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 #colormap = CordColormap (khipu) 734 #colormap = SpecialCordColormap (khipu) 735 colormap = PopularCordColormap (khipu) 736 #colormap = MultiletterPopularCordColormap (khipu) 737 if options.get ('labelmap'): 738 labelmap = Labelmap (khipu) 739 labelmapOpt = options['labelmap'] 740 if isinstance (labelmapOpt, str): 741 #labelmapOpt = yaml.load (open (labelmapOpt, 'r').read ()) 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 #labelmap = Labelmap (khipu) 749 #labelmap = StringLabelmap (khipu) 750 labelmap = NumberLabelmap (khipu) 751 # clear the knot cache 752 global knotCache 753 knotCache = {} 754 #top = KhipuTreeAccurateVertical.make (khipu).postscript (ps, 0) 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'): # experimental hyperlink 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
768 -def render_pdf (khipu, **options):
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 #code = os.spawnlp (os.P_WAIT, 'perl', 'perl', '/usr/bin/epstopdf', epsout) 783 #if code: 784 # print 'Failed to convert to', pdfout, 'with code', code 785 #else: 786 # print 'Converted to', pdfout 787 return pdfout
788
789 -def render (khipu, **options):
790 format = options.get ('format') 791 if format == 'svg': 792 render_svg (khipu, **options) 793 elif format == 'ps': 794 render_ps (khipu, **options) 795 else: 796 render_pdf (khipu, **options)
797
798 -def main ():
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 #if not khipu.investigator_num.startswith ('UR'): continue 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