(empty message)
[qanava.git] / src / la / laTimeTree.cpp
blob0499733014713bacc542a8d3e9fe1a8526c67242
1 /*
2 Qanava - Graph drawing library for QT
3 Copyright (C) 2005 Benoit AUTHEMAN
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 //-----------------------------------------------------------------------------
21 // This file is a part of the Qanava software.
23 // \file laLayout.cpp
24 // \author Benoit Autheman (benoit@faktiss.net)
25 // \date 2004 December 05
26 //-----------------------------------------------------------------------------
29 // Qanava haders
30 #include "laTimeTree.h"
33 // Std headers
34 #include <iterator>
37 // Boost headers
38 #ifndef DOXYSKIP
39 #include "boost/date_time/posix_time/posix_time.hpp"
41 using namespace boost::posix_time;
42 using namespace boost::gregorian;
43 #endif
46 namespace qan { // ::qan
47 namespace la { // ::qan::la
50 bool TimeTree::NodeDateComp::operator()( const Node* n1, const Node* n2 ) const
52 const boost::posix_time::ptime* d1 = n1->getDate( );
53 const boost::posix_time::ptime* d2 = n2->getDate( );
54 if ( d1 == 0 || d2 == 0 )
55 return false;
56 return ( *d1 < *d2 );
59 /* TimeTree Grid Management *///-----------------------------------------------
60 /*!
61 \param cy Y coordinate of the cluster top left corner
62 \param w Width of the cluster.
63 \param h Height of the cluster.
65 void TimeTree::GridBuilder::buildClusterGrid( int cy, int w, int h, int r, int g, int b )
67 _grid.addLine( 0, cy + h, w, cy + h, 1, false, false );
68 _grid.addRectangle( 0, cy, w, h + 1, r, g, b );
71 /*!
72 \param sortedNodes Set of time sorted nodes already placed on the canvas.
73 \param maxX, maxY Maximum coordinate for the nodes in the current layout.
75 void TimeTree::GridBuilder::buildTimeGrid( NodeSet& nodes, int, int height )
77 // Draw vertical time grid lines
78 float x = 10.f;
79 const boost::posix_time::ptime* lastNodeDate = 0;
80 for ( NodeSet::iterator nodeIter = nodes.begin( ); nodeIter != nodes.end( ); nodeIter++ )
82 // Set default separation line settings
83 bool dash = false;
84 bool dot = true;
86 const ptime* nodeDate = ( *nodeIter )->getDate( );
87 if ( nodeDate != 0 && lastNodeDate != 0 && ( *nodeDate == *lastNodeDate ) )
88 x -= incX;
90 // Different days are separated with a strong line
91 if ( ( nodeDate != 0 && lastNodeDate == 0 ) || // First day
92 ( nodeDate != 0 && lastNodeDate != 0 && ( nodeDate->date( ) > lastNodeDate->date( ) ) ) ) // New Day
94 dash = true;
95 dot = false;
97 // Add the date text
98 std::string dateText( "" );
99 try
101 dateText = to_simple_string( nodeDate->date( ) );
102 } catch( std::exception& e ) { }
103 _grid.addText( dateText, ( int )x, 3, true );
104 _grid.addText( dateText, ( int )x, height - 15, true );
107 // Add separation line to grid
108 _grid.addLine( ( int )x - 5, 0, ( int )x - 5, height, 1, dash, dot );
110 lastNodeDate = nodeDate;
111 x += incX;
115 void TimeTree::GridBuilder::buildTimeLabel( Node& node, int x, int y )
117 const ptime* nodeDate = node.getDate( );
118 if ( nodeDate != 0 )
120 // Add date label to grid
121 std::string timeText( "" );
124 timeText = boost::posix_time::to_simple_string( nodeDate->time_of_day( ) );
125 } catch( std::exception& e ) { timeText = "00:01:00"; }
126 _grid.addText( timeText, x, y );
129 //-----------------------------------------------------------------------------
131 bool TimeTree::Group::GroupMedDateComp::operator()( const Group* g1, const Group* g2 ) const
133 ptime tg1 = g1->getMedianDate( );
134 ptime tg2 = g2->getMedianDate( );
135 return ( tg1 < tg2 );
139 \return A pointer to the line node list, 0 if line index is negative or superior to the group lines count.
141 la::Node::Nodes* TimeTree::Group::getLine( unsigned int line )
143 if ( line >= _lines.size( ) )
144 return 0;
145 return _lines[ line ];
148 void TimeTree::Group::collectNodes( la::Node::Nodes& nodes ) const
150 for ( Lines::const_iterator lineIter = _lines.begin( ); lineIter != _lines.end( ); lineIter++ )
152 const la::Node::Nodes* lineNodes = *lineIter;
153 std::copy( lineNodes->begin( ), lineNodes->end( ),
154 std::back_insert_iterator< la::Node::Nodes >( nodes ) );
158 void TimeTree::Group::setNodesType( int type )
160 la::Node::Nodes nodes;
161 collectNodes( nodes );
162 for ( la::Node::Nodes::iterator nodeIter = nodes.begin( ); nodeIter != nodes.end( ); nodeIter++ )
163 ( *nodeIter )->setType( type );
167 To avoid aggressive merge during layout, the method return true when a node with no date
168 occurs.
170 bool TimeTree::Group::intersect( const Group& group ) const
172 for ( Lines::const_iterator lineIter = _lines.begin( ); lineIter != _lines.end( ); lineIter++ )
174 const la::Node::Nodes* lineNodes = *lineIter;
175 if ( lineNodes->size( ) < 1 )
176 continue;
177 const la::Node* nodeFirst = lineNodes->front( );
178 const la::Node* nodeLast = lineNodes->back( );
180 const ptime* nodeFirstDate = nodeFirst->getDate( );
181 const ptime* nodeLastDate = nodeLast->getDate( );
182 if ( nodeFirstDate == 0 || nodeLastDate == 0 )
183 continue;
185 // Test the current line time interval with the given group ones
186 for ( Lines::const_iterator lineDstIter = group._lines.begin( ); lineDstIter != group._lines.end( ); lineDstIter++ )
188 const la::Node::Nodes* lineDstNodes = *lineDstIter;
189 if ( lineDstNodes->size( ) < 1 )
190 continue;
191 const la::Node* nodeDstFirst = lineDstNodes->front( );
192 const la::Node* nodeDstLast = lineDstNodes->back( );
194 const ptime* nodeDstFirstDate = nodeDstFirst->getDate( );
195 const ptime* nodeDstLastDate = nodeDstLast->getDate( );
196 if ( nodeDstFirstDate == 0 || nodeDstLastDate == 0 )
197 continue;
199 // Test mutual intersection
201 // Dst node area has an extremity in the node area
202 if ( ( ( *nodeDstFirstDate >= *nodeFirstDate ) && ( *nodeDstFirstDate <= *nodeLastDate ) ) ||
203 ( ( *nodeDstLastDate >= *nodeFirstDate ) && ( *nodeDstLastDate <= *nodeLastDate ) ) )
204 return true;
206 // Dst node area is larger than the node are
207 if ( ( *nodeDstFirstDate <= *nodeFirstDate ) && ( *nodeDstLastDate >= *nodeLastDate ) )
208 return true;
211 return false;
214 boost::posix_time::ptime TimeTree::Group::getMedianDate( ) const
216 la::Node::Nodes groupNodes;
217 collectNodes( groupNodes );
219 // Get the min and max date in the cluster articles
220 ptime dateMin = ptime( date( boost::date_time::pos_infin ) );
221 ptime dateMax = ptime( date( boost::date_time::neg_infin ) );
222 for ( la::Node::Nodes::const_iterator nodeIter = groupNodes.begin( ); nodeIter != groupNodes.end( ); nodeIter++ )
224 const ptime* nodeDate = ( *nodeIter )->getDate( );
225 if ( nodeDate != 0 )
227 if ( *nodeDate < dateMin )
228 dateMin = *nodeDate;
229 if ( *nodeDate > dateMax )
230 dateMax = *nodeDate;
234 // Get the median time
235 time_duration groupDuration = dateMax - dateMin;
236 return dateMin + ( groupDuration / 2 );
241 This method assumer that the groups inside the two track are disjoint within their own track groups.
243 bool TimeTree::Track::isDisjointOf( Track& track )
245 // Check that given track groups are disjoint from thie track groups
246 Group::Groups::iterator groupIter = track.getGroups( ).begin( );
247 for ( ; groupIter != track.getGroups( ).end( ); groupIter++ )
249 Group::Groups::iterator groupDstIter = getGroups( ).begin( );
250 for ( ; groupDstIter != getGroups( ).end( ); groupDstIter++ )
252 if ( ( *groupIter )->intersect( **groupDstIter ) )
253 return false;
256 return true;
259 /*! This method will merge tracks even if they have intersecting groups, so please verfiy track intersection if
260 such behaviour is not desired.
262 void TimeTree::Track::mergeTrack( Track& track )
264 Group::Groups& trackGroups = track.getGroups( );
265 for ( Group::Groups::iterator groupIter = trackGroups.begin( ); groupIter != trackGroups.end( ); groupIter++ )
266 _groups.push_front( *groupIter );
267 trackGroups.clear( );
271 \return Maximum number of lines in all track node groups, 0 if there is no lines.
273 int TimeTree::Track::getLineCount( ) const
275 int lineCount = 0;
276 for ( Group::Groups::const_iterator groupIter = _groups.begin( ); groupIter != _groups.end( ); groupIter++ )
278 if ( ( *groupIter )->getLineCount( ) > lineCount )
279 lineCount = ( *groupIter )->getLineCount( );
281 return lineCount;
285 \return Height of the track after the layout.
287 float TimeTree::Track::layout( float trackY, float width, GridBuilder& gridBuilder )
289 float trackYStart = trackY;
290 trackY += trackIntervalY;
291 int lineCount = getLineCount( );
292 for ( int line = 0; line < lineCount; line++ )
294 la::Node::Nodes lineNodes;
296 // Regroup track group current line as one line
297 for ( Group::Groups::iterator groupIter = getGroups( ).begin( ); groupIter != getGroups( ).end( ); groupIter++ )
299 la::Node::Nodes* groupLineNodes = ( *groupIter )->getLine( line );
300 if ( groupLineNodes == 0 )
301 continue;
302 std::copy( groupLineNodes->begin( ), groupLineNodes->end( ),
303 std::back_insert_iterator< la::Node::Nodes >( lineNodes ) );
306 // Get current line maximum height
307 float lineHeight = 0.f;
308 for ( la::Node::Nodes::iterator lineNodeIter = lineNodes.begin( ); lineNodeIter != lineNodes.end( ); lineNodeIter++ )
310 la::Node* lineNode = ( *lineNodeIter );
311 if ( lineNode->getDimension( )( 1 ) > lineHeight )
312 lineHeight = lineNode->getDimension( )( 1 );
315 // Center line nodes vertically
316 for ( la::Node::Nodes::iterator lineNodeIter = lineNodes.begin( ); lineNodeIter != lineNodes.end( ); lineNodeIter++ )
318 la::Node* lineNode = ( *lineNodeIter );
319 gridBuilder.buildTimeLabel( *lineNode, ( int )lineNode->getPosition( )( 0 ), ( int )trackY - 13 );
320 float deltaY = ( ( lineHeight - lineNode->getDimension( )( 1 ) ) / 2.f );
321 lineNode->getPosition( )( 1 ) = trackY + deltaY;
324 // Switch to next line
325 trackY += lineHeight;
326 if ( line < lineCount - 1 )
328 trackY += 2;
330 // Add line grid separation line
331 gridBuilder.getGrid( ).addLine( 0, ( int )trackY, ( int )width, ( int )trackY, 1, false, true );
333 trackY += lineIntervalY;
336 trackY += 2;
337 return ( trackY - trackYStart );
340 void TimeTree::Track::collectGroups( Group::Groups& groups )
342 // Sort this track groups according to their average date thanks to a sorted multiset
343 Group::GroupSet groupSet;
344 std::copy( _groups.begin( ), _groups.end( ), std::inserter( groupSet, groupSet.begin( ) ) );
346 // Copy the sorted groups to the groups collection list
347 std::copy( groupSet.begin( ), groupSet.end( ), std::back_insert_iterator< Group::Groups >( groups ) );
350 int TimeTree::Track::getNodeCount( ) const
352 int nodeCount = 0;
353 Group::Groups::const_iterator groupIter = _groups.begin( );
354 for ( ; groupIter != _groups.end( ); groupIter++ )
356 la::Node::Nodes nodes;
357 ( *groupIter )->collectNodes( nodes );
358 nodeCount += nodes.size( );
359 nodes.clear( );
361 return nodeCount;
364 void TimeTree::Tracks::addTrack( Track* track )
366 _tracks.push_back( track );
369 void TimeTree::Tracks::mergeTracks( )
371 std::set< Track* > tracksMerged;
373 // Try to insert each tracks in the other ones
374 for ( Track::Tracks::iterator trackIter = _tracks.begin( ); trackIter != _tracks.end( ); trackIter++ )
376 Track* track = *trackIter;
378 Track::Tracks::iterator trackDstIter = _tracks.begin( );
379 for ( ; trackDstIter != _tracks.end( ); trackDstIter++ )
381 Track* trackDst = *trackDstIter;
382 if ( track == trackDst )
383 continue;
385 // Don't merge to a track that is merged and no longer exists
386 if ( tracksMerged.find( trackDst ) != tracksMerged.end( ) )
387 continue;
389 // Test if track content can be merged in dst track
390 if ( trackDst->isDisjointOf( *track ) )
392 trackDst->mergeTrack( *track );
393 tracksMerged.insert( track );
398 // Supress merged tracks
400 std::set< Track* >::iterator trackIter = tracksMerged.begin( );
401 for ( ; trackIter != tracksMerged.end( ); trackIter++ )
402 _tracks.remove( *trackIter );
407 \return Height of the resulting layout.
409 float TimeTree::Tracks::layout( float width, GridBuilder& gridBuilder )
411 // Try to insert each tracs in the other ones
412 float trackY = 0.f;
413 unsigned int trackId = 0;
414 for ( Track::Tracks::iterator trackIter = _tracks.begin( ); trackIter != _tracks.end( ); trackId++, trackIter++ )
416 Track* track = *trackIter;
418 // For the first cluster increase space for the top days labels
419 float trackStart = trackY;
420 if ( trackId == 0 )
421 trackY += trackIntervalY + 2;
423 // Layout track groups
424 float height = track->layout( trackY, width, gridBuilder );
425 trackY += height;
427 /* int r = ( trackId % 2 == 0 ) ? 88 : 170;
428 int g = ( trackId % 2 == 0 ) ? 90 : 171;
429 int b = ( trackId % 2 == 0 ) ? 154 : 205;*/
430 int r = ( trackId % 2 == 0 ) ? 105 : 170;
431 int g = ( trackId % 2 == 0 ) ? 105 : 171;
432 int b = ( trackId % 2 == 0 ) ? 175 : 205;
434 // For the last cluster increase space for the top days labels
435 if ( trackId >= _tracks.size( ) - 1 )
436 trackY += trackIntervalY;
437 float trackEnd = trackY;
439 gridBuilder.buildClusterGrid( ( int )trackStart, ( int )width, ( int )( trackEnd - trackStart ), r, g, b );
441 return trackY;
444 void TimeTree::Tracks::collectGroups( Group::Groups& groups )
446 // Collect tracks in their natural order (in fact they are garanteed to be sorted as they have been
447 // added (and they have been added according to their group average date)
448 for ( Track::Tracks::iterator trackIter = _tracks.begin( ); trackIter != _tracks.end( ); trackIter++ )
449 ( *trackIter )->collectGroups( groups );
453 bool TimeTree::TrackDensityComp::operator()( const TimeTree::Track* t1, const TimeTree::Track* t2 ) const
456 return ( t1->getNodeCount( ) / t1->getLineCount( ) ) > ( t2->getNodeCount( ) / t2->getLineCount( ) );
460 void TimeTree::Tracks::sortByNodeDensity( )
462 std::set< TimeTree::Track*, TrackDensityComp > tracksSetDensity;
463 std::copy( _tracks.begin( ), _tracks.end( ), std::inserter( tracksSetDensity, tracksSetDensity.begin( ) ) );
464 _tracks.clear( );
465 std::copy( tracksSetDensity.begin( ), tracksSetDensity.end( ),
466 std::back_insert_iterator< Track::Tracks >( _tracks ) );
469 // Layout settings
470 float TimeTree::incX = 83.f;
472 float TimeTree::lineIntervalY = 16.f;
474 float TimeTree::trackIntervalY = 16.f;
477 /* TimeTree Layout Generation Management *///----------------------------------
479 \note If cluster track merge option is enabled, then a valid style manager must
480 have been provided in the TimeTree object constructor, otherwise, consecutive groups (clusters)
481 will eventually be affected the same graphic style, creating undiscernable groups.
483 void TimeTree::layout( la::Graph& graph, Grid& grid, int /*width*/, int /*height*/, utl::Progress& /*progress*/ )
485 // Layout the nodes horizontally
486 NodeSet nodes;
487 std::copy( graph.getNodes( ).begin( ), graph.getNodes( ).end( ),
488 std::inserter( nodes, nodes.begin( ) ) );
489 float layoutWidth = layoutNodesX( nodes );
491 // Generate groups for all nodes set to layout
492 Group::GroupSet groups;
493 NodeSetManager::iterator nodeSetIter = _nodeSetManager.begin( );
494 for ( ; nodeSetIter != _nodeSetManager.end( ); nodeSetIter++ )
496 Group* clusterGroup = buildGroup( **nodeSetIter );
497 groups.insert( clusterGroup );
500 // Create a track per group (tracks are sorted by time, since groups already are)
501 Tracks* tracks = new Tracks( );
502 for ( Group::GroupSet::iterator groupIter = groups.begin( ); groupIter != groups.end( ); groupIter++ )
504 Track* track = new Track( );
505 track->addGroup( *groupIter );
506 tracks->addTrack( track );
509 // Optimize tracks placement
510 tracks->mergeTracks( );
512 // Sort tracks per average node density (to maximize node count on first page)
513 // tracks->sortByNodeDensity( );
515 // Affect styles such that two consecutive groups on a track do not have the same style
517 int groupId = 0;
518 Group::Groups groups;
519 tracks->collectGroups( groups );
520 for ( Group::Groups::iterator groupIter = groups.begin( ); groupIter != groups.end( ); groupId++, groupIter++ )
521 ( *groupIter )->setNodesType( 1 + ( groupId % 3 ) );
524 // Fix the tracks layout, ie assign concrete coordinate to graph nodes
525 GridBuilder gridBuilder( grid );
526 float layoutHeight = tracks->layout( layoutWidth, gridBuilder );
528 // Draw the time grid
529 gridBuilder.buildTimeGrid( nodes, ( int )layoutWidth, ( int )layoutHeight );
532 float TimeTree::layoutNodesX( NodeSet& nodes )
534 // Set nodes horizontal (x) position according to their date
535 float x = 10.f;
536 Node* lastNode = 0;
537 for ( NodeSet::iterator sortedNodeIter = nodes.begin( ); sortedNodeIter != nodes.end( ); sortedNodeIter++ )
539 Node* node = *sortedNodeIter;
540 if ( lastNode != 0 )
542 const ptime* nodeDate = node->getDate( );
543 const ptime* lastNodeDate = lastNode->getDate( );
544 if ( nodeDate != 0 && lastNodeDate != 0 && ( *nodeDate == *lastNodeDate ) )
545 x -= incX;
548 node->setPosition( x, -1.0f ); // y=-1 used later to indicate the node as still not been placed
549 x += incX;
551 lastNode = node;
553 return x;
556 TimeTree::Group* TimeTree::buildGroup( NodeSet& clusterNodes )
558 Group* clusterGroup = new Group( );
560 la::Node::Nodes nodes;
561 std::copy( clusterNodes.begin( ), clusterNodes.end( ),
562 std::back_insert_iterator< la::Node::Nodes >( nodes ) );
564 // Consume all nodes, ie affect all nodes to a line
565 unsigned int runCount = 0;
566 while ( ( nodes.size( ) > 0 ) && ( runCount < clusterNodes.size( ) ) )
568 // Consume as many nodes as possible for a line (ie all non simultaneous nodes or a node with a subnode in this cluster)
569 la::Node::Nodes* lineNodes = new la::Node::Nodes( );
570 la::Node* prevNode = 0;
571 for ( la::Node::Nodes::iterator nodeIter = nodes.begin( ); nodeIter != nodes.end( ); nodeIter++ )
573 la::Node* node = *nodeIter;
575 // Detect if the node hasn't already been positionned (ex: it is a sub node of an existing node in this cluster)
576 if ( node->getPosition( )( 1 ) >= 0.f )
577 continue;
579 if ( ( prevNode == 0 ) ||
580 ( prevNode != 0 && ( node->getPosition( )( 0 ) > prevNode->getPosition( )( 0 ) ) ) )
582 // Consume the node
583 lineNodes->push_back( node );
584 node->getPosition( )( 1 ) = 1.0f;
585 prevNode = node;
587 // Consume the node subnodes (and sort subnodes by date)
588 NodeSet sortedOutNodes;
589 la::Node::NodesSet* outNodes = node->getOutNodesSet( );
590 std::copy( outNodes->begin( ), outNodes->end( ), std::inserter( sortedOutNodes, sortedOutNodes.begin( ) ) );
591 delete outNodes;
593 for ( NodeSet::iterator outNodeIter = sortedOutNodes.begin( ); outNodeIter != sortedOutNodes.end( ); outNodeIter++ )
595 la::Node* outNode = *outNodeIter;
596 if ( clusterNodes.find( outNode ) == clusterNodes.end( ) )
597 continue; // Dismiss nodes not in this cluster (is not in the cluster sorted nodes set)
599 if ( outNode->getPosition( )( 1 ) >= 0.f )
600 continue;
602 // Test that a node with a previous same date is not already in this line. Ex: Two sub
603 // articles have the same date, so the same x position, they must be on different lines.
604 bool skipNode = false;
605 la::Node::Nodes::iterator lineNodeIter = lineNodes->begin( );
606 for ( ; lineNodeIter != lineNodes->end( ); lineNodeIter++ )
608 if ( outNode->getPosition( )( 0 ) == ( *lineNodeIter )->getPosition( )( 0 ) )
609 skipNode = true;
611 if ( skipNode )
612 continue;
614 lineNodes->push_back( outNode );
615 outNode->getPosition( )( 1 ) = 1.0f;
616 prevNode = outNode;
621 // Supress all line nodes
622 for ( la::Node::Nodes::iterator lineNodeIter = lineNodes->begin( ); lineNodeIter != lineNodes->end( ); lineNodeIter++ )
623 nodes.remove( *lineNodeIter );
625 clusterGroup->addLine( lineNodes );
627 // Avoid infinite recursion, allow no more runs than there is nodes
628 runCount++;
631 return clusterGroup;
633 //-----------------------------------------------------------------------------
636 } // ::qan::la
637 } // ::qan