package org.opensha2.geo;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.opensha2.eq.Earthquakes.checkDepth;
import static org.opensha2.geo.Coordinates.checkLatitude;
import static org.opensha2.geo.Coordinates.checkLongitude;
import org.opensha2.internal.Parsing;
import org.opensha2.internal.Parsing.Delimiter;
import org.opensha2.util.Maths;
import com.google.common.base.Converter;
import com.google.common.collect.FluentIterable;
import com.google.common.primitives.Doubles;
import java.util.List;
import java.util.Objects;
/**
* A {@code Location} represents a point with reference to the earth's
* ellipsoid. It is expressed in terms of latitude, longitude, and depth. As in
* seismology, the convention adopted in here is for depth to be positive-down,
* always. Locations may be defined using longitude values in the range: [-180°,
* 360°]. Location instances are immutable.
*
* <p>Note that although static factory methods take arguments in the order:
* {@code [lat, lon, depth]}, {@code String} representations of a location are
* in the order: {@code [lon, lat, depth]}, consistent with KML, GeoJSON, and
* other digital coordinate formats that match standard plotting coordinate
* order: {@code [x, y, z]}.
*
* <p>For computational convenience, latitude and longitude values are converted
* and stored internally in radians. Special {@code get***Rad()} methods are
* provided to access this native format.
*
* @author Peter Powers
*/
public final class Location implements Comparable<Location> {
private final double lat;
private final double lon;
private final double depth;
private Location(double lat, double lon, double depth) {
this.lat = checkLatitude(lat) * Maths.TO_RAD;
this.lon = checkLongitude(lon) * Maths.TO_RAD;
this.depth = checkDepth(depth);
}
/**
* Create a new {@code Location} with the supplied latitude and longitude and
* a depth of 0 km.
*
* @param lat latitude in decimal degrees
* @param lon longitude in decimal degrees
* @throws IllegalArgumentException if any supplied values are out of range
* @see Coordinates
*/
public static Location create(double lat, double lon) {
return create(lat, lon, 0);
}
/**
* Create a new {@code Location} with the supplied latitude, longitude, and
* depth.
*
* @param lat latitude in decimal degrees
* @param lon longitude in decimal degrees
* @param depth in km (positive down)
* @throws IllegalArgumentException if any supplied values are out of range
* @see Coordinates
*/
public static Location create(double lat, double lon, double depth) {
return new Location(lat, lon, depth);
}
/**
* Generate a new {@code Location} by parsing the supplied {@code String}.
* Method is intended for use with the result of {@link #toString()}.
*
* @param s string to parse
* @throws NumberFormatException if {@code s} is unparseable
* @throws IndexOutOfBoundsException if {@code s} contains fewer than 3
* comma-separated values; any additional values in the suppied string
* are ignored
* @see #toString()
* @see #stringConverter()
*/
public static Location fromString(String s) {
return StringConverter.INSTANCE.reverse().convert(s);
}
/**
* Returns the depth of this {@code Location} in km.
*/
public double depth() {
return depth;
}
/**
* The latitude of this {@code Location} in decimal degrees.
*/
public double lat() {
return lat * Maths.TO_DEG;
}
/**
* The longitude of this {@code Location} in decimal degrees.
*/
public double lon() {
return lon * Maths.TO_DEG;
}
/**
* The latitude of this {@code Location} in radians.
*/
public double latRad() {
return lat;
}
/**
* The longitude of this {@code Location} in radians.
*/
public double lonRad() {
return lon;
}
/**
* Return a KML compatible tuple: 'lon,lat,depth' (no spaces).
* @see #fromString(String)
* @see #stringConverter()
*/
@Override
public String toString() {
return stringConverter().convert(this);
}
/**
* Return a {@link Converter} that converts between {@code Location}s and
* {@code String}s.
*
* <p>Calls to {@code converter.reverse().convert(String)} will throw a
* {@code NumberFormatException} if the values in the supplied string are
* unparseable; or an {@code IndexOutOfBoundsException} if the supplied string
* contains fewer than 3 comma-separated values; any additional values in the
* suppied string are ignored.
*/
public static Converter<Location, String> stringConverter() {
return StringConverter.INSTANCE;
}
private static final class StringConverter extends Converter<Location, String> {
static final StringConverter INSTANCE = new StringConverter();
static final String FORMAT = "%.5f,%.5f,%.5f";
@Override
protected String doForward(Location loc) {
return String.format(FORMAT, loc.lon(), loc.lat(), loc.depth());
}
@Override
protected Location doBackward(String s) {
List<Double> values = FluentIterable
.from(Parsing.split(checkNotNull(s), Delimiter.COMMA))
.transform(Doubles.stringConverter())
.toList();
return create(values.get(1), values.get(0), values.get(2));
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Location)) {
return false;
}
Location loc = (Location) obj;
return this.lat == loc.lat &&
this.lon == loc.lon &&
this.depth == loc.depth;
}
@Override
public int hashCode() {
return Objects.hash(lat, lon, depth);
}
/**
* Compare this {@code Location} to another and sort first by latitude, then
* by longitude. When sorting a list of {@code Location}s, the resultant
* ordering is left-to-right, bottom-to-top.
*
* @param loc {@code Location} to compare {@code this} to
* @return a negative integer, zero, or a positive integer if this
* {@code Location} is less than, equal to, or greater than the
* specified {@code Location}.
*/
@Override
public int compareTo(Location loc) {
double d = (lat == loc.lat) ? lon - loc.lon : lat - loc.lat;
return (d != 0) ? (d < 0) ? -1 : 1 : 0;
}
}