annotate grapher.cpp @ 139:e8a481789607

* Avoid unnecessary duplicate layout; fix off-by-one in resetting changeset children
author Chris Cannam
date Tue, 30 Nov 2010 14:27:34 +0000
parents c3e8342d2de9
children e6c6b88d19b9
rev   line source
Chris@57 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@57 2
Chris@57 3 /*
Chris@57 4 EasyMercurial
Chris@57 5
Chris@57 6 Based on HgExplorer by Jari Korhonen
Chris@57 7 Copyright (c) 2010 Jari Korhonen
Chris@57 8 Copyright (c) 2010 Chris Cannam
Chris@57 9 Copyright (c) 2010 Queen Mary, University of London
Chris@57 10
Chris@57 11 This program is free software; you can redistribute it and/or
Chris@57 12 modify it under the terms of the GNU General Public License as
Chris@57 13 published by the Free Software Foundation; either version 2 of the
Chris@57 14 License, or (at your option) any later version. See the file
Chris@57 15 COPYING included with this distribution for more information.
Chris@57 16 */
Chris@44 17
Chris@44 18 #include "grapher.h"
Chris@46 19 #include "connectionitem.h"
Chris@53 20 #include "dateitem.h"
Chris@114 21 #include "debug.h"
Chris@119 22 #include "changesetscene.h"
Chris@44 23
Chris@44 24 #include <iostream>
Chris@44 25
Chris@106 26 int Grapher::findAvailableColumn(int row, int parent, bool preferParentCol)
cannam@45 27 {
cannam@45 28 int col = parent;
cannam@45 29 if (preferParentCol) {
Chris@106 30 if (!m_alloc[row].contains(col)) {
Chris@106 31 return col;
Chris@106 32 }
cannam@45 33 }
cannam@45 34 while (col > 0) {
Chris@106 35 if (!m_alloc[row].contains(--col)) return col;
cannam@45 36 }
cannam@45 37 while (col < 0) {
Chris@106 38 if (!m_alloc[row].contains(++col)) return col;
cannam@45 39 }
cannam@45 40 col = parent;
cannam@45 41 int sign = (col < 0 ? -1 : 1);
cannam@45 42 while (1) {
Chris@106 43 col += sign;
Chris@106 44 if (!m_alloc[row].contains(col)) return col;
cannam@45 45 }
cannam@45 46 }
Chris@44 47
Chris@106 48 void Grapher::layoutRow(QString id)
Chris@44 49 {
cannam@45 50 if (m_handled.contains(id)) {
Chris@106 51 return;
Chris@44 52 }
Chris@46 53 if (!m_changesets.contains(id)) {
Chris@106 54 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
Chris@44 55 }
cannam@45 56 if (!m_items.contains(id)) {
Chris@106 57 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
Chris@44 58 }
Chris@46 59 Changeset *cs = m_changesets[id];
cannam@45 60 ChangesetItem *item = m_items[id];
Chris@114 61 DEBUG << "layoutRow: Looking at " << id.toStdString() << endl;
cannam@45 62
Chris@44 63 int row = 0;
cannam@45 64 int nparents = cs->parents().size();
cannam@45 65
cannam@45 66 if (nparents > 0) {
Chris@106 67 bool haveRow = false;
Chris@106 68 foreach (QString parentId, cs->parents()) {
cannam@45 69
Chris@106 70 if (!m_changesets.contains(parentId)) continue;
Chris@106 71 if (!m_items.contains(parentId)) continue;
cannam@45 72
Chris@106 73 if (!m_handled.contains(parentId)) {
Chris@106 74 layoutRow(parentId);
Chris@106 75 }
cannam@45 76
Chris@106 77 ChangesetItem *parentItem = m_items[parentId];
Chris@106 78 if (!haveRow || parentItem->row() < row) {
Chris@106 79 row = parentItem->row();
Chris@106 80 haveRow = true;
Chris@106 81 }
Chris@106 82 }
Chris@106 83 row = row - 1;
cannam@45 84 }
cannam@45 85
Chris@52 86 // row is now an upper bound on our eventual row (because we want
Chris@52 87 // to be above all parents). But we also want to ensure we are
Chris@52 88 // above all nodes that have earlier dates (to the nearest day).
Chris@52 89 // m_rowDates maps each row to a date: use that.
Chris@52 90
Chris@53 91 QString date = cs->age();
Chris@53 92 while (m_rowDates.contains(row) && m_rowDates[row] != date) {
Chris@106 93 --row;
Chris@52 94 }
Chris@52 95
Chris@52 96 // We have already laid out all nodes that have earlier timestamps
Chris@52 97 // than this one, so we know (among other things) that we can
Chris@52 98 // safely fill in this row has having this date, if it isn't in
Chris@52 99 // the map yet (it cannot have an earlier date)
Chris@52 100
Chris@52 101 if (!m_rowDates.contains(row)) {
Chris@106 102 m_rowDates[row] = date;
Chris@52 103 }
Chris@52 104
Chris@114 105 DEBUG << "putting " << cs->id().toStdString() << " at row " << row
Chris@114 106 << endl;
cannam@45 107
Chris@44 108 item->setRow(row);
cannam@45 109 m_handled.insert(id);
Chris@44 110 }
Chris@44 111
Chris@106 112 void Grapher::layoutCol(QString id)
Chris@44 113 {
cannam@45 114 if (m_handled.contains(id)) {
Chris@114 115 DEBUG << "Already looked at " << id.toStdString() << endl;
Chris@106 116 return;
cannam@45 117 }
Chris@46 118 if (!m_changesets.contains(id)) {
Chris@106 119 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
cannam@45 120 }
cannam@45 121 if (!m_items.contains(id)) {
Chris@106 122 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
cannam@45 123 }
Chris@53 124
Chris@46 125 Changeset *cs = m_changesets[id];
Chris@122 126 DEBUG << "layoutCol: Looking at " << id.toStdString() << endl;
Chris@53 127
cannam@45 128 ChangesetItem *item = m_items[id];
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@106 141 col = m_branchHomes[cs->branch()];
Chris@106 142 col = findAvailableColumn(row, col, true);
Chris@106 143 break;
cannam@45 144
Chris@46 145 case 1:
Chris@106 146 parentId = cs->parents()[0];
cannam@45 147
Chris@106 148 if (!m_changesets.contains(parentId) ||
Chris@106 149 m_changesets[parentId]->branch() != branch) {
Chris@106 150 // new branch
Chris@106 151 col = m_branchHomes[branch];
Chris@106 152 } else {
Chris@106 153 col = m_items[parentId]->column();
Chris@106 154 }
cannam@45 155
Chris@106 156 col = findAvailableColumn(row, col, true);
Chris@106 157 break;
Chris@46 158
Chris@46 159 case 2:
Chris@106 160 // a merge: behave differently if parents are both on the same
Chris@106 161 // branch (we also want to behave differently for nodes that
Chris@106 162 // have multiple children on the same branch -- spreading them
Chris@106 163 // out rather than having affinity to a specific branch)
Chris@46 164
Chris@106 165 foreach (QString parentId, cs->parents()) {
Chris@106 166 if (!m_changesets.contains(parentId)) continue;
Chris@106 167 if (m_changesets[parentId]->branch() == branch) {
Chris@106 168 ChangesetItem *parentItem = m_items[parentId];
Chris@106 169 col += parentItem->column();
Chris@106 170 parentsOnSameBranch++;
Chris@106 171 }
Chris@106 172 }
Chris@46 173
Chris@106 174 if (parentsOnSameBranch > 0) {
Chris@106 175 col /= parentsOnSameBranch;
Chris@106 176 col = findAvailableColumn(item->row(), col, true);
Chris@106 177 } else {
Chris@106 178 col = findAvailableColumn(item->row(), col, false);
Chris@106 179 }
Chris@106 180 break;
Chris@44 181 }
Chris@44 182
Chris@122 183 DEBUG << "putting " << cs->id().toStdString() << " at col " << col << 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@53 194 // look for merging children and children distant from us but in a
Chris@53 195 // straight line, and make sure nobody is going to overwrite their
Chris@53 196 // connection lines
Chris@51 197
Chris@49 198 foreach (QString childId, cs->children()) {
Chris@139 199 DEBUG << "reserving connection line space" << endl;
Chris@49 200 if (!m_changesets.contains(childId)) continue;
Chris@49 201 Changeset *child = m_changesets[childId];
Chris@106 202 int childRow = m_items[childId]->row();
Chris@55 203 if (child->parents().size() > 1 ||
Chris@106 204 child->branch() == cs->branch()) {
Chris@55 205 for (int r = row-1; r > childRow; --r) {
Chris@49 206 m_alloc[r].insert(col);
Chris@49 207 }
Chris@106 208 }
Chris@49 209 }
Chris@49 210
Chris@51 211 // look for the case where exactly two children have the same
Chris@51 212 // branch as us: split them to a little either side of our position
Chris@51 213
Chris@47 214 if (nchildren > 1) {
Chris@106 215 QList<QString> special;
Chris@106 216 foreach (QString childId, cs->children()) {
Chris@106 217 if (!m_changesets.contains(childId)) continue;
Chris@106 218 Changeset *child = m_changesets[childId];
Chris@106 219 if (child->branch() == branch &&
Chris@106 220 child->parents().size() == 1) {
Chris@106 221 special.push_back(childId);
Chris@106 222 }
Chris@106 223 }
Chris@106 224 if (special.size() == 2) {
Chris@139 225 DEBUG << "handling split-in-two for children " << special[0] << " and " << special[1] << endl;
Chris@106 226 for (int i = 0; i < 2; ++i) {
Chris@106 227 int off = i * 2 - 1; // 0 -> -1, 1 -> 1
Chris@106 228 ChangesetItem *it = m_items[special[i]];
Chris@106 229 it->setColumn(findAvailableColumn(it->row(), col + off, true));
Chris@106 230 for (int r = row-1; r >= it->row(); --r) {
Chris@106 231 m_alloc[r].insert(it->column());
Chris@106 232 }
Chris@106 233 m_handled.insert(special[i]);
Chris@106 234 }
Chris@106 235 }
Chris@47 236 }
cannam@45 237 }
cannam@45 238
Chris@106 239 bool Grapher::rangesConflict(const Range &r1, const Range &r2)
Chris@46 240 {
Chris@46 241 // allow some additional space at edges. we really ought also to
Chris@46 242 // permit space at the end of a branch up to the point where the
Chris@46 243 // merge happens
Chris@46 244 int a1 = r1.first - 2, b1 = r1.second + 2;
Chris@46 245 int a2 = r2.first - 2, b2 = r2.second + 2;
Chris@46 246 if (a1 > b2 || b1 < a2) return false;
Chris@46 247 if (a2 > b1 || b2 < a1) return false;
Chris@46 248 return true;
Chris@46 249 }
Chris@46 250
Chris@106 251 void Grapher::allocateBranchHomes(Changesets csets)
Chris@46 252 {
Chris@46 253 foreach (Changeset *cs, csets) {
Chris@106 254 QString branch = cs->branch();
Chris@106 255 ChangesetItem *item = m_items[cs->id()];
Chris@106 256 if (!item) continue;
Chris@106 257 int row = item->row();
Chris@106 258 if (!m_branchRanges.contains(branch)) {
Chris@106 259 m_branchRanges[branch] = Range(row, row);
Chris@106 260 } else {
Chris@106 261 Range p = m_branchRanges[branch];
Chris@106 262 if (row < p.first) p.first = row;
Chris@106 263 if (row > p.second) p.second = row;
Chris@106 264 m_branchRanges[branch] = p;
Chris@106 265 }
Chris@46 266 }
Chris@46 267
Chris@46 268 m_branchHomes[""] = 0;
Chris@46 269
Chris@46 270 foreach (QString branch, m_branchRanges.keys()) {
Chris@106 271 if (branch == "") continue;
Chris@106 272 QSet<int> taken;
Chris@106 273 taken.insert(0);
Chris@106 274 Range myRange = m_branchRanges[branch];
Chris@106 275 foreach (QString other, m_branchRanges.keys()) {
Chris@106 276 if (other == branch || other == "") continue;
Chris@106 277 Range otherRange = m_branchRanges[other];
Chris@106 278 if (rangesConflict(myRange, otherRange)) {
Chris@106 279 if (m_branchHomes.contains(other)) {
Chris@106 280 taken.insert(m_branchHomes[other]);
Chris@106 281 }
Chris@106 282 }
Chris@106 283 }
Chris@106 284 int home = 2;
Chris@106 285 while (taken.contains(home)) {
Chris@106 286 if (home > 0) {
Chris@106 287 if (home % 2 == 1) {
Chris@106 288 home = -home;
Chris@106 289 } else {
Chris@106 290 home = home + 1;
Chris@106 291 }
Chris@106 292 } else {
Chris@106 293 if ((-home) % 2 == 1) {
Chris@106 294 home = home + 1;
Chris@106 295 } else {
Chris@106 296 home = -(home-2);
Chris@106 297 }
Chris@106 298 }
Chris@106 299 }
Chris@106 300 m_branchHomes[branch] = home;
Chris@46 301 }
Chris@46 302
Chris@46 303 foreach (QString branch, m_branchRanges.keys()) {
Chris@114 304 DEBUG << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << endl;
Chris@46 305 }
Chris@46 306 }
Chris@46 307
Chris@52 308 static bool
Chris@106 309 compareChangesetsByDate(Changeset *const &a, Changeset *const &b)
Chris@52 310 {
Chris@52 311 return a->timestamp() < b->timestamp();
Chris@52 312 }
Chris@52 313
Chris@53 314 ChangesetItem *
Chris@106 315 Grapher::getItemFor(Changeset *cs)
Chris@53 316 {
Chris@53 317 if (!cs || !m_items.contains(cs->id())) return 0;
Chris@53 318 return m_items[cs->id()];
Chris@53 319 }
Chris@53 320
Chris@106 321 void Grapher::layout(Changesets csets)
cannam@45 322 {
Chris@46 323 m_changesets.clear();
cannam@45 324 m_items.clear();
cannam@45 325 m_alloc.clear();
Chris@46 326 m_branchHomes.clear();
cannam@45 327
Chris@139 328 DEBUG << "Grapher::layout: Have " << csets.size() << " changesets" << endl;
Chris@139 329
Chris@53 330 if (csets.empty()) return;
Chris@53 331
Chris@44 332 foreach (Changeset *cs, csets) {
cannam@45 333
Chris@106 334 QString id = cs->id();
Chris@139 335 // DEBUG << id.toStdString() << endl;
cannam@45 336
Chris@106 337 if (id == "") {
Chris@106 338 throw LayoutException("Changeset has no ID");
Chris@106 339 }
Chris@106 340 if (m_changesets.contains(id)) {
Chris@106 341 throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
Chris@106 342 }
cannam@45 343
Chris@106 344 m_changesets[id] = cs;
cannam@45 345
cannam@45 346 ChangesetItem *item = new ChangesetItem(cs);
cannam@45 347 item->setX(0);
cannam@45 348 item->setY(0);
Chris@106 349 m_items[id] = item;
cannam@45 350 m_scene->addItem(item);
Chris@119 351 QObject::connect(item, SIGNAL(detailShown()),
Chris@119 352 m_scene, SLOT(changesetDetailShown()));
cannam@45 353 }
cannam@45 354
Chris@74 355 // Add the connecting lines
Chris@74 356
Chris@46 357 foreach (Changeset *cs, csets) {
Chris@106 358 QString id = cs->id();
Chris@106 359 ChangesetItem *item = m_items[id];
Chris@106 360 bool merge = (cs->parents().size() > 1);
Chris@106 361 foreach (QString parentId, cs->parents()) {
Chris@106 362 if (!m_changesets.contains(parentId)) continue;
Chris@106 363 Changeset *parent = m_changesets[parentId];
Chris@106 364 parent->addChild(id);
Chris@106 365 ConnectionItem *conn = new ConnectionItem();
Chris@106 366 if (merge) conn->setConnectionType(ConnectionItem::Merge);
Chris@106 367 conn->setChild(item);
Chris@106 368 conn->setParent(m_items[parentId]);
Chris@106 369 m_scene->addItem(conn);
Chris@106 370 }
Chris@46 371 }
Chris@46 372
Chris@74 373 // Add the branch labels
Chris@74 374 foreach (Changeset *cs, csets) {
Chris@74 375 QString id = cs->id();
Chris@74 376 ChangesetItem *item = m_items[id];
Chris@74 377 bool haveChildOnSameBranch = false;
Chris@74 378 foreach (QString childId, cs->children()) {
Chris@74 379 Changeset *child = m_changesets[childId];
Chris@74 380 if (child->branch() == cs->branch()) {
Chris@74 381 haveChildOnSameBranch = true;
Chris@74 382 break;
Chris@74 383 }
Chris@74 384 }
Chris@74 385 item->setShowBranch(!haveChildOnSameBranch);
Chris@74 386 }
Chris@74 387
Chris@52 388 // We need to lay out the changesets in forward chronological
Chris@52 389 // order. We have no guarantees about the order in which
Chris@52 390 // changesets appear in the list -- in a simple repository they
Chris@52 391 // will generally be reverse chronological, but that's far from
Chris@52 392 // guaranteed. So, sort explicitly using the date comparator
Chris@52 393 // above
Chris@51 394
Chris@52 395 qStableSort(csets.begin(), csets.end(), compareChangesetsByDate);
Chris@51 396
Chris@53 397 foreach (Changeset *cs, csets) {
Chris@114 398 DEBUG << "id " << cs->id().toStdString() << ", ts " << cs->timestamp() << ", date " << cs->datetime().toStdString() << endl;
Chris@53 399 }
Chris@53 400
cannam@45 401 m_handled.clear();
Chris@53 402 foreach (Changeset *cs, csets) {
Chris@106 403 layoutRow(cs->id());
cannam@45 404 }
Chris@46 405
Chris@46 406 allocateBranchHomes(csets);
Chris@46 407
cannam@45 408 m_handled.clear();
Chris@53 409 foreach (Changeset *cs, csets) {
Chris@106 410 foreach (QString parentId, cs->parents()) {
Chris@106 411 if (!m_handled.contains(parentId) &&
Chris@106 412 m_changesets.contains(parentId)) {
Chris@106 413 layoutCol(parentId);
Chris@106 414 }
Chris@106 415 }
Chris@106 416 layoutCol(cs->id());
Chris@53 417 }
Chris@53 418
Chris@55 419 foreach (Changeset *cs, csets) {
Chris@106 420 ChangesetItem *item = m_items[cs->id()];
Chris@106 421 if (!m_alloc[item->row()].contains(item->column()-1) &&
Chris@106 422 !m_alloc[item->row()].contains(item->column()+1)) {
Chris@106 423 item->setWide(true);
Chris@106 424 }
Chris@55 425 }
Chris@55 426
Chris@53 427 // we know that 0 is an upper bound on row, and that mincol must
Chris@53 428 // be <= 0 and maxcol >= 0, so these initial values are good
Chris@53 429 int minrow = 0, maxrow = 0;
Chris@53 430 int mincol = 0, maxcol = 0;
Chris@53 431
Chris@53 432 foreach (int r, m_alloc.keys()) {
Chris@106 433 if (r < minrow) minrow = r;
Chris@106 434 if (r > maxrow) maxrow = r;
Chris@106 435 ColumnSet &c = m_alloc[r];
Chris@106 436 foreach (int i, c) {
Chris@106 437 if (i < mincol) mincol = i;
Chris@106 438 if (i > maxcol) maxcol = i;
Chris@106 439 }
Chris@53 440 }
Chris@53 441
Chris@53 442 QString prevDate;
Chris@53 443 int changeRow = 0;
Chris@53 444
Chris@53 445 bool even = false;
Chris@53 446 int n = 0;
Chris@53 447
Chris@53 448 for (int row = minrow; row <= maxrow; ++row) {
Chris@53 449
Chris@106 450 QString date = m_rowDates[row];
Chris@106 451 n++;
Chris@106 452
Chris@106 453 if (date != prevDate) {
Chris@106 454 if (prevDate != "") {
Chris@106 455 DateItem *item = new DateItem();
Chris@106 456 item->setDateString(prevDate);
Chris@106 457 item->setCols(mincol, maxcol - mincol + 1);
Chris@106 458 item->setRows(changeRow, n);
Chris@106 459 item->setEven(even);
Chris@106 460 item->setZValue(-1);
Chris@106 461 m_scene->addItem(item);
Chris@106 462 even = !even;
Chris@106 463 }
Chris@106 464 prevDate = date;
Chris@106 465 changeRow = row;
Chris@106 466 n = 0;
Chris@106 467 }
Chris@53 468 }
Chris@53 469
Chris@53 470 if (n > 0) {
Chris@106 471 DateItem *item = new DateItem();
Chris@106 472 item->setDateString(prevDate);
Chris@106 473 item->setCols(mincol, maxcol - mincol + 1);
Chris@106 474 item->setRows(changeRow, n+1);
Chris@106 475 item->setEven(even);
Chris@106 476 item->setZValue(-1);
Chris@106 477 m_scene->addItem(item);
Chris@106 478 even = !even;
cannam@45 479 }
Chris@44 480 }
Chris@44 481