Chris@44
|
1
|
Chris@44
|
2 #include "grapher.h"
|
Chris@46
|
3 #include "connectionitem.h"
|
Chris@44
|
4
|
cannam@45
|
5 #include <QGraphicsScene>
|
Chris@44
|
6
|
Chris@44
|
7 #include <iostream>
|
Chris@44
|
8
|
cannam@45
|
9 int
|
cannam@45
|
10 Grapher::findAvailableColumn(int row, int parent, bool preferParentCol)
|
cannam@45
|
11 {
|
cannam@45
|
12 int col = parent;
|
cannam@45
|
13 if (preferParentCol) {
|
cannam@45
|
14 if (!m_alloc[row].contains(col)) {
|
cannam@45
|
15 return col;
|
cannam@45
|
16 }
|
cannam@45
|
17 }
|
cannam@45
|
18 while (col > 0) {
|
cannam@45
|
19 if (!m_alloc[row].contains(--col)) return col;
|
cannam@45
|
20 }
|
cannam@45
|
21 while (col < 0) {
|
cannam@45
|
22 if (!m_alloc[row].contains(++col)) return col;
|
cannam@45
|
23 }
|
cannam@45
|
24 col = parent;
|
cannam@45
|
25 int sign = (col < 0 ? -1 : 1);
|
cannam@45
|
26 while (1) {
|
cannam@45
|
27 col += sign;
|
cannam@45
|
28 if (!m_alloc[row].contains(col)) return col;
|
cannam@45
|
29 }
|
cannam@45
|
30 }
|
Chris@44
|
31
|
cannam@45
|
32 void
|
cannam@45
|
33 Grapher::layoutRow(QString id)
|
Chris@44
|
34 {
|
cannam@45
|
35 if (m_handled.contains(id)) {
|
cannam@45
|
36 return;
|
Chris@44
|
37 }
|
Chris@46
|
38 if (!m_changesets.contains(id)) {
|
cannam@45
|
39 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
|
Chris@44
|
40 }
|
cannam@45
|
41 if (!m_items.contains(id)) {
|
cannam@45
|
42 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
|
Chris@44
|
43 }
|
Chris@46
|
44 Changeset *cs = m_changesets[id];
|
cannam@45
|
45 ChangesetItem *item = m_items[id];
|
cannam@45
|
46 std::cerr << "Looking at " << id.toStdString() << std::endl;
|
cannam@45
|
47
|
Chris@44
|
48 int row = 0;
|
cannam@45
|
49 int nparents = cs->parents().size();
|
cannam@45
|
50
|
cannam@45
|
51 if (nparents > 0) {
|
Chris@44
|
52 bool haveRow = false;
|
Chris@44
|
53 foreach (QString parentId, cs->parents()) {
|
cannam@45
|
54
|
Chris@46
|
55 if (!m_changesets.contains(parentId)) continue;
|
cannam@45
|
56 if (!m_items.contains(parentId)) continue;
|
cannam@45
|
57
|
cannam@45
|
58 if (!m_handled.contains(parentId)) {
|
cannam@45
|
59 layoutRow(parentId);
|
cannam@45
|
60 }
|
cannam@45
|
61
|
cannam@45
|
62 ChangesetItem *parentItem = m_items[parentId];
|
Chris@44
|
63 if (!haveRow || parentItem->row() < row) {
|
Chris@44
|
64 row = parentItem->row();
|
Chris@44
|
65 haveRow = true;
|
Chris@44
|
66 }
|
Chris@44
|
67 }
|
Chris@44
|
68 row = row - 1;
|
cannam@45
|
69 }
|
cannam@45
|
70
|
Chris@52
|
71 // row is now an upper bound on our eventual row (because we want
|
Chris@52
|
72 // to be above all parents). But we also want to ensure we are
|
Chris@52
|
73 // above all nodes that have earlier dates (to the nearest day).
|
Chris@52
|
74 // m_rowDates maps each row to a date: use that.
|
Chris@52
|
75
|
Chris@52
|
76 QString date = cs->date();
|
Chris@52
|
77
|
Chris@52
|
78 // n.b. this relies on the fact that the date component of an ISO
|
Chris@52
|
79 // date/time sorts correctly in a dictionary sort
|
Chris@52
|
80 while (m_rowDates.contains(row) && m_rowDates[row] < date) {
|
Chris@52
|
81 --row;
|
Chris@52
|
82 }
|
Chris@52
|
83
|
Chris@52
|
84 // We have already laid out all nodes that have earlier timestamps
|
Chris@52
|
85 // than this one, so we know (among other things) that we can
|
Chris@52
|
86 // safely fill in this row has having this date, if it isn't in
|
Chris@52
|
87 // the map yet (it cannot have an earlier date)
|
Chris@52
|
88
|
Chris@52
|
89 if (!m_rowDates.contains(row)) {
|
Chris@52
|
90 m_rowDates[row] = date;
|
Chris@52
|
91 }
|
Chris@52
|
92
|
cannam@45
|
93 std::cerr << "putting " << cs->id().toStdString() << " at row " << row
|
cannam@45
|
94 << std::endl;
|
cannam@45
|
95
|
Chris@44
|
96 item->setRow(row);
|
cannam@45
|
97 m_handled.insert(id);
|
Chris@44
|
98 }
|
Chris@44
|
99
|
Chris@44
|
100 void
|
cannam@45
|
101 Grapher::layoutCol(QString id)
|
Chris@44
|
102 {
|
cannam@45
|
103 if (m_handled.contains(id)) {
|
cannam@45
|
104 std::cerr << "Already looked at " << id.toStdString() << std::endl;
|
cannam@45
|
105 return;
|
cannam@45
|
106 }
|
Chris@46
|
107 if (!m_changesets.contains(id)) {
|
cannam@45
|
108 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
|
cannam@45
|
109 }
|
cannam@45
|
110 if (!m_items.contains(id)) {
|
cannam@45
|
111 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
|
cannam@45
|
112 }
|
Chris@46
|
113 Changeset *cs = m_changesets[id];
|
cannam@45
|
114 ChangesetItem *item = m_items[id];
|
cannam@45
|
115 std::cerr << "Looking at " << id.toStdString() << std::endl;
|
cannam@45
|
116
|
Chris@46
|
117 foreach (QString parentId, cs->parents()) {
|
Chris@46
|
118 if (!m_changesets.contains(parentId)) continue;
|
Chris@46
|
119 if (!m_handled.contains(parentId)) {
|
Chris@46
|
120 layoutCol(parentId);
|
Chris@46
|
121 }
|
Chris@46
|
122 }
|
Chris@46
|
123
|
Chris@47
|
124 // Parent may have layed out child in the recursive call
|
Chris@47
|
125 if (m_handled.contains(id)) {
|
Chris@47
|
126 std::cerr << "Looks like we've dealt with " << id.toStdString() << std::endl;
|
Chris@47
|
127 return;
|
Chris@47
|
128 }
|
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@46
|
141 col = m_branchHomes[cs->branch()];
|
Chris@46
|
142 col = findAvailableColumn(row, col, true);
|
Chris@46
|
143 break;
|
cannam@45
|
144
|
Chris@46
|
145 case 1:
|
Chris@46
|
146 parentId = cs->parents()[0];
|
cannam@45
|
147
|
Chris@46
|
148 if (!m_changesets.contains(parentId) ||
|
Chris@46
|
149 m_changesets[parentId]->branch() != branch) {
|
Chris@46
|
150 // new branch
|
Chris@46
|
151 col = m_branchHomes[branch];
|
Chris@46
|
152 } else {
|
Chris@46
|
153 col = m_items[parentId]->column();
|
Chris@44
|
154 }
|
cannam@45
|
155
|
Chris@46
|
156 col = findAvailableColumn(row, col, true);
|
Chris@46
|
157 break;
|
Chris@46
|
158
|
Chris@46
|
159 case 2:
|
Chris@46
|
160 // a merge: behave differently if parents are both on the same
|
Chris@46
|
161 // branch (we also want to behave differently for nodes that
|
Chris@46
|
162 // have multiple children on the same branch -- spreading them
|
Chris@46
|
163 // out rather than having affinity to a specific branch)
|
Chris@46
|
164
|
Chris@46
|
165 foreach (QString parentId, cs->parents()) {
|
Chris@46
|
166 if (!m_changesets.contains(parentId)) continue;
|
Chris@46
|
167 if (m_changesets[parentId]->branch() == branch) {
|
Chris@46
|
168 ChangesetItem *parentItem = m_items[parentId];
|
Chris@46
|
169 col += parentItem->column();
|
Chris@46
|
170 parentsOnSameBranch++;
|
Chris@46
|
171 }
|
Chris@46
|
172 }
|
Chris@46
|
173
|
Chris@46
|
174 if (parentsOnSameBranch > 0) {
|
Chris@46
|
175 col /= parentsOnSameBranch;
|
Chris@46
|
176 col = findAvailableColumn(item->row(), col, true);
|
Chris@46
|
177 } else {
|
Chris@46
|
178 col = findAvailableColumn(item->row(), col, false);
|
Chris@46
|
179 }
|
Chris@46
|
180 break;
|
Chris@44
|
181 }
|
Chris@44
|
182
|
cannam@45
|
183 std::cerr << "putting " << cs->id().toStdString() << " at col " << col << std::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@49
|
194 // look for merging children and make sure nobody
|
Chris@51
|
195 // is going to overwrite their "merge lines" if they extend further
|
Chris@51
|
196 // than a single step
|
Chris@51
|
197
|
Chris@49
|
198 foreach (QString childId, cs->children()) {
|
Chris@49
|
199 if (!m_changesets.contains(childId)) continue;
|
Chris@49
|
200 Changeset *child = m_changesets[childId];
|
Chris@49
|
201 if (child->parents().size() > 1) {
|
Chris@49
|
202 int childRow = m_items[childId]->row();
|
Chris@51
|
203 for (int r = row; r > childRow; --r) {
|
Chris@49
|
204 m_alloc[r].insert(col);
|
Chris@49
|
205 }
|
Chris@49
|
206 }
|
Chris@49
|
207 }
|
Chris@49
|
208
|
Chris@51
|
209 // look for the case where exactly two children have the same
|
Chris@51
|
210 // branch as us: split them to a little either side of our position
|
Chris@51
|
211
|
Chris@47
|
212 if (nchildren > 1) {
|
Chris@51
|
213
|
Chris@47
|
214 QList<QString> special;
|
Chris@47
|
215 foreach (QString childId, cs->children()) {
|
Chris@47
|
216 if (!m_changesets.contains(childId)) continue;
|
Chris@47
|
217 Changeset *child = m_changesets[childId];
|
Chris@47
|
218 if (child->branch() == branch &&
|
Chris@47
|
219 child->parents().size() == 1) {
|
Chris@47
|
220 special.push_back(childId);
|
Chris@47
|
221 }
|
Chris@47
|
222 }
|
Chris@47
|
223 if (special.size() == 2) {
|
Chris@52
|
224 for (int i = 0; i < 2; ++i) {
|
Chris@52
|
225 int off = i * 2 - 1; // 0 -> -1, 1 -> 1
|
Chris@52
|
226 ChangesetItem *it = m_items[special[i]];
|
Chris@52
|
227 it->setColumn(findAvailableColumn(it->row(), col + off, true));
|
Chris@52
|
228 m_alloc[it->row()].insert(it->column());
|
Chris@52
|
229 m_handled.insert(special[i]);
|
Chris@52
|
230 }
|
Chris@47
|
231 }
|
Chris@47
|
232 }
|
cannam@45
|
233 }
|
cannam@45
|
234
|
Chris@46
|
235 bool
|
Chris@46
|
236 Grapher::rangesConflict(const Range &r1, const Range &r2)
|
Chris@46
|
237 {
|
Chris@46
|
238 // allow some additional space at edges. we really ought also to
|
Chris@46
|
239 // permit space at the end of a branch up to the point where the
|
Chris@46
|
240 // merge happens
|
Chris@46
|
241 int a1 = r1.first - 2, b1 = r1.second + 2;
|
Chris@46
|
242 int a2 = r2.first - 2, b2 = r2.second + 2;
|
Chris@46
|
243 if (a1 > b2 || b1 < a2) return false;
|
Chris@46
|
244 if (a2 > b1 || b2 < a1) return false;
|
Chris@46
|
245 return true;
|
Chris@46
|
246 }
|
Chris@46
|
247
|
Chris@46
|
248 void
|
Chris@46
|
249 Grapher::allocateBranchHomes(Changesets csets)
|
Chris@46
|
250 {
|
Chris@46
|
251 foreach (Changeset *cs, csets) {
|
Chris@46
|
252 QString branch = cs->branch();
|
Chris@46
|
253 ChangesetItem *item = m_items[cs->id()];
|
Chris@46
|
254 if (!item) continue;
|
Chris@46
|
255 int row = item->row();
|
Chris@46
|
256 if (!m_branchRanges.contains(branch)) {
|
Chris@46
|
257 m_branchRanges[branch] = Range(row, row);
|
Chris@46
|
258 } else {
|
Chris@46
|
259 Range p = m_branchRanges[branch];
|
Chris@46
|
260 if (row < p.first) p.first = row;
|
Chris@46
|
261 if (row > p.second) p.second = row;
|
Chris@46
|
262 m_branchRanges[branch] = p;
|
Chris@46
|
263 }
|
Chris@46
|
264 }
|
Chris@46
|
265
|
Chris@46
|
266 m_branchHomes[""] = 0;
|
Chris@46
|
267
|
Chris@46
|
268 foreach (QString branch, m_branchRanges.keys()) {
|
Chris@46
|
269 if (branch == "") continue;
|
Chris@46
|
270 QSet<int> taken;
|
Chris@46
|
271 taken.insert(0);
|
Chris@46
|
272 Range myRange = m_branchRanges[branch];
|
Chris@46
|
273 foreach (QString other, m_branchRanges.keys()) {
|
Chris@46
|
274 if (other == branch || other == "") continue;
|
Chris@46
|
275 Range otherRange = m_branchRanges[other];
|
Chris@46
|
276 if (rangesConflict(myRange, otherRange)) {
|
Chris@46
|
277 if (m_branchHomes.contains(other)) {
|
Chris@46
|
278 taken.insert(m_branchHomes[other]);
|
Chris@46
|
279 }
|
Chris@46
|
280 }
|
Chris@46
|
281 }
|
Chris@47
|
282 int home = 3;
|
Chris@46
|
283 while (taken.contains(home)) {
|
Chris@46
|
284 if (home > 0) home = -home;
|
Chris@47
|
285 else home = -(home-3);
|
Chris@46
|
286 }
|
Chris@46
|
287 m_branchHomes[branch] = home;
|
Chris@46
|
288 }
|
Chris@46
|
289
|
Chris@46
|
290 foreach (QString branch, m_branchRanges.keys()) {
|
Chris@46
|
291 std::cerr << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << std::endl;
|
Chris@46
|
292 }
|
Chris@46
|
293 }
|
Chris@46
|
294
|
Chris@52
|
295 static bool
|
Chris@52
|
296 compareChangesetsByDate(Changeset *const &a, Changeset *const &b)
|
Chris@52
|
297 {
|
Chris@52
|
298 return a->timestamp() < b->timestamp();
|
Chris@52
|
299 }
|
Chris@52
|
300
|
cannam@45
|
301 void
|
cannam@45
|
302 Grapher::layout(Changesets csets)
|
cannam@45
|
303 {
|
Chris@46
|
304 m_changesets.clear();
|
cannam@45
|
305 m_items.clear();
|
cannam@45
|
306 m_alloc.clear();
|
Chris@46
|
307 m_branchHomes.clear();
|
cannam@45
|
308
|
Chris@44
|
309 foreach (Changeset *cs, csets) {
|
cannam@45
|
310
|
cannam@45
|
311 QString id = cs->id();
|
cannam@45
|
312 std::cerr << id.toStdString() << std::endl;
|
cannam@45
|
313
|
cannam@45
|
314 if (id == "") {
|
cannam@45
|
315 throw LayoutException("Changeset has no ID");
|
cannam@45
|
316 }
|
Chris@46
|
317 if (m_changesets.contains(id)) {
|
cannam@45
|
318 throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
|
cannam@45
|
319 }
|
cannam@45
|
320
|
Chris@46
|
321 m_changesets[id] = cs;
|
cannam@45
|
322
|
cannam@45
|
323 ChangesetItem *item = new ChangesetItem(cs);
|
cannam@45
|
324 item->setX(0);
|
cannam@45
|
325 item->setY(0);
|
cannam@45
|
326 m_items[id] = item;
|
cannam@45
|
327 m_scene->addItem(item);
|
cannam@45
|
328 }
|
cannam@45
|
329
|
Chris@46
|
330 foreach (Changeset *cs, csets) {
|
Chris@46
|
331 QString id = cs->id();
|
Chris@46
|
332 ChangesetItem *item = m_items[id];
|
Chris@46
|
333 foreach (QString parentId, cs->parents()) {
|
Chris@47
|
334 if (!m_changesets.contains(parentId)) continue;
|
Chris@47
|
335 Changeset *parent = m_changesets[parentId];
|
Chris@47
|
336 parent->addChild(id);
|
Chris@46
|
337 ConnectionItem *conn = new ConnectionItem();
|
Chris@46
|
338 conn->setChild(item);
|
Chris@46
|
339 conn->setParent(m_items[parentId]);
|
Chris@46
|
340 m_scene->addItem(conn);
|
Chris@46
|
341 }
|
Chris@46
|
342 }
|
Chris@46
|
343
|
Chris@52
|
344 // We need to lay out the changesets in forward chronological
|
Chris@52
|
345 // order. We have no guarantees about the order in which
|
Chris@52
|
346 // changesets appear in the list -- in a simple repository they
|
Chris@52
|
347 // will generally be reverse chronological, but that's far from
|
Chris@52
|
348 // guaranteed. So, sort explicitly using the date comparator
|
Chris@52
|
349 // above
|
Chris@51
|
350
|
Chris@52
|
351 qStableSort(csets.begin(), csets.end(), compareChangesetsByDate);
|
Chris@51
|
352
|
cannam@45
|
353 m_handled.clear();
|
cannam@45
|
354 for (int i = csets.size() - 1; i >= 0; --i) {
|
cannam@45
|
355 layoutRow(csets[i]->id());
|
cannam@45
|
356 }
|
Chris@46
|
357
|
Chris@46
|
358 allocateBranchHomes(csets);
|
Chris@46
|
359
|
cannam@45
|
360 m_handled.clear();
|
cannam@45
|
361 for (int i = csets.size() - 1; i >= 0; --i) {
|
cannam@45
|
362 layoutCol(csets[i]->id());
|
cannam@45
|
363 }
|
Chris@44
|
364 }
|
Chris@44
|
365
|