1/*
2 * To change this template, choose Tools | Templates
3 * and open the template in the editor.
4 */
5package jme3tools.navigation;
6
7import com.jme3.math.Vector3f;
8import 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 */
21public 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