1 /*
2     SPDX-FileCopyrightText: 2007 James B. Bowlin <bowlin@mindspring.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "skylabeler.h"
8 
9 #include <cstdio>
10 
11 #include <QPainter>
12 #include <QPixmap>
13 
14 #include "Options.h"
15 #include "kstarsdata.h" // MINZOOM
16 #include "skymap.h"
17 #include "projections/projector.h"
18 
19 //---------------------------------------------------------------------------//
20 // A Little data container class
21 //---------------------------------------------------------------------------//
22 
23 typedef struct LabelRun
24 {
LabelRunLabelRun25     LabelRun(int s, int e) : start(s), end(e) {}
26     int start;
27     int end;
28 
29 } LabelRun;
30 
31 //----- Now for the main event ----------------------------------------------//
32 
33 //----- Static Methods ------------------------------------------------------//
34 
35 SkyLabeler *SkyLabeler::pinstance = nullptr;
36 
Instance()37 SkyLabeler *SkyLabeler::Instance()
38 {
39     if (!pinstance)
40         pinstance = new SkyLabeler();
41     return pinstance;
42 }
43 
setZoomFont()44 void SkyLabeler::setZoomFont()
45 {
46 #ifndef KSTARS_LITE
47     QFont font(m_p.font());
48 #else
49     QFont font(m_stdFont);
50 #endif
51     int deltaSize = 0;
52     if (Options::zoomFactor() < 2.0 * MINZOOM)
53         deltaSize = 2;
54     else if (Options::zoomFactor() < 10.0 * MINZOOM)
55         deltaSize = 1;
56 
57 #ifndef KSTARS_LITE
58     if (deltaSize)
59     {
60         font.setPointSize(font.pointSize() - deltaSize);
61         m_p.setFont(font);
62     }
63 #else
64     if (deltaSize)
65     {
66         font.setPointSize(font.pointSize() - deltaSize);
67     }
68     if (m_drawFont.pointSize() != font.pointSize())
69     {
70         m_drawFont = font;
71     }
72 #endif
73 }
74 
ZoomOffset()75 double SkyLabeler::ZoomOffset()
76 {
77     double offset = dms::PI * Options::zoomFactor() / 10800.0 / 3600.0;
78     return 4.0 + offset * 0.5;
79 }
80 
81 //----- Constructor ---------------------------------------------------------//
82 
SkyLabeler()83 SkyLabeler::SkyLabeler()
84     : m_fontMetrics(QFont()), m_picture(-1), labelList(NUM_LABEL_TYPES)
85 {
86 #ifdef KSTARS_LITE
87     //Painter is needed to get default font and we use it only once to have only one warning
88     m_stdFont = QFont();
89 
90 //For some reason there is no point size in default font on Android
91 #ifdef ANDROID
92     m_stdFont.setPointSize(16);
93 #else
94     m_stdFont.setPointSize(m_stdFont.pointSize()+2);
95 #endif
96 
97 #endif
98 }
99 
~SkyLabeler()100 SkyLabeler::~SkyLabeler()
101 {
102     for (auto &row : screenRows)
103     {
104         for (auto &item : *row)
105         {
106             delete item;
107         }
108         delete row;
109     }
110 }
111 
drawGuideLabel(QPointF & o,const QString & text,double angle)112 bool SkyLabeler::drawGuideLabel(QPointF &o, const QString &text, double angle)
113 {
114     // Create bounding rectangle by rotating the (height x width) rectangle
115     qreal h = m_fontMetrics.height();
116     qreal w = m_fontMetrics.width(text);
117     qreal s = sin(angle * dms::PI / 180.0);
118     qreal c = cos(angle * dms::PI / 180.0);
119 
120     qreal w2 = w / 2.0;
121 
122     qreal top, bot, left, right;
123 
124     // These numbers really do depend on the sign of the angle like this
125     if (angle >= 0.0)
126     {
127         top   = o.y() - s * w2;
128         bot   = o.y() + c * h + s * w2;
129         left  = o.x() - c * w2 - s * h;
130         right = o.x() + c * w2;
131     }
132     else
133     {
134         top   = o.y() + s * w2;
135         bot   = o.y() + c * h - s * w2;
136         left  = o.x() - c * w2;
137         right = o.x() + c * w2 - s * h;
138     }
139 
140     // return false if label would overlap existing label
141     if (!markRegion(left, right, top, bot))
142         return false;
143 
144     // for debugging the bounding rectangle:
145     //psky.drawLine( QPointF( left,  top ), QPointF( right, top ) );
146     //psky.drawLine( QPointF( right, top ), QPointF( right, bot ) );
147     //psky.drawLine( QPointF( right, bot ), QPointF( left,  bot ) );
148     //psky.drawLine( QPointF( left,  bot ), QPointF( left,  top ) );
149 
150     // otherwise draw the label and return true
151     m_p.save();
152     m_p.translate(o);
153 
154     m_p.rotate(angle); //rotate the coordinate system
155     m_p.drawText(QPointF(-w2, h), text);
156     m_p.restore(); //reset coordinate system
157 
158     return true;
159 }
160 
drawNameLabel(SkyObject * obj,const QPointF & _p,const qreal padding_factor)161 bool SkyLabeler::drawNameLabel(SkyObject *obj, const QPointF &_p,
162                                const qreal padding_factor)
163 {
164     QString sLabel = obj->labelString();
165     if (sLabel.isEmpty())
166         return false;
167 
168     double offset = obj->labelOffset();
169     QPointF p(_p.x() + offset, _p.y() + offset);
170 
171     if (!markText(p, sLabel, padding_factor))
172     {
173         return false;
174     }
175     else
176     {
177         double factor       = log(Options::zoomFactor() / 750.0);
178         double newPointSize = qBound(12.0, factor * m_stdFont.pointSizeF(), 18.0);
179         QFont zoomFont(m_p.font());
180         zoomFont.setPointSizeF(newPointSize);
181         m_p.setFont(zoomFont);
182         m_p.drawText(p, sLabel);
183         return true;
184     }
185 }
186 
setFont(const QFont & font)187 void SkyLabeler::setFont(const QFont &font)
188 {
189 #ifndef KSTARS_LITE
190     m_p.setFont(font);
191 #else
192     m_drawFont = font;
193 #endif
194     m_fontMetrics = QFontMetrics(font);
195 }
196 
setPen(const QPen & pen)197 void SkyLabeler::setPen(const QPen &pen)
198 {
199 #ifdef KSTARS_LITE
200     Q_UNUSED(pen);
201 #else
202     m_p.setPen(pen);
203 #endif
204 }
205 
shrinkFont(int delta)206 void SkyLabeler::shrinkFont(int delta)
207 {
208 #ifndef KSTARS_LITE
209     QFont font(m_p.font());
210 #else
211     QFont font(m_drawFont);
212 #endif
213     font.setPointSize(font.pointSize() - delta);
214     setFont(font);
215 }
216 
useStdFont()217 void SkyLabeler::useStdFont()
218 {
219     setFont(m_stdFont);
220 }
221 
resetFont()222 void SkyLabeler::resetFont()
223 {
224     setFont(m_skyFont);
225 }
226 
getMargins(const QString & text,float * left,float * right,float * top,float * bot)227 void SkyLabeler::getMargins(const QString &text, float *left, float *right, float *top, float *bot)
228 {
229     float height     = m_fontMetrics.height();
230     float width      = m_fontMetrics.width(text);
231     float sideMargin = m_fontMetrics.width("MM") + width / 2.0;
232 
233     // Create the margins within which it is okay to draw the label
234     double winHeight;
235     double winWidth;
236 #ifdef KSTARS_LITE
237     winHeight = SkyMapLite::Instance()->height();
238     winWidth  = SkyMapLite::Instance()->width();
239 #else
240     winHeight = m_p.window().height();
241     winWidth  = m_p.window().width();
242 #endif
243 
244     *right = winWidth - sideMargin;
245     *left  = sideMargin;
246     *top   = height;
247     *bot   = winHeight - 2.0 * height;
248 }
249 
reset(SkyMap * skyMap)250 void SkyLabeler::reset(SkyMap *skyMap)
251 {
252     // ----- Set up Projector ---
253     m_proj = skyMap->projector();
254     // ----- Set up Painter -----
255     if (m_p.isActive())
256         m_p.end();
257     m_picture = QPicture();
258     m_p.begin(&m_picture);
259     //This works around BUG 10496 in Qt
260     m_p.drawPoint(0, 0);
261     m_p.drawPoint(skyMap->width() + 1, skyMap->height() + 1);
262     // ----- Set up Zoom Dependent Font -----
263 
264     m_stdFont = QFont(m_p.font());
265     setZoomFont();
266     m_skyFont     = m_p.font();
267     m_fontMetrics = QFontMetrics(m_skyFont);
268     m_minDeltaX   = (int)m_fontMetrics.width("MMMMM");
269 
270     // ----- Set up Zoom Dependent Offset -----
271     m_offset = SkyLabeler::ZoomOffset();
272 
273     // ----- Prepare Virtual Screen -----
274     m_yScale = (m_fontMetrics.height() + 1.0);
275 
276     int maxY = int(skyMap->height() / m_yScale);
277     if (maxY < 1)
278         maxY = 1; // prevents a crash below?
279 
280     int m_maxX = skyMap->width();
281     m_size     = (maxY + 1) * m_maxX;
282 
283     // Resize if needed:
284     if (maxY > m_maxY)
285     {
286         screenRows.resize(m_maxY);
287         for (int y = m_maxY; y <= maxY; y++)
288         {
289             screenRows.append(new LabelRow());
290         }
291         //printf("resize: %d -> %d, size:%d\n", m_maxY, maxY, screenRows.size());
292     }
293 
294     // Clear all pre-existing rows as needed
295 
296     int minMaxY = (maxY < m_maxY) ? maxY : m_maxY;
297 
298     for (int y = 0; y <= minMaxY; y++)
299     {
300         LabelRow *row = screenRows[y];
301 
302         for (auto &item : *row)
303         {
304             delete item;
305         }
306         row->clear();
307     }
308 
309     // never decrease m_maxY:
310     if (m_maxY < maxY)
311         m_maxY = maxY;
312 
313     // reset the counters
314     m_marks = m_hits = m_misses = m_elements = 0;
315 
316     //----- Clear out labelList -----
317     for (auto &item : labelList)
318     {
319         item.clear();
320     }
321 }
322 
323 #ifdef KSTARS_LITE
reset()324 void SkyLabeler::reset()
325 {
326     SkyMapLite *skyMap = SkyMapLite::Instance();
327     // ----- Set up Projector ---
328     m_proj = skyMap->projector();
329 
330     //m_stdFont was moved to constructor
331     setZoomFont();
332     m_skyFont     = m_drawFont;
333     m_fontMetrics = QFontMetrics(m_skyFont);
334     m_minDeltaX   = (int)m_fontMetrics.width("MMMMM");
335     // ----- Set up Zoom Dependent Offset -----
336     m_offset = ZoomOffset();
337 
338     // ----- Prepare Virtual Screen -----
339     m_yScale = (m_fontMetrics.height() + 1.0);
340 
341     int maxY = int(skyMap->height() / m_yScale);
342     if (maxY < 1)
343         maxY = 1; // prevents a crash below?
344 
345     int m_maxX = skyMap->width();
346     m_size     = (maxY + 1) * m_maxX;
347 
348     // Resize if needed:
349     if (maxY > m_maxY)
350     {
351         screenRows.resize(m_maxY);
352         for (int y = m_maxY; y <= maxY; y++)
353         {
354             screenRows.append(new LabelRow());
355         }
356         //printf("resize: %d -> %d, size:%d\n", m_maxY, maxY, screenRows.size());
357     }
358 
359     // Clear all pre-existing rows as needed
360 
361     int minMaxY = (maxY < m_maxY) ? maxY : m_maxY;
362 
363     for (int y = 0; y <= minMaxY; y++)
364     {
365         LabelRow *row = screenRows[y];
366         for (int i = 0; i < row->size(); i++)
367         {
368             delete row->at(i);
369         }
370         row->clear();
371     }
372 
373     // never decrease m_maxY:
374     if (m_maxY < maxY)
375         m_maxY = maxY;
376 
377     // reset the counters
378     m_marks = m_hits = m_misses = m_elements = 0;
379 
380     //----- Clear out labelList -----
381     for (int i = 0; i < labelList.size(); i++)
382     {
383         labelList[i].clear();
384     }
385 }
386 #endif
387 
draw(QPainter & p)388 void SkyLabeler::draw(QPainter &p)
389 {
390     //FIXME: need a better soln. Apparently starting a painter
391     //clears the picture.
392     // But it's not like that's something that should be in the docs, right?
393     // No, that's definitely better to leave to people to figure out on their own.
394     if (m_p.isActive())
395     {
396         m_p.end();
397     }
398     m_picture.play(&p); //can't replay while it's being painted on
399     //this is also undocumented btw.
400     //m_p.begin(&m_picture);
401 }
402 
403 // We use Run Length Encoding to hold the information instead of an array of
404 // chars.  This is both faster and smaller but the code is more complicated.
405 //
406 // This code is easy to break and hard to fix.
407 
markText(const QPointF & p,const QString & text,qreal padding_factor)408 bool SkyLabeler::markText(const QPointF &p, const QString &text, qreal padding_factor)
409 {
410     static const auto ramp_zoom = log10(MINZOOM) + log10(MAXZOOM) * .3;
411 
412     if (padding_factor != 1)
413     {
414         padding_factor =
415             (1 - ((std::min(log10(Options::zoomFactor()), ramp_zoom)) / ramp_zoom)) *
416                 padding_factor +
417             1;
418     }
419 
420     const qreal maxX = p.x() + m_fontMetrics.width(text) * padding_factor;
421     const qreal minY = p.y() - m_fontMetrics.height() * padding_factor;
422     return markRegion(p.x(), maxX, p.y(), minY);
423 }
424 
markRegion(qreal left,qreal right,qreal top,qreal bot)425 bool SkyLabeler::markRegion(qreal left, qreal right, qreal top, qreal bot)
426 {
427     if (m_maxY < 1)
428     {
429         if (!m_errors++)
430             qDebug() << QString("Someone forgot to reset the SkyLabeler!");
431         return true;
432     }
433 
434     // setup x coordinates of rectangular region
435     int minX = int(left);
436     int maxX = int(right);
437     if (maxX < minX)
438     {
439         maxX = minX;
440         minX = int(right);
441     }
442 
443     // setup y coordinates
444     int maxY = int(bot / m_yScale);
445     int minY = int(top / m_yScale);
446 
447     if (maxY < 0)
448         maxY = 0;
449     if (maxY > m_maxY)
450         maxY = m_maxY;
451     if (minY < 0)
452         minY = 0;
453     if (minY > m_maxY)
454         minY = m_maxY;
455 
456     if (maxY < minY)
457     {
458         int temp = maxY;
459         maxY     = minY;
460         minY     = temp;
461     }
462 
463     // check to see if we overlap any existing label
464     // We must check all rows before we start marking
465     for (int y = minY; y <= maxY; y++)
466     {
467         LabelRow *row = screenRows[y];
468         int i;
469         for (i = 0; i < row->size(); i++)
470         {
471             if (row->at(i)->end < minX)
472                 continue; // skip past these
473             if (row->at(i)->start > maxX)
474                 break;
475             m_misses++;
476             return false;
477         }
478     }
479 
480     m_hits++;
481     m_marks += (maxX - minX + 1) * (maxY - minY + 1);
482 
483     // Okay, there was no overlap so let's insert the current rectangle into
484     // screenRows.
485 
486     for (int y = minY; y <= maxY; y++)
487     {
488         LabelRow *row = screenRows[y];
489 
490         // Simplest case: an empty row
491         if (row->size() < 1)
492         {
493             row->append(new LabelRun(minX, maxX));
494             m_elements++;
495             continue;
496         }
497 
498         // Find out our place in the universe (or row).
499         // H'mm.  Maybe we could cache these numbers above.
500         int i;
501         for (i = 0; i < row->size(); i++)
502         {
503             if (row->at(i)->end >= minX)
504                 break;
505         }
506 
507         // i now points to first label PAST ours
508 
509         // if we are first, append or merge at start of list
510         if (i == 0)
511         {
512             if (row->at(0)->start - maxX < m_minDeltaX)
513             {
514                 row->at(0)->start = minX;
515             }
516             else
517             {
518                 row->insert(0, new LabelRun(minX, maxX));
519                 m_elements++;
520             }
521             continue;
522         }
523 
524         // if we are past the last label, merge or append at end
525         else if (i == row->size())
526         {
527             if (minX - row->at(i - 1)->end < m_minDeltaX)
528             {
529                 row->at(i - 1)->end = maxX;
530             }
531             else
532             {
533                 row->append(new LabelRun(minX, maxX));
534                 m_elements++;
535             }
536             continue;
537         }
538 
539         // if we got here, we must insert or merge the new label
540         //  between [i-1] and [i]
541 
542         bool mergeHead = (minX - row->at(i - 1)->end < m_minDeltaX);
543         bool mergeTail = (row->at(i)->start - maxX < m_minDeltaX);
544 
545         // double merge => combine all 3 into one
546         if (mergeHead && mergeTail)
547         {
548             row->at(i - 1)->end = row->at(i)->end;
549             delete row->at(i);
550             row->removeAt(i);
551             m_elements--;
552         }
553 
554         // Merge label with [i-1]
555         else if (mergeHead)
556         {
557             row->at(i - 1)->end = maxX;
558         }
559 
560         // Merge label with [i]
561         else if (mergeTail)
562         {
563             row->at(i)->start = minX;
564         }
565 
566         // insert between the two
567         else
568         {
569             row->insert(i, new LabelRun(minX, maxX));
570             m_elements++;
571         }
572     }
573 
574     return true;
575 }
576 
addLabel(SkyObject * obj,SkyLabeler::label_t type)577 void SkyLabeler::addLabel(SkyObject *obj, SkyLabeler::label_t type)
578 {
579     bool visible = false;
580     QPointF p    = m_proj->toScreen(obj, true, &visible);
581     if (!visible || !m_proj->onScreen(p) || obj->translatedName().isEmpty())
582         return;
583     labelList[(int)type].append(SkyLabel(p, obj));
584 }
585 
586 #ifdef KSTARS_LITE
addLabel(SkyObject * obj,QPointF pos,label_t type)587 void SkyLabeler::addLabel(SkyObject *obj, QPointF pos, label_t type)
588 {
589     labelList[(int)type].append(SkyLabel(pos, obj));
590 }
591 #endif
592 
drawQueuedLabels()593 void SkyLabeler::drawQueuedLabels()
594 {
595     KStarsData *data = KStarsData::Instance();
596 
597     resetFont();
598     m_p.setPen(QColor(data->colorScheme()->colorNamed("PNameColor")));
599     drawQueuedLabelsType(PLANET_LABEL);
600 
601     if (labelList[SATURN_MOON_LABEL].size() > 0)
602     {
603         shrinkFont(2);
604         drawQueuedLabelsType(SATURN_MOON_LABEL);
605         resetFont();
606     }
607 
608     if (labelList[JUPITER_MOON_LABEL].size() > 0)
609     {
610         shrinkFont(2);
611         drawQueuedLabelsType(JUPITER_MOON_LABEL);
612         resetFont();
613     }
614 
615     // No colors for these fellas? Just following planets along?
616     drawQueuedLabelsType(ASTEROID_LABEL);
617     drawQueuedLabelsType(COMET_LABEL);
618 
619     m_p.setPen(QColor(data->colorScheme()->colorNamed("SatLabelColor")));
620     drawQueuedLabelsType(SATELLITE_LABEL);
621 
622     // Whelp we're here and we don't have a Rude Label color?
623     // Will just set it to Planet color since this is how it used to be!!
624     m_p.setPen(QColor(data->colorScheme()->colorNamed("PNameColor")));
625     LabelList list = labelList[RUDE_LABEL];
626 
627     for (const auto &item : list)
628     {
629         drawRudeNameLabel(item.obj, item.o);
630     }
631 }
632 
drawQueuedLabelsType(SkyLabeler::label_t type)633 void SkyLabeler::drawQueuedLabelsType(SkyLabeler::label_t type)
634 {
635     LabelList list = labelList[type];
636 
637     for (const auto &item : list)
638     {
639         drawNameLabel(item.obj, item.o);
640     }
641 }
642 
643 //Rude name labels don't check for collisions with other labels,
644 //these get drawn no matter what.  Transient labels are rude labels.
645 //To mitigate confusion from possibly "underlapping" labels, paint a
646 //semi-transparent background.
drawRudeNameLabel(SkyObject * obj,const QPointF & p)647 void SkyLabeler::drawRudeNameLabel(SkyObject *obj, const QPointF &p)
648 {
649     QString sLabel = obj->labelString();
650     double offset  = obj->labelOffset();
651     QRectF rect    = m_p.fontMetrics().boundingRect(sLabel);
652     rect.moveTo(p.x() + offset, p.y() + offset);
653 
654     //Interestingly, the fontMetric boundingRect isn't where you might think...
655     //We need to tweak rect to get the BG rectangle rect2
656     QRectF rect2 = rect;
657     rect2.moveTop(rect.top() - 0.6 * rect.height());
658     rect2.setHeight(0.8 * rect.height());
659 
660     //FIXME: Implement label background options
661     QColor color(KStarsData::Instance()->colorScheme()->colorNamed("SkyColor"));
662     color.setAlpha(m_p.pen().color().alpha()); //same transparency for the text and the background
663     m_p.fillRect(rect2, QBrush(color));
664     m_p.drawText(rect.topLeft(), sLabel);
665 }
666 
667 //----- Diagnostic and information routines -----
668 
fillRatio()669 float SkyLabeler::fillRatio()
670 {
671     if (m_size == 0)
672         return 0.0;
673     return 100.0 * float(m_marks) / float(m_size);
674 }
675 
hitRatio()676 float SkyLabeler::hitRatio()
677 {
678     if (m_hits == 0)
679         return 0.0;
680     return 100.0 * float(m_hits) / (float(m_hits + m_misses));
681 }
682 
printInfo()683 void SkyLabeler::printInfo()
684 {
685     printf("SkyLabeler:\n");
686     printf("  fillRatio=%.1f%%\n", fillRatio());
687     printf("  hits=%d  misses=%d  ratio=%.1f%%\n", m_hits, m_misses, hitRatio());
688     printf("  yScale=%.1f maxY=%d\n", m_yScale, m_maxY);
689 
690     printf("  screenRows=%d elements=%d virtualSize=%.1f Kbytes\n", screenRows.size(), m_elements,
691            float(m_size) / 1024.0);
692 
693 //    static const char *labelName[NUM_LABEL_TYPES];
694 //
695 //    labelName[STAR_LABEL]         = "Star";
696 //    labelName[ASTEROID_LABEL]     = "Asteroid";
697 //    labelName[COMET_LABEL]        = "Comet";
698 //    labelName[PLANET_LABEL]       = "Planet";
699 //    labelName[JUPITER_MOON_LABEL] = "Jupiter Moon";
700 //    labelName[SATURN_MOON_LABEL]  = "Saturn Moon";
701 //    labelName[DEEP_SKY_LABEL]     = "Deep Sky Object";
702 //    labelName[CONSTEL_NAME_LABEL] = "Constellation Name";
703 //
704 //    for (int i = 0; i < NUM_LABEL_TYPES; i++)
705 //    {
706 //        printf("  %20ss: %d\n", labelName[i], labelList[i].size());
707 //    }
708 //
709 //    // Check for errors in the data structure
710 //    for (int y = 0; y <= m_maxY; y++)
711 //    {
712 //        LabelRow *row = screenRows[y];
713 //        int size      = row->size();
714 //        if (size < 2)
715 //            continue;
716 //
717 //        bool error = false;
718 //        for (int i = 1; i < size; i++)
719 //        {
720 //            if (row->at(i - 1)->end > row->at(i)->start)
721 //                error = true;
722 //        }
723 //        if (!error)
724 //            continue;
725 //
726 //        printf("ERROR: %3d: ", y);
727 //        for (int i = 0; i < row->size(); i++)
728 //        {
729 //            printf("(%d, %d) ", row->at(i)->start, row->at(i)->end);
730 //        }
731 //        printf("\n");
732 //    }
733 }
734