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 String _trackString = null;
53 private int _numPoints = -1;
54 private double _scale;
55 private double _offsetX, _offsetY, _zoomScale;
56 private int _lastSelectedPoint = -1;
57 private int _dragStartX = -1, _dragStartY = -1;
58 private int _zoomDragFromX = -1, _zoomDragFromY = -1;
59 private int _zoomDragToX = -1, _zoomDragToY = -1;
60 private boolean _zoomDragging = false;
65 * @param inApp App object for callbacks
66 * @param inTrackInfo track info object
68 public MapChart(App inApp, TrackInfo inTrackInfo)
73 addMouseListener(this);
74 addMouseWheelListener(this);
75 addMouseMotionListener(this);
78 MINIMUM_SIZE = new Dimension(200, 250);
84 * Override track updating to refresh image
86 public void dataUpdated(byte inUpdateType)
88 // Check if number of points has changed or data has been edited
89 if (_track.getNumPoints() != _numPoints || (inUpdateType & DATA_EDITED) > 0)
92 _lastSelectedPoint = -1;
93 _numPoints = _track.getNumPoints();
95 super.dataUpdated(inUpdateType);
100 * Override paint method to draw map
102 public void paint(Graphics g)
110 int width = getWidth();
111 int height = getHeight();
114 // Find x and y ranges, and scale to fit
115 double scaleX = (_track.getXRange().getMaximum() - _track.getXRange().getMinimum())
116 / (width - 2 * (BORDER_WIDTH + POINT_RADIUS));
117 double scaleY = (_track.getYRange().getMaximum() - _track.getYRange().getMinimum())
118 / (height - 2 * (BORDER_WIDTH + POINT_RADIUS));
120 if (scaleY > _scale) _scale = scaleY;
122 // Autopan if necessary
123 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
124 if (_autoPanMenuItem.isSelected() && selectedPoint >= 0 && selectedPoint != _lastSelectedPoint)
126 // Autopan is enabled and a point is selected - work out x and y to see if it's within range
127 x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
128 y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
129 if (x <= BORDER_WIDTH)
132 _offsetX -= (width / 4 - x) * _scale / _zoomScale;
135 else if (x >= (width - BORDER_WIDTH))
138 _offsetX += (x - width * 3/4) * _scale / _zoomScale;
141 if (y <= BORDER_WIDTH)
144 _offsetY += (height / 4 - y) * _scale / _zoomScale;
147 else if (y >= (height - BORDER_WIDTH))
150 _offsetY -= (y - height * 3/4) * _scale / _zoomScale;
154 _lastSelectedPoint = selectedPoint;
156 if (_image == null || width != _image.getWidth() || height != _image.getHeight())
158 createBackgroundImage();
160 // draw buffered image onto g
161 g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
163 // draw selected range, if any
164 if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
166 int rangeStart = _trackInfo.getSelection().getStart();
167 int rangeEnd = _trackInfo.getSelection().getEnd();
168 g.setColor(COLOR_CURR_RANGE);
169 for (int i=rangeStart; i<=rangeEnd; i++)
171 x = width/2 + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
172 y = height/2 - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
173 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
174 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
176 g.drawOval(x - 2, y - 2, 4, 4);
181 // Highlight selected point
182 if (selectedPoint >= 0 && !_zoomDragging)
184 g.setColor(COLOR_CROSSHAIRS);
185 x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
186 y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
187 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
188 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
190 // Draw cross-hairs for current point
191 g.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
192 g.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
194 // Show selected point afterwards to make sure it's on top
195 g.drawOval(x - 2, y - 2, 4, 4);
196 g.drawOval(x - 3, y - 3, 6, 6);
200 // Draw rectangle for dragging zoom area
203 g.setColor(COLOR_CROSSHAIRS);
204 g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
205 g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
206 g.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
207 g.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
210 // Attempt to grab keyboard focus if possible
216 * Draw the map onto an offscreen image
218 private void createBackgroundImage()
220 int width = getWidth();
221 int height = getHeight();
223 // Make a new image and initialise it
224 _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
225 Graphics bufferedG = _image.getGraphics();
226 super.paint(bufferedG);
228 // Loop and show all points
229 int numPoints = _track.getNumPoints();
230 bufferedG.setColor(COLOR_POINT);
231 int halfWidth = width/2;
232 int halfHeight = height/2;
233 for (int i=0; i<numPoints; i++)
235 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
236 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
237 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
238 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
240 bufferedG.drawOval(x - 2, y - 2, 4, 4);
244 // Loop again and show waypoints with names
245 bufferedG.setColor(COLOR_WAYPT_NAME);
246 FontMetrics fm = bufferedG.getFontMetrics();
247 int nameHeight = fm.getHeight();
248 int numWaypointNamesShown = 0;
249 for (int i=0; i<numPoints; i++)
251 DataPoint point = _track.getPoint(i);
252 String waypointName = point.getWaypointName();
253 if (waypointName != null && !waypointName.equals("") && numWaypointNamesShown < LIMIT_WAYPOINT_NAMES)
255 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
256 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
257 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
258 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
260 bufferedG.fillOval(x - 3, y - 3, 6, 6);
261 // Figure out where to draw name so it doesn't obscure track
262 int nameWidth = fm.stringWidth(waypointName);
263 if (nameWidth < (width - 2 * BORDER_WIDTH))
265 double nameAngle = 0.3;
266 double nameRadius = 1.0;
267 boolean drawnName = false;
270 int nameX = x + (int) (nameRadius * Math.cos(nameAngle)) - (nameWidth/2);
271 int nameY = y + (int) (nameRadius * Math.sin(nameAngle)) + (nameHeight/2);
272 if (nameX > BORDER_WIDTH && (nameX + nameWidth) < (width - BORDER_WIDTH)
273 && nameY < (height - BORDER_WIDTH) && (nameY - nameHeight) > BORDER_WIDTH)
275 // name can fit in grid - does it overlap data points?
276 if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
278 bufferedG.drawString(waypointName, nameX, nameY);
280 numWaypointNamesShown++;
285 // wasn't room within the radius, so don't print name
286 if (nameRadius > 50.0)
299 * Tests whether there are any data points within the specified x,y rectangle
300 * @param inX left X coordinate
301 * @param inY bottom Y coordinate
302 * @param inWidth width of rectangle
303 * @param inHeight height of rectangle
304 * @return true if there's at least one data point in the rectangle
306 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
308 // if (true) return true;
309 for (int x=0; x<inWidth; x++)
311 for (int y=0; y<inHeight; y++)
313 int pixelColor = _image.getRGB(inX + x, inY - y);
314 if (pixelColor != -1) return true;
322 * Make the popup menu for right-clicking the map
324 private void makePopup()
326 _popup = new JPopupMenu();
327 JMenuItem zoomIn = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
328 zoomIn.addActionListener(new ActionListener() {
329 public void actionPerformed(ActionEvent e)
333 zoomIn.setEnabled(true);
335 JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
336 zoomOut.addActionListener(new ActionListener() {
337 public void actionPerformed(ActionEvent e)
341 zoomOut.setEnabled(true);
343 JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
344 zoomFull.addActionListener(new ActionListener() {
345 public void actionPerformed(ActionEvent e)
349 zoomFull.setEnabled(true);
350 _popup.add(zoomFull);
351 _autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
352 _autoPanMenuItem.setSelected(true);
353 _popup.add(_autoPanMenuItem);
358 * Zoom map to full scale
360 private void zoomToFullScale()
366 dataUpdated(DataSubscriber.ALL);
371 * Zoom map either in or out by one step
372 * @param inZoomIn true to zoom in, false for out
374 private void zoomMap(boolean inZoomIn)
379 _zoomScale *= ZOOM_SCALE_FACTOR;
384 _zoomScale /= ZOOM_SCALE_FACTOR;
385 if (_zoomScale < 0.5) _zoomScale = 0.5;
388 dataUpdated(DataSubscriber.ALL);
393 * Pan the map by the specified amounts
394 * @param inUp upwards pan
395 * @param inRight rightwards pan
397 private void panMap(int inUp, int inRight)
399 double panFactor = _scale / _zoomScale;
400 _offsetY = _offsetY + (inUp * panFactor);
401 _offsetX = _offsetX - (inRight * panFactor);
402 // Limit pan to sensible range??
410 * React to click on map display
412 public void mouseClicked(MouseEvent e)
417 int xClick = e.getX();
418 int yClick = e.getY();
419 // Check click is within main area (not in border)
420 if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
421 && yClick < (getHeight() - BORDER_WIDTH))
423 // Check left click or right click
426 // Only show popup if track has data
427 if (_track != null && _track.getNumPoints() > 0)
428 _popup.show(this, e.getX(), e.getY());
432 // Find point within range of click point
433 double pointX = (xClick - getWidth()/2) * _scale / _zoomScale + _offsetX;
434 double pointY = (getHeight()/2 - yClick) * _scale / _zoomScale + _offsetY;
435 int selectedPointIndex = _track.getNearestPointIndex(
436 pointX, pointY, CLICK_SENSITIVITY * _scale, false);
437 // Select the given point (or deselect if no point was found)
438 _trackInfo.getSelection().selectPoint(selectedPointIndex);
446 * Respond to mouse released to reset dragging
447 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
449 public void mouseReleased(MouseEvent e)
451 _dragStartX = _dragStartY = -1;
454 if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0)
456 // zoom area marked out - calculate offset and zoom
457 int xPan = (getWidth() - _zoomDragFromX - e.getX()) / 2;
458 int yPan = (getHeight() - _zoomDragFromY - e.getY()) / 2;
459 double xZoom = Math.abs(getWidth() * 1.0 / (e.getX() - _zoomDragFromX));
460 double yZoom = Math.abs(getHeight() * 1.0 / (e.getY() - _zoomDragFromY));
461 double extraZoom = (xZoom>yZoom?yZoom:xZoom);
462 // deselect point if selected (to stop autopan)
463 _trackInfo.getSelection().selectPoint(-1);
464 // Pan first to ensure pan occurs with correct scale
466 // Then zoom in and request repaint
467 _zoomScale = _zoomScale * extraZoom;
471 _zoomDragFromX = _zoomDragFromY = -1;
472 _zoomDragging = false;
478 * Respond to mouse wheel events to zoom the map
479 * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
481 public void mouseWheelMoved(MouseWheelEvent e)
483 zoomMap(e.getWheelRotation() < 0);
488 * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
490 public void keyPressed(KeyEvent e)
492 int code = e.getKeyCode();
493 // Check for meta key
494 if (e.isControlDown())
496 // Check for arrow keys to zoom in and out
497 if (code == KeyEvent.VK_UP)
499 else if (code == KeyEvent.VK_DOWN)
501 // Key nav for next/prev point
502 else if (code == KeyEvent.VK_LEFT)
503 _trackInfo.getSelection().selectPreviousPoint();
504 else if (code == KeyEvent.VK_RIGHT)
505 _trackInfo.getSelection().selectNextPoint();
509 // Check for arrow keys to pan
511 if (code == KeyEvent.VK_UP)
512 upwardsPan = PAN_DISTANCE;
513 else if (code == KeyEvent.VK_DOWN)
514 upwardsPan = -PAN_DISTANCE;
515 int rightwardsPan = 0;
516 if (code == KeyEvent.VK_RIGHT)
517 rightwardsPan = -PAN_DISTANCE;
518 else if (code == KeyEvent.VK_LEFT)
519 rightwardsPan = PAN_DISTANCE;
520 panMap(upwardsPan, rightwardsPan);
521 // Check for delete key to delete current point
522 if (code == KeyEvent.VK_DELETE && _trackInfo.getSelection().getCurrentPointIndex() >= 0)
524 _app.deleteCurrentPoint();
525 // reset last selected point to trigger autopan
526 _lastSelectedPoint = -1;
533 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
535 public void keyReleased(KeyEvent e)
542 * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
544 public void keyTyped(KeyEvent e)
551 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
553 public void mouseDragged(MouseEvent e)
559 int xShift = e.getX() - _dragStartX;
560 int yShift = e.getY() - _dragStartY;
561 panMap(yShift, xShift);
563 _dragStartX = e.getX();
564 _dragStartY = e.getY();
568 // Right click-and-drag for zoom
569 if (_zoomDragFromX < 0 || _zoomDragFromY < 0)
571 _zoomDragFromX = e.getX();
572 _zoomDragFromY = e.getY();
576 _zoomDragToX = e.getX();
577 _zoomDragToY = e.getY();
578 _zoomDragging = true;
586 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
588 public void mouseMoved(MouseEvent e)