odoo/openerp/pychart/basecanvas.py

507 lines
18 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
#
# Jockey 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, or (at your option) any
# later version.
#
# Jockey 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.
#
import math
import sys
import time
import re
import font
import pychart_util
import version
from scaling import *
def _compute_bounding_box(points):
"""Given the list of coordinates (x,y), this procedure computes
the smallest rectangle that covers all the points."""
(xmin, ymin, xmax, ymax) = (999999, 999999, -999999, -999999)
for p in points:
xmin = min(xmin, p[0])
xmax = max(xmax, p[0])
ymin = min(ymin, p[1])
ymax = max(ymax, p[1])
return (xmin, ymin, xmax, ymax)
def _intersect_box(b1, b2):
xmin = max(b1[0], b2[0])
ymin = max(b1[1], b2[1])
xmax = min(b1[2], b2[2])
ymax = min(b1[3], b2[3])
return (xmin, ymin, xmax, ymax)
def invisible_p(x, y):
"""Return true if the point (X, Y) is visible in the canvas."""
if x < -499999 or y < -499999:
return 1
return 0
def to_radian(deg):
return deg*2*math.pi / 360.0
def midpoint(p1, p2):
return ( (p1[0]+p2[0])/2.0, (p1[1]+p2[1])/2.0 )
active_canvases = []
InvalidCoord = 999999
class T(object):
def __init__(self):
global active_canvases
self.__xmax = -InvalidCoord
self.__xmin = InvalidCoord
self.__ymax = -InvalidCoord
self.__ymin = InvalidCoord
self.__clip_box = (-InvalidCoord, -InvalidCoord, InvalidCoord, InvalidCoord)
self.__clip_stack = []
self.__nr_gsave = 0
self.title = re.sub("(.*)\\.py$", "\\1", sys.argv[0])
self.creator = "pychart %s" % (version.version,)
self.creation_date = time.strftime("(%m/%d/%y) (%I:%M %p)")
self.aux_comments = ""
self.author = None
active_canvases.append(self)
def set_title(self, s):
"""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."""
self.title = s
def set_creator(self, tag):
"""Define the string to be shown in EPS %%Creator or PDF Producer field. The default value is "pychart"."""
self.creator = tag
def set_creation_date(self, s):
"""Define the string to shown in EPS/PDF "CreationDate" field. Defalt value of this field is the current time."""
self.creation_date = s
def set_author(self, s):
"""Set the author string. Unless this method is called, the Author field is not output in EPS or PDF."""
self.author = s
def add_aux_comments(self, s):
"""Define an auxiliary comments to be output to the file, just after the required headers"""
self.aux_comments += s
def close(self):
"""This method closes the canvas and writes
contents to the associated file.
Calling this procedure is optional, because
Pychart calls this procedure for every open canvas on normal exit."""
for i in range(0, len(active_canvases)):
if active_canvases[i] == self:
del active_canvases[i]
return
def open_output(self, fname):
"""Open the output file FNAME. Returns tuple (FD, NEED_CLOSE),
where FD is a file (or file-like) object, and NEED_CLOSE is a
boolean flag that tells whether FD.close() should be called
after finishing writing to the file.
FNAME can be one of the three things:
(1) None, in which case (sys.stdout, False) is returned.
(2) A file-like object, in which case (fname, False) is returned.
(3) A string, in which case this procedure opens the file and returns
(fd, True)."""
if not fname:
return (sys.stdout, False)
elif isinstance(fname, str):
return (file(fname, "wb"), True)
else:
if not hasattr(fname, "write"):
raise Exception, "Expecting either a filename or a file-like object, but got %s" % fname
return (fname, False)
def setbb(self, x, y):
"""Call this method when point (X,Y) is to be drawn in the
canvas. This methods expands the bounding box to include
this point."""
self.__xmin = min(self.__xmin, max(x, self.__clip_box[0]))
self.__xmax = max(self.__xmax, min(x, self.__clip_box[2]))
self.__ymin = min(self.__ymin, max(y, self.__clip_box[1]))
self.__ymax = max(self.__ymax, min(y, self.__clip_box[3]))
def fill_with_pattern(self, pat, x1, y1, x2, y2):
if invisible_p(x2, y2): return
self.comment("FILL pat=%s (%d %d)-(%d %d)\n" % (pat, x1, y1, x2, y2))
self.set_fill_color(pat.bgcolor)
self._path_polygon([(x1, y1), (x1, y2), (x2, y2), (x2, y1)])
self.fill()
pat.draw(self, x1, y1, x2, y2)
self.comment("end FILL.\n")
def _path_polygon(self, points):
"Low-level polygon-drawing routine."
(xmin, ymin, xmax, ymax) = _compute_bounding_box(points)
if invisible_p(xmax, ymax):
return
self.setbb(xmin, ymin)
self.setbb(xmax, ymax)
self.newpath()
self.moveto(xscale(points[0][0]), yscale(points[0][1]))
for point in points[1:]:
self.lineto(xscale(point[0]), yscale(point[1]))
self.closepath()
def polygon(self, edge_style, pat, points, shadow = None):
"""Draw a polygon with EDGE_STYLE, fill with PAT, and the edges
POINTS. POINTS is a sequence of coordinates, e.g., ((10,10), (15,5),
(20,8)). SHADOW is either None or a tuple (XDELTA, YDELTA,
fillstyle). If non-null, a shadow of FILLSTYLE is drawn beneath
the polygon at the offset of (XDELTA, YDELTA)."""
if pat:
self.comment("POLYGON points=[%s] pat=[%s]"
% (str(points), str(pat)))
(xmin, ymin, xmax, ymax) = _compute_bounding_box(points)
if shadow:
xoff, yoff, shadow_pat = shadow
self.gsave()
self._path_polygon(map(lambda p, xoff=xoff, yoff=yoff: (p[0]+xoff, p[1]+yoff), points))
self.clip_sub()
self.fill_with_pattern(shadow_pat, xmin+xoff, ymin+yoff,
xmax+xoff, ymax+yoff)
self.grestore()
self.gsave()
self._path_polygon(points)
self.clip_sub()
self.fill_with_pattern(pat, xmin, ymin, xmax, ymax)
self.grestore()
if edge_style:
self.comment("POLYGON points=[%s] edge=[%s]"
% (str(points), str(edge_style)))
self.set_line_style(edge_style)
self._path_polygon(points)
self.stroke()
def set_background(self, pat, x1, y1, x2, y2):
xmax, xmin, ymax, ymin = self.__xmax, self.__xmin, self.__ymax, self.__ymin
self.rectangle(None, pat, x1, y1, x2, y2)
self.__xmax, self.__xmin, self.__ymax, self.__ymin = xmax, xmin, ymax, ymin
def rectangle(self, edge_style, pat, x1, y1, x2, y2, shadow = None):
"""Draw a rectangle with EDGE_STYLE, fill with PAT, and the
bounding box (X1, Y1, X2, Y2). SHADOW is either None or a
tuple (XDELTA, YDELTA, fillstyle). If non-null, a shadow of
FILLSTYLE is drawn beneath the polygon at the offset of
(XDELTA, YDELTA)."""
self.polygon(edge_style, pat, [(x1,y1), (x1,y2), (x2,y2), (x2, y1)],
shadow)
def _path_ellipsis(self, x, y, radius, ratio, start_angle, end_angle):
self.setbb(x - radius, y - radius*ratio)
self.setbb(x + radius, y + radius*ratio)
oradius = nscale(radius)
centerx, centery = xscale(x), yscale(y)
startx, starty = centerx+oradius * math.cos(to_radian(start_angle)), \
centery+oradius * math.sin(to_radian(start_angle))
self.moveto(centerx, centery)
if start_angle % 360 != end_angle % 360:
self.moveto(centerx, centery)
self.lineto(startx, starty)
else:
self.moveto(startx, starty)
self.path_arc(xscale(x), yscale(y), nscale(radius),
ratio, start_angle, end_angle)
self.closepath()
def ellipsis(self, line_style, pattern, x, y, radius, ratio = 1.0,
start_angle=0, end_angle=360, shadow=None):
"""Draw an ellipsis with line_style and fill PATTERN. The center is \
(X, Y), X radius is RADIUS, and Y radius is RADIUS*RATIO, whose \
default value is 1.0. SHADOW is either None or a tuple (XDELTA,
YDELTA, fillstyle). If non-null, a shadow of FILLSTYLE is drawn
beneath the polygon at the offset of (XDELTA, YDELTA)."""
if invisible_p(x + radius, y + radius*ratio):
return
if pattern:
if shadow:
x_off, y_off, shadow_pat = shadow
self.gsave()
self.newpath()
self._path_ellipsis(x+x_off, y+y_off, radius, ratio,
start_angle, end_angle)
self.clip_sub()
self.fill_with_pattern(shadow_pat,
x-radius*2+x_off,
y-radius*ratio*2+y_off,
x+radius*2+x_off,
y+radius*ratio*2+y_off)
self.grestore()
self.gsave()
self.newpath()
self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle)
self.clip_sub()
self.fill_with_pattern(pattern,
(x-radius*2), (y-radius*ratio*2),
(x+radius*2), (y+radius*ratio*2))
self.grestore()
if line_style:
self.set_line_style(line_style)
self.newpath()
self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle)
self.stroke()
def clip_ellipsis(self, x, y, radius, ratio = 1.0):
"""Create an elliptical clip region. You must call endclip() after
you completed drawing. See also the ellipsis method."""
self.gsave()
self.newpath()
self.moveto(xscale(x)+nscale(radius), yscale(y))
self.path_arc(xscale(x), yscale(y), nscale(radius), ratio, 0, 360)
self.closepath()
self.__clip_stack.append(self.__clip_box)
self.clip_sub()
def clip_polygon(self, points):
"""Create a polygonal clip region. You must call endclip() after
you completed drawing. See also the polygon method."""
self.gsave()
self._path_polygon(points)
self.__clip_stack.append(self.__clip_box)
self.__clip_box = _intersect_box(self.__clip_box, _compute_bounding_box(points))
self.clip_sub()
def clip(self, x1, y1, x2, y2):
"""Activate a rectangular clip region, (X1, Y1) - (X2, Y2).
You must call endclip() after you completed drawing.
canvas.clip(x,y,x2,y2)
draw something ...
canvas.endclip()
"""
self.__clip_stack.append(self.__clip_box)
self.__clip_box = _intersect_box(self.__clip_box, (x1, y1, x2, y2))
self.gsave()
self.newpath()
self.moveto(xscale(x1), yscale(y1))
self.lineto(xscale(x1), yscale(y2))
self.lineto(xscale(x2), yscale(y2))
self.lineto(xscale(x2), yscale(y1))
self.closepath()
self.clip_sub()
def endclip(self):
"""End the current clip region. When clip calls are nested, it
ends the most recently created crip region."""
self.__clip_box = self.__clip_stack[-1]
del self.__clip_stack[-1]
self.grestore()
def curve(self, style, points):
for p in points:
self.setbb(p[0], p[1])
self.newpath()
self.set_line_style(style)
self.moveto(xscale(points[0][0]), xscale(points[0][1]))
i = 1
n = 1
while i < len(points):
if n == 1:
x2 = points[i]
n += 1
elif n == 2:
x3 = points[i]
n += 1
elif n == 3:
x4 = midpoint(x3, points[i])
self.curveto(xscale(x2[0]), xscale(x2[1]),
xscale(x3[0]), xscale(x3[1]),
xscale(x4[0]), xscale(x4[1]))
n = 1
i += 1
if n == 1:
pass
if n == 2:
self.lineto(xscale(x2[0]), xscale(x2[1]))
if n == 3:
self.curveto(xscale(x2[0]), xscale(x2[1]),
xscale(x2[0]), xscale(x2[1]),
xscale(x3[0]), xscale(x3[1]))
self.stroke()
def line(self, style, x1, y1, x2, y2):
if not style:
return
if invisible_p(x2, y2) and invisible_p(x1, y1):
return
self.setbb(x1, y1)
self.setbb(x2, y2)
self.newpath()
self.set_line_style(style)
self.moveto(xscale(x1), yscale(y1))
self.lineto(xscale(x2), yscale(y2))
self.stroke()
def lines(self, style, segments):
if not style:
return
(xmin, ymin, xmax, ymax) = _compute_bounding_box(segments)
if invisible_p(xmax, ymax):
return
self.setbb(xmin, ymin)
self.setbb(xmax, ymax)
self.newpath()
self.set_line_style(style)
self.moveto(xscale(segments[0][0]), xscale(segments[0][1]))
for i in range(1, len(segments)):
self.lineto(xscale(segments[i][0]), yscale(segments[i][1]))
self.stroke()
def _path_round_rectangle(self, x1, y1, x2, y2, radius):
self.moveto(xscale(x1 + radius), yscale(y1))
self.lineto(xscale(x2 - radius), yscale(y1))
self.path_arc(xscale(x2-radius), yscale(y1+radius), nscale(radius), 1, 270, 360)
self.lineto(xscale(x2), yscale(y2-radius))
self.path_arc(xscale(x2-radius), yscale(y2-radius), nscale(radius), 1, 0, 90)
self.lineto(xscale(x1+radius), yscale(y2))
self.path_arc(xscale(x1 + radius), yscale(y2 - radius), nscale(radius), 1, 90, 180)
self.lineto(xscale(x1), xscale(y1+radius))
self.path_arc(xscale(x1 + radius), yscale(y1 + radius), nscale(radius), 1, 180, 270)
def round_rectangle(self, style, fill, x1, y1, x2, y2, radius, shadow=None):
"""Draw a rectangle with rounded four corners. Parameter <radius> specifies the radius of each corner."""
if invisible_p(x2, y2):
return
self.setbb(x1, y1)
self.setbb(x2, y2)
if fill:
if shadow:
x_off, y_off, shadow_fill = shadow
self.gsave();
self.newpath()
self._path_round_rectangle(x1+x_off, y1+y_off, x2+x_off, y2+y_off,
radius)
self.closepath()
self.clip_sub()
self.fill_with_pattern(shadow_fill, x1+x_off, y1+y_off,
x2+x_off, y2+y_off)
self.grestore()
self.gsave();
self.newpath()
self._path_round_rectangle(x1, y1, x2, y2, radius)
self.closepath()
self.clip_sub()
self.fill_with_pattern(fill, x1, y1, x2, y2)
self.grestore()
if style:
self.set_line_style(style)
self.newpath()
self._path_round_rectangle(x1, y1, x2, y2, radius)
self.closepath()
self.stroke()
def show(self, x, y, str):
global out
y_org = y
org_str = str
if invisible_p(x, y):
return
(xmin, xmax, ymin, ymax) = font.get_dimension(str)
# rectangle(line_style.default, None, x+xmin, y+ymin, x+xmax, y+ymax)
# ellipsis(line_style.default, None, x, y, 1)
self.setbb(x+xmin, y+ymin)
self.setbb(x+xmax, y+ymax)
(halign, valign, angle) = font.get_align(str)
base_x = x
base_y = y
# Handle vertical alignment
if valign == "B":
y = font.unaligned_text_height(str)
elif valign == "T":
y = 0
elif valign == "M":
y = font.unaligned_text_height(str) / 2.0
(xmin, xmax, ymin, ymax) = font.get_dimension(org_str)
# print org_str, xmin, xmax, ymin, ymax, x, y_org, y
self.setbb(x+xmin, y_org+y+ymin)
self.setbb(x+xmax, y_org+y+ymax)
itr = font.text_iterator(None)
max_width = 0
lines = []
for line in str.split('\n'):
cur_width = 0
cur_height = 0
itr.reset(line)
strs = []
while 1:
elem = itr.next()
if not elem:
break
(font_name, size, line_height, color, _h, _v, _a, str) = elem
cur_width += font.line_width(font_name, size, str)
max_width = max(cur_width, max_width)
cur_height = max(cur_height, line_height)
# replace '(' -> '\(', ')' -> '\)' to make
# Postscript string parser happy.
str = str.replace("(", "\\(")
str = str.replace(")", "\\)")
strs.append((font_name, size, color, str))
lines.append((cur_width, cur_height, strs))
for line in lines:
cur_width, cur_height, strs = line
cur_y = y - cur_height
y = y - cur_height
self.comment("cury: %d hei %d str %s\n" % (cur_y, cur_height, strs))
if halign == 'C':
cur_x = -cur_width/2.0
elif halign == 'R':
cur_x = -cur_width
else:
cur_x = 0
rel_x, rel_y = pychart_util.rotate(cur_x, cur_y, angle)
self.text_begin()
self.text_moveto(xscale(base_x + rel_x),
yscale(base_y + rel_y), angle)
for segment in strs:
font_name, size, color, str = segment
self.text_show(font_name, nscale(size), color, str)
self.text_end()