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