#!/usr/bin/env python
from __future__ import division
import wx
import math

SCALE = 1.0

class MyFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(600,400))
        self.panel = MyPanel(self)

class MyPanel(wx.Panel):
    def __init__(self,frame):
        wx.Panel.__init__(self,frame,-1)
        self.obj = []
        self.obj.append(MessageEvent(self,20,20,SCALE))
        self.obj.append(TimerEvent(self,120,20,SCALE))
        self.obj.append(MessageEvent(self,220,20,SCALE))
        self.obj.append(Task(self,20,220,100,50,'Start'))
        self.obj.append(Task(self,220,120,100,50,'Task 1'))
        self.obj.append(Task(self,220,220,100,50,'Task 2'))
        self.obj.append(Task(self,220,320,100,50,'Task 3'))
        self.obj.append(Task(self,420,220,100,50,'Finish'))
        self.currentObj = None

        self.Bind(wx.EVT_PAINT,self.onPaint)
        self.Bind(wx.EVT_LEFT_DOWN,self.onLeftDown)
        self.Bind(wx.EVT_LEFT_UP,self.onLeftUp)
        self.Bind(wx.EVT_MOTION,self.onMotion)

    def onPaint(self,evt):
        dc = wx.PaintDC(self)
        for obj in self.obj:
            obj.onPaint(dc)
        self.drawConnect(dc)

    def drawConnect(self,dc,clear=False):
        gc = wx.GraphicsContext.Create(dc)
        if clear:
            gc.SetPen(wx.Pen(self.BackgroundColour, 1))
            gc.SetBrush(wx.Brush(self.BackgroundColour))
        else:
            gc.SetPen(wx.Pen("black", 1))
            gc.SetBrush(wx.Brush(wx.BLACK))
        self.connect((self.obj[3],0,3),(self.obj[4],0,1),gc)
        self.connect((self.obj[3],1,3),(self.obj[5],0,1),gc)
        self.connect((self.obj[3],2,3),(self.obj[6],0,1),gc)
        self.connect((self.obj[4],0,1),(self.obj[7],0,3),gc)
        self.connect((self.obj[5],0,1),(self.obj[7],1,3),gc)
        self.connect((self.obj[6],0,1),(self.obj[7],2,3),gc)

    def connect(self,(src_obj,src_ndx,src_tot),(tgt_obj,tgt_ndx,tgt_tot),gc):
        source_x,source_y = src_obj.getSource(src_ndx,src_tot)
        target_x,target_y = tgt_obj.getTarget(tgt_ndx,tgt_tot)
        if source_y == target_y:
            gc.StrokeLine(source_x,source_y,target_x-10,target_y)
        else:
#           lines = []
#           lines.append((source_x,source_y))
#           lines.append((mid_x,source_y))
#           lines.append((mid_x,target_y))
#           lines.append((target_x-10,target_y))
#           gc.StrokeLines(lines)
            mid_x = source_x+((target_x-source_x)/2)

            path = gc.CreatePath()
            path.MoveToPoint(source_x,source_y)
            path.AddLineToPoint(mid_x-10,source_y)
            if target_y > source_y:
                path.AddArc(mid_x-5,source_y+5,5,math.radians(270),math.radians(0))
                path.AddLineToPoint(mid_x,target_y-10)
                path.AddArc(mid_x+5,target_y-5,5,math.radians(180),math.radians(90),False)
            else:
                path.AddArc(mid_x-5,source_y-5,5,math.radians(90),math.radians(0),False)
                path.AddLineToPoint(mid_x,target_y+10)
                path.AddArc(mid_x+5,target_y+5,5,math.radians(180),math.radians(270))
            path.AddLineToPoint(target_x-10,target_y)
            gc.StrokePath(path)

        # draw arrow-head
        gc.DrawLines([(target_x-10,target_y-4),
                        (target_x-10,target_y+4),
                        (target_x,target_y),
                        (target_x-10,target_y-4)])

    def onLeftDown(self,evt):
        self.currentObj = None
        for obj in self.obj:
            if obj.HitTest(evt.X,evt.Y):
                self.currentObj = obj
                self.point = evt.X,evt.Y
                break

    def onLeftUp(self,evt):
        if self.currentObj:
            obj = self.currentObj
            point = evt.X,evt.Y
            obj.x += (point[0] - self.point[0])
            obj.y += (point[1] - self.point[1])
            obj.region = obj.getRegion(obj.x,obj.y)
            self.currentObj = None
#           dc = wx.ClientDC(self)
#           self.drawConnect(dc,True)

    def onMotion(self,evt):
        if self.currentObj is None:
            return
        point = evt.X,evt.Y
        obj = self.currentObj

        # this works well, without flicker, but does not redraw lines
        #self.RefreshRect(obj.getRect())  # erase old image
        #obj.x += (point[0] - self.point[0])  # reset new co-ords
        #obj.y += (point[1] - self.point[1])
        #dc = wx.ClientDC(self)
        #obj.onPaint(dc)

        # this redraws everything, including lines, but causes flicker on msw
        obj.x += (point[0] - self.point[0])  # reset new co-ords
        obj.y += (point[1] - self.point[1])
        self.Refresh()

        self.point = point

#-----------------------------------------------------------------------------

class Event(object):
    regbmp = None  # region bitmap is the same for *all* Event shapes

    def getRegion(self,x,y):
        if Event.regbmp is None:
            bmp = wx.EmptyBitmap(self.d,self.d)
            dc = wx.MemoryDC()
            dc.SelectObject(bmp)

            # black will be our transparent colour
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()

            # draw the circle (opaque)
            dc.SetBrush(wx.Brush("white"))
            dc.SetPen(wx.Pen("white"))
            dc.DrawCircle(27*self.scale,27*self.scale,27*self.scale)

            dc.SelectObject(wx.NullBitmap)
            del dc

            bmp.SetMaskColour("black")

            Event.regbmp = bmp

        region = wx.RegionFromBitmap(Event.regbmp)
        region.Offset(x,y)
        return region

    def getRect(self):
        return wx.Rect(self.x,self.y,self.d,self.d)

#-----------------------------------------------------------------------------

class MessageEvent(Event):
    bmp = None

    def __init__(self,panel,x,y,scale=1.0):
        self.panel = panel
        self.scale = scale
        self.x = x
        self.y = y

        self.d = 54*scale  # diameter

        self.region = self.getRegion(x,y)

        if MessageEvent.bmp is None:
            x0,y0,r = 27*scale,27*scale,16*scale

            deg = 180 - (6 * 9)
            angle = math.radians(deg)
            x1 = int(round(x0 + r * math.sin(angle)))
            y1 = int(round(y0 + r * math.cos(angle)))

            deg = 180 - (6 * 21)
            angle = math.radians(deg)
            x2 = int(round(x0 + r * math.sin(angle)))
            y2 = int(round(y0 + r * math.cos(angle)))

    #       deg = 180 - (6 * 39)
    #       angle = math.radians(deg)
    #       x3 = int(round(x + r * math.sin(angle)))
    #       y3 = int(round(y + r * math.cos(angle)))

            deg = 180 - (6 * 51)
            angle = math.radians(deg)
            x4 = int(round(x0 + r * math.sin(angle)))
            y4 = int(round(y0 + r * math.cos(angle)))

            x5 = x0
            y5 = y0

            bmp = wx.EmptyBitmap(self.d,self.d)
            dc = wx.MemoryDC()
            dc.SelectObject(bmp)

            # black will be our transparent colour
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()

            gc = wx.GraphicsContext.Create(dc)
            gc.SetPen(wx.Pen("black", 1))
            gc.SetBrush(wx.Brush(wx.WHITE))
            gc.DrawEllipse(2*scale,2*scale,50*scale,50*scale)
            gc.DrawEllipse(6*scale,6*scale,42*scale,42*scale)

            gc.DrawRectangle(x4,y4,(x2-x4+1),(y2-y4+1))

            gc.StrokeLine(x4,y4,x5,y5)
            gc.StrokeLine(x1,y1,x5,y5)

            dc.SelectObject(wx.NullBitmap)
            del dc

            MessageEvent.bmp = bmp

    def HitTest(self,x,y):
        return self.region.Contains(x,y)

    def onPaint(self,dc):
        self.bmp.SetMaskColour("black")
        memDC = wx.MemoryDC()
        memDC.SelectObject(self.bmp)
        dc.Blit(self.x,self.y,self.d,self.d,memDC,0,0,useMask=True)
        memDC.SelectObject(wx.NullBitmap)
        del memDC

#-----------------------------------------------------------------------------

class TimerEvent(Event):
    bmp = None

    def __init__(self,panel,x,y,scale=1.0):
        self.panel = panel
        self.scale = scale
        self.x = x
        self.y = y

        self.d = 54*self.scale  # diameter

        self.region = self.getRegion(x,y)

        if TimerEvent.bmp is None:

            bmp = wx.EmptyBitmap(self.d,self.d)
            dc = wx.MemoryDC()
            dc.SelectObject(bmp)

            # black will be our transparent colour
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()

            gc = wx.GraphicsContext.Create(dc)
            gc.SetPen(wx.Pen("black", 1))
            gc.SetBrush(wx.Brush(wx.WHITE))
            gc.DrawEllipse(2*scale,2*scale,50*scale,50*scale)
            gc.DrawEllipse(6*scale,6*scale,42*scale,42*scale)

            gc.DrawEllipse(12*scale,12*scale,30*scale,30*scale)

            x,y,r = 27*scale,27*scale,16*scale

            for i in xrange(12):
                deg = 180 - 30 * (i + 1)
                angle = math.radians(deg)
                x1 = x + (r-self.scale) * math.sin(angle)
                y1 = y + (r-self.scale) * math.cos(angle)
                x2 = x + (r-(4*self.scale)) * math.sin(angle)
                y2 = y + (r-(4*self.scale)) * math.cos(angle)
                gc.DrawLines([(x1,y1),(x2,y2)])

            deg = 180 - (6 * 4)
            angle = math.radians(deg)
            x1 = x + (r-(2*self.scale)) * math.sin(angle)
            y1 = y + (r-(2*self.scale)) * math.cos(angle)
            gc.DrawLines([(x1,y1),(x,y)])

            deg = 180 - (6 * 16)
            angle = math.radians(deg)
            x1 = x + (r-(10*self.scale)) * math.sin(angle)
            y1 = y + (r-(10*self.scale)) * math.cos(angle)
            gc.DrawLines([(x1,y1),(x,y)])

            dc.SelectObject(wx.NullBitmap)
            del dc

            TimerEvent.bmp = bmp

    def HitTest(self,x,y):
        return self.region.Contains(x,y)

    def onPaint(self,dc):
        self.bmp.SetMaskColour("black")
        memDC = wx.MemoryDC()
        memDC.SelectObject(self.bmp)
        dc.Blit(self.x,self.y,self.d,self.d,memDC,0,0,useMask=True)
        memDC.SelectObject(wx.NullBitmap)
        del memDC

#-----------------------------------------------------------------------------

class Task(object):
    bmp = None
    regbmp = None

    def __init__(self,panel,x,y,w,h,text,scale=1.0):
        self.panel = panel
        self.scale = scale
        self.text = text

        self.x = x
        self.y = y
        self.w = w
        self.h = h

        self.region = self.getRegion(x,y)

        if Task.bmp is None:
            bmp = wx.EmptyBitmap(w,h)
            dc = wx.MemoryDC()
            dc.SelectObject(bmp)

            # black will be our transparent colour
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()

            gc = wx.GraphicsContext.Create(dc)
            gc.SetPen(wx.Pen("black", 1))
            gc.SetBrush(wx.Brush(wx.WHITE))
            gc.DrawRoundedRectangle(1,1,w-2,h-2,10)

            dc.SelectObject(wx.NullBitmap)
            del dc

            Task.bmp = bmp

    def getRegion(self,x,y):
        if Task.regbmp is None:
            bmp = wx.EmptyBitmap(self.w,self.h)
            dc = wx.MemoryDC()
            dc.SelectObject(bmp)

            # black will be our transparent colour
            dc.SetBackground(wx.Brush("black"))
            dc.Clear()

            # draw the rectangle (opaque)
            dc.SetBrush(wx.Brush("white"))
            dc.SetPen(wx.Pen("white"))
            dc.DrawRoundedRectangle(0,0,self.w,self.h,10)

            dc.SelectObject(wx.NullBitmap)
            del dc

            bmp.SetMaskColour("black")

            Task.regbmp = bmp

        region = wx.RegionFromBitmap(Task.regbmp)
        region.Offset(x,y)
        return region

    def getRect(self):
        return wx.Rect(self.x,self.y,self.w,self.h)

    def HitTest(self,x,y):
        return self.region.Contains(x,y)

    def onPaint(self,dc):
        self.bmp.SetMaskColour("black")
        memDC = wx.MemoryDC()
        memDC.SelectObject(self.bmp)
        dc.Blit(self.x,self.y,self.w,self.h,memDC,0,0,useMask=True)
        memDC.SelectObject(wx.NullBitmap)
        del memDC

        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
        font.SetWeight(wx.BOLD)
        dc.SetFont(font)
        dc.SetPen(wx.Pen("black", 1))
        dc.DrawText(self.text,self.x+20,self.y+20)

    def getSource(self,ndx,tot):
        return self.x+self.w,self.y+int(round(self.h/(tot+1)*(ndx+1)))

    def getTarget(self,ndx,tot):
        return self.x,self.y+int(round(self.h/(tot+1)*(ndx+1)))

    def onPaint2(self,gc):
        gc.SetPen(wx.Pen("black", 1))
        gc.SetBrush(wx.Brush(wx.WHITE))
        gc.DrawRoundedRectangle(self.x,self.y,self.w,self.h,10)

        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
        font.SetWeight(wx.BOLD)
        gc.SetFont(font)
        gc.DrawText(self.text,self.x+20,self.y+20)

class MyApp(wx.App):
    def OnInit(self):
        frame = MyFrame(None, -1, "BPMN Symbols")
        frame.Show(True)
        self.SetTopWindow(frame)
        return True

app = MyApp(0)     # Create an instance of the application class
app.MainLoop()     # Tell it to start processing events
