    001package org.cpsolver.ifs.util;
    002
    003import java.util.HashMap;
    004import java.util.Map;
    005import java.util.concurrent.locks.ReentrantReadWriteLock;
    006
    007import org.cpsolver.studentsct.constraint.HardDistanceConflicts;
    008
    009/**
    010 * Common class for computing distances and back-to-back instructor / student conflicts.
    011 *
    012 * When property Distances.Ellipsoid is set, the distances are computed using the given (e.g., WGS84, see {@link Ellipsoid}).
    013 * In the legacy mode (when ellipsoid is not set), distances are computed using Euclidian distance and 1 unit is considered 10 meters.
    014 * <br><br>
    015 * For student back-to-back conflicts, Distances.Speed (in meters per minute) is considered and compared with the break time
    016 * of the earlier class.
    017 * <br><br>
    018 * For instructors, the preference is computed using the distance in meters and the three constants
    019 * Instructor.NoPreferenceLimit (distance &lt;= limit &rarr; no preference), Instructor.DiscouragedLimit (distance &lt;= limit &rarr; discouraged),
    020 * Instructor.ProhibitedLimit (distance &lt;= limit &rarr; strongly discouraged), the back-to-back placement is prohibited when the distance is over the last limit.
    021 *
    022 * @author  Tomáš Müller
    023 * @version IFS 1.3 (Iterative Forward Search)<br>
    024 *          Copyright (C) 2006 - 2014 Tomáš Müller<br>
    025 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    026 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    027 * <br>
    028 *          This library is free software; you can redistribute it and/or modify
    029 *          it under the terms of the GNU Lesser General Public License as
    030 *          published by the Free Software Foundation; either version 3 of the
    031 *          License, or (at your option) any later version. <br>
    032 * <br>
    033 *          This library is distributed in the hope that it will be useful, but
    034 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    035 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    036 *          Lesser General Public License for more details. <br>
    037 * <br>
    038 *          You should have received a copy of the GNU Lesser General Public
    039 *          License along with this library; if not see
    040 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    041 */
    042public class DistanceMetric {
    043    public static enum Ellipsoid {
    044        LEGACY ("Euclidean metric (1 unit equals to 10 meters)", "X-Coordinate", "Y-Coordinate", 0, 0, 0),
    045        WGS84 ("WGS-84 (GPS)", 6378137, 6356752.3142, 1.0 / 298.257223563),
    046        GRS80 ("GRS-80", 6378137, 6356752.3141, 1.0 / 298.257222101),
    047        Airy1830 ("Airy (1830)", 6377563.396, 6356256.909, 1.0 / 299.3249646),
    048        Intl1924 ("Int'l 1924", 6378388, 6356911.946, 1.0 / 297),
    049        Clarke1880 ("Clarke (1880)", 6378249.145, 6356514.86955, 1.0 / 293.465),
    050        GRS67 ("GRS-67", 6378160, 6356774.719, 1.0 / 298.25);
    051
    052        private double iA, iB, iF;
    053        private String iName, iFirstCoord, iSecondCoord;
    054
    055        Ellipsoid(String name, double a, double b) {
    056            this(name, "Latitude", "Longitude", a, b, (a - b) / a);
    057        }
    058        Ellipsoid(String name, double a, double b, double f) {
    059            this(name, "Latitude", "Longitude", a, b, f);
    060        }
    061        Ellipsoid(String name, String xCoord, String yCoord, double a, double b, double f) {
    062            iName = name;
    063            iFirstCoord = xCoord; iSecondCoord = yCoord;
    064            iA = a; iB = b; iF = f;
    065        }
    066
    067        /** Major semiaxe A
    068         * @return major semiaxe A
    069         **/
    070        public double a() { return iA; }
    071        /** Minor semiaxe B
    072         * @return major semiaxe B
    073         **/
    074        public double b() { return iB; }
    075        /** Flattening (A-B) / A
    076         * @return Flattening (A-B) / A
    077         **/
    078        public double f() { return iF; }
    079
    080        /** Name of this coordinate system
    081         * @return elipsoid name
    082         **/
    083        public String getEclipsoindName() { return iName; }
    084        /** Name of the fist coordinate (e.g., Latitude)
    085         * @return first coordinate's name
    086         **/
    087        public String getFirstCoordinateName() { return iFirstCoord; }
    088        /** Name of the second coordinate (e.g., Longitude)
    089         * @return second coordinate's name
    090         **/
    091        public String getSecondCoordinateName() { return iSecondCoord; }
    092    }
    093
    094    /** Elliposid parameters, default to WGS-84 */
    095    private Ellipsoid iModel = Ellipsoid.WGS84;
    096    /** Student speed in meters per minute (defaults to 1000 meters in 15 minutes) */
    097    private double iSpeed = 1000.0 / 15;
    098    /** Back-to-back classes: maximal distance for no preference */
    099    private double iInstructorNoPreferenceLimit = 0.0;
    100    /** Back-to-back classes: maximal distance for discouraged preference */
    101    private double iInstructorDiscouragedLimit = 50.0;
    102    /**
    103     * Back-to-back classes: maximal distance for strongly discouraged preference
    104     * (everything above is prohibited)
    105     */
    106    private double iInstructorProhibitedLimit = 200.0;
    107    /**
    108     * When Distances.ComputeDistanceConflictsBetweenNonBTBClasses is enabled, distance limit (in minutes)
    109     * for a long travel.
    110     */
    111    private double iInstructorLongTravelInMinutes = 30.0;
    112
    113    /** Default distance when given coordinates are null. */
    114    private double iNullDistance = 10000.0;
    115    /** Maximal travel time in minutes when no coordinates are given. */
    116    private int iMaxTravelTime = 60;
    117    /** Travel times overriding the distances computed from coordintaes */
    118    private Map<Long, Map<Long, Integer>> iTravelTimes = new HashMap<Long, Map<Long,Integer>>();
    119    /** Distance cache  */
    120    private Map<String, Double> iDistanceCache = new HashMap<String, Double>();
    121    /** True if distances should be considered between classes that are NOT back-to-back */
    122    private boolean iComputeDistanceConflictsBetweenNonBTBClasses = false;
    123    /** Reference of the accommodation of students that need short distances */
    124    private String iShortDistanceAccommodationReference = "SD";
    125    /** Allowed distance in minutes (for {@link HardDistanceConflicts}) */
    126    private int iAllowedDistanceInMinutes = 30;
    127    /** Hard distance limit in minutes (for {@link HardDistanceConflicts}) */
    128    private int iDistanceHardLimitInMinutes = 60;
    129    /** Long distance limit in minutes (for display) */
    130    private int iDistanceLongLimitInMinutes = 60;
    131    /** Hard distance conflicts enabled (for {@link HardDistanceConflicts}) */
    132    private boolean iHardDistanceConflicts = false;
    133
    134    private final ReentrantReadWriteLock iLock = new ReentrantReadWriteLock();
    135
    136    /** Default properties */
    137    public DistanceMetric() {
    138    }
    139
    140    public DistanceMetric(DistanceMetric m) {
    141        iModel = m.iModel;
    142        iSpeed = m.iSpeed;
    143        iInstructorNoPreferenceLimit = m.iInstructorNoPreferenceLimit;
    144        iInstructorDiscouragedLimit = m.iInstructorDiscouragedLimit;
    145        iInstructorProhibitedLimit = m.iInstructorProhibitedLimit;
    146        iInstructorLongTravelInMinutes = m.iInstructorLongTravelInMinutes;
    147        iNullDistance = m.iNullDistance;
    148        iMaxTravelTime = m.iMaxTravelTime;
    149        iComputeDistanceConflictsBetweenNonBTBClasses = m.iComputeDistanceConflictsBetweenNonBTBClasses;
    150        iShortDistanceAccommodationReference = m.iShortDistanceAccommodationReference;
    151        iAllowedDistanceInMinutes = m.iAllowedDistanceInMinutes;
    152        iDistanceHardLimitInMinutes = m.iDistanceHardLimitInMinutes;
    153        iDistanceLongLimitInMinutes = m.iDistanceLongLimitInMinutes;
    154        iHardDistanceConflicts = m.iHardDistanceConflicts;
    155        m.iLock.readLock().lock();
    156        try {
    157            for (Map.Entry<Long, Map<Long, Integer>> e: m.iTravelTimes.entrySet())
    158                iTravelTimes.put(e.getKey(), new HashMap<Long, Integer>(e.getValue()));
    159        } finally {
    160            m.iLock.readLock().unlock();
    161        }
    162    }
    163
    164    /** With provided ellipsoid
    165     * @param model ellipsoid model
    166     **/
    167    public DistanceMetric(Ellipsoid model) {
    168        iModel = model;
    169        if (iModel == Ellipsoid.LEGACY) {
    170            iSpeed = 100.0 / 15;
    171            iInstructorDiscouragedLimit = 5.0;
    172            iInstructorProhibitedLimit = 20.0;
    173        }
    174    }
    175
    176    /** With provided ellipsoid and student speed
    177     * @param model ellipsoid model
    178     * @param speed student speed in meters per minute
    179     **/
    180    public DistanceMetric(Ellipsoid model, double speed) {
    181        iModel = model;
    182        iSpeed = speed;
    183    }
    184
    185    /** Configured using properties
    186     * @param properties solver configuration
    187     **/
    188    public DistanceMetric(DataProperties properties) {
    189        if (Ellipsoid.LEGACY.name().equals(properties.getProperty("Distances.Ellipsoid",Ellipsoid.LEGACY.name()))) {
    190            //LEGACY MODE
    191            iModel = Ellipsoid.LEGACY;
    192            iSpeed = properties.getPropertyDouble("Student.DistanceLimit", 1000.0 / 15) / 10.0;
    193            iInstructorNoPreferenceLimit = properties.getPropertyDouble("Instructor.NoPreferenceLimit", 0.0);
    194            iInstructorDiscouragedLimit = properties.getPropertyDouble("Instructor.DiscouragedLimit", 5.0);
    195            iInstructorProhibitedLimit = properties.getPropertyDouble("Instructor.ProhibitedLimit", 20.0);
    196            iNullDistance = properties.getPropertyDouble("Distances.NullDistance", 1000.0);
    197            iMaxTravelTime = properties.getPropertyInt("Distances.MaxTravelDistanceInMinutes", 60);
    198        } else {
    199            iModel = Ellipsoid.valueOf(properties.getProperty("Distances.Ellipsoid", Ellipsoid.WGS84.name()));
    200            if (iModel == null) iModel = Ellipsoid.WGS84;
    201            iSpeed = properties.getPropertyDouble("Distances.Speed", properties.getPropertyDouble("Student.DistanceLimit", 1000.0 / 15));
    202            iInstructorNoPreferenceLimit = properties.getPropertyDouble("Instructor.NoPreferenceLimit", iInstructorNoPreferenceLimit);
    203            iInstructorDiscouragedLimit = properties.getPropertyDouble("Instructor.DiscouragedLimit", iInstructorDiscouragedLimit);
    204            iInstructorProhibitedLimit = properties.getPropertyDouble("Instructor.ProhibitedLimit", iInstructorProhibitedLimit);
    205            iNullDistance = properties.getPropertyDouble("Distances.NullDistance", iNullDistance);
    206            iMaxTravelTime = properties.getPropertyInt("Distances.MaxTravelDistanceInMinutes", 60);
    207        }
    208        iComputeDistanceConflictsBetweenNonBTBClasses = properties.getPropertyBoolean(
    209                "Distances.ComputeDistanceConflictsBetweenNonBTBClasses", iComputeDistanceConflictsBetweenNonBTBClasses);
    210        iShortDistanceAccommodationReference = properties.getProperty(
    211                "Distances.ShortDistanceAccommodationReference", iShortDistanceAccommodationReference);
    212        iInstructorLongTravelInMinutes = properties.getPropertyDouble("Instructor.InstructorLongTravelInMinutes", 30.0);
    213        iAllowedDistanceInMinutes = properties.getPropertyInt("HardDistanceConflict.AllowedDistanceInMinutes", iAllowedDistanceInMinutes);
    214        iDistanceHardLimitInMinutes = properties.getPropertyInt("HardDistanceConflict.DistanceHardLimitInMinutes", iDistanceHardLimitInMinutes);
    215        iDistanceLongLimitInMinutes = properties.getPropertyInt("HardDistanceConflict.DistanceLongLimitInMinutes", iDistanceLongLimitInMinutes);
    216        iHardDistanceConflicts = properties.getPropertyBoolean("Sectioning.HardDistanceConflict", iHardDistanceConflicts);
    217    }
    218
    219    /** Degrees to radians
    220     * @param deg degrees
    221     * @return radians
    222     **/
    223    protected double deg2rad(double deg) {
    224        return deg * Math.PI / 180;
    225    }
    226
    227    /** Compute distance between the two given coordinates
    228     * @param lat1 first coordinate's latitude
    229     * @param lon1 first coordinate's longitude
    230     * @param lat2 second coordinate's latitude
    231     * @param lon2 second coordinate's longitude
    232     * @return distance in meters
    233     * @deprecated Use @{link {@link DistanceMetric#getDistanceInMeters(Long, Double, Double, Long, Double, Double)} instead (to include travel time matrix when available).
    234     */
    235    @Deprecated
    236    public double getDistanceInMeters(Double lat1, Double lon1, Double lat2, Double lon2) {
    237        if (lat1 == null || lat2 == null || lon1 == null || lon2 == null)
    238            return iNullDistance;
    239
    240        if (lat1.equals(lat2) && lon1.equals(lon2)) return 0.0;
    241
    242        // legacy mode -- euclidian distance, 1 unit is 10 meters
    243        if (iModel == Ellipsoid.LEGACY) {
    244            if (lat1 < 0 || lat2 < 0 || lon1 < 0 || lon2 < 0) return iNullDistance;
    245            double dx = lat1 - lat2;
    246            double dy = lon1 - lon2;
    247            return Math.sqrt(dx * dx + dy * dy);
    248        }
    249
    250        String id = null;
    251        if (lat1 < lat2 || (lat1 == lat2 && lon1 <= lon2)) {
    252            id =
    253                Long.toHexString(Double.doubleToRawLongBits(lat1)) +
    254                Long.toHexString(Double.doubleToRawLongBits(lon1)) +
    255                Long.toHexString(Double.doubleToRawLongBits(lat2)) +
    256                Long.toHexString(Double.doubleToRawLongBits(lon2));
    257        } else {
    258            id =
    259                Long.toHexString(Double.doubleToRawLongBits(lat1)) +
    260                Long.toHexString(Double.doubleToRawLongBits(lon1)) +
    261                Long.toHexString(Double.doubleToRawLongBits(lat2)) +
    262                Long.toHexString(Double.doubleToRawLongBits(lon2));
    263        }
    264
    265        iLock.readLock().lock();
    266        try {
    267            Double distance = iDistanceCache.get(id);
    268            if (distance != null) return distance;
    269        } finally {
    270            iLock.readLock().unlock();
    271        }
    272
    273        iLock.writeLock().lock();
    274        try {
    275            Double distance = iDistanceCache.get(id);
    276            if (distance != null) return distance;
    277
    278            double a = iModel.a(), b = iModel.b(),  f = iModel.f();  // ellipsoid params
    279            double L = deg2rad(lon2-lon1);
    280            double U1 = Math.atan((1-f) * Math.tan(deg2rad(lat1)));
    281            double U2 = Math.atan((1-f) * Math.tan(deg2rad(lat2)));
    282            double sinU1 = Math.sin(U1), cosU1 = Math.cos(U1);
    283            double sinU2 = Math.sin(U2), cosU2 = Math.cos(U2);
    284
    285            double lambda = L, lambdaP, iterLimit = 100;
    286            double cosSqAlpha, cos2SigmaM, sinSigma, cosSigma, sigma, sinLambda, cosLambda;
    287            do {
    288              sinLambda = Math.sin(lambda);
    289              cosLambda = Math.cos(lambda);
    290              sinSigma = Math.sqrt((cosU2*sinLambda) * (cosU2*sinLambda) +
    291                (cosU1*sinU2-sinU1*cosU2*cosLambda) * (cosU1*sinU2-sinU1*cosU2*cosLambda));
    292              if (sinSigma==0) return 0;  // co-incident points
    293              cosSigma = sinU1*sinU2 + cosU1*cosU2*cosLambda;
    294              sigma = Math.atan2(sinSigma, cosSigma);
    295              double sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
    296              cosSqAlpha = 1 - sinAlpha*sinAlpha;
    297              cos2SigmaM = cosSigma - 2*sinU1*sinU2/cosSqAlpha;
    298              if (Double.isNaN(cos2SigmaM)) cos2SigmaM = 0;  // equatorial line: cosSqAlpha=0 (�6)
    299              double C = f/16*cosSqAlpha*(4+f*(4-3*cosSqAlpha));
    300              lambdaP = lambda;
    301              lambda = L + (1-C) * f * sinAlpha *
    302                (sigma + C*sinSigma*(cos2SigmaM+C*cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)));
    303            } while (Math.abs(lambda-lambdaP) > 1e-12 && --iterLimit>0);
    304            if (iterLimit==0) return Double.NaN; // formula failed to converge
    305
    306            double uSq = cosSqAlpha * (a*a - b*b) / (b*b);
    307            double A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
    308            double B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
    309            double deltaSigma = B*sinSigma*(cos2SigmaM+B/4*(cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)-
    310              B/6*cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));
    311
    312            // initial & final bearings
    313            // double fwdAz = Math.atan2(cosU2*sinLambda, cosU1*sinU2-sinU1*cosU2*cosLambda);
    314            // double revAz = Math.atan2(cosU1*sinLambda, -sinU1*cosU2+cosU1*sinU2*cosLambda);
    315
    316            // s = s.toFixed(3); // round to 1mm precision
    317
    318            distance = b*A*(sigma-deltaSigma);
    319            iDistanceCache.put(id, distance);
    320            return distance;
    321        } finally {
    322            iLock.writeLock().unlock();
    323        }
    324    }
    325
    326    /**
    327     * Compute distance in minutes.
    328     * Property Distances.Speed (in meters per minute) is used to convert meters to minutes, defaults to 1000 meters per 15 minutes (that means 66.67 meters per minute).
    329     * @param lat1 first coordinate's latitude
    330     * @param lon1 first coordinate's longitude
    331     * @param lat2 second coordinate's latitude
    332     * @param lon2 second coordinate's longitude
    333     * @return distance in minutes
    334     * @deprecated Use @{link {@link DistanceMetric#getDistanceInMinutes(Long, Double, Double, Long, Double, Double)} instead (to include travel time matrix when available).
    335     */
    336    @Deprecated
    337    public int getDistanceInMinutes(double lat1, double lon1, double lat2, double lon2) {
    338        return (int) Math.round(getDistanceInMeters(lat1, lon1, lat2, lon2) / iSpeed);
    339    }
    340
    341    /**
    342     * Converts minutes to meters.
    343     * Property Distances.Speed (in meters per minute) is used, defaults to 1000 meters per 15 minutes.
    344     * @param min minutes to travel
    345     * @return meters to travel
    346     */
    347    public double minutes2meters(int min) {
    348        return iSpeed * min;
    349    }
    350
    351
    352    /** Back-to-back classes in rooms within this limit have neutral preference
    353     * @return limit in meters
    354     **/
    355    public double getInstructorNoPreferenceLimit() {
    356        return iInstructorNoPreferenceLimit;
    357    }
    358
    359    /** Back-to-back classes in rooms within this limit have discouraged preference
    360     * @return limit in meters
    361     **/
    362    public double getInstructorDiscouragedLimit() {
    363        return iInstructorDiscouragedLimit;
    364    }
    365
    366    /** Back-to-back classes in rooms within this limit have strongly discouraged preference, it is prohibited to exceed this limit.
    367     * @return limit in meters
    368     **/
    369    public double getInstructorProhibitedLimit() {
    370        return iInstructorProhibitedLimit;
    371    }
    372
    373    /**
    374     * When Distances.ComputeDistanceConflictsBetweenNonBTBClasses is enabled, distance limit (in minutes)
    375     * for a long travel.
    376     * @return travel time in minutes
    377     */
    378    public double getInstructorLongTravelInMinutes() {
    379        return iInstructorLongTravelInMinutes;
    380    }
    381
    382    /** True if legacy mode is used (Euclidian distance where 1 unit is 10 meters)
    383     * @return true if the ellipsoid model is the old one
    384     **/
    385    public boolean isLegacy() {
    386        return iModel == Ellipsoid.LEGACY;
    387    }
    388
    389    /** Maximal travel distance between rooms when no coordinates are given
    390     * @return travel time in minutes
    391     **/
    392    public int getMaxTravelDistanceInMinutes() {
    393        return iMaxTravelTime;
    394    }
    395
    396    /** Set maximal travel distance between rooms when no coordinates are given
    397     * @param maxTravelTime max travel time in minutes
    398     */
    399    public void setMaxTravelDistanceInMinutes(int maxTravelTime) {
    400        iMaxTravelTime = maxTravelTime;
    401    }
    402
    403    /** Add travel time between two locations
    404     * @param roomId1 first room's id
    405     * @param roomId2 second room's id
    406     * @param travelTimeInMinutes travel time in minutes
    407     **/
    408    public void addTravelTime(Long roomId1, Long roomId2, Integer travelTimeInMinutes) {
    409        iLock.writeLock().lock();
    410        try {
    411            if (roomId1 == null || roomId2 == null) return;
    412            if (roomId1 < roomId2) {
    413                Map<Long, Integer> times = iTravelTimes.get(roomId1);
    414                if (times == null) { times = new HashMap<Long, Integer>(); iTravelTimes.put(roomId1, times); }
    415                if (travelTimeInMinutes == null)
    416                    times.remove(roomId2);
    417                else
    418                    times.put(roomId2, travelTimeInMinutes);
    419            } else {
    420                Map<Long, Integer> times = iTravelTimes.get(roomId2);
    421                if (times == null) { times = new HashMap<Long, Integer>(); iTravelTimes.put(roomId2, times); }
    422                if (travelTimeInMinutes == null)
    423                    times.remove(roomId1);
    424                else
    425                    times.put(roomId1, travelTimeInMinutes);
    426            }
    427        } finally {
    428            iLock.writeLock().unlock();
    429        }
    430    }
    431
    432    /** Return travel time between two locations.
    433     * @param roomId1 first room's id
    434     * @param roomId2 second room's id
    435     * @return travel time in minutes
    436     **/
    437    public Integer getTravelTimeInMinutes(Long roomId1, Long roomId2) {
    438        iLock.readLock().lock();
    439        try {
    440            if (roomId1 == null || roomId2 == null) return null;
    441            if (roomId1 < roomId2) {
    442                Map<Long, Integer> times = iTravelTimes.get(roomId1);
    443                return (times == null ? null : times.get(roomId2));
    444            } else {
    445                Map<Long, Integer> times = iTravelTimes.get(roomId2);
    446                return (times == null ? null : times.get(roomId1));
    447            }
    448        } finally {
    449            iLock.readLock().unlock();
    450        }
    451    }
    452
    453    /** Return travel time between two locations. Travel times are used when available, use coordinates otherwise.
    454     * @param roomId1 first room's id
    455     * @param lat1 first room's latitude
    456     * @param lon1 first room's longitude
    457     * @param roomId2 second room's id
    458     * @param lat2 second room's latitude
    459     * @param lon2 second room's longitude
    460     * @return distance in minutes
    461     **/
    462    public Integer getDistanceInMinutes(Long roomId1, Double lat1, Double lon1, Long roomId2, Double lat2, Double lon2) {
    463        Integer distance = getTravelTimeInMinutes(roomId1, roomId2);
    464        if (distance != null) return distance;
    465
    466        if (lat1 == null || lat2 == null || lon1 == null || lon2 == null)
    467            return getMaxTravelDistanceInMinutes();
    468        else
    469            return (int) Math.min(getMaxTravelDistanceInMinutes(), Math.round(getDistanceInMeters(lat1, lon1, lat2, lon2) / iSpeed));
    470    }
    471
    472    /** Return travel distance between two locations.  Travel times are used when available, use coordinates otherwise
    473     * @param roomId1 first room's id
    474     * @param lat1 first room's latitude
    475     * @param lon1 first room's longitude
    476     * @param roomId2 second room's id
    477     * @param lat2 second room's latitude
    478     * @param lon2 second room's longitude
    479     * @return distance in meters
    480     **/
    481    public double getDistanceInMeters(Long roomId1, Double lat1, Double lon1, Long roomId2, Double lat2, Double lon2) {
    482        Integer distance = getTravelTimeInMinutes(roomId1, roomId2);
    483        if (distance != null) return minutes2meters(distance);
    484
    485        return getDistanceInMeters(lat1, lon1, lat2, lon2);
    486    }
    487
    488    /** Return travel times matrix
    489     * @return travel times matrix
    490     **/
    491    public Map<Long, Map<Long, Integer>> getTravelTimes() { return iTravelTimes; }
    492
    493    /**
    494     * True if distances should be considered between classes that are NOT back-to-back. Distance in minutes is then
    495     * to be compared with the difference between end of the last class and start of the second class plus break time of the first class.
    496     * @return true if distances should be considered between classes that are NOT back-to-back
    497     **/
    498    public boolean doComputeDistanceConflictsBetweenNonBTBClasses() {
    499        return iComputeDistanceConflictsBetweenNonBTBClasses;
    500    }
    501
    502    public void setComputeDistanceConflictsBetweenNonBTBClasses(boolean computeDistanceConflictsBetweenNonBTBClasses) {
    503        iComputeDistanceConflictsBetweenNonBTBClasses = computeDistanceConflictsBetweenNonBTBClasses;
    504    }
    505
    506    /**
    507     * Reference of the accommodation of students that need short distances
    508     */
    509    public String getShortDistanceAccommodationReference() {
    510        return iShortDistanceAccommodationReference;
    511    }
    512
    513    /** Allowed distance in minutes (for {@link HardDistanceConflicts}) */
    514    public int getAllowedDistanceInMinutes() {
    515        return iAllowedDistanceInMinutes;
    516    }
    517    /** Hard distance limit in minutes (for {@link HardDistanceConflicts}) */
    518    public int getDistanceHardLimitInMinutes() {
    519        return iDistanceHardLimitInMinutes;
    520    }
    521    /** Long distance limit in minutes (for display) */
    522    public int getDistanceLongLimitInMinutes() {
    523        return iDistanceLongLimitInMinutes;
    524    }
    525    /** Hard distance conflicts enabled (for {@link HardDistanceConflicts}) */
    526    public boolean isHardDistanceConflictsEnabled() {
    527        return iHardDistanceConflicts;
    528    }
    529
    530
    531    /** Few tests
    532     * @param args program arguments
    533     **/
    534    public static void main(String[] args) {
    535        System.out.println("Distance between Prague and Zlin: " + new DistanceMetric().getDistanceInMeters(50.087661, 14.420535, 49.226736, 17.668856) / 1000.0 + " km");
    536        System.out.println("Distance between ENAD and PMU: " + new DistanceMetric().getDistanceInMeters(40.428323, -86.912785, 40.425078, -86.911474) + " m");
    537        System.out.println("Distance between ENAD and ME: " + new DistanceMetric().getDistanceInMeters(40.428323, -86.912785, 40.429338, -86.91267) + " m");
    538        System.out.println("Distance between Prague and Zlin: " + new DistanceMetric().getDistanceInMinutes(50.087661, 14.420535, 49.226736, 17.668856) / 60 + " hours");
    539        System.out.println("Distance between ENAD and PMU: " + new DistanceMetric().getDistanceInMinutes(40.428323, -86.912785, 40.425078, -86.911474) + " minutes");
    540        System.out.println("Distance between ENAD and ME: " + new DistanceMetric().getDistanceInMinutes(40.428323, -86.912785, 40.429338, -86.91267) + " minutes");
    541    }
    542
    543}