diff src/grapher.cpp @ 370:b9c153e00e84

Move source files to src/
author Chris Cannam
date Thu, 24 Mar 2011 10:27:51 +0000
parents grapher.cpp@4811eb34e819
children f051d210521e
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/grapher.cpp	Thu Mar 24 10:27:51 2011 +0000
@@ -0,0 +1,603 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    EasyMercurial
+
+    Based on HgExplorer by Jari Korhonen
+    Copyright (c) 2010 Jari Korhonen
+    Copyright (c) 2011 Chris Cannam
+    Copyright (c) 2011 Queen Mary, University of London
+    
+    This program 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 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "grapher.h"
+#include "connectionitem.h"
+#include "dateitem.h"
+#include "debug.h"
+#include "changesetscene.h"
+
+#include <QSettings>
+
+#include <iostream>
+
+Grapher::Grapher(ChangesetScene *scene) :
+    m_scene(scene)
+{
+    QSettings settings;
+    settings.beginGroup("Presentation");
+    m_showDates = (settings.value("dateformat", 0) == 1);
+}
+
+int Grapher::findAvailableColumn(int row, int parent, bool preferParentCol)
+{
+    int col = parent;
+    if (preferParentCol) {
+        if (isAvailable(row, col)) return col;
+    }
+    while (col > 0) {
+        if (isAvailable(row, --col)) return col;
+    }
+    while (col < 0) {
+        if (isAvailable(row, ++col)) return col;
+    }
+    col = parent;
+    int sign = (col < 0 ? -1 : 1);
+    while (1) {
+        col += sign;
+        if (isAvailable(row, col)) return col;
+    }
+}
+
+bool Grapher::isAvailable(int row, int col)
+{
+    if (m_alloc.contains(row) && m_alloc[row].contains(col)) return false;
+    if (!m_haveAllocatedUncommittedColumn) return true;
+    if (!m_uncommitted) return true;
+    return !(row <= m_uncommittedParentRow && col == m_uncommitted->column());
+}
+
+void Grapher::layoutRow(QString id)
+{
+    if (m_handled.contains(id)) {
+        return;
+    }
+    if (!m_changesets.contains(id)) {
+        throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
+    }
+    if (!m_items.contains(id)) {
+        throw LayoutException(QString("Changeset %1 not in item map").arg(id));
+    }
+    Changeset *cs = m_changesets[id];
+    ChangesetItem *item = m_items[id];
+    DEBUG << "layoutRow: Looking at " << id.toStdString() << endl;
+
+    int row = 0;
+    int nparents = cs->parents().size();
+
+    if (nparents > 0) {
+        bool haveRow = false;
+        foreach (QString parentId, cs->parents()) {
+
+            if (!m_changesets.contains(parentId)) continue;
+            if (!m_items.contains(parentId)) continue;
+
+            if (!m_handled.contains(parentId)) {
+                layoutRow(parentId);
+            }
+
+            ChangesetItem *parentItem = m_items[parentId];
+            if (!haveRow || parentItem->row() < row) {
+                row = parentItem->row();
+                haveRow = true;
+            }
+        }
+        row = row - 1;
+    }
+
+    // row is now an upper bound on our eventual row (because we want
+    // to be above all parents).  But we also want to ensure we are
+    // above all nodes that have earlier dates (to the nearest day).
+    // m_rowDates maps each row to a date: use that.
+
+    QString date;
+    if (m_showDates) {
+        date = cs->date();
+    } else {
+        date = cs->age();
+    }
+    while (m_rowDates.contains(row) && m_rowDates[row] != date) {
+        --row;
+    }
+
+    // We have already laid out all nodes that have earlier timestamps
+    // than this one, so we know (among other things) that we can
+    // safely fill in this row has having this date, if it isn't in
+    // the map yet (it cannot have an earlier date)
+
+    if (!m_rowDates.contains(row)) {
+        m_rowDates[row] = date;
+    }
+
+    // If we're the parent of the uncommitted item, make a note of our
+    // row (we need it later, to avoid overwriting the connecting line)
+    if (!m_uncommittedParents.empty() && m_uncommittedParents[0] == id) {
+        m_uncommittedParentRow = row;
+    }
+
+    DEBUG << "putting " << cs->id().toStdString() << " at row " << row 
+          << endl;
+
+    item->setRow(row);
+    m_handled.insert(id);
+}
+
+void Grapher::layoutCol(QString id)
+{
+    if (m_handled.contains(id)) {
+        DEBUG << "Already looked at " << id.toStdString() << endl;
+        return;
+    }
+    if (!m_changesets.contains(id)) {
+        throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
+    }
+    if (!m_items.contains(id)) {
+        throw LayoutException(QString("Changeset %1 not in item map").arg(id));
+    }
+
+    Changeset *cs = m_changesets[id];
+    DEBUG << "layoutCol: Looking at " << id.toStdString() << endl;
+
+    ChangesetItem *item = m_items[id];
+
+    int col = 0;
+    int row = item->row();
+    QString branch = cs->branch();
+
+    int nparents = cs->parents().size();
+    QString parentId;
+    int parentsOnSameBranch = 0;
+
+    switch (nparents) {
+
+    case 0:
+        col = m_branchHomes[cs->branch()];
+        col = findAvailableColumn(row, col, true);
+        break;
+
+    case 1:
+        parentId = cs->parents()[0];
+
+        if (!m_changesets.contains(parentId) ||
+            !m_changesets[parentId]->isOnBranch(branch)) {
+            // new branch
+            col = m_branchHomes[branch];
+        } else {
+            col = m_items[parentId]->column();
+        }
+
+        col = findAvailableColumn(row, col, true);
+        break;
+
+    case 2:
+        // a merge: behave differently if parents are both on the same
+        // branch (we also want to behave differently for nodes that
+        // have multiple children on the same branch -- spreading them
+        // out rather than having affinity to a specific branch)
+
+        foreach (QString parentId, cs->parents()) {
+            if (!m_changesets.contains(parentId)) continue;
+            if (m_changesets[parentId]->isOnBranch(branch)) {
+                ChangesetItem *parentItem = m_items[parentId];
+                col += parentItem->column();
+                parentsOnSameBranch++;
+            }
+        }
+
+        if (parentsOnSameBranch > 0) {
+            col /= parentsOnSameBranch;
+            col = findAvailableColumn(item->row(), col, true);
+        } else {
+            col = findAvailableColumn(item->row(), col, false);
+        }
+        break;
+    }
+
+    DEBUG << "putting " << cs->id().toStdString() << " at col " << col << endl;
+
+    m_alloc[row].insert(col);
+    item->setColumn(col);
+    m_handled.insert(id);
+
+    // If we're the first parent of the uncommitted item, it should be
+    // given the same column as us (we already noted that its
+    // connecting line would end at our row)
+
+    if (m_uncommittedParents.contains(id)) {
+        if (m_uncommittedParents[0] == id) {
+            int ucol = findAvailableColumn(row-1, col, true);
+            m_uncommitted->setColumn(ucol);
+            m_haveAllocatedUncommittedColumn = true;
+        }
+        // also, if the uncommitted item has a different branch from
+        // any of its parents, tell it to show the branch
+        if (!cs->isOnBranch(m_uncommitted->branch())) {
+            DEBUG << "Uncommitted branch " << m_uncommitted->branch()
+                  << " differs from my branch " << cs->branch()
+                  << ", asking it to show branch" << endl;
+            m_uncommitted->setShowBranch(true);
+        }
+    }
+
+
+    // Normally the children will lay out themselves, but we can do
+    // a better job in some special cases:
+
+    int nchildren = cs->children().size();
+
+    // look for merging children and children distant from us but in a
+    // straight line, and make sure nobody is going to overwrite their
+    // connection lines
+
+    foreach (QString childId, cs->children()) {
+        DEBUG << "reserving connection line space" << endl;
+        if (!m_changesets.contains(childId)) continue;
+        Changeset *child = m_changesets[childId];
+        int childRow = m_items[childId]->row();
+        if (child->parents().size() > 1 ||
+            child->isOnBranch(cs->branch())) {
+            for (int r = row-1; r > childRow; --r) {
+                m_alloc[r].insert(col);
+            }
+        }
+    }
+
+    // look for the case where exactly two children have the same
+    // branch as us: split them to a little either side of our position
+
+    if (nchildren > 1) {
+        QList<QString> special;
+        foreach (QString childId, cs->children()) {
+            if (!m_changesets.contains(childId)) continue;
+            Changeset *child = m_changesets[childId];
+            if (child->isOnBranch(branch) &&
+                child->parents().size() == 1) {
+                special.push_back(childId);
+            }
+        }
+        if (special.size() == 2) {
+            DEBUG << "handling split-in-two for children " << special[0] << " and " << special[1] << endl;
+            for (int i = 0; i < 2; ++i) {
+                int off = i * 2 - 1; // 0 -> -1, 1 -> 1
+                ChangesetItem *it = m_items[special[i]];
+                it->setColumn(findAvailableColumn(it->row(), col + off, true));
+                for (int r = row-1; r >= it->row(); --r) {
+                    m_alloc[r].insert(it->column());
+                }
+                m_handled.insert(special[i]);
+            }
+        }
+    }
+}
+
+bool Grapher::rangesConflict(const Range &r1, const Range &r2)
+{
+    // allow some additional space at edges.  we really ought also to
+    // permit space at the end of a branch up to the point where the
+    // merge happens
+    int a1 = r1.first - 2, b1 = r1.second + 2;
+    int a2 = r2.first - 2, b2 = r2.second + 2;
+    if (a1 > b2 || b1 < a2) return false;
+    if (a2 > b1 || b2 < a1) return false;
+    return true;
+}
+
+void Grapher::allocateBranchHomes(Changesets csets)
+{
+    foreach (Changeset *cs, csets) {
+        QString branch = cs->branch();
+        ChangesetItem *item = m_items[cs->id()];
+        if (!item) continue;
+        int row = item->row();
+        if (!m_branchRanges.contains(branch)) {
+            m_branchRanges[branch] = Range(row, row);
+        } else {
+            Range p = m_branchRanges[branch];
+            if (row < p.first) p.first = row;
+            if (row > p.second) p.second = row;
+            m_branchRanges[branch] = p;
+        }
+    }
+
+    m_branchHomes[""] = 0;
+    m_branchHomes["default"] = 0;
+
+    foreach (QString branch, m_branchRanges.keys()) {
+        if (branch == "") continue;
+        QSet<int> taken;
+        taken.insert(0);
+        Range myRange = m_branchRanges[branch];
+        foreach (QString other, m_branchRanges.keys()) {
+            if (other == branch || other == "") continue;
+            Range otherRange = m_branchRanges[other];
+            if (rangesConflict(myRange, otherRange)) {
+                if (m_branchHomes.contains(other)) {
+                    taken.insert(m_branchHomes[other]);
+                }
+            }
+        }
+        int home = 2;
+        while (taken.contains(home)) {
+            if (home > 0) {
+                if (home % 2 == 1) {
+                    home = -home;
+                } else {
+                    home = home + 1;
+                }
+            } else {
+                if ((-home) % 2 == 1) {
+                    home = home + 1;
+                } else {
+                    home = -(home-2);
+                }
+            }
+        }
+        m_branchHomes[branch] = home;
+    }
+
+    foreach (QString branch, m_branchRanges.keys()) {
+        DEBUG << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << endl;
+    }
+}
+
+static bool
+compareChangesetsByDate(Changeset *const &a, Changeset *const &b)
+{
+    return a->timestamp() < b->timestamp();
+}
+
+ChangesetItem *
+Grapher::getItemFor(Changeset *cs)
+{
+    if (!cs || !m_items.contains(cs->id())) return 0;
+    return m_items[cs->id()];
+}
+
+void Grapher::layout(Changesets csets,
+                     QStringList uncommittedParents,
+                     QString uncommittedBranch)
+{
+    m_changesets.clear();
+    m_items.clear();
+    m_alloc.clear();
+    m_branchHomes.clear();
+
+    m_uncommittedParents = uncommittedParents;
+    m_haveAllocatedUncommittedColumn = false;
+    m_uncommittedParentRow = 0;
+    m_uncommitted = 0;
+
+    DEBUG << "Grapher::layout: Have " << csets.size() << " changesets" << endl;
+
+    if (csets.empty()) return;
+
+    // Create (but don't yet position) the changeset items
+
+    foreach (Changeset *cs, csets) {
+
+        QString id = cs->id();
+
+        if (id == "") {
+            throw LayoutException("Changeset has no ID");
+        }
+        if (m_changesets.contains(id)) {
+            DEBUG << "Duplicate changeset ID " << id
+                  << " in Grapher::layout()" << endl;
+            throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
+        }
+
+        m_changesets[id] = cs;
+
+        ChangesetItem *item = new ChangesetItem(cs);
+        item->setX(0);
+        item->setY(0);
+        item->setZValue(0);
+        m_items[id] = item;
+        m_scene->addChangesetItem(item);
+    }
+
+    // Add the connecting lines
+
+    foreach (Changeset *cs, csets) {
+        QString id = cs->id();
+        ChangesetItem *item = m_items[id];
+        bool merge = (cs->parents().size() > 1);
+        foreach (QString parentId, cs->parents()) {
+            if (!m_changesets.contains(parentId)) continue;
+            Changeset *parent = m_changesets[parentId];
+            parent->addChild(id);
+            ConnectionItem *conn = new ConnectionItem();
+            if (merge) conn->setConnectionType(ConnectionItem::Merge);
+            conn->setChild(item);
+            conn->setParent(m_items[parentId]);
+            conn->setZValue(-1);
+            m_scene->addItem(conn);
+        }
+    }
+    
+    // Add uncommitted item and connecting line as necessary
+
+    if (!m_uncommittedParents.empty()) {
+
+        m_uncommitted = new UncommittedItem();
+        m_uncommitted->setBranch(uncommittedBranch);
+        m_uncommitted->setZValue(10);
+        m_scene->addUncommittedItem(m_uncommitted);
+
+        bool haveParentOnBranch = false;
+        foreach (QString p, m_uncommittedParents) {
+            ConnectionItem *conn = new ConnectionItem();
+            conn->setConnectionType(ConnectionItem::Merge);
+            ChangesetItem *pitem = m_items[p];
+            conn->setParent(pitem);
+            conn->setChild(m_uncommitted);
+            conn->setZValue(0);
+            m_scene->addItem(conn);
+            if (pitem) {
+                if (pitem->getChangeset()->branch() == uncommittedBranch) {
+                    haveParentOnBranch = true;
+                }
+            }
+        }
+
+        // If the uncommitted item has no parents on the same branch,
+        // tell it it has a new branch (the "show branch" flag is set
+        // elsewhere for this item)
+        m_uncommitted->setIsNewBranch(!haveParentOnBranch);
+    }
+
+    // Add the branch labels
+
+    foreach (Changeset *cs, csets) {
+        QString id = cs->id();
+        ChangesetItem *item = m_items[id];
+        bool haveChildOnSameBranch = false;
+        foreach (QString childId, cs->children()) {
+            Changeset *child = m_changesets[childId];
+            if (child->branch() == cs->branch()) {
+                haveChildOnSameBranch = true;
+                break;
+            }
+        }
+        item->setShowBranch(!haveChildOnSameBranch);
+    }
+
+    // We need to lay out the changesets in forward chronological
+    // order.  We have no guarantees about the order in which
+    // changesets appear in the list -- in a simple repository they
+    // will generally be reverse chronological, but that's far from
+    // guaranteed.  So, sort explicitly using the date comparator
+    // above
+
+    qStableSort(csets.begin(), csets.end(), compareChangesetsByDate);
+
+    foreach (Changeset *cs, csets) {
+        DEBUG << "id " << cs->id().toStdString() << ", ts " << cs->timestamp()
+              << ", date " << cs->datetime().toStdString() << endl;
+    }
+
+    m_handled.clear();
+    foreach (Changeset *cs, csets) {
+        layoutRow(cs->id());
+    }
+
+    allocateBranchHomes(csets);
+
+    m_handled.clear();
+    foreach (Changeset *cs, csets) {
+        foreach (QString parentId, cs->parents()) {
+            if (!m_handled.contains(parentId) &&
+                m_changesets.contains(parentId)) {
+                layoutCol(parentId);
+            }
+        }
+        layoutCol(cs->id());
+    }
+
+    // Find row and column extents.  We know that 0 is an upper bound
+    // on row, and that mincol must be <= 0 and maxcol >= 0, so these
+    // initial values are good
+
+    int minrow = 0, maxrow = 0;
+    int mincol = 0, maxcol = 0;
+
+    foreach (int r, m_alloc.keys()) {
+        if (r < minrow) minrow = r;
+        if (r > maxrow) maxrow = r;
+        ColumnSet &c = m_alloc[r];
+        foreach (int i, c) {
+            if (i < mincol) mincol = i;
+            if (i > maxcol) maxcol = i;
+        }
+    }
+
+    int datemincol = mincol, datemaxcol = maxcol;
+
+    if (mincol == maxcol) {
+        --datemincol;
+        ++datemaxcol;
+    } else if (m_alloc[minrow].contains(mincol)) {
+        --datemincol;
+    }
+
+    // We've given the uncommitted item a column, but not a row yet --
+    // it always goes at the top
+
+    if (m_uncommitted) {
+        --minrow;
+        DEBUG << "putting uncommitted item at row " << minrow << endl;
+        m_uncommitted->setRow(minrow);
+    }
+
+    // Changeset items that have nothing to either side of them can be
+    // made double-width
+
+    foreach (Changeset *cs, csets) {
+        ChangesetItem *item = m_items[cs->id()];
+        if (isAvailable(item->row(), item->column()-1) &&
+            isAvailable(item->row(), item->column()+1)) {
+            item->setWide(true);
+        }
+    }
+
+    if (m_uncommitted) {
+        if (isAvailable(m_uncommitted->row(), m_uncommitted->column()-1) &&
+            isAvailable(m_uncommitted->row(), m_uncommitted->column()+1)) {
+            m_uncommitted->setWide(true);
+        }
+    }
+
+    QString prevDate;
+    int changeRow = 0;
+
+    bool even = false;
+    int n = 0;
+
+    for (int row = minrow; row <= maxrow; ++row) {
+
+        QString date = m_rowDates[row];
+        n++;
+
+        if (date != prevDate) {
+            if (prevDate != "") {
+                DateItem *item = new DateItem();
+                item->setDateString(prevDate);
+                item->setCols(datemincol, datemaxcol - datemincol + 1);
+                item->setRows(changeRow, n);
+                item->setEven(even);
+                item->setZValue(-2);
+                m_scene->addDateItem(item);
+                even = !even;
+            }
+            prevDate = date;
+            changeRow = row;
+            n = 0;
+        }
+    }
+    
+    if (n > 0) {
+        DateItem *item = new DateItem();
+        item->setDateString(prevDate);
+        item->setCols(datemincol, datemaxcol - datemincol + 1);
+        item->setRows(changeRow, n+1);
+        item->setEven(even);
+        item->setZValue(-2);
+        m_scene->addDateItem(item);
+        even = !even;
+    }
+}
+