1 package tim.prune.gui.map;
3 import java.awt.BasicStroke;
4 import java.awt.BorderLayout;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.FontMetrics;
9 import java.awt.Graphics;
10 import java.awt.Graphics2D;
11 import java.awt.Image;
12 import java.awt.RenderingHints;
13 import java.awt.event.ActionEvent;
14 import java.awt.event.ActionListener;
15 import java.awt.event.ItemEvent;
16 import java.awt.event.ItemListener;
17 import java.awt.event.KeyEvent;
18 import java.awt.event.KeyListener;
19 import java.awt.event.MouseEvent;
20 import java.awt.event.MouseListener;
21 import java.awt.event.MouseMotionListener;
22 import java.awt.event.MouseWheelEvent;
23 import java.awt.event.MouseWheelListener;
24 import java.awt.image.BufferedImage;
25 import java.awt.image.RescaleOp;
27 import javax.swing.BorderFactory;
28 import javax.swing.BoxLayout;
29 import javax.swing.JButton;
30 import javax.swing.JCheckBox;
31 import javax.swing.JMenuItem;
32 import javax.swing.JPanel;
33 import javax.swing.JPopupMenu;
34 import javax.swing.JSlider;
35 import javax.swing.event.ChangeEvent;
36 import javax.swing.event.ChangeListener;
39 import tim.prune.DataSubscriber;
40 import tim.prune.FunctionLibrary;
41 import tim.prune.I18nManager;
42 import tim.prune.UpdateMessageBroker;
43 import tim.prune.config.ColourScheme;
44 import tim.prune.config.Config;
45 import tim.prune.data.Checker;
46 import tim.prune.data.Coordinate;
47 import tim.prune.data.DataPoint;
48 import tim.prune.data.DoubleRange;
49 import tim.prune.data.Latitude;
50 import tim.prune.data.Longitude;
51 import tim.prune.data.Selection;
52 import tim.prune.data.Track;
53 import tim.prune.data.TrackInfo;
54 import tim.prune.gui.IconManager;
57 * Class for the map canvas, to display a background map and draw on it
59 public class MapCanvas extends JPanel implements MouseListener, MouseMotionListener, DataSubscriber,
60 KeyListener, MouseWheelListener
62 /** App object for callbacks */
63 private App _app = null;
65 private Track _track = null;
66 /** TrackInfo object */
67 private TrackInfo _trackInfo = null;
68 /** Selection object */
69 private Selection _selection = null;
70 /** Previously selected point */
71 private int _prevSelectedPoint = -1;
73 private MapTileManager _tileManager = new MapTileManager(this);
74 /** Image to display */
75 private BufferedImage _mapImage = null;
76 /** Slider for transparency */
77 private JSlider _transparencySlider = null;
78 /** Checkbox for scale bar */
79 private JCheckBox _scaleCheckBox = null;
80 /** Checkbox for maps */
81 private JCheckBox _mapCheckBox = null;
82 /** Checkbox for autopan */
83 private JCheckBox _autopanCheckBox = null;
84 /** Checkbox for connecting track points */
85 private JCheckBox _connectCheckBox = null;
86 /** Right-click popup menu */
87 private JPopupMenu _popup = null;
88 /** Top component panel */
89 private JPanel _topPanel = null;
90 /** Side component panel */
91 private JPanel _sidePanel = null;
93 private ScaleBar _scaleBar = null;
95 private DoubleRange _latRange = null, _lonRange = null;
96 private DoubleRange _xRange = null, _yRange = null;
97 private boolean _recalculate = false;
98 /** Flag to check bounds on next paint */
99 private boolean _checkBounds = false;
101 private MapPosition _mapPosition = null;
102 /** x coordinate of drag from point */
103 private int _dragFromX = -1;
104 /** y coordinate of drag from point */
105 private int _dragFromY = -1;
106 /** x coordinate of drag to point */
107 private int _dragToX = -1;
108 /** y coordinate of drag to point */
109 private int _dragToY = -1;
110 /** x coordinate of popup menu */
111 private int _popupMenuX = -1;
112 /** y coordinate of popup menu */
113 private int _popupMenuY = -1;
114 /** Flag to prevent showing too often the error message about loading maps */
115 private boolean _shownOsmErrorAlready = false;
116 /** Current drawing mode */
117 private int _drawMode = MODE_DEFAULT;
119 /** Constant for click sensitivity when selecting nearest point */
120 private static final int CLICK_SENSITIVITY = 10;
121 /** Constant for pan distance from key presses */
122 private static final int PAN_DISTANCE = 20;
123 /** Constant for pan distance from autopan */
124 private static final int AUTOPAN_DISTANCE = 75;
127 private static final Color COLOR_MESSAGES = Color.GRAY;
130 private static final int MODE_DEFAULT = 0;
131 private static final int MODE_ZOOM_RECT = 1;
132 private static final int MODE_DRAW_POINTS_START = 2;
133 private static final int MODE_DRAW_POINTS_CONT = 3;
137 * @param inApp App object for callbacks
138 * @param inTrackInfo track info object
140 public MapCanvas(App inApp, TrackInfo inTrackInfo)
143 _trackInfo = inTrackInfo;
144 _track = inTrackInfo.getTrack();
145 _selection = inTrackInfo.getSelection();
146 _mapPosition = new MapPosition();
147 addMouseListener(this);
148 addMouseMotionListener(this);
149 addMouseWheelListener(this);
150 addKeyListener(this);
152 // Make listener for changes to controls
153 ItemListener itemListener = new ItemListener() {
154 public void itemStateChanged(ItemEvent e)
160 // Make special listener for changes to map checkbox
161 ItemListener mapCheckListener = new ItemListener() {
162 public void itemStateChanged(ItemEvent e)
164 _tileManager.clearMemoryCaches();
166 Config.setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
167 UpdateMessageBroker.informSubscribers(); // to let menu know
170 _topPanel = new JPanel();
171 _topPanel.setLayout(new FlowLayout());
172 _topPanel.setOpaque(false);
173 // Make slider for transparency
174 _transparencySlider = new JSlider(0, 5, 0);
175 _transparencySlider.setPreferredSize(new Dimension(100, 20));
176 _transparencySlider.setMajorTickSpacing(1);
177 _transparencySlider.setSnapToTicks(true);
178 _transparencySlider.setOpaque(false);
179 _transparencySlider.addChangeListener(new ChangeListener() {
180 public void stateChanged(ChangeEvent e)
186 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
187 _topPanel.add(_transparencySlider);
188 // Add checkbox button for enabling scale bar
189 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
190 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
191 _scaleCheckBox.setOpaque(false);
192 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
193 _scaleCheckBox.addItemListener(new ItemListener() {
194 public void itemStateChanged(ItemEvent e) {
195 _scaleBar.setVisible(_scaleCheckBox.isSelected());
198 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
199 _topPanel.add(_scaleCheckBox);
200 // Add checkbox button for enabling maps or not
201 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
202 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
203 _mapCheckBox.setOpaque(false);
204 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
205 _mapCheckBox.addItemListener(mapCheckListener);
206 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
207 _topPanel.add(_mapCheckBox);
208 // Add checkbox button for enabling autopan or not
209 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
210 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
211 _autopanCheckBox.setOpaque(false);
212 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
213 _autopanCheckBox.addItemListener(itemListener);
214 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
215 _topPanel.add(_autopanCheckBox);
216 // Add checkbox button for connecting points or not
217 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
218 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
219 _connectCheckBox.setOpaque(false);
220 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
221 _connectCheckBox.addItemListener(itemListener);
222 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
223 _topPanel.add(_connectCheckBox);
225 // Add zoom in, zoom out buttons
226 _sidePanel = new JPanel();
227 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
228 _sidePanel.setOpaque(false);
229 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
230 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
231 zoomInButton.setContentAreaFilled(false);
232 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
233 zoomInButton.addActionListener(new ActionListener() {
234 public void actionPerformed(ActionEvent e)
239 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
240 _sidePanel.add(zoomInButton);
241 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
242 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
243 zoomOutButton.setContentAreaFilled(false);
244 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
245 zoomOutButton.addActionListener(new ActionListener() {
246 public void actionPerformed(ActionEvent e)
251 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
252 _sidePanel.add(zoomOutButton);
254 // Bottom panel for scale bar
255 _scaleBar = new ScaleBar();
257 // add control panels to this one
258 setLayout(new BorderLayout());
259 _topPanel.setVisible(false);
260 _sidePanel.setVisible(false);
261 add(_topPanel, BorderLayout.NORTH);
262 add(_sidePanel, BorderLayout.WEST);
263 add(_scaleBar, BorderLayout.SOUTH);
270 * Make the popup menu for right-clicking the map
272 private void makePopup()
274 _popup = new JPopupMenu();
275 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
276 zoomInItem.addActionListener(new ActionListener() {
277 public void actionPerformed(ActionEvent e)
279 panMap((_popupMenuX - getWidth()/2)/2, (_popupMenuY - getHeight()/2)/2);
282 _popup.add(zoomInItem);
283 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
284 zoomOutItem.addActionListener(new ActionListener() {
285 public void actionPerformed(ActionEvent e)
287 panMap(-(_popupMenuX - getWidth()/2), -(_popupMenuY - getHeight()/2));
290 _popup.add(zoomOutItem);
291 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
292 zoomFullItem.addActionListener(new ActionListener() {
293 public void actionPerformed(ActionEvent e)
299 _popup.add(zoomFullItem);
300 _popup.addSeparator();
302 JMenuItem setMapBgItem = new JMenuItem(
303 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
304 setMapBgItem.addActionListener(new ActionListener() {
305 public void actionPerformed(ActionEvent e)
307 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
309 _popup.add(setMapBgItem);
311 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
312 newPointItem.addActionListener(new ActionListener() {
313 public void actionPerformed(ActionEvent e)
315 _app.createPoint(createPointFromClick(_popupMenuX, _popupMenuY));
317 _popup.add(newPointItem);
319 JMenuItem drawPointsItem = new JMenuItem(I18nManager.getText("menu.map.drawpoints"));
320 drawPointsItem.addActionListener(new ActionListener() {
321 public void actionPerformed(ActionEvent e)
323 _drawMode = MODE_DRAW_POINTS_START;
326 _popup.add(drawPointsItem);
331 * Zoom to fit the current data area
333 private void zoomToFit()
335 _latRange = _track.getLatRange();
336 _lonRange = _track.getLonRange();
337 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
338 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
339 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
340 MapUtils.getYFromLatitude(_latRange.getMaximum()));
341 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
342 getWidth(), getHeight());
348 * @see java.awt.Canvas#paint(java.awt.Graphics)
350 public void paint(Graphics inG)
353 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
356 if (_track.getNumPoints() > 0)
358 // Check for autopan if enabled / necessary
359 if (_autopanCheckBox.isSelected())
361 int selectedPoint = _selection.getCurrentPointIndex();
362 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
364 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
365 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
368 if (px < PAN_DISTANCE) {
369 panX = px - AUTOPAN_DISTANCE;
371 else if (px > (getWidth()-PAN_DISTANCE)) {
372 panX = AUTOPAN_DISTANCE + px - getWidth();
374 if (py < PAN_DISTANCE) {
375 panY = py - AUTOPAN_DISTANCE;
377 if (py > (getHeight()-PAN_DISTANCE)) {
378 panY = AUTOPAN_DISTANCE + py - getHeight();
380 if (panX != 0 || panY != 0) {
381 _mapPosition.pan(panX, panY);
384 _prevSelectedPoint = selectedPoint;
387 // Draw the map contents if necessary
388 if ((_mapImage == null || _recalculate))
391 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
393 // Draw the prepared image onto the panel
394 if (_mapImage != null) {
395 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
397 // Draw the zoom rectangle if necessary
398 if (_drawMode == MODE_ZOOM_RECT)
400 inG.setColor(Color.RED);
401 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
402 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
403 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
404 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
406 else if (_drawMode == MODE_DRAW_POINTS_CONT)
408 // draw line to mouse position to show drawing mode
409 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT));
410 int prevIndex = _track.getNumPoints()-1;
411 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(prevIndex));
412 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(prevIndex));
413 inG.drawLine(px, py, _dragToX, _dragToY);
418 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
419 inG.fillRect(0, 0, getWidth(), getHeight());
420 inG.setColor(COLOR_MESSAGES);
421 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
422 _scaleBar.updateScale(-1, 0);
424 // Draw slider etc on top
430 * Paint the map tiles and the points on to the _mapImage
432 private void paintMapContents()
434 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
436 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
440 Graphics g = _mapImage.getGraphics();
441 // Clear to background
442 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
443 g.fillRect(0, 0, getWidth(), getHeight());
445 // Check whether maps are on or not
446 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
447 _mapCheckBox.setSelected(showMap);
449 // reset error message
450 if (!showMap) {_shownOsmErrorAlready = false;}
451 _recalculate = false;
452 // Only get map tiles if selected
456 _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
458 boolean loadingFailed = false;
459 if (_mapImage == null) return;
461 if (_tileManager.isOverzoomed())
463 // display overzoom message
464 g.setColor(COLOR_MESSAGES);
465 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
469 int numLayers = _tileManager.getNumLayers();
470 // Loop over tiles drawing each one
471 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
472 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
473 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
475 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
476 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
478 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
480 for (int l=0; l<numLayers; l++)
482 Image image = _tileManager.getTile(l, tileX, tileY);
484 g.drawImage(image, x, y, 256, 256, null);
490 // Make maps brighter / fainter
491 final float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.2f};
492 final float scaleFactor = scaleFactors[_transparencySlider.getValue()];
493 if (scaleFactor > 1.0f)
495 RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
496 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
497 RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
498 op.filter(_mapImage, _mapImage);
503 // Paint the track points on top
504 int pointsPainted = 1;
507 pointsPainted = paintPoints(g);
509 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
515 // Zoom to fit if no points found
516 if (pointsPainted <= 0 && _checkBounds) {
521 _checkBounds = false;
522 // enable / disable transparency slider
523 _transparencySlider.setEnabled(showMap);
528 * Paint the points using the given graphics object
529 * @param inG Graphics object to use for painting
530 * @return number of points painted, if any
532 private int paintPoints(Graphics inG)
535 final Color pointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
536 final Color rangeColour = Config.getColourScheme().getColour(ColourScheme.IDX_SELECTION);
537 final Color currentColour = Config.getColourScheme().getColour(ColourScheme.IDX_PRIMARY);
538 final Color secondColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
539 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
541 // try to set line width for painting
542 if (inG instanceof Graphics2D)
544 int lineWidth = Config.getConfigInt(Config.KEY_LINE_WIDTH);
545 if (lineWidth < 1 || lineWidth > 4) {lineWidth = 2;}
546 ((Graphics2D) inG).setStroke(new BasicStroke(lineWidth));
548 int pointsPainted = 0;
550 inG.setColor(pointColour);
551 int prevX = -1, prevY = -1;
552 boolean connectPoints = _connectCheckBox.isSelected();
553 boolean prevPointVisible = false, currPointVisible = false;
554 boolean anyWaypoints = false;
555 boolean isWaypoint = false;
556 for (int i=0; i<_track.getNumPoints(); i++)
558 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
559 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
560 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
561 isWaypoint = _track.getPoint(i).isWaypoint();
562 anyWaypoints = anyWaypoints || isWaypoint;
563 if (currPointVisible)
567 // Draw rectangle for track point
568 if (_track.getPoint(i).getDeleteFlag()) {
569 inG.setColor(currentColour);
572 inG.setColor(pointColour);
574 inG.drawRect(px-2, py-2, 3, 3);
580 // Connect track points if either of them are visible
581 if (connectPoints && (currPointVisible || prevPointVisible)
582 && !(prevX == -1 && prevY == -1)
583 && !_track.getPoint(i).getSegmentStart())
585 inG.drawLine(prevX, prevY, px, py);
587 prevX = px; prevY = py;
589 prevPointVisible = currPointVisible;
592 // Loop over points, just drawing blobs for waypoints
593 inG.setColor(textColour);
594 FontMetrics fm = inG.getFontMetrics();
595 int nameHeight = fm.getHeight();
596 int width = getWidth();
597 int height = getHeight();
599 for (int i=0; i<_track.getNumPoints(); i++)
601 if (_track.getPoint(i).isWaypoint())
603 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
604 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
605 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
607 inG.fillRect(px-3, py-3, 6, 6);
612 // Loop over points again, now draw names for waypoints
613 for (int i=0; i<_track.getNumPoints(); i++)
615 if (_track.getPoint(i).isWaypoint())
617 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
618 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
619 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
621 // Figure out where to draw waypoint name so it doesn't obscure track
622 String waypointName = _track.getPoint(i).getWaypointName();
623 int nameWidth = fm.stringWidth(waypointName);
624 boolean drawnName = false;
625 // Make arrays for coordinates right left up down
626 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
627 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
628 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
630 // Shift arrays for coordinates right left up down
631 nameXs[0] += 2; nameXs[1] -= 2;
632 nameYs[2] -= 2; nameYs[3] += 2;
633 // Check each direction in turn right left up down
634 for (int a=0; a<4; a++)
636 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
637 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
638 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
640 // Found a rectangle to fit - draw name here and quit
641 inG.drawString(waypointName, nameXs[a], nameYs[a]);
651 // Loop over points, drawing blobs for photo / audio points
652 inG.setColor(secondColour);
653 for (int i=0; i<_track.getNumPoints(); i++)
655 if (_track.getPoint(i).hasMedia())
657 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
658 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
659 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
661 inG.drawRect(px-1, py-1, 2, 2);
662 inG.drawRect(px-2, py-2, 4, 4);
668 // Draw selected range
669 if (_selection.hasRangeSelected())
671 inG.setColor(rangeColour);
672 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
674 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
675 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
676 inG.drawRect(px-1, py-1, 2, 2);
680 // Draw selected point, crosshairs
681 int selectedPoint = _selection.getCurrentPointIndex();
682 if (selectedPoint >= 0)
684 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
685 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
686 inG.setColor(currentColour);
688 inG.drawLine(px, 0, px, getHeight());
689 inG.drawLine(0, py, getWidth(), py);
691 inG.drawOval(px - 2, py - 2, 4, 4);
692 inG.drawOval(px - 3, py - 3, 6, 6);
694 // Return the number of points painted
695 return pointsPainted;
700 * Tests whether there are any dark pixels within the specified x,y rectangle
701 * @param inX left X coordinate
702 * @param inY bottom Y coordinate
703 * @param inWidth width of rectangle
704 * @param inHeight height of rectangle
705 * @param inTextColour colour of text
706 * @return true if the rectangle overlaps stuff too close to the given colour
708 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
710 // each of the colour channels must be further away than this to count as empty
711 final int BRIGHTNESS_LIMIT = 80;
712 final int textRGB = inTextColour.getRGB();
713 final int textLow = textRGB & 255;
714 final int textMid = (textRGB >> 8) & 255;
715 final int textHigh = (textRGB >> 16) & 255;
718 // loop over x coordinate of rectangle
719 for (int x=0; x<inWidth; x++)
721 // loop over y coordinate of rectangle
722 for (int y=0; y<inHeight; y++)
724 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
725 // split into four components rgba
726 int pixLow = pixelColor & 255;
727 int pixMid = (pixelColor >> 8) & 255;
728 int pixHigh = (pixelColor >> 16) & 255;
729 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
730 // If colours are too close in any channel then it's an overlap
731 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
732 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
733 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
737 catch (NullPointerException e) {
738 // ignore null pointers, just return false
745 * Inform that tiles have been updated and the map can be repainted
746 * @param inIsOk true if data loaded ok, false for error
748 public synchronized void tilesUpdated(boolean inIsOk)
750 // Show message if loading failed (but not too many times)
751 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
753 _shownOsmErrorAlready = true;
754 // use separate thread to show message about failing to load osm images
755 new Thread(new Runnable() {
757 try {Thread.sleep(500);} catch (InterruptedException ie) {}
758 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
767 * Zoom out, if not already at minimum zoom
769 public void zoomOut()
771 _mapPosition.zoomOut();
777 * Zoom in, if not already at maximum zoom
781 _mapPosition.zoomIn();
788 * @param inDeltaX x shift
789 * @param inDeltaY y shift
791 public void panMap(int inDeltaX, int inDeltaY)
793 _mapPosition.pan(inDeltaX, inDeltaY);
799 * Create a DataPoint object from the given click coordinates
800 * @param inX x coordinate of click
801 * @param inY y coordinate of click
802 * @return DataPoint with given coordinates and no altitude
804 private DataPoint createPointFromClick(int inX, int inY)
806 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(inY, getHeight()));
807 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(inX, getWidth()));
808 return new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
809 new Longitude(lon, Coordinate.FORMAT_NONE), null);
813 * @see javax.swing.JComponent#getMinimumSize()
815 public Dimension getMinimumSize()
817 final Dimension minSize = new Dimension(512, 300);
822 * @see javax.swing.JComponent#getPreferredSize()
824 public Dimension getPreferredSize()
826 return getMinimumSize();
831 * Respond to mouse click events
832 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
834 public void mouseClicked(MouseEvent inE)
836 if (_track != null && _track.getNumPoints() > 0)
838 // select point if it's a left-click
839 if (!inE.isMetaDown())
841 if (inE.getClickCount() == 1)
844 if (_drawMode == MODE_DEFAULT)
846 int pointIndex = _track.getNearestPointIndex(
847 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
848 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
849 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
850 // Extend selection for shift-click
851 if (inE.isShiftDown()) {
852 _trackInfo.extendSelection(pointIndex);
855 _trackInfo.selectPoint(pointIndex);
858 else if (_drawMode == MODE_DRAW_POINTS_START)
860 _app.createPoint(createPointFromClick(inE.getX(), inE.getY()));
861 _dragToX = inE.getX();
862 _dragToY = inE.getY();
863 _drawMode = MODE_DRAW_POINTS_CONT;
865 else if (_drawMode == MODE_DRAW_POINTS_CONT)
867 DataPoint point = createPointFromClick(inE.getX(), inE.getY());
868 _app.createPoint(point);
869 point.setSegmentStart(false);
872 else if (inE.getClickCount() == 2) {
874 if (_drawMode == MODE_DEFAULT) {
875 panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
878 else if (_drawMode == MODE_DRAW_POINTS_START || _drawMode == MODE_DRAW_POINTS_CONT) {
879 _drawMode = MODE_DEFAULT;
885 // show the popup menu for right-clicks
886 _popupMenuX = inE.getX();
887 _popupMenuY = inE.getY();
888 _popup.show(this, _popupMenuX, _popupMenuY);
894 * Ignore mouse enter events
895 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
897 public void mouseEntered(MouseEvent inE)
903 * Ignore mouse exited events
904 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
906 public void mouseExited(MouseEvent inE)
912 * Ignore mouse pressed events
913 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
915 public void mousePressed(MouseEvent inE)
921 * Respond to mouse released events
922 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
924 public void mouseReleased(MouseEvent inE)
927 if (_drawMode == MODE_ZOOM_RECT && Math.abs(_dragToX - _dragFromX) > 20
928 && Math.abs(_dragToY - _dragFromY) > 20)
930 //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
931 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
932 _drawMode = MODE_DEFAULT;
934 _dragFromX = _dragFromY = -1;
939 * Respond to mouse drag events
940 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
942 public void mouseDragged(MouseEvent inE)
944 if (!inE.isMetaDown())
946 // Left mouse drag - pan map by appropriate amount
947 if (_dragFromX != -1)
949 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
953 _dragFromX = _dragToX = inE.getX();
954 _dragFromY = _dragToY = inE.getY();
958 // Right-click and drag - draw rectangle and control zoom
959 _drawMode = MODE_ZOOM_RECT;
960 if (_dragFromX == -1) {
961 _dragFromX = inE.getX();
962 _dragFromY = inE.getY();
964 _dragToX = inE.getX();
965 _dragToY = inE.getY();
971 * Respond to mouse move events without button pressed
972 * @param inEvent ignored
974 public void mouseMoved(MouseEvent inEvent)
976 // Ignore unless we're drawing points
977 if (_drawMode == MODE_DRAW_POINTS_CONT)
979 _dragToX = inEvent.getX();
980 _dragToY = inEvent.getY();
986 * Respond to status bar message from broker
987 * @param inMessage message, ignored
989 public void actionCompleted(String inMessage)
995 * Respond to data updated message from broker
996 * @param inUpdateType type of update
998 public void dataUpdated(byte inUpdateType)
1000 _recalculate = true;
1001 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
1002 _checkBounds = true;
1004 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
1005 _tileManager.resetConfig();
1008 // enable or disable components
1009 boolean hasData = _track.getNumPoints() > 0;
1010 _topPanel.setVisible(hasData);
1011 _sidePanel.setVisible(hasData);
1012 // grab focus for the key presses
1013 this.requestFocus();
1017 * Respond to key presses on the map canvas
1018 * @param inE key event
1020 public void keyPressed(KeyEvent inE)
1022 int code = inE.getKeyCode();
1023 int currPointIndex = _selection.getCurrentPointIndex();
1024 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
1025 if (inE.isControlDown() || inE.isMetaDown())
1027 // Check for arrow keys to zoom in and out
1028 if (code == KeyEvent.VK_UP)
1030 else if (code == KeyEvent.VK_DOWN)
1032 // Key nav for next/prev point
1033 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
1034 _trackInfo.selectPoint(currPointIndex-1);
1035 else if (code == KeyEvent.VK_RIGHT)
1036 _trackInfo.selectPoint(currPointIndex+1);
1037 else if (code == KeyEvent.VK_PAGE_UP)
1038 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
1039 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1040 else if (code == KeyEvent.VK_PAGE_DOWN)
1041 _trackInfo.selectPoint(Checker.getNextSegmentStart(
1042 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1043 // Check for home and end
1044 else if (code == KeyEvent.VK_HOME)
1045 _trackInfo.selectPoint(0);
1046 else if (code == KeyEvent.VK_END)
1047 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
1051 // Check for arrow keys to pan
1053 if (code == KeyEvent.VK_UP)
1054 upwardsPan = -PAN_DISTANCE;
1055 else if (code == KeyEvent.VK_DOWN)
1056 upwardsPan = PAN_DISTANCE;
1057 int rightwardsPan = 0;
1058 if (code == KeyEvent.VK_RIGHT)
1059 rightwardsPan = PAN_DISTANCE;
1060 else if (code == KeyEvent.VK_LEFT)
1061 rightwardsPan = -PAN_DISTANCE;
1062 panMap(rightwardsPan, upwardsPan);
1064 if (code == KeyEvent.VK_ESCAPE)
1065 _drawMode = MODE_DEFAULT;
1066 // Check for backspace key to delete current point (delete key already handled by menu)
1067 else if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
1068 _app.deleteCurrentPoint();
1074 * @param inE key released event, ignored
1076 public void keyReleased(KeyEvent e)
1082 * @param inE key typed event, ignored
1084 public void keyTyped(KeyEvent inE)
1090 * @param inE mouse wheel event indicating scroll direction
1092 public void mouseWheelMoved(MouseWheelEvent inE)
1094 int clicks = inE.getWheelRotation();
1096 panMap((inE.getX() - getWidth()/2)/2, (inE.getY() - getHeight()/2)/2);
1099 else if (clicks > 0) {
1100 panMap(-(inE.getX() - getWidth()/2), -(inE.getY() - getHeight()/2));
1106 * @return current map position
1108 public MapPosition getMapPosition()
1110 return _mapPosition;