]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/gui/map/MapCanvas.java
Version 11, August 2010
[GpsPrune.git] / tim / prune / gui / map / MapCanvas.java
1 package tim.prune.gui.map;
2
3 import java.awt.BasicStroke;
4 import java.awt.BorderLayout;
5 import java.awt.Color;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.FontMetrics;
9 import java.awt.Graphics;
10 import java.awt.Graphics2D;
11 import java.awt.Image;
12 import java.awt.RenderingHints;
13 import java.awt.event.ActionEvent;
14 import java.awt.event.ActionListener;
15 import java.awt.event.ItemEvent;
16 import java.awt.event.ItemListener;
17 import java.awt.event.KeyEvent;
18 import java.awt.event.KeyListener;
19 import java.awt.event.MouseEvent;
20 import java.awt.event.MouseListener;
21 import java.awt.event.MouseMotionListener;
22 import java.awt.event.MouseWheelEvent;
23 import java.awt.event.MouseWheelListener;
24 import java.awt.image.BufferedImage;
25 import java.awt.image.RescaleOp;
26
27 import javax.swing.BorderFactory;
28 import javax.swing.BoxLayout;
29 import javax.swing.JButton;
30 import javax.swing.JCheckBox;
31 import javax.swing.JMenuItem;
32 import javax.swing.JPanel;
33 import javax.swing.JPopupMenu;
34 import javax.swing.JSlider;
35 import javax.swing.event.ChangeEvent;
36 import javax.swing.event.ChangeListener;
37
38 import tim.prune.App;
39 import tim.prune.DataSubscriber;
40 import tim.prune.FunctionLibrary;
41 import tim.prune.I18nManager;
42 import tim.prune.UpdateMessageBroker;
43 import tim.prune.config.ColourScheme;
44 import tim.prune.config.Config;
45 import tim.prune.data.Checker;
46 import tim.prune.data.Coordinate;
47 import tim.prune.data.DataPoint;
48 import tim.prune.data.DoubleRange;
49 import tim.prune.data.Latitude;
50 import tim.prune.data.Longitude;
51 import tim.prune.data.Selection;
52 import tim.prune.data.Track;
53 import tim.prune.data.TrackInfo;
54 import tim.prune.gui.IconManager;
55
56 /**
57  * Class for the map canvas, to display a background map and draw on it
58  */
59 public class MapCanvas extends JPanel implements MouseListener, MouseMotionListener, DataSubscriber,
60         KeyListener, MouseWheelListener
61 {
62         /** App object for callbacks */
63         private App _app = null;
64         /** Track object */
65         private Track _track = null;
66         /** TrackInfo object */
67         private TrackInfo _trackInfo = null;
68         /** Selection object */
69         private Selection _selection = null;
70         /** Previously selected point */
71         private int _prevSelectedPoint = -1;
72         /** Tile manager */
73         private MapTileManager _tileManager = new MapTileManager(this);
74         /** Image to display */
75         private BufferedImage _mapImage = null;
76         /** Slider for transparency */
77         private JSlider _transparencySlider = null;
78         /** Checkbox for scale bar */
79         private JCheckBox _scaleCheckBox = null;
80         /** Checkbox for maps */
81         private JCheckBox _mapCheckBox = null;
82         /** Checkbox for autopan */
83         private JCheckBox _autopanCheckBox = null;
84         /** Checkbox for connecting track points */
85         private JCheckBox _connectCheckBox = null;
86         /** Right-click popup menu */
87         private JPopupMenu _popup = null;
88         /** Top component panel */
89         private JPanel _topPanel = null;
90         /** Side component panel */
91         private JPanel _sidePanel = null;
92         /** Scale bar */
93         private ScaleBar _scaleBar = null;
94         /* Data */
95         private DoubleRange _latRange = null, _lonRange = null;
96         private DoubleRange _xRange = null, _yRange = null;
97         private boolean _recalculate = false;
98         /** Flag to check bounds on next paint */
99         private boolean _checkBounds = false;
100         /** Map position */
101         private MapPosition _mapPosition = null;
102         /** x coordinate of drag from point */
103         private int _dragFromX = -1;
104         /** y coordinate of drag from point */
105         private int _dragFromY = -1;
106         /** Flag set to true for right-click dragging */
107         private boolean _zoomDragging = false;
108         /** x coordinate of drag to point */
109         private int _dragToX = -1;
110         /** y coordinate of drag to point */
111         private int _dragToY = -1;
112         /** x coordinate of popup menu */
113         private int _popupMenuX = -1;
114         /** y coordinate of popup menu */
115         private int _popupMenuY = -1;
116         /** Flag to prevent showing too often the error message about loading maps */
117         private boolean _shownOsmErrorAlready = false;
118
119         /** Constant for click sensitivity when selecting nearest point */
120         private static final int CLICK_SENSITIVITY = 10;
121         /** Constant for pan distance from key presses */
122         private static final int PAN_DISTANCE = 20;
123         /** Constant for pan distance from autopan */
124         private static final int AUTOPAN_DISTANCE = 75;
125
126         // Colours
127         private static final Color COLOR_MESSAGES   = Color.GRAY;
128
129
130         /**
131          * Constructor
132          * @param inApp App object for callbacks
133          * @param inTrackInfo track info object
134          */
135         public MapCanvas(App inApp, TrackInfo inTrackInfo)
136         {
137                 _app = inApp;
138                 _trackInfo = inTrackInfo;
139                 _track = inTrackInfo.getTrack();
140                 _selection = inTrackInfo.getSelection();
141                 _mapPosition = new MapPosition();
142                 addMouseListener(this);
143                 addMouseMotionListener(this);
144                 addMouseWheelListener(this);
145                 addKeyListener(this);
146
147                 // Make listener for changes to controls
148                 ItemListener itemListener = new ItemListener() {
149                         public void itemStateChanged(ItemEvent e)
150                         {
151                                 _recalculate = true;
152                                 repaint();
153                         }
154                 };
155                 // Make special listener for changes to map checkbox
156                 ItemListener mapCheckListener = new ItemListener() {
157                         public void itemStateChanged(ItemEvent e)
158                         {
159                                 _tileManager.clearMemoryCaches();
160                                 _recalculate = true;
161                                 Config.setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
162                                 UpdateMessageBroker.informSubscribers(); // to let menu know
163                         }
164                 };
165                 _topPanel = new JPanel();
166                 _topPanel.setLayout(new FlowLayout());
167                 _topPanel.setOpaque(false);
168                 // Make slider for transparency
169                 _transparencySlider = new JSlider(0, 5, 0);
170                 _transparencySlider.setPreferredSize(new Dimension(100, 20));
171                 _transparencySlider.setMajorTickSpacing(1);
172                 _transparencySlider.setSnapToTicks(true);
173                 _transparencySlider.setOpaque(false);
174                 _transparencySlider.addChangeListener(new ChangeListener() {
175                         public void stateChanged(ChangeEvent e)
176                         {
177                                 _recalculate = true;
178                                 repaint();
179                         }
180                 });
181                 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
182                 _topPanel.add(_transparencySlider);
183                 // Add checkbox button for enabling scale bar
184                 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
185                 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
186                 _scaleCheckBox.setOpaque(false);
187                 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
188                 _scaleCheckBox.addItemListener(new ItemListener() {
189                         public void itemStateChanged(ItemEvent e) {
190                                 _scaleBar.setVisible(_scaleCheckBox.isSelected());
191                         }
192                 });
193                 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
194                 _topPanel.add(_scaleCheckBox);
195                 // Add checkbox button for enabling maps or not
196                 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
197                 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
198                 _mapCheckBox.setOpaque(false);
199                 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
200                 _mapCheckBox.addItemListener(mapCheckListener);
201                 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
202                 _topPanel.add(_mapCheckBox);
203                 // Add checkbox button for enabling autopan or not
204                 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
205                 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
206                 _autopanCheckBox.setOpaque(false);
207                 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
208                 _autopanCheckBox.addItemListener(itemListener);
209                 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
210                 _topPanel.add(_autopanCheckBox);
211                 // Add checkbox button for connecting points or not
212                 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
213                 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
214                 _connectCheckBox.setOpaque(false);
215                 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
216                 _connectCheckBox.addItemListener(itemListener);
217                 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
218                 _topPanel.add(_connectCheckBox);
219
220                 // Add zoom in, zoom out buttons
221                 _sidePanel = new JPanel();
222                 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
223                 _sidePanel.setOpaque(false);
224                 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
225                 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
226                 zoomInButton.setContentAreaFilled(false);
227                 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
228                 zoomInButton.addActionListener(new ActionListener() {
229                         public void actionPerformed(ActionEvent e)
230                         {
231                                 zoomIn();
232                         }
233                 });
234                 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
235                 _sidePanel.add(zoomInButton);
236                 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
237                 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
238                 zoomOutButton.setContentAreaFilled(false);
239                 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
240                 zoomOutButton.addActionListener(new ActionListener() {
241                         public void actionPerformed(ActionEvent e)
242                         {
243                                 zoomOut();
244                         }
245                 });
246                 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
247                 _sidePanel.add(zoomOutButton);
248
249                 // Bottom panel for scale bar
250                 _scaleBar = new ScaleBar();
251
252                 // add control panels to this one
253                 setLayout(new BorderLayout());
254                 _topPanel.setVisible(false);
255                 _sidePanel.setVisible(false);
256                 add(_topPanel, BorderLayout.NORTH);
257                 add(_sidePanel, BorderLayout.WEST);
258                 add(_scaleBar, BorderLayout.SOUTH);
259                 // Make popup menu
260                 makePopup();
261         }
262
263
264         /**
265          * Make the popup menu for right-clicking the map
266          */
267         private void makePopup()
268         {
269                 _popup = new JPopupMenu();
270                 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
271                 zoomInItem.addActionListener(new ActionListener() {
272                         public void actionPerformed(ActionEvent e)
273                         {
274                                 zoomIn();
275                         }});
276                 zoomInItem.setEnabled(true);
277                 _popup.add(zoomInItem);
278                 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
279                 zoomOutItem.addActionListener(new ActionListener() {
280                         public void actionPerformed(ActionEvent e)
281                         {
282                                 zoomOut();
283                         }});
284                 zoomOutItem.setEnabled(true);
285                 _popup.add(zoomOutItem);
286                 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
287                 zoomFullItem.addActionListener(new ActionListener() {
288                         public void actionPerformed(ActionEvent e)
289                         {
290                                 zoomToFit();
291                                 _recalculate = true;
292                                 repaint();
293                         }});
294                 zoomFullItem.setEnabled(true);
295                 _popup.add(zoomFullItem);
296                 _popup.addSeparator();
297                 // Set background
298                 JMenuItem setMapBgItem = new JMenuItem(
299                         I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
300                 setMapBgItem.addActionListener(new ActionListener() {
301                         public void actionPerformed(ActionEvent e)
302                         {
303                                 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
304                         }});
305                 _popup.add(setMapBgItem);
306                 // new point option
307                 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
308                 newPointItem.addActionListener(new ActionListener() {
309                         public void actionPerformed(ActionEvent e)
310                         {
311                                 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_popupMenuY, getHeight()));
312                                 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_popupMenuX, getWidth()));
313                                 _app.createPoint(new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
314                                         new Longitude(lon, Coordinate.FORMAT_NONE), null));
315                         }});
316                 newPointItem.setEnabled(true);
317                 _popup.add(newPointItem);
318         }
319
320
321         /**
322          * Zoom to fit the current data area
323          */
324         private void zoomToFit()
325         {
326                 _latRange = _track.getLatRange();
327                 _lonRange = _track.getLonRange();
328                 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
329                         MapUtils.getXFromLongitude(_lonRange.getMaximum()));
330                 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
331                         MapUtils.getYFromLatitude(_latRange.getMaximum()));
332                 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
333                                 getWidth(), getHeight());
334         }
335
336
337         /**
338          * Paint method
339          * @see java.awt.Canvas#paint(java.awt.Graphics)
340          */
341         public void paint(Graphics inG)
342         {
343                 super.paint(inG);
344                 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
345                         _mapImage = null;
346                 }
347                 if (_track.getNumPoints() > 0)
348                 {
349                         // Check for autopan if enabled / necessary
350                         if (_autopanCheckBox.isSelected())
351                         {
352                                 int selectedPoint = _selection.getCurrentPointIndex();
353                                 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
354                                 {
355                                         int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
356                                         int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
357                                         int panX = 0;
358                                         int panY = 0;
359                                         if (px < PAN_DISTANCE) {
360                                                 panX = px - AUTOPAN_DISTANCE;
361                                         }
362                                         else if (px > (getWidth()-PAN_DISTANCE)) {
363                                                 panX = AUTOPAN_DISTANCE + px - getWidth();
364                                         }
365                                         if (py < PAN_DISTANCE) {
366                                                 panY = py - AUTOPAN_DISTANCE;
367                                         }
368                                         if (py > (getHeight()-PAN_DISTANCE)) {
369                                                 panY = AUTOPAN_DISTANCE + py - getHeight();
370                                         }
371                                         if (panX != 0 || panY != 0) {
372                                                 _mapPosition.pan(panX, panY);
373                                         }
374                                 }
375                                 _prevSelectedPoint = selectedPoint;
376                         }
377
378                         // Draw the map contents if necessary
379                         if ((_mapImage == null || _recalculate))
380                         {
381                                 paintMapContents();
382                                 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
383                         }
384                         // Draw the prepared image onto the panel
385                         if (_mapImage != null) {
386                                 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
387                         }
388                         // Draw the zoom rectangle if necessary
389                         if (_zoomDragging)
390                         {
391                                 inG.setColor(Color.RED);
392                                 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
393                                 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
394                                 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
395                                 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
396                         }
397                 }
398                 else
399                 {
400                         inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
401                         inG.fillRect(0, 0, getWidth(), getHeight());
402                         inG.setColor(COLOR_MESSAGES);
403                         inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
404                         _scaleBar.updateScale(-1, 0);
405                 }
406                 // Draw slider etc on top
407                 paintChildren(inG);
408         }
409
410
411         /**
412          * Paint the map tiles and the points on to the _mapImage
413          */
414         private void paintMapContents()
415         {
416                 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
417                 {
418                         _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
419                 }
420
421                 // Clear map
422                 Graphics g = _mapImage.getGraphics();
423                 // Clear to background
424                 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
425                 g.fillRect(0, 0, getWidth(), getHeight());
426
427                 // Check whether maps are on or not
428                 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
429                 _mapCheckBox.setSelected(showMap);
430
431                 // reset error message
432                 if (!showMap) {_shownOsmErrorAlready = false;}
433                 _recalculate = false;
434                 // Only get map tiles if selected
435                 if (showMap)
436                 {
437                         // init tile cacher
438                         _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
439
440                         boolean loadingFailed = false;
441                         if (_mapImage == null) return;
442
443                         if (_tileManager.isOverzoomed())
444                         {
445                                 // display overzoom message
446                                 g.setColor(COLOR_MESSAGES);
447                                 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
448                         }
449                         else
450                         {
451                                 int numLayers = _tileManager.getNumLayers();
452                                 // Loop over tiles drawing each one
453                                 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
454                                 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
455                                 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
456                                 {
457                                         int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
458                                         for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
459                                         {
460                                                 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
461                                                 // Loop over layers
462                                                 for (int l=0; l<numLayers; l++)
463                                                 {
464                                                         Image image = _tileManager.getTile(l, tileX, tileY);
465                                                         if (image != null) {
466                                                                 g.drawImage(image, x, y, 256, 256, null);
467                                                         }
468                                                 }
469                                         }
470                                 }
471
472                                 // Make maps brighter / fainter
473                                 final float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.2f};
474                                 final float scaleFactor = scaleFactors[_transparencySlider.getValue()];
475                                 if (scaleFactor > 1.0f)
476                                 {
477                                         RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
478                                         hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
479                                         RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
480                                         op.filter(_mapImage, _mapImage);
481                                 }
482                         }
483                 }
484
485                 // Paint the track points on top
486                 int pointsPainted = 1;
487                 try
488                 {
489                         pointsPainted = paintPoints(g);
490                 }
491                 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
492                 }
493
494                 // free g
495                 g.dispose();
496
497                 // Zoom to fit if no points found
498                 if (pointsPainted <= 0 && _checkBounds) {
499                         zoomToFit();
500                         _recalculate = true;
501                         repaint();
502                 }
503                 _checkBounds = false;
504                 // enable / disable transparency slider
505                 _transparencySlider.setEnabled(showMap);
506         }
507
508
509         /**
510          * Paint the points using the given graphics object
511          * @param inG Graphics object to use for painting
512          * @return number of points painted, if any
513          */
514         private int paintPoints(Graphics inG)
515         {
516                 // Set up colours
517                 final Color pointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
518                 final Color rangeColour = Config.getColourScheme().getColour(ColourScheme.IDX_SELECTION);
519                 final Color currentColour = Config.getColourScheme().getColour(ColourScheme.IDX_PRIMARY);
520                 final Color secondColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
521                 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
522
523                 // try to set double line width for painting
524                 if (inG instanceof Graphics2D) {
525                         ((Graphics2D) inG).setStroke(new BasicStroke(2.0f));
526                 }
527                 int pointsPainted = 0;
528                 // draw track points
529                 inG.setColor(pointColour);
530                 int prevX = -1, prevY = -1;
531                 boolean connectPoints = _connectCheckBox.isSelected();
532                 boolean prevPointVisible = false, currPointVisible = false;
533                 boolean anyWaypoints = false;
534                 boolean isWaypoint = false;
535                 for (int i=0; i<_track.getNumPoints(); i++)
536                 {
537                         int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
538                         int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
539                         currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
540                         isWaypoint = _track.getPoint(i).isWaypoint();
541                         anyWaypoints = anyWaypoints || isWaypoint;
542                         if (currPointVisible)
543                         {
544                                 if (!isWaypoint)
545                                 {
546                                         // Draw rectangle for track point
547                                         if (_track.getPoint(i).getDeleteFlag()) {
548                                                 inG.setColor(currentColour);
549                                         }
550                                         else {
551                                                 inG.setColor(pointColour);
552                                         }
553                                         inG.drawRect(px-2, py-2, 3, 3);
554                                         pointsPainted++;
555                                 }
556                         }
557                         if (!isWaypoint)
558                         {
559                                 // Connect track points if either of them are visible
560                                 if (connectPoints && (currPointVisible || prevPointVisible)
561                                  && !(prevX == -1 && prevY == -1)
562                                  && !_track.getPoint(i).getSegmentStart())
563                                 {
564                                         inG.drawLine(prevX, prevY, px, py);
565                                 }
566                                 prevX = px; prevY = py;
567                         }
568                         prevPointVisible = currPointVisible;
569                 }
570
571                 // Loop over points, just drawing blobs for waypoints
572                 inG.setColor(textColour);
573                 FontMetrics fm = inG.getFontMetrics();
574                 int nameHeight = fm.getHeight();
575                 int width = getWidth();
576                 int height = getHeight();
577                 if (anyWaypoints) {
578                         for (int i=0; i<_track.getNumPoints(); i++)
579                         {
580                                 if (_track.getPoint(i).isWaypoint())
581                                 {
582                                         int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
583                                         int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
584                                         if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
585                                         {
586                                                 inG.fillRect(px-3, py-3, 6, 6);
587                                                 pointsPainted++;
588                                         }
589                                 }
590                         }
591                         // Loop over points again, now draw names for waypoints
592                         for (int i=0; i<_track.getNumPoints(); i++)
593                         {
594                                 if (_track.getPoint(i).isWaypoint())
595                                 {
596                                         int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
597                                         int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
598                                         if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
599                                         {
600                                                 // Figure out where to draw waypoint name so it doesn't obscure track
601                                                 String waypointName = _track.getPoint(i).getWaypointName();
602                                                 int nameWidth = fm.stringWidth(waypointName);
603                                                 boolean drawnName = false;
604                                                 // Make arrays for coordinates right left up down
605                                                 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
606                                                 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
607                                                 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
608                                                 {
609                                                         // Shift arrays for coordinates right left up down
610                                                         nameXs[0] += 2; nameXs[1] -= 2;
611                                                         nameYs[2] -= 2; nameYs[3] += 2;
612                                                         // Check each direction in turn right left up down
613                                                         for (int a=0; a<4; a++)
614                                                         {
615                                                                 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
616                                                                         && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
617                                                                         && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
618                                                                 {
619                                                                         // Found a rectangle to fit - draw name here and quit
620                                                                         inG.drawString(waypointName, nameXs[a], nameYs[a]);
621                                                                         drawnName = true;
622                                                                         break;
623                                                                 }
624                                                         }
625                                                 }
626                                         }
627                                 }
628                         }
629                 }
630                 // Loop over points, drawing blobs for photo points
631                 inG.setColor(secondColour);
632                 for (int i=0; i<_track.getNumPoints(); i++)
633                 {
634                         if (_track.getPoint(i).getPhoto() != null)
635                         {
636                                 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
637                                 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
638                                 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
639                                 {
640                                         inG.drawRect(px-1, py-1, 2, 2);
641                                         inG.drawRect(px-2, py-2, 4, 4);
642                                         pointsPainted++;
643                                 }
644                         }
645                 }
646
647                 // Draw selected range
648                 if (_selection.hasRangeSelected())
649                 {
650                         inG.setColor(rangeColour);
651                         for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
652                         {
653                                 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
654                                 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
655                                 inG.drawRect(px-1, py-1, 2, 2);
656                         }
657                 }
658
659                 // Draw selected point, crosshairs
660                 int selectedPoint = _selection.getCurrentPointIndex();
661                 if (selectedPoint >= 0)
662                 {
663                         int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
664                         int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
665                         inG.setColor(currentColour);
666                         // crosshairs
667                         inG.drawLine(px, 0, px, getHeight());
668                         inG.drawLine(0, py, getWidth(), py);
669                         // oval
670                         inG.drawOval(px - 2, py - 2, 4, 4);
671                         inG.drawOval(px - 3, py - 3, 6, 6);
672                 }
673                 // Return the number of points painted
674                 return pointsPainted;
675         }
676
677
678         /**
679          * Tests whether there are any dark pixels within the specified x,y rectangle
680          * @param inX left X coordinate
681          * @param inY bottom Y coordinate
682          * @param inWidth width of rectangle
683          * @param inHeight height of rectangle
684          * @param inTextColour colour of text
685          * @return true if the rectangle overlaps stuff too close to the given colour
686          */
687         private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
688         {
689                 // each of the colour channels must be further away than this to count as empty
690                 final int BRIGHTNESS_LIMIT = 80;
691                 final int textRGB = inTextColour.getRGB();
692                 final int textLow = textRGB & 255;
693                 final int textMid = (textRGB >> 8) & 255;
694                 final int textHigh = (textRGB >> 16) & 255;
695                 try
696                 {
697                         // loop over x coordinate of rectangle
698                         for (int x=0; x<inWidth; x++)
699                         {
700                                 // loop over y coordinate of rectangle
701                                 for (int y=0; y<inHeight; y++)
702                                 {
703                                         int pixelColor = _mapImage.getRGB(inX + x, inY - y);
704                                         // split into four components rgba
705                                         int pixLow = pixelColor & 255;
706                                         int pixMid = (pixelColor >> 8) & 255;
707                                         int pixHigh = (pixelColor >> 16) & 255;
708                                         //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
709                                         // If colours are too close in any channel then it's an overlap
710                                         if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
711                                                 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
712                                                 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
713                                 }
714                         }
715                 }
716                 catch (NullPointerException e) {
717                         // ignore null pointers, just return false
718                 }
719                 return false;
720         }
721
722
723         /**
724          * Inform that tiles have been updated and the map can be repainted
725          * @param inIsOk true if data loaded ok, false for error
726          */
727         public synchronized void tilesUpdated(boolean inIsOk)
728         {
729                 // Show message if loading failed (but not too many times)
730                 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
731                 {
732                         _shownOsmErrorAlready = true;
733                         // use separate thread to show message about failing to load osm images
734                         new Thread(new Runnable() {
735                                 public void run() {
736                                         try {Thread.sleep(500);} catch (InterruptedException ie) {}
737                                         _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
738                                 }
739                         }).start();
740                 }
741                 _recalculate = true;
742                 repaint();
743         }
744
745         /**
746          * Zoom out, if not already at minimum zoom
747          */
748         public void zoomOut()
749         {
750                 _mapPosition.zoomOut();
751                 _recalculate = true;
752                 repaint();
753         }
754
755         /**
756          * Zoom in, if not already at maximum zoom
757          */
758         public void zoomIn()
759         {
760                 _mapPosition.zoomIn();
761                 _recalculate = true;
762                 repaint();
763         }
764
765         /**
766          * Pan map
767          * @param inDeltaX x shift
768          * @param inDeltaY y shift
769          */
770         public void panMap(int inDeltaX, int inDeltaY)
771         {
772                 _mapPosition.pan(inDeltaX, inDeltaY);
773                 _recalculate = true;
774                 repaint();
775         }
776
777         /**
778          * @see javax.swing.JComponent#getMinimumSize()
779          */
780         public Dimension getMinimumSize()
781         {
782                 final Dimension minSize = new Dimension(512, 300);
783                 return minSize;
784         }
785
786         /**
787          * @see javax.swing.JComponent#getPreferredSize()
788          */
789         public Dimension getPreferredSize()
790         {
791                 return getMinimumSize();
792         }
793
794
795         /**
796          * Respond to mouse click events
797          * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
798          */
799         public void mouseClicked(MouseEvent inE)
800         {
801                 if (_track != null && _track.getNumPoints() > 0)
802                 {
803                          // select point if it's a left-click
804                         if (!inE.isMetaDown())
805                         {
806                                 if (inE.getClickCount() == 1)
807                                 {
808                                         // single click
809                                         int pointIndex = _track.getNearestPointIndex(
810                                                  _mapPosition.getXFromPixels(inE.getX(), getWidth()),
811                                                  _mapPosition.getYFromPixels(inE.getY(), getHeight()),
812                                                  _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
813                                         // Extend selection for shift-click
814                                         if (inE.isShiftDown()) {
815                                                 _trackInfo.extendSelection(pointIndex);
816                                         }
817                                         else {
818                                                 _trackInfo.selectPoint(pointIndex);
819                                         }
820                                 }
821                                 else if (inE.getClickCount() == 2) {
822                                         // double click
823                                         panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
824                                         zoomIn();
825                                 }
826                         }
827                         else
828                         {
829                                 // show the popup menu for right-clicks
830                                 _popupMenuX = inE.getX();
831                                 _popupMenuY = inE.getY();
832                                 _popup.show(this, _popupMenuX, _popupMenuY);
833                         }
834                 }
835         }
836
837         /**
838          * Ignore mouse enter events
839          * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
840          */
841         public void mouseEntered(MouseEvent inE)
842         {
843                 // ignore
844         }
845
846         /**
847          * Ignore mouse exited events
848          * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
849          */
850         public void mouseExited(MouseEvent inE)
851         {
852                 // ignore
853         }
854
855         /**
856          * Ignore mouse pressed events
857          * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
858          */
859         public void mousePressed(MouseEvent inE)
860         {
861                 // ignore
862         }
863
864         /**
865          * Respond to mouse released events
866          * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
867          */
868         public void mouseReleased(MouseEvent inE)
869         {
870                 _recalculate = true;
871                 if (_zoomDragging && Math.abs(_dragToX - _dragFromX) > 20 && Math.abs(_dragToY - _dragFromY) > 20)
872                 {
873                         //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
874                         _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
875                 }
876                 _dragFromX = _dragFromY = -1;
877                 _zoomDragging = false;
878                 repaint();
879         }
880
881         /**
882          * Respond to mouse drag events
883          * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
884          */
885         public void mouseDragged(MouseEvent inE)
886         {
887                 if (!inE.isMetaDown())
888                 {
889                         // Left mouse drag - pan map by appropriate amount
890                         _zoomDragging = false;
891                         if (_dragFromX != -1)
892                         {
893                                 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
894                                 _recalculate = true;
895                                 repaint();
896                         }
897                         _dragFromX = inE.getX();
898                         _dragFromY = inE.getY();
899                 }
900                 else
901                 {
902                         // Right-click and drag - draw rectangle and control zoom
903                         _zoomDragging = true;
904                         if (_dragFromX == -1) {
905                                 _dragFromX = inE.getX();
906                                 _dragFromY = inE.getY();
907                         }
908                         _dragToX = inE.getX();
909                         _dragToY = inE.getY();
910                         repaint();
911                 }
912         }
913
914         /**
915          * Respond to mouse move events without button pressed
916          * @param inEvent ignored
917          */
918         public void mouseMoved(MouseEvent inEvent)
919         {
920                 // ignore
921         }
922
923         /**
924          * Respond to status bar message from broker
925          * @param inMessage message, ignored
926          */
927         public void actionCompleted(String inMessage)
928         {
929                 // ignore
930         }
931
932         /**
933          * Respond to data updated message from broker
934          * @param inUpdateType type of update
935          */
936         public void dataUpdated(byte inUpdateType)
937         {
938                 _recalculate = true;
939                 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
940                         _checkBounds = true;
941                 }
942                 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
943                         _tileManager.resetConfig();
944                 }
945                 repaint();
946                 // enable or disable components
947                 boolean hasData = _track.getNumPoints() > 0;
948                 _topPanel.setVisible(hasData);
949                 _sidePanel.setVisible(hasData);
950                 // grab focus for the key presses
951                 this.requestFocus();
952         }
953
954         /**
955          * Respond to key presses on the map canvas
956          * @param inE key event
957          */
958         public void keyPressed(KeyEvent inE)
959         {
960                 int code = inE.getKeyCode();
961                 int currPointIndex = _selection.getCurrentPointIndex();
962                 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
963                 if (inE.isControlDown() || inE.isMetaDown())
964                 {
965                         // Check for arrow keys to zoom in and out
966                         if (code == KeyEvent.VK_UP)
967                                 zoomIn();
968                         else if (code == KeyEvent.VK_DOWN)
969                                 zoomOut();
970                         // Key nav for next/prev point
971                         else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
972                                 _trackInfo.selectPoint(currPointIndex-1);
973                         else if (code == KeyEvent.VK_RIGHT)
974                                 _trackInfo.selectPoint(currPointIndex+1);
975                         else if (code == KeyEvent.VK_PAGE_UP)
976                                 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
977                                         _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
978                         else if (code == KeyEvent.VK_PAGE_DOWN)
979                                 _trackInfo.selectPoint(Checker.getNextSegmentStart(
980                                         _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
981                         // Check for home and end
982                         else if (code == KeyEvent.VK_HOME)
983                                 _trackInfo.selectPoint(0);
984                         else if (code == KeyEvent.VK_END)
985                                 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
986                 }
987                 else
988                 {
989                         // Check for arrow keys to pan
990                         int upwardsPan = 0;
991                         if (code == KeyEvent.VK_UP)
992                                 upwardsPan = -PAN_DISTANCE;
993                         else if (code == KeyEvent.VK_DOWN)
994                                 upwardsPan = PAN_DISTANCE;
995                         int rightwardsPan = 0;
996                         if (code == KeyEvent.VK_RIGHT)
997                                 rightwardsPan = PAN_DISTANCE;
998                         else if (code == KeyEvent.VK_LEFT)
999                                 rightwardsPan = -PAN_DISTANCE;
1000                         panMap(rightwardsPan, upwardsPan);
1001                         // Check for backspace key to delete current point (delete key already handled by menu)
1002                         if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
1003                                 _app.deleteCurrentPoint();
1004                         }
1005                 }
1006         }
1007
1008         /**
1009          * @param inE key released event, ignored
1010          */
1011         public void keyReleased(KeyEvent e)
1012         {
1013                 // ignore
1014         }
1015
1016         /**
1017          * @param inE key typed event, ignored
1018          */
1019         public void keyTyped(KeyEvent inE)
1020         {
1021                 // ignore
1022         }
1023
1024         /**
1025          * @param inE mouse wheel event indicating scroll direction
1026          */
1027         public void mouseWheelMoved(MouseWheelEvent inE)
1028         {
1029                 int clicks = inE.getWheelRotation();
1030                 if (clicks < 0)
1031                         zoomIn();
1032                 else if (clicks > 0)
1033                         zoomOut();
1034         }
1035
1036         /**
1037          * @return current map position
1038          */
1039         public MapPosition getMapPosition()
1040         {
1041                 return _mapPosition;
1042         }
1043 }