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