/* * Copyright © 2010 Anthony Towns * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. */ package org.altusmetrum.altosuilib_14; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*; import java.io.*; import java.lang.Math; import java.awt.geom.*; import java.util.*; import java.util.concurrent.*; import javax.imageio.*; import org.altusmetrum.altoslib_14.*; public class AltosUIMap extends JComponent implements AltosFlightDisplay, AltosMapInterface { AltosMap map; Graphics2D g; Font tile_font; Font line_font; AltosMapMark nearest_mark; static Point2D.Double point2d(AltosPointDouble pt) { return new Point2D.Double(pt.x, pt.y); } static final AltosPointDouble point_double(Point pt) { return new AltosPointDouble(pt.x, pt.y); } class MapMark extends AltosMapMark { public void paint(AltosMapTransform t) { double lat = lat_lon.lat; double lon; double first_lon = t.first_lon(lat_lon.lon); double last_lon = t.last_lon(lat_lon.lon); for (lon = first_lon; lon <= last_lon; lon += 360.0) { AltosPointDouble pt = t.screen(lat, lon); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); if (0 <= state && state < AltosUIMap.stateColors.length) g.setColor(AltosUIMap.stateColors[state]); else g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]); g.drawOval((int)pt.x-5, (int)pt.y-5, 10, 10); g.drawOval((int)pt.x-20, (int)pt.y-20, 40, 40); g.drawOval((int)pt.x-35, (int)pt.y-35, 70, 70); if (label != null) { Rectangle2D bounds; bounds = line_font.getStringBounds(label, g.getFontRenderContext()); float x = (float) pt.x; float y = (float) pt.y + (float) bounds.getHeight() / 2.0f; g.setFont(line_font); g.setColor(Color.WHITE); for (int dy = -2; dy <= 2; dy += 2) for (int dx = -2; dx <= 2; dx += 2) g.drawString(label, x + dx, y + dy); if (0 <= state && state < AltosUIMap.stateColors.length) g.setColor(AltosUIMap.stateColors[state]); else g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]); g.drawString(label, x, y); } } } MapMark(double lat, double lon, int state, String label) { super(lat, lon, state, label); } MapMark(double lat, double lon, int state) { super(lat, lon, state); } } class MapView extends JComponent implements MouseMotionListener, MouseListener, ComponentListener, MouseWheelListener { private VolatileImage create_back_buffer() { return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight()); } private void do_paint(Graphics my_g) { g = (Graphics2D) my_g; map.paint(); } public void paint(Graphics my_g) { VolatileImage back_buffer = create_back_buffer(); Graphics2D top_g = (Graphics2D) my_g; do { GraphicsConfiguration gc = getGraphicsConfiguration(); int code = back_buffer.validate(gc); if (code == VolatileImage.IMAGE_INCOMPATIBLE) back_buffer = create_back_buffer(); Graphics g_back = back_buffer.getGraphics(); g_back.setClip(top_g.getClip()); do_paint(g_back); g_back.dispose(); top_g.drawImage(back_buffer, 0, 0, this); } while (back_buffer.contentsLost()); back_buffer.flush(); } public void repaint(AltosRectangle damage) { repaint(damage.x, damage.y, damage.width, damage.height); } private boolean is_drag_event(MouseEvent e) { return e.getModifiersEx() == InputEvent.BUTTON1_DOWN_MASK; } /* MouseMotionListener methods */ public void mouseDragged(MouseEvent e) { map.touch_continue(e.getPoint().x, e.getPoint().y, is_drag_event(e)); } String pos(double p, String pos, String neg) { if (p == AltosLib.MISSING) return ""; String h = pos; if (p < 0) { h = neg; p = -p; } int deg = (int) Math.floor(p); double min = (p - Math.floor(p)) * 60.0; return String.format("%s %4d° %9.6f'", h, deg, min); } String height(double h, String label) { if (h == AltosLib.MISSING) return ""; return String.format(" %s%s", AltosConvert.height.show(6, h), label); } String speed(double s, String label) { if (s == AltosLib.MISSING) return ""; return String.format(" %s%s", AltosConvert.speed.show(6, s), label); } public void mouseMoved(MouseEvent e) { AltosMapPathPoint point = map.nearest(e.getPoint().x, e.getPoint().y); if (point != null) { if (nearest_mark == null) nearest_mark = map.add_mark(point.gps.lat, point.gps.lon, point.state); else { nearest_mark.lat_lon.lat = point.gps.lat; nearest_mark.lat_lon.lon = point.gps.lon; nearest_mark.state = point.state; } nearest_label.setText(String.format("%9.2f sec %s%s%s%s", point.time, pos(point.gps.lat, "N", "S"), pos(point.gps.lon, "E", "W"), height(point.gps_height, ""), speed(point.gps.ground_speed, "(h)"), speed(point.gps.climb_rate, "(v)"))); } else { nearest_label.setText(""); } repaint(); } /* MouseListener methods */ public void mouseClicked(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { map.touch_start(e.getPoint().x, e.getPoint().y, is_drag_event(e)); } public void mouseReleased(MouseEvent e) { } /* MouseWheelListener methods */ public void mouseWheelMoved(MouseWheelEvent e) { int zoom_change = e.getWheelRotation(); map.set_zoom_centre(map.get_zoom() - zoom_change, new AltosPointInt(e.getPoint().x, e.getPoint().y)); } /* ComponentListener methods */ public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { } public void componentResized(ComponentEvent e) { map.set_transform(); } public void componentShown(ComponentEvent e) { map.set_transform(); } MapView() { addComponentListener(this); addMouseMotionListener(this); addMouseListener(this); addMouseWheelListener(this); } } class MapLine extends AltosMapLine { public void paint(AltosMapTransform t) { if (start == null || end == null) return; g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); Line2D.Double line = new Line2D.Double(point2d(t.screen(start)), point2d(t.screen(end))); g.setColor(Color.WHITE); g.setStroke(new BasicStroke(stroke_width+4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.draw(line); g.setColor(Color.BLUE); g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g.draw(line); String message = line_dist(); Rectangle2D bounds; bounds = line_font.getStringBounds(message, g.getFontRenderContext()); float x = (float) line.x1; float y = (float) line.y1 + (float) bounds.getHeight() / 2.0f; if (line.x1 < line.x2) { x -= (float) bounds.getWidth() + 2.0f; } else { x += 2.0f; } g.setFont(line_font); g.setColor(Color.WHITE); for (int dy = -2; dy <= 2; dy += 2) for (int dx = -2; dx <= 2; dx += 2) g.drawString(message, x + dx, y + dy); g.setColor(Color.BLUE); g.drawString(message, x, y); } public MapLine() { } } class MapPath extends AltosMapPath { public void paint(AltosMapTransform t) { Point2D.Double prev = null; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); for (AltosMapPathPoint point : points) { Point2D.Double cur = point2d(t.screen(point.gps.lat, point.gps.lon)); if (prev != null) { Line2D.Double line = new Line2D.Double (prev, cur); Rectangle bounds = line.getBounds(); if (g.hitClip(bounds.x, bounds.y, bounds.width, bounds.height)) { if (0 <= point.state && point.state < AltosUIMap.stateColors.length) g.setColor(AltosUIMap.stateColors[point.state]); else g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]); g.draw(line); } } prev = cur; } } } class MapTile extends AltosMapTile { public MapTile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) { super(cache, upper_left, center, zoom, maptype, px_size, scale); } public void paint(AltosMapTransform t) { AltosPointDouble point_double = t.screen(upper_left); Point point = new Point((int) (point_double.x + 0.5), (int) (point_double.y + 0.5)); if (!g.hitClip(point.x, point.y, px_size, px_size)) return; AltosImage altos_image = get_image(); AltosUIImage ui_image = (AltosUIImage) altos_image; Image image = null; if (ui_image != null) image = ui_image.image; if (image != null) { g.drawImage(image, point.x, point.y, null); /* * Useful when debugging map fetching problems * String message = String.format("%.6f %.6f", center.lat, center.lon); g.setFont(tile_font); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); Rectangle2D bounds = tile_font.getStringBounds(message, g.getFontRenderContext()); float x = px_size / 2.0f; float y = px_size / 2.0f; x = x - (float) bounds.getWidth() / 2.0f; y = y + (float) bounds.getHeight() / 2.0f; g.setColor(Color.RED); g.drawString(message, (float) point_double.x + x, (float) point_double.y + y); */ } else { g.setColor(Color.GRAY); g.fillRect(point.x, point.y, px_size, px_size); if (t.has_location()) { String message = null; switch (status) { case AltosMapTile.fetching: message = "Fetching..."; break; case AltosMapTile.bad_request: message = "Internal error"; break; case AltosMapTile.failed: message = "Network error"; break; case AltosMapTile.forbidden: message = "Outside of known launch areas"; break; } if (message != null && tile_font != null) { g.setFont(tile_font); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); Rectangle2D bounds = tile_font.getStringBounds(message, g.getFontRenderContext()); float x = px_size / 2.0f; float y = px_size / 2.0f; x = x - (float) bounds.getWidth() / 2.0f; y = y + (float) bounds.getHeight() / 2.0f; g.setColor(Color.BLACK); g.drawString(message, (float) point_double.x + x, (float) point_double.y + y); } } } } } public static final Color stateColors[] = { Color.WHITE, // startup Color.WHITE, // idle Color.WHITE, // pad Color.RED, // boost Color.PINK, // fast Color.YELLOW, // coast Color.CYAN, // drogue Color.BLUE, // main Color.BLACK, // landed Color.BLACK, // invalid Color.CYAN, // stateless }; /* AltosMapInterface functions */ public AltosMapPath new_path() { return new MapPath(); } public AltosMapLine new_line() { return new MapLine(); } public AltosImage load_image(File file) throws Exception { return new AltosUIImage(ImageIO.read(file)); } public AltosMapMark new_mark(double lat, double lon, int state) { return new MapMark(lat, lon, state); } public AltosMapMark new_mark(double lat, double lon, int state, String label) { return new MapMark(lat, lon, state, label); } public AltosMapTile new_tile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) { return new MapTile(cache, upper_left, center, zoom, maptype, px_size, scale); } public int width() { return view.getWidth(); } public int height() { return view.getHeight(); } public void repaint() { view.repaint(); } public void repaint(AltosRectangle damage) { view.repaint(damage); } public void set_zoom_label(String label) { zoom_label.setText(label); } public void select_object(AltosLatLon latlon) { debug("select at %f,%f\n", latlon.lat, latlon.lon); } public void debug(String format, Object ... arguments) { if (AltosUIPreferences.serial_debug()) System.out.printf(format, arguments); } /* AltosFlightDisplay interface */ public void set_font() { tile_font = AltosUILib.value_font; line_font = AltosUILib.status_font; if (nearest_label != null) nearest_label.setFont(AltosUILib.value_font); } public void font_size_changed(int font_size) { set_font(); repaint(); } public void units_changed(boolean imperial_units) { repaint(); } JLabel zoom_label; JLabel nearest_label; public void set_maptype(int type) { /* map.set_maptype(type); maptype_combo.setSelectedIndex(type); */ } /* AltosUIMapPreload functions */ public void set_zoom(int zoom) { map.set_zoom(zoom); } public void add_mark(double lat, double lon, int status) { map.add_mark(lat, lon, status); } public void add_mark(double lat, double lon, int status, String label) { map.add_mark(lat, lon, status, label); } public void clear_marks() { map.clear_marks(); } /* AltosFlightDisplay interface */ public void reset() { // nothing } public void show(AltosState state, AltosListenerState listener_state) { map.show(state, listener_state); } public void show(AltosGPS gps, double time, int state, double gps_height) { map.show(gps, time, state, gps_height); } public String getName() { return "Map"; } /* AltosGraphUI interface */ public void centre(AltosState state) { map.centre(state); } public void centre(AltosGPS gps) { map.centre(gps); } /* internal layout bits */ private GridBagLayout layout = new GridBagLayout(); /* JComboBox maptype_combo; */ MapView view; public AltosUIMap() { set_font(); view = new MapView(); view.setPreferredSize(new Dimension(500,500)); view.setVisible(true); view.setEnabled(true); GridBagLayout my_layout = new GridBagLayout(); setLayout(my_layout); GridBagConstraints c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.BOTH; c.gridx = 0; c.gridy = 0; c.gridwidth = 1; c.gridheight = 10; c.weightx = 1; c.weighty = 1; add(view, c); int y = 0; zoom_label = new JLabel("", JLabel.CENTER); c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 1; c.gridy = y++; c.weightx = 0; c.weighty = 0; add(zoom_label, c); JButton zoom_reset = new JButton("0"); zoom_reset.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { map.set_zoom(map.default_zoom); } }); c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 1; c.gridy = y++; c.weightx = 0; c.weighty = 0; add(zoom_reset, c); JButton zoom_in = new JButton("+"); zoom_in.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { map.set_zoom(map.get_zoom() + 1); } }); c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 1; c.gridy = y++; c.weightx = 0; c.weighty = 0; add(zoom_in, c); JButton zoom_out = new JButton("-"); zoom_out.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { map.set_zoom(map.get_zoom() - 1); } }); c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 1; c.gridy = y++; c.weightx = 0; c.weighty = 0; add(zoom_out, c); nearest_label = new JLabel("", JLabel.LEFT); nearest_label.setFont(tile_font); c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 0; c.gridy = 11; c.weightx = 0; c.weighty = 0; c.gridwidth = 1; c.gridheight = 1; add(nearest_label, c); /* maptype_combo = new JComboBox(map.maptype_labels); maptype_combo.setEditable(false); maptype_combo.setMaximumRowCount(maptype_combo.getItemCount()); maptype_combo.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { map.set_maptype(maptype_combo.getSelectedIndex()); } }); c = new GridBagConstraints(); c.anchor = GridBagConstraints.CENTER; c.fill = GridBagConstraints.HORIZONTAL; c.gridx = 1; c.gridy = y++; c.weightx = 0; c.weighty = 0; add(maptype_combo, c); */ map = new AltosMap(this); } }