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.I18nManager;
23 import tim.prune.data.DataPoint;
24 import tim.prune.data.Field;
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()
88 // Check if number of points has changed or Track
89 // object has a different signature
90 if (_track.getNumPoints() != _numPoints)
93 _numPoints = _track.getNumPoints();
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);
202 g.setColor(COLOR_CROSSHAIRS);
203 g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
204 g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
205 g.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
206 g.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
212 * Draw the map onto an offscreen image
214 private void createBackgroundImage()
216 int width = getWidth();
217 int height = getHeight();
219 // Make a new image and initialise it
220 _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
221 Graphics bufferedG = _image.getGraphics();
222 super.paint(bufferedG);
224 // Loop and show all points
225 int numPoints = _track.getNumPoints();
226 bufferedG.setColor(COLOR_POINT);
227 int halfWidth = width/2;
228 int halfHeight = height/2;
229 for (int i=0; i<numPoints; i++)
231 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
232 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
233 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
234 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
236 bufferedG.drawOval(x - 2, y - 2, 4, 4);
240 // Loop again and show waypoints with names
241 bufferedG.setColor(COLOR_WAYPT_NAME);
242 FontMetrics fm = bufferedG.getFontMetrics();
243 int nameHeight = fm.getHeight();
244 int numWaypointNamesShown = 0;
245 for (int i=0; i<numPoints; i++)
247 DataPoint point = _track.getPoint(i);
248 String waypointName = point.getFieldValue(Field.WAYPT_NAME);
249 if (waypointName != null && !waypointName.equals("") && numWaypointNamesShown < LIMIT_WAYPOINT_NAMES)
251 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
252 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
253 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
254 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
256 bufferedG.fillOval(x - 3, y - 3, 6, 6);
257 // Figure out where to draw name so it doesn't obscure track
258 int nameWidth = fm.stringWidth(waypointName);
259 if (nameWidth < (width - 2 * BORDER_WIDTH))
261 double nameAngle = 0.3;
262 double nameRadius = 1.0;
263 boolean drawnName = false;
266 int nameX = x + (int) (nameRadius * Math.cos(nameAngle)) - (nameWidth/2);
267 int nameY = y + (int) (nameRadius * Math.sin(nameAngle)) + (nameHeight/2);
268 if (nameX > BORDER_WIDTH && (nameX + nameWidth) < (width - BORDER_WIDTH)
269 && nameY < (height - BORDER_WIDTH) && (nameY - nameHeight) > BORDER_WIDTH)
271 // name can fit in grid - does it overlap data points?
272 if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
274 bufferedG.drawString(waypointName, nameX, nameY);
276 numWaypointNamesShown++;
281 // wasn't room within the radius, so don't print name
282 if (nameRadius > 50.0)
295 * Tests whether there are any data points within the specified x,y rectangle
296 * @param inX left X coordinate
297 * @param inY bottom Y coordinate
298 * @param inWidth width of rectangle
299 * @param inHeight height of rectangle
300 * @return true if there's at least one data point in the rectangle
302 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
304 // if (true) return true;
305 for (int x=0; x<inWidth; x++)
307 for (int y=0; y<inHeight; y++)
309 int pixelColor = _image.getRGB(inX + x, inY - y);
310 if (pixelColor != -1) return true;
318 * Make the popup menu for right-clicking the map
320 private void makePopup()
322 _popup = new JPopupMenu();
323 JMenuItem zoomIn = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
324 zoomIn.addActionListener(new ActionListener() {
325 public void actionPerformed(ActionEvent e)
329 zoomIn.setEnabled(true);
331 JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
332 zoomOut.addActionListener(new ActionListener() {
333 public void actionPerformed(ActionEvent e)
337 zoomOut.setEnabled(true);
339 JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
340 zoomFull.addActionListener(new ActionListener() {
341 public void actionPerformed(ActionEvent e)
345 zoomFull.setEnabled(true);
346 _popup.add(zoomFull);
347 _autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
348 _autoPanMenuItem.setSelected(true);
349 _popup.add(_autoPanMenuItem);
354 * Zoom map to full scale
356 private void zoomToFullScale()
367 * Zoom map either in or out by one step
368 * @param inZoomIn true to zoom in, false for out
370 private void zoomMap(boolean inZoomIn)
375 _zoomScale *= ZOOM_SCALE_FACTOR;
380 _zoomScale /= ZOOM_SCALE_FACTOR;
381 if (_zoomScale < 0.5) _zoomScale = 0.5;
389 * Pan the map by the specified amounts
390 * @param inUp upwards pan
391 * @param inRight rightwards pan
393 private void panMap(int inUp, int inRight)
395 double panFactor = _scale / _zoomScale;
396 _offsetY = _offsetY + (inUp * panFactor);
397 _offsetX = _offsetX - (inRight * panFactor);
398 // Limit pan to sensible range??
405 * React to click on map display
407 public void mouseClicked(MouseEvent e)
412 int xClick = e.getX();
413 int yClick = e.getY();
414 // Check click is within main area (not in border)
415 if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
416 && yClick < (getHeight() - BORDER_WIDTH))
418 // Check left click or right click
421 // Only show popup if track has data
422 if (_track != null && _track.getNumPoints() > 0)
423 _popup.show(this, e.getX(), e.getY());
427 // Find point within range of click point
428 double pointX = (xClick - getWidth()/2) * _scale / _zoomScale + _offsetX;
429 double pointY = (getHeight()/2 - yClick) * _scale / _zoomScale + _offsetY;
430 int selectedPointIndex = _track.getNearestPointIndex(
431 pointX, pointY, CLICK_SENSITIVITY * _scale, false);
432 // Select the given point (or deselect if no point was found)
433 _trackInfo.getSelection().selectPoint(selectedPointIndex);
441 * Respond to mouse released to reset dragging
442 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
444 public void mouseReleased(MouseEvent e)
446 _dragStartX = _dragStartY = -1;
449 if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0)
451 // zoom area marked out - calculate offset and zoom
452 int xPan = (getWidth() - _zoomDragFromX - e.getX()) / 2;
453 int yPan = (getHeight() - _zoomDragFromY - e.getY()) / 2;
454 double xZoom = Math.abs(getWidth() * 1.0 / (e.getX() - _zoomDragFromX));
455 double yZoom = Math.abs(getHeight() * 1.0 / (e.getY() - _zoomDragFromY));
456 double extraZoom = (xZoom>yZoom?yZoom:xZoom);
457 // Pan first to ensure pan occurs with correct scale
459 // Then zoom in and request repaint
460 _zoomScale = _zoomScale * extraZoom;
464 _zoomDragFromX = _zoomDragFromY = -1;
465 _zoomDragging = false;
471 * Respond to mouse wheel events to zoom the map
472 * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
474 public void mouseWheelMoved(MouseWheelEvent e)
476 zoomMap(e.getWheelRotation() < 0);
481 * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
483 public void keyPressed(KeyEvent e)
485 int code = e.getKeyCode();
486 // Check for meta key
487 if (e.isControlDown())
489 // Check for arrow keys to zoom in and out
490 if (code == KeyEvent.VK_UP)
492 else if (code == KeyEvent.VK_DOWN)
494 // Key nav for next/prev point
495 else if (code == KeyEvent.VK_LEFT)
496 _trackInfo.getSelection().selectPreviousPoint();
497 else if (code == KeyEvent.VK_RIGHT)
498 _trackInfo.getSelection().selectNextPoint();
502 // Check for arrow keys to pan
504 if (code == KeyEvent.VK_UP)
505 upwardsPan = PAN_DISTANCE;
506 else if (code == KeyEvent.VK_DOWN)
507 upwardsPan = -PAN_DISTANCE;
508 int rightwardsPan = 0;
509 if (code == KeyEvent.VK_RIGHT)
510 rightwardsPan = -PAN_DISTANCE;
511 else if (code == KeyEvent.VK_LEFT)
512 rightwardsPan = PAN_DISTANCE;
513 panMap(upwardsPan, rightwardsPan);
514 // Check for delete key to delete current point
515 if (code == KeyEvent.VK_DELETE && _trackInfo.getSelection().getCurrentPointIndex() >= 0)
516 _app.deleteCurrentPoint();
522 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
524 public void keyReleased(KeyEvent e)
531 * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
533 public void keyTyped(KeyEvent e)
540 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
542 public void mouseDragged(MouseEvent e)
548 int xShift = e.getX() - _dragStartX;
549 int yShift = e.getY() - _dragStartY;
550 panMap(yShift, xShift);
552 _dragStartX = e.getX();
553 _dragStartY = e.getY();
557 // Right click-and-drag for zoom
558 if (_zoomDragFromX < 0 || _zoomDragFromY < 0)
560 _zoomDragFromX = e.getX();
561 _zoomDragFromY = e.getY();
565 _zoomDragToX = e.getX();
566 _zoomDragToY = e.getY();
567 _zoomDragging = true;
575 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
577 public void mouseMoved(MouseEvent e)