Marble

RoutingManager.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2010 Dennis Nienhüser <[email protected]>
4 //
5 
6 #include "RoutingManager.h"
7 
8 #include "AlternativeRoutesModel.h"
9 #include "MarbleModel.h"
10 #include "RouteRequest.h"
11 #include "RoutingModel.h"
12 #include "RoutingProfilesModel.h"
13 #include "RoutingRunnerPlugin.h"
14 #include "GeoWriter.h"
15 #include "GeoDataDocument.h"
16 #include "GeoDataExtendedData.h"
17 #include "GeoDataData.h"
18 #include "GeoDataFolder.h"
19 #include "GeoDataParser.h"
20 #include "GeoDataPlacemark.h"
21 #include "GeoDataTreeModel.h"
22 #include "MarbleColors.h"
23 #include "MarbleDirs.h"
24 #include "MarbleDebug.h"
25 #include "PositionTracking.h"
26 #include "PluginManager.h"
27 #include "PositionProviderPlugin.h"
28 #include "Route.h"
29 #include "RoutingRunnerManager.h"
30 #include <KmlElementDictionary.h>
31 
32 #include <QFile>
33 #include <QMessageBox>
34 #include <QCheckBox>
35 #include <QMutexLocker>
36 
37 namespace Marble
38 {
39 
40 class RoutingManagerPrivate
41 {
42 public:
43  RoutingManager* q;
44 
45  RouteRequest m_routeRequest;
46 
47  RoutingModel m_routingModel;
48 
49  RoutingProfilesModel m_profilesModel;
50 
51  RoutingManager::State m_state;
52 
53  const PluginManager *const m_pluginManager;
54 
55  GeoDataTreeModel *const m_treeModel;
56 
57  PositionTracking *const m_positionTracking;
58 
59  AlternativeRoutesModel m_alternativeRoutesModel;
60 
61  RoutingRunnerManager m_runnerManager;
62 
63  bool m_haveRoute;
64 
65  bool m_guidanceModeEnabled;
66 
67  QMutex m_fileMutex;
68 
69  bool m_shutdownPositionTracking;
70 
71  bool m_guidanceModeWarning;
72 
73  QString m_lastOpenPath;
74 
75  QString m_lastSavePath;
76 
77  QColor m_routeColorStandard;
78 
79  QColor m_routeColorHighlighted;
80 
81  QColor m_routeColorAlternative;
82 
83  RoutingManagerPrivate(MarbleModel *marbleModel, RoutingManager *manager);
84 
85  static GeoDataFolder *createFolderFromRequest(const RouteRequest &request);
86 
87  static QString stateFile( const QString &name = QString( "route.kml" ) );
88 
89  void saveRoute( const QString &filename );
90 
91  void loadRoute( const QString &filename );
92 
93  void addRoute( GeoDataDocument* route );
94 
95  void routingFinished();
96 
97  void setCurrentRoute(const GeoDataDocument *route);
98 
99  void recalculateRoute( bool deviated );
100 
101  static void importPlacemark( RouteSegment &outline, QVector<RouteSegment> &segments, const GeoDataPlacemark *placemark );
102 };
103 
104 RoutingManagerPrivate::RoutingManagerPrivate(MarbleModel *model, RoutingManager *manager) :
105  q( manager ),
106  m_routeRequest( manager ),
107  m_routingModel(&m_routeRequest, model->positionTracking(), manager),
108  m_profilesModel( model->pluginManager() ),
109  m_state( RoutingManager::Retrieved ),
110  m_pluginManager( model->pluginManager() ),
111  m_treeModel( model->treeModel() ),
112  m_positionTracking( model->positionTracking() ),
113  m_alternativeRoutesModel(manager),
114  m_runnerManager(model, manager),
115  m_haveRoute( false ),
116  m_guidanceModeEnabled( false ),
117  m_shutdownPositionTracking( false ),
118  m_guidanceModeWarning( true ),
119  m_routeColorStandard( Oxygen::skyBlue4 ),
120  m_routeColorHighlighted( Oxygen::skyBlue1 ),
121  m_routeColorAlternative( Oxygen::aluminumGray4 )
122 {
123  m_routeColorStandard.setAlpha( 200 );
124  m_routeColorHighlighted.setAlpha( 200 );
125  m_routeColorAlternative.setAlpha( 200 );
126 }
127 
128 GeoDataFolder *RoutingManagerPrivate::createFolderFromRequest(const RouteRequest &request)
129 {
130  GeoDataFolder* result = new GeoDataFolder;
131 
132  result->setName(QStringLiteral("Route Request"));
133 
134  for (int i = 0; i < request.size(); ++i) {
135  GeoDataPlacemark *placemark = new GeoDataPlacemark(request[i]);
136  result->append( placemark );
137  }
138 
139  return result;
140 }
141 
142 QString RoutingManagerPrivate::stateFile( const QString &name)
143 {
144  QString const subdir = "routing";
145  QDir dir( MarbleDirs::localPath() );
146  if ( !dir.exists( subdir ) ) {
147  if ( !dir.mkdir( subdir ) ) {
148  mDebug() << "Unable to create dir " << dir.absoluteFilePath( subdir );
149  return dir.absolutePath();
150  }
151  }
152 
153  if ( !dir.cd( subdir ) ) {
154  mDebug() << "Cannot change into " << dir.absoluteFilePath( subdir );
155  }
156 
157  return dir.absoluteFilePath( name );
158 }
159 
160 void RoutingManagerPrivate::saveRoute(const QString &filename)
161 {
162  GeoWriter writer;
163  writer.setDocumentType( kml::kmlTag_nameSpaceOgc22 );
164 
165  QMutexLocker locker( &m_fileMutex );
166  QFile file( filename );
167  if ( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
168  {
169  mDebug() << "Cannot write to " << file.fileName();
170  return;
171  }
172 
173  GeoDataDocument container;
174  container.setName(QStringLiteral("Route"));
175  GeoDataFolder *request = createFolderFromRequest(m_routeRequest);
176  if ( request ) {
177  container.append( request );
178  }
179 
180  const GeoDataDocument *route = m_alternativeRoutesModel.currentRoute();
181  if ( route ) {
182  container.append( new GeoDataDocument( *route ) );
183  }
184 
185  if ( !writer.write( &file, &container ) ) {
186  mDebug() << "Can not write route state to " << file.fileName();
187  }
188  file.close();
189 }
190 
191 void RoutingManagerPrivate::loadRoute(const QString &filename)
192 {
193  QFile file( filename );
194  if ( !file.open( QIODevice::ReadOnly ) ) {
195  mDebug() << "Can not read route from " << file.fileName();
196  return;
197  }
198 
199  GeoDataParser parser( GeoData_KML );
200  if ( !parser.read( &file ) ) {
201  mDebug() << "Could not parse file: " << parser.errorString();
202  return;
203  }
204 
205  GeoDocument *doc = parser.releaseDocument();
206  file.close();
207  bool loaded = false;
208 
209  GeoDataDocument* container = dynamic_cast<GeoDataDocument*>( doc );
210  if (container && !container->isEmpty()) {
211  GeoDataFolder* viaPoints = dynamic_cast<GeoDataFolder*>( &container->first() );
212  if ( viaPoints ) {
213  loaded = true;
214  QVector<GeoDataPlacemark*> placemarks = viaPoints->placemarkList();
215  for( int i=0; i<placemarks.size(); ++i ) {
216  if ( i < m_routeRequest.size() ) {
217  m_routeRequest[i] = *placemarks[i];
218  } else {
219  m_routeRequest.append( *placemarks[i] );
220  }
221  }
222 
223  // clear unneeded via points
224  const int viaPoints_needed = placemarks.size();
225  for ( int i = m_routeRequest.size(); i > viaPoints_needed; --i ) {
226  m_routeRequest.remove( viaPoints_needed );
227  }
228  } else {
229  mDebug() << "Expected a GeoDataDocument with at least one child, didn't get one though";
230  }
231  }
232 
233  if ( container && container->size() == 2 ) {
234  GeoDataDocument* route = dynamic_cast<GeoDataDocument*>(&container->last());
235  if ( route ) {
236  loaded = true;
237  m_alternativeRoutesModel.clear();
238  m_alternativeRoutesModel.addRoute( new GeoDataDocument(*route), AlternativeRoutesModel::Instant );
239  m_alternativeRoutesModel.setCurrentRoute( 0 );
240  m_state = RoutingManager::Retrieved;
241  emit q->stateChanged( m_state );
242  emit q->routeRetrieved( route );
243  } else {
244  mDebug() << "Expected a GeoDataDocument child, didn't get one though";
245  }
246  }
247 
248  if (loaded) {
249  delete doc; // == container
250  } else {
251  mDebug() << "File " << filename << " is not a valid Marble route .kml file";
252  if ( container ) {
253  m_treeModel->addDocument( container );
254  }
255  }
256 }
257 
259  QObject(parent),
260  d(new RoutingManagerPrivate(marbleModel, this))
261 {
262  connect( &d->m_runnerManager, SIGNAL(routeRetrieved(GeoDataDocument*)),
263  this, SLOT(addRoute(GeoDataDocument*)) );
264  connect( &d->m_runnerManager, SIGNAL(routingFinished()),
265  this, SLOT(routingFinished()) );
266  connect(&d->m_alternativeRoutesModel, SIGNAL(currentRouteChanged(const GeoDataDocument*)),
267  this, SLOT(setCurrentRoute(const GeoDataDocument*)));
268  connect( &d->m_routingModel, SIGNAL(deviatedFromRoute(bool)),
269  this, SLOT(recalculateRoute(bool)) );
270 }
271 
273 {
274  delete d;
275 }
276 
277 RoutingProfilesModel *RoutingManager::profilesModel()
278 {
279  return &d->m_profilesModel;
280 }
281 
283 {
284  return &d->m_routingModel;
285 }
286 
287 const RoutingModel *RoutingManager::routingModel() const
288 {
289  return &d->m_routingModel;
290 }
291 
293 {
294  return &d->m_routeRequest;
295 }
296 
297 RoutingManager::State RoutingManager::state() const
298 {
299  return d->m_state;
300 }
301 
303 {
304  d->m_haveRoute = false;
305 
306  int realSize = 0;
307  for ( int i = 0; i < d->m_routeRequest.size(); ++i ) {
308  // Sort out dummy targets
309  if ( d->m_routeRequest.at( i ).isValid() ) {
310  ++realSize;
311  }
312  }
313 
314  d->m_alternativeRoutesModel.newRequest( &d->m_routeRequest );
315  if ( realSize > 1 ) {
316  d->m_state = RoutingManager::Downloading;
317  d->m_runnerManager.retrieveRoute( &d->m_routeRequest );
318  } else {
319  d->m_routingModel.clear();
320  d->m_state = RoutingManager::Retrieved;
321  }
322  emit stateChanged( d->m_state );
323 }
324 
325 void RoutingManagerPrivate::addRoute( GeoDataDocument* route )
326 {
327  if ( route ) {
328  m_alternativeRoutesModel.addRoute( route );
329  }
330 
331  if ( !m_haveRoute ) {
332  m_haveRoute = route != nullptr;
333  }
334 
335  emit q->routeRetrieved( route );
336 }
337 
338 void RoutingManagerPrivate::routingFinished()
339 {
340  m_state = RoutingManager::Retrieved;
341  emit q->stateChanged( m_state );
342 }
343 
344 void RoutingManagerPrivate::setCurrentRoute(const GeoDataDocument *document)
345 {
346  QVector<RouteSegment> segments;
347  RouteSegment outline;
348 
349  if (document != nullptr) {
350  const auto folders = document->folderList();
351  for (const auto folder : folders) {
352  for (const auto placemark : folder->placemarkList()) {
353  importPlacemark(outline, segments, placemark);
354  }
355  }
356 
357  for (const auto placemark : document->placemarkList()) {
358  importPlacemark(outline, segments, placemark);
359  }
360  }
361 
362  if ( segments.isEmpty() ) {
363  segments << outline;
364  }
365 
366  // Map via points onto segments
367  if ( m_routeRequest.size() > 1 && segments.size() > 1 ) {
368  int index = 0;
369  for ( int j = 0; j < m_routeRequest.size(); ++j ) {
370  QPair<int, qreal> minimum( -1, -1.0 );
371  int viaIndex = -1;
372  for ( int i = index; i < segments.size(); ++i ) {
373  const RouteSegment &segment = segments[i];
374  GeoDataCoordinates closest;
375  const qreal distance = segment.distanceTo( m_routeRequest.at( j ), closest, closest );
376  if ( minimum.first < 0 || distance < minimum.second ) {
377  minimum.first = i;
378  minimum.second = distance;
379  viaIndex = j;
380  }
381  }
382 
383  if ( minimum.first >= 0 ) {
384  index = minimum.first;
385  Maneuver viaPoint = segments[ minimum.first ].maneuver();
386  viaPoint.setWaypoint( m_routeRequest.at( viaIndex ), viaIndex );
387  segments[ minimum.first ].setManeuver( viaPoint );
388  }
389  }
390  }
391 
392  Route route;
393 
394  if ( segments.size() > 0 ) {
395  for( const RouteSegment &segment: segments ) {
396  route.addRouteSegment( segment );
397  }
398  }
399 
400  m_routingModel.setRoute( route );
401 }
402 
403 void RoutingManagerPrivate::importPlacemark( RouteSegment &outline, QVector<RouteSegment> &segments, const GeoDataPlacemark *placemark )
404 {
405  const GeoDataGeometry* geometry = placemark->geometry();
406  const GeoDataLineString* lineString = dynamic_cast<const GeoDataLineString*>( geometry );
407  QStringList blacklist = QStringList() << "" << "Route" << "Tessellated";
408  RouteSegment segment;
409  bool isOutline = true;
410  if ( !blacklist.contains( placemark->name() ) ) {
411  if( lineString ) {
412  Maneuver maneuver;
413  maneuver.setInstructionText( placemark->name() );
414  maneuver.setPosition( lineString->at( 0 ) );
415 
416  if (placemark->extendedData().contains(QStringLiteral("turnType"))) {
417  QVariant turnType = placemark->extendedData().value(QStringLiteral("turnType")).value();
418  // The enum value is converted to/from an int in the QVariant
419  // because only a limited set of data types can be serialized with QVariant's
420  // toString() method (which is used to serialize <ExtendedData>/<Data> values)
421  maneuver.setDirection( Maneuver::Direction( turnType.toInt() ) );
422  }
423 
424  if (placemark->extendedData().contains(QStringLiteral("roadName"))) {
425  QVariant roadName = placemark->extendedData().value(QStringLiteral("roadName")).value();
426  maneuver.setRoadName( roadName.toString() );
427  }
428 
429  segment.setManeuver( maneuver );
430  isOutline = false;
431  }
432  }
433 
434  if ( lineString ) {
435  segment.setPath( *lineString );
436 
437  if ( isOutline ) {
438  outline = segment;
439  } else {
440  segments.push_back( segment );
441  }
442  }
443 }
444 
445 AlternativeRoutesModel* RoutingManager::alternativeRoutesModel()
446 {
447  return &d->m_alternativeRoutesModel;
448 }
449 
451 {
452  d->saveRoute( d->stateFile() );
453 }
454 
455 void RoutingManager::saveRoute( const QString &filename ) const
456 {
457  d->saveRoute( filename );
458 }
459 
460 void RoutingManager::loadRoute( const QString &filename )
461 {
462  d->loadRoute( filename );
463 }
464 
465 RoutingProfile RoutingManager::defaultProfile( RoutingProfile::TransportType transportType ) const
466 {
467  RoutingProfile profile;
468  RoutingProfilesModel::ProfileTemplate tpl = RoutingProfilesModel::CarFastestTemplate;
469  switch ( transportType ) {
470  case RoutingProfile::Motorcar:
471  tpl = RoutingProfilesModel::CarFastestTemplate;
472  profile.setName(QStringLiteral("Motorcar"));
473  profile.setTransportType( RoutingProfile::Motorcar );
474  break;
475  case RoutingProfile::Bicycle:
476  tpl = RoutingProfilesModel::BicycleTemplate;
477  profile.setName(QStringLiteral("Bicycle"));
478  profile.setTransportType( RoutingProfile::Bicycle );
479  break;
480  case RoutingProfile::Pedestrian:
481  tpl = RoutingProfilesModel::PedestrianTemplate;
482  profile.setName(QStringLiteral("Pedestrian"));
483  profile.setTransportType( RoutingProfile::Pedestrian );
484  break;
485  }
486 
487  for( RoutingRunnerPlugin* plugin: d->m_pluginManager->routingRunnerPlugins() ) {
488  if ( plugin->supportsTemplate( tpl ) ) {
489  profile.pluginSettings()[plugin->nameId()] = plugin->templateSettings( tpl );
490  }
491  }
492 
493  return profile;
494 }
495 
497 {
498  d->loadRoute( d->stateFile() );
499 }
500 
502 {
503  if ( d->m_guidanceModeEnabled == enabled ) {
504  return;
505  }
506 
507  d->m_guidanceModeEnabled = enabled;
508 
509  if ( enabled ) {
510  d->saveRoute( d->stateFile( "guidance.kml" ) );
511 
512  if ( d->m_guidanceModeWarning ) {
513  QString text = QLatin1String("<p>") + tr("Caution: Driving instructions may be incomplete or wrong.") +
514  QLatin1Char(' ') + tr("Road construction, weather and other unforeseen variables can result in the suggested route not to be the most expedient or safest route to your destination.") +
515  QLatin1Char(' ') + tr("Please use common sense while navigating.") + QLatin1String("</p>") +
516  QLatin1String("<p>") + tr("The Marble development team wishes you a pleasant and safe journey.") + QLatin1String("</p>");
517  QPointer<QMessageBox> messageBox = new QMessageBox(QMessageBox::Information, tr("Guidance Mode"), text, QMessageBox::Ok);
518  QCheckBox *showAgain = new QCheckBox( tr( "Show again" ) );
519  showAgain->setChecked( true );
520  showAgain->blockSignals( true ); // otherwise it'd close the dialog
521  messageBox->addButton( showAgain, QMessageBox::ActionRole );
522  const bool smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
523  messageBox->resize( 380, smallScreen ? 400 : 240 );
524  messageBox->exec();
525  if ( !messageBox.isNull() ) {
526  d->m_guidanceModeWarning = showAgain->isChecked();
527  }
528  delete messageBox;
529  }
530  } else {
531  d->loadRoute( d->stateFile( "guidance.kml" ) );
532  }
533 
534  PositionProviderPlugin* positionProvider = d->m_positionTracking->positionProviderPlugin();
535  if ( !positionProvider && enabled ) {
536  QList<const PositionProviderPlugin*> plugins = d->m_pluginManager->positionProviderPlugins();
537  if ( plugins.size() > 0 ) {
538  positionProvider = plugins.first()->newInstance();
539  }
540  d->m_positionTracking->setPositionProviderPlugin( positionProvider );
541  d->m_shutdownPositionTracking = true;
542  } else if ( positionProvider && !enabled && d->m_shutdownPositionTracking ) {
543  d->m_shutdownPositionTracking = false;
544  d->m_positionTracking->setPositionProviderPlugin( nullptr );
545  }
546 
547  emit guidanceModeEnabledChanged( d->m_guidanceModeEnabled );
548 }
549 
550 void RoutingManagerPrivate::recalculateRoute( bool deviated )
551 {
552  if ( m_guidanceModeEnabled && deviated ) {
553  for ( int i=m_routeRequest.size()-3; i>=0; --i ) {
554  if ( m_routeRequest.visited( i ) ) {
555  m_routeRequest.remove( i );
556  }
557  }
558 
559  if ( m_routeRequest.size() == 2 && m_routeRequest.visited( 0 ) && !m_routeRequest.visited( 1 ) ) {
560  m_routeRequest.setPosition( 0, m_positionTracking->currentLocation(), QObject::tr( "Current Location" ) );
561  q->retrieveRoute();
562  } else if ( m_routeRequest.size() != 0 && !m_routeRequest.visited( m_routeRequest.size()-1 ) ) {
563  m_routeRequest.insert( 0, m_positionTracking->currentLocation(), QObject::tr( "Current Location" ) );
564  q->retrieveRoute();
565  }
566  }
567 }
568 
570 {
571  d->m_routeRequest.reverse();
572  retrieveRoute();
573 }
574 
576 {
577  d->m_routeRequest.clear();
578  retrieveRoute();
579 }
580 
582 {
583  d->m_guidanceModeWarning = show;
584 }
585 
587 {
588  return d->m_guidanceModeWarning;
589 }
590 
592 {
593  d->m_lastOpenPath = path;
594 }
595 
597 {
598  return d->m_lastOpenPath;
599 }
600 
602 {
603  d->m_lastSavePath = path;
604 }
605 
607 {
608  return d->m_lastSavePath;
609 }
610 
612 {
613  d->m_routeColorStandard = color;
614 }
615 
617 {
618  return d->m_routeColorStandard;
619 }
620 
622 {
623  d->m_routeColorHighlighted = color;
624 }
625 
627 {
628  return d->m_routeColorHighlighted;
629 }
630 
632 {
633  d->m_routeColorAlternative = color;
634 }
635 
637 {
638  return d->m_routeColorAlternative;
639 }
640 
641 bool RoutingManager::guidanceModeEnabled() const
642 {
643  return d->m_guidanceModeEnabled;
644 }
645 
646 } // namespace Marble
647 
648 #include "moc_RoutingManager.cpp"
T & first()
void retrieveRoute()
Retrieve a route suiting the routeRequest.
bool isEmpty() const const
QString lastSavePath() const
Return last directory the user saved a route to.
void setRouteColorStandard(const QColor &color)
Set color for standard route rendering.
QColor routeColorHighlighted() const
Get color for highlighted route rendering.
QColor routeColorStandard() const
Get color for standard route rendering.
T value() const const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
The abstract class that provides position information.
void append(const T &value)
void setShowGuidanceModeStartupWarning(bool show)
Set whether a warning message should be shown to the user before starting guidance mode.
void stateChanged(RoutingManager::State newState)
Directions and waypoints for the given route are being downloaded or have been retrieved – newState t...
void push_back(const T &value)
QColor routeColorAlternative() const
Get color for alternative route rendering.
bool showGuidanceModeStartupWarning() const
Returns true (default) if a warning is shown to the user when starting guidance mode.
void setGuidanceModeEnabled(bool enabled)
Toggle turn by turn navigation mode.
void setChecked(bool)
void setRouteColorHighlighted(const QColor &color)
Set color for highlighted route rendering.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
RoutingProfilesModel * profilesModel()
Provides access to the model which contains all possible routing profiles.
QString lastOpenPath() const
Return last directory the user opened a route from.
void setLastSavePath(const QString &path)
Set last directory the user saved a route to.
void setRouteColorAlternative(const QColor &color)
Set color for alternative route rendering.
int size() const const
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
AlternativeRoutesModel * alternativeRoutesModel()
Provides access to the model which contains a list of alternative routes.
RoutingModel * routingModel()
Provides access to the routing model which contains a list of routing instructions describing steps t...
bool blockSignals(bool block)
void saveRoute(const QString &filename) const
Saves the current route to the file with the given filename.
~RoutingManager() override
Destructor.
void reverseRoute()
Reverse the previously requested route, i.e.
A container for Features, Styles and in the future Schemas.
void setAlpha(int alpha)
int toInt(bool *ok) const const
void setLastOpenPath(const QString &path)
Set last directory the user opened a route from.
Binds a QML item to a specific geodetic location in screen coordinates.
void loadRoute(const QString &filename)
Opens the given filename (kml format) and loads the route contained in it.
RouteRequest * routeRequest()
Returns the current route request.
A plugin for Marble to execute a routing task.
void writeSettings() const
Saves the current route request and the current route to disk.
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
void readSettings()
Restores a previously saved route request and route from disk, if any.
RoutingManager(MarbleModel *marbleModel, QObject *parent=nullptr)
Constructor.
int size() const const
Points to be included in a route.
Definition: RouteRequest.h:26
QString tr(const char *sourceText, const char *disambiguation, int n)
RoutingProfile defaultProfile(RoutingProfile::TransportType transportType) const
Generates a routing profile with default settings for the given transport type.
The data model (not based on QAbstractModel) for a MarbleWidget.
Definition: MarbleModel.h:86
void clearRoute()
Clear all via points.
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
QString toString() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Feb 7 2023 03:59:09 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.