annotate grapher.cpp @ 57:f583e44d9d31

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