Marble

CylindricalProjection.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2007 Inge Wallin <[email protected]>
4 // SPDX-FileCopyrightText: 2007-2012 Torsten Rahn <[email protected]>
5 // SPDX-FileCopyrightText: 2012 Cezar Mocan <[email protected]>
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.
23 static const int maxTessellationNodes = 200;
24 
25 namespace Marble {
26 
27 CylindricalProjection::CylindricalProjection()
28  : AbstractProjection( new CylindricalProjectionPrivate( this ) )
29 {
30 }
31 
32 CylindricalProjection::CylindricalProjection( CylindricalProjectionPrivate* dd )
33  : AbstractProjection( dd )
34 {
35 }
36 
37 CylindricalProjection::~CylindricalProjection()
38 {
39 }
40 
41 CylindricalProjectionPrivate::CylindricalProjectionPrivate( CylindricalProjection * parent )
42  : AbstractProjectionPrivate( parent ),
43  q_ptr( parent )
44 {
45 
46 }
47 
48 
49 QPainterPath 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 
79 bool CylindricalProjection::screenCoordinates( const GeoDataLineString &lineString,
80  const ViewportParams *viewport,
81  QVector<QPolygonF *> &polygons ) const
82 {
83 
84  Q_D( const CylindricalProjection );
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 }
98 int 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 
154 int 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 
239 int 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 
264 bool 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 
390 void CylindricalProjectionPrivate::translatePolygons( const QVector<QPolygonF *> &polygons,
391  QVector<QPolygonF *> &translatedPolygons,
392  qreal xOffset )
393 {
394  // mDebug() << "Translation: " << xOffset;
395  translatedPolygons.reserve(polygons.size());
396 
397  QVector<QPolygonF *>::const_iterator itPolygon = polygons.constBegin();
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 
408 void 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 
460 qreal 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 
bool isEmpty() const const
T & last()
void append(const T &value)
QVector::const_iterator constEnd() const const
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
A LineString that allows to store a contiguous set of line segments.
A public class that controls what is visible in the viewport of a Marble map.
const GeoDataLatLonAltBox & latLonAltBox() const override
Returns the smallest latLonAltBox that contains the LineString.
A base class for the Equirectangular and Mercator projections in Marble.
void addRect(const QRectF &rectangle)
Binds a QML item to a specific geodetic location in screen coordinates.
void translate(qreal dx, qreal dy)
void reserve(int size)
int size() const const
QVector::const_iterator constBegin() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon Dec 11 2023 04:09:38 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.