Commit 1d1c95c6 authored by Dilson Guimarães's avatar Dilson Guimarães Committed by Caio Tonetti
Browse files

User interface for radial tree layout

parent c73d6b71
......@@ -45,6 +45,10 @@ GraphLayoutWidget::GraphLayoutWidget(GraphDocumentPtr document, QWidget *parent)
, m_areaFactor(50)
, m_repellingForce(50)
, m_attractionForce(50)
, m_currentTabIndex(0)
, m_root(-1)
, m_nodeSeparation(50)
, m_treeType(TreeType::Free)
{
setWindowTitle(i18nc("@title:window", "Graph Layout"));
......@@ -56,18 +60,40 @@ GraphLayoutWidget::GraphLayoutWidget(GraphDocumentPtr document, QWidget *parent)
ui->setupUi(widget);
mainLayout->addWidget(widget);
connect(ui->tabs, &QTabWidget::currentChanged, this, &GraphLayoutWidget::setCurrentTabIndex);
connect(ui->mainButtons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->mainButtons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(this, &QDialog::accepted, this, &GraphLayoutWidget::layoutGraph);
connect(ui->areaFactorSlider, &QSlider::valueChanged, this, &GraphLayoutWidget::setAreaFactor);
connect(ui->repellingForceSlider, &QSlider::valueChanged, this, &GraphLayoutWidget::setRepellingForce);
connect(ui->attractionForceSlider, &QSlider::valueChanged, this, &GraphLayoutWidget::setAttractionForce);
connect(ui->repellingForceSlider, &QSlider::valueChanged, this,
&GraphLayoutWidget::setRepellingForce);
connect(ui->attractionForceSlider, &QSlider::valueChanged, this,
&GraphLayoutWidget::setAttractionForce);
connect(ui->nodeSeparationSlider, &QSlider::valueChanged, this,
&GraphLayoutWidget::setNodeSeparation);
connect(ui->rootComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&GraphLayoutWidget::setRoot);
connect(ui->freeTreeRadioButton, &QRadioButton::toggled, this,
&GraphLayoutWidget::freeTreeTypeToggle);
connect(ui->rootedTreeRadioButton, &QRadioButton::toggled, this,
&GraphLayoutWidget::rootedTreeTypeToggle);
//Adds items to center/root combo box in the radial layout tab
ui->rootComboBox->addItem("Automatic selection", QVariant(-1));
for (const auto nodePtr : document->nodes()) {
const int nodeId = nodePtr->id();
ui->rootComboBox->addItem(QString::number(nodeId), QVariant(nodeId));
}
// default values
ui->tabs->setCurrentIndex(m_currentTabIndex);
ui->areaFactorSlider->setValue(50);
ui->repellingForceSlider->setValue(50);
ui->attractionForceSlider->setValue(50);
ui->nodeSeparationSlider->setValue(50);
}
......@@ -89,9 +115,27 @@ void GraphLayoutWidget::setSeed(int seed)
m_seed = seed;
}
void GraphLayoutWidget::setCurrentTabIndex(const int index)
{
m_currentTabIndex = index;
}
void GraphLayoutWidget::layoutGraph()
{
const QString currentTabName = getCurrentTabName();
if (currentTabName == "forceBasedLayoutTab") {
handleForceBasedLayout();
} else if (currentTabName == "radialTreeLayoutTab") {
handleRadialTreeLayout();
}
close();
deleteLater();
}
void GraphLayoutWidget::handleForceBasedLayout()
{
//Slider values map to parameters with a non-linear scale
const qreal areaFactor = qPow(10, qreal(m_areaFactor - 50) / 50);
const qreal repellingForce = qPow(10, qreal(m_repellingForce - 50) / 50);
......@@ -109,9 +153,68 @@ void GraphLayoutWidget::layoutGraph()
Topology::applyForceBasedLayout(m_document, nodeRadius, margin, areaFactor, repellingForce,
attractionForce, randomizeInitialPositions, seed);
}
close();
deleteLater();
void GraphLayoutWidget::handleRadialTreeLayout()
{
//TODO: Check if the graph is a tree.
//Finds the root node. In case of automatic selection, a null pointer is used.
NodePtr root = nullptr;
for (const NodePtr node : m_document->nodes()) {
if (node->id() == m_root) {
root = node;
break;
}
}
const qreal nodeRadius = 10.;
const qreal margin = 5.;
const qreal nodeSeparation = m_nodeSeparation;
if (m_treeType == TreeType::Free) {
const qreal wedgeAngle = 2. * M_PI;
const qreal rotationAngle = 0.;
Topology::applyRadialLayoutToTree(m_document, nodeRadius, margin, nodeSeparation, root,
wedgeAngle, rotationAngle);
} else {
const qreal wedgeAngle = M_PI / 2.;
const qreal rotationAngle = (M_PI - wedgeAngle) / 2.;
Topology::applyRadialLayoutToTree(m_document, nodeRadius, margin, nodeSeparation, root,
wedgeAngle, rotationAngle);
}
}
void GraphLayoutWidget::setNodeSeparation(const int nodeSeparation)
{
m_nodeSeparation = nodeSeparation;
}
void GraphLayoutWidget::setRoot(const int index)
{
m_root = ui->rootComboBox->itemData(index).toInt();
}
void GraphLayoutWidget::freeTreeTypeToggle(const bool checked)
{
if (checked) {
m_treeType = TreeType::Free;
}
}
void GraphLayoutWidget::rootedTreeTypeToggle(const bool checked)
{
if (checked) {
m_treeType = TreeType::Rooted;
}
}
QString GraphLayoutWidget::getCurrentTabName() const
{
const QWidget* currentTab = ui->tabs->widget(m_currentTabIndex);
return currentTab->objectName();
}
GraphLayoutWidget::~GraphLayoutWidget()
......
......@@ -60,15 +60,57 @@ public slots:
*/
void setAttractionForce(int attractionForce);
/**
* Updates the index of the current tab.
*/
void setCurrentTabIndex(const int index);
/**
* Updates the root parameter for the radial tree layout.
*/
void setRoot(const int index);
/**
* Updates the node separation parameter for the radial tree layout.
*/
void setNodeSeparation(const int nodeSeparation);
/**
* Updates the type of tree for the radial tree layout algorithm.
*/
void freeTreeTypeToggle(const bool checked);
void rootedTreeTypeToggle(const bool checked);
private:
enum class TreeType {Free, Rooted};
GraphDocumentPtr m_document;
int m_seed;
int m_areaFactor;
int m_repellingForce;
int m_attractionForce;
int m_currentTabIndex;
int m_root;
TreeType m_treeType;
int m_nodeSeparation;
Ui::GraphLayoutWidget *ui;
/**
* Returns the name of the current tab.
*/
QString getCurrentTabName() const;
/**
* Handles the application of the force-based layout.
*/
void handleForceBasedLayout();
/**
* Handles the application of the radial tree layout.
*/
void handleRadialTreeLayout();
};
}
......
......@@ -6,24 +6,185 @@
<rect>
<x>0</x>
<y>0</y>
<width>306</width>
<height>161</height>
<width>374</width>
<height>264</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>306</width>
<height>161</height>
<width>374</width>
<height>264</height>
</size>
</property>
<property name="windowTitle">
<string>Graph Layout</string>
</property>
<widget class="QTabWidget" name="tabs">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>351</width>
<height>191</height>
</rect>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="forceBasedLayoutTab">
<attribute name="title">
<string>Force Based Layout</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
<rect>
<x>40</x>
<y>30</y>
<width>271</width>
<height>81</height>
</rect>
</property>
<layout class="QFormLayout" name="forceBasedLayoutTabFormLayout">
<property name="horizontalSpacing">
<number>16</number>
</property>
<property name="verticalSpacing">
<number>16</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="areaFactorLabel">
<property name="text">
<string>Area factor:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSlider" name="areaFactorSlider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="repellingForceLabel">
<property name="text">
<string>Repelling force:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="repellingForceSlider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="attractionForceLabel">
<property name="text">
<string>Attraction force:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSlider" name="attractionForceSlider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="radialTreeLayoutTab">
<attribute name="title">
<string>Radial Tree Layout</string>
</attribute>
<widget class="QWidget" name="formLayoutWidget_2">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>301</width>
<height>131</height>
</rect>
</property>
<layout class="QFormLayout" name="radialTreeLayoutTabFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="rootLabel">
<property name="text">
<string>Center/Root:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="rootComboBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="nodeSeparationLabel">
<property name="text">
<string>Node separation:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="nodeSeparationSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="treeTypeLabel">
<property name="text">
<string>Tree type:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QRadioButton" name="freeTreeRadioButton">
<property name="text">
<string>Free tree</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QRadioButton" name="rootedTreeRadioButton">
<property name="text">
<string>Rooted tree</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
<widget class="QDialogButtonBox" name="mainButtons">
<property name="geometry">
<rect>
<x>120</x>
<y>120</y>
<x>190</x>
<y>220</y>
<width>171</width>
<height>32</height>
</rect>
......@@ -41,84 +202,6 @@
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>281</width>
<height>91</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="horizontalSpacing">
<number>16</number>
</property>
<property name="verticalSpacing">
<number>16</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="areaFactorLabel">
<property name="text">
<string>Area factor:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSlider" name="areaFactorSlider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="repellingForceLabel">
<property name="text">
<string>Repelling force:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="attractionForceLabel">
<property name="text">
<string>Attraction force:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="repellingForceSlider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSlider" name="attractionForceSlider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
......
......@@ -846,22 +846,22 @@ int findTreeCenter(const RemappedGraph& graph) {
* @param minY Minimum y-coordinate that can be assigned to a node.
* @param nodeRadius The radius of the circles used to draw nodes.
* @param nodeSeparation A lower bound on the distance between two nodes.
* @param root Root node of the tree.
* @param initialWedgeAngle Angle of the wedge used for the root.
* @param initialRotationAngle Rotation angle for the root.
*/
QVector<QPointF> radialLayout(const RemappedGraph& graph, const qreal minX, const qreal minY,
const qreal nodeRadius, const qreal nodeSeparation)
const qreal nodeRadius, const qreal nodeSeparation, const int root,
const qreal initialWedgeAngle, const qreal initialRotationAngle)
{
const qreal initialWedgeAngle = 2. * M_PI;
const int root = findTreeCenter(graph);
QVector<bool> visited(graph.numberOfNodes);
QVector<int> numberOfLeafs(graph.numberOfNodes);
calculateNumberOfLeafs(graph, root, visited, numberOfLeafs);
visited.fill(false);
QVector<QPointF> positions(graph.numberOfNodes);
radialLayoutHelper(graph, numberOfLeafs, nodeRadius, initialWedgeAngle, 0., 0., nodeSeparation,
root, visited, positions);
radialLayoutHelper(graph, numberOfLeafs, nodeRadius, initialWedgeAngle, initialRotationAngle,
0., nodeSeparation, root, visited, positions);
translateGraphToUpperLeftCorner(minX, qInf(), minY, qInf(), positions);
......@@ -871,7 +871,9 @@ QVector<QPointF> radialLayout(const RemappedGraph& graph, const qreal minX, cons
void Topology::applyRadialLayoutToTree(GraphDocumentPtr document, const qreal nodeRadius,
const qreal margin, const qreal nodeSeparation)
const qreal margin, const qreal nodeSeparation,
const NodePtr root, const qreal wedgeAngle,
const qreal rotationAngle)
{
//There is nothing to do with an empty graph.
if (document->nodes().empty()) {
......@@ -879,12 +881,19 @@ void Topology::applyRadialLayoutToTree(GraphDocumentPtr document, const qreal no
}
const RemappedGraph graph = remapGraph(document);
int rootIndex = 0;
if (root == nullptr) {
rootIndex = findTreeCenter(graph);
} else {
rootIndex = graph.nodeToIndexMap[root];
}
const qreal minX = nodeRadius + margin;
const qreal minY = nodeRadius + margin;
QVector<QPointF> positions = radialLayout(graph, minX, minY, nodeRadius, nodeSeparation);
QVector<QPointF> positions = radialLayout(graph, minX, minY, nodeRadius, nodeSeparation,
rootIndex, wedgeAngle, rotationAngle);
moveNodes(document->nodes(), graph.nodeToIndexMap, positions);
}
......@@ -111,15 +111,21 @@ public:
/**
* Aplies a radial layout to a tree.
* Applies a radial layout to a tree.
*
* @param document The graph document to be laid out. This document should represented a tree.
* @param document The graph document to be laid out. This document should represent a tree.
* @param nodeRadius The radius of the circles that are used to represent nodes.
* @param margin The size of the top and left margins.
* @param nodeSeparation The minimum distance between two nodes.
* @param root Node to be used as root. Use nullptr to indicate that the root should be selected
* automatically.
* @param wedgeAngle Angle of the wedge into which the nodes should be placed.
* @param rotation Angle to rotate all nodes around the root.
*/
static void applyRadialLayoutToTree(GraphDocumentPtr document, const qreal nodeRadius,
const qreal margin, const qreal nodeSeparation);
const qreal margin, const qreal nodeSeparation,
const NodePtr root, const qreal wedgeAngle,
const qreal rotationAngle);
};
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment