Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background tracking support #5917

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
50d3e04
Implement background positioning collection
nirvn Dec 27, 2024
4e4f0ac
Add an invokable function into the coordinate transformer to allow no…
nirvn Dec 27, 2024
5817668
Fix missing vertical grid changed signal
nirvn Dec 27, 2024
faee98b
Avoid signals when serving projected position, saves a few cycles
nirvn Dec 27, 2024
1aae3ee
Allow for ongoing tracking sessions to save collected positions when …
nirvn Dec 27, 2024
e9f2c36
Support background tracking
nirvn Dec 27, 2024
0886933
Optimize tracker replay for line and polygon geometries
nirvn Dec 27, 2024
affe722
Major improvement in the way we handle tracker properties
nirvn Dec 27, 2024
7d768b8
Fix starting new track takes on the last track's vertex within a sing…
nirvn Dec 27, 2024
95fce9f
Fix cppcheck
nirvn Dec 27, 2024
1627950
Add missing documentation, improve background mode documentation
nirvn Dec 27, 2024
872eb40
Rework tracker time constraint to rely on position information dateti…
nirvn Dec 27, 2024
7c19385
Address const review
nirvn Dec 27, 2024
b200d95
TIL: Truncate doesn't truncate if nothing is written
nirvn Dec 27, 2024
5a1f924
Implement a feature model batch mode to speed up tracking replay
nirvn Dec 28, 2024
c5bf0bf
Freeze map canvas when replaying
nirvn Dec 28, 2024
a61ec90
Add an action in the tracker badge toaster message to stop ongoing tr…
nirvn Dec 28, 2024
5d5827e
Remove toaster that interferes with tracking due to icon name change
nirvn Dec 28, 2024
8a1b3ad
Make it clearer in the code that batch mode is only used for points w…
nirvn Dec 28, 2024
8668a27
Thanks clang-tidy bot
nirvn Dec 28, 2024
72de9a8
When clicking on the notification bar, unsuspend QField activity
nirvn Dec 28, 2024
78c0e03
Add a copy to clipboard action to the positioning notification bar
nirvn Dec 28, 2024
7d3df79
Insure positioning service strings are translatable
nirvn Dec 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 158 additions & 114 deletions src/core/featuremodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> attributesAllowEdit = mAttributesAllowEdit;
QList<QgsFeature> 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<bool> attributesAllowEdit = mAttributesAllowEdit;
QList<QgsFeature> 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()
Expand Down Expand Up @@ -777,114 +787,148 @@ void FeatureModel::removeLayer( QObject *layer )
sRememberings->remove( static_cast<QgsVectorLayer *>( 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<QPair<QgsRelation, QgsFeatureRequest>> revisitRelations;
if ( mProject )
if ( mLayer )
{
// Gather any relationship children which would have relied on an auto-generated field value
const QList<QgsRelation> 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<QPair<QgsRelation, QgsFeatureRequest>> 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<QgsRelation> 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<QgsRelation, QgsFeatureRequest> &revisitRelation : std::as_const( revisitRelations ) )
setFeature( feat );

if ( hasRelations )
{
const QList<QgsRelation::FieldPair> 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<QgsRelation, QgsFeatureRequest> &revisitRelation : std::as_const( revisitRelations ) )
{
for ( const QgsRelation::FieldPair fieldPair : fieldPairs )
const QList<QgsRelation::FieldPair> 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 )
nirvn marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}

Expand Down
15 changes: 15 additions & 0 deletions src/core/featuremodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 );
Expand All @@ -297,6 +310,7 @@ class FeatureModel : public QAbstractListModel
void positionLockedChanged();
void projectChanged();
void cloudUserInformationChanged();
void batchModeChanged();

void warning( const QString &text );

Expand Down Expand Up @@ -331,6 +345,7 @@ class FeatureModel : public QAbstractListModel
QgsProject *mProject = nullptr;
QString mTempName;
bool mPositionLocked = false;
bool mBatchMode = false;
};

#endif // FEATUREMODEL_H
Loading
Loading