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