]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/gui/MapChart.java
1236f130558630c4278b7a8cc31762ec34630ad4
[GpsPrune.git] / tim / prune / gui / MapChart.java
1 package tim.prune.gui;
2
3 import java.awt.Color;
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;
16
17 import javax.swing.JCheckBoxMenuItem;
18 import javax.swing.JMenuItem;
19 import javax.swing.JPopupMenu;
20
21 import tim.prune.App;
22 import tim.prune.DataSubscriber;
23 import tim.prune.I18nManager;
24 import tim.prune.data.DataPoint;
25 import tim.prune.data.TrackInfo;
26
27
28 /**
29  * Display component for the main map
30  */
31 public class MapChart extends GenericChart implements MouseWheelListener, KeyListener, MouseMotionListener
32 {
33         // Constants
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;
39
40         // Colours
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;
46
47         // Instance variables
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;
61
62
63         /**
64          * Constructor
65          * @param inApp App object for callbacks
66          * @param inTrackInfo track info object
67          */
68         public MapChart(App inApp, TrackInfo inTrackInfo)
69         {
70                 super(inTrackInfo);
71                 _app = inApp;
72                 makePopup();
73                 addMouseListener(this);
74                 addMouseWheelListener(this);
75                 addMouseMotionListener(this);
76                 setFocusable(true);
77                 addKeyListener(this);
78                 MINIMUM_SIZE = new Dimension(200, 250);
79                 _zoomScale = 1.0;
80         }
81
82
83         /**
84          * Override track updating to refresh image
85          */
86         public void dataUpdated(byte inUpdateType)
87         {
88                 // Check if number of points has changed or data has been edited
89                 if (_track.getNumPoints() != _numPoints || (inUpdateType & DATA_EDITED) > 0)
90                 {
91                         _image = null;
92                         _lastSelectedPoint = -1;
93                         _numPoints = _track.getNumPoints();
94                 }
95                 super.dataUpdated(inUpdateType);
96         }
97
98
99         /**
100          * Override paint method to draw map
101          */
102         public void paint(Graphics g)
103         {
104                 if (_track == null)
105                 {
106                         super.paint(g);
107                         return;
108                 }
109
110                 int width = getWidth();
111                 int height = getHeight();
112                 int x, y;
113
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));
119                 _scale = scaleX;
120                 if (scaleY > _scale) _scale = scaleY;
121
122                 // Autopan if necessary
123                 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
124                 if (_autoPanMenuItem.isSelected() && selectedPoint >= 0 && selectedPoint != _lastSelectedPoint)
125                 {
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)
130                         {
131                                 // autopan left
132                                 _offsetX -= (width / 4 - x) * _scale / _zoomScale;
133                                 _image = null;
134                         }
135                         else if (x >= (width - BORDER_WIDTH))
136                         {
137                                 // autopan right
138                                 _offsetX += (x - width * 3/4) * _scale / _zoomScale;
139                                 _image = null;
140                         }
141                         if (y <= BORDER_WIDTH)
142                         {
143                                 // autopan up
144                                 _offsetY += (height / 4 - y) * _scale / _zoomScale;
145                                 _image = null;
146                         }
147                         else if (y >= (height - BORDER_WIDTH))
148                         {
149                                 // autopan down
150                                 _offsetY -= (y - height * 3/4) * _scale / _zoomScale;
151                                 _image = null;
152                         }
153                 }
154                 _lastSelectedPoint = selectedPoint;
155
156                 if (_image == null || width != _image.getWidth() || height != _image.getHeight())
157                 {
158                         createBackgroundImage();
159                 }
160                 // draw buffered image onto g
161                 g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
162
163                 // draw selected range, if any
164                 if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
165                 {
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++)
170                         {
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)
175                                 {
176                                         g.drawOval(x - 2, y - 2, 4, 4);
177                                 }
178                         }
179                 }
180
181                 // Highlight selected point
182                 if (selectedPoint >= 0 && !_zoomDragging)
183                 {
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)
189                         {
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);
193
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);
197                         }
198                 }
199
200                 // Draw rectangle for dragging zoom area
201                 if (_zoomDragging)
202                 {
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);
208                 }
209
210                 // Attempt to grab keyboard focus if possible
211                 this.requestFocus();
212         }
213
214
215         /**
216          * Draw the map onto an offscreen image
217          */
218         private void createBackgroundImage()
219         {
220                 int width = getWidth();
221                 int height = getHeight();
222                 int x, y;
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);
227
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++)
234                 {
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)
239                         {
240                                 bufferedG.drawOval(x - 2, y - 2, 4, 4);
241                         }
242                 }
243
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++)
250                 {
251                         DataPoint point = _track.getPoint(i);
252                         String waypointName = point.getWaypointName();
253                         if (waypointName != null && !waypointName.equals("") && numWaypointNamesShown < LIMIT_WAYPOINT_NAMES)
254                         {
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)
259                                 {
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))
264                                         {
265                                                 double nameAngle = 0.3;
266                                                 double nameRadius = 1.0;
267                                                 boolean drawnName = false;
268                                                 while (!drawnName)
269                                                 {
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)
274                                                         {
275                                                                 // name can fit in grid - does it overlap data points?
276                                                                 if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
277                                                                 {
278                                                                         bufferedG.drawString(waypointName, nameX, nameY);
279                                                                         drawnName = true;
280                                                                         numWaypointNamesShown++;
281                                                                 }
282                                                         }
283                                                         nameAngle += 0.08;
284                                                         nameRadius += 0.2;
285                                                         // wasn't room within the radius, so don't print name
286                                                         if (nameRadius > 50.0)
287                                                         {
288                                                                 drawnName = true;
289                                                         }
290                                                 }
291                                         }
292                                 }
293                         }
294                 }
295         }
296
297
298         /**
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
305          */
306         private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
307         {
308                 // if (true) return true;
309                 for (int x=0; x<inWidth; x++)
310                 {
311                         for (int y=0; y<inHeight; y++)
312                         {
313                                 int pixelColor = _image.getRGB(inX + x, inY - y);
314                                 if (pixelColor != -1) return true;
315                         }
316                 }
317                 return false;
318         }
319
320
321         /**
322          * Make the popup menu for right-clicking the map
323          */
324         private void makePopup()
325         {
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)
330                         {
331                                 zoomMap(true);
332                         }});
333                 zoomIn.setEnabled(true);
334                 _popup.add(zoomIn);
335                 JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
336                 zoomOut.addActionListener(new ActionListener() {
337                         public void actionPerformed(ActionEvent e)
338                         {
339                                 zoomMap(false);
340                         }});
341                 zoomOut.setEnabled(true);
342                 _popup.add(zoomOut);
343                 JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
344                 zoomFull.addActionListener(new ActionListener() {
345                         public void actionPerformed(ActionEvent e)
346                         {
347                                 zoomToFullScale();
348                         }});
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);
354         }
355
356
357         /**
358          * Zoom map to full scale
359          */
360         private void zoomToFullScale()
361         {
362                 _zoomScale = 1.0;
363                 _offsetX = 0.0;
364                 _offsetY = 0.0;
365                 _numPoints = 0;
366                 dataUpdated(DataSubscriber.ALL);
367         }
368
369
370         /**
371          * Zoom map either in or out by one step
372          * @param inZoomIn true to zoom in, false for out
373          */
374         private void zoomMap(boolean inZoomIn)
375         {
376                 if (inZoomIn)
377                 {
378                         // Zoom in
379                         _zoomScale *= ZOOM_SCALE_FACTOR;
380                 }
381                 else
382                 {
383                         // Zoom out
384                         _zoomScale /= ZOOM_SCALE_FACTOR;
385                         if (_zoomScale < 0.5) _zoomScale = 0.5;
386                 }
387                 _numPoints = 0;
388                 dataUpdated(DataSubscriber.ALL);
389         }
390
391
392         /**
393          * Pan the map by the specified amounts
394          * @param inUp upwards pan
395          * @param inRight rightwards pan
396          */
397         private void panMap(int inUp, int inRight)
398         {
399                 double panFactor = _scale / _zoomScale;
400                 _offsetY = _offsetY + (inUp * panFactor);
401                 _offsetX = _offsetX - (inRight * panFactor);
402                 // Limit pan to sensible range??
403                 _numPoints = 0;
404                 _image = null;
405                 repaint();
406         }
407
408
409         /**
410          * React to click on map display
411          */
412         public void mouseClicked(MouseEvent e)
413         {
414                 this.requestFocus();
415                 if (_track != null)
416                 {
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))
422                         {
423                                 // Check left click or right click
424                                 if (e.isMetaDown())
425                                 {
426                                         // Only show popup if track has data
427                                         if (_track != null && _track.getNumPoints() > 0)
428                                                 _popup.show(this, e.getX(), e.getY());
429                                 }
430                                 else
431                                 {
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);
439                                 }
440                         }
441                 }
442         }
443
444
445         /**
446          * Respond to mouse released to reset dragging
447          * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
448          */
449         public void mouseReleased(MouseEvent e)
450         {
451                 _dragStartX = _dragStartY = -1;
452                 if (e.isMetaDown())
453                 {
454                         if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0)
455                         {
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
465                                 panMap(yPan, xPan);
466                                 // Then zoom in and request repaint
467                                 _zoomScale = _zoomScale * extraZoom;
468                                 _image = null;
469                                 repaint();
470                         }
471                         _zoomDragFromX = _zoomDragFromY = -1;
472                         _zoomDragging = false;
473                 }
474         }
475
476
477         /**
478          * Respond to mouse wheel events to zoom the map
479          * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
480          */
481         public void mouseWheelMoved(MouseWheelEvent e)
482         {
483                 zoomMap(e.getWheelRotation() < 0);
484         }
485
486
487         /**
488          * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
489          */
490         public void keyPressed(KeyEvent e)
491         {
492                 int code = e.getKeyCode();
493                 // Check for meta key
494                 if (e.isControlDown())
495                 {
496                         // Check for arrow keys to zoom in and out
497                         if (code == KeyEvent.VK_UP)
498                                 zoomMap(true);
499                         else if (code == KeyEvent.VK_DOWN)
500                                 zoomMap(false);
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();
506                 }
507                 else
508                 {
509                         // Check for arrow keys to pan
510                         int upwardsPan = 0;
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)
523                         {
524                                 _app.deleteCurrentPoint();
525                                 // reset last selected point to trigger autopan
526                                 _lastSelectedPoint = -1;
527                         }
528                 }
529         }
530
531
532         /**
533          * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
534          */
535         public void keyReleased(KeyEvent e)
536         {
537                 // ignore
538         }
539
540
541         /**
542          * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
543          */
544         public void keyTyped(KeyEvent e)
545         {
546                 // ignore
547         }
548
549
550         /**
551          * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
552          */
553         public void mouseDragged(MouseEvent e)
554         {
555                 if (!e.isMetaDown())
556                 {
557                         if (_dragStartX > 0)
558                         {
559                                 int xShift = e.getX() - _dragStartX;
560                                 int yShift = e.getY() - _dragStartY;
561                                 panMap(yShift, xShift);
562                         }
563                         _dragStartX = e.getX();
564                         _dragStartY = e.getY();
565                 }
566                 else
567                 {
568                         // Right click-and-drag for zoom
569                         if (_zoomDragFromX < 0 || _zoomDragFromY < 0)
570                         {
571                                 _zoomDragFromX = e.getX();
572                                 _zoomDragFromY = e.getY();
573                         }
574                         else
575                         {
576                                 _zoomDragToX = e.getX();
577                                 _zoomDragToY = e.getY();
578                                 _zoomDragging = true;
579                         }
580                         repaint();
581                 }
582         }
583
584
585         /**
586          * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
587          */
588         public void mouseMoved(MouseEvent e)
589         {
590                 // ignore
591         }
592 }