Skip to content

Commit

Permalink
Merge branch 'master' into tracking-background
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasMizera committed Jul 25, 2023
2 parents 4c3caaf + ff44904 commit 0523856
Show file tree
Hide file tree
Showing 36 changed files with 2,123 additions and 773 deletions.
4 changes: 2 additions & 2 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"description": "<p>Mergin Maps Input app is a QGIS based mobile app for Android and iOS devices.</p>",
"license": "GPLv3",
"title": "Mergin Maps Input: QGIS in your pocket",
"version": "2.2.0",
"version": "2.3.0",
"upload_type": "software",
"publication_date": "2022-02-24",
"creators": [
Expand Down Expand Up @@ -43,7 +43,7 @@
"related_identifiers": [
{
"scheme": "url",
"identifier": "https://github.com/MerginMaps/input/tree/2.2.0",
"identifier": "https://github.com/MerginMaps/input/tree/2.3.0",
"relation": "isSupplementTo"
},
{
Expand Down
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cff-version: 2.2.0
cff-version: 2.3.0
message: "If you use this software, please cite it as below."
authors:
- family-names: "Martin"
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.22)

# TODO automatically change with the scripts/update version script
set(MM_VERSION_MAJOR "2")
set(MM_VERSION_MINOR "2")
set(MM_VERSION_MINOR "3")
set(MM_VERSION_PATCH "0")
set(QT_VERSION_DEFAULT "6.4.2")

Expand Down
81 changes: 80 additions & 1 deletion app/attributes/attributecontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ void AttributeController::recalculateDerivedItems( bool isFormValueChange, bool
++i;
}

if ( anyChanges )
if ( isFormValueChange )
emit formRecalculated();
}

Expand Down Expand Up @@ -941,6 +941,8 @@ bool AttributeController::save()
if ( !mFeatureLayerPair.layer() )
return false;

renamePhotos();

if ( !startEditing() )
{
return false;
Expand Down Expand Up @@ -1276,3 +1278,80 @@ void AttributeController::onFeatureAdded( QgsFeatureId newFeatureId )
setFeatureLayerPair( FeatureLayerPair( f, mFeatureLayerPair.layer() ) );
emit featureIdChanged();
}

void AttributeController::renamePhotos()
{
const QStringList photoNameFormat = QgsProject::instance()->entryList( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoNaming/%1" ).arg( mFeatureLayerPair.layer()->id() ) );
if ( photoNameFormat.isEmpty() )
{
return;
}

QgsExpressionContext expressionContext = mFeatureLayerPair.layer()->createExpressionContext();
expressionContext << QgsExpressionContextUtils::formScope( mFeatureLayerPair.feature() );
if ( mVariablesManager )
expressionContext << mVariablesManager->positionScope();

expressionContext.setFields( mFeatureLayerPair.feature().fields() );
expressionContext.setFeature( mFeatureLayerPair.featureRef() );

// check for new photos
QMap<QUuid, std::shared_ptr<FormItem>>::iterator formItemsIterator = mFormItems.begin();
while ( formItemsIterator != mFormItems.end() )
{
std::shared_ptr<FormItem> item = formItemsIterator.value();
if ( item->type() == FormItem::Field && item->editorWidgetType() == QStringLiteral( "ExternalResource" ) )
{
QVariantMap config = item->editorWidgetConfig();
const QgsField field = item->field();
if ( !photoNameFormat.contains( field.name() ) )
{
formItemsIterator++;
continue;
}

if ( item->originalValue() != mFeatureLayerPair.feature().attribute( item->fieldIndex() ) )
{
QString expString = QgsProject::instance()->readEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoNaming/%1/%2" ).arg( mFeatureLayerPair.layer()->id() ).arg( field.name() ) );
QgsExpression exp( expString );
exp.prepare( &expressionContext );
if ( exp.hasParserError() )
{
CoreUtils::log( QStringLiteral( "Photo name format" ), QStringLiteral( "Expression for %1:%2 has parser error: %3" ).arg( mFeatureLayerPair.layer()->name() ).arg( field.name() ).arg( exp.parserErrorString() ) );
formItemsIterator++;
continue;
}

QVariant value = exp.evaluate( &expressionContext );
if ( exp.hasEvalError() )
{
CoreUtils::log( QStringLiteral( "Photo name format" ), QStringLiteral( "Expression for %1:%2 has evaluation error: %3" ).arg( mFeatureLayerPair.layer()->name() ).arg( field.name() ).arg( exp.evalErrorString() ) );
formItemsIterator++;
continue;
}

QVariant val( value );
if ( !field.convertCompatible( val ) )
{
CoreUtils::log( QStringLiteral( "Photo name format" ), QStringLiteral( "Value \"%1\" %4 could not be converted to a compatible value for field %2 (%3)." ).arg( value.toString() ).arg( field.name() ).arg( field.typeName() ).arg( value.isNull() ? "NULL" : "NOT NULL" ) );
formItemsIterator++;
continue;
}

const QString targetDir = InputUtils::resolveTargetDir( QgsProject::instance()->homePath(), config, mFeatureLayerPair, QgsProject::instance() );
const QString prefix = InputUtils::resolvePrefixForRelativePath( config[ QStringLiteral( "RelativeStorage" ) ].toInt(), QgsProject::instance()->homePath(), targetDir );
const QString src = InputUtils::getAbsolutePath( mFeatureLayerPair.feature().attribute( item->fieldIndex() ).toString(), prefix );
QFileInfo fi( src );
QString newName = QStringLiteral( "%1.%2" ).arg( val.toString() ).arg( fi.completeSuffix() );
const QString dst = InputUtils::getAbsolutePath( newName, prefix );
if ( InputUtils::renameFile( src, dst ) )
{
mFeatureLayerPair.featureRef().setAttribute( item->fieldIndex(), newName );
expressionContext.setFeature( featureLayerPair().featureRef() );
}
}
}

++formItemsIterator;
}
}
3 changes: 3 additions & 0 deletions app/attributes/attributecontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ class AttributeController : public QObject
*/
bool allowTabs( QgsAttributeEditorContainer *container );

//! renames photos if necessary
void renamePhotos();

bool mConstraintsHardValid = false;
bool mConstraintsSoftValid = false;
bool mHasValidationErrors = false;
Expand Down
93 changes: 72 additions & 21 deletions app/featuresmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@

#include "qgsproject.h"
#include "qgsexpressioncontextutils.h"
#include "qgsvectorlayerfeatureiterator.h"

#include <QLocale>
#include <QTimer>
#include <QtConcurrent>


FeaturesModel::FeaturesModel( QObject *parent )
: QAbstractListModel( parent ),
mLayer( nullptr )
{
connect( &mSearchResultWatcher, &QFutureWatcher<QgsFeatureList>::finished, this, &FeaturesModel::onFutureFinished );
}

FeaturesModel::~FeaturesModel() = default;
Expand All @@ -25,31 +32,77 @@ void FeaturesModel::populate()
{
if ( mLayer )
{
mFetchingResults = true;
emit fetchingResultsChanged( mFetchingResults );
beginResetModel();
mFeatures.clear();
endResetModel();

QgsFeatureRequest req;
setupFeatureRequest( req );

QgsFeatureIterator it = mLayer->getFeatures( req );
QgsFeature f;
int searchId = mNextSearchId.fetchAndAddOrdered( 1 );
QgsVectorLayerFeatureSource *source = new QgsVectorLayerFeatureSource( mLayer );
mSearchResultWatcher.setFuture( QtConcurrent::run( &FeaturesModel::fetchFeatures, this, source, req, searchId ) );
}
}

QgsFeatureList FeaturesModel::fetchFeatures( QgsVectorLayerFeatureSource *source, QgsFeatureRequest req, int searchId )
{
std::unique_ptr<QgsVectorLayerFeatureSource> fs( source );
QgsFeatureList fl;

// a search might have been queued if no threads were available in the pool, so we also
// check if canceled before we start as the first iteration can potentially be slow
bool canceled = searchId + 1 != mNextSearchId.loadAcquire();
if ( canceled )
{
qDebug() << QString( "Search (%1) was cancelled before it started!" ).arg( searchId );
return fl;
}

QElapsedTimer t;
t.start();
QgsFeatureIterator it = fs->getFeatures( req );
QgsFeature f;

while ( it.nextFeature( f ) )
while ( it.nextFeature( f ) )
{
if ( searchId + 1 != mNextSearchId.loadAcquire() )
{
if ( FID_IS_NEW( f.id() ) || FID_IS_NULL( f.id() ) )
{
continue; // ignore uncommited features
}
canceled = true;
break;
}

mFeatures << FeatureLayerPair( f, mLayer );
if ( FID_IS_NEW( f.id() ) || FID_IS_NULL( f.id() ) )
{
continue; // ignore uncommited features
}

emit layerFeaturesCountChanged( layerFeaturesCount() );
fl.append( f );
}

endResetModel();
qDebug() << QString( "Search (%1) %2 after %3ms, results: %4" ).arg( searchId ).arg( canceled ? "was canceled" : "completed" ).arg( t.elapsed() ).arg( fl.count() );
return fl;
}

void FeaturesModel::onFutureFinished()
{
QFutureWatcher<QgsFeatureList> *watcher = static_cast< QFutureWatcher<QgsFeatureList> *>( sender() );
const QgsFeatureList features = watcher->future().result();
beginResetModel();
mFeatures.clear();
for ( const auto &f : features )
{
mFeatures << FeatureLayerPair( f, mLayer );
}
emit layerFeaturesCountChanged( layerFeaturesCount() );
endResetModel();
mFetchingResults = false;
emit fetchingResultsChanged( mFetchingResults );
}


void FeaturesModel::setup()
{
// define in submodels
Expand Down Expand Up @@ -111,7 +164,7 @@ QString FeaturesModel::searchResultPair( const FeatureLayerPair &pair ) const
return QString();

QgsFields fields = pair.feature().fields();
QStringList words = mSearchExpression.split( ' ', Qt::SkipEmptyParts );
const QStringList words = mSearchExpression.split( ' ', Qt::SkipEmptyParts );
QStringList foundPairs;

for ( const QString &word : words )
Expand Down Expand Up @@ -145,30 +198,29 @@ QString FeaturesModel::buildSearchExpression()
QStringList expressionParts;
QStringList wordExpressions;

QStringList words = mSearchExpression.split( ' ', Qt::SkipEmptyParts );
const QLocale locale;
const QStringList words = mSearchExpression.split( ' ', Qt::SkipEmptyParts );

for ( const QString &word : words )
{
bool searchExpressionIsNumeric;
int filterInt = word.toInt( &searchExpressionIsNumeric );
Q_UNUSED( filterInt ); // we only need to know if expression is numeric, int value is not used
// we only need to know if expression is numeric, return value is not used
locale.toFloat( word, &searchExpressionIsNumeric );


for ( const QgsField &field : fields )
{
if ( field.configurationFlags().testFlag( QgsField::ConfigurationFlag::NotSearchable ) )
if ( field.configurationFlags().testFlag( QgsField::ConfigurationFlag::NotSearchable ) ||
( field.isNumeric() && !searchExpressionIsNumeric ) )
continue;

if ( field.isNumeric() && searchExpressionIsNumeric )
expressionParts << QStringLiteral( "%1 ~ '%2.*'" ).arg( QgsExpression::quotedColumnRef( field.name() ), word );
else if ( field.type() == QVariant::String )
else if ( field.type() == QVariant::String || field.isNumeric() )
expressionParts << QStringLiteral( "%1 ILIKE '%%2%'" ).arg( QgsExpression::quotedColumnRef( field.name() ), word );
}
wordExpressions << QStringLiteral( "(%1)" ).arg( expressionParts.join( QLatin1String( " ) OR ( " ) ) );
expressionParts.clear();
}

QString expression = QStringLiteral( "(%1)" ).arg( wordExpressions.join( QLatin1String( " ) AND ( " ) ) );
const QString expression = QStringLiteral( "(%1)" ).arg( wordExpressions.join( QLatin1String( " ) AND ( " ) ) );

return expression;
}
Expand Down Expand Up @@ -255,7 +307,6 @@ void FeaturesModel::setSearchExpression( const QString &searchExpression )
{
mSearchExpression = searchExpression;
emit searchExpressionChanged( mSearchExpression );

populate();
}
}
Expand Down
22 changes: 22 additions & 0 deletions app/featuresmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@
#define FEATURESMODEL_H

#include <QAbstractListModel>
#include <QFutureWatcher>
#include <QAtomicInt>

#include "qgsvectorlayer.h"
#include "featurelayerpair.h"

#include "inputconfig.h"


class QgsVectorLayerFeatureSource;


/**
* FeaturesModel class fetches features from layer and provides them via Qt Model's interface
*/
Expand All @@ -38,6 +44,9 @@ class FeaturesModel : public QAbstractListModel
// Returns number of features in layer (property). Can be different number than rowCount() due to a searchExpression
Q_PROPERTY( int layerFeaturesCount READ layerFeaturesCount NOTIFY layerFeaturesCountChanged )

// Returns if there is a pending feature request that will populate the model
Q_PROPERTY( bool fetchingResults MEMBER mFetchingResults NOTIFY fetchingResultsChanged )

public:

enum ModelRoles
Expand Down Expand Up @@ -99,6 +108,9 @@ class FeaturesModel : public QAbstractListModel

void layerFeaturesCountChanged( int layerFeaturesCount );

//! \a isFetching is TRUE when still fetching results, FALSE when done fetching
bool fetchingResultsChanged( bool isFetching );

protected:

virtual void setupFeatureRequest( QgsFeatureRequest &request );
Expand All @@ -109,9 +121,15 @@ class FeaturesModel : public QAbstractListModel

virtual QVariant featureTitle( const FeatureLayerPair &featurePair ) const;

private slots:
void onFutureFinished();

private:
QString buildSearchExpression();

//! Performs getFeatures on layer. Takes ownership of \a layer and tries to move it to current thread.
QgsFeatureList fetchFeatures( QgsVectorLayerFeatureSource *layer, QgsFeatureRequest req, int searchId );

//! Returns found attribute and its value from search expression for feature
QString searchResultPair( const FeatureLayerPair &feat ) const;

Expand All @@ -120,6 +138,10 @@ class FeaturesModel : public QAbstractListModel
FeatureLayerPairs mFeatures;
QString mSearchExpression;
QgsVectorLayer *mLayer = nullptr;

QAtomicInt mNextSearchId = 0;
QFutureWatcher<QgsFeatureList> mSearchResultWatcher;
bool mFetchingResults = false;
};

#endif // FEATURESMODEL_H
Binary file modified app/i18n/input_ca.qm
Binary file not shown.
Loading

1 comment on commit 0523856

@inputapp-bot
Copy link
Collaborator

Choose a reason for hiding this comment

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

iOS - version 23.07.441111 just submitted!

Please sign in to comment.