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
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
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 3d point representation.
GeoDataCoordinates nlerp(const GeoDataCoordinates &target, double t) const
nlerp (normalized linear interpolation) between this coordinates and the given target coordinates
void setAltitude(const qreal altitude)
set the altitude of the Point in meters
qreal altitude() const
return the altitude of the Point in meters
A base class for all projections in Marble.
qreal minLat() const
Returns the arbitrarily chosen minimum (southern) latitude.
qreal maxLat() const
Returns the arbitrarily chosen maximum (northern) latitude.
A base class for the Equirectangular and Mercator projections in Marble.
QPainterPath mapShape(const ViewportParams *viewport) const override
Returns the shape/outline of a map projection.
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)
bool testFlag(Enum flag) const const
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
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 14 2025 12:00:24 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.