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 ¤tFB : 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"