/*
* Copyright 2013 ubaldino.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//
// _____ ____ __ __
///\ __`\ /\ _`\ /\ \__ /\ \__
//\ \ \/\ \ _____ __ ___ \ \,\L\_\ __ __ _\ \ ,_\ __ ___ \ \ ,_\
// \ \ \ \ \ /\ '__`\ /'__`\ /' _ `\ \/_\__ \ /'__`\/\ \/'\\ \ \/ /'__`\ /' _ `\\ \ \/
// \ \ \_\ \\ \ \L\ \/\ __/ /\ \/\ \ /\ \L\ \ /\ __/\/> </ \ \ \_ /\ \L\.\_ /\ \/\ \\ \ \_
// \ \_____\\ \ ,__/\ \____\\ \_\ \_\ \ `\____\\ \____\/\_/\_\ \ \__\\ \__/.\_\\ \_\ \_\\ \__\
// \/_____/ \ \ \/ \/____/ \/_/\/_/ \/_____/ \/____/\//\/_/ \/__/ \/__/\/_/ \/_/\/_/ \/__/
// \ \_\
// \/_/
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
//
package org.opensextant.util;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.opensextant.data.GeoBase;
import org.opensextant.data.LatLon;
import com.spatial4j.core.io.GeohashUtils;
/**
* A collection of geodetic routines used within OpenSextant.
* This is a light wrapper around the most common routines - a full API exists
* in other APIs such as GISCore, Geodesy, or Spatial4J
*
* @author ubaldino
*/
public class GeodeticUtility {
/**
*
*/
public static final int LAT_MAX = 90;
/**
*
*/
public static final int LON_MAX = 180;
/**
* TODO: consider using geodesy, however that API has no obvious simple
* validator.
*
* @param lat latitude
* @param lon longitude
* @return if lat/lon is valid
*/
public final static boolean validateCoordinate(double lat, double lon) {
// Java behavior for NaN -- use object/class routines to compare.
//
if (Double.isNaN(lon) || Double.isNaN(lat)) {
return false;
}
if (Math.abs(lat) >= LAT_MAX) {
return false;
}
if (Math.abs(lon) >= LON_MAX) {
return false;
}
return true;
}
/**
* A common check required by practical applications -- 0,0 is not interesting,
* so this is a simple java-based check. double (and all number) values by
* default have a value = 0. This appears to be true for class attributes,
* but not for locals. Hence the NaN check in validateCoordinate.
*
* @param lat in degrees
* @param lon in degress
* @return true if coordinate is non-zero (0.000, 0.000) AND is valid abs(lon) < 180.0, etc.
*/
public final static boolean isValidNonZeroCoordinate(double lat, double lon) {
if (validateCoordinate(lat, lon)) {
return (lat != 0 && lon != 0);
}
return false;
}
/**
* This returns distance in degrees, e.g., this is a Cartesian distance.
* Only to be used for fast comparison of two locations relatively close
* together, e.g., within the same 1 or 2 degrees of lat or lon. Beyond that
* there can be a lot of distortion in the physical distance.
*
*@param p1 point
*@param p2 point
* @return distance between p1 and p2 in degrees.
*/
public final static double distanceDegrees(GeoBase p1, GeoBase p2) {
if (p1 == null || p2 == null) {
return Double.MAX_VALUE;
}
return Math.sqrt(Math.pow((p1.getLatitude() - p2.getLatitude()), 2)
+ Math.pow((p1.getLongitude() - p2.getLongitude()), 2));
}
public static final long EARTH_RADIUS = 6372800L; // In meters
/** Haversine distance using LL1 to LL2;
*
* @param p1 geodesy API LatLon
* @param p2 geodesy API LatLon
* @return distance in meters.
*/
public final static long distanceMeters(LatLon p1, LatLon p2) {
double lat1 = p1.getLatitude();
double lon1 = p1.getLongitude();
double lat2 = p2.getLatitude();
double lon2 = p2.getLongitude();
/* Courtesy of http://rosettacode.org/wiki/Haversine_formula#Java */
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 (long) (EARTH_RADIUS * c);
}
/**
* This returns distance in degrees, e.g., this is a Cartesian distance.
* Only to be used for fast comparison of two locations relatively close
* together, e.g., within the same 1 or 2 degrees of lat or lon. Beyond that
* there can be a lot of distortion in the physical distance.
*
* @param lat1 P1.lat
* @param lon1 P1.lon
* @param lat2 P2.lat
* @param lon2 P2.lon
* @return distance between p1 and p2 in degrees.
*/
public static double distanceDegrees(double lat1, double lon1, double lat2, double lon2) {
return Math.sqrt(Math.pow((lat1 - lat2), 2) + Math.pow((lon1 - lon2), 2));
}
/**
* Precision -- this is a first draft attempt at assigning some error bars
* to geocoding results.
*
* TODO: move this to a configuration file
*
* feat/code: prec # precision is meters of error for a given gazetteer
* entry with feat/code)
*
* A/ADM1: 50000 # ADM1 is generally +/- 50km, world wide P/PPL: 1000 # city
* is generally +/- 1km within center point P/PPLC: 10000 # major capital
* city is 10km of error, etc.
*
*/
public static final Map<String, Integer> FEATURE_PRECISION = new HashMap<String, Integer>();
public static final Map<String, Integer> FEATURE_GEOHASH_PRECISION = new HashMap<String, Integer>();
public static final int DEFAULT_PRECISION = 50000; // +/- 50KM
public static final int DEFAULT_GEOHASH_PRECISION = 5;
static {
FEATURE_PRECISION.put("P", 5000);
FEATURE_PRECISION.put("A", DEFAULT_PRECISION);
FEATURE_PRECISION.put("S", 1000);
FEATURE_PRECISION.put("A/ADM1", DEFAULT_PRECISION);
FEATURE_PRECISION.put("A/ADM2", 20000);
FEATURE_PRECISION.put("P/PPL", 5000);
FEATURE_PRECISION.put("P/PPLC", 10000);
// This helps guage how long should a geohash be for a given feature.
FEATURE_GEOHASH_PRECISION.put("A/PCLI", 3);
FEATURE_GEOHASH_PRECISION.put("CTRY", 3);
FEATURE_GEOHASH_PRECISION.put("P", 6);
FEATURE_GEOHASH_PRECISION.put("A", 4);
FEATURE_GEOHASH_PRECISION.put("S", 8);
FEATURE_GEOHASH_PRECISION.put("A/ADM2", 5);
}
/**
* For a given feature type and code, determine what sort of resolution or
* precision should be considered for that place, approximately.
* @param feat_type major feature type
* @param feat_code minor feature type or designation
* @return precision approx error in meters for a given feature. -1 if no
* feature type given.
*/
public static int getFeaturePrecision(String feat_type, String feat_code) {
if (feat_type == null && feat_code == null) {
// Unknown, uncategorized feature
return DEFAULT_PRECISION;
}
String lookup = (feat_code != null ? feat_type + "/" + feat_code : feat_type);
Integer prec = FEATURE_PRECISION.get(lookup);
if (prec != null) {
return prec.intValue();
}
prec = FEATURE_PRECISION.get(feat_type);
if (prec != null) {
return prec.intValue();
}
return DEFAULT_PRECISION;
}
/** For a given Geonames feature class/designation provide a guess about how long
* geohash should be. Geohash in this use is very approximate
* @param feat_type major feature type
* @param feat_code minor feature type or designation
* @return prefix length for a geohash, e.g., for a province in general is 3 chars of geohash sufficient?
*/
public static int getGeohashPrecision(String feat_type, String feat_code) {
if (feat_type == null && feat_code == null) {
// Unknown, uncategorized feature
return DEFAULT_GEOHASH_PRECISION;
}
String lookup = (feat_code != null ? feat_type + "/" + feat_code : feat_type);
Integer prec = FEATURE_GEOHASH_PRECISION.get(lookup);
if (prec != null) {
return prec.intValue();
}
prec = FEATURE_GEOHASH_PRECISION.get(feat_type);
if (prec != null) {
return prec.intValue();
}
return DEFAULT_GEOHASH_PRECISION;
}
/**
* The most simplistic parsing and validation of "lat lon" or "lat, lon"
* any amount of whitespace is allowed, provided the lat lon order is there.
* @param lat_lon string form of a simple lat/lon, e.g., "Y X"; No symbols
* @return LatLon object
* @throws ParseException if string is unparsable
*/
public static LatLon parseLatLon(String lat_lon) throws ParseException {
if (StringUtils.isBlank(lat_lon)) {
return null;
}
String delim = lat_lon.contains(",") ? "," : " ";
List<String> LL = TextUtils.string2list(lat_lon, delim);
LatLon geo = null;
try {
geo = new GeoBase(null, lat_lon);
geo.setLatitude(Double.parseDouble(LL.get(0)));
geo.setLongitude(Double.parseDouble(LL.get(1)));
} catch (Exception parseerr) {
throw new ParseException("Unable to Parse text as XY:" + parseerr.getMessage(), 0);
}
if (!validateCoordinate(geo.getLatitude(), geo.getLongitude())) {
throw new ParseException("Invalid Coordinate values", 0);
}
return geo;
}
/**
* Parse coordinate from object
* @param lat latitude
* @param lon longitude
* @return LatLon object
* @throws ParseException if objects are not valid numbers
*/
public static LatLon parseLatLon(Object lat, Object lon) throws ParseException {
if (lat == null || lon == null) {
// incomplete data.
// Caller should test
throw new ParseException("Incomplete data, null lat or lon", 0);
}
LatLon yx = new GeoBase();
yx.setLatitude(Double.parseDouble(lat.toString()));
yx.setLongitude(Double.parseDouble(lon.toString()));
return yx;
}
/**
* Create a string representation of a decimal lat/lon.
* @param yx LatLon object
* @return "lat, lon" formatted with 4 decimal places; that is an average amount of precision for common XY=> String uses.
*/
public final static String formatLatLon(final LatLon yx) {
return String.format("%2.4f,%3.4f", yx.getLatitude(), yx.getLongitude());
}
/**
*
* @param yx lat,lon obj
* @return geohash representation of the lat,lon
*/
public final static String geohash(final LatLon yx){
return GeohashUtils.encodeLatLon(yx.getLatitude(), yx.getLongitude());
}
public final static String geohash(double lat, double lon){
return GeohashUtils.encodeLatLon(lat, lon);
}
}