001package org.cpsolver.studentsct.constraint;
002
003import java.util.Set;
004
005import org.cpsolver.coursett.Constants;
006import org.cpsolver.coursett.model.TimeLocation;
007import org.cpsolver.ifs.assignment.Assignment;
008import org.cpsolver.ifs.model.GlobalConstraint;
009import org.cpsolver.studentsct.StudentSectioningModel;
010import org.cpsolver.studentsct.extension.StudentQuality;
011import org.cpsolver.studentsct.model.Config;
012import org.cpsolver.studentsct.model.Enrollment;
013import org.cpsolver.studentsct.model.Request;
014import org.cpsolver.studentsct.model.SctAssignment;
015import org.cpsolver.studentsct.model.Section;
016import org.cpsolver.studentsct.model.Student;
017import org.cpsolver.studentsct.model.Unavailability;
018
019/**
020 * Hard distance conflicts constraint. This global constraint checks for distance conflicts
021 * that should not be allowed. These are distance conflicts where the distance betweem the
022 * two sections is longer than HardDistanceConflict.DistanceHardLimitInMinutes minutes (defaults to 60)
023 * and the distance to travel between the two sections is longer than
024 * HardDistanceConflict.AllowedDistanceInMinutes minutes (defaults to 30).
025 * The constraint checks both pairs of sections that the student is to be enrolled in 
026 * and distance conflicts with unavailabilities.
027 * Hard distance conflicts are allowed between sections that allow for time conflicts.
028 * 
029 * @author  Tomáš Müller
030 * @version StudentSct 1.4 (Student Sectioning)<br>
031 *          Copyright (C) 2007 - 2025 Tomáš Müller<br>
032 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
033 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
034 * <br>
035 *          This library is free software; you can redistribute it and/or modify
036 *          it under the terms of the GNU Lesser General Public License as
037 *          published by the Free Software Foundation; either version 3 of the
038 *          License, or (at your option) any later version. <br>
039 * <br>
040 *          This library is distributed in the hope that it will be useful, but
041 *          WITHOUT ANY WARRANTY; without even the implied warranty of
042 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
043 *          Lesser General Public License for more details. <br>
044 * <br>
045 *          You should have received a copy of the GNU Lesser General Public
046 *          License along with this library; if not see
047 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
048 */
049public class HardDistanceConflicts extends GlobalConstraint<Request, Enrollment> {
050    /**
051     * A given enrollment is conflicting, if there is a section that
052     * is disabled and there is not a matching reservation that would allow for that.
053     * 
054     * @param enrollment {@link Enrollment} that is being considered
055     * @param conflicts all computed conflicting requests are added into this set
056     */
057    @Override
058    public void computeConflicts(Assignment<Request, Enrollment> assignment, Enrollment enrollment, Set<Enrollment> conflicts) {
059        if (enrollment.variable().getModel() == null || !(enrollment.variable().getModel() instanceof StudentSectioningModel)) return;
060        StudentSectioningModel model = (StudentSectioningModel)enrollment.variable().getModel();
061        StudentQuality studentQuality = model.getStudentQuality();
062        if (studentQuality == null) return;
063        StudentQuality.Context cx = studentQuality.getStudentQualityContext();
064        
065        // no distance conflicts when overlaps are allowed by a reservation
066        if (enrollment.getReservation() != null && enrollment.getReservation().isAllowOverlap()) return;
067        
068        // enrollment's student
069        Student student = enrollment.getStudent();
070        // no unavailabilities > no distance conflicts
071        if (student.getUnavailabilities().isEmpty()) return;
072        
073        // enrollment's config
074        Config config = enrollment.getConfig();
075
076        // exclude free time requests
077        if (config == null) return;
078        
079        // check for an unavailability distance conflict
080        if (cx.getUnavailabilityDistanceMetric().isHardDistanceConflictsEnabled()) {
081            for (Section s1: enrollment.getSections()) {
082                // no time or no room > no conflict
083                if (!s1.hasTime() || s1.getNrRooms() == 0 || s1.isAllowOverlap()) continue;
084                for (Unavailability s2: student.getUnavailabilities()) {
085                    // no time or no room > no conflict
086                    if (s2.getTime() == null || s2.getNrRooms() == 0) continue;
087                    TimeLocation t1 = s1.getTime();
088                    TimeLocation t2 = s2.getTime();
089                    // no shared day > no conflict
090                    if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) continue;
091                    int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
092                    if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
093                        int dist = cx.getUnavailabilityDistanceInMinutes(s1.getPlacement(), s2);
094                        if (dist >= cx.getUnavailabilityDistanceMetric().getDistanceHardLimitInMinutes()
095                                && dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getUnavailabilityDistanceMetric().getAllowedDistanceInMinutes()) {
096                            conflicts.add(enrollment);
097                            return;
098                        }
099                    } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
100                        int dist = cx.getUnavailabilityDistanceInMinutes(s1.getPlacement(), s2);
101                        if (dist >= cx.getUnavailabilityDistanceMetric().getDistanceHardLimitInMinutes()
102                                && dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getUnavailabilityDistanceMetric().getAllowedDistanceInMinutes()) {
103                            conflicts.add(enrollment);
104                            return;
105                        }
106                    }
107                }
108            }
109        }
110        
111        // check for distance conflicts within the enrollment
112        if (cx.getDistanceMetric().isHardDistanceConflictsEnabled()) {
113            for (Section s1: enrollment.getSections()) {
114                // no time or no room > no conflict
115                if (!s1.hasTime() || s1.getNrRooms() == 0 || s1.isAllowOverlap()) continue;
116                for (Section s2: enrollment.getSections()) {
117                    if (s1.getId() < s2.getId()) {
118                        // no time or no room > no conflict
119                        if (!s2.hasTime() || s2.getNrRooms() == 0 || s2.isAllowOverlap() || s1.isToIgnoreStudentConflictsWith(s2.getId())) continue;
120                        TimeLocation t1 = s1.getTime();
121                        TimeLocation t2 = s2.getTime();
122                        // no shared day > no conflict
123                        if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) continue;
124                        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
125                        if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
126                            if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
127                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
128                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
129                                        && dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
130                                    conflicts.add(enrollment);
131                                    return;
132                                }
133                            } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
134                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
135                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
136                                        && dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
137                                    conflicts.add(enrollment);
138                                    return;
139                                }
140                            }
141                        } else {
142                            if (a1 + t1.getNrSlotsPerMeeting() == a2) {
143                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
144                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
145                                        && dist > t1.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
146                                    conflicts.add(enrollment);
147                                    return;
148                                }
149                            } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
150                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
151                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
152                                        && dist > t2.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
153                                    conflicts.add(enrollment);
154                                    return;
155                                }
156                            }
157                        }
158                    }
159                }
160            }
161            
162            // check conflicts with other enrollments of the student
163            other: for (Request other: student.getRequests()) {
164                if (other.equals(enrollment.variable())) continue;
165                Enrollment e2 = other.getAssignment(assignment);
166                if (e2 == null || conflicts.contains(e2)) continue;
167                if (e2.getReservation() != null && e2.getReservation().isAllowOverlap()) continue;
168                for (Section s1: enrollment.getSections()) {
169                    // no time or no room > no conflict
170                    if (!s1.hasTime() || s1.getNrRooms() == 0 || s1.isAllowOverlap()) continue;
171                    for (Section s2: e2.getSections()) {
172                        // no time or no room > no conflict
173                        if (!s2.hasTime() || s2.getNrRooms() == 0 || s2.isAllowOverlap() || s1.isToIgnoreStudentConflictsWith(s2.getId())) continue;
174                        TimeLocation t1 = s1.getTime();
175                        TimeLocation t2 = s2.getTime();
176                        // no shared day > no conflict
177                        if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) continue;
178                        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
179                        if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
180                            if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
181                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
182                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
183                                        && dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
184                                    conflicts.add(e2);
185                                    continue other;
186                                }
187                            } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
188                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
189                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
190                                        && dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
191                                    conflicts.add(e2);
192                                    continue other;
193                                }
194                            }
195                        } else {
196                            if (a1 + t1.getNrSlotsPerMeeting() == a2) {
197                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
198                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
199                                        && dist > t1.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
200                                    conflicts.add(e2);
201                                    continue other;
202                                }
203                            } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
204                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
205                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
206                                        && dist > t2.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes()) {
207                                    conflicts.add(e2);
208                                    continue other;
209                                }
210                            }
211                        }
212                    }
213                }
214            }
215        }
216    }
217    
218    /**
219     * A given enrollment is conflicting, if there is a section that
220     * is disabled and there is not a matching reservation that would allow for that.
221     * 
222     * @param enrollment {@link Enrollment} that is being considered
223     * @return true, if the enrollment does not follow a reservation that must be used 
224     */
225    @Override
226    public boolean inConflict(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
227        if (enrollment.variable().getModel() == null || !(enrollment.variable().getModel() instanceof StudentSectioningModel)) return false;
228        StudentSectioningModel model = (StudentSectioningModel)enrollment.variable().getModel();
229        StudentQuality studentQuality = model.getStudentQuality();
230        if (studentQuality == null) return false;
231        StudentQuality.Context cx = studentQuality.getStudentQualityContext();
232        
233        // no distance conflicts when overlaps are allowed by a reservation
234        if (enrollment.getReservation() != null && enrollment.getReservation().isAllowOverlap()) return false;
235        
236        // enrollment's student
237        Student student = enrollment.getStudent();
238        // no unavailabilities > no distance conflicts
239        if (student.getUnavailabilities().isEmpty()) return false;
240        
241        // enrollment's config
242        Config config = enrollment.getConfig();
243
244        // exclude free time requests
245        if (config == null) return false;
246        
247        // check for an unavailability distance conflict
248        if (cx.getUnavailabilityDistanceMetric().isHardDistanceConflictsEnabled()) {
249            for (Section s1: enrollment.getSections()) {
250                // no time or no room > no conflict
251                if (!s1.hasTime() || s1.getNrRooms() == 0 || s1.isAllowOverlap()) continue;
252                for (Unavailability s2: student.getUnavailabilities()) {
253                    // no time or no room > no conflict
254                    if (s2.getTime() == null || s2.getNrRooms() == 0 || s2.isAllowOverlap()) continue;
255                    TimeLocation t1 = s1.getTime();
256                    TimeLocation t2 = s2.getTime();
257                    // no shared day > no conflict
258                    if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) continue;
259                    int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
260                    if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
261                        int dist = cx.getUnavailabilityDistanceInMinutes(s1.getPlacement(), s2);
262                        if (dist >= cx.getUnavailabilityDistanceMetric().getDistanceHardLimitInMinutes()
263                                && dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getUnavailabilityDistanceMetric().getAllowedDistanceInMinutes())
264                            return true;
265                    } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
266                        int dist = cx.getUnavailabilityDistanceInMinutes(s1.getPlacement(), s2);
267                        if (dist >= cx.getUnavailabilityDistanceMetric().getDistanceHardLimitInMinutes()
268                                && dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getUnavailabilityDistanceMetric().getAllowedDistanceInMinutes())
269                            return true;
270                    }
271                }
272            }
273        }
274        
275        // check for distance conflicts within the enrollment
276        if (cx.getDistanceMetric().isHardDistanceConflictsEnabled()) {
277            for (Section s1: enrollment.getSections()) {
278                // no time or no room > no conflict
279                if (!s1.hasTime() || s1.getNrRooms() == 0 || s1.isAllowOverlap()) continue;
280                for (Section s2: enrollment.getSections()) {
281                    if (s1.getId() < s2.getId()) {
282                        // no time or no room > no conflict
283                        if (!s2.hasTime() || s2.getNrRooms() == 0 || s2.isAllowOverlap() || s1.isToIgnoreStudentConflictsWith(s2.getId())) continue;
284                        TimeLocation t1 = s1.getTime();
285                        TimeLocation t2 = s2.getTime();
286                        // no shared day > no conflict
287                        if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) continue;
288                        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
289                        if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
290                            if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
291                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
292                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() 
293                                       && dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes())
294                                    return true;
295                            } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
296                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
297                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
298                                        && dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes())
299                                    return true;
300                            }
301                        } else {
302                            if (a1 + t1.getNrSlotsPerMeeting() == a2) {
303                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
304                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
305                                        && dist > t1.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes())
306                                    return true;
307                            } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
308                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
309                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes()
310                                        && dist > t2.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes())
311                                    return true;
312                            }
313                        }
314                    }
315                }
316            }
317            
318            // check conflicts with other enrollments of the student
319            for (Request other: student.getRequests()) {
320                if (other.equals(enrollment.variable())) continue;
321                Enrollment e2 = other.getAssignment(assignment);
322                if (e2 == null) continue;
323                if (e2.getReservation() != null && e2.getReservation().isAllowOverlap()) continue;
324                for (Section s1: enrollment.getSections()) {
325                    // no time or no room > no conflict
326                    if (!s1.hasTime() || s1.getNrRooms() == 0 || s1.isAllowOverlap()) continue;
327                    for (Section s2: e2.getSections()) {
328                        // no time or no room > no conflict
329                        if (!s2.hasTime() || s2.getNrRooms() == 0 || s2.isAllowOverlap() || s1.isToIgnoreStudentConflictsWith(s2.getId())) continue;
330                        TimeLocation t1 = s1.getTime();
331                        TimeLocation t2 = s2.getTime();
332                        // no shared day > no conflict
333                        if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) continue;
334                        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
335                        if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
336                            if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
337                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
338                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() && dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes())
339                                    return true;
340                            } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
341                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
342                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() && dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes())
343                                    return true;
344                            }
345                        } else {
346                            if (a1 + t1.getNrSlotsPerMeeting() == a2) {
347                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
348                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() && dist > t1.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes())
349                                    return true;
350                            } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
351                                int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
352                                if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() && dist > t2.getBreakTime() + cx.getDistanceMetric().getAllowedDistanceInMinutes())
353                                    return true;
354                            }
355                        }
356                        
357                    }
358                    
359                }
360            }
361        }
362        
363        return false;
364    }
365    
366    public static boolean inConflict(StudentQuality sq, Section s1, Unavailability s2) {
367        if (sq == null || s1 == null || s2 == null) return false;
368        if (s1.getPlacement() == null || s2.getTime() == null || s2.getNrRooms() == 0
369                || s1.isAllowOverlap() || s2.isAllowOverlap()) return false;
370        StudentQuality.Context cx = sq.getStudentQualityContext();
371        if (!cx.getUnavailabilityDistanceMetric().isHardDistanceConflictsEnabled()) return false;
372        TimeLocation t1 = s1.getTime();
373        TimeLocation t2 = s2.getTime();
374        if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
375            return false;
376        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
377        if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
378            int dist = cx.getUnavailabilityDistanceInMinutes(s1.getPlacement(), s2);
379            if (dist >= cx.getUnavailabilityDistanceMetric().getDistanceHardLimitInMinutes() &&
380                    dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getUnavailabilityDistanceMetric().getAllowedDistanceInMinutes())
381                return true;
382        } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
383            int dist = cx.getUnavailabilityDistanceInMinutes(s1.getPlacement(), s2);
384            if (dist >= cx.getUnavailabilityDistanceMetric().getDistanceHardLimitInMinutes() &&
385                    dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getUnavailabilityDistanceMetric().getAllowedDistanceInMinutes())
386                return true;
387        }
388        return false;
389    }
390    
391    public static boolean inConflict(StudentQuality sq, Section s1, Section s2) {
392        if (sq == null || s1 == null || s2 == null) return false;
393        if (s1.getPlacement() == null || s2.getPlacement() == null
394                || s1.isAllowOverlap() || s2.isAllowOverlap() || s1.isToIgnoreStudentConflictsWith(s2.getId())) return false;
395        StudentQuality.Context cx = sq.getStudentQualityContext();
396        if (!cx.getDistanceMetric().isHardDistanceConflictsEnabled()) return false;
397        TimeLocation t1 = s1.getTime();
398        TimeLocation t2 = s2.getTime();
399        if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
400            return false;
401        int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
402        if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
403            int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
404            if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() &&
405                    dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes())
406                return true;
407        } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
408            int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
409            if (dist >= cx.getDistanceMetric().getDistanceHardLimitInMinutes() &&
410                    dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()) + cx.getDistanceMetric().getAllowedDistanceInMinutes())
411                return true;
412        }
413        return false;
414    }
415    
416    public static boolean inConflict(StudentQuality sq, SctAssignment s1, Enrollment e) {
417        if (sq == null || s1 == null || e == null) return false;
418        if (!sq.getStudentQualityContext().getDistanceMetric().isHardDistanceConflictsEnabled()) return false;
419        if (e.getReservation() != null && e.getReservation().isAllowOverlap()) return false;
420        if (s1 instanceof Section && e.getCourse() != null)
421            for (SctAssignment s2: e.getAssignments())
422                if (s2 instanceof Section && inConflict(sq, (Section)s1, (Section)s2)) return true;
423        return false;
424    }
425
426    @Override
427    public String toString() {
428        return "DistanceConflict";
429    }
430}