1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2014 Gábor Péterffy <peterffy95@gmail.org>
4 //
5 
6 // Local
7 #include "AzimuthalProjection.h"
8 #include "AzimuthalProjection_p.h"
9 #include "AbstractProjection_p.h"
10 
11 // Marble
12 #include "GeoDataLinearRing.h"
13 #include "GeoDataLineString.h"
14 #include "GeoDataCoordinates.h"
15 #include "GeoDataLatLonAltBox.h"
16 #include "ViewportParams.h"
17 
18 #include <QPainterPath>
19 
20 
21 namespace Marble {
22 
isClippedToSphere() const23 bool AzimuthalProjection::isClippedToSphere() const
24 {
25     return true;
26 }
27 
clippingRadius() const28 qreal AzimuthalProjection::clippingRadius() const
29 {
30     return 1;
31 }
32 
screenCoordinates(const GeoDataLineString & lineString,const ViewportParams * viewport,QVector<QPolygonF * > & polygons) const33 bool AzimuthalProjection::screenCoordinates( const GeoDataLineString &lineString,
34                                                   const ViewportParams *viewport,
35                                                   QVector<QPolygonF *> &polygons ) const
36 {
37 
38     Q_D( const AzimuthalProjection );
39     // Compare bounding box size of the line string with the angularResolution
40     // Immediately return if the latLonAltBox is smaller.
41     if ( !viewport->resolves( lineString.latLonAltBox() ) ) {
42 //      mDebug() << "Object too small to be resolved";
43         return false;
44     }
45 
46     d->lineStringToPolygon( lineString, viewport, polygons );
47     return true;
48 }
49 
mapCoversViewport(const ViewportParams * viewport) const50 bool AzimuthalProjection::mapCoversViewport( const ViewportParams *viewport ) const
51 {
52         qint64  radius = viewport->radius() * viewport->currentProjection()->clippingRadius();
53         qint64  width  = viewport->width();
54         qint64  height = viewport->height();
55 
56         // This first test is a quick one that will catch all really big
57         // radii and prevent overflow in the real test.
58         if ( radius > width + height )
59             return true;
60 
61         // This is the real test.  The 4 is because we are really
62         // comparing to width/2 and height/2.
63         if ( 4 * radius * radius >= width * width + height * height )
64             return true;
65 
66         return false;
67 }
68 
latLonAltBox(const QRect & screenRect,const ViewportParams * viewport) const69 GeoDataLatLonAltBox AzimuthalProjection::latLonAltBox( const QRect& screenRect,
70                                                        const ViewportParams *viewport ) const
71 {
72     // For the case where the whole viewport gets covered there is a
73     // pretty dirty and generic detection algorithm:
74     GeoDataLatLonAltBox latLonAltBox = AbstractProjection::latLonAltBox( screenRect, viewport );
75 
76     // If the whole globe is visible we can easily calculate
77     // analytically the lon-/lat- range.
78     qreal pitch = GeoDataCoordinates::normalizeLat( viewport->planetAxis().pitch() );
79 
80     if ( 2.0 * viewport->radius() <= viewport->height()
81          &&  2.0 * viewport->radius() <= viewport->width() )
82     {
83         // Unless the planetaxis is in the screen plane the allowed longitude range
84         // covers full -180 deg to +180 deg:
85         if ( pitch > 0.0 && pitch < +M_PI ) {
86             latLonAltBox.setWest(  -M_PI );
87             latLonAltBox.setEast(  +M_PI );
88             latLonAltBox.setNorth( +fabs( M_PI / 2.0 - fabs( pitch ) ) );
89             latLonAltBox.setSouth( -M_PI / 2.0 );
90         }
91         if ( pitch < 0.0 && pitch > -M_PI ) {
92             latLonAltBox.setWest(  -M_PI );
93             latLonAltBox.setEast(  +M_PI );
94             latLonAltBox.setNorth( +M_PI / 2.0 );
95             latLonAltBox.setSouth( -fabs( M_PI / 2.0 - fabs( pitch ) ) );
96         }
97 
98         // Last but not least we deal with the rare case where the
99         // globe is fully visible and pitch = 0.0 or pitch = -M_PI or
100         // pitch = +M_PI
101         if ( pitch == 0.0 || pitch == -M_PI || pitch == +M_PI ) {
102             qreal yaw = viewport->planetAxis().yaw();
103             latLonAltBox.setWest( GeoDataCoordinates::normalizeLon( yaw - M_PI / 2.0 ) );
104             latLonAltBox.setEast( GeoDataCoordinates::normalizeLon( yaw + M_PI / 2.0 ) );
105             latLonAltBox.setNorth( +M_PI / 2.0 );
106             latLonAltBox.setSouth( -M_PI / 2.0 );
107         }
108 
109         return latLonAltBox;
110     }
111 
112     // Now we check whether maxLat (e.g. the north pole) gets displayed
113     // inside the viewport to get more accurate values for east and west.
114 
115     // We need a point on the screen at maxLat that definitely gets displayed:
116     qreal averageLongitude = ( latLonAltBox.west() + latLonAltBox.east() ) / 2.0;
117 
118     GeoDataCoordinates maxLatPoint( averageLongitude, maxLat(), 0.0, GeoDataCoordinates::Radian );
119     GeoDataCoordinates minLatPoint( averageLongitude, minLat(), 0.0, GeoDataCoordinates::Radian );
120 
121     qreal dummyX, dummyY; // not needed
122     bool dummyVal;
123 
124     if ( screenCoordinates( maxLatPoint, viewport, dummyX, dummyY, dummyVal ) ||
125          screenCoordinates( minLatPoint, viewport, dummyX, dummyY, dummyVal ) ) {
126         latLonAltBox.setWest( -M_PI );
127         latLonAltBox.setEast( +M_PI );
128     }
129 
130     return latLonAltBox;
131 }
132 
mapShape(const ViewportParams * viewport) const133 QPainterPath AzimuthalProjection::mapShape( const ViewportParams *viewport ) const
134 {
135     int  radius    = viewport->radius() * viewport->currentProjection()->clippingRadius();
136     int  imgWidth  = viewport->width();
137     int  imgHeight = viewport->height();
138 
139     QPainterPath fullRect;
140     fullRect.addRect(  0 , 0 , imgWidth, imgHeight );
141 
142     // If the globe covers the whole image, then the projected region represents
143     // all of the image.
144     // Otherwise the active region has got the shape of the visible globe.
145 
146     if ( !viewport->mapCoversViewport() ) {
147         QPainterPath mapShape;
148         mapShape.addEllipse(
149             imgWidth  / 2 - radius,
150             imgHeight / 2 - radius,
151             2 * radius,
152             2 * radius );
153         return mapShape.intersected( fullRect );
154     }
155 
156     return fullRect;
157 }
158 
AzimuthalProjection(AzimuthalProjectionPrivate * dd)159 AzimuthalProjection::AzimuthalProjection(AzimuthalProjectionPrivate * dd) :
160     AbstractProjection( dd )
161 {
162 }
163 
~AzimuthalProjection()164 AzimuthalProjection::~AzimuthalProjection()
165 {
166 }
167 
tessellateLineSegment(const GeoDataCoordinates & aCoords,qreal ax,qreal ay,const GeoDataCoordinates & bCoords,qreal bx,qreal by,QVector<QPolygonF * > & polygons,const ViewportParams * viewport,TessellationFlags f,bool allowLatePolygonCut) const168 void AzimuthalProjectionPrivate::tessellateLineSegment( const GeoDataCoordinates &aCoords,
169                                                 qreal ax, qreal ay,
170                                                 const GeoDataCoordinates &bCoords,
171                                                 qreal bx, qreal by,
172                                                 QVector<QPolygonF*> &polygons,
173                                                 const ViewportParams *viewport,
174                                                 TessellationFlags f,
175                                                 bool allowLatePolygonCut ) const
176 {
177     // We take the manhattan length as a distance approximation
178     // that can be too big by a factor of sqrt(2)
179     qreal distance = fabs((bx - ax)) + fabs((by - ay));
180 #ifdef SAFE_DISTANCE
181     // Interpolate additional nodes if the line segment that connects the
182     // current or previous nodes might cross the viewport.
183     // The latter can pretty safely be excluded for most projections if both points
184     // are located on the same side relative to the viewport boundaries and if they are
185     // located more than half the line segment distance away from the viewport.
186     const qreal safeDistance = - 0.5 * distance;
187     if (   !( bx < safeDistance && ax < safeDistance )
188         || !( by < safeDistance && ay < safeDistance )
189         || !( bx + safeDistance > viewport->width()
190             && ax + safeDistance > viewport->width() )
191         || !( by + safeDistance > viewport->height()
192             && ay + safeDistance > viewport->height() )
193     )
194     {
195 #endif
196         int maxTessellationFactor = viewport->radius() < 20000 ? 10 : 20;
197         int const finalTessellationPrecision = qBound(2, viewport->radius()/200, maxTessellationFactor) * tessellationPrecision;
198 
199         // Let the line segment follow the spherical surface
200         // if the distance between the previous point and the current point
201         // on screen is too big
202 
203         if ( distance > finalTessellationPrecision ) {
204             const int tessellatedNodes = qMin<int>( distance / finalTessellationPrecision, maxTessellationNodes );
205 
206             processTessellation( aCoords, bCoords,
207                                  tessellatedNodes,
208                                  polygons,
209                                  viewport,
210                                  f,
211                                  allowLatePolygonCut);
212         }
213         else {
214             crossHorizon( bCoords, polygons, viewport, allowLatePolygonCut );
215         }
216 #ifdef SAFE_DISTANCE
217     }
218 #endif
219 }
220 
221 
processTessellation(const GeoDataCoordinates & previousCoords,const GeoDataCoordinates & currentCoords,int tessellatedNodes,QVector<QPolygonF * > & polygons,const ViewportParams * viewport,TessellationFlags f,bool allowLatePolygonCut) const222 void AzimuthalProjectionPrivate::processTessellation( const GeoDataCoordinates &previousCoords,
223                                                     const GeoDataCoordinates &currentCoords,
224                                                     int tessellatedNodes,
225                                                     QVector<QPolygonF*> &polygons,
226                                                     const ViewportParams *viewport,
227                                                     TessellationFlags f,
228                                                     bool allowLatePolygonCut ) const
229 {
230 
231     const bool clampToGround = f.testFlag( FollowGround );
232     const bool followLatitudeCircle = f.testFlag( RespectLatitudeCircle )
233                                       && previousCoords.latitude() == currentCoords.latitude();
234 
235     // Calculate steps for tessellation: lonDiff and altDiff
236     qreal lonDiff = 0.0;
237     if ( followLatitudeCircle ) {
238         const int previousSign = previousCoords.longitude() > 0 ? 1 : -1;
239         const int currentSign = currentCoords.longitude() > 0 ? 1 : -1;
240 
241         lonDiff = currentCoords.longitude() - previousCoords.longitude();
242         if ( previousSign != currentSign
243              && fabs(previousCoords.longitude()) + fabs(currentCoords.longitude()) > M_PI ) {
244             if ( previousSign > currentSign ) {
245                 // going eastwards ->
246                 lonDiff += 2 * M_PI ;
247             } else {
248                 // going westwards ->
249                 lonDiff -= 2 * M_PI;
250             }
251         }
252     }
253 
254     // Create the tessellation nodes.
255     GeoDataCoordinates previousTessellatedCoords = previousCoords;
256     for ( int i = 1; i <= tessellatedNodes; ++i ) {
257         const qreal t = (qreal)(i) / (qreal)( tessellatedNodes + 1 );
258 
259         GeoDataCoordinates currentTessellatedCoords;
260 
261         if ( followLatitudeCircle ) {
262             // To tessellate along latitude circles use the
263             // linear interpolation of the longitude.
264             const qreal altDiff = currentCoords.altitude() - previousCoords.altitude();
265             const qreal altitude = altDiff * t + previousCoords.altitude();
266             const qreal lon = lonDiff * t + previousCoords.longitude();
267             const qreal lat = previousTessellatedCoords.latitude();
268 
269             currentTessellatedCoords = GeoDataCoordinates(lon, lat, altitude);
270         }
271         else {
272             // To tessellate along great circles use the
273             // normalized linear interpolation ("NLERP") for latitude and longitude.
274             currentTessellatedCoords = previousCoords.nlerp(currentCoords, t);
275         }
276 
277         if (clampToGround) {
278             currentTessellatedCoords.setAltitude(0);
279         }
280 
281         crossHorizon( currentTessellatedCoords, polygons, viewport, allowLatePolygonCut );
282         previousTessellatedCoords = currentTessellatedCoords;
283     }
284 
285     // For the clampToGround case add the "current" coordinate after adding all other nodes.
286     GeoDataCoordinates currentModifiedCoords( currentCoords );
287     if ( clampToGround ) {
288         currentModifiedCoords.setAltitude( 0.0 );
289     }
290     crossHorizon( currentModifiedCoords, polygons, viewport, allowLatePolygonCut );
291 }
292 
crossHorizon(const GeoDataCoordinates & bCoord,QVector<QPolygonF * > & polygons,const ViewportParams * viewport,bool allowLatePolygonCut) const293 void AzimuthalProjectionPrivate::crossHorizon( const GeoDataCoordinates & bCoord,
294                                               QVector<QPolygonF*> &polygons,
295                                               const ViewportParams *viewport,
296                                               bool allowLatePolygonCut
297                                              ) const
298 {
299     qreal x, y;
300     bool globeHidesPoint;
301 
302     Q_Q( const AbstractProjection );
303 
304     q->screenCoordinates( bCoord, viewport, x, y, globeHidesPoint );
305 
306     if( !globeHidesPoint ) {
307         *polygons.last() << QPointF( x, y );
308     }
309     else {
310         if ( allowLatePolygonCut && !polygons.last()->isEmpty() ) {
311             QPolygonF *path = new QPolygonF;
312             polygons.append( path );
313         }
314     }
315 }
316 
lineStringToPolygon(const GeoDataLineString & lineString,const ViewportParams * viewport,QVector<QPolygonF * > & polygons) const317 bool AzimuthalProjectionPrivate::lineStringToPolygon( const GeoDataLineString &lineString,
318                                               const ViewportParams *viewport,
319                                               QVector<QPolygonF *> &polygons ) const
320 {
321     Q_Q( const AzimuthalProjection );
322 
323     const TessellationFlags f = lineString.tessellationFlags();
324     bool const tessellate = lineString.tessellate();
325     const bool noFilter = f.testFlag(PreventNodeFiltering);
326 
327 
328     qreal x = 0;
329     qreal y = 0;
330     bool globeHidesPoint = false;
331 
332     qreal previousX = -1.0;
333     qreal previousY = -1.0;
334     bool previousGlobeHidesPoint = false;
335 
336     qreal horizonX = -1.0;
337     qreal horizonY = -1.0;
338 
339     QPolygonF * polygon = new QPolygonF;
340     if (!tessellate) {
341         polygon->reserve(lineString.size());
342     }
343     polygons.append( polygon );
344 
345     GeoDataLineString::ConstIterator itCoords = lineString.constBegin();
346     GeoDataLineString::ConstIterator itPreviousCoords = lineString.constBegin();
347 
348     // Some projections display the earth in a way so that there is a
349     // foreside and a backside.
350     // The horizon is the line (usually a circle) which separates both
351     // sides from each other and resembles the map shape.
352     GeoDataCoordinates horizonCoords;
353 
354     // A horizon pair is a pair of two subsequent horizon crossings:
355     // The first one describes the point where a line string disappears behind the horizon.
356     // and where horizonPair is set to true.
357     // The second one describes the point where the line string reappears.
358     // In this case the two points are connected and horizonPair is set to false again.
359     bool horizonPair = false;
360     GeoDataCoordinates horizonDisappearCoords;
361 
362     // If the first horizon crossing in a line string describes the appearance of
363     // a line string then we call it a "horizon orphan" and horizonOrphan is set to true.
364     // In this case once the last horizon crossing in the line string is reached
365     // it needs to be connected to the orphan.
366     bool horizonOrphan = false;
367     GeoDataCoordinates horizonOrphanCoords;
368 
369     GeoDataLineString::ConstIterator itBegin = lineString.constBegin();
370     GeoDataLineString::ConstIterator itEnd = lineString.constEnd();
371 
372     bool processingLastNode = false;
373 
374     // We use a while loop to be able to cover linestrings as well as linear rings:
375     // Linear rings require to tessellate the path from the last node to the first node
376     // which isn't really convenient to achieve with a for loop ...
377 
378     const bool isLong = lineString.size() > 10;
379     const int maximumDetail = levelForResolution(viewport->angularResolution());
380     // The first node of optimized linestrings has a non-zero detail value.
381     const bool hasDetail = itBegin->detail() != 0;
382 
383     while ( itCoords != itEnd )
384     {
385         // Optimization for line strings with a big amount of nodes
386         bool skipNode = (hasDetail ? itCoords->detail() > maximumDetail
387                 : itCoords != itBegin && isLong && !processingLastNode &&
388                 !viewport->resolves( *itPreviousCoords, *itCoords ) );
389 
390         if ( !skipNode || noFilter) {
391 
392             q->screenCoordinates( *itCoords, viewport, x, y, globeHidesPoint );
393 
394             // Initializing variables that store the values of the previous iteration
395             if ( !processingLastNode && itCoords == itBegin ) {
396                 previousGlobeHidesPoint = globeHidesPoint;
397                 itPreviousCoords = itCoords;
398                 previousX = x;
399                 previousY = y;
400             }
401 
402             // Check for the "horizon case" (which is present e.g. for the spherical projection
403             const bool isAtHorizon = ( globeHidesPoint || previousGlobeHidesPoint ) &&
404                                      ( globeHidesPoint !=  previousGlobeHidesPoint );
405 
406             if ( isAtHorizon ) {
407                 // Handle the "horizon case"
408                 horizonCoords = findHorizon( *itPreviousCoords, *itCoords, viewport, f );
409 
410                 if ( lineString.isClosed() ) {
411                     if ( horizonPair ) {
412                         horizonToPolygon( viewport, horizonDisappearCoords, horizonCoords, polygons.last() );
413                         horizonPair = false;
414                     }
415                     else {
416                         if ( globeHidesPoint ) {
417                             horizonDisappearCoords = horizonCoords;
418                             horizonPair = true;
419                         }
420                         else {
421                             horizonOrphanCoords = horizonCoords;
422                             horizonOrphan = true;
423                         }
424                     }
425                 }
426 
427                 q->screenCoordinates( horizonCoords, viewport, horizonX, horizonY );
428 
429                 // If the line appears on the visible half we need
430                 // to add an interpolated point at the horizon as the previous point.
431                 if ( previousGlobeHidesPoint ) {
432                     *polygons.last() << QPointF( horizonX, horizonY );
433                 }
434             }
435 
436             // This if-clause contains the section that tessellates the line
437             // segments of a linestring. If you are about to learn how the code of
438             // this class works you can safely ignore this section for a start.
439 
440             if ( lineString.tessellate() /* && ( isVisible || previousIsVisible ) */ ) {
441 
442                 if ( !isAtHorizon ) {
443 
444                     tessellateLineSegment( *itPreviousCoords, previousX, previousY,
445                                            *itCoords, x, y,
446                                            polygons, viewport,
447                                            f, !lineString.isClosed() );
448 
449                 }
450                 else {
451                     // Connect the interpolated  point at the horizon with the
452                     // current or previous point in the line.
453                     if ( previousGlobeHidesPoint ) {
454                         tessellateLineSegment( horizonCoords, horizonX, horizonY,
455                                                *itCoords, x, y,
456                                                polygons, viewport,
457                                                f, !lineString.isClosed() );
458                     }
459                     else {
460                         tessellateLineSegment( *itPreviousCoords, previousX, previousY,
461                                                horizonCoords, horizonX, horizonY,
462                                                polygons, viewport,
463                                                f, !lineString.isClosed() );
464                     }
465                 }
466             }
467             else {
468                 if ( !globeHidesPoint ) {
469                     *polygons.last() << QPointF( x, y );
470                 }
471                 else {
472                     if ( !previousGlobeHidesPoint && isAtHorizon ) {
473                         *polygons.last() << QPointF( horizonX, horizonY );
474                     }
475                 }
476             }
477 
478             if ( globeHidesPoint ) {
479                 if (   !previousGlobeHidesPoint
480                     && !lineString.isClosed()
481                     ) {
482                     polygons.append( new QPolygonF );
483                 }
484             }
485 
486             previousGlobeHidesPoint = globeHidesPoint;
487             itPreviousCoords = itCoords;
488             previousX = x;
489             previousY = y;
490         }
491 
492         // Here we modify the condition to be able to process the
493         // first node after the last node in a LinearRing.
494 
495         if ( processingLastNode ) {
496             break;
497         }
498         ++itCoords;
499 
500         if ( itCoords == itEnd  && lineString.isClosed() ) {
501             itCoords = itBegin;
502             processingLastNode = true;
503         }
504     }
505 
506     // In case of horizon crossings, make sure that we always get a
507     // polygon closed correctly.
508     if ( horizonOrphan && lineString.isClosed() ) {
509         horizonToPolygon( viewport, horizonCoords, horizonOrphanCoords, polygons.last() );
510     }
511 
512     if ( polygons.last()->size() <= 1 ){
513         delete polygons.last();
514         polygons.pop_back(); // Clean up "unused" empty polygon instances
515     }
516 
517     return polygons.isEmpty();
518 }
519 
horizonToPolygon(const ViewportParams * viewport,const GeoDataCoordinates & disappearCoords,const GeoDataCoordinates & reappearCoords,QPolygonF * polygon) const520 void AzimuthalProjectionPrivate::horizonToPolygon( const ViewportParams *viewport,
521                                            const GeoDataCoordinates & disappearCoords,
522                                            const GeoDataCoordinates & reappearCoords,
523                                            QPolygonF * polygon ) const
524 {
525     qreal x, y;
526 
527     const qreal imageHalfWidth  = viewport->width() / 2;
528     const qreal imageHalfHeight = viewport->height() / 2;
529 
530     bool dummyGlobeHidesPoint = false;
531 
532     Q_Q( const AzimuthalProjection );
533     // Calculate the angle of the position vectors of both coordinates
534     q->screenCoordinates( disappearCoords, viewport, x, y, dummyGlobeHidesPoint );
535     qreal alpha = atan2( y - imageHalfHeight,
536                          x - imageHalfWidth );
537 
538     q->screenCoordinates( reappearCoords, viewport, x, y, dummyGlobeHidesPoint );
539     qreal beta =  atan2( y - imageHalfHeight,
540                          x - imageHalfWidth );
541 
542     // Calculate the difference between both
543     qreal diff = GeoDataCoordinates::normalizeLon( beta - alpha );
544 
545     qreal sgndiff = diff < 0 ? -1 : 1;
546 
547     const qreal arcradius = q->clippingRadius() * viewport->radius();
548     const int itEnd = fabs(diff * RAD2DEG);
549 
550     // Create a polygon that resembles an arc between the two position vectors
551     polygon->reserve(polygon->size() + itEnd);
552     for ( int it = 1; it <= itEnd; ++it ) {
553         const qreal angle = alpha + DEG2RAD * sgndiff * it;
554         const qreal itx = imageHalfWidth  +  arcradius * cos( angle );
555         const qreal ity = imageHalfHeight +  arcradius * sin( angle );
556         *polygon << QPointF( itx, ity );
557     }
558 }
559 
560 
findHorizon(const GeoDataCoordinates & previousCoords,const GeoDataCoordinates & currentCoords,const ViewportParams * viewport,TessellationFlags f) const561 GeoDataCoordinates AzimuthalProjectionPrivate::findHorizon( const GeoDataCoordinates & previousCoords,
562                                                     const GeoDataCoordinates & currentCoords,
563                                                     const ViewportParams *viewport,
564                                                     TessellationFlags f) const
565 {
566     bool currentHide = globeHidesPoint( currentCoords, viewport ) ;
567 
568     return doFindHorizon(previousCoords, currentCoords, viewport, f, currentHide, 0);
569 }
570 
571 
doFindHorizon(const GeoDataCoordinates & previousCoords,const GeoDataCoordinates & currentCoords,const ViewportParams * viewport,TessellationFlags f,bool currentHide,int recursionCounter) const572 GeoDataCoordinates AzimuthalProjectionPrivate::doFindHorizon( const GeoDataCoordinates & previousCoords,
573                                                     const GeoDataCoordinates & currentCoords,
574                                                     const ViewportParams *viewport,
575                                                     TessellationFlags f,
576                                                     bool currentHide,
577                                                     int recursionCounter ) const
578 {
579     if ( recursionCounter > 20 ) {
580         return currentHide ? previousCoords : currentCoords;
581     }
582     ++recursionCounter;
583 
584     bool followLatitudeCircle = false;
585 
586     // Calculate steps for tessellation: lonDiff and altDiff
587     qreal lonDiff = 0.0;
588     qreal previousLongitude = 0.0;
589     qreal previousLatitude = 0.0;
590 
591     if ( f.testFlag( RespectLatitudeCircle ) ) {
592         previousCoords.geoCoordinates( previousLongitude, previousLatitude );
593         qreal previousSign = previousLongitude > 0 ? 1 : -1;
594 
595         qreal currentLongitude = 0.0;
596         qreal currentLatitude = 0.0;
597         currentCoords.geoCoordinates( currentLongitude, currentLatitude );
598         qreal currentSign = currentLongitude > 0 ? 1 : -1;
599 
600         if ( previousLatitude == currentLatitude ) {
601             followLatitudeCircle = true;
602 
603             lonDiff = currentLongitude - previousLongitude;
604             if ( previousSign != currentSign
605                  && fabs(previousLongitude) + fabs(currentLongitude) > M_PI ) {
606                 if ( previousSign > currentSign ) {
607                     // going eastwards ->
608                     lonDiff += 2 * M_PI ;
609                 } else {
610                     // going westwards ->
611                     lonDiff -= 2 * M_PI;
612                 }
613             }
614 
615         }
616         else {
617 //            mDebug() << "Don't FollowLatitudeCircle";
618         }
619     }
620 
621     GeoDataCoordinates horizonCoords;
622 
623     if ( followLatitudeCircle ) {
624         // To tessellate along latitude circles use the
625         // linear interpolation of the longitude.
626         const qreal altDiff = currentCoords.altitude() - previousCoords.altitude();
627         const qreal altitude = previousCoords.altitude() + 0.5 * altDiff;
628         const qreal lon = lonDiff * 0.5 + previousLongitude;
629         const qreal lat = previousLatitude;
630 
631         horizonCoords = GeoDataCoordinates(lon, lat, altitude);
632     }
633     else {
634         // To tessellate along great circles use the
635         // normalized linear interpolation ("NLERP") for latitude and longitude.
636         horizonCoords = previousCoords.nlerp(currentCoords, 0.5);
637     }
638 
639     bool horizonHide = globeHidesPoint( horizonCoords, viewport );
640 
641     if ( horizonHide != currentHide ) {
642         return doFindHorizon(horizonCoords, currentCoords, viewport, f, currentHide, recursionCounter);
643     }
644 
645     return doFindHorizon(previousCoords, horizonCoords, viewport, f, horizonHide, recursionCounter);
646 }
647 
648 
globeHidesPoint(const GeoDataCoordinates & coordinates,const ViewportParams * viewport) const649 bool AzimuthalProjectionPrivate::globeHidesPoint( const GeoDataCoordinates &coordinates,
650                                           const ViewportParams *viewport ) const
651 {
652     bool globeHidesPoint;
653     qreal dummyX, dummyY;
654 
655     Q_Q( const AzimuthalProjection );
656     q->screenCoordinates(coordinates, viewport, dummyX, dummyY, globeHidesPoint);
657     return globeHidesPoint;
658 }
659 
660 
661 }
662 
663 
664