[MERGE] Remove the embedded pychart library, and use the online version
[odoo/odoo.git] /
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
4
5 # Jockey is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2, or (at your option) any
8 # later version.
9 #
10 # Jockey is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
13 # for more details.
14 #
15 import math
16 import sys
17 import time
18 import re
19
20 import font
21 import pychart_util
22 import version
23 from scaling import *
24
25 def _compute_bounding_box(points):
26     """Given the list of coordinates (x,y), this procedure computes
27     the smallest rectangle that covers all the points."""
28     (xmin, ymin, xmax, ymax) = (999999, 999999, -999999, -999999)
29     for p in points:
30         xmin = min(xmin, p[0])
31         xmax = max(xmax, p[0])
32         ymin = min(ymin, p[1])
33         ymax = max(ymax, p[1])
34     return (xmin, ymin, xmax, ymax)
35
36 def _intersect_box(b1, b2):
37     xmin = max(b1[0], b2[0])
38     ymin = max(b1[1], b2[1])
39     xmax = min(b1[2], b2[2])
40     ymax = min(b1[3], b2[3])
41     return (xmin, ymin, xmax, ymax)
42
43 def invisible_p(x, y):
44     """Return true if the point (X, Y) is visible in the canvas."""
45     if x < -499999 or y < -499999:
46         return 1
47     return 0
48
49 def to_radian(deg):
50     return deg*2*math.pi / 360.0
51
52 def midpoint(p1, p2):
53     return ( (p1[0]+p2[0])/2.0, (p1[1]+p2[1])/2.0 )
54
55
56 active_canvases = []
57
58 InvalidCoord = 999999
59 class T(object):
60     def __init__(self):
61         global active_canvases
62         
63         self.__xmax = -InvalidCoord
64         self.__xmin = InvalidCoord
65         self.__ymax = -InvalidCoord
66         self.__ymin = InvalidCoord
67         self.__clip_box = (-InvalidCoord, -InvalidCoord, InvalidCoord, InvalidCoord)
68         self.__clip_stack = []
69         self.__nr_gsave = 0
70         
71         self.title = re.sub("(.*)\\.py$", "\\1", sys.argv[0])
72         self.creator = "pychart %s" % (version.version,)
73         self.creation_date = time.strftime("(%m/%d/%y) (%I:%M %p)")
74         self.aux_comments = ""
75         self.author = None
76         active_canvases.append(self)
77
78     def set_title(self, s):
79         """Define the string to shown in EPS/PDF "Title" field. The default value is the name of the script that creates the EPS/PDF file."""
80         self.title = s
81         
82     def set_creator(self, tag):
83         """Define the string to be shown in EPS %%Creator or PDF Producer field. The default value is "pychart"."""
84         self.creator = tag
85         
86     def set_creation_date(self, s):
87         """Define the string to shown in EPS/PDF "CreationDate" field. Defalt value of this field is the current time."""
88         self.creation_date = s
89         
90     def set_author(self, s):
91         """Set the author string. Unless this method is called, the Author field is not output in EPS or PDF."""
92         self.author = s
93         
94     def add_aux_comments(self, s):
95         """Define an auxiliary comments to be output to the file, just after the required headers"""
96         self.aux_comments += s
97         
98     def close(self):
99         """This method closes the canvas and writes
100         contents to the associated file.
101         Calling this procedure is optional, because
102         Pychart calls this procedure for every open canvas on normal exit."""
103         for i in range(0, len(active_canvases)):
104             if active_canvases[i] == self:
105                 del active_canvases[i]
106                 return
107
108     def open_output(self, fname):
109         """Open the output file FNAME. Returns tuple (FD, NEED_CLOSE),
110         where FD is a file (or file-like) object, and NEED_CLOSE is a
111         boolean flag that tells whether FD.close() should be called
112         after finishing writing to the file.
113         
114         FNAME can be one of the three things:
115         (1) None, in which case (sys.stdout, False) is returned.
116         (2) A file-like object, in which case (fname, False) is returned.
117         (3) A string, in which case this procedure opens the file and returns
118         (fd, True)."""
119
120         if not fname:
121             return (sys.stdout, False)
122         elif isinstance(fname, str):
123             return (file(fname, "wb"), True)
124         else:
125             if not hasattr(fname, "write"):
126                 raise Exception, "Expecting either a filename or a file-like object, but got %s" % fname
127             return (fname, False)
128         
129     def setbb(self, x, y):
130         """Call this method when point (X,Y) is to be drawn in the
131         canvas. This methods expands the bounding box to include
132         this point.""" 
133         self.__xmin = min(self.__xmin, max(x, self.__clip_box[0]))
134         self.__xmax = max(self.__xmax, min(x, self.__clip_box[2]))
135         self.__ymin = min(self.__ymin, max(y, self.__clip_box[1]))
136         self.__ymax = max(self.__ymax, min(y, self.__clip_box[3]))
137
138     def fill_with_pattern(self, pat, x1, y1, x2, y2):
139         if invisible_p(x2, y2): return
140         
141         self.comment("FILL pat=%s (%d %d)-(%d %d)\n" % (pat, x1, y1, x2, y2))
142         self.set_fill_color(pat.bgcolor)
143         self._path_polygon([(x1, y1), (x1, y2), (x2, y2), (x2, y1)])
144         self.fill()
145         pat.draw(self, x1, y1, x2, y2)
146         self.comment("end FILL.\n")
147
148     def _path_polygon(self, points):
149         "Low-level polygon-drawing routine."
150         (xmin, ymin, xmax, ymax) = _compute_bounding_box(points)
151         if invisible_p(xmax, ymax):
152             return
153         self.setbb(xmin, ymin)
154         self.setbb(xmax, ymax)
155         
156         self.newpath()
157         self.moveto(xscale(points[0][0]), yscale(points[0][1]))
158         for point in points[1:]:
159             self.lineto(xscale(point[0]), yscale(point[1]))
160         self.closepath()
161
162     def polygon(self, edge_style, pat, points, shadow = None):
163         """Draw a polygon with EDGE_STYLE, fill with PAT, and the edges
164         POINTS. POINTS is a sequence of coordinates, e.g., ((10,10), (15,5),
165         (20,8)). SHADOW is either None or a tuple (XDELTA, YDELTA,
166         fillstyle). If non-null, a shadow of FILLSTYLE is drawn beneath
167         the polygon at the offset of (XDELTA, YDELTA)."""
168
169         if pat:
170             self.comment("POLYGON points=[%s] pat=[%s]"
171                         % (str(points), str(pat)))
172             (xmin, ymin, xmax, ymax) = _compute_bounding_box(points)
173
174             if shadow:
175                 xoff, yoff, shadow_pat = shadow
176                 self.gsave()
177                 self._path_polygon(map(lambda p, xoff=xoff, yoff=yoff: (p[0]+xoff, p[1]+yoff), points))
178                 self.clip_sub()
179                 self.fill_with_pattern(shadow_pat, xmin+xoff, ymin+yoff,
180                                        xmax+xoff, ymax+yoff)
181                 self.grestore()
182
183             self.gsave()
184             self._path_polygon(points)
185             self.clip_sub()
186             self.fill_with_pattern(pat, xmin, ymin, xmax, ymax)
187             self.grestore()
188         if edge_style:
189             self.comment("POLYGON points=[%s] edge=[%s]"
190                          % (str(points), str(edge_style)))
191             self.set_line_style(edge_style)
192             self._path_polygon(points)
193             self.stroke()
194                 
195     def set_background(self, pat, x1, y1, x2, y2):
196         xmax, xmin, ymax, ymin = self.__xmax, self.__xmin, self.__ymax, self.__ymin
197         self.rectangle(None, pat, x1, y1, x2, y2)
198         self.__xmax, self.__xmin, self.__ymax, self.__ymin = xmax, xmin, ymax, ymin 
199         
200     def rectangle(self, edge_style, pat, x1, y1, x2, y2, shadow = None):
201         """Draw a rectangle with EDGE_STYLE, fill with PAT, and the
202         bounding box (X1, Y1, X2, Y2).  SHADOW is either None or a
203         tuple (XDELTA, YDELTA, fillstyle). If non-null, a shadow of
204         FILLSTYLE is drawn beneath the polygon at the offset of
205         (XDELTA, YDELTA)."""
206         
207         self.polygon(edge_style, pat, [(x1,y1), (x1,y2), (x2,y2), (x2, y1)],
208                      shadow)
209
210     def _path_ellipsis(self, x, y, radius, ratio, start_angle, end_angle):
211         self.setbb(x - radius, y - radius*ratio)
212         self.setbb(x + radius, y + radius*ratio)
213         oradius = nscale(radius)
214         centerx, centery = xscale(x), yscale(y)
215         startx, starty = centerx+oradius * math.cos(to_radian(start_angle)), \
216                          centery+oradius * math.sin(to_radian(start_angle))
217         self.moveto(centerx, centery)
218         if start_angle % 360 != end_angle % 360:
219             self.moveto(centerx, centery)
220             self.lineto(startx, starty)
221         else:
222             self.moveto(startx, starty)
223         self.path_arc(xscale(x), yscale(y), nscale(radius),
224                      ratio, start_angle, end_angle)
225         self.closepath()
226
227     def ellipsis(self, line_style, pattern, x, y, radius, ratio = 1.0,
228                  start_angle=0, end_angle=360, shadow=None):
229         """Draw an ellipsis with line_style and fill PATTERN. The center is \
230         (X, Y), X radius is RADIUS, and Y radius is RADIUS*RATIO, whose \
231         default value is 1.0. SHADOW is either None or a tuple (XDELTA,
232         YDELTA, fillstyle). If non-null, a shadow of FILLSTYLE is drawn
233         beneath the polygon at the offset of (XDELTA, YDELTA)."""
234
235         if invisible_p(x + radius, y + radius*ratio):
236             return
237
238         if pattern:
239             if shadow:
240                 x_off, y_off, shadow_pat = shadow
241                 self.gsave()
242                 self.newpath()
243                 self._path_ellipsis(x+x_off, y+y_off, radius, ratio,
244                                     start_angle, end_angle)
245                 self.clip_sub()
246                 self.fill_with_pattern(shadow_pat,
247                                        x-radius*2+x_off,
248                                        y-radius*ratio*2+y_off,
249                                        x+radius*2+x_off,
250                                        y+radius*ratio*2+y_off)
251                 self.grestore()
252             self.gsave()
253             self.newpath()              
254             self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle)
255             self.clip_sub()
256             self.fill_with_pattern(pattern,
257                                    (x-radius*2), (y-radius*ratio*2),
258                                    (x+radius*2), (y+radius*ratio*2))
259             self.grestore()
260         if line_style:
261             self.set_line_style(line_style)
262             self.newpath()
263             self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle)
264             self.stroke()
265
266     def clip_ellipsis(self, x, y, radius, ratio = 1.0):
267         """Create an elliptical clip region. You must call endclip() after
268         you completed drawing. See also the ellipsis method."""
269
270         self.gsave()
271         self.newpath()
272         self.moveto(xscale(x)+nscale(radius), yscale(y))
273         self.path_arc(xscale(x), yscale(y), nscale(radius), ratio, 0, 360)
274         self.closepath()
275         self.__clip_stack.append(self.__clip_box)
276         self.clip_sub()
277         
278     def clip_polygon(self, points):
279         """Create a polygonal clip region. You must call endclip() after
280         you completed drawing. See also the polygon method."""
281         self.gsave()
282         self._path_polygon(points)
283         self.__clip_stack.append(self.__clip_box)
284         self.__clip_box = _intersect_box(self.__clip_box, _compute_bounding_box(points))
285         self.clip_sub()
286                 
287     def clip(self, x1, y1, x2, y2):
288         """Activate a rectangular clip region, (X1, Y1) - (X2, Y2).
289         You must call endclip() after you completed drawing.
290         
291 canvas.clip(x,y,x2,y2)
292 draw something ...
293 canvas.endclip()
294 """
295     
296         self.__clip_stack.append(self.__clip_box)
297         self.__clip_box = _intersect_box(self.__clip_box, (x1, y1, x2, y2))
298         self.gsave()
299         self.newpath()
300         self.moveto(xscale(x1), yscale(y1))
301         self.lineto(xscale(x1), yscale(y2))
302         self.lineto(xscale(x2), yscale(y2))
303         self.lineto(xscale(x2), yscale(y1))
304         self.closepath()
305         self.clip_sub()
306         
307     def endclip(self):
308         """End the current clip region. When clip calls are nested, it
309         ends the most recently created crip region."""
310         self.__clip_box = self.__clip_stack[-1]
311         del self.__clip_stack[-1]
312         self.grestore()
313
314     def curve(self, style, points):
315         for p in points:
316             self.setbb(p[0], p[1])
317         self.newpath()
318         self.set_line_style(style)
319         self.moveto(xscale(points[0][0]), xscale(points[0][1]))
320         i = 1
321         n = 1
322         while i < len(points):
323             if n == 1:
324                 x2 = points[i]
325                 n += 1
326             elif n == 2:
327                 x3 = points[i]
328                 n += 1
329             elif n == 3:
330                 x4 = midpoint(x3, points[i])
331                 self.curveto(xscale(x2[0]), xscale(x2[1]),
332                             xscale(x3[0]), xscale(x3[1]),
333                             xscale(x4[0]), xscale(x4[1]))
334                 n = 1
335             i += 1
336             if n == 1:
337                 pass
338             if n == 2:
339                 self.lineto(xscale(x2[0]), xscale(x2[1]))
340             if n == 3:
341                 self.curveto(xscale(x2[0]), xscale(x2[1]),
342                             xscale(x2[0]), xscale(x2[1]),
343                             xscale(x3[0]), xscale(x3[1]))
344             self.stroke()
345                 
346     def line(self, style, x1, y1, x2, y2):
347         if not style:
348             return
349         if invisible_p(x2, y2) and invisible_p(x1, y1):
350             return
351
352         self.setbb(x1, y1)
353         self.setbb(x2, y2)
354
355         self.newpath()
356         self.set_line_style(style)
357         self.moveto(xscale(x1), yscale(y1))
358         self.lineto(xscale(x2), yscale(y2))
359         self.stroke()
360
361     def lines(self, style, segments):
362         if not style:
363             return
364         (xmin, ymin, xmax, ymax) = _compute_bounding_box(segments)
365         if invisible_p(xmax, ymax):
366             return
367
368         self.setbb(xmin, ymin)
369         self.setbb(xmax, ymax)
370         self.newpath()
371         self.set_line_style(style)
372         self.moveto(xscale(segments[0][0]), xscale(segments[0][1]))
373         for i in range(1, len(segments)):
374             self.lineto(xscale(segments[i][0]), yscale(segments[i][1]))
375         self.stroke()
376
377     def _path_round_rectangle(self, x1, y1, x2, y2, radius):
378         self.moveto(xscale(x1 + radius), yscale(y1))
379         self.lineto(xscale(x2 - radius), yscale(y1))
380         self.path_arc(xscale(x2-radius), yscale(y1+radius), nscale(radius), 1, 270, 360)
381         self.lineto(xscale(x2), yscale(y2-radius))
382         self.path_arc(xscale(x2-radius), yscale(y2-radius), nscale(radius), 1, 0, 90)
383         self.lineto(xscale(x1+radius), yscale(y2))
384         self.path_arc(xscale(x1 + radius), yscale(y2 - radius), nscale(radius), 1, 90, 180)
385         self.lineto(xscale(x1), xscale(y1+radius))
386         self.path_arc(xscale(x1 + radius), yscale(y1 + radius), nscale(radius), 1, 180, 270)
387         
388     def round_rectangle(self, style, fill, x1, y1, x2, y2, radius, shadow=None):
389         """Draw a rectangle with rounded four corners. Parameter <radius> specifies the radius of each corner."""
390
391         if invisible_p(x2, y2):
392             return
393         self.setbb(x1, y1)
394         self.setbb(x2, y2)
395
396         if fill:
397             if shadow:
398                 x_off, y_off, shadow_fill = shadow
399                 self.gsave();
400                 self.newpath()
401                 self._path_round_rectangle(x1+x_off, y1+y_off, x2+x_off, y2+y_off,
402                                            radius)
403                 self.closepath()
404                 self.clip_sub()
405                 self.fill_with_pattern(shadow_fill, x1+x_off, y1+y_off,
406                                        x2+x_off, y2+y_off)
407                 self.grestore()
408
409             self.gsave();
410             self.newpath()
411             self._path_round_rectangle(x1, y1, x2, y2, radius)
412             self.closepath()
413             self.clip_sub()
414             self.fill_with_pattern(fill, x1, y1, x2, y2)
415             self.grestore()
416         if style:
417             self.set_line_style(style)
418             self.newpath()
419             self._path_round_rectangle(x1, y1, x2, y2, radius)
420             self.closepath()
421             self.stroke()
422
423     def show(self, x, y, str):
424         global out
425         y_org = y
426         org_str = str
427
428         if invisible_p(x, y):
429             return
430
431         (xmin, xmax, ymin, ymax) = font.get_dimension(str)
432
433         # rectangle(line_style.default, None, x+xmin, y+ymin, x+xmax, y+ymax)
434         # ellipsis(line_style.default, None, x, y, 1)
435         self.setbb(x+xmin, y+ymin)
436         self.setbb(x+xmax, y+ymax)
437
438         (halign, valign, angle) = font.get_align(str)
439
440         base_x = x
441         base_y = y
442
443         # Handle vertical alignment
444         if valign == "B":
445             y = font.unaligned_text_height(str)
446         elif valign == "T":
447             y = 0
448         elif valign == "M":
449             y = font.unaligned_text_height(str) / 2.0
450
451         (xmin, xmax, ymin, ymax) = font.get_dimension(org_str)
452         # print org_str, xmin, xmax, ymin, ymax, x, y_org, y
453         self.setbb(x+xmin, y_org+y+ymin)
454         self.setbb(x+xmax, y_org+y+ymax)
455         itr = font.text_iterator(None)
456
457         max_width = 0
458
459         lines = []
460         for line in str.split('\n'):
461             cur_width = 0
462             cur_height = 0
463
464             itr.reset(line)
465
466             strs = []
467
468             while 1:
469                 elem = itr.next()
470                 if not elem:
471                     break
472
473                 (font_name, size, line_height, color, _h, _v, _a, str) = elem
474                 cur_width += font.line_width(font_name, size, str)
475                 max_width = max(cur_width, max_width)
476                 cur_height = max(cur_height, line_height)
477
478                 # replace '(' -> '\(', ')' -> '\)' to make
479                 # Postscript string parser happy.
480                 str = str.replace("(", "\\(")
481                 str = str.replace(")", "\\)")
482                 strs.append((font_name, size, color, str))
483             lines.append((cur_width, cur_height, strs))
484
485         for line in lines:
486             cur_width, cur_height, strs = line
487             cur_y = y - cur_height
488             y = y - cur_height
489             self.comment("cury: %d hei %d str %s\n" % (cur_y, cur_height, strs))
490             if halign == 'C':
491                 cur_x = -cur_width/2.0
492             elif halign == 'R':
493                 cur_x = -cur_width
494             else:
495                 cur_x = 0
496
497             rel_x, rel_y = pychart_util.rotate(cur_x, cur_y, angle)
498             self.text_begin()
499             self.text_moveto(xscale(base_x + rel_x),
500                             yscale(base_y + rel_y), angle)
501             for segment in strs:
502                 font_name, size, color, str = segment
503                 self.text_show(font_name, nscale(size), color, str)
504             self.text_end()
505
506