//Copyright 2017 Ryan Wick
//This file is part of Bandage
//Bandage 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.
//Bandage is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU General Public License for more details.
//You should have received a copy of the GNU General Public License
//along with Bandage. If not, see .
#include "graphicsitemnode.h"
#include "debruijnnode.h"
#include "ogdfnode.h"
#include
#include "../graph/ogdfnode.h"
#include "../program/settings.h"
#include
#include
#include
#include "debruijnedge.h"
#include "graphicsitemedge.h"
#include "../ogdf/basic/GraphAttributes.h"
#include
#include
#include
#include
#include
#include "../ui/mygraphicsscene.h"
#include
#include "../ui/mygraphicsview.h"
#include
#include "../blast/blasthit.h"
#include "../blast/blastquery.h"
#include "../blast/blasthitpart.h"
#include "assemblygraph.h"
#include
#include
#include "../program/memory.h"
GraphicsItemNode::GraphicsItemNode(DeBruijnNode * deBruijnNode,
ogdf::GraphAttributes * graphAttributes, QGraphicsItem * parent) :
QGraphicsItem(parent), m_deBruijnNode(deBruijnNode),
m_hasArrow(g_settings->doubleMode || g_settings->arrowheadsInSingleMode)
{
setWidth();
OgdfNode * pathOgdfNode = deBruijnNode->getOgdfNode();
if (pathOgdfNode != 0)
{
for (size_t i = 0; i < pathOgdfNode->m_ogdfNodes.size(); ++i)
{
ogdf::node ogdfNode = pathOgdfNode->m_ogdfNodes[i];
QPointF point(graphAttributes->x(ogdfNode), graphAttributes->y(ogdfNode));
m_linePoints.push_back(point);
}
}
else
{
pathOgdfNode = deBruijnNode->getReverseComplement()->getOgdfNode();
for (int i = int(pathOgdfNode->m_ogdfNodes.size()) - 1; i >= 0; --i)
{
ogdf::node ogdfNode = pathOgdfNode->m_ogdfNodes[i];
QPointF point(graphAttributes->x(ogdfNode), graphAttributes->y(ogdfNode));
m_linePoints.push_back(point);
}
}
//If we are in double mode and this node's complement is also drawn,
//then we should shift the points so the two nodes are not drawn directly
//on top of each other.
if (g_settings->doubleMode && deBruijnNode->getReverseComplement()->isDrawn())
shiftPointsLeft();
remakePath();
}
//This constructor makes a new GraphicsItemNode by copying the line points of
//the given node.
GraphicsItemNode::GraphicsItemNode(DeBruijnNode * deBruijnNode,
GraphicsItemNode * toCopy,
QGraphicsItem * parent) :
QGraphicsItem(parent), m_deBruijnNode(deBruijnNode),
m_hasArrow(toCopy->m_hasArrow),
m_linePoints(toCopy->m_linePoints)
{
setWidth();
remakePath();
}
//This constructor makes a new GraphicsItemNode with a specific collection of
//line points.
GraphicsItemNode::GraphicsItemNode(DeBruijnNode * deBruijnNode,
std::vector linePoints,
QGraphicsItem * parent) :
QGraphicsItem(parent), m_deBruijnNode(deBruijnNode),
m_hasArrow(g_settings->doubleMode),
m_linePoints(linePoints)
{
setWidth();
remakePath();
}
void GraphicsItemNode::paint(QPainter * painter, const QStyleOptionGraphicsItem *, QWidget *)
{
//This code lets me see the node's bounding box.
//I use it for debugging graphics issues.
// painter->setBrush(Qt::NoBrush);
// painter->setPen(QPen(Qt::black, 1.0));
// painter->drawRect(boundingRect());
QPainterPath outlinePath = shape();
//Fill the node's colour
QBrush brush(m_colour);
painter->fillPath(outlinePath, brush);
bool nodeHasBlastHits;
if (g_settings->doubleMode)
nodeHasBlastHits = m_deBruijnNode->thisNodeHasBlastHits();
else
nodeHasBlastHits = m_deBruijnNode->thisNodeOrReverseComplementHasBlastHits();
//If the node contains a BLAST hit, draw that on top.
if (nodeHasBlastHits && (g_settings->nodeColourScheme == BLAST_HITS_RAINBOW_COLOUR ||
g_settings->nodeColourScheme == BLAST_HITS_SOLID_COLOUR))
{
std::vector parts;
//The scaled node length is passed to the function which makes the
//BlastHitPart objects, because we don't want those parts to be much
//less than 1 pixel in size, which isn't necessary and can cause weird
//visual artefacts.
double scaledNodeLength = getNodePathLength() * g_absoluteZoom;
if (g_settings->doubleMode)
{
if (m_deBruijnNode->thisNodeHasBlastHits())
parts = m_deBruijnNode->getBlastHitPartsForThisNode(scaledNodeLength);
}
else
{
if (m_deBruijnNode->thisNodeOrReverseComplementHasBlastHits())
parts = m_deBruijnNode->getBlastHitPartsForThisNodeOrReverseComplement(scaledNodeLength);
}
QPen partPen;
partPen.setWidthF(m_width);
partPen.setCapStyle(Qt::FlatCap);
partPen.setJoinStyle(Qt::BevelJoin);
//If the node has an arrow, then it's necessary to use the outline
//as a clipping path so the colours don't extend past the edge of the
//node.
if (m_hasArrow)
painter->setClipPath(outlinePath);
for (size_t i = 0; i < parts.size(); ++i)
{
partPen.setColor(parts[i].m_colour);
painter->setPen(partPen);
painter->drawPath(makePartialPath(parts[i].m_nodeFractionStart,
parts[i].m_nodeFractionEnd));
}
painter->setClipping(false);
}
//Draw the node outline
QColor outlineColour = g_settings->outlineColour;
double outlineThickness = g_settings->outlineThickness;
if (isSelected())
{
outlineColour = g_settings->selectionColour;
outlineThickness = g_settings->selectionThickness;
}
if (outlineThickness > 0.0)
{
outlinePath = outlinePath.simplified();
QPen outlinePen(QBrush(outlineColour), outlineThickness, Qt::SolidLine,
Qt::SquareCap, Qt::RoundJoin);
painter->setPen(outlinePen);
painter->drawPath(outlinePath);
}
//Draw the path highlighting outline, if appropriate
if (g_memory->pathDialogIsVisible)
exactPathHighlightNode(painter);
//Draw the query path, if appropriate
if (g_memory->queryPathDialogIsVisible)
queryPathHighlightNode(painter);
//Draw node labels if there are any to display.
if (anyNodeDisplayText())
{
QStringList nodeText = getNodeText();
QPainterPath textPath;
QFontMetrics metrics(g_settings->labelFont);
double fontHeight = metrics.ascent();
for (int i = 0; i < nodeText.size(); ++i)
{
QString text = nodeText.at(i);
int stepsUntilLast = nodeText.size() - 1 - i;
double shiftLeft = -metrics.boundingRect(text).width() / 2.0;
textPath.addText(shiftLeft, -stepsUntilLast * fontHeight, g_settings->labelFont, text);
}
std::vector centres;
if (g_settings->positionTextNodeCentre)
centres.push_back(getCentre(m_linePoints));
else
centres = getCentres();
for (size_t i = 0; i < centres.size(); ++i)
drawTextPathAtLocation(painter, textPath, centres[i]);
}
//Draw BLAST hit labels, if appropriate.
if (g_settings->displayBlastHits && nodeHasBlastHits)
{
std::vector blastHitText;
std::vector blastHitLocation;
if (g_settings->doubleMode)
getBlastHitsTextAndLocationThisNode(&blastHitText, &blastHitLocation);
else
getBlastHitsTextAndLocationThisNodeOrReverseComplement(&blastHitText, &blastHitLocation);
for (size_t i = 0; i < blastHitText.size(); ++i)
{
QString text = blastHitText[i];
QPointF centre = blastHitLocation[i];
QPainterPath textPath;
QFontMetrics metrics(g_settings->labelFont);
double shiftLeft = -metrics.boundingRect(text).width() / 2.0;
textPath.addText(shiftLeft, 0.0, g_settings->labelFont, text);
drawTextPathAtLocation(painter, textPath, centre);
}
}
}
void GraphicsItemNode::drawTextPathAtLocation(QPainter * painter, QPainterPath textPath, QPointF centre)
{
QRectF textBoundingRect = textPath.boundingRect();
double textHeight = textBoundingRect.height();
QPointF offset(0.0, textHeight / 2.0);
double zoom = g_absoluteZoom;
if (zoom == 0.0)
zoom = 1.0;
double zoomAdjustment = 1.0 / (1.0 + ((zoom - 1.0) * g_settings->textZoomScaleFactor));
double inverseZoomAdjustment = 1.0 / zoomAdjustment;
painter->translate(centre);
painter->rotate(-g_graphicsView->getRotation());
painter->scale(zoomAdjustment, zoomAdjustment);
painter->translate(offset);
if (g_settings->textOutline)
{
painter->setPen(QPen(g_settings->textOutlineColour,
g_settings->textOutlineThickness * 2.0,
Qt::SolidLine,
Qt::SquareCap,
Qt::RoundJoin));
painter->drawPath(textPath);
}
painter->fillPath(textPath, QBrush(g_settings->textColour));
painter->translate(-offset);
painter->scale(inverseZoomAdjustment, inverseZoomAdjustment);
painter->rotate(g_graphicsView->getRotation());
painter->translate(-centre);
}
void GraphicsItemNode::setNodeColour()
{
switch (g_settings->nodeColourScheme)
{
case UNIFORM_COLOURS:
if (m_deBruijnNode->isSpecialNode())
m_colour = g_settings->uniformNodeSpecialColour;
else if (usePositiveNodeColour())
m_colour = g_settings->uniformPositiveNodeColour;
else
m_colour = g_settings->uniformNegativeNodeColour;
break;
case RANDOM_COLOURS:
{
//Make a colour with a random hue. Assign a colour to both this node and
//it complement so their hue matches.
int hue = rand() % 360;
QColor posColour;
posColour.setHsl(hue,
g_settings->randomColourPositiveSaturation,
g_settings->randomColourPositiveLightness);
posColour.setAlpha(g_settings->randomColourPositiveOpacity);
QColor negColour;
negColour.setHsl(hue,
g_settings->randomColourNegativeSaturation,
g_settings->randomColourNegativeLightness);
negColour.setAlpha(g_settings->randomColourNegativeOpacity);
QColor colour1, colour2;
if (m_deBruijnNode->isPositiveNode())
{
colour1 = posColour;
colour2 = negColour;
}
else
{
colour1 = negColour;
colour2 = posColour;
}
m_colour = colour1;
DeBruijnNode * revCompNode = m_deBruijnNode->getReverseComplement();
if (revCompNode != 0)
{
GraphicsItemNode * revCompGraphNode = revCompNode->getGraphicsItemNode();
if (revCompGraphNode != 0)
revCompGraphNode->m_colour = colour2;
}
break;
}
case DEPTH_COLOUR:
{
m_colour = getDepthColour();
break;
}
case BLAST_HITS_RAINBOW_COLOUR:
{
m_colour = g_settings->noBlastHitsColour;
break;
}
case BLAST_HITS_SOLID_COLOUR:
{
m_colour = g_settings->noBlastHitsColour;
break;
}
case CUSTOM_COLOURS:
{
m_colour = m_deBruijnNode->getCustomColourForDisplay();
break;
}
default: //CONTIGUITY COLOUR
{
//For single nodes, display the colour of whichever of the
//twin nodes has the greatest contiguity status.
ContiguityStatus contiguityStatus = m_deBruijnNode->getContiguityStatus();
if (!m_hasArrow)
{
ContiguityStatus twinContiguityStatus = m_deBruijnNode->getReverseComplement()->getContiguityStatus();
if (twinContiguityStatus < contiguityStatus)
contiguityStatus = twinContiguityStatus;
}
switch (contiguityStatus)
{
case STARTING:
m_colour = g_settings->contiguityStartingColour;
break;
case CONTIGUOUS_STRAND_SPECIFIC:
m_colour = g_settings->contiguousStrandSpecificColour;
break;
case CONTIGUOUS_EITHER_STRAND:
m_colour = g_settings->contiguousEitherStrandColour;
break;
case MAYBE_CONTIGUOUS:
m_colour = g_settings->maybeContiguousColour;
break;
default: //NOT_CONTIGUOUS
m_colour = g_settings->notContiguousColour;
break;
}
}
}
}
QPainterPath GraphicsItemNode::shape() const
{
//If there is only one segment and it is shorter than half its
//width, then the arrow head will not be made with 45 degree
//angles, but rather whatever angle is made by going from the
//end to the back corners (the final node will be a triangle).
if (m_hasArrow
&& m_linePoints.size() == 2
&& distance(getLast(), getSecondLast()) < m_width / 2.0)
{
QLineF backline = QLineF(getSecondLast(), getLast()).normalVector();
backline.setLength(m_width / 2.0);
QPointF backVector = backline.p2() - backline.p1();
QPainterPath trianglePath;
trianglePath.moveTo(getLast());
trianglePath.lineTo(getSecondLast() + backVector);
trianglePath.lineTo(getSecondLast() - backVector);
trianglePath.lineTo(getLast());
return trianglePath;
}
//Create a path that outlines the main node shape.
QPainterPathStroker stroker;
stroker.setWidth(m_width);
stroker.setCapStyle(Qt::FlatCap);
stroker.setJoinStyle(Qt::RoundJoin);
QPainterPath mainNodePath = stroker.createStroke(m_path);
if (!m_hasArrow)
return mainNodePath;
//If the node has an arrow head, subtract the part of its
//final segment to give it a pointy end.
//NOTE: THIS APPROACH CAN LEAD TO WEIRD EFFECTS WHEN THE NODE'S
//POINTY END OVERLAPS WITH ANOTHER PART OF THE NODE. PERHAPS THERE
//IS A BETTER WAY TO MAKE ARROWHEADS?
QLineF frontline = QLineF(getLast(), getSecondLast()).normalVector();
frontline.setLength(m_width / 2.0);
QPointF frontVector = frontline.p2() - frontline.p1();
QLineF arrowheadLine(getLast(), getSecondLast());
arrowheadLine.setLength(1.42 * (m_width / 2.0));
arrowheadLine.setAngle(arrowheadLine.angle() + 45.0);
QPointF arrow1 = arrowheadLine.p2();
arrowheadLine.setAngle(arrowheadLine.angle() - 90.0);
QPointF arrow2 = arrowheadLine.p2();
QLineF lastSegmentLine(getSecondLast(), getLast());
lastSegmentLine.setLength(0.01);
QPointF additionalForwardBit = lastSegmentLine.p2() - lastSegmentLine.p1();
QPainterPath subtractionPath;
subtractionPath.moveTo(getLast());
subtractionPath.lineTo(arrow1);
subtractionPath.lineTo(getLast() + frontVector + additionalForwardBit);
subtractionPath.lineTo(getLast() - frontVector + additionalForwardBit);
subtractionPath.lineTo(arrow2);
subtractionPath.lineTo(getLast());
return mainNodePath.subtracted(subtractionPath);
}
void GraphicsItemNode::mousePressEvent(QGraphicsSceneMouseEvent * event)
{
m_grabIndex = 0;
QPointF grabPoint = event->pos();
double closestPointDistance = distance(grabPoint, m_linePoints[0]);
for (size_t i = 1; i < m_linePoints.size(); ++i)
{
double pointDistance = distance(grabPoint, m_linePoints[i]);
if (pointDistance < closestPointDistance)
{
closestPointDistance = pointDistance;
m_grabIndex = i;
}
}
}
//When this node graphics item is moved, each of the connected edge
//graphics items will need to be adjusted accordingly.
void GraphicsItemNode::mouseMoveEvent(QGraphicsSceneMouseEvent * event)
{
QPointF difference = event->pos() - event->lastPos();
//If this node is selected, then move all of the other selected nodes too.
//If it is not selected, then only move this node.
std::vector nodesToMove;
MyGraphicsScene * graphicsScene = dynamic_cast(scene());
if (isSelected())
nodesToMove = graphicsScene->getSelectedGraphicsItemNodes();
else
nodesToMove.push_back(this);
for (size_t i = 0; i < nodesToMove.size(); ++i)
{
nodesToMove[i]->shiftPoints(difference);
nodesToMove[i]->remakePath();
}
graphicsScene->possiblyExpandSceneRectangle(&nodesToMove);
fixEdgePaths(&nodesToMove);
}
//This function remakes edge paths. If nodes is passed, it will remake the
//edge paths for all of the nodes. If nodes isn't passed, then it will just
//do it for this node.
void GraphicsItemNode::fixEdgePaths(std::vector * nodes)
{
std::set edgesToFix;
if (nodes == 0)
{
const std::vector * edges = m_deBruijnNode->getEdgesPointer();
for (size_t j = 0; j < edges->size(); ++j)
edgesToFix.insert((*edges)[j]);
}
else
{
for (size_t i = 0; i < nodes->size(); ++i)
{
DeBruijnNode * node = (*nodes)[i]->m_deBruijnNode;
const std::vector * edges = node->getEdgesPointer();
for (size_t j = 0; j < edges->size(); ++j)
edgesToFix.insert((*edges)[j]);
}
}
for (std::set::iterator i = edgesToFix.begin(); i != edgesToFix.end(); ++i)
{
DeBruijnEdge * deBruijnEdge = *i;
GraphicsItemEdge * graphicsItemEdge = deBruijnEdge->getGraphicsItemEdge();
//If this edge has a graphics item, adjust it now.
if (graphicsItemEdge != 0)
graphicsItemEdge->calculateAndSetPath();
//If this edge does not have a graphics item, then perhaps its
//reverse complment does. Only do this check if the graph was drawn
//on single mode.
else if (!g_settings->doubleMode)
{
graphicsItemEdge = deBruijnEdge->getReverseComplement()->getGraphicsItemEdge();
if (graphicsItemEdge != 0)
graphicsItemEdge->calculateAndSetPath();
}
}
}
void GraphicsItemNode::shiftPoints(QPointF difference)
{
prepareGeometryChange();
if (g_settings->nodeDragging == NO_DRAGGING)
return;
else if (isSelected()) //Move all pieces for selected nodes
{
for (size_t i = 0; i < m_linePoints.size(); ++i)
m_linePoints[i] += difference;
}
else if (g_settings->nodeDragging == ONE_PIECE)
m_linePoints[m_grabIndex] += difference;
else if (g_settings->nodeDragging == NEARBY_PIECES)
{
for (size_t i = 0; i < m_linePoints.size(); ++i)
{
int indexDistance = abs(int(i) - int(m_grabIndex));
double dragStrength = pow(2.0, -1.0 * pow(double(indexDistance), 1.8) / g_settings->dragStrength); //constants chosen for dropoff of drag strength
m_linePoints[i] += difference * dragStrength;
}
}
}
void GraphicsItemNode::remakePath()
{
QPainterPath path;
path.moveTo(m_linePoints[0]);
for (size_t i = 1; i < m_linePoints.size(); ++i)
path.lineTo(m_linePoints[i]);
m_path = path;
}
QPainterPath GraphicsItemNode::makePartialPath(double startFraction, double endFraction)
{
if (endFraction < startFraction)
std::swap(startFraction, endFraction);
double totalLength = getNodePathLength();
QPainterPath path;
bool pathStarted = false;
double lengthSoFar = 0.0;
for (size_t i = 0; i < m_linePoints.size() - 1; ++i)
{
QPointF point1 = m_linePoints[i];
QPointF point2 = m_linePoints[i + 1];
QLineF line(point1, point2);
double point1Fraction = lengthSoFar / totalLength;
lengthSoFar += line.length();
double point2Fraction = lengthSoFar / totalLength;
//If the path hasn't yet begun and this segment is before
//the starting fraction, do nothing.
if (!pathStarted && point2Fraction < startFraction)
continue;
//If the path hasn't yet begun but this segment covers the starting
//fraction, start the path now.
if (!pathStarted && point2Fraction >= startFraction)
{
pathStarted = true;
path.moveTo(findIntermediatePoint(point1, point2, point1Fraction, point2Fraction, startFraction));
}
//If the path is in progress and this segment hasn't yet reached the end,
//just continue the path.
if (pathStarted && point2Fraction < endFraction)
path.lineTo(point2);
//If the path is in progress and this segment passes the end, finish the line.
if (pathStarted && point2Fraction >= endFraction)
{
path.lineTo(findIntermediatePoint(point1, point2, point1Fraction, point2Fraction, endFraction));
return path;
}
}
return path;
}
double GraphicsItemNode::getNodePathLength()
{
double totalLength = 0.0;
for (size_t i = 0; i < m_linePoints.size() - 1; ++i)
{
QLineF line(m_linePoints[i], m_linePoints[i + 1]);
totalLength += line.length();
}
return totalLength;
}
//This function will find the point that is a certain fraction of the way along the node's path.
QPointF GraphicsItemNode::findLocationOnPath(double fraction)
{
double totalLength = getNodePathLength();
double lengthSoFar = 0.0;
for (size_t i = 0; i < m_linePoints.size() - 1; ++i)
{
QPointF point1 = m_linePoints[i];
QPointF point2 = m_linePoints[i + 1];
QLineF line(point1, point2);
double point1Fraction = lengthSoFar / totalLength;
lengthSoFar += line.length();
double point2Fraction = lengthSoFar / totalLength;
//If point2 hasn't yet reached the target, do nothing.
if (point2Fraction < fraction)
continue;
//If the path hasn't yet begun but this segment covers the starting
//fraction, start the path now.
if (point2Fraction >= fraction)
return findIntermediatePoint(point1, point2, point1Fraction, point2Fraction, fraction);
}
//The code shouldn't get here, as the target point should have been found in the above loop.
return QPointF();
}
QPointF GraphicsItemNode::findIntermediatePoint(QPointF p1, QPointF p2, double p1Value, double p2Value, double targetValue)
{
QPointF difference = p2 - p1;
double fraction = (targetValue - p1Value) / (p2Value - p1Value);
return difference * fraction + p1;
}
double GraphicsItemNode::distance(QPointF p1, QPointF p2) const
{
double xDiff = p1.x() - p2.x();
double yDiff = p1.y() - p2.y();
return sqrt(xDiff * xDiff + yDiff * yDiff);
}
bool GraphicsItemNode::usePositiveNodeColour()
{
return !m_hasArrow || m_deBruijnNode->isPositiveNode();
}
//This function returns the nodes' visible centres. If the entire node is visible,
//then there is just one visible centre. If none of the node is visible, then
//there are no visible centres. If multiple parts of the node are visible, then there
//are multiple visible centres.
std::vector GraphicsItemNode::getCentres() const
{
std::vector centres;
std::vector currentRun;
QPointF lastP;
bool lastPointVisible = false;
for (size_t i = 0; i < m_linePoints.size(); ++i)
{
QPointF p = m_linePoints[i];
bool pVisible = g_graphicsView->isPointVisible(p);
//If this point is visible, but the last wasn't, a new run is started.
if (pVisible && !lastPointVisible)
{
//If this is not the first point, then we need to find the intermediate
//point that lies on the visible boundary and start the path with that.
if (i > 0)
currentRun.push_back(g_graphicsView->findIntersectionWithViewportBoundary(QLineF(p, lastP)));
currentRun.push_back(p);
}
//If th last point is visible and this one is too, add it to the current run.
else if (pVisible && lastPointVisible)
currentRun.push_back(p);
//If the last point is visible and this one isn't, then a run has ended.
else if (!pVisible && lastPointVisible)
{
//We need to find the intermediate point that is on the visible boundary.
currentRun.push_back(g_graphicsView->findIntersectionWithViewportBoundary(QLineF(p, lastP)));
centres.push_back(getCentre(currentRun));
currentRun.clear();
}
//If neither this point nor the last were visible, we still need to check whether
//the line segment between them is. If so, then then this may be a case where
//we are really zoomed in (and so line segments are large compared to the scene rect).
else if (i > 0 && !pVisible && !lastPointVisible)
{
bool success;
QLineF v = g_graphicsView->findVisiblePartOfLine(QLineF(lastP, p), &success);
if (success)
{
QPointF vCentre = QPointF((v.p1().x() + v.p2().x()) / 2.0, (v.p1().y() + v.p2().y()) / 2.0);
centres.push_back(vCentre);
}
}
lastPointVisible = pVisible;
lastP = p;
}
//If there is a current run, add its centre
if (currentRun.size() > 0)
centres.push_back(getCentre(currentRun));
return centres;
}
//This function finds the centre point on the path defined by linePoints.
QPointF GraphicsItemNode::getCentre(std::vector linePoints) const
{
if (linePoints.size() == 0)
return QPointF();
if (linePoints.size() == 1)
return linePoints[0];
double pathLength = 0.0;
for (size_t i = 0; i < linePoints.size() - 1; ++i)
pathLength += distance(linePoints[i], linePoints[i+1]);
double endToCentre = pathLength / 2.0;
double lengthSoFar = 0.0;
for (size_t i = 0; i < linePoints.size() - 1; ++i)
{
QPointF a = linePoints[i];
QPointF b = linePoints[i+1];
double segmentLength = distance(a, b);
//If this segment will push the distance over halfway, then it
//contains the centre point.
if (lengthSoFar + segmentLength >= endToCentre)
{
double additionalLengthNeeded = endToCentre - lengthSoFar;
double fractionOfCurrentSegment = additionalLengthNeeded / segmentLength;
return (b - a) * fractionOfCurrentSegment + a;
}
lengthSoFar += segmentLength;
}
//Code should never get here.
return QPointF();
}
QStringList GraphicsItemNode::getNodeText()
{
QStringList nodeText;
if (g_settings->displayNodeCustomLabels)
nodeText << m_deBruijnNode->getCustomLabelForDisplay();
if (g_settings->displayNodeNames)
{
QString nodeName = m_deBruijnNode->getName();
if (!g_settings->doubleMode)
nodeName.chop(1);
nodeText << nodeName;
}
if (g_settings->displayNodeLengths)
nodeText << formatIntForDisplay(m_deBruijnNode->getLength()) + " bp";
if (g_settings->displayNodeDepth)
nodeText << formatDepthForDisplay(m_deBruijnNode->getDepth());
if (g_settings->displayNodeCsvData && m_deBruijnNode->hasCsvData())
nodeText << m_deBruijnNode->getCsvLine(g_settings->displayNodeCsvDataCol);
return nodeText;
}
QSize GraphicsItemNode::getNodeTextSize(QString text)
{
QFontMetrics fontMetrics(g_settings->labelFont);
return fontMetrics.size(0, text);
}
QColor GraphicsItemNode::getDepthColour()
{
double depth = m_deBruijnNode->getDepth();
double lowValue;
double highValue;
if (g_settings->autoDepthValue)
{
lowValue = g_assemblyGraph->m_firstQuartileDepth;
highValue = g_assemblyGraph->m_thirdQuartileDepth;
}
else
{
lowValue = g_settings->lowDepthValue;
highValue = g_settings->highDepthValue;
}
if (depth <= lowValue)
return g_settings->lowDepthColour;
if (depth >= highValue)
return g_settings->highDepthColour;
double fraction = (depth - lowValue) / (highValue - lowValue);
int redDifference = g_settings->highDepthColour.red() - g_settings->lowDepthColour.red();
int greenDifference = g_settings->highDepthColour.green() - g_settings->lowDepthColour.green();
int blueDifference = g_settings->highDepthColour.blue() - g_settings->lowDepthColour.blue();
int alphaDifference = g_settings->highDepthColour.alpha() - g_settings->lowDepthColour.alpha();
int red = int(g_settings->lowDepthColour.red() + (fraction * redDifference) + 0.5);
int green = int(g_settings->lowDepthColour.green() + (fraction * greenDifference) + 0.5);
int blue = int(g_settings->lowDepthColour.blue() + (fraction * blueDifference) + 0.5);
int alpha = int(g_settings->lowDepthColour.alpha() + (fraction * alphaDifference) + 0.5);
return QColor(red, green, blue, alpha);
}
void GraphicsItemNode::setWidth()
{
m_width = getNodeWidth(m_deBruijnNode->getDepthRelativeToMeanDrawnDepth(), g_settings->depthPower,
g_settings->depthEffectOnWidth, g_settings->averageNodeWidth);
if (m_width < 0.0)
m_width = 0.0;
}
//The bounding rectangle of a node has to be a little bit bigger than
//the node's path, because of the outline. The selection outline is
//the largest outline we can expect, so use that to define the bounding
//rectangle.
QRectF GraphicsItemNode::boundingRect() const
{
double extraSize = g_settings->selectionThickness / 2.0;
QRectF bound = shape().boundingRect();
bound.setTop(bound.top() - extraSize);
bound.setBottom(bound.bottom() + extraSize);
bound.setLeft(bound.left() - extraSize);
bound.setRight(bound.right() + extraSize);
return bound;
}
double GraphicsItemNode::getNodeWidth(double depthRelativeToMeanDrawnDepth, double depthPower,
double depthEffectOnWidth, double averageNodeWidth)
{
if (depthRelativeToMeanDrawnDepth < 0.0)
depthRelativeToMeanDrawnDepth = 0.0;
double widthRelativeToAverage = (pow(depthRelativeToMeanDrawnDepth, depthPower) - 1.0) * depthEffectOnWidth + 1.0;
return averageNodeWidth * widthRelativeToAverage;
}
//This function shifts all the node's points to the left (relative to its
//direction). This is used in double mode to prevent nodes from displaying
//directly on top of their complement nodes.
void GraphicsItemNode::shiftPointsLeft()
{
shiftPointSideways(true);
}
void GraphicsItemNode::shiftPointsRight()
{
shiftPointSideways(false);
}
void GraphicsItemNode::shiftPointSideways(bool left)
{
prepareGeometryChange();
//The collection of line points should be at least
//two large. But just to be safe, quit now if it
//is not.
size_t linePointsSize = m_linePoints.size();
if (linePointsSize < 2)
return;
//Shift by a quarter of the segment length. This should make
//nodes one half segment length separated from their complements.
double shiftDistance = g_settings->doubleModeNodeSeparation;
for (size_t i = 0; i < linePointsSize; ++i)
{
QPointF point = m_linePoints[i];
QLineF nodeDirection;
//If the point is on the end, then determine the node direction
//using this point and its adjacent point.
if (i == 0)
{
QPointF nextPoint = m_linePoints[i+1];
nodeDirection = QLineF(point, nextPoint);
}
else if (i == linePointsSize - 1)
{
QPointF previousPoint = m_linePoints[i-1];
nodeDirection = QLineF(previousPoint, point);
}
// If the point is in the middle, then determine the node direction
//using both adjacent points.
else
{
QPointF previousPoint = m_linePoints[i-1];
QPointF nextPoint = m_linePoints[i+1];
nodeDirection = QLineF(previousPoint, nextPoint);
}
QLineF shiftLine = nodeDirection.normalVector().unitVector();
shiftLine.setLength(shiftDistance);
QPointF shiftVector;
if (left)
shiftVector = shiftLine.p2() - shiftLine.p1();
else
shiftVector = shiftLine.p1() - shiftLine.p2();
QPointF newPoint = point + shiftVector;
m_linePoints[i] = newPoint;
}
remakePath();
}
void GraphicsItemNode::getBlastHitsTextAndLocationThisNode(std::vector * blastHitText,
std::vector * blastHitLocation)
{
const std::vector * blastHits = m_deBruijnNode->getBlastHitsPointer();
for (size_t i = 0; i < blastHits->size(); ++i)
{
BlastHit * hit = (*blastHits)[i];
blastHitText->push_back(hit->m_query->getName());
blastHitLocation->push_back(findLocationOnPath(hit->getNodeCentreFraction()));
}
}
void GraphicsItemNode::getBlastHitsTextAndLocationThisNodeOrReverseComplement(std::vector * blastHitText,
std::vector * blastHitLocation)
{
getBlastHitsTextAndLocationThisNode(blastHitText, blastHitLocation);
const std::vector * blastHits = m_deBruijnNode->getReverseComplement()->getBlastHitsPointer();
for (size_t i = 0; i < blastHits->size(); ++i)
{
BlastHit * hit = (*blastHits)[i];
blastHitText->push_back(hit->m_query->getName());
blastHitLocation->push_back(findLocationOnPath(1.0 - hit->getNodeCentreFraction()));
}
}
//This function outlines and shades the appropriate part of a node if it is
//in the user-specified path.
void GraphicsItemNode::exactPathHighlightNode(QPainter * painter)
{
if (g_memory->userSpecifiedPath.containsNode(m_deBruijnNode))
pathHighlightNode2(painter, m_deBruijnNode, false, &g_memory->userSpecifiedPath);
if (!g_settings->doubleMode &&
g_memory->userSpecifiedPath.containsNode(m_deBruijnNode->getReverseComplement()))
pathHighlightNode2(painter, m_deBruijnNode->getReverseComplement(), true, &g_memory->userSpecifiedPath);
}
//This function outlines and shades the appropriate part of a node if it is
//in the user-specified path.
void GraphicsItemNode::queryPathHighlightNode(QPainter * painter)
{
if (g_memory->queryPaths.size() == 0)
return;
for (int i = 0; i < g_memory->queryPaths.size(); ++i)
{
Path * path = &(g_memory->queryPaths[i]);
if (path->containsNode(m_deBruijnNode))
pathHighlightNode2(painter, m_deBruijnNode, false, path);
if (!g_settings->doubleMode &&
path->containsNode(m_deBruijnNode->getReverseComplement()))
pathHighlightNode2(painter, m_deBruijnNode->getReverseComplement(), true, path);
}
}
void GraphicsItemNode::pathHighlightNode2(QPainter * painter,
DeBruijnNode * node,
bool reverse,
Path * path)
{
int numberOfTimesInMiddle = path->numberOfOccurrencesInMiddleOfPath(node);
for (int i = 0; i < numberOfTimesInMiddle; ++i)
pathHighlightNode3(painter, shape());
bool isStartingNode = path->isStartingNode(node);
bool isEndingNode = path->isEndingNode(node);
//If this is the only node in the path, then we limit the highlighting to the appropriate region.
if (isStartingNode && isEndingNode && path->getNodeCount() == 1)
{
pathHighlightNode3(painter, buildPartialHighlightPath(path->getStartFraction(), path->getEndFraction(), reverse));
return;
}
if (isStartingNode)
pathHighlightNode3(painter, buildPartialHighlightPath(path->getStartFraction(), 1.0, reverse));
if (isEndingNode)
pathHighlightNode3(painter, buildPartialHighlightPath(0.0, path->getEndFraction(), reverse));
}
void GraphicsItemNode::pathHighlightNode3(QPainter * painter,
QPainterPath highlightPath)
{
QBrush shadingBrush(g_settings->pathHighlightShadingColour);
painter->fillPath(highlightPath, shadingBrush);
highlightPath = highlightPath.simplified();
QPen outlinePen(QBrush(g_settings->pathHighlightOutlineColour),
g_settings->selectionThickness, Qt::SolidLine,
Qt::SquareCap, Qt::RoundJoin);
painter->setPen(outlinePen);
painter->drawPath(highlightPath);
}
QPainterPath GraphicsItemNode::buildPartialHighlightPath(double startFraction,
double endFraction,
bool reverse)
{
if (reverse)
{
startFraction = 1.0 - startFraction;
endFraction = 1.0 - endFraction;
std::swap(startFraction, endFraction);
}
QPainterPath partialPath = makePartialPath(startFraction,
endFraction);
QPainterPathStroker stroker;
//If the node has an arrow, we need a path intersection with the
//shape to make sure the arrowhead is part of the path. Adding a bit
//to the width seems to help with the intersection.
if (m_hasArrow)
stroker.setWidth(m_width + 0.1);
else
stroker.setWidth(m_width);
stroker.setCapStyle(Qt::FlatCap);
stroker.setJoinStyle(Qt::RoundJoin);
QPainterPath highlightPath = stroker.createStroke(partialPath);
if (m_hasArrow)
highlightPath = highlightPath.intersected(shape());
return highlightPath;
}
bool GraphicsItemNode::anyNodeDisplayText()
{
return g_settings->displayNodeCustomLabels ||
g_settings->displayNodeNames ||
g_settings->displayNodeLengths ||
g_settings->displayNodeDepth ||
g_settings->displayNodeCsvData;
}