Marble

GeoLineStringGraphicsItem.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2009 Andrew Manson <g.real.ate@gmail.com>
4//
5
6#include "GeoLineStringGraphicsItem.h"
7
8#include "GeoDataColorStyle.h"
9#include "GeoDataLabelStyle.h"
10#include "GeoDataLineStyle.h"
11#include "GeoDataPlacemark.h"
12#include "GeoDataPolyStyle.h"
13#include "GeoDataStyle.h"
14#include "GeoPainter.h"
15#include "MarbleDebug.h"
16#include "OsmPlacemarkData.h"
17#include "StyleBuilder.h"
18#include "ViewportParams.h"
19
20#include <QPainterPathStroker>
21#include <qmath.h>
22
23namespace Marble
24{
25
26const GeoDataStyle *GeoLineStringGraphicsItem::s_previousStyle = nullptr;
27bool GeoLineStringGraphicsItem::s_paintInline = true;
28bool GeoLineStringGraphicsItem::s_paintOutline = true;
29
30GeoLineStringGraphicsItem::GeoLineStringGraphicsItem(const GeoDataPlacemark *placemark, const GeoDataLineString *lineString)
31 : GeoGraphicsItem(placemark)
32 , m_lineString(lineString)
33 , m_renderLineString(lineString)
34 , m_renderLabel(false)
35 , m_penWidth(0.0)
36 , m_name(placemark->name())
37{
38 QString const category = StyleBuilder::visualCategoryName(placemark->visualCategory());
39 QStringList paintLayers;
40 paintLayers << QLatin1StringView("LineString/") + category + QLatin1StringView("/outline");
41 paintLayers << QLatin1StringView("LineString/") + category + QLatin1StringView("/inline");
42 if (!m_name.isEmpty()) {
43 paintLayers << QLatin1StringView("LineString/") + category + QLatin1StringView("/label");
44 }
45 setPaintLayers(paintLayers);
46}
47
48GeoLineStringGraphicsItem::~GeoLineStringGraphicsItem()
49{
50 qDeleteAll(m_cachedPolygons);
51}
52
53void GeoLineStringGraphicsItem::setLineString(const GeoDataLineString *lineString)
54{
55 m_lineString = lineString;
56 m_renderLineString = lineString;
57}
58
59const GeoDataLineString *GeoLineStringGraphicsItem::lineString() const
60{
61 return m_lineString;
62}
63
64GeoDataLineString GeoLineStringGraphicsItem::merge(const QList<const GeoDataLineString *> &lineStrings_)
65{
66 if (lineStrings_.isEmpty()) {
67 return GeoDataLineString();
68 }
69
70 Q_ASSERT(!lineStrings_.isEmpty());
71 auto lineStrings = lineStrings_;
72 GeoDataLineString result = *lineStrings.first();
73 lineStrings.pop_front();
74 for (bool matched = true; matched && !lineStrings.isEmpty();) {
75 matched = false;
76 for (auto lineString : lineStrings) {
77 if (canMerge(result.first(), lineString->first())) {
78 result.remove(0);
79 result.reverse();
80 result << *lineString;
81 lineStrings.removeOne(lineString);
82 matched = true;
83 break;
84 } else if (canMerge(result.last(), lineString->first())) {
85 result.remove(result.size() - 1);
86 result << *lineString;
87 lineStrings.removeOne(lineString);
88 matched = true;
89 break;
90 } else if (canMerge(result.first(), lineString->last())) {
91 GeoDataLineString behind = result;
92 result = *lineString;
93 behind.remove(0);
94 result << behind;
95 lineStrings.removeOne(lineString);
96 matched = true;
97 break;
98 } else if (canMerge(result.last(), lineString->last())) {
99 GeoDataLineString behind = *lineString;
100 behind.reverse();
101 behind.remove(0);
102 result << behind;
103 lineStrings.removeOne(lineString);
104 matched = true;
105 break;
106 }
107 }
108
109 if (!matched) {
110 return GeoDataLineString();
111 }
112 }
113 return lineStrings.isEmpty() ? result : GeoDataLineString();
114}
115
116void GeoLineStringGraphicsItem::setMergedLineString(const GeoDataLineString &mergedLineString)
117{
118 m_mergedLineString = mergedLineString;
119 m_renderLineString = mergedLineString.isEmpty() ? m_lineString : &m_mergedLineString;
120}
121
122const GeoDataLatLonAltBox &GeoLineStringGraphicsItem::latLonAltBox() const
123{
124 return m_renderLineString->latLonAltBox();
125}
126
127void GeoLineStringGraphicsItem::paint(GeoPainter *painter, const ViewportParams *viewport, const QString &layer, int tileLevel)
128{
129 setRenderContext(RenderContext(tileLevel));
130
131 if (layer.endsWith(QLatin1StringView("/outline"))) {
132 qDeleteAll(m_cachedPolygons);
133 m_cachedPolygons.clear();
134 m_cachedRegion = QRegion();
135 painter->polygonsFromLineString(*m_renderLineString, m_cachedPolygons);
136 if (m_cachedPolygons.empty()) {
137 return;
138 }
139 if (painter->mapQuality() == HighQuality || painter->mapQuality() == PrintQuality) {
140 paintOutline(painter, viewport);
141 }
142 } else if (layer.endsWith(QLatin1StringView("/inline"))) {
143 if (m_cachedPolygons.empty()) {
144 return;
145 }
146 paintInline(painter, viewport);
147 } else if (layer.endsWith(QLatin1StringView("/label"))) {
148 if (!m_cachedPolygons.empty()) {
149 if (m_renderLabel) {
150 paintLabel(painter, viewport);
151 }
152 }
153 } else {
154 qDeleteAll(m_cachedPolygons);
155 m_cachedPolygons.clear();
156 m_cachedRegion = QRegion();
157 painter->polygonsFromLineString(*m_renderLineString, m_cachedPolygons);
158 if (m_cachedPolygons.empty()) {
159 return;
160 }
161 if (s_previousStyle != style().data()) {
162 configurePainterForLine(painter, viewport, false);
163 }
164 s_previousStyle = style().data();
165 for (const QPolygonF *itPolygon : std::as_const(m_cachedPolygons)) {
166 painter->drawPolyline(*itPolygon);
167 }
168 }
169}
170
171bool GeoLineStringGraphicsItem::contains(const QPoint &screenPosition, const ViewportParams *) const
172{
173 if (m_penWidth <= 0.0) {
174 return false;
175 }
176
177 if (m_cachedRegion.isNull()) {
178 QPainterPath painterPath;
179 for (auto polygon : m_cachedPolygons) {
180 painterPath.addPolygon(*polygon);
181 }
182 QPainterPathStroker stroker;
183 qreal const margin = 6.0;
184 stroker.setWidth(m_penWidth + margin);
185 QPainterPath strokePath = stroker.createStroke(painterPath);
186 m_cachedRegion = QRegion(strokePath.toFillPolygon().toPolygon(), Qt::WindingFill);
187 }
188 return m_cachedRegion.contains(screenPosition);
189}
190
191void GeoLineStringGraphicsItem::handleRelationUpdate(const QList<const GeoDataRelation *> &relations)
192{
194 for (auto relation : relations) {
195 auto const ref = relation->osmData().tagValue(QStringLiteral("ref"));
196 if (relation->isVisible() && !ref.isEmpty()) {
197 names[relation->relationType()] << ref;
198 }
199 }
200 if (names.isEmpty()) {
201 m_name = feature()->name();
202 } else {
203 QStringList namesList;
204 for (auto iter = names.begin(); iter != names.end(); ++iter) {
205 QString value;
206 switch (iter.key()) {
207 case GeoDataRelation::UnknownType:
208 case GeoDataRelation::RouteRoad:
209 break;
210 case GeoDataRelation::RouteDetour:
211 value = tr("Detour");
212 break;
213 case GeoDataRelation::RouteFerry:
214 value = tr("Ferry Route");
215 break;
216 case GeoDataRelation::RouteTrain:
217 value = tr("Train");
218 break;
219 case GeoDataRelation::RouteSubway:
220 value = tr("Subway");
221 break;
222 case GeoDataRelation::RouteTram:
223 value = tr("Tram");
224 break;
225 case GeoDataRelation::RouteBus:
226 value = tr("Bus");
227 break;
228 case GeoDataRelation::RouteTrolleyBus:
229 value = tr("Trolley Bus");
230 break;
231 case GeoDataRelation::RouteBicycle:
232 value = tr("Bicycle Route");
233 break;
234 case GeoDataRelation::RouteMountainbike:
235 value = tr("Mountainbike Route");
236 break;
237 case GeoDataRelation::RouteFoot:
238 value = tr("Walking Route");
239 break;
240 case GeoDataRelation::RouteHiking:
241 value = tr("Hiking Route");
242 break;
243 case GeoDataRelation::RouteHorse:
244 value = tr("Bridleway");
245 break;
246 case GeoDataRelation::RouteInlineSkates:
247 value = tr("Inline Skates Route");
248 break;
249 case GeoDataRelation::RouteSkiDownhill:
250 value = tr("Downhill Piste");
251 break;
252 case GeoDataRelation::RouteSkiNordic:
253 value = tr("Nordic Ski Trail");
254 break;
255 case GeoDataRelation::RouteSkitour:
256 value = tr("Skitour");
257 break;
258 case GeoDataRelation::RouteSled:
259 value = tr("Sled Trail");
260 break;
261 }
262
263 QStringList &references = iter.value();
264 std::sort(references.begin(), references.end());
265 auto const last = std::unique(references.begin(), references.end());
266 references.erase(last, references.end());
267 auto const routes = references.join(QStringLiteral(", "));
268 namesList << (value.isEmpty() ? routes : QStringLiteral("%1 %2").arg(value, routes));
269 }
270 auto const allRoutes = namesList.join(QStringLiteral("; "));
271 if (feature()->name().isEmpty()) {
272 m_name = allRoutes;
273 } else {
274 m_name = QStringLiteral("%1 (%2)").arg(feature()->name(), allRoutes);
275 }
276 }
277}
278
279void GeoLineStringGraphicsItem::paintInline(GeoPainter *painter, const ViewportParams *viewport)
280{
281 if ((!viewport->resolves(m_renderLineString->latLonAltBox(), 2))) {
282 return;
283 }
284
285 if (s_previousStyle != style().data()) {
286 s_paintInline = configurePainterForLine(painter, viewport, false);
287 }
288 s_previousStyle = style().data();
289
290 if (s_paintInline) {
291 m_renderLabel = painter->pen().widthF() >= 6.0f;
292 m_penWidth = painter->pen().widthF();
293 for (const QPolygonF *itPolygon : std::as_const(m_cachedPolygons)) {
294 painter->drawPolyline(*itPolygon);
295 }
296 }
297}
298
299void GeoLineStringGraphicsItem::paintOutline(GeoPainter *painter, const ViewportParams *viewport) const
300{
301 if ((!viewport->resolves(m_renderLineString->latLonAltBox(), 2))) {
302 return;
303 }
304
305 if (s_previousStyle != style().data()) {
306 s_paintOutline = configurePainterForLine(painter, viewport, true);
307 }
308 s_previousStyle = style().data();
309
310 if (s_paintOutline) {
311 for (const QPolygonF *itPolygon : m_cachedPolygons) {
312 painter->drawPolyline(*itPolygon);
313 }
314 }
315}
316
317void GeoLineStringGraphicsItem::paintLabel(GeoPainter *painter, const ViewportParams *viewport) const
318{
319 if ((!viewport->resolves(m_renderLineString->latLonAltBox(), 2))) {
320 return;
321 }
322
323 LabelPositionFlags labelPositionFlags = NoLabel;
324 bool isValid = configurePainterForLabel(painter, viewport, labelPositionFlags);
325
326 if (isValid) {
327 GeoDataStyle::ConstPtr style = this->style();
328
329 // Activate the lines below to paint a label background which
330 // prevents antialiasing overpainting glitches, but leads to
331 // other glitches.
332 // QColor const color = style->polyStyle().paintedColor();
333 // painter->setBackground(QBrush(color));
334 // painter->setBackgroundMode(Qt::OpaqueMode);
335
336 const GeoDataLabelStyle &labelStyle = style->labelStyle();
337 painter->drawLabelsForPolygons(m_cachedPolygons, m_name, FollowLine, labelStyle.paintedColor());
338 }
339}
340
341bool GeoLineStringGraphicsItem::configurePainterForLine(GeoPainter *painter, const ViewportParams *viewport, const bool isOutline) const
342{
343 QPen currentPen = painter->pen();
344 GeoDataStyle::ConstPtr style = this->style();
345 if (!style) {
346 painter->setPen(QPen());
347 painter->setBackground(QBrush(Qt::transparent));
348 painter->setBackgroundMode(Qt::TransparentMode);
349 } else {
350 if (isOutline && !style->polyStyle().outline()) {
351 return false;
352 }
353
354 const GeoDataLineStyle &lineStyle = style->lineStyle();
355
356 // To save performance we avoid making changes to the painter's pen.
357 // So we first take a copy of the actual painter pen, make changes to it
358 // and only if the resulting pen is different from the actual pen
359 // we replace the painter's pen with our new pen.
360
361 // We want to avoid the mandatory detach in QPen::setColor(),
362 // so we carefully check whether applying the setter is needed
363 const QColor linePaintedColor = (!isOutline && (lineStyle.cosmeticOutline() && lineStyle.penStyle() == Qt::SolidLine))
364 ? style->polyStyle().paintedColor()
365 : lineStyle.paintedColor();
366 if (currentPen.color() != linePaintedColor) {
367 if (linePaintedColor.alpha() == 255) {
368 currentPen.setColor(linePaintedColor);
369 } else {
370 if (painter->mapQuality() != Marble::HighQuality && painter->mapQuality() != Marble::PrintQuality) {
371 QColor penColor = linePaintedColor;
372 if (penColor.alpha() != 0) {
373 penColor.setAlpha(255);
374 }
375 if (currentPen.color() != penColor) {
376 currentPen.setColor(penColor);
377 }
378 } else {
379 currentPen.setColor(linePaintedColor);
380 }
381 }
382 }
383
384 const float lineWidth = lineStyle.width();
385 const float linePhysicalWidth = lineStyle.physicalWidth();
386 float newLineWidth = lineWidth;
387 if (linePhysicalWidth != 0.0) {
388 const float scaledLinePhysicalWidth = float(viewport->radius()) / EARTH_RADIUS * linePhysicalWidth;
389 newLineWidth = scaledLinePhysicalWidth > lineWidth ? scaledLinePhysicalWidth : lineWidth;
390 }
391
392 if (!isOutline && lineStyle.cosmeticOutline() && lineStyle.penStyle() == Qt::SolidLine) {
393 if (newLineWidth > 2.5) {
394 newLineWidth -= 2.0;
395 }
396 }
397
398 const qreal lineDrawThreshold = isOutline ? 2.5 : 0.5;
399
400 // We want to avoid the mandatory detach in QPen::setWidthF(),
401 // so we carefully check whether applying the setter is needed
402 if (currentPen.widthF() != newLineWidth && newLineWidth != 0.0) {
403 if (newLineWidth < lineDrawThreshold) {
404 if (painter->pen().style() != Qt::NoPen) {
405 painter->setPen(Qt::NoPen);
406 }
407 return false; // Don't draw any outline and abort painter configuration early
408 }
409 currentPen.setWidthF(newLineWidth);
410 }
411
412 // No need to avoid detaches inside QPen::setCapsStyle() since Qt does that for us
413 const Qt::PenCapStyle lineCapStyle = lineStyle.capStyle();
414 currentPen.setCapStyle(lineCapStyle);
415
416 // No need to avoid detaches inside QPen::setStyle() since Qt does that for us
417 if (painter->mapQuality() == HighQuality || painter->mapQuality() == PrintQuality) {
418 const Qt::PenStyle linePenStyle = lineStyle.penStyle();
419 currentPen.setStyle(linePenStyle);
420
421 if (linePenStyle == Qt::CustomDashLine) {
422 // We want to avoid the mandatory detach in QPen::setDashPattern(),
423 // so we carefully check whether applying the setter is needed
424 if (currentPen.dashPattern() != lineStyle.dashPattern()) {
425 currentPen.setDashPattern(lineStyle.dashPattern());
426 }
427 }
428 } else {
429 currentPen.setStyle(Qt::SolidLine);
430 }
431
432 if (painter->pen() != currentPen) {
433 painter->setPen(currentPen);
434 }
435
436 // Set the background
437
438 if (!isOutline) {
439 if (lineStyle.background()) {
440 QBrush brush = painter->background();
441 brush.setColor(style->polyStyle().paintedColor());
442 painter->setBackground(brush);
443
444 painter->setBackgroundMode(Qt::OpaqueMode);
445 } else {
446 painter->setBackground(QBrush(Qt::transparent));
447 painter->setBackgroundMode(Qt::TransparentMode);
448 }
449 } else {
450 painter->setBackground(QBrush(Qt::transparent));
451 painter->setBackgroundMode(Qt::TransparentMode);
452 }
453 }
454
455 return true;
456}
457
458bool GeoLineStringGraphicsItem::configurePainterForLabel(GeoPainter *painter, const ViewportParams *viewport, LabelPositionFlags &labelPositionFlags) const
459{
460 QPen currentPen = painter->pen();
461 GeoDataStyle::ConstPtr style = this->style();
462 if (!style) {
463 painter->setPen(QPen());
464 } else {
465 const GeoDataLineStyle &lineStyle = style->lineStyle();
466
467 // To save performance we avoid making changes to the painter's pen.
468 // So we first take a copy of the actual painter pen, make changes to it
469 // and only if the resulting pen is different from the actual pen
470 // we replace the painter's pen with our new pen.
471
472 // We want to avoid the mandatory detach in QPen::setColor(),
473 // so we carefully check whether applying the setter is needed
474
475 const float lineWidth = lineStyle.width();
476 const float linePhysicalWidth = lineStyle.physicalWidth();
477 float newLineWidth = lineWidth;
478 if (linePhysicalWidth != 0.0) {
479 const float scaledLinePhysicalWidth = float(viewport->radius()) / EARTH_RADIUS * linePhysicalWidth;
480 newLineWidth = scaledLinePhysicalWidth > lineWidth ? scaledLinePhysicalWidth : lineWidth;
481 }
482
483 // We want to avoid the mandatory detach in QPen::setWidthF(),
484 // so we carefully check whether applying the setter is needed
485 if (currentPen.widthF() != newLineWidth && newLineWidth != 0.0) {
486 if (newLineWidth < 6.0) {
487 return false; // Don't draw any labels and abort painter configuration early
488 }
489 currentPen.setWidthF(newLineWidth);
490 }
491
492 if (painter->pen() != currentPen) {
493 painter->setPen(currentPen);
494 }
495 // else qDebug() << "Detach and painter change successfully Avoided!";
496
497 if (painter->brush().color() != Qt::transparent) {
498 painter->setBrush(QColor(Qt::transparent));
499 }
500 if (painter->backgroundMode() == Qt::OpaqueMode) {
501 painter->setBackgroundMode(Qt::TransparentMode);
502 painter->setBackground(QBrush(Qt::transparent));
503 }
504
505 // label styles
506 const GeoDataLabelStyle &labelStyle = style->labelStyle();
507 painter->setFont(labelStyle.font());
508 switch (labelStyle.alignment()) {
509 case GeoDataLabelStyle::Corner:
510 case GeoDataLabelStyle::Right:
511 labelPositionFlags |= LineStart;
512 break;
513 case GeoDataLabelStyle::Center:
514 labelPositionFlags |= LineCenter;
515 break;
516 }
517 }
518
519 return true;
520}
521
522bool GeoLineStringGraphicsItem::canMerge(const GeoDataCoordinates &a, const GeoDataCoordinates &b)
523{
524 return a.sphericalDistanceTo(b) * EARTH_RADIUS < 0.1;
525}
526
527}
This file contains the headers for ViewportParams.
QString name(GameStandardAction id)
bool isValid(QStringView ifopt)
Category category(StandardShortcut id)
Binds a QML item to a specific geodetic location in screen coordinates.
@ HighQuality
High quality (e.g. antialiasing for lines)
@ PrintQuality
Print quality.
void setColor(Qt::GlobalColor color)
int alpha() const const
void setAlpha(int alpha)
iterator begin()
iterator end()
bool isEmpty() const const
iterator begin()
iterator end()
iterator erase(const_iterator begin, const_iterator end)
T & first()
bool isEmpty() const const
T value(qsizetype i) const const
void addPolygon(const QPolygonF &polygon)
QPolygonF toFillPolygon(const QTransform &matrix) const const
QPainterPath createStroke(const QPainterPath &path) const const
void setWidth(qreal width)
QColor color() const const
QList< qreal > dashPattern() const const
void setCapStyle(Qt::PenCapStyle style)
void setColor(const QColor &color)
void setDashPattern(const QList< qreal > &pattern)
void setStyle(Qt::PenStyle style)
void setWidthF(qreal width)
qreal widthF() const const
QPolygon toPolygon() const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString join(QChar separator) const const
TransparentMode
WindingFill
transparent
PenCapStyle
SolidLine
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 8 2024 12:02:43 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.