comparison 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
comparison
equal deleted inserted replaced
369:19cce6d2c470 370:b9c153e00e84
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 EasyMercurial
5
6 Based on HgExplorer by Jari Korhonen
7 Copyright (c) 2010 Jari Korhonen
8 Copyright (c) 2011 Chris Cannam
9 Copyright (c) 2011 Queen Mary, University of London
10
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License as
13 published by the Free Software Foundation; either version 2 of the
14 License, or (at your option) any later version. See the file
15 COPYING included with this distribution for more information.
16 */
17
18 #include "grapher.h"
19 #include "connectionitem.h"
20 #include "dateitem.h"
21 #include "debug.h"
22 #include "changesetscene.h"
23
24 #include <QSettings>
25
26 #include <iostream>
27
28 Grapher::Grapher(ChangesetScene *scene) :
29 m_scene(scene)
30 {
31 QSettings settings;
32 settings.beginGroup("Presentation");
33 m_showDates = (settings.value("dateformat", 0) == 1);
34 }
35
36 int Grapher::findAvailableColumn(int row, int parent, bool preferParentCol)
37 {
38 int col = parent;
39 if (preferParentCol) {
40 if (isAvailable(row, col)) return col;
41 }
42 while (col > 0) {
43 if (isAvailable(row, --col)) return col;
44 }
45 while (col < 0) {
46 if (isAvailable(row, ++col)) return col;
47 }
48 col = parent;
49 int sign = (col < 0 ? -1 : 1);
50 while (1) {
51 col += sign;
52 if (isAvailable(row, col)) return col;
53 }
54 }
55
56 bool Grapher::isAvailable(int row, int col)
57 {
58 if (m_alloc.contains(row) && m_alloc[row].contains(col)) return false;
59 if (!m_haveAllocatedUncommittedColumn) return true;
60 if (!m_uncommitted) return true;
61 return !(row <= m_uncommittedParentRow && col == m_uncommitted->column());
62 }
63
64 void Grapher::layoutRow(QString id)
65 {
66 if (m_handled.contains(id)) {
67 return;
68 }
69 if (!m_changesets.contains(id)) {
70 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
71 }
72 if (!m_items.contains(id)) {
73 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
74 }
75 Changeset *cs = m_changesets[id];
76 ChangesetItem *item = m_items[id];
77 DEBUG << "layoutRow: Looking at " << id.toStdString() << endl;
78
79 int row = 0;
80 int nparents = cs->parents().size();
81
82 if (nparents > 0) {
83 bool haveRow = false;
84 foreach (QString parentId, cs->parents()) {
85
86 if (!m_changesets.contains(parentId)) continue;
87 if (!m_items.contains(parentId)) continue;
88
89 if (!m_handled.contains(parentId)) {
90 layoutRow(parentId);
91 }
92
93 ChangesetItem *parentItem = m_items[parentId];
94 if (!haveRow || parentItem->row() < row) {
95 row = parentItem->row();
96 haveRow = true;
97 }
98 }
99 row = row - 1;
100 }
101
102 // row is now an upper bound on our eventual row (because we want
103 // to be above all parents). But we also want to ensure we are
104 // above all nodes that have earlier dates (to the nearest day).
105 // m_rowDates maps each row to a date: use that.
106
107 QString date;
108 if (m_showDates) {
109 date = cs->date();
110 } else {
111 date = cs->age();
112 }
113 while (m_rowDates.contains(row) && m_rowDates[row] != date) {
114 --row;
115 }
116
117 // We have already laid out all nodes that have earlier timestamps
118 // than this one, so we know (among other things) that we can
119 // safely fill in this row has having this date, if it isn't in
120 // the map yet (it cannot have an earlier date)
121
122 if (!m_rowDates.contains(row)) {
123 m_rowDates[row] = date;
124 }
125
126 // If we're the parent of the uncommitted item, make a note of our
127 // row (we need it later, to avoid overwriting the connecting line)
128 if (!m_uncommittedParents.empty() && m_uncommittedParents[0] == id) {
129 m_uncommittedParentRow = row;
130 }
131
132 DEBUG << "putting " << cs->id().toStdString() << " at row " << row
133 << endl;
134
135 item->setRow(row);
136 m_handled.insert(id);
137 }
138
139 void Grapher::layoutCol(QString id)
140 {
141 if (m_handled.contains(id)) {
142 DEBUG << "Already looked at " << id.toStdString() << endl;
143 return;
144 }
145 if (!m_changesets.contains(id)) {
146 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
147 }
148 if (!m_items.contains(id)) {
149 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
150 }
151
152 Changeset *cs = m_changesets[id];
153 DEBUG << "layoutCol: Looking at " << id.toStdString() << endl;
154
155 ChangesetItem *item = m_items[id];
156
157 int col = 0;
158 int row = item->row();
159 QString branch = cs->branch();
160
161 int nparents = cs->parents().size();
162 QString parentId;
163 int parentsOnSameBranch = 0;
164
165 switch (nparents) {
166
167 case 0:
168 col = m_branchHomes[cs->branch()];
169 col = findAvailableColumn(row, col, true);
170 break;
171
172 case 1:
173 parentId = cs->parents()[0];
174
175 if (!m_changesets.contains(parentId) ||
176 !m_changesets[parentId]->isOnBranch(branch)) {
177 // new branch
178 col = m_branchHomes[branch];
179 } else {
180 col = m_items[parentId]->column();
181 }
182
183 col = findAvailableColumn(row, col, true);
184 break;
185
186 case 2:
187 // a merge: behave differently if parents are both on the same
188 // branch (we also want to behave differently for nodes that
189 // have multiple children on the same branch -- spreading them
190 // out rather than having affinity to a specific branch)
191
192 foreach (QString parentId, cs->parents()) {
193 if (!m_changesets.contains(parentId)) continue;
194 if (m_changesets[parentId]->isOnBranch(branch)) {
195 ChangesetItem *parentItem = m_items[parentId];
196 col += parentItem->column();
197 parentsOnSameBranch++;
198 }
199 }
200
201 if (parentsOnSameBranch > 0) {
202 col /= parentsOnSameBranch;
203 col = findAvailableColumn(item->row(), col, true);
204 } else {
205 col = findAvailableColumn(item->row(), col, false);
206 }
207 break;
208 }
209
210 DEBUG << "putting " << cs->id().toStdString() << " at col " << col << endl;
211
212 m_alloc[row].insert(col);
213 item->setColumn(col);
214 m_handled.insert(id);
215
216 // If we're the first parent of the uncommitted item, it should be
217 // given the same column as us (we already noted that its
218 // connecting line would end at our row)
219
220 if (m_uncommittedParents.contains(id)) {
221 if (m_uncommittedParents[0] == id) {
222 int ucol = findAvailableColumn(row-1, col, true);
223 m_uncommitted->setColumn(ucol);
224 m_haveAllocatedUncommittedColumn = true;
225 }
226 // also, if the uncommitted item has a different branch from
227 // any of its parents, tell it to show the branch
228 if (!cs->isOnBranch(m_uncommitted->branch())) {
229 DEBUG << "Uncommitted branch " << m_uncommitted->branch()
230 << " differs from my branch " << cs->branch()
231 << ", asking it to show branch" << endl;
232 m_uncommitted->setShowBranch(true);
233 }
234 }
235
236
237 // Normally the children will lay out themselves, but we can do
238 // a better job in some special cases:
239
240 int nchildren = cs->children().size();
241
242 // look for merging children and children distant from us but in a
243 // straight line, and make sure nobody is going to overwrite their
244 // connection lines
245
246 foreach (QString childId, cs->children()) {
247 DEBUG << "reserving connection line space" << endl;
248 if (!m_changesets.contains(childId)) continue;
249 Changeset *child = m_changesets[childId];
250 int childRow = m_items[childId]->row();
251 if (child->parents().size() > 1 ||
252 child->isOnBranch(cs->branch())) {
253 for (int r = row-1; r > childRow; --r) {
254 m_alloc[r].insert(col);
255 }
256 }
257 }
258
259 // look for the case where exactly two children have the same
260 // branch as us: split them to a little either side of our position
261
262 if (nchildren > 1) {
263 QList<QString> special;
264 foreach (QString childId, cs->children()) {
265 if (!m_changesets.contains(childId)) continue;
266 Changeset *child = m_changesets[childId];
267 if (child->isOnBranch(branch) &&
268 child->parents().size() == 1) {
269 special.push_back(childId);
270 }
271 }
272 if (special.size() == 2) {
273 DEBUG << "handling split-in-two for children " << special[0] << " and " << special[1] << endl;
274 for (int i = 0; i < 2; ++i) {
275 int off = i * 2 - 1; // 0 -> -1, 1 -> 1
276 ChangesetItem *it = m_items[special[i]];
277 it->setColumn(findAvailableColumn(it->row(), col + off, true));
278 for (int r = row-1; r >= it->row(); --r) {
279 m_alloc[r].insert(it->column());
280 }
281 m_handled.insert(special[i]);
282 }
283 }
284 }
285 }
286
287 bool Grapher::rangesConflict(const Range &r1, const Range &r2)
288 {
289 // allow some additional space at edges. we really ought also to
290 // permit space at the end of a branch up to the point where the
291 // merge happens
292 int a1 = r1.first - 2, b1 = r1.second + 2;
293 int a2 = r2.first - 2, b2 = r2.second + 2;
294 if (a1 > b2 || b1 < a2) return false;
295 if (a2 > b1 || b2 < a1) return false;
296 return true;
297 }
298
299 void Grapher::allocateBranchHomes(Changesets csets)
300 {
301 foreach (Changeset *cs, csets) {
302 QString branch = cs->branch();
303 ChangesetItem *item = m_items[cs->id()];
304 if (!item) continue;
305 int row = item->row();
306 if (!m_branchRanges.contains(branch)) {
307 m_branchRanges[branch] = Range(row, row);
308 } else {
309 Range p = m_branchRanges[branch];
310 if (row < p.first) p.first = row;
311 if (row > p.second) p.second = row;
312 m_branchRanges[branch] = p;
313 }
314 }
315
316 m_branchHomes[""] = 0;
317 m_branchHomes["default"] = 0;
318
319 foreach (QString branch, m_branchRanges.keys()) {
320 if (branch == "") continue;
321 QSet<int> taken;
322 taken.insert(0);
323 Range myRange = m_branchRanges[branch];
324 foreach (QString other, m_branchRanges.keys()) {
325 if (other == branch || other == "") continue;
326 Range otherRange = m_branchRanges[other];
327 if (rangesConflict(myRange, otherRange)) {
328 if (m_branchHomes.contains(other)) {
329 taken.insert(m_branchHomes[other]);
330 }
331 }
332 }
333 int home = 2;
334 while (taken.contains(home)) {
335 if (home > 0) {
336 if (home % 2 == 1) {
337 home = -home;
338 } else {
339 home = home + 1;
340 }
341 } else {
342 if ((-home) % 2 == 1) {
343 home = home + 1;
344 } else {
345 home = -(home-2);
346 }
347 }
348 }
349 m_branchHomes[branch] = home;
350 }
351
352 foreach (QString branch, m_branchRanges.keys()) {
353 DEBUG << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << endl;
354 }
355 }
356
357 static bool
358 compareChangesetsByDate(Changeset *const &a, Changeset *const &b)
359 {
360 return a->timestamp() < b->timestamp();
361 }
362
363 ChangesetItem *
364 Grapher::getItemFor(Changeset *cs)
365 {
366 if (!cs || !m_items.contains(cs->id())) return 0;
367 return m_items[cs->id()];
368 }
369
370 void Grapher::layout(Changesets csets,
371 QStringList uncommittedParents,
372 QString uncommittedBranch)
373 {
374 m_changesets.clear();
375 m_items.clear();
376 m_alloc.clear();
377 m_branchHomes.clear();
378
379 m_uncommittedParents = uncommittedParents;
380 m_haveAllocatedUncommittedColumn = false;
381 m_uncommittedParentRow = 0;
382 m_uncommitted = 0;
383
384 DEBUG << "Grapher::layout: Have " << csets.size() << " changesets" << endl;
385
386 if (csets.empty()) return;
387
388 // Create (but don't yet position) the changeset items
389
390 foreach (Changeset *cs, csets) {
391
392 QString id = cs->id();
393
394 if (id == "") {
395 throw LayoutException("Changeset has no ID");
396 }
397 if (m_changesets.contains(id)) {
398 DEBUG << "Duplicate changeset ID " << id
399 << " in Grapher::layout()" << endl;
400 throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
401 }
402
403 m_changesets[id] = cs;
404
405 ChangesetItem *item = new ChangesetItem(cs);
406 item->setX(0);
407 item->setY(0);
408 item->setZValue(0);
409 m_items[id] = item;
410 m_scene->addChangesetItem(item);
411 }
412
413 // Add the connecting lines
414
415 foreach (Changeset *cs, csets) {
416 QString id = cs->id();
417 ChangesetItem *item = m_items[id];
418 bool merge = (cs->parents().size() > 1);
419 foreach (QString parentId, cs->parents()) {
420 if (!m_changesets.contains(parentId)) continue;
421 Changeset *parent = m_changesets[parentId];
422 parent->addChild(id);
423 ConnectionItem *conn = new ConnectionItem();
424 if (merge) conn->setConnectionType(ConnectionItem::Merge);
425 conn->setChild(item);
426 conn->setParent(m_items[parentId]);
427 conn->setZValue(-1);
428 m_scene->addItem(conn);
429 }
430 }
431
432 // Add uncommitted item and connecting line as necessary
433
434 if (!m_uncommittedParents.empty()) {
435
436 m_uncommitted = new UncommittedItem();
437 m_uncommitted->setBranch(uncommittedBranch);
438 m_uncommitted->setZValue(10);
439 m_scene->addUncommittedItem(m_uncommitted);
440
441 bool haveParentOnBranch = false;
442 foreach (QString p, m_uncommittedParents) {
443 ConnectionItem *conn = new ConnectionItem();
444 conn->setConnectionType(ConnectionItem::Merge);
445 ChangesetItem *pitem = m_items[p];
446 conn->setParent(pitem);
447 conn->setChild(m_uncommitted);
448 conn->setZValue(0);
449 m_scene->addItem(conn);
450 if (pitem) {
451 if (pitem->getChangeset()->branch() == uncommittedBranch) {
452 haveParentOnBranch = true;
453 }
454 }
455 }
456
457 // If the uncommitted item has no parents on the same branch,
458 // tell it it has a new branch (the "show branch" flag is set
459 // elsewhere for this item)
460 m_uncommitted->setIsNewBranch(!haveParentOnBranch);
461 }
462
463 // Add the branch labels
464
465 foreach (Changeset *cs, csets) {
466 QString id = cs->id();
467 ChangesetItem *item = m_items[id];
468 bool haveChildOnSameBranch = false;
469 foreach (QString childId, cs->children()) {
470 Changeset *child = m_changesets[childId];
471 if (child->branch() == cs->branch()) {
472 haveChildOnSameBranch = true;
473 break;
474 }
475 }
476 item->setShowBranch(!haveChildOnSameBranch);
477 }
478
479 // We need to lay out the changesets in forward chronological
480 // order. We have no guarantees about the order in which
481 // changesets appear in the list -- in a simple repository they
482 // will generally be reverse chronological, but that's far from
483 // guaranteed. So, sort explicitly using the date comparator
484 // above
485
486 qStableSort(csets.begin(), csets.end(), compareChangesetsByDate);
487
488 foreach (Changeset *cs, csets) {
489 DEBUG << "id " << cs->id().toStdString() << ", ts " << cs->timestamp()
490 << ", date " << cs->datetime().toStdString() << endl;
491 }
492
493 m_handled.clear();
494 foreach (Changeset *cs, csets) {
495 layoutRow(cs->id());
496 }
497
498 allocateBranchHomes(csets);
499
500 m_handled.clear();
501 foreach (Changeset *cs, csets) {
502 foreach (QString parentId, cs->parents()) {
503 if (!m_handled.contains(parentId) &&
504 m_changesets.contains(parentId)) {
505 layoutCol(parentId);
506 }
507 }
508 layoutCol(cs->id());
509 }
510
511 // Find row and column extents. We know that 0 is an upper bound
512 // on row, and that mincol must be <= 0 and maxcol >= 0, so these
513 // initial values are good
514
515 int minrow = 0, maxrow = 0;
516 int mincol = 0, maxcol = 0;
517
518 foreach (int r, m_alloc.keys()) {
519 if (r < minrow) minrow = r;
520 if (r > maxrow) maxrow = r;
521 ColumnSet &c = m_alloc[r];
522 foreach (int i, c) {
523 if (i < mincol) mincol = i;
524 if (i > maxcol) maxcol = i;
525 }
526 }
527
528 int datemincol = mincol, datemaxcol = maxcol;
529
530 if (mincol == maxcol) {
531 --datemincol;
532 ++datemaxcol;
533 } else if (m_alloc[minrow].contains(mincol)) {
534 --datemincol;
535 }
536
537 // We've given the uncommitted item a column, but not a row yet --
538 // it always goes at the top
539
540 if (m_uncommitted) {
541 --minrow;
542 DEBUG << "putting uncommitted item at row " << minrow << endl;
543 m_uncommitted->setRow(minrow);
544 }
545
546 // Changeset items that have nothing to either side of them can be
547 // made double-width
548
549 foreach (Changeset *cs, csets) {
550 ChangesetItem *item = m_items[cs->id()];
551 if (isAvailable(item->row(), item->column()-1) &&
552 isAvailable(item->row(), item->column()+1)) {
553 item->setWide(true);
554 }
555 }
556
557 if (m_uncommitted) {
558 if (isAvailable(m_uncommitted->row(), m_uncommitted->column()-1) &&
559 isAvailable(m_uncommitted->row(), m_uncommitted->column()+1)) {
560 m_uncommitted->setWide(true);
561 }
562 }
563
564 QString prevDate;
565 int changeRow = 0;
566
567 bool even = false;
568 int n = 0;
569
570 for (int row = minrow; row <= maxrow; ++row) {
571
572 QString date = m_rowDates[row];
573 n++;
574
575 if (date != prevDate) {
576 if (prevDate != "") {
577 DateItem *item = new DateItem();
578 item->setDateString(prevDate);
579 item->setCols(datemincol, datemaxcol - datemincol + 1);
580 item->setRows(changeRow, n);
581 item->setEven(even);
582 item->setZValue(-2);
583 m_scene->addDateItem(item);
584 even = !even;
585 }
586 prevDate = date;
587 changeRow = row;
588 n = 0;
589 }
590 }
591
592 if (n > 0) {
593 DateItem *item = new DateItem();
594 item->setDateString(prevDate);
595 item->setCols(datemincol, datemaxcol - datemincol + 1);
596 item->setRows(changeRow, n+1);
597 item->setEven(even);
598 item->setZValue(-2);
599 m_scene->addDateItem(item);
600 even = !even;
601 }
602 }
603