001    package net.sf.cpsolver.studentsct.report;
002    
003    import java.text.DecimalFormat;
004    import java.util.Collections;
005    import java.util.Comparator;
006    import java.util.HashSet;
007    import java.util.HashMap;
008    import java.util.Iterator;
009    import java.util.Map;
010    import java.util.Set;
011    import java.util.TreeSet;
012    
013    import net.sf.cpsolver.ifs.util.CSVFile;
014    import net.sf.cpsolver.ifs.util.DistanceMetric;
015    import net.sf.cpsolver.studentsct.StudentSectioningModel;
016    import net.sf.cpsolver.studentsct.extension.DistanceConflict;
017    import net.sf.cpsolver.studentsct.extension.DistanceConflict.Conflict;
018    import net.sf.cpsolver.studentsct.model.Course;
019    import net.sf.cpsolver.studentsct.model.Enrollment;
020    import net.sf.cpsolver.studentsct.model.Request;
021    import net.sf.cpsolver.studentsct.model.Section;
022    import net.sf.cpsolver.studentsct.model.Student;
023    
024    /**
025     * This class lists distance student conflicts in a {@link CSVFile} comma
026     * separated text file. Two sections that are attended by the same student are
027     * considered in a distance conflict if they are back-to-back taught in
028     * locations that are two far away. See {@link DistanceConflict} for more
029     * details. <br>
030     * <br>
031     * 
032     * Each line represent a pair if courses that have one or more distance
033     * conflicts in between (columns Course1, Course2), column NrStud displays the
034     * number of student distance conflicts (weighted by requests weights), and
035     * column AvgDist displays the average distance for all the distance conflicts
036     * between these two courses. The column NoAlt is Y when every possible
037     * enrollment of the first course is either overlapping or there is a distance
038     * conflict with every possible enrollment of the second course (it is N
039     * otherwise) and a column Reason which lists the sections that are involved in
040     * a distance conflict.
041     * 
042     * <br>
043     * <br>
044     * 
045     * Usage: new DistanceConflictTable(model),createTable(true, true).save(aFile);
046     * 
047     * <br>
048     * <br>
049     * 
050     * @version StudentSct 1.2 (Student Sectioning)<br>
051     *          Copyright (C) 2007 - 2010 Tomas Muller<br>
052     *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
053     *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
054     * <br>
055     *          This library is free software; you can redistribute it and/or modify
056     *          it under the terms of the GNU Lesser General Public License as
057     *          published by the Free Software Foundation; either version 3 of the
058     *          License, or (at your option) any later version. <br>
059     * <br>
060     *          This library is distributed in the hope that it will be useful, but
061     *          WITHOUT ANY WARRANTY; without even the implied warranty of
062     *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
063     *          Lesser General Public License for more details. <br>
064     * <br>
065     *          You should have received a copy of the GNU Lesser General Public
066     *          License along with this library; if not see
067     *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
068     */
069    public class DistanceConflictTable {
070        private static org.apache.log4j.Logger sLog = org.apache.log4j.Logger.getLogger(DistanceConflictTable.class);
071        private static DecimalFormat sDF = new DecimalFormat("0.000");
072    
073        private StudentSectioningModel iModel = null;
074        private DistanceConflict iDC = null;
075        private DistanceMetric iDM = null;
076    
077        /**
078         * Constructor
079         * 
080         * @param model
081         *            student sectioning model
082         */
083        public DistanceConflictTable(StudentSectioningModel model) {
084            iModel = model;
085            iDC = model.getDistanceConflict();
086            if (iDC == null) {
087                iDM = new DistanceMetric(model.getProperties());
088                iDC = new DistanceConflict(iDM, model.getProperties());
089            } else {
090                iDM = iDC.getDistanceMetric();
091            }
092        }
093    
094        /** Return student sectioning model */
095        public StudentSectioningModel getModel() {
096            return iModel;
097        }
098    
099        /**
100         * True, if there is no pair of enrollments of r1 and r2 that is not in a
101         * hard conflict and without a distance conflict
102         */
103        private boolean areInHardConfict(Request r1, Request r2) {
104            for (Enrollment e1 : r1.values()) {
105                for (Enrollment e2 : r2.values()) {
106                    if (!e1.isOverlapping(e2) && iDC.nrConflicts(e1, e2) == 0)
107                        return false;
108                }
109            }
110            return true;
111        }
112    
113        /**
114         * Create report
115         * 
116         * @param includeLastLikeStudents
117         *            true, if last-like students should be included (i.e.,
118         *            {@link Student#isDummy()} is true)
119         * @param includeRealStudents
120         *            true, if real students should be included (i.e.,
121         *            {@link Student#isDummy()} is false)
122         * @return report as comma separated text file
123         */
124        @SuppressWarnings("unchecked")
125        public CSVFile createTable(boolean includeLastLikeStudents, boolean includeRealStudents) {
126            CSVFile csv = new CSVFile();
127            csv.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course1"), new CSVFile.CSVField("Course2"),
128                    new CSVFile.CSVField("NrStud"), new CSVFile.CSVField("AvgDist"),
129                    new CSVFile.CSVField("NoAlt"), new CSVFile.CSVField("Reason") });
130            Set<Conflict> confs = iDC.computeAllConflicts();
131            HashMap<Course, HashMap<Course, Object[]>> distConfTable = new HashMap<Course, HashMap<Course, Object[]>>();
132            for (Conflict conflict : confs) {
133                if (conflict.getStudent().isDummy() && !includeLastLikeStudents)
134                    continue;
135                if (!conflict.getStudent().isDummy() && !includeRealStudents)
136                    continue;
137                Section s1 = conflict.getS1(), s2 = conflict.getS2();
138                Course c1 = null, c2 = null;
139                Request r1 = null, r2 = null;
140                for (Request request : conflict.getStudent().getRequests()) {
141                    Enrollment enrollment = request.getAssignment();
142                    if (enrollment == null || !enrollment.isCourseRequest())
143                        continue;
144                    if (c1 == null && enrollment.getAssignments().contains(s1)) {
145                        c1 = enrollment.getCourse();
146                        r1 = request;
147                    }
148                    if (c2 == null && enrollment.getAssignments().contains(s2)) {
149                        c2 = enrollment.getCourse();
150                        r2 = request;
151                    }
152                }
153                if (c1 == null) {
154                    sLog.error("Unable to find a course for " + s1);
155                    continue;
156                }
157                if (c2 == null) {
158                    sLog.error("Unable to find a course for " + s2);
159                    continue;
160                }
161                if (c1.getName().compareTo(c2.getName()) > 0) {
162                    Course x = c1;
163                    c1 = c2;
164                    c2 = x;
165                    Section y = s1;
166                    s1 = s2;
167                    s2 = y;
168                }
169                if (c1.equals(c2) && s1.getName().compareTo(s2.getName()) > 0) {
170                    Section y = s1;
171                    s1 = s2;
172                    s2 = y;
173                }
174                HashMap<Course, Object[]> firstCourseTable = distConfTable.get(c1);
175                if (firstCourseTable == null) {
176                    firstCourseTable = new HashMap<Course, Object[]>();
177                    distConfTable.put(c1, firstCourseTable);
178                }
179                Object[] secondCourseTable = firstCourseTable.get(c2);
180                double nrStud = (secondCourseTable == null ? 0.0 : ((Double) secondCourseTable[0]).doubleValue()) + 1.0;
181                double dist = (secondCourseTable == null ? 0.0 : ((Double) secondCourseTable[1]).doubleValue()) + (conflict.getDistance(iDM));
182                boolean hard = (secondCourseTable == null ? areInHardConfict(r1, r2) : ((Boolean) secondCourseTable[2]).booleanValue());
183                HashSet<String> expl = (HashSet<String>) (secondCourseTable == null ? null : secondCourseTable[3]);
184                if (expl == null)
185                    expl = new HashSet<String>();
186                expl.add(s1.getSubpart().getName() + " " + s1.getTime().getLongName() + " "
187                        + s1.getPlacement().getRoomName(",") + " vs " + s2.getSubpart().getName() + " "
188                        + s2.getTime().getLongName() + " " + s2.getPlacement().getRoomName(","));
189                firstCourseTable.put(c2, new Object[] { new Double(nrStud), new Double(dist), new Boolean(hard), expl });
190            }
191            for (Map.Entry<Course, HashMap<Course, Object[]>> entry : distConfTable.entrySet()) {
192                Course c1 = entry.getKey();
193                HashMap<Course, Object[]> firstCourseTable = entry.getValue();
194                for (Map.Entry<Course, Object[]> entry2 : firstCourseTable.entrySet()) {
195                    Course c2 = entry2.getKey();
196                    Object[] secondCourseTable = entry2.getValue();
197                    HashSet<String> expl = (HashSet<String>) secondCourseTable[3];
198                    String explStr = "";
199                    for (Iterator<String> k = new TreeSet<String>(expl).iterator(); k.hasNext();)
200                        explStr += k.next() + (k.hasNext() ? "\n" : "");
201                    double nrStud = ((Double) secondCourseTable[0]).doubleValue();
202                    double dist = ((Double) secondCourseTable[1]).doubleValue() / nrStud;
203                    csv.addLine(new CSVFile.CSVField[] { new CSVFile.CSVField(c1.getName()),
204                            new CSVFile.CSVField(c2.getName()), new CSVFile.CSVField(sDF.format(nrStud)),
205                            new CSVFile.CSVField(sDF.format(dist)),
206                            new CSVFile.CSVField(((Boolean) secondCourseTable[2]).booleanValue() ? "Y" : "N"),
207                            new CSVFile.CSVField(explStr) });
208                }
209            }
210            if (csv.getLines() != null)
211                Collections.sort(csv.getLines(), new Comparator<CSVFile.CSVLine>() {
212                    @Override
213                    public int compare(CSVFile.CSVLine l1, CSVFile.CSVLine l2) {
214                        // int cmp =
215                        // l2.getField(4).toString().compareTo(l1.getField(4).toString());
216                        // if (cmp!=0) return cmp;
217                        int cmp = Double.compare(l2.getField(2).toDouble(), l1.getField(2).toDouble());
218                        if (cmp != 0)
219                            return cmp;
220                        cmp = l1.getField(0).toString().compareTo(l2.getField(0).toString());
221                        if (cmp != 0)
222                            return cmp;
223                        return l1.getField(1).toString().compareTo(l2.getField(1).toString());
224                    }
225                });
226            return csv;
227        }
228    }