package edu.colostate.vchill;
import edu.colostate.vchill.chill.ChillMomentFieldScale;
import java.text.NumberFormat;
/**
* Utility methods for displaying information in the View module
*
* @author Jochen Deyke
* @author jpont
* @version 2010-08-02
*/
public final class ViewUtil {
private static final NumberFormat nf = Config.getInstance().getNumberFormat();
private static final ScaleManager sm = ScaleManager.getInstance();
private static final LocationManager lm = LocationManager.getInstance();
/* Constants */
private static final double K_E = 4.0 / 3.0;
/**
* The types of ellipsoids available.
* <p/>
* These are used to choose one of several available ellipsoid models
* of the earth. The ellipsoid used depends on the data source. GPS
* data makes use of the WGS-84 earth-centric model. If no specific
* model is to be used, use the DEFAULT_ELLIPSOID constant, which
* currently selects the WGS-84 model.
*/
private enum Ellipsoid {
/**
* World Geodetic System, 1984 update -- used by GPS
*/
WGS_1984(6378137, 298.257223563),
/**
* Clarke Ellipsoid (Nories)
*/
CLARKE_1880(6378249.145, 293.465),
/**
* World Geodetic System, 1972 update
*/
WGS_1972(6378153, 298.26),
/**
* Australian National Speroid, 1966
*/
ANS_1966(6378160, 298.25),
/**
* 1980 Geodetic Reference System ellipsoid
*/
GRS_1980(6378137, 298.257222101),;
/**
* Semimajor axis
*/
public final double a;
/**
* Reciprocal of Flattening factor
*/
public final double f;
Ellipsoid(final double a, final double f) {
this.a = a;
this.f = f;
}
}
private static final Ellipsoid DEFAULT_ELLIPSOID = Ellipsoid.WGS_1984;
/**
* Earth model to use for height calculation.
* <p/>
* The Flat model assumes the earth is flat, and uses a simple
* trigonometric solution to find the height.
* The curved model assumes the earth is spherical, which is
* valid over the typical ranges at which radar targets can be
* seen
*/
private enum EarthModel {
FLAT,
CURVED,
}
private static double meridionalDist(final double lat, final double e, final double a) {
double e2 = e * e;
double e4 = e2 * e2;
double e6 = e2 * e4;
double v1 = 1.0 - e2 / 4.0 - 3.0 * e4 / 64.0 - 5.0 * e6 / 256.0;
double v2 = 3.0 / 8.0 * (e2 - e4 / 4.0 + 15.0 * e6 / 128.0);
double v3 = 15.0 / 256.0 * (e4 + 3.0 * e6 / 4.0);
return a * (v1 * lat - v2 * Math.sin(2.0 * lat) + v3 * Math.sin(4.0 * lat));
}
private static double meridionalPart(final double lat, final double e) {
double temp = e * Math.sin(lat);
return Math.log(Math.tan(Math.PI / 4.0 + lat / 2.0)) - 0.5 * e * Math.log((1 + temp) / (1 - temp));
}
/**
* Integrate along path to find approximate target latitude
*/
private static double invSolution(final double ref_lat_r,
final double e, final double a,
final double range, final double azimuth_r) {
double m_from = meridionalDist(ref_lat_r, e, a);
double m_guess = m_from + range * Math.cos(azimuth_r);
double lat = ref_lat_r + Math.PI * (m_guess - m_from) / (1852 * 60.0 * 180.0);
for (int iteration = 0; iteration < 10; ++iteration) {
double m_partial = meridionalDist(lat, e, a);
lat = lat + Math.PI * (m_guess - m_partial) / (1852 * 60.0 * 180.0);
}
return lat;
}
/**
* Convert from lat/long to range/heading
*
* @param lat_d Destination point latitude (in degrees)
* @param long_d Destination point longitude (in degrees)
* @param ref_lat_d Reference point latitude (in degrees)
* @param ref_long_d Reference point longitude (in degrees)
* @param ellipsoid The reference ellipsoid to use when making computations.
* Can be set to DEFAULT_ELLIPSOID to use the default (WGS-84)
* @return [range (in m), azimuth (in degrees)] from reference to destination
*/
private static double[] latlongToRtheta(
double lat_d, double long_d,
double ref_lat_d, double ref_long_d,
Ellipsoid ellipsoid) {
double d_lat, d_long;
double course;
double distance;
double delta_mdist;
double delta_mpart;
double lat_r = Math.toRadians(lat_d);
double long_r = Math.toRadians(long_d);
double ref_lat_r = Math.toRadians(ref_lat_d);
double ref_long_r = Math.toRadians(ref_long_d);
double flattening = 1 / ellipsoid.f;
double eccentricity = Math.sqrt(2.0 * flattening - flattening * flattening);
double major_axis = ellipsoid.a;
if (Math.signum(ref_long_r) == Math.signum(long_r)) {
d_long = long_r - ref_long_r;
} else {
d_long = 2.0 * Math.PI - Math.abs(long_r) - Math.abs(ref_long_r);
if ((long_r - ref_long_r) >= 0.0) {
d_long = -d_long;
}
}
d_lat = lat_r - ref_lat_r;
if (lat_r == ref_lat_r) { /* Special case of the same latitude */
course = (d_long > 0.0) ? 90.0 : 270.0;
double temp = eccentricity * Math.sin(lat_r);
distance = major_axis * d_long * Math.cos(lat_r) / Math.sqrt(1 - temp * temp);
} else if (long_r == ref_long_r) { /* Special case of the same longitude */
delta_mdist = meridionalDist(lat_r, eccentricity, major_axis) -
meridionalDist(ref_lat_r, eccentricity, major_axis);
distance = Math.abs(delta_mdist * 60.0);
course = (d_lat > 0.0) ? 0.0 : 180.0;
} else {
delta_mpart = meridionalPart(lat_r, eccentricity) -
meridionalPart(ref_lat_r, eccentricity);
delta_mdist = meridionalDist(lat_r, eccentricity, major_axis) -
meridionalDist(ref_lat_r, eccentricity, major_axis);
course = Math.atan2(d_long, delta_mpart);
if (course < 0.0) course += (2.0 * Math.PI);
distance = delta_mdist / Math.cos(course);
}
return new double[]{distance, Math.toDegrees(course)};
}
/**
* Convert from range/heading to lat/long
*
* @param rng_m Range (in m) from reference pt to destination is stored here
* @param azimuth_d Azimuth (in degrees) from ref. pt to destination stored here
* @param ref_lat_d Reference point latitude
* @param ref_long_d Reference point longitude
* @param ellipsoid The reference ellipsoid to use when making computations.
* Can be set to DEFAULT_ELLIPSOID to use the default (WGS-84)
* @return [lat, long] (in degrees)
*/
private static double[] rthetaToLatlong(
double rng_m, double azimuth_d,
double ref_lat_d, double ref_long_d,
Ellipsoid ellipsoid) {
double lat_r, long_r;
double flattening = 1 / ellipsoid.f;
double eccentricity = Math.sqrt(2.0 * flattening - flattening * flattening);
double major_axis = ellipsoid.a;
double azimuth_r = Math.toRadians(azimuth_d);
double ref_lat_r = Math.toRadians(ref_lat_d);
double ref_long_r = Math.toRadians(ref_long_d);
/* Special case various cases which would cause tan and atan to blow up */
if ((azimuth_r == 0.0) || (azimuth_r == Math.PI)) {
lat_r = invSolution(ref_lat_r, eccentricity, major_axis, rng_m, azimuth_r);
long_r = ref_long_r;
} else if ((azimuth_r == Math.PI / 2.0) || (azimuth_r == (3.0 * Math.PI / 2.0))) {
double temp = eccentricity * Math.sin(ref_lat_r);
lat_r = ref_lat_r;
long_r = ref_long_r + ((azimuth_r == Math.PI / 2.0) ? rng_m : -rng_m) * Math.sqrt(1 - temp * temp) /
(major_axis * Math.cos(ref_lat_r));
} else {
lat_r = invSolution(ref_lat_r, eccentricity, major_axis, rng_m, azimuth_r);
double delta_mpart = meridionalPart(lat_r, eccentricity) -
meridionalPart(ref_lat_r, eccentricity);
long_r = ref_long_r + delta_mpart * Math.tan(azimuth_r);
if (azimuth_r < Math.PI) {
long_r = ref_long_r + Math.abs(delta_mpart * Math.tan(azimuth_r));
} else {
long_r = ref_long_r - Math.abs(delta_mpart * Math.tan(azimuth_r));
}
}
if (long_r > Math.PI) long_r -= (2.0 * Math.PI);
if (long_r < -Math.PI) long_r += (2.0 * Math.PI);
return new double[]{Math.toDegrees(lat_r), Math.toDegrees(long_r)};
}
/**
* Convert from target slant range/elevation to height above ground
* and distance along ground
*
* @param earthModel Earth model to use when performing calculations
* @param ellipsoid The reference ellipsoid to use when making computations.
* Can be set to DEFAULT_ELLIPSOID to use the default (WGS-84)
* @param range Slant range to target, in meters
* @param elevation Elevation to target, in degrees
* @return [height above ground, distance along ground] in meters
*/
private static double[] getHeight(final EarthModel earthModel, final Ellipsoid ellipsoid, final double range, final double elevation) {
double earth_radius = ellipsoid.a;
double elevation_r = Math.toRadians(elevation);
switch (earthModel) {
case FLAT:
return new double[]{range * Math.tan(elevation_r), range * Math.cos(elevation_r)};
case CURVED:
double height = Math.sqrt(range * range + K_E * K_E * earth_radius * earth_radius + 2.0 * range * K_E * earth_radius * Math.sin(elevation_r)) - K_E * earth_radius;
return new double[]{height, K_E * earth_radius * Math.asin(range * Math.cos(elevation_r) / (K_E * earth_radius + height))};
}
return null;
}
/**
* Converts km from radar to range/heading
*
* @param x km east of the radar
* @param y km north of the radar
* @return [range (in m), azimuth (in degrees)]
*/
public static double[] kmToRtheta(final double x, final double y) {
double range = 1e3 * Math.sqrt(x * x + y * y);
double azimuth = 90 - Math.toDegrees(Math.PI / 2 - Math.atan2(x, y));
if (azimuth < 0) azimuth += 360;
return new double[]{range, azimuth};
}
/**
* Converts range/heading to km from the radar
*
* @param range in m
* @param azimuth in degrees
* @param isMap specifies if this conversion is for a map
* @return [x , y] in km east, north of the radar
*/
public static double[] rthetaToKm(final double range, final double azimuth, final boolean isMap) {
if (isMap) {
double radians = Math.toRadians(90 - azimuth);
if (radians < 0) radians += 2 * Math.PI;
double x = range * 1e-3 * Math.cos(radians);
double y = range * 1e-3 * Math.sin(radians);
return new double[]{x, y};
} else
return new double[]{range * Math.cos(Math.toRadians(azimuth)), range * Math.sin(Math.toRadians(azimuth))};
}
/**
* Private default constructor prevents instantiation
*/
private ViewUtil() {
}
/**
* Format a double into a string
*
* @param value the value to format
* @return the formatted string version of <code>value</code>
*/
public static String format(final double value) {
return nf.format(value);
}
/**
* Calculates the elevation above ground level from a given curved elevation
*
* @param height the distance (km) above the radar
* @param range the distance (km) from the radar
* @return the distance (km) above ground level
public static double getElevationAngle (final double height, final double range)
{
return 180 + (Math.atan(Math.sin(Math.PI - range / a)/(a / (height + a) + Math.cos(Math.PI - range / a))) - Math.PI / 2)/(Math.PI / 180.0);
}
*/
/**
* Calculates the elevation taking into account the Earth's curve
*
* @param el the eleavation angle
* @param range the distance (km) from the radar
* @return the distance (km) above the radar
*/
public static double getKmElevation(final double el, final double range) {
return getHeight(EarthModel.CURVED, DEFAULT_ELLIPSOID, range * 1000, el)[0] / 1000;
}
/**
* Converts lat/long to km from the radar
*
* @param lon longitude of target (in degrees)
* @param lat latitude of target (in degrees)
* @param isMap specifies if this conversion is for a map
* @return [x, y] in km east, north of location given by LocationManager
*/
public static double[] getKm(final double lon, final double lat, final boolean isMap) {
double[] rtheta = latlongToRtheta(lat, lon, lm.getLatitude(), lm.getLongitude(), DEFAULT_ELLIPSOID);
double[] km = rthetaToKm(rtheta[0], rtheta[1], isMap);
return km;
}
/**
* Converts km north/east of the radar into degrees latitude/longitude
*
* @param x km east of the radar
* @param y km north of the radar
* @return longitude, latitude in degrees
*/
public static double[] getDegrees(final double x, final double y) {
return getDegrees(x, y, lm.getLongitude(), lm.getLatitude());
}
/**
* Converts km north/east of the specified center into degrees latitude/longitude
*
* @param x km east of the radar
* @param y km north of the radar
* @param centerLongitude in degrees
* @param centerLatitude in degrees
* @return longitude, latitude in degrees
*/
public static double[] getDegrees(final double x, final double y, final double centerLongitude, final double centerLatitude) {
double[] rtheta = kmToRtheta(x, y);
double[] degrees = rthetaToLatlong(rtheta[0], rtheta[1], centerLatitude, centerLongitude, DEFAULT_ELLIPSOID);
return new double[]{degrees[1], degrees[0]};
}
/**
* Converts km distances to kft
*
* @param km the distance to convert
* @return the distance in kft
*/
public static double getKFtFromKm(final double km) {
return km * 3 / 0.9144;
}
/**
* Converts a compressed byte value to
* an uncompressed double-precision floating point equivalent
*
* @param byteValue the compressed value - valid range is 0-255 inclusive
* @param type the name of the data type
* @return the uncompressed value
*/
public static double getValue(final int byteValue, final String type) {
return sm.getScale(type).getValue(byteValue);
}
/**
* Converts an uncompressed double-precision floating point value to
* a compressed byte equivalent
*
* @param doubleValue the uncompressed value
* @param type the name of the data type
* @return the compressed value - valid range is 0-255 inclusive
*/
public static int getHash(final double doubleValue, final String type) {
int hash = sm.getScale(type).getHash(doubleValue);
return hash;
}
/**
* Converts an array of compressed byte values to
* an array of uncompressed double-precision floating point equivalents
*
* @param bytes the compressed values
* @param type the name of the data type
* @return the uncompressed values
*/
public static double[] getValues(final byte[] bytes, final String type) {
ChillMomentFieldScale scale = sm.getScale(type);
double[] values = new double[bytes.length];
for (int i = 0; i < bytes.length; ++i) values[i] = scale.getValue(bytes[i] & 0xff);
return values;
}
/**
* Converts an array of uncompressed double-precision floating point values to
* an array of compressed byte equivalents
*
* @param values the uncompressed values
* @param type the name of the data type
* @return the compressed values
*/
public static byte[] getBytes(final double[] values, final String type) {
ChillMomentFieldScale scale = sm.getScale(type);
byte[] bytes = new byte[values.length];
for (int i = 0; i < values.length; ++i) bytes[i] = (byte) scale.getHash(values[i]);
return bytes;
}
}