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