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