/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 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; } }