diff --git a/DB/ImageInfo.cpp b/DB/ImageInfo.cpp index 24243485..130ba255 100644 --- a/DB/ImageInfo.cpp +++ b/DB/ImageInfo.cpp @@ -1,827 +1,829 @@ /* Copyright (C) 2003-2015 Jesper K. Pedersen 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. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ImageInfo.h" #include "FileInfo.h" #include "Logging.h" #include #include #include #include #include #include #include #include #include #include #include using namespace DB; ImageInfo::ImageInfo() :m_null( true ), m_rating(-1), m_stackId(0), m_stackOrder(0) , m_videoLength(-1) , m_locked( false ), m_dirty( false ), m_delaySaving( false ) { } ImageInfo::ImageInfo( const DB::FileName& fileName, MediaType type, bool readExifInfo, bool storeExifInfo) : m_imageOnDisk( YesOnDisk ), m_null( false ), m_size( -1, -1 ), m_type( type ) , m_rating(-1), m_stackId(0), m_stackOrder(0) , m_videoLength(-1) , m_locked(false), m_delaySaving( true ) { QFileInfo fi( fileName.absolute() ); m_label = fi.completeBaseName(); m_angle = 0; setFileName(fileName); // Read EXIF information if ( readExifInfo ) { ExifMode mode = EXIFMODE_INIT; if ( ! storeExifInfo) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif(fileName, mode); } m_dirty = false; m_delaySaving = false; } /** Change delaying of saving changes. * * Will save changes when set to false. * * Use this method to set multiple attributes with only one * database operation. * * Example: * \code * info.delaySavingChanges(true); * info.setLabel("Hello"); * info.setDescription("Hello world"); * info.delaySavingChanges(false); * \endcode * * \see saveChanges() */ void ImageInfo::delaySavingChanges(bool b) { m_delaySaving = b; if (!b) saveChanges(); } void ImageInfo::setLabel( const QString& desc ) { if (desc != m_label) m_dirty = true; m_label = desc; saveChangesIfNotDelayed(); } QString ImageInfo::label() const { return m_label; } void ImageInfo::setDescription( const QString& desc ) { if (desc != m_description) m_dirty = true; m_description = desc.trimmed(); saveChangesIfNotDelayed(); } QString ImageInfo::description() const { return m_description; } void ImageInfo::setCategoryInfo( const QString& key, const StringSet& value ) { // Don't check if really changed, because it's too slow. m_dirty = true; m_categoryInfomation[key] = value; saveChangesIfNotDelayed(); } bool ImageInfo::hasCategoryInfo( const QString& key, const QString& value ) const { return m_categoryInfomation[key].contains(value); } bool DB::ImageInfo::hasCategoryInfo( const QString& key, const StringSet& values ) const { // OpenSuse leap 42.1 still ships with Qt 5.5 // TODO: remove this version check once we don't care about Qt 5.6 anymore... #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) return values.intersects( m_categoryInfomation[key] ); #else StringSet tmp = values; return ! tmp.intersect( m_categoryInfomation[key] ).isEmpty(); #endif } StringSet ImageInfo::itemsOfCategory( const QString& key ) const { return m_categoryInfomation[key]; } void ImageInfo::renameItem( const QString& category, const QString& oldValue, const QString& newValue ) { if (m_taggedAreas.contains(category)) { if (m_taggedAreas[category].contains(oldValue)) { m_taggedAreas[category][newValue] = m_taggedAreas[category][oldValue]; m_taggedAreas[category].remove(oldValue); } } StringSet& set = m_categoryInfomation[category]; StringSet::iterator it = set.find( oldValue ); if ( it != set.end() ) { m_dirty = true; set.erase( it ); set.insert( newValue ); saveChangesIfNotDelayed(); } } DB::FileName ImageInfo::fileName() const { return m_fileName; } void ImageInfo::setFileName( const DB::FileName& fileName ) { if (fileName != m_fileName) m_dirty = true; m_fileName = fileName; m_imageOnDisk = Unchecked; DB::CategoryPtr folderCategory = DB::ImageDB::instance()->categoryCollection()-> categoryForSpecial(DB::Category::FolderCategory); if (folderCategory) { DB::MemberMap& map = DB::ImageDB::instance()->memberMap(); createFolderCategoryItem( folderCategory, map ); //ImageDB::instance()->setMemberMap( map ); } saveChangesIfNotDelayed(); } void ImageInfo::rotate( int degrees, RotationMode mode ) { // ensure positive degrees: degrees += 360; degrees = degrees % 360; if ( degrees == 0 ) return; m_dirty = true; m_angle = ( m_angle + degrees ) % 360; if (degrees == 90 || degrees == 270) { m_size.transpose(); } // the AnnotationDialog manages this by itself and sets RotateImageInfoOnly: if ( mode == RotateImageInfoAndAreas ) { for ( auto& areasOfCategory : m_taggedAreas ) { for ( auto& area : areasOfCategory ) { QRect rotatedArea; // parameter order for QRect::setCoords: // setCoords( left, top, right, bottom ) // keep in mind that _size is already transposed switch (degrees) { case 90: rotatedArea.setCoords( m_size.width() - area.bottom(), area.left(), m_size.width() - area.top(), area.right() ); break; case 180: rotatedArea.setCoords( m_size.width() - area.right(), m_size.height() - area.bottom(), m_size.width() - area.left(), m_size.height() - area.top() ); break; case 270: rotatedArea.setCoords( area.top(), m_size.height() - area.right(), area.bottom(), m_size.height() - area.left() ); break; default: // degrees==0; "odd" values won't happen. rotatedArea = area; break; } // update _taggedAreas[category][tag]: area = rotatedArea; } } } saveChangesIfNotDelayed(); } int ImageInfo::angle() const { return m_angle; } void ImageInfo::setAngle( int angle ) { if (angle != m_angle) m_dirty = true; m_angle = angle; saveChangesIfNotDelayed(); } short ImageInfo::rating() const { return m_rating; } void ImageInfo::setRating( short rating ) { Q_ASSERT( (rating >= 0 && rating <= 10) || rating == -1 ); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; if ( m_rating != rating ) m_dirty = true; m_rating = rating; saveChangesIfNotDelayed(); } DB::StackID ImageInfo::stackId() const { return m_stackId; } void ImageInfo::setStackId( const DB::StackID stackId ) { if ( stackId != m_stackId ) m_dirty = true; m_stackId = stackId; saveChangesIfNotDelayed(); } unsigned int ImageInfo::stackOrder() const { return m_stackOrder; } void ImageInfo::setStackOrder( const unsigned int stackOrder ) { if ( stackOrder != m_stackOrder ) m_dirty = true; m_stackOrder = stackOrder; saveChangesIfNotDelayed(); } void ImageInfo::setVideoLength(int length) { if ( m_videoLength != length ) m_dirty = true; m_videoLength = length; saveChangesIfNotDelayed(); } int ImageInfo::videoLength() const { return m_videoLength; } void ImageInfo::setDate( const ImageDate& date ) { if (date != m_date) m_dirty = true; m_date = date; saveChangesIfNotDelayed(); } ImageDate& ImageInfo::date() { return m_date; } ImageDate ImageInfo::date() const { return m_date; } bool ImageInfo::operator!=( const ImageInfo& other ) const { return !(*this == other); } bool ImageInfo::operator==( const ImageInfo& other ) const { bool changed = ( m_fileName != other.m_fileName || m_label != other.m_label || ( !m_description.isEmpty() && !other.m_description.isEmpty() && m_description != other.m_description ) || // one might be isNull. m_date != other.m_date || m_angle != other.m_angle || m_rating != other.m_rating || ( m_stackId != other.m_stackId || ! ( ( m_stackId == 0 ) ? true : ( m_stackOrder == other.m_stackOrder ) ) ) ); if ( !changed ) { QStringList keys = DB::ImageDB::instance()->categoryCollection()->categoryNames(); for( QStringList::ConstIterator it = keys.constBegin(); it != keys.constEnd(); ++it ) changed |= m_categoryInfomation[*it] != other.m_categoryInfomation[*it]; } return !changed; } void ImageInfo::renameCategory( const QString& oldName, const QString& newName ) { m_dirty = true; m_categoryInfomation[newName] = m_categoryInfomation[oldName]; m_categoryInfomation.remove(oldName); m_taggedAreas[newName] = m_taggedAreas[oldName]; m_taggedAreas.remove(oldName); saveChangesIfNotDelayed(); } void ImageInfo::setMD5Sum( const MD5& sum, bool storeEXIF ) { if (sum != m_md5sum) { // if we make a QObject derived class out of imageinfo, we might invalidate thumbnails from here // file changed -> reload/invalidate metadata: ExifMode mode = EXIFMODE_ORIENTATION | EXIFMODE_DATABASE_UPDATE; // fuzzy dates are usually set for a reason if (!m_date.isFuzzy()) mode |= EXIFMODE_DATE; // FIXME (ZaJ): the "right" thing to do would be to update the description // - if it is currently empty (done.) // - if it has been set from the exif info and not been changed (TODO) if (m_description.isEmpty()) mode |= EXIFMODE_DESCRIPTION; if (!storeEXIF) mode &= ~EXIFMODE_DATABASE_UPDATE; readExif( fileName(), mode); // FIXME (ZaJ): it *should* make sense to set the ImageDB::md5Map() from here, but I want // to make sure I fully understand everything first... // this could also be done as signal md5Changed(old,new) // image size is invalidated by the thumbnail builder, if needed m_dirty = true; } m_md5sum = sum; saveChangesIfNotDelayed(); } void ImageInfo::setLocked( bool locked ) { m_locked = locked; } bool ImageInfo::isLocked() const { return m_locked; } void ImageInfo::readExif(const DB::FileName& fullPath, DB::ExifMode mode) { DB::FileInfo exifInfo = DB::FileInfo::read( fullPath, mode ); bool oldDelaySaving = m_delaySaving; delaySavingChanges(true); // Date if ( updateDateInformation(mode) ) { const ImageDate newDate ( exifInfo.dateTime() ); setDate( newDate ); } // Orientation if ( (mode & EXIFMODE_ORIENTATION) && Settings::SettingsData::instance()->useEXIFRotate() ) { setAngle( exifInfo.angle() ); } // Description if ( (mode & EXIFMODE_DESCRIPTION) && Settings::SettingsData::instance()->useEXIFComments() ) { bool doSetDescription = true; QString desc = exifInfo.description(); if ( Settings::SettingsData::instance()->stripEXIFComments() ) { for( const auto& ignoredComment : Settings::SettingsData::instance()->EXIFCommentsToStrip() ) { if ( desc == ignoredComment ) { doSetDescription = false; break; } } } if (doSetDescription) { setDescription(desc); } } delaySavingChanges(false); m_delaySaving = oldDelaySaving; // Database update if ( mode & EXIFMODE_DATABASE_UPDATE ) { Exif::Database::instance()->remove( fullPath ); Exif::Database::instance()->add( fullPath ); #ifdef HAVE_KGEOMAP // GPS coords might have changed... m_coordsIsSet = false; #endif } } QStringList ImageInfo::availableCategories() const { return m_categoryInfomation.keys(); } QSize ImageInfo::size() const { return m_size; } void ImageInfo::setSize( const QSize& size ) { if (size != m_size) m_dirty = true; m_size = size; saveChangesIfNotDelayed(); } bool ImageInfo::imageOnDisk( const DB::FileName& fileName ) { return fileName.exists(); } ImageInfo::ImageInfo( const DB::FileName& fileName, const QString& label, const QString& description, const ImageDate& date, int angle, const MD5& md5sum, const QSize& size, MediaType type, short rating, unsigned int stackId, unsigned int stackOrder ) { m_delaySaving = true; m_fileName = fileName; m_label =label; m_description =description; m_date = date; m_angle =angle; m_md5sum =md5sum; m_size = size; m_imageOnDisk = Unchecked; m_locked = false; m_null = false; m_type = type; m_dirty = true; delaySavingChanges(false); if ( rating > 10 ) rating = 10; if ( rating < -1 ) rating = -1; m_rating = rating; m_stackId = stackId; m_stackOrder = stackOrder; m_videoLength= -1; } // TODO: we should get rid of this operator. It seems only be necessary // because of the 'delaySavings' field that gets a special value. // ImageInfo should just be a dumb data object holder and not incorporate // storing strategies. ImageInfo& ImageInfo::operator=( const ImageInfo& other ) { m_fileName = other.m_fileName; m_label = other.m_label; m_description = other.m_description; m_date = other.m_date; m_categoryInfomation = other.m_categoryInfomation; m_taggedAreas = other.m_taggedAreas; m_angle = other.m_angle; m_imageOnDisk = other.m_imageOnDisk; m_md5sum = other.m_md5sum; m_null = other.m_null; m_size = other.m_size; m_type = other.m_type; m_dirty = other.m_dirty; m_rating = other.m_rating; m_stackId = other.m_stackId; m_stackOrder = other.m_stackOrder; m_videoLength = other.m_videoLength; delaySavingChanges(false); return *this; } MediaType DB::ImageInfo::mediaType() const { return m_type; } bool ImageInfo::isVideo() const { return m_type == Video; } void DB::ImageInfo::createFolderCategoryItem( DB::CategoryPtr folderCategory, DB::MemberMap& memberMap ) { QString folderName = Utilities::relativeFolderName( m_fileName.relative() ); if ( folderName.isEmpty() ) return; - QStringList directories = folderName.split(QString::fromLatin1( "/" ) ); - - QString curPath; - for( QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt ) { - if ( curPath.isEmpty() ) - curPath = *directoryIt; - else { - QString oldPath = curPath; - curPath = curPath + QString::fromLatin1( "/" ) + *directoryIt; - memberMap.addMemberToGroup( folderCategory->name(), oldPath, curPath ); + if ( ! memberMap.contains( folderCategory->name(), folderName ) ) { + QStringList directories = folderName.split(QString::fromLatin1( "/" ) ); + + QString curPath; + for( QStringList::ConstIterator directoryIt = directories.constBegin(); directoryIt != directories.constEnd(); ++directoryIt ) { + if ( curPath.isEmpty() ) + curPath = *directoryIt; + else { + QString oldPath = curPath; + curPath = curPath + QString::fromLatin1( "/" ) + *directoryIt; + memberMap.addMemberToGroup( folderCategory->name(), oldPath, curPath ); + } } + folderCategory->addItem( folderName ); } m_categoryInfomation.insert( folderCategory->name() , StringSet() << folderName ); - folderCategory->addItem( folderName ); } void DB::ImageInfo::copyExtraData( const DB::ImageInfo& from, bool copyAngle) { m_categoryInfomation = from.m_categoryInfomation; m_description = from.m_description; // Hmm... what should the date be? orig or modified? // _date = from._date; if (copyAngle) m_angle = from.m_angle; m_rating = from.m_rating; } void DB::ImageInfo::removeExtraData () { m_categoryInfomation.clear(); m_description.clear(); m_rating = -1; } void ImageInfo::merge(const ImageInfo &other) { // Merge description if ( !other.description().isEmpty() ) { if ( m_description.isEmpty() ) m_description = other.description(); else if (m_description != other.description()) m_description += QString::fromUtf8("\n-----------\n") + other.m_description; } // Clear untagged tag if one of the images was untagged const QString untaggedCategory = Settings::SettingsData::instance()->untaggedCategory(); const QString untaggedTag = Settings::SettingsData::instance()->untaggedTag(); const bool isCompleted = !m_categoryInfomation[untaggedCategory].contains(untaggedTag) || !other.m_categoryInfomation[untaggedCategory].contains(untaggedTag); // Merge tags QSet keys = QSet::fromList(m_categoryInfomation.keys()); keys.unite(QSet::fromList(other.m_categoryInfomation.keys())); for( const QString& key : keys) { m_categoryInfomation[key].unite(other.m_categoryInfomation[key]); } // Clear untagged tag if one of the images was untagged if (isCompleted) m_categoryInfomation[untaggedCategory].remove(untaggedTag); // merge stacks: if (isStacked() || other.isStacked()) { DB::FileNameList stackImages; if (!isStacked()) stackImages.append(fileName()); else stackImages.append(DB::ImageDB::instance()->getStackFor(fileName())); stackImages.append(DB::ImageDB::instance()->getStackFor(other.fileName())); DB::ImageDB::instance()->unstack(stackImages); if (!DB::ImageDB::instance()->stack(stackImages)) qCWarning(DBLog, "Could not merge stacks!"); } } void DB::ImageInfo::addCategoryInfo( const QString& category, const StringSet& values ) { for ( StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt ) { if (! m_categoryInfomation[category].contains( *valueIt ) ) { m_dirty = true; m_categoryInfomation[category].insert( *valueIt ); } } saveChangesIfNotDelayed(); } void DB::ImageInfo::clearAllCategoryInfo() { m_categoryInfomation.clear(); m_taggedAreas.clear(); } void DB::ImageInfo::removeCategoryInfo( const QString& category, const StringSet& values ) { for ( StringSet::const_iterator valueIt = values.constBegin(); valueIt != values.constEnd(); ++valueIt ) { if ( m_categoryInfomation[category].contains( *valueIt ) ) { m_dirty = true; m_categoryInfomation[category].remove(*valueIt); m_taggedAreas[category].remove(*valueIt); } } saveChangesIfNotDelayed(); } void DB::ImageInfo::addCategoryInfo( const QString& category, const QString& value, const QRect& area ) { if (! m_categoryInfomation[category].contains( value ) ) { m_dirty = true; m_categoryInfomation[category].insert( value ); if (area.isValid()) { m_taggedAreas[category][value] = area; } } saveChangesIfNotDelayed(); } void DB::ImageInfo::removeCategoryInfo( const QString& category, const QString& value ) { if ( m_categoryInfomation[category].contains( value ) ) { m_dirty = true; m_categoryInfomation[category].remove( value ); m_taggedAreas[category].remove( value ); } saveChangesIfNotDelayed(); } void DB::ImageInfo::setPositionedTags(const QString& category, const QMap &positionedTags) { m_dirty = true; m_taggedAreas[category] = positionedTags; saveChangesIfNotDelayed(); } bool DB::ImageInfo::updateDateInformation( int mode ) const { if ((mode & EXIFMODE_DATE) == 0) return false; if ( (mode & EXIFMODE_FORCE) != 0 ) return true; return true; } QMap> DB::ImageInfo::taggedAreas() const { return m_taggedAreas; } QRect DB::ImageInfo::areaForTag(QString category, QString tag) const { // QMap::value returns a default constructed value if the key is not found: return m_taggedAreas.value(category).value(tag); } #ifdef HAVE_KGEOMAP KGeoMap::GeoCoordinates DB::ImageInfo::coordinates() const { if (m_coordsIsSet) { return m_coordinates; } static const int EXIF_GPS_VERSIONID = 0; static const int EXIF_GPS_LATREF = 1; static const int EXIF_GPS_LAT = 2; static const int EXIF_GPS_LONREF = 3; static const int EXIF_GPS_LON = 4; static const int EXIF_GPS_ALTREF = 5; static const int EXIF_GPS_ALT = 6; static const QString S = QString::fromUtf8("S"); static const QString W = QString::fromUtf8("W"); static QList fields; if (fields.isEmpty()) { // the order here matters! we use the named int constants afterwards to refer to them: fields.append( new Exif::IntExifElement( "Exif.GPSInfo.GPSVersionID" ) ); // actually a byte value fields.append( new Exif::StringExifElement( "Exif.GPSInfo.GPSLatitudeRef" ) ); fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSLatitude" ) ); fields.append( new Exif::StringExifElement( "Exif.GPSInfo.GPSLongitudeRef" ) ); fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSLongitude" ) ); fields.append( new Exif::IntExifElement( "Exif.GPSInfo.GPSAltitudeRef" ) ); // actually a byte value fields.append( new Exif::RationalExifElement( "Exif.GPSInfo.GPSAltitude" ) ); } // read field values from database: bool foundIt = Exif::Database::instance()->readFields( m_fileName, fields ); // if the Database query result doesn't contain exif GPS info (-> upgraded exifdb from DBVersion < 2), it is null // if the result is int 0, then there's no exif gps information in the image // otherwise we can proceed to parse the information if ( foundIt && fields[EXIF_GPS_VERSIONID]->value().isNull() ) { // update exif DB and repeat the search: Exif::Database::instance()->remove( fileName() ); Exif::Database::instance()->add( fileName() ); Exif::Database::instance()->readFields( m_fileName, fields ); Q_ASSERT( !fields[EXIF_GPS_VERSIONID]->value().isNull() ); } KGeoMap::GeoCoordinates coords; // gps info set? // don't use the versionid field here, because some cameras use 0 as its value if ( foundIt && fields[EXIF_GPS_LAT]->value().toInt() != -1.0 && fields[EXIF_GPS_LON]->value().toInt() != -1.0 ) { // lat/lon/alt reference determines sign of float: double latr = (fields[EXIF_GPS_LATREF]->value().toString() == S ) ? -1.0 : 1.0; double lat = fields[EXIF_GPS_LAT]->value().toFloat(); double lonr = (fields[EXIF_GPS_LONREF]->value().toString() == W ) ? -1.0 : 1.0; double lon = fields[EXIF_GPS_LON]->value().toFloat(); double altr = (fields[EXIF_GPS_ALTREF]->value().toInt() == 1 ) ? -1.0 : 1.0; double alt = fields[EXIF_GPS_ALT]->value().toFloat(); if (lat != -1.0 && lon != -1.0) { coords.setLatLon(latr * lat, lonr * lon); if (alt != 0.0f) { coords.setAlt(altr * alt); } } } m_coordinates = coords; m_coordsIsSet = true; return m_coordinates; } #endif // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MemberMap.cpp b/DB/MemberMap.cpp index 501dfcd1..4b6d3ac5 100644 --- a/DB/MemberMap.cpp +++ b/DB/MemberMap.cpp @@ -1,342 +1,361 @@ /* Copyright (C) 2003-2018 Jesper K. Pedersen 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. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "MemberMap.h" #include "Logging.h" #include "DB/Category.h" using namespace DB; MemberMap::MemberMap() :QObject(nullptr), m_dirty( true ), m_loading( false ) { } /** returns the groups directly available from category (non closure that is) */ QStringList MemberMap::groups( const QString& category ) const { return QStringList( m_members[ category ].keys() ); } +bool MemberMap::contains( const QString& category, const QString& item) const +{ + return m_flatMembers[category].contains(item); +} + +void MemberMap::doDirty( const QString& category ) +{ + if ( m_loading ) + regenerateFlatList( category ); + else + emit dirty(); +} + void MemberMap::deleteGroup( const QString& category, const QString& name ) { m_members[category].remove(name); m_dirty = true; - if ( !m_loading ) - emit dirty(); + doDirty(category); } /** return all the members of memberGroup */ QStringList MemberMap::members( const QString& category, const QString& memberGroup, bool closure ) const { if ( closure ) { if ( m_dirty ) calculate(); return m_closureMembers[category][memberGroup].toList(); } else return m_members[category][memberGroup].toList(); } void MemberMap::setMembers( const QString& category, const QString& memberGroup, const QStringList& members ) { StringSet allowedMembers = members.toSet(); for (QStringList::const_iterator i = members.begin(); i != members.end(); ++i) if (!canAddMemberToGroup(category, memberGroup, *i)) allowedMembers.remove(*i); m_members[category][memberGroup] = allowedMembers; m_dirty = true; - if ( !m_loading ) - emit dirty(); + doDirty( category ); } bool MemberMap::isEmpty() const { return m_members.empty(); } /** returns true if item is a group for category. */ bool MemberMap::isGroup( const QString& category, const QString& item ) const { return m_members[category].find(item) != m_members[category].end(); } /** return a map from groupName to list of items for category example: { USA |-> [Chicago, Grand Canyon, Santa Clara], Denmark |-> [Esbjerg, Odense] } */ QMap MemberMap::groupMap( const QString& category ) const { if ( m_dirty ) calculate(); return m_closureMembers[category]; } /** Calculates the closure for group, that is finds all members for group. Imagine there is a group called USA, and that this groups has a group inside it called Califonia, Califonia consists of members San Fransisco and Los Angeless. This function then maps USA to include Califonia, San Fransisco and Los Angeless. */ QStringList MemberMap::calculateClosure( QMap& resultSoFar, const QString& category, const QString& group ) const { resultSoFar[group] = StringSet(); // Prevent against cykles. StringSet members = m_members[category][group]; StringSet result = members; for( StringSet::const_iterator it = members.begin(); it != members.end(); ++it ) { if ( resultSoFar.contains( *it ) ) { result += resultSoFar[*it]; } else if ( isGroup(category, *it ) ) { result += calculateClosure( resultSoFar, category, *it ).toSet(); } } resultSoFar[group] = result; return result.toList(); } /** This methods create the map _closureMembers from _members This is simply to avoid finding the closure each and every time it is needed. */ void MemberMap::calculate() const { m_closureMembers.clear(); // run through all categories for( QMap< QString,QMap >::ConstIterator categoryIt= m_members.begin(); categoryIt != m_members.end(); ++categoryIt ) { QString category = categoryIt.key(); QMap groupMap = categoryIt.value(); // Run through each of the groups for the given categories for( QMap::const_iterator groupIt= groupMap.constBegin() ; groupIt != groupMap.constEnd() ; ++groupIt ) { QString group = groupIt.key(); if ( m_closureMembers[category].find( group ) == m_closureMembers[category].end() ) { (void) calculateClosure( m_closureMembers[category], category, group ); } } } m_dirty = false; } void MemberMap::renameGroup( const QString& category, const QString& oldName, const QString& newName ) { // Don't allow overwriting to avoid creating cycles if (m_members[category].contains(newName)) return; m_dirty = true; - if ( !m_loading ) - emit dirty(); + doDirty( category ); QMap& groupMap = m_members[category]; groupMap.insert(newName,m_members[category][oldName] ); groupMap.remove( oldName ); for( StringSet &set: groupMap ) { if ( set.contains( oldName ) ) { set.remove( oldName ); set.insert( newName ); } } } MemberMap::MemberMap( const MemberMap& other ) : QObject( nullptr ), m_members( other.memberMap() ), m_dirty( true ), m_loading( false ) { } void MemberMap::deleteItem( DB::Category* category, const QString& name) { QMap& groupMap = m_members[category->name()]; for( StringSet &items: groupMap ) { items.remove( name ); } m_members[category->name()].remove(name); m_dirty = true; - if ( !m_loading ) - emit dirty(); + doDirty( category->name() ); } void MemberMap::renameItem( DB::Category* category, const QString& oldName, const QString& newName ) { if (oldName == newName) return; QMap& groupMap = m_members[category->name()]; for( StringSet &items: groupMap ) { if (items.contains( oldName ) ) { items.remove( oldName ); items.insert( newName ); } } if ( groupMap.contains( oldName ) ) { groupMap[newName] = groupMap[oldName]; groupMap.remove(oldName); } m_dirty = true; - if ( !m_loading ) - emit dirty(); + doDirty( category->name() ); } MemberMap& MemberMap::operator=( const MemberMap& other ) { if ( this != &other ) { m_members = other.memberMap(); m_dirty = true; } return *this; } - +void MemberMap::regenerateFlatList( const QString& category ) +{ + m_flatMembers[category].clear(); + for (QMap::const_iterator i = m_members[category].constBegin(); + i != m_members[category].constEnd(); i++) { + for (StringSet::const_iterator j = i.value().constBegin(); j != i.value().constEnd(); j++) { + m_flatMembers[category].insert( *j ); + } + } +} void MemberMap::addMemberToGroup( const QString& category, const QString& group, const QString& item ) { // Only test for cycles after database is already loaded if (!m_loading && !canAddMemberToGroup(category, group, item)) return; if ( item.isEmpty() ) { qCWarning(DBLog, "Null item tried inserted into group %s", qPrintable(group)); return; } m_members[category][group].insert( item ); + m_flatMembers[category].insert( item ); if (m_loading) { m_dirty = true; } else if (!m_dirty) { // Update _closureMembers to avoid marking it dirty QMap& categoryClosure = m_closureMembers[category]; categoryClosure[group].insert(item); QMap::const_iterator closureOfItem = categoryClosure.constFind(item); const StringSet* closureOfItemPtr(nullptr); if (closureOfItem != categoryClosure.constEnd()) { closureOfItemPtr = &(*closureOfItem); categoryClosure[group] += *closureOfItem; } for (QMap::iterator i = categoryClosure.begin(); i != categoryClosure.end(); ++i) if ((*i).contains(group)) { (*i).insert(item); if (closureOfItemPtr) (*i) += *closureOfItemPtr; } } + // If we are loading, we do *not* want to regenerate the list! if ( !m_loading ) emit dirty(); } void MemberMap::removeMemberFromGroup( const QString& category, const QString& group, const QString& item ) { Q_ASSERT( m_members.contains(category) ); - if ( m_members[category].contains( group ) ) + if ( m_members[category].contains( group ) ) { m_members[category][group].remove( item ); - m_dirty = true; - if ( !m_loading ) - emit dirty(); + // We shouldn't be doing this very often, so just regenerate + // the flat list + m_dirty = true; + doDirty( category ); + } } void MemberMap::addGroup( const QString& category, const QString& group ) { if ( ! m_members[category].contains( group ) ) { m_members[category].insert( group, StringSet() ); } - if ( !m_loading ) - emit dirty(); + doDirty( category ); } void MemberMap::renameCategory( const QString& oldName, const QString& newName ) { if (oldName == newName) return; m_members[newName] = m_members[oldName]; m_members.remove(oldName); m_closureMembers[newName] = m_closureMembers[oldName]; m_closureMembers.remove(oldName); if ( !m_loading ) emit dirty(); } void MemberMap::deleteCategory(const QString &category) { m_members.remove(category); m_closureMembers.remove(category); - if ( !m_loading ) - emit dirty(); + doDirty( category ); } QMap DB::MemberMap::inverseMap( const QString& category ) const { QMap res; const QMap& map = m_members[category]; for( QMap::ConstIterator mapIt = map.begin(); mapIt != map.end(); ++mapIt ) { QString group = mapIt.key(); StringSet members = mapIt.value(); for( StringSet::const_iterator memberIt = members.begin(); memberIt != members.end(); ++memberIt ) { res[*memberIt].insert( group ); } } return res; } bool DB::MemberMap::hasPath( const QString& category, const QString& from, const QString& to ) const { if (from == to) return true; else if (!m_members[category].contains(from)) // Try to avoid calculate(), which is quite time consuming. return false; else { // return members(category, from, true).contains(to); if ( m_dirty ) calculate(); return m_closureMembers[category][from].contains(to); } } void DB::MemberMap::setLoading( bool b ) { if (m_loading && !b) { // TODO: Remove possible loaded cycles. } m_loading = b; } // vi:expandtab:tabstop=4 shiftwidth=4: diff --git a/DB/MemberMap.h b/DB/MemberMap.h index 0b2e79c5..a12643e5 100644 --- a/DB/MemberMap.h +++ b/DB/MemberMap.h @@ -1,97 +1,101 @@ /* Copyright (C) 2003-2010 Jesper K. Pedersen 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. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MEMBERMAP_H #define MEMBERMAP_H #include #include #include #include "Utilities/StringSet.h" namespace DB { using Utilities::StringSet; class Category; class MemberMap :public QObject { Q_OBJECT public: MemberMap(); MemberMap( const MemberMap& ); virtual MemberMap& operator=( const MemberMap& ); // TODO: this should return a StringSet virtual QStringList groups( const QString& category ) const; virtual void deleteGroup( const QString& category, const QString& name ); // TODO: this should return a StringSet virtual QStringList members( const QString& category, const QString& memberGroup, bool closure ) const; virtual void setMembers( const QString& category, const QString& memberGroup, const QStringList& members ); virtual bool isEmpty() const; virtual bool isGroup( const QString& category, const QString& memberGroup ) const; virtual QMap groupMap( const QString& category ) const; virtual QMap inverseMap( const QString& category ) const; virtual void renameGroup( const QString& category, const QString& oldName, const QString& newName ); virtual void renameCategory( const QString& oldName, const QString& newName ); virtual void addGroup( const QString& category, const QString& group ); bool canAddMemberToGroup( const QString& category, const QString& group, const QString& item ) const { // If there already is a path from item to group then adding the // item to group would create a cycle, which we don't want. return !hasPath(category, item, group); } virtual void addMemberToGroup( const QString& category, const QString& group, const QString& item ); virtual void removeMemberFromGroup( const QString& category, const QString& group, const QString& item ); virtual const QMap >& memberMap() const { return m_members; } virtual bool hasPath( const QString& category, const QString& from, const QString& to ) const; + virtual bool contains(const QString& category, const QString& item) const; protected: void calculate() const; QStringList calculateClosure( QMap& resultSoFar, const QString& category, const QString& group ) const; public slots: virtual void deleteCategory( const QString& category ); virtual void deleteItem( DB::Category* category, const QString& name); virtual void renameItem( DB::Category* category, const QString& oldName, const QString& newName ); void setLoading( bool b ); signals: void dirty(); private: + void doDirty( const QString& category ); + void regenerateFlatList( const QString& category ); // This is the primary data structure // { category |-> { group |-> [ member ] } } <- VDM syntax ;-) QMap > m_members; + mutable QMap > m_flatMembers; // These are the data structures used to develop closures, they are only // needed to speed up the program *SIGNIFICANTLY* ;-) mutable bool m_dirty; mutable QMap > m_closureMembers; bool m_loading; }; } #endif /* MEMBERMAP_H */ // vi:expandtab:tabstop=4 shiftwidth=4: