annotate grapher.cpp @ 52:384420567575

* Use the date when laying out rows
author Chris Cannam
date Fri, 12 Nov 2010 11:32:01 +0000
parents bf3ab0ffb559
children 3c46b2ac45d3
rev   line source
Chris@44 1
Chris@44 2 #include "grapher.h"
Chris@46 3 #include "connectionitem.h"
Chris@44 4
cannam@45 5 #include <QGraphicsScene>
Chris@44 6
Chris@44 7 #include <iostream>
Chris@44 8
cannam@45 9 int
cannam@45 10 Grapher::findAvailableColumn(int row, int parent, bool preferParentCol)
cannam@45 11 {
cannam@45 12 int col = parent;
cannam@45 13 if (preferParentCol) {
cannam@45 14 if (!m_alloc[row].contains(col)) {
cannam@45 15 return col;
cannam@45 16 }
cannam@45 17 }
cannam@45 18 while (col > 0) {
cannam@45 19 if (!m_alloc[row].contains(--col)) return col;
cannam@45 20 }
cannam@45 21 while (col < 0) {
cannam@45 22 if (!m_alloc[row].contains(++col)) return col;
cannam@45 23 }
cannam@45 24 col = parent;
cannam@45 25 int sign = (col < 0 ? -1 : 1);
cannam@45 26 while (1) {
cannam@45 27 col += sign;
cannam@45 28 if (!m_alloc[row].contains(col)) return col;
cannam@45 29 }
cannam@45 30 }
Chris@44 31
cannam@45 32 void
cannam@45 33 Grapher::layoutRow(QString id)
Chris@44 34 {
cannam@45 35 if (m_handled.contains(id)) {
cannam@45 36 return;
Chris@44 37 }
Chris@46 38 if (!m_changesets.contains(id)) {
cannam@45 39 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
Chris@44 40 }
cannam@45 41 if (!m_items.contains(id)) {
cannam@45 42 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
Chris@44 43 }
Chris@46 44 Changeset *cs = m_changesets[id];
cannam@45 45 ChangesetItem *item = m_items[id];
cannam@45 46 std::cerr << "Looking at " << id.toStdString() << std::endl;
cannam@45 47
Chris@44 48 int row = 0;
cannam@45 49 int nparents = cs->parents().size();
cannam@45 50
cannam@45 51 if (nparents > 0) {
Chris@44 52 bool haveRow = false;
Chris@44 53 foreach (QString parentId, cs->parents()) {
cannam@45 54
Chris@46 55 if (!m_changesets.contains(parentId)) continue;
cannam@45 56 if (!m_items.contains(parentId)) continue;
cannam@45 57
cannam@45 58 if (!m_handled.contains(parentId)) {
cannam@45 59 layoutRow(parentId);
cannam@45 60 }
cannam@45 61
cannam@45 62 ChangesetItem *parentItem = m_items[parentId];
Chris@44 63 if (!haveRow || parentItem->row() < row) {
Chris@44 64 row = parentItem->row();
Chris@44 65 haveRow = true;
Chris@44 66 }
Chris@44 67 }
Chris@44 68 row = row - 1;
cannam@45 69 }
cannam@45 70
Chris@52 71 // row is now an upper bound on our eventual row (because we want
Chris@52 72 // to be above all parents). But we also want to ensure we are
Chris@52 73 // above all nodes that have earlier dates (to the nearest day).
Chris@52 74 // m_rowDates maps each row to a date: use that.
Chris@52 75
Chris@52 76 QString date = cs->date();
Chris@52 77
Chris@52 78 // n.b. this relies on the fact that the date component of an ISO
Chris@52 79 // date/time sorts correctly in a dictionary sort
Chris@52 80 while (m_rowDates.contains(row) && m_rowDates[row] < date) {
Chris@52 81 --row;
Chris@52 82 }
Chris@52 83
Chris@52 84 // We have already laid out all nodes that have earlier timestamps
Chris@52 85 // than this one, so we know (among other things) that we can
Chris@52 86 // safely fill in this row has having this date, if it isn't in
Chris@52 87 // the map yet (it cannot have an earlier date)
Chris@52 88
Chris@52 89 if (!m_rowDates.contains(row)) {
Chris@52 90 m_rowDates[row] = date;
Chris@52 91 }
Chris@52 92
cannam@45 93 std::cerr << "putting " << cs->id().toStdString() << " at row " << row
cannam@45 94 << std::endl;
cannam@45 95
Chris@44 96 item->setRow(row);
cannam@45 97 m_handled.insert(id);
Chris@44 98 }
Chris@44 99
Chris@44 100 void
cannam@45 101 Grapher::layoutCol(QString id)
Chris@44 102 {
cannam@45 103 if (m_handled.contains(id)) {
cannam@45 104 std::cerr << "Already looked at " << id.toStdString() << std::endl;
cannam@45 105 return;
cannam@45 106 }
Chris@46 107 if (!m_changesets.contains(id)) {
cannam@45 108 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
cannam@45 109 }
cannam@45 110 if (!m_items.contains(id)) {
cannam@45 111 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
cannam@45 112 }
Chris@46 113 Changeset *cs = m_changesets[id];
cannam@45 114 ChangesetItem *item = m_items[id];
cannam@45 115 std::cerr << "Looking at " << id.toStdString() << std::endl;
cannam@45 116
Chris@46 117 foreach (QString parentId, cs->parents()) {
Chris@46 118 if (!m_changesets.contains(parentId)) continue;
Chris@46 119 if (!m_handled.contains(parentId)) {
Chris@46 120 layoutCol(parentId);
Chris@46 121 }
Chris@46 122 }
Chris@46 123
Chris@47 124 // Parent may have layed out child in the recursive call
Chris@47 125 if (m_handled.contains(id)) {
Chris@47 126 std::cerr << "Looks like we've dealt with " << id.toStdString() << std::endl;
Chris@47 127 return;
Chris@47 128 }
Chris@47 129
cannam@45 130 int col = 0;
Chris@46 131 int row = item->row();
Chris@46 132 QString branch = cs->branch();
Chris@47 133
cannam@45 134 int nparents = cs->parents().size();
Chris@46 135 QString parentId;
Chris@46 136 int parentsOnSameBranch = 0;
cannam@45 137
Chris@46 138 switch (nparents) {
cannam@45 139
Chris@46 140 case 0:
Chris@46 141 col = m_branchHomes[cs->branch()];
Chris@46 142 col = findAvailableColumn(row, col, true);
Chris@46 143 break;
cannam@45 144
Chris@46 145 case 1:
Chris@46 146 parentId = cs->parents()[0];
cannam@45 147
Chris@46 148 if (!m_changesets.contains(parentId) ||
Chris@46 149 m_changesets[parentId]->branch() != branch) {
Chris@46 150 // new branch
Chris@46 151 col = m_branchHomes[branch];
Chris@46 152 } else {
Chris@46 153 col = m_items[parentId]->column();
Chris@44 154 }
cannam@45 155
Chris@46 156 col = findAvailableColumn(row, col, true);
Chris@46 157 break;
Chris@46 158
Chris@46 159 case 2:
Chris@46 160 // a merge: behave differently if parents are both on the same
Chris@46 161 // branch (we also want to behave differently for nodes that
Chris@46 162 // have multiple children on the same branch -- spreading them
Chris@46 163 // out rather than having affinity to a specific branch)
Chris@46 164
Chris@46 165 foreach (QString parentId, cs->parents()) {
Chris@46 166 if (!m_changesets.contains(parentId)) continue;
Chris@46 167 if (m_changesets[parentId]->branch() == branch) {
Chris@46 168 ChangesetItem *parentItem = m_items[parentId];
Chris@46 169 col += parentItem->column();
Chris@46 170 parentsOnSameBranch++;
Chris@46 171 }
Chris@46 172 }
Chris@46 173
Chris@46 174 if (parentsOnSameBranch > 0) {
Chris@46 175 col /= parentsOnSameBranch;
Chris@46 176 col = findAvailableColumn(item->row(), col, true);
Chris@46 177 } else {
Chris@46 178 col = findAvailableColumn(item->row(), col, false);
Chris@46 179 }
Chris@46 180 break;
Chris@44 181 }
Chris@44 182
cannam@45 183 std::cerr << "putting " << cs->id().toStdString() << " at col " << col << std::endl;
cannam@45 184
Chris@46 185 m_alloc[row].insert(col);
cannam@45 186 item->setColumn(col);
cannam@45 187 m_handled.insert(id);
Chris@47 188
Chris@51 189 // Normally the children will lay out themselves, but we can do
Chris@51 190 // a better job in some special cases:
Chris@51 191
Chris@47 192 int nchildren = cs->children().size();
Chris@49 193
Chris@49 194 // look for merging children and make sure nobody
Chris@51 195 // is going to overwrite their "merge lines" if they extend further
Chris@51 196 // than a single step
Chris@51 197
Chris@49 198 foreach (QString childId, cs->children()) {
Chris@49 199 if (!m_changesets.contains(childId)) continue;
Chris@49 200 Changeset *child = m_changesets[childId];
Chris@49 201 if (child->parents().size() > 1) {
Chris@49 202 int childRow = m_items[childId]->row();
Chris@51 203 for (int r = row; r > childRow; --r) {
Chris@49 204 m_alloc[r].insert(col);
Chris@49 205 }
Chris@49 206 }
Chris@49 207 }
Chris@49 208
Chris@51 209 // look for the case where exactly two children have the same
Chris@51 210 // branch as us: split them to a little either side of our position
Chris@51 211
Chris@47 212 if (nchildren > 1) {
Chris@51 213
Chris@47 214 QList<QString> special;
Chris@47 215 foreach (QString childId, cs->children()) {
Chris@47 216 if (!m_changesets.contains(childId)) continue;
Chris@47 217 Changeset *child = m_changesets[childId];
Chris@47 218 if (child->branch() == branch &&
Chris@47 219 child->parents().size() == 1) {
Chris@47 220 special.push_back(childId);
Chris@47 221 }
Chris@47 222 }
Chris@47 223 if (special.size() == 2) {
Chris@52 224 for (int i = 0; i < 2; ++i) {
Chris@52 225 int off = i * 2 - 1; // 0 -> -1, 1 -> 1
Chris@52 226 ChangesetItem *it = m_items[special[i]];
Chris@52 227 it->setColumn(findAvailableColumn(it->row(), col + off, true));
Chris@52 228 m_alloc[it->row()].insert(it->column());
Chris@52 229 m_handled.insert(special[i]);
Chris@52 230 }
Chris@47 231 }
Chris@47 232 }
cannam@45 233 }
cannam@45 234
Chris@46 235 bool
Chris@46 236 Grapher::rangesConflict(const Range &r1, const Range &r2)
Chris@46 237 {
Chris@46 238 // allow some additional space at edges. we really ought also to
Chris@46 239 // permit space at the end of a branch up to the point where the
Chris@46 240 // merge happens
Chris@46 241 int a1 = r1.first - 2, b1 = r1.second + 2;
Chris@46 242 int a2 = r2.first - 2, b2 = r2.second + 2;
Chris@46 243 if (a1 > b2 || b1 < a2) return false;
Chris@46 244 if (a2 > b1 || b2 < a1) return false;
Chris@46 245 return true;
Chris@46 246 }
Chris@46 247
Chris@46 248 void
Chris@46 249 Grapher::allocateBranchHomes(Changesets csets)
Chris@46 250 {
Chris@46 251 foreach (Changeset *cs, csets) {
Chris@46 252 QString branch = cs->branch();
Chris@46 253 ChangesetItem *item = m_items[cs->id()];
Chris@46 254 if (!item) continue;
Chris@46 255 int row = item->row();
Chris@46 256 if (!m_branchRanges.contains(branch)) {
Chris@46 257 m_branchRanges[branch] = Range(row, row);
Chris@46 258 } else {
Chris@46 259 Range p = m_branchRanges[branch];
Chris@46 260 if (row < p.first) p.first = row;
Chris@46 261 if (row > p.second) p.second = row;
Chris@46 262 m_branchRanges[branch] = p;
Chris@46 263 }
Chris@46 264 }
Chris@46 265
Chris@46 266 m_branchHomes[""] = 0;
Chris@46 267
Chris@46 268 foreach (QString branch, m_branchRanges.keys()) {
Chris@46 269 if (branch == "") continue;
Chris@46 270 QSet<int> taken;
Chris@46 271 taken.insert(0);
Chris@46 272 Range myRange = m_branchRanges[branch];
Chris@46 273 foreach (QString other, m_branchRanges.keys()) {
Chris@46 274 if (other == branch || other == "") continue;
Chris@46 275 Range otherRange = m_branchRanges[other];
Chris@46 276 if (rangesConflict(myRange, otherRange)) {
Chris@46 277 if (m_branchHomes.contains(other)) {
Chris@46 278 taken.insert(m_branchHomes[other]);
Chris@46 279 }
Chris@46 280 }
Chris@46 281 }
Chris@47 282 int home = 3;
Chris@46 283 while (taken.contains(home)) {
Chris@46 284 if (home > 0) home = -home;
Chris@47 285 else home = -(home-3);
Chris@46 286 }
Chris@46 287 m_branchHomes[branch] = home;
Chris@46 288 }
Chris@46 289
Chris@46 290 foreach (QString branch, m_branchRanges.keys()) {
Chris@46 291 std::cerr << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << std::endl;
Chris@46 292 }
Chris@46 293 }
Chris@46 294
Chris@52 295 static bool
Chris@52 296 compareChangesetsByDate(Changeset *const &a, Changeset *const &b)
Chris@52 297 {
Chris@52 298 return a->timestamp() < b->timestamp();
Chris@52 299 }
Chris@52 300
cannam@45 301 void
cannam@45 302 Grapher::layout(Changesets csets)
cannam@45 303 {
Chris@46 304 m_changesets.clear();
cannam@45 305 m_items.clear();
cannam@45 306 m_alloc.clear();
Chris@46 307 m_branchHomes.clear();
cannam@45 308
Chris@44 309 foreach (Changeset *cs, csets) {
cannam@45 310
cannam@45 311 QString id = cs->id();
cannam@45 312 std::cerr << id.toStdString() << std::endl;
cannam@45 313
cannam@45 314 if (id == "") {
cannam@45 315 throw LayoutException("Changeset has no ID");
cannam@45 316 }
Chris@46 317 if (m_changesets.contains(id)) {
cannam@45 318 throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
cannam@45 319 }
cannam@45 320
Chris@46 321 m_changesets[id] = cs;
cannam@45 322
cannam@45 323 ChangesetItem *item = new ChangesetItem(cs);
cannam@45 324 item->setX(0);
cannam@45 325 item->setY(0);
cannam@45 326 m_items[id] = item;
cannam@45 327 m_scene->addItem(item);
cannam@45 328 }
cannam@45 329
Chris@46 330 foreach (Changeset *cs, csets) {
Chris@46 331 QString id = cs->id();
Chris@46 332 ChangesetItem *item = m_items[id];
Chris@46 333 foreach (QString parentId, cs->parents()) {
Chris@47 334 if (!m_changesets.contains(parentId)) continue;
Chris@47 335 Changeset *parent = m_changesets[parentId];
Chris@47 336 parent->addChild(id);
Chris@46 337 ConnectionItem *conn = new ConnectionItem();
Chris@46 338 conn->setChild(item);
Chris@46 339 conn->setParent(m_items[parentId]);
Chris@46 340 m_scene->addItem(conn);
Chris@46 341 }
Chris@46 342 }
Chris@46 343
Chris@52 344 // We need to lay out the changesets in forward chronological
Chris@52 345 // order. We have no guarantees about the order in which
Chris@52 346 // changesets appear in the list -- in a simple repository they
Chris@52 347 // will generally be reverse chronological, but that's far from
Chris@52 348 // guaranteed. So, sort explicitly using the date comparator
Chris@52 349 // above
Chris@51 350
Chris@52 351 qStableSort(csets.begin(), csets.end(), compareChangesetsByDate);
Chris@51 352
cannam@45 353 m_handled.clear();
cannam@45 354 for (int i = csets.size() - 1; i >= 0; --i) {
cannam@45 355 layoutRow(csets[i]->id());
cannam@45 356 }
Chris@46 357
Chris@46 358 allocateBranchHomes(csets);
Chris@46 359
cannam@45 360 m_handled.clear();
cannam@45 361 for (int i = csets.size() - 1; i >= 0; --i) {
cannam@45 362 layoutCol(csets[i]->id());
cannam@45 363 }
Chris@44 364 }
Chris@44 365