コード例 #1
0
ファイル: pathhelix.py プロジェクト: MatthewMcAteer/cadnano2
 def __init__(self, vhelix, pathHelixGroup):
     super(PathHelix, self).__init__()
     self.setAcceptHoverEvents(True)  # for pathtools
     self._pathHelixGroup = pathHelixGroup
     self._scafBreakpointHandles = []
     self._stapBreakpointHandles = []
     self._scafXoverHandles = []
     self._stapXoverHandles = []
     self._preXOverHandles = None
     self._XOverCacheEnvironment = None
     self._segmentPaths = None
     self._endptPaths = None
     self._minorGridPainterPath = None
     self._majorGridPainterPath = None
     self.step = vhelix.part().crossSectionStep()
     self.setZValue(styles.ZPATHHELIX)
     self.rect = QRectF()
     self._vhelix = None
     self._handle = None
     self._mouseDownBase = None
     self.addBasesButton = SVGButton(":/pathtools/add-bases", self)
     self.addBasesButton.clicked.connect(self.addBasesClicked)
     self.removeBasesButton = SVGButton(":/pathtools/remove-bases", self)
     self.removeBasesButton.clicked.connect(self.removeBasesClicked)
     self.setVHelix(vhelix)
     self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption)
     self.setPreXOverHandlesVisible(False)
     if app().ph != None:  # Convenience for the command line -i mode
         app().ph[vhelix.number()] = self
コード例 #2
0
ファイル: pathhelix.py プロジェクト: MatthewMcAteer/cadnano2
class PathHelix(QGraphicsObject):
    """
    PathHelix is the primary "view" of the VirtualHelix data.
    It manages the ui interactions from the user, such as
    dragging breakpoints or crossovers addition/removal,
    and updates the data model accordingly.

    parent should be set to...
    """
    minorGridPen = QPen(styles.minorgridstroke, styles.MINOR_GRID_STROKE_WIDTH)
    minorGridPen.setCosmetic(True)
    majorGridPen = QPen(styles.majorgridstroke, styles.MAJOR_GRID_STROKE_WIDTH)
    majorGridPen.setCosmetic(True)

    scafPen = QPen(styles.scafstroke, 2)
    nobrush = QBrush(Qt.NoBrush)
    baseWidth = styles.PATH_BASE_WIDTH

    # The next block of code does setup necessary for
    # drawing the sequence text onto the PathView
    sequenceFont = QFont("Monaco")
    if hasattr(QFont, 'Monospace'):
        sequenceFont.setStyleHint(QFont.Monospace)
    sequenceFont.setFixedPitch(True)
    sequenceFontH = baseWidth / 3.
    sequenceFont.setPixelSize(sequenceFontH)
    sequenceFontMetrics = QFontMetricsF(sequenceFont)
    sequenceFontCharWidth = sequenceFontMetrics.width('A')
    sequerceFontCharHeight = sequenceFontMetrics.height()
    sequenceFontExtraWidth = baseWidth - sequenceFontCharWidth
    sequenceFont.setLetterSpacing(QFont.AbsoluteSpacing,
                                  sequenceFontExtraWidth)
    sequenceTextXCenteringOffset = sequenceFontExtraWidth / 2.
    sequenceTextYCenteringOffset = baseWidth / 2.

    # Items that calculate paths for loops and skips
    _skipitem = SkipItem()
    _loopitem = LoopItem()
    # Bases are drawn along and above the loop path.
    # These calculations revolve around fixing the
    # characters at a certain percentage of the total
    # arclength.
    # The fraction of the loop that comes before the
    # first character and after the last character is
    # the padding, and the rest is divided evenly.
    fractionLoopToPad = .10    

    def __init__(self, vhelix, pathHelixGroup):
        super(PathHelix, self).__init__()
        self.setAcceptHoverEvents(True)  # for pathtools
        self._pathHelixGroup = pathHelixGroup
        self._scafBreakpointHandles = []
        self._stapBreakpointHandles = []
        self._scafXoverHandles = []
        self._stapXoverHandles = []
        self._preXOverHandles = None
        self._XOverCacheEnvironment = None
        self._segmentPaths = None
        self._endptPaths = None
        self._minorGridPainterPath = None
        self._majorGridPainterPath = None
        self.step = vhelix.part().crossSectionStep()
        self.setZValue(styles.ZPATHHELIX)
        self.rect = QRectF()
        self._vhelix = None
        self._handle = None
        self._mouseDownBase = None
        self.addBasesButton = SVGButton(":/pathtools/add-bases", self)
        self.addBasesButton.clicked.connect(self.addBasesClicked)
        self.removeBasesButton = SVGButton(":/pathtools/remove-bases", self)
        self.removeBasesButton.clicked.connect(self.removeBasesClicked)
        self.setVHelix(vhelix)
        self.setFlag(QGraphicsItem.ItemUsesExtendedStyleOption)
        self.setPreXOverHandlesVisible(False)
        if app().ph != None:  # Convenience for the command line -i mode
            app().ph[vhelix.number()] = self
    # end def

    def activeTool(self):
        return self.controller().activeTool()

    def controller(self):
        return self._pathHelixGroup.controller()

    def pathHelixGroup(self):
        return self._pathHelixGroup

    def vhelix(self):
        return self._vhelix

    def palette(self):
        return self._vhelix.palette()

    def phgroup(self):
        return self._pathHelixGroup
    # end def

    def undoStack(self):
        return self.vhelix().undoStack()

    def setVHelix(self, newVH):
        if self._vhelix:
            self._vhelix.basesModified.disconnect(self.vhelixBasesModified)
            self._vhelix.vhelixDimensionsModified.disconnect(\
                                             self.vhelixDimensionsModified)
        self._vhelix = newVH
        newVH.basesModified.connect(self.vhelixBasesModified)
        newVH.dimensionsModified.connect(self.vhelixDimensionsModified)
        self.vhelixDimensionsModified()
        self.vhelixBasesModified()

    def handle(self):
        if self._handle:
            return self._handle
        self._handle = PathHelixHandle(self.vhelix(),\
                                       parent=self._pathHelixGroup)
        return self._handle

    def number(self):
        return self._vhelix.number()

    def row(self):
        return self._vhelix.row()

    def col(self):
        return self._vhelix.col()

    def evenParity(self):
        return self._vhelix.evenParity()
    
    def addBasesClicked(self):
        part = self.vhelix().part()
        dlg = QInputDialog(self.window())
        dlg.setInputMode(QInputDialog.IntInput)
        dlg.setIntMinimum(0)
        dlg.setIntValue(part.step)
        dlg.setIntMaximum(1000000)
        dlg.setIntStep(part.step)
        dlg.setLabelText(( "Number of bases to add to the existing"\
                         + " %i bases\n(must be a multiple of %i)")\
                         % (part.numBases(),part.step))
        dlg.intValueSelected.connect(self.userChoseToAddNBases)
        dlg.open()
        self.addBasesDialog = dlg  # Prevent GC from eating it
    
    @pyqtSlot(int)
    def userChoseToAddNBases(self, numBases):
        part = self.vhelix().part()
        dim = list(part.dimensions())
        numBases = int(numBases) / 21 * 21
        part.setDimensions((dim[0], dim[1], dim[2]+numBases))
        self.addBasesDialog.intValueSelected.disconnect(self.userChoseToAddNBases)
        del self.addBasesDialog

    def removeBasesClicked(self):
        part = self.vhelix().part()
        # First try to shrink to fit used bases
        newNumBases = part.indexOfRightmostNonemptyBase() + 1
        newNumBases = int(ceil(float(newNumBases)/part.step))*part.step
        newNumBases = util.clamp(newNumBases, part.step, 10000)
        
        dim = list(part.dimensions())
        # If that didn't do anything, reduce the length
        # of the vhelix by one step.
        if dim[2] == newNumBases and dim[2] > part.step:
            newNumBases = newNumBases - part.step
        part.setDimensions((dim[0], dim[1], newNumBases))

    def vhelixDimensionsModified(self):
        """Sets rect width to reflect number of bases in vhelix. Sets
        rect height to the width of two bases (one for scaffold and
        one for staple)"""
        canvasSize = self._vhelix.part().numBases()
        self.prepareGeometryChange()
        self.rect.setWidth(self.baseWidth * canvasSize)
        self.rect.setHeight(2 * self.baseWidth)
        self._minorGridPainterPath = None
        self._majorGridPainterPath = None
        addx = self.rect.width()
        addy = -self.addBasesButton.boundingRect().height()
        self.addBasesButton.setPos(addx, addy)
        self.addBasesButton.show()
        remx = self.rect.width()-self.removeBasesButton.boundingRect().width()
        remy = -self.removeBasesButton.boundingRect().height()
        bbr = self.removeBasesButton.boundingRect()
        self.removeBasesButton.setPos(remx, remy)
    
    def positionInPhgChanged(self):
        if self._pathHelixGroup.topmostPathHelix() == self:
            self.addBasesButton.show()
            self.removeBasesButton.show()
        else:
            self.addBasesButton.hide()
            self.removeBasesButton.hide()

    def boundingRect(self):
        return self.rect

    ################# Crossover Handles #################
    def preXOverHandlesVisible(self):
        return self._preXOverHandles != None

    def setPreXOverHandlesVisible(self, shouldBeVisible):
        areVisible = self._preXOverHandles != None
        if areVisible and not shouldBeVisible:
            for pch in self._preXOverHandles:
                if pch.scene():
                    pch.scene().removeItem(pch)
            self._preXOverHandles = None
            self.vhelix().part().virtualHelixAtCoordsChanged.disconnect(\
                                                   self.updatePreXOverHandles)
        elif not areVisible and shouldBeVisible:
            self._preXOverHandles = []
            for strandType, facingRight in\
              product((StrandType.Scaffold, StrandType.Staple), (True, False)):
                # Get potential crossovers in [neighborVirtualHelix, index] format
                potentialXOvers = self.vhelix().potentialCrossoverList(facingRight, strandType)
                numBases = self.vhelix().numBases()
                assert(all(index < numBases for neighborVH, index in potentialXOvers))
                for (neighborVH, fromIdx) in potentialXOvers:
                    pch = PreCrossoverHandle(self, strandType, fromIdx,\
                                             neighborVH, fromIdx,\
                                             not facingRight)
                    self._preXOverHandles.append(pch)
            self.vhelix().part().virtualHelixAtCoordsChanged.connect(self.updatePreXOverHandles)
        self._XOverCacheEnvironment = (self.vhelix().neighbors(), self.vhelix().numBases())

    def updatePreXOverHandles(self):
        cacheConstructionEnvironment = self._XOverCacheEnvironment
        currentEnvironment = (self.vhelix().neighbors(), self.vhelix().numBases())
        if cacheConstructionEnvironment != currentEnvironment:
            self.setPreXOverHandlesVisible(False)
            self.setPreXOverHandlesVisible(True)

    def makeSelfActiveHelix(self):
        self._pathHelixGroup.setActiveHelix(self)

    ################# Loading and Updating State From VHelix #################
    def vhelixBasesModified(self):
        self._endpoints = None  # Clear endpoint drawing cache
        self._segmentPaths = None  # Clear drawing cache of lines
        # Reset active helix if necessary
        if self.phgroup().getActiveHelix() == self:
            self.makeSelfActiveHelix()
        self.update()

    ############################# Drawing ##########################
    def paint(self, painter, option, widget=None):
        # Note that the methods that fetch the paths
        # cache the paths and that those caches are
        # invalidated as the primary mechanism
        # of updating after a change in vhelix's bases
        if not self.boundingRect().intersects(option.exposedRect):
            return
        painter.save()
        painter.setBrush(self.nobrush)
        painter.setPen(self.minorGridPen)
        painter.drawPath(self.minorGridPainterPath())  # Minor grid lines
        painter.setPen(self.majorGridPen)
        painter.drawPath(self.majorGridPainterPath())  # Major grid lines
        painter.setBrush(Qt.NoBrush)
        segmentPaths, endptPths = self.segmentAndEndptPaths()
        for sp in segmentPaths:
            pen, path = sp
            strandRect = path.controlPointRect().adjusted(0, 0, 5, 5)
            if not strandRect.intersects(option.exposedRect):
                continue
            painter.setPen(pen)
            painter.drawPath(path)
        painter.setPen(Qt.NoPen)
        for ep in endptPths:
            brush, path = ep
            if not path.controlPointRect().intersects(option.exposedRect):
                continue
            painter.setBrush(brush)
            painter.drawPath(path)
        self.paintLoopsAndSkips(painter)
        self.paintHorizontalBaseText(painter)
        painter.restore()

    def paintLoopsAndSkips(self, painter):
        vh = self.vhelix()
        for strandType in (StrandType.Scaffold, StrandType.Staple):
            top = self.strandIsTop(strandType)
            for index, loopsize in vh._loop(strandType).iteritems():
                ul = self.baseLocation(strandType, index)
                if loopsize > 0:
                    path = self._loopitem.getLoop(top)
                    path = path.translated(*ul)
                    painter.setPen(self._loopitem.getPen())
                    painter.setPen(QPen(vh.colorOfBase(strandType, index), 2))
                    painter.setBrush(Qt.NoBrush)
                    painter.drawPath(path)
                    baseText = vh.sequenceForLoopAt(strandType, index)
                    if len(baseText) > 20:
                        baseText = baseText[:17] + '...'
                    fractionArclenPerChar = (1.-2*self.fractionLoopToPad)/(len(baseText)+1)
                    painter.setPen(QPen(Qt.black))
                    painter.setBrush(Qt.NoBrush)
                    painter.setFont(self.sequenceFont)
                    for i in range(len(baseText)):
                        frac = self.fractionLoopToPad + (i+1)*fractionArclenPerChar
                        pt = path.pointAtPercent(frac)
                        tangAng = path.angleAtPercent(frac)
                        painter.save()
                        painter.translate(pt)
                        painter.rotate(-tangAng)
                        painter.translate(QPointF(-self.sequenceFontCharWidth/2.,
                                                  -2 if top else self.sequenceFontH))
                        painter.drawText(0, 0, baseText[i if top else -i-1])
                        painter.restore()
                else:  # loopsize < 0 (a skip)
                    path = self._skipitem.getSkip()
                    path = path.translated(*ul)
                    painter.setPen(self._skipitem.getPen())
                    painter.drawPath(path)

    def paintHorizontalBaseText(self, painter):
        vh = self.vhelix()
        scafTxt = vh.sequenceForVirtualStrand(StrandType.Scaffold)
        scafY = self.baseWidth*0 + self.sequenceTextYCenteringOffset
        stapTxt = vh.sequenceForVirtualStrand(StrandType.Staple)
        stapY = self.baseWidth*1 + self.sequenceTextYCenteringOffset
        if self.strandIsTop(StrandType.Staple):
            # We assumed scaffold was on top. Correct that.
            scafY, stapY = stapY, scafY
        if vh.directionOfStrandIs5to3(StrandType.Scaffold):
            # Text goes from 5 to 3, so staple gets vertically flipped
            shouldVFlipScaf = False
            # We still want the text to be drawn at the same Y coordinate,
            # just upside down, so we undo the transform as it applies
            # to the Y coord
            stapY = -stapY
        else:
            shouldVFlipScaf = True
            scafY = -scafY
        scafX = stapX = self.sequenceTextXCenteringOffset
        painter.setPen(QPen(Qt.black))
        painter.setBrush(Qt.NoBrush)
        painter.setFont(self.sequenceFont)
        if shouldVFlipScaf:
            painter.scale(1, -1)
        painter.drawText(scafX, scafY, self.baseWidth*vh.numBases(),\
                         self.baseWidth/2., Qt.AlignVCenter, scafTxt)
        painter.scale(1, -1)
        painter.drawText(stapX, stapY, self.baseWidth*vh.numBases(),\
                         self.baseWidth/2., Qt.AlignVCenter, stapTxt)

    def minorGridPainterPath(self):
        """
        Returns a QPainterPath object for the minor grid lines.
        The path also includes a border outline and a midline for
        dividing scaffold and staple bases.
        """
        if self._minorGridPainterPath:
            return self._minorGridPainterPath
        path = QPainterPath()
        canvasSize = self._vhelix.part().numBases()
        # border
        path.addRect(0, 0, self.baseWidth * canvasSize, 2 * self.baseWidth)
        # minor tick marks
        for i in range(canvasSize):
            if (i % self._vhelix.part().majorGrid() != 0):
                x = round(self.baseWidth * i) + .5
                path.moveTo(x, 0)
                path.lineTo(x, 2 * self.baseWidth)
        # staple-scaffold divider
        path.moveTo(0, self.baseWidth)
        path.lineTo(self.baseWidth * canvasSize, self.baseWidth)
        self._minorGridPainterPath = path
        return path

    def majorGridPainterPath(self):
        """
        Returns a QPainterPath object for the major grid lines.
        This is separated from the minor grid lines so different
        pens can be used for each.
        """
        if self._majorGridPainterPath:
            return self._majorGridPainterPath
        path = QPainterPath()
        canvasSize = self._vhelix.part().numBases()
        # major tick marks  FIX: 7 is honeycomb-specific
        for i in range(0, canvasSize + 1, self._vhelix.part().majorGrid()):
            x = round(self.baseWidth * i) + .5
            path.moveTo(x, .5)
            path.lineTo(x, 2 * self.baseWidth - .5)
        self._majorGridPainterPath = path
        return path

    def segmentAndEndptPaths(self):
        """Returns an array of (pen, penPainterPath, brush, brushPainterPath)
        for drawing segment lines and handles."""
        if self._segmentPaths and self._endptPaths:
            return (self._segmentPaths, self._endptPaths)
        self._segmentPaths = []
        self._endptPaths = []
        vh = self.vhelix()
        for strandType in (StrandType.Scaffold, StrandType.Staple):
            top = self.strandIsTop(strandType)
            segments, ends3, ends5 = self._vhelix.getSegmentsAndEndpoints(strandType)
            # print "[%i:%s] "%(vh.number(), "scaf" if strandType==StrandType.Scaffold else "stap") + " ".join(str(b) for b in segments)
            for (startIndex, endIndex) in segments:
                numBasesInOligo = vh.numberOfBasesConnectedTo(strandType,\
                                                              int(startIndex))
                highlight = (numBasesInOligo > styles.oligoLenAboveWhichHighlight or\
                             numBasesInOligo < styles.oligoLenBelowWhichHighlight) and\
                             strandType == StrandType.Staple

                startPt = self.baseLocation(strandType, startIndex, centerY=True)
                endPt = self.baseLocation(strandType, endIndex, centerY=True)

                # Only draw to the edge of breakpoints.
                if self._vhelix.hasEndAt(strandType, startIndex):
                    startPt = (startPt[0]+styles.PATH_BASE_WIDTH/2, startPt[1])
                elif highlight and self._vhelix.hasCrossoverAt(strandType, startIndex):
                    # compensate for width of stroke in crossover path
                    startPt = (startPt[0]+styles.PATH_STRAND_HIGHLIGHT_STROKE_WIDTH/2, startPt[1])
                if self._vhelix.hasEndAt(strandType, endIndex):
                    endPt = (endPt[0]-styles.PATH_BASE_WIDTH/2, endPt[1])
                elif highlight and self._vhelix.hasCrossoverAt(strandType, endIndex):
                    endPt = (endPt[0]-styles.PATH_STRAND_HIGHLIGHT_STROKE_WIDTH/2, endPt[1])

                pp = QPainterPath()
                pp.moveTo(*startPt)
                pp.lineTo(*endPt)
                color = vh.colorOfBase(strandType, int(startIndex))
                width = styles.PATH_STRAND_STROKE_WIDTH
                if numBasesInOligo > styles.oligoLenAboveWhichHighlight or\
                   numBasesInOligo < styles.oligoLenBelowWhichHighlight:
                    if strandType == StrandType.Staple:
                        color.setAlpha(128)
                        width = styles.PATH_STRAND_HIGHLIGHT_STROKE_WIDTH
                else:
                    color.setAlpha(255)
                pen = QPen(color, width)
                pen.setCapStyle(Qt.FlatCap)
                self._segmentPaths.append((pen, pp))
            for e3 in ends3:
                upperLeft = self.baseLocation(strandType, e3)
                bp = QPainterPath()
                color = QColor(vh.colorOfBase(strandType, e3))
                color.setAlpha(255)
                brush = QBrush(color)
                bp.addPath(ppR3.translated(*upperLeft) if top else\
                                                    ppL3.translated(*upperLeft))
                self._endptPaths.append((brush, bp))
            for e5 in ends5:
                upperLeft = self.baseLocation(strandType, e5)
                bp = QPainterPath()
                color = QColor(vh.colorOfBase(strandType, e5))
                color.setAlpha(255)
                brush = QBrush(color)
                bp.addPath(ppL5.translated(*upperLeft) if top else\
                                                    ppR5.translated(*upperLeft))
                self._endptPaths.append((brush, bp))
        return (self._segmentPaths, self._endptPaths)

    def strandIsTop(self, strandType):
        return self.evenParity() and strandType == StrandType.Scaffold\
           or not self.evenParity() and strandType == StrandType.Staple

    def baseAtLocation(self, x, y, clampX=False, clampY=False):
        """Returns the (strandType, index) under the location x,y or None.
        
        It shouldn't be possible to click outside a pathhelix and still call
        this function. However, this sometimes happens if you click exactly
        on the top or bottom edge, resulting in a negative y value.
        """
        baseIdx = int(floor(x / self.baseWidth))
        minBase, maxBase = 0, self.vhelix().numBases()
        if baseIdx < minBase or baseIdx >= maxBase:
            if clampX:
                baseIdx = util.clamp(baseIdx, minBase, maxBase-1)
            else:
                return None
        if y < 0:
            y = 0  # HACK: zero out y due to erroneous click
        strandIdx = floor(y * 1. / self.baseWidth)
        if strandIdx < 0 or strandIdx > 1:
            if clampY:
                strandIdx = int(util.clamp(strandIdx, 0, 1))
            else:
                return None
        if self.strandIsTop(StrandType.Scaffold):
            strands = StrandType.Scaffold, StrandType.Staple
        else:
            strands = StrandType.Staple, StrandType.Scaffold
        return (strands[int(strandIdx)], baseIdx)

    def baseLocation(self, strandType, baseIdx, center=False, centerY=False):
        """Returns the coordinates of the upper left corner of the base
        referenced by strandType and baseIdx. If center=True, returns the
        center of the base instead of the upper left corner."""
        if self.strandIsTop(strandType):
            y = 0
        else:
            y = self.baseWidth
        x = baseIdx * self.baseWidth
        if center:
            x += self.baseWidth / 2
            y += self.baseWidth / 2
        if centerY:
            y += self.baseWidth / 2
        return (x, y)

    def sceneEvent(self, event):
        """Included for unit testing in order to grab events that are sent
        via QGraphicsScene.sendEvent()."""
        if self.controller().testRecorder:
            self.controller().testRecorder.pathSceneEvent(event, self.number())
        if event.type() == QEvent.MouseButtonPress:
            self.mousePressEvent(event)
            return True
        elif event.type() == QEvent.MouseButtonRelease:
            self.mouseReleaseEvent(event)
            return True
        elif event.type() == QEvent.MouseMove:
            self.mouseMoveEvent(event)
            return True
        QGraphicsObject.sceneEvent(self, event)
        return False