1 /*
2 * License: LGPL - See file COPYING.LIB for details.
3 * Author: Stefan Hundhammer <sh@suse.de>
4 * Joshua Hodosh <kdirstat@grumpypenguin.org>
5 */
6
7 #include <QDebug>
8 #include <algorithm>
9 #include <math.h>
10 #include <qimage.h>
11 #include <qpainter.h>
12
13 #include "kdirtreeview.h"
14 #include "ktreemaptile.h"
15 #include "ktreemapview.h"
16
17 using namespace KDirStat;
18 using std::max;
19 using std::min;
20
21 struct childSizeComparator {
operator ()childSizeComparator22 bool operator() (KFileInfo* f1, KFileInfo* f2) {
23 return f1->totalSize() > f2->totalSize();
24 }
25 };
26
sortedChildBySize(KFileInfo * info,KFileSize minSize)27 std::vector<KFileInfo*> sortedChildBySize(KFileInfo * info, KFileSize minSize) {
28 std::vector<KFileInfo*> r;
29 r.reserve(info->numChildren());
30 for(size_t i = 0; i < info->numChildren(); i++) {
31 if(info->child(i)->totalSize() >= minSize)
32 r.push_back(info->child(i));
33 }
34 if(info->dotEntry() && info->dotEntry()->totalSize() >= minSize)
35 r.push_back(info->dotEntry());
36 std::sort(r.begin(), r.end(), childSizeComparator());
37 r.shrink_to_fit();
38 return r;
39 }
40
KTreemapTile(KTreemapView * parentView,KTreemapTile * parentTile,KFileInfo * orig,const QRectF & rect,KOrientation orientation)41 KTreemapTile::KTreemapTile(KTreemapView *parentView, KTreemapTile *parentTile,
42 KFileInfo *orig, const QRectF &rect,
43 KOrientation orientation)
44 : QGraphicsRectItem(rect, parentTile), _parentView(parentView),
45 _parentTile(parentTile), _orig(orig) {
46 init();
47
48 if (parentTile)
49 _cushionSurface = parentTile->cushionSurface();
50
51 createChildren(rect, orientation);
52 }
53
KTreemapTile(KTreemapView * parentView,KTreemapTile * parentTile,KFileInfo * orig,const QRect & rect,const KCushionSurface & cushionSurface,KOrientation orientation)54 KTreemapTile::KTreemapTile(KTreemapView *parentView, KTreemapTile *parentTile,
55 KFileInfo *orig, const QRect &rect,
56 const KCushionSurface &cushionSurface,
57 KOrientation orientation)
58 : QGraphicsRectItem(rect, parentTile), _parentView(parentView),
59 _parentTile(parentTile), _orig(orig), _cushionSurface(cushionSurface) {
60 init();
61
62 // Intentionally not copying the parent's cushion surface!
63
64 createChildren(rect, orientation);
65 }
66
~KTreemapTile()67 KTreemapTile::~KTreemapTile() {
68 // NOP
69 }
70
init()71 void KTreemapTile::init() {
72 // Set up height (z coordinate) - one level higher than the parent so this
73 // will be closer to the foreground.
74 //
75 // Note that this must happen before any children are created.
76 // I found that out the hard way. ;-)
77
78 setZValue(_parentTile ? (_parentTile->zValue() + 1.0) : 0.0);
79
80 setBrush(QColor(0x60, 0x60, 0x60));
81 setPen(Qt::NoPen);
82
83 show(); // QCanvasItems are invisible by default!
84
85 // qDebug() << "Creating treemap tile for " << _orig
86 // << " size " << formatSize( _orig->totalSize() ) << endl;
87 }
88
createChildren(const QRectF & rect,KOrientation orientation)89 void KTreemapTile::createChildren(const QRectF &rect,
90 KOrientation orientation) {
91 if (_orig->totalSize() == 0) // Prevent division by zero
92 return;
93
94 if (_parentView->squarify())
95 createSquarifiedChildren(rect);
96 else
97 createChildrenSimple(rect, orientation);
98 }
99
createChildrenSimple(const QRectF & rect,KOrientation orientation)100 void KTreemapTile::createChildrenSimple(const QRectF &rect,
101 KOrientation orientation) {
102
103 KOrientation dir = orientation;
104 KOrientation childDir = orientation;
105
106 if (dir == KTreemapAuto)
107 dir = rect.width() > rect.height() ? KTreemapHorizontal : KTreemapVertical;
108
109 if (orientation == KTreemapHorizontal)
110 childDir = KTreemapVertical;
111 if (orientation == KTreemapVertical)
112 childDir = KTreemapHorizontal;
113
114 int offset = 0;
115 int size = dir == KTreemapHorizontal ? rect.width() : rect.height();
116 int count = 0;
117 double scale = (double)size / (double)_orig->totalSize();
118
119 _cushionSurface.addRidge(childDir, _cushionSurface.height(), rect);
120 std::vector<KFileInfo*> sorted = sortedChildBySize(
121 _orig, _parentView->minTileSize() / scale);
122
123 for(size_t i = 0; i < sorted.size(); i++) {
124 QRect childRect;
125 int childSize = scale * sorted[i]->totalSize();
126 assert(childSize >= _parentView->minTileSize());;
127 if (dir == KTreemapHorizontal)
128 childRect = QRect(rect.x() + offset, rect.y(), childSize, rect.height());
129 else
130 childRect = QRect(rect.x(), rect.y() + offset, rect.width(), childSize);
131
132 KTreemapTile *tile = new KTreemapTile(_parentView, this, sorted[i], childRect, childDir);
133 Q_CHECK_PTR(tile);
134
135 tile->cushionSurface().addRidge(
136 dir, _cushionSurface.height() * _parentView->heightScaleFactor(),
137 childRect);
138
139 offset += childSize;
140 ++count;
141 }
142 }
143
createSquarifiedChildren(const QRectF & rect)144 void KTreemapTile::createSquarifiedChildren(const QRectF &rect) {
145 if (_orig->totalSize() == 0) {
146 qCritical() << Q_FUNC_INFO << "Zero totalSize()" << Qt::endl;
147 return;
148 }
149
150 double scale = rect.width() * (double)rect.height() / _orig->totalSize();
151 KFileSize minSize = (KFileSize)(_parentView->minTileSize() / scale);
152
153 #if 0
154 if ( _orig->hasChildren() )
155 {
156 _cushionSurface.addRidge( KTreemapHorizontal, _cushionSurface.height(), rect );
157 _cushionSurface.addRidge( KTreemapVertical, _cushionSurface.height(), rect );
158 }
159 #endif
160
161 std::vector<KFileInfo*> sorted = sortedChildBySize(_orig, minSize);
162 std::vector<KFileInfo*>::iterator it = sorted.begin();
163 QRectF childrenRect = rect;
164 for(size_t i = 0; i < sorted.size(); i++) {
165
166 }
167 std::vector<KFileInfo*> row;
168 while (it != sorted.end()) {
169 row.clear();
170 squarify(childrenRect, scale, it, sorted.end(), row);
171 childrenRect = layoutRow(childrenRect, scale, row);
172 }
173 }
174
squarify(const QRectF & rect,double scale,std::vector<KFileInfo * >::iterator & it,std::vector<KFileInfo * >::iterator end,std::vector<KFileInfo * > & row)175 void KTreemapTile::squarify(const QRectF &rect, double scale,
176 std::vector<KFileInfo*>::iterator &it,
177 std::vector<KFileInfo*>::iterator end,
178 std::vector<KFileInfo*> & row) {
179 // qDebug() << "squarify() " << _orig << " " << rect << endl;
180 int length = max(rect.width(), rect.height());
181
182 if (length == 0) // Sanity check
183 {
184 qWarning() << Q_FUNC_INFO << "Zero length";
185
186 if (it != end) // Prevent endless loop in case of error:
187 ++it; // Advance iterator.
188 return;
189 }
190
191 bool improvingAspectRatio = true;
192 double lastWorstAspectRatio = -1.0;
193 double sum = 0;
194
195 // This is a bit ugly, but doing all calculations in the 'size' dimension
196 // is more efficient here since that requires only one scaling before
197 // doing all other calculations in the loop.
198 const double scaledLengthSquare = length * (double)length / scale;
199
200 while (it != end && improvingAspectRatio) {
201 sum += (*it)->totalSize();
202
203 if (!row.empty() && sum != 0 && (*it)->totalSize() != 0) {
204 double sumSquare = sum * sum;
205 double worstAspectRatio =
206 max(scaledLengthSquare * row[0]->totalSize() / sumSquare,
207 sumSquare / (scaledLengthSquare * (*it)->totalSize()));
208
209 if (lastWorstAspectRatio >= 0.0 &&
210 worstAspectRatio > lastWorstAspectRatio) {
211 improvingAspectRatio = false;
212 }
213
214 lastWorstAspectRatio = worstAspectRatio;
215 }
216
217 if (improvingAspectRatio) {
218 // qDebug() << "Adding " << *it << " size " << (*it)->totalSize() << endl;
219 row.push_back(*it);
220 ++it;
221 } else {
222 // qDebug() << "Getting worse after adding " << *it << " size " <<
223 // (*it)->totalSize() << endl;
224 }
225 }
226 }
227
layoutRow(const QRectF & rect,double scale,std::vector<KFileInfo * > & row)228 QRectF KTreemapTile::layoutRow(const QRectF &rect, double scale,
229 std::vector<KFileInfo*> &row) {
230 if (row.empty())
231 return rect;
232
233 // Determine the direction in which to subdivide.
234 // We always use the longer side of the rectangle.
235 KOrientation dir =
236 rect.width() > rect.height() ? KTreemapHorizontal : KTreemapVertical;
237
238 // This row's primary length is the longer one.
239 int primary = max(rect.width(), rect.height());
240
241 // This row's secondary length is determined by the area (the number of
242 // pixels) to be allocated for all of the row's items.
243 KFileSize sum = 0;
244 for(size_t i = 0; i < row.size(); i++)
245 sum += row[i]->totalSize();
246 int secondary = (int)(sum * scale / primary);
247
248 if (sum == 0) // Prevent division by zero.
249 return rect;
250
251 if (secondary < _parentView->minTileSize()) // We don't want tiles that small.
252 return rect;
253
254 // Set up a cushion surface for this layout row:
255 // Add another ridge perpendicular to the row's direction
256 // that optically groups this row's tiles together.
257
258 KCushionSurface rowCushionSurface = _cushionSurface;
259
260 rowCushionSurface.addRidge(
261 dir == KTreemapHorizontal ? KTreemapVertical : KTreemapHorizontal,
262 _cushionSurface.height() * _parentView->heightScaleFactor(), rect);
263
264 int offset = 0;
265 int remaining = primary;
266
267 foreach (KFileInfo *it, row) {
268 int childSize = (int)(it->totalSize() / (double)sum * primary + 0.5);
269
270 if (childSize >
271 remaining) // Prevent overflow because of accumulated rounding errors
272 childSize = remaining;
273
274 remaining -= childSize;
275
276 if (childSize >= _parentView->minTileSize()) {
277 QRect childRect;
278
279 if (dir == KTreemapHorizontal)
280 childRect = QRect(rect.x() + offset, rect.y(), childSize, secondary);
281 else
282 childRect = QRect(rect.x(), rect.y() + offset, secondary, childSize);
283
284 KTreemapTile *tile =
285 new KTreemapTile(_parentView, this, it, childRect, rowCushionSurface);
286 Q_CHECK_PTR(tile);
287
288 tile->cushionSurface().addRidge(
289 dir, rowCushionSurface.height() * _parentView->heightScaleFactor(),
290 childRect);
291 offset += childSize;
292 }
293
294 ++it;
295 }
296
297 // Subtract the layouted area from the rectangle.
298
299 QRect newRect;
300
301 if (dir == KTreemapHorizontal)
302 newRect = QRect(rect.x(), rect.y() + secondary, rect.width(),
303 rect.height() - secondary);
304 else
305 newRect = QRect(rect.x() + secondary, rect.y(), rect.width() - secondary,
306 rect.height());
307
308 // qDebug() << "Left over:" << " " << newRect << " " << _orig << endl;
309
310 return newRect;
311 }
312
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)313 void KTreemapTile::paint(QPainter *painter,
314 const QStyleOptionGraphicsItem *option,
315 QWidget *widget) {
316 QSizeF size = rect().size();
317
318 if (size.height() < 1 || size.width() < 1)
319 return;
320
321 if (_parentView->doCushionShading()) {
322 if (_orig->isDir() || _orig->isDotEntry()) {
323 QGraphicsRectItem::paint(painter, option, widget);
324 } else {
325 if (_cushion.isNull())
326 _cushion = renderCushion();
327
328 QRectF rect = QGraphicsRectItem::rect();
329
330 if (!_cushion.isNull())
331 painter->drawPixmap(rect, _cushion, _cushion.rect());
332
333 if (_parentView->forceCushionGrid()) {
334 // Draw a clearly visible boundary
335
336 painter->setPen(QPen(_parentView->cushionGridColor(), 1));
337
338 if (rect.x() > 0)
339 painter->drawLine(rect.topLeft(), rect.bottomLeft() + QPoint(0, 1));
340
341 if (rect.y() > 0)
342 painter->drawLine(rect.topLeft(), rect.topRight() + QPoint(1, 0));
343 }
344 }
345 } else // No cushion shading, use plain tiles
346 {
347 painter->setPen(QPen(_parentView->outlineColor(), 1));
348
349 if (_orig->isDir() || _orig->isDotEntry())
350 painter->setBrush(_parentView->dirFillColor());
351 else {
352 painter->setBrush(_parentView->tileColor(_orig));
353 #if 0
354 painter->setBrush( _parentView->fileFillColor() );
355 #endif
356 }
357 painter->drawRect(rect());
358 }
359 }
360
renderCushion()361 QPixmap KTreemapTile::renderCushion() {
362 QRectF rect = QGraphicsRectItem::rect();
363
364 if (rect.width() < 1 || rect.height() < 1)
365 return QPixmap();
366
367 // qDebug() << Q_FUNC_INFO << endl;
368
369 double nx;
370 double ny;
371 double cosa;
372 int x, y;
373 int red, green, blue;
374
375 // Cache some values. They are used for each loop iteration, so let's try
376 // to keep multiple indirect references down.
377
378 int ambientLight = parentView()->ambientLight();
379 double lightX = parentView()->lightX();
380 double lightY = parentView()->lightY();
381 double lightZ = parentView()->lightZ();
382
383 double xx2 = cushionSurface().xx2();
384 double xx1 = cushionSurface().xx1();
385 double yy2 = cushionSurface().yy2();
386 double yy1 = cushionSurface().yy1();
387
388 int x0 = rect.x();
389 int y0 = rect.y();
390
391 QColor color = parentView()->tileColor(_orig);
392 int maxRed = max(0, color.red() - ambientLight);
393 int maxGreen = max(0, color.green() - ambientLight);
394 int maxBlue = max(0, color.blue() - ambientLight);
395
396 QImage image(rect.width(), rect.height(), QImage::Format_RGB32);
397
398 for (y = 0; y < rect.height(); y++) {
399 for (x = 0; x < rect.width(); x++) {
400 nx = 2.0 * xx2 * (x + x0) + xx1;
401 ny = 2.0 * yy2 * (y + y0) + yy1;
402 cosa =
403 (nx * lightX + ny * lightY + lightZ) / sqrt(nx * nx + ny * ny + 1.0);
404
405 red = (int)(maxRed * cosa + 0.5);
406 green = (int)(maxGreen * cosa + 0.5);
407 blue = (int)(maxBlue * cosa + 0.5);
408
409 if (red < 0)
410 red = 0;
411 if (green < 0)
412 green = 0;
413 if (blue < 0)
414 blue = 0;
415
416 red += ambientLight;
417 green += ambientLight;
418 blue += ambientLight;
419
420 image.setPixel(x, y, qRgb(red, green, blue));
421 }
422 }
423
424 if (_parentView->ensureContrast())
425 ensureContrast(image);
426
427 return QPixmap::fromImage(image);
428 }
429
ensureContrast(QImage & image)430 void KTreemapTile::ensureContrast(QImage &image) {
431 if (image.width() > 5) {
432 // Check contrast along the right image boundary:
433 //
434 // Compare samples from the outmost boundary to samples a few pixels to
435 // the inside and count identical pixel values. A number of identical
436 // pixels are tolerated, but not too many.
437
438 int x1 = image.width() - 6;
439 int x2 = image.width() - 1;
440 int interval = max(image.height() / 10, 5);
441 int sameColorCount = 0;
442
443 // Take samples
444
445 for (int y = interval; y < image.height(); y += interval) {
446 if (image.pixel(x1, y) == image.pixel(x2, y))
447 sameColorCount++;
448 }
449
450 if (sameColorCount * 10 > image.height()) {
451 // Add a line at the right boundary
452
453 QRgb val = contrastingColor(image.pixel(x2, image.height() / 2));
454
455 for (int y = 0; y < image.height(); y++)
456 image.setPixel(x2, y, val);
457 }
458 }
459
460 if (image.height() > 5) {
461 // Check contrast along the bottom boundary
462
463 int y1 = image.height() - 6;
464 int y2 = image.height() - 1;
465 int interval = max(image.width() / 10, 5);
466 int sameColorCount = 0;
467
468 for (int x = interval; x < image.width(); x += interval) {
469 if (image.pixel(x, y1) == image.pixel(x, y2))
470 sameColorCount++;
471 }
472
473 if (sameColorCount * 10 > image.height()) {
474 // Add a grey line at the bottom boundary
475
476 QRgb val = contrastingColor(image.pixel(image.width() / 2, y2));
477
478 for (int x = 0; x < image.width(); x++)
479 image.setPixel(x, y2, val);
480 }
481 }
482 }
483
contrastingColor(QRgb col)484 QRgb KTreemapTile::contrastingColor(QRgb col) {
485 if (qGray(col) < 128)
486 return qRgb(qRed(col) * 2, qGreen(col) * 2, qBlue(col) * 2);
487 else
488 return qRgb(qRed(col) / 2, qGreen(col) / 2, qBlue(col) / 2);
489 }
490
KCushionSurface()491 KCushionSurface::KCushionSurface() {
492 _xx2 = 0.0;
493 _xx1 = 0.0;
494 _yy2 = 0.0;
495 _yy1 = 0.0;
496 _height = CushionHeight;
497 }
498
addRidge(KOrientation dim,double height,const QRectF & rect)499 void KCushionSurface::addRidge(KOrientation dim, double height,
500 const QRectF &rect) {
501 _height = height;
502
503 if (dim == KTreemapHorizontal) {
504 _xx2 = squareRidge(_xx2, _height, rect.left(), rect.right());
505 _xx1 = linearRidge(_xx1, _height, rect.left(), rect.right());
506 } else {
507 _yy2 = squareRidge(_yy2, _height, rect.top(), rect.bottom());
508 _yy1 = linearRidge(_yy1, _height, rect.top(), rect.bottom());
509 }
510 }
511
squareRidge(double squareCoefficient,double height,int x1,int x2)512 double KCushionSurface::squareRidge(double squareCoefficient, double height,
513 int x1, int x2) {
514 if (x2 != x1) // Avoid division by zero
515 squareCoefficient -= 4.0 * height / (x2 - x1);
516
517 return squareCoefficient;
518 }
519
linearRidge(double linearCoefficient,double height,int x1,int x2)520 double KCushionSurface::linearRidge(double linearCoefficient, double height,
521 int x1, int x2) {
522 if (x2 != x1) // Avoid division by zero
523 linearCoefficient += 4.0 * height * (x2 + x1) / (x2 - x1);
524
525 return linearCoefficient;
526 }
527
528