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