altosuilib: Repaint map when starting line draw
[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                         repaint();
140                 }
141         }
142
143         public void mouseReleased(MouseEvent e) {
144         }
145
146         /* MouseWheelListener methods */
147
148         public void mouseWheelMoved(MouseWheelEvent e) {
149                 int     zoom_change = e.getWheelRotation();
150
151                 notice_user_input();
152                 AltosUILatLon   mouse_lat_lon = transform.screen_lat_lon(e.getPoint());
153                 set_zoom(zoom() - zoom_change);
154
155                 Point2D.Double  new_mouse = transform.screen(mouse_lat_lon);
156
157                 int     dx = getWidth()/2 - e.getPoint().x;
158                 int     dy = getHeight()/2 - e.getPoint().y;
159
160                 AltosUILatLon   new_centre = transform.screen_lat_lon(new Point((int) new_mouse.x + dx, (int) new_mouse.y + dy));
161
162                 centre(new_centre.lat, new_centre.lon);
163         }
164
165         /* ComponentListener methods */
166
167         public void componentHidden(ComponentEvent e) {
168         }
169
170         public void componentMoved(ComponentEvent e) {
171         }
172
173         public void componentResized(ComponentEvent e) {
174                 set_transform();
175         }
176
177         public void componentShown(ComponentEvent e) {
178                 set_transform();
179         }
180
181         public void repaint(Rectangle r, int pad) {
182                 repaint(r.x - pad, r.y - pad, r.width + pad*2, r.height + pad*2);
183         }
184
185         public void repaint(AltosUIMapRectangle rect, int pad) {
186                 repaint (transform.screen(rect), pad);
187         }
188
189         private boolean far_from_centre(AltosUILatLon lat_lon) {
190
191                 if (centre == null || transform == null)
192                         return true;
193
194                 Point2D.Double  screen = transform.screen(lat_lon);
195
196                 int             width = getWidth();
197                 int             dx = Math.abs ((int) screen.x - width/2);
198
199                 if (dx > width / 4)
200                         return true;
201
202                 int             height = getHeight();
203                 int             dy = Math.abs ((int) screen.y - height/2);
204
205                 if (dy > height / 4)
206                         return true;
207
208                 return false;
209         }
210
211         public void show(AltosState state, AltosListenerState listener_state) {
212
213                 /* If insufficient gps data, nothing to update
214                  */
215                 AltosGPS        gps = state.gps;
216
217                 if (gps == null)
218                         return;
219
220                 if (!gps.locked && gps.nsat < 4)
221                         return;
222
223                 AltosUIMapRectangle     damage = path.add(gps.lat, gps.lon, state.state);
224
225                 switch (state.state) {
226                 case AltosLib.ao_flight_boost:
227                         if (!have_boost) {
228                                 add_mark(gps.lat, gps.lon, state.state);
229                                 have_boost = true;
230                         }
231                         break;
232                 case AltosLib.ao_flight_landed:
233                         if (!have_landed) {
234                                 add_mark(gps.lat, gps.lon, state.state);
235                                 have_landed = true;
236                         }
237                         break;
238                 }
239
240                 if (damage != null)
241                         repaint(damage, AltosUIMapPath.stroke_width);
242                 maybe_centre(gps.lat, gps.lon);
243         }
244
245         private void set_transform() {
246                 Rectangle       bounds = getBounds();
247
248                 transform = new AltosUIMapTransform(bounds.width, bounds.height, zoom, centre);
249                 repaint();
250         }
251
252         public boolean set_zoom(int zoom) {
253                 if (min_zoom <= zoom && zoom <= max_zoom && zoom != this.zoom) {
254                         this.zoom = zoom;
255                         tiles.clear();
256                         set_transform();
257
258                         for (AltosUIMapZoomListener listener : zoom_listeners)
259                                 listener.zoom_changed(this.zoom);
260
261                         return true;
262                 }
263                 return false;
264         }
265
266         public void add_zoom_listener(AltosUIMapZoomListener listener) {
267                 if (!zoom_listeners.contains(listener))
268                         zoom_listeners.add(listener);
269         }
270
271         public void remove_zoom_listener(AltosUIMapZoomListener listener) {
272                 zoom_listeners.remove(listener);
273         }
274
275         public void set_load_params(double lat, double lon, int radius, AltosUIMapTileListener listener) {
276                 load_centre = new AltosUILatLon(lat, lon);
277                 load_radius = radius;
278                 load_listener = listener;
279                 centre(lat, lon);
280                 make_tiles();
281                 for (AltosUIMapTile tile : tiles.values()) {
282                         tile.add_store_listener(this);
283                         if (tile.store_status() != AltosUIMapStore.loading)
284                                 listener.notify_tile(tile, tile.store_status());
285                 }
286                 repaint();
287         }
288
289         public boolean all_fetched() {
290                 for (AltosUIMapTile tile : tiles.values()) {
291                         if (tile.store_status() == AltosUIMapStore.loading)
292                                 return false;
293                 }
294                 return true;
295         }
296
297         public boolean set_maptype(int maptype) {
298                 if (maptype != this.maptype) {
299                         this.maptype = maptype;
300                         tiles.clear();
301                         repaint();
302                         return true;
303                 }
304                 return false;
305         }
306
307         public int get_maptype() {
308                 return maptype;
309         }
310
311         public int zoom() {
312                 return zoom;
313         }
314
315         public void centre(AltosUILatLon lat_lon) {
316                 centre = lat_lon;
317                 set_transform();
318         }
319
320         public void centre(double lat, double lon) {
321                 centre(new AltosUILatLon(lat, lon));
322         }
323
324         public void maybe_centre(double lat, double lon) {
325                 AltosUILatLon   lat_lon = new AltosUILatLon(lat, lon);
326                 if (centre == null || (!recent_user_input() && far_from_centre(lat_lon)))
327                         centre(lat_lon);
328         }
329
330         private VolatileImage create_back_buffer() {
331                 return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
332         }
333
334         private Point floor(Point2D.Double point) {
335                 return new Point ((int) Math.floor(point.x / px_size) * px_size,
336                                   (int) Math.floor(point.y / px_size) * px_size);
337         }
338
339         private Point ceil(Point2D.Double point) {
340                 return new Point ((int) Math.ceil(point.x / px_size) * px_size,
341                                   (int) Math.ceil(point.y / px_size) * px_size);
342         }
343
344         private void make_tiles() {
345                 Point   upper_left;
346                 Point   lower_right;
347
348                 if (load_centre != null) {
349                         Point centre = floor(transform.point(load_centre));
350
351                         upper_left = new Point(centre.x - load_radius * px_size,
352                                                centre.y - load_radius * px_size);
353                         lower_right = new Point(centre.x + load_radius * px_size,
354                                                centre.y + load_radius * px_size);
355                 } else {
356                         upper_left = floor(transform.screen_point(new Point(0, 0)));
357                         lower_right = floor(transform.screen_point(new Point(getWidth(), getHeight())));
358                 }
359                 LinkedList<Point> to_remove = new LinkedList<Point>();
360
361                 for (Point point : tiles.keySet()) {
362                         if (point.x < upper_left.x || lower_right.x < point.x ||
363                             point.y < upper_left.y || lower_right.y < point.y) {
364                                 to_remove.add(point);
365                         }
366                 }
367
368                 for (Point point : to_remove)
369                         tiles.remove(point);
370
371                 AltosUIMapCache.set_cache_size(((lower_right.y - upper_left.y) / px_size + 1) * ((lower_right.x - upper_left.x) / px_size + 1));
372                 for (int y = upper_left.y; y <= lower_right.y; y += px_size) {
373                         for (int x = upper_left.x; x <= lower_right.x; x += px_size) {
374                                 Point point = new Point(x, y);
375
376                                 if (!tiles.containsKey(point)) {
377                                         AltosUILatLon   ul = transform.lat_lon(new Point2D.Double(x, y));
378                                         AltosUILatLon   center = transform.lat_lon(new Point2D.Double(x + px_size/2, y + px_size/2));
379                                         AltosUIMapTile tile = new AltosUIMapTile(this, ul, center, zoom, maptype,
380                                                                                  px_size, AltosUILib.value_font);
381                                         tiles.put(point, tile);
382                                 }
383                         }
384                 }
385         }
386
387         /* AltosUIMapTileListener methods */
388         public void notify_tile(AltosUIMapTile tile, int status) {
389                 for (Point point : tiles.keySet()) {
390                         if (tile == tiles.get(point)) {
391                                 Point   screen = transform.screen(point);
392                                 repaint(screen.x, screen.y, px_size, px_size);
393                         }
394                 }
395         }
396
397         /* AltosUIMapStoreListener methods */
398         public void notify_store(AltosUIMapStore store, int status) {
399                 if (load_listener != null) {
400                         for (AltosUIMapTile tile : tiles.values())
401                                 if (store.equals(tile.store))
402                                         load_listener.notify_tile(tile, status);
403                 }
404         }
405
406         private void do_paint(Graphics g) {
407                 Graphics2D      g2d = (Graphics2D) g;
408
409                 make_tiles();
410
411                 for (AltosUIMapTile tile : tiles.values())
412                         tile.paint(g2d, transform);
413
414                 synchronized(marks) {
415                         for (AltosUIMapMark mark : marks)
416                                 mark.paint(g2d, transform);
417                 }
418
419                 path.paint(g2d, transform);
420
421                 line.paint(g2d, transform);
422         }
423
424         public void paint(Graphics g) {
425
426                 VolatileImage   back_buffer = create_back_buffer();
427                 do {
428                         GraphicsConfiguration gc = getGraphicsConfiguration();
429                         int code = back_buffer.validate(gc);
430                         if (code == VolatileImage.IMAGE_INCOMPATIBLE)
431                                 back_buffer = create_back_buffer();
432
433                         Graphics g_back = back_buffer.getGraphics();
434                         g_back.setClip(g.getClip());
435                         do_paint(g_back);
436                         g_back.dispose();
437
438                         g.drawImage(back_buffer, 0, 0, this);
439                 } while (back_buffer.contentsLost());
440                 back_buffer.flush();
441         }
442
443         public void update(Graphics g) {
444                 paint(g);
445         }
446
447         public void add_mark(double lat, double lon, int state) {
448                 synchronized(marks) {
449                         marks.add(new AltosUIMapMark(lat, lon, state));
450                 }
451                 repaint();
452         }
453
454         public void clear_marks() {
455                 synchronized(marks) {
456                         marks.clear();
457                 }
458         }
459
460         public AltosUIMapView() {
461                 centre(0, 0);
462
463                 addComponentListener(this);
464                 addMouseMotionListener(this);
465                 addMouseListener(this);
466                 addMouseWheelListener(this);
467                 set_font();
468         }
469 }