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