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