mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-07 00:15:48 -04:00
3951 lines
125 KiB
C++
3951 lines
125 KiB
C++
/***************************************************************************
|
|
qgsproject.cpp - description
|
|
-------------------
|
|
begin : July 23, 2004
|
|
copyright : (C) 2004 by Mark Coletti
|
|
email : mcoletti at gmail.com
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "qgsproject.h"
|
|
|
|
#include "qgsdatasourceuri.h"
|
|
#include "qgslabelingenginesettings.h"
|
|
#include "qgslayertree.h"
|
|
#include "qgslayertreeutils.h"
|
|
#include "qgslayertreeregistrybridge.h"
|
|
#include "qgslogger.h"
|
|
#include "qgsmessagelog.h"
|
|
#include "qgsmaplayerfactory.h"
|
|
#include "qgspluginlayer.h"
|
|
#include "qgspluginlayerregistry.h"
|
|
#include "qgsprojectfiletransform.h"
|
|
#include "qgssnappingconfig.h"
|
|
#include "qgspathresolver.h"
|
|
#include "qgsprojectstorage.h"
|
|
#include "qgsprojectstorageregistry.h"
|
|
#include "qgsprojectversion.h"
|
|
#include "qgsrasterlayer.h"
|
|
#include "qgsreadwritecontext.h"
|
|
#include "qgsrectangle.h"
|
|
#include "qgsrelationmanager.h"
|
|
#include "qgsannotationmanager.h"
|
|
#include "qgsvectorlayerjoininfo.h"
|
|
#include "qgsmapthemecollection.h"
|
|
#include "qgslayerdefinition.h"
|
|
#include "qgsunittypes.h"
|
|
#include "qgstransaction.h"
|
|
#include "qgstransactiongroup.h"
|
|
#include "qgsvectordataprovider.h"
|
|
#include "qgsprojectbadlayerhandler.h"
|
|
#include "qgsmaplayerlistutils.h"
|
|
#include "qgsmeshlayer.h"
|
|
#include "qgslayoutmanager.h"
|
|
#include "qgsbookmarkmanager.h"
|
|
#include "qgsmaplayerstore.h"
|
|
#include "qgsziputils.h"
|
|
#include "qgsauxiliarystorage.h"
|
|
#include "qgssymbollayerutils.h"
|
|
#include "qgsapplication.h"
|
|
#include "qgsexpressioncontextutils.h"
|
|
#include "qgsstyleentityvisitor.h"
|
|
#include "qgsprojectviewsettings.h"
|
|
#include "qgsprojectdisplaysettings.h"
|
|
#include "qgsprojecttimesettings.h"
|
|
#include "qgsvectortilelayer.h"
|
|
#include "qgsruntimeprofiler.h"
|
|
#include "qgsannotationlayer.h"
|
|
#include "qgspointcloudlayer.h"
|
|
#include "qgsattributeeditorcontainer.h"
|
|
#include "qgsgrouplayer.h"
|
|
|
|
|
|
#include <algorithm>
|
|
#include <QApplication>
|
|
#include <QFileInfo>
|
|
#include <QDomNode>
|
|
#include <QObject>
|
|
#include <QTextStream>
|
|
#include <QTemporaryFile>
|
|
#include <QDir>
|
|
#include <QUrl>
|
|
#include <QStandardPaths>
|
|
#include <QUuid>
|
|
#include <QRegularExpression>
|
|
|
|
#ifdef _MSC_VER
|
|
#include <sys/utime.h>
|
|
#else
|
|
#include <utime.h>
|
|
#endif
|
|
|
|
// canonical project instance
|
|
QgsProject *QgsProject::sProject = nullptr;
|
|
|
|
/**
|
|
* Takes the given scope and key and convert them to a string list of key
|
|
* tokens that will be used to navigate through a Property hierarchy
|
|
*
|
|
* E.g., scope "someplugin" and key "/foo/bar/baz" will become a string list
|
|
* of { "properties", "someplugin", "foo", "bar", "baz" }. "properties" is
|
|
* always first because that's the permanent ``root'' Property node.
|
|
*/
|
|
QStringList makeKeyTokens_( const QString &scope, const QString &key )
|
|
{
|
|
QStringList keyTokens = QStringList( scope );
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
|
|
keyTokens += key.split( '/', QString::SkipEmptyParts );
|
|
#else
|
|
keyTokens += key.split( '/', Qt::SkipEmptyParts );
|
|
#endif
|
|
|
|
// be sure to include the canonical root node
|
|
keyTokens.push_front( QStringLiteral( "properties" ) );
|
|
|
|
//check validy of keys since an invalid xml name will will be dropped upon saving the xml file. If not valid, we print a message to the console.
|
|
for ( int i = 0; i < keyTokens.size(); ++i )
|
|
{
|
|
const QString keyToken = keyTokens.at( i );
|
|
|
|
//invalid chars in XML are found at http://www.w3.org/TR/REC-xml/#NT-NameChar
|
|
//note : it seems \x10000-\xEFFFF is valid, but it when added to the regexp, a lot of unwanted characters remain
|
|
const thread_local QRegularExpression sInvalidRegexp = QRegularExpression( "([^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\-\\.0-9\\x{B7}\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}]|^[^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}])" );
|
|
if ( keyToken.contains( sInvalidRegexp ) )
|
|
{
|
|
const QString errorString = QObject::tr( "Entry token invalid : '%1'. The token will not be saved to file." ).arg( keyToken );
|
|
QgsMessageLog::logMessage( errorString, QString(), Qgis::MessageLevel::Critical );
|
|
}
|
|
}
|
|
|
|
return keyTokens;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Returns the property that matches the given key sequence, if any
|
|
*
|
|
* \param scope scope of key
|
|
* \param key keyname
|
|
* \param rootProperty is likely to be the top level QgsProjectPropertyKey in QgsProject:e:Imp.
|
|
*
|
|
* \return null if not found, otherwise located Property
|
|
*/
|
|
QgsProjectProperty *findKey_( const QString &scope,
|
|
const QString &key,
|
|
QgsProjectPropertyKey &rootProperty )
|
|
{
|
|
QgsProjectPropertyKey *currentProperty = &rootProperty;
|
|
QgsProjectProperty *nextProperty; // link to next property down hierarchy
|
|
|
|
QStringList keySequence = makeKeyTokens_( scope, key );
|
|
|
|
while ( !keySequence.isEmpty() )
|
|
{
|
|
// if the current head of the sequence list matches the property name,
|
|
// then traverse down the property hierarchy
|
|
if ( keySequence.first() == currentProperty->name() )
|
|
{
|
|
// remove front key since we're traversing down a level
|
|
keySequence.pop_front();
|
|
|
|
if ( 1 == keySequence.count() )
|
|
{
|
|
// if we have only one key name left, then return the key found
|
|
return currentProperty->find( keySequence.front() );
|
|
}
|
|
else if ( keySequence.isEmpty() )
|
|
{
|
|
// if we're out of keys then the current property is the one we
|
|
// want; i.e., we're in the rate case of being at the top-most
|
|
// property node
|
|
return currentProperty;
|
|
}
|
|
else if ( ( nextProperty = currentProperty->find( keySequence.first() ) ) )
|
|
{
|
|
if ( nextProperty->isKey() )
|
|
{
|
|
currentProperty = static_cast<QgsProjectPropertyKey *>( nextProperty );
|
|
}
|
|
else if ( nextProperty->isValue() && 1 == keySequence.count() )
|
|
{
|
|
// it may be that this may be one of several property value
|
|
// nodes keyed by QDict string; if this is the last remaining
|
|
// key token and the next property is a value node, then
|
|
// that's the situation, so return the currentProperty
|
|
return currentProperty;
|
|
}
|
|
else
|
|
{
|
|
// QgsProjectPropertyValue not Key, so return null
|
|
return nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if the next key down isn't found
|
|
// then the overall key sequence doesn't exist
|
|
return nullptr;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Adds the given key and value.
|
|
*
|
|
* \param scope scope of key
|
|
* \param key key name
|
|
* \param rootProperty is the property from which to start adding
|
|
* \param value the value associated with the key
|
|
* \param propertiesModified the parameter will be set to true if the written entry modifies pre-existing properties
|
|
*/
|
|
QgsProjectProperty *addKey_( const QString &scope,
|
|
const QString &key,
|
|
QgsProjectPropertyKey *rootProperty,
|
|
const QVariant &value,
|
|
bool &propertiesModified )
|
|
{
|
|
QStringList keySequence = makeKeyTokens_( scope, key );
|
|
|
|
// cursor through property key/value hierarchy
|
|
QgsProjectPropertyKey *currentProperty = rootProperty;
|
|
QgsProjectProperty *nextProperty; // link to next property down hierarchy
|
|
QgsProjectPropertyKey *newPropertyKey = nullptr;
|
|
|
|
propertiesModified = false;
|
|
while ( ! keySequence.isEmpty() )
|
|
{
|
|
// if the current head of the sequence list matches the property name,
|
|
// then traverse down the property hierarchy
|
|
if ( keySequence.first() == currentProperty->name() )
|
|
{
|
|
// remove front key since we're traversing down a level
|
|
keySequence.pop_front();
|
|
|
|
// if key sequence has one last element, then we use that as the
|
|
// name to store the value
|
|
if ( 1 == keySequence.count() )
|
|
{
|
|
QgsProjectProperty *property = currentProperty->find( keySequence.front() );
|
|
if ( !property || property->value() != value )
|
|
{
|
|
currentProperty->setValue( keySequence.front(), value );
|
|
propertiesModified = true;
|
|
}
|
|
|
|
return currentProperty;
|
|
}
|
|
// we're at the top element if popping the keySequence element
|
|
// will leave it empty; in that case, just add the key
|
|
else if ( keySequence.isEmpty() )
|
|
{
|
|
if ( currentProperty->value() != value )
|
|
{
|
|
currentProperty->setValue( value );
|
|
propertiesModified = true;
|
|
}
|
|
|
|
return currentProperty;
|
|
}
|
|
else if ( ( nextProperty = currentProperty->find( keySequence.first() ) ) )
|
|
{
|
|
currentProperty = dynamic_cast<QgsProjectPropertyKey *>( nextProperty );
|
|
|
|
if ( currentProperty )
|
|
{
|
|
continue;
|
|
}
|
|
else // QgsProjectPropertyValue not Key, so return null
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
else // the next subkey doesn't exist, so add it
|
|
{
|
|
if ( ( newPropertyKey = currentProperty->addKey( keySequence.first() ) ) )
|
|
{
|
|
currentProperty = newPropertyKey;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
* Removes a given key.
|
|
*
|
|
* \param scope scope of key
|
|
* \param key key name
|
|
* \param rootProperty is the property from which to start adding
|
|
*/
|
|
void removeKey_( const QString &scope,
|
|
const QString &key,
|
|
QgsProjectPropertyKey &rootProperty )
|
|
{
|
|
QgsProjectPropertyKey *currentProperty = &rootProperty;
|
|
|
|
QgsProjectProperty *nextProperty = nullptr; // link to next property down hierarchy
|
|
QgsProjectPropertyKey *previousQgsPropertyKey = nullptr; // link to previous property up hierarchy
|
|
|
|
QStringList keySequence = makeKeyTokens_( scope, key );
|
|
|
|
while ( ! keySequence.isEmpty() )
|
|
{
|
|
// if the current head of the sequence list matches the property name,
|
|
// then traverse down the property hierarchy
|
|
if ( keySequence.first() == currentProperty->name() )
|
|
{
|
|
// remove front key since we're traversing down a level
|
|
keySequence.pop_front();
|
|
|
|
// if we have only one key name left, then try to remove the key
|
|
// with that name
|
|
if ( 1 == keySequence.count() )
|
|
{
|
|
currentProperty->removeKey( keySequence.front() );
|
|
}
|
|
// if we're out of keys then the current property is the one we
|
|
// want to remove, but we can't delete it directly; we need to
|
|
// delete it from the parent property key container
|
|
else if ( keySequence.isEmpty() )
|
|
{
|
|
previousQgsPropertyKey->removeKey( currentProperty->name() );
|
|
}
|
|
else if ( ( nextProperty = currentProperty->find( keySequence.first() ) ) )
|
|
{
|
|
previousQgsPropertyKey = currentProperty;
|
|
currentProperty = dynamic_cast<QgsProjectPropertyKey *>( nextProperty );
|
|
|
|
if ( currentProperty )
|
|
{
|
|
continue;
|
|
}
|
|
else // QgsProjectPropertyValue not Key, so return null
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else // if the next key down isn't found
|
|
{
|
|
// then the overall key sequence doesn't exist
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsProject::QgsProject( QObject *parent )
|
|
: QObject( parent )
|
|
, mLayerStore( new QgsMapLayerStore( this ) )
|
|
, mBadLayerHandler( new QgsProjectBadLayerHandler() )
|
|
, mSnappingConfig( this )
|
|
, mRelationManager( new QgsRelationManager( this ) )
|
|
, mAnnotationManager( new QgsAnnotationManager( this ) )
|
|
, mLayoutManager( new QgsLayoutManager( this ) )
|
|
, mBookmarkManager( QgsBookmarkManager::createProjectBasedManager( this ) )
|
|
, mViewSettings( new QgsProjectViewSettings( this ) )
|
|
, mTimeSettings( new QgsProjectTimeSettings( this ) )
|
|
, mDisplaySettings( new QgsProjectDisplaySettings( this ) )
|
|
, mRootGroup( new QgsLayerTree )
|
|
, mLabelingEngineSettings( new QgsLabelingEngineSettings )
|
|
, mArchive( new QgsArchive() )
|
|
, mAuxiliaryStorage( new QgsAuxiliaryStorage() )
|
|
{
|
|
mProperties.setName( QStringLiteral( "properties" ) );
|
|
|
|
mMainAnnotationLayer = new QgsAnnotationLayer( QObject::tr( "Annotations" ), QgsAnnotationLayer::LayerOptions( mTransformContext ) );
|
|
mMainAnnotationLayer->setParent( this );
|
|
|
|
clear();
|
|
|
|
// bind the layer tree to the map layer registry.
|
|
// whenever layers are added to or removed from the registry,
|
|
// layer tree will be updated
|
|
mLayerTreeRegistryBridge = new QgsLayerTreeRegistryBridge( mRootGroup, this, this );
|
|
connect( this, &QgsProject::layersAdded, this, &QgsProject::onMapLayersAdded );
|
|
connect( this, &QgsProject::layersRemoved, this, [ = ] { cleanTransactionGroups(); } );
|
|
connect( this, qOverload< const QList<QgsMapLayer *> & >( &QgsProject::layersWillBeRemoved ), this, &QgsProject::onMapLayersRemoved );
|
|
|
|
// proxy map layer store signals to this
|
|
connect( mLayerStore.get(), qOverload<const QStringList &>( &QgsMapLayerStore::layersWillBeRemoved ),
|
|
this, [ = ]( const QStringList & layers ) { mProjectScope.reset(); emit layersWillBeRemoved( layers ); } );
|
|
connect( mLayerStore.get(), qOverload< const QList<QgsMapLayer *> & >( &QgsMapLayerStore::layersWillBeRemoved ),
|
|
this, [ = ]( const QList<QgsMapLayer *> &layers ) { mProjectScope.reset(); emit layersWillBeRemoved( layers ); } );
|
|
connect( mLayerStore.get(), qOverload< const QString & >( &QgsMapLayerStore::layerWillBeRemoved ),
|
|
this, [ = ]( const QString & layer ) { mProjectScope.reset(); emit layerWillBeRemoved( layer ); } );
|
|
connect( mLayerStore.get(), qOverload< QgsMapLayer * >( &QgsMapLayerStore::layerWillBeRemoved ),
|
|
this, [ = ]( QgsMapLayer * layer ) { mProjectScope.reset(); emit layerWillBeRemoved( layer ); } );
|
|
connect( mLayerStore.get(), qOverload<const QStringList & >( &QgsMapLayerStore::layersRemoved ), this,
|
|
[ = ]( const QStringList & layers ) { mProjectScope.reset(); emit layersRemoved( layers ); } );
|
|
connect( mLayerStore.get(), &QgsMapLayerStore::layerRemoved, this,
|
|
[ = ]( const QString & layer ) { mProjectScope.reset(); emit layerRemoved( layer ); } );
|
|
connect( mLayerStore.get(), &QgsMapLayerStore::allLayersRemoved, this,
|
|
[ = ]() { mProjectScope.reset(); emit removeAll(); } );
|
|
connect( mLayerStore.get(), &QgsMapLayerStore::layersAdded, this,
|
|
[ = ]( const QList< QgsMapLayer * > &layers ) { mProjectScope.reset(); emit layersAdded( layers ); } );
|
|
connect( mLayerStore.get(), &QgsMapLayerStore::layerWasAdded, this,
|
|
[ = ]( QgsMapLayer * layer ) { mProjectScope.reset(); emit layerWasAdded( layer ); } );
|
|
|
|
if ( QgsApplication::instance() )
|
|
{
|
|
connect( QgsApplication::instance(), &QgsApplication::requestForTranslatableObjects, this, &QgsProject::registerTranslatableObjects );
|
|
}
|
|
|
|
connect( mLayerStore.get(), qOverload< const QList<QgsMapLayer *> & >( &QgsMapLayerStore::layersWillBeRemoved ), this,
|
|
[ = ]( const QList<QgsMapLayer *> &layers )
|
|
{
|
|
for ( const auto &layer : layers )
|
|
{
|
|
disconnect( layer, &QgsMapLayer::dataSourceChanged, mRelationManager, &QgsRelationManager::updateRelationsStatus );
|
|
}
|
|
}
|
|
);
|
|
connect( mLayerStore.get(), qOverload< const QList<QgsMapLayer *> & >( &QgsMapLayerStore::layersAdded ), this,
|
|
[ = ]( const QList<QgsMapLayer *> &layers )
|
|
{
|
|
for ( const auto &layer : layers )
|
|
{
|
|
connect( layer, &QgsMapLayer::dataSourceChanged, mRelationManager, &QgsRelationManager::updateRelationsStatus );
|
|
}
|
|
}
|
|
);
|
|
|
|
Q_NOWARN_DEPRECATED_PUSH
|
|
connect( mViewSettings, &QgsProjectViewSettings::mapScalesChanged, this, &QgsProject::mapScalesChanged );
|
|
Q_NOWARN_DEPRECATED_POP
|
|
}
|
|
|
|
|
|
QgsProject::~QgsProject()
|
|
{
|
|
mIsBeingDeleted = true;
|
|
|
|
clear();
|
|
delete mBadLayerHandler;
|
|
delete mRelationManager;
|
|
delete mLayerTreeRegistryBridge;
|
|
delete mRootGroup;
|
|
if ( this == sProject )
|
|
{
|
|
sProject = nullptr;
|
|
}
|
|
}
|
|
|
|
void QgsProject::setInstance( QgsProject *project )
|
|
{
|
|
sProject = project;
|
|
}
|
|
|
|
|
|
QgsProject *QgsProject::instance()
|
|
{
|
|
if ( !sProject )
|
|
{
|
|
sProject = new QgsProject;
|
|
}
|
|
return sProject;
|
|
}
|
|
|
|
void QgsProject::setTitle( const QString &title )
|
|
{
|
|
if ( title == mMetadata.title() )
|
|
return;
|
|
|
|
mMetadata.setTitle( title );
|
|
mProjectScope.reset();
|
|
emit metadataChanged();
|
|
|
|
setDirty( true );
|
|
}
|
|
|
|
QString QgsProject::title() const
|
|
{
|
|
return mMetadata.title();
|
|
}
|
|
|
|
QString QgsProject::saveUser() const
|
|
{
|
|
return mSaveUser;
|
|
}
|
|
|
|
QString QgsProject::saveUserFullName() const
|
|
{
|
|
return mSaveUserFull;
|
|
}
|
|
|
|
QDateTime QgsProject::lastSaveDateTime() const
|
|
{
|
|
return mSaveDateTime;
|
|
}
|
|
|
|
QgsProjectVersion QgsProject::lastSaveVersion() const
|
|
{
|
|
return mSaveVersion;
|
|
}
|
|
|
|
bool QgsProject::isDirty() const
|
|
{
|
|
return mDirty;
|
|
}
|
|
|
|
void QgsProject::setDirty( const bool dirty )
|
|
{
|
|
if ( dirty && mDirtyBlockCount > 0 )
|
|
return;
|
|
|
|
if ( dirty )
|
|
emit dirtySet();
|
|
|
|
if ( mDirty == dirty )
|
|
return;
|
|
|
|
mDirty = dirty;
|
|
emit isDirtyChanged( mDirty );
|
|
}
|
|
|
|
void QgsProject::setPresetHomePath( const QString &path )
|
|
{
|
|
if ( path == mHomePath )
|
|
return;
|
|
|
|
mHomePath = path;
|
|
mCachedHomePath.clear();
|
|
mProjectScope.reset();
|
|
|
|
emit homePathChanged();
|
|
|
|
setDirty( true );
|
|
}
|
|
|
|
void QgsProject::registerTranslatableContainers( QgsTranslationContext *translationContext, QgsAttributeEditorContainer *parent, const QString &layerId )
|
|
{
|
|
const QList<QgsAttributeEditorElement *> elements = parent->children();
|
|
|
|
for ( QgsAttributeEditorElement *element : elements )
|
|
{
|
|
if ( element->type() == QgsAttributeEditorElement::AeTypeContainer )
|
|
{
|
|
QgsAttributeEditorContainer *container = dynamic_cast<QgsAttributeEditorContainer *>( element );
|
|
|
|
translationContext->registerTranslation( QStringLiteral( "project:layers:%1:formcontainers" ).arg( layerId ), container->name() );
|
|
|
|
if ( !container->children().empty() )
|
|
registerTranslatableContainers( translationContext, container, layerId );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsProject::registerTranslatableObjects( QgsTranslationContext *translationContext )
|
|
{
|
|
//register layers
|
|
const QList<QgsLayerTreeLayer *> layers = mRootGroup->findLayers();
|
|
|
|
for ( const QgsLayerTreeLayer *layer : layers )
|
|
{
|
|
translationContext->registerTranslation( QStringLiteral( "project:layers:%1" ).arg( layer->layerId() ), layer->name() );
|
|
|
|
QgsMapLayer *mapLayer = layer->layer();
|
|
if ( mapLayer && mapLayer->type() == QgsMapLayerType::VectorLayer )
|
|
{
|
|
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( mapLayer );
|
|
|
|
//register aliases and fields
|
|
const QgsFields fields = vlayer->fields();
|
|
for ( const QgsField &field : fields )
|
|
{
|
|
QString fieldName;
|
|
if ( field.alias().isEmpty() )
|
|
fieldName = field.name();
|
|
else
|
|
fieldName = field.alias();
|
|
|
|
translationContext->registerTranslation( QStringLiteral( "project:layers:%1:fieldaliases" ).arg( vlayer->id() ), fieldName );
|
|
|
|
if ( field.editorWidgetSetup().type() == QLatin1String( "ValueRelation" ) )
|
|
{
|
|
translationContext->registerTranslation( QStringLiteral( "project:layers:%1:fields:%2:valuerelationvalue" ).arg( vlayer->id(), field.name() ), field.editorWidgetSetup().config().value( QStringLiteral( "Value" ) ).toString() );
|
|
}
|
|
}
|
|
|
|
//register formcontainers
|
|
registerTranslatableContainers( translationContext, vlayer->editFormConfig().invisibleRootContainer(), vlayer->id() );
|
|
|
|
}
|
|
}
|
|
|
|
//register layergroups
|
|
const QList<QgsLayerTreeGroup *> groupLayers = mRootGroup->findGroups();
|
|
for ( const QgsLayerTreeGroup *groupLayer : groupLayers )
|
|
{
|
|
translationContext->registerTranslation( QStringLiteral( "project:layergroups" ), groupLayer->name() );
|
|
}
|
|
|
|
//register relations
|
|
const QList<QgsRelation> &relations = mRelationManager->relations().values();
|
|
for ( const QgsRelation &relation : relations )
|
|
{
|
|
translationContext->registerTranslation( QStringLiteral( "project:relations" ), relation.name() );
|
|
}
|
|
}
|
|
|
|
void QgsProject::setDataDefinedServerProperties( const QgsPropertyCollection &properties )
|
|
{
|
|
mDataDefinedServerProperties = properties;
|
|
}
|
|
|
|
QgsPropertyCollection QgsProject::dataDefinedServerProperties() const
|
|
{
|
|
return mDataDefinedServerProperties;
|
|
}
|
|
|
|
void QgsProject::setFileName( const QString &name )
|
|
{
|
|
if ( name == mFile.fileName() )
|
|
return;
|
|
|
|
const QString oldHomePath = homePath();
|
|
|
|
mFile.setFileName( name );
|
|
mCachedHomePath.clear();
|
|
mProjectScope.reset();
|
|
|
|
emit fileNameChanged();
|
|
|
|
const QString newHomePath = homePath();
|
|
if ( newHomePath != oldHomePath )
|
|
emit homePathChanged();
|
|
|
|
setDirty( true );
|
|
}
|
|
|
|
QString QgsProject::fileName() const
|
|
{
|
|
return mFile.fileName();
|
|
}
|
|
|
|
void QgsProject::setOriginalPath( const QString &path )
|
|
{
|
|
mOriginalPath = path;
|
|
}
|
|
|
|
QString QgsProject::originalPath() const
|
|
{
|
|
return mOriginalPath;
|
|
}
|
|
|
|
QFileInfo QgsProject::fileInfo() const
|
|
{
|
|
return QFileInfo( mFile );
|
|
}
|
|
|
|
QgsProjectStorage *QgsProject::projectStorage() const
|
|
{
|
|
return QgsApplication::projectStorageRegistry()->projectStorageFromUri( mFile.fileName() );
|
|
}
|
|
|
|
QDateTime QgsProject::lastModified() const
|
|
{
|
|
if ( QgsProjectStorage *storage = projectStorage() )
|
|
{
|
|
QgsProjectStorage::Metadata metadata;
|
|
storage->readProjectStorageMetadata( mFile.fileName(), metadata );
|
|
return metadata.lastModified;
|
|
}
|
|
else
|
|
{
|
|
return QFileInfo( mFile.fileName() ).lastModified();
|
|
}
|
|
}
|
|
|
|
QString QgsProject::absolutePath() const
|
|
{
|
|
if ( projectStorage() )
|
|
return QString();
|
|
|
|
if ( mFile.fileName().isEmpty() )
|
|
return QString(); // this is to protect ourselves from getting current directory from QFileInfo::absoluteFilePath()
|
|
|
|
return QFileInfo( mFile.fileName() ).absolutePath();
|
|
}
|
|
|
|
QString QgsProject::absoluteFilePath() const
|
|
{
|
|
if ( projectStorage() )
|
|
return QString();
|
|
|
|
if ( mFile.fileName().isEmpty() )
|
|
return QString(); // this is to protect ourselves from getting current directory from QFileInfo::absoluteFilePath()
|
|
|
|
return QFileInfo( mFile.fileName() ).absoluteFilePath();
|
|
}
|
|
|
|
QString QgsProject::baseName() const
|
|
{
|
|
if ( QgsProjectStorage *storage = projectStorage() )
|
|
{
|
|
QgsProjectStorage::Metadata metadata;
|
|
storage->readProjectStorageMetadata( mFile.fileName(), metadata );
|
|
return metadata.name;
|
|
}
|
|
else
|
|
{
|
|
return QFileInfo( mFile.fileName() ).completeBaseName();
|
|
}
|
|
}
|
|
|
|
Qgis::FilePathType QgsProject::filePathStorage() const
|
|
{
|
|
const bool absolutePaths = readBoolEntry( QStringLiteral( "Paths" ), QStringLiteral( "/Absolute" ), false );
|
|
return absolutePaths ? Qgis::FilePathType::Absolute : Qgis::FilePathType::Relative;
|
|
}
|
|
|
|
void QgsProject::setFilePathStorage( Qgis::FilePathType type )
|
|
{
|
|
switch ( type )
|
|
{
|
|
case Qgis::FilePathType::Absolute:
|
|
writeEntry( QStringLiteral( "Paths" ), QStringLiteral( "/Absolute" ), true );
|
|
break;
|
|
case Qgis::FilePathType::Relative:
|
|
writeEntry( QStringLiteral( "Paths" ), QStringLiteral( "/Absolute" ), false );
|
|
break;
|
|
}
|
|
}
|
|
|
|
QgsCoordinateReferenceSystem QgsProject::crs() const
|
|
{
|
|
return mCrs;
|
|
}
|
|
|
|
void QgsProject::setCrs( const QgsCoordinateReferenceSystem &crs, bool adjustEllipsoid )
|
|
{
|
|
if ( crs != mCrs )
|
|
{
|
|
mCrs = crs;
|
|
writeEntry( QStringLiteral( "SpatialRefSys" ), QStringLiteral( "/ProjectionsEnabled" ), crs.isValid() ? 1 : 0 );
|
|
mProjectScope.reset();
|
|
|
|
// if annotation layer doesn't have a crs (i.e. in a newly created project), it should
|
|
// initially inherit the project CRS
|
|
if ( !mMainAnnotationLayer->crs().isValid() )
|
|
mMainAnnotationLayer->setCrs( crs );
|
|
|
|
setDirty( true );
|
|
emit crsChanged();
|
|
}
|
|
|
|
if ( adjustEllipsoid )
|
|
setEllipsoid( crs.ellipsoidAcronym() );
|
|
}
|
|
|
|
QString QgsProject::ellipsoid() const
|
|
{
|
|
if ( !crs().isValid() )
|
|
return geoNone();
|
|
|
|
return readEntry( QStringLiteral( "Measure" ), QStringLiteral( "/Ellipsoid" ), geoNone() );
|
|
}
|
|
|
|
void QgsProject::setEllipsoid( const QString &ellipsoid )
|
|
{
|
|
if ( ellipsoid == readEntry( QStringLiteral( "Measure" ), QStringLiteral( "/Ellipsoid" ) ) )
|
|
return;
|
|
|
|
mProjectScope.reset();
|
|
writeEntry( QStringLiteral( "Measure" ), QStringLiteral( "/Ellipsoid" ), ellipsoid );
|
|
emit ellipsoidChanged( ellipsoid );
|
|
}
|
|
|
|
QgsCoordinateTransformContext QgsProject::transformContext() const
|
|
{
|
|
return mTransformContext;
|
|
}
|
|
|
|
void QgsProject::setTransformContext( const QgsCoordinateTransformContext &context )
|
|
{
|
|
if ( context == mTransformContext )
|
|
return;
|
|
|
|
mTransformContext = context;
|
|
mProjectScope.reset();
|
|
|
|
mMainAnnotationLayer->setTransformContext( context );
|
|
for ( auto &layer : mLayerStore.get()->mapLayers() )
|
|
{
|
|
layer->setTransformContext( context );
|
|
}
|
|
emit transformContextChanged();
|
|
}
|
|
|
|
void QgsProject::clear()
|
|
{
|
|
ScopedIntIncrementor snapSingleBlocker( &mBlockSnappingUpdates );
|
|
|
|
mProjectScope.reset();
|
|
mFile.setFileName( QString() );
|
|
mProperties.clearKeys();
|
|
mSaveUser.clear();
|
|
mSaveUserFull.clear();
|
|
mSaveDateTime = QDateTime();
|
|
mSaveVersion = QgsProjectVersion();
|
|
mHomePath.clear();
|
|
mCachedHomePath.clear();
|
|
mAutoTransaction = false;
|
|
mEvaluateDefaultValues = false;
|
|
mDirty = false;
|
|
mTrustLayerMetadata = false;
|
|
mCustomVariables.clear();
|
|
mCrs = QgsCoordinateReferenceSystem();
|
|
mMetadata = QgsProjectMetadata();
|
|
if ( !mSettings.value( QStringLiteral( "projects/anonymize_new_projects" ), false, QgsSettings::Core ).toBool() )
|
|
{
|
|
mMetadata.setCreationDateTime( QDateTime::currentDateTime() );
|
|
mMetadata.setAuthor( QgsApplication::userFullName() );
|
|
}
|
|
emit metadataChanged();
|
|
|
|
QgsCoordinateTransformContext context;
|
|
context.readSettings();
|
|
setTransformContext( context );
|
|
|
|
mEmbeddedLayers.clear();
|
|
mRelationManager->clear();
|
|
mAnnotationManager->clear();
|
|
mLayoutManager->clear();
|
|
mBookmarkManager->clear();
|
|
mViewSettings->reset();
|
|
mTimeSettings->reset();
|
|
mDisplaySettings->reset();
|
|
mSnappingConfig.reset();
|
|
mAvoidIntersectionsMode = AvoidIntersectionsMode::AllowIntersections;
|
|
emit avoidIntersectionsModeChanged();
|
|
emit topologicalEditingChanged();
|
|
|
|
mMapThemeCollection.reset( new QgsMapThemeCollection( this ) );
|
|
emit mapThemeCollectionChanged();
|
|
|
|
mLabelingEngineSettings->clear();
|
|
|
|
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage() );
|
|
mArchive.reset( new QgsArchive() );
|
|
|
|
emit labelingEngineSettingsChanged();
|
|
|
|
if ( !mIsBeingDeleted )
|
|
{
|
|
// possibly other signals should also not be thrown on destruction -- e.g. labelEngineSettingsChanged, etc.
|
|
emit projectColorsChanged();
|
|
}
|
|
|
|
// reset some default project properties
|
|
// XXX THESE SHOULD BE MOVED TO STATUSBAR RELATED SOURCE
|
|
writeEntry( QStringLiteral( "PositionPrecision" ), QStringLiteral( "/Automatic" ), true );
|
|
writeEntry( QStringLiteral( "PositionPrecision" ), QStringLiteral( "/DecimalPlaces" ), 2 );
|
|
|
|
const bool defaultRelativePaths = mSettings.value( QStringLiteral( "/qgis/defaultProjectPathsRelative" ), true ).toBool();
|
|
setFilePathStorage( defaultRelativePaths ? Qgis::FilePathType::Relative : Qgis::FilePathType::Absolute );
|
|
|
|
//copy default units to project
|
|
writeEntry( QStringLiteral( "Measurement" ), QStringLiteral( "/DistanceUnits" ), mSettings.value( QStringLiteral( "/qgis/measure/displayunits" ) ).toString() );
|
|
writeEntry( QStringLiteral( "Measurement" ), QStringLiteral( "/AreaUnits" ), mSettings.value( QStringLiteral( "/qgis/measure/areaunits" ) ).toString() );
|
|
|
|
int red = mSettings.value( QStringLiteral( "qgis/default_canvas_color_red" ), 255 ).toInt();
|
|
int green = mSettings.value( QStringLiteral( "qgis/default_canvas_color_green" ), 255 ).toInt();
|
|
int blue = mSettings.value( QStringLiteral( "qgis/default_canvas_color_blue" ), 255 ).toInt();
|
|
setBackgroundColor( QColor( red, green, blue ) );
|
|
|
|
red = mSettings.value( QStringLiteral( "qgis/default_selection_color_red" ), 255 ).toInt();
|
|
green = mSettings.value( QStringLiteral( "qgis/default_selection_color_green" ), 255 ).toInt();
|
|
blue = mSettings.value( QStringLiteral( "qgis/default_selection_color_blue" ), 0 ).toInt();
|
|
const int alpha = mSettings.value( QStringLiteral( "qgis/default_selection_color_alpha" ), 255 ).toInt();
|
|
setSelectionColor( QColor( red, green, blue, alpha ) );
|
|
|
|
mSnappingConfig.clearIndividualLayerSettings();
|
|
|
|
removeAllMapLayers();
|
|
mRootGroup->clear();
|
|
if ( mMainAnnotationLayer )
|
|
mMainAnnotationLayer->reset();
|
|
|
|
snapSingleBlocker.release();
|
|
|
|
if ( !mBlockSnappingUpdates )
|
|
emit snappingConfigChanged( mSnappingConfig );
|
|
|
|
setDirty( false );
|
|
emit homePathChanged();
|
|
emit cleared();
|
|
}
|
|
|
|
// basically a debugging tool to dump property list values
|
|
void dump_( const QgsProjectPropertyKey &topQgsPropertyKey )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "current properties:" ), 3 );
|
|
topQgsPropertyKey.dump();
|
|
}
|
|
|
|
/**
|
|
* Restores any optional properties found in "doc" to "properties".
|
|
*
|
|
* properties tags for all optional properties. Within that there will be scope
|
|
* tags. In the following example there exist one property in the "fsplugin"
|
|
* scope. "layers" is a list containing three string values.
|
|
*
|
|
* \code{.xml}
|
|
* <properties>
|
|
* <fsplugin>
|
|
* <foo type="int" >42</foo>
|
|
* <baz type="int" >1</baz>
|
|
* <layers type="QStringList" >
|
|
* <value>railroad</value>
|
|
* <value>airport</value>
|
|
* </layers>
|
|
* <xyqzzy type="int" >1</xyqzzy>
|
|
* <bar type="double" >123.456</bar>
|
|
* <feature_types type="QStringList" >
|
|
* <value>type</value>
|
|
* </feature_types>
|
|
* </fsplugin>
|
|
* </properties>
|
|
* \endcode
|
|
*
|
|
* \param doc xml document
|
|
* \param project_properties should be the top QgsProjectPropertyKey node.
|
|
*/
|
|
void _getProperties( const QDomDocument &doc, QgsProjectPropertyKey &project_properties )
|
|
{
|
|
const QDomElement propertiesElem = doc.documentElement().firstChildElement( QStringLiteral( "properties" ) );
|
|
|
|
if ( propertiesElem.isNull() ) // no properties found, so we're done
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QDomNodeList scopes = propertiesElem.childNodes();
|
|
|
|
if ( propertiesElem.firstChild().isNull() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "empty ``properties'' XML tag ... bailing" ) );
|
|
return;
|
|
}
|
|
|
|
if ( ! project_properties.readXml( propertiesElem ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Project_properties.readXml() failed" ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the data defined server properties collection found in "doc" to "dataDefinedServerProperties".
|
|
* \param doc xml document
|
|
* \param dataDefinedServerPropertyDefinitions property collection of the server overrides
|
|
* \since QGIS 3.14
|
|
*/
|
|
QgsPropertyCollection getDataDefinedServerProperties( const QDomDocument &doc, const QgsPropertiesDefinition &dataDefinedServerPropertyDefinitions )
|
|
{
|
|
QgsPropertyCollection ddServerProperties;
|
|
// Read data defined server properties
|
|
const QDomElement ddElem = doc.documentElement().firstChildElement( QStringLiteral( "dataDefinedServerProperties" ) );
|
|
if ( !ddElem.isNull() )
|
|
{
|
|
if ( !ddServerProperties.readXml( ddElem, dataDefinedServerPropertyDefinitions ) )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "dataDefinedServerProperties.readXml() failed" ) );
|
|
}
|
|
}
|
|
return ddServerProperties;
|
|
}
|
|
|
|
/**
|
|
* Get the project title
|
|
* \todo XXX we should go with the attribute xor title, not both.
|
|
*/
|
|
static void _getTitle( const QDomDocument &doc, QString &title )
|
|
{
|
|
const QDomElement titleNode = doc.documentElement().firstChildElement( QStringLiteral( "title" ) );
|
|
|
|
title.clear(); // by default the title will be empty
|
|
|
|
if ( titleNode.isNull() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "unable to find title element" ), 2 );
|
|
return;
|
|
}
|
|
|
|
if ( !titleNode.hasChildNodes() ) // if not, then there's no actual text
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "unable to find title element" ), 2 );
|
|
return;
|
|
}
|
|
|
|
const QDomNode titleTextNode = titleNode.firstChild(); // should only have one child
|
|
|
|
if ( !titleTextNode.isText() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "unable to find title element" ), 2 );
|
|
return;
|
|
}
|
|
|
|
const QDomText titleText = titleTextNode.toText();
|
|
|
|
title = titleText.data();
|
|
|
|
}
|
|
|
|
static void readProjectFileMetadata( const QDomDocument &doc, QString &lastUser, QString &lastUserFull, QDateTime &lastSaveDateTime )
|
|
{
|
|
const QDomNodeList nl = doc.elementsByTagName( QStringLiteral( "qgis" ) );
|
|
|
|
if ( !nl.count() )
|
|
{
|
|
QgsDebugMsg( "unable to find qgis element" );
|
|
return;
|
|
}
|
|
|
|
const QDomNode qgisNode = nl.item( 0 ); // there should only be one, so zeroth element OK
|
|
|
|
const QDomElement qgisElement = qgisNode.toElement(); // qgis node should be element
|
|
lastUser = qgisElement.attribute( QStringLiteral( "saveUser" ), QString() );
|
|
lastUserFull = qgisElement.attribute( QStringLiteral( "saveUserFull" ), QString() );
|
|
lastSaveDateTime = QDateTime::fromString( qgisElement.attribute( QStringLiteral( "saveDateTime" ), QString() ), Qt::ISODate );
|
|
}
|
|
|
|
|
|
QgsProjectVersion getVersion( const QDomDocument &doc )
|
|
{
|
|
const QDomNodeList nl = doc.elementsByTagName( QStringLiteral( "qgis" ) );
|
|
|
|
if ( !nl.count() )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( " unable to find qgis element in project file" ) );
|
|
return QgsProjectVersion( 0, 0, 0, QString() );
|
|
}
|
|
|
|
const QDomNode qgisNode = nl.item( 0 ); // there should only be one, so zeroth element OK
|
|
|
|
const QDomElement qgisElement = qgisNode.toElement(); // qgis node should be element
|
|
QgsProjectVersion projectVersion( qgisElement.attribute( QStringLiteral( "version" ) ) );
|
|
return projectVersion;
|
|
}
|
|
|
|
|
|
QgsSnappingConfig QgsProject::snappingConfig() const
|
|
{
|
|
return mSnappingConfig;
|
|
}
|
|
|
|
void QgsProject::setSnappingConfig( const QgsSnappingConfig &snappingConfig )
|
|
{
|
|
if ( mSnappingConfig == snappingConfig )
|
|
return;
|
|
|
|
mSnappingConfig = snappingConfig;
|
|
setDirty( true );
|
|
emit snappingConfigChanged( mSnappingConfig );
|
|
}
|
|
|
|
void QgsProject::setAvoidIntersectionsMode( const AvoidIntersectionsMode mode )
|
|
{
|
|
if ( mAvoidIntersectionsMode == mode )
|
|
return;
|
|
|
|
mAvoidIntersectionsMode = mode;
|
|
emit avoidIntersectionsModeChanged();
|
|
}
|
|
|
|
bool QgsProject::_getMapLayers( const QDomDocument &doc, QList<QDomNode> &brokenNodes, QgsProject::ReadFlags flags )
|
|
{
|
|
// Layer order is set by the restoring the legend settings from project file.
|
|
// This is done on the 'readProject( ... )' signal
|
|
|
|
QDomElement layerElement = doc.documentElement().firstChildElement( QStringLiteral( "projectlayers" ) ).firstChildElement( QStringLiteral( "maplayer" ) );
|
|
|
|
// process the map layer nodes
|
|
|
|
if ( layerElement.isNull() ) // if we have no layers to process, bail
|
|
{
|
|
return true; // Decided to return "true" since it's
|
|
// possible for there to be a project with no
|
|
// layers; but also, more imporantly, this
|
|
// would cause the tests/qgsproject to fail
|
|
// since the test suite doesn't currently
|
|
// support test layers
|
|
}
|
|
|
|
bool returnStatus = true;
|
|
int numLayers = 0;
|
|
|
|
while ( ! layerElement.isNull() )
|
|
{
|
|
numLayers++;
|
|
layerElement = layerElement.nextSiblingElement( QStringLiteral( "maplayer" ) );
|
|
}
|
|
|
|
// order layers based on their dependencies
|
|
QgsScopedRuntimeProfile profile( tr( "Sorting layers" ), QStringLiteral( "projectload" ) );
|
|
const QgsLayerDefinition::DependencySorter depSorter( doc );
|
|
if ( depSorter.hasCycle() )
|
|
return false;
|
|
|
|
// Missing a dependency? We still load all the layers, otherwise the project is completely broken!
|
|
if ( depSorter.hasMissingDependency() )
|
|
returnStatus = false;
|
|
|
|
emit layerLoaded( 0, numLayers );
|
|
|
|
const QVector<QDomNode> sortedLayerNodes = depSorter.sortedLayerNodes();
|
|
const int totalLayerCount = sortedLayerNodes.count();
|
|
|
|
int i = 0;
|
|
for ( const QDomNode &node : sortedLayerNodes )
|
|
{
|
|
const QDomElement element = node.toElement();
|
|
|
|
const QString name = translate( QStringLiteral( "project:layers:%1" ).arg( node.namedItem( QStringLiteral( "id" ) ).toElement().text() ), node.namedItem( QStringLiteral( "layername" ) ).toElement().text() );
|
|
if ( !name.isNull() )
|
|
emit loadingLayer( tr( "Loading layer %1" ).arg( name ) );
|
|
|
|
profile.switchTask( name );
|
|
|
|
if ( element.attribute( QStringLiteral( "embedded" ) ) == QLatin1String( "1" ) )
|
|
{
|
|
createEmbeddedLayer( element.attribute( QStringLiteral( "id" ) ), readPath( element.attribute( QStringLiteral( "project" ) ) ), brokenNodes, true, flags );
|
|
}
|
|
else
|
|
{
|
|
QgsReadWriteContext context;
|
|
context.setPathResolver( pathResolver() );
|
|
context.setProjectTranslator( this );
|
|
context.setTransformContext( transformContext() );
|
|
|
|
if ( !addLayer( element, brokenNodes, context, flags ) )
|
|
{
|
|
returnStatus = false;
|
|
}
|
|
const auto messages = context.takeMessages();
|
|
if ( !messages.isEmpty() )
|
|
{
|
|
emit loadingLayerMessageReceived( tr( "Loading layer %1" ).arg( name ), messages );
|
|
}
|
|
}
|
|
emit layerLoaded( i + 1, totalLayerCount );
|
|
i++;
|
|
}
|
|
|
|
return returnStatus;
|
|
}
|
|
|
|
bool QgsProject::addLayer( const QDomElement &layerElem, QList<QDomNode> &brokenNodes, QgsReadWriteContext &context, QgsProject::ReadFlags flags )
|
|
{
|
|
const QString type = layerElem.attribute( QStringLiteral( "type" ) );
|
|
QgsDebugMsgLevel( "Layer type is " + type, 4 );
|
|
std::unique_ptr<QgsMapLayer> mapLayer;
|
|
|
|
QgsScopedRuntimeProfile profile( tr( "Create layer" ), QStringLiteral( "projectload" ) );
|
|
|
|
bool ok = false;
|
|
const QgsMapLayerType layerType( QgsMapLayerFactory::typeFromString( type, ok ) );
|
|
if ( !ok )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Unknown layer type \"%1\"" ).arg( type ) );
|
|
return false;
|
|
}
|
|
|
|
switch ( layerType )
|
|
{
|
|
case QgsMapLayerType::VectorLayer:
|
|
{
|
|
mapLayer = std::make_unique<QgsVectorLayer>();
|
|
// apply specific settings to vector layer
|
|
if ( QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mapLayer.get() ) )
|
|
{
|
|
vl->setReadExtentFromXml( mTrustLayerMetadata || ( flags & QgsProject::ReadFlag::FlagTrustLayerMetadata ) );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case QgsMapLayerType::RasterLayer:
|
|
mapLayer = std::make_unique<QgsRasterLayer>();
|
|
break;
|
|
|
|
case QgsMapLayerType::MeshLayer:
|
|
mapLayer = std::make_unique<QgsMeshLayer>();
|
|
break;
|
|
|
|
case QgsMapLayerType::VectorTileLayer:
|
|
mapLayer = std::make_unique<QgsVectorTileLayer>();
|
|
break;
|
|
|
|
case QgsMapLayerType::PointCloudLayer:
|
|
mapLayer = std::make_unique<QgsPointCloudLayer>();
|
|
break;
|
|
|
|
case QgsMapLayerType::PluginLayer:
|
|
{
|
|
const QString typeName = layerElem.attribute( QStringLiteral( "name" ) );
|
|
mapLayer.reset( QgsApplication::pluginLayerRegistry()->createLayer( typeName ) );
|
|
break;
|
|
}
|
|
|
|
case QgsMapLayerType::AnnotationLayer:
|
|
{
|
|
const QgsAnnotationLayer::LayerOptions options( mTransformContext );
|
|
mapLayer = std::make_unique<QgsAnnotationLayer>( QString(), options );
|
|
break;
|
|
}
|
|
|
|
case QgsMapLayerType::GroupLayer:
|
|
{
|
|
const QgsGroupLayer::LayerOptions options( mTransformContext );
|
|
mapLayer = std::make_unique<QgsGroupLayer>( QString(), options );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !mapLayer )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Unable to create layer" ) );
|
|
return false;
|
|
}
|
|
|
|
Q_CHECK_PTR( mapLayer ); // NOLINT
|
|
|
|
// This is tricky: to avoid a leak we need to check if the layer was already in the store
|
|
// because if it was, the newly created layer will not be added to the store and it would leak.
|
|
const QString layerId { layerElem.namedItem( QStringLiteral( "id" ) ).toElement().text() };
|
|
Q_ASSERT( ! layerId.isEmpty() );
|
|
const bool layerWasStored { layerStore()->mapLayer( layerId ) != nullptr };
|
|
|
|
// have the layer restore state that is stored in Dom node
|
|
QgsMapLayer::ReadFlags layerFlags = QgsMapLayer::ReadFlags();
|
|
if ( flags & QgsProject::ReadFlag::FlagDontResolveLayers )
|
|
layerFlags |= QgsMapLayer::FlagDontResolveLayers;
|
|
// Propagate trust layer metadata flag
|
|
if ( mTrustLayerMetadata || ( flags & QgsProject::ReadFlag::FlagTrustLayerMetadata ) )
|
|
layerFlags |= QgsMapLayer::FlagTrustLayerMetadata;
|
|
|
|
profile.switchTask( tr( "Load layer source" ) );
|
|
const bool layerIsValid = mapLayer->readLayerXml( layerElem, context, layerFlags ) && mapLayer->isValid();
|
|
|
|
profile.switchTask( tr( "Add layer to project" ) );
|
|
QList<QgsMapLayer *> newLayers;
|
|
newLayers << mapLayer.get();
|
|
if ( layerIsValid || flags & QgsProject::ReadFlag::FlagDontResolveLayers )
|
|
{
|
|
emit readMapLayer( mapLayer.get(), layerElem );
|
|
addMapLayers( newLayers );
|
|
}
|
|
else
|
|
{
|
|
// It's a bad layer: do not add to legend (the user will decide if she wants to do so)
|
|
addMapLayers( newLayers, false );
|
|
newLayers.first();
|
|
QgsDebugMsg( "Unable to load " + type + " layer" );
|
|
brokenNodes.push_back( layerElem );
|
|
}
|
|
|
|
// It should be safe to delete the layer now if layer was stored, because all the store
|
|
// had to to was to reset the data source in case the validity changed.
|
|
if ( ! layerWasStored )
|
|
{
|
|
mapLayer.release();
|
|
}
|
|
|
|
return layerIsValid;
|
|
}
|
|
|
|
bool QgsProject::read( const QString &filename, QgsProject::ReadFlags flags )
|
|
{
|
|
mFile.setFileName( filename );
|
|
mCachedHomePath.clear();
|
|
mProjectScope.reset();
|
|
|
|
return read( flags );
|
|
}
|
|
|
|
bool QgsProject::read( QgsProject::ReadFlags flags )
|
|
{
|
|
const QString filename = mFile.fileName();
|
|
bool returnValue;
|
|
|
|
if ( QgsProjectStorage *storage = projectStorage() )
|
|
{
|
|
QTemporaryFile inDevice;
|
|
if ( !inDevice.open() )
|
|
{
|
|
setError( tr( "Unable to open %1" ).arg( inDevice.fileName() ) );
|
|
return false;
|
|
}
|
|
|
|
QgsReadWriteContext context;
|
|
context.setProjectTranslator( this );
|
|
if ( !storage->readProject( filename, &inDevice, context ) )
|
|
{
|
|
QString err = tr( "Unable to open %1" ).arg( filename );
|
|
QList<QgsReadWriteContext::ReadWriteMessage> messages = context.takeMessages();
|
|
if ( !messages.isEmpty() )
|
|
err += QStringLiteral( "\n\n" ) + messages.last().message();
|
|
setError( err );
|
|
return false;
|
|
}
|
|
returnValue = unzip( inDevice.fileName(), flags ); // calls setError() if returning false
|
|
}
|
|
else
|
|
{
|
|
if ( QgsZipUtils::isZipFile( mFile.fileName() ) )
|
|
{
|
|
returnValue = unzip( mFile.fileName(), flags );
|
|
}
|
|
else
|
|
{
|
|
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( *this ) );
|
|
const QFileInfo finfo( mFile.fileName() );
|
|
const QString attachmentsZip = finfo.absoluteDir().absoluteFilePath( QStringLiteral( "%1_attachments.zip" ).arg( finfo.completeBaseName() ) );
|
|
if ( QFile( attachmentsZip ).exists() )
|
|
{
|
|
std::unique_ptr<QgsArchive> archive( new QgsArchive() );
|
|
if ( archive->unzip( attachmentsZip ) )
|
|
{
|
|
mArchive = std::move( archive );
|
|
}
|
|
}
|
|
returnValue = readProjectFile( mFile.fileName(), flags );
|
|
}
|
|
|
|
//on translation we should not change the filename back
|
|
if ( !mTranslator )
|
|
{
|
|
mFile.setFileName( filename );
|
|
mCachedHomePath.clear();
|
|
mProjectScope.reset();
|
|
}
|
|
else
|
|
{
|
|
//but delete the translator
|
|
mTranslator.reset( nullptr );
|
|
}
|
|
}
|
|
emit homePathChanged();
|
|
return returnValue;
|
|
}
|
|
|
|
bool QgsProject::readProjectFile( const QString &filename, QgsProject::ReadFlags flags )
|
|
{
|
|
// avoid multiple emission of snapping updated signals
|
|
ScopedIntIncrementor snapSignalBlock( &mBlockSnappingUpdates );
|
|
|
|
QFile projectFile( filename );
|
|
clearError();
|
|
|
|
QgsApplication::profiler()->clear( QStringLiteral( "projectload" ) );
|
|
QgsScopedRuntimeProfile profile( tr( "Setting up translations" ), QStringLiteral( "projectload" ) );
|
|
|
|
const QString localeFileName = QStringLiteral( "%1_%2" ).arg( QFileInfo( projectFile.fileName() ).baseName(), QgsApplication::settingsLocaleUserLocale.value() );
|
|
|
|
if ( QFile( QStringLiteral( "%1/%2.qm" ).arg( QFileInfo( projectFile.fileName() ).absolutePath(), localeFileName ) ).exists() )
|
|
{
|
|
mTranslator.reset( new QTranslator() );
|
|
( void )mTranslator->load( localeFileName, QFileInfo( projectFile.fileName() ).absolutePath() );
|
|
}
|
|
|
|
profile.switchTask( tr( "Reading project file" ) );
|
|
std::unique_ptr<QDomDocument> doc( new QDomDocument( QStringLiteral( "qgis" ) ) );
|
|
|
|
if ( !projectFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
|
|
{
|
|
projectFile.close();
|
|
|
|
setError( tr( "Unable to open %1" ).arg( projectFile.fileName() ) );
|
|
|
|
return false;
|
|
}
|
|
|
|
// location of problem associated with errorMsg
|
|
int line, column;
|
|
QString errorMsg;
|
|
|
|
if ( !doc->setContent( &projectFile, &errorMsg, &line, &column ) )
|
|
{
|
|
const QString errorString = tr( "Project file read error in file %1: %2 at line %3 column %4" )
|
|
.arg( projectFile.fileName(), errorMsg ).arg( line ).arg( column );
|
|
|
|
QgsDebugMsg( errorString );
|
|
|
|
projectFile.close();
|
|
|
|
setError( tr( "%1 for file %2" ).arg( errorString, projectFile.fileName() ) );
|
|
|
|
return false;
|
|
}
|
|
|
|
projectFile.close();
|
|
|
|
QgsDebugMsgLevel( "Opened document " + projectFile.fileName(), 2 );
|
|
|
|
// get project version string, if any
|
|
const QgsProjectVersion fileVersion = getVersion( *doc );
|
|
const QgsProjectVersion thisVersion( Qgis::version() );
|
|
|
|
profile.switchTask( tr( "Updating project file" ) );
|
|
if ( thisVersion > fileVersion )
|
|
{
|
|
QgsLogger::warning( "Loading a file that was saved with an older "
|
|
"version of qgis (saved in " + fileVersion.text() +
|
|
", loaded in " + Qgis::version() +
|
|
"). Problems may occur." );
|
|
|
|
QgsProjectFileTransform projectFile( *doc, fileVersion );
|
|
|
|
// Shows a warning when an old project file is read.
|
|
emit oldProjectVersionWarning( fileVersion.text() );
|
|
|
|
projectFile.updateRevision( thisVersion );
|
|
}
|
|
|
|
// start new project, just keep the file name and auxiliary storage
|
|
profile.switchTask( tr( "Creating auxiliary storage" ) );
|
|
const QString fileName = mFile.fileName();
|
|
std::unique_ptr<QgsAuxiliaryStorage> aStorage = std::move( mAuxiliaryStorage );
|
|
std::unique_ptr<QgsArchive> archive = std::move( mArchive );
|
|
clear();
|
|
mAuxiliaryStorage = std::move( aStorage );
|
|
mArchive = std::move( archive );
|
|
mFile.setFileName( fileName );
|
|
mCachedHomePath.clear();
|
|
mProjectScope.reset();
|
|
mSaveVersion = fileVersion;
|
|
|
|
// now get any properties
|
|
profile.switchTask( tr( "Reading properties" ) );
|
|
_getProperties( *doc, mProperties );
|
|
|
|
// now get the data defined server properties
|
|
mDataDefinedServerProperties = getDataDefinedServerProperties( *doc, dataDefinedServerPropertyDefinitions() );
|
|
|
|
QgsDebugMsgLevel( QString::number( mProperties.count() ) + " properties read", 2 );
|
|
|
|
#if 0
|
|
dump_( mProperties );
|
|
#endif
|
|
|
|
// get older style project title
|
|
QString oldTitle;
|
|
_getTitle( *doc, oldTitle );
|
|
|
|
readProjectFileMetadata( *doc, mSaveUser, mSaveUserFull, mSaveDateTime );
|
|
|
|
const QDomNodeList homePathNl = doc->elementsByTagName( QStringLiteral( "homePath" ) );
|
|
if ( homePathNl.count() > 0 )
|
|
{
|
|
const QDomElement homePathElement = homePathNl.at( 0 ).toElement();
|
|
const QString homePath = homePathElement.attribute( QStringLiteral( "path" ) );
|
|
if ( !homePath.isEmpty() )
|
|
setPresetHomePath( homePath );
|
|
}
|
|
else
|
|
{
|
|
emit homePathChanged();
|
|
}
|
|
|
|
const QColor backgroundColor( readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorRedPart" ), 255 ),
|
|
readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorGreenPart" ), 255 ),
|
|
readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorBluePart" ), 255 ) );
|
|
setBackgroundColor( backgroundColor );
|
|
const QColor selectionColor( readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorRedPart" ), 255 ),
|
|
readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorGreenPart" ), 255 ),
|
|
readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorBluePart" ), 255 ),
|
|
readNumEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorAlphaPart" ), 255 ) );
|
|
setSelectionColor( selectionColor );
|
|
|
|
QgsReadWriteContext context;
|
|
context.setPathResolver( pathResolver() );
|
|
context.setProjectTranslator( this );
|
|
|
|
//crs
|
|
QgsCoordinateReferenceSystem projectCrs;
|
|
if ( readNumEntry( QStringLiteral( "SpatialRefSys" ), QStringLiteral( "/ProjectionsEnabled" ), 0 ) )
|
|
{
|
|
// first preference - dedicated projectCrs node
|
|
const QDomNode srsNode = doc->documentElement().namedItem( QStringLiteral( "projectCrs" ) );
|
|
if ( !srsNode.isNull() )
|
|
{
|
|
projectCrs.readXml( srsNode );
|
|
}
|
|
|
|
if ( !projectCrs.isValid() )
|
|
{
|
|
const QString projCrsString = readEntry( QStringLiteral( "SpatialRefSys" ), QStringLiteral( "/ProjectCRSProj4String" ) );
|
|
const long currentCRS = readNumEntry( QStringLiteral( "SpatialRefSys" ), QStringLiteral( "/ProjectCRSID" ), -1 );
|
|
const QString authid = readEntry( QStringLiteral( "SpatialRefSys" ), QStringLiteral( "/ProjectCrs" ) );
|
|
|
|
// authid should be prioritized over all
|
|
const bool isUserAuthId = authid.startsWith( QLatin1String( "USER:" ), Qt::CaseInsensitive );
|
|
if ( !authid.isEmpty() && !isUserAuthId )
|
|
projectCrs = QgsCoordinateReferenceSystem( authid );
|
|
|
|
// try the CRS
|
|
if ( !projectCrs.isValid() && currentCRS >= 0 )
|
|
{
|
|
projectCrs = QgsCoordinateReferenceSystem::fromSrsId( currentCRS );
|
|
}
|
|
|
|
// if that didn't produce a match, try the proj.4 string
|
|
if ( !projCrsString.isEmpty() && ( authid.isEmpty() || isUserAuthId ) && ( !projectCrs.isValid() || projectCrs.toProj() != projCrsString ) )
|
|
{
|
|
projectCrs = QgsCoordinateReferenceSystem::fromProj( projCrsString );
|
|
}
|
|
|
|
// last just take the given id
|
|
if ( !projectCrs.isValid() )
|
|
{
|
|
projectCrs = QgsCoordinateReferenceSystem::fromSrsId( currentCRS );
|
|
}
|
|
}
|
|
}
|
|
mCrs = projectCrs;
|
|
|
|
QStringList datumErrors;
|
|
if ( !mTransformContext.readXml( doc->documentElement(), context, datumErrors ) && !datumErrors.empty() )
|
|
{
|
|
emit missingDatumTransforms( datumErrors );
|
|
}
|
|
emit transformContextChanged();
|
|
|
|
//add variables defined in project file - do this early in the reading cycle, as other components
|
|
//(e.g. layouts) may depend on these variables
|
|
const QStringList variableNames = readListEntry( QStringLiteral( "Variables" ), QStringLiteral( "/variableNames" ) );
|
|
const QStringList variableValues = readListEntry( QStringLiteral( "Variables" ), QStringLiteral( "/variableValues" ) );
|
|
|
|
mCustomVariables.clear();
|
|
if ( variableNames.length() == variableValues.length() )
|
|
{
|
|
for ( int i = 0; i < variableNames.length(); ++i )
|
|
{
|
|
mCustomVariables.insert( variableNames.at( i ), variableValues.at( i ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Project Variables Invalid" ), tr( "The project contains invalid variable settings." ) );
|
|
}
|
|
|
|
QDomElement element = doc->documentElement().firstChildElement( QStringLiteral( "projectMetadata" ) );
|
|
|
|
if ( !element.isNull() )
|
|
{
|
|
mMetadata.readMetadataXml( element );
|
|
}
|
|
else
|
|
{
|
|
// older project, no metadata => remove auto generated metadata which is populated on QgsProject::clear()
|
|
mMetadata = QgsProjectMetadata();
|
|
}
|
|
if ( mMetadata.title().isEmpty() && !oldTitle.isEmpty() )
|
|
{
|
|
// upgrade older title storage to storing within project metadata.
|
|
mMetadata.setTitle( oldTitle );
|
|
}
|
|
emit metadataChanged();
|
|
|
|
element = doc->documentElement().firstChildElement( QStringLiteral( "autotransaction" ) );
|
|
if ( ! element.isNull() )
|
|
{
|
|
if ( element.attribute( QStringLiteral( "active" ), QStringLiteral( "0" ) ).toInt() == 1 )
|
|
mAutoTransaction = true;
|
|
}
|
|
|
|
element = doc->documentElement().firstChildElement( QStringLiteral( "evaluateDefaultValues" ) );
|
|
if ( !element.isNull() )
|
|
{
|
|
if ( element.attribute( QStringLiteral( "active" ), QStringLiteral( "0" ) ).toInt() == 1 )
|
|
mEvaluateDefaultValues = true;
|
|
}
|
|
|
|
// Read trust layer metadata config in the project
|
|
element = doc->documentElement().firstChildElement( QStringLiteral( "trust" ) );
|
|
if ( !element.isNull() )
|
|
{
|
|
if ( element.attribute( QStringLiteral( "active" ), QStringLiteral( "0" ) ).toInt() == 1 )
|
|
mTrustLayerMetadata = true;
|
|
}
|
|
|
|
// read the layer tree from project file
|
|
profile.switchTask( tr( "Loading layer tree" ) );
|
|
mRootGroup->setCustomProperty( QStringLiteral( "loading" ), 1 );
|
|
|
|
QDomElement layerTreeElem = doc->documentElement().firstChildElement( QStringLiteral( "layer-tree-group" ) );
|
|
if ( !layerTreeElem.isNull() )
|
|
{
|
|
// Use a temporary tree to read the nodes to prevent signals being delivered to the models
|
|
QgsLayerTree tempTree;
|
|
tempTree.readChildrenFromXml( layerTreeElem, context );
|
|
mRootGroup->insertChildNodes( -1, tempTree.abandonChildren() );
|
|
}
|
|
else
|
|
{
|
|
QgsLayerTreeUtils::readOldLegend( mRootGroup, doc->documentElement().firstChildElement( QStringLiteral( "legend" ) ) );
|
|
}
|
|
|
|
mLayerTreeRegistryBridge->setEnabled( false );
|
|
|
|
// get the map layers
|
|
profile.switchTask( tr( "Reading map layers" ) );
|
|
|
|
QList<QDomNode> brokenNodes;
|
|
const bool clean = _getMapLayers( *doc, brokenNodes, flags );
|
|
|
|
// review the integrity of the retrieved map layers
|
|
if ( !clean )
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Unable to get map layers from project file." ) );
|
|
|
|
if ( !brokenNodes.isEmpty() )
|
|
{
|
|
QgsDebugMsg( "there are " + QString::number( brokenNodes.size() ) + " broken layers" );
|
|
}
|
|
|
|
// we let a custom handler decide what to do with missing layers
|
|
// (default implementation ignores them, there's also a GUI handler that lets user choose correct path)
|
|
mBadLayerHandler->handleBadLayers( brokenNodes );
|
|
}
|
|
|
|
mMainAnnotationLayer->readLayerXml( doc->documentElement().firstChildElement( QStringLiteral( "main-annotation-layer" ) ), context );
|
|
mMainAnnotationLayer->setTransformContext( mTransformContext );
|
|
|
|
// load embedded groups and layers
|
|
profile.switchTask( tr( "Loading embedded layers" ) );
|
|
loadEmbeddedNodes( mRootGroup, flags );
|
|
|
|
// Resolve references to other layers
|
|
// Needs to be done here once all dependent layers are loaded
|
|
profile.switchTask( tr( "Resolving layer references" ) );
|
|
QMap<QString, QgsMapLayer *> layers = mLayerStore->mapLayers();
|
|
for ( QMap<QString, QgsMapLayer *>::iterator it = layers.begin(); it != layers.end(); ++it )
|
|
{
|
|
it.value()->resolveReferences( this );
|
|
}
|
|
|
|
mLayerTreeRegistryBridge->setEnabled( true );
|
|
|
|
// now that layers are loaded, we can resolve layer tree's references to the layers
|
|
profile.switchTask( tr( "Resolving references" ) );
|
|
mRootGroup->resolveReferences( this );
|
|
|
|
if ( !layerTreeElem.isNull() )
|
|
{
|
|
mRootGroup->readLayerOrderFromXml( layerTreeElem );
|
|
}
|
|
|
|
// Load pre 3.0 configuration
|
|
const QDomElement layerTreeCanvasElem = doc->documentElement().firstChildElement( QStringLiteral( "layer-tree-canvas" ) );
|
|
if ( !layerTreeCanvasElem.isNull( ) )
|
|
{
|
|
mRootGroup->readLayerOrderFromXml( layerTreeCanvasElem );
|
|
}
|
|
|
|
// Convert pre 3.4 to create layers flags
|
|
if ( QgsProjectVersion( 3, 4, 0 ) > mSaveVersion )
|
|
{
|
|
const QStringList requiredLayerIds = readListEntry( QStringLiteral( "RequiredLayers" ), QStringLiteral( "Layers" ) );
|
|
for ( const QString &layerId : requiredLayerIds )
|
|
{
|
|
if ( QgsMapLayer *layer = mapLayer( layerId ) )
|
|
{
|
|
layer->setFlags( layer->flags() & ~QgsMapLayer::Removable );
|
|
}
|
|
}
|
|
const QStringList disabledLayerIds = readListEntry( QStringLiteral( "Identify" ), QStringLiteral( "/disabledLayers" ) );
|
|
for ( const QString &layerId : disabledLayerIds )
|
|
{
|
|
if ( QgsMapLayer *layer = mapLayer( layerId ) )
|
|
{
|
|
layer->setFlags( layer->flags() & ~QgsMapLayer::Identifiable );
|
|
}
|
|
}
|
|
}
|
|
|
|
// After bad layer handling we might still have invalid layers,
|
|
// store them in case the user wanted to handle them later
|
|
// or wanted to pass them through when saving
|
|
if ( !( flags & QgsProject::ReadFlag::FlagDontStoreOriginalStyles ) )
|
|
{
|
|
profile.switchTask( tr( "Storing original layer properties" ) );
|
|
QgsLayerTreeUtils::storeOriginalLayersProperties( mRootGroup, doc.get() );
|
|
}
|
|
|
|
mRootGroup->removeCustomProperty( QStringLiteral( "loading" ) );
|
|
|
|
profile.switchTask( tr( "Loading map themes" ) );
|
|
mMapThemeCollection.reset( new QgsMapThemeCollection( this ) );
|
|
emit mapThemeCollectionChanged();
|
|
mMapThemeCollection->readXml( *doc );
|
|
|
|
profile.switchTask( tr( "Loading label settings" ) );
|
|
mLabelingEngineSettings->readSettingsFromProject( this );
|
|
emit labelingEngineSettingsChanged();
|
|
|
|
profile.switchTask( tr( "Loading annotations" ) );
|
|
mAnnotationManager->readXml( doc->documentElement(), context );
|
|
if ( !( flags & QgsProject::ReadFlag::FlagDontLoadLayouts ) )
|
|
{
|
|
profile.switchTask( tr( "Loading layouts" ) );
|
|
mLayoutManager->readXml( doc->documentElement(), *doc );
|
|
}
|
|
profile.switchTask( tr( "Loading bookmarks" ) );
|
|
mBookmarkManager->readXml( doc->documentElement(), *doc );
|
|
|
|
// reassign change dependencies now that all layers are loaded
|
|
QMap<QString, QgsMapLayer *> existingMaps = mapLayers();
|
|
for ( QMap<QString, QgsMapLayer *>::iterator it = existingMaps.begin(); it != existingMaps.end(); ++it )
|
|
{
|
|
it.value()->setDependencies( it.value()->dependencies() );
|
|
}
|
|
|
|
profile.switchTask( tr( "Loading snapping settings" ) );
|
|
mSnappingConfig.readProject( *doc );
|
|
mAvoidIntersectionsMode = static_cast<AvoidIntersectionsMode>( readNumEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/AvoidIntersectionsMode" ), static_cast<int>( AvoidIntersectionsMode::AvoidIntersectionsLayers ) ) );
|
|
|
|
profile.switchTask( tr( "Loading view settings" ) );
|
|
// restore older project scales settings
|
|
mViewSettings->setUseProjectScales( readBoolEntry( QStringLiteral( "Scales" ), QStringLiteral( "/useProjectScales" ) ) );
|
|
const QStringList scales = readListEntry( QStringLiteral( "Scales" ), QStringLiteral( "/ScalesList" ) );
|
|
QVector<double> res;
|
|
for ( const QString &scale : scales )
|
|
{
|
|
const QStringList parts = scale.split( ':' );
|
|
if ( parts.size() != 2 )
|
|
continue;
|
|
|
|
bool ok = false;
|
|
const double denominator = QLocale().toDouble( parts[1], &ok );
|
|
if ( ok )
|
|
{
|
|
res << denominator;
|
|
}
|
|
}
|
|
mViewSettings->setMapScales( res );
|
|
const QDomElement viewSettingsElement = doc->documentElement().firstChildElement( QStringLiteral( "ProjectViewSettings" ) );
|
|
if ( !viewSettingsElement.isNull() )
|
|
mViewSettings->readXml( viewSettingsElement, context );
|
|
|
|
// restore time settings
|
|
profile.switchTask( tr( "Loading temporal settings" ) );
|
|
const QDomElement timeSettingsElement = doc->documentElement().firstChildElement( QStringLiteral( "ProjectTimeSettings" ) );
|
|
if ( !timeSettingsElement.isNull() )
|
|
mTimeSettings->readXml( timeSettingsElement, context );
|
|
|
|
profile.switchTask( tr( "Loading display settings" ) );
|
|
const QDomElement displaySettingsElement = doc->documentElement().firstChildElement( QStringLiteral( "ProjectDisplaySettings" ) );
|
|
if ( !displaySettingsElement.isNull() )
|
|
mDisplaySettings->readXml( displaySettingsElement, context );
|
|
|
|
profile.switchTask( tr( "Updating variables" ) );
|
|
emit customVariablesChanged();
|
|
profile.switchTask( tr( "Updating CRS" ) );
|
|
emit crsChanged();
|
|
emit ellipsoidChanged( ellipsoid() );
|
|
|
|
// read the project: used by map canvas and legend
|
|
profile.switchTask( tr( "Reading external settings" ) );
|
|
emit readProject( *doc );
|
|
emit readProjectWithContext( *doc, context );
|
|
|
|
profile.switchTask( tr( "Updating interface" ) );
|
|
|
|
snapSignalBlock.release();
|
|
if ( !mBlockSnappingUpdates )
|
|
emit snappingConfigChanged( mSnappingConfig );
|
|
|
|
emit avoidIntersectionsModeChanged();
|
|
emit topologicalEditingChanged();
|
|
emit projectColorsChanged();
|
|
|
|
// if all went well, we're allegedly in pristine state
|
|
if ( clean )
|
|
setDirty( false );
|
|
|
|
QgsDebugMsgLevel( QString( "Project save user: %1" ).arg( mSaveUser ), 2 );
|
|
QgsDebugMsgLevel( QString( "Project save user: %1" ).arg( mSaveUserFull ), 2 );
|
|
|
|
Q_NOWARN_DEPRECATED_PUSH
|
|
emit nonIdentifiableLayersChanged( nonIdentifiableLayers() );
|
|
Q_NOWARN_DEPRECATED_POP
|
|
|
|
if ( mTranslator )
|
|
{
|
|
//project possibly translated -> rename it with locale postfix
|
|
const QString newFileName( QStringLiteral( "%1/%2.qgs" ).arg( QFileInfo( projectFile.fileName() ).absolutePath(), localeFileName ) );
|
|
setFileName( newFileName );
|
|
|
|
if ( write() )
|
|
{
|
|
setTitle( localeFileName );
|
|
QgsMessageLog::logMessage( tr( "Translated project saved with locale prefix %1" ).arg( newFileName ), QObject::tr( "Project translation" ), Qgis::MessageLevel::Success );
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error saving translated project with locale prefix %1" ).arg( newFileName ), QObject::tr( "Project translation" ), Qgis::MessageLevel::Critical );
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool QgsProject::loadEmbeddedNodes( QgsLayerTreeGroup *group, QgsProject::ReadFlags flags )
|
|
{
|
|
bool valid = true;
|
|
const auto constChildren = group->children();
|
|
for ( QgsLayerTreeNode *child : constChildren )
|
|
{
|
|
if ( QgsLayerTree::isGroup( child ) )
|
|
{
|
|
QgsLayerTreeGroup *childGroup = QgsLayerTree::toGroup( child );
|
|
if ( childGroup->customProperty( QStringLiteral( "embedded" ) ).toInt() )
|
|
{
|
|
// make sure to convert the path from relative to absolute
|
|
const QString projectPath = readPath( childGroup->customProperty( QStringLiteral( "embedded_project" ) ).toString() );
|
|
childGroup->setCustomProperty( QStringLiteral( "embedded_project" ), projectPath );
|
|
QgsLayerTreeGroup *newGroup = createEmbeddedGroup( childGroup->name(), projectPath, childGroup->customProperty( QStringLiteral( "embedded-invisible-layers" ) ).toStringList(), flags );
|
|
if ( newGroup )
|
|
{
|
|
QList<QgsLayerTreeNode *> clonedChildren;
|
|
const auto constChildren = newGroup->children();
|
|
for ( QgsLayerTreeNode *newGroupChild : constChildren )
|
|
clonedChildren << newGroupChild->clone();
|
|
delete newGroup;
|
|
|
|
childGroup->insertChildNodes( 0, clonedChildren );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
loadEmbeddedNodes( childGroup, flags );
|
|
}
|
|
}
|
|
else if ( QgsLayerTree::isLayer( child ) )
|
|
{
|
|
if ( child->customProperty( QStringLiteral( "embedded" ) ).toInt() )
|
|
{
|
|
QList<QDomNode> brokenNodes;
|
|
if ( ! createEmbeddedLayer( QgsLayerTree::toLayer( child )->layerId(), readPath( child->customProperty( QStringLiteral( "embedded_project" ) ).toString() ), brokenNodes, true, flags ) )
|
|
{
|
|
valid = valid && false;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
QVariantMap QgsProject::customVariables() const
|
|
{
|
|
return mCustomVariables;
|
|
}
|
|
|
|
void QgsProject::setCustomVariables( const QVariantMap &variables )
|
|
{
|
|
if ( variables == mCustomVariables )
|
|
return;
|
|
|
|
//write variable to project
|
|
QStringList variableNames;
|
|
QStringList variableValues;
|
|
|
|
QVariantMap::const_iterator it = variables.constBegin();
|
|
for ( ; it != variables.constEnd(); ++it )
|
|
{
|
|
variableNames << it.key();
|
|
variableValues << it.value().toString();
|
|
}
|
|
|
|
writeEntry( QStringLiteral( "Variables" ), QStringLiteral( "/variableNames" ), variableNames );
|
|
writeEntry( QStringLiteral( "Variables" ), QStringLiteral( "/variableValues" ), variableValues );
|
|
|
|
mCustomVariables = variables;
|
|
mProjectScope.reset();
|
|
|
|
emit customVariablesChanged();
|
|
}
|
|
|
|
void QgsProject::setLabelingEngineSettings( const QgsLabelingEngineSettings &settings )
|
|
{
|
|
*mLabelingEngineSettings = settings;
|
|
emit labelingEngineSettingsChanged();
|
|
}
|
|
|
|
const QgsLabelingEngineSettings &QgsProject::labelingEngineSettings() const
|
|
{
|
|
return *mLabelingEngineSettings;
|
|
}
|
|
|
|
QgsMapLayerStore *QgsProject::layerStore()
|
|
{
|
|
mProjectScope.reset();
|
|
return mLayerStore.get();
|
|
}
|
|
|
|
const QgsMapLayerStore *QgsProject::layerStore() const
|
|
{
|
|
return mLayerStore.get();
|
|
}
|
|
|
|
QList<QgsVectorLayer *> QgsProject::avoidIntersectionsLayers() const
|
|
{
|
|
QList<QgsVectorLayer *> layers;
|
|
const QStringList layerIds = readListEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/AvoidIntersectionsList" ), QStringList() );
|
|
const auto constLayerIds = layerIds;
|
|
for ( const QString &layerId : constLayerIds )
|
|
{
|
|
if ( QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( mapLayer( layerId ) ) )
|
|
layers << vlayer;
|
|
}
|
|
return layers;
|
|
}
|
|
|
|
void QgsProject::setAvoidIntersectionsLayers( const QList<QgsVectorLayer *> &layers )
|
|
{
|
|
QStringList list;
|
|
const auto constLayers = layers;
|
|
for ( QgsVectorLayer *layer : constLayers )
|
|
list << layer->id();
|
|
writeEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/AvoidIntersectionsList" ), list );
|
|
emit avoidIntersectionsLayersChanged();
|
|
}
|
|
|
|
QgsExpressionContext QgsProject::createExpressionContext() const
|
|
{
|
|
QgsExpressionContext context;
|
|
|
|
context << QgsExpressionContextUtils::globalScope()
|
|
<< QgsExpressionContextUtils::projectScope( this );
|
|
|
|
return context;
|
|
}
|
|
|
|
QgsExpressionContextScope *QgsProject::createExpressionContextScope() const
|
|
{
|
|
// MUCH cheaper to clone than build
|
|
if ( mProjectScope )
|
|
{
|
|
std::unique_ptr< QgsExpressionContextScope > projectScope = std::make_unique< QgsExpressionContextScope >( *mProjectScope );
|
|
// we can't cache these
|
|
projectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_distance_units" ), QgsUnitTypes::toString( distanceUnits() ), true, true ) );
|
|
projectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_area_units" ), QgsUnitTypes::toString( areaUnits() ), true, true ) );
|
|
return projectScope.release();
|
|
}
|
|
|
|
mProjectScope = std::make_unique< QgsExpressionContextScope >( QObject::tr( "Project" ) );
|
|
|
|
const QVariantMap vars = customVariables();
|
|
|
|
QVariantMap::const_iterator it = vars.constBegin();
|
|
|
|
for ( ; it != vars.constEnd(); ++it )
|
|
{
|
|
mProjectScope->setVariable( it.key(), it.value(), true );
|
|
}
|
|
|
|
QString projectPath = projectStorage() ? fileName() : absoluteFilePath();
|
|
if ( projectPath.isEmpty() )
|
|
projectPath = mOriginalPath;
|
|
const QString projectFolder = QFileInfo( projectPath ).path();
|
|
const QString projectFilename = QFileInfo( projectPath ).fileName();
|
|
const QString projectBasename = baseName();
|
|
|
|
//add other known project variables
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_title" ), title(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_path" ), QDir::toNativeSeparators( projectPath ), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_folder" ), QDir::toNativeSeparators( projectFolder ), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_filename" ), projectFilename, true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_basename" ), projectBasename, true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_home" ), QDir::toNativeSeparators( homePath() ), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_last_saved" ), mSaveDateTime.isNull() ? QVariant() : QVariant( mSaveDateTime ), true, true ) );
|
|
const QgsCoordinateReferenceSystem projectCrs = crs();
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs" ), projectCrs.authid(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_definition" ), projectCrs.toProj(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_description" ), projectCrs.description(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_ellipsoid" ), ellipsoid(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "_project_transform_context" ), QVariant::fromValue<QgsCoordinateTransformContext>( transformContext() ), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_units" ), QgsUnitTypes::toString( projectCrs.mapUnits() ), true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_acronym" ), projectCrs.projectionAcronym(), true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_ellipsoid" ), projectCrs.ellipsoidAcronym(), true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_proj4" ), projectCrs.toProj(), true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_wkt" ), projectCrs.toWkt( QgsCoordinateReferenceSystem::WKT_PREFERRED ), true ) );
|
|
|
|
// metadata
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_author" ), metadata().author(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_abstract" ), metadata().abstract(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_creation_date" ), metadata().creationDateTime(), true, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_identifier" ), metadata().identifier(), true, true ) );
|
|
|
|
// keywords
|
|
QVariantMap keywords;
|
|
const QgsAbstractMetadataBase::KeywordMap metadataKeywords = metadata().keywords();
|
|
for ( auto it = metadataKeywords.constBegin(); it != metadataKeywords.constEnd(); ++it )
|
|
{
|
|
keywords.insert( it.key(), it.value() );
|
|
}
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_keywords" ), keywords, true, true ) );
|
|
|
|
// layers
|
|
QVariantList layersIds;
|
|
QVariantList layers;
|
|
const QMap<QString, QgsMapLayer *> layersInProject = mLayerStore->mapLayers();
|
|
layersIds.reserve( layersInProject.count() );
|
|
layers.reserve( layersInProject.count() );
|
|
for ( auto it = layersInProject.constBegin(); it != layersInProject.constEnd(); ++it )
|
|
{
|
|
layersIds << it.value()->id();
|
|
layers << QVariant::fromValue<QgsWeakMapLayerPointer>( QgsWeakMapLayerPointer( it.value() ) );
|
|
}
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layer_ids" ), layersIds, true ) );
|
|
mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layers" ), layers, true ) );
|
|
|
|
mProjectScope->addFunction( QStringLiteral( "project_color" ), new GetNamedProjectColor( this ) );
|
|
|
|
return createExpressionContextScope();
|
|
}
|
|
|
|
void QgsProject::onMapLayersAdded( const QList<QgsMapLayer *> &layers )
|
|
{
|
|
const QMap<QString, QgsMapLayer *> existingMaps = mapLayers();
|
|
|
|
bool tgChanged = false;
|
|
|
|
const auto constLayers = layers;
|
|
for ( QgsMapLayer *layer : constLayers )
|
|
{
|
|
if ( layer->isValid() )
|
|
{
|
|
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( layer );
|
|
if ( vlayer )
|
|
{
|
|
if ( autoTransaction() )
|
|
{
|
|
if ( QgsTransaction::supportsTransaction( vlayer ) )
|
|
{
|
|
const QString connString = QgsTransaction::connectionString( vlayer->source() );
|
|
const QString key = vlayer->providerType();
|
|
|
|
QgsTransactionGroup *tg = mTransactionGroups.value( qMakePair( key, connString ) );
|
|
|
|
if ( !tg )
|
|
{
|
|
tg = new QgsTransactionGroup();
|
|
mTransactionGroups.insert( qMakePair( key, connString ), tg );
|
|
tgChanged = true;
|
|
}
|
|
tg->addLayer( vlayer );
|
|
}
|
|
}
|
|
vlayer->dataProvider()->setProviderProperty( QgsVectorDataProvider::EvaluateDefaultValues, evaluateDefaultValues() );
|
|
}
|
|
|
|
if ( tgChanged )
|
|
emit transactionGroupsChanged();
|
|
|
|
connect( layer, &QgsMapLayer::configChanged, this, [ = ] { setDirty(); } );
|
|
|
|
// check if we have to update connections for layers with dependencies
|
|
for ( QMap<QString, QgsMapLayer *>::const_iterator it = existingMaps.cbegin(); it != existingMaps.cend(); ++it )
|
|
{
|
|
const QSet<QgsMapLayerDependency> deps = it.value()->dependencies();
|
|
if ( deps.contains( layer->id() ) )
|
|
{
|
|
// reconnect to change signals
|
|
it.value()->setDependencies( deps );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !mBlockSnappingUpdates && mSnappingConfig.addLayers( layers ) )
|
|
emit snappingConfigChanged( mSnappingConfig );
|
|
}
|
|
|
|
void QgsProject::onMapLayersRemoved( const QList<QgsMapLayer *> &layers )
|
|
{
|
|
if ( !mBlockSnappingUpdates && mSnappingConfig.removeLayers( layers ) )
|
|
emit snappingConfigChanged( mSnappingConfig );
|
|
}
|
|
|
|
void QgsProject::cleanTransactionGroups( bool force )
|
|
{
|
|
bool changed = false;
|
|
for ( QMap< QPair< QString, QString>, QgsTransactionGroup *>::Iterator tg = mTransactionGroups.begin(); tg != mTransactionGroups.end(); )
|
|
{
|
|
if ( tg.value()->isEmpty() || force )
|
|
{
|
|
delete tg.value();
|
|
tg = mTransactionGroups.erase( tg );
|
|
changed = true;
|
|
}
|
|
else
|
|
{
|
|
++tg;
|
|
}
|
|
}
|
|
if ( changed )
|
|
emit transactionGroupsChanged();
|
|
}
|
|
|
|
bool QgsProject::readLayer( const QDomNode &layerNode )
|
|
{
|
|
QgsReadWriteContext context;
|
|
context.setPathResolver( pathResolver() );
|
|
context.setProjectTranslator( this );
|
|
context.setTransformContext( transformContext() );
|
|
QList<QDomNode> brokenNodes;
|
|
if ( addLayer( layerNode.toElement(), brokenNodes, context ) )
|
|
{
|
|
// have to try to update joins for all layers now - a previously added layer may be dependent on this newly
|
|
// added layer for joins
|
|
const QVector<QgsVectorLayer *> vectorLayers = layers<QgsVectorLayer *>();
|
|
const auto constVectorLayers = vectorLayers;
|
|
for ( QgsVectorLayer *layer : constVectorLayers )
|
|
{
|
|
// TODO: should be only done later - and with all layers (other layers may have referenced this layer)
|
|
layer->resolveReferences( this );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool QgsProject::write( const QString &filename )
|
|
{
|
|
mFile.setFileName( filename );
|
|
mCachedHomePath.clear();
|
|
return write();
|
|
}
|
|
|
|
bool QgsProject::write()
|
|
{
|
|
mProjectScope.reset();
|
|
if ( QgsProjectStorage *storage = projectStorage() )
|
|
{
|
|
QgsReadWriteContext context;
|
|
// for projects stored in a custom storage, we have to check for the support
|
|
// of relative paths since the storage most likely will not be in a file system
|
|
const QString storageFilePath { storage->filePath( mFile.fileName() ) };
|
|
if ( storageFilePath.isEmpty() )
|
|
{
|
|
setFilePathStorage( Qgis::FilePathType::Absolute );
|
|
}
|
|
context.setPathResolver( pathResolver() );
|
|
|
|
const QString tempPath = QStandardPaths::standardLocations( QStandardPaths::TempLocation ).at( 0 );
|
|
const QString tmpZipFilename( tempPath + QDir::separator() + QUuid::createUuid().toString() );
|
|
|
|
if ( !zip( tmpZipFilename ) )
|
|
return false; // zip() already calls setError() when returning false
|
|
|
|
QFile tmpZipFile( tmpZipFilename );
|
|
if ( !tmpZipFile.open( QIODevice::ReadOnly ) )
|
|
{
|
|
setError( tr( "Unable to read file %1" ).arg( tmpZipFilename ) );
|
|
return false;
|
|
}
|
|
|
|
context.setTransformContext( transformContext() );
|
|
if ( !storage->writeProject( mFile.fileName(), &tmpZipFile, context ) )
|
|
{
|
|
QString err = tr( "Unable to save project to storage %1" ).arg( mFile.fileName() );
|
|
QList<QgsReadWriteContext::ReadWriteMessage> messages = context.takeMessages();
|
|
if ( !messages.isEmpty() )
|
|
err += QStringLiteral( "\n\n" ) + messages.last().message();
|
|
setError( err );
|
|
return false;
|
|
}
|
|
|
|
tmpZipFile.close();
|
|
QFile::remove( tmpZipFilename );
|
|
|
|
return true;
|
|
}
|
|
|
|
if ( QgsZipUtils::isZipFile( mFile.fileName() ) )
|
|
{
|
|
return zip( mFile.fileName() );
|
|
}
|
|
else
|
|
{
|
|
// write project file even if the auxiliary storage is not correctly
|
|
// saved
|
|
const bool asOk = saveAuxiliaryStorage();
|
|
const bool writeOk = writeProjectFile( mFile.fileName() );
|
|
bool attachmentsOk = true;
|
|
if ( !mArchive->files().isEmpty() )
|
|
{
|
|
const QFileInfo finfo( mFile.fileName() );
|
|
const QString attachmentsZip = finfo.absoluteDir().absoluteFilePath( QStringLiteral( "%1_attachments.zip" ).arg( finfo.completeBaseName() ) );
|
|
attachmentsOk = mArchive->zip( attachmentsZip );
|
|
}
|
|
|
|
// errors raised during writing project file are more important
|
|
if ( ( !asOk || !attachmentsOk ) && writeOk )
|
|
{
|
|
QStringList errorMessage;
|
|
if ( !asOk )
|
|
{
|
|
const QString err = mAuxiliaryStorage->errorString();
|
|
errorMessage.append( tr( "Unable to save auxiliary storage ('%1')" ).arg( err ) );
|
|
}
|
|
if ( !attachmentsOk )
|
|
{
|
|
errorMessage.append( tr( "Unable to save attachments archive" ) );
|
|
}
|
|
setError( errorMessage.join( "\n" ) );
|
|
}
|
|
|
|
return asOk && writeOk && attachmentsOk;
|
|
}
|
|
}
|
|
|
|
bool QgsProject::writeProjectFile( const QString &filename )
|
|
{
|
|
QFile projectFile( filename );
|
|
clearError();
|
|
|
|
// if we have problems creating or otherwise writing to the project file,
|
|
// let's find out up front before we go through all the hand-waving
|
|
// necessary to create all the Dom objects
|
|
const QFileInfo myFileInfo( projectFile );
|
|
if ( myFileInfo.exists() && !myFileInfo.isWritable() )
|
|
{
|
|
setError( tr( "%1 is not writable. Please adjust permissions (if possible) and try again." )
|
|
.arg( projectFile.fileName() ) );
|
|
return false;
|
|
}
|
|
|
|
QgsReadWriteContext context;
|
|
context.setPathResolver( pathResolver() );
|
|
context.setTransformContext( transformContext() );
|
|
|
|
QDomImplementation DomImplementation;
|
|
DomImplementation.setInvalidDataPolicy( QDomImplementation::DropInvalidChars );
|
|
|
|
const QDomDocumentType documentType =
|
|
DomImplementation.createDocumentType( QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ),
|
|
QStringLiteral( "SYSTEM" ) );
|
|
std::unique_ptr<QDomDocument> doc( new QDomDocument( documentType ) );
|
|
|
|
QDomElement qgisNode = doc->createElement( QStringLiteral( "qgis" ) );
|
|
qgisNode.setAttribute( QStringLiteral( "projectname" ), title() );
|
|
qgisNode.setAttribute( QStringLiteral( "version" ), Qgis::version() );
|
|
|
|
if ( !mSettings.value( QStringLiteral( "projects/anonymize_saved_projects" ), false, QgsSettings::Core ).toBool() )
|
|
{
|
|
const QString newSaveUser = QgsApplication::userLoginName();
|
|
const QString newSaveUserFull = QgsApplication::userFullName();
|
|
qgisNode.setAttribute( QStringLiteral( "saveUser" ), newSaveUser );
|
|
qgisNode.setAttribute( QStringLiteral( "saveUserFull" ), newSaveUserFull );
|
|
mSaveUser = newSaveUser;
|
|
mSaveUserFull = newSaveUserFull;
|
|
mSaveDateTime = QDateTime::currentDateTime();
|
|
qgisNode.setAttribute( QStringLiteral( "saveDateTime" ), mSaveDateTime.toString( Qt::ISODate ) );
|
|
}
|
|
else
|
|
{
|
|
mSaveUser.clear();
|
|
mSaveUserFull.clear();
|
|
mSaveDateTime = QDateTime();
|
|
}
|
|
doc->appendChild( qgisNode );
|
|
mSaveVersion = QgsProjectVersion( Qgis::version() );
|
|
|
|
QDomElement homePathNode = doc->createElement( QStringLiteral( "homePath" ) );
|
|
homePathNode.setAttribute( QStringLiteral( "path" ), mHomePath );
|
|
qgisNode.appendChild( homePathNode );
|
|
|
|
// title
|
|
QDomElement titleNode = doc->createElement( QStringLiteral( "title" ) );
|
|
qgisNode.appendChild( titleNode );
|
|
|
|
QDomElement transactionNode = doc->createElement( QStringLiteral( "autotransaction" ) );
|
|
transactionNode.setAttribute( QStringLiteral( "active" ), mAutoTransaction ? 1 : 0 );
|
|
qgisNode.appendChild( transactionNode );
|
|
|
|
QDomElement evaluateDefaultValuesNode = doc->createElement( QStringLiteral( "evaluateDefaultValues" ) );
|
|
evaluateDefaultValuesNode.setAttribute( QStringLiteral( "active" ), mEvaluateDefaultValues ? 1 : 0 );
|
|
qgisNode.appendChild( evaluateDefaultValuesNode );
|
|
|
|
QDomElement trustNode = doc->createElement( QStringLiteral( "trust" ) );
|
|
trustNode.setAttribute( QStringLiteral( "active" ), mTrustLayerMetadata ? 1 : 0 );
|
|
qgisNode.appendChild( trustNode );
|
|
|
|
const QDomText titleText = doc->createTextNode( title() ); // XXX why have title TWICE?
|
|
titleNode.appendChild( titleText );
|
|
|
|
// write project CRS
|
|
QDomElement srsNode = doc->createElement( QStringLiteral( "projectCrs" ) );
|
|
mCrs.writeXml( srsNode, *doc );
|
|
qgisNode.appendChild( srsNode );
|
|
|
|
// write layer tree - make sure it is without embedded subgroups
|
|
QgsLayerTreeNode *clonedRoot = mRootGroup->clone();
|
|
QgsLayerTreeUtils::replaceChildrenOfEmbeddedGroups( QgsLayerTree::toGroup( clonedRoot ) );
|
|
QgsLayerTreeUtils::updateEmbeddedGroupsProjectPath( QgsLayerTree::toGroup( clonedRoot ), this ); // convert absolute paths to relative paths if required
|
|
|
|
clonedRoot->writeXml( qgisNode, context );
|
|
delete clonedRoot;
|
|
|
|
mSnappingConfig.writeProject( *doc );
|
|
writeEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/AvoidIntersectionsMode" ), static_cast<int>( mAvoidIntersectionsMode ) );
|
|
|
|
// let map canvas and legend write their information
|
|
emit writeProject( *doc );
|
|
|
|
// within top level node save list of layers
|
|
const QMap<QString, QgsMapLayer *> &layers = mapLayers();
|
|
|
|
QDomElement annotationLayerNode = doc->createElement( QStringLiteral( "main-annotation-layer" ) );
|
|
mMainAnnotationLayer->writeLayerXml( annotationLayerNode, *doc, context );
|
|
qgisNode.appendChild( annotationLayerNode );
|
|
|
|
// Iterate over layers in zOrder
|
|
// Call writeXml() on each
|
|
QDomElement projectLayersNode = doc->createElement( QStringLiteral( "projectlayers" ) );
|
|
|
|
QMap<QString, QgsMapLayer *>::ConstIterator li = layers.constBegin();
|
|
while ( li != layers.end() )
|
|
{
|
|
QgsMapLayer *ml = li.value();
|
|
|
|
if ( ml )
|
|
{
|
|
const QHash< QString, QPair< QString, bool> >::const_iterator emIt = mEmbeddedLayers.constFind( ml->id() );
|
|
if ( emIt == mEmbeddedLayers.constEnd() )
|
|
{
|
|
QDomElement maplayerElem;
|
|
// If layer is not valid, prefer to restore saved properties from invalidLayerProperties. But if that's
|
|
// not available, just write what we DO have
|
|
if ( ml->isValid() || ml->originalXmlProperties().isEmpty() )
|
|
{
|
|
// general layer metadata
|
|
maplayerElem = doc->createElement( QStringLiteral( "maplayer" ) );
|
|
ml->writeLayerXml( maplayerElem, *doc, context );
|
|
}
|
|
else if ( ! ml->originalXmlProperties().isEmpty() )
|
|
{
|
|
QDomDocument document;
|
|
if ( document.setContent( ml->originalXmlProperties() ) )
|
|
{
|
|
maplayerElem = document.firstChildElement();
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsg( QStringLiteral( "Could not restore layer properties for layer %1" ).arg( ml->id() ) );
|
|
}
|
|
}
|
|
|
|
emit writeMapLayer( ml, maplayerElem, *doc );
|
|
|
|
projectLayersNode.appendChild( maplayerElem );
|
|
}
|
|
else
|
|
{
|
|
// layer defined in an external project file
|
|
// only save embedded layer if not managed by a legend group
|
|
if ( emIt.value().second )
|
|
{
|
|
QDomElement mapLayerElem = doc->createElement( QStringLiteral( "maplayer" ) );
|
|
mapLayerElem.setAttribute( QStringLiteral( "embedded" ), 1 );
|
|
mapLayerElem.setAttribute( QStringLiteral( "project" ), writePath( emIt.value().first ) );
|
|
mapLayerElem.setAttribute( QStringLiteral( "id" ), ml->id() );
|
|
projectLayersNode.appendChild( mapLayerElem );
|
|
}
|
|
}
|
|
}
|
|
li++;
|
|
}
|
|
|
|
qgisNode.appendChild( projectLayersNode );
|
|
|
|
QDomElement layerOrderNode = doc->createElement( QStringLiteral( "layerorder" ) );
|
|
const auto constCustomLayerOrder = mRootGroup->customLayerOrder();
|
|
for ( QgsMapLayer *layer : constCustomLayerOrder )
|
|
{
|
|
QDomElement mapLayerElem = doc->createElement( QStringLiteral( "layer" ) );
|
|
mapLayerElem.setAttribute( QStringLiteral( "id" ), layer->id() );
|
|
layerOrderNode.appendChild( mapLayerElem );
|
|
}
|
|
qgisNode.appendChild( layerOrderNode );
|
|
|
|
mLabelingEngineSettings->writeSettingsToProject( this );
|
|
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorRedPart" ), mBackgroundColor.red() );
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorGreenPart" ), mBackgroundColor.green() );
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorBluePart" ), mBackgroundColor.blue() );
|
|
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorRedPart" ), mSelectionColor.red() );
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorGreenPart" ), mSelectionColor.green() );
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorBluePart" ), mSelectionColor.blue() );
|
|
writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/SelectionColorAlphaPart" ), mSelectionColor.alpha() );
|
|
|
|
// now add the optional extra properties
|
|
#if 0
|
|
dump_( mProperties );
|
|
#endif
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "there are %1 property scopes" ).arg( static_cast<int>( mProperties.count() ) ), 2 );
|
|
|
|
if ( !mProperties.isEmpty() ) // only worry about properties if we
|
|
// actually have any properties
|
|
{
|
|
mProperties.writeXml( QStringLiteral( "properties" ), qgisNode, *doc );
|
|
}
|
|
|
|
QDomElement ddElem = doc->createElement( QStringLiteral( "dataDefinedServerProperties" ) );
|
|
mDataDefinedServerProperties.writeXml( ddElem, dataDefinedServerPropertyDefinitions() );
|
|
qgisNode.appendChild( ddElem );
|
|
|
|
mMapThemeCollection->writeXml( *doc );
|
|
|
|
mTransformContext.writeXml( qgisNode, context );
|
|
|
|
QDomElement metadataElem = doc->createElement( QStringLiteral( "projectMetadata" ) );
|
|
mMetadata.writeMetadataXml( metadataElem, *doc );
|
|
qgisNode.appendChild( metadataElem );
|
|
|
|
const QDomElement annotationsElem = mAnnotationManager->writeXml( *doc, context );
|
|
qgisNode.appendChild( annotationsElem );
|
|
|
|
const QDomElement layoutElem = mLayoutManager->writeXml( *doc );
|
|
qgisNode.appendChild( layoutElem );
|
|
|
|
const QDomElement bookmarkElem = mBookmarkManager->writeXml( *doc );
|
|
qgisNode.appendChild( bookmarkElem );
|
|
|
|
const QDomElement viewSettingsElem = mViewSettings->writeXml( *doc, context );
|
|
qgisNode.appendChild( viewSettingsElem );
|
|
|
|
const QDomElement timeSettingsElement = mTimeSettings->writeXml( *doc, context );
|
|
qgisNode.appendChild( timeSettingsElement );
|
|
|
|
const QDomElement displaySettingsElem = mDisplaySettings->writeXml( *doc, context );
|
|
qgisNode.appendChild( displaySettingsElem );
|
|
|
|
// now wrap it up and ship it to the project file
|
|
doc->normalize(); // XXX I'm not entirely sure what this does
|
|
|
|
// Create backup file
|
|
if ( QFile::exists( fileName() ) )
|
|
{
|
|
QFile backupFile( QStringLiteral( "%1~" ).arg( filename ) );
|
|
bool ok = true;
|
|
ok &= backupFile.open( QIODevice::WriteOnly | QIODevice::Truncate );
|
|
ok &= projectFile.open( QIODevice::ReadOnly );
|
|
|
|
QByteArray ba;
|
|
while ( ok && !projectFile.atEnd() )
|
|
{
|
|
ba = projectFile.read( 10240 );
|
|
ok &= backupFile.write( ba ) == ba.size();
|
|
}
|
|
|
|
projectFile.close();
|
|
backupFile.close();
|
|
|
|
if ( !ok )
|
|
{
|
|
setError( tr( "Unable to create backup file %1" ).arg( backupFile.fileName() ) );
|
|
return false;
|
|
}
|
|
|
|
const QFileInfo fi( fileName() );
|
|
struct utimbuf tb = { static_cast<time_t>( fi.lastRead().toSecsSinceEpoch() ), static_cast<time_t>( fi.lastModified().toSecsSinceEpoch() ) };
|
|
utime( backupFile.fileName().toUtf8().constData(), &tb );
|
|
}
|
|
|
|
if ( !projectFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
|
|
{
|
|
projectFile.close(); // even though we got an error, let's make
|
|
// sure it's closed anyway
|
|
|
|
setError( tr( "Unable to save to file %1" ).arg( projectFile.fileName() ) );
|
|
return false;
|
|
}
|
|
|
|
QTemporaryFile tempFile;
|
|
bool ok = tempFile.open();
|
|
if ( ok )
|
|
{
|
|
QTextStream projectFileStream( &tempFile );
|
|
doc->save( projectFileStream, 2 ); // save as utf-8
|
|
ok &= projectFileStream.pos() > -1;
|
|
|
|
ok &= tempFile.seek( 0 );
|
|
|
|
QByteArray ba;
|
|
while ( ok && !tempFile.atEnd() )
|
|
{
|
|
ba = tempFile.read( 10240 );
|
|
ok &= projectFile.write( ba ) == ba.size();
|
|
}
|
|
|
|
ok &= projectFile.error() == QFile::NoError;
|
|
|
|
projectFile.close();
|
|
}
|
|
|
|
tempFile.close();
|
|
|
|
if ( !ok )
|
|
{
|
|
setError( tr( "Unable to save to file %1. Your project "
|
|
"may be corrupted on disk. Try clearing some space on the volume and "
|
|
"check file permissions before pressing save again." )
|
|
.arg( projectFile.fileName() ) );
|
|
return false;
|
|
}
|
|
|
|
setDirty( false ); // reset to pristine state
|
|
|
|
emit projectSaved();
|
|
return true;
|
|
}
|
|
|
|
bool QgsProject::writeEntry( const QString &scope, QString const &key, bool value )
|
|
{
|
|
bool propertiesModified;
|
|
const bool success = addKey_( scope, key, &mProperties, value, propertiesModified );
|
|
|
|
if ( propertiesModified )
|
|
setDirty( true );
|
|
|
|
return success;
|
|
}
|
|
|
|
bool QgsProject::writeEntry( const QString &scope, const QString &key, double value )
|
|
{
|
|
bool propertiesModified;
|
|
const bool success = addKey_( scope, key, &mProperties, value, propertiesModified );
|
|
|
|
if ( propertiesModified )
|
|
setDirty( true );
|
|
|
|
return success;
|
|
}
|
|
|
|
bool QgsProject::writeEntry( const QString &scope, QString const &key, int value )
|
|
{
|
|
bool propertiesModified;
|
|
const bool success = addKey_( scope, key, &mProperties, value, propertiesModified );
|
|
|
|
if ( propertiesModified )
|
|
setDirty( true );
|
|
|
|
return success;
|
|
}
|
|
|
|
bool QgsProject::writeEntry( const QString &scope, const QString &key, const QString &value )
|
|
{
|
|
bool propertiesModified;
|
|
const bool success = addKey_( scope, key, &mProperties, value, propertiesModified );
|
|
|
|
if ( propertiesModified )
|
|
setDirty( true );
|
|
|
|
return success;
|
|
}
|
|
|
|
bool QgsProject::writeEntry( const QString &scope, const QString &key, const QStringList &value )
|
|
{
|
|
bool propertiesModified;
|
|
const bool success = addKey_( scope, key, &mProperties, value, propertiesModified );
|
|
|
|
if ( propertiesModified )
|
|
setDirty( true );
|
|
|
|
return success;
|
|
}
|
|
|
|
QStringList QgsProject::readListEntry( const QString &scope,
|
|
const QString &key,
|
|
const QStringList &def,
|
|
bool *ok ) const
|
|
{
|
|
QgsProjectProperty *property = findKey_( scope, key, mProperties );
|
|
|
|
QVariant value;
|
|
|
|
if ( property )
|
|
{
|
|
value = property->value();
|
|
|
|
const bool valid = QVariant::StringList == value.type();
|
|
if ( ok )
|
|
*ok = valid;
|
|
|
|
if ( valid )
|
|
{
|
|
return value.toStringList();
|
|
}
|
|
}
|
|
else if ( ok )
|
|
*ok = false;
|
|
|
|
|
|
return def;
|
|
}
|
|
|
|
|
|
QString QgsProject::readEntry( const QString &scope,
|
|
const QString &key,
|
|
const QString &def,
|
|
bool *ok ) const
|
|
{
|
|
QgsProjectProperty *property = findKey_( scope, key, mProperties );
|
|
|
|
QVariant value;
|
|
|
|
if ( property )
|
|
{
|
|
value = property->value();
|
|
|
|
const bool valid = value.canConvert( QVariant::String );
|
|
if ( ok )
|
|
*ok = valid;
|
|
|
|
if ( valid )
|
|
return value.toString();
|
|
}
|
|
else if ( ok )
|
|
*ok = false;
|
|
|
|
return def;
|
|
}
|
|
|
|
int QgsProject::readNumEntry( const QString &scope, const QString &key, int def,
|
|
bool *ok ) const
|
|
{
|
|
QgsProjectProperty *property = findKey_( scope, key, mProperties );
|
|
|
|
QVariant value;
|
|
|
|
if ( property )
|
|
{
|
|
value = property->value();
|
|
}
|
|
|
|
const bool valid = value.canConvert( QVariant::Int );
|
|
|
|
if ( ok )
|
|
{
|
|
*ok = valid;
|
|
}
|
|
|
|
if ( valid )
|
|
{
|
|
return value.toInt();
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
double QgsProject::readDoubleEntry( const QString &scope, const QString &key,
|
|
double def,
|
|
bool *ok ) const
|
|
{
|
|
QgsProjectProperty *property = findKey_( scope, key, mProperties );
|
|
if ( property )
|
|
{
|
|
const QVariant value = property->value();
|
|
|
|
const bool valid = value.canConvert( QVariant::Double );
|
|
if ( ok )
|
|
*ok = valid;
|
|
|
|
if ( valid )
|
|
return value.toDouble();
|
|
}
|
|
else if ( ok )
|
|
*ok = false;
|
|
|
|
return def;
|
|
}
|
|
|
|
bool QgsProject::readBoolEntry( const QString &scope, const QString &key, bool def,
|
|
bool *ok ) const
|
|
{
|
|
QgsProjectProperty *property = findKey_( scope, key, mProperties );
|
|
|
|
if ( property )
|
|
{
|
|
const QVariant value = property->value();
|
|
|
|
const bool valid = value.canConvert( QVariant::Bool );
|
|
if ( ok )
|
|
*ok = valid;
|
|
|
|
if ( valid )
|
|
return value.toBool();
|
|
}
|
|
else if ( ok )
|
|
*ok = false;
|
|
|
|
return def;
|
|
}
|
|
|
|
bool QgsProject::removeEntry( const QString &scope, const QString &key )
|
|
{
|
|
if ( findKey_( scope, key, mProperties ) )
|
|
{
|
|
removeKey_( scope, key, mProperties );
|
|
setDirty( true );
|
|
}
|
|
|
|
return !findKey_( scope, key, mProperties );
|
|
}
|
|
|
|
|
|
QStringList QgsProject::entryList( const QString &scope, const QString &key ) const
|
|
{
|
|
QgsProjectProperty *foundProperty = findKey_( scope, key, mProperties );
|
|
|
|
QStringList entries;
|
|
|
|
if ( foundProperty )
|
|
{
|
|
QgsProjectPropertyKey *propertyKey = dynamic_cast<QgsProjectPropertyKey *>( foundProperty );
|
|
|
|
if ( propertyKey )
|
|
{ propertyKey->entryList( entries ); }
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
QStringList QgsProject::subkeyList( const QString &scope, const QString &key ) const
|
|
{
|
|
QgsProjectProperty *foundProperty = findKey_( scope, key, mProperties );
|
|
|
|
QStringList entries;
|
|
|
|
if ( foundProperty )
|
|
{
|
|
QgsProjectPropertyKey *propertyKey = dynamic_cast<QgsProjectPropertyKey *>( foundProperty );
|
|
|
|
if ( propertyKey )
|
|
{ propertyKey->subkeyList( entries ); }
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
void QgsProject::dumpProperties() const
|
|
{
|
|
dump_( mProperties );
|
|
}
|
|
|
|
QgsPathResolver QgsProject::pathResolver() const
|
|
{
|
|
QString filePath;
|
|
switch ( filePathStorage() )
|
|
{
|
|
case Qgis::FilePathType::Absolute:
|
|
break;
|
|
|
|
case Qgis::FilePathType::Relative:
|
|
{
|
|
// for projects stored in a custom storage, we need to ask to the
|
|
// storage for the path, if the storage returns an empty path
|
|
// relative paths are not supported
|
|
if ( QgsProjectStorage *storage = projectStorage() )
|
|
{
|
|
filePath = storage->filePath( mFile.fileName() );
|
|
}
|
|
else
|
|
{
|
|
filePath = fileName();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return QgsPathResolver( filePath, mArchive->dir() );
|
|
}
|
|
|
|
QString QgsProject::readPath( const QString &src ) const
|
|
{
|
|
return pathResolver().readPath( src );
|
|
}
|
|
|
|
QString QgsProject::writePath( const QString &src ) const
|
|
{
|
|
return pathResolver().writePath( src );
|
|
}
|
|
|
|
void QgsProject::setError( const QString &errorMessage )
|
|
{
|
|
mErrorMessage = errorMessage;
|
|
}
|
|
|
|
QString QgsProject::error() const
|
|
{
|
|
return mErrorMessage;
|
|
}
|
|
|
|
void QgsProject::clearError()
|
|
{
|
|
setError( QString() );
|
|
}
|
|
|
|
void QgsProject::setBadLayerHandler( QgsProjectBadLayerHandler *handler )
|
|
{
|
|
delete mBadLayerHandler;
|
|
mBadLayerHandler = handler;
|
|
}
|
|
|
|
QString QgsProject::layerIsEmbedded( const QString &id ) const
|
|
{
|
|
const QHash< QString, QPair< QString, bool > >::const_iterator it = mEmbeddedLayers.find( id );
|
|
if ( it == mEmbeddedLayers.constEnd() )
|
|
{
|
|
return QString();
|
|
}
|
|
return it.value().first;
|
|
}
|
|
|
|
bool QgsProject::createEmbeddedLayer( const QString &layerId, const QString &projectFilePath, QList<QDomNode> &brokenNodes,
|
|
bool saveFlag, QgsProject::ReadFlags flags )
|
|
{
|
|
QgsDebugCall;
|
|
|
|
static QString sPrevProjectFilePath;
|
|
static QDateTime sPrevProjectFileTimestamp;
|
|
static QDomDocument sProjectDocument;
|
|
|
|
QString qgsProjectFile = projectFilePath;
|
|
QgsProjectArchive archive;
|
|
if ( projectFilePath.endsWith( QLatin1String( ".qgz" ), Qt::CaseInsensitive ) )
|
|
{
|
|
archive.unzip( projectFilePath );
|
|
qgsProjectFile = archive.projectFile();
|
|
}
|
|
|
|
const QDateTime projectFileTimestamp = QFileInfo( projectFilePath ).lastModified();
|
|
|
|
if ( projectFilePath != sPrevProjectFilePath || projectFileTimestamp != sPrevProjectFileTimestamp )
|
|
{
|
|
sPrevProjectFilePath.clear();
|
|
|
|
QFile projectFile( qgsProjectFile );
|
|
if ( !projectFile.open( QIODevice::ReadOnly ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( !sProjectDocument.setContent( &projectFile ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
sPrevProjectFilePath = projectFilePath;
|
|
sPrevProjectFileTimestamp = projectFileTimestamp;
|
|
}
|
|
|
|
// does project store paths absolute or relative?
|
|
bool useAbsolutePaths = true;
|
|
|
|
const QDomElement propertiesElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "properties" ) );
|
|
if ( !propertiesElem.isNull() )
|
|
{
|
|
const QDomElement absElem = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) ).firstChildElement( QStringLiteral( "Absolute" ) );
|
|
if ( !absElem.isNull() )
|
|
{
|
|
useAbsolutePaths = absElem.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
|
|
}
|
|
}
|
|
|
|
QgsReadWriteContext embeddedContext;
|
|
if ( !useAbsolutePaths )
|
|
embeddedContext.setPathResolver( QgsPathResolver( projectFilePath ) );
|
|
embeddedContext.setProjectTranslator( this );
|
|
embeddedContext.setTransformContext( transformContext() );
|
|
|
|
const QDomElement projectLayersElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "projectlayers" ) );
|
|
if ( projectLayersElem.isNull() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QDomElement mapLayerElem = projectLayersElem.firstChildElement( QStringLiteral( "maplayer" ) );
|
|
while ( ! mapLayerElem.isNull() )
|
|
{
|
|
// get layer id
|
|
const QString id = mapLayerElem.firstChildElement( QStringLiteral( "id" ) ).text();
|
|
if ( id == layerId )
|
|
{
|
|
// layer can be embedded only once
|
|
if ( mapLayerElem.attribute( QStringLiteral( "embedded" ) ) == QLatin1String( "1" ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mEmbeddedLayers.insert( layerId, qMakePair( projectFilePath, saveFlag ) );
|
|
|
|
if ( addLayer( mapLayerElem, brokenNodes, embeddedContext, flags ) )
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
mEmbeddedLayers.remove( layerId );
|
|
return false;
|
|
}
|
|
}
|
|
mapLayerElem = mapLayerElem.nextSiblingElement( QStringLiteral( "maplayer" ) );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
QgsLayerTreeGroup *QgsProject::createEmbeddedGroup( const QString &groupName, const QString &projectFilePath, const QStringList &invisibleLayers, QgsProject::ReadFlags flags )
|
|
{
|
|
QString qgsProjectFile = projectFilePath;
|
|
QgsProjectArchive archive;
|
|
if ( projectFilePath.endsWith( QLatin1String( ".qgz" ), Qt::CaseInsensitive ) )
|
|
{
|
|
archive.unzip( projectFilePath );
|
|
qgsProjectFile = archive.projectFile();
|
|
}
|
|
|
|
// open project file, get layer ids in group, add the layers
|
|
QFile projectFile( qgsProjectFile );
|
|
if ( !projectFile.open( QIODevice::ReadOnly ) )
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
QDomDocument projectDocument;
|
|
if ( !projectDocument.setContent( &projectFile ) )
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
QgsReadWriteContext context;
|
|
context.setPathResolver( pathResolver() );
|
|
context.setProjectTranslator( this );
|
|
context.setTransformContext( transformContext() );
|
|
|
|
QgsLayerTreeGroup *root = new QgsLayerTreeGroup;
|
|
|
|
QDomElement layerTreeElem = projectDocument.documentElement().firstChildElement( QStringLiteral( "layer-tree-group" ) );
|
|
if ( !layerTreeElem.isNull() )
|
|
{
|
|
root->readChildrenFromXml( layerTreeElem, context );
|
|
}
|
|
else
|
|
{
|
|
QgsLayerTreeUtils::readOldLegend( root, projectDocument.documentElement().firstChildElement( QStringLiteral( "legend" ) ) );
|
|
}
|
|
|
|
QgsLayerTreeGroup *group = root->findGroup( groupName );
|
|
if ( !group || group->customProperty( QStringLiteral( "embedded" ) ).toBool() )
|
|
{
|
|
// embedded groups cannot be embedded again
|
|
delete root;
|
|
return nullptr;
|
|
}
|
|
|
|
// clone the group sub-tree (it is used already in a tree, we cannot just tear it off)
|
|
QgsLayerTreeGroup *newGroup = QgsLayerTree::toGroup( group->clone() );
|
|
delete root;
|
|
root = nullptr;
|
|
|
|
newGroup->setCustomProperty( QStringLiteral( "embedded" ), 1 );
|
|
newGroup->setCustomProperty( QStringLiteral( "embedded_project" ), projectFilePath );
|
|
|
|
// set "embedded" to all children + load embedded layers
|
|
mLayerTreeRegistryBridge->setEnabled( false );
|
|
initializeEmbeddedSubtree( projectFilePath, newGroup, flags );
|
|
mLayerTreeRegistryBridge->setEnabled( true );
|
|
|
|
// consider the layers might be identify disabled in its project
|
|
const auto constFindLayerIds = newGroup->findLayerIds();
|
|
for ( const QString &layerId : constFindLayerIds )
|
|
{
|
|
QgsLayerTreeLayer *layer = newGroup->findLayer( layerId );
|
|
if ( layer )
|
|
{
|
|
layer->resolveReferences( this );
|
|
layer->setItemVisibilityChecked( !invisibleLayers.contains( layerId ) );
|
|
}
|
|
}
|
|
|
|
return newGroup;
|
|
}
|
|
|
|
void QgsProject::initializeEmbeddedSubtree( const QString &projectFilePath, QgsLayerTreeGroup *group, QgsProject::ReadFlags flags )
|
|
{
|
|
const auto constChildren = group->children();
|
|
for ( QgsLayerTreeNode *child : constChildren )
|
|
{
|
|
// all nodes in the subtree will have "embedded" custom property set
|
|
child->setCustomProperty( QStringLiteral( "embedded" ), 1 );
|
|
|
|
if ( QgsLayerTree::isGroup( child ) )
|
|
{
|
|
initializeEmbeddedSubtree( projectFilePath, QgsLayerTree::toGroup( child ), flags );
|
|
}
|
|
else if ( QgsLayerTree::isLayer( child ) )
|
|
{
|
|
// load the layer into our project
|
|
QList<QDomNode> brokenNodes;
|
|
createEmbeddedLayer( QgsLayerTree::toLayer( child )->layerId(), projectFilePath, brokenNodes, false, flags );
|
|
}
|
|
}
|
|
}
|
|
|
|
bool QgsProject::evaluateDefaultValues() const
|
|
{
|
|
return mEvaluateDefaultValues;
|
|
}
|
|
|
|
void QgsProject::setEvaluateDefaultValues( bool evaluateDefaultValues )
|
|
{
|
|
if ( evaluateDefaultValues == mEvaluateDefaultValues )
|
|
return;
|
|
|
|
const QMap<QString, QgsMapLayer *> layers = mapLayers();
|
|
QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
|
|
for ( ; layerIt != layers.constEnd(); ++layerIt )
|
|
{
|
|
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layerIt.value() );
|
|
if ( vl )
|
|
{
|
|
vl->dataProvider()->setProviderProperty( QgsVectorDataProvider::EvaluateDefaultValues, evaluateDefaultValues );
|
|
}
|
|
}
|
|
|
|
mEvaluateDefaultValues = evaluateDefaultValues;
|
|
}
|
|
|
|
void QgsProject::setTopologicalEditing( bool enabled )
|
|
{
|
|
writeEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/TopologicalEditing" ), ( enabled ? 1 : 0 ) );
|
|
emit topologicalEditingChanged();
|
|
}
|
|
|
|
bool QgsProject::topologicalEditing() const
|
|
{
|
|
return readNumEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/TopologicalEditing" ), 0 );
|
|
}
|
|
|
|
QgsUnitTypes::DistanceUnit QgsProject::distanceUnits() const
|
|
{
|
|
const QString distanceUnitString = readEntry( QStringLiteral( "Measurement" ), QStringLiteral( "/DistanceUnits" ), QString() );
|
|
if ( !distanceUnitString.isEmpty() )
|
|
return QgsUnitTypes::decodeDistanceUnit( distanceUnitString );
|
|
|
|
//fallback to QGIS default measurement unit
|
|
bool ok = false;
|
|
const QgsUnitTypes::DistanceUnit type = QgsUnitTypes::decodeDistanceUnit( mSettings.value( QStringLiteral( "/qgis/measure/displayunits" ) ).toString(), &ok );
|
|
return ok ? type : QgsUnitTypes::DistanceMeters;
|
|
}
|
|
|
|
void QgsProject::setDistanceUnits( QgsUnitTypes::DistanceUnit unit )
|
|
{
|
|
writeEntry( QStringLiteral( "Measurement" ), QStringLiteral( "/DistanceUnits" ), QgsUnitTypes::encodeUnit( unit ) );
|
|
}
|
|
|
|
QgsUnitTypes::AreaUnit QgsProject::areaUnits() const
|
|
{
|
|
const QString areaUnitString = readEntry( QStringLiteral( "Measurement" ), QStringLiteral( "/AreaUnits" ), QString() );
|
|
if ( !areaUnitString.isEmpty() )
|
|
return QgsUnitTypes::decodeAreaUnit( areaUnitString );
|
|
|
|
//fallback to QGIS default area unit
|
|
bool ok = false;
|
|
const QgsUnitTypes::AreaUnit type = QgsUnitTypes::decodeAreaUnit( mSettings.value( QStringLiteral( "/qgis/measure/areaunits" ) ).toString(), &ok );
|
|
return ok ? type : QgsUnitTypes::AreaSquareMeters;
|
|
}
|
|
|
|
void QgsProject::setAreaUnits( QgsUnitTypes::AreaUnit unit )
|
|
{
|
|
writeEntry( QStringLiteral( "Measurement" ), QStringLiteral( "/AreaUnits" ), QgsUnitTypes::encodeUnit( unit ) );
|
|
}
|
|
|
|
QString QgsProject::homePath() const
|
|
{
|
|
if ( !mCachedHomePath.isEmpty() )
|
|
return mCachedHomePath;
|
|
|
|
const QFileInfo pfi( fileName() );
|
|
|
|
if ( !mHomePath.isEmpty() )
|
|
{
|
|
const QFileInfo homeInfo( mHomePath );
|
|
if ( !homeInfo.isRelative() )
|
|
{
|
|
mCachedHomePath = mHomePath;
|
|
return mHomePath;
|
|
}
|
|
}
|
|
else if ( !fileName().isEmpty() )
|
|
{
|
|
mCachedHomePath = pfi.path();
|
|
|
|
return mCachedHomePath;
|
|
}
|
|
|
|
if ( !pfi.exists() )
|
|
{
|
|
mCachedHomePath = mHomePath;
|
|
return mHomePath;
|
|
}
|
|
|
|
if ( !mHomePath.isEmpty() )
|
|
{
|
|
// path is relative to project file
|
|
mCachedHomePath = QDir::cleanPath( pfi.path() + '/' + mHomePath );
|
|
}
|
|
else
|
|
{
|
|
mCachedHomePath = pfi.canonicalPath();
|
|
}
|
|
return mCachedHomePath;
|
|
}
|
|
|
|
QString QgsProject::presetHomePath() const
|
|
{
|
|
return mHomePath;
|
|
}
|
|
|
|
QgsRelationManager *QgsProject::relationManager() const
|
|
{
|
|
return mRelationManager;
|
|
}
|
|
|
|
const QgsLayoutManager *QgsProject::layoutManager() const
|
|
{
|
|
return mLayoutManager.get();
|
|
}
|
|
|
|
QgsLayoutManager *QgsProject::layoutManager()
|
|
{
|
|
return mLayoutManager.get();
|
|
}
|
|
|
|
const QgsBookmarkManager *QgsProject::bookmarkManager() const
|
|
{
|
|
return mBookmarkManager;
|
|
}
|
|
|
|
QgsBookmarkManager *QgsProject::bookmarkManager()
|
|
{
|
|
return mBookmarkManager;
|
|
}
|
|
|
|
const QgsProjectViewSettings *QgsProject::viewSettings() const
|
|
{
|
|
return mViewSettings;
|
|
}
|
|
|
|
QgsProjectViewSettings *QgsProject::viewSettings()
|
|
{
|
|
return mViewSettings;
|
|
}
|
|
|
|
const QgsProjectTimeSettings *QgsProject::timeSettings() const
|
|
{
|
|
return mTimeSettings;
|
|
}
|
|
|
|
QgsProjectTimeSettings *QgsProject::timeSettings()
|
|
{
|
|
return mTimeSettings;
|
|
}
|
|
|
|
const QgsProjectDisplaySettings *QgsProject::displaySettings() const
|
|
{
|
|
return mDisplaySettings;
|
|
}
|
|
|
|
QgsProjectDisplaySettings *QgsProject::displaySettings()
|
|
{
|
|
return mDisplaySettings;
|
|
}
|
|
|
|
QgsLayerTree *QgsProject::layerTreeRoot() const
|
|
{
|
|
return mRootGroup;
|
|
}
|
|
|
|
QgsMapThemeCollection *QgsProject::mapThemeCollection()
|
|
{
|
|
return mMapThemeCollection.get();
|
|
}
|
|
|
|
QgsAnnotationManager *QgsProject::annotationManager()
|
|
{
|
|
return mAnnotationManager.get();
|
|
}
|
|
|
|
const QgsAnnotationManager *QgsProject::annotationManager() const
|
|
{
|
|
return mAnnotationManager.get();
|
|
}
|
|
|
|
void QgsProject::setNonIdentifiableLayers( const QList<QgsMapLayer *> &layers )
|
|
{
|
|
const QMap<QString, QgsMapLayer *> &projectLayers = mapLayers();
|
|
for ( QMap<QString, QgsMapLayer *>::const_iterator it = projectLayers.constBegin(); it != projectLayers.constEnd(); ++it )
|
|
{
|
|
if ( layers.contains( it.value() ) == !it.value()->flags().testFlag( QgsMapLayer::Identifiable ) )
|
|
continue;
|
|
|
|
if ( layers.contains( it.value() ) )
|
|
it.value()->setFlags( it.value()->flags() & ~QgsMapLayer::Identifiable );
|
|
else
|
|
it.value()->setFlags( it.value()->flags() | QgsMapLayer::Identifiable );
|
|
}
|
|
|
|
Q_NOWARN_DEPRECATED_PUSH
|
|
emit nonIdentifiableLayersChanged( nonIdentifiableLayers() );
|
|
Q_NOWARN_DEPRECATED_POP
|
|
}
|
|
|
|
void QgsProject::setNonIdentifiableLayers( const QStringList &layerIds )
|
|
{
|
|
QList<QgsMapLayer *> nonIdentifiableLayers;
|
|
nonIdentifiableLayers.reserve( layerIds.count() );
|
|
for ( const QString &layerId : layerIds )
|
|
{
|
|
QgsMapLayer *layer = mapLayer( layerId );
|
|
if ( layer )
|
|
nonIdentifiableLayers << layer;
|
|
}
|
|
Q_NOWARN_DEPRECATED_PUSH
|
|
setNonIdentifiableLayers( nonIdentifiableLayers );
|
|
Q_NOWARN_DEPRECATED_POP
|
|
}
|
|
|
|
QStringList QgsProject::nonIdentifiableLayers() const
|
|
{
|
|
QStringList nonIdentifiableLayers;
|
|
|
|
const QMap<QString, QgsMapLayer *> &layers = mapLayers();
|
|
for ( QMap<QString, QgsMapLayer *>::const_iterator it = layers.constBegin(); it != layers.constEnd(); ++it )
|
|
{
|
|
if ( !it.value()->flags().testFlag( QgsMapLayer::Identifiable ) )
|
|
{
|
|
nonIdentifiableLayers.append( it.value()->id() );
|
|
}
|
|
}
|
|
return nonIdentifiableLayers;
|
|
}
|
|
|
|
bool QgsProject::autoTransaction() const
|
|
{
|
|
return mAutoTransaction;
|
|
}
|
|
|
|
void QgsProject::setAutoTransaction( bool autoTransaction )
|
|
{
|
|
if ( autoTransaction != mAutoTransaction )
|
|
{
|
|
mAutoTransaction = autoTransaction;
|
|
|
|
if ( autoTransaction )
|
|
onMapLayersAdded( mapLayers().values() );
|
|
else
|
|
cleanTransactionGroups( true );
|
|
}
|
|
}
|
|
|
|
QMap<QPair<QString, QString>, QgsTransactionGroup *> QgsProject::transactionGroups()
|
|
{
|
|
return mTransactionGroups;
|
|
}
|
|
|
|
|
|
//
|
|
// QgsMapLayerStore methods
|
|
//
|
|
|
|
|
|
int QgsProject::count() const
|
|
{
|
|
return mLayerStore->count();
|
|
}
|
|
|
|
int QgsProject::validCount() const
|
|
{
|
|
return mLayerStore->validCount();
|
|
}
|
|
|
|
QgsMapLayer *QgsProject::mapLayer( const QString &layerId ) const
|
|
{
|
|
return mLayerStore->mapLayer( layerId );
|
|
}
|
|
|
|
QList<QgsMapLayer *> QgsProject::mapLayersByName( const QString &layerName ) const
|
|
{
|
|
return mLayerStore->mapLayersByName( layerName );
|
|
}
|
|
|
|
QList<QgsMapLayer *> QgsProject::mapLayersByShortName( const QString &shortName ) const
|
|
{
|
|
QList<QgsMapLayer *> layers;
|
|
const auto constMapLayers { mLayerStore->mapLayers() };
|
|
for ( const auto &l : constMapLayers )
|
|
{
|
|
if ( ! l->shortName().isEmpty() )
|
|
{
|
|
if ( l->shortName() == shortName )
|
|
layers << l;
|
|
}
|
|
else if ( l->name() == shortName )
|
|
{
|
|
layers << l;
|
|
}
|
|
}
|
|
return layers;
|
|
}
|
|
|
|
bool QgsProject::unzip( const QString &filename, QgsProject::ReadFlags flags )
|
|
{
|
|
clearError();
|
|
std::unique_ptr<QgsProjectArchive> archive( new QgsProjectArchive() );
|
|
|
|
// unzip the archive
|
|
if ( !archive->unzip( filename ) )
|
|
{
|
|
setError( tr( "Unable to unzip file '%1'" ).arg( filename ) );
|
|
return false;
|
|
}
|
|
|
|
// test if zip provides a .qgs file
|
|
if ( archive->projectFile().isEmpty() )
|
|
{
|
|
setError( tr( "Zip archive does not provide a project file" ) );
|
|
return false;
|
|
}
|
|
|
|
// Keep the archive
|
|
mArchive = std::move( archive );
|
|
|
|
// load auxiliary storage
|
|
if ( !static_cast<QgsProjectArchive *>( mArchive.get() )->auxiliaryStorageFile().isEmpty() )
|
|
{
|
|
// database file is already a copy as it's been unzipped. So we don't open
|
|
// auxiliary storage in copy mode in this case
|
|
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( static_cast<QgsProjectArchive *>( mArchive.get() )->auxiliaryStorageFile(), false ) );
|
|
}
|
|
else
|
|
{
|
|
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( *this ) );
|
|
}
|
|
|
|
// read the project file
|
|
if ( ! readProjectFile( static_cast<QgsProjectArchive *>( mArchive.get() )->projectFile(), flags ) )
|
|
{
|
|
setError( tr( "Cannot read unzipped qgs project file" ) );
|
|
return false;
|
|
}
|
|
|
|
// Remove the temporary .qgs file
|
|
static_cast<QgsProjectArchive *>( mArchive.get() )->clearProjectFile();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QgsProject::zip( const QString &filename )
|
|
{
|
|
clearError();
|
|
|
|
// save the current project in a temporary .qgs file
|
|
std::unique_ptr<QgsProjectArchive> archive( new QgsProjectArchive() );
|
|
const QString baseName = QFileInfo( filename ).baseName();
|
|
const QString qgsFileName = QStringLiteral( "%1.qgs" ).arg( baseName );
|
|
QFile qgsFile( QDir( archive->dir() ).filePath( qgsFileName ) );
|
|
|
|
bool writeOk = false;
|
|
if ( qgsFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
|
|
{
|
|
writeOk = writeProjectFile( qgsFile.fileName() );
|
|
qgsFile.close();
|
|
}
|
|
|
|
// stop here with an error message
|
|
if ( ! writeOk )
|
|
{
|
|
setError( tr( "Unable to write temporary qgs file" ) );
|
|
return false;
|
|
}
|
|
|
|
// save auxiliary storage
|
|
const QFileInfo info( qgsFile );
|
|
const QString asExt = QStringLiteral( ".%1" ).arg( QgsAuxiliaryStorage::extension() );
|
|
const QString asFileName = info.path() + QDir::separator() + info.completeBaseName() + asExt;
|
|
|
|
bool auxiliaryStorageSavedOk = true;
|
|
if ( ! saveAuxiliaryStorage( asFileName ) )
|
|
{
|
|
const QString err = mAuxiliaryStorage->errorString();
|
|
setError( tr( "Unable to save auxiliary storage file ('%1'). The project has been saved but the latest changes to auxiliary data cannot be recovered. It is recommended to reload the project." ).arg( err ) );
|
|
auxiliaryStorageSavedOk = false;
|
|
|
|
// fixes the current archive and keep the previous version of qgd
|
|
if ( !mArchive->exists() )
|
|
{
|
|
mArchive.reset( new QgsProjectArchive() );
|
|
mArchive->unzip( mFile.fileName() );
|
|
static_cast<QgsProjectArchive *>( mArchive.get() )->clearProjectFile();
|
|
|
|
const QString auxiliaryStorageFile = static_cast<QgsProjectArchive *>( mArchive.get() )->auxiliaryStorageFile();
|
|
if ( ! auxiliaryStorageFile.isEmpty() )
|
|
{
|
|
archive->addFile( auxiliaryStorageFile );
|
|
mAuxiliaryStorage.reset( new QgsAuxiliaryStorage( auxiliaryStorageFile, false ) );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// in this case, an empty filename means that the auxiliary database is
|
|
// empty, so we don't want to save it
|
|
if ( QFile::exists( asFileName ) )
|
|
{
|
|
archive->addFile( asFileName );
|
|
}
|
|
}
|
|
|
|
// create the archive
|
|
archive->addFile( qgsFile.fileName() );
|
|
|
|
// Add all other files
|
|
const QStringList &files = mArchive->files();
|
|
for ( const QString &file : files )
|
|
{
|
|
if ( !file.endsWith( ".qgs", Qt::CaseInsensitive ) && !file.endsWith( asExt, Qt::CaseInsensitive ) )
|
|
{
|
|
archive->addFile( file );
|
|
}
|
|
}
|
|
|
|
// zip
|
|
bool zipOk = true;
|
|
if ( !archive->zip( filename ) )
|
|
{
|
|
setError( tr( "Unable to perform zip" ) );
|
|
zipOk = false;
|
|
}
|
|
|
|
return auxiliaryStorageSavedOk && zipOk;
|
|
}
|
|
|
|
bool QgsProject::isZipped() const
|
|
{
|
|
return QgsZipUtils::isZipFile( mFile.fileName() );
|
|
}
|
|
|
|
QList<QgsMapLayer *> QgsProject::addMapLayers(
|
|
const QList<QgsMapLayer *> &layers,
|
|
bool addToLegend,
|
|
bool takeOwnership )
|
|
{
|
|
const QList<QgsMapLayer *> myResultList { mLayerStore->addMapLayers( layers, takeOwnership ) };
|
|
if ( !myResultList.isEmpty() )
|
|
{
|
|
// Update transform context
|
|
for ( auto &l : myResultList )
|
|
{
|
|
l->setTransformContext( transformContext() );
|
|
}
|
|
if ( addToLegend )
|
|
{
|
|
emit legendLayersAdded( myResultList );
|
|
}
|
|
}
|
|
|
|
if ( mAuxiliaryStorage )
|
|
{
|
|
for ( QgsMapLayer *mlayer : myResultList )
|
|
{
|
|
if ( mlayer->type() != QgsMapLayerType::VectorLayer )
|
|
continue;
|
|
|
|
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mlayer );
|
|
if ( vl )
|
|
{
|
|
vl->loadAuxiliaryLayer( *mAuxiliaryStorage );
|
|
}
|
|
}
|
|
}
|
|
|
|
mProjectScope.reset();
|
|
|
|
return myResultList;
|
|
}
|
|
|
|
QgsMapLayer *
|
|
QgsProject::addMapLayer( QgsMapLayer *layer,
|
|
bool addToLegend,
|
|
bool takeOwnership )
|
|
{
|
|
QList<QgsMapLayer *> addedLayers;
|
|
addedLayers = addMapLayers( QList<QgsMapLayer *>() << layer, addToLegend, takeOwnership );
|
|
return addedLayers.isEmpty() ? nullptr : addedLayers[0];
|
|
}
|
|
|
|
void QgsProject::removeMapLayers( const QStringList &layerIds )
|
|
{
|
|
mProjectScope.reset();
|
|
mLayerStore->removeMapLayers( layerIds );
|
|
}
|
|
|
|
void QgsProject::removeMapLayers( const QList<QgsMapLayer *> &layers )
|
|
{
|
|
mProjectScope.reset();
|
|
mLayerStore->removeMapLayers( layers );
|
|
}
|
|
|
|
void QgsProject::removeMapLayer( const QString &layerId )
|
|
{
|
|
mProjectScope.reset();
|
|
mLayerStore->removeMapLayer( layerId );
|
|
}
|
|
|
|
void QgsProject::removeMapLayer( QgsMapLayer *layer )
|
|
{
|
|
mProjectScope.reset();
|
|
mLayerStore->removeMapLayer( layer );
|
|
}
|
|
|
|
QgsMapLayer *QgsProject::takeMapLayer( QgsMapLayer *layer )
|
|
{
|
|
mProjectScope.reset();
|
|
return mLayerStore->takeMapLayer( layer );
|
|
}
|
|
|
|
QgsAnnotationLayer *QgsProject::mainAnnotationLayer()
|
|
{
|
|
return mMainAnnotationLayer;
|
|
}
|
|
|
|
void QgsProject::removeAllMapLayers()
|
|
{
|
|
if ( mLayerStore->count() == 0 )
|
|
return;
|
|
|
|
ScopedIntIncrementor snapSingleBlocker( &mBlockSnappingUpdates );
|
|
mProjectScope.reset();
|
|
mLayerStore->removeAllMapLayers();
|
|
|
|
snapSingleBlocker.release();
|
|
mSnappingConfig.clearIndividualLayerSettings();
|
|
if ( !mBlockSnappingUpdates )
|
|
emit snappingConfigChanged( mSnappingConfig );
|
|
}
|
|
|
|
void QgsProject::reloadAllLayers()
|
|
{
|
|
const QMap<QString, QgsMapLayer *> layers = mLayerStore->mapLayers();
|
|
QMap<QString, QgsMapLayer *>::const_iterator it = layers.constBegin();
|
|
for ( ; it != layers.constEnd(); ++it )
|
|
{
|
|
it.value()->reload();
|
|
}
|
|
}
|
|
|
|
QMap<QString, QgsMapLayer *> QgsProject::mapLayers( const bool validOnly ) const
|
|
{
|
|
return validOnly ? mLayerStore->validMapLayers() : mLayerStore->mapLayers();
|
|
}
|
|
|
|
QgsTransactionGroup *QgsProject::transactionGroup( const QString &providerKey, const QString &connString )
|
|
{
|
|
return mTransactionGroups.value( qMakePair( providerKey, connString ) );
|
|
}
|
|
|
|
QgsCoordinateReferenceSystem QgsProject::defaultCrsForNewLayers() const
|
|
{
|
|
QgsCoordinateReferenceSystem defaultCrs;
|
|
|
|
// TODO QGIS 4.0 -- remove this method, and place it somewhere in app (where it belongs)
|
|
// in the meantime, we have a slightly hacky way to read the settings key using an enum which isn't available (since it lives in app)
|
|
if ( mSettings.value( QStringLiteral( "/projections/unknownCrsBehavior" ), QStringLiteral( "NoAction" ), QgsSettings::App ).toString() == QStringLiteral( "UseProjectCrs" )
|
|
|| mSettings.value( QStringLiteral( "/projections/unknownCrsBehavior" ), 0, QgsSettings::App ).toString() == QLatin1String( "2" ) )
|
|
{
|
|
// for new layers if the new layer crs method is set to either prompt or use project, then we use the project crs
|
|
defaultCrs = crs();
|
|
}
|
|
else
|
|
{
|
|
// global crs
|
|
const QString layerDefaultCrs = mSettings.value( QStringLiteral( "/Projections/layerDefaultCrs" ), geoEpsgCrsAuthId() ).toString();
|
|
defaultCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( layerDefaultCrs );
|
|
}
|
|
|
|
return defaultCrs;
|
|
}
|
|
|
|
void QgsProject::setTrustLayerMetadata( bool trust )
|
|
{
|
|
mTrustLayerMetadata = trust;
|
|
|
|
const auto layers = mapLayers();
|
|
for ( auto it = layers.constBegin(); it != layers.constEnd(); ++it )
|
|
{
|
|
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( it.value() );
|
|
if ( vl )
|
|
{
|
|
vl->setReadExtentFromXml( trust );
|
|
}
|
|
}
|
|
}
|
|
|
|
bool QgsProject::saveAuxiliaryStorage( const QString &filename )
|
|
{
|
|
const QMap<QString, QgsMapLayer *> layers = mapLayers();
|
|
bool empty = true;
|
|
for ( auto it = layers.constBegin(); it != layers.constEnd(); ++it )
|
|
{
|
|
if ( it.value()->type() != QgsMapLayerType::VectorLayer )
|
|
continue;
|
|
|
|
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( it.value() );
|
|
if ( vl && vl->auxiliaryLayer() )
|
|
{
|
|
vl->auxiliaryLayer()->save();
|
|
empty &= vl->auxiliaryLayer()->auxiliaryFields().isEmpty();
|
|
}
|
|
}
|
|
|
|
if ( !mAuxiliaryStorage->exists( *this ) && empty )
|
|
{
|
|
return true; // it's not an error
|
|
}
|
|
else if ( !filename.isEmpty() )
|
|
{
|
|
return mAuxiliaryStorage->saveAs( filename );
|
|
}
|
|
else
|
|
{
|
|
return mAuxiliaryStorage->saveAs( *this );
|
|
}
|
|
}
|
|
|
|
QgsPropertiesDefinition &QgsProject::dataDefinedServerPropertyDefinitions()
|
|
{
|
|
static QgsPropertiesDefinition sPropertyDefinitions
|
|
{
|
|
{
|
|
QgsProject::DataDefinedServerProperty::WMSOnlineResource,
|
|
QgsPropertyDefinition( "WMSOnlineResource", QObject::tr( "WMS Online Resource" ), QgsPropertyDefinition::String )
|
|
},
|
|
};
|
|
return sPropertyDefinitions;
|
|
}
|
|
|
|
const QgsAuxiliaryStorage *QgsProject::auxiliaryStorage() const
|
|
{
|
|
return mAuxiliaryStorage.get();
|
|
}
|
|
|
|
QgsAuxiliaryStorage *QgsProject::auxiliaryStorage()
|
|
{
|
|
return mAuxiliaryStorage.get();
|
|
}
|
|
|
|
QString QgsProject::createAttachedFile( const QString &nameTemplate )
|
|
{
|
|
const QDir archiveDir( mArchive->dir() );
|
|
QTemporaryFile tmpFile( archiveDir.filePath( "XXXXXX_" + nameTemplate ), this );
|
|
tmpFile.setAutoRemove( false );
|
|
tmpFile.open();
|
|
mArchive->addFile( tmpFile.fileName() );
|
|
return tmpFile.fileName();
|
|
}
|
|
|
|
QStringList QgsProject::attachedFiles() const
|
|
{
|
|
QStringList attachments;
|
|
const QString baseName = QFileInfo( fileName() ).baseName();
|
|
for ( const QString &file : mArchive->files() )
|
|
{
|
|
if ( QFileInfo( file ).baseName() != baseName )
|
|
{
|
|
attachments.append( file );
|
|
}
|
|
}
|
|
return attachments;
|
|
}
|
|
|
|
bool QgsProject::removeAttachedFile( const QString &path )
|
|
{
|
|
return mArchive->removeFile( path );
|
|
}
|
|
|
|
QString QgsProject::attachmentIdentifier( const QString &attachedFile ) const
|
|
{
|
|
return QStringLiteral( "attachment:///%1" ).arg( QFileInfo( attachedFile ).fileName() );
|
|
}
|
|
|
|
QString QgsProject::resolveAttachmentIdentifier( const QString &identifier ) const
|
|
{
|
|
if ( identifier.startsWith( "attachment:///" ) )
|
|
{
|
|
return QDir( mArchive->dir() ).absoluteFilePath( identifier.mid( 14 ) );
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
const QgsProjectMetadata &QgsProject::metadata() const
|
|
{
|
|
return mMetadata;
|
|
}
|
|
|
|
void QgsProject::setMetadata( const QgsProjectMetadata &metadata )
|
|
{
|
|
if ( metadata == mMetadata )
|
|
return;
|
|
|
|
mMetadata = metadata;
|
|
mProjectScope.reset();
|
|
|
|
emit metadataChanged();
|
|
|
|
setDirty( true );
|
|
}
|
|
|
|
QSet<QgsMapLayer *> QgsProject::requiredLayers() const
|
|
{
|
|
QSet<QgsMapLayer *> requiredLayers;
|
|
|
|
const QMap<QString, QgsMapLayer *> &layers = mapLayers();
|
|
for ( QMap<QString, QgsMapLayer *>::const_iterator it = layers.constBegin(); it != layers.constEnd(); ++it )
|
|
{
|
|
if ( !it.value()->flags().testFlag( QgsMapLayer::Removable ) )
|
|
{
|
|
requiredLayers.insert( it.value() );
|
|
}
|
|
}
|
|
return requiredLayers;
|
|
}
|
|
|
|
void QgsProject::setRequiredLayers( const QSet<QgsMapLayer *> &layers )
|
|
{
|
|
const QMap<QString, QgsMapLayer *> &projectLayers = mapLayers();
|
|
for ( QMap<QString, QgsMapLayer *>::const_iterator it = projectLayers.constBegin(); it != projectLayers.constEnd(); ++it )
|
|
{
|
|
if ( layers.contains( it.value() ) == !it.value()->flags().testFlag( QgsMapLayer::Removable ) )
|
|
continue;
|
|
|
|
if ( layers.contains( it.value() ) )
|
|
it.value()->setFlags( it.value()->flags() & ~QgsMapLayer::Removable );
|
|
else
|
|
it.value()->setFlags( it.value()->flags() | QgsMapLayer::Removable );
|
|
}
|
|
}
|
|
|
|
void QgsProject::setProjectColors( const QgsNamedColorList &colors )
|
|
{
|
|
// save colors to project
|
|
QStringList customColors;
|
|
QStringList customColorLabels;
|
|
|
|
QgsNamedColorList::const_iterator colorIt = colors.constBegin();
|
|
for ( ; colorIt != colors.constEnd(); ++colorIt )
|
|
{
|
|
const QString color = QgsSymbolLayerUtils::encodeColor( ( *colorIt ).first );
|
|
const QString label = ( *colorIt ).second;
|
|
customColors.append( color );
|
|
customColorLabels.append( label );
|
|
}
|
|
writeEntry( QStringLiteral( "Palette" ), QStringLiteral( "/Colors" ), customColors );
|
|
writeEntry( QStringLiteral( "Palette" ), QStringLiteral( "/Labels" ), customColorLabels );
|
|
mProjectScope.reset();
|
|
emit projectColorsChanged();
|
|
}
|
|
|
|
void QgsProject::setBackgroundColor( const QColor &color )
|
|
{
|
|
if ( mBackgroundColor == color )
|
|
return;
|
|
|
|
mBackgroundColor = color;
|
|
emit backgroundColorChanged();
|
|
}
|
|
|
|
QColor QgsProject::backgroundColor() const
|
|
{
|
|
return mBackgroundColor;
|
|
}
|
|
|
|
void QgsProject::setSelectionColor( const QColor &color )
|
|
{
|
|
if ( mSelectionColor == color )
|
|
return;
|
|
|
|
mSelectionColor = color;
|
|
emit selectionColorChanged();
|
|
}
|
|
|
|
QColor QgsProject::selectionColor() const
|
|
{
|
|
return mSelectionColor;
|
|
}
|
|
|
|
void QgsProject::setMapScales( const QVector<double> &scales )
|
|
{
|
|
mViewSettings->setMapScales( scales );
|
|
}
|
|
|
|
QVector<double> QgsProject::mapScales() const
|
|
{
|
|
return mViewSettings->mapScales();
|
|
}
|
|
|
|
void QgsProject::setUseProjectScales( bool enabled )
|
|
{
|
|
mViewSettings->setUseProjectScales( enabled );
|
|
}
|
|
|
|
bool QgsProject::useProjectScales() const
|
|
{
|
|
return mViewSettings->useProjectScales();
|
|
}
|
|
|
|
void QgsProject::generateTsFile( const QString &locale )
|
|
{
|
|
QgsTranslationContext translationContext;
|
|
translationContext.setProject( this );
|
|
translationContext.setFileName( QStringLiteral( "%1/%2.ts" ).arg( absolutePath(), baseName() ) );
|
|
|
|
QgsApplication::instance()->collectTranslatableObjects( &translationContext );
|
|
|
|
translationContext.writeTsFile( locale );
|
|
}
|
|
|
|
QString QgsProject::translate( const QString &context, const QString &sourceText, const char *disambiguation, int n ) const
|
|
{
|
|
if ( !mTranslator )
|
|
{
|
|
return sourceText;
|
|
}
|
|
|
|
QString result = mTranslator->translate( context.toUtf8(), sourceText.toUtf8(), disambiguation, n );
|
|
|
|
if ( result.isEmpty() )
|
|
{
|
|
return sourceText;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool QgsProject::accept( QgsStyleEntityVisitorInterface *visitor ) const
|
|
{
|
|
const QMap<QString, QgsMapLayer *> layers = mapLayers( false );
|
|
if ( !layers.empty() )
|
|
{
|
|
for ( auto it = layers.constBegin(); it != layers.constEnd(); ++it )
|
|
{
|
|
// NOTE: if visitEnter returns false it means "don't visit this layer", not "abort all further visitations"
|
|
if ( visitor->visitEnter( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layer, ( *it )->id(), ( *it )->name() ) ) )
|
|
{
|
|
if ( !( ( *it )->accept( visitor ) ) )
|
|
return false;
|
|
|
|
if ( !visitor->visitExit( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layer, ( *it )->id(), ( *it )->name() ) ) )
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !mLayoutManager->accept( visitor ) )
|
|
return false;
|
|
|
|
if ( !mAnnotationManager->accept( visitor ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// @cond PRIVATE
|
|
GetNamedProjectColor::GetNamedProjectColor( const QgsProject *project )
|
|
: QgsScopedExpressionFunction( QStringLiteral( "project_color" ), 1, QStringLiteral( "Color" ) )
|
|
{
|
|
if ( !project )
|
|
return;
|
|
|
|
//build up color list from project. Do this in advance for speed
|
|
QStringList colorStrings = project->readListEntry( QStringLiteral( "Palette" ), QStringLiteral( "/Colors" ) );
|
|
const QStringList colorLabels = project->readListEntry( QStringLiteral( "Palette" ), QStringLiteral( "/Labels" ) );
|
|
|
|
//generate list from custom colors
|
|
int colorIndex = 0;
|
|
for ( QStringList::iterator it = colorStrings.begin();
|
|
it != colorStrings.end(); ++it )
|
|
{
|
|
const QColor color = QgsSymbolLayerUtils::decodeColor( *it );
|
|
QString label;
|
|
if ( colorLabels.length() > colorIndex )
|
|
{
|
|
label = colorLabels.at( colorIndex );
|
|
}
|
|
|
|
mColors.insert( label.toLower(), color );
|
|
colorIndex++;
|
|
}
|
|
}
|
|
|
|
GetNamedProjectColor::GetNamedProjectColor( const QHash<QString, QColor> &colors )
|
|
: QgsScopedExpressionFunction( QStringLiteral( "project_color" ), 1, QStringLiteral( "Color" ) )
|
|
, mColors( colors )
|
|
{
|
|
}
|
|
|
|
QVariant GetNamedProjectColor::func( const QVariantList &values, const QgsExpressionContext *, QgsExpression *, const QgsExpressionNodeFunction * )
|
|
{
|
|
const QString colorName = values.at( 0 ).toString().toLower();
|
|
if ( mColors.contains( colorName ) )
|
|
{
|
|
return QStringLiteral( "%1,%2,%3" ).arg( mColors.value( colorName ).red() ).arg( mColors.value( colorName ).green() ).arg( mColors.value( colorName ).blue() );
|
|
}
|
|
else
|
|
return QVariant();
|
|
}
|
|
|
|
QgsScopedExpressionFunction *GetNamedProjectColor::clone() const
|
|
{
|
|
return new GetNamedProjectColor( mColors );
|
|
}
|
|
///@endcond
|