[Kstars-devel] branches/work/kdeedu_kstars_htm/kstars/kstars/skycomponents

James Bowlin bowlin at mindspring.com
Thu Aug 9 02:46:51 CEST 2007


SVN commit 698075 by jbowlin:

The "Ecliptic" and "Equator" labels no longer overlap with each other
or with any of the horizontally drawn labels.  AFAIK, the only labels
that don't belong to the no-overlap club are the Horizon labels.

Now that I have the tools to prevent overlap of slanted labels it
should be pretty easy to add the Horizon labels to the club.  Most of
the code to draw slanted labels will probably be moved to SkyLabeler
when this happens to avoid code duplication.

For now, I exclude labels from the entire (vertical/horizontal)
bounding rectangle that contains a slanted label.

There is a single parameter, comfyAngle that controls the left/right
vs. low-angle priorities.  If it is zero or negative then the label
position with the minimum angle will always be used.  If it is 90 or
greater then the angle is essentially ignored.  I think the
non-overlap feature makes the left/right priority much less important.

For now, the Equator and Ecliptic labels are only being drawn when
useAntiAlias() is true.  If it is false, the labels are not drawn.

CCMAIL: kstars-devel at kde.org



 M  +102 -33   linelistcomponent.cpp  
 M  +26 -5     linelistcomponent.h  
 M  +26 -11    skylabeler.cpp  
 M  +19 -13    skylabeler.h  


--- branches/work/kdeedu_kstars_htm/kstars/kstars/skycomponents/linelistcomponent.cpp #698074:698075
@@ -38,13 +38,14 @@
 LineListComponent::~LineListComponent()
 {}
 
+
+// I don't think the ecliptic or the celestial equator should precess. -jbb
 void LineListComponent::update( KStarsData *data, KSNumbers *num )
 {
-    if ( ! num ) return;
     if ( ! selected() ) return;
 
     foreach ( SkyPoint* p, pointList ) {
-        p->updateCoords( num );
+        //if ( num ) p->updateCoords( num );
         p->EquatorialToHorizontal( data->lst(), data->geo()->lat() );
     }
 }
@@ -60,23 +61,33 @@
 	bool isVisible, isVisibleLast;
     SkyPoint  *pLast, *pThis;
 
+	// These are used to keep track of the element that is farrthest left,
+	// farthest right, etc.
 	float xLeft  = 100000.;
 	float xRight = 0.;
     float yTop   = 100000.;
 	float yBot   = 0.;
 
+	// These are the indices of the farthest left point, farthest right point,
+	// etc.  The are data members so the drawLabels() routine can use them.
+	// Zero indicates an index that was never set and is considered invalid
+	// inside of drawLabels().
+	m_iLeft = m_iRight = m_iTop = m_iBot = 0;
+
+	// We don't draw the label here but we need the proper font in order to set
+	// the margins correctly.  Since psky no contains the zoom dependent font as
+	// its default font, we need to play the little dance below.
 	m_skyLabeler->useStdFont( psky );
 	QFontMetricsF fm( psky.font() );
 	m_skyLabeler->resetFont( psky );
 
-	float margin = fm.width("MMM");
+	// Create the margins within which is is okay to draw the label
+	float margin      = fm.width("MMM");
 	float rightMargin = psky.window().width() - margin - fm.width( Label );
 	float leftMargin  = margin;
 	float topMargin   = fm.height();
 	float botMargin   = psky.window().height() - 4.0 * fm.height();
 
-	m_iLeft = m_iRight = m_iTop = m_iBot = 0;
-
     if ( Options::useAntialias() ) {
 
         QPointF oThis, oLast, oMid;
@@ -93,7 +104,9 @@
             if ( map->onScreen(oThis, oLast ) ) {
                 if ( isVisible && isVisibleLast ) {
                     psky.drawLine( oLast, oThis );
-
+					
+					// Keep track of index of leftmost, rightmost, etc point.
+					// Only allow points that fit within the margins.
 					qreal x = oThis.x();
 					qreal y = oThis.y();
 					if ( x > leftMargin && x < rightMargin &&
@@ -152,7 +165,6 @@
                 }
                 else if ( isVisibleLast ) {
                     oMid = map->clipLineI( pLast, pThis, scale );
-                    // -jbb printf("oMid: %4d %4d\n", oMid.x(), oMid.y());
                     psky.drawLine( oLast.x(), oLast.y(), oMid.x(), oMid.y() );
                 }
                 else if ( isVisible ) {
@@ -172,13 +184,27 @@
 
 void LineListComponent::drawLabels( KStars* kstars, QPainter& psky, double scale )
 {
-
 	if ( LabelPosition == NoLabel ) return;
 
-	int     i[4];
-	double  a[4] = { 360.0, 360.0, 360.0, 360.0 };
-	QPointF o[4];
+	SkyMap *map = kstars->map();
 
+	double comfyAngle = 40.0;  // the first valid candidate with an angle
+							   // smaller than this gets displayed.  If you set
+							   // this to > 90. then the first valid candidate
+							   // will be displayed, regardless of angle.
+
+	// We store info about the four candidate points in arrays to make several
+	// of the steps easier, particularly choosing the valid candiate with the
+	// smallest angle from the horizontal.
+
+	int     i[4];                                  // index of candidate
+	double  a[4] = { 360.0, 360.0, 360.0, 360.0 }; // angle, default to large value
+	QPointF o[4];                                  // candidate point
+	bool okay[4] = { true, true, true, true };     // flag  candidate false if it
+	                                               // overlaps a previous label.
+
+	// Try candidate in different orders depending on if the label was to be
+	// near the left or right side of the screen.
 	if ( LabelPosition == LeftEdgeLabel ) {
 		i[0] = m_iLeft;
 		i[1] = m_iTop;
@@ -193,63 +219,106 @@
 	}
 
 	// Make sure we have at least one valid point
-	for ( int j = 1; j < 4; j++) {
-		if ( i[0] ) break;
-		i[0] = i[j];
+	int minI = 0;
+
+	for ( ; minI < 4; minI++ ) {
+		if ( i[minI] ) break;
 	}
-	if ( i[0] == 0 ) return;
 
-	SkyMap *map = kstars->map();
-	double comfyAngle = 45.0;
+	// return if there are no valid candidates
+	if ( minI >= 4 ) return;
 
 	// Try the points in order and print the label if we can draw it at
 	// a comfortable angle for viewing;
-
-	for ( int j = 0; j < 4; j++) {
+	for ( int j = minI; j < 4; j++ ) {
 		o[j] = angleAt( map, i[j], &a[j], scale );
-		//printf("a[%d]=%.f  ", j, a[j]);
-		if ( fabs( a[j] ) < comfyAngle ) {
-			//printf("\n");
-			return drawTheLabel( psky, o[j], a[j] );
-		}
+		if ( fabs( a[j] ) > comfyAngle ) continue;
+		if ( drawTheLabel( psky, o[j], a[j] ) ) return;
+		okay[j] = false;
 	}
-	//printf("\n");
 
-	// No angle was great so pick the best angle
-	int ii = 0;
-	for ( int j = 1; j < 4; j++) {
-		if ( fabs(a[j]) < fabs(a[ii]) ) ii = j;
+	//--- No angle was comfy so pick the one with the smallest angle ---
+
+	// Index of the index/angle/point that gets displayed	
+	int ii = minI;
+
+	// find first valid candidate that does not overlap existing labels
+	for ( ; ii < 4; ii++ ) {
+		if ( i[ii] && okay[ii] ) break;
 	}
 
-	return drawTheLabel( psky, o[ii], a[ii] );
+	// return if all candiates either overlap or are invalid
+	if ( ii >= 4 ) return;
+
+	// find the valid non-overlap candidate with the smallest angle
+	for ( int j = ii + 1; j < 4; j++ ) {
+		if ( i[j] && okay[j] && fabs(a[j]) < fabs(a[ii]) ) ii = j;
+	}
+
+	drawTheLabel( psky, o[ii], a[ii] );
 }
 
-void LineListComponent::drawTheLabel( QPainter& psky, QPointF& o, double angle )
+bool LineListComponent::drawTheLabel( QPainter& psky, QPointF& o, double angle )
 {
 	m_skyLabeler->useStdFont( psky );
 	QFontMetricsF fm( psky.font() );
 
+	// Create bounding rectangle by rotating the (height x width) rectangle
+	qreal h = fm.height();
+	qreal w = fm.width(Label);
+	qreal s = sin( angle * dms::PI / 180.);
+	qreal c = cos( angle * dms::PI / 180.);
+
+	qreal top, bot, left, right;
+	
+	// These numbers really do depend on the sign of the angle like this
+	if ( angle >= 0.0 ) {
+		top   =  o.y();
+		bot   =  o.y() + c * h + s * w;
+		left  =  o.x() - s * h;
+		right =  o.x() + c * w;
+	}
+	else {
+		top   = o.y() + s * w;
+		bot   = o.y() + c * h;
+		left  = o.x();
+		right = o.x() + c * w - s * h;
+	}
+
+	// return false if label would overlap existing label
+	if ( ! m_skyLabeler->markRegion( top, bot, left, right ) )
+		return false;
+
+	// otherwise draw the label and return true
 	psky.save();
 	psky.translate( o );
 
-	psky.rotate( double( angle ) );              //rotate the coordinate system
-	psky.drawText( QPointF( 0., fm.height() ), Label );
+	psky.rotate( angle );                        //rotate the coordinate system
+	psky.drawText( QPointF( 0.0, fm.height() ), Label );
 	psky.restore();                              //reset coordinate system
 
 	m_skyLabeler->resetFont( psky );
+
+	return true;
 }
 
 QPointF LineListComponent::angleAt( SkyMap* map, int i, double *angle, double scale )
 {
 	SkyPoint* pThis = points()->at( i );
 	SkyPoint* pLast = points()->at( i - 1 );
+
 	QPointF oThis = map->toScreen( pThis, scale, false );
 	QPointF oLast = map->toScreen( pLast, scale, false );
+
 	double sx = double( oThis.x() - oLast.x() );
 	double sy = double( oThis.y() - oLast.y() );
+
 	*angle = atan2( sy, sx ) * 180.0 / dms::PI;
+
+	// Never draw the label upside down
 	if ( *angle < -90.0 ) *angle += 180.0;
 	if ( *angle >  90.0 ) *angle -= 180.0;
+
 	return oThis;
 }
 
--- branches/work/kdeedu_kstars_htm/kstars/kstars/skycomponents/linelistcomponent.h #698074:698075
@@ -58,10 +58,15 @@
 		inline const QPen& pen() const { return Pen; }
 		inline void setPen( const QPen &p ) { Pen = p; }
 
-		/**Draw the list of objects on the SkyMap*/
+		/* @short Draw the list of objects on the SkyMap
+		 */
 		virtual void draw( KStars *ks, QPainter& psky, double scale );
 		
-
+		/* @short draw the label if any.  Is currently called at the bottom of
+		 * draw() but that call could be removed and it could be called
+		 * externally AFTER draw() has been called so draw() can set up the label
+		 * position candidates.
+		 */
 		void drawLabels( KStars* kstars, QPainter& psky, double scale );
 
 		/**Draw the object, if it is exportable to an image
@@ -84,8 +89,12 @@
 			*/
 		virtual void update( KStarsData *data, KSNumbers *num=0 );
 		
+		/* @short returns pointer to the points list.
+		 */
         inline QList<SkyPoint*>* points() { return &pointList; }
 
+		/* @short a convenience routine to append a SkyPoint to the points list.
+		 */
         inline void appendP( SkyPoint* p ) {
             pointList.append( p ); 
         }
@@ -98,12 +107,24 @@
 		QPen Pen;
 
 		SkyLabeler* m_skyLabeler;
-		//int m_labelIndex[4];
-		int m_iLeft, m_iRight, m_iTop, m_iBot;
+		int m_iLeft, m_iRight, m_iTop, m_iBot;  // the four label position
+												// candidates
 
+
+		/* @short This routine does two things at once.  It returns the QPointF
+		 * coresponding to pointList[i] and also computes the angle using
+		 * pointList[i] and pointList[i-1] therefore you MUST ensure that:
+		 *
+		 *       1 <= i < pointList.size().
+		 */
 		QPointF angleAt( SkyMap* map, int i, double *angle, double scale );
 
-		void drawTheLabel( QPainter& psky, QPointF& o, double angle );
+		/* @short Tries to draw the label at the position and angle specfied. If
+		 * the label would overlap an existing label it is not drawn and we
+		 * return false, otherwise the label is drawn, its position is marked
+		 * and we return true.
+		 */
+		bool drawTheLabel( QPainter& psky, QPointF& o, double angle );
 };
 
 #endif
--- branches/work/kdeedu_kstars_htm/kstars/kstars/skycomponents/skylabeler.cpp #698074:698075
@@ -103,7 +103,7 @@
 
 void SkyLabeler::drawLabel( QPainter& psky, const QPointF& p, const QString& text )
 {
-    if ( ! mark( p, text ) ) return;
+    if ( ! markText( p, text ) ) return;
 
     if ( Options::useAntialias() )  {
         psky.drawText( p, text );
@@ -203,29 +203,44 @@
 //
 // This code is easy to break and hard to fix.
 
-bool SkyLabeler::mark( const QPointF& p, const QString& text )
+bool SkyLabeler::markText( const QPointF& p, const QString& text )
 {
+
+    qreal maxX =  p.x() + m_fontMetrics.width( text );
+	qreal minY = p.y() - m_fontMetrics.height();
+	return markRegion( p.y(), minY, p.x(), maxX );
+}
+
+bool SkyLabeler::markRegion( qreal top, qreal bot, qreal left, qreal right )
+{
     if ( m_maxY < 1 ) {
         if ( ! m_errors++ )
             fprintf(stderr, "Someone forgot to reset the SkyLabeler!\n");
         return true;
     }
 
-    // setup min/max x/y of rectangluar region covering text
-    // no range checking for x
+    // setup x coordinates of rectangular region
+	int minX = int( left );
+	int maxX = int( right );
+	if (maxX < minX) {
+		maxX = minX;
+		minX = int( right );
+	}
 
-    int minX = int( p.x() );
-    int maxX = int( p.x() + m_fontMetrics.width( text ) );
+    // setup y coordinates
+    int maxY = int( bot / m_yScale );
+	int minY = int( top / m_yScale );
 
-    // but we still need range checking for y
-    //
-    int maxY = int( p.y()  / m_yScale);
     if ( maxY < 0 ) maxY = 0;
     if ( maxY > m_maxY ) maxY = m_maxY;
-
-    int minY = int( (p.y() - m_fontMetrics.height() ) / m_yScale);
     if ( minY < 0 ) minY = 0;
+    if ( minY > m_maxY ) minY = m_maxY;
 
+	if ( maxY < minY ) {
+		maxY = minY;
+		minY = int( bot / m_yScale );
+	}
+
     // check to see if we overlap any existing label
     // We must check all rows before we start marking
 
--- branches/work/kdeedu_kstars_htm/kstars/kstars/skycomponents/skylabeler.h #698074:698075
@@ -45,7 +45,7 @@
           PLANET_LABEL,
     JUPITER_MOON_LABEL,
         DEEP_SKY_LABEL,
-    CONSTEL_NAME_LABEL,         
+    CONSTEL_NAME_LABEL,
        NUM_LABEL_TYPES
 };
 
@@ -58,8 +58,8 @@
  * a label, call mark( QPointF, QString ) of that label.  We will check to see
  * if it would overlap any existing label.  If there is overlap we return false.
  * If there is no overlap then we mark all the pixels that cover the new label
- * and return true.  
- * 
+ * and return true.
+ *
  * Since we need to check for overlap for every label every time it is
  * potentially drawn on the screen, efficiency is essential.  So instead of
  * having a 2-dimensional array of boolean values we use Run Length Encoding
@@ -76,7 +76,7 @@
  * pixel.  A LabelRow is a list of LabelRun's stored in ascending order.  This
  * saves a lot of space over an explicit array and it also makes checking for
  * overlaps faster and even makes inserting new overlaps faster on average.
- * 
+ *
  * Synopsis:
  *
  *   1) Create a new SkyLabeler
@@ -105,7 +105,7 @@
  * drawn.
  *
  * Finally, even though this code was written to be very efficient, we might
- * want to take some care in how many labels we throw at it.  Sending it 
+ * want to take some care in how many labels we throw at it.  Sending it
  * a large number of overlapping labels can be wasteful. Also, if one type
  * of object floods it with labels early on then there may not be any room
  * left for other types of labels.  Therefore for some types of objects (stars)
@@ -118,7 +118,7 @@
  * the magnitude limits.  It may even be possible to have KStars do some of
  * this throttling automatically but I haven't really thought about that
  * problem yet.
- * 
+ *
  * -- James B. Bowlin  2007-08-02
  */
 
@@ -126,7 +126,7 @@
 class SkyLabeler {
 	public:
 
-        //----- Static Methods ----------------------------------------------//        
+        //----- Static Methods ----------------------------------------------//
 
         /* @short adjusts the font in psky to be smaller if we are zoomed out.
          * This static function allows us to prevent code duplication since it
@@ -146,6 +146,7 @@
         //----- Constructor, Destructor -------------------------------------//
 
         SkyLabeler();
+
         ~SkyLabeler();
 
         /* @short clears the virtual screen (if needed) and resizes the virtual
@@ -194,18 +195,23 @@
          * It is usually easier to use drawLabel() or drawLabelOffest() instead
          * which both call mark() internally.
          */
-        bool mark( const QPointF& p, const QString& text);
+        bool markText( const QPointF& p, const QString& text);
 
+		/* @short Works just like markText() above but for an arbitrary
+		 * rectangular region bounded by top, bot, left, and right.
+		 */
+        bool markRegion( qreal top, qreal bot, qreal left, qreal right );
+
         /* @short sets the font in SkyLabeler and in psky to the font psky
          * had originally when reset() was called.  Used by ConstellationNames.
          */
-        void useStdFont(QPainter& psky); 
+        void useStdFont(QPainter& psky);
 
         /* @short sets the font in SkyLabeler and in psky back to the zoom
          * dependent value that was set in reset().  Also used in
          * ConstellationLines.
          */
-        void resetFont(QPainter& psky);  
+        void resetFont(QPainter& psky);
 
         /* @short queues the label in the "type" buffer for later drawing.
          */
@@ -261,10 +267,8 @@
         int hits()  { return m_hits; };
         int marks() { return m_marks; }
 
-        char *labelName[NUM_LABEL_TYPES];
-
     private:
-        ScreenRows screenRows;       
+        ScreenRows screenRows;
 
         int m_maxX;
         int m_maxY;
@@ -286,6 +290,8 @@
 
         QVector<LabelList>   labelList;
 
+        char *labelName[NUM_LABEL_TYPES];
+
 };
 
 #endif


More information about the Kstars-devel mailing list