blob: f3d296c6f317cff8f76aac70053f9c58c2f7f6ab [file] [log] [blame]
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package jme3tools.navigation;
import com.jme3.math.Vector3f;
import java.text.DecimalFormat;
/**
* A representation of the actual map in terms of lat/long and x,y,z co-ordinates.
* The Map class contains various helper methods such as methods for determining
* the world unit positions for lat/long coordinates and vice versa. This map projection
* does not handle screen/pixel coordinates.
*
* @author Benjamin Jakobus (thanks to Cormac Gebruers)
* @version 1.0
* @since 1.0
*/
public class MapModel3D {
/* The number of radians per degree */
private final static double RADIANS_PER_DEGREE = 57.2957;
/* The number of degrees per radian */
private final static double DEGREES_PER_RADIAN = 0.0174532925;
/* The map's width in longitude */
public final static int DEFAULT_MAP_WIDTH_LONGITUDE = 360;
/* The top right hand corner of the map */
private Position centre;
/* The x and y co-ordinates for the viewport's centre */
private int xCentre;
private int zCentre;
/* The width (in world units (wu)) of the viewport holding the map */
private int worldWidth;
/* The viewport height in pixels */
private int worldHeight;
/* The number of minutes that one pixel represents */
private double minutesPerWorldUnit;
/**
* Constructor.
*
* @param worldWidth The world unit width the map's area
* @since 1.0
*/
public MapModel3D(int worldWidth) {
try {
this.centre = new Position(0, 0);
} catch (InvalidPositionException e) {
e.printStackTrace();
}
this.worldWidth = worldWidth;
// Calculate the number of minutes that one pixel represents along the longitude
calculateMinutesPerWorldUnit(DEFAULT_MAP_WIDTH_LONGITUDE);
// Calculate the viewport height based on its width and the number of degrees (85)
// in our map
worldHeight = ((int) NavCalculator.computeDMPClarkeSpheroid(0, 85) / (int) minutesPerWorldUnit) * 2;
// Determine the map's x,y centre
xCentre = 0;
zCentre = 0;
// xCentre = worldWidth / 2;
// zCentre = worldHeight / 2;
}
/**
* Returns the height of the viewport in pixels.
*
* @return The height of the viewport in pixels.
* @since 1.0
*/
public int getWorldHeight() {
return worldHeight;
}
/**
* Calculates the number of minutes per pixels using a given
* map width in longitude.
*
* @param mapWidthInLongitude The map's with in degrees of longitude.
* @since 1.0
*/
public void calculateMinutesPerWorldUnit(double mapWidthInLongitude) {
// Multiply mapWidthInLongitude by 60 to convert it to minutes.
minutesPerWorldUnit = (mapWidthInLongitude * 60) / (double) worldWidth;
}
/**
* Returns the width of the viewport in pixels.
*
* @return The width of the viewport in pixels.
* @since 1.0
*/
public int getWorldWidth() {
return worldWidth;
}
/**
* Sets the world's desired width.
*
* @param viewportWidth The world's desired width in WU.
* @since 1.0
*/
public void setWorldWidth(int viewportWidth) {
this.worldWidth = viewportWidth;
}
/**
* Sets the world's desired height.
*
* @param viewportHeight The world's desired height in WU.
* @since 1.0
*/
public void setWorldHeight(int viewportHeight) {
this.worldHeight = viewportHeight;
}
/**
* Sets the map's centre.
*
* @param centre The <code>Position</code> denoting the map's
* desired centre.
* @since 1.0
*/
public void setCentre(Position centre) {
this.centre = centre;
}
/**
* Returns the number of minutes there are per WU.
*
* @return The number of minutes per WU.
* @since 1.0
*/
public double getMinutesPerWu() {
return minutesPerWorldUnit;
}
/**
* Returns the meters per WU.
*
* @return The meters per WU.
* @since 1.0
*/
public double getMetersPerWu() {
return 1853 * minutesPerWorldUnit;
}
/**
* Converts a latitude/longitude position into a WU coordinate.
*
* @param position The <code>Position</code> to convert.
* @return The <code>Point</code> a pixel coordinate.
* @since 1.0
*/
public Vector3f toWorldUnit(Position position) {
// Get the difference between position and the centre for calculating
// the position's longitude translation
double distance = NavCalculator.computeLongDiff(centre.getLongitude(),
position.getLongitude());
// Use the difference from the centre to calculate the pixel x co-ordinate
double distanceInPixels = (distance / minutesPerWorldUnit);
// Use the difference in meridional parts to calculate the pixel y co-ordinate
double dmp = NavCalculator.computeDMPClarkeSpheroid(centre.getLatitude(),
position.getLatitude());
int x = 0;
int z = 0;
if (centre.getLatitude() == position.getLatitude()) {
z = zCentre;
}
if (centre.getLongitude() == position.getLongitude()) {
x = xCentre;
}
// Distinguish between northern and southern hemisphere for latitude calculations
if (centre.getLatitude() > 0 && position.getLatitude() > centre.getLatitude()) {
// Centre is north. Position is north of centre
z = zCentre - (int) ((dmp) / minutesPerWorldUnit);
} else if (centre.getLatitude() > 0 && position.getLatitude() < centre.getLatitude()) {
// Centre is north. Position is south of centre
z = zCentre + (int) ((dmp) / minutesPerWorldUnit);
} else if (centre.getLatitude() < 0 && position.getLatitude() > centre.getLatitude()) {
// Centre is south. Position is north of centre
z = zCentre - (int) ((dmp) / minutesPerWorldUnit);
} else if (centre.getLatitude() < 0 && position.getLatitude() < centre.getLatitude()) {
// Centre is south. Position is south of centre
z = zCentre + (int) ((dmp) / minutesPerWorldUnit);
} else if (centre.getLatitude() == 0 && position.getLatitude() > centre.getLatitude()) {
// Centre is at the equator. Position is north of the equator
z = zCentre - (int) ((dmp) / minutesPerWorldUnit);
} else if (centre.getLatitude() == 0 && position.getLatitude() < centre.getLatitude()) {
// Centre is at the equator. Position is south of the equator
z = zCentre + (int) ((dmp) / minutesPerWorldUnit);
}
// Distinguish between western and eastern hemisphere for longitude calculations
if (centre.getLongitude() < 0 && position.getLongitude() < centre.getLongitude()) {
// Centre is west. Position is west of centre
x = xCentre - (int) distanceInPixels;
} else if (centre.getLongitude() < 0 && position.getLongitude() > centre.getLongitude()) {
// Centre is west. Position is south of centre
x = xCentre + (int) distanceInPixels;
} else if (centre.getLongitude() > 0 && position.getLongitude() < centre.getLongitude()) {
// Centre is east. Position is west of centre
x = xCentre - (int) distanceInPixels;
} else if (centre.getLongitude() > 0 && position.getLongitude() > centre.getLongitude()) {
// Centre is east. Position is east of centre
x = xCentre + (int) distanceInPixels;
} else if (centre.getLongitude() == 0 && position.getLongitude() > centre.getLongitude()) {
// Centre is at the equator. Position is east of centre
x = xCentre + (int) distanceInPixels;
} else if (centre.getLongitude() == 0 && position.getLongitude() < centre.getLongitude()) {
// Centre is at the equator. Position is west of centre
x = xCentre - (int) distanceInPixels;
}
// Distinguish between northern and southern hemisphere for longitude calculations
return new Vector3f(x, 0, z);
}
/**
* Converts a world position into a Mercator position.
*
* @param posVec <code>Vector</code> containing the world unit
* coordinates that are to be converted into
* longitude / latitude coordinates.
* @return The resulting <code>Position</code> in degrees of
* latitude and longitude.
* @since 1.0
*/
public Position toPosition(Vector3f posVec) {
double lat, lon;
Position pos = null;
try {
Vector3f worldCentre = toWorldUnit(new Position(0, 0));
// Get the difference between position and the centre
double xDistance = difference(xCentre, posVec.getX());
double yDistance = difference(worldCentre.getZ(), posVec.getZ());
double lonDistanceInDegrees = (xDistance * minutesPerWorldUnit) / 60;
double mp = (yDistance * minutesPerWorldUnit);
// If we are zoomed in past a certain point, then use linear search.
// Otherwise use binary search
if (getMinutesPerWu() < 0.05) {
lat = findLat(mp, getCentre().getLatitude());
if (lat == -1000) {
System.out.println("lat: " + lat);
}
} else {
lat = findLat(mp, 0.0, 85.0);
}
lon = (posVec.getX() < xCentre ? centre.getLongitude() - lonDistanceInDegrees
: centre.getLongitude() + lonDistanceInDegrees);
if (posVec.getZ() > worldCentre.getZ()) {
lat = -1 * lat;
}
if (lat == -1000 || lon == -1000) {
return pos;
}
pos = new Position(lat, lon);
} catch (InvalidPositionException ipe) {
ipe.printStackTrace();
}
return pos;
}
/**
* Calculates difference between two points on the map in WU.
*
* @param a
* @param b
* @return difference The difference between a and b in WU.
* @since 1.0
*/
private double difference(double a, double b) {
return Math.abs(a - b);
}
/**
* Defines the centre of the map in pixels.
*
* @param posVec <code>Vector3f</code> object denoting the map's new centre.
* @since 1.0
*/
public void setCentre(Vector3f posVec) {
try {
Position newCentre = toPosition(posVec);
if (newCentre != null) {
centre = newCentre;
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Returns the WU (x,y,z) centre of the map.
*
* @return <code>Vector3f</code> object marking the map's (x,y) centre.
* @since 1.0
*/
public Vector3f getCentreWu() {
return new Vector3f(xCentre, 0, zCentre);
}
/**
* Returns the <code>Position</code> centre of the map.
*
* @return <code>Position</code> object marking the map's (lat, long)
* centre.
* @since 1.0
*/
public Position getCentre() {
return centre;
}
/**
* Uses binary search to find the latitude of a given MP.
*
* @param mp Maridian part whose latitude to determine.
* @param low Minimum latitude bounds.
* @param high Maximum latitude bounds.
* @return The latitude of the MP value
* @since 1.0
*/
private double findLat(double mp, double low, double high) {
DecimalFormat form = new DecimalFormat("#.####");
mp = Math.round(mp);
double midLat = (low + high) / 2.0;
// ctr is used to make sure that with some
// numbers which can't be represented exactly don't inifitely repeat
double guessMP = NavCalculator.computeDMPClarkeSpheroid(0, (float) midLat);
while (low <= high) {
if (guessMP == mp) {
return midLat;
} else {
if (guessMP > mp) {
high = midLat - 0.0001;
} else {
low = midLat + 0.0001;
}
}
midLat = Double.valueOf(form.format(((low + high) / 2.0)));
guessMP = NavCalculator.computeDMPClarkeSpheroid(0, (float) midLat);
guessMP = Math.round(guessMP);
}
return -1000;
}
/**
* Uses linear search to find the latitude of a given MP.
*
* @param mp The meridian part for which to find the latitude.
* @param previousLat The previous latitude. Used as a upper / lower bound.
* @return The latitude of the MP value.
* @since 1.0
*/
private double findLat(double mp, double previousLat) {
DecimalFormat form = new DecimalFormat("#.#####");
mp = Double.parseDouble(form.format(mp));
double guessMP;
for (double lat = previousLat - 0.25; lat < previousLat + 1; lat += 0.00001) {
guessMP = NavCalculator.computeDMPClarkeSpheroid(0, lat);
guessMP = Double.parseDouble(form.format(guessMP));
if (guessMP == mp || Math.abs(guessMP - mp) < 0.05) {
return lat;
}
}
return -1000;
}
}