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;
26 //import tim.prune.gui.map.MapWindow;
30 * Display component for the main map
32 public class MapChart extends GenericChart implements MouseWheelListener, KeyListener, MouseMotionListener
35 private static final int POINT_RADIUS = 4;
36 private static final int CLICK_SENSITIVITY = 10;
37 private static final double ZOOM_SCALE_FACTOR = 1.2;
38 private static final int PAN_DISTANCE = 10;
39 private static final int LIMIT_WAYPOINT_NAMES = 40;
42 private static final Color COLOR_BG = Color.WHITE;
43 private static final Color COLOR_POINT = Color.BLUE;
44 private static final Color COLOR_CURR_RANGE = Color.GREEN;
45 private static final Color COLOR_CROSSHAIRS = Color.RED;
46 private static final Color COLOR_WAYPT_NAME = Color.BLACK;
49 private App _app = null;
50 private BufferedImage _image = null;
51 private JPopupMenu _popup = null;
52 private JCheckBoxMenuItem _autoPanMenuItem = null;
53 private JCheckBoxMenuItem _connectPointsMenuItem = null;
54 private int _numPoints = -1;
55 private double _scale;
56 private double _offsetX, _offsetY, _zoomScale;
57 private int _lastSelectedPoint = -1;
58 private int _dragStartX = -1, _dragStartY = -1;
59 private int _zoomDragFromX = -1, _zoomDragFromY = -1;
60 private int _zoomDragToX = -1, _zoomDragToY = -1;
61 private boolean _zoomDragging = false;
66 * @param inApp App object for callbacks
67 * @param inTrackInfo track info object
69 public MapChart(App inApp, TrackInfo inTrackInfo)
74 addMouseListener(this);
75 addMouseWheelListener(this);
76 addMouseMotionListener(this);
79 MINIMUM_SIZE = new Dimension(200, 250);
85 * Override track updating to refresh image
87 public void dataUpdated(byte inUpdateType)
89 // Check if number of points has changed or data has been edited
90 if (_track.getNumPoints() != _numPoints || (inUpdateType & DATA_EDITED) > 0)
93 _lastSelectedPoint = -1;
94 _numPoints = _track.getNumPoints();
96 super.dataUpdated(inUpdateType);
101 * Override paint method to draw map
102 * @param inG graphics object
104 public void paint(Graphics inG)
112 int width = getWidth();
113 int height = getHeight();
116 // Find x and y ranges, and scale to fit
117 double scaleX = (_track.getXRange().getMaximum() - _track.getXRange().getMinimum())
118 / (width - 2 * (BORDER_WIDTH + POINT_RADIUS));
119 double scaleY = (_track.getYRange().getMaximum() - _track.getYRange().getMinimum())
120 / (height - 2 * (BORDER_WIDTH + POINT_RADIUS));
122 if (scaleY > _scale) _scale = scaleY;
124 // Autopan if necessary
125 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
126 if (_autoPanMenuItem.isSelected() && selectedPoint >= 0 && selectedPoint != _lastSelectedPoint)
128 // Autopan is enabled and a point is selected - work out x and y to see if it's within range
129 x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
130 y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
131 if (x <= BORDER_WIDTH)
134 _offsetX -= (width / 4 - x) * _scale / _zoomScale;
137 else if (x >= (width - BORDER_WIDTH))
140 _offsetX += (x - width * 3/4) * _scale / _zoomScale;
143 if (y <= BORDER_WIDTH)
146 _offsetY += (height / 4 - y) * _scale / _zoomScale;
149 else if (y >= (height - BORDER_WIDTH))
152 _offsetY -= (y - height * 3/4) * _scale / _zoomScale;
156 _lastSelectedPoint = selectedPoint;
158 // Create background if necessary
159 if (_image == null || width != _image.getWidth() || height != _image.getHeight())
161 createBackgroundImage();
163 // return if image has been set to null by other thread
164 if (_image == null) {return;}
166 // draw buffered image onto g
167 inG.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
169 // draw selected range, if any
170 if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
172 int rangeStart = _trackInfo.getSelection().getStart();
173 int rangeEnd = _trackInfo.getSelection().getEnd();
174 inG.setColor(COLOR_CURR_RANGE);
175 for (int i=rangeStart; i<=rangeEnd; i++)
177 x = width/2 + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
178 y = height/2 - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
179 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
180 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
182 inG.drawRect(x - 2, y - 2, 4, 4);
187 // Highlight selected point
188 if (selectedPoint >= 0 && !_zoomDragging)
190 inG.setColor(COLOR_CROSSHAIRS);
191 x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
192 y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
193 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
194 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
196 // Draw cross-hairs for current point
197 inG.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
198 inG.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
200 // Show selected point afterwards to make sure it's on top
201 inG.drawOval(x - 2, y - 2, 4, 4);
202 inG.drawOval(x - 3, y - 3, 6, 6);
206 // Draw rectangle for dragging zoom area
209 inG.setColor(COLOR_CROSSHAIRS);
210 inG.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
211 inG.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
212 inG.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
213 inG.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
216 // Attempt to grab keyboard focus if possible
217 //requestFocus(); (causes problems here)
222 * Plot the points onto an offscreen image
223 * which doesn't have to be redrawn when the selection changes
225 private void createBackgroundImage()
227 int width = getWidth();
228 int height = getHeight();
230 int lastX = 0, lastY = 0;
232 if (_image == null || _image.getWidth() != width || _image.getHeight() != height) {
233 _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
235 Graphics bufferedG = _image.getGraphics();
236 super.paint(bufferedG);
238 // Loop and show all points
239 int numPoints = _track.getNumPoints();
240 bufferedG.setColor(COLOR_POINT);
241 int halfWidth = width/2;
242 int halfHeight = height/2;
243 boolean currPointTrackpoint = false, lastPointTrackpoint = false;
244 for (int i=0; i<numPoints; i++)
246 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
247 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
248 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
249 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
251 // draw block for point (a bit faster than circles)
252 bufferedG.drawRect(x - 2, y - 2, 3, 3);
254 // See whether to connect the point with previous one or not
255 currPointTrackpoint = !_track.getPoint(i).isWaypoint() && _track.getPoint(i).getPhoto() == null;
256 if (_connectPointsMenuItem.isSelected() && currPointTrackpoint && lastPointTrackpoint)
258 bufferedG.drawLine(lastX, lastY, x, y);
260 lastPointTrackpoint = currPointTrackpoint;
263 lastPointTrackpoint = false;
265 lastX = x; lastY = y;
268 // Loop again and show waypoints with names
269 bufferedG.setColor(COLOR_WAYPT_NAME);
270 FontMetrics fm = bufferedG.getFontMetrics();
271 int nameHeight = fm.getHeight();
272 int numWaypointNamesShown = 0;
273 for (int i=0; i<numPoints; i++)
275 DataPoint point = _track.getPoint(i);
276 String waypointName = point.getWaypointName();
277 if (waypointName != null && !waypointName.equals(""))
279 // escape if nothing more to do
280 if (numWaypointNamesShown >= LIMIT_WAYPOINT_NAMES || _image == null) {break;}
281 // calculate coordinates of point
282 x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
283 y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale);
284 if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
285 && y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
287 bufferedG.fillOval(x - 3, y - 3, 6, 6);
288 // Figure out where to draw name so it doesn't obscure track
289 int nameWidth = fm.stringWidth(waypointName);
290 if (nameWidth < (width - 2 * BORDER_WIDTH))
292 boolean drawnName = false;
293 // Make arrays for coordinates right left up down
294 int[] nameXs = {x + 2, x - nameWidth - 2, x - nameWidth/2, x - nameWidth/2};
295 int[] nameYs = {y + (nameHeight/2), y + (nameHeight/2), y - 2, y + nameHeight + 2};
296 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
298 // Shift arrays for coordinates right left up down
299 nameXs[0] += 2; nameXs[1] -= 2;
300 nameYs[2] -= 2; nameYs[3] += 2;
301 // Check each direction in turn right left up down
302 for (int a=0; a<4; a++)
304 if (nameXs[a] > BORDER_WIDTH && (nameXs[a] + nameWidth) < (width - BORDER_WIDTH)
305 && nameYs[a] < (height - BORDER_WIDTH) && (nameYs[a] - nameHeight) > BORDER_WIDTH
306 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight))
308 // Found a rectangle to fit - draw name here and quit
309 bufferedG.drawString(waypointName, nameXs[a], nameYs[a]);
324 * Tests whether there are any data points within the specified x,y rectangle
325 * @param inX left X coordinate
326 * @param inY bottom Y coordinate
327 * @param inWidth width of rectangle
328 * @param inHeight height of rectangle
329 * @return true if there's at least one data point in the rectangle
331 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
335 // loop over x coordinate of rectangle
336 for (int x=0; x<inWidth; x++)
338 // loop over y coordinate of rectangle
339 for (int y=0; y<inHeight; y++)
341 int pixelColor = _image.getRGB(inX + x, inY - y);
342 if (pixelColor != -1) return true;
346 catch (NullPointerException e) {
347 // ignore null pointers, just return false
354 * Make the popup menu for right-clicking the map
356 private void makePopup()
358 _popup = new JPopupMenu();
359 JMenuItem zoomIn = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
360 zoomIn.addActionListener(new ActionListener() {
361 public void actionPerformed(ActionEvent e)
365 zoomIn.setEnabled(true);
367 JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
368 zoomOut.addActionListener(new ActionListener() {
369 public void actionPerformed(ActionEvent e)
373 zoomOut.setEnabled(true);
375 JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
376 zoomFull.addActionListener(new ActionListener() {
377 public void actionPerformed(ActionEvent e)
381 zoomFull.setEnabled(true);
382 _popup.add(zoomFull);
383 _connectPointsMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.connect"));
384 _connectPointsMenuItem.addActionListener(new ActionListener() {
385 public void actionPerformed(ActionEvent e)
388 dataUpdated(DataSubscriber.ALL);
391 _connectPointsMenuItem.setSelected(false);
392 _popup.add(_connectPointsMenuItem);
393 _autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
394 _autoPanMenuItem.setSelected(true);
395 _popup.add(_autoPanMenuItem);
397 JMenuItem mapItem = new JMenuItem("Show map");
398 mapItem.addActionListener(new ActionListener() {
399 public void actionPerformed(ActionEvent e)
410 * Zoom map to full scale
412 private void zoomToFullScale()
418 dataUpdated(DataSubscriber.ALL);
423 * Zoom map either in or out by one step
424 * @param inZoomIn true to zoom in, false for out
426 private void zoomMap(boolean inZoomIn)
431 _zoomScale *= ZOOM_SCALE_FACTOR;
436 _zoomScale /= ZOOM_SCALE_FACTOR;
437 if (_zoomScale < 0.5) _zoomScale = 0.5;
440 dataUpdated(DataSubscriber.ALL);
445 * Pan the map by the specified amounts
446 * @param inUp upwards pan
447 * @param inRight rightwards pan
449 private void panMap(int inUp, int inRight)
451 double panFactor = _scale / _zoomScale;
452 _offsetY = _offsetY + (inUp * panFactor);
453 _offsetX = _offsetX - (inRight * panFactor);
454 // Limit pan to sensible range??
462 * React to click on map display
463 * @param inE mouse event
465 public void mouseClicked(MouseEvent inE)
470 int xClick = inE.getX();
471 int yClick = inE.getY();
472 // Check click is within main area (not in border)
473 if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
474 && yClick < (getHeight() - BORDER_WIDTH))
476 // Check left click or right click
477 if (inE.isMetaDown())
479 // Only show popup if track has data
480 if (_track != null && _track.getNumPoints() > 0)
481 _popup.show(this, xClick, yClick);
485 // Find point within range of click point
486 double pointX = (xClick - getWidth()/2) * _scale / _zoomScale + _offsetX;
487 double pointY = (getHeight()/2 - yClick) * _scale / _zoomScale + _offsetY;
488 int selectedPointIndex = _track.getNearestPointIndex(
489 pointX, pointY, CLICK_SENSITIVITY * _scale, false);
490 // Select the given point (or deselect if no point was found)
491 _trackInfo.getSelection().selectPoint(selectedPointIndex);
499 * Respond to mouse released to reset dragging
500 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
502 public void mouseReleased(MouseEvent e)
504 _dragStartX = _dragStartY = -1;
507 if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0)
509 // zoom area marked out - calculate offset and zoom
510 int xPan = (getWidth() - _zoomDragFromX - e.getX()) / 2;
511 int yPan = (getHeight() - _zoomDragFromY - e.getY()) / 2;
512 double xZoom = Math.abs(getWidth() * 1.0 / (e.getX() - _zoomDragFromX));
513 double yZoom = Math.abs(getHeight() * 1.0 / (e.getY() - _zoomDragFromY));
514 double extraZoom = (xZoom>yZoom?yZoom:xZoom);
515 // deselect point if selected (to stop autopan)
516 _trackInfo.getSelection().selectPoint(-1);
517 // Pan first to ensure pan occurs with correct scale
519 // Then zoom in and request repaint
520 _zoomScale = _zoomScale * extraZoom;
524 _zoomDragFromX = _zoomDragFromY = -1;
525 _zoomDragging = false;
531 * Respond to mouse wheel events to zoom the map
532 * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
534 public void mouseWheelMoved(MouseWheelEvent e)
536 zoomMap(e.getWheelRotation() < 0);
541 * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
543 public void keyPressed(KeyEvent e)
545 int code = e.getKeyCode();
546 // Check for meta key
547 if (e.isControlDown())
549 // Check for arrow keys to zoom in and out
550 if (code == KeyEvent.VK_UP)
552 else if (code == KeyEvent.VK_DOWN)
554 // Key nav for next/prev point
555 else if (code == KeyEvent.VK_LEFT)
556 _trackInfo.getSelection().selectPreviousPoint();
557 else if (code == KeyEvent.VK_RIGHT)
558 _trackInfo.getSelection().selectNextPoint();
562 // Check for arrow keys to pan
564 if (code == KeyEvent.VK_UP)
565 upwardsPan = PAN_DISTANCE;
566 else if (code == KeyEvent.VK_DOWN)
567 upwardsPan = -PAN_DISTANCE;
568 int rightwardsPan = 0;
569 if (code == KeyEvent.VK_RIGHT)
570 rightwardsPan = -PAN_DISTANCE;
571 else if (code == KeyEvent.VK_LEFT)
572 rightwardsPan = PAN_DISTANCE;
573 panMap(upwardsPan, rightwardsPan);
574 // Check for delete key to delete current point
575 if (code == KeyEvent.VK_DELETE && _trackInfo.getSelection().getCurrentPointIndex() >= 0)
577 _app.deleteCurrentPoint();
578 // reset last selected point to trigger autopan
579 _lastSelectedPoint = -1;
586 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
588 public void keyReleased(KeyEvent e)
595 * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
597 public void keyTyped(KeyEvent e)
604 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
606 public void mouseDragged(MouseEvent e)
612 int xShift = e.getX() - _dragStartX;
613 int yShift = e.getY() - _dragStartY;
614 panMap(yShift, xShift);
616 _dragStartX = e.getX();
617 _dragStartY = e.getY();
621 // Right click-and-drag for zoom
622 if (_zoomDragFromX < 0 || _zoomDragFromY < 0)
624 _zoomDragFromX = e.getX();
625 _zoomDragFromY = e.getY();
629 _zoomDragToX = e.getX();
630 _zoomDragToY = e.getY();
631 _zoomDragging = true;
639 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
641 public void mouseMoved(MouseEvent e)
647 * Show a map window - probably only temporarily here until it gets fixed
650 private void showMap()
652 MapWindow map = new MapWindow(_track);