annotate grapher.cpp @ 53:3c46b2ac45d3

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