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
|