2 * Copyright © 2010 Anthony Towns <aj@erisian.com.au>
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.
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.
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.
19 package org.altusmetrum.altosuilib_14;
22 import java.awt.event.*;
23 import java.awt.image.*;
26 import java.lang.Math;
27 import java.awt.geom.*;
29 import java.util.concurrent.*;
30 import javax.imageio.*;
31 import org.altusmetrum.altoslib_14.*;
33 public class AltosUIMap extends JComponent implements AltosFlightDisplay, AltosMapInterface {
39 AltosMapMark nearest_mark;
41 static Point2D.Double point2d(AltosPointDouble pt) {
42 return new Point2D.Double(pt.x, pt.y);
45 static final AltosPointDouble point_double(Point pt) {
46 return new AltosPointDouble(pt.x, pt.y);
49 class MapMark extends AltosMapMark {
50 public void paint(AltosMapTransform t) {
51 AltosPointDouble pt = t.screen(lat_lon);
53 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
54 RenderingHints.VALUE_ANTIALIAS_ON);
55 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
57 if (0 <= state && state < AltosUIMap.stateColors.length)
58 g.setColor(AltosUIMap.stateColors[state]);
60 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
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);
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;
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]);
80 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
81 g.drawString(label, x, y);
85 MapMark(double lat, double lon, int state, String label) {
86 super(lat, lon, state, label);
89 MapMark(double lat, double lon, int state) {
90 super(lat, lon, state);
94 class MapView extends JComponent implements MouseMotionListener, MouseListener, ComponentListener, MouseWheelListener {
96 private VolatileImage create_back_buffer() {
97 return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
100 private void do_paint(Graphics my_g) {
101 g = (Graphics2D) my_g;
106 public void paint(Graphics my_g) {
107 VolatileImage back_buffer = create_back_buffer();
109 Graphics2D top_g = (Graphics2D) my_g;
112 GraphicsConfiguration gc = getGraphicsConfiguration();
113 int code = back_buffer.validate(gc);
114 if (code == VolatileImage.IMAGE_INCOMPATIBLE)
115 back_buffer = create_back_buffer();
117 Graphics g_back = back_buffer.getGraphics();
118 g_back.setClip(top_g.getClip());
122 top_g.drawImage(back_buffer, 0, 0, this);
123 } while (back_buffer.contentsLost());
127 public void repaint(AltosRectangle damage) {
128 repaint(damage.x, damage.y, damage.width, damage.height);
131 private boolean is_drag_event(MouseEvent e) {
132 return e.getModifiersEx() == InputEvent.BUTTON1_DOWN_MASK;
135 /* MouseMotionListener methods */
137 public void mouseDragged(MouseEvent e) {
138 map.touch_continue(e.getPoint().x, e.getPoint().y, is_drag_event(e));
141 String pos(double p, String pos, String neg) {
142 if (p == AltosLib.MISSING)
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);
154 String height(double h, String label) {
155 if (h == AltosLib.MISSING)
157 return String.format(" %s%s",
158 AltosConvert.height.show(6, h),
162 String speed(double s, String label) {
163 if (s == AltosLib.MISSING)
165 return String.format(" %s%s",
166 AltosConvert.speed.show(6, s),
170 public void mouseMoved(MouseEvent e) {
171 AltosMapPathPoint point = map.nearest(e.getPoint().x, e.getPoint().y);
174 if (nearest_mark == null)
175 nearest_mark = map.add_mark(point.gps.lat,
179 nearest_mark.lat_lon.lat = point.gps.lat;
180 nearest_mark.lat_lon.lon = point.gps.lon;
181 nearest_mark.state = point.state;
183 nearest_label.setText(String.format("%9.2f sec %s%s%s%s",
189 height(point.gps_height, ""),
190 speed(point.gps.ground_speed, "(h)"),
191 speed(point.gps.climb_rate, "(v)")));
193 nearest_label.setText("");
198 /* MouseListener methods */
199 public void mouseClicked(MouseEvent e) {
202 public void mouseEntered(MouseEvent e) {
205 public void mouseExited(MouseEvent e) {
208 public void mousePressed(MouseEvent e) {
209 map.touch_start(e.getPoint().x, e.getPoint().y, is_drag_event(e));
212 public void mouseReleased(MouseEvent e) {
215 /* MouseWheelListener methods */
217 public void mouseWheelMoved(MouseWheelEvent e) {
218 int zoom_change = e.getWheelRotation();
220 map.set_zoom_centre(map.get_zoom() - zoom_change, new AltosPointInt(e.getPoint().x, e.getPoint().y));
223 /* ComponentListener methods */
225 public void componentHidden(ComponentEvent e) {
228 public void componentMoved(ComponentEvent e) {
231 public void componentResized(ComponentEvent e) {
235 public void componentShown(ComponentEvent e) {
240 addComponentListener(this);
241 addMouseMotionListener(this);
242 addMouseListener(this);
243 addMouseWheelListener(this);
247 class MapLine extends AltosMapLine {
249 public void paint(AltosMapTransform t) {
251 if (start == null || end == null)
254 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
256 Line2D.Double line = new Line2D.Double(point2d(t.screen(start)),
257 point2d(t.screen(end)));
259 g.setColor(Color.WHITE);
260 g.setStroke(new BasicStroke(stroke_width+4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
263 g.setColor(Color.BLUE);
264 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
267 String message = line_dist();
269 bounds = line_font.getStringBounds(message, g.getFontRenderContext());
271 float x = (float) line.x1;
272 float y = (float) line.y1 + (float) bounds.getHeight() / 2.0f;
274 if (line.x1 < line.x2) {
275 x -= (float) bounds.getWidth() + 2.0f;
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);
293 class MapPath extends AltosMapPath {
294 public void paint(AltosMapTransform t) {
295 Point2D.Double prev = null;
297 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
298 RenderingHints.VALUE_ANTIALIAS_ON);
299 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
301 for (AltosMapPathPoint point : points) {
302 Point2D.Double cur = point2d(t.screen(point.gps.lat, point.gps.lon));
304 Line2D.Double line = new Line2D.Double (prev, cur);
305 Rectangle bounds = line.getBounds();
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]);
311 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
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);
326 public void paint(AltosMapTransform t) {
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));
332 if (!g.hitClip(point.x, point.y, px_size, px_size))
335 AltosImage altos_image = get_image();
336 AltosUIImage ui_image = (AltosUIImage) altos_image;
339 if (ui_image != null)
340 image = ui_image.image;
343 g.drawImage(image, point.x, point.y, null);
345 * Useful when debugging map fetching problems
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());
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);
360 g.setColor(Color.GRAY);
361 g.fillRect(point.x, point.y, px_size, px_size);
363 if (t.has_location()) {
364 String message = null;
366 case AltosMapTile.fetching:
367 message = "Fetching...";
369 case AltosMapTile.bad_request:
370 message = "Internal error";
372 case AltosMapTile.failed:
373 message = "Network error";
375 case AltosMapTile.forbidden:
376 message = "Outside of known launch areas";
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());
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);
396 public static final Color stateColors[] = {
397 Color.WHITE, // startup
402 Color.YELLOW, // coast
403 Color.CYAN, // drogue
405 Color.BLACK, // landed
406 Color.BLACK, // invalid
407 Color.CYAN, // stateless
410 /* AltosMapInterface functions */
412 public AltosMapPath new_path() {
413 return new MapPath();
416 public AltosMapLine new_line() {
417 return new MapLine();
420 public AltosImage load_image(File file) throws Exception {
421 return new AltosUIImage(ImageIO.read(file));
424 public AltosMapMark new_mark(double lat, double lon, int state) {
425 return new MapMark(lat, lon, state);
428 public AltosMapMark new_mark(double lat, double lon, int state, String label) {
429 return new MapMark(lat, lon, state, label);
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);
437 return view.getWidth();
440 public int height() {
441 return view.getHeight();
444 public void repaint() {
448 public void repaint(AltosRectangle damage) {
449 view.repaint(damage);
452 public void set_zoom_label(String label) {
453 zoom_label.setText(label);
456 public void select_object(AltosLatLon latlon) {
457 debug("select at %f,%f\n", latlon.lat, latlon.lon);
460 public void debug(String format, Object ... arguments) {
461 if (AltosUIPreferences.serial_debug())
462 System.out.printf(format, arguments);
466 /* AltosFlightDisplay interface */
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);
475 public void font_size_changed(int font_size) {
480 public void units_changed(boolean imperial_units) {
486 JLabel nearest_label;
488 public void set_maptype(int type) {
490 map.set_maptype(type);
491 maptype_combo.setSelectedIndex(type);
495 /* AltosUIMapPreload functions */
497 public void set_zoom(int zoom) {
501 public void add_mark(double lat, double lon, int status) {
502 map.add_mark(lat, lon, status);
505 public void add_mark(double lat, double lon, int status, String label) {
506 map.add_mark(lat, lon, status, label);
509 public void clear_marks() {
513 /* AltosFlightDisplay interface */
514 public void reset() {
518 public void show(AltosState state, AltosListenerState listener_state) {
519 map.show(state, listener_state);
522 public void show(AltosGPS gps, double time, int state, double gps_height) {
523 map.show(gps, time, state, gps_height);
526 public String getName() {
530 /* AltosGraphUI interface */
531 public void centre(AltosState state) {
535 public void centre(AltosGPS gps) {
539 /* internal layout bits */
540 private GridBagLayout layout = new GridBagLayout();
543 JComboBox<String> maptype_combo;
548 public AltosUIMap() {
552 view = new MapView();
554 view.setPreferredSize(new Dimension(500,500));
555 view.setVisible(true);
556 view.setEnabled(true);
558 GridBagLayout my_layout = new GridBagLayout();
560 setLayout(my_layout);
562 GridBagConstraints c = new GridBagConstraints();
563 c.anchor = GridBagConstraints.CENTER;
564 c.fill = GridBagConstraints.BOTH;
575 zoom_label = new JLabel("", JLabel.CENTER);
577 c = new GridBagConstraints();
578 c.anchor = GridBagConstraints.CENTER;
579 c.fill = GridBagConstraints.HORIZONTAL;
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);
593 c = new GridBagConstraints();
594 c.anchor = GridBagConstraints.CENTER;
595 c.fill = GridBagConstraints.HORIZONTAL;
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);
609 c = new GridBagConstraints();
610 c.anchor = GridBagConstraints.CENTER;
611 c.fill = GridBagConstraints.HORIZONTAL;
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);
624 c = new GridBagConstraints();
625 c.anchor = GridBagConstraints.CENTER;
626 c.fill = GridBagConstraints.HORIZONTAL;
634 nearest_label = new JLabel("", JLabel.LEFT);
635 nearest_label.setFont(tile_font);
637 c = new GridBagConstraints();
638 c.anchor = GridBagConstraints.CENTER;
639 c.fill = GridBagConstraints.HORIZONTAL;
646 add(nearest_label, c);
648 maptype_combo = new JComboBox<String>(map.maptype_labels);
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());
658 c = new GridBagConstraints();
659 c.anchor = GridBagConstraints.CENTER;
660 c.fill = GridBagConstraints.HORIZONTAL;
665 add(maptype_combo, c);
667 map = new AltosMap(this);