1 /* 2 SPDX-FileCopyrightText: 2007 James B. Bowlin <bowlin@mindspring.com> 3 SPDX-License-Identifier: GPL-2.0-or-later 4 */ 5 6 #pragma once 7 8 #include "skylabel.h" 9 10 #include <QFontMetricsF> 11 #include <QList> 12 #include <QVector> 13 #include <QPainter> 14 #include <QPicture> 15 #include <QFont> 16 17 class QString; 18 class QPointF; 19 class SkyMap; 20 class Projector; 21 struct LabelRun; 22 23 typedef QList<LabelRun *> LabelRow; 24 typedef QVector<LabelRow *> ScreenRows; 25 26 /** 27 *@class SkyLabeler 28 * The purpose of this class is to prevent labels from overlapping. We do this 29 * by creating a virtual (lower Y-resolution) screen. Each "pixel" of this 30 * screen essentially contains a boolean value telling us whether or not there 31 * is an existing label covering at least part of that pixel. Before you draw 32 * a label, call mark( QPointF, QString ) of that label. We will check to see 33 * if it would overlap any existing label. If there is overlap we return false. 34 * If there is no overlap then we mark all the pixels that cover the new label 35 * and return true. 36 * 37 * Since we need to check for overlap for every label every time it is 38 * potentially drawn on the screen, efficiency is essential. So instead of 39 * having a 2-dimensional array of boolean values we use Run Length Encoding 40 * and store the virtual array in a QVector of QLists. Each element of the 41 * vector, a LabelRow, corresponds to a horizontal strip of pixels on the actual 42 * screen. How many vertical pixels are in each strip is controlled by 43 * m_yDensity. The higher the density, the fewer vertical pixels per strip and 44 * hence a larger number of strips are needed to cover the screen. As 45 * m_yDensity increases so does the density of the strips. 46 * 47 * The information in the X-dimension is completed run length encoded. A 48 * consecutive run of pixels in one strip that are covered by one or more labels 49 * is stored in a LabelRun object that merely stores the start pixel and the end 50 * pixel. A LabelRow is a list of LabelRun's stored in ascending order. This 51 * saves a lot of space over an explicit array and it also makes checking for 52 * overlaps faster and even makes inserting new overlaps faster on average. 53 * 54 * Synopsis: 55 * 56 * 1) Create a new SkyLabeler 57 * 58 * 2) every time you want to draw a new screen, reset the labeler. 59 * 60 * 3) Either: 61 * 62 * A) Call drawLabel() or drawOffsetLabel(), or 63 * 64 * B) Call addLabel() or addOffsetLabel() 65 * 66 * 4) Call draw() if addLabel() or addOffsetLabel() were used. 67 * 68 * 69 * 70 * SkyLabeler works totally on a first come, first served basis which is almost 71 * the direct opposite of a z-buffer where the objects drawn last are most 72 * visible. This is why the addLabel() and draw() routines were created. 73 * They allow us to time-shift the drawing of labels and thus gives us control 74 * over their priority. The drawLabel() routines are still available but are 75 * not being used. The addLabel() routines adds a label to a specific buffer. 76 * Each type of label has its own buffer which lets us control the font and 77 * color as well as the priority. The priority is now manually set in the 78 * draw() routine by adjusting the order in which the various buffers get 79 * drawn. 80 * 81 * Finally, even though this code was written to be very efficient, we might 82 * want to take some care in how many labels we throw at it. Sending it 83 * a large number of overlapping labels can be wasteful. Also, if one type 84 * of object floods it with labels early on then there may not be any room 85 * left for other types of labels. Therefore for some types of objects (stars) 86 * we may want to have a zoom dependent label threshold magnitude just like 87 * we have for drawing the stars themselves. This would throw few labels 88 * at the labeler when we are zoomed at and they would mostly overlap anyway 89 * and it would give us more labels when the user is zoomed in and there 90 * is more room for them. The "b" key currently causes the labeler statistics 91 * to be printed. This may be useful in figuring out the best settings for 92 * the magnitude limits. It may even be possible to have KStars do some of 93 * this throttling automatically but I haven't really thought about that 94 * problem yet. 95 * 96 * -- James B. Bowlin 2007-08-02 97 */ 98 class SkyLabeler 99 { 100 protected: 101 SkyLabeler(); 102 SkyLabeler(SkyLabeler &skyLabler); 103 104 public: 105 enum label_t 106 { 107 STAR_LABEL, 108 ASTEROID_LABEL, 109 COMET_LABEL, 110 PLANET_LABEL, 111 JUPITER_MOON_LABEL, 112 SATURN_MOON_LABEL, 113 DEEP_SKY_LABEL, 114 CONSTEL_NAME_LABEL, 115 SATELLITE_LABEL, 116 RUDE_LABEL, ///Rude labels block other labels FIXME: find a better solution 117 NUM_LABEL_TYPES 118 }; 119 120 //----- Static Methods ----------------------------------------------// 121 122 static SkyLabeler *Instance(); 123 124 /** 125 * @short returns the zoom dependent label offset. This is used in this 126 * class and in SkyObject. It is important that the offsets be the same 127 * so highlighted labels are always drawn exactly on top of the normally 128 * drawn labels. 129 */ 130 static double ZoomOffset(); 131 132 /** 133 * @short static version of addLabel() below. 134 */ AddLabel(SkyObject * obj,label_t type)135 inline static void AddLabel(SkyObject *obj, label_t type) { pinstance->addLabel(obj, type); } 136 137 //--------------------------------------------------------------------// 138 ~SkyLabeler(); 139 140 /** 141 * @short clears the virtual screen (if needed) and resizes the virtual 142 * screen (if needed) to match skyMap. A font must be specified which 143 * is taken to be the average or normal font that will be used. The 144 * size of the horizontal strips will be (slightly) optimized for this 145 * font. We also adjust the font size in psky to smaller fonts if the 146 * screen is zoomed out. You can mimic this setting with the static 147 * method SkyLabeler::setZoomFont( psky ). 148 */ 149 void reset(SkyMap *skyMap); 150 151 /** 152 * @short KStars Lite version of the function above 153 */ 154 #ifdef KSTARS_LITE 155 void reset(); 156 #endif 157 158 /** 159 * @short Draws labels using the given painter 160 * @param p the painter to draw labels with 161 */ 162 void draw(QPainter &p); 163 164 //----- Font Setting -----// 165 166 /** 167 * @short adjusts the font in psky to be smaller if we are zoomed out. 168 */ 169 void setZoomFont(); 170 171 /** 172 * @short sets the pen used for drawing labels on the sky. 173 */ 174 void setPen(const QPen &pen); 175 176 /** 177 * @short tells the labeler the font you will be using so it can figure 178 * out the height and width of the labels. Also sets this font in the 179 * psky since this is almost always what is wanted. 180 */ 181 void setFont(const QFont &font); 182 183 /** 184 * @short decreases the size of the font in psky and in the SkyLabeler 185 * by the delta points. Negative deltas will increase the font size. 186 */ 187 void shrinkFont(int delta); 188 189 /** 190 * @short sets the font in SkyLabeler and in psky to the font psky 191 * had originally when reset() was called. Used by ConstellationNames. 192 */ 193 void useStdFont(); 194 195 /** 196 * @short sets the font in SkyLabeler and in psky back to the zoom 197 * dependent value that was set in reset(). Also used in 198 * ConstellationLines. 199 */ 200 void resetFont(); 201 202 /** 203 * @short returns the fontMetricsF we have already created. 204 */ fontMetrics()205 QFontMetricsF &fontMetrics() { return m_fontMetrics; } 206 207 //----- Drawing/Adding Labels -----// 208 209 /** 210 *@short sets four margins for help in keeping labels entirely on the 211 * screen. 212 */ 213 void getMargins(const QString &text, float *left, float *right, float *top, float *bot); 214 215 /** 216 * @short Tries to draw the text at the position and angle specified. If 217 * the label would overlap an existing label it is not drawn and we 218 * return false, otherwise the label is drawn, its position is marked 219 * and we return true. 220 */ 221 bool drawGuideLabel(QPointF &o, const QString &text, double angle); 222 223 /** 224 * @short Tries to draw a label for an object. 225 * @param obj the object to draw the label for 226 * @param _p the position of that object 227 * @return true if the label was drawn 228 * //FIXME: should this just take an object pointer and do its own projection? 229 * 230 * \p padding_factor is the factor by which the real label size is scaled 231 */ 232 bool drawNameLabel(SkyObject *obj, const QPointF &_p, const qreal padding_factor = 1); 233 234 /** 235 *@short draw the object's name label on the map, without checking for 236 *overlap with other labels. 237 *@param obj reference to the QPainter on which to draw (either the sky pixmap or printer device) 238 *@param _p The screen position for the label (in pixels; typically as found by SkyMap::toScreen()) 239 */ 240 void drawRudeNameLabel(SkyObject *obj, const QPointF &_p); 241 242 /** 243 * @short queues the label in the "type" buffer for later drawing. 244 */ 245 void addLabel(SkyObject *obj, label_t type); 246 247 #ifdef KSTARS_LITE 248 /** 249 * @short queues the label in the "type" buffer for later drawing. Doesn't calculate the position of 250 * SkyObject but uses pos as a position of label. 251 */ 252 void addLabel(SkyObject *obj, QPointF pos, label_t type); 253 #endif 254 /** 255 *@short draws the labels stored in all the buffers. You can change the 256 * priority by editing the .cpp file and changing the order in which 257 * buffers are drawn. You can also change the fonts and colors there 258 * too. 259 */ 260 void drawQueuedLabels(); 261 262 /** 263 * @short a convenience routine that draws all the labels from a single 264 * buffer. Currently this is only called from within draw() above. 265 */ 266 void drawQueuedLabelsType(SkyLabeler::label_t type); 267 268 //----- Marking Regions -----// 269 270 /** 271 * @short tells the labeler the location and text of a label you want 272 * to draw. Returns true if there is room for the label and returns 273 * false otherwise. If it returns true, the location of the label is 274 * marked on the virtual screen so future labels won't overlap it. 275 * 276 * It is usually easier to use drawLabel() or drawLabelOffest() instead 277 * which both call mark() internally. 278 * 279 * \p padding_factor is the factor by which the real label size is 280 * scaled 281 */ 282 bool markText(const QPointF &p, const QString &text, qreal padding_factor = 1); 283 284 /** 285 * @short Works just like markText() above but for an arbitrary 286 * rectangular region bounded by top, bot, left, and right. 287 */ 288 bool markRegion(qreal left, qreal right, qreal top, qreal bot); 289 290 //----- Diagnostics and Information -----// 291 292 /** 293 * @short diagnostic. the *percentage* of pixels that have been filled. 294 * Expect return values between 0.0 and 100.0. A fillRatio above 20 295 * is pretty busy and crowded. I think a fillRatio of about 10 looks 296 * good. The fillRatio will be lowered of the screen is zoomed out 297 * so are big blank spaces around the celestial sphere. 298 */ 299 float fillRatio(); 300 301 /** 302 * @short diagnostic, the number of times mark() returned true divided by 303 * the total number of times mark was called multiplied by 100. Expect 304 * return values between 0.0 an 100. A hit ratio of 100 means no labels 305 * would have overlapped. A ratio of zero means no labels got drawn 306 * (which should never happen). A hitRatio around 50 might be a good 307 * target to shoot for. Expect it to be lower when fully zoomed out and 308 * higher when zoomed in. 309 */ 310 float hitRatio(); 311 312 /** 313 * @short diagnostic, prints some brief statistics to the console. 314 * Currently this is connected to the "b" key in SkyMapEvents. 315 */ 316 void printInfo(); 317 stdFont()318 inline QFont stdFont() { return m_stdFont; } skyFont()319 inline QFont skyFont() { return m_skyFont; } 320 #ifdef KSTARS_LITE drawFont()321 inline QFont drawFont() { return m_drawFont; } 322 #endif 323 hits()324 int hits() { return m_hits; } marks()325 int marks() { return m_marks; } 326 327 private: 328 ScreenRows screenRows; 329 int m_maxX { 0 }; 330 int m_maxY { 0 }; 331 int m_size { 0 }; 332 /// When to merge two adjacent regions 333 int m_minDeltaX { 30 }; 334 int m_marks { 0 }; 335 int m_hits { 0 }; 336 int m_misses { 0 }; 337 int m_elements { 0 }; 338 int m_errors { 0 }; 339 qreal m_yScale { 0 }; 340 double m_offset { 0 }; 341 QFont m_stdFont, m_skyFont; 342 QFontMetricsF m_fontMetrics; 343 //In KStars Lite this font should be used wherever font of m_p was changed or used 344 #ifdef KSTARS_LITE 345 QFont m_drawFont; 346 #endif 347 QPainter m_p; 348 QPicture m_picture; 349 QVector<LabelList> labelList; 350 const Projector *m_proj { nullptr }; 351 static SkyLabeler *pinstance; 352 }; 353