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