/* * Copyright (C) 2012 The Serval Project * * This file is part of the Serval Maps Software * * Serval Maps Software is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This source code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this source code; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.servalproject.maps.utils; import java.text.DecimalFormat; import org.servalproject.maps.R; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; /** * a utility class that contains geographic utility methods */ public class GeoUtils { /* * declare public class level constants */ /** * flag to indicate use of the vincenty formula for distance calculations */ public static final int VINCENTY_FORMULA = 0; /** * flag to indicate use of the haversine formula for distance calculations */ public static final int HAVERSINE_FORMULA = 1; /** * flag to indicate that the distance must be in metres */ public static final int METRE_UNITS = 10; /** * flag to indicate that the distance must be in miles */ public static final int MILE_UNITS = 11; /** * flag to indicate the the distance must be in nautical miles */ public static final int NAUTICAL_MILE_UNITS = 12; /* * declare private class level constants */ public static final double MILE_CONVERT_FACTOR = 0.000621371192; public static final double NAUTICAL_MILE_CONVERT_FACTOR = 0.000539956803; /** * calculate the distance between the provided geocoordinate pair using preferences * for the algorithm and units * * @param lat1 the latitude of the first coordinate pair * @param lon1 the longitude of the first coordinate pair * @param lat2 the latitude of the second coordinate pair * @param lon2 the longitude of the second coordinate pair * @param context a context to use to get access to resources and preferences * @return */ public static String calculateDistanceWithDefaults(double lat1, double lon1, double lat2, double lon2, Context context) { // get the preferences for the distance calculation SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); String mPreference = mPreferences.getString("preferences_measurement_units", null); int mUnits = GeoUtils.METRE_UNITS; if(mPreference != null) { mUnits = Integer.parseInt(mPreference); } mPreference = mPreferences.getString("preferences_measurement_algorithm", null); int mAlgorithm = GeoUtils.HAVERSINE_FORMULA; if(mPreference != null) { mAlgorithm = Integer.parseInt(mPreference); } double mDistance = GeoUtils.calculateDistance( lat1, lon1, lat2, lon2, mAlgorithm, mUnits); String mDistanceAsString = null; if(mDistance != Double.NaN) { // round to two decimal places DecimalFormat mFormat = new DecimalFormat("#.##"); switch(mUnits){ case GeoUtils.METRE_UNITS: // use metres string if(mDistance > 1) { mDistanceAsString = String.format(context.getString(R.string.misc_disance_kms), mFormat.format(mDistance)); } else { mDistance = mDistance * 1000; mDistanceAsString = String.format(context.getString(R.string.misc_disance_metres), mFormat.format(mDistance)); } break; case GeoUtils.MILE_UNITS: // use mile units mDistanceAsString = String.format(context.getString(R.string.misc_disance_miles), mFormat.format(mDistance)); break; case GeoUtils.NAUTICAL_MILE_UNITS: // use nautical mile units mDistanceAsString = String.format(context.getString(R.string.misc_disance_nautical_miles), mFormat.format(mDistance)); break; } } else { mDistanceAsString = context.getString(R.string.misc_not_available); } return mDistanceAsString; } /** * calculate the distance between two sets of latitude and longitude coordinate pairs * * @param lat1 the latitude of the first coordinate pair * @param lon1 the longitude of the first coordinate pair * @param lat2 the latitude of the second coordinate pair * @param lon2 the longitude of the second coordinate pair * @param formula a flag to indicate which formula to use * @param units a flag to indicate the units of the result * * @return the distance between the coordinate pairs in the units specified calculated with the specified formula * may return Double.NaN if the vincenty formula is used and the formula fails */ public static double calculateDistance(double lat1, double lon1, double lat2, double lon2, int formula, int units) { double mDistance = 0; // calculate the distance switch(formula){ case VINCENTY_FORMULA: mDistance = vincentyFormula(lat1, lon1, lat2, lon2); break; case HAVERSINE_FORMULA: mDistance = haversineFormula(lat1, lon1, lat2, lon2); //mDistance = mDistance / 1000; break; default: throw new IllegalArgumentException("unknown formula flag detected"); } // check to make sure the conversion went ok if(mDistance == Double.NaN) { return mDistance; } // convert distance to the required units switch(units) { case METRE_UNITS: return mDistance; case MILE_UNITS: return mDistance * MILE_CONVERT_FACTOR; case NAUTICAL_MILE_UNITS: return mDistance * NAUTICAL_MILE_CONVERT_FACTOR; default: throw new IllegalArgumentException("unknown unit flag detected"); } } /* * calculate the distance between a coordinate pair using the haversine formula * adapted from code found at the URL below: * http://rosettacode.org/wiki/Haversine_formula#Java */ private static double haversineFormula(double lat1, double lon1, double lat2, double lon2) { double R = 6372.8; // In kilometers double dLat = Math.toRadians(lat2 - lat1); double dLon = Math.toRadians(lon2 - lon1); lat1 = Math.toRadians(lat1); lat2 = Math.toRadians(lat2); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); double c = 2 * Math.asin(Math.sqrt(a)); return R * c; } /* * calculate the distance between a coordinate pair using the vincenty formula * adapted from code found at the URL below: * http://stackoverflow.com/questions/120283/working-with-latitude-longitude-values-in-java#9822531 * * Original JavaScript implementation of the formula available here: * http://www.movable-type.co.uk/scripts/latlong-vincenty.html * * Returns the distance in metres */ private static double vincentyFormula(double lat1, double lon1, double lat2, double lon2) { double a = 6378137, b = 6356752.314245, f = 1 / 298.257223563; // WGS-84 ellipsoid params double L = Math.toRadians(lon2 - lon1); double U1 = Math.atan((1 - f) * Math.tan(Math.toRadians(lat1))); double U2 = Math.atan((1 - f) * Math.tan(Math.toRadians(lat2))); double sinU1 = Math.sin(U1), cosU1 = Math.cos(U1); double sinU2 = Math.sin(U2), cosU2 = Math.cos(U2); double sinLambda, cosLambda, sinSigma, cosSigma, sigma, sinAlpha, cosSqAlpha, cos2SigmaM; double lambda = L, lambdaP, iterLimit = 100; do { sinLambda = Math.sin(lambda); cosLambda = Math.cos(lambda); sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); if (sinSigma == 0) return 0; // co-incident points cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; sigma = Math.atan2(sinSigma, cosSigma); sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma; cosSqAlpha = 1 - sinAlpha * sinAlpha; cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha; if (Double.isNaN(cos2SigmaM)) cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (ยง6) double C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)); lambdaP = lambda; lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))); } while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0); if (iterLimit == 0) return Double.NaN; // formula failed to converge double uSq = cosSqAlpha * (a * a - b * b) / (b * b); double A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))); double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); double deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM))); double dist = b * A * (sigma - deltaSigma); return dist; } }