    001package net.sf.cpsolver.studentsct.report;
    002
    003import java.text.DecimalFormat;
    004import java.util.ArrayList;
    005import java.util.Comparator;
    006import java.util.HashSet;
    007import java.util.HashMap;
    008import java.util.List;
    009import java.util.Map;
    010import java.util.Set;
    011import java.util.TreeSet;
    012
    013import net.sf.cpsolver.coursett.model.Placement;
    014import net.sf.cpsolver.coursett.model.RoomLocation;
    015import net.sf.cpsolver.ifs.util.CSVFile;
    016import net.sf.cpsolver.ifs.util.DataProperties;
    017import net.sf.cpsolver.ifs.util.DistanceMetric;
    018import net.sf.cpsolver.studentsct.StudentSectioningModel;
    019import net.sf.cpsolver.studentsct.extension.DistanceConflict;
    020import net.sf.cpsolver.studentsct.extension.DistanceConflict.Conflict;
    021import net.sf.cpsolver.studentsct.model.Course;
    022import net.sf.cpsolver.studentsct.model.CourseRequest;
    023import net.sf.cpsolver.studentsct.model.Enrollment;
    024import net.sf.cpsolver.studentsct.model.Request;
    025import net.sf.cpsolver.studentsct.model.Section;
    026import net.sf.cpsolver.studentsct.model.Student;
    027
    028/**
    029 * This class lists distance student conflicts in a {@link CSVFile} comma
    030 * separated text file. Two sections that are attended by the same student are
    031 * considered in a distance conflict if they are back-to-back taught in
    032 * locations that are two far away. See {@link DistanceConflict} for more
    033 * details. <br>
    034 * <br>
    035 *
    036 * Each line represent a pair if classes that are in a distance conflict and have
    037 * one or more students in common.
    038 *
    039 * <br>
    040 * <br>
    041 *
    042 * Usage: new DistanceConflictTable(model),createTable(true, true).save(aFile);
    043 *
    044 * <br>
    045 * <br>
    046 *
    047 * @version StudentSct 1.2 (Student Sectioning)<br>
    048 *          Copyright (C) 2007 - 2013 Tomáš Müller<br>
    049 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    050 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    051 * <br>
    052 *          This library is free software; you can redistribute it and/or modify
    053 *          it under the terms of the GNU Lesser General Public License as
    054 *          published by the Free Software Foundation; either version 3 of the
    055 *          License, or (at your option) any later version. <br>
    056 * <br>
    057 *          This library is distributed in the hope that it will be useful, but
    058 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    059 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    060 *          Lesser General Public License for more details. <br>
    061 * <br>
    062 *          You should have received a copy of the GNU Lesser General Public
    063 *          License along with this library; if not see
    064 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    065 */
    066public class DistanceConflictTable implements StudentSectioningReport {
    067    private static org.apache.log4j.Logger sLog = org.apache.log4j.Logger.getLogger(DistanceConflictTable.class);
    068    private static DecimalFormat sDF1 = new DecimalFormat("0.####");
    069    private static DecimalFormat sDF2 = new DecimalFormat("0.0000");
    070
    071    private StudentSectioningModel iModel = null;
    072    private DistanceConflict iDC = null;
    073    private DistanceMetric iDM = null;
    074
    075    /**
    076     * Constructor
    077     *
    078     * @param model
    079     *            student sectioning model
    080     */
    081    public DistanceConflictTable(StudentSectioningModel model) {
    082        iModel = model;
    083        iDC = model.getDistanceConflict();
    084        if (iDC == null) {
    085            iDM = new DistanceMetric(model.getProperties());
    086            iDC = new DistanceConflict(iDM, model.getProperties());
    087        } else {
    088            iDM = iDC.getDistanceMetric();
    089        }
    090    }
    091
    092    /** Return student sectioning model */
    093    public StudentSectioningModel getModel() {
    094        return iModel;
    095    }
    096
    097    /**
    098     * Create report
    099     *
    100     * @param includeLastLikeStudents
    101     *            true, if last-like students should be included (i.e.,
    102     *            {@link Student#isDummy()} is true)
    103     * @param includeRealStudents
    104     *            true, if real students should be included (i.e.,
    105     *            {@link Student#isDummy()} is false)
    106     * @return report as comma separated text file
    107     */
    108    public CSVFile createTable(boolean includeLastLikeStudents, boolean includeRealStudents) {
    109        CSVFile csv = new CSVFile();
    110        csv.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course"), new CSVFile.CSVField("Total\nConflicts"),
    111                new CSVFile.CSVField("Class"), new CSVFile.CSVField("Meeting Time"), new CSVFile.CSVField("Room"),
    112                new CSVFile.CSVField("Distance\nConflicts"), new CSVFile.CSVField("% of Total\nConflicts"),
    113                new CSVFile.CSVField("Conflicting\nClass"), new CSVFile.CSVField("Conflicting\nMeeting Time"), new CSVFile.CSVField("Conflicting\nRoom"),
    114                new CSVFile.CSVField("Distance [m]"), new CSVFile.CSVField("Distance [min]"), new CSVFile.CSVField("Joined\nConflicts"), new CSVFile.CSVField("% of Total\nConflicts")
    115                });
    116
    117        Set<Conflict> confs = new HashSet<Conflict>();
    118        for (Request r1 : getModel().variables()) {
    119            if (r1.getAssignment() == null || !(r1 instanceof CourseRequest))
    120                continue;
    121            confs.addAll(iDC.conflicts(r1.getAssignment()));
    122            for (Request r2 : r1.getStudent().getRequests()) {
    123                if (r2.getAssignment() == null || r1.getId() >= r2.getId() || !(r2 instanceof CourseRequest))
    124                    continue;
    125                confs.addAll(iDC.conflicts(r1.getAssignment(), r2.getAssignment()));
    126            }
    127        }
    128
    129        HashMap<Course, Set<Long>> totals = new HashMap<Course, Set<Long>>();
    130        HashMap<CourseSection, Map<CourseSection, Double>> conflictingPairs = new HashMap<CourseSection, Map<CourseSection,Double>>();
    131        HashMap<CourseSection, Set<Long>> sectionOverlaps = new HashMap<CourseSection, Set<Long>>();
    132
    133        for (Conflict conflict : confs) {
    134            if (conflict.getStudent().isDummy() && !includeLastLikeStudents) continue;
    135            if (!conflict.getStudent().isDummy() && !includeRealStudents) continue;
    136            Section s1 = conflict.getS1(), s2 = conflict.getS2();
    137            Course c1 = null, c2 = null;
    138            Request r1 = null, r2 = null;
    139            for (Request request : conflict.getStudent().getRequests()) {
    140                Enrollment enrollment = request.getAssignment();
    141                if (enrollment == null || !enrollment.isCourseRequest()) continue;
    142                if (c1 == null && enrollment.getAssignments().contains(s1)) {
    143                    c1 = enrollment.getCourse();
    144                    r1 = request;
    145                    Set<Long> total = totals.get(enrollment.getCourse());
    146                    if (total == null) {
    147                        total = new HashSet<Long>();
    148                        totals.put(enrollment.getCourse(), total);
    149                    }
    150                    total.add(enrollment.getStudent().getId());
    151                }
    152                if (c2 == null && enrollment.getAssignments().contains(s2)) {
    153                    c2 = enrollment.getCourse();
    154                    r2 = request;
    155                    Set<Long> total = totals.get(enrollment.getCourse());
    156                    if (total == null) {
    157                        total = new HashSet<Long>();
    158                        totals.put(enrollment.getCourse(), total);
    159                    }
    160                    total.add(enrollment.getStudent().getId());
    161                }
    162            }
    163            if (c1 == null) {
    164                sLog.error("Unable to find a course for " + s1);
    165                continue;
    166            }
    167            if (c2 == null) {
    168                sLog.error("Unable to find a course for " + s2);
    169                continue;
    170            }
    171            CourseSection a = new CourseSection(c1, s1);
    172            CourseSection b = new CourseSection(c2, s2);
    173
    174            Set<Long> total = sectionOverlaps.get(a);
    175            if (total == null) {
    176                total = new HashSet<Long>();
    177                sectionOverlaps.put(a, total);
    178            }
    179            total.add(r1.getStudent().getId());
    180            Map<CourseSection, Double> pair = conflictingPairs.get(a);
    181            if (pair == null) {
    182                pair = new HashMap<CourseSection, Double>();
    183                conflictingPairs.put(a, pair);
    184            }
    185            Double prev = pair.get(b);
    186            pair.put(b, r2.getWeight() + (prev == null ? 0.0 : prev.doubleValue()));
    187
    188            total = sectionOverlaps.get(b);
    189            if (total == null) {
    190                total = new HashSet<Long>();
    191                sectionOverlaps.put(b, total);
    192            }
    193            total.add(r2.getStudent().getId());
    194            pair = conflictingPairs.get(b);
    195            if (pair == null) {
    196                pair = new HashMap<CourseSection, Double>();
    197                conflictingPairs.put(b, pair);
    198            }
    199            prev = pair.get(a);
    200            pair.put(a, r1.getWeight() + (prev == null ? 0.0 : prev.doubleValue()));
    201        }
    202
    203        Comparator<Course> courseComparator = new Comparator<Course>() {
    204            @Override
    205            public int compare(Course a, Course b) {
    206                int cmp = a.getName().compareTo(b.getName());
    207                if (cmp != 0) return cmp;
    208                return a.getId() < b.getId() ? -1 : a.getId() == b.getId() ? 0 : 1;
    209            }
    210        };
    211        Comparator<Section> sectionComparator = new Comparator<Section>() {
    212            @Override
    213            public int compare(Section a, Section b) {
    214                int cmp = a.getSubpart().getConfig().getOffering().getName().compareTo(b.getSubpart().getConfig().getOffering().getName());
    215                if (cmp != 0) return cmp;
    216                cmp = a.getSubpart().getInstructionalType().compareTo(b.getSubpart().getInstructionalType());
    217                // if (cmp != 0) return cmp;
    218                // cmp = a.getName().compareTo(b.getName());
    219                if (cmp != 0) return cmp;
    220                return a.getId() < b.getId() ? -1 : a.getId() == b.getId() ? 0 : 1;
    221            }
    222        };
    223
    224        TreeSet<Course> courses = new TreeSet<Course>(courseComparator);
    225        courses.addAll(totals.keySet());
    226        for (Course course: courses) {
    227            Set<Long> total = totals.get(course);
    228
    229            TreeSet<Section> sections = new TreeSet<Section>(sectionComparator);
    230            for (Map.Entry<CourseSection, Set<Long>> entry: sectionOverlaps.entrySet())
    231                if (course.equals(entry.getKey().getCourse()))
    232                    sections.add(entry.getKey().getSection());
    233
    234            boolean firstCourse = true;
    235            for (Section section: sections) {
    236                Set<Long> sectionOverlap = sectionOverlaps.get(new CourseSection(course, section));
    237                Map<CourseSection, Double> pair = conflictingPairs.get(new CourseSection(course, section));
    238                boolean firstClass = true;
    239
    240                String rooms = "";
    241                if (section.getRooms() != null)
    242                    for (RoomLocation r: section.getRooms()) {
    243                        if (!rooms.isEmpty()) rooms += "\n";
    244                        rooms += r.getName();
    245                    }
    246
    247                for (CourseSection other: new TreeSet<CourseSection>(pair.keySet())) {
    248                    List<CSVFile.CSVField> line = new ArrayList<CSVFile.CSVField>();
    249                    line.add(new CSVFile.CSVField(firstCourse && firstClass ? course.getName() : ""));
    250                    line.add(new CSVFile.CSVField(firstCourse && firstClass ? total.size() : ""));
    251
    252                    line.add(new CSVFile.CSVField(firstClass ? section.getSubpart().getName() + " " + section.getName(course.getId()): ""));
    253                    line.add(new CSVFile.CSVField(firstClass ? section.getTime() == null ? "" : section.getTime().getDayHeader() + " " + section.getTime().getStartTimeHeader() + " - " + section.getTime().getEndTimeHeader(): ""));
    254
    255                    line.add(new CSVFile.CSVField(firstClass ? rooms : ""));
    256
    257                    line.add(new CSVFile.CSVField(firstClass && sectionOverlap != null ? String.valueOf(sectionOverlap.size()): ""));
    258                    line.add(new CSVFile.CSVField(firstClass && sectionOverlap != null ? sDF2.format(((double)sectionOverlap.size()) / total.size()) : ""));
    259
    260                    line.add(new CSVFile.CSVField(other.getCourse().getName() + " " + other.getSection().getSubpart().getName() + " " + other.getSection().getName(other.getCourse().getId())));
    261                    line.add(new CSVFile.CSVField(other.getSection().getTime().getDayHeader() + " " + other.getSection().getTime().getStartTimeHeader() + " - " + other.getSection().getTime().getEndTimeHeader()));
    262
    263                    String or = "";
    264                    if (other.getSection().getRooms() != null)
    265                        for (RoomLocation r: other.getSection().getRooms()) {
    266                            if (!or.isEmpty()) or += "\n";
    267                            or += r.getName();
    268                        }
    269                    line.add(new CSVFile.CSVField(or));
    270
    271                    line.add(new CSVFile.CSVField(sDF2.format(Placement.getDistanceInMeters(iDM, section.getPlacement(), other.getSection().getPlacement()))));
    272                    line.add(new CSVFile.CSVField(String.valueOf(Placement.getDistanceInMinutes(iDM, section.getPlacement(), other.getSection().getPlacement()))));
    273                    line.add(new CSVFile.CSVField(sDF1.format(pair.get(other))));
    274                    line.add(new CSVFile.CSVField(sDF2.format(pair.get(other) / total.size())));
    275
    276                    csv.addLine(line);
    277                    firstClass = false;
    278                }
    279                firstCourse = false;
    280            }
    281
    282            csv.addLine();
    283        }
    284
    285
    286        return csv;
    287    }
    288
    289    @Override
    290    public CSVFile create(DataProperties properties) {
    291        return createTable(properties.getPropertyBoolean("lastlike", false), properties.getPropertyBoolean("real", true));
    292    }
    293}