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