/* * File name: ktreemaptile.cpp * Summary: High level classes for KDirStat * License: LGPL - See file COPYING.LIB for details. * Author: Stefan Hundhammer * * Updated: 2003-01-30 */ #include #include #include #include #include #include #include #include "ktreemaptile.h" #include "ktreemapview.h" #include "kdirtreeiterators.h" #include "kdirtreeview.h" using namespace KDirStat; using std::max; using std::min; KTreemapTile::KTreemapTile( KTreemapView * parentView, KTreemapTile * parentTile, KFileInfo * orig, const TQRect & rect, KOrientation orientation ) : TQCanvasRectangle( rect, parentView->canvas() ) , _parentView( parentView ) , _parentTile( parentTile ) , _orig( orig ) { init(); if ( parentTile ) _cushionSurface = parentTile->cushionSurface(); createChildren( rect, orientation ); } KTreemapTile::KTreemapTile( KTreemapView * parentView, KTreemapTile * parentTile, KFileInfo * orig, const TQRect & rect, const KCushionSurface & cushionSurface, KOrientation orientation ) : TQCanvasRectangle( rect, parentView->canvas() ) , _parentView( parentView ) , _parentTile( parentTile ) , _orig( orig ) , _cushionSurface( cushionSurface ) { init(); // Intentionally not copying the parent's cushion surface! createChildren( rect, orientation ); } KTreemapTile::~KTreemapTile() { // NOP } void KTreemapTile::init() { // Set up height (z coordinate) - one level higher than the parent so this // will be closer to the foreground. // // Note that this must happen before any children are created. // I found that out the hard way. ;-) setZ( _parentTile ? ( _parentTile->z() + 1.0 ) : 0.0 ); setBrush( TQColor( 0x60, 0x60, 0x60 ) ); setPen( TQt::NoPen ); show(); // TQCanvasItems are invisible by default! // kdDebug() << "Creating treemap tile for " << orig << " " << rect << " size " << orig->totalSize() << endl; } void KTreemapTile::createChildren( const TQRect & rect, KOrientation orientation ) { if ( _orig->totalSize() == 0 ) // Prevent division by zero return; if ( _parentView->squarify() ) createSquarifiedChildren( rect ); else createChildrenSimple( rect, orientation ); } void KTreemapTile::createChildrenSimple( const TQRect & rect, KOrientation orientation ) { KOrientation dir = orientation; KOrientation childDir = orientation; if ( dir == KTreemapAuto ) dir = rect.width() > rect.height() ? KTreemapHorizontal : KTreemapVertical; if ( orientation == KTreemapHorizontal ) childDir = KTreemapVertical; if ( orientation == KTreemapVertical ) childDir = KTreemapHorizontal; int offset = 0; int size = dir == KTreemapHorizontal ? rect.width() : rect.height(); int count = 0; double scale = (double) size / (double) _orig->totalSize(); _cushionSurface.addRidge( childDir, _cushionSurface.height(), rect ); KFileInfoSortedBySizeIterator it( _orig, (KFileSize) ( _parentView->minTileSize() / scale ), KDotEntryAsSubDir ); while ( *it ) { int childSize = 0; childSize = (int) ( scale * (*it)->totalSize() ); if ( childSize >= _parentView->minTileSize() ) { TQRect childRect; if ( dir == KTreemapHorizontal ) childRect = TQRect( rect.x() + offset, rect.y(), childSize, rect.height() ); else childRect = TQRect( rect.x(), rect.y() + offset, rect.width(), childSize ); KTreemapTile * tile = new KTreemapTile( _parentView, this, *it, childRect, childDir ); TQ_CHECK_PTR( tile ); tile->cushionSurface().addRidge( dir, _cushionSurface.height() * _parentView->heightScaleFactor(), childRect ); offset += childSize; } ++count; ++it; } } void KTreemapTile::createSquarifiedChildren( const TQRect & rect ) { if ( _orig->totalSize() == 0 ) { kdError() << k_funcinfo << "Zero totalSize()" << endl; return; } double scale = rect.width() * (double) rect.height() / _orig->totalSize(); KFileSize minSize = (KFileSize) ( _parentView->minTileSize() / scale ); #if 0 if ( _orig->hasChildren() ) { _cushionSurface.addRidge( KTreemapHorizontal, _cushionSurface.height(), rect ); _cushionSurface.addRidge( KTreemapVertical, _cushionSurface.height(), rect ); } #endif KFileInfoSortedBySizeIterator it( _orig, minSize, KDotEntryAsSubDir ); TQRect childrenRect = rect; while ( *it ) { KFileInfoList row = squarify( childrenRect, scale, it ); childrenRect = layoutRow( childrenRect, scale, row ); } } KFileInfoList KTreemapTile::squarify( const TQRect & rect, double scale, KFileInfoSortedBySizeIterator & it ) { // kdDebug() << "squarify() " << _orig << " " << rect << endl; KFileInfoList row; int length = max( rect.width(), rect.height() ); if ( length == 0 ) // Sanity check { kdWarning() << k_funcinfo << "Zero length" << endl; if ( *it ) // Prevent endless loop in case of error: ++it; // Advance iterator. return row; } bool improvingAspectRatio = true; double lastWorstAspectRatio = -1.0; double sum = 0; // This is a bit ugly, but doing all calculations in the 'size' dimension // is more efficient here since that requires only one scaling before // doing all other calculations in the loop. const double scaledLengthSquare = length * (double) length / scale; while ( *it && improvingAspectRatio ) { sum += (*it)->totalSize(); if ( ! row.isEmpty() && sum != 0 && (*it)->totalSize() != 0 ) { double sumSquare = sum * sum; double worstAspectRatio = max( scaledLengthSquare * row.first()->totalSize() / sumSquare, sumSquare / ( scaledLengthSquare * (*it)->totalSize() ) ); if ( lastWorstAspectRatio >= 0.0 && worstAspectRatio > lastWorstAspectRatio ) { improvingAspectRatio = false; } lastWorstAspectRatio = worstAspectRatio; } if ( improvingAspectRatio ) { // kdDebug() << "Adding " << *it << " size " << (*it)->totalSize() << endl; row.append( *it ); ++it; } else { // kdDebug() << "Getting worse after adding " << *it << " size " << (*it)->totalSize() << endl; } } return row; } TQRect KTreemapTile::layoutRow( const TQRect & rect, double scale, KFileInfoList & row ) { if ( row.isEmpty() ) return rect; // Determine the direction in which to subdivide. // We always use the longer side of the rectangle. KOrientation dir = rect.width() > rect.height() ? KTreemapHorizontal : KTreemapVertical; // This row's primary length is the longer one. int primary = max( rect.width(), rect.height() ); // This row's secondary length is determined by the area (the number of // pixels) to be allocated for all of the row's items. KFileSize sum = row.sumTotalSizes(); int secondary = (int) ( sum * scale / primary ); if ( sum == 0 ) // Prevent division by zero. return rect; if ( secondary < _parentView->minTileSize() ) // We don't want tiles that small. return rect; // Set up a cushion surface for this layout row: // Add another ridge perpendicular to the row's direction // that optically groups this row's tiles together. KCushionSurface rowCushionSurface = _cushionSurface; rowCushionSurface.addRidge( dir == KTreemapHorizontal ? KTreemapVertical : KTreemapHorizontal, _cushionSurface.height() * _parentView->heightScaleFactor(), rect ); int offset = 0; int remaining = primary; KFileInfoListIterator it( row ); while ( *it ) { int childSize = (int) ( (*it)->totalSize() / (double) sum * primary + 0.5 ); if ( childSize > remaining ) // Prevent overflow because of accumulated rounding errors childSize = remaining; remaining -= childSize; if ( childSize >= _parentView->minTileSize() ) { TQRect childRect; if ( dir == KTreemapHorizontal ) childRect = TQRect( rect.x() + offset, rect.y(), childSize, secondary ); else childRect = TQRect( rect.x(), rect.y() + offset, secondary, childSize ); KTreemapTile * tile = new KTreemapTile( _parentView, this, *it, childRect, rowCushionSurface ); TQ_CHECK_PTR( tile ); tile->cushionSurface().addRidge( dir, rowCushionSurface.height() * _parentView->heightScaleFactor(), childRect ); offset += childSize; } ++it; } // Subtract the layouted area from the rectangle. TQRect newRect; if ( dir == KTreemapHorizontal ) newRect = TQRect( rect.x(), rect.y() + secondary, rect.width(), rect.height() - secondary ); else newRect = TQRect( rect.x() + secondary, rect.y(), rect.width() - secondary, rect.height() ); // kdDebug() << "Left over:" << " " << newRect << " " << _orig << endl; return newRect; } void KTreemapTile::drawShape( TQPainter & painter ) { // kdDebug() << k_funcinfo << endl; TQSize size = rect().size(); if ( size.height() < 1 || size.width() < 1 ) return; if ( _parentView->doCushionShading() ) { if ( _orig->isDir() || _orig->isDotEntry() ) { TQCanvasRectangle::drawShape( painter ); } else { if ( _cushion.isNull() ) _cushion = renderCushion(); TQRect rect = TQCanvasRectangle::rect(); if ( ! _cushion.isNull() ) painter.drawPixmap( rect, _cushion ); if ( _parentView->forceCushionGrid() ) { // Draw a clearly visible boundary painter.setPen( TQPen( _parentView->cushionGridColor(), 1 ) ); if ( rect.x() > 0 ) painter.drawLine( rect.topLeft(), rect.bottomLeft() + TQPoint( 0, 1 ) ); if ( rect.y() > 0 ) painter.drawLine( rect.topLeft(), rect.topRight() + TQPoint( 1, 0 ) ); } } } else // No cushion shading, use plain tiles { painter.setPen( TQPen( _parentView->outlineColor(), 1 ) ); if ( _orig->isDir() || _orig->isDotEntry() ) painter.setBrush( _parentView->dirFillColor() ); else { painter.setBrush( _parentView->tileColor( _orig ) ); #if 0 painter.setBrush( _parentView->fileFillColor() ); #endif } TQCanvasRectangle::drawShape( painter ); } } TQPixmap KTreemapTile::renderCushion() { TQRect rect = TQCanvasRectangle::rect(); if ( rect.width() < 1 || rect.height() < 1 ) return TQPixmap(); // kdDebug() << k_funcinfo << endl; double nx; double ny; double cosa; int x, y; int red, green, blue; // Cache some values. They are used for each loop iteration, so let's try // to keep multiple indirect references down. int ambientLight = parentView()->ambientLight(); double lightX = parentView()->lightX(); double lightY = parentView()->lightY(); double lightZ = parentView()->lightZ(); double xx2 = cushionSurface().xx2(); double xx1 = cushionSurface().xx1(); double yy2 = cushionSurface().yy2(); double yy1 = cushionSurface().yy1(); int x0 = rect.x(); int y0 = rect.y(); TQColor color = parentView()->tileColor( _orig ); int maxRed = max( 0, color.red() - ambientLight ); int maxGreen = max( 0, color.green() - ambientLight ); int maxBlue = max( 0, color.blue() - ambientLight ); TQImage image( rect.width(), rect.height(), 32 ); for ( y = 0; y < rect.height(); y++ ) { for ( x = 0; x < rect.width(); x++ ) { nx = 2.0 * xx2 * (x+x0) + xx1; ny = 2.0 * yy2 * (y+y0) + yy1; cosa = ( nx * lightX + ny * lightY + lightZ ) / sqrt( nx*nx + ny*ny + 1.0 ); red = (int) ( maxRed * cosa + 0.5 ); green = (int) ( maxGreen * cosa + 0.5 ); blue = (int) ( maxBlue * cosa + 0.5 ); if ( red < 0 ) red = 0; if ( green < 0 ) green = 0; if ( blue < 0 ) blue = 0; red += ambientLight; green += ambientLight; blue += ambientLight; image.setPixel( x, y, tqRgb( red, green, blue) ); } } if ( _parentView->ensureContrast() ) ensureContrast( image ); return TQPixmap( image ); } void KTreemapTile::ensureContrast( TQImage & image ) { if ( image.width() > 5 ) { // Check contrast along the right image boundary: // // Compare samples from the outmost boundary to samples a few pixels to // the inside and count identical pixel values. A number of identical // pixels are tolerated, but not too many. int x1 = image.width() - 6; int x2 = image.width() - 1; int interval = max( image.height() / 10, 5 ); int sameColorCount = 0; // Take samples for ( int y = interval; y < image.height(); y+= interval ) { if ( image.pixel( x1, y ) == image.pixel( x2, y ) ) sameColorCount++; } if ( sameColorCount * 10 > image.height() ) { // Add a line at the right boundary TQRgb val = contrastingColor( image.pixel( x2, image.height() / 2 ) ); for ( int y = 0; y < image.height(); y++ ) image.setPixel( x2, y, val ); } } if ( image.height() > 5 ) { // Check contrast along the bottom boundary int y1 = image.height() - 6; int y2 = image.height() - 1; int interval = max( image.width() / 10, 5 ); int sameColorCount = 0; for ( int x = interval; x < image.width(); x += interval ) { if ( image.pixel( x, y1 ) == image.pixel( x, y2 ) ) sameColorCount++; } if ( sameColorCount * 10 > image.height() ) { // Add a grey line at the bottom boundary TQRgb val = contrastingColor( image.pixel( image.width() / 2, y2 ) ); for ( int x = 0; x < image.width(); x++ ) image.setPixel( x, y2, val ); } } } TQRgb KTreemapTile::contrastingColor( TQRgb col ) { if ( tqGray( col ) < 128 ) return tqRgb( tqRed( col ) * 2, tqGreen( col ) * 2, tqBlue( col ) * 2 ); else return tqRgb( tqRed( col ) / 2, tqGreen( col ) / 2, tqBlue( col ) / 2 ); } KCushionSurface::KCushionSurface() { _xx2 = 0.0; _xx1 = 0.0; _yy2 = 0.0; _yy1 = 0.0; _height = CushionHeight; } void KCushionSurface::addRidge( KOrientation dim, double height, const TQRect & rect ) { _height = height; if ( dim == KTreemapHorizontal ) { _xx2 = squareRidge( _xx2, _height, rect.left(), rect.right() ); _xx1 = linearRidge( _xx1, _height, rect.left(), rect.right() ); } else { _yy2 = squareRidge( _yy2, _height, rect.top(), rect.bottom() ); _yy1 = linearRidge( _yy1, _height, rect.top(), rect.bottom() ); } } double KCushionSurface::squareRidge( double squareCoefficient, double height, int x1, int x2 ) { if ( x2 != x1 ) // Avoid division by zero squareCoefficient -= 4.0 * height / ( x2 - x1 ); return squareCoefficient; } double KCushionSurface::linearRidge( double linearCoefficient, double height, int x1, int x2 ) { if ( x2 != x1 ) // Avoid division by zero linearCoefficient += 4.0 * height * ( x2 + x1 ) / ( x2 - x1 ); return linearCoefficient; } // EOF