Initial commit: IT Site Survey AI v2 with all features
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/widgets/grids.py
|
||||
__version__='3.3.0'
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isColorOrNone, isBoolean, isListOfNumbers, OneOf, isListOfColors, isNumberOrNone
|
||||
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
||||
from reportlab.graphics.shapes import Drawing, Group, Line, Rect, LineShape, definePath, EmptyClipPath
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from math import radians
|
||||
from reportlab.graphics.transform import translate, rotate, mmult, transformPoints, inverse
|
||||
from reportlab.lib.utils import flatten
|
||||
|
||||
def frange(start, end=None, inc=None):
|
||||
"A range function, that does accept float increments..."
|
||||
|
||||
if end == None:
|
||||
end = start + 0.0
|
||||
start = 0.0
|
||||
|
||||
if inc == None:
|
||||
inc = 1.0
|
||||
|
||||
L = []
|
||||
end = end - inc*0.0001 #to avoid numrical problems
|
||||
while 1:
|
||||
next = start + len(L) * inc
|
||||
if inc > 0 and next >= end:
|
||||
break
|
||||
elif inc < 0 and next <= end:
|
||||
break
|
||||
L.append(next)
|
||||
|
||||
return L
|
||||
|
||||
|
||||
def makeDistancesList(list):
|
||||
"""Returns a list of distances between adjacent numbers in some input list.
|
||||
|
||||
E.g. [1, 1, 2, 3, 5, 7] -> [0, 1, 1, 2, 2]
|
||||
"""
|
||||
|
||||
d = []
|
||||
for i in range(len(list[:-1])):
|
||||
d.append(list[i+1] - list[i])
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class Grid(Widget):
|
||||
"""This makes a rectangular grid of equidistant stripes.
|
||||
|
||||
The grid contains an outer border rectangle, and stripes
|
||||
inside which can be drawn with lines and/or as solid tiles.
|
||||
The drawing order is: outer rectangle, then lines and tiles.
|
||||
|
||||
The stripes' width is indicated as 'delta'. The sequence of
|
||||
stripes can have an offset named 'delta0'. Both values need
|
||||
to be positive!
|
||||
"""
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
|
||||
y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
|
||||
width = AttrMapValue(isNumber, desc="The grid's width."),
|
||||
height = AttrMapValue(isNumber, desc="The grid's height."),
|
||||
orientation = AttrMapValue(OneOf(('vertical', 'horizontal')),
|
||||
desc='Determines if stripes are vertical or horizontal.'),
|
||||
useLines = AttrMapValue(OneOf((0, 1)),
|
||||
desc='Determines if stripes are drawn with lines.'),
|
||||
useRects = AttrMapValue(OneOf((0, 1)),
|
||||
desc='Determines if stripes are drawn with solid rectangles.'),
|
||||
delta = AttrMapValue(isNumber,
|
||||
desc='Determines the width/height of the stripes.'),
|
||||
delta0 = AttrMapValue(isNumber,
|
||||
desc='Determines the stripes initial width/height offset.'),
|
||||
deltaSteps = AttrMapValue(isListOfNumbers,
|
||||
desc='List of deltas to be used cyclically.'),
|
||||
stripeColors = AttrMapValue(isListOfColors,
|
||||
desc='Colors applied cyclically in the right or upper direction.'),
|
||||
fillColor = AttrMapValue(isColorOrNone,
|
||||
desc='Background color for entire rectangle.'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,
|
||||
desc='Color used for lines.'),
|
||||
strokeWidth = AttrMapValue(isNumber,
|
||||
desc='Width used for lines.'),
|
||||
rectStrokeColor = AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'),
|
||||
rectStrokeWidth = AttrMapValue(isNumberOrNone, desc='Width for outer rect stroke.'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
self.orientation = 'vertical'
|
||||
self.useLines = 0
|
||||
self.useRects = 1
|
||||
self.delta = 20
|
||||
self.delta0 = 0
|
||||
self.deltaSteps = []
|
||||
self.fillColor = colors.white
|
||||
self.stripeColors = [colors.red, colors.green, colors.blue]
|
||||
self.strokeColor = colors.black
|
||||
self.strokeWidth = 2
|
||||
|
||||
|
||||
def demo(self):
|
||||
D = Drawing(100, 100)
|
||||
|
||||
g = Grid()
|
||||
D.add(g)
|
||||
|
||||
return D
|
||||
|
||||
def makeOuterRect(self):
|
||||
strokeColor = getattr(self,'rectStrokeColor',self.strokeColor)
|
||||
strokeWidth = getattr(self,'rectStrokeWidth',self.strokeWidth)
|
||||
if self.fillColor or (strokeColor and strokeWidth):
|
||||
rect = Rect(self.x, self.y, self.width, self.height)
|
||||
rect.fillColor = self.fillColor
|
||||
rect.strokeColor = strokeColor
|
||||
rect.strokeWidth = strokeWidth
|
||||
return rect
|
||||
else:
|
||||
return None
|
||||
|
||||
def makeLinePosList(self, start, isX=0):
|
||||
"Returns a list of positions where to place lines."
|
||||
|
||||
w, h = self.width, self.height
|
||||
if isX:
|
||||
length = w
|
||||
else:
|
||||
length = h
|
||||
if self.deltaSteps:
|
||||
r = [start + self.delta0]
|
||||
i = 0
|
||||
while 1:
|
||||
if r[-1] > start + length:
|
||||
del r[-1]
|
||||
break
|
||||
r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)])
|
||||
i = i + 1
|
||||
else:
|
||||
r = frange(start + self.delta0, start + length, self.delta)
|
||||
|
||||
r.append(start + length)
|
||||
if self.delta0 != 0:
|
||||
r.insert(0, start)
|
||||
#print 'Grid.makeLinePosList() -> %s' % r
|
||||
return r
|
||||
|
||||
|
||||
def makeInnerLines(self):
|
||||
# inner grid lines
|
||||
group = Group()
|
||||
|
||||
w, h = self.width, self.height
|
||||
|
||||
if self.useLines == 1:
|
||||
if self.orientation == 'vertical':
|
||||
r = self.makeLinePosList(self.x, isX=1)
|
||||
for x in r:
|
||||
line = Line(x, self.y, x, self.y + h)
|
||||
line.strokeColor = self.strokeColor
|
||||
line.strokeWidth = self.strokeWidth
|
||||
group.add(line)
|
||||
elif self.orientation == 'horizontal':
|
||||
r = self.makeLinePosList(self.y, isX=0)
|
||||
for y in r:
|
||||
line = Line(self.x, y, self.x + w, y)
|
||||
line.strokeColor = self.strokeColor
|
||||
line.strokeWidth = self.strokeWidth
|
||||
group.add(line)
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def makeInnerTiles(self):
|
||||
# inner grid lines
|
||||
group = Group()
|
||||
|
||||
w, h = self.width, self.height
|
||||
|
||||
# inner grid stripes (solid rectangles)
|
||||
if self.useRects == 1:
|
||||
cols = self.stripeColors
|
||||
|
||||
if self.orientation == 'vertical':
|
||||
r = self.makeLinePosList(self.x, isX=1)
|
||||
elif self.orientation == 'horizontal':
|
||||
r = self.makeLinePosList(self.y, isX=0)
|
||||
|
||||
dist = makeDistancesList(r)
|
||||
|
||||
i = 0
|
||||
for j in range(len(dist)):
|
||||
if self.orientation == 'vertical':
|
||||
x = r[j]
|
||||
stripe = Rect(x, self.y, dist[j], h)
|
||||
elif self.orientation == 'horizontal':
|
||||
y = r[j]
|
||||
stripe = Rect(self.x, y, w, dist[j])
|
||||
stripe.fillColor = cols[i % len(cols)]
|
||||
stripe.strokeColor = None
|
||||
group.add(stripe)
|
||||
i = i + 1
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def draw(self):
|
||||
# general widget bits
|
||||
group = Group()
|
||||
|
||||
group.add(self.makeOuterRect())
|
||||
group.add(self.makeInnerTiles())
|
||||
group.add(self.makeInnerLines(),name='_gridLines')
|
||||
|
||||
return group
|
||||
|
||||
|
||||
class DoubleGrid(Widget):
|
||||
"""This combines two ordinary Grid objects orthogonal to each other.
|
||||
"""
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
|
||||
y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
|
||||
width = AttrMapValue(isNumber, desc="The grid's width."),
|
||||
height = AttrMapValue(isNumber, desc="The grid's height."),
|
||||
grid0 = AttrMapValue(None, desc="The first grid component."),
|
||||
grid1 = AttrMapValue(None, desc="The second grid component."),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
|
||||
g0 = Grid()
|
||||
g0.x = self.x
|
||||
g0.y = self.y
|
||||
g0.width = self.width
|
||||
g0.height = self.height
|
||||
g0.orientation = 'vertical'
|
||||
g0.useLines = 1
|
||||
g0.useRects = 0
|
||||
g0.delta = 20
|
||||
g0.delta0 = 0
|
||||
g0.deltaSteps = []
|
||||
g0.fillColor = colors.white
|
||||
g0.stripeColors = [colors.red, colors.green, colors.blue]
|
||||
g0.strokeColor = colors.black
|
||||
g0.strokeWidth = 1
|
||||
|
||||
g1 = Grid()
|
||||
g1.x = self.x
|
||||
g1.y = self.y
|
||||
g1.width = self.width
|
||||
g1.height = self.height
|
||||
g1.orientation = 'horizontal'
|
||||
g1.useLines = 1
|
||||
g1.useRects = 0
|
||||
g1.delta = 20
|
||||
g1.delta0 = 0
|
||||
g1.deltaSteps = []
|
||||
g1.fillColor = colors.white
|
||||
g1.stripeColors = [colors.red, colors.green, colors.blue]
|
||||
g1.strokeColor = colors.black
|
||||
g1.strokeWidth = 1
|
||||
|
||||
self.grid0 = g0
|
||||
self.grid1 = g1
|
||||
|
||||
|
||||
## # This gives an AttributeError:
|
||||
## # DoubleGrid instance has no attribute 'grid0'
|
||||
## def __setattr__(self, name, value):
|
||||
## if name in ('x', 'y', 'width', 'height'):
|
||||
## setattr(self.grid0, name, value)
|
||||
## setattr(self.grid1, name, value)
|
||||
|
||||
|
||||
def demo(self):
|
||||
D = Drawing(100, 100)
|
||||
g = DoubleGrid()
|
||||
D.add(g)
|
||||
return D
|
||||
|
||||
|
||||
def draw(self):
|
||||
group = Group()
|
||||
g0, g1 = self.grid0, self.grid1
|
||||
# Order groups to make sure both v and h lines
|
||||
# are visible (works only when there is only
|
||||
# one kind of stripes, v or h).
|
||||
G = g0.useRects == 1 and g1.useRects == 0 and (g0,g1) or (g1,g0)
|
||||
for g in G:
|
||||
group.add(g.makeOuterRect())
|
||||
for g in G:
|
||||
group.add(g.makeInnerTiles())
|
||||
group.add(g.makeInnerLines(),name='_gridLines')
|
||||
|
||||
return group
|
||||
|
||||
|
||||
class ShadedRect(Widget):
|
||||
"""This makes a rectangle with shaded colors between two colors.
|
||||
|
||||
Colors are interpolated linearly between 'fillColorStart'
|
||||
and 'fillColorEnd', both of which appear at the margins.
|
||||
If 'numShades' is set to one, though, only 'fillColorStart'
|
||||
is used.
|
||||
"""
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
|
||||
y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
|
||||
width = AttrMapValue(isNumber, desc="The grid's width."),
|
||||
height = AttrMapValue(isNumber, desc="The grid's height."),
|
||||
orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'),
|
||||
numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'),
|
||||
fillColorStart = AttrMapValue(isColorOrNone, desc='Start value of the color shade.'),
|
||||
fillColorEnd = AttrMapValue(isColorOrNone, desc='End value of the color shade.'),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border line.'),
|
||||
strokeWidth = AttrMapValue(isNumber, desc='Width used for lines.'),
|
||||
cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'),
|
||||
)
|
||||
|
||||
def __init__(self,**kw):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
self.orientation = 'vertical'
|
||||
self.numShades = 20
|
||||
self.fillColorStart = colors.pink
|
||||
self.fillColorEnd = colors.black
|
||||
self.strokeColor = colors.black
|
||||
self.strokeWidth = 2
|
||||
self.cylinderMode = 0
|
||||
self.setProperties(kw)
|
||||
|
||||
def demo(self):
|
||||
D = Drawing(100, 100)
|
||||
g = ShadedRect()
|
||||
D.add(g)
|
||||
|
||||
return D
|
||||
|
||||
def _flipRectCorners(self):
|
||||
"Flip rectangle's corners if width or height is negative."
|
||||
x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd
|
||||
if width < 0 and height > 0:
|
||||
x = x + width
|
||||
width = -width
|
||||
if self.orientation=='vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart
|
||||
elif height<0 and width>0:
|
||||
y = y + height
|
||||
height = -height
|
||||
if self.orientation=='horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart
|
||||
elif height < 0 and height < 0:
|
||||
x = x + width
|
||||
width = -width
|
||||
y = y + height
|
||||
height = -height
|
||||
return x, y, width, height, fillColorStart, fillColorEnd
|
||||
|
||||
def draw(self):
|
||||
# general widget bits
|
||||
group = Group()
|
||||
x, y, w, h, c0, c1 = self._flipRectCorners()
|
||||
vertical = self.orientation == 'vertical'
|
||||
cylinderMode = self.cylinderMode
|
||||
linG = getattr(getattr(self,'_canvas',None),'linearGradient',None)
|
||||
if linG:
|
||||
canv = linG.__self__
|
||||
canv.saveState()
|
||||
p = canv.beginPath()
|
||||
p.rect(x, y, w, h)
|
||||
canv.clipPath(p, stroke=0)
|
||||
if cylinderMode:
|
||||
if vertical:
|
||||
linG(x, y, x+w/2, y, (c0,c1), extend=False)
|
||||
linG(x+w/2, y, x+w, y, (c1,c0), extend=False)
|
||||
else:
|
||||
linG(x, y, x, y+h/2, (c0,c1), extend=False)
|
||||
linG(x, y+h/2, x, y+h, (c1,c0), extend=False)
|
||||
else:
|
||||
if vertical:
|
||||
linG(x, y, x+w, y, (c0,c1), extend=False)
|
||||
else:
|
||||
linG(x, y, x, y+h, (c0,c1), extend=False)
|
||||
canv.restoreState()
|
||||
else:
|
||||
numShades = self.numShades
|
||||
if cylinderMode:
|
||||
if not numShades%2: numShades = numShades+1
|
||||
halfNumShades = int((numShades-1)/2) + 1
|
||||
num = float(numShades) # must make it float!
|
||||
if vertical:
|
||||
if numShades == 1:
|
||||
V = [x]
|
||||
else:
|
||||
V = frange(x, x + w, w/num)
|
||||
else:
|
||||
if numShades == 1:
|
||||
V = [y]
|
||||
else:
|
||||
V = frange(y, y + h, h/num)
|
||||
|
||||
for v in V:
|
||||
stripe = vertical and Rect(v, y, w/num, h) or Rect(x, v, w, h/num)
|
||||
if cylinderMode:
|
||||
if V.index(v)>=halfNumShades:
|
||||
col = colors.linearlyInterpolatedColor(c1,c0,V[halfNumShades],V[-1], v)
|
||||
else:
|
||||
col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[halfNumShades], v)
|
||||
else:
|
||||
col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[-1], v)
|
||||
stripe.fillColor = col
|
||||
stripe.strokeColor = col
|
||||
stripe.strokeWidth = 1
|
||||
group.add(stripe)
|
||||
if self.strokeColor and self.strokeWidth>=0:
|
||||
rect = Rect(x, y, w, h)
|
||||
rect.strokeColor = self.strokeColor
|
||||
rect.strokeWidth = self.strokeWidth
|
||||
rect.fillColor = None
|
||||
group.add(rect)
|
||||
return group
|
||||
|
||||
|
||||
def colorRange(c0, c1, n):
|
||||
"Return a range of intermediate colors between c0 and c1"
|
||||
if n==1: return [c0]
|
||||
|
||||
C = []
|
||||
if n>1:
|
||||
lim = n-1
|
||||
for i in range(n):
|
||||
C.append(colors.linearlyInterpolatedColor(c0,c1,0,lim, i))
|
||||
return C
|
||||
|
||||
|
||||
def centroid(P):
|
||||
'''compute average point of a set of points'''
|
||||
cx = 0
|
||||
cy = 0
|
||||
for x,y in P:
|
||||
cx+=x
|
||||
cy+=y
|
||||
n = len(P)
|
||||
return cx/n, cy/n
|
||||
|
||||
def rotatedEnclosingRect(P, angle, rect):
|
||||
'''
|
||||
given P a sequence P of x,y coordinate pairs and an angle in degrees
|
||||
find the centroid of P and the axis at angle theta through it
|
||||
find the extreme points of P wrt axis parallel distance and axis
|
||||
orthogonal distance. Then compute the least rectangle that will still
|
||||
enclose P when rotated by angle. Positive angles correspond to clockwise
|
||||
rotation of the enclosing rect.
|
||||
'''
|
||||
x0, y0 = centroid(P)
|
||||
theta = radians(angle)
|
||||
#translate to the centroid and rotate
|
||||
mx = mmult(translate(x0,y0),rotate(angle))
|
||||
|
||||
#compute min and max of x and y of the rotated points
|
||||
tp = flatten(transformPoints(mx,P))
|
||||
xx = tp[::2]
|
||||
yx = tp[1::2]
|
||||
xn = min(xx)
|
||||
xx = max(xx)
|
||||
yn = min(yx)
|
||||
yx = max(yx)
|
||||
|
||||
#make the enclosing rect and invert the original transform
|
||||
rect.x = xn
|
||||
rect.width = xx-xn
|
||||
rect.y = yn
|
||||
rect.height = yx-yn
|
||||
g = Group(transform=inverse(mx))
|
||||
g.add(rect)
|
||||
return g
|
||||
|
||||
class ShadedPolygon(Widget,LineShape):
|
||||
'''given a list of points [(x0,y0),....] we construct an enclosing
|
||||
shaded rectangle and mask using the polygon points.
|
||||
At angle 0 the shading fillColorStart left --> fillColorEnd right.
|
||||
positive angles rotate the shading clockwise.
|
||||
'''
|
||||
_attrMap = AttrMap(BASE=LineShape,
|
||||
angle = AttrMapValue(isNumber,desc="Shading angle"),
|
||||
fillColorStart = AttrMapValue(isColorOrNone),
|
||||
fillColorEnd = AttrMapValue(isColorOrNone),
|
||||
numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'),
|
||||
cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'),
|
||||
points = AttrMapValue(isListOfNumbers),
|
||||
)
|
||||
|
||||
def __init__(self,**kw):
|
||||
self.angle = 90
|
||||
self.fillColorStart = colors.red
|
||||
self.fillColorEnd = colors.green
|
||||
self.cylinderMode = 0
|
||||
self.numShades = 50
|
||||
self.points = [-1,-1,2,2,3,-1]
|
||||
LineShape.__init__(self,kw)
|
||||
|
||||
def draw(self):
|
||||
P = self.points
|
||||
P = list(zip(P[::2],P[1::2]))
|
||||
path = definePath([('moveTo',)+P[0]]+[('lineTo',)+x for x in P[1:]]+['closePath'],
|
||||
fillColor=None, strokeColor=None)
|
||||
path.isClipPath = 1
|
||||
g = Group()
|
||||
g.add(path)
|
||||
angle = self.angle % 360
|
||||
orientation = 'horizontal' if 0<=angle<=45 or 315<=angle<=360 or 135<=angle<=225 else 'vertical'
|
||||
rect = ShadedRect(strokeWidth=0,strokeColor=None,orientation=orientation)
|
||||
for k in 'fillColorStart', 'fillColorEnd', 'numShades', 'cylinderMode':
|
||||
setattr(rect,k,getattr(self,k))
|
||||
g.add(rotatedEnclosingRect(P, angle, rect))
|
||||
g.add(EmptyClipPath)
|
||||
path = path.copy()
|
||||
path.isClipPath = 0
|
||||
path.strokeColor = self.strokeColor
|
||||
path.strokeWidth = self.strokeWidth
|
||||
g.add(path)
|
||||
return g
|
||||
|
||||
if __name__=='__main__': #noruntests
|
||||
angle=45
|
||||
D = Drawing(120,120)
|
||||
D.add(ShadedPolygon(points=(10,10,60,60,110,10),strokeColor=None,strokeWidth=1,angle=90,numShades=50,cylinderMode=0))
|
||||
D.save(formats=['pdf','gif'],fnRoot='shobj',outDir='/tmp')
|
||||
Reference in New Issue
Block a user