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