1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2010 Siddharth Srivastava <akssps011@gmail.com>
4 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
5 //
6 
7 
8 #include "AutoNavigation.h"
9 
10 #include "GeoDataCoordinates.h"
11 #include "PositionTracking.h"
12 #include "MarbleDebug.h"
13 #include "MarbleModel.h"
14 #include "ViewportParams.h"
15 #include "RoutingManager.h"
16 #include "RoutingModel.h"
17 #include "Route.h"
18 
19 #include <QWidget>
20 #include <QRect>
21 #include <QPointF>
22 #include <QTimer>
23 #include <cmath>
24 
25 namespace Marble {
26 
27 class Q_DECL_HIDDEN AutoNavigation::Private
28 {
29 public:
30 
31     AutoNavigation *const m_parent;
32     const MarbleModel *const m_model;
33     const ViewportParams *const m_viewport;
34     const PositionTracking *const m_tracking;
35     AutoNavigation::CenterMode m_recenterMode;
36     bool m_adjustZoom;
37     QTimer m_lastWidgetInteraction;
38     bool m_selfInteraction;
39 
40     /** Constructor */
41     Private( MarbleModel *model, const ViewportParams *viewport, AutoNavigation *parent );
42 
43     /**
44      * @brief To center on when reaching custom defined border
45      * @param position current gps location
46      * @param speed optional speed argument
47      */
48      void moveOnBorderToCenter( const GeoDataCoordinates &position, qreal speed );
49 
50     /**
51      * For calculating intersection point of projected LineString from
52      * current gps location with the map border
53      * @param position current gps location
54      */
55      GeoDataCoordinates findIntersection( qreal currentX, qreal currentY ) const;
56 
57     /**
58      * @brief Adjust the zoom value of the map
59      * @param currentPosition current location of the gps device
60      */
61      void adjustZoom( const GeoDataCoordinates &currentPosition, qreal speed );
62 
63      /**
64        * Center the widget on the given position unless recentering is currently inhibited
65        */
66      void centerOn( const GeoDataCoordinates &position );
67 };
68 
Private(MarbleModel * model,const ViewportParams * viewport,AutoNavigation * parent)69 AutoNavigation::Private::Private( MarbleModel *model, const ViewportParams *viewport, AutoNavigation *parent ) :
70         m_parent( parent ),
71         m_model( model ),
72         m_viewport( viewport ),
73         m_tracking( model->positionTracking() ),
74         m_recenterMode( AutoNavigation::DontRecenter ),
75         m_adjustZoom( 0 ),
76         m_selfInteraction( false )
77 {
78     m_lastWidgetInteraction.setInterval( 10 * 1000 );
79     m_lastWidgetInteraction.setSingleShot( true );
80 }
81 
moveOnBorderToCenter(const GeoDataCoordinates & position,qreal)82 void AutoNavigation::Private::moveOnBorderToCenter( const GeoDataCoordinates &position, qreal )
83 {
84     qreal x = 0.0;
85     qreal y = 0.0;
86     //recenter if initially the gps location is not visible on the screen
87     if(!( m_viewport->screenCoordinates( position, x, y ) ) ) {
88          centerOn( position );
89     }
90     qreal centerLon = m_viewport->centerLongitude();
91     qreal centerLat = m_viewport->centerLatitude();
92 
93     qreal centerX = 0.0;
94     qreal centerY = 0.0;
95 
96     m_viewport->screenCoordinates( centerLon, centerLat, centerX, centerY );
97 
98     const qreal borderRatio = 0.25;
99     //defining the default border distance from map center
100     int shiftX = qRound( centerX * borderRatio );
101     int shiftY = qRound( centerY * borderRatio );
102 
103     QRect recenterBorderBound;
104     recenterBorderBound.setCoords( centerX-shiftX, centerY-shiftY, centerX+shiftX,  centerY+shiftY );
105 
106     if( !recenterBorderBound.contains( x,y ) ) {
107         centerOn( position );
108     }
109 }
110 
findIntersection(qreal currentX,qreal currentY) const111 GeoDataCoordinates AutoNavigation::Private::findIntersection( qreal currentX, qreal currentY ) const
112 {
113     qreal direction = m_tracking->direction();
114     if ( direction >= 360 ) {
115         direction = fmod( direction,360.0 );
116     }
117 
118     const qreal width = m_viewport->width();
119     const qreal height = m_viewport->height();
120 
121     QPointF intercept;
122     QPointF destinationHorizontal;
123     QPointF destinationVertical;
124     QPointF destination;
125 
126     bool crossHorizontal =  false;
127     bool crossVertical = false;
128 
129     //calculation of intersection point
130     if( 0 < direction && direction < 90 ) {
131         const qreal angle = direction * DEG2RAD;
132 
133         //Intersection with line x = width
134         intercept.setX( width - currentX );
135         intercept.setY( intercept.x() / tan( angle ) );
136         destinationVertical.setX( width );
137         destinationVertical.setY( currentY-intercept.y() );
138 
139         //Intersection with line y = 0
140         intercept.setY( currentY );
141         intercept.setX( intercept.y() * tan( angle ) );
142         destinationHorizontal.setX( currentX + intercept.x() );
143         destinationHorizontal.setY( 0 );
144 
145         if ( destinationVertical.y() < 0 ) {
146             crossHorizontal = true;
147         }
148         else if( destinationHorizontal.x() > width ) {
149             crossVertical = true;
150         }
151 
152     }
153     else if( 270 < direction && direction < 360 ) {
154         const qreal angle = (direction - 270) * DEG2RAD;
155 
156         //Intersection with line y = 0
157         intercept.setY( currentY );
158         intercept.setX( intercept.y() / tan( angle ) );
159         destinationHorizontal.setX( currentX - intercept.x() );
160         destinationHorizontal.setY( 0 );
161 
162         //Intersection with line x = 0
163         intercept.setX( currentX );
164         intercept.setY( intercept.x() * tan( angle ) );
165         destinationVertical.setY( currentY - intercept.y() );
166         destinationVertical.setX( 0 );
167 
168         if( destinationHorizontal.x() > width ) {
169             crossVertical = true;
170         }
171         else if( destinationVertical.y() < 0 ) {
172             crossHorizontal = true;
173         }
174 
175     }
176     else if( 180 < direction && direction < 270  ) {
177         const qreal angle = (direction - 180) * DEG2RAD;
178 
179         //Intersection with line x = 0
180         intercept.setX( currentX );
181         intercept.setY( intercept.x() / tan( angle ) );
182         destinationVertical.setY( currentY + intercept.y() );
183         destinationVertical.setX( 0 );
184 
185         //Intersection with line y = height
186         intercept.setY( currentY );
187         intercept.setX( intercept.y() * tan( angle ) );
188         destinationHorizontal.setX( currentX - intercept.x() );
189         destinationHorizontal.setY( height );
190 
191         if ( destinationVertical.y() > height ) {
192             crossHorizontal = true;
193         }
194         else if ( destinationHorizontal.x() < 0 ) {
195             crossVertical = true;
196         }
197 
198     }
199     else if( 90 < direction && direction < 180  ) {
200         const qreal angle = (direction - 90) * DEG2RAD;
201 
202         //Intersection with line y = height
203         intercept.setY( height - currentY );
204         intercept.setX( intercept.y() / tan( angle ) );
205         destinationHorizontal.setX( currentX + intercept.x() );
206         destinationHorizontal.setY( height );
207 
208         //Intersection with line x = width
209         intercept.setX( width - currentX );
210         intercept.setY( intercept.x() * tan( angle ) );
211         destinationVertical.setX( width );
212         destinationVertical.setY( currentY + intercept.y() );
213 
214         if ( destinationHorizontal.x() > width ) {
215             crossVertical = true;
216         }
217         else if( destinationVertical.y() > height ) {
218             crossHorizontal = true;
219         }
220 
221     }
222     else if( direction == 0 ) {
223         destinationHorizontal.setX( currentX );
224         destinationHorizontal.setY( 0 );
225         crossHorizontal = true;
226     }
227     else if( direction == 90 ) {
228         destinationVertical.setX( width );
229         destinationVertical.setY( currentY );
230         crossVertical = true;
231     }
232     else if( direction == 180 ) {
233         destinationHorizontal.setX( currentX );
234         destinationHorizontal.setY( height );
235         crossHorizontal = true;
236     }
237     else if( direction == 270 ) {
238         destinationVertical.setX( 0 );
239         destinationVertical.setY( currentY );
240         crossVertical = true;
241     }
242 
243     if ( crossHorizontal == true && crossVertical == false ) {
244         destination.setX( destinationHorizontal.x() );
245         destination.setY( destinationHorizontal.y() );
246     }
247     else if ( crossVertical == true && crossHorizontal == false ) {
248         destination.setX( destinationVertical.x() );
249         destination.setY( destinationVertical.y() );
250     }
251 
252     qreal destinationLon = 0.0;
253     qreal destinationLat = 0.0;
254     m_viewport->geoCoordinates( destination.x(), destination.y(), destinationLon, destinationLat,
255                               GeoDataCoordinates::Radian );
256     GeoDataCoordinates destinationCoord( destinationLon, destinationLat, GeoDataCoordinates::Radian );
257 
258     return destinationCoord;
259 }
260 
adjustZoom(const GeoDataCoordinates & currentPosition,qreal speed)261 void AutoNavigation::Private::adjustZoom( const GeoDataCoordinates &currentPosition, qreal speed )
262 {
263     qreal currentX = 0;
264     qreal currentY = 0;
265     if( !m_viewport->screenCoordinates(currentPosition, currentX, currentY ) ) {
266         return;
267     }
268 
269     const GeoDataCoordinates destination = findIntersection( currentX, currentY );
270 
271     const qreal greatCircleDistance = currentPosition.sphericalDistanceTo(destination);
272     qreal radius = m_model->planetRadius();
273     qreal distance = greatCircleDistance *  radius;
274 
275     if( speed != 0 ) {
276         // time (in seconds) remaining to reach the border of the map
277         qreal  remainingTime = distance / speed;
278 
279         // tolerance time limits (in seconds) before auto zooming
280         qreal thresholdLow = 15;
281         qreal thresholdHigh = 120;
282 
283         m_selfInteraction = true;
284         if ( remainingTime < thresholdLow ) {
285             emit m_parent->zoomOut( Instant );
286         }
287         else if ( remainingTime > thresholdHigh ) {
288             emit m_parent->zoomIn( Instant );
289         }
290         m_selfInteraction = false;
291     }
292 }
293 
centerOn(const GeoDataCoordinates & position)294 void AutoNavigation::Private::centerOn( const GeoDataCoordinates &position )
295 {
296     m_selfInteraction = true;
297     RoutingManager const * routingManager = m_model->routingManager();
298     RoutingModel const * routingModel = routingManager->routingModel();
299     if (!routingManager->guidanceModeEnabled() || routingModel->deviatedFromRoute()){
300         emit m_parent->centerOn( position, false );
301     } else {
302         GeoDataCoordinates positionOnRoute = routingModel->route().positionOnRoute();
303         emit m_parent->centerOn( positionOnRoute, false );
304     }
305     m_selfInteraction = false;
306 }
307 
AutoNavigation(MarbleModel * model,const ViewportParams * viewport,QObject * parent)308 AutoNavigation::AutoNavigation( MarbleModel *model, const ViewportParams *viewport, QObject *parent ) :
309     QObject( parent ),
310     d( new AutoNavigation::Private( model, viewport, this ) )
311 {
312     connect( d->m_tracking, SIGNAL(gpsLocation(GeoDataCoordinates,qreal)),
313                 this, SLOT(adjust(GeoDataCoordinates,qreal)) );
314 }
315 
~AutoNavigation()316 AutoNavigation::~AutoNavigation()
317 {
318     delete d;
319 }
320 
adjust(const GeoDataCoordinates & position,qreal speed)321 void AutoNavigation::adjust( const GeoDataCoordinates &position, qreal speed )
322 {
323     if ( d->m_lastWidgetInteraction.isActive() ) {
324         return;
325     }
326 
327     switch( d->m_recenterMode ) {
328     case DontRecenter:
329         /* nothing to do */
330         break;
331     case AlwaysRecenter:
332         d->centerOn( position );
333         break;
334     case RecenterOnBorder:
335         d->moveOnBorderToCenter( position, speed );
336         break;
337     }
338 
339     if ( d->m_adjustZoom ) {
340         switch( d->m_recenterMode ) {
341         case DontRecenter:
342             /* nothing to do */
343             break;
344         case AlwaysRecenter:
345         case RecenterOnBorder: // fallthrough
346             d->adjustZoom( position, speed );
347             break;
348         }
349     }
350 }
351 
setAutoZoom(bool autoZoom)352 void AutoNavigation::setAutoZoom( bool autoZoom )
353 {
354     d->m_adjustZoom = autoZoom;
355     emit autoZoomToggled( autoZoom );
356 }
357 
setRecenter(CenterMode recenterMode)358 void AutoNavigation::setRecenter( CenterMode recenterMode )
359 {
360     d->m_recenterMode = recenterMode;
361     emit recenterModeChanged( recenterMode );
362 }
363 
inhibitAutoAdjustments()364 void AutoNavigation::inhibitAutoAdjustments()
365 {
366     if ( !d->m_selfInteraction ) {
367         d->m_lastWidgetInteraction.start();
368     }
369 }
370 
recenterMode() const371 AutoNavigation::CenterMode AutoNavigation::recenterMode() const
372 {
373     return d->m_recenterMode;
374 }
375 
autoZoom() const376 bool AutoNavigation::autoZoom() const
377 {
378     return d->m_adjustZoom;
379 }
380 
381 } // namespace Marble
382 
383 #include "moc_AutoNavigation.cpp"
384