Home | History | Annotate | Download | only in navigation
      1 /*
      2  * To change this template, choose Tools | Templates
      3  * and open the template in the editor.
      4  */
      5 package jme3tools.navigation;
      6 
      7 import com.jme3.math.Vector3f;
      8 import java.text.DecimalFormat;
      9 
     10 
     11 /**
     12  * A representation of the actual map in terms of lat/long and x,y,z co-ordinates.
     13  * The Map class contains various helper methods such as methods for determining
     14  * the world unit positions for lat/long coordinates and vice versa. This map projection
     15  * does not handle screen/pixel coordinates.
     16  *
     17  * @author Benjamin Jakobus (thanks to Cormac Gebruers)
     18  * @version 1.0
     19  * @since 1.0
     20  */
     21 public class MapModel3D {
     22 
     23     /* The number of radians per degree */
     24     private final static double RADIANS_PER_DEGREE = 57.2957;
     25 
     26     /* The number of degrees per radian */
     27     private final static double DEGREES_PER_RADIAN = 0.0174532925;
     28 
     29     /* The map's width in longitude */
     30     public final static int DEFAULT_MAP_WIDTH_LONGITUDE = 360;
     31 
     32     /* The top right hand corner of the map */
     33     private Position centre;
     34 
     35     /* The x and y co-ordinates for the viewport's centre */
     36     private int xCentre;
     37     private int zCentre;
     38 
     39     /* The width (in world units (wu)) of the viewport holding the map */
     40     private int worldWidth;
     41 
     42     /* The viewport height in pixels */
     43     private int worldHeight;
     44 
     45     /* The number of minutes that one pixel represents */
     46     private double minutesPerWorldUnit;
     47 
     48     /**
     49      * Constructor.
     50      *
     51      * @param worldWidth         The world unit width the map's area
     52      * @since 1.0
     53      */
     54     public MapModel3D(int worldWidth) {
     55         try {
     56             this.centre = new Position(0, 0);
     57         } catch (InvalidPositionException e) {
     58             e.printStackTrace();
     59         }
     60 
     61         this.worldWidth = worldWidth;
     62 
     63         // Calculate the number of minutes that one pixel represents along the longitude
     64         calculateMinutesPerWorldUnit(DEFAULT_MAP_WIDTH_LONGITUDE);
     65 
     66         // Calculate the viewport height based on its width and the number of degrees (85)
     67         // in our map
     68         worldHeight = ((int) NavCalculator.computeDMPClarkeSpheroid(0, 85) / (int) minutesPerWorldUnit) * 2;
     69 
     70         // Determine the map's x,y centre
     71         xCentre = 0;
     72         zCentre = 0;
     73 //        xCentre = worldWidth / 2;
     74 //        zCentre = worldHeight / 2;
     75     }
     76 
     77     /**
     78      * Returns the height of the viewport in pixels.
     79      *
     80      * @return          The height of the viewport in pixels.
     81      * @since 1.0
     82      */
     83     public int getWorldHeight() {
     84         return worldHeight;
     85     }
     86 
     87     /**
     88      * Calculates the number of minutes per pixels using a given
     89      * map width in longitude.
     90      *
     91      * @param mapWidthInLongitude               The map's with in degrees of longitude.
     92      * @since 1.0
     93      */
     94     public void calculateMinutesPerWorldUnit(double mapWidthInLongitude) {
     95         // Multiply mapWidthInLongitude by 60 to convert it to minutes.
     96         minutesPerWorldUnit = (mapWidthInLongitude * 60) / (double) worldWidth;
     97     }
     98 
     99     /**
    100      * Returns the width of the viewport in pixels.
    101      *
    102      * @return              The width of the viewport in pixels.
    103      * @since 1.0
    104      */
    105     public int getWorldWidth() {
    106         return worldWidth;
    107     }
    108 
    109     /**
    110      * Sets the world's desired width.
    111      *
    112      * @param viewportWidth     The world's desired width in WU.
    113      * @since 1.0
    114      */
    115     public void setWorldWidth(int viewportWidth) {
    116         this.worldWidth = viewportWidth;
    117     }
    118 
    119      /**
    120      * Sets the world's desired height.
    121      *
    122      * @param viewportHeight     The world's desired height in WU.
    123      * @since 1.0
    124      */
    125     public void setWorldHeight(int viewportHeight) {
    126         this.worldHeight = viewportHeight;
    127     }
    128 
    129     /**
    130      * Sets the map's centre.
    131      *
    132      * @param centre            The <code>Position</code> denoting the map's
    133      *                          desired centre.
    134      * @since 1.0
    135      */
    136     public void setCentre(Position centre) {
    137         this.centre = centre;
    138     }
    139 
    140     /**
    141      * Returns the number of minutes there are per WU.
    142      *
    143      * @return                  The number of minutes per WU.
    144      * @since 1.0
    145      */
    146     public double getMinutesPerWu() {
    147         return minutesPerWorldUnit;
    148     }
    149 
    150     /**
    151      * Returns the meters per WU.
    152      *
    153      * @return                  The meters per WU.
    154      * @since 1.0
    155      */
    156     public double getMetersPerWu() {
    157         return 1853 * minutesPerWorldUnit;
    158     }
    159 
    160     /**
    161      * Converts a latitude/longitude position into a WU coordinate.
    162      *
    163      * @param position          The <code>Position</code> to convert.
    164      * @return                  The <code>Point</code> a pixel coordinate.
    165      * @since 1.0
    166      */
    167     public Vector3f toWorldUnit(Position position) {
    168         // Get the difference between position and the centre for calculating
    169         // the position's longitude translation
    170         double distance = NavCalculator.computeLongDiff(centre.getLongitude(),
    171                 position.getLongitude());
    172 
    173         // Use the difference from the centre to calculate the pixel x co-ordinate
    174         double distanceInPixels = (distance / minutesPerWorldUnit);
    175 
    176         // Use the difference in meridional parts to calculate the pixel y co-ordinate
    177         double dmp = NavCalculator.computeDMPClarkeSpheroid(centre.getLatitude(),
    178                 position.getLatitude());
    179 
    180         int x = 0;
    181         int z = 0;
    182 
    183         if (centre.getLatitude() == position.getLatitude()) {
    184             z = zCentre;
    185         }
    186         if (centre.getLongitude() == position.getLongitude()) {
    187             x = xCentre;
    188         }
    189 
    190         // Distinguish between northern and southern hemisphere for latitude calculations
    191         if (centre.getLatitude() > 0 && position.getLatitude() > centre.getLatitude()) {
    192             // Centre is north. Position is north of centre
    193             z = zCentre - (int) ((dmp) / minutesPerWorldUnit);
    194         } else if (centre.getLatitude() > 0 && position.getLatitude() < centre.getLatitude()) {
    195             // Centre is north. Position is south of centre
    196             z = zCentre + (int) ((dmp) / minutesPerWorldUnit);
    197         } else if (centre.getLatitude() < 0 && position.getLatitude() > centre.getLatitude()) {
    198             // Centre is south. Position is north of centre
    199             z = zCentre - (int) ((dmp) / minutesPerWorldUnit);
    200         } else if (centre.getLatitude() < 0 && position.getLatitude() < centre.getLatitude()) {
    201             // Centre is south. Position is south of centre
    202             z = zCentre + (int) ((dmp) / minutesPerWorldUnit);
    203         } else if (centre.getLatitude() == 0 && position.getLatitude() > centre.getLatitude()) {
    204             // Centre is at the equator. Position is north of the equator
    205             z = zCentre - (int) ((dmp) / minutesPerWorldUnit);
    206         } else if (centre.getLatitude() == 0 && position.getLatitude() < centre.getLatitude()) {
    207             // Centre is at the equator. Position is south of the equator
    208             z = zCentre + (int) ((dmp) / minutesPerWorldUnit);
    209         }
    210 
    211         // Distinguish between western and eastern hemisphere for longitude calculations
    212         if (centre.getLongitude() < 0 && position.getLongitude() < centre.getLongitude()) {
    213             // Centre is west. Position is west of centre
    214             x = xCentre - (int) distanceInPixels;
    215         } else if (centre.getLongitude() < 0 && position.getLongitude() > centre.getLongitude()) {
    216             // Centre is west. Position is south of centre
    217             x = xCentre + (int) distanceInPixels;
    218         } else if (centre.getLongitude() > 0 && position.getLongitude() < centre.getLongitude()) {
    219             // Centre is east. Position is west of centre
    220             x = xCentre - (int) distanceInPixels;
    221         } else if (centre.getLongitude() > 0 && position.getLongitude() > centre.getLongitude()) {
    222             // Centre is east. Position is east of centre
    223             x = xCentre + (int) distanceInPixels;
    224         } else if (centre.getLongitude() == 0 && position.getLongitude() > centre.getLongitude()) {
    225             // Centre is at the equator. Position is east of centre
    226             x = xCentre + (int) distanceInPixels;
    227         } else if (centre.getLongitude() == 0 && position.getLongitude() < centre.getLongitude()) {
    228             // Centre is at the equator. Position is west of centre
    229             x = xCentre - (int) distanceInPixels;
    230         }
    231 
    232         // Distinguish between northern and southern hemisphere for longitude calculations
    233         return new Vector3f(x, 0, z);
    234     }
    235 
    236     /**
    237      * Converts a world position into a Mercator position.
    238      *
    239      * @param posVec                     <code>Vector</code> containing the world unit
    240      *                              coordinates that are to be converted into
    241      *                              longitude / latitude coordinates.
    242      * @return                      The resulting <code>Position</code> in degrees of
    243      *                              latitude and longitude.
    244      * @since 1.0
    245      */
    246     public Position toPosition(Vector3f posVec) {
    247         double lat, lon;
    248         Position pos = null;
    249         try {
    250             Vector3f worldCentre = toWorldUnit(new Position(0, 0));
    251 
    252             // Get the difference between position and the centre
    253             double xDistance = difference(xCentre, posVec.getX());
    254             double yDistance = difference(worldCentre.getZ(), posVec.getZ());
    255             double lonDistanceInDegrees = (xDistance * minutesPerWorldUnit) / 60;
    256             double mp = (yDistance * minutesPerWorldUnit);
    257             // If we are zoomed in past a certain point, then use linear search.
    258             // Otherwise use binary search
    259             if (getMinutesPerWu() < 0.05) {
    260                 lat = findLat(mp, getCentre().getLatitude());
    261                 if (lat == -1000) {
    262                     System.out.println("lat: " + lat);
    263                 }
    264             } else {
    265                 lat = findLat(mp, 0.0, 85.0);
    266             }
    267             lon = (posVec.getX() < xCentre ? centre.getLongitude() - lonDistanceInDegrees
    268                     : centre.getLongitude() + lonDistanceInDegrees);
    269 
    270             if (posVec.getZ() > worldCentre.getZ()) {
    271                 lat = -1 * lat;
    272             }
    273             if (lat == -1000 || lon == -1000) {
    274                 return pos;
    275             }
    276             pos = new Position(lat, lon);
    277         } catch (InvalidPositionException ipe) {
    278             ipe.printStackTrace();
    279         }
    280         return pos;
    281     }
    282 
    283     /**
    284      * Calculates difference between two points on the map in WU.
    285      *
    286      * @param a
    287      * @param b
    288      * @return difference           The difference between a and b in WU.
    289      * @since 1.0
    290      */
    291     private double difference(double a, double b) {
    292         return Math.abs(a - b);
    293     }
    294 
    295     /**
    296      * Defines the centre of the map in pixels.
    297      *
    298      * @param posVec             <code>Vector3f</code> object denoting the map's new centre.
    299      * @since 1.0
    300      */
    301     public void setCentre(Vector3f posVec) {
    302         try {
    303             Position newCentre = toPosition(posVec);
    304             if (newCentre != null) {
    305                 centre = newCentre;
    306             }
    307         } catch (Exception e) {
    308             e.printStackTrace();
    309         }
    310     }
    311 
    312     /**
    313      * Returns the WU (x,y,z) centre of the map.
    314      *
    315      * @return              <code>Vector3f</code> object marking the map's (x,y) centre.
    316      * @since 1.0
    317      */
    318     public Vector3f getCentreWu() {
    319         return new Vector3f(xCentre, 0, zCentre);
    320     }
    321 
    322     /**
    323      * Returns the <code>Position</code> centre of the map.
    324      *
    325      * @return              <code>Position</code> object marking the map's (lat, long)
    326      *                      centre.
    327      * @since 1.0
    328      */
    329     public Position getCentre() {
    330         return centre;
    331     }
    332 
    333     /**
    334      * Uses binary search to find the latitude of a given MP.
    335      *
    336      * @param mp                Maridian part whose latitude to determine.
    337      * @param low               Minimum latitude bounds.
    338      * @param high              Maximum latitude bounds.
    339      * @return                  The latitude of the MP value
    340      * @since 1.0
    341      */
    342     private double findLat(double mp, double low, double high) {
    343         DecimalFormat form = new DecimalFormat("#.####");
    344         mp = Math.round(mp);
    345         double midLat = (low + high) / 2.0;
    346         // ctr is used to make sure that with some
    347         // numbers which can't be represented exactly don't inifitely repeat
    348         double guessMP = NavCalculator.computeDMPClarkeSpheroid(0, (float) midLat);
    349 
    350         while (low <= high) {
    351             if (guessMP == mp) {
    352                 return midLat;
    353             } else {
    354                 if (guessMP > mp) {
    355                     high = midLat - 0.0001;
    356                 } else {
    357                     low = midLat + 0.0001;
    358                 }
    359             }
    360 
    361             midLat = Double.valueOf(form.format(((low + high) / 2.0)));
    362             guessMP = NavCalculator.computeDMPClarkeSpheroid(0, (float) midLat);
    363             guessMP = Math.round(guessMP);
    364         }
    365         return -1000;
    366     }
    367 
    368     /**
    369      * Uses linear search to find the latitude of a given MP.
    370      *
    371      * @param mp                The meridian part for which to find the latitude.
    372      * @param previousLat       The previous latitude. Used as a upper / lower bound.
    373      * @return                  The latitude of the MP value.
    374      * @since 1.0
    375      */
    376     private double findLat(double mp, double previousLat) {
    377         DecimalFormat form = new DecimalFormat("#.#####");
    378         mp = Double.parseDouble(form.format(mp));
    379         double guessMP;
    380         for (double lat = previousLat - 0.25; lat < previousLat + 1; lat += 0.00001) {
    381             guessMP = NavCalculator.computeDMPClarkeSpheroid(0, lat);
    382             guessMP = Double.parseDouble(form.format(guessMP));
    383             if (guessMP == mp || Math.abs(guessMP - mp) < 0.05) {
    384                 return lat;
    385             }
    386         }
    387         return -1000;
    388     }
    389 }
    390