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