org.opensextant.util.GeodeticUtility.java Source code

Java tutorial

Introduction

Here is the source code for org.opensextant.util.GeodeticUtility.java

Source

/*
 * 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) &lt; 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=&gt; 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);
    }

}