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