    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}