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_13;
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_13.*;
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);
67 MapMark(double lat, double lon, int state) {
68 super(lat, lon, state);
72 class MapView extends JComponent implements MouseMotionListener, MouseListener, ComponentListener, MouseWheelListener {
74 private VolatileImage create_back_buffer() {
75 return getGraphicsConfiguration().createCompatibleVolatileImage(getWidth(), getHeight());
78 private void do_paint(Graphics my_g) {
79 g = (Graphics2D) my_g;
84 public void paint(Graphics my_g) {
85 VolatileImage back_buffer = create_back_buffer();
87 Graphics2D top_g = (Graphics2D) my_g;
90 GraphicsConfiguration gc = getGraphicsConfiguration();
91 int code = back_buffer.validate(gc);
92 if (code == VolatileImage.IMAGE_INCOMPATIBLE)
93 back_buffer = create_back_buffer();
95 Graphics g_back = back_buffer.getGraphics();
96 g_back.setClip(top_g.getClip());
100 top_g.drawImage(back_buffer, 0, 0, this);
101 } while (back_buffer.contentsLost());
105 public void repaint(AltosRectangle damage) {
106 repaint(damage.x, damage.y, damage.width, damage.height);
109 private boolean is_drag_event(MouseEvent e) {
110 return e.getModifiersEx() == InputEvent.BUTTON1_DOWN_MASK;
113 /* MouseMotionListener methods */
115 public void mouseDragged(MouseEvent e) {
116 map.touch_continue(e.getPoint().x, e.getPoint().y, is_drag_event(e));
119 String pos(double p, String pos, String neg) {
125 int deg = (int) Math.floor(p);
126 double min = (p - Math.floor(p)) * 60.0;
127 return String.format("%s %4d° %9.6f'", h, deg, min);
130 public void mouseMoved(MouseEvent e) {
131 AltosMapPathPoint point = map.nearest(e.getPoint().x, e.getPoint().y);
133 if (nearest_mark == null)
134 nearest_mark = map.add_mark(point.lat_lon.lat,
138 nearest_mark.lat_lon.lat = point.lat_lon.lat;
139 nearest_mark.lat_lon.lon = point.lat_lon.lon;
140 nearest_mark.state = point.state;
143 nearest_label.setText(String.format("Time: %9.2f Position: %s %s",
145 pos(point.lat_lon.lat,
147 pos(point.lat_lon.lon,
150 nearest_label.setText("");
155 /* MouseListener methods */
156 public void mouseClicked(MouseEvent e) {
159 public void mouseEntered(MouseEvent e) {
162 public void mouseExited(MouseEvent e) {
165 public void mousePressed(MouseEvent e) {
166 map.touch_start(e.getPoint().x, e.getPoint().y, is_drag_event(e));
169 public void mouseReleased(MouseEvent e) {
172 /* MouseWheelListener methods */
174 public void mouseWheelMoved(MouseWheelEvent e) {
175 int zoom_change = e.getWheelRotation();
177 map.set_zoom_centre(map.get_zoom() - zoom_change, new AltosPointInt(e.getPoint().x, e.getPoint().y));
180 /* ComponentListener methods */
182 public void componentHidden(ComponentEvent e) {
185 public void componentMoved(ComponentEvent e) {
188 public void componentResized(ComponentEvent e) {
192 public void componentShown(ComponentEvent e) {
197 addComponentListener(this);
198 addMouseMotionListener(this);
199 addMouseListener(this);
200 addMouseWheelListener(this);
204 class MapLine extends AltosMapLine {
206 public void paint(AltosMapTransform t) {
208 if (start == null || end == null)
211 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
213 Line2D.Double line = new Line2D.Double(point2d(t.screen(start)),
214 point2d(t.screen(end)));
216 g.setColor(Color.WHITE);
217 g.setStroke(new BasicStroke(stroke_width+4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
220 g.setColor(Color.BLUE);
221 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
224 String message = line_dist();
226 bounds = line_font.getStringBounds(message, g.getFontRenderContext());
228 float x = (float) line.x1;
229 float y = (float) line.y1 + (float) bounds.getHeight() / 2.0f;
231 if (line.x1 < line.x2) {
232 x -= (float) bounds.getWidth() + 2.0f;
237 g.setFont(line_font);
238 g.setColor(Color.WHITE);
239 for (int dy = -2; dy <= 2; dy += 2)
240 for (int dx = -2; dx <= 2; dx += 2)
241 g.drawString(message, x + dx, y + dy);
242 g.setColor(Color.BLUE);
243 g.drawString(message, x, y);
250 class MapPath extends AltosMapPath {
251 public void paint(AltosMapTransform t) {
252 Point2D.Double prev = null;
254 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
255 RenderingHints.VALUE_ANTIALIAS_ON);
256 g.setStroke(new BasicStroke(stroke_width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
258 for (AltosMapPathPoint point : points) {
259 Point2D.Double cur = point2d(t.screen(point.lat_lon));
261 Line2D.Double line = new Line2D.Double (prev, cur);
262 Rectangle bounds = line.getBounds();
264 if (g.hitClip(bounds.x, bounds.y, bounds.width, bounds.height)) {
265 if (0 <= point.state && point.state < AltosUIMap.stateColors.length)
266 g.setColor(AltosUIMap.stateColors[point.state]);
268 g.setColor(AltosUIMap.stateColors[AltosLib.ao_flight_invalid]);
278 class MapTile extends AltosMapTile {
279 public MapTile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
280 super(cache, upper_left, center, zoom, maptype, px_size, scale);
283 public void paint(AltosMapTransform t) {
285 AltosPointDouble point_double = t.screen(upper_left);
286 Point point = new Point((int) (point_double.x + 0.5),
287 (int) (point_double.y + 0.5));
289 if (!g.hitClip(point.x, point.y, px_size, px_size))
292 AltosImage altos_image = get_image();
293 AltosUIImage ui_image = (AltosUIImage) altos_image;
296 if (ui_image != null)
297 image = ui_image.image;
300 g.drawImage(image, point.x, point.y, null);
302 * Useful when debugging map fetching problems
304 String message = String.format("%.6f %.6f", center.lat, center.lon);
305 g.setFont(tile_font);
306 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
307 Rectangle2D bounds = tile_font.getStringBounds(message, g.getFontRenderContext());
309 float x = px_size / 2.0f;
310 float y = px_size / 2.0f;
311 x = x - (float) bounds.getWidth() / 2.0f;
312 y = y + (float) bounds.getHeight() / 2.0f;
313 g.setColor(Color.RED);
314 g.drawString(message, (float) point_double.x + x, (float) point_double.y + y);
317 g.setColor(Color.GRAY);
318 g.fillRect(point.x, point.y, px_size, px_size);
320 if (t.has_location()) {
321 String message = null;
323 case AltosMapTile.fetching:
324 message = "Fetching...";
326 case AltosMapTile.bad_request:
327 message = "Internal error";
329 case AltosMapTile.failed:
330 message = "Network error";
332 case AltosMapTile.forbidden:
333 message = "Outside of known launch areas";
336 if (message != null && tile_font != null) {
337 g.setFont(tile_font);
338 g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
339 Rectangle2D bounds = tile_font.getStringBounds(message, g.getFontRenderContext());
341 float x = px_size / 2.0f;
342 float y = px_size / 2.0f;
343 x = x - (float) bounds.getWidth() / 2.0f;
344 y = y + (float) bounds.getHeight() / 2.0f;
345 g.setColor(Color.BLACK);
346 g.drawString(message, (float) point_double.x + x, (float) point_double.y + y);
353 public static final Color stateColors[] = {
354 Color.WHITE, // startup
359 Color.YELLOW, // coast
360 Color.CYAN, // drogue
362 Color.BLACK, // landed
363 Color.BLACK, // invalid
364 Color.CYAN, // stateless
367 /* AltosMapInterface functions */
369 public AltosMapPath new_path() {
370 return new MapPath();
373 public AltosMapLine new_line() {
374 return new MapLine();
377 public AltosImage load_image(File file) throws Exception {
378 return new AltosUIImage(ImageIO.read(file));
381 public AltosMapMark new_mark(double lat, double lon, int state) {
382 return new MapMark(lat, lon, state);
385 public AltosMapTile new_tile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
386 return new MapTile(cache, upper_left, center, zoom, maptype, px_size, scale);
390 return view.getWidth();
393 public int height() {
394 return view.getHeight();
397 public void repaint() {
401 public void repaint(AltosRectangle damage) {
402 view.repaint(damage);
405 public void set_zoom_label(String label) {
406 zoom_label.setText(label);
409 public void select_object(AltosLatLon latlon) {
410 debug("select at %f,%f\n", latlon.lat, latlon.lon);
413 public void debug(String format, Object ... arguments) {
414 if (AltosUIPreferences.serial_debug())
415 System.out.printf(format, arguments);
419 /* AltosFlightDisplay interface */
421 public void set_font() {
422 tile_font = AltosUILib.value_font;
423 line_font = AltosUILib.status_font;
424 if (nearest_label != null)
425 nearest_label.setFont(AltosUILib.value_font);
428 public void font_size_changed(int font_size) {
433 public void units_changed(boolean imperial_units) {
439 JLabel nearest_label;
441 public void set_maptype(int type) {
443 map.set_maptype(type);
444 maptype_combo.setSelectedIndex(type);
448 /* AltosUIMapPreload functions */
450 public void set_zoom(int zoom) {
454 public void add_mark(double lat, double lon, int status) {
455 map.add_mark(lat, lon, status);
458 public void clear_marks() {
462 /* AltosFlightDisplay interface */
463 public void reset() {
467 public void show(AltosState state, AltosListenerState listener_state) {
468 map.show(state, listener_state);
471 public void show(AltosGPS gps, double time, int state) {
472 map.show(gps, time, state);
475 public String getName() {
479 /* AltosGraphUI interface */
480 public void centre(AltosState state) {
484 public void centre(AltosGPS gps) {
488 /* internal layout bits */
489 private GridBagLayout layout = new GridBagLayout();
492 JComboBox<String> maptype_combo;
497 public AltosUIMap() {
501 view = new MapView();
503 view.setPreferredSize(new Dimension(500,500));
504 view.setVisible(true);
505 view.setEnabled(true);
507 GridBagLayout my_layout = new GridBagLayout();
509 setLayout(my_layout);
511 GridBagConstraints c = new GridBagConstraints();
512 c.anchor = GridBagConstraints.CENTER;
513 c.fill = GridBagConstraints.BOTH;
524 zoom_label = new JLabel("", JLabel.CENTER);
526 c = new GridBagConstraints();
527 c.anchor = GridBagConstraints.CENTER;
528 c.fill = GridBagConstraints.HORIZONTAL;
535 JButton zoom_reset = new JButton("0");
536 zoom_reset.addActionListener(new ActionListener() {
537 public void actionPerformed(ActionEvent e) {
538 map.set_zoom(map.default_zoom);
542 c = new GridBagConstraints();
543 c.anchor = GridBagConstraints.CENTER;
544 c.fill = GridBagConstraints.HORIZONTAL;
551 JButton zoom_in = new JButton("+");
552 zoom_in.addActionListener(new ActionListener() {
553 public void actionPerformed(ActionEvent e) {
554 map.set_zoom(map.get_zoom() + 1);
558 c = new GridBagConstraints();
559 c.anchor = GridBagConstraints.CENTER;
560 c.fill = GridBagConstraints.HORIZONTAL;
567 JButton zoom_out = new JButton("-");
568 zoom_out.addActionListener(new ActionListener() {
569 public void actionPerformed(ActionEvent e) {
570 map.set_zoom(map.get_zoom() - 1);
573 c = new GridBagConstraints();
574 c.anchor = GridBagConstraints.CENTER;
575 c.fill = GridBagConstraints.HORIZONTAL;
583 nearest_label = new JLabel("", JLabel.LEFT);
584 nearest_label.setFont(tile_font);
586 c = new GridBagConstraints();
587 c.anchor = GridBagConstraints.CENTER;
588 c.fill = GridBagConstraints.HORIZONTAL;
595 add(nearest_label, c);
597 maptype_combo = new JComboBox<String>(map.maptype_labels);
599 maptype_combo.setEditable(false);
600 maptype_combo.setMaximumRowCount(maptype_combo.getItemCount());
601 maptype_combo.addItemListener(new ItemListener() {
602 public void itemStateChanged(ItemEvent e) {
603 map.set_maptype(maptype_combo.getSelectedIndex());
607 c = new GridBagConstraints();
608 c.anchor = GridBagConstraints.CENTER;
609 c.fill = GridBagConstraints.HORIZONTAL;
614 add(maptype_combo, c);
616 map = new AltosMap(this);