Marble

CylindricalProjection.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later
2//
3// SPDX-FileCopyrightText: 2007 Inge Wallin <ingwa@kde.org>
4// SPDX-FileCopyrightText: 2007-2012 Torsten Rahn <rahn@kde.org>
5// SPDX-FileCopyrightText: 2012 Cezar Mocan <mocancezar@gmail.com>
6//
7
8// Local
10
11#include "CylindricalProjection_p.h"
12
13// Marble
14#include "GeoDataCoordinates.h"
15#include "GeoDataLatLonAltBox.h"
16#include "GeoDataLineString.h"
17#include "GeoDataLinearRing.h"
18#include "ViewportParams.h"
19
20#include <QPainterPath>
21
22// Maximum amount of nodes that are created automatically between actual nodes.
23static const int maxTessellationNodes = 200;
24
25namespace Marble
26{
27
28CylindricalProjection::CylindricalProjection()
29 : AbstractProjection(new CylindricalProjectionPrivate(this))
30{
31}
32
33CylindricalProjection::CylindricalProjection(CylindricalProjectionPrivate *dd)
34 : AbstractProjection(dd)
35{
36}
37
38CylindricalProjection::~CylindricalProjection() = default;
39
40CylindricalProjectionPrivate::CylindricalProjectionPrivate(CylindricalProjection *parent)
41 : AbstractProjectionPrivate(parent)
42 , q_ptr(parent)
43{
44}
45
46QPainterPath CylindricalProjection::mapShape(const ViewportParams *viewport) const
47{
48 // Convenience variables
49 int width = viewport->width();
50 int height = viewport->height();
51
52 qreal yTop;
53 qreal yBottom;
54 qreal xDummy;
55
56 // Get the top and bottom coordinates of the projected map.
57 screenCoordinates(0.0, maxLat(), viewport, xDummy, yTop);
58 screenCoordinates(0.0, minLat(), viewport, xDummy, yBottom);
59
60 // Don't let the map area be outside the image
61 if (yTop < 0)
62 yTop = 0;
63 if (yBottom > height)
64 yBottom = height;
65
66 QPainterPath mapShape;
67 mapShape.addRect(0, yTop, width, yBottom - yTop);
68
69 return mapShape;
70}
71
72bool CylindricalProjection::screenCoordinates(const GeoDataLineString &lineString, const ViewportParams *viewport, QList<QPolygonF *> &polygons) const
73{
75 // Compare bounding box size of the line string with the angularResolution
76 // Immediately return if the latLonAltBox is smaller.
77 if (!viewport->resolves(lineString.latLonAltBox())) {
78 // mDebug() << "Object too small to be resolved";
79 return false;
80 }
81
82 QList<QPolygonF *> subPolygons;
83 d->lineStringToPolygon(lineString, viewport, subPolygons);
84
85 polygons << subPolygons;
86 return polygons.isEmpty();
87}
88int CylindricalProjectionPrivate::tessellateLineSegment(const GeoDataCoordinates &aCoords,
89 qreal ax,
90 qreal ay,
91 const GeoDataCoordinates &bCoords,
92 qreal bx,
93 qreal by,
94 QList<QPolygonF *> &polygons,
95 const ViewportParams *viewport,
96 TessellationFlags f,
97 int mirrorCount,
98 qreal repeatDistance) const
99{
100 // We take the manhattan length as a distance approximation
101 // that can be too big by a factor of sqrt(2)
102 qreal distance = fabs((bx - ax)) + fabs((by - ay));
103#ifdef SAFE_DISTANCE
104 // Interpolate additional nodes if the line segment that connects the
105 // current or previous nodes might cross the viewport.
106 // The latter can pretty safely be excluded for most projections if both points
107 // are located on the same side relative to the viewport boundaries and if they are
108 // located more than half the line segment distance away from the viewport.
109 const qreal safeDistance = -0.5 * distance;
110 if (!(bx < safeDistance && ax < safeDistance) || !(by < safeDistance && ay < safeDistance)
111 || !(bx + safeDistance > viewport->width() && ax + safeDistance > viewport->width())
112 || !(by + safeDistance > viewport->height() && ay + safeDistance > viewport->height())) {
113#endif
114 int maxTessellationFactor = viewport->radius() < 20000 ? 10 : 20;
115 int const finalTessellationPrecision = qBound(2, viewport->radius() / 200, maxTessellationFactor) * tessellationPrecision;
116
117 // Let the line segment follow the spherical surface
118 // if the distance between the previous point and the current point
119 // on screen is too big
120 if (distance > finalTessellationPrecision) {
121 const int tessellatedNodes = qMin<int>(distance / finalTessellationPrecision, maxTessellationNodes);
122
123 mirrorCount = processTessellation(aCoords, bCoords, tessellatedNodes, polygons, viewport, f, mirrorCount, repeatDistance);
124 } else {
125 mirrorCount = crossDateLine(aCoords, bCoords, bx, by, polygons, mirrorCount, repeatDistance);
126 }
127#ifdef SAFE_DISTANCE
128 }
129#endif
130 return mirrorCount;
131}
132
133int CylindricalProjectionPrivate::processTessellation(const GeoDataCoordinates &previousCoords,
134 const GeoDataCoordinates &currentCoords,
135 int tessellatedNodes,
136 QList<QPolygonF *> &polygons,
137 const ViewportParams *viewport,
138 TessellationFlags f,
139 int mirrorCount,
140 qreal repeatDistance) const
141{
142 const bool clampToGround = f.testFlag(FollowGround);
143 const bool followLatitudeCircle = f.testFlag(RespectLatitudeCircle) && previousCoords.latitude() == currentCoords.latitude();
144
145 // Calculate steps for tessellation: lonDiff and altDiff
146 qreal lonDiff = 0.0;
147 if (followLatitudeCircle) {
148 const int previousSign = previousCoords.longitude() > 0 ? 1 : -1;
149 const int currentSign = currentCoords.longitude() > 0 ? 1 : -1;
150
151 lonDiff = currentCoords.longitude() - previousCoords.longitude();
152 if (previousSign != currentSign && fabs(previousCoords.longitude()) + fabs(currentCoords.longitude()) > M_PI) {
153 if (previousSign > currentSign) {
154 // going eastwards ->
155 lonDiff += 2 * M_PI;
156 } else {
157 // going westwards ->
158 lonDiff -= 2 * M_PI;
159 }
160 }
161 if (fabs(lonDiff) == 2 * M_PI) {
162 return mirrorCount;
163 }
164 }
165
166 // Create the tessellation nodes.
167 GeoDataCoordinates previousTessellatedCoords = previousCoords;
168 for (int i = 1; i <= tessellatedNodes; ++i) {
169 const qreal t = (qreal)(i) / (qreal)(tessellatedNodes + 1);
170
171 GeoDataCoordinates currentTessellatedCoords;
172
173 if (followLatitudeCircle) {
174 // To tessellate along latitude circles use the
175 // linear interpolation of the longitude.
176 // interpolate the altitude, too
177 const qreal altDiff = currentCoords.altitude() - previousCoords.altitude();
178 const qreal altitude = altDiff * t + previousCoords.altitude();
179 const qreal lon = lonDiff * t + previousCoords.longitude();
180 const qreal lat = previousTessellatedCoords.latitude();
181
182 currentTessellatedCoords = GeoDataCoordinates(lon, lat, altitude);
183 } else {
184 // To tessellate along great circles use the
185 // normalized linear interpolation ("NLERP") for latitude and longitude.
186 currentTessellatedCoords = previousCoords.nlerp(currentCoords, t);
187 }
188
189 if (clampToGround) {
190 currentTessellatedCoords.setAltitude(0);
191 }
192
193 Q_Q(const CylindricalProjection);
194 qreal bx, by;
195 q->screenCoordinates(currentTessellatedCoords, viewport, bx, by);
196 mirrorCount = crossDateLine(previousTessellatedCoords, currentTessellatedCoords, bx, by, polygons, mirrorCount, repeatDistance);
197 previousTessellatedCoords = currentTessellatedCoords;
198 }
199
200 // For the clampToGround case add the "current" coordinate after adding all other nodes.
201 GeoDataCoordinates currentModifiedCoords(currentCoords);
202 if (clampToGround) {
203 currentModifiedCoords.setAltitude(0.0);
204 }
205 Q_Q(const CylindricalProjection);
206 qreal bx, by;
207 q->screenCoordinates(currentModifiedCoords, viewport, bx, by);
208 mirrorCount = crossDateLine(previousTessellatedCoords, currentModifiedCoords, bx, by, polygons, mirrorCount, repeatDistance);
209 return mirrorCount;
210}
211
212int CylindricalProjectionPrivate::crossDateLine(const GeoDataCoordinates &aCoord,
213 const GeoDataCoordinates &bCoord,
214 qreal bx,
215 qreal by,
216 QList<QPolygonF *> &polygons,
217 int mirrorCount,
218 qreal repeatDistance)
219{
220 qreal aLon = aCoord.longitude();
221 qreal aSign = aLon > 0 ? 1 : -1;
222
223 qreal bLon = bCoord.longitude();
224 qreal bSign = bLon > 0 ? 1 : -1;
225
226 qreal delta = 0;
227 if (aSign != bSign && fabs(aLon) + fabs(bLon) > M_PI) {
228 int sign = aSign > bSign ? 1 : -1;
229 mirrorCount += sign;
230 }
231 delta = repeatDistance * mirrorCount;
232 *polygons.last() << QPointF(bx + delta, by);
233
234 return mirrorCount;
235}
236
237bool CylindricalProjectionPrivate::lineStringToPolygon(const GeoDataLineString &lineString, const ViewportParams *viewport, QList<QPolygonF *> &polygons) const
238{
239 const TessellationFlags f = lineString.tessellationFlags();
240 bool const tessellate = lineString.tessellate();
241 const bool noFilter = f.testFlag(PreventNodeFiltering);
242
243 qreal x = 0;
244 qreal y = 0;
245
246 qreal previousX = -1.0;
247 qreal previousY = -1.0;
248
249 int mirrorCount = 0;
250 qreal distance = repeatDistance(viewport);
251
252 auto polygon = new QPolygonF;
253 if (!tessellate) {
254 polygon->reserve(lineString.size());
255 }
256 polygons.append(polygon);
257
258 GeoDataLineString::ConstIterator itCoords = lineString.constBegin();
259 GeoDataLineString::ConstIterator itPreviousCoords = lineString.constBegin();
260
261 GeoDataLineString::ConstIterator itBegin = lineString.constBegin();
262 GeoDataLineString::ConstIterator itEnd = lineString.constEnd();
263
264 bool processingLastNode = false;
265
266 // We use a while loop to be able to cover linestrings as well as linear rings:
267 // Linear rings require to tessellate the path from the last node to the first node
268 // which isn't really convenient to achieve with a for loop ...
269
270 const bool isLong = lineString.size() > 10;
271 const int maximumDetail = levelForResolution(viewport->angularResolution());
272 // The first node of optimized linestrings has a non-zero detail value.
273 const bool hasDetail = itBegin->detail() != 0;
274
275 bool isStraight = lineString.latLonAltBox().height() == 0 || lineString.latLonAltBox().width() == 0;
276
277 Q_Q(const CylindricalProjection);
278 bool const isClosed = lineString.isClosed();
279 while (itCoords != itEnd) {
280 // Optimization for line strings with a big amount of nodes
281 bool skipNode = (hasDetail ? itCoords->detail() > maximumDetail
282 : isLong && !processingLastNode && itCoords != itBegin && !viewport->resolves(*itPreviousCoords, *itCoords));
283
284 if (!skipNode || noFilter) {
285 q->screenCoordinates(*itCoords, viewport, x, y);
286
287 // Initializing variables that store the values of the previous iteration
288 if (!processingLastNode && itCoords == itBegin) {
289 itPreviousCoords = itCoords;
290 previousX = x;
291 previousY = y;
292 }
293
294 // This if-clause contains the section that tessellates the line
295 // segments of a linestring. If you are about to learn how the code of
296 // this class works you can safely ignore this section for a start.
297 if (tessellate && !isStraight) {
298 mirrorCount = tessellateLineSegment(*itPreviousCoords, previousX, previousY, *itCoords, x, y, polygons, viewport, f, mirrorCount, distance);
299 }
300
301 else {
302 // special case for polys which cross dateline but have no Tesselation Flag
303 // the expected rendering is a screen coordinates straight line between
304 // points, but in projections with repeatX things are not smooth
305 mirrorCount = crossDateLine(*itPreviousCoords, *itCoords, x, y, polygons, mirrorCount, distance);
306 }
307
308 itPreviousCoords = itCoords;
309 previousX = x;
310 previousY = y;
311 }
312
313 // Here we modify the condition to be able to process the
314 // first node after the last node in a LinearRing.
315
316 if (processingLastNode) {
317 break;
318 }
319 ++itCoords;
320
321 if (isClosed && itCoords == itEnd) {
322 itCoords = itBegin;
323 processingLastNode = true;
324 }
325 }
326
327 // Closing e.g. in the Antarctica case.
328 // This code makes the assumption that
329 // - the first node is located at 180 E
330 // - and the last node is located at 180 W
331 // TODO: add a similar pattern in the crossDateLine() code.
332 /*
333 GeoDataLatLonAltBox box = lineString.latLonAltBox();
334 if( lineString.isClosed() && box.width() == 2*M_PI ) {
335 QPolygonF *poly = polygons.last();
336 if( box.containsPole( NorthPole ) ) {
337 qreal topMargin = 0.0;
338 qreal dummy = 0.0;
339 q_ptr->screenCoordinates(0.0, q_ptr->maxLat(), viewport, topMargin, dummy );
340 poly->push_back( QPointF( poly->last().x(), topMargin ) );
341 poly->push_back( QPointF( poly->first().x(), topMargin ) );
342 } else {
343 qreal bottomMargin = 0.0;
344 qreal dummy = 0.0;
345 q_ptr->screenCoordinates(0.0, q_ptr->minLat(), viewport, bottomMargin, dummy );
346 poly->push_back( QPointF( poly->last().x(), bottomMargin ) );
347 poly->push_back( QPointF( poly->first().x(), bottomMargin ) );
348 }
349 } */
350
351 repeatPolygons(viewport, polygons);
352
353 return polygons.isEmpty();
354}
355
356void CylindricalProjectionPrivate::translatePolygons(const QList<QPolygonF *> &polygons, QList<QPolygonF *> &translatedPolygons, qreal xOffset)
357{
358 // mDebug() << "Translation: " << xOffset;
359 translatedPolygons.reserve(polygons.size());
360
361 QList<QPolygonF *>::const_iterator itPolygon = polygons.constBegin();
363
364 for (; itPolygon != itEnd; ++itPolygon) {
365 auto polygon = new QPolygonF;
366 *polygon = **itPolygon;
367 polygon->translate(xOffset, 0);
368 translatedPolygons.append(polygon);
369 }
370}
371
372void CylindricalProjectionPrivate::repeatPolygons(const ViewportParams *viewport, QList<QPolygonF *> &polygons) const
373{
374 Q_Q(const CylindricalProjection);
375
376 qreal xEast = 0;
377 qreal xWest = 0;
378 qreal y = 0;
379
380 // Choose a latitude that is inside the viewport.
381 const qreal centerLatitude = viewport->viewLatLonAltBox().center().latitude();
382
383 const GeoDataCoordinates westCoords(-M_PI, centerLatitude);
384 const GeoDataCoordinates eastCoords(+M_PI, centerLatitude);
385
386 q->screenCoordinates(westCoords, viewport, xWest, y);
387 q->screenCoordinates(eastCoords, viewport, xEast, y);
388
389 if (xWest <= 0 && xEast >= viewport->width() - 1) {
390 // mDebug() << "No repeats";
391 return;
392 }
393
394 const qreal repeatXInterval = xEast - xWest;
395
396 const int repeatsLeft = (xWest > 0) ? (int)(xWest / repeatXInterval) + 1 : 0;
397 const int repeatsRight = (xEast < viewport->width()) ? (int)((viewport->width() - xEast) / repeatXInterval) + 1 : 0;
398
399 QList<QPolygonF *> repeatedPolygons;
400
401 for (int it = repeatsLeft; it > 0; --it) {
402 const qreal xOffset = -it * repeatXInterval;
403 QList<QPolygonF *> translatedPolygons;
404 translatePolygons(polygons, translatedPolygons, xOffset);
405 repeatedPolygons << translatedPolygons;
406 }
407
408 repeatedPolygons << polygons;
409
410 for (int it = 1; it <= repeatsRight; ++it) {
411 const qreal xOffset = +it * repeatXInterval;
412 QList<QPolygonF *> translatedPolygons;
413 translatePolygons(polygons, translatedPolygons, xOffset);
414 repeatedPolygons << translatedPolygons;
415 }
416
417 polygons = repeatedPolygons;
418
419 // mDebug() << "Coordinates: " << xWest << xEast
420 // << "Repeats: " << repeatsLeft << repeatsRight;
421}
422
423qreal CylindricalProjectionPrivate::repeatDistance(const ViewportParams *viewport) const
424{
425 // Choose a latitude that is inside the viewport.
426 qreal centerLatitude = viewport->viewLatLonAltBox().center().latitude();
427
428 GeoDataCoordinates westCoords(-M_PI, centerLatitude);
429 GeoDataCoordinates eastCoords(+M_PI, centerLatitude);
430 qreal xWest, xEast, dummyY;
431
432 Q_Q(const AbstractProjection);
433
434 q->screenCoordinates(westCoords, viewport, xWest, dummyY);
435 q->screenCoordinates(eastCoords, viewport, xEast, dummyY);
436
437 return xEast - xWest;
438}
439
440}
This file contains the headers for CylindricalProjection.
This file contains the headers for ViewportParams.
A base class for the Equirectangular and Mercator projections in Marble.
A LineString that allows to store a contiguous set of line segments.
const GeoDataLatLonAltBox & latLonAltBox() const override
Returns the smallest latLonAltBox that contains the LineString.
A public class that controls what is visible in the viewport of a Marble map.
Binds a QML item to a specific geodetic location in screen coordinates.
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
void append(QList< T > &&value)
const_iterator constBegin() const const
const_iterator constEnd() const const
bool isEmpty() const const
T & last()
void reserve(qsizetype size)
qsizetype size() const const
void addRect(const QRectF &rectangle)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 8 2024 12:02:44 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.