diff --git a/platform/android/res/values/strings.xml b/platform/android/res/values/strings.xml index f76751448c..c765194922 100644 --- a/platform/android/res/values/strings.xml +++ b/platform/android/res/values/strings.xml @@ -65,4 +65,7 @@ Your device does not support this import operation. Your device does not support this export operation. Processing… + Positioning + Positioning service running + Copy to clipboard diff --git a/platform/android/src/ch/opengis/qfield/QFieldCloudService.java b/platform/android/src/ch/opengis/qfield/QFieldCloudService.java index 9d0d20b845..3aab00a6c1 100644 --- a/platform/android/src/ch/opengis/qfield/QFieldCloudService.java +++ b/platform/android/src/ch/opengis/qfield/QFieldCloudService.java @@ -93,7 +93,7 @@ private void showNotification() { new Notification.Builder(this) .setSmallIcon(R.drawable.qfield_logo) .setWhen(System.currentTimeMillis()) - .setContentTitle("QField") + .setContentTitle("QFieldCloud") .setContentText(getString(R.string.upload_pending_attachments)) .setProgress(0, 0, true); diff --git a/platform/android/src/ch/opengis/qfield/QFieldPositioningService.java b/platform/android/src/ch/opengis/qfield/QFieldPositioningService.java index fb26ddb94a..b19f1db038 100644 --- a/platform/android/src/ch/opengis/qfield/QFieldPositioningService.java +++ b/platform/android/src/ch/opengis/qfield/QFieldPositioningService.java @@ -34,6 +34,8 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.pm.ServiceInfo; @@ -66,9 +68,10 @@ public static void stopQFieldPositioningService(Context context) { context.stopService(intent); } - public static void triggerShowNotification(String message) { + public static void triggerShowNotification(String message, + boolean addCopyToClipboard) { if (getInstance() != null) { - getInstance().showNotification(message); + getInstance().showNotification(message, addCopyToClipboard); } else { Log.v("QFieldPositioningService", "Showing message failed, no instance available."); @@ -91,7 +94,7 @@ public void onCreate() { if (getInstance() != null) { Log.v("QFieldPositioningService", - "service already running, aborting."); + "service already running, aborting onCreate."); stopSelf(); return; } @@ -102,15 +105,24 @@ public void onDestroy() { Log.v("QFieldPositioningService", "onDestroy triggered"); notificationManager.cancel(NOTIFICATION_ID); super.onDestroy(); + instance = null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.v("QFieldPositioningService", "onStartCommand triggered"); - int ret = super.onStartCommand(intent, flags, startId); + if (intent.hasExtra("content")) { + ClipboardManager clipboard = + (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setText(intent.getStringExtra("content")); + return START_NOT_STICKY; + } + + int ret = super.onStartCommand(intent, flags, startId); if (instance != null) { - stopSelf(); + Log.v("QFieldPositioningService", + "service already running, aborting onStartCommand."); return START_NOT_STICKY; } @@ -122,7 +134,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { notificationChannel = new NotificationChannel( CHANNEL_ID, "QField", NotificationManager.IMPORTANCE_DEFAULT); - notificationChannel.setDescription("QField positioning"); + notificationChannel.setDescription("QField Positioning"); notificationChannel.setImportance( NotificationManager.IMPORTANCE_LOW); notificationChannel.enableLights(false); @@ -135,8 +147,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { .setSmallIcon(R.drawable.qfield_logo) .setWhen(System.currentTimeMillis()) .setOngoing(true) - .setContentTitle("QField") - .setContentText("Positioning service running"); + .setContentTitle(getString(R.string.positioning_title)) + .setContentText(getString(R.string.positioning_running)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { builder.setChannelId(CHANNEL_ID); @@ -154,17 +166,35 @@ public int onStartCommand(Intent intent, int flags, int startId) { return START_STICKY; } - public void showNotification(String contentText) { - Notification.Builder builder = new Notification.Builder(this) - .setSmallIcon(R.drawable.qfield_logo) - .setWhen(System.currentTimeMillis()) - .setOngoing(true) - .setContentTitle("QField") - .setContentText(contentText); + public void showNotification(String contentText, + boolean addCopyToClipboard) { + // Return to QField activity when clicking on the notification + PendingIntent contentIntent = PendingIntent.getActivity( + this, 0, new Intent(this, QFieldActivity.class), + PendingIntent.FLAG_MUTABLE); + + Notification.Builder builder = + new Notification.Builder(this) + .setSmallIcon(R.drawable.qfield_logo) + .setWhen(System.currentTimeMillis()) + .setOngoing(true) + .setContentTitle(getString(R.string.positioning_title)) + .setContentText(contentText) + .setContentIntent(contentIntent); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { builder.setChannelId(CHANNEL_ID); } + if (addCopyToClipboard) { + // Allow for position details to be copied to the clipboard + Intent copyIntent = + new Intent(this, QFieldPositioningService.class); + copyIntent.putExtra("content", contentText); + PendingIntent copyPendingIntent = PendingIntent.getService( + this, 0, copyIntent, PendingIntent.FLAG_MUTABLE); + builder.addAction(0, getString(R.string.copy_to_clipboard), + copyPendingIntent); + } Notification notification = builder.build(); notificationManager.notify(NOTIFICATION_ID, notification); diff --git a/src/core/featuremodel.cpp b/src/core/featuremodel.cpp index 2b22e5d925..50dd1643f8 100644 --- a/src/core/featuremodel.cpp +++ b/src/core/featuremodel.cpp @@ -484,78 +484,88 @@ bool FeatureModel::save() if ( !mLayer ) return false; - bool rv = true; + bool isSuccess = true; - if ( !startEditing() ) + if ( mBatchMode ) { - rv = false; - } + // We take charge of default values that are set to be applied on feature update to take into account positioning and cloud context + updateDefaultValues(); - switch ( mModelMode ) + QgsFeature temporaryFeature = mFeature; + isSuccess = !mLayer->updateFeature( temporaryFeature, true ); + } + else { - case SingleFeatureModel: + if ( !startEditing() ) { - // We take charge of default values that are set to be applied on feature update to take into account positioning and cloud context - updateDefaultValues(); - - QgsFeature feat = mFeature; - if ( !mLayer->updateFeature( feat, true ) ) - QgsMessageLog::logMessage( tr( "Cannot update feature" ), QStringLiteral( "QField" ), Qgis::Warning ); + isSuccess = false; + } - if ( mProject && mProject->topologicalEditing() ) + switch ( mModelMode ) + { + case SingleFeatureModel: { - applyVertexModelTopography(); - } + // We take charge of default values that are set to be applied on feature update to take into account positioning and cloud context + updateDefaultValues(); - rv &= commit(); + QgsFeature temporaryFeature = mFeature; + if ( !mLayer->updateFeature( temporaryFeature, true ) ) + QgsMessageLog::logMessage( tr( "Cannot update feature" ), QStringLiteral( "QField" ), Qgis::Warning ); - if ( rv ) - { - QgsFeature modifiedFeature; - if ( mLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeature.id() ) ).nextFeature( modifiedFeature ) ) + if ( mProject && mProject->topologicalEditing() ) { - if ( modifiedFeature != mFeature ) + applyVertexModelTopography(); + } + + isSuccess &= commit(); + if ( isSuccess ) + { + QgsFeature modifiedFeature; + if ( mLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeature.id() ) ).nextFeature( modifiedFeature ) ) { - setFeature( modifiedFeature ); + if ( modifiedFeature != mFeature ) + { + setFeature( modifiedFeature ); + } + else + { + emit featureUpdated(); + } } else { - emit featureUpdated(); + QgsMessageLog::logMessage( tr( "Feature %1 could not be fetched after commit" ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Warning ); } } - else - { - QgsMessageLog::logMessage( tr( "Feature %1 could not be fetched after commit" ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Warning ); - } + break; } - break; - } - case MultiFeatureModel: - { - // We need to copy these members as the first feature updated triggers a refresh of the selected features, leading to changes in feature model members - const QgsFeature referenceFeature = mFeature; - const QList attributesAllowEdit = mAttributesAllowEdit; - QList features = mFeatures; - for ( QgsFeature &feature : features ) + case MultiFeatureModel: { - for ( int i = 0; i < referenceFeature.attributes().count(); i++ ) + // We need to copy these members as the first feature updated triggers a refresh of the selected features, leading to changes in feature model members + const QgsFeature referenceFeature = mFeature; + const QList attributesAllowEdit = mAttributesAllowEdit; + QList features = mFeatures; + for ( QgsFeature &feature : features ) { - if ( !attributesAllowEdit[i] ) - continue; + for ( int i = 0; i < referenceFeature.attributes().count(); i++ ) + { + if ( !attributesAllowEdit[i] ) + continue; - feature.setAttribute( i, referenceFeature.attributes().at( i ) ); - } - if ( !mLayer->updateFeature( feature ) ) - { - QgsMessageLog::logMessage( tr( "Cannot update feature" ), QStringLiteral( "QField" ), Qgis::Warning ); + feature.setAttribute( i, referenceFeature.attributes().at( i ) ); + } + if ( !mLayer->updateFeature( feature ) ) + { + QgsMessageLog::logMessage( tr( "Cannot update feature" ), QStringLiteral( "QField" ), Qgis::Warning ); + } } + isSuccess &= commit(); } - rv &= commit(); } } - return rv; + return isSuccess; } void FeatureModel::reset() @@ -777,114 +787,148 @@ void FeatureModel::removeLayer( QObject *layer ) sRememberings->remove( static_cast( layer ) ); } -bool FeatureModel::create() +void FeatureModel::setBatchMode( bool batchMode ) { - if ( !mLayer ) - return false; + if ( mBatchMode == batchMode ) + return; - if ( !startEditing() ) - { - QgsMessageLog::logMessage( tr( "Cannot start editing on layer \"%1\" to create feature %2" ).arg( mLayer->name() ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Critical ); - return false; - } + mBatchMode = batchMode; - bool hasRelations = false; - QList> revisitRelations; - if ( mProject ) + if ( mLayer ) { - // Gather any relationship children which would have relied on an auto-generated field value - const QList relations = mProject->relationManager()->referencedRelations( mLayer ); - hasRelations = !relations.isEmpty(); - QgsFeature temporaryFeature = mFeature; - for ( const QgsRelation &relation : relations ) + if ( mBatchMode ) { - const QgsAttributeList rereferencedFields = relation.referencedFields(); - bool needsRevisit = false; - for ( const int fieldIndex : rereferencedFields ) - { - if ( mLayer->dataProvider() && !mLayer->dataProvider()->defaultValueClause( fieldIndex ).isEmpty() ) - { - temporaryFeature.setAttribute( fieldIndex, QVariant() ); - needsRevisit = true; - } - } - if ( needsRevisit ) - { - revisitRelations << qMakePair( relation, relation.getRelatedFeaturesRequest( temporaryFeature ) ); - } + mLayer->startEditing(); + } + else + { + mLayer->commitChanges(); } } + emit batchModeChanged(); +} +bool FeatureModel::create() +{ + if ( !mLayer ) + return false; + + bool isSuccess = true; + // The connection below will be triggered when the new feature is committed and will provide // the saved feature ID needed to fetch the saved feature back from the data provider QgsFeatureId createdFeatureId; QMetaObject::Connection connection = connect( mLayer, &QgsVectorLayer::featureAdded, this, [&createdFeatureId]( QgsFeatureId fid ) { createdFeatureId = fid; } ); - bool isSuccess = true; - if ( mLayer->addFeature( mFeature ) ) + if ( mBatchMode ) + { + isSuccess = mLayer->addFeature( mFeature ); + if ( isSuccess ) + { + mFeature.setId( createdFeatureId ); + } + } + else { - if ( mProject && mProject->topologicalEditing() ) - mLayer->addTopologicalPoints( mFeature.geometry() ); + if ( !startEditing() ) + { + QgsMessageLog::logMessage( tr( "Cannot start editing on layer \"%1\" to create feature %2" ).arg( mLayer->name() ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Critical ); + return false; + } - if ( commit() ) + bool hasRelations = false; + QList> revisitRelations; + if ( mProject ) { - QgsFeature feat; - if ( mLayer->getFeatures( QgsFeatureRequest().setFilterFid( createdFeatureId ) ).nextFeature( feat ) ) + // Gather any relationship children which would have relied on an auto-generated field value + const QList relations = mProject->relationManager()->referencedRelations( mLayer ); + hasRelations = !relations.isEmpty(); + QgsFeature temporaryFeature = mFeature; + for ( const QgsRelation &relation : relations ) { - setFeature( feat ); + const QgsAttributeList rereferencedFields = relation.referencedFields(); + bool needsRevisit = false; + for ( const int fieldIndex : rereferencedFields ) + { + if ( mLayer->dataProvider() && !mLayer->dataProvider()->defaultValueClause( fieldIndex ).isEmpty() ) + { + temporaryFeature.setAttribute( fieldIndex, QVariant() ); + needsRevisit = true; + } + } + if ( needsRevisit ) + { + revisitRelations << qMakePair( relation, relation.getRelatedFeaturesRequest( temporaryFeature ) ); + } + } + } + + if ( mLayer->addFeature( mFeature ) ) + { + if ( mProject && mProject->topologicalEditing() ) + mLayer->addTopologicalPoints( mFeature.geometry() ); - if ( hasRelations ) + if ( commit() ) + { + QgsFeature feat; + if ( mLayer->getFeatures( QgsFeatureRequest().setFilterFid( createdFeatureId ) ).nextFeature( feat ) ) { - // Revisit relations in need of attribute updates - for ( const QPair &revisitRelation : std::as_const( revisitRelations ) ) + setFeature( feat ); + + if ( hasRelations ) { - const QList fieldPairs = revisitRelation.first.fieldPairs(); - revisitRelation.first.referencingLayer()->startEditing(); - QgsFeatureIterator it = revisitRelation.first.referencingLayer()->getFeatures( revisitRelation.second ); - QgsFeature childFeature; - while ( it.nextFeature( childFeature ) ) + // Revisit relations in need of attribute updates + for ( const QPair &revisitRelation : std::as_const( revisitRelations ) ) { - for ( const QgsRelation::FieldPair fieldPair : fieldPairs ) + const QList fieldPairs = revisitRelation.first.fieldPairs(); + revisitRelation.first.referencingLayer()->startEditing(); + QgsFeatureIterator it = revisitRelation.first.referencingLayer()->getFeatures( revisitRelation.second ); + QgsFeature childFeature; + while ( it.nextFeature( childFeature ) ) { - childFeature.setAttribute( fieldPair.referencingField(), feat.attribute( fieldPair.referencedField() ) ); + for ( const QgsRelation::FieldPair &fieldPair : fieldPairs ) + { + childFeature.setAttribute( fieldPair.referencingField(), feat.attribute( fieldPair.referencedField() ) ); + } + revisitRelation.first.referencingLayer()->updateFeature( childFeature ); } - revisitRelation.first.referencingLayer()->updateFeature( childFeature ); + revisitRelation.first.referencingLayer()->commitChanges(); } - revisitRelation.first.referencingLayer()->commitChanges(); - } - // We need to update default values after creation to insure expression relying on relation children compute properly - updateDefaultValues(); - save(); + // We need to update default values after creation to insure expression relying on relation children compute properly + updateDefaultValues(); + save(); + } + } + else + { + QgsMessageLog::logMessage( tr( "Layer \"%1\" has been commited but the newly created feature %2 could not be fetched" ).arg( mLayer->name() ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Critical ); + isSuccess = false; } } else { - QgsMessageLog::logMessage( tr( "Layer \"%1\" has been commited but the newly created feature %2 could not be fetched" ).arg( mLayer->name() ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Critical ); + const QString msgs = mLayer->commitErrors().join( QStringLiteral( "\n" ) ); + QgsMessageLog::logMessage( tr( "Layer \"%1\" cannot be commited with the newly created feature %2. Reason:\n%3" ).arg( mLayer->name() ).arg( mFeature.id() ).arg( msgs ), QStringLiteral( "QField" ), Qgis::Critical ); isSuccess = false; } } else { - const QString msgs = mLayer->commitErrors().join( QStringLiteral( "\n" ) ); - QgsMessageLog::logMessage( tr( "Layer \"%1\" cannot be commited with the newly created feature %2. Reason:\n%3" ).arg( mLayer->name() ).arg( mFeature.id() ).arg( msgs ), QStringLiteral( "QField" ), Qgis::Critical ); + QgsMessageLog::logMessage( tr( "Feature %2 could not be added in layer \"%1\"" ).arg( mLayer->name() ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Critical ); isSuccess = false; } - } - else - { - QgsMessageLog::logMessage( tr( "Feature %2 could not be added in layer \"%1\"" ).arg( mLayer->name() ).arg( mFeature.id() ), QStringLiteral( "QField" ), Qgis::Critical ); - isSuccess = false; - } - if ( isSuccess && sRememberings->contains( mLayer ) ) - { - QMutex *mutex = sMutex; - QMutexLocker locker( mutex ); - ( *sRememberings )[mLayer].rememberedFeature = mFeature; + if ( isSuccess && sRememberings->contains( mLayer ) ) + { + QMutex *mutex = sMutex; + QMutexLocker locker( mutex ); + ( *sRememberings )[mLayer].rememberedFeature = mFeature; + } } disconnect( connection ); + return isSuccess; } diff --git a/src/core/featuremodel.h b/src/core/featuremodel.h index 7244858dbe..c433e09a39 100644 --- a/src/core/featuremodel.h +++ b/src/core/featuremodel.h @@ -52,6 +52,7 @@ class FeatureModel : public QAbstractListModel Q_PROPERTY( bool positionLocked READ positionLocked WRITE setPositionLocked NOTIFY positionLockedChanged ) Q_PROPERTY( CloudUserInformation cloudUserInformation READ cloudUserInformation WRITE setCloudUserInformation NOTIFY cloudUserInformationChanged ) Q_PROPERTY( QgsProject *project READ project WRITE setProject NOTIFY projectChanged ) + Q_PROPERTY( bool batchMode READ batchMode WRITE setBatchMode NOTIFY batchModeChanged ) public: //! keeping the information what attributes are remembered and the last edited feature @@ -271,6 +272,18 @@ class FeatureModel : public QAbstractListModel QgsExpressionContext createExpressionContext() const; + /** + * Returns TRUE if the feature model is in batch mode. When enabled, the vector layer + * will remain in editing mode until batch mode is disabled. + */ + bool batchMode() const { return mBatchMode; } + + /** + * Toggles the feature model batch mode. When enabled, the vector layer + * will remain in editing mode until batch mode is disabled. + */ + void setBatchMode( bool batchMode ); + public slots: void applyGeometry(); void removeLayer( QObject *layer ); @@ -297,6 +310,7 @@ class FeatureModel : public QAbstractListModel void positionLockedChanged(); void projectChanged(); void cloudUserInformationChanged(); + void batchModeChanged(); void warning( const QString &text ); @@ -331,6 +345,7 @@ class FeatureModel : public QAbstractListModel QgsProject *mProject = nullptr; QString mTempName; bool mPositionLocked = false; + bool mBatchMode = false; }; #endif // FEATUREMODEL_H diff --git a/src/core/positioning/positioning.cpp b/src/core/positioning/positioning.cpp index 0c6066f4e7..a8e9b25c23 100644 --- a/src/core/positioning/positioning.cpp +++ b/src/core/positioning/positioning.cpp @@ -14,7 +14,6 @@ * * ***************************************************************************/ -#include "platformutilities.h" #include "positioning.h" #include "positioningutils.h" #include "tcpreceiver.h" @@ -23,6 +22,11 @@ #include "serialportreceiver.h" #endif +#if defined( Q_OS_ANDROID ) +#include "platformutilities.h" +#include "qfield_android.h" +#endif + #include #include #include @@ -37,9 +41,8 @@ Positioning::Positioning( QObject *parent ) if ( QFile::exists( PositioningSource::backgroundFilePath ) ) { QFile::remove( PositioningSource::backgroundFilePath ); + mPropertiesToSync["backgroundMode"] = false; } - - connect( QgsApplication::instance(), &QGuiApplication::applicationStateChanged, this, &Positioning::onApplicationStateChanged ); } void Positioning::setupSource() @@ -48,7 +51,7 @@ void Positioning::setupSource() #if defined( Q_OS_ANDROID ) PlatformUtilities::instance()->startPositioningService(); - mNode.connectToNode( QUrl( QStringLiteral( "localabstract:replica" ) ) ); + mNode.connectToNode( QUrl( QStringLiteral( "localabstract:" APP_PACKAGE_NAME "replica" ) ) ); positioningService = true; #endif @@ -64,6 +67,11 @@ void Positioning::setupSource() mPositioningSourceReplica.reset( mNode.acquireDynamic( "PositioningSource" ) ); mPositioningSourceReplica->waitForSource(); + const QList properties = mPropertiesToSync.keys(); + for ( const QString &property : properties ) + { + mPositioningSourceReplica->setProperty( property.toLatin1(), mPropertiesToSync[property] ); + } connect( mPositioningSourceReplica.data(), SIGNAL( activeChanged() ), this, SIGNAL( activeChanged() ) ); connect( mPositioningSourceReplica.data(), SIGNAL( validChanged() ), this, SIGNAL( validChanged() ) ); @@ -83,11 +91,12 @@ void Positioning::setupSource() connect( this, SIGNAL( triggerConnectDevice() ), mPositioningSourceReplica.data(), SLOT( triggerConnectDevice() ) ); connect( this, SIGNAL( triggerDisconnectDevice() ), mPositioningSourceReplica.data(), SLOT( triggerDisconnectDevice() ) ); - const QList properties = mPropertiesToSync.keys(); - for ( const QString &property : properties ) - { - mPositioningSourceReplica->setProperty( property.toLatin1(), mPropertiesToSync[property] ); - } + connect( QgsApplication::instance(), &QGuiApplication::applicationStateChanged, this, &Positioning::onApplicationStateChanged ); +} + +bool Positioning::isSourceAvailable() const +{ + return mPositioningSourceReplica && mPositioningSourceReplica->isInitialized(); } void Positioning::onApplicationStateChanged( Qt::ApplicationState state ) @@ -95,9 +104,6 @@ void Positioning::onApplicationStateChanged( Qt::ApplicationState state ) #ifdef Q_OS_ANDROID // Google Play policy only allows for background access if it's explicitly stated and justified // Not stopping on Activity::onPause is detected as violation - if ( !mPositioningSourceReplica ) - return; - if ( !mPositioningSource ) { // Service path @@ -133,7 +139,7 @@ void Positioning::onApplicationStateChanged( Qt::ApplicationState state ) bool Positioning::active() const { - return mPositioningSourceReplica ? mPositioningSourceReplica->property( "active" ).toBool() : false; + return isSourceAvailable() ? mPositioningSourceReplica->property( "active" ).toBool() : false; } void Positioning::setActive( bool active ) @@ -229,12 +235,12 @@ void Positioning::setActive( bool active ) bool Positioning::valid() const { - return mPositioningSourceReplica ? mPositioningSourceReplica->property( "valid" ).toBool() : mValid; + return isSourceAvailable() ? mPositioningSourceReplica->property( "valid" ).toBool() : mValid; } void Positioning::setValid( bool valid ) { - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { mPositioningSourceReplica->setProperty( "valid", valid ); } @@ -247,12 +253,12 @@ void Positioning::setValid( bool valid ) QString Positioning::deviceId() const { - return ( mPositioningSourceReplica ? mPositioningSourceReplica->property( "deviceId" ) : mPropertiesToSync.value( "deviceId" ) ).toString(); + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "deviceId" ) : mPropertiesToSync.value( "deviceId" ) ).toString(); } void Positioning::setDeviceId( const QString &id ) { - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { mPositioningSourceReplica->setProperty( "deviceId", id ); } @@ -266,7 +272,7 @@ void Positioning::setDeviceId( const QString &id ) GnssPositionDetails Positioning::deviceDetails() const { GnssPositionDetails list; - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { list = mPositioningSourceReplica->property( "deviceDetails" ).value(); } @@ -275,22 +281,22 @@ GnssPositionDetails Positioning::deviceDetails() const QString Positioning::deviceLastError() const { - return mPositioningSourceReplica ? mPositioningSourceReplica->property( "deviceLastError" ).toString() : QString(); + return isSourceAvailable() ? mPositioningSourceReplica->property( "deviceLastError" ).toString() : QString(); } QAbstractSocket::SocketState Positioning::deviceSocketState() const { - return mPositioningSourceReplica ? mPositioningSourceReplica->property( "deviceSocketState" ).value() : QAbstractSocket::UnconnectedState; + return isSourceAvailable() ? mPositioningSourceReplica->property( "deviceSocketState" ).value() : QAbstractSocket::UnconnectedState; } QString Positioning::deviceSocketStateString() const { - return mPositioningSourceReplica ? mPositioningSourceReplica->property( "deviceSocketStateString" ).toString() : QString(); + return isSourceAvailable() ? mPositioningSourceReplica->property( "deviceSocketStateString" ).toString() : QString(); } AbstractGnssReceiver::Capabilities Positioning::deviceCapabilities() const { - const QString deviceId = ( mPositioningSourceReplica ? mPositioningSourceReplica->property( "deviceId" ) : mPropertiesToSync.value( "deviceId" ) ).toString(); + const QString deviceId = ( isSourceAvailable() ? mPositioningSourceReplica->property( "deviceId" ) : mPropertiesToSync.value( "deviceId" ) ).toString(); if ( !deviceId.isEmpty() || deviceId.startsWith( TcpReceiver::identifier + ":" ) || deviceId.startsWith( UdpReceiver::identifier + ":" ) ) { // NMEA-based devices @@ -309,17 +315,17 @@ AbstractGnssReceiver::Capabilities Positioning::deviceCapabilities() const int Positioning::averagedPositionCount() const { - return mPositioningSourceReplica ? mPositioningSourceReplica->property( "averagedPositionCount" ).toInt() : 0; + return isSourceAvailable() ? mPositioningSourceReplica->property( "averagedPositionCount" ).toInt() : 0; } bool Positioning::averagedPosition() const { - return ( mPositioningSourceReplica ? mPositioningSourceReplica->property( "averagedPosition" ) : mPropertiesToSync.value( "averagedPosition", false ) ).toBool(); + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "averagedPosition" ) : mPropertiesToSync.value( "averagedPosition", false ) ).toBool(); } void Positioning::setAveragedPosition( bool averaged ) { - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { mPositioningSourceReplica->setProperty( "averagedPosition", averaged ); } @@ -332,12 +338,12 @@ void Positioning::setAveragedPosition( bool averaged ) bool Positioning::logging() const { - return ( mPositioningSourceReplica ? mPositioningSourceReplica->property( "logging" ) : mPropertiesToSync.value( "logging", false ) ).toBool(); + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "logging" ) : mPropertiesToSync.value( "logging", false ) ).toBool(); } void Positioning::setLogging( bool logging ) { - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { mPositioningSourceReplica->setProperty( "logging", logging ); } @@ -374,7 +380,7 @@ void Positioning::setBackgroundMode( bool backgroundMode ) } } - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { // Note that on Android, the property will not be set if the application is suspended _until_ it has become active again mPositioningSourceReplica->setProperty( "backgroundMode", backgroundMode ); @@ -383,14 +389,29 @@ void Positioning::setBackgroundMode( bool backgroundMode ) emit backgroundModeChanged(); } +QList Positioning::getBackgroundPositionInformation() const +{ + QList positionInformationList; + + if ( isSourceAvailable() ) + { + QRemoteObjectPendingCall call; + QMetaObject::invokeMethod( mPositioningSourceReplica.data(), "getBackgroundPositionInformation", Qt::DirectConnection, Q_RETURN_ARG( QRemoteObjectPendingCall, call ) ); + call.waitForFinished(); + positionInformationList = call.returnValue().value>(); + } + + return std::move( positionInformationList ); +} + PositioningSource::ElevationCorrectionMode Positioning::elevationCorrectionMode() const { - return static_cast( ( mPositioningSourceReplica ? mPositioningSourceReplica->property( "elevationCorrectionMode" ) : mPropertiesToSync.value( "elevationCorrectionMode", static_cast( PositioningSource::ElevationCorrectionMode::None ) ) ).toInt() ); + return static_cast( ( isSourceAvailable() ? mPositioningSourceReplica->property( "elevationCorrectionMode" ) : mPropertiesToSync.value( "elevationCorrectionMode", static_cast( PositioningSource::ElevationCorrectionMode::None ) ) ).toInt() ); } void Positioning::setElevationCorrectionMode( PositioningSource::ElevationCorrectionMode elevationCorrectionMode ) { - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { mPositioningSourceReplica->setProperty( "elevationCorrectionMode", static_cast( elevationCorrectionMode ) ); } @@ -403,12 +424,12 @@ void Positioning::setElevationCorrectionMode( PositioningSource::ElevationCorrec double Positioning::antennaHeight() const { - return ( mPositioningSourceReplica ? mPositioningSourceReplica->property( "antennaHeight" ) : mPropertiesToSync.value( "antennaHeight", 0.0 ) ).toDouble(); + return ( isSourceAvailable() ? mPositioningSourceReplica->property( "antennaHeight" ) : mPropertiesToSync.value( "antennaHeight", 0.0 ) ).toDouble(); } void Positioning::setAntennaHeight( double antennaHeight ) { - if ( mPositioningSourceReplica ) + if ( isSourceAvailable() ) { mPositioningSourceReplica->setProperty( "antennaHeight", antennaHeight ); } @@ -426,7 +447,7 @@ GnssPositionInformation Positioning::positionInformation() const double Positioning::orientation() const { - return mPositioningSourceReplica ? adjustOrientation( mPositioningSourceReplica->property( "orientation" ).toDouble() ) : std::numeric_limits::quiet_NaN(); + return isSourceAvailable() ? adjustOrientation( mPositioningSourceReplica->property( "orientation" ).toDouble() ) : std::numeric_limits::quiet_NaN(); } double Positioning::adjustOrientation( double orientation ) const @@ -454,13 +475,7 @@ void Positioning::setCoordinateTransformer( QgsQuickCoordinateTransformer *coord if ( mCoordinateTransformer == coordinateTransformer ) return; - if ( mCoordinateTransformer ) - { - disconnect( mCoordinateTransformer, &QgsQuickCoordinateTransformer::projectedPositionChanged, this, &Positioning::projectedPositionTransformed ); - } - mCoordinateTransformer = coordinateTransformer; - connect( mCoordinateTransformer, &QgsQuickCoordinateTransformer::projectedPositionChanged, this, &Positioning::projectedPositionTransformed ); emit coordinateTransformerChanged(); } @@ -495,7 +510,20 @@ void Positioning::processGnssPositionInformation() if ( mCoordinateTransformer ) { - mCoordinateTransformer->setSourcePosition( mSourcePosition ); + mProjectedPosition = mCoordinateTransformer->transformPosition( mSourcePosition ); + mProjectedHorizontalAccuracy = mPositionInformation.hacc(); + if ( mPositionInformation.haccValid() ) + { + if ( mCoordinateTransformer->destinationCrs().mapUnits() != Qgis::DistanceUnit::Unknown ) + { + mProjectedHorizontalAccuracy *= QgsUnitTypes::fromUnitToUnitFactor( Qgis::DistanceUnit::Meters, + mCoordinateTransformer->destinationCrs().mapUnits() ); + } + else + { + mProjectedHorizontalAccuracy = 0.0; + } + } } if ( mPositionInformation.orientationValid() ) @@ -505,23 +533,3 @@ void Positioning::processGnssPositionInformation() emit positionInformationChanged(); } - -void Positioning::projectedPositionTransformed() -{ - mProjectedPosition = mCoordinateTransformer->projectedPosition(); - mProjectedHorizontalAccuracy = mPositionInformation.hacc(); - if ( mPositionInformation.haccValid() ) - { - if ( mCoordinateTransformer->destinationCrs().mapUnits() != Qgis::DistanceUnit::Unknown ) - { - mProjectedHorizontalAccuracy *= QgsUnitTypes::fromUnitToUnitFactor( Qgis::DistanceUnit::Meters, - mCoordinateTransformer->destinationCrs().mapUnits() ); - } - else - { - mProjectedHorizontalAccuracy = 0.0; - } - } - - emit projectedPositionChanged(); -} diff --git a/src/core/positioning/positioning.h b/src/core/positioning/positioning.h index 0b1d3e5404..6f537559db 100644 --- a/src/core/positioning/positioning.h +++ b/src/core/positioning/positioning.h @@ -51,8 +51,8 @@ class Positioning : public QObject Q_PROPERTY( GnssPositionInformation positionInformation READ positionInformation NOTIFY positionInformationChanged ) Q_PROPERTY( QgsPoint sourcePosition READ sourcePosition NOTIFY positionInformationChanged ) - Q_PROPERTY( QgsPoint projectedPosition READ projectedPosition NOTIFY projectedPositionChanged ) - Q_PROPERTY( double projectedHorizontalAccuracy READ projectedHorizontalAccuracy NOTIFY projectedPositionChanged ) + Q_PROPERTY( QgsPoint projectedPosition READ projectedPosition NOTIFY positionInformationChanged ) + Q_PROPERTY( double projectedHorizontalAccuracy READ projectedHorizontalAccuracy NOTIFY positionInformationChanged ) Q_PROPERTY( bool averagedPosition READ averagedPosition WRITE setAveragedPosition NOTIFY averagedPositionChanged ) Q_PROPERTY( int averagedPositionCount READ averagedPositionCount NOTIFY averagedPositionCountChanged ) @@ -218,15 +218,26 @@ class Positioning : public QObject void setLogging( bool logging ); /** - * Returns TRUE if the background mode is active. + * Returns TRUE if the background mode is active. When activated, position information details + * will not be signaled but instead saved to disk until deactivated. + * \see getBackgroundPositionInformation() */ bool backgroundMode() const; /** - * Sets whether the background mode is active. + * Sets whether the background mode is active. When activated, position information details + * will not be signaled but instead saved to disk until deactivated. + * \see getBackgroundPositionInformation() */ void setBackgroundMode( bool backgroundMode ); + /** + * Returns a list of position information collected while background mode is active. + * \see backgroundMode() + * \see setBackgroundMode() + */ + Q_INVOKABLE QList getBackgroundPositionInformation() const; + signals: void activeChanged(); void validChanged(); @@ -250,11 +261,12 @@ class Positioning : public QObject private slots: void onApplicationStateChanged( Qt::ApplicationState state ); - void projectedPositionTransformed(); void processGnssPositionInformation(); private: void setupSource(); + bool isSourceAvailable() const; + double adjustOrientation( double orientation ) const; bool mValid = true; @@ -269,7 +281,7 @@ class Positioning : public QObject QgsQuickCoordinateTransformer *mCoordinateTransformer = nullptr; QgsPoint mSourcePosition; QgsPoint mProjectedPosition; - double mProjectedHorizontalAccuracy; + double mProjectedHorizontalAccuracy = 0.0; virtual QList> details() const { return {}; } bool mInternalPermissionChecked = false; diff --git a/src/core/positioning/positioningsource.cpp b/src/core/positioning/positioningsource.cpp index 2cf09781a2..8179486335 100644 --- a/src/core/positioning/positioningsource.cpp +++ b/src/core/positioning/positioningsource.cpp @@ -150,9 +150,39 @@ void PositioningSource::setBackgroundMode( bool backgroundMode ) mBackgroundMode = backgroundMode; + if ( mBackgroundMode ) + { + if ( QFile::exists( QStringLiteral( "%1.information" ).arg( backgroundFilePath ) ) ) + { + // Remove previously collected position information + QFile::remove( QStringLiteral( "%1.information" ).arg( backgroundFilePath ) ); + } + } + emit backgroundModeChanged(); } +QList PositioningSource::getBackgroundPositionInformation() const +{ + QList positionInformationList; + + QFile file( QStringLiteral( "%1.information" ).arg( backgroundFilePath ) ); + if ( file.exists() ) + { + file.open( QFile::ReadOnly ); + QDataStream stream( &file ); + while ( !stream.atEnd() ) + { + GnssPositionInformation positionInformation; + stream >> positionInformation; + positionInformationList << positionInformation; + } + file.close(); + } + + return std::move( positionInformationList ); +} + void PositioningSource::setElevationCorrectionMode( ElevationCorrectionMode elevationCorrectionMode ) { if ( mElevationCorrectionMode == elevationCorrectionMode ) @@ -265,7 +295,7 @@ void PositioningSource::lastGnssPositionInformationChanged( const GnssPositionIn lastGnssPositionInformation.vdop(), lastGnssPositionInformation.hacc(), lastGnssPositionInformation.vacc(), - lastGnssPositionInformation.utcDateTime(), + lastGnssPositionInformation.utcDateTime().isValid() ? lastGnssPositionInformation.utcDateTime() : QDateTime::currentDateTimeUtc(), lastGnssPositionInformation.fixMode(), lastGnssPositionInformation.fixType(), lastGnssPositionInformation.quality(), @@ -298,6 +328,14 @@ void PositioningSource::lastGnssPositionInformationChanged( const GnssPositionIn emit averagedPositionCountChanged(); } } + else + { + QFile file( QStringLiteral( "%1.information" ).arg( backgroundFilePath ) ); + file.open( QFile::Append ); + QDataStream stream( &file ); + stream << mPositionInformation; + file.close(); + } } void PositioningSource::processCompassReading() diff --git a/src/core/positioning/positioningsource.h b/src/core/positioning/positioningsource.h index dc22ba353c..2ee898de6e 100644 --- a/src/core/positioning/positioningsource.h +++ b/src/core/positioning/positioningsource.h @@ -197,15 +197,26 @@ class PositioningSource : public QObject void setLogging( bool logging ); /** - * Returns TRUE if the background mode is active. + * Returns TRUE if the background mode is active. When activated, position information details + * will not be signaled but instead saved to disk until deactivated. + * \see getBackgroundPositionInformation() */ bool backgroundMode() const { return mBackgroundMode; } /** - * Sets whether the background mode is active. + * Sets whether the background mode is active. When activated, position information details + * will not be signaled but instead saved to disk until deactivated. + * \see getBackgroundPositionInformation() */ void setBackgroundMode( bool backgroundMode ); + /** + * Returns a list of position information collected while background mode is active. + * \see backgroundMode() + * \see setBackgroundMode() + */ + Q_INVOKABLE QList getBackgroundPositionInformation() const; + static QString backgroundFilePath; signals: diff --git a/src/core/projectinfo.cpp b/src/core/projectinfo.cpp index bb3b8cea4a..bf8e42820f 100644 --- a/src/core/projectinfo.cpp +++ b/src/core/projectinfo.cpp @@ -152,24 +152,25 @@ QModelIndex ProjectInfo::restoreTracker( QgsVectorLayer *layer ) return QModelIndex(); QModelIndex index = mTrackingModel->createTracker( layer ); + Tracker *tracker = mTrackingModel->data( index, TrackingModel::TrackerPointer ).value(); mSettings.beginGroup( QStringLiteral( "/qgis/projectInfo/trackers/%1" ).arg( layer->id() ) ); - mTrackingModel->setData( index, mSettings.value( "minimumDistance", 0 ).toDouble(), TrackingModel::MinimumDistance ); - mTrackingModel->setData( index, mSettings.value( "timeInterval", 0 ).toDouble(), TrackingModel::TimeInterval ); - mTrackingModel->setData( index, mSettings.value( "sensorCapture", false ).toBool(), TrackingModel::SensorCapture ); - mTrackingModel->setData( index, mSettings.value( "conjunction", false ).toBool(), TrackingModel::Conjunction ); - mTrackingModel->setData( index, mSettings.value( "maximumDistance", 0 ).toDouble(), TrackingModel::MaximumDistance ); - mTrackingModel->setData( index, static_cast( mSettings.value( "measureType", 0 ).toInt() ), TrackingModel::MeasureType ); - mTrackingModel->setData( index, mSettings.value( "visible", true ).toBool(), TrackingModel::Visible ); + tracker->setTimeInterval( mSettings.value( "timeInterval", 0 ).toInt() ); + tracker->setMinimumDistance( mSettings.value( "minimumDistance", 0 ).toDouble() ); + tracker->setMaximumDistance( mSettings.value( "maximumDistance", 0 ).toDouble() ); + tracker->setSensorCapture( mSettings.value( "sensorCapture", false ).toBool() ); + tracker->setConjunction( mSettings.value( "conjunction", false ).toBool() ); + tracker->setMeasureType( static_cast( mSettings.value( "measureType", 0 ).toInt() ) ); + tracker->setVisible( mSettings.value( "visible", true ).toBool() ); const QgsFeatureId fid = mSettings.value( "featureId", FID_NULL ).toLongLong(); if ( fid >= 0 ) { QgsFeature feature = layer->getFeature( fid ); - mTrackingModel->setData( index, QVariant::fromValue( feature ), TrackingModel::Feature ); + tracker->setFeature( feature ); } else { - mTrackingModel->setData( index, QVariant::fromValue( QgsFeature( layer->fields() ) ), TrackingModel::Feature ); + tracker->setFeature( QgsFeature( layer->fields() ) ); } mSettings.endGroup(); diff --git a/src/core/qgsquick/qgsquickcoordinatetransformer.cpp b/src/core/qgsquick/qgsquickcoordinatetransformer.cpp index 83468c61c5..3060072249 100644 --- a/src/core/qgsquick/qgsquickcoordinatetransformer.cpp +++ b/src/core/qgsquick/qgsquickcoordinatetransformer.cpp @@ -107,11 +107,22 @@ QgsCoordinateTransformContext QgsQuickCoordinateTransformer::transformContext() return mCoordinateTransform.context(); } +QgsPoint QgsQuickCoordinateTransformer::transformPosition( const QgsPoint &position ) const +{ + return processPosition( position ); +} + void QgsQuickCoordinateTransformer::updatePosition() { - double x = mSourcePosition.x(); - double y = mSourcePosition.y(); - double z = mSourcePosition.z(); + mProjectedPosition = processPosition( mSourcePosition ); + emit projectedPositionChanged(); +} + +QgsPoint QgsQuickCoordinateTransformer::processPosition( const QgsPoint &position ) const +{ + double x = position.x(); + double y = position.y(); + double z = position.z(); // If Z is NaN, proj's coordinate transformation will // also set X and Y to NaN. But we also want to get projected @@ -137,13 +148,13 @@ void QgsQuickCoordinateTransformer::updatePosition() } if ( mSkipAltitudeTransformation ) - z = mSourcePosition.z(); + z = position.z(); if ( !mVerticalGridPath.isEmpty() ) { - std::vector xVector = { mSourcePosition.x() }; - std::vector yVector = { mSourcePosition.y() }; - std::vector zVector = { !std::isnan( mSourcePosition.z() ) ? mSourcePosition.z() : 0 }; + std::vector xVector = { position.x() }; + std::vector yVector = { position.y() }; + std::vector zVector = { !std::isnan( position.z() ) ? position.z() : 0 }; try { double zDummy = 0.0; // we don't want to manipulate the elevation data yet, use a dummy z value to transform coordinates first @@ -173,10 +184,10 @@ void QgsQuickCoordinateTransformer::updatePosition() } } - mProjectedPosition = QgsPoint( x, y ); - mProjectedPosition.addZValue( z + mDeltaZ ); + QgsPoint projectedPoint( x, y ); + projectedPoint.addZValue( z + mDeltaZ ); - emit projectedPositionChanged(); + return std::move( projectedPoint ); } bool QgsQuickCoordinateTransformer::skipAltitudeTransformation() const @@ -242,52 +253,54 @@ void QgsQuickCoordinateTransformer::setVerticalGrid( const QString &grid ) mVerticalGrid = grid; mVerticalGridPath.clear(); - if ( mVerticalGrid.isEmpty() ) - return; - - if ( QFile::exists( mVerticalGrid ) ) + if ( !mVerticalGrid.isEmpty() ) { - mVerticalGridPath = mVerticalGrid; - } - else - { - QStringList dataDirs = PlatformUtilities::instance()->appDataDirs(); - if ( !dataDirs.isEmpty() ) + if ( QFile::exists( mVerticalGrid ) ) { - for ( const QString &dataDir : dataDirs ) + mVerticalGridPath = mVerticalGrid; + } + else + { + QStringList dataDirs = PlatformUtilities::instance()->appDataDirs(); + if ( !dataDirs.isEmpty() ) { - const QString verticalGridPath = QStringLiteral( "%1proj/%2" ).arg( dataDir, mVerticalGrid ); - if ( QFile::exists( verticalGridPath ) ) + for ( const QString &dataDir : dataDirs ) { - mVerticalGridPath = verticalGridPath; - break; + const QString verticalGridPath = QStringLiteral( "%1proj/%2" ).arg( dataDir, mVerticalGrid ); + if ( QFile::exists( verticalGridPath ) ) + { + mVerticalGridPath = verticalGridPath; + break; + } } } } - } - if ( !mVerticalGridPath.isEmpty() ) - { - GDALDatasetH hDataset; - GDALAllRegister(); - hDataset = GDALOpen( mVerticalGridPath.toUtf8().constData(), GA_ReadOnly ); - if ( hDataset != NULL ) + if ( !mVerticalGridPath.isEmpty() ) { - OGRSpatialReferenceH spatialRef = GDALGetSpatialRef( hDataset ); - char *pszWkt = nullptr; - const QByteArray multiLineOption = QStringLiteral( "MULTILINE=NO" ).toLocal8Bit(); - const QByteArray formatOption = QStringLiteral( "FORMAT=WKT2" ).toLocal8Bit(); - const char *const options[] = { multiLineOption.constData(), formatOption.constData(), nullptr }; - OSRExportToWktEx( spatialRef, &pszWkt, options ); - mCoordinateVerticalGridTransform.setDestinationCrs( QgsCoordinateReferenceSystem::fromWkt( QString( pszWkt ) ) ); - mCoordinateVerticalGridTransform.setContext( mCoordinateTransform.context() ); - CPLFree( pszWkt ); - } - else - { - // Invalid grid file, skip - mVerticalGridPath.clear(); + GDALDatasetH hDataset; + GDALAllRegister(); + hDataset = GDALOpen( mVerticalGridPath.toUtf8().constData(), GA_ReadOnly ); + if ( hDataset != NULL ) + { + OGRSpatialReferenceH spatialRef = GDALGetSpatialRef( hDataset ); + char *pszWkt = nullptr; + const QByteArray multiLineOption = QStringLiteral( "MULTILINE=NO" ).toLocal8Bit(); + const QByteArray formatOption = QStringLiteral( "FORMAT=WKT2" ).toLocal8Bit(); + const char *const options[] = { multiLineOption.constData(), formatOption.constData(), nullptr }; + OSRExportToWktEx( spatialRef, &pszWkt, options ); + mCoordinateVerticalGridTransform.setDestinationCrs( QgsCoordinateReferenceSystem::fromWkt( QString( pszWkt ) ) ); + mCoordinateVerticalGridTransform.setContext( mCoordinateTransform.context() ); + CPLFree( pszWkt ); + } + else + { + // Invalid grid file, skip + mVerticalGridPath.clear(); + } + GDALClose( hDataset ); } - GDALClose( hDataset ); } + + emit verticalGridChanged(); } diff --git a/src/core/qgsquick/qgsquickcoordinatetransformer.h b/src/core/qgsquick/qgsquickcoordinatetransformer.h index 95636a51fc..add01f58a4 100644 --- a/src/core/qgsquick/qgsquickcoordinatetransformer.h +++ b/src/core/qgsquick/qgsquickcoordinatetransformer.h @@ -125,6 +125,8 @@ class QgsQuickCoordinateTransformer : public QObject //!\copydoc QgsQuickCoordinateTransformer::verticalGrid void setVerticalGrid( const QString &grid ); + Q_INVOKABLE QgsPoint transformPosition( const QgsPoint &position ) const; + signals: //!\copydoc QgsQuickCoordinateTransformer::projectedPosition void projectedPositionChanged(); @@ -154,6 +156,7 @@ class QgsQuickCoordinateTransformer : public QObject private: void updatePosition(); + QgsPoint processPosition( const QgsPoint &position ) const; QgsPoint mProjectedPosition; QgsPoint mSourcePosition; diff --git a/src/core/rubberbandmodel.cpp b/src/core/rubberbandmodel.cpp index 80c229ee42..d8a44cddab 100644 --- a/src/core/rubberbandmodel.cpp +++ b/src/core/rubberbandmodel.cpp @@ -319,6 +319,8 @@ void RubberbandModel::removeVertex() void RubberbandModel::reset() { removeVertices( 0, mPointList.size() - 1 ); + mPointList.replace( 0, QgsPoint() ); + mFrozen = false; emit frozenChanged(); } diff --git a/src/core/tracker.cpp b/src/core/tracker.cpp index d6fae6c980..12bb18568f 100644 --- a/src/core/tracker.cpp +++ b/src/core/tracker.cpp @@ -13,32 +13,77 @@ * * ***************************************************************************/ +#include "featuremodel.h" +#include "qgsquickcoordinatetransformer.h" #include "rubberbandmodel.h" #include "tracker.h" -#include #include #include #include #define MAXIMUM_DISTANCE_FAILURES 20 -Tracker::Tracker( QgsVectorLayer *layer ) - : mLayer( layer ) +Tracker::Tracker( QgsVectorLayer *vectorLayer ) + : mVectorLayer( vectorLayer ) { } -RubberbandModel *Tracker::model() const +void Tracker::setVisible( bool visible ) +{ + if ( mVisible == visible ) + return; + + mVisible = visible; + emit visibleChanged(); +} + +void Tracker::setVectorLayer( QgsVectorLayer *vectorLayer ) +{ + if ( mVectorLayer == vectorLayer ) + return; + + mVectorLayer = vectorLayer; + emit vectorLayerChanged(); +} + +RubberbandModel *Tracker::rubberbandModel() const { return mRubberbandModel; } -void Tracker::setModel( RubberbandModel *model ) +void Tracker::setRubberbandModel( RubberbandModel *rubberbandModel ) { - if ( mRubberbandModel == model ) + if ( mRubberbandModel == rubberbandModel ) return; - mRubberbandModel = model; + if ( mRubberbandModel ) + { + disconnect( mRubberbandModel, &RubberbandModel::vertexCountChanged, this, &Tracker::rubberbandModelVertexCountChanged ); + } + + mRubberbandModel = rubberbandModel; + + if ( mRubberbandModel ) + { + connect( mRubberbandModel, &RubberbandModel::vertexCountChanged, this, &Tracker::rubberbandModelVertexCountChanged ); + } + + emit rubberbandModelChanged(); +} + +FeatureModel *Tracker::featureModel() const +{ + return mFeatureModel; +} + +void Tracker::setFeatureModel( FeatureModel *featureModel ) +{ + if ( mFeatureModel == featureModel ) + return; + + mFeatureModel = featureModel; + emit featureModelChanged(); } QgsFeature Tracker::feature() const @@ -52,11 +97,66 @@ void Tracker::setFeature( const QgsFeature &feature ) return; mFeature = feature; + emit featureChanged(); +} + +void Tracker::setTimeInterval( double timeInterval ) +{ + if ( mTimeInterval == timeInterval ) + return; + + mTimeInterval = timeInterval; + emit timeIntervalChanged(); +} + +void Tracker::setMinimumDistance( double minimumDistance ) +{ + if ( mMinimumDistance == minimumDistance ) + return; + + mMinimumDistance = minimumDistance; + emit minimumDistanceChanged(); +} + +void Tracker::setMaximumDistance( double maximumDistance ) +{ + if ( mMaximumDistance == maximumDistance ) + return; + + mMaximumDistance = maximumDistance; + emit maximumDistanceChanged(); +} + +void Tracker::setSensorCapture( bool capture ) +{ + if ( mSensorCapture == capture ) + return; + + mSensorCapture = capture; + emit sensorCaptureChanged(); +} + +void Tracker::setConjunction( bool conjunction ) +{ + if ( mConjunction == conjunction ) + return; + + mConjunction = conjunction; + emit conjunctionChanged(); +} + +void Tracker::setMeasureType( MeasureType type ) +{ + if ( mMeasureType == type ) + return; + + mMeasureType = type; + emit measureTypeChanged(); } void Tracker::trackPosition() { - if ( !model() || std::isnan( model()->currentCoordinate().x() ) || std::isnan( model()->currentCoordinate().y() ) ) + if ( !mRubberbandModel || std::isnan( mRubberbandModel->currentCoordinate().x() ) || std::isnan( mRubberbandModel->currentCoordinate().y() ) ) { return; } @@ -71,8 +171,9 @@ void Tracker::trackPosition() } mSkipPositionReceived = true; - model()->addVertex(); + mRubberbandModel->addVertex(); + mLastVertexPositionTimestamp = mLastDevicePositionTimestamp; mMaximumDistanceFailuresCount = 0; mCurrentDistance = 0.0; mTimeIntervalFulfilled = qgsDoubleNear( mTimeInterval, 0.0 ); @@ -84,7 +185,7 @@ void Tracker::positionReceived() { if ( mSkipPositionReceived ) { - // When calling model()->addVertex(), the signal we listen to for new position received is triggered, skip that one + // When calling mRubberbandModel->addVertex(), the signal we listen to for new position received is triggered, skip that one mSkipPositionReceived = false; return; } @@ -109,27 +210,30 @@ void Tracker::positionReceived() if ( !qgsDoubleNear( mMinimumDistance, 0.0 ) ) { - if ( mCurrentDistance > mMinimumDistance ) + mMinimumDistanceFulfilled = mCurrentDistance >= mMinimumDistance; + + if ( !mConjunction && mMinimumDistanceFulfilled ) { - mMinimumDistanceFulfilled = true; + trackPosition(); + return; } } - else - { - mMinimumDistanceFulfilled = true; - } - if ( ( !mConjunction && mMinimumDistanceFulfilled ) || ( mMinimumDistanceFulfilled && mTimeIntervalFulfilled && mSensorCaptureFulfilled ) ) + if ( !qgsDoubleNear( mTimeInterval, 0.0 ) ) { - trackPosition(); - } -} + mTimeIntervalFulfilled = ( mLastDevicePositionTimestamp.toMSecsSinceEpoch() - mLastVertexPositionTimestamp.toMSecsSinceEpoch() ) >= mTimeInterval * 1000; + qDebug() << mLastDevicePositionTimestamp; + qDebug() << mTimeInterval; + qDebug() << ( mLastDevicePositionTimestamp.toMSecsSinceEpoch() - mLastVertexPositionTimestamp.toMSecsSinceEpoch() ); -void Tracker::timeReceived() -{ - mTimeIntervalFulfilled = true; + if ( !mConjunction && mTimeIntervalFulfilled ) + { + trackPosition(); + return; + } + } - if ( !mConjunction || ( mMinimumDistanceFulfilled && mSensorCaptureFulfilled ) ) + if ( mMinimumDistanceFulfilled && mTimeIntervalFulfilled && mSensorCaptureFulfilled ) { trackPosition(); } @@ -145,51 +249,40 @@ void Tracker::sensorDataReceived() } } -void Tracker::start() +void Tracker::start( const GnssPositionInformation &positionInformation, const QgsPoint &projectedPosition ) { mIsActive = true; emit isActiveChanged(); - if ( mTimeInterval > 0 ) - { - connect( &mTimer, &QTimer::timeout, this, &Tracker::timeReceived ); - mTimer.start( mTimeInterval * 1000 ); - } - else - { - mTimeIntervalFulfilled = true; - } - if ( mMinimumDistance > 0 || ( qgsDoubleNear( mTimeInterval, 0.0 ) && !mSensorCapture ) ) + if ( mMinimumDistance > 0 || mTimeInterval > 0 || !mSensorCapture ) { connect( mRubberbandModel, &RubberbandModel::currentCoordinateChanged, this, &Tracker::positionReceived ); } - else - { - mMinimumDistanceFulfilled = true; - } if ( mSensorCapture ) { connect( QgsProject::instance()->sensorManager(), &QgsSensorManager::sensorDataCaptured, this, &Tracker::sensorDataReceived ); } - else - { - mSensorCaptureFulfilled = true; - } - - //set the start time - setStartPositionTimestamp( QDateTime::currentDateTime() ); if ( mMeasureType == Tracker::SecondsSinceStart ) { - model()->setMeasureValue( 0 ); + mRubberbandModel->setMeasureValue( 0 ); } mSkipPositionReceived = false; mMaximumDistanceFailuresCount = 0; mCurrentDistance = mMaximumDistance; + mTimeIntervalFulfilled = qgsDoubleNear( mTimeInterval, 0.0 ); + mMinimumDistanceFulfilled = qgsDoubleNear( mMinimumDistance, 0.0 ); + mSensorCaptureFulfilled = !mSensorCapture; - //track first position - trackPosition(); + if ( !projectedPosition.isEmpty() ) + { + //set the start time of first position + setStartPositionTimestamp( positionInformation.utcDateTime().isValid() ? positionInformation.utcDateTime() : QDateTime::currentDateTime() ); + + //track first position + processPositionInformation( positionInformation, projectedPosition ); + } } void Tracker::stop() @@ -200,12 +293,7 @@ void Tracker::stop() mIsActive = false; emit isActiveChanged(); - if ( mTimeInterval > 0 ) - { - mTimer.stop(); - disconnect( &mTimer, &QTimer::timeout, this, &Tracker::trackPosition ); - } - if ( mMinimumDistance > 0 || ( qgsDoubleNear( mTimeInterval, 0.0 ) && !mSensorCapture ) ) + if ( mMinimumDistance > 0 || mTimeInterval > 0 || !mSensorCapture ) { disconnect( mRubberbandModel, &RubberbandModel::currentCoordinateChanged, this, &Tracker::positionReceived ); } @@ -214,3 +302,130 @@ void Tracker::stop() disconnect( QgsProject::instance()->sensorManager(), &QgsSensorManager::sensorDataCaptured, this, &Tracker::sensorDataReceived ); } } + +void Tracker::processPositionInformation( const GnssPositionInformation &positionInformation, const QgsPoint &projectedPosition ) +{ + if ( !mIsActive && !mIsReplaying ) + return; + + mLastDevicePositionTimestamp = positionInformation.utcDateTime(); + + switch ( mMeasureType ) + { + case Tracker::SecondsSinceStart: + mRubberbandModel->setMeasureValue( positionInformation.utcDateTime().toSecsSinceEpoch() - mStartPositionTimestamp.toSecsSinceEpoch() ); + break; + case Tracker::Timestamp: + mRubberbandModel->setMeasureValue( positionInformation.utcDateTime().toSecsSinceEpoch() ); + break; + case Tracker::GroundSpeed: + mRubberbandModel->setMeasureValue( positionInformation.speed() ); + break; + case Tracker::Bearing: + mRubberbandModel->setMeasureValue( positionInformation.direction() ); + break; + case Tracker::HorizontalAccuracy: + mRubberbandModel->setMeasureValue( positionInformation.hacc() ); + break; + case Tracker::VerticalAccuracy: + mRubberbandModel->setMeasureValue( positionInformation.vacc() ); + break; + case Tracker::PDOP: + mRubberbandModel->setMeasureValue( positionInformation.pdop() ); + break; + case Tracker::HDOP: + mRubberbandModel->setMeasureValue( positionInformation.hdop() ); + break; + case Tracker::VDOP: + mRubberbandModel->setMeasureValue( positionInformation.vdop() ); + break; + } + + mRubberbandModel->setCurrentCoordinate( projectedPosition ); +} + +void Tracker::replayPositionInformationList( const QList &positionInformationList, QgsQuickCoordinateTransformer *coordinateTransformer ) +{ + qDebug() << "ttt replayPositionInformationList with count " << positionInformationList.size(); + bool wasActive = false; + if ( mIsActive ) + { + wasActive = true; + stop(); + } + + mIsReplaying = true; + emit isReplayingChanged(); + + const Qgis::GeometryType geometryType = mRubberbandModel->geometryType(); + mFeatureModel->setBatchMode( geometryType == Qgis::GeometryType::Point ); + connect( mRubberbandModel, &RubberbandModel::currentCoordinateChanged, this, &Tracker::positionReceived ); + for ( const GnssPositionInformation &positionInformation : positionInformationList ) + { + processPositionInformation( positionInformation, + coordinateTransformer ? coordinateTransformer->transformPosition( QgsPoint( positionInformation.longitude(), positionInformation.latitude(), positionInformation.elevation() ) ) : QgsPoint() ); + } + disconnect( mRubberbandModel, &RubberbandModel::currentCoordinateChanged, this, &Tracker::positionReceived ); + mFeatureModel->setBatchMode( false ); + + const int vertexCount = mRubberbandModel->vertexCount(); + if ( ( geometryType == Qgis::GeometryType::Line && vertexCount > 2 ) || ( geometryType == Qgis::GeometryType::Polygon && vertexCount > 3 ) ) + { + mFeatureModel->applyGeometry(); + if ( mFeature.id() == FID_NULL ) + { + mFeatureModel->create(); + mFeature = mFeatureModel->feature(); + emit featureCreated(); + } + else + { + mFeatureModel->save(); + } + } + + mIsReplaying = false; + emit isReplayingChanged(); + + if ( wasActive ) + { + start(); + } +} + +void Tracker::rubberbandModelVertexCountChanged() +{ + if ( ( !mIsActive && !mIsReplaying ) || mRubberbandModel->vertexCount() == 0 ) + return; + + const Qgis::GeometryType geometryType = mRubberbandModel->geometryType(); + const int vertexCount = mRubberbandModel->vertexCount(); + if ( geometryType == Qgis::GeometryType::Point ) + { + mFeatureModel->applyGeometry(); + mFeatureModel->resetFeatureId(); + mFeatureModel->resetAttributes( true ); + mFeatureModel->create(); + } + else + { + // When replaying, we can optimize things and do this only once + if ( mIsActive ) + { + if ( ( geometryType == Qgis::GeometryType::Line && vertexCount > 2 ) || ( geometryType == Qgis::GeometryType::Polygon && vertexCount > 3 ) ) + { + mFeatureModel->applyGeometry(); + if ( ( geometryType == Qgis::GeometryType::Line && vertexCount == 3 ) || ( geometryType == Qgis::GeometryType::Polygon && vertexCount == 4 ) ) + { + mFeatureModel->create(); + mFeature = mFeatureModel->feature(); + emit featureCreated(); + } + else + { + mFeatureModel->save(); + } + } + } + } +} diff --git a/src/core/tracker.h b/src/core/tracker.h index 17bd89e876..6aa2624e47 100644 --- a/src/core/tracker.h +++ b/src/core/tracker.h @@ -16,11 +16,15 @@ #ifndef TRACKER_H #define TRACKER_H -#include "qgsvectorlayer.h" +#include "gnsspositioninformation.h" #include #include +#include +#include +class FeatureModel; +class QgsQuickCoordinateTransformer; class RubberbandModel; /** @@ -30,6 +34,24 @@ class Tracker : public QObject { Q_OBJECT + Q_PROPERTY( bool isActive READ isActive NOTIFY isActiveChanged ) + Q_PROPERTY( bool isReplaying READ isReplaying NOTIFY isReplayingChanged ) + + Q_PROPERTY( bool visible READ visible WRITE setVisible NOTIFY visibleChanged ) + + Q_PROPERTY( QgsVectorLayer *vectorLayer READ vectorLayer WRITE setVectorLayer NOTIFY vectorLayerChanged ) + Q_PROPERTY( QgsFeature feature READ feature WRITE setFeature NOTIFY featureChanged ) + + Q_PROPERTY( RubberbandModel *rubberbandModel READ rubberbandModel WRITE setRubberbandModel NOTIFY rubberbandModelChanged ) + Q_PROPERTY( FeatureModel *featureModel READ featureModel WRITE setFeatureModel NOTIFY featureModelChanged ) + + Q_PROPERTY( double timeInterval READ timeInterval WRITE setTimeInterval NOTIFY timeIntervalChanged ) + Q_PROPERTY( double minimumDistance READ minimumDistance WRITE setMinimumDistance NOTIFY minimumDistanceChanged ) + Q_PROPERTY( double maximumDistance READ maximumDistance WRITE setMaximumDistance NOTIFY maximumDistanceChanged ) + Q_PROPERTY( bool sensorCapture READ sensorCapture WRITE setSensorCapture NOTIFY sensorCaptureChanged ) + Q_PROPERTY( bool conjunction READ conjunction WRITE setConjunction NOTIFY conjunctionChanged ) + Q_PROPERTY( MeasureType measureType READ measureType WRITE setMeasureType NOTIFY measureTypeChanged ) + Q_PROPERTY( QDateTime startPositionTimestamp READ startPositionTimestamp WRITE setStartPositionTimestamp NOTIFY startPositionTimestampChanged ) public: @@ -47,35 +69,42 @@ class Tracker : public QObject }; Q_ENUM( MeasureType ) - explicit Tracker( QgsVectorLayer *layer ); + explicit Tracker( QgsVectorLayer *vectorLayer ); + + //! Returns the rubber band model used to generate the track geometry + RubberbandModel *rubberbandModel() const; + //! Sets the rubber band model used to generate the track geometry + void setRubberbandModel( RubberbandModel *rubberbandModel ); - RubberbandModel *model() const; - void setModel( RubberbandModel *model ); + //! Returns the feature model used to generate the track attributes + FeatureModel *featureModel() const; + //! Sets the feature model used to generate the track attributes + void setFeatureModel( FeatureModel *featureModel ); //! Returns the minimum time interval constraint between each tracked point double timeInterval() const { return mTimeInterval; } //! Sets the minimum time interval constraint between each tracked point - void setTimeInterval( const double timeInterval ) { mTimeInterval = timeInterval; } + void setTimeInterval( double timeInterval ); //! Returns the minimum distance constraint between each tracked point double minimumDistance() const { return mMinimumDistance; } //! Sets the minimum distance constraint between each tracked point - void setMinimumDistance( const double minimumDistance ) { mMinimumDistance = minimumDistance; }; + void setMinimumDistance( double minimumDistance ); //! Returns the maximum distance tolerated beyond which a position will be considered errenous double maximumDistance() const { return mMaximumDistance; } //! Sets the maximum distance tolerated beyond which a position will be considered errenous - void setMaximumDistance( const double maximumDistance ) { mMaximumDistance = maximumDistance; }; + void setMaximumDistance( double maximumDistance ); //! Returns if TRUE, newly captured sensor data is needed between each tracked point bool sensorCapture() const { return mSensorCapture; } //! Sets whether newly captured sensor data is needed between each tracked point - void setSensorCapture( const bool capture ) { mSensorCapture = capture; } + void setSensorCapture( bool capture ); //! Returns TRUE if all constraints need to be fulfilled between each tracked point bool conjunction() const { return mConjunction; } //! Sets where all constraints need to be fulfilled between each tracked point - void setConjunction( const bool conjunction ) { mConjunction = conjunction; } + void setConjunction( bool conjunction ); //! Returns the timestamp of the first recorded point QDateTime startPositionTimestamp() const { return mStartPositionTimestamp; } @@ -83,9 +112,9 @@ class Tracker : public QObject void setStartPositionTimestamp( const QDateTime &startPositionTimestamp ) { mStartPositionTimestamp = startPositionTimestamp; } //! Returns the current layer - QgsVectorLayer *layer() const { return mLayer.data(); } + QgsVectorLayer *vectorLayer() const { return mVectorLayer.data(); } //! Sets the current layer - void setLayer( QgsVectorLayer *layer ) { mLayer = layer; } + void setVectorLayer( QgsVectorLayer *vectorLayer ); //! Returns the created feature QgsFeature feature() const; @@ -95,36 +124,64 @@ class Tracker : public QObject //! Returns TRUE if the tracker rubberband is visible bool visible() const { return mVisible; } //! Sets whether the tracker rubberband is visible - void setVisible( const bool visible ) { mVisible = visible; } + void setVisible( bool visible ); //! Returns the measure type used with the tracker geometry's M dimension when available MeasureType measureType() const { return mMeasureType; } //! Sets the measure type used with the tracker geometry's M dimension when available - void setMeasureType( MeasureType type ) { mMeasureType = type; } + void setMeasureType( MeasureType type ); //! Returns whether the tracker has been started bool isActive() const { return mIsActive; } - void start(); + //! Returns whether the tracker is replaying positions + bool isReplaying() const { return mIsReplaying; } + + //! Starts tracking + void start( const GnssPositionInformation &positionInformation = GnssPositionInformation(), const QgsPoint &projectedPosition = QgsPoint() ); + //! Stops tracking void stop(); + //! Process the given position information and projected position passed onto the tracker + Q_INVOKABLE void processPositionInformation( const GnssPositionInformation &positionInformation, const QgsPoint &projectedPosition ); + + //! Replays a list of position information taking into account the tracker settings + void replayPositionInformationList( const QList &positionInformationList, QgsQuickCoordinateTransformer *coordinateTransformer = nullptr ); + signals: - void startPositionTimestampChanged(); void isActiveChanged(); + void isReplayingChanged(); + void visibleChanged(); + void vectorLayerChanged(); + void rubberbandModelChanged(); + void featureModelChanged(); + + void timeIntervalChanged(); + void minimumDistanceChanged(); + void maximumDistanceChanged(); + void sensorCaptureChanged(); + void conjunctionChanged(); + void measureTypeChanged(); + + void featureCreated(); + void featureChanged(); + + void startPositionTimestampChanged(); private slots: void positionReceived(); - void timeReceived(); void sensorDataReceived(); + void rubberbandModelVertexCountChanged(); private: void trackPosition(); bool mIsActive = false; + bool mIsReplaying = false; RubberbandModel *mRubberbandModel = nullptr; + FeatureModel *mFeatureModel = nullptr; - QTimer mTimer; double mTimeInterval = 0.0; double mMinimumDistance = 0.0; double mMaximumDistance = 0.0; @@ -137,12 +194,14 @@ class Tracker : public QObject bool mSensorCaptureFulfilled = false; bool mSkipPositionReceived = false; - QPointer mLayer; + QPointer mVectorLayer; QgsFeature mFeature; bool mVisible = true; QDateTime mStartPositionTimestamp; + QDateTime mLastDevicePositionTimestamp; + QDateTime mLastVertexPositionTimestamp; MeasureType mMeasureType = Tracker::SecondsSinceStart; }; diff --git a/src/core/trackingmodel.cpp b/src/core/trackingmodel.cpp index b3636582f2..9aa89422db 100644 --- a/src/core/trackingmodel.cpp +++ b/src/core/trackingmodel.cpp @@ -14,7 +14,6 @@ * * ***************************************************************************/ -#include "rubberbandmodel.h" #include "trackingmodel.h" #include @@ -35,18 +34,7 @@ QHash TrackingModel::roleNames() const QHash roles = QAbstractItemModel::roleNames(); roles[DisplayString] = "displayString"; - roles[VectorLayer] = "vectorLayer"; - roles[TimeInterval] = "timeInterval"; - roles[MinimumDistance] = "minimumDistance"; - roles[MaximumDistance] = "maximumDistance"; - roles[Conjunction] = "conjunction"; - roles[Feature] = "feature"; - roles[RubberModel] = "rubberModel"; - roles[Visible] = "visible"; - roles[StartPositionTimestamp] = "startPositionTimestamp"; - roles[MeasureType] = "measureType"; - roles[SensorCapture] = "sensorCapture"; - roles[IsActive] = "isActive"; + roles[TrackerPointer] = "tracker"; return roles; } @@ -87,31 +75,9 @@ QVariant TrackingModel::data( const QModelIndex &index, int role ) const switch ( role ) { case DisplayString: - return QString( "Tracker on layer %1" ).arg( tracker->layer()->name() ); - case VectorLayer: - return QVariant::fromValue( tracker->layer() ); - case Feature: - return QVariant::fromValue( tracker->feature() ); - case Visible: - return tracker->visible(); - case StartPositionTimestamp: - return tracker->startPositionTimestamp(); - case TimeInterval: - return tracker->timeInterval(); - case MinimumDistance: - return tracker->minimumDistance(); - case Conjunction: - return tracker->conjunction(); - case RubberModel: - return QVariant::fromValue( tracker->model() ); - case MeasureType: - return tracker->measureType(); - case SensorCapture: - return tracker->sensorCapture(); - case MaximumDistance: - return tracker->maximumDistance(); - case IsActive: - return tracker->isActive(); + return QString( "Tracker on layer %1" ).arg( tracker->vectorLayer()->name() ); + case TrackerPointer: + return QVariant::fromValue( tracker ); default: return QVariant(); } @@ -119,44 +85,7 @@ QVariant TrackingModel::data( const QModelIndex &index, int role ) const bool TrackingModel::setData( const QModelIndex &index, const QVariant &value, int role ) { - if ( index.row() < 0 || index.row() >= mTrackers.size() ) - return false; - - Tracker *tracker = mTrackers[index.row()]; - switch ( role ) - { - case Feature: - tracker->setFeature( value.value() ); - break; - case Visible: - tracker->setVisible( value.toBool() ); - break; - case TimeInterval: - tracker->setTimeInterval( value.toDouble() ); - break; - case MinimumDistance: - tracker->setMinimumDistance( value.toDouble() ); - break; - case Conjunction: - tracker->setConjunction( value.toBool() ); - break; - case RubberModel: - tracker->setModel( value.value() ); - break; - case MeasureType: - tracker->setMeasureType( static_cast( value.toInt() ) ); - break; - case SensorCapture: - tracker->setSensorCapture( value.toBool() ); - break; - case MaximumDistance: - tracker->setMaximumDistance( value.toDouble() ); - break; - default: - return false; - } - emit dataChanged( index, index, QVector() << role ); - return true; + return false; } bool TrackingModel::featureInTracking( QgsVectorLayer *layer, const QgsFeatureId featureId ) @@ -210,37 +139,49 @@ QModelIndex TrackingModel::createTracker( QgsVectorLayer *layer ) return index( mTrackers.size() - 1, 0 ); } -void TrackingModel::startTracker( QgsVectorLayer *layer ) +void TrackingModel::startTracker( QgsVectorLayer *layer, const GnssPositionInformation &positionInformation, const QgsPoint &projectedPosition ) { - int listIndex = trackerIterator( layer ) - mTrackers.constBegin(); - mTrackers[listIndex]->start(); - - QModelIndex idx = index( listIndex, 0 ); - emit dataChanged( idx, idx, QVector() << TrackingModel::IsActive ); - emit layerInTrackingChanged( layer, true ); + const int idx = trackerIterator( layer ) - mTrackers.constBegin(); + if ( idx >= 0 ) + { + mTrackers[idx]->start( positionInformation, projectedPosition ); + emit layerInTrackingChanged( layer, true ); + } } void TrackingModel::stopTracker( QgsVectorLayer *layer ) { - int listIndex = trackerIterator( layer ) - mTrackers.constBegin(); - mTrackers[listIndex]->stop(); - - QModelIndex idx = index( listIndex, 0 ); - emit dataChanged( idx, idx, QVector() << TrackingModel::IsActive ); + const int idx = trackerIterator( layer ) - mTrackers.constBegin(); + if ( idx >= 0 ) + { + mTrackers[idx]->stop(); - beginRemoveRows( QModelIndex(), listIndex, listIndex ); - delete mTrackers.takeAt( listIndex ); - endRemoveRows(); + beginRemoveRows( QModelIndex(), idx, idx ); + Tracker *tracker = mTrackers.takeAt( idx ); + endRemoveRows(); + delete tracker; + emit layerInTrackingChanged( layer, false ); + } +} - emit layerInTrackingChanged( layer, false ); +void TrackingModel::replayPositionInformationList( const QList &positionInformationList, QgsQuickCoordinateTransformer *coordinateTransformer ) +{ + for ( int i = 0; i < mTrackers.size(); i++ ) + { + Tracker *tracker = mTrackers[i]; + if ( tracker->isActive() ) + { + tracker->replayPositionInformationList( positionInformationList, coordinateTransformer ); + } + } } void TrackingModel::setTrackerVisibility( QgsVectorLayer *layer, bool visible ) { if ( trackerIterator( layer ) != mTrackers.constEnd() ) { - int listIndex = trackerIterator( layer ) - mTrackers.constBegin(); - setData( index( listIndex, 0, QModelIndex() ), visible, Visible ); + const int idx = trackerIterator( layer ) - mTrackers.constBegin(); + mTrackers[idx]->setVisible( visible ); } } diff --git a/src/core/trackingmodel.h b/src/core/trackingmodel.h index 8f15b198be..59600e1562 100644 --- a/src/core/trackingmodel.h +++ b/src/core/trackingmodel.h @@ -20,6 +20,7 @@ #include +class QgsQuickCoordinateTransformer; class RubberbandModel; class Track; @@ -37,18 +38,7 @@ class TrackingModel : public QAbstractItemModel enum TrackingRoles { DisplayString = Qt::UserRole, - VectorLayer, //! layer in the current tracking session - RubberModel, //! rubberbandmodel used in the current tracking session - TimeInterval, //! minimum time interval constraint between each tracked point - MinimumDistance, //! minimum distance constraint between each tracked point - Conjunction, //! if TRUE, all constraints needs to be fulfilled before tracking a point - Visible, //! if TRUE, the tracking session rubberband is visible - Feature, //! feature in the current tracking session - StartPositionTimestamp, //! timestamp when the current tracking session started - MeasureType, //! measurement type used to set the measure value - SensorCapture, //! if TRUE, newly captured sensor data constraint will be required between each tracked point - MaximumDistance, //! maximum distance tolerated beyond which a position will be considered errenous - IsActive, //! if TRUE, the tracker has been started + TrackerPointer, }; QHash roleNames() const override; @@ -64,7 +54,7 @@ class TrackingModel : public QAbstractItemModel //! Creates a tracking session for the provided vector \a layer. Q_INVOKABLE QModelIndex createTracker( QgsVectorLayer *layer ); //! Starts tracking for the provided vector \a layer provided it has a tracking session created. - Q_INVOKABLE void startTracker( QgsVectorLayer *layer ); + Q_INVOKABLE void startTracker( QgsVectorLayer *layer, const GnssPositionInformation &positionInformation = GnssPositionInformation(), const QgsPoint &projectedPosition = QgsPoint() ); //! Stops the tracking session of the provided vector \a layer. Q_INVOKABLE void stopTracker( QgsVectorLayer *layer ); //! Sets whether the tracking session rubber band is \a visible. @@ -78,6 +68,9 @@ class TrackingModel : public QAbstractItemModel //! Returns the tracker for the vector \a layer if a tracking session is present, otherwise returns NULLPTR. Tracker *trackerForLayer( QgsVectorLayer *layer ); + //! Replays a list of position information for all active trackers + Q_INVOKABLE void replayPositionInformationList( const QList &positionInformationList, QgsQuickCoordinateTransformer *coordinateTransformer = nullptr ); + void reset(); /** @@ -98,7 +91,7 @@ class TrackingModel : public QAbstractItemModel QList mTrackers; QList::const_iterator trackerIterator( QgsVectorLayer *layer ) { - return std::find_if( mTrackers.constBegin(), mTrackers.constEnd(), [layer]( const Tracker *tracker ) { return tracker->layer() == layer; } ); + return std::find_if( mTrackers.constBegin(), mTrackers.constEnd(), [layer]( const Tracker *tracker ) { return tracker->vectorLayer() == layer; } ); } }; diff --git a/src/core/utils/geometryutils.h b/src/core/utils/geometryutils.h index 7b87bf2443..429beb7587 100644 --- a/src/core/utils/geometryutils.h +++ b/src/core/utils/geometryutils.h @@ -100,8 +100,8 @@ class QFIELD_CORE_EXPORT GeometryUtils : public QObject //! Returns an empty (i.e. null) point. static Q_INVOKABLE QgsPoint emptyPoint() { return QgsPoint(); } - //! Creates a point from \a x and \a y. - static Q_INVOKABLE QgsPoint point( double x, double y ) { return QgsPoint( x, y ); } + //! Creates a point from \a x and \a y with optional \a z and \a values + static Q_INVOKABLE QgsPoint point( double x, double y, double z = std::numeric_limits::quiet_NaN(), double m = std::numeric_limits::quiet_NaN() ) { return QgsPoint( x, y, z, m ); } //! Creates a centroid point from a given \a geometry. static Q_INVOKABLE QgsPoint centroid( const QgsGeometry &geometry ); diff --git a/src/qml/Legend.qml b/src/qml/Legend.qml index ee4730b169..ae98aec222 100644 --- a/src/qml/Legend.qml +++ b/src/qml/Legend.qml @@ -248,7 +248,12 @@ ListView { icon.color: Theme.mainTextColor onClicked: { - displayToast(qsTr('This layer is is currently tracking the device position.')); + displayToast(qsTr('This layer is is currently tracking positions.'), 'info', qsTr('Stop'), function () { + if (trackingModel.layerInTracking(VectorLayerPointer)) { + trackingModel.stopTracker(VectorLayerPointer); + displayToast(qsTr('Track on layer %1 stopped').arg(VectorLayerPointer.name)); + } + }); } SequentialAnimation on bgcolor { diff --git a/src/qml/TrackerSettings.qml b/src/qml/TrackerSettings.qml index 620c1990bb..a8e82e0528 100644 --- a/src/qml/TrackerSettings.qml +++ b/src/qml/TrackerSettings.qml @@ -37,7 +37,7 @@ Popup { if (embeddedAttributeFormModel.rowCount() > 0 && !featureModel.suppressFeatureForm()) { embeddedFeatureForm.active = true; } else { - trackingModel.startTracker(tracker.vectorLayer); + trackingModel.startTracker(tracker.vectorLayer, positionSource.positionInformation, positionSource.projectedPosition); displayToast(qsTr('Track on layer %1 started').arg(tracker.vectorLayer.name)); if (featureModel.currentLayer.geometryType === Qgis.GeometryType.Point) { projectInfo.saveTracker(featureModel.currentLayer); @@ -483,7 +483,7 @@ Popup { if (embeddedAttributeFormModel.rowCount() > 0 && !featureModel.suppressFeatureForm()) { embeddedFeatureForm.active = true; } else { - trackingModel.startTracker(tracker.vectorLayer); + trackingModel.startTracker(tracker.vectorLayer, positionSource.positionInformation, positionSource.projectedPosition); displayToast(qsTr('Track on layer %1 started').arg(tracker.vectorLayer.name)); if (featureModel.currentLayer.geometryType === Qgis.GeometryType.Point) { projectInfo.saveTracker(featureModel.currentLayer); @@ -508,7 +508,7 @@ Popup { onClicked: { applySettings(); - trackingModel.startTracker(tracker.vectorLayer); + trackingModel.startTracker(tracker.vectorLayer, positionSource.positionInformation, positionSource.projectedPosition); displayToast(qsTr('Track on layer %1 started').arg(tracker.vectorLayer.name)); projectInfo.saveTracker(featureModel.currentLayer); trackerSettings.close(); @@ -577,7 +577,7 @@ Popup { tracker.feature = featureModel.feature; embeddedFeatureFormPopup.close(); embeddedFeatureForm.active = false; - trackingModel.startTracker(tracker.vectorLayer); + trackingModel.startTracker(tracker.vectorLayer, positionSource.positionInformation, positionSource.projectedPosition); displayToast(qsTr('Track on layer %1 started').arg(tracker.vectorLayer.name)); if (featureModel.currentLayer.geometryType === Qgis.GeometryType.Point) { projectInfo.saveTracker(featureModel.currentLayer); diff --git a/src/qml/TrackingSession.qml b/src/qml/TrackingSession.qml index ee541e8835..2c52f9ed6d 100644 --- a/src/qml/TrackingSession.qml +++ b/src/qml/TrackingSession.qml @@ -11,72 +11,40 @@ import Theme Item { id: trackingSession - property var tracker: model + property var tracker: model.tracker Component.onCompleted: { - tracker.rubberModel = rubberbandModel; + tracker.rubberbandModel = rubberbandModel; + tracker.featureModel = featureModel; } - RubberbandModel { - id: rubberbandModel - frozen: false - vectorLayer: tracker.vectorLayer - currentCoordinate: positionSource.projectedPosition + Connections { + target: positionSource + enabled: tracker.isActive - property int measureType: tracker.measureType - measureValue: { - switch (measureType) { - case Tracker.SecondsSinceStart: - return (positionSource.positionInformation.utcDateTime - tracker.startPositionTimestamp) / 1000; - case Tracker.Timestamp: - return positionSource.positionInformation.utcDateTime.getTime(); - case Tracker.GroundSpeed: - return positionSource.positionInformation.speed; - case Tracker.Bearing: - return positionSource.positionInformation.direction; - case Tracker.HorizontalAccuracy: - return positionSource.positionInformation.hacc; - case Tracker.VerticalAccuracy: - return positionSource.positionInformation.vacc; - case Tracker.PDOP: - return positionSource.positionInformation.pdop; - case Tracker.HDOP: - return positionSource.positionInformation.hdop; - case Tracker.VDOP: - return positionSource.positionInformation.vdop; - } - return 0; + function onPositionInformationChanged() { + featureModel.positionInformation = positionSource.positionInformation; + tracker.processPositionInformation(positionSource.positionInformation, positionSource.projectedPosition); } + } - currentPositionTimestamp: positionSource.positionInformation.utcDateTime - crs: mapCanvas.mapSettings.destinationCrs + Connections { + target: tracker - onVertexCountChanged: { - if (!tracker.isActive || vertexCount == 0) { - return; - } - if (geometryType === Qgis.GeometryType.Point) { - featureModel.applyGeometry(); - featureModel.resetFeatureId(); - featureModel.resetAttributes(true); - featureModel.create(); - } else { - if ((geometryType === Qgis.GeometryType.Line && vertexCount > 2) || (geometryType === Qgis.GeometryType.Polygon && vertexCount > 3)) { - featureModel.applyGeometry(); - if ((geometryType === Qgis.GeometryType.Line && vertexCount == 3) || (geometryType === Qgis.GeometryType.Polygon && vertexCount == 4)) { - // indirect action, no need to check for success and display a toast, the log is enough - featureModel.create(); - tracker.feature = featureModel.feature; - projectInfo.saveTracker(featureModel.currentLayer); - } else { - // indirect action, no need to check for success and display a toast, the log is enough - featureModel.save(); - } - } + function onFeatureCreated() { + if (tracker.isActive) { + projectInfo.saveTracker(featureModel.currentLayer); } } } + RubberbandModel { + id: rubberbandModel + frozen: false + vectorLayer: tracker.vectorLayer + crs: mapCanvas.mapSettings.destinationCrs + } + Rubberband { id: rubberband visible: tracker.visible @@ -95,7 +63,7 @@ Item { feature: tracker.feature onFeatureChanged: { - if (!tracker.isActive) { + if (!tracker.isActive && !tracker.isReplaying) { updateRubberband(); } } @@ -106,7 +74,6 @@ Item { vectorLayer: tracker.vectorLayer } - positionInformation: coordinateLocator.positionInformation positionLocked: true cloudUserInformation: projectInfo.cloudUserInformation } diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 71650207b9..a069f25d71 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -251,7 +251,7 @@ ApplicationWindow { antennaHeight: positioningSettings.antennaHeightActivated ? positioningSettings.antennaHeight : 0 logging: positioningSettings.logging - onProjectedPositionChanged: { + onPositionInformationChanged: { if (active) { bearingTrueNorth = PositioningUtils.bearingTrueNorth(positionSource.projectedPosition, mapCanvas.mapSettings.destinationCrs); if (gnssButton.followActive) { @@ -269,6 +269,18 @@ ApplicationWindow { onDeviceLastErrorChanged: { displayToast(qsTr('Positioning device error: %1').arg(positionSource.deviceLastError), 'error'); } + + onBackgroundModeChanged: { + if (!backgroundMode) { + console.log('qqq onBackgroundModeChanged'); + mapCanvasMap.freeze('trackerreplay'); + let list = positionSource.getBackgroundPositionInformation(); + // Qt bug weirdly returns an empty list on first invokation to source, call twice to insure we've got the actual list + list = positionSource.getBackgroundPositionInformation(); + trackingModel.replayPositionInformationList(list, coordinateTransformer); + mapCanvasMap.unfreeze('trackerreplay'); + } + } } PositioningSettings { @@ -1833,16 +1845,6 @@ ApplicationWindow { anchors.right: parent.right - onIconSourceChanged: { - if (state === "On") { - if (positionSource.positionInformation && positionSource.positionInformation.latitudeValid) { - displayToast(qsTr("Received position")); - } else { - displayToast(qsTr("Searching for position")); - } - } - } - /* / When set to true, the map will follow the device's current position; the map / will stop following the position whe the user manually drag the map. diff --git a/src/service/qfieldpositioningservice.cpp b/src/service/qfieldpositioningservice.cpp index 297fb03361..88f9154184 100644 --- a/src/service/qfieldpositioningservice.cpp +++ b/src/service/qfieldpositioningservice.cpp @@ -28,8 +28,10 @@ QFieldPositioningService::QFieldPositioningService( int &argc, char **argv ) : QAndroidService( argc, argv ) { + qRegisterMetaType( "GnssPositionInformation" ); + mPositioningSource = new PositioningSource( this ); - mHost.setHostUrl( QUrl( QStringLiteral( "localabstract:replica" ) ) ); + mHost.setHostUrl( QUrl( QStringLiteral( "localabstract:" APP_PACKAGE_NAME "replica" ) ) ); mHost.enableRemoting( mPositioningSource, "PositioningSource" ); mNotificationTimer.setInterval( 1000 ); @@ -44,15 +46,33 @@ QFieldPositioningService::QFieldPositioningService( int &argc, char **argv ) } ); connect( mPositioningSource, &PositioningSource::backgroundModeChanged, this, [=] { - if ( mPositioningSource->backgroundMode() && mPositioningSource->active() ) + if ( mPositioningSource->active() ) + { + if ( mPositioningSource->backgroundMode() ) + { + triggerShowNotification(); + mNotificationTimer.start(); + } + else + { + mNotificationTimer.stop(); + triggerReturnNotification(); + } + } + } ); + + connect( mPositioningSource, &PositioningSource::activeChanged, this, [=] { + if ( mPositioningSource->active() ) { - triggerShowNotification(); - mNotificationTimer.start(); + if ( mPositioningSource->backgroundMode() ) + { + triggerShowNotification(); + mNotificationTimer.start(); + } } else { mNotificationTimer.stop(); - triggerCloseNotification(); } } ); } @@ -60,18 +80,20 @@ QFieldPositioningService::QFieldPositioningService( int &argc, char **argv ) void QFieldPositioningService::triggerShowNotification() { const GnssPositionInformation pos = mPositioningSource->positionInformation(); - QJniObject message = QJniObject::fromString( tr( "Latitude %1 | Longitude %2 | Altitude %3 | Orientation %4" ).arg( QLocale::system().toString( pos.latitude(), 'f', 7 ), QLocale::system().toString( pos.longitude(), 'f', 7 ), QLocale::system().toString( pos.elevation(), 'f', 3 ), QLocale::system().toString( mPositioningSource->orientation(), 'f', 1 ) ) ); + QJniObject message = QJniObject::fromString( tr( "Latitude %1 | Longitude %2 | Altitude %3 m | Speed %4 m/s | Direction %5°" ).arg( QLocale::system().toString( pos.latitude(), 'f', 7 ), QLocale::system().toString( pos.longitude(), 'f', 7 ), QLocale::system().toString( pos.elevation(), 'f', 3 ), QLocale::system().toString( pos.speed(), 'f', 1 ), QLocale::system().toString( pos.direction(), 'f', 1 ) ) ); QJniObject::callStaticMethod( "ch/opengis/" APP_PACKAGE_NAME "/QFieldPositioningService", "triggerShowNotification", - message.object() ); + message.object(), + true ); } -void QFieldPositioningService::triggerCloseNotification() +void QFieldPositioningService::triggerReturnNotification() { QJniObject message = QJniObject::fromString( tr( "Positioning service running" ) ); QJniObject::callStaticMethod( "ch/opengis/" APP_PACKAGE_NAME "/QFieldPositioningService", "triggerShowNotification", - message.object() ); + message.object(), + false ); } QFieldPositioningService::~QFieldPositioningService() diff --git a/src/service/qfieldpositioningservice.h b/src/service/qfieldpositioningservice.h index d0fd83cc9f..b863a3b150 100644 --- a/src/service/qfieldpositioningservice.h +++ b/src/service/qfieldpositioningservice.h @@ -36,7 +36,7 @@ class QFIELD_SERVICE_EXPORT QFieldPositioningService : public QAndroidService private slots: void triggerShowNotification(); - void triggerCloseNotification(); + void triggerReturnNotification(); private: PositioningSource *mPositioningSource = nullptr;