]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/gui/MapChart.java
e705f843aab3c74eb688f173766d5bbb5e7c8820
[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 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;
60
61
62         /**
63          * Constructor
64          * @param inApp App object for callbacks
65          * @param inTrackInfo track info object
66          */
67         public MapChart(App inApp, TrackInfo inTrackInfo)
68         {
69                 super(inTrackInfo);
70                 _app = inApp;
71                 makePopup();
72                 addMouseListener(this);
73                 addMouseWheelListener(this);
74                 addMouseMotionListener(this);
75                 setFocusable(true);
76                 addKeyListener(this);
77                 MINIMUM_SIZE = new Dimension(200, 250);
78                 _zoomScale = 1.0;
79         }
80
81
82         /**
83          * Override track updating to refresh image
84          */
85         public void dataUpdated(byte inUpdateType)
86         {
87                 // Check if number of points has changed or data has been edited
88                 if (_track.getNumPoints() != _numPoints || (inUpdateType & DATA_EDITED) > 0)
89                 {
90                         _image = null;
91                         _lastSelectedPoint = -1;
92                         _numPoints = _track.getNumPoints();
93                 }
94                 super.dataUpdated(inUpdateType);
95         }
96
97
98         /**
99          * Override paint method to draw map
100          */
101         public void paint(Graphics g)
102         {
103                 if (_track == null)
104                 {
105                         super.paint(g);
106                         return;
107                 }
108
109                 int width = getWidth();
110                 int height = getHeight();
111                 int x, y;
112
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));
118                 _scale = scaleX;
119                 if (scaleY > _scale) _scale = scaleY;
120
121                 // Autopan if necessary
122                 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
123                 if (_autoPanMenuItem.isSelected() && selectedPoint >= 0 && selectedPoint != _lastSelectedPoint)
124                 {
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)
129                         {
130                                 // autopan left
131                                 _offsetX -= (width / 4 - x) * _scale / _zoomScale;
132                                 _image = null;
133                         }
134                         else if (x >= (width - BORDER_WIDTH))
135                         {
136                                 // autopan right
137                                 _offsetX += (x - width * 3/4) * _scale / _zoomScale;
138                                 _image = null;
139                         }
140                         if (y <= BORDER_WIDTH)
141                         {
142                                 // autopan up
143                                 _offsetY += (height / 4 - y) * _scale / _zoomScale;
144                                 _image = null;
145                         }
146                         else if (y >= (height - BORDER_WIDTH))
147                         {
148                                 // autopan down
149                                 _offsetY -= (y - height * 3/4) * _scale / _zoomScale;
150                                 _image = null;
151                         }
152                 }
153                 _lastSelectedPoint = selectedPoint;
154
155                 // Create background if necessary
156                 if (_image == null || width != _image.getWidth() || height != _image.getHeight())
157                 {
158                         createBackgroundImage();
159                 }
160                 // return if image has been set to null by other thread
161                 if (_image == null) {return;}
162
163                 // draw buffered image onto g
164                 g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
165
166                 // draw selected range, if any
167                 if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
168                 {
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++)
173                         {
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)
178                                 {
179                                         g.drawOval(x - 2, y - 2, 4, 4);
180                                 }
181                         }
182                 }
183
184                 // Highlight selected point
185                 if (selectedPoint >= 0 && !_zoomDragging)
186                 {
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)
192                         {
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);
196
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);
200                         }
201                 }
202
203                 // Draw rectangle for dragging zoom area
204                 if (_zoomDragging)
205                 {
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);
211                 }
212
213                 // Attempt to grab keyboard focus if possible
214                 //this.requestFocus();
215         }
216
217
218         /**
219          * Draw the map onto an offscreen image
220          */
221         private void createBackgroundImage()
222         {
223                 int width = getWidth();
224                 int height = getHeight();
225                 int x, y;
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);
230
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++)
237                 {
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)
242                         {
243                                 bufferedG.drawOval(x - 2, y - 2, 4, 4);
244                         }
245                 }
246
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++)
253                 {
254                         DataPoint point = _track.getPoint(i);
255                         String waypointName = point.getWaypointName();
256                         if (waypointName != null && !waypointName.equals(""))
257                         {
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)
265                                 {
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))
270                                         {
271                                                 double nameAngle = 0.3;
272                                                 double nameRadius = 1.0;
273                                                 boolean drawnName = false;
274                                                 while (!drawnName)
275                                                 {
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)
280                                                         {
281                                                                 // name can fit in grid - does it overlap data points?
282                                                                 if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
283                                                                 {
284                                                                         bufferedG.drawString(waypointName, nameX, nameY);
285                                                                         drawnName = true;
286                                                                         numWaypointNamesShown++;
287                                                                 }
288                                                         }
289                                                         nameAngle += 0.08;
290                                                         nameRadius += 0.2;
291                                                         // wasn't room within the radius, so don't print name
292                                                         if (nameRadius > 50.0)
293                                                         {
294                                                                 drawnName = true;
295                                                         }
296                                                 }
297                                         }
298                                 }
299                         }
300                 }
301         }
302
303
304         /**
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
311          */
312         private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
313         {
314                 try
315                 {
316                         // loop over x coordinate of rectangle
317                         for (int x=0; x<inWidth; x++)
318                         {
319                                 // loop over y coordinate of rectangle
320                                 for (int y=0; y<inHeight; y++)
321                                 {
322                                         int pixelColor = _image.getRGB(inX + x, inY - y);
323                                         if (pixelColor != -1) return true;
324                                 }
325                         }
326                 }
327                 catch (NullPointerException e) {
328                         // ignore null pointers, just return false
329                 }
330                 return false;
331         }
332
333
334         /**
335          * Make the popup menu for right-clicking the map
336          */
337         private void makePopup()
338         {
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)
343                         {
344                                 zoomMap(true);
345                         }});
346                 zoomIn.setEnabled(true);
347                 _popup.add(zoomIn);
348                 JMenuItem zoomOut = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
349                 zoomOut.addActionListener(new ActionListener() {
350                         public void actionPerformed(ActionEvent e)
351                         {
352                                 zoomMap(false);
353                         }});
354                 zoomOut.setEnabled(true);
355                 _popup.add(zoomOut);
356                 JMenuItem zoomFull = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
357                 zoomFull.addActionListener(new ActionListener() {
358                         public void actionPerformed(ActionEvent e)
359                         {
360                                 zoomToFullScale();
361                         }});
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);
367         }
368
369
370         /**
371          * Zoom map to full scale
372          */
373         private void zoomToFullScale()
374         {
375                 _zoomScale = 1.0;
376                 _offsetX = 0.0;
377                 _offsetY = 0.0;
378                 _numPoints = 0;
379                 dataUpdated(DataSubscriber.ALL);
380         }
381
382
383         /**
384          * Zoom map either in or out by one step
385          * @param inZoomIn true to zoom in, false for out
386          */
387         private void zoomMap(boolean inZoomIn)
388         {
389                 if (inZoomIn)
390                 {
391                         // Zoom in
392                         _zoomScale *= ZOOM_SCALE_FACTOR;
393                 }
394                 else
395                 {
396                         // Zoom out
397                         _zoomScale /= ZOOM_SCALE_FACTOR;
398                         if (_zoomScale < 0.5) _zoomScale = 0.5;
399                 }
400                 _numPoints = 0;
401                 dataUpdated(DataSubscriber.ALL);
402         }
403
404
405         /**
406          * Pan the map by the specified amounts
407          * @param inUp upwards pan
408          * @param inRight rightwards pan
409          */
410         private void panMap(int inUp, int inRight)
411         {
412                 double panFactor = _scale / _zoomScale;
413                 _offsetY = _offsetY + (inUp * panFactor);
414                 _offsetX = _offsetX - (inRight * panFactor);
415                 // Limit pan to sensible range??
416                 _numPoints = 0;
417                 _image = null;
418                 repaint();
419         }
420
421
422         /**
423          * React to click on map display
424          */
425         public void mouseClicked(MouseEvent e)
426         {
427                 this.requestFocus();
428                 if (_track != null)
429                 {
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))
435                         {
436                                 // Check left click or right click
437                                 if (e.isMetaDown())
438                                 {
439                                         // Only show popup if track has data
440                                         if (_track != null && _track.getNumPoints() > 0)
441                                                 _popup.show(this, e.getX(), e.getY());
442                                 }
443                                 else
444                                 {
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);
452                                 }
453                         }
454                 }
455         }
456
457
458         /**
459          * Respond to mouse released to reset dragging
460          * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
461          */
462         public void mouseReleased(MouseEvent e)
463         {
464                 _dragStartX = _dragStartY = -1;
465                 if (e.isMetaDown())
466                 {
467                         if (_zoomDragFromX >= 0 || _zoomDragFromY >= 0)
468                         {
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
478                                 panMap(yPan, xPan);
479                                 // Then zoom in and request repaint
480                                 _zoomScale = _zoomScale * extraZoom;
481                                 _image = null;
482                                 repaint();
483                         }
484                         _zoomDragFromX = _zoomDragFromY = -1;
485                         _zoomDragging = false;
486                 }
487         }
488
489
490         /**
491          * Respond to mouse wheel events to zoom the map
492          * @see java.awt.event.MouseWheelListener#mouseWheelMoved(java.awt.event.MouseWheelEvent)
493          */
494         public void mouseWheelMoved(MouseWheelEvent e)
495         {
496                 zoomMap(e.getWheelRotation() < 0);
497         }
498
499
500         /**
501          * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
502          */
503         public void keyPressed(KeyEvent e)
504         {
505                 int code = e.getKeyCode();
506                 // Check for meta key
507                 if (e.isControlDown())
508                 {
509                         // Check for arrow keys to zoom in and out
510                         if (code == KeyEvent.VK_UP)
511                                 zoomMap(true);
512                         else if (code == KeyEvent.VK_DOWN)
513                                 zoomMap(false);
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();
519                 }
520                 else
521                 {
522                         // Check for arrow keys to pan
523                         int upwardsPan = 0;
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)
536                         {
537                                 _app.deleteCurrentPoint();
538                                 // reset last selected point to trigger autopan
539                                 _lastSelectedPoint = -1;
540                         }
541                 }
542         }
543
544
545         /**
546          * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
547          */
548         public void keyReleased(KeyEvent e)
549         {
550                 // ignore
551         }
552
553
554         /**
555          * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
556          */
557         public void keyTyped(KeyEvent e)
558         {
559                 // ignore
560         }
561
562
563         /**
564          * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
565          */
566         public void mouseDragged(MouseEvent e)
567         {
568                 if (!e.isMetaDown())
569                 {
570                         if (_dragStartX > 0)
571                         {
572                                 int xShift = e.getX() - _dragStartX;
573                                 int yShift = e.getY() - _dragStartY;
574                                 panMap(yShift, xShift);
575                         }
576                         _dragStartX = e.getX();
577                         _dragStartY = e.getY();
578                 }
579                 else
580                 {
581                         // Right click-and-drag for zoom
582                         if (_zoomDragFromX < 0 || _zoomDragFromY < 0)
583                         {
584                                 _zoomDragFromX = e.getX();
585                                 _zoomDragFromY = e.getY();
586                         }
587                         else
588                         {
589                                 _zoomDragToX = e.getX();
590                                 _zoomDragToY = e.getY();
591                                 _zoomDragging = true;
592                         }
593                         repaint();
594                 }
595         }
596
597
598         /**
599          * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
600          */
601         public void mouseMoved(MouseEvent e)
602         {
603                 // ignore
604         }
605 }