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

KDE's Doxygen guidelines are available online.