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