mengqvist / DNApy

Code for DNA plasmid editing.
GNU General Public License v3.0
43 stars 13 forks source link

Plasmid view #4

Open mengqvist opened 9 years ago

mengqvist commented 9 years ago

The layout of labels in plasmid view looks rather horrible and needs fixing. This is mainly a problem when there are very many labels. Much too many labels will also throw an error.

openpaul commented 9 years ago

Also the rendering of the plasmid can be improved. It has some rough edges. Is there a better way to draw arcs with arrowheads in wxpython? Or should we use a grafics implementation like cairo?

I just did some research on the topic, because I think that we should try to get the plasmid view more beautiful. And as soon as we decided how to do it there, maybe we can also make the dna view panel as a drawing, allowing to dra restriction sites, labelnames and colors to the dna code.

openpaul commented 9 years ago

I just got my hands on cairo and wanted to see if drawing is different with it. The coding is fairly simple and the results look good. I will see if I can make a working example to compare wxpython to cairo.

As of now I can say I like the rendering of the arrows. it is a little smoother, because cairo can draw arcs nativly. wxpythonvscairo

What do you say? Cairo enables also easy zooming and export of images as .svg or .png

mengqvist commented 9 years ago

I agree that the rendering with GCDC is far from ideal. At some point I was researching other options (including Cairo) but I never went through with it because I was not quite sure how to manage the interactivity when using Cairo. The ability to export as .svg is also something that I would really like to implement. What is the portability of Cairo? Does it work well on Windows?

I've also been thinking that the DNA view would be much more beautiful if drawn as well. I did find the task daunting at the time tough and kicked the can down the road.

So, in summary, if you know how to draw in Cairo and make the drawing interactive I'm all for it! You could start with the plasmid view and if it goes well we could take it from there.

openpaul commented 9 years ago

Sounds good. I will see if I can make it as interactive as the GCDC version.

As of now I started implementing to draw the features and that works very well. cairo

As of tomorrow I will have to attent a class, so it might take a while for me (depending how difficult the class will be)

I'll let you know as soon, as I know something new.

openpaul commented 9 years ago

Seems like getting the object in cairo really is kind of impossible. Sadly...

I will keep my eyes open if there is a way, but I guess we'll have to test another libary.

mengqvist commented 9 years ago

Was the interactivity the problem? I was toying with the idea of converting the "visible" GCDC to a Cairo rendering and leaving the "hidden" GCDC (that handles the interactivity) untouched. That way the mouse movements and clicks could be captured and used to influence the Cairo drawing. I think the resulting code would be horrible though since every object will be drawn using two different libraries.

I have looked into a lot of different things. It's rather frustrating because it seems in web browsers dealing with interactivity in .svg files is fairly straight forward. Converting the whole thing to a web app does not seem to be the way to go though.

openpaul commented 9 years ago

Yes, I was thinking about this approach too. But I guess it will be very messy, as there is no way of really converting objects from GCDC to cairo. Also both plasmids must match 100%, which is very difficult to achive, I think.

It would be possible to make a webkit window and just draw there. I guess thats fairly easy. I used to develop a html5 plasmid viewer (https://github.com/openpaul/html5plasmid) but never followed trough, cause the performance of JS is not as good as python (opening gb-Files works not so well, plasmid view is not good implementet, but it works). But I guess making an WebKit window displaying svg and use JS to make it responsive would be a way to go.

To you know of any other (grafics) libarys? So far I know (have read about) TKinker, wxpython, qt4 or qt5 and cairo. All of them are cross platform, not all of them have antialiasing.

openpaul commented 9 years ago

cairo has a hittest function, alright. So according to this documentation we just would have to save every path or object and then loop trough them whenever there is a click or mouseover. That could work and I will try it on sunday with my example code to see how its performance is.

http://cairographics.org/hittestpython/

mengqvist commented 9 years ago

That would be awesome! Hope it works well. Having everything look pretty is so important. No-one likes an ugly program.

openpaul commented 9 years ago

So, just that you can try it yourself here is my working example code for mouseover. It gets highlighted and looks ok.

The code right now is very bad, because I just learned cairo with it. I will do some cleaning up and then show it again, but I thought maybe you already want to see that it works.

Just save this as python and run it directly via command line. Its a modified plasmid_GUI.py file.

#!/usr/bin/env python

#This file is part of DNApy. DNApy is a DNA editor written purely in python. 
#The program is intended to be an intuitive, fully featured, 
#extendable, editor for molecular and synthetic biology.  
#Enjoy!
#
#Copyright (C) 2014  Martin Engqvist | 
#
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#LICENSE:
#This file is part of DNApy.
#
#DNApy 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 3 of the License, or
#(at your option) any later version.
# 
#DNApy 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 Library General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software Foundation,
#Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#Get source code at: https://github.com/0b0bby0/DNApy
#

#TODO
#fix long plasmid names
#add 'dna ruler'
#add rightclick menus

import wx
import cairo
from wx.lib.wxcairo import ContextFromDC
import genbank
import copy
import math

import os, sys
import string
from base_class import DNApyBaseDrawingClass
from base_class import DNApyBaseClass
import featureedit_GUI

import colcol

files={}   #list with all configuration files
files['default_dir'] = os.path.abspath(os.path.dirname(sys.argv[0]))+"/"
files['default_dir']=string.replace(files['default_dir'], "\\", "/")
files['default_dir']=string.replace(files['default_dir'], "library.zip", "")
settings=files['default_dir']+"settings"   ##path to the file of the global settings
execfile(settings) #gets all the pre-assigned settings

class PlasmidView(DNApyBaseDrawingClass):
    highlighted_feature = False
    genbank.search_hits = []
    label_type = 'circular' 
    def __init__(self, parent, id):

        # hit test variable
        self.hittest = {}
        self.Highlight = None

        # initialise the window
        DNApyBaseDrawingClass.__init__(self, parent, wx.ID_ANY)

        genbank.dna_selection = (1,1)

        #self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        #self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        #self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        #self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDouble)

############ Setting required methods ####################

    def update_globalUI(self):
        '''
        Method should be modified as to update other panels in response to changes in own panel.
        '''
        pass

    def update_ownUI(self):
        """
        This would get called if the drawing needed to change, for whatever reason.

        The idea here is that the drawing is based on some data generated
        elsewhere in the system. If that data changes, the drawing needs to
        be updated.

        This code re-draws the buffer, then calls Update, which forces a paint event.
        """
        dc = wx.MemoryDC()
        dc.SelectObject(self._Buffer)
        self.DrawCairo(dc)
        dc.SelectObject(wx.NullBitmap) # need to get rid of the MemoryDC before Update() is called.
        self.Refresh()
        self.Update()

    def set_dna_selection(self, selection):
        '''Receives requests for DNA selection and then sends it.'''
        assert type(selection) == tuple, 'Error, dna selection must be a tuple'
        selection = (int(selection[0]-1), int(selection[1]))
        genbank.dna_selection = selection

############### Done setting required methods #######################

    def find_overlap(self, drawn_locations, new_range):
        '''
        Takes two ranges and determines whether the new range has overlaps with the old one.
        If there are overlaps the overlap locations are returned.
        This is used when drawing features. If two features overlap I want them drawn on different levels.
        '''
        assert type(drawn_locations) == list            
        assert type(new_range) == tuple

        if drawn_locations == []:
            drawn_locations.append([new_range])
            return drawn_locations, 0
        else:
            i = 0
            while i < len(drawn_locations):
                overlap_found = False
                for n in range(0,len(drawn_locations[i])):
                    if drawn_locations[i][n][0]<=new_range[0]<=drawn_locations[i][n][1] or drawn_locations[i][n][0]<=new_range[1]<=drawn_locations[i][n][1]: #if they overlap
                        overlap_found = True
                    elif new_range[0]<=drawn_locations[i][n][0]<=new_range[1] or new_range[0]<=drawn_locations[i][n][1]<=new_range[1]: #if they overlap
                        overlap_found = True
                if overlap_found == False:
                    drawn_locations[i].append(new_range)
                    return drawn_locations, i
                    break   
                elif i+1==len(drawn_locations):
                    drawn_locations.append([new_range])
                    return drawn_locations, i+1
                    break
                i += 1

    def DrawCairo(self, dc):
        self.centre_x = self.size[0]/2 #centre of window in x
        self.centre_y = self.size[1]/2 #centro of window in y
        self.min_centre = min(self.centre_x, self.centre_y)

        self.Radius = min(self.size[0], self.size[1])/3 - self.min_centre/8 #the last one is the label line length

#       dc.SetDeviceOrigin(size_x/2, size_y/2)

        dc.SetBackground(wx.Brush("White"))
        dc.Clear() # make sure you clear the bitmap!

        self.ctx = ContextFromDC(dc)

        #ctx = cairo.Context (surface)
        canvasWidth  = self.size[0]
        canvasHeight = self.size[1]
        ratio = canvasHeight/canvasWidth
        print canvasWidth/100
        self.ctx.scale(canvasWidth, canvasWidth*ratio) # Normalizing the canvas
        self.ctx.scale(0.01, 0.01) # Normalizing the canvas
        #pat = cairo.LinearGradient (0.0, 0.0, 0.0, 1.0)
        #pat.add_color_stop_rgba (1, 0.7, 0, 0, 0.5) # First stop, 50% opacity
        #pat.add_color_stop_rgba (0, 0.9, 0.7, 0.2, 1) # Last stop, 100% opacity

        # white background
        self.ctx.rectangle (0, 0, 1, 1) # Rectangle(x0, y0, x1, y1)
        self.ctx.set_source_rgb (255,255,255)
        self.ctx.fill ()

        self.ctx.translate (50,50) # Changing the current transformation matrix

        radius = 30

        #ctx.move_to (0.2, 0.2)
        #ctx.arc (0.2, 0.1, 0.1, -math.pi/2, 0) # Arc(cx, cy, radius, start_angle, stop_angle)
        #ctx.line_to (0.5, 0.1) # Line to (x,y)
        #ctx.curve_to (0.5, 0.2, 0.5, 0.4, 0.2, 0.8) # Curve(x1, y1, x2, y2, x3, y3)
        #ctx.close_path ()

        # draw the plasmid
        self.ctx.arc(0,0,radius+0.4,0,2*math.pi)
        self.ctx.set_source_rgb (0, 0, 0) # Solid color
        self.ctx.set_line_width (0.4)
        self.ctx.stroke()

        # inner plasmid

        self.ctx.arc(0,0,radius-0.4,0,2*math.pi)

        self.ctx.stroke()

        #draw plasmid name
        self.drawCairoPlasmidName(self.ctx)

        # draw features
        self.drawCairoFeatures(self.ctx, radius)

        #gcdc = wx.GCDC(dc)

        return True

    def radial2cartesian(self,radius, angle, cx=0,cy=0,width=1, height=1):
        x = radius * math.cos(angle) + cx
        y = radius * math.sin(angle) + cy

        return x,y

    def drawCairoFeatures(self, ctx, radius):

        # set some variables
        featurelist = genbank.gb.get_all_feature_positions()
        self.feature_catalog = {} #for matching features with the unique colors
        self.feature_catalog['(255, 255, 255, 255)'] = False #the background is white, have to add that key

        drawn_fw_locations = [] #for keeping track of how many times a certain region has been painted on
        drawn_rv_locations = [] #for keeping track of how many times a certain region has been painted on

        arrow_thickness = 2
        radius_change   = arrow_thickness/2

        lev = arrow_thickness + 0.5

        arrow_head_length = 2.5 # in degree

        length     = float(len(genbank.gb.GetDNA()))
        degreeMult = float(360/length)

        self.unique_color = (0, 0, 0)

        outI = 1
        inI  = 1
        for i in range(1,len(featurelist)): 
            self.feature_catalog[str(self.NextRGB()+(255,))] = i #add to catalog with new RGB color

            featuretype, complement, start, finish, name, index = featurelist[i]

            # variable to save as a hittest
            hittestName = "%s%s%s" % (name, index, start)
            self.hittest[hittestName] = []

            # highlitght?
            if self.Highlight == hittestName:
                bordercolor = [0, 0.5, 1]
                borderwidth = 0.5

            else:
                bordercolor = [0, 0, 0 ]
                borderwidth = 0.1

            #print "new index: ",name, start, finish

            #print name

            # draw foreward features
            if complement == False:
                drawn_fw_locations, levelMult = self.find_overlap(drawn_fw_locations, (start, finish))
                if levelMult>3 or finish-start<=3: #Only allow for tree levels. Also, for very short features, draw them at bottom level
                    levelMult = 0
                level = lev * (1+levelMult)

                angle_start     = float(start)  * degreeMult            # in degree
                angle_stop      = float(finish) * degreeMult

                angle_start_rad = (angle_start * math.pi/180) - math.pi/2 
                angle_end_rad   = (angle_stop * math.pi/180) - math.pi/2 

                angle_end_rad_without_head = ((angle_stop-arrow_head_length) * math.pi/180) - math.pi/2 
                angle_end_rad_without_headD = ((angle_stop-2*arrow_head_length) * math.pi/180) - math.pi/2 
                # weather to draw a head or not
                drawHead = True
                if angle_end_rad_without_headD < angle_start_rad:
                    angle_end_rad_without_head = angle_end_rad
                    drawHead = False

                arrow_head_x, arrow_head_y  = self.radial2cartesian(radius+level, angle_end_rad)
                arrow_head_inv_x, arrow_head_inv_y  = self.radial2cartesian(radius+level, angle_start_rad)

                # get the color:
                color = eval(featuretype)['fw'] #get the color of feature (as string)
                assert type(color) == str

                # move to start
                x_start, y_start = self.radial2cartesian(radius+level+radius_change, angle_start_rad)
                x_end, y_end = self.radial2cartesian(radius+level+radius_change, angle_end_rad)

                self.ctx.move_to(x_start, y_start)
                self.ctx.arc(0, 0, radius+level+radius_change,angle_start_rad, angle_end_rad_without_head);
                if drawHead:
                    ctx.line_to(arrow_head_x,arrow_head_y)
                self.ctx.arc_negative(0, 0, radius+level-radius_change, angle_end_rad_without_head, angle_start_rad);
                self.ctx.close_path ()

            else:
                drawn_rv_locations, levelMult = self.find_overlap(drawn_rv_locations, (start, finish))
                if levelMult>3 or finish-start<=3: #Only allow for tree levels. Also, for very short features, draw them at bottom level
                    levelMult = 0
                level = -lev * (1 + levelMult)
                angle_start     = float(finish)  * degreeMult           # in degree
                angle_stop      = float(start) * degreeMult

                angle_start_rad = (angle_start * math.pi/180) - math.pi/2 
                angle_end_rad   = (angle_stop * math.pi/180)  - math.pi/2 

                angle_end_rad_without_head = ((angle_stop+arrow_head_length) * math.pi/180) - math.pi/2 
                angle_end_rad_without_headD = ((angle_stop+2*arrow_head_length) * math.pi/180) - math.pi/2 
                # weather to draw a head or not
                drawHead = True
                #print angle_start_rad*180/math.pi, angle_end_rad*180/math.pi,angle_end_rad_without_head*180/math.pi
                if angle_end_rad_without_headD > angle_start_rad:
                    angle_end_rad_without_head = angle_end_rad
                    drawHead = False

                arrow_head_x, arrow_head_y          = self.radial2cartesian(radius+level, angle_end_rad)
                arrow_head_inv_x, arrow_head_inv_y  = self.radial2cartesian(radius+level, angle_start_rad)
                # get the color:
                color = eval(featuretype)['rv'] #get the color of feature (as string)
                assert type(color) == str

                # move to start
                x_start, y_start = self.radial2cartesian(radius+level+radius_change, angle_start_rad)
                x_end, y_end = self.radial2cartesian(radius+level+radius_change, angle_end_rad)

                #print name, complement,start, finish, angle_start, angle_stop, angle_start_rad, angle_end_rad, angle_end_rad_without_head,arrow_head_x,arrow_head_y
                self.ctx.move_to(x_start, y_start); 
                self.ctx.arc_negative(0, 0, radius+level-radius_change,angle_start_rad, angle_end_rad_without_head);
                if drawHead:
                    ctx.line_to(arrow_head_x,arrow_head_y)
                    #ctx.line_to(arrow_head_inv_x,arrow_head_inv_y)
                #ctx.line_to(x_end, y_end)  
                self.ctx.arc(0, 0, radius+level+radius_change,angle_end_rad_without_head,angle_start_rad);
                #ctx.close_path ()

            # TODO:
            # clean the code, so the arrow drawing part is more understandable

            r,g,b = colcol.hex_to_rgb(color)
            r = float(r)/255
            g = float(g)/255
            b = float(b)/255
            #print color
            #print r,g,b
            self.ctx.set_source_rgba (r,g,b,1.0) # Solid color
            self.ctx.set_line_width (borderwidth) # or 0.1

            self.ctx.fill_preserve ()
            self.ctx.set_source_rgb (bordercolor[0],bordercolor[1],bordercolor[2])

            # save feature to hittest:
            self.hitpath = self.ctx.copy_path()
            self.hittest[hittestName].append(self.hitpath)

            self.ctx.stroke()

            # TODO:
            # highlighted fargemnts should be drawn last to be on top!

            #print start

        return True

    def drawCairoPlasmidName(self, ctx):
        '''Draw the plasmid name and basepairs in the middle'''

        width  = 100
        height = 100
        self.hittest['plasmidname'] = []

        name = genbank.gb.fileName.split('.')[0]
        basepairs = '0 bp'
        if genbank.gb.GetDNA() != None:
            basepairs = str(len(genbank.gb.GetDNA())) + ' bp'

        # name
        self.ctx.select_font_face('Arial', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        self.ctx.set_font_size(2.5);
        xbearing, ybearing, TextWidth, TextHeight, xadvance, yadvance = self.ctx.text_extents(name)
        self.ctx.move_to(-TextWidth/2, 0);  

        # save feature to hittest:
        #self.hitpath = self.ctx.copy_path()
        #self.hittest['plasmidname'].append(self.hitpath)

        self.ctx.show_text(name);

        # bp
        self.ctx.select_font_face('Arial', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        self.ctx.set_font_size(2);
        xbearing, ybearing, TextWidth, TextHeight, xadvance, yadvance = self.ctx.text_extents(basepairs)
        self.ctx.move_to(-TextWidth/2, 2.7);  
        self.ctx.show_text(basepairs);

        # if we want to rotate, we could make that here or so:
#       self.ctx.rotate(1) 

        #print self.ctx.identity_matrix()

        return True

############### Setting methods for interconverting angles to dna positions ##############

    def angle_to_pos(self, angle):
        '''Convert an angle of a circle to a DNA position'''
        len_dna = float(len(genbank.gb.GetDNA()))
        dna_pos = int(self.AngleToFraction(angle)*len_dna)
        return dna_pos

    def pos_to_angle(self, pos):
        '''Calculate angles from DNA positions'''
        assert type(pos) == int, 'Error, position needs to be an integer'
        len_dna = float(len(genbank.gb.GetDNA()))
        if len_dna == 0:
            angle = 0
        else:
            angle = self.FractionToAngle(pos/float(len_dna))
        return angle        

########## Done with angle to dna methods ####################

######### Mouse methods #####################

    def HitTest(self):
        '''Tests whether the mouse is over any feature or label'''
        hit = None
        x, y = self.ScreenToClient(wx.GetMousePosition())   

        # get the mouse positions
        x2, y2 = self.ctx.device_to_user(x,y)

        for path in self.hittest:
            cairoCtx = self.ctx
            for i in self.hittest[path]:
                # load the path
                self.ctx.append_path(i)
                result = cairoCtx.in_fill(x2,y2) 
                # check if this path is hit
                if result == True:# or self.ctx.in_stroke(x2,y2):
                    hit = path
                    #print "True: ",x2, y2, path, cairoCtx.in_fill(x2,y2)
                    #break
                #else:
                    #print "False!", result
                #
                self.ctx.fill()

        return hit

    def OnLeftDown(self, event):
        '''When left mouse button is pressed down, store angle at which this happened.'''
        self.centre_x = self.size[0]/2 #centre of window in x
        self.centre_y = self.size[1]/2 #centro of window in y
        x, y = self.ScreenToClient(wx.GetMousePosition())   
        angle = self.PointsToAngle(self.centre_x, self.centre_y, x, y)
        self.left_down_angle = angle #save the angle at which left button was clicked for later use

    def OnLeftUp(self, event):
        '''When left mouse button is lifted up, determine the DNA selection from angles generated at down an up events.'''
        self.centre_x = self.size[0]/2 #centre of window in x
        self.centre_y = self.size[1]/2 #centro of window in y
        x, y = self.ScreenToClient(wx.GetMousePosition())   

        up_angle = self.PointsToAngle(self.centre_x, self.centre_y, x, y)
        down_angle = self.left_down_angle

        '''if abs(down_angle-up_angle) <= 0.2: # want to do 'down == up' but I need some tolerance
            self.highlighted_feature = self.HitTest()
            if self.highlighted_feature is False: #if there is no feature, then there is not selection, just an insertion of the charet. Draw a line
                start = self.angle_to_pos(down_angle) 
                finish = -1 
            else:
                featuretype, complement, start, finish, name, index = genbank.gb.get_all_feature_positions()[self.highlighted_feature] #get info for the feature that was 'hit'
                start += 1 #need to adjust for some reason
        elif down_angle < up_angle:
            start = self.angle_to_pos(down_angle)
            finish = self.angle_to_pos(up_angle)
        elif down_angle > up_angle:
            start = self.angle_to_pos(up_angle)
            finish = self.angle_to_pos(down_angle)
        '''
        self.set_dna_selection((start, finish))
        self.update_ownUI()

    def OnMotion(self, event):
        '''When mouse is moved with the left button down determine the DNA selection from angle generated at mouse down and mouse move event.'''
        oldhit = self.Highlight
        hit = self.HitTest()
        if hit:
            self.Highlight = hit
            # only update after change
            if oldhit != hit:
                self.update_ownUI()
        else:
            self.Highlight = None
            if oldhit != hit:
                self.update_ownUI()

        #if event.Dragging() and event.LeftIsDown():

            #up_angle = self.PointsToAngle(self.centre_x, self.centre_y, x, y)
            #down_angle = self.left_down_angle

            #if down_angle <= up_angle:
            #   start = self.angle_to_pos(down_angle)
            #   finish = self.angle_to_pos(up_angle)
            #elif down_angle > up_angle:
            #   start = self.angle_to_pos(up_angle)
            #   finish = self.angle_to_pos(down_angle)          

            #self.set_dna_selection((start, finish))
            #self.update_ownUI()
        '''else:
            new_index = self.HitTest()
            if new_index is self.highlighted_feature: #if the index did not change
                pass
            else:
                self.highlighted_feature = new_index
                self.update_ownUI()'''

    def OnLeftDouble(self, event):
        '''When left button is double clicked, launch the feature edit dialog.'''
        '''new_index = self.HitTest() #this does not get the "true" feature index. Some featues are split and this is an index that accounts for that.
        if new_index is not False: #False is returned for the background
            featurelist = genbank.gb.get_all_feature_positions()
            featuretype, complement, start, finish, name, index = featurelist[new_index]
            genbank.feature_selection = copy.copy(index)

            dlg = featureedit_GUI.FeatureEditDialog(None, 'Edit Feature') # creation of a dialog with a title'''
        dlg.ShowModal()
        dlg.Center()

    def OnRightUp(self, event):
        print('plasmid right')

############ Done with mouse methods ####################

class PlasmidView2(DNApyBaseClass):
    '''
    This class is intended to glue together the plasmid drawing with control buttons.
    '''
    def __init__(self, parent, id):
        DNApyBaseClass.__init__(self, parent, id)
        self.plasmid_view = PlasmidView(self, -1)   

        ##########  Add buttons and methods to control their behaviour ###########  
        #buttons
        padding = 10 #how much to add around the picture

        imageFile = files['default_dir']+"/icon/circle.png"
        image1 = wx.Image(imageFile, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
        circle = wx.BitmapButton(self, id=10, bitmap=image1, size = (image1.GetWidth()+padding, image1.GetHeight()+padding), name = "share")

        imageFile = files['default_dir']+"/icon/group.png"
        image1 = wx.Image(imageFile, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
        group = wx.BitmapButton(self, id=11, bitmap=image1, size = (image1.GetWidth()+padding, image1.GetHeight()+padding), name = "share")

        imageFile = files['default_dir']+"/icon/radiating.png"
        image1 = wx.Image(imageFile, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
        radiating = wx.BitmapButton(self, id=12, bitmap=image1, size = (image1.GetWidth()+padding, image1.GetHeight()+padding), name = "share")

        imageFile = files['default_dir']+"/icon/new_small.png"
        image1 = wx.Image(imageFile, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
        newfeature = wx.BitmapButton(self, id=1, bitmap=image1, size = (image1.GetWidth()+padding, image1.GetHeight()+padding), name = "share")

        imageFile = files['default_dir']+"/icon/remove_small.png"
        image1 = wx.Image(imageFile, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
        deletefeature = wx.BitmapButton(self, id=2, bitmap=image1, size = (image1.GetWidth()+padding, image1.GetHeight()+padding), name = "share")

        imageFile = files['default_dir']+"/icon/edit.png"
        image1 = wx.Image(imageFile, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
        edit = wx.BitmapButton(self, id=6, bitmap=image1, size = (image1.GetWidth()+padding, image1.GetHeight()+padding), name = "edit")

        #bind feature list buttons
        self.Bind(wx.EVT_BUTTON, self.OnCircularLabels, id=10)
        self.Bind(wx.EVT_BUTTON, self.OnGroupLabels, id=11)
        self.Bind(wx.EVT_BUTTON, self.OnRadiatingLabels, id=12)

        self.Bind(wx.EVT_BUTTON, self.OnNew, id=1)
        self.Bind(wx.EVT_BUTTON, self.OnDelete, id=2)
        self.Bind(wx.EVT_BUTTON, self.OnEditFeature, id=6)

        #arrange buttons vertically     
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(item=circle)
        sizer.Add(item=group)
        sizer.Add(item=radiating)
        sizer.Add(item=newfeature)
        sizer.Add(item=deletefeature)
        sizer.Add(item=edit)

        #add feature list and buttons horizontally  
        sizer2 = wx.BoxSizer(wx.HORIZONTAL)
        sizer2.Add(item=sizer, proportion=0, flag=wx.EXPAND)
        sizer2.Add(item=self.plasmid_view, proportion=-1, flag=wx.EXPAND)

        self.SetSizer(sizer2)

    def update_ownUI(self):
        '''
        User interface updates.
        '''
        self.plasmid_view.update_ownUI()

    def OnCircularLabels(self, evt):
        self.plasmid_view.label_type = 'circular'
        self.update_ownUI()

    def OnGroupLabels(self, evt):
        self.plasmid_view.label_type = 'group'
        self.update_ownUI()

    def OnRadiatingLabels(self, evt):
        self.plasmid_view.label_type = 'radiating'
        self.update_ownUI()

    def OnNew(self, evt):
        pass

    def OnDelete(self, evt):
        pass

    def OnEditFeature(self, evt):
        pass

    #######################################################################

##### main loop
class MyApp(wx.App):
    def OnInit(self):
        frame = wx.Frame(None, -1, title="Plasmid View", size=(700,700), style = wx.NO_FULL_REPAINT_ON_RESIZE)
        panel = PlasmidView2(frame, -1)

        frame.Centre()
        frame.Show(True)
        self.SetTopWindow(frame)
        return True

if __name__ == '__main__': #if script is run by itself and not loaded   

    files={}   #list with all configuration files
    files['default_dir'] = os.path.abspath(os.path.dirname(sys.argv[0]))+"/"
    files['default_dir']=string.replace(files['default_dir'], "\\", "/")
    files['default_dir']=string.replace(files['default_dir'], "library.zip", "")
    settings=files['default_dir']+"settings"   ##path to the file of the global settings
    execfile(settings) #gets all the pre-assigned settings

    genbank.dna_selection = (1, 1)   #variable for storing current DNA selection
    genbank.feature_selection = False #variable for storing current feature selection

    import sys
    assert len(sys.argv) == 2, 'Error, this script requires a path to a genbank file as an argument.'
    print('Opening %s' % str(sys.argv[1]))

    genbank.gb = genbank.gbobject(str(sys.argv[1])) #make a genbank object and read file

    app = MyApp(0)
    app.MainLoop()
openpaul commented 9 years ago

OK, I have a branch with a working example. I did not start on drawing labels yet. It seems like I nee to write a lot of code for that, so I guess that might take a while.

Here the first working example: https://github.com/openpaul/DNApy/tree/newDNAGui

Looks like this right now (left old, right cairo): cairo_status

I also draw arrow heads, as I think it looks a little bit more pretty. But it seems as the colors are not yet correct, but that is just a minor error.

mengqvist commented 9 years ago

Looks beautiful! And this is completely interactive? I'm quite excited about the fact that you got cairo drawing working.

openpaul commented 9 years ago

yes, it is interactive. As far as I know every action (mousemovement, click, doubleclick etc) is possible to implement, as this is implemented at the level of wxpython.

So I will poke around with labels for now. And there are still some bugs I want to chase... (All drawing goes black as soon as I click the first time on the canvas or change windows for example)

openpaul commented 9 years ago

Tryed to draw the label into the arrows if it fits, but I guess it could be improved... does not look pixel perfect jet... highlight

openpaul commented 9 years ago

Good news everyone. Drawing the label in the arrow works good, saves space and looks alright I think: highlight3

mengqvist commented 9 years ago

It's looking very pretty indeed. And it sure looks better than when I tried to do have the name inside the features with GCDC. The letters did not arc smoothly somehow....

The label system is tricky (and still needed for smaller features) and I only had a crude implementation of it. I pre-computed positions where labels could be an then tried to distribute the labels to those slots in a good way. But there is much improvement that can be done on what I did.

I've also commited my UI changes so that the DNA and plasmid views can be seen at the same time. I had to re-introduce the publisher system to do that. I've had compatability problems with it in the past. Let me know if you have any problems.

screenshot from 2015-01-11 22 52 30

openpaul commented 9 years ago

The new window split works really good. I like it a lot. Makes it more easy to see what I am dooing in the DNA.

I just implemented selections in the new plasmid GUI. Even though my code is a little bit messy it works fine. Labels are still missing, as I wait for an good idea how to make them fast and easy to draw.

Looks like that with selection of feature: selection

mengqvist commented 9 years ago

This is looking spectacular! The drawing is very clean and I like the new way of showing the selection. I think the labels need some clever underlying computation to look good. I find that the way it's implemented in SnapGene looks quite good.

snapgene-mapview

Basically there is a circle of pre-computed label positions around the plasmid. These are distributed such that text cannot overlap. Then it simply goes through and adds in the labels in the order which they appear on the DNA. The first choice is to put the label in a straight line outside the feature or enzyme which is to be labeled. If that position is taken, then it gets bumped down to the next label position, if that is taken then the next, and so on. I have much of this implemented for my labels in GCDC. The main problem there is that it went through the features in the order they are presented in the GenBank file, not in the order in which they appear on the DNA. If that is fixed we would have something very similar to that in SnapGene.

I'd be happy to implement this if you clean up your Cairo code so that I can read it and then submit a pull request.

openpaul commented 9 years ago

Yes, SnapGene does it very well. That was also my approach. If you have a good idea how to make that happen I would be gald if you can do it. I will provide a function where you can calculate the positions and if you need help with drawing in cairo I now have some ideas of the possibilities.

SnapGene also just informs you whenever it is not able to show everything, cause there is not enough space. Maybe we should do it similar.

I will clean the code tomorrow I suppose and then I will search for a bug in my digestion that produces wrong virtual cuts.

mengqvist commented 9 years ago

Sounds like a plan!

openpaul commented 9 years ago

So I tryed implementing the labels myself (as I had some time last week) and I am not very satisfied with the solution I came up with. And I have no Idea how to make those labels works as well as SnapGene does it. It seems like you need a very good idea or a lot of code to do that.

I wrote 200 lines just to sort the labels and group them when there is not enough space to show them all "normally". But even now I have lines crossing and it does not look that beautiful.

I was just wondering if you had time to think about this issue?

labels

openpaul commented 9 years ago

just found out, that snapgene does not actually use a circle but rather a oval to place labels. This way more labels fit in the same space. snap_oval

openpaul commented 9 years ago

Even though you seem occupied lately, here my latest update on the label issue:

I implemented a new label algorythm sorting the labels first an then just iterating and pushing positions until it fits.

It is not as fast as ist should be and I guess I will improve it further, but the result is good i think:

new labels

I will try to show restrictionsites also and then I will look into speed.

mengqvist commented 9 years ago

Sorry I've been completely absent for a while. I've moved to a new country with my family and that, combined with a new job, has taken a severe toll on my ability to do anything for the project. Things are getting better now though. I still have some papers to finish up in the evenings and then I can get back to working on DNApy. I'm still commited to it.

Using an oval for the labesl is the way to go I think. That's what I had for one of my implementations as well. What you have so far looks really good! If you want a genbank files with a lot of labels for testing I have a genbank file with the yeast mitochondrial genome.

openpaul commented 9 years ago

Sounds like you had enough on your plate the last few weeks. I will keep working on the plasmid view but discovered, that maybe the cairo lib was not the best idea, as it is very slow and not really for interaction with the user (but makes awesome svg files). I thought, that maybe a game engine would provide the power to make a good GUI, but I do not know, when I will find the time to look further into this (I had the possibility to try http://www.geneious.com/ and they have a pretty nice plasmid viewing tool).

I woul like to try your genbank file, maybe we can add a folder with "testfiles" (if you are allwoed to share this file) or we just exchange emails.

openpaul commented 9 years ago

I had some time on my hands and redid almost the entire plasmid drawing code and could improve performance and readability.

Some features are not reimplemented yet, but I will continue to work on them. Moreover I am thinking about implementing code to display linear DNA.

mengqvist commented 9 years ago

Sounds great! Glad you're back! I'm finally more or less done with all my backlogged work so I should be returning to project as well. I'll make an effort this coming week to merge everything and get back into the DNApy project.

Date: Sun, 19 Jul 2015 00:59:15 -0700 From: notifications@github.com To: DNApy@noreply.github.com Subject: Re: [DNApy] Plasmid view (#4)

I had some time on my hands and redid almost the entire plasmid drawing code and could improve performance and readability.

Some features are not reimplemented yet, but I will continue to work on them. Moreover I am thinking about implementing code to display linear DNA.

— Reply to this email directly or view it on GitHub.