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
|
cannam@45
|
71 std::cerr << "putting " << cs->id().toStdString() << " at row " << row
|
cannam@45
|
72 << std::endl;
|
cannam@45
|
73
|
Chris@44
|
74 item->setRow(row);
|
cannam@45
|
75 m_handled.insert(id);
|
Chris@44
|
76 }
|
Chris@44
|
77
|
Chris@44
|
78 void
|
cannam@45
|
79 Grapher::layoutCol(QString id)
|
Chris@44
|
80 {
|
cannam@45
|
81 if (m_handled.contains(id)) {
|
cannam@45
|
82 std::cerr << "Already looked at " << id.toStdString() << std::endl;
|
cannam@45
|
83 return;
|
cannam@45
|
84 }
|
Chris@46
|
85 if (!m_changesets.contains(id)) {
|
cannam@45
|
86 throw LayoutException(QString("Changeset %1 not in ID map").arg(id));
|
cannam@45
|
87 }
|
cannam@45
|
88 if (!m_items.contains(id)) {
|
cannam@45
|
89 throw LayoutException(QString("Changeset %1 not in item map").arg(id));
|
cannam@45
|
90 }
|
Chris@46
|
91 Changeset *cs = m_changesets[id];
|
cannam@45
|
92 ChangesetItem *item = m_items[id];
|
cannam@45
|
93 std::cerr << "Looking at " << id.toStdString() << std::endl;
|
cannam@45
|
94
|
Chris@46
|
95 foreach (QString parentId, cs->parents()) {
|
Chris@46
|
96 if (!m_changesets.contains(parentId)) continue;
|
Chris@46
|
97 if (!m_handled.contains(parentId)) {
|
Chris@46
|
98 layoutCol(parentId);
|
Chris@46
|
99 }
|
Chris@46
|
100 }
|
Chris@46
|
101
|
Chris@47
|
102 // Parent may have layed out child in the recursive call
|
Chris@47
|
103 if (m_handled.contains(id)) {
|
Chris@47
|
104 std::cerr << "Looks like we've dealt with " << id.toStdString() << std::endl;
|
Chris@47
|
105 return;
|
Chris@47
|
106 }
|
Chris@47
|
107
|
cannam@45
|
108 int col = 0;
|
Chris@46
|
109 int row = item->row();
|
Chris@46
|
110 QString branch = cs->branch();
|
Chris@47
|
111
|
cannam@45
|
112 int nparents = cs->parents().size();
|
Chris@46
|
113 QString parentId;
|
Chris@46
|
114 int parentsOnSameBranch = 0;
|
cannam@45
|
115
|
Chris@46
|
116 switch (nparents) {
|
cannam@45
|
117
|
Chris@46
|
118 case 0:
|
Chris@46
|
119 col = m_branchHomes[cs->branch()];
|
Chris@46
|
120 col = findAvailableColumn(row, col, true);
|
Chris@46
|
121 break;
|
cannam@45
|
122
|
Chris@46
|
123 case 1:
|
Chris@46
|
124 parentId = cs->parents()[0];
|
cannam@45
|
125
|
Chris@46
|
126 if (!m_changesets.contains(parentId) ||
|
Chris@46
|
127 m_changesets[parentId]->branch() != branch) {
|
Chris@46
|
128 // new branch
|
Chris@46
|
129 col = m_branchHomes[branch];
|
Chris@46
|
130 } else {
|
Chris@46
|
131 col = m_items[parentId]->column();
|
Chris@44
|
132 }
|
cannam@45
|
133
|
Chris@46
|
134 col = findAvailableColumn(row, col, true);
|
Chris@46
|
135 break;
|
Chris@46
|
136
|
Chris@46
|
137 case 2:
|
Chris@46
|
138 // a merge: behave differently if parents are both on the same
|
Chris@46
|
139 // branch (we also want to behave differently for nodes that
|
Chris@46
|
140 // have multiple children on the same branch -- spreading them
|
Chris@46
|
141 // out rather than having affinity to a specific branch)
|
Chris@46
|
142
|
Chris@46
|
143 foreach (QString parentId, cs->parents()) {
|
Chris@46
|
144 if (!m_changesets.contains(parentId)) continue;
|
Chris@46
|
145 if (m_changesets[parentId]->branch() == branch) {
|
Chris@46
|
146 ChangesetItem *parentItem = m_items[parentId];
|
Chris@46
|
147 col += parentItem->column();
|
Chris@46
|
148 parentsOnSameBranch++;
|
Chris@46
|
149 }
|
Chris@46
|
150 }
|
Chris@46
|
151
|
Chris@46
|
152 if (parentsOnSameBranch > 0) {
|
Chris@46
|
153 col /= parentsOnSameBranch;
|
Chris@46
|
154 col = findAvailableColumn(item->row(), col, true);
|
Chris@46
|
155 } else {
|
Chris@46
|
156 col = findAvailableColumn(item->row(), col, false);
|
Chris@46
|
157 }
|
Chris@46
|
158 break;
|
Chris@44
|
159 }
|
Chris@44
|
160
|
cannam@45
|
161 std::cerr << "putting " << cs->id().toStdString() << " at col " << col << std::endl;
|
cannam@45
|
162
|
Chris@46
|
163 m_alloc[row].insert(col);
|
cannam@45
|
164 item->setColumn(col);
|
cannam@45
|
165 m_handled.insert(id);
|
Chris@47
|
166
|
Chris@51
|
167 // Normally the children will lay out themselves, but we can do
|
Chris@51
|
168 // a better job in some special cases:
|
Chris@51
|
169
|
Chris@47
|
170 int nchildren = cs->children().size();
|
Chris@49
|
171
|
Chris@49
|
172 // look for merging children and make sure nobody
|
Chris@51
|
173 // is going to overwrite their "merge lines" if they extend further
|
Chris@51
|
174 // than a single step
|
Chris@51
|
175
|
Chris@49
|
176 foreach (QString childId, cs->children()) {
|
Chris@49
|
177 if (!m_changesets.contains(childId)) continue;
|
Chris@49
|
178 Changeset *child = m_changesets[childId];
|
Chris@49
|
179 if (child->parents().size() > 1) {
|
Chris@49
|
180 int childRow = m_items[childId]->row();
|
Chris@49
|
181 std::cerr << "I'm at " << row << ", child with >1 parents is at " << childRow << std::endl;
|
Chris@51
|
182 for (int r = row; r > childRow; --r) {
|
Chris@49
|
183 std::cerr << "setting row " << r << ", col " << col << std::endl;
|
Chris@49
|
184 m_alloc[r].insert(col);
|
Chris@49
|
185 }
|
Chris@49
|
186 }
|
Chris@49
|
187 }
|
Chris@49
|
188
|
Chris@51
|
189 // look for the case where exactly two children have the same
|
Chris@51
|
190 // branch as us: split them to a little either side of our position
|
Chris@51
|
191
|
Chris@47
|
192 if (nchildren > 1) {
|
Chris@51
|
193
|
Chris@47
|
194 QList<QString> special;
|
Chris@47
|
195 foreach (QString childId, cs->children()) {
|
Chris@47
|
196 if (!m_changesets.contains(childId)) continue;
|
Chris@47
|
197 Changeset *child = m_changesets[childId];
|
Chris@47
|
198 if (child->branch() == branch &&
|
Chris@47
|
199 child->parents().size() == 1) {
|
Chris@47
|
200 special.push_back(childId);
|
Chris@47
|
201 }
|
Chris@47
|
202 }
|
Chris@47
|
203 if (special.size() == 2) {
|
Chris@47
|
204 m_items[special[0]]->setColumn
|
Chris@47
|
205 (findAvailableColumn(item->row() - 1, col - 1, true));
|
Chris@47
|
206 m_items[special[1]]->setColumn
|
Chris@47
|
207 (findAvailableColumn(item->row() - 1, col + 1, true));
|
Chris@47
|
208 m_handled.insert(special[0]);
|
Chris@47
|
209 m_handled.insert(special[1]);
|
Chris@47
|
210 }
|
Chris@47
|
211 }
|
cannam@45
|
212 }
|
cannam@45
|
213
|
Chris@46
|
214 bool
|
Chris@46
|
215 Grapher::rangesConflict(const Range &r1, const Range &r2)
|
Chris@46
|
216 {
|
Chris@46
|
217 // allow some additional space at edges. we really ought also to
|
Chris@46
|
218 // permit space at the end of a branch up to the point where the
|
Chris@46
|
219 // merge happens
|
Chris@46
|
220 int a1 = r1.first - 2, b1 = r1.second + 2;
|
Chris@46
|
221 int a2 = r2.first - 2, b2 = r2.second + 2;
|
Chris@46
|
222 if (a1 > b2 || b1 < a2) return false;
|
Chris@46
|
223 if (a2 > b1 || b2 < a1) return false;
|
Chris@46
|
224 return true;
|
Chris@46
|
225 }
|
Chris@46
|
226
|
Chris@46
|
227 void
|
Chris@46
|
228 Grapher::allocateBranchHomes(Changesets csets)
|
Chris@46
|
229 {
|
Chris@46
|
230 foreach (Changeset *cs, csets) {
|
Chris@46
|
231 QString branch = cs->branch();
|
Chris@46
|
232 ChangesetItem *item = m_items[cs->id()];
|
Chris@46
|
233 if (!item) continue;
|
Chris@46
|
234 int row = item->row();
|
Chris@46
|
235 if (!m_branchRanges.contains(branch)) {
|
Chris@46
|
236 m_branchRanges[branch] = Range(row, row);
|
Chris@46
|
237 } else {
|
Chris@46
|
238 Range p = m_branchRanges[branch];
|
Chris@46
|
239 if (row < p.first) p.first = row;
|
Chris@46
|
240 if (row > p.second) p.second = row;
|
Chris@46
|
241 m_branchRanges[branch] = p;
|
Chris@46
|
242 }
|
Chris@46
|
243 }
|
Chris@46
|
244
|
Chris@46
|
245 m_branchHomes[""] = 0;
|
Chris@46
|
246
|
Chris@46
|
247 foreach (QString branch, m_branchRanges.keys()) {
|
Chris@46
|
248 if (branch == "") continue;
|
Chris@46
|
249 QSet<int> taken;
|
Chris@46
|
250 taken.insert(0);
|
Chris@46
|
251 Range myRange = m_branchRanges[branch];
|
Chris@46
|
252 foreach (QString other, m_branchRanges.keys()) {
|
Chris@46
|
253 if (other == branch || other == "") continue;
|
Chris@46
|
254 Range otherRange = m_branchRanges[other];
|
Chris@46
|
255 if (rangesConflict(myRange, otherRange)) {
|
Chris@46
|
256 if (m_branchHomes.contains(other)) {
|
Chris@46
|
257 taken.insert(m_branchHomes[other]);
|
Chris@46
|
258 }
|
Chris@46
|
259 }
|
Chris@46
|
260 }
|
Chris@47
|
261 int home = 3;
|
Chris@46
|
262 while (taken.contains(home)) {
|
Chris@46
|
263 if (home > 0) home = -home;
|
Chris@47
|
264 else home = -(home-3);
|
Chris@46
|
265 }
|
Chris@46
|
266 m_branchHomes[branch] = home;
|
Chris@46
|
267 }
|
Chris@46
|
268
|
Chris@46
|
269 foreach (QString branch, m_branchRanges.keys()) {
|
Chris@46
|
270 std::cerr << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << std::endl;
|
Chris@46
|
271 }
|
Chris@46
|
272 }
|
Chris@46
|
273
|
cannam@45
|
274 void
|
cannam@45
|
275 Grapher::layout(Changesets csets)
|
cannam@45
|
276 {
|
Chris@46
|
277 m_changesets.clear();
|
cannam@45
|
278 m_items.clear();
|
cannam@45
|
279 m_alloc.clear();
|
Chris@46
|
280 m_branchHomes.clear();
|
cannam@45
|
281
|
Chris@44
|
282 foreach (Changeset *cs, csets) {
|
cannam@45
|
283
|
cannam@45
|
284 QString id = cs->id();
|
cannam@45
|
285 std::cerr << id.toStdString() << std::endl;
|
cannam@45
|
286
|
cannam@45
|
287 if (id == "") {
|
cannam@45
|
288 throw LayoutException("Changeset has no ID");
|
cannam@45
|
289 }
|
Chris@46
|
290 if (m_changesets.contains(id)) {
|
cannam@45
|
291 throw LayoutException(QString("Duplicate changeset ID %1").arg(id));
|
cannam@45
|
292 }
|
cannam@45
|
293
|
Chris@46
|
294 m_changesets[id] = cs;
|
cannam@45
|
295
|
cannam@45
|
296 ChangesetItem *item = new ChangesetItem(cs);
|
cannam@45
|
297 item->setX(0);
|
cannam@45
|
298 item->setY(0);
|
cannam@45
|
299 m_items[id] = item;
|
cannam@45
|
300 m_scene->addItem(item);
|
cannam@45
|
301 }
|
cannam@45
|
302
|
Chris@46
|
303 foreach (Changeset *cs, csets) {
|
Chris@46
|
304 QString id = cs->id();
|
Chris@46
|
305 ChangesetItem *item = m_items[id];
|
Chris@46
|
306 foreach (QString parentId, cs->parents()) {
|
Chris@47
|
307 if (!m_changesets.contains(parentId)) continue;
|
Chris@47
|
308 Changeset *parent = m_changesets[parentId];
|
Chris@47
|
309 parent->addChild(id);
|
Chris@46
|
310 ConnectionItem *conn = new ConnectionItem();
|
Chris@46
|
311 conn->setChild(item);
|
Chris@46
|
312 conn->setParent(m_items[parentId]);
|
Chris@46
|
313 m_scene->addItem(conn);
|
Chris@46
|
314 }
|
Chris@46
|
315 }
|
Chris@46
|
316
|
cannam@45
|
317 // Layout in reverse order, i.e. forward chronological order.
|
cannam@45
|
318 // This ensures that parents will normally be laid out before
|
cannam@45
|
319 // their children -- though we can recurse from layout() if we
|
cannam@45
|
320 // find any weird exceptions
|
Chris@51
|
321
|
Chris@51
|
322 //!!! changesets are not in any chronological order, necessarily:
|
Chris@51
|
323 // we need to sort by datetime() [or, better, numerical date]
|
Chris@51
|
324 // and then use m_rowDateMap
|
Chris@51
|
325
|
cannam@45
|
326 m_handled.clear();
|
cannam@45
|
327 for (int i = csets.size() - 1; i >= 0; --i) {
|
cannam@45
|
328 layoutRow(csets[i]->id());
|
cannam@45
|
329 }
|
Chris@46
|
330
|
Chris@46
|
331 allocateBranchHomes(csets);
|
Chris@46
|
332
|
cannam@45
|
333 m_handled.clear();
|
cannam@45
|
334 for (int i = csets.size() - 1; i >= 0; --i) {
|
cannam@45
|
335 layoutCol(csets[i]->id());
|
cannam@45
|
336 }
|
Chris@44
|
337 }
|
Chris@44
|
338
|