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 <= limit → no preference), Instructor.DiscouragedLimit (distance <= limit → discouraged), 018 * Instructor.ProhibitedLimit (distance <= limit → 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}