5b981d14e6113f2fa9d24c5a3616168295329c9d
[fw/altos] / altosuilib / AltosUIMap.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; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
17  */
18
19 package org.altusmetrum.altosuilib_13;
20
21 import java.awt.*;
22 import java.awt.event.*;
23 import java.awt.image.*;
24 import javax.swing.*;
25 import java.io.*;
26 import java.lang.Math;
27 import java.awt.geom.*;
28 import java.util.*;
29 import java.util.concurrent.*;
30 import javax.imageio.*;
31 import org.altusmetrum.altoslib_13.*;
32
33 public class AltosUIMap extends JComponent implements AltosFlightDisplay, AltosMapInterface {
34
35         AltosMap        map;
36         Graphics2D      g;
37         Font            tile_font;
38         Font            line_font;
39         AltosMapMark    nearest_mark;
40
41         static Point2D.Double point2d(AltosPointDouble pt) {
42                 return new Point2D.Double(pt.x, pt.y);
43         }
44
45         static final AltosPointDouble point_double(Point pt) {
46                 return new AltosPointDouble(pt.x, pt.y);
47         }
48
49         class MapMark extends AltosMapMark {
50                 public void paint(AltosMapTransform t) {
51                         AltosPointDouble pt = t.screen(lat_lon);
52
53                         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
54                                            RenderingHints.VALUE_ANTIALIAS_ON);
55                         g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
56
57                         if (0 <= state && state < AltosUIMap.stateColors.length)
58                                 g.setColor(AltosUIMap.stateColors[state]);
59                         else
60                                 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
61
62                         g.drawOval((int)pt.x-5, (int)pt.y-5, 10, 10);
63                         g.drawOval((int)pt.x-20, (int)pt.y-20, 40, 40);
64                         g.drawOval((int)pt.x-35, (int)pt.y-35, 70, 70);
65                 }
66
67                 MapMark(double lat, double lon, int state) {
68                         super(lat, lon, state);
69                 }
70         }
71
72         class MapView extends JComponent implements MouseMotionListener, MouseListener, ComponentListener, MouseWheelListener {
73
74                 private VolatileImage create_back_buffer() {
75                         return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
76                 }
77
78                 private void do_paint(Graphics my_g) {
79                         g = (Graphics2D) my_g;
80
81                         map.paint();
82                 }
83
84                 public void paint(Graphics my_g) {
85                         VolatileImage   back_buffer = create_back_buffer();
86
87                         Graphics2D      top_g = (Graphics2D) my_g;
88
89                         do {
90                                 GraphicsConfiguration gc = getGraphicsConfiguration();
91                                 int code = back_buffer.validate(gc);
92                                 if (code == VolatileImage.IMAGE_INCOMPATIBLE)
93                                         back_buffer = create_back_buffer();
94
95                                 Graphics g_back = back_buffer.getGraphics();
96                                 g_back.setClip(top_g.getClip());
97                                 do_paint(g_back);
98                                 g_back.dispose();
99
100                                 top_g.drawImage(back_buffer, 0, 0, this);
101                         } while (back_buffer.contentsLost());
102                         back_buffer.flush();
103                 }
104
105                 public void repaint(AltosRectangle damage) {
106                         repaint(damage.x, damage.y, damage.width, damage.height);
107                 }
108
109                 private boolean is_drag_event(MouseEvent e) {
110                         return e.getModifiersEx() == InputEvent.BUTTON1_DOWN_MASK;
111                 }
112
113                 /* MouseMotionListener methods */
114
115                 public void mouseDragged(MouseEvent e) {
116                         map.touch_continue(e.getPoint().x, e.getPoint().y, is_drag_event(e));
117                 }
118
119                 String pos(double p, String pos, String neg) {
120                         if (p == AltosLib.MISSING)
121                                 return "";
122                         String  h = pos;
123                         if (p < 0) {
124                                 h = neg;
125                                 p = -p;
126                         }
127                         int deg = (int) Math.floor(p);
128                         double min = (p - Math.floor(p)) * 60.0;
129                         return String.format("%s %4d° %9.6f'", h, deg, min);
130                 }
131
132                 String height(double h, String label) {
133                         if (h == AltosLib.MISSING)
134                                 return "";
135                         return String.format(" %s%s",
136                                              AltosConvert.height.show(6, h),
137                                              label);
138                 }
139
140                 String speed(double s, String label) {
141                         if (s == AltosLib.MISSING)
142                                 return "";
143                         return String.format(" %s%s",
144                                              AltosConvert.speed.show(6, s),
145                                              label);
146                 }
147
148                 public void mouseMoved(MouseEvent e) {
149                         AltosMapPathPoint point = map.nearest(e.getPoint().x, e.getPoint().y);
150
151                         if (nearest_mark == null)
152                                 nearest_mark = map.add_mark(point.gps.lat,
153                                                             point.gps.lon,
154                                                             point.state);
155                         else {
156                                 nearest_mark.lat_lon.lat = point.gps.lat;
157                                 nearest_mark.lat_lon.lon = point.gps.lon;
158                                 nearest_mark.state = point.state;
159                         }
160                         if (point != null) {
161                                 nearest_label.setText(String.format("%9.2f sec %s%s%s%s",
162                                                                     point.time,
163                                                                     pos(point.gps.lat,
164                                                                         "N", "S"),
165                                                                     pos(point.gps.lon,
166                                                                         "E", "W"),
167                                                                     height(point.gps_height, ""),
168                                                                     speed(point.gps.ground_speed, "(h)"),
169                                                                     speed(point.gps.climb_rate, "(v)")));
170                         } else {
171                                 nearest_label.setText("");
172                         }
173                         repaint();
174                 }
175
176                 /* MouseListener methods */
177                 public void mouseClicked(MouseEvent e) {
178                 }
179
180                 public void mouseEntered(MouseEvent e) {
181                 }
182
183                 public void mouseExited(MouseEvent e) {
184                 }
185
186                 public void mousePressed(MouseEvent e) {
187                         map.touch_start(e.getPoint().x, e.getPoint().y, is_drag_event(e));
188                 }
189
190                 public void mouseReleased(MouseEvent e) {
191                 }
192
193                 /* MouseWheelListener methods */
194
195                 public void mouseWheelMoved(MouseWheelEvent e) {
196                         int     zoom_change = e.getWheelRotation();
197
198                         map.set_zoom_centre(map.get_zoom() - zoom_change, new AltosPointInt(e.getPoint().x, e.getPoint().y));
199                 }
200
201                 /* ComponentListener methods */
202
203                 public void componentHidden(ComponentEvent e) {
204                 }
205
206                 public void componentMoved(ComponentEvent e) {
207                 }
208
209                 public void componentResized(ComponentEvent e) {
210                         map.set_transform();
211                 }
212
213                 public void componentShown(ComponentEvent e) {
214                         map.set_transform();
215                 }
216
217                 MapView() {
218                         addComponentListener(this);
219                         addMouseMotionListener(this);
220                         addMouseListener(this);
221                         addMouseWheelListener(this);
222                 }
223         }
224
225         class MapLine extends AltosMapLine {
226
227                 public void paint(AltosMapTransform t) {
228
229                         if (start == null || end == null)
230                                 return;
231
232                         g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
233
234                         Line2D.Double line = new Line2D.Double(point2d(t.screen(start)),
235                                                                point2d(t.screen(end)));
236
237                         g.setColor(Color.WHITE);
238                         g.setStroke(new BasicStroke(stroke_width+4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
239                         g.draw(line);
240
241                         g.setColor(Color.BLUE);
242                         g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
243                         g.draw(line);
244
245                         String  message = line_dist();
246                         Rectangle2D     bounds;
247                         bounds = line_font.getStringBounds(message, g.getFontRenderContext());
248
249                         float x = (float) line.x1;
250                         float y = (float) line.y1 + (float) bounds.getHeight() / 2.0f;
251
252                         if (line.x1 < line.x2) {
253                                 x -= (float) bounds.getWidth() + 2.0f;
254                         } else {
255                                 x += 2.0f;
256                         }
257
258                         g.setFont(line_font);
259                         g.setColor(Color.WHITE);
260                         for (int dy = -2; dy <= 2; dy += 2)
261                                 for (int dx = -2; dx <= 2; dx += 2)
262                                         g.drawString(message, x + dx, y + dy);
263                         g.setColor(Color.BLUE);
264                         g.drawString(message, x, y);
265                 }
266
267                 public MapLine() {
268                 }
269         }
270
271         class MapPath extends AltosMapPath {
272                 public void paint(AltosMapTransform t) {
273                         Point2D.Double  prev = null;
274
275                         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
276                                            RenderingHints.VALUE_ANTIALIAS_ON);
277                         g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
278
279                         for (AltosMapPathPoint point : points) {
280                                 Point2D.Double  cur = point2d(t.screen(point.gps.lat, point.gps.lon));
281                                 if (prev != null) {
282                                         Line2D.Double   line = new Line2D.Double (prev, cur);
283                                         Rectangle       bounds = line.getBounds();
284
285                                         if (g.hitClip(bounds.x, bounds.y, bounds.width, bounds.height)) {
286                                                 if (0 <= point.state && point.state < AltosUIMap.stateColors.length)
287                                                         g.setColor(AltosUIMap.stateColors[point.state]);
288                                                 else
289                                                         g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
290
291                                                 g.draw(line);
292                                         }
293                                 }
294                                 prev = cur;
295                         }
296                 }
297         }
298
299         class MapTile extends AltosMapTile {
300                 public MapTile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
301                         super(cache, upper_left, center, zoom, maptype, px_size, scale);
302                 }
303
304                 public void paint(AltosMapTransform t) {
305
306                         AltosPointDouble        point_double = t.screen(upper_left);
307                         Point                   point = new Point((int) (point_double.x + 0.5),
308                                                                   (int) (point_double.y + 0.5));
309
310                         if (!g.hitClip(point.x, point.y, px_size, px_size))
311                                 return;
312
313                         AltosImage      altos_image = get_image();
314                         AltosUIImage    ui_image = (AltosUIImage) altos_image;
315                         Image           image = null;
316
317                         if (ui_image != null)
318                                 image = ui_image.image;
319
320                         if (image != null) {
321                                 g.drawImage(image, point.x, point.y, null);
322 /*
323  * Useful when debugging map fetching problems
324  *
325                                 String message = String.format("%.6f %.6f", center.lat, center.lon);
326                                 g.setFont(tile_font);
327                                 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
328                                 Rectangle2D bounds = tile_font.getStringBounds(message, g.getFontRenderContext());
329
330                                 float x = px_size / 2.0f;
331                                 float y = px_size / 2.0f;
332                                 x = x - (float) bounds.getWidth() / 2.0f;
333                                 y = y + (float) bounds.getHeight() / 2.0f;
334                                 g.setColor(Color.RED);
335                                 g.drawString(message, (float) point_double.x + x, (float) point_double.y + y);
336 */
337                         } else {
338                                 g.setColor(Color.GRAY);
339                                 g.fillRect(point.x, point.y, px_size, px_size);
340
341                                 if (t.has_location()) {
342                                         String  message = null;
343                                         switch (status) {
344                                         case AltosMapTile.fetching:
345                                                 message = "Fetching...";
346                                                 break;
347                                         case AltosMapTile.bad_request:
348                                                 message = "Internal error";
349                                                 break;
350                                         case AltosMapTile.failed:
351                                                 message = "Network error";
352                                                 break;
353                                         case AltosMapTile.forbidden:
354                                                 message = "Outside of known launch areas";
355                                                 break;
356                                         }
357                                         if (message != null && tile_font != null) {
358                                                 g.setFont(tile_font);
359                                                 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
360                                                 Rectangle2D bounds = tile_font.getStringBounds(message, g.getFontRenderContext());
361
362                                                 float x = px_size / 2.0f;
363                                                 float y = px_size / 2.0f;
364                                                 x = x - (float) bounds.getWidth() / 2.0f;
365                                                 y = y + (float) bounds.getHeight() / 2.0f;
366                                                 g.setColor(Color.BLACK);
367                                                 g.drawString(message, (float) point_double.x + x, (float) point_double.y + y);
368                                         }
369                                 }
370                         }
371                 }
372         }
373
374         public static final Color stateColors[] = {
375                 Color.WHITE,  // startup
376                 Color.WHITE,  // idle
377                 Color.WHITE,  // pad
378                 Color.RED,    // boost
379                 Color.PINK,   // fast
380                 Color.YELLOW, // coast
381                 Color.CYAN,   // drogue
382                 Color.BLUE,   // main
383                 Color.BLACK,  // landed
384                 Color.BLACK,  // invalid
385                 Color.CYAN,   // stateless
386         };
387
388         /* AltosMapInterface functions */
389
390         public AltosMapPath new_path() {
391                 return new MapPath();
392         }
393
394         public AltosMapLine new_line() {
395                 return new MapLine();
396         }
397
398         public AltosImage load_image(File file) throws Exception {
399                 return new AltosUIImage(ImageIO.read(file));
400         }
401
402         public AltosMapMark new_mark(double lat, double lon, int state) {
403                 return new MapMark(lat, lon, state);
404         }
405
406         public AltosMapTile new_tile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
407                 return new MapTile(cache, upper_left, center, zoom, maptype, px_size, scale);
408         }
409
410         public int width() {
411                 return view.getWidth();
412         }
413
414         public int height() {
415                 return view.getHeight();
416         }
417
418         public void repaint() {
419                 view.repaint();
420         }
421
422         public void repaint(AltosRectangle damage) {
423                 view.repaint(damage);
424         }
425
426         public void set_zoom_label(String label) {
427                 zoom_label.setText(label);
428         }
429
430         public void select_object(AltosLatLon latlon) {
431                 debug("select at %f,%f\n", latlon.lat, latlon.lon);
432         }
433
434         public void debug(String format, Object ... arguments) {
435                 if (AltosUIPreferences.serial_debug())
436                         System.out.printf(format, arguments);
437         }
438
439
440         /* AltosFlightDisplay interface */
441
442         public void set_font() {
443                 tile_font = AltosUILib.value_font;
444                 line_font = AltosUILib.status_font;
445                 if (nearest_label != null)
446                         nearest_label.setFont(AltosUILib.value_font);
447         }
448
449         public void font_size_changed(int font_size) {
450                 set_font();
451                 repaint();
452         }
453
454         public void units_changed(boolean imperial_units) {
455                 repaint();
456         }
457
458         JLabel  zoom_label;
459
460         JLabel  nearest_label;
461
462         public void set_maptype(int type) {
463 /*
464                 map.set_maptype(type);
465                 maptype_combo.setSelectedIndex(type);
466 */
467         }
468
469         /* AltosUIMapPreload functions */
470
471         public void set_zoom(int zoom) {
472                 map.set_zoom(zoom);
473         }
474
475         public void add_mark(double lat, double lon, int status) {
476                 map.add_mark(lat, lon, status);
477         }
478
479         public void clear_marks() {
480                 map.clear_marks();
481         }
482
483         /* AltosFlightDisplay interface */
484         public void reset() {
485                 // nothing
486         }
487
488         public void show(AltosState state, AltosListenerState listener_state) {
489                 map.show(state, listener_state);
490         }
491
492         public void show(AltosGPS gps, double time, int state, double gps_height) {
493                 map.show(gps, time, state, gps_height);
494         }
495
496         public String getName() {
497                 return "Map";
498         }
499
500         /* AltosGraphUI interface */
501         public void centre(AltosState state) {
502                 map.centre(state);
503         }
504
505         public void centre(AltosGPS gps) {
506                 map.centre(gps);
507         }
508
509         /* internal layout bits */
510         private GridBagLayout layout = new GridBagLayout();
511
512 /*
513         JComboBox<String>       maptype_combo;
514 */
515
516         MapView view;
517
518         public AltosUIMap() {
519
520                 set_font();
521
522                 view = new MapView();
523
524                 view.setPreferredSize(new Dimension(500,500));
525                 view.setVisible(true);
526                 view.setEnabled(true);
527
528                 GridBagLayout   my_layout = new GridBagLayout();
529
530                 setLayout(my_layout);
531
532                 GridBagConstraints c = new GridBagConstraints();
533                 c.anchor = GridBagConstraints.CENTER;
534                 c.fill = GridBagConstraints.BOTH;
535                 c.gridx = 0;
536                 c.gridy = 0;
537                 c.gridwidth = 1;
538                 c.gridheight = 10;
539                 c.weightx = 1;
540                 c.weighty = 1;
541                 add(view, c);
542
543                 int     y = 0;
544
545                 zoom_label = new JLabel("", JLabel.CENTER);
546
547                 c = new GridBagConstraints();
548                 c.anchor = GridBagConstraints.CENTER;
549                 c.fill = GridBagConstraints.HORIZONTAL;
550                 c.gridx = 1;
551                 c.gridy = y++;
552                 c.weightx = 0;
553                 c.weighty = 0;
554                 add(zoom_label, c);
555
556                 JButton zoom_reset = new JButton("0");
557                 zoom_reset.addActionListener(new ActionListener() {
558                                 public void actionPerformed(ActionEvent e) {
559                                         map.set_zoom(map.default_zoom);
560                                 }
561                         });
562
563                 c = new GridBagConstraints();
564                 c.anchor = GridBagConstraints.CENTER;
565                 c.fill = GridBagConstraints.HORIZONTAL;
566                 c.gridx = 1;
567                 c.gridy = y++;
568                 c.weightx = 0;
569                 c.weighty = 0;
570                 add(zoom_reset, c);
571
572                 JButton zoom_in = new JButton("+");
573                 zoom_in.addActionListener(new ActionListener() {
574                                 public void actionPerformed(ActionEvent e) {
575                                         map.set_zoom(map.get_zoom() + 1);
576                                 }
577                         });
578
579                 c = new GridBagConstraints();
580                 c.anchor = GridBagConstraints.CENTER;
581                 c.fill = GridBagConstraints.HORIZONTAL;
582                 c.gridx = 1;
583                 c.gridy = y++;
584                 c.weightx = 0;
585                 c.weighty = 0;
586                 add(zoom_in, c);
587
588                 JButton zoom_out = new JButton("-");
589                 zoom_out.addActionListener(new ActionListener() {
590                                 public void actionPerformed(ActionEvent e) {
591                                         map.set_zoom(map.get_zoom() - 1);
592                                 }
593                         });
594                 c = new GridBagConstraints();
595                 c.anchor = GridBagConstraints.CENTER;
596                 c.fill = GridBagConstraints.HORIZONTAL;
597                 c.gridx = 1;
598                 c.gridy = y++;
599                 c.weightx = 0;
600                 c.weighty = 0;
601                 add(zoom_out, c);
602
603
604                 nearest_label = new JLabel("", JLabel.LEFT);
605                 nearest_label.setFont(tile_font);
606
607                 c = new GridBagConstraints();
608                 c.anchor = GridBagConstraints.CENTER;
609                 c.fill = GridBagConstraints.HORIZONTAL;
610                 c.gridx = 0;
611                 c.gridy = 11;
612                 c.weightx = 0;
613                 c.weighty = 0;
614                 c.gridwidth = 1;
615                 c.gridheight = 1;
616                 add(nearest_label, c);
617 /*
618                 maptype_combo = new JComboBox<String>(map.maptype_labels);
619
620                 maptype_combo.setEditable(false);
621                 maptype_combo.setMaximumRowCount(maptype_combo.getItemCount());
622                 maptype_combo.addItemListener(new ItemListener() {
623                                 public void itemStateChanged(ItemEvent e) {
624                                         map.set_maptype(maptype_combo.getSelectedIndex());
625                                 }
626                         });
627
628                 c = new GridBagConstraints();
629                 c.anchor = GridBagConstraints.CENTER;
630                 c.fill = GridBagConstraints.HORIZONTAL;
631                 c.gridx = 1;
632                 c.gridy = y++;
633                 c.weightx = 0;
634                 c.weighty = 0;
635                 add(maptype_combo, c);
636 */
637                 map = new AltosMap(this);
638         }
639 }