Skip to content

Commit

Permalink
[gps][feature] Add option to directly log GPS points and tracks
Browse files Browse the repository at this point in the history
to a Geopackage or Spatialite db

When activated via the GPS toolbar - settings button - "Log to
Geopackage/Spatialite" action, the user will be prompted
to select and existing GPKG/spatialite file or enter a new file
name. A "gps_points" and "gps_tracks" table will be created in
the file with a predefined structure.

All incoming GPS messages will be logged to the gps_points layer,
along with speed/bearing/altitude/accuracy information from the
GPS

When the GPS is disconnected (or QGIS closed), the entire recorded
GPS track will be added to the gps_tracks table (along with
some calculated information like track length, start and end times)

Sponsored by NIWA
  • Loading branch information
nyalldawson committed Nov 16, 2022
1 parent cdffe2e commit 7c3703f
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 13 deletions.
263 changes: 252 additions & 11 deletions src/app/gps/qgsappgpslogging.cpp
Expand Up @@ -19,32 +19,67 @@
#include "qgsmessagebar.h"
#include "qgsgpsconnection.h"
#include "qgsappgpsconnection.h"
#include "qgsvectorlayergpslogger.h"
#include "qgsproviderregistry.h"
#include "qgsprovidermetadata.h"

const std::vector< std::tuple< Qgis::GpsInformationComponent, std::tuple< QVariant::Type, QString >>> QgsAppGpsLogging::sPointFields
{
{ Qgis::GpsInformationComponent::Timestamp, { QVariant::DateTime, QStringLiteral( "timestamp" )}},
{ Qgis::GpsInformationComponent::Altitude, { QVariant::Double, QStringLiteral( "altitude" )}},
{ Qgis::GpsInformationComponent::GroundSpeed, { QVariant::Double, QStringLiteral( "ground_speed" )}},
{ Qgis::GpsInformationComponent::Bearing, { QVariant::Double, QStringLiteral( "bearing" )}},
{ Qgis::GpsInformationComponent::Pdop, { QVariant::Double, QStringLiteral( "pdop" )}},
{ Qgis::GpsInformationComponent::Hdop, { QVariant::Double, QStringLiteral( "hdop" )}},
{ Qgis::GpsInformationComponent::Vdop, { QVariant::Double, QStringLiteral( "vdop" )}},
{ Qgis::GpsInformationComponent::HorizontalAccuracy, { QVariant::Double, QStringLiteral( "horizontal_accuracy" )}},
{ Qgis::GpsInformationComponent::VerticalAccuracy, { QVariant::Double, QStringLiteral( "vertical_accuracy" )}},
{ Qgis::GpsInformationComponent::HvAccuracy, { QVariant::Double, QStringLiteral( "hv_accuracy" )}},
{ Qgis::GpsInformationComponent::SatellitesUsed, { QVariant::Double, QStringLiteral( "satellites_used" )}},
{ Qgis::GpsInformationComponent::TrackDistanceSinceLastPoint, { QVariant::Double, QStringLiteral( "distance_since_previous" )}},
{ Qgis::GpsInformationComponent::TrackTimeSinceLastPoint, { QVariant::Double, QStringLiteral( "time_since_previous" )}},
};

const std::vector< std::tuple< Qgis::GpsInformationComponent, std::tuple< QVariant::Type, QString >>> QgsAppGpsLogging::sTrackFields
{
{ Qgis::GpsInformationComponent::TrackStartTime, { QVariant::DateTime, QStringLiteral( "start_time" )}},
{ Qgis::GpsInformationComponent::TrackEndTime, { QVariant::DateTime, QStringLiteral( "end_time" )}},
{ Qgis::GpsInformationComponent::TotalTrackLength, { QVariant::Double, QStringLiteral( "track_length" )}},
{ Qgis::GpsInformationComponent::TrackDistanceFromStart, { QVariant::Double, QStringLiteral( "distance_from_start" )}},
};


QgsAppGpsLogging::QgsAppGpsLogging( QgsAppGpsConnection *connection, QObject *parent )
: QgsGpsLogger( nullptr, parent )
: QObject( parent )
, mConnection( connection )
{
connect( QgsProject::instance(), &QgsProject::transformContextChanged, this, [ = ]
{
setTransformContext( QgsProject::instance()->transformContext() );
if ( mGpkgLogger )
mGpkgLogger->setTransformContext( QgsProject::instance()->transformContext() );
} );
setTransformContext( QgsProject::instance()->transformContext() );

setEllipsoid( QgsProject::instance()->ellipsoid() );
connect( QgsProject::instance(), &QgsProject::ellipsoidChanged, this, [ = ]
{
setEllipsoid( QgsProject::instance()->ellipsoid() );
if ( mGpkgLogger )
mGpkgLogger->setEllipsoid( QgsProject::instance()->ellipsoid() );
} );

connect( mConnection, &QgsAppGpsConnection::connected, this, &QgsAppGpsLogging::gpsConnected );
connect( mConnection, &QgsAppGpsConnection::disconnected, this, &QgsAppGpsLogging::gpsDisconnected );

connect( QgsGui::instance(), &QgsGui::optionsChanged, this, &QgsAppGpsLogging::updateGpsSettings );
updateGpsSettings();
connect( QgsGui::instance(), &QgsGui::optionsChanged, this, [ = ]
{
if ( mGpkgLogger )
mGpkgLogger->updateGpsSettings();
} );
}

QgsAppGpsLogging::~QgsAppGpsLogging()
{
if ( mGpkgLogger )
{
mGpkgLogger->endCurrentTrack();
}
}

void QgsAppGpsLogging::setNmeaLogFile( const QString &filename )
Expand Down Expand Up @@ -82,23 +117,48 @@ void QgsAppGpsLogging::setNmeaLoggingEnabled( bool enabled )

void QgsAppGpsLogging::setGpkgLogFile( const QString &filename )
{
mGpkgLogFile = filename;
if ( filename.isEmpty() )
{
// stop logging
if ( mGpkgLogger )
{
mGpkgLogger->endCurrentTrack();
mGpkgLogger.reset();

}
QgisApp::instance()->messageBar()->pushInfo( QString(), tr( "GPS logging stopped" ) );
}
}
else
{
if ( !createOrUpdateLogDatabase() )
return;
createGpkgLogger();

QgisApp::instance()->messageBar()->pushInfo( QString(), tr( "Saving GPS log to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( mGpkgLogFile ).toString(), QDir::toNativeSeparators( mGpkgLogFile ) ) );
}
}

void QgsAppGpsLogging::gpsConnected()
{
if ( !mLogFile && mEnableNmeaLogging && !mNmeaLogFile.isEmpty() )
{
startNmeaLogging();
}
setConnection( mConnection->connection() );
if ( mGpkgLogger )
{
mGpkgLogger->setConnection( mConnection->connection() );
}
}

void QgsAppGpsLogging::gpsDisconnected()
{
stopNmeaLogging();
setConnection( nullptr );
if ( mGpkgLogger )
{
mGpkgLogger->endCurrentTrack();
mGpkgLogger->setConnection( nullptr );
}
}

void QgsAppGpsLogging::logNmeaSentence( const QString &nmeaString )
Expand Down Expand Up @@ -144,3 +204,184 @@ void QgsAppGpsLogging::stopNmeaLogging()
}
}

void QgsAppGpsLogging::createGpkgLogger()
{
mGpkgLogger = std::make_unique< QgsVectorLayerGpsLogger >( mConnection->connection() );
mGpkgLogger->setTransformContext( QgsProject::instance()->transformContext() );
mGpkgLogger->setEllipsoid( QgsProject::instance()->ellipsoid() );
mGpkgLogger->updateGpsSettings();
// write direct to data provider, just in case the QGIS session is closed unexpectedly (because the laptop
// battery ran out that is, not because we want to protect against QGIS crashes ;)
mGpkgLogger->setWriteToEditBuffer( false );

QVariantMap uriParts;
uriParts.insert( QStringLiteral( "path" ), mGpkgLogFile );
uriParts.insert( QStringLiteral( "layerName" ), QStringLiteral( "gps_points" ) );

mGpkgPointsLayer = std::make_unique< QgsVectorLayer >( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), uriParts ) );
if ( mGpkgPointsLayer->isValid() )
{
for ( const auto &it : sPointFields )
{
Qgis::GpsInformationComponent component;
std::tuple< QVariant::Type, QString > fieldTypeToName;
QVariant::Type fieldType;
QString fieldName;
std::tie( component, fieldTypeToName ) = it;
std::tie( fieldType, fieldName ) = fieldTypeToName;

const int fieldIndex = mGpkgPointsLayer->fields().lookupField( fieldName );
if ( fieldIndex >= 0 )
{
mGpkgLogger->setDestinationField( component, fieldName );
}
}
mGpkgLogger->setPointsLayer( mGpkgPointsLayer.get() );
}
else
{
QgisApp::instance()->messageBar()->pushCritical( tr( "Log GPS Tracks" ), tr( "Could not load gps_points layer" ) );
emit gpkgLoggingFailed();
mGpkgPointsLayer.reset();
return;
}

uriParts.insert( QStringLiteral( "layerName" ), QStringLiteral( "gps_tracks" ) );
mGpkgTracksLayer = std::make_unique< QgsVectorLayer >( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), uriParts ) );
if ( mGpkgTracksLayer->isValid() )
{
for ( const auto &it : sTrackFields )
{
Qgis::GpsInformationComponent component;
std::tuple< QVariant::Type, QString > fieldTypeToName;
QVariant::Type fieldType;
QString fieldName;
std::tie( component, fieldTypeToName ) = it;
std::tie( fieldType, fieldName ) = fieldTypeToName;

const int fieldIndex = mGpkgTracksLayer->fields().lookupField( fieldName );
if ( fieldIndex >= 0 )
{
mGpkgLogger->setDestinationField( component, fieldName );
}
}
mGpkgLogger->setTracksLayer( mGpkgTracksLayer.get() );
}
else
{
QgisApp::instance()->messageBar()->pushCritical( tr( "Log GPS Tracks" ), tr( "Could not load gps_tracks layer" ) );
emit gpkgLoggingFailed();
mGpkgTracksLayer.reset();
return;
}

}

bool QgsAppGpsLogging::createOrUpdateLogDatabase()
{
const QFileInfo fi( mGpkgLogFile );

if ( QgsProviderMetadata *ogrMetadata = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) ) )
{
QString error;

// if database doesn't already exist, create it
bool newFile = false;
if ( !QFile::exists( mGpkgLogFile ) )
{
if ( ! ogrMetadata->createDatabase( mGpkgLogFile, error ) )
{
QgisApp::instance()->messageBar()->pushCritical( tr( "Create GPS Log" ), tr( "Database creation failed: %1" ).arg( error ) );
emit gpkgLoggingFailed();
return false;
}
newFile = true;
}

// does gps_points layer already exist?
bool createPointLayer = true;
if ( !newFile )
{
std::unique_ptr< QgsVectorLayer > testLayer = std::make_unique< QgsVectorLayer>( ogrMetadata->encodeUri( {{QStringLiteral( "path" ), mGpkgLogFile }, {QStringLiteral( "layerName" ), QStringLiteral( "gps_points" )}} ), QString(), QStringLiteral( "ogr" ) );
if ( testLayer->isValid() )
{
createPointLayer = false;
}
}

QMap< int, int > unusedMap;
QVariantMap options;
options.insert( QStringLiteral( "driverName" ), QgsVectorFileWriter::driverForExtension( fi.suffix() ) );
options.insert( QStringLiteral( "update" ), true );
options.insert( QStringLiteral( "layerName" ), QStringLiteral( "gps_points" ) );
if ( createPointLayer )
{
QgsFields pointFields;
for ( const auto &it : sPointFields )
{
Qgis::GpsInformationComponent component;
std::tuple< QVariant::Type, QString > fieldTypeToName;
QVariant::Type fieldType;
QString fieldName;
std::tie( component, fieldTypeToName ) = it;
std::tie( fieldType, fieldName ) = fieldTypeToName;
pointFields.append( QgsField( fieldName, fieldType ) );
}

const Qgis::VectorExportResult result = ogrMetadata->createEmptyLayer( mGpkgLogFile,
pointFields,
QgsWkbTypes::PointZ,
QgsCoordinateReferenceSystem( "EPSG:4326" ),
false, unusedMap, error, &options );
if ( result != Qgis::VectorExportResult::Success )
{
QgisApp::instance()->messageBar()->pushCritical( tr( "Create GPS Log" ), tr( "Database creation failed: %1" ).arg( error ) );
emit gpkgLoggingFailed();
return false;
}
}

options.insert( QStringLiteral( "layerName" ), QStringLiteral( "gps_tracks" ) );

// does gps_tracks layer already exist?
bool createTracksLayer = true;
if ( !newFile )
{
std::unique_ptr< QgsVectorLayer > testLayer = std::make_unique< QgsVectorLayer>( ogrMetadata->encodeUri( {{QStringLiteral( "path" ), mGpkgLogFile }, {QStringLiteral( "layerName" ), QStringLiteral( "gps_tracks" )}} ), QString(), QStringLiteral( "ogr" ) );
if ( testLayer->isValid() )
{
createTracksLayer = false;
}
}

if ( createTracksLayer )
{
QgsFields tracksFields;
for ( const auto &it : sTrackFields )
{
Qgis::GpsInformationComponent component;
std::tuple< QVariant::Type, QString > fieldTypeToName;
QVariant::Type fieldType;
QString fieldName;
std::tie( component, fieldTypeToName ) = it;
std::tie( fieldType, fieldName ) = fieldTypeToName;
tracksFields.append( QgsField( fieldName, fieldType ) );
}

const Qgis::VectorExportResult result = ogrMetadata->createEmptyLayer( mGpkgLogFile,
tracksFields,
QgsWkbTypes::LineStringZ,
QgsCoordinateReferenceSystem( "EPSG:4326" ),
false, unusedMap, error, &options );
if ( result != Qgis::VectorExportResult::Success )
{
QgisApp::instance()->messageBar()->pushCritical( tr( "Create GPS Log" ), tr( "Database creation failed: %1" ).arg( error ) );
emit gpkgLoggingFailed();
return false;
}
}
return true;
}
return false;
}

24 changes: 22 additions & 2 deletions src/app/gps/qgsappgpslogging.h
Expand Up @@ -20,14 +20,15 @@

#include "qgis_app.h"
#include "qgssettingsentryimpl.h"
#include "qgsgpslogger.h"

#include <QTextStream>

class QgsAppGpsConnection;
class QgsVectorLayerGpsLogger;
class QFile;
class QgsVectorLayer;

class APP_EXPORT QgsAppGpsLogging: public QgsGpsLogger
class APP_EXPORT QgsAppGpsLogging: public QObject
{
Q_OBJECT

Expand All @@ -44,6 +45,10 @@ class APP_EXPORT QgsAppGpsLogging: public QgsGpsLogger
void setNmeaLoggingEnabled( bool enabled );
void setGpkgLogFile( const QString &filename );

signals:

void gpkgLoggingFailed();

private slots:

void gpsConnected();
Expand All @@ -55,13 +60,28 @@ class APP_EXPORT QgsAppGpsLogging: public QgsGpsLogger
void stopNmeaLogging();

private:

void createGpkgLogger();
bool createOrUpdateLogDatabase();
void createGpkgLogDatabase();
void createSpatialiteLogDatabase();

QgsAppGpsConnection *mConnection = nullptr;

QString mNmeaLogFile;
bool mEnableNmeaLogging = false;

std::unique_ptr< QFile > mLogFile;
QTextStream mLogFileTextStream;

QString mGpkgLogFile;
std::unique_ptr< QgsVectorLayerGpsLogger > mGpkgLogger;
std::unique_ptr< QgsVectorLayer > mGpkgPointsLayer;
std::unique_ptr< QgsVectorLayer > mGpkgTracksLayer;

static const std::vector< std::tuple< Qgis::GpsInformationComponent, std::tuple< QVariant::Type, QString >>> sPointFields;
static const std::vector< std::tuple< Qgis::GpsInformationComponent, std::tuple< QVariant::Type, QString >>> sTrackFields;

};

#endif // QGSAPPGPSLOGGING_H
5 changes: 5 additions & 0 deletions src/app/gps/qgsappgpssettingsmenu.cpp
Expand Up @@ -327,6 +327,11 @@ Qgis::MapRecenteringMode QgsAppGpsSettingsMenu::mapCenteringMode() const
}
}

void QgsAppGpsSettingsMenu::onGpkgLoggingFailed()
{
mActionGpkgLog->setChecked( false );
}

void QgsAppGpsSettingsMenu::timeStampMenuAboutToShow()
{
mFieldProxyModel->sourceFieldModel()->setLayer( QgsProject::instance()->gpsSettings()->destinationLayer() );
Expand Down
4 changes: 4 additions & 0 deletions src/app/gps/qgsappgpssettingsmenu.h
Expand Up @@ -57,6 +57,10 @@ class APP_EXPORT QgsAppGpsSettingsMenu : public QMenu
bool rotateMap() const;
Qgis::MapRecenteringMode mapCenteringMode() const;

public slots:

void onGpkgLoggingFailed();

signals:

void locationMarkerToggled( bool visible );
Expand Down

0 comments on commit 7c3703f

Please sign in to comment.