1 /***************************************************************************
2                          qgsalgorithmgpsbabeltools.cpp
3                          ------------------
4     begin                : July 2021
5     copyright            : (C) 2021 by Nyall Dawson
6     email                : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include <QtGlobal>
19 #if QT_CONFIG(process)
20 
21 
22 #include "qgsalgorithmgpsbabeltools.h"
23 #include "qgsvectorlayer.h"
24 #include "qgsrunprocess.h"
25 #include "qgsproviderutils.h"
26 #include "qgssettings.h"
27 #include "qgssettingsregistrycore.h"
28 #include "qgsbabelformatregistry.h"
29 #include "qgsbabelformat.h"
30 #include "qgsgpsdetector.h"
31 #include "qgsbabelgpsdevice.h"
32 
33 ///@cond PRIVATE
34 
name() const35 QString QgsConvertGpxFeatureTypeAlgorithm::name() const
36 {
37   return QStringLiteral( "convertgpxfeaturetype" );
38 }
39 
displayName() const40 QString QgsConvertGpxFeatureTypeAlgorithm::displayName() const
41 {
42   return QObject::tr( "Convert GPX feature type" );
43 }
44 
tags() const45 QStringList QgsConvertGpxFeatureTypeAlgorithm::tags() const
46 {
47   return QObject::tr( "gps,tools,babel,tracks,waypoints,routes" ).split( ',' );
48 }
49 
group() const50 QString QgsConvertGpxFeatureTypeAlgorithm::group() const
51 {
52   return QObject::tr( "GPS" );
53 }
54 
groupId() const55 QString QgsConvertGpxFeatureTypeAlgorithm::groupId() const
56 {
57   return QStringLiteral( "gps" );
58 }
59 
initAlgorithm(const QVariantMap &)60 void QgsConvertGpxFeatureTypeAlgorithm::initAlgorithm( const QVariantMap & )
61 {
62   addParameter( new QgsProcessingParameterFile( QStringLiteral( "INPUT" ), QObject::tr( "Input file" ), QgsProcessingParameterFile::File, QString(), QVariant(), false,
63                 QObject::tr( "GPX files" ) + QStringLiteral( " (*.gpx *.GPX)" ) ) );
64 
65   addParameter( new QgsProcessingParameterEnum( QStringLiteral( "CONVERSION" ), QObject::tr( "Conversion" ),
66   {
67     QObject::tr( "Waypoints from a Route" ),
68     QObject::tr( "Waypoints from a Track" ),
69     QObject::tr( "Route from Waypoints" ),
70     QObject::tr( "Track from Waypoints" )
71   }, false, 0 ) );
72 
73   addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Output" ), QObject::tr( "GPX files" ) + QStringLiteral( " (*.gpx *.GPX)" ) ) );
74 
75   addOutput( new QgsProcessingOutputVectorLayer( QStringLiteral( "OUTPUT_LAYER" ), QObject::tr( "Output layer" ) ) );
76 }
77 
icon() const78 QIcon QgsConvertGpxFeatureTypeAlgorithm::icon() const
79 {
80   return QgsApplication::getThemeIcon( QStringLiteral( "/mIconGps.svg" ) );
81 }
82 
svgIconPath() const83 QString QgsConvertGpxFeatureTypeAlgorithm::svgIconPath() const
84 {
85   return QgsApplication::iconPath( QStringLiteral( "/mIconGps.svg" ) );
86 }
87 
shortHelpString() const88 QString QgsConvertGpxFeatureTypeAlgorithm::shortHelpString() const
89 {
90   return QObject::tr( "This algorithm uses the GPSBabel tool to convert GPX features from one type to another (e.g. converting all waypoint features to a route feature)." );
91 }
92 
createInstance() const93 QgsConvertGpxFeatureTypeAlgorithm *QgsConvertGpxFeatureTypeAlgorithm::createInstance() const
94 {
95   return new QgsConvertGpxFeatureTypeAlgorithm();
96 }
97 
98 
processAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)99 QVariantMap QgsConvertGpxFeatureTypeAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
100 {
101   const QStringList convertStrings;
102 
103   const QString inputPath = parameterAsString( parameters, QStringLiteral( "INPUT" ), context );
104   const QString outputPath = parameterAsString( parameters, QStringLiteral( "OUTPUT" ), context );
105 
106   const ConversionType convertType = static_cast< ConversionType >( parameterAsEnum( parameters, QStringLiteral( "CONVERSION" ), context ) );
107 
108   QString babelPath = QgsSettingsRegistryCore::settingsGpsBabelPath.value();
109   if ( babelPath.isEmpty() )
110     babelPath = QStringLiteral( "gpsbabel" );
111 
112   QStringList processArgs;
113   QStringList logArgs;
114   createArgumentLists( inputPath, outputPath, convertType, processArgs, logArgs );
115   feedback->pushCommandInfo( QObject::tr( "Conversion command: " ) + babelPath + ' ' + logArgs.join( ' ' ) );
116 
117   QgsBlockingProcess babelProcess( babelPath, processArgs );
118   babelProcess.setStdErrHandler( [ = ]( const QByteArray & ba )
119   {
120     feedback->reportError( ba );
121   } );
122   babelProcess.setStdOutHandler( [ = ]( const QByteArray & ba )
123   {
124     feedback->pushDebugInfo( ba );
125   } );
126 
127   const int res = babelProcess.run( feedback );
128   if ( feedback->isCanceled() && res != 0 )
129   {
130     feedback->pushInfo( QObject::tr( "Process was canceled and did not complete" ) )  ;
131   }
132   else if ( !feedback->isCanceled() && babelProcess.exitStatus() == QProcess::CrashExit )
133   {
134     throw QgsProcessingException( QObject::tr( "Process was unexpectedly terminated" ) );
135   }
136   else if ( res == 0 )
137   {
138     feedback->pushInfo( QObject::tr( "Process completed successfully" ) );
139   }
140   else if ( babelProcess.processError() == QProcess::FailedToStart )
141   {
142     throw QgsProcessingException( QObject::tr( "Process %1 failed to start. Either %1 is missing, or you may have insufficient permissions to run the program." ).arg( babelPath ) );
143   }
144   else
145   {
146     throw QgsProcessingException( QObject::tr( "Process returned error code %1" ).arg( res ) );
147   }
148 
149   std::unique_ptr< QgsVectorLayer > layer;
150   const QString layerName = QgsProviderUtils::suggestLayerNameFromFilePath( outputPath );
151   // add the layer
152   switch ( convertType )
153   {
154     case QgsConvertGpxFeatureTypeAlgorithm::WaypointsFromRoute:
155     case QgsConvertGpxFeatureTypeAlgorithm::WaypointsFromTrack:
156       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=waypoint", layerName, QStringLiteral( "gpx" ) );
157       break;
158     case QgsConvertGpxFeatureTypeAlgorithm::RouteFromWaypoints:
159       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=route", layerName, QStringLiteral( "gpx" ) );
160       break;
161     case QgsConvertGpxFeatureTypeAlgorithm::TrackFromWaypoints:
162       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=track", layerName, QStringLiteral( "gpx" ) );
163       break;
164   }
165 
166   QVariantMap outputs;
167   if ( !layer->isValid() )
168   {
169     feedback->reportError( QObject::tr( "Resulting file is not a valid GPX layer" ) );
170   }
171   else
172   {
173     const QString layerId = layer->id();
174     outputs.insert( QStringLiteral( "OUTPUT_LAYER" ), layerId );
175     const QgsProcessingContext::LayerDetails details( layer->name(), context.project(), QStringLiteral( "OUTPUT_LAYER" ), QgsProcessingUtils::LayerHint::Vector );
176     context.addLayerToLoadOnCompletion( layerId, details );
177     context.temporaryLayerStore()->addMapLayer( layer.release() );
178   }
179 
180   outputs.insert( QStringLiteral( "OUTPUT" ), outputPath );
181   return outputs;
182 }
183 
createArgumentLists(const QString & inputPath,const QString & outputPath,ConversionType conversion,QStringList & processArgs,QStringList & logArgs)184 void QgsConvertGpxFeatureTypeAlgorithm::createArgumentLists( const QString &inputPath, const QString &outputPath, ConversionType conversion, QStringList &processArgs, QStringList &logArgs )
185 {
186   logArgs.reserve( 10 );
187   processArgs.reserve( 10 );
188   for ( const QString &arg : { QStringLiteral( "-i" ), QStringLiteral( "gpx" ), QStringLiteral( "-f" ) } )
189   {
190     logArgs << arg;
191     processArgs << arg;
192   }
193 
194   // when showing the babel command, wrap filenames in "", which is what QProcess does internally.
195   logArgs << QStringLiteral( "\"%1\"" ).arg( inputPath );
196   processArgs << inputPath;
197 
198   QStringList convertStrings;
199   switch ( conversion )
200   {
201     case QgsConvertGpxFeatureTypeAlgorithm::WaypointsFromRoute:
202       convertStrings << QStringLiteral( "-x" ) << QStringLiteral( "transform,wpt=rte,del" );
203       break;
204     case QgsConvertGpxFeatureTypeAlgorithm::WaypointsFromTrack:
205       convertStrings << QStringLiteral( "-x" ) << QStringLiteral( "transform,wpt=trk,del" );
206       break;
207     case QgsConvertGpxFeatureTypeAlgorithm::RouteFromWaypoints:
208       convertStrings << QStringLiteral( "-x" ) << QStringLiteral( "transform,rte=wpt,del" );
209       break;
210     case QgsConvertGpxFeatureTypeAlgorithm::TrackFromWaypoints:
211       convertStrings << QStringLiteral( "-x" ) << QStringLiteral( "transform,trk=wpt,del" );
212       break;
213   }
214   logArgs << convertStrings;
215   processArgs << convertStrings;
216 
217   for ( const QString &arg : { QStringLiteral( "-o" ), QStringLiteral( "gpx" ), QStringLiteral( "-F" ) } )
218   {
219     logArgs << arg;
220     processArgs << arg;
221   }
222 
223   logArgs << QStringLiteral( "\"%1\"" ).arg( outputPath );
224   processArgs << outputPath;
225 
226 }
227 
228 
229 //
230 // QgsConvertGpsDataAlgorithm
231 //
232 
name() const233 QString QgsConvertGpsDataAlgorithm::name() const
234 {
235   return QStringLiteral( "convertgpsdata" );
236 }
237 
displayName() const238 QString QgsConvertGpsDataAlgorithm::displayName() const
239 {
240   return QObject::tr( "Convert GPS data" );
241 }
242 
tags() const243 QStringList QgsConvertGpsDataAlgorithm::tags() const
244 {
245   return QObject::tr( "gps,tools,babel,tracks,waypoints,routes,gpx,import,export" ).split( ',' );
246 }
247 
group() const248 QString QgsConvertGpsDataAlgorithm::group() const
249 {
250   return QObject::tr( "GPS" );
251 }
252 
groupId() const253 QString QgsConvertGpsDataAlgorithm::groupId() const
254 {
255   return QStringLiteral( "gps" );
256 }
257 
initAlgorithm(const QVariantMap &)258 void QgsConvertGpsDataAlgorithm::initAlgorithm( const QVariantMap & )
259 {
260   addParameter( new QgsProcessingParameterFile( QStringLiteral( "INPUT" ), QObject::tr( "Input file" ), QgsProcessingParameterFile::File, QString(), QVariant(), false,
261                 QgsApplication::gpsBabelFormatRegistry()->importFileFilter() + QStringLiteral( ";;%1" ).arg( QObject::tr( "All files (*.*)" ) ) ) );
262 
263   std::unique_ptr< QgsProcessingParameterString > formatParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "FORMAT" ), QObject::tr( "Format" ) );
264 
265   QStringList formats;
266   const QStringList formatNames = QgsApplication::gpsBabelFormatRegistry()->importFormatNames();
267   for ( const QString &format : formatNames )
268     formats << QgsApplication::gpsBabelFormatRegistry()->importFormat( format )->description();
269 
270   std::sort( formats.begin(), formats.end(), []( const QString & a, const QString & b )
271   {
272     return a.compare( b, Qt::CaseInsensitive ) < 0;
273   } );
274 
275   formatParam->setMetadata( {{
276       QStringLiteral( "widget_wrapper" ), QVariantMap(
277       {{QStringLiteral( "value_hints" ), formats }}
278       )
279     }
280   } );
281   addParameter( formatParam.release() );
282 
283   addParameter( new QgsProcessingParameterEnum( QStringLiteral( "FEATURE_TYPE" ), QObject::tr( "Feature type" ),
284   {
285     QObject::tr( "Waypoints" ),
286     QObject::tr( "Routes" ),
287     QObject::tr( "Tracks" )
288   }, false, 0 ) );
289 
290   addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Output" ), QObject::tr( "GPX files" ) + QStringLiteral( " (*.gpx *.GPX)" ) ) );
291 
292   addOutput( new QgsProcessingOutputVectorLayer( QStringLiteral( "OUTPUT_LAYER" ), QObject::tr( "Output layer" ) ) );
293 }
294 
icon() const295 QIcon QgsConvertGpsDataAlgorithm::icon() const
296 {
297   return QgsApplication::getThemeIcon( QStringLiteral( "/mIconGps.svg" ) );
298 }
299 
svgIconPath() const300 QString QgsConvertGpsDataAlgorithm::svgIconPath() const
301 {
302   return QgsApplication::iconPath( QStringLiteral( "/mIconGps.svg" ) );
303 }
304 
shortHelpString() const305 QString QgsConvertGpsDataAlgorithm::shortHelpString() const
306 {
307   return QObject::tr( "This algorithm uses the GPSBabel tool to convert a GPS data file from a range of formats to the GPX standard format." );
308 }
309 
createInstance() const310 QgsConvertGpsDataAlgorithm *QgsConvertGpsDataAlgorithm::createInstance() const
311 {
312   return new QgsConvertGpsDataAlgorithm();
313 }
314 
processAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)315 QVariantMap QgsConvertGpsDataAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
316 {
317   const QStringList convertStrings;
318 
319   const QString inputPath = parameterAsString( parameters, QStringLiteral( "INPUT" ), context );
320   const QString outputPath = parameterAsString( parameters, QStringLiteral( "OUTPUT" ), context );
321 
322   const Qgis::GpsFeatureType featureType = static_cast< Qgis::GpsFeatureType >( parameterAsEnum( parameters, QStringLiteral( "FEATURE_TYPE" ), context ) );
323 
324   QString babelPath = QgsSettingsRegistryCore::settingsGpsBabelPath.value();
325   if ( babelPath.isEmpty() )
326     babelPath = QStringLiteral( "gpsbabel" );
327 
328   const QString formatName = parameterAsString( parameters, QStringLiteral( "FORMAT" ), context );
329   const QgsBabelSimpleImportFormat *format = QgsApplication::gpsBabelFormatRegistry()->importFormat( formatName );
330   if ( !format ) // second try, match using descriptions instead of names
331     format =  QgsApplication::gpsBabelFormatRegistry()->importFormatByDescription( formatName );
332 
333   if ( !format )
334   {
335     throw QgsProcessingException( QObject::tr( "Unknown GPSBabel format “%1”. Valid formats are: %2" )
336                                   .arg( formatName,
337                                         QgsApplication::gpsBabelFormatRegistry()->importFormatNames().join( QLatin1String( ", " ) ) ) );
338   }
339 
340   switch ( featureType )
341   {
342     case Qgis::GpsFeatureType::Waypoint:
343       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Waypoints ) )
344       {
345         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support converting waypoints." )
346                                       .arg( formatName ) );
347       }
348       break;
349 
350     case Qgis::GpsFeatureType::Route:
351       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Routes ) )
352       {
353         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support converting routes." )
354                                       .arg( formatName ) );
355       }
356       break;
357 
358     case Qgis::GpsFeatureType::Track:
359       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Tracks ) )
360       {
361         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support converting tracks." )
362                                       .arg( formatName ) );
363       }
364       break;
365   }
366 
367   // note that for the log we should quote file paths, but for the actual command we don't. That's
368   // because QProcess does this internally for us, and double quoting causes issues
369   const QStringList logCommand = format->importCommand( babelPath, featureType, inputPath, outputPath, Qgis::BabelCommandFlag::QuoteFilePaths );
370   const QStringList processCommand = format->importCommand( babelPath, featureType, inputPath, outputPath );
371   feedback->pushCommandInfo( QObject::tr( "Conversion command: " ) + logCommand.join( ' ' ) );
372 
373   QgsBlockingProcess babelProcess( processCommand.value( 0 ), processCommand.mid( 1 ) );
374   babelProcess.setStdErrHandler( [ = ]( const QByteArray & ba )
375   {
376     feedback->reportError( ba );
377   } );
378   babelProcess.setStdOutHandler( [ = ]( const QByteArray & ba )
379   {
380     feedback->pushDebugInfo( ba );
381   } );
382 
383   const int res = babelProcess.run( feedback );
384   if ( feedback->isCanceled() && res != 0 )
385   {
386     feedback->pushInfo( QObject::tr( "Process was canceled and did not complete" ) )  ;
387   }
388   else if ( !feedback->isCanceled() && babelProcess.exitStatus() == QProcess::CrashExit )
389   {
390     throw QgsProcessingException( QObject::tr( "Process was unexpectedly terminated" ) );
391   }
392   else if ( res == 0 )
393   {
394     feedback->pushInfo( QObject::tr( "Process completed successfully" ) );
395   }
396   else if ( babelProcess.processError() == QProcess::FailedToStart )
397   {
398     throw QgsProcessingException( QObject::tr( "Process %1 failed to start. Either %1 is missing, or you may have insufficient permissions to run the program." ).arg( babelPath ) );
399   }
400   else
401   {
402     throw QgsProcessingException( QObject::tr( "Process returned error code %1" ).arg( res ) );
403   }
404 
405   std::unique_ptr< QgsVectorLayer > layer;
406   const QString layerName = QgsProviderUtils::suggestLayerNameFromFilePath( outputPath );
407   // add the layer
408   switch ( featureType )
409   {
410     case Qgis::GpsFeatureType::Waypoint:
411       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=waypoint", layerName, QStringLiteral( "gpx" ) );
412       break;
413     case Qgis::GpsFeatureType::Route:
414       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=route", layerName, QStringLiteral( "gpx" ) );
415       break;
416     case Qgis::GpsFeatureType::Track:
417       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=track", layerName, QStringLiteral( "gpx" ) );
418       break;
419   }
420 
421   QVariantMap outputs;
422   if ( !layer->isValid() )
423   {
424     feedback->reportError( QObject::tr( "Resulting file is not a valid GPX layer" ) );
425   }
426   else
427   {
428     const QString layerId = layer->id();
429     outputs.insert( QStringLiteral( "OUTPUT_LAYER" ), layerId );
430     const QgsProcessingContext::LayerDetails details( layer->name(), context.project(), QStringLiteral( "OUTPUT_LAYER" ), QgsProcessingUtils::LayerHint::Vector );
431     context.addLayerToLoadOnCompletion( layerId, details );
432     context.temporaryLayerStore()->addMapLayer( layer.release() );
433   }
434 
435   outputs.insert( QStringLiteral( "OUTPUT" ), outputPath );
436   return outputs;
437 }
438 
439 //
440 // QgsDownloadGpsDataAlgorithm
441 //
442 
name() const443 QString QgsDownloadGpsDataAlgorithm::name() const
444 {
445   return QStringLiteral( "downloadgpsdata" );
446 }
447 
displayName() const448 QString QgsDownloadGpsDataAlgorithm::displayName() const
449 {
450   return QObject::tr( "Download GPS data from device" );
451 }
452 
tags() const453 QStringList QgsDownloadGpsDataAlgorithm::tags() const
454 {
455   return QObject::tr( "gps,tools,babel,tracks,waypoints,routes,gpx,import,export,export,device,serial" ).split( ',' );
456 }
457 
group() const458 QString QgsDownloadGpsDataAlgorithm::group() const
459 {
460   return QObject::tr( "GPS" );
461 }
462 
groupId() const463 QString QgsDownloadGpsDataAlgorithm::groupId() const
464 {
465   return QStringLiteral( "gps" );
466 }
467 
initAlgorithm(const QVariantMap &)468 void QgsDownloadGpsDataAlgorithm::initAlgorithm( const QVariantMap & )
469 {
470   std::unique_ptr< QgsProcessingParameterString > deviceParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "DEVICE" ), QObject::tr( "Device" ) );
471 
472   QStringList deviceNames = QgsApplication::gpsBabelFormatRegistry()->deviceNames();
473   std::sort( deviceNames.begin(), deviceNames.end(), []( const QString & a, const QString & b )
474   {
475     return a.compare( b, Qt::CaseInsensitive ) < 0;
476   } );
477 
478   deviceParam->setMetadata( {{
479       QStringLiteral( "widget_wrapper" ), QVariantMap(
480       {{QStringLiteral( "value_hints" ), deviceNames }}
481       )
482     }
483   } );
484   addParameter( deviceParam.release() );
485 
486 
487   const QList< QPair<QString, QString> > devices = QgsGpsDetector::availablePorts() << QPair<QString, QString>( QStringLiteral( "usb:" ), QStringLiteral( "usb:" ) );
488   std::unique_ptr< QgsProcessingParameterString > portParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "PORT" ), QObject::tr( "Port" ) );
489 
490   QStringList ports;
491   for ( auto it = devices.constBegin(); it != devices.constEnd(); ++ it )
492     ports << it->second;
493   std::sort( ports.begin(), ports.end(), []( const QString & a, const QString & b )
494   {
495     return a.compare( b, Qt::CaseInsensitive ) < 0;
496   } );
497 
498   portParam->setMetadata( {{
499       QStringLiteral( "widget_wrapper" ), QVariantMap(
500       {{QStringLiteral( "value_hints" ), ports }}
501       )
502     }
503   } );
504   addParameter( portParam.release() );
505 
506   addParameter( new QgsProcessingParameterEnum( QStringLiteral( "FEATURE_TYPE" ), QObject::tr( "Feature type" ),
507   {
508     QObject::tr( "Waypoints" ),
509     QObject::tr( "Routes" ),
510     QObject::tr( "Tracks" )
511   }, false, 0 ) );
512 
513   addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Output" ), QObject::tr( "GPX files" ) + QStringLiteral( " (*.gpx *.GPX)" ) ) );
514 
515   addOutput( new QgsProcessingOutputVectorLayer( QStringLiteral( "OUTPUT_LAYER" ), QObject::tr( "Output layer" ) ) );
516 }
517 
icon() const518 QIcon QgsDownloadGpsDataAlgorithm::icon() const
519 {
520   return QgsApplication::getThemeIcon( QStringLiteral( "/mIconGps.svg" ) );
521 }
522 
svgIconPath() const523 QString QgsDownloadGpsDataAlgorithm::svgIconPath() const
524 {
525   return QgsApplication::iconPath( QStringLiteral( "/mIconGps.svg" ) );
526 }
527 
shortHelpString() const528 QString QgsDownloadGpsDataAlgorithm::shortHelpString() const
529 {
530   return QObject::tr( "This algorithm uses the GPSBabel tool to download data from a GPS device into the GPX standard format." );
531 }
532 
createInstance() const533 QgsDownloadGpsDataAlgorithm *QgsDownloadGpsDataAlgorithm::createInstance() const
534 {
535   return new QgsDownloadGpsDataAlgorithm();
536 }
537 
processAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)538 QVariantMap QgsDownloadGpsDataAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
539 {
540   const QString outputPath = parameterAsString( parameters, QStringLiteral( "OUTPUT" ), context );
541   const Qgis::GpsFeatureType featureType = static_cast< Qgis::GpsFeatureType >( parameterAsEnum( parameters, QStringLiteral( "FEATURE_TYPE" ), context ) );
542 
543   QString babelPath = QgsSettingsRegistryCore::settingsGpsBabelPath.value();
544   if ( babelPath.isEmpty() )
545     babelPath = QStringLiteral( "gpsbabel" );
546 
547   const QString deviceName = parameterAsString( parameters, QStringLiteral( "DEVICE" ), context );
548   const QgsBabelGpsDeviceFormat *format = QgsApplication::gpsBabelFormatRegistry()->deviceFormat( deviceName );
549   if ( !format )
550   {
551     throw QgsProcessingException( QObject::tr( "Unknown GPSBabel device “%1”. Valid devices are: %2" )
552                                   .arg( deviceName,
553                                         QgsApplication::gpsBabelFormatRegistry()->deviceNames().join( QLatin1String( ", " ) ) ) );
554   }
555 
556   const QString portName = parameterAsString( parameters, QStringLiteral( "PORT" ), context );
557   QString inputPort;
558   const QList< QPair<QString, QString> > devices = QgsGpsDetector::availablePorts() << QPair<QString, QString>( QStringLiteral( "usb:" ), QStringLiteral( "usb:" ) );
559   QStringList validPorts;
560   for ( auto it = devices.constBegin(); it != devices.constEnd(); ++it )
561   {
562     if ( it->first.compare( portName, Qt::CaseInsensitive ) == 0 || it->second.compare( portName, Qt::CaseInsensitive ) == 0 )
563     {
564       inputPort = it->first;
565     }
566     validPorts << it->first;
567   }
568   if ( inputPort.isEmpty() )
569   {
570     throw QgsProcessingException( QObject::tr( "Unknown port “%1”. Valid ports are: %2" )
571                                   .arg( portName,
572                                         validPorts.join( QLatin1String( ", " ) ) ) );
573   }
574 
575   switch ( featureType )
576   {
577     case Qgis::GpsFeatureType::Waypoint:
578       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Waypoints ) )
579       {
580         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support converting waypoints." )
581                                       .arg( deviceName ) );
582       }
583       break;
584 
585     case Qgis::GpsFeatureType::Route:
586       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Routes ) )
587       {
588         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support converting routes." )
589                                       .arg( deviceName ) );
590       }
591       break;
592 
593     case Qgis::GpsFeatureType::Track:
594       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Tracks ) )
595       {
596         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support converting tracks." )
597                                       .arg( deviceName ) );
598       }
599       break;
600   }
601 
602   // note that for the log we should quote file paths, but for the actual command we don't. That's
603   // because QProcess does this internally for us, and double quoting causes issues
604   const QStringList logCommand = format->importCommand( babelPath, featureType, inputPort, outputPath, Qgis::BabelCommandFlag::QuoteFilePaths );
605   const QStringList processCommand = format->importCommand( babelPath, featureType, inputPort, outputPath );
606   feedback->pushCommandInfo( QObject::tr( "Download command: " ) + logCommand.join( ' ' ) );
607 
608   QgsBlockingProcess babelProcess( processCommand.value( 0 ), processCommand.mid( 1 ) );
609   babelProcess.setStdErrHandler( [ = ]( const QByteArray & ba )
610   {
611     feedback->reportError( ba );
612   } );
613   babelProcess.setStdOutHandler( [ = ]( const QByteArray & ba )
614   {
615     feedback->pushDebugInfo( ba );
616   } );
617 
618   const int res = babelProcess.run( feedback );
619   if ( feedback->isCanceled() && res != 0 )
620   {
621     feedback->pushInfo( QObject::tr( "Process was canceled and did not complete" ) )  ;
622   }
623   else if ( !feedback->isCanceled() && babelProcess.exitStatus() == QProcess::CrashExit )
624   {
625     throw QgsProcessingException( QObject::tr( "Process was unexpectedly terminated" ) );
626   }
627   else if ( res == 0 )
628   {
629     feedback->pushInfo( QObject::tr( "Process completed successfully" ) );
630   }
631   else if ( babelProcess.processError() == QProcess::FailedToStart )
632   {
633     throw QgsProcessingException( QObject::tr( "Process %1 failed to start. Either %1 is missing, or you may have insufficient permissions to run the program." ).arg( babelPath ) );
634   }
635   else
636   {
637     throw QgsProcessingException( QObject::tr( "Process returned error code %1" ).arg( res ) );
638   }
639 
640   std::unique_ptr< QgsVectorLayer > layer;
641   const QString layerName = QgsProviderUtils::suggestLayerNameFromFilePath( outputPath );
642   // add the layer
643   switch ( featureType )
644   {
645     case Qgis::GpsFeatureType::Waypoint:
646       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=waypoint", layerName, QStringLiteral( "gpx" ) );
647       break;
648     case Qgis::GpsFeatureType::Route:
649       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=route", layerName, QStringLiteral( "gpx" ) );
650       break;
651     case Qgis::GpsFeatureType::Track:
652       layer = std::make_unique< QgsVectorLayer >( outputPath + "?type=track", layerName, QStringLiteral( "gpx" ) );
653       break;
654   }
655 
656   QVariantMap outputs;
657   if ( !layer->isValid() )
658   {
659     feedback->reportError( QObject::tr( "Resulting file is not a valid GPX layer" ) );
660   }
661   else
662   {
663     const QString layerId = layer->id();
664     outputs.insert( QStringLiteral( "OUTPUT_LAYER" ), layerId );
665     const QgsProcessingContext::LayerDetails details( layer->name(), context.project(), QStringLiteral( "OUTPUT_LAYER" ), QgsProcessingUtils::LayerHint::Vector );
666     context.addLayerToLoadOnCompletion( layerId, details );
667     context.temporaryLayerStore()->addMapLayer( layer.release() );
668   }
669 
670   outputs.insert( QStringLiteral( "OUTPUT" ), outputPath );
671   return outputs;
672 }
673 
674 
675 //
676 // QgsUploadGpsDataAlgorithm
677 //
678 
name() const679 QString QgsUploadGpsDataAlgorithm::name() const
680 {
681   return QStringLiteral( "uploadgpsdata" );
682 }
683 
displayName() const684 QString QgsUploadGpsDataAlgorithm::displayName() const
685 {
686   return QObject::tr( "Upload GPS data to device" );
687 }
688 
tags() const689 QStringList QgsUploadGpsDataAlgorithm::tags() const
690 {
691   return QObject::tr( "gps,tools,babel,tracks,waypoints,routes,gpx,import,export,export,device,serial" ).split( ',' );
692 }
693 
group() const694 QString QgsUploadGpsDataAlgorithm::group() const
695 {
696   return QObject::tr( "GPS" );
697 }
698 
groupId() const699 QString QgsUploadGpsDataAlgorithm::groupId() const
700 {
701   return QStringLiteral( "gps" );
702 }
703 
initAlgorithm(const QVariantMap &)704 void QgsUploadGpsDataAlgorithm::initAlgorithm( const QVariantMap & )
705 {
706   addParameter( new QgsProcessingParameterFile( QStringLiteral( "INPUT" ), QObject::tr( "Input file" ), QgsProcessingParameterFile::File, QString(), QVariant(), false,
707                 QObject::tr( "GPX files" ) + QStringLiteral( " (*.gpx *.GPX)" ) ) );
708 
709   std::unique_ptr< QgsProcessingParameterString > deviceParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "DEVICE" ), QObject::tr( "Device" ) );
710 
711   QStringList deviceNames = QgsApplication::gpsBabelFormatRegistry()->deviceNames();
712   std::sort( deviceNames.begin(), deviceNames.end(), []( const QString & a, const QString & b )
713   {
714     return a.compare( b, Qt::CaseInsensitive ) < 0;
715   } );
716 
717   deviceParam->setMetadata( {{
718       QStringLiteral( "widget_wrapper" ), QVariantMap(
719       {{QStringLiteral( "value_hints" ), deviceNames }}
720       )
721     }
722   } );
723   addParameter( deviceParam.release() );
724 
725   const QList< QPair<QString, QString> > devices = QgsGpsDetector::availablePorts() << QPair<QString, QString>( QStringLiteral( "usb:" ), QStringLiteral( "usb:" ) );
726   std::unique_ptr< QgsProcessingParameterString > portParam = std::make_unique< QgsProcessingParameterString >( QStringLiteral( "PORT" ), QObject::tr( "Port" ) );
727 
728   QStringList ports;
729   for ( auto it = devices.constBegin(); it != devices.constEnd(); ++ it )
730     ports << it->second;
731   std::sort( ports.begin(), ports.end(), []( const QString & a, const QString & b )
732   {
733     return a.compare( b, Qt::CaseInsensitive ) < 0;
734   } );
735 
736   portParam->setMetadata( {{
737       QStringLiteral( "widget_wrapper" ), QVariantMap(
738       {{QStringLiteral( "value_hints" ), ports }}
739       )
740     }
741   } );
742   addParameter( portParam.release() );
743 
744   addParameter( new QgsProcessingParameterEnum( QStringLiteral( "FEATURE_TYPE" ), QObject::tr( "Feature type" ),
745   {
746     QObject::tr( "Waypoints" ),
747     QObject::tr( "Routes" ),
748     QObject::tr( "Tracks" )
749   }, false, 0 ) );
750 
751 }
752 
icon() const753 QIcon QgsUploadGpsDataAlgorithm::icon() const
754 {
755   return QgsApplication::getThemeIcon( QStringLiteral( "/mIconGps.svg" ) );
756 }
757 
svgIconPath() const758 QString QgsUploadGpsDataAlgorithm::svgIconPath() const
759 {
760   return QgsApplication::iconPath( QStringLiteral( "/mIconGps.svg" ) );
761 }
762 
shortHelpString() const763 QString QgsUploadGpsDataAlgorithm::shortHelpString() const
764 {
765   return QObject::tr( "This algorithm uses the GPSBabel tool to upload data to a GPS device from the GPX standard format." );
766 }
767 
createInstance() const768 QgsUploadGpsDataAlgorithm *QgsUploadGpsDataAlgorithm::createInstance() const
769 {
770   return new QgsUploadGpsDataAlgorithm();
771 }
772 
processAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)773 QVariantMap QgsUploadGpsDataAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
774 {
775   const QString inputPath = parameterAsString( parameters, QStringLiteral( "INPUT" ), context );
776   const Qgis::GpsFeatureType featureType = static_cast< Qgis::GpsFeatureType >( parameterAsEnum( parameters, QStringLiteral( "FEATURE_TYPE" ), context ) );
777 
778   QString babelPath = QgsSettingsRegistryCore::settingsGpsBabelPath.value();
779   if ( babelPath.isEmpty() )
780     babelPath = QStringLiteral( "gpsbabel" );
781 
782   const QString deviceName = parameterAsString( parameters, QStringLiteral( "DEVICE" ), context );
783   const QgsBabelGpsDeviceFormat *format = QgsApplication::gpsBabelFormatRegistry()->deviceFormat( deviceName );
784   if ( !format )
785   {
786     throw QgsProcessingException( QObject::tr( "Unknown GPSBabel device “%1”. Valid devices are: %2" )
787                                   .arg( deviceName,
788                                         QgsApplication::gpsBabelFormatRegistry()->deviceNames().join( QLatin1String( ", " ) ) ) );
789   }
790 
791   const QString portName = parameterAsString( parameters, QStringLiteral( "PORT" ), context );
792   QString outputPort;
793   const QList< QPair<QString, QString> > devices = QgsGpsDetector::availablePorts() << QPair<QString, QString>( QStringLiteral( "usb:" ), QStringLiteral( "usb:" ) );
794   QStringList validPorts;
795   for ( auto it = devices.constBegin(); it != devices.constEnd(); ++it )
796   {
797     if ( it->first.compare( portName, Qt::CaseInsensitive ) == 0 || it->second.compare( portName, Qt::CaseInsensitive ) == 0 )
798     {
799       outputPort = it->first;
800     }
801     validPorts << it->first;
802   }
803   if ( outputPort.isEmpty() )
804   {
805     throw QgsProcessingException( QObject::tr( "Unknown port “%1”. Valid ports are: %2" )
806                                   .arg( portName,
807                                         validPorts.join( QLatin1String( ", " ) ) ) );
808   }
809 
810 
811   switch ( featureType )
812   {
813     case Qgis::GpsFeatureType::Waypoint:
814       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Waypoints ) )
815       {
816         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support waypoints." )
817                                       .arg( deviceName ) );
818       }
819       break;
820 
821     case Qgis::GpsFeatureType::Route:
822       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Routes ) )
823       {
824         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support routes." )
825                                       .arg( deviceName ) );
826       }
827       break;
828 
829     case Qgis::GpsFeatureType::Track:
830       if ( !( format->capabilities() & Qgis::BabelFormatCapability::Tracks ) )
831       {
832         throw QgsProcessingException( QObject::tr( "The GPSBabel format “%1” does not support tracks." )
833                                       .arg( deviceName ) );
834       }
835       break;
836   }
837 
838   // note that for the log we should quote file paths, but for the actual command we don't. That's
839   // because QProcess does this internally for us, and double quoting causes issues
840   const QStringList logCommand = format->exportCommand( babelPath, featureType, inputPath, outputPort, Qgis::BabelCommandFlag::QuoteFilePaths );
841   const QStringList processCommand = format->exportCommand( babelPath, featureType, inputPath, outputPort );
842   feedback->pushCommandInfo( QObject::tr( "Upload command: " ) + logCommand.join( ' ' ) );
843 
844   QgsBlockingProcess babelProcess( processCommand.value( 0 ), processCommand.mid( 1 ) );
845   babelProcess.setStdErrHandler( [ = ]( const QByteArray & ba )
846   {
847     feedback->reportError( ba );
848   } );
849   babelProcess.setStdOutHandler( [ = ]( const QByteArray & ba )
850   {
851     feedback->pushDebugInfo( ba );
852   } );
853 
854   const int res = babelProcess.run( feedback );
855   if ( feedback->isCanceled() && res != 0 )
856   {
857     feedback->pushInfo( QObject::tr( "Process was canceled and did not complete" ) )  ;
858   }
859   else if ( !feedback->isCanceled() && babelProcess.exitStatus() == QProcess::CrashExit )
860   {
861     throw QgsProcessingException( QObject::tr( "Process was unexpectedly terminated" ) );
862   }
863   else if ( res == 0 )
864   {
865     feedback->pushInfo( QObject::tr( "Process completed successfully" ) );
866   }
867   else if ( babelProcess.processError() == QProcess::FailedToStart )
868   {
869     throw QgsProcessingException( QObject::tr( "Process %1 failed to start. Either %1 is missing, or you may have insufficient permissions to run the program." ).arg( babelPath ) );
870   }
871   else
872   {
873     throw QgsProcessingException( QObject::tr( "Process returned error code %1" ).arg( res ) );
874   }
875 
876   return {};
877 }
878 
879 ///@endcond
880 #endif // process
881