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 double lat = lat_lon.lat;
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);
58 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
59 RenderingHints.VALUE_ANTIALIAS_ON);
60 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
62 if (0 <= state && state < AltosUIMap.stateColors.length)
63 g.setColor(AltosUIMap.stateColors[state]);
65 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
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);
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;
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]);
85 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
86 g.drawString(label, x, y);
91 MapMark(double lat, double lon, int state, String label) {
92 super(lat, lon, state, label);
95 MapMark(double lat, double lon, int state) {
96 super(lat, lon, state);
100 class MapView extends JComponent implements MouseMotionListener, MouseListener, ComponentListener, MouseWheelListener {
102 private VolatileImage create_back_buffer() {
103 return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
106 private void do_paint(Graphics my_g) {
107 g = (Graphics2D) my_g;
112 public void paint(Graphics my_g) {
113 VolatileImage back_buffer = create_back_buffer();
115 Graphics2D top_g = (Graphics2D) my_g;
118 GraphicsConfiguration gc = getGraphicsConfiguration();
119 int code = back_buffer.validate(gc);
120 if (code == VolatileImage.IMAGE_INCOMPATIBLE)
121 back_buffer = create_back_buffer();
123 Graphics g_back = back_buffer.getGraphics();
124 g_back.setClip(top_g.getClip());
128 top_g.drawImage(back_buffer, 0, 0, this);
129 } while (back_buffer.contentsLost());
133 public void repaint(AltosRectangle damage) {
134 repaint(damage.x, damage.y, damage.width, damage.height);
137 private boolean is_drag_event(MouseEvent e) {
138 return e.getModifiersEx() == InputEvent.BUTTON1_DOWN_MASK;
141 /* MouseMotionListener methods */
143 public void mouseDragged(MouseEvent e) {
144 map.touch_continue(e.getPoint().x, e.getPoint().y, is_drag_event(e));
147 String pos(double p, String pos, String neg) {
148 if (p == AltosLib.MISSING)
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);
160 String height(double h, String label) {
161 if (h == AltosLib.MISSING)
163 return String.format(" %s%s",
164 AltosConvert.height.show(6, h),
168 String speed(double s, String label) {
169 if (s == AltosLib.MISSING)
171 return String.format(" %s%s",
172 AltosConvert.speed.show(6, s),
176 public void mouseMoved(MouseEvent e) {
177 AltosMapPathPoint point = map.nearest(e.getPoint().x, e.getPoint().y);
180 if (nearest_mark == null)
181 nearest_mark = map.add_mark(point.gps.lat,
185 nearest_mark.lat_lon.lat = point.gps.lat;
186 nearest_mark.lat_lon.lon = point.gps.lon;
187 nearest_mark.state = point.state;
189 nearest_label.setText(String.format("%9.2f sec %s%s%s%s",
195 height(point.gps_height, ""),
196 speed(point.gps.ground_speed, "(h)"),
197 speed(point.gps.climb_rate, "(v)")));
199 nearest_label.setText("");
204 /* MouseListener methods */
205 public void mouseClicked(MouseEvent e) {
208 public void mouseEntered(MouseEvent e) {
211 public void mouseExited(MouseEvent e) {
214 public void mousePressed(MouseEvent e) {
215 map.touch_start(e.getPoint().x, e.getPoint().y, is_drag_event(e));
218 public void mouseReleased(MouseEvent e) {
221 /* MouseWheelListener methods */
223 public void mouseWheelMoved(MouseWheelEvent e) {
224 int zoom_change = e.getWheelRotation();
226 map.set_zoom_centre(map.get_zoom() - zoom_change, new AltosPointInt(e.getPoint().x, e.getPoint().y));
229 /* ComponentListener methods */
231 public void componentHidden(ComponentEvent e) {
234 public void componentMoved(ComponentEvent e) {
237 public void componentResized(ComponentEvent e) {
241 public void componentShown(ComponentEvent e) {
246 addComponentListener(this);
247 addMouseMotionListener(this);
248 addMouseListener(this);
249 addMouseWheelListener(this);
253 class MapLine extends AltosMapLine {
255 public void paint(AltosMapTransform t) {
257 if (start == null || end == null)
260 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
262 Line2D.Double line = new Line2D.Double(point2d(t.screen(start)),
263 point2d(t.screen(end)));
265 g.setColor(Color.WHITE);
266 g.setStroke(new BasicStroke(stroke_width+4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
269 g.setColor(Color.BLUE);
270 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
273 String message = line_dist();
275 bounds = line_font.getStringBounds(message, g.getFontRenderContext());
277 float x = (float) line.x1;
278 float y = (float) line.y1 + (float) bounds.getHeight() / 2.0f;
280 if (line.x1 < line.x2) {
281 x -= (float) bounds.getWidth() + 2.0f;
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);
299 class MapPath extends AltosMapPath {
300 public void paint(AltosMapTransform t) {
301 Point2D.Double prev = null;
303 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
304 RenderingHints.VALUE_ANTIALIAS_ON);
305 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
307 for (AltosMapPathPoint point : points) {
308 Point2D.Double cur = point2d(t.screen(point.gps.lat, point.gps.lon));
310 Line2D.Double line = new Line2D.Double (prev, cur);
311 Rectangle bounds = line.getBounds();
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]);
317 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
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);
332 public void paint(AltosMapTransform t) {
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));
338 if (!g.hitClip(point.x, point.y, px_size, px_size))
341 AltosImage altos_image = get_image();
342 AltosUIImage ui_image = (AltosUIImage) altos_image;
345 if (ui_image != null)
346 image = ui_image.image;
349 g.drawImage(image, point.x, point.y, null);
351 * Useful when debugging map fetching problems
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());
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);
366 g.setColor(Color.GRAY);
367 g.fillRect(point.x, point.y, px_size, px_size);
369 if (t.has_location()) {
370 String message = null;
372 case AltosMapTile.fetching:
373 message = "Fetching...";
375 case AltosMapTile.bad_request:
376 message = "Internal error";
378 case AltosMapTile.failed:
379 message = "Network error";
381 case AltosMapTile.forbidden:
382 message = "Outside of known launch areas";
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());
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);
402 public static final Color stateColors[] = {
403 Color.WHITE, // startup
408 Color.YELLOW, // coast
409 Color.CYAN, // drogue
411 Color.BLACK, // landed
412 Color.BLACK, // invalid
413 Color.CYAN, // stateless
416 /* AltosMapInterface functions */
418 public AltosMapPath new_path() {
419 return new MapPath();
422 public AltosMapLine new_line() {
423 return new MapLine();
426 public AltosImage load_image(File file) throws Exception {
427 return new AltosUIImage(ImageIO.read(file));
430 public AltosMapMark new_mark(double lat, double lon, int state) {
431 return new MapMark(lat, lon, state);
434 public AltosMapMark new_mark(double lat, double lon, int state, String label) {
435 return new MapMark(lat, lon, state, label);
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);
443 return view.getWidth();
446 public int height() {
447 return view.getHeight();
450 public void repaint() {
454 public void repaint(AltosRectangle damage) {
455 view.repaint(damage);
458 public void set_zoom_label(String label) {
459 zoom_label.setText(label);
462 public void select_object(AltosLatLon latlon) {
463 debug("select at %f,%f\n", latlon.lat, latlon.lon);
466 public void debug(String format, Object ... arguments) {
467 if (AltosUIPreferences.serial_debug())
468 System.out.printf(format, arguments);
472 /* AltosFlightDisplay interface */
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);
481 public void font_size_changed(int font_size) {
486 public void units_changed(boolean imperial_units) {
492 JLabel nearest_label;
494 public void set_maptype(int type) {
496 map.set_maptype(type);
497 maptype_combo.setSelectedIndex(type);
501 /* AltosUIMapPreload functions */
503 public void set_zoom(int zoom) {
507 public void add_mark(double lat, double lon, int status) {
508 map.add_mark(lat, lon, status);
511 public void add_mark(double lat, double lon, int status, String label) {
512 map.add_mark(lat, lon, status, label);
515 public void clear_marks() {
519 /* AltosFlightDisplay interface */
520 public void reset() {
524 public void show(AltosState state, AltosListenerState listener_state) {
525 map.show(state, listener_state);
528 public void show(AltosGPS gps, double time, int state, double gps_height) {
529 map.show(gps, time, state, gps_height);
532 public String getName() {
536 /* AltosGraphUI interface */
537 public void centre(AltosState state) {
541 public void centre(AltosGPS gps) {
545 /* internal layout bits */
546 private GridBagLayout layout = new GridBagLayout();
549 JComboBox<String> maptype_combo;
554 public AltosUIMap() {
558 view = new MapView();
560 view.setPreferredSize(new Dimension(500,500));
561 view.setVisible(true);
562 view.setEnabled(true);
564 GridBagLayout my_layout = new GridBagLayout();
566 setLayout(my_layout);
568 GridBagConstraints c = new GridBagConstraints();
569 c.anchor = GridBagConstraints.CENTER;
570 c.fill = GridBagConstraints.BOTH;
581 zoom_label = new JLabel("", JLabel.CENTER);
583 c = new GridBagConstraints();
584 c.anchor = GridBagConstraints.CENTER;
585 c.fill = GridBagConstraints.HORIZONTAL;
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);
599 c = new GridBagConstraints();
600 c.anchor = GridBagConstraints.CENTER;
601 c.fill = GridBagConstraints.HORIZONTAL;
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);
615 c = new GridBagConstraints();
616 c.anchor = GridBagConstraints.CENTER;
617 c.fill = GridBagConstraints.HORIZONTAL;
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);
630 c = new GridBagConstraints();
631 c.anchor = GridBagConstraints.CENTER;
632 c.fill = GridBagConstraints.HORIZONTAL;
640 nearest_label = new JLabel("", JLabel.LEFT);
641 nearest_label.setFont(tile_font);
643 c = new GridBagConstraints();
644 c.anchor = GridBagConstraints.CENTER;
645 c.fill = GridBagConstraints.HORIZONTAL;
652 add(nearest_label, c);
654 maptype_combo = new JComboBox<String>(map.maptype_labels);
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());
664 c = new GridBagConstraints();
665 c.anchor = GridBagConstraints.CENTER;
666 c.fill = GridBagConstraints.HORIZONTAL;
671 add(maptype_combo, c);
673 map = new AltosMap(this);