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.CourseRequest;
023    import net.sf.cpsolver.studentsct.model.Enrollment;
024    import net.sf.cpsolver.studentsct.model.Request;
025    import net.sf.cpsolver.studentsct.model.Section;
026    import 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 Tomas Muller<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     */
066    public 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    }