altosuilib: Handle font and units changes in maps and stats table
[fw/altos] / altosuilib / AltosUIMapView.java
1 /*
2  * Copyright © 2014 Keith Packard <keithp@keithp.com>
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.altosuilib_2;
19
20 import java.awt.*;
21 import java.awt.event.*;
22 import java.awt.image.*;
23 import javax.swing.*;
24 import java.io.*;
25 import java.lang.*;
26 import java.awt.geom.*;
27 import java.util.*;
28 import java.util.concurrent.*;
29 import org.altusmetrum.altoslib_4.*;
30
31 public class AltosUIMapView extends Canvas implements MouseMotionListener, MouseListener, MouseWheelListener, ComponentListener, AltosUIMapTileListener, AltosUIMapStoreListener {
32
33         AltosUIMapPath  path = new AltosUIMapPath();
34
35         AltosUIMapLine  line = new AltosUIMapLine();
36
37         LinkedList<AltosUIMapMark> marks = new LinkedList<AltosUIMapMark>();
38
39         LinkedList<AltosUIMapZoomListener> zoom_listeners = new LinkedList<AltosUIMapZoomListener>();
40
41         boolean         have_boost = false;
42         boolean         have_landed = false;
43
44         ConcurrentHashMap<Point,AltosUIMapTile> tiles = new ConcurrentHashMap<Point,AltosUIMapTile>();
45
46         static final int default_zoom = 15;
47         static final int min_zoom = 3;
48         static final int max_zoom = 21;
49         static final int px_size = 512;
50
51         int             load_radius;
52         AltosUILatLon   load_centre = null;
53         AltosUIMapTileListener  load_listener;
54
55         int             zoom = default_zoom;
56         int             maptype = AltosUIMap.maptype_default;
57
58         long            user_input_time;
59
60         /* Milliseconds to wait after user action before auto-scrolling
61          */
62         static final long auto_scroll_delay = 20 * 1000;
63
64         AltosUIMapTransform     transform;
65         AltosUILatLon           centre;
66
67         public void set_font() {
68                 line.set_font(AltosUILib.value_font);
69                 for (AltosUIMapTile tile : tiles.values())
70                         tile.set_font(AltosUILib.value_font);
71                 repaint();
72         }
73
74         public void set_units() {
75                 repaint();
76         }
77
78         private boolean is_drag_event(MouseEvent e) {
79                 return e.getModifiers() == InputEvent.BUTTON1_MASK;
80         }
81
82         Point   drag_start;
83
84         private void drag(MouseEvent e) {
85                 if (drag_start == null)
86                         return;
87
88                 int dx = e.getPoint().x - drag_start.x;
89                 int dy = e.getPoint().y - drag_start.y;
90
91                 AltosUILatLon   new_centre = transform.screen_lat_lon(new Point(getWidth() / 2 - dx, getHeight() / 2 - dy));
92                 centre (new_centre.lat, new_centre.lon);
93                 drag_start = e.getPoint();
94         }
95
96         private void drag_start(MouseEvent e) {
97                 drag_start = e.getPoint();
98         }
99
100         private void notice_user_input() {
101                 user_input_time = System.currentTimeMillis();
102         }
103
104         private boolean recent_user_input() {
105                 return (System.currentTimeMillis() - user_input_time) < auto_scroll_delay;
106         }
107
108         /* MouseMotionListener methods */
109
110         public void mouseDragged(MouseEvent e) {
111                 notice_user_input();
112                 if (is_drag_event(e))
113                         drag(e);
114                 else {
115                         line.dragged(e, transform);
116                         repaint();
117                 }
118         }
119
120         public void mouseMoved(MouseEvent e) {
121         }
122
123         /* MouseListener methods */
124         public void mouseClicked(MouseEvent e) {
125         }
126
127         public void mouseEntered(MouseEvent e) {
128         }
129
130         public void mouseExited(MouseEvent e) {
131         }
132
133         public void mousePressed(MouseEvent e) {
134                 notice_user_input();
135                 if (is_drag_event(e))
136                         drag_start(e);
137                 else
138                         line.pressed(e, transform);
139         }
140
141         public void mouseReleased(MouseEvent e) {
142         }
143
144         /* MouseWheelListener methods */
145
146         public void mouseWheelMoved(MouseWheelEvent e) {
147                 int     zoom_change = e.getWheelRotation();
148
149                 notice_user_input();
150                 AltosUILatLon   mouse_lat_lon = transform.screen_lat_lon(e.getPoint());
151                 set_zoom(zoom() - zoom_change);
152
153                 Point2D.Double  new_mouse = transform.screen(mouse_lat_lon);
154
155                 int     dx = getWidth()/2 - e.getPoint().x;
156                 int     dy = getHeight()/2 - e.getPoint().y;
157
158                 AltosUILatLon   new_centre = transform.screen_lat_lon(new Point((int) new_mouse.x + dx, (int) new_mouse.y + dy));
159
160                 centre(new_centre.lat, new_centre.lon);
161         }
162
163         /* ComponentListener methods */
164
165         public void componentHidden(ComponentEvent e) {
166         }
167
168         public void componentMoved(ComponentEvent e) {
169         }
170
171         public void componentResized(ComponentEvent e) {
172                 set_transform();
173         }
174
175         public void componentShown(ComponentEvent e) {
176                 set_transform();
177         }
178
179         public void repaint(Rectangle r, int pad) {
180                 repaint(r.x - pad, r.y - pad, r.width + pad*2, r.height + pad*2);
181         }
182
183         public void repaint(AltosUIMapRectangle rect, int pad) {
184                 repaint (transform.screen(rect), pad);
185         }
186
187         private boolean far_from_centre(AltosUILatLon lat_lon) {
188
189                 if (centre == null || transform == null)
190                         return true;
191
192                 Point2D.Double  screen = transform.screen(lat_lon);
193
194                 int             width = getWidth();
195                 int             dx = Math.abs ((int) screen.x - width/2);
196
197                 if (dx > width / 4)
198                         return true;
199
200                 int             height = getHeight();
201                 int             dy = Math.abs ((int) screen.y - height/2);
202
203                 if (dy > height / 4)
204                         return true;
205
206                 return false;
207         }
208
209         public void show(AltosState state, AltosListenerState listener_state) {
210
211                 /* If insufficient gps data, nothing to update
212                  */
213                 AltosGPS        gps = state.gps;
214
215                 if (gps == null)
216                         return;
217
218                 if (!gps.locked && gps.nsat < 4)
219                         return;
220
221                 AltosUIMapRectangle     damage = path.add(gps.lat, gps.lon, state.state);
222
223                 switch (state.state) {
224                 case AltosLib.ao_flight_boost:
225                         if (!have_boost) {
226                                 add_mark(gps.lat, gps.lon, state.state);
227                                 have_boost = true;
228                         }
229                         break;
230                 case AltosLib.ao_flight_landed:
231                         if (!have_landed) {
232                                 add_mark(gps.lat, gps.lon, state.state);
233                                 have_landed = true;
234                         }
235                         break;
236                 }
237
238                 if (damage != null)
239                         repaint(damage, AltosUIMapPath.stroke_width);
240                 maybe_centre(gps.lat, gps.lon);
241         }
242
243         private void set_transform() {
244                 Rectangle       bounds = getBounds();
245
246                 transform = new AltosUIMapTransform(bounds.width, bounds.height, zoom, centre);
247                 repaint();
248         }
249
250         public boolean set_zoom(int zoom) {
251                 if (min_zoom <= zoom && zoom <= max_zoom && zoom != this.zoom) {
252                         this.zoom = zoom;
253                         tiles.clear();
254                         set_transform();
255
256                         for (AltosUIMapZoomListener listener : zoom_listeners)
257                                 listener.zoom_changed(this.zoom);
258
259                         return true;
260                 }
261                 return false;
262         }
263
264         public void add_zoom_listener(AltosUIMapZoomListener listener) {
265                 if (!zoom_listeners.contains(listener))
266                         zoom_listeners.add(listener);
267         }
268
269         public void remove_zoom_listener(AltosUIMapZoomListener listener) {
270                 zoom_listeners.remove(listener);
271         }
272
273         public void set_load_params(double lat, double lon, int radius, AltosUIMapTileListener listener) {
274                 load_centre = new AltosUILatLon(lat, lon);
275                 load_radius = radius;
276                 load_listener = listener;
277                 centre(lat, lon);
278                 make_tiles();
279                 for (AltosUIMapTile tile : tiles.values()) {
280                         tile.add_store_listener(this);
281                         if (tile.store_status() != AltosUIMapStore.loading)
282                                 listener.notify_tile(tile, tile.store_status());
283                 }
284                 repaint();
285         }
286
287         public boolean all_fetched() {
288                 for (AltosUIMapTile tile : tiles.values()) {
289                         if (tile.store_status() == AltosUIMapStore.loading)
290                                 return false;
291                 }
292                 return true;
293         }
294
295         public boolean set_maptype(int maptype) {
296                 if (maptype != this.maptype) {
297                         this.maptype = maptype;
298                         tiles.clear();
299                         repaint();
300                         return true;
301                 }
302                 return false;
303         }
304
305         public int get_maptype() {
306                 return maptype;
307         }
308
309         public int zoom() {
310                 return zoom;
311         }
312
313         public void centre(AltosUILatLon lat_lon) {
314                 centre = lat_lon;
315                 set_transform();
316         }
317
318         public void centre(double lat, double lon) {
319                 centre(new AltosUILatLon(lat, lon));
320         }
321
322         public void maybe_centre(double lat, double lon) {
323                 AltosUILatLon   lat_lon = new AltosUILatLon(lat, lon);
324                 if (centre == null || (!recent_user_input() && far_from_centre(lat_lon)))
325                         centre(lat_lon);
326         }
327
328         private VolatileImage create_back_buffer() {
329                 return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
330         }
331
332         private Point floor(Point2D.Double point) {
333                 return new Point ((int) Math.floor(point.x / px_size) * px_size,
334                                   (int) Math.floor(point.y / px_size) * px_size);
335         }
336
337         private Point ceil(Point2D.Double point) {
338                 return new Point ((int) Math.ceil(point.x / px_size) * px_size,
339                                   (int) Math.ceil(point.y / px_size) * px_size);
340         }
341
342         private void make_tiles() {
343                 Point   upper_left;
344                 Point   lower_right;
345
346                 if (load_centre != null) {
347                         Point centre = floor(transform.point(load_centre));
348
349                         upper_left = new Point(centre.x - load_radius * px_size,
350                                                centre.y - load_radius * px_size);
351                         lower_right = new Point(centre.x + load_radius * px_size,
352                                                centre.y + load_radius * px_size);
353                 } else {
354                         upper_left = floor(transform.screen_point(new Point(0, 0)));
355                         lower_right = floor(transform.screen_point(new Point(getWidth(), getHeight())));
356                 }
357                 LinkedList<Point> to_remove = new LinkedList<Point>();
358
359                 for (Point point : tiles.keySet()) {
360                         if (point.x < upper_left.x || lower_right.x < point.x ||
361                             point.y < upper_left.y || lower_right.y < point.y) {
362                                 to_remove.add(point);
363                         }
364                 }
365
366                 for (Point point : to_remove)
367                         tiles.remove(point);
368
369                 AltosUIMapCache.set_cache_size(((lower_right.y - upper_left.y) / px_size + 1) * ((lower_right.x - upper_left.x) / px_size + 1));
370                 for (int y = upper_left.y; y <= lower_right.y; y += px_size) {
371                         for (int x = upper_left.x; x <= lower_right.x; x += px_size) {
372                                 Point point = new Point(x, y);
373
374                                 if (!tiles.containsKey(point)) {
375                                         AltosUILatLon   ul = transform.lat_lon(new Point2D.Double(x, y));
376                                         AltosUILatLon   center = transform.lat_lon(new Point2D.Double(x + px_size/2, y + px_size/2));
377                                         AltosUIMapTile tile = new AltosUIMapTile(this, ul, center, zoom, maptype,
378                                                                                  px_size, AltosUILib.value_font);
379                                         tiles.put(point, tile);
380                                 }
381                         }
382                 }
383         }
384
385         /* AltosUIMapTileListener methods */
386         public void notify_tile(AltosUIMapTile tile, int status) {
387                 for (Point point : tiles.keySet()) {
388                         if (tile == tiles.get(point)) {
389                                 Point   screen = transform.screen(point);
390                                 repaint(screen.x, screen.y, px_size, px_size);
391                         }
392                 }
393         }
394
395         /* AltosUIMapStoreListener methods */
396         public void notify_store(AltosUIMapStore store, int status) {
397                 if (load_listener != null) {
398                         for (AltosUIMapTile tile : tiles.values())
399                                 if (store.equals(tile.store))
400                                         load_listener.notify_tile(tile, status);
401                 }
402         }
403
404         private void do_paint(Graphics g) {
405                 Graphics2D      g2d = (Graphics2D) g;
406
407                 make_tiles();
408
409                 for (AltosUIMapTile tile : tiles.values())
410                         tile.paint(g2d, transform);
411
412                 synchronized(marks) {
413                         for (AltosUIMapMark mark : marks)
414                                 mark.paint(g2d, transform);
415                 }
416
417                 path.paint(g2d, transform);
418
419                 line.paint(g2d, transform);
420         }
421
422         public void paint(Graphics g) {
423
424                 VolatileImage   back_buffer = create_back_buffer();
425                 do {
426                         GraphicsConfiguration gc = getGraphicsConfiguration();
427                         int code = back_buffer.validate(gc);
428                         if (code == VolatileImage.IMAGE_INCOMPATIBLE)
429                                 back_buffer = create_back_buffer();
430
431                         Graphics g_back = back_buffer.getGraphics();
432                         g_back.setClip(g.getClip());
433                         do_paint(g_back);
434                         g_back.dispose();
435
436                         g.drawImage(back_buffer, 0, 0, this);
437                 } while (back_buffer.contentsLost());
438                 back_buffer.flush();
439         }
440
441         public void update(Graphics g) {
442                 paint(g);
443         }
444
445         public void add_mark(double lat, double lon, int state) {
446                 synchronized(marks) {
447                         marks.add(new AltosUIMapMark(lat, lon, state));
448                 }
449                 repaint();
450         }
451
452         public void clear_marks() {
453                 synchronized(marks) {
454                         marks.clear();
455                 }
456         }
457
458         public AltosUIMapView() {
459                 centre(0, 0);
460
461                 addComponentListener(this);
462                 addMouseMotionListener(this);
463                 addMouseListener(this);
464                 addMouseWheelListener(this);
465                 set_font();
466         }
467 }