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