File indexing completed on 2024-04-28 05:11:31

0001 /*
0002   SPDX-FileCopyrightText: 2000, 2001, 2004 Cornelius Schumacher <schumacher@kde.org>
0003   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0004   SPDX-FileCopyrightText: 2010 Andras Mantia <andras@kdab.com>
0005   SPDX-FileCopyrightText: 2010 Casey Link <casey@kdab.com>
0006 
0007   SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "conflictresolver.h"
0011 #include "incidenceeditor_debug.h"
0012 #include <CalendarSupport/FreeBusyItemModel>
0013 
0014 #include <QDate>
0015 
0016 static const int DEFAULT_RESOLUTION_SECONDS = 15 * 60; // 15 minutes, 1 slot = 15 minutes
0017 
0018 using namespace IncidenceEditorNG;
0019 
0020 ConflictResolver::ConflictResolver(QWidget *parentWidget, QObject *parent)
0021     : QObject(parent)
0022     , mFBModel(new CalendarSupport::FreeBusyItemModel(this))
0023     , mParentWidget(parentWidget)
0024     , mWeekdays(7)
0025     , mSlotResolutionSeconds(DEFAULT_RESOLUTION_SECONDS)
0026 {
0027     const QDateTime currentLocalDateTime = QDateTime::currentDateTime();
0028     mTimeframeConstraint = KCalendarCore::Period(currentLocalDateTime, currentLocalDateTime);
0029 
0030     // trigger a reload in case any attendees were inserted before
0031     // the connection was made
0032     // triggerReload();
0033 
0034     // set default values, all the days
0035     mWeekdays.setBit(0); // Monday
0036     mWeekdays.setBit(1);
0037     mWeekdays.setBit(2);
0038     mWeekdays.setBit(3);
0039     mWeekdays.setBit(4);
0040     mWeekdays.setBit(5);
0041     mWeekdays.setBit(6); // Sunday
0042 
0043     mMandatoryRoles.reserve(4);
0044     mMandatoryRoles << KCalendarCore::Attendee::ReqParticipant << KCalendarCore::Attendee::OptParticipant << KCalendarCore::Attendee::NonParticipant
0045                     << KCalendarCore::Attendee::Chair;
0046 
0047     connect(mFBModel, &CalendarSupport::FreeBusyItemModel::dataChanged, this, &ConflictResolver::freebusyDataChanged);
0048 
0049     connect(&mCalculateTimer, &QTimer::timeout, this, &ConflictResolver::findAllFreeSlots);
0050     mCalculateTimer.setSingleShot(true);
0051 }
0052 
0053 void ConflictResolver::insertAttendee(const KCalendarCore::Attendee &attendee)
0054 {
0055     if (!mFBModel->containsAttendee(attendee)) {
0056         mFBModel->addItem(CalendarSupport::FreeBusyItem::Ptr(new CalendarSupport::FreeBusyItem(attendee, mParentWidget)));
0057     }
0058 }
0059 
0060 void ConflictResolver::insertAttendee(const CalendarSupport::FreeBusyItem::Ptr &freebusy)
0061 {
0062     if (!mFBModel->containsAttendee(freebusy->attendee())) {
0063         mFBModel->addItem(freebusy);
0064     }
0065 }
0066 
0067 void ConflictResolver::removeAttendee(const KCalendarCore::Attendee &attendee)
0068 {
0069     mFBModel->removeAttendee(attendee);
0070     calculateConflicts();
0071 }
0072 
0073 void ConflictResolver::clearAttendees()
0074 {
0075     mFBModel->clear();
0076 }
0077 
0078 bool ConflictResolver::containsAttendee(const KCalendarCore::Attendee &attendee)
0079 {
0080     return mFBModel->containsAttendee(attendee);
0081 }
0082 
0083 void ConflictResolver::setEarliestDate(QDate newDate)
0084 {
0085     QDateTime newStart = mTimeframeConstraint.start();
0086     newStart.setDate(newDate);
0087     mTimeframeConstraint = KCalendarCore::Period(newStart, mTimeframeConstraint.end());
0088     calculateConflicts();
0089 }
0090 
0091 void ConflictResolver::setEarliestTime(QTime newTime)
0092 {
0093     QDateTime newStart = mTimeframeConstraint.start();
0094     newStart.setTime(newTime);
0095     mTimeframeConstraint = KCalendarCore::Period(newStart, mTimeframeConstraint.end());
0096     calculateConflicts();
0097 }
0098 
0099 void ConflictResolver::setLatestDate(QDate newDate)
0100 {
0101     QDateTime newEnd = mTimeframeConstraint.end();
0102     newEnd.setDate(newDate);
0103     mTimeframeConstraint = KCalendarCore::Period(mTimeframeConstraint.start(), newEnd);
0104     calculateConflicts();
0105 }
0106 
0107 void ConflictResolver::setLatestTime(QTime newTime)
0108 {
0109     QDateTime newEnd = mTimeframeConstraint.end();
0110     newEnd.setTime(newTime);
0111     mTimeframeConstraint = KCalendarCore::Period(mTimeframeConstraint.start(), newEnd);
0112     calculateConflicts();
0113 }
0114 
0115 void ConflictResolver::setEarliestDateTime(const QDateTime &newDateTime)
0116 {
0117     mTimeframeConstraint = KCalendarCore::Period(newDateTime, mTimeframeConstraint.end());
0118     calculateConflicts();
0119 }
0120 
0121 void ConflictResolver::setLatestDateTime(const QDateTime &newDateTime)
0122 {
0123     mTimeframeConstraint = KCalendarCore::Period(mTimeframeConstraint.start(), newDateTime);
0124     calculateConflicts();
0125 }
0126 
0127 void ConflictResolver::freebusyDataChanged()
0128 {
0129     calculateConflicts();
0130 }
0131 
0132 int ConflictResolver::tryDate(QDateTime &tryFrom, QDateTime &tryTo)
0133 {
0134     int conflicts_count = 0;
0135     for (int i = 0; i < mFBModel->rowCount(); ++i) {
0136         QModelIndex index = mFBModel->index(i);
0137         auto attendee = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value<KCalendarCore::Attendee>();
0138         if (!matchesRoleConstraint(attendee)) {
0139             continue;
0140         }
0141         auto freebusy = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value<KCalendarCore::FreeBusy::Ptr>();
0142         if (!tryDate(freebusy, tryFrom, tryTo)) {
0143             ++conflicts_count;
0144         }
0145     }
0146     return conflicts_count;
0147 }
0148 
0149 bool ConflictResolver::tryDate(const KCalendarCore::FreeBusy::Ptr &fb, QDateTime &tryFrom, QDateTime &tryTo)
0150 {
0151     // If we don't have any free/busy information, assume the
0152     // participant is free. Otherwise a participant without available
0153     // information would block the whole allocation.
0154     if (!fb) {
0155         return true;
0156     }
0157 
0158     KCalendarCore::Period::List busyPeriods = fb->busyPeriods();
0159     for (auto it = busyPeriods.begin(); it != busyPeriods.end(); ++it) {
0160         if ((*it).end() <= tryFrom // busy period ends before try period
0161             || (*it).start() >= tryTo) { // busy period starts after try period
0162             continue;
0163         } else {
0164             // the current busy period blocks the try period, try
0165             // after the end of the current busy period
0166             const qint64 secsDuration = tryFrom.secsTo(tryTo);
0167             tryFrom = (*it).end();
0168             tryTo = tryFrom.addSecs(secsDuration);
0169             // try again with the new try period
0170             tryDate(fb, tryFrom, tryTo);
0171             // we had to change the date at least once
0172             return false;
0173         }
0174     }
0175     return true;
0176 }
0177 
0178 bool ConflictResolver::findFreeSlot(const KCalendarCore::Period &dateTimeRange)
0179 {
0180     QDateTime dtFrom = dateTimeRange.start();
0181     QDateTime dtTo = dateTimeRange.end();
0182     if (tryDate(dtFrom, dtTo)) {
0183         // Current time is acceptable
0184         return true;
0185     }
0186 
0187     QDateTime tryFrom = dtFrom;
0188     QDateTime tryTo = dtTo;
0189 
0190     // Make sure that we never suggest a date in the past, even if the
0191     // user originally scheduled the meeting to be in the past.
0192     QDateTime now = QDateTime::currentDateTimeUtc();
0193     if (tryFrom < now) {
0194         // The slot to look for is at least partially in the past.
0195         const qint64 secs = tryFrom.secsTo(tryTo);
0196         tryFrom = now;
0197         tryTo = tryFrom.addSecs(secs);
0198     }
0199 
0200     bool found = false;
0201     while (!found) {
0202         found = tryDate(tryFrom, tryTo);
0203         // PENDING(kalle) Make the interval configurable
0204         if (!found && dtFrom.daysTo(tryFrom) > 365) {
0205             break; // don't look more than one year in the future
0206         }
0207     }
0208 
0209     dtFrom = tryFrom;
0210     dtTo = tryTo;
0211 
0212     return found;
0213 }
0214 
0215 void ConflictResolver::findAllFreeSlots()
0216 {
0217     // Uses an O(p*n) (n number of attendees, p timeframe range / timeslot resolution ) algorithm to
0218     // locate all free blocks in a given timeframe that match the search constraints.
0219     // Does so by:
0220     // 1. convert each attendees schedule for the timeframe into a bitarray according to
0221     //    the time resolution, where each time slot has a value of 1 = busy, 0 = free.
0222     // 2. align the arrays vertically, and sum the columns
0223     // 3. the resulting summation indicates # of conflicts at each timeslot
0224     // 4. locate contiguous timeslots with a values of 0. these are the free time blocks.
0225 
0226     // define these locally for readability
0227     const QDateTime begin = mTimeframeConstraint.start();
0228     const QDateTime end = mTimeframeConstraint.end();
0229 
0230     // calculate the time resolution
0231     // each timeslot in the arrays represents a unit of time
0232     // specified here.
0233     if (mSlotResolutionSeconds < 1) {
0234         // fallback to default, if the user's value is invalid
0235         mSlotResolutionSeconds = DEFAULT_RESOLUTION_SECONDS;
0236     }
0237 
0238     // calculate the length of the timeframe in terms of the amount of timeslots.
0239     // Example: 1 week timeframe, with resolution of 15 minutes
0240     //          1 week = 10080 minutes / 15 = 672 15 min timeslots
0241     //          So, the array would have a length of 672
0242     const int range = begin.secsTo(end) / mSlotResolutionSeconds;
0243     if (range <= 0) {
0244         qCWarning(INCIDENCEEDITOR_LOG) << "free slot calculation: invalid range. range( " << begin.secsTo(end) << ") / mSlotResolutionSeconds("
0245                                        << mSlotResolutionSeconds << ") = " << range;
0246         return;
0247     }
0248 
0249     qCDebug(INCIDENCEEDITOR_LOG) << "from " << begin << " to " << end << "; mSlotResolutionSeconds = " << mSlotResolutionSeconds << "; range = " << range;
0250     // filter out attendees for which we don't have FB data
0251     // and which don't match the mandatory role constraint
0252     QList<KCalendarCore::FreeBusy::Ptr> filteredFBItems;
0253     for (int i = 0; i < mFBModel->rowCount(); ++i) {
0254         QModelIndex index = mFBModel->index(i);
0255         auto attendee = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::AttendeeRole).value<KCalendarCore::Attendee>();
0256         if (!matchesRoleConstraint(attendee)) {
0257             continue;
0258         }
0259         auto freebusy = mFBModel->data(index, CalendarSupport::FreeBusyItemModel::FreeBusyRole).value<KCalendarCore::FreeBusy::Ptr>();
0260         if (freebusy) {
0261             filteredFBItems << freebusy;
0262         }
0263     }
0264 
0265     // now we know the number of attendees we are calculating for
0266     const int number_attendees = filteredFBItems.size();
0267     if (number_attendees <= 0) {
0268         qCDebug(INCIDENCEEDITOR_LOG) << "no attendees match search criteria";
0269         return;
0270     }
0271     qCDebug(INCIDENCEEDITOR_LOG) << "num attendees: " << number_attendees;
0272     // this is a 2 dimensional array where the rows are attendees
0273     // and the columns are 0 or 1 denoting free or busy respectively.
0274     QList<QList<int>> fbTable;
0275 
0276     // Explanation of the following loop:
0277     // iterate: through each attendee
0278     //   allocate: an array of length <range> and fill it with 0s
0279     //   iterate: through each attendee's busy period
0280     //     if: the period lies inside our timeframe
0281     //     then:
0282     //       calculate the array index within the timeframe range of the beginning of the busy period
0283     //       fill from that index until the period ends with a 1, representing busy
0284     //     fi
0285     //   etareti
0286     // append the allocated array to <fbTable>
0287     // etareti
0288     for (const KCalendarCore::FreeBusy::Ptr &currentFB : std::as_const(filteredFBItems)) {
0289         Q_ASSERT(currentFB); // sanity check
0290         const KCalendarCore::Period::List busyPeriods = currentFB->busyPeriods();
0291         QList<int> fbArray(range);
0292         fbArray.fill(0); // initialize to zero
0293         for (const auto &period : busyPeriods) {
0294             if (period.end() >= begin && period.start() <= end) {
0295                 int start_index = -1; // Initialize it to an invalid value.
0296                 int duration = -1; // Initialize it to an invalid value.
0297                 // case1: the period is completely in our timeframe
0298                 if (period.end() <= end && period.start() >= begin) {
0299                     start_index = begin.secsTo(period.start()) / mSlotResolutionSeconds;
0300                     duration = period.start().secsTo(period.end()) / mSlotResolutionSeconds;
0301                     duration -= 1; // vector starts at 0
0302                     // case2: the period begins before our timeframe begins
0303                 } else if (period.start() <= begin && period.end() <= end) {
0304                     start_index = 0;
0305                     duration = (begin.secsTo(period.end()) / mSlotResolutionSeconds) - 1;
0306                     // case3: the period ends after our timeframe ends
0307                 } else if (period.end() >= end && period.start() >= begin) {
0308                     start_index = begin.secsTo(period.start()) / mSlotResolutionSeconds;
0309                     duration = range - start_index - 1;
0310                     // case4: case2+case3: our timeframe is inside the period
0311                 } else if (period.start() <= begin && period.end() >= end) {
0312                     start_index = 0;
0313                     duration = range - 1;
0314                 } else {
0315                     // QT5
0316                     // qCCritical(INCIDENCEEDITOR_LOG) << "impossible condition reached" << period.start() << period.end();
0317                 }
0318                 //      qCDebug(INCIDENCEEDITOR_LOG) << start_index << "+" << duration << "="
0319                 //               << start_index + duration << "<=" << range;
0320                 Q_ASSERT((start_index + duration) < range); // sanity check
0321                 for (int i = start_index; i <= start_index + duration; ++i) {
0322                     fbArray[i] = 1;
0323                 }
0324             }
0325         }
0326         Q_ASSERT(fbArray.size() == range); // sanity check
0327         fbTable.append(fbArray);
0328     }
0329 
0330     Q_ASSERT(fbTable.size() == number_attendees);
0331 
0332     // Now, create another array to represent the allowed weekdays constraints
0333     // All days which are not allowed, will be marked as busy
0334     QList<int> fbArray(range);
0335     fbArray.fill(0); // initialize to zero
0336     for (int slot = 0; slot < fbArray.size(); ++slot) {
0337         const QDateTime dateTime = begin.addSecs(slot * mSlotResolutionSeconds);
0338         const int dayOfWeek = dateTime.date().dayOfWeek() - 1; // bitarray is 0 indexed
0339         if (!mWeekdays[dayOfWeek]) {
0340             fbArray[slot] = 1;
0341         }
0342     }
0343     fbTable.append(fbArray);
0344 
0345     // Create the composite array that will hold the sums for
0346     // each 15 minute timeslot
0347     QList<int> summed(range);
0348     summed.fill(0); // initialize to zero
0349 
0350     // Sum the columns of the table
0351     for (int i = 0; i < fbTable.size(); ++i) {
0352         for (int j = 0; j < range; ++j) {
0353             summed[j] += fbTable[i][j];
0354         }
0355     }
0356 
0357     // Finally, iterate through the composite array locating contiguous free timeslots
0358     int free_count = 0;
0359     bool free_found = false;
0360     mAvailableSlots.clear();
0361     for (int i = 0; i < range; ++i) {
0362         // free timeslot encountered, increment counter
0363         if (summed[i] == 0) {
0364             ++free_count;
0365         }
0366         if (summed[i] != 0 || (i == (range - 1) && summed[i] == 0)) {
0367             // current slot is not free, so push the previous free blocks
0368             // OR we are in the last slot and it is free
0369             if (free_count > 0) {
0370                 int free_start_i; // start index of the free block
0371                 int free_end_i; // end index of the free block
0372                 if (summed[i] == 0) {
0373                     // special case: we are on the last slot and it is free
0374                     //               so we want to include this slot in the free block
0375                     free_start_i = i - free_count + 1; // add one, to set us back inside the array because
0376                     // free_count was incremented already this iteration
0377                     free_end_i = i + 1; // add one to compensate for the fact that the array is 0 indexed
0378                 } else {
0379                     free_start_i = i - free_count;
0380                     free_end_i = i - 1 + 1; // add one to compensate for the fact that the array is 0 indexed
0381                     // compiler will optimize out the -1+1, but I leave it here to make the reasoning apparent.
0382                 }
0383                 // convert from our timeslot interval back into to normal seconds
0384                 // then calculate the date times of the free block based on
0385                 // our initial timeframe
0386                 const QDateTime freeBegin = begin.addSecs(free_start_i * mSlotResolutionSeconds);
0387                 const QDateTime freeEnd = freeBegin.addSecs((free_end_i - free_start_i) * mSlotResolutionSeconds);
0388                 // push the free block onto the list
0389                 mAvailableSlots << KCalendarCore::Period(freeBegin, freeEnd);
0390                 free_count = 0;
0391                 if (!free_found) {
0392                     free_found = true;
0393                 }
0394             }
0395         }
0396     }
0397     if (free_found) {
0398         Q_EMIT freeSlotsAvailable(mAvailableSlots);
0399     }
0400 #if 0
0401     //DEBUG, dump the arrays. very helpful for debugging
0402     QTextStream dump(stdout);
0403     dump << "    ";
0404     dump.setFieldWidth(3);
0405     for (int i = 0; i < range; ++i) {   // header
0406         dump << i;
0407     }
0408     dump.setFieldWidth(1);
0409     dump << "\n\n";
0410     for (int i = 0; i < number_attendees; ++i) {
0411         dump.setFieldWidth(1);
0412         dump << i << ":  ";
0413         dump.setFieldWidth(3);
0414         for (int j = 0; j < range; ++j) {
0415             dump << fbTable[i][j];
0416         }
0417         dump << "\n\n";
0418     }
0419     dump.setFieldWidth(1);
0420     dump << "    ";
0421     dump.setFieldWidth(3);
0422     for (int i = 0; i < range; ++i) {
0423         dump << summed[i];
0424     }
0425     dump << "\n";
0426 #endif
0427 }
0428 
0429 void ConflictResolver::calculateConflicts()
0430 {
0431     QDateTime start = mTimeframeConstraint.start();
0432     QDateTime end = mTimeframeConstraint.end();
0433     const int count = tryDate(start, end);
0434     Q_EMIT conflictsDetected(count);
0435 
0436     if (!mCalculateTimer.isActive()) {
0437         mCalculateTimer.start(0);
0438     }
0439 }
0440 
0441 void ConflictResolver::setAllowedWeekdays(const QBitArray &weekdays)
0442 {
0443     mWeekdays = weekdays;
0444     calculateConflicts();
0445 }
0446 
0447 void ConflictResolver::setMandatoryRoles(const QSet<KCalendarCore::Attendee::Role> &roles)
0448 {
0449     mMandatoryRoles = roles;
0450     calculateConflicts();
0451 }
0452 
0453 bool ConflictResolver::matchesRoleConstraint(const KCalendarCore::Attendee &attendee)
0454 {
0455     return mMandatoryRoles.contains(attendee.role());
0456 }
0457 
0458 KCalendarCore::Period::List ConflictResolver::availableSlots() const
0459 {
0460     return mAvailableSlots;
0461 }
0462 
0463 void ConflictResolver::setResolution(int seconds)
0464 {
0465     mSlotResolutionSeconds = seconds;
0466 }
0467 
0468 CalendarSupport::FreeBusyItemModel *ConflictResolver::model() const
0469 {
0470     return mFBModel;
0471 }
0472 
0473 #include "moc_conflictresolver.cpp"