Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ set(MM_SRCS
androidutils.cpp
mapsketchingcontroller.cpp
appsettings.cpp
drafts/featuredraftstorage.cpp
drafts/featuredraftcontroller.cpp
autosynccontroller.cpp
bluetoothdiscoverymodel.cpp
qrcodedecoder.cpp
Expand Down Expand Up @@ -147,6 +149,8 @@ set(MM_HDRS
androidutils.h
mapsketchingcontroller.h
appsettings.h
drafts/featuredraftstorage.h
drafts/featuredraftcontroller.h
autosynccontroller.h
bluetoothdiscoverymodel.h
qrcodedecoder.h
Expand Down Expand Up @@ -335,6 +339,7 @@ target_include_directories(
MerginMaps
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/
${CMAKE_CURRENT_SOURCE_DIR}/attributes
${CMAKE_CURRENT_SOURCE_DIR}/drafts
${CMAKE_CURRENT_SOURCE_DIR}/filter
${CMAKE_CURRENT_SOURCE_DIR}/map
${CMAKE_CURRENT_SOURCE_DIR}/layer
Expand Down
8 changes: 8 additions & 0 deletions app/activeproject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ ActiveProject::ActiveProject( AppSettings &appSettings

mFilterController = std::make_unique<FilterController>();
connect( this, &ActiveProject::projectReloaded, mFilterController.get(), &FilterController::loadFilterConfig );

mFeatureDraftController = std::make_unique<FeatureDraftController>();
connect( this, &ActiveProject::loadingFinished, mFeatureDraftController.get(), &FeatureDraftController::checkForDraft );
}

ActiveProject::~ActiveProject() = default;
Expand Down Expand Up @@ -683,3 +686,8 @@ FilterController *ActiveProject::filterController() const
{
return mFilterController.get();
}

FeatureDraftController *ActiveProject::featureDraftController() const
{
return mFeatureDraftController.get();
}
10 changes: 10 additions & 0 deletions app/activeproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "merginprojectmetadata.h"
#include "synchronizationoptions.h"
#include "filter/filtercontroller.h"
#include "drafts/featuredraftcontroller.h"

/**
* \brief The ActiveProject class can load a QGIS project and holds its data.
Expand All @@ -36,6 +37,7 @@ class ActiveProject: public QObject
Q_PROPERTY( QgsProject *qgsProject READ qgsProject NOTIFY qgsProjectChanged ) // QgsProject instance of active project, never changes
Q_PROPERTY( AutosyncController *autosyncController READ autosyncController NOTIFY autosyncControllerChanged )
Q_PROPERTY( FilterController *filterController READ filterController NOTIFY filterControllerChanged )
Q_PROPERTY( FeatureDraftController *featureDraftController READ featureDraftController NOTIFY featureDraftControllerChanged )
Q_PROPERTY( InputMapSettings *mapSettings READ mapSettings WRITE setMapSettings NOTIFY mapSettingsChanged )
Q_PROPERTY( QString projectRole READ projectRole WRITE setProjectRole NOTIFY projectRoleChanged )

Expand Down Expand Up @@ -156,6 +158,11 @@ class ActiveProject: public QObject
*/
FilterController *filterController() const;

/**
* Returns featureDraftController, which detects unsaved feature drafts left behind for this project
*/
FeatureDraftController *featureDraftController() const;

signals:
void qgsProjectChanged();
void localProjectChanged( LocalProject project );
Expand Down Expand Up @@ -193,6 +200,8 @@ class ActiveProject: public QObject

void filterControllerChanged( FilterController *controller );

void featureDraftControllerChanged( FeatureDraftController *controller );

public slots:
// Reloads project if current project path matches given path (it's the same project)
bool reloadProject( QString projectDir );
Expand Down Expand Up @@ -235,6 +244,7 @@ class ActiveProject: public QObject
InputMapSettings *mMapSettings = nullptr;
std::unique_ptr<AutosyncController> mAutosyncController;
std::unique_ptr<FilterController> mFilterController;
std::unique_ptr<FeatureDraftController> mFeatureDraftController;

QString mProjectLoadingLog;
QString mProjectRole;
Expand Down
91 changes: 91 additions & 0 deletions app/attributes/attributecontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@

#include <QDebug>
#include <QSet>
#include <QTimer>
#include <QDateTime>
#include <QJsonObject>
#include <QJsonArray>

#include "featuredraftstorage.h"

#include "qgis.h"
#include "qgsproject.h"
Expand All @@ -41,7 +47,11 @@
AttributeController::AttributeController( QObject *parent )
: QObject( parent )
, mAttributeTabProxyModel( new AttributeTabProxyModel() )
, mDraftSaveTimer( new QTimer( this ) )
{
mDraftSaveTimer->setSingleShot( true );
mDraftSaveTimer->setInterval( 1000 );
connect( mDraftSaveTimer, &QTimer::timeout, this, &AttributeController::saveDraft );
}

void AttributeController::reset()
Expand All @@ -67,8 +77,17 @@ void AttributeController::setFeatureLayerPair( const FeatureLayerPair &pair )
blockSignals( true );

bool hasLayerChanged = mFeatureLayerPair.layer() != pair.layer();
// geometry edits round-trip back into this same setter (via the live QML
// binding once the geometry-editing map tool hands the feature back) - that
// must not wipe attribute changes already tracked for this same feature
bool isSameFeature = !hasLayerChanged && mFeatureLayerPair.feature().id() == pair.feature().id();

// Set new active pair
mFeatureLayerPair = pair;
if ( !isSameFeature )
{
mTouchedFieldIndices.clear();
}
if ( hasLayerChanged )
{
// layer changed!
Expand Down Expand Up @@ -646,6 +665,72 @@ bool AttributeController::isNewFeature() const
return FID_IS_NEW( id ) || FID_IS_NULL( id );
}

QJsonObject AttributeController::attributeToJson( const QgsFields &fields, const QgsFeature &feature, int fieldIndex ) const
{
QJsonObject attribute;
attribute[ QStringLiteral( "name" ) ] = fields.at( fieldIndex ).name();
attribute[ QStringLiteral( "type" ) ] = fields.at( fieldIndex ).typeName();
attribute[ QStringLiteral( "value" ) ] = QJsonValue::fromVariant( feature.attribute( fieldIndex ) );
return attribute;
}

void AttributeController::saveDraft()
{
if ( !mFeatureLayerPair.layer() )
return;

const QgsFeature feature = mFeatureLayerPair.feature();
const QgsFields fields = feature.fields();
const bool featureIsNew = isNewFeature();

QJsonArray attributes;

if ( featureIsNew )
{
for ( int i = 0; i < feature.attributeCount(); ++i )
{
attributes.append( attributeToJson( fields, feature, i ) );
}
}
else
{
for ( int fieldIndex : mTouchedFieldIndices )
{
if ( fieldIndex >= 0 && fieldIndex < feature.attributeCount() )
{
attributes.append( attributeToJson( fields, feature, fieldIndex ) );
}
}
Comment on lines +697 to +703

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we use this variant in all forms and if the new feature doesn't fill out everything let those missing fields use default value

}

QJsonObject draft;
draft[ QStringLiteral( "layerId" ) ] = mFeatureLayerPair.layer()->id();
draft[ QStringLiteral( "stage" ) ] = QStringLiteral( "attributeForm" );
draft[ QStringLiteral( "timestamp" ) ] = QDateTime::currentDateTimeUtc().toString( Qt::ISODate );
draft[ QStringLiteral( "attributes" ) ] = attributes;

if ( featureIsNew )
{
// existing-feature geometry edits are drafted separately, by RecordingMapTool
draft[ QStringLiteral( "geometry" ) ] = feature.geometry().asWkt();
}
else
{
draft[ QStringLiteral( "featureId" ) ] = QJsonValue( static_cast<qint64>( feature.id() ) );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
draft[ QStringLiteral( "featureId" ) ] = QJsonValue( static_cast<qint64>( feature.id() ) );
draft[ QStringLiteral( "featureId" ) ] = QJsonValue( feature.id() );

redundant cast

}

FeatureDraftStorage::saveDraft( QgsProject::instance()->homePath(), draft );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QgsProject::instance()->homePath()

I'm not so sure about this, we should use project id's if possible

}

void AttributeController::clearDraft()
{
// a pending debounced write must not be allowed to resurrect the draft
// after we've just told the storage (and possibly the user) it's gone
mDraftSaveTimer->stop();

FeatureDraftStorage::clearDraft( QgsProject::instance()->homePath() );
}

void AttributeController::acquireId()
{
if ( !mFeatureLayerPair.layer() )
Expand Down Expand Up @@ -1203,6 +1288,7 @@ bool AttributeController::deleteFeature()
{
mFeatureLayerPair = FeatureLayerPair();
emit featureLayerPairChanged();
clearDraft();
emit changesCommited();
}

Expand All @@ -1214,6 +1300,8 @@ bool AttributeController::rollback()
if ( !mFeatureLayerPair.layer() )
return false;

clearDraft();

if ( !mFeatureLayerPair.layer()->isEditable() )
{
return false;
Expand Down Expand Up @@ -1281,6 +1369,7 @@ bool AttributeController::save()

if ( rv )
{
clearDraft();
emit changesCommited();
}
else
Expand Down Expand Up @@ -1509,6 +1598,8 @@ bool AttributeController::setFormValue( const QUuid &id, QVariant value )
{
mFeatureLayerPair.featureRef().setAttribute( item->fieldIndex(), val );
emit formDataChanged( item->id(), { AttributeFormModel::AttributeValue, AttributeFormModel::RawValueIsNull, AttributeFormModel::HasMixedValues } );
mTouchedFieldIndices.insert( item->fieldIndex() );
mDraftSaveTimer->start();
}
recalculateDerivedItems( true, false );
return true;
Expand Down
19 changes: 19 additions & 0 deletions app/attributes/attributecontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
#include <QVariant>
#include <memory>
#include <QMap>
#include <QSet>
#include <QVector>
#include <QUuid>
#include <QJsonObject>

#include "featurelayerpair.h"
#include "attributedata.h"
Expand All @@ -40,6 +42,7 @@
class AttributeFormModel;
class AttributeTabModel;
class QgsVectorLayer;
class QTimer;

/**
* This is implementation of the controller between Attribute*Model
Expand Down Expand Up @@ -185,6 +188,17 @@ class AttributeController : public QObject

bool isNewFeature() const;

// Persists attribute changes as a draft, debounced. New feature: all attributes.
// Existing feature: only touched fields, so an untouched one is never clobbered
// by a concurrent change made elsewhere via sync.
void saveDraft();

//! Removes any persisted draft for the current project
void clearDraft();

//! Builds the {name, type, value} JSON object for one attribute, used by saveDraft()
QJsonObject attributeToJson( const QgsFields &fields, const QgsFeature &feature, int fieldIndex ) const;

/**
* Recalculates visibility & constrains & default values
* Note that reevaluate default values is needed only when an attribnute has changed.
Expand Down Expand Up @@ -246,5 +260,10 @@ class AttributeController : public QObject

AttributeController *mParentController = nullptr; // not owned
QgsRelation mLinkedRelation;

QTimer *mDraftSaveTimer = nullptr; // owned by this, debounces saveDraft()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
QTimer *mDraftSaveTimer = nullptr; // owned by this, debounces saveDraft()
QTimer mDraftSaveTimer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw pointer is unnecessary here


//! Indices of fields the user has actually changed this session - only valid for existing (edit-mode) features
QSet<int> mTouchedFieldIndices;
};
#endif // ATTRIBUTECONTROLLER_H
Loading
Loading