altoslib: Create display-independent map support code
[fw/altos] / altoslib / AltosMap.java
1 /*
2  * Copyright © 2010 Anthony Towns <aj@erisian.com.au>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.altoslib_6;
19
20 import java.io.*;
21 import java.lang.*;
22 import java.util.*;
23 import java.util.concurrent.*;
24
25 public class AltosMap implements AltosFlightDisplay, AltosMapTileListener, AltosMapStoreListener {
26
27         static final int px_size = 512;
28
29         static final int maptype_hybrid = 0;
30         static final int maptype_roadmap = 1;
31         static final int maptype_satellite = 2;
32         static final int maptype_terrain = 3;
33         static final int maptype_default = maptype_hybrid;
34
35         static final int default_zoom = 15;
36         static final int min_zoom = 3;
37         static final int max_zoom = 21;
38
39         static final String[] maptype_names = {
40                 "hybrid",
41                 "roadmap",
42                 "satellite",
43                 "terrain"
44         };
45
46         public static final String[] maptype_labels = {
47                 "Hybrid",
48                 "Roadmap",
49                 "Satellite",
50                 "Terrain"
51         };
52
53         AltosMapInterface       map_interface;
54
55         AltosMapCache           cache;
56
57         LinkedList<AltosMapMark> marks = new LinkedList<AltosMapMark>();
58
59         LinkedList<AltosMapZoomListener> zoom_listeners = new LinkedList<AltosMapZoomListener>();
60
61         public void add_zoom_listener(AltosMapZoomListener listener) {
62                 if (!zoom_listeners.contains(listener))
63                         zoom_listeners.add(listener);
64         }
65
66         public void remove_zoom_listener(AltosMapZoomListener listener) {
67                 zoom_listeners.remove(listener);
68         }
69
70         boolean         have_boost = false;
71         boolean         have_landed = false;
72
73         ConcurrentHashMap<AltosPointInt,AltosMapTile> tiles = new ConcurrentHashMap<AltosPointInt,AltosMapTile>();
74         int             load_radius;
75         AltosLatLon     load_centre = null;
76         AltosMapTileListener    load_listener;
77
78         int             zoom = AltosMap.default_zoom;
79         int             maptype = AltosMap.maptype_default;
80
81         long            user_input_time;
82
83         /* Milliseconds to wait after user action before auto-scrolling
84          */
85         static final long auto_scroll_delay = 20 * 1000;
86
87         AltosMapTransform       transform;
88         AltosLatLon             centre;
89
90         public void reset() {
91                 // nothing
92         }
93
94         /* MapInterface wrapping functions */
95         public void set_units() {
96                 map_interface.set_units();
97         }
98
99         public void repaint(AltosMapRectangle damage, int pad) {
100                 map_interface.repaint(damage, pad);
101         }
102
103         public void repaint(double x, double y, double w, double h) {
104                 map_interface.repaint(x, y, w, h);
105         }
106
107         public void repaint() {
108                 map_interface.repaint();
109         }
110
111         public int width() {
112                 return map_interface.width();
113         }
114
115         public int height() {
116                 return map_interface.height();
117         }
118
119         public AltosPointInt floor(AltosPointDouble point) {
120                 return new AltosPointInt ((int) Math.floor(point.x / AltosMap.px_size) * AltosMap.px_size,
121                                               (int) Math.floor(point.y / AltosMap.px_size) * AltosMap.px_size);
122         }
123
124         public AltosPointInt ceil(AltosPointDouble point) {
125                 return new AltosPointInt ((int) Math.ceil(point.x / AltosMap.px_size) * AltosMap.px_size,
126                                               (int) Math.ceil(point.y / AltosMap.px_size) * AltosMap.px_size);
127         }
128
129         public void notice_user_input() {
130                 user_input_time = System.currentTimeMillis();
131         }
132
133         public boolean recent_user_input() {
134                 return (System.currentTimeMillis() - user_input_time) < auto_scroll_delay;
135         }
136
137         public boolean far_from_centre(AltosLatLon lat_lon) {
138
139                 if (centre == null || transform == null)
140                         return true;
141
142                 AltosPointDouble        screen = transform.screen(lat_lon);
143
144                 int             width = width();
145                 int             dx = Math.abs ((int) (double) screen.x - width/2);
146
147                 if (dx > width / 4)
148                         return true;
149
150                 int             height = height();
151                 int             dy = Math.abs ((int) (double) screen.y - height/2);
152
153                 if (dy > height / 4)
154                         return true;
155
156                 return false;
157         }
158
159         public void font_size_changed(int font_size) {
160                 map_interface.line.font_size_changed(font_size);
161                 for (AltosMapTile tile : tiles.values())
162                         tile.font_size_changed(font_size);
163                 repaint();
164         }
165
166         public void units_changed(boolean imperial_units) {
167         }
168
169         private void set_transform() {
170                 transform = new AltosMapTransform(width(), height(), zoom, centre);
171                 repaint();
172         }
173
174         public boolean set_zoom(int zoom) {
175                 if (AltosMap.min_zoom <= zoom && zoom <= AltosMap.max_zoom && zoom != this.zoom) {
176                         this.zoom = zoom;
177                         tiles.clear();
178                         set_transform();
179
180                         for (AltosMapZoomListener listener : zoom_listeners)
181                                 listener.zoom_changed(this.zoom);
182
183                         return true;
184                 }
185                 return false;
186         }
187
188         public int get_zoom() {
189                 return zoom;
190         }
191
192         public boolean set_maptype(int maptype) {
193                 if (maptype != this.maptype) {
194                         this.maptype = maptype;
195                         tiles.clear();
196                         repaint();
197                         return true;
198                 }
199                 return false;
200         }
201
202         public void show(AltosState state, AltosListenerState listener_state) {
203
204                 /* If insufficient gps data, nothing to update
205                  */
206                 AltosGPS        gps = state.gps;
207
208                 if (gps == null)
209                         return;
210
211                 if (!gps.locked && gps.nsat < 4)
212                         return;
213
214                 AltosMapRectangle       damage = map_interface.path.add(gps.lat, gps.lon, state.state);
215
216                 switch (state.state) {
217                 case AltosLib.ao_flight_boost:
218                         if (!have_boost) {
219                                 add_mark(gps.lat, gps.lon, state.state);
220                                 have_boost = true;
221                         }
222                         break;
223                 case AltosLib.ao_flight_landed:
224                         if (!have_landed) {
225                                 add_mark(gps.lat, gps.lon, state.state);
226                                 have_landed = true;
227                         }
228                         break;
229                 }
230
231                 if (damage != null)
232                         repaint(damage, AltosMapPath.stroke_width);
233                 maybe_centre(gps.lat, gps.lon);
234         }
235
236         public void centre(AltosLatLon lat_lon) {
237                 centre = lat_lon;
238                 set_transform();
239         }
240
241         public void centre(double lat, double lon) {
242                 centre(new AltosLatLon(lat, lon));
243         }
244
245         public void centre(AltosState state) {
246                 if (!state.gps.locked && state.gps.nsat < 4)
247                         return;
248                 centre(state.gps.lat, state.gps.lon);
249         }
250
251         public void maybe_centre(double lat, double lon) {
252                 AltosLatLon     lat_lon = new AltosLatLon(lat, lon);
253                 if (centre == null || (!recent_user_input() && far_from_centre(lat_lon)))
254                         centre(lat_lon);
255         }
256
257         public void add_mark(double lat, double lon, int state) {
258                 synchronized(marks) {
259                         marks.add(map_interface.new_mark(lat, lon, state));
260                 }
261                 repaint();
262         }
263
264         public void clear_marks() {
265                 synchronized(marks) {
266                         marks.clear();
267                 }
268         }
269
270         private void make_tiles() {
271                 AltosPointInt   upper_left;
272                 AltosPointInt   lower_right;
273
274                 if (load_centre != null) {
275                         AltosPointInt centre = floor(transform.point(load_centre));
276
277                         upper_left = new AltosPointInt(centre.x - load_radius * AltosMap.px_size,
278                                                                centre.y - load_radius * AltosMap.px_size);
279                         lower_right = new AltosPointInt(centre.x + load_radius * AltosMap.px_size,
280                                                                 centre.y + load_radius * AltosMap.px_size);
281                 } else {
282                         upper_left = floor(transform.screen_point(new AltosPointDouble(0.0, 0.0)));
283                         lower_right = floor(transform.screen_point(new AltosPointDouble(width(), height())));
284                 }
285                 LinkedList<AltosPointInt> to_remove = new LinkedList<AltosPointInt>();
286
287                 for (AltosPointInt point : tiles.keySet()) {
288                         if (point.x < upper_left.x || lower_right.x < point.x ||
289                             point.y < upper_left.y || lower_right.y < point.y) {
290                                 to_remove.add(point);
291                         }
292                 }
293
294                 for (AltosPointInt point : to_remove)
295                         tiles.remove(point);
296
297                 cache.set_cache_size((width() / AltosMap.px_size + 2) * (height() / AltosMap.px_size + 2));
298                 for (int y = (int) upper_left.y; y <= lower_right.y; y += AltosMap.px_size) {
299                         for (int x = (int) upper_left.x; x <= lower_right.x; x += AltosMap.px_size) {
300                                 AltosPointInt point = new AltosPointInt(x, y);
301
302                                 if (!tiles.containsKey(point)) {
303                                         AltosLatLon     ul = transform.lat_lon(new AltosPointDouble(x, y));
304                                         AltosLatLon     center = transform.lat_lon(new AltosPointDouble(x + AltosMap.px_size/2, y + AltosMap.px_size/2));
305                                         AltosMapTile tile = new AltosMapTile(this, ul, center, zoom, maptype,
306                                                                              AltosMap.px_size);
307                                         tiles.put(point, tile);
308                                 }
309                         }
310                 }
311         }
312
313         public void set_load_params(double lat, double lon, int radius, AltosMapTileListener listener) {
314                 load_centre = new AltosLatLon(lat, lon);
315                 load_radius = radius;
316                 load_listener = listener;
317                 centre(lat, lon);
318                 make_tiles();
319                 for (AltosMapTile tile : tiles.values()) {
320                         tile.add_store_listener(this);
321                         if (tile.store_status() != AltosMapTile.loading)
322                                 listener.notify_tile(tile, tile.store_status());
323                 }
324                 repaint();
325         }
326
327         public String getName() {
328                 return "Map";
329         }
330
331         /* AltosMapTileListener methods */
332         public synchronized void notify_tile(AltosMapTile tile, int status) {
333                 for (AltosPointInt point : tiles.keySet()) {
334                         if (tile == tiles.get(point)) {
335                                 AltosPointInt   screen = transform.screen(point);
336                                 repaint(screen.x, screen.y, AltosMap.px_size, AltosMap.px_size);
337                         }
338                 }
339         }
340
341         /* AltosMapStoreListener methods */
342         public synchronized void notify_store(AltosMapStore store, int status) {
343                 if (load_listener != null) {
344                         for (AltosMapTile tile : tiles.values())
345                                 if (store.equals(tile.store))
346                                         load_listener.notify_tile(tile, status);
347                 }
348         }
349
350         public AltosMap(AltosMapInterface map_interface) {
351                 this.map_interface = map_interface;
352                 cache = new AltosMapCache(map_interface);
353                 centre(0, 0);
354         }
355 }