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