4 import java.awt.Dimension;
5 import java.awt.FontMetrics;
6 import java.awt.Graphics;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.KeyEvent;
10 import java.awt.event.KeyListener;
11 import java.awt.event.MouseEvent;
12 import java.awt.event.MouseMotionListener;
13 import java.awt.event.MouseWheelEvent;
14 import java.awt.event.MouseWheelListener;
15 import java.awt.image.BufferedImage;
17 import javax.swing.JCheckBoxMenuItem;
18 import javax.swing.JMenuItem;
19 import javax.swing.JPopupMenu;
22 import tim.prune.DataSubscriber;
23 import tim.prune.I18nManager;
24 import tim.prune.data.DataPoint;
25 import tim.prune.data.TrackInfo;
29 * Display component for the main map
31 public class MapChart extends GenericChart implements MouseWheelListener, KeyListener, MouseMotionListener
34 private static final int POINT_RADIUS = 4;
35 private static final int CLICK_SENSITIVITY = 10;
36 private static final double ZOOM_SCALE_FACTOR = 1.2;
37 private static final int PAN_DISTANCE = 10;
38 private static final int LIMIT_WAYPOINT_NAMES = 40;
41 private static final Color COLOR_BG = Color.WHITE;
42 private static final Color COLOR_POINT = Color.BLUE;
43 private static final Color COLOR_CURR_RANGE = Color.GREEN;
44 private static final Color COLOR_CROSSHAIRS = Color.RED;
45 private static final Color COLOR_WAYPT_NAME = Color.BLACK;
48 private App _app = null;
49 private BufferedImage _image = null;
50 private JPopupMenu _popup = null;
51 private JCheckBoxMenuItem _autoPanMenuItem = null;
52 private int _numPoints = -1;
53 private double _scale;
54 private double _offsetX, _offsetY, _zoomScale;
55 private int _lastSelectedPoint = -1;
56 private int _dragStartX = -1, _dragStartY = -1;
57 private int _zoomDragFromX = -1, _zoomDragFromY = -1;
58 private int _zoomDragToX = -1, _zoomDragToY = -1;
59 private boolean _zoomDragging = false;
64 * @param inApp App object for callbacks
65 * @param inTrackInfo track info object
67 public MapChart(App inApp, TrackInfo inTrackInfo)
72 addMouseListener(this);
73 addMouseWheelListener(this);
74 addMouseMotionListener(this);
77 MINIMUM_SIZE = new Dimension(200, 250);
83 * Override track updating to refresh image
85 public void dataUpdated(byte inUpdateType)
87 // Check if number of points has changed or data has been edited
88 if (_track.getNumPoints() != _numPoints || (inUpdateType & DATA_EDITED) > 0)
91 _lastSelectedPoint = -1;
92 _numPoints = _track.getNumPoints();
94 super.dataUpdated(inUpdateType);
99 * Override paint method to draw map
101 public void paint(Graphics g)
109 int width = getWidth();
110 int height = getHeight();
113 // Find x and y ranges, and scale to fit
114 double scaleX = (_track.getXRange().getMaximum() - _track.getXRange().getMinimum())
115 / (width - 2 * (BORDER_WIDTH + POINT_RADIUS));
116 double scaleY = (_track.getYRange().getMaximum() - _track.getYRange().getMinimum())
117 / (height - 2 * (BORDER_WIDTH + POINT_RADIUS));
119 if (scaleY > _scale) _scale = scaleY;
121 // Autopan if necessary
122 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
123 if (_autoPanMenuItem.isSelected() && selectedPoint >= 0 && selectedPoint != _lastSelectedPoint)
125 // Autopan is enabled and a point is selected - work out x and y to see if it's within range
126 x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
127 y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
128 if (x <= BORDER_WIDTH)
131 _offsetX -= (width / 4 - x) * _scale / _zoomScale;
134 else if (x >= (width - BORDER_WIDTH))
137 _offsetX += (x - width * 3/4) * _scale / _zoomScale;
140 if (y <= BORDER_WIDTH)
143 _offsetY += (height / 4 - y) * _scale / _zoomScale;
146 else if (y >= (height - BORDER_WIDTH))
149 _offsetY -= (y - height * 3/4) * _scale / _zoomScale;
153 _lastSelectedPoint = selectedPoint;
155 // Create background if necessary
156 if (_image == null || width != _image.getWidth() || height != _image.getHeight())
158 createBackgroundImage();
160 // return if image has been set to null by other thread
161 if (_image == null) {return;}
163 // draw buffered image onto g
164 g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
166 // draw selected range, if any
167 if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
169 int rangeStart = _trackInfo.getSelection().getStart();
170 int rangeEnd = _trackInfo.getSelection().getEnd();
171 g.setColor(COLOR_CURR_RANGE);
172 for (int i=rangeStart; i<=rangeEnd; i++)
174 x = width/2 + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
175 y = height/2 - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
176 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
177 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
179 g.drawOval(x - 2, y - 2, 4, 4);
184 // Highlight selected point
185 if (selectedPoint >= 0 && !_zoomDragging)
187 g.setColor(COLOR_CROSSHAIRS);
188 x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
189 y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
190 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
191 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
193 // Draw cross-hairs for current point
194 g.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
195 g.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
197 // Show selected point afterwards to make sure it's on top
198 g.drawOval(x - 2, y - 2, 4, 4);
199 g.drawOval(x - 3, y - 3, 6, 6);
203 // Draw rectangle for dragging zoom area
206 g.setColor(COLOR_CROSSHAIRS);
207 g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
208 g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
209 g.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
210 g.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
213 // Attempt to grab keyboard focus if possible
214 //this.requestFocus();
219 * Draw the map onto an offscreen image
221 private void createBackgroundImage()
223 int width = getWidth();
224 int height = getHeight();
226 // Make a new image and initialise it
227 _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
228 Graphics bufferedG = _image.getGraphics();
229 super.paint(bufferedG);
231 // Loop and show all points
232 int numPoints = _track.getNumPoints();
233 bufferedG.setColor(COLOR_POINT);
234 int halfWidth = width/2;
235 int halfHeight = height/2;
236 for (int i=0; i<numPoints; i++)
238 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
239 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
240 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
241 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
243 bufferedG.drawOval(x - 2, y - 2, 4, 4);
247 // Loop again and show waypoints with names
248 bufferedG.setColor(COLOR_WAYPT_NAME);
249 FontMetrics fm = bufferedG.getFontMetrics();
250 int nameHeight = fm.getHeight();
251 int numWaypointNamesShown = 0;
252 for (int i=0; i<numPoints; i++)
254 DataPoint point = _track.getPoint(i);
255 String waypointName = point.getWaypointName();
256 if (waypointName != null && !waypointName.equals(""))
258 // escape if nothing more to do
259 if (numWaypointNamesShown >= LIMIT_WAYPOINT_NAMES || _image == null) {break;}
260 // calculate coordinates of point
261 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
262 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
263 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
264 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
266 bufferedG.fillOval(x - 3, y - 3, 6, 6);
267 // Figure out where to draw name so it doesn't obscure track
268 int nameWidth = fm.stringWidth(waypointName);
269 if (nameWidth < (width - 2 * BORDER_WIDTH))
271 double nameAngle = 0.3;
272 double nameRadius = 1.0;
273 boolean drawnName = false;
276 int nameX = x + (int) (nameRadius * Math.cos(nameAngle)) - (nameWidth/2);
277 int nameY = y + (int) (nameRadius * Math.sin(nameAngle)) + (nameHeight/2);
278 if (nameX > BORDER_WIDTH && (nameX + nameWidth) < (width - BORDER_WIDTH)
279 && nameY < (height - BORDER_WIDTH) && (nameY - nameHeight) > BORDER_WIDTH)
281 // name can fit in grid - does it overlap data points?
282 if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
284 bufferedG.drawString(waypointName, nameX, nameY);
286 numWaypointNamesShown++;
291 // wasn't room within the radius, so don't print name
292 if (nameRadius > 50.0)
305 * Tests whether there are any data points within the specified x,y rectangle
306 * @param inX left X coordinate
307 * @param inY bottom Y coordinate
308 * @param inWidth width of rectangle
309 * @param inHeight height of rectangle
310 * @return true if there's at least one data point in the rectangle
312 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
316 // loop over x coordinate of rectangle
317 for (int x=0; x<inWidth; x++)
319 // loop over y coordinate of rectangle
320 for (int y=0; y<inHeight; y++)
322 int pixelColor = _image.getRGB(inX + x, inY - y);
323 if (pixelColor != -1) return true;
327 catch (NullPointerException e) {
328 // ignore null pointers, just return false
335 * Make the popup menu for right-clicking the map
337 private void makePopup()
339 _popup = new JPopupMenu();
340 JMenuItem zoomIn = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
341 zoomIn.addActionListener(new ActionListener() {
342 public void actionPerformed(ActionEvent e)
346 zoomIn.setEnabled(true);
348 JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
349 zoomOut.addActionListener(new ActionListener() {
350 public void actionPerformed(ActionEvent e)
354 zoomOut.setEnabled(true);
356 JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
357 zoomFull.addActionListener(new ActionListener() {
358 public void actionPerformed(ActionEvent e)
362 zoomFull.setEnabled(true);
363 _popup.add(zoomFull);
364 _autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
365 _autoPanMenuItem.setSelected(true);
366 _popup.add(_autoPanMenuItem);
371 * Zoom map to full scale
373 private void zoomToFullScale()
379 dataUpdated(DataSubscriber.ALL);
384 * Zoom map either in or out by one step
385 * @param inZoomIn true to zoom in, false for out
387 private void zoomMap(boolean inZoomIn)
392 _zoomScale *= ZOOM_SCALE_FACTOR;
397 _zoomScale /= ZOOM_SCALE_FACTOR;
398 if (_zoomScale < 0.5) _zoomScale = 0.5;
401 dataUpdated(DataSubscriber.ALL);
406 * Pan the map by the specified amounts
407 * @param inUp upwards pan
408 * @param inRight rightwards pan
410 private void panMap(int inUp, int inRight)
412 double panFactor = _scale / _zoomScale;
413 _offsetY = _offsetY + (inUp * panFactor);
414 _offsetX = _offsetX - (inRight * panFactor);
415 // Limit pan to sensible range??
423 * React to click on map display
425 public void mouseClicked(MouseEvent e)
430 int xClick = e.getX();
431 int yClick = e.getY();
432 // Check click is within main area (not in border)
433 if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
434 && yClick < (getHeight() - BORDER_WIDTH))
436 // Check left click or right click
439 // Only show popup if track has data
440 if (_track != null && _track.getNumPoints() > 0)
441 _popup.show(this, e.getX(), e.getY());
445 // Find point within range of click point
446 double pointX = (xClick - getWidth()/2) * _scale / _zoomScale + _offsetX;
447 double pointY = (getHeight()/2 - yClick) * _scale / _zoomScale + _offsetY;
448 int selectedPointIndex = _track.getNearestPointIndex(
449 pointX, pointY, CLICK_SENSITIVITY * _scale, false);
450 // Select the given point (or deselect if no point was found)
451 _trackInfo.getSelection().selectPoint(selectedPointIndex);
459 * Respond to mouse released to reset dragging
460 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
462 public void mouseReleased(MouseEvent e)
464 _dragStartX = _dragStartY = -1;
467 if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0)
469 // zoom area marked out - calculate offset and zoom
470 int xPan = (getWidth() - _zoomDragFromX - e.getX()) / 2;
471 int yPan = (getHeight() - _zoomDragFromY - e.getY()) / 2;
472 double xZoom = Math.abs(getWidth() * 1.0 / (e.getX() - _zoomDragFromX));
473 double yZoom = Math.abs(getHeight() * 1.0 / (e.getY() - _zoomDragFromY));
474 double extraZoom = (xZoom>yZoom?yZoom:xZoom);
475 // deselect point if selected (to stop autopan)
476 _trackInfo.getSelection().selectPoint(-1);
477 // Pan first to ensure pan occurs with correct scale
479 // Then zoom in and request repaint
480 _zoomScale = _zoomScale * extraZoom;
484 _zoomDragFromX = _zoomDragFromY = -1;
485 _zoomDragging = false;
491 * Respond to mouse wheel events to zoom the map
492 * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
494 public void mouseWheelMoved(MouseWheelEvent e)
496 zoomMap(e.getWheelRotation() < 0);
501 * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
503 public void keyPressed(KeyEvent e)
505 int code = e.getKeyCode();
506 // Check for meta key
507 if (e.isControlDown())
509 // Check for arrow keys to zoom in and out
510 if (code == KeyEvent.VK_UP)
512 else if (code == KeyEvent.VK_DOWN)
514 // Key nav for next/prev point
515 else if (code == KeyEvent.VK_LEFT)
516 _trackInfo.getSelection().selectPreviousPoint();
517 else if (code == KeyEvent.VK_RIGHT)
518 _trackInfo.getSelection().selectNextPoint();
522 // Check for arrow keys to pan
524 if (code == KeyEvent.VK_UP)
525 upwardsPan = PAN_DISTANCE;
526 else if (code == KeyEvent.VK_DOWN)
527 upwardsPan = -PAN_DISTANCE;
528 int rightwardsPan = 0;
529 if (code == KeyEvent.VK_RIGHT)
530 rightwardsPan = -PAN_DISTANCE;
531 else if (code == KeyEvent.VK_LEFT)
532 rightwardsPan = PAN_DISTANCE;
533 panMap(upwardsPan, rightwardsPan);
534 // Check for delete key to delete current point
535 if (code == KeyEvent.VK_DELETE && _trackInfo.getSelection().getCurrentPointIndex() >= 0)
537 _app.deleteCurrentPoint();
538 // reset last selected point to trigger autopan
539 _lastSelectedPoint = -1;
546 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
548 public void keyReleased(KeyEvent e)
555 * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
557 public void keyTyped(KeyEvent e)
564 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
566 public void mouseDragged(MouseEvent e)
572 int xShift = e.getX() - _dragStartX;
573 int yShift = e.getY() - _dragStartY;
574 panMap(yShift, xShift);
576 _dragStartX = e.getX();
577 _dragStartY = e.getY();
581 // Right click-and-drag for zoom
582 if (_zoomDragFromX < 0 || _zoomDragFromY < 0)
584 _zoomDragFromX = e.getX();
585 _zoomDragFromY = e.getY();
589 _zoomDragToX = e.getX();
590 _zoomDragToY = e.getY();
591 _zoomDragging = true;
599 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
601 public void mouseMoved(MouseEvent e)