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