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.event.ActionEvent;
13 import java.awt.event.ActionListener;
14 import java.awt.event.ItemEvent;
15 import java.awt.event.ItemListener;
16 import java.awt.event.KeyEvent;
17 import java.awt.event.KeyListener;
18 import java.awt.event.MouseEvent;
19 import java.awt.event.MouseListener;
20 import java.awt.event.MouseMotionListener;
21 import java.awt.event.MouseWheelEvent;
22 import java.awt.event.MouseWheelListener;
23 import java.awt.image.BufferedImage;
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;
37 import tim.prune.DataSubscriber;
38 import tim.prune.FunctionLibrary;
39 import tim.prune.I18nManager;
40 import tim.prune.UpdateMessageBroker;
41 import tim.prune.config.ColourScheme;
42 import tim.prune.config.Config;
43 import tim.prune.data.Checker;
44 import tim.prune.data.Coordinate;
45 import tim.prune.data.DataPoint;
46 import tim.prune.data.DoubleRange;
47 import tim.prune.data.Latitude;
48 import tim.prune.data.Longitude;
49 import tim.prune.data.Selection;
50 import tim.prune.data.Track;
51 import tim.prune.data.TrackInfo;
52 import tim.prune.gui.IconManager;
55 * Class for the map canvas, to display a background map and draw on it
57 public class MapCanvas extends JPanel implements MouseListener, MouseMotionListener, DataSubscriber,
58 KeyListener, MouseWheelListener
60 /** App object for callbacks */
61 private App _app = null;
63 private Track _track = null;
64 /** TrackInfo object */
65 private TrackInfo _trackInfo = null;
66 /** Selection object */
67 private Selection _selection = null;
68 /** Previously selected point */
69 private int _prevSelectedPoint = -1;
71 private MapTileManager _tileManager = new MapTileManager(this);
72 /** Image to display */
73 private BufferedImage _mapImage = null;
74 /** Slider for transparency */
75 private JSlider _transparencySlider = null;
76 /** Checkbox for scale bar */
77 private JCheckBox _scaleCheckBox = null;
78 /** Checkbox for maps */
79 private JCheckBox _mapCheckBox = null;
80 /** Checkbox for autopan */
81 private JCheckBox _autopanCheckBox = null;
82 /** Checkbox for connecting track points */
83 private JCheckBox _connectCheckBox = null;
84 /** Right-click popup menu */
85 private JPopupMenu _popup = null;
86 /** Top component panel */
87 private JPanel _topPanel = null;
88 /** Side component panel */
89 private JPanel _sidePanel = null;
91 private ScaleBar _scaleBar = null;
93 private DoubleRange _latRange = null, _lonRange = null;
94 private DoubleRange _xRange = null, _yRange = null;
95 private boolean _recalculate = false;
96 /** Flag to check bounds on next paint */
97 private boolean _checkBounds = false;
99 private MapPosition _mapPosition = null;
100 /** x coordinate of drag from point */
101 private int _dragFromX = -1;
102 /** y coordinate of drag from point */
103 private int _dragFromY = -1;
104 /** x coordinate of drag to point */
105 private int _dragToX = -1;
106 /** y coordinate of drag to point */
107 private int _dragToY = -1;
108 /** x coordinate of popup menu */
109 private int _popupMenuX = -1;
110 /** y coordinate of popup menu */
111 private int _popupMenuY = -1;
112 /** Flag to prevent showing too often the error message about loading maps */
113 private boolean _shownOsmErrorAlready = false;
114 /** Current drawing mode */
115 private int _drawMode = MODE_DEFAULT;
117 /** Constant for click sensitivity when selecting nearest point */
118 private static final int CLICK_SENSITIVITY = 10;
119 /** Constant for pan distance from key presses */
120 private static final int PAN_DISTANCE = 20;
121 /** Constant for pan distance from autopan */
122 private static final int AUTOPAN_DISTANCE = 75;
125 private static final Color COLOR_MESSAGES = Color.GRAY;
128 private static final int MODE_DEFAULT = 0;
129 private static final int MODE_ZOOM_RECT = 1;
130 private static final int MODE_DRAW_POINTS_START = 2;
131 private static final int MODE_DRAW_POINTS_CONT = 3;
135 * @param inApp App object for callbacks
136 * @param inTrackInfo track info object
138 public MapCanvas(App inApp, TrackInfo inTrackInfo)
141 _trackInfo = inTrackInfo;
142 _track = inTrackInfo.getTrack();
143 _selection = inTrackInfo.getSelection();
144 _mapPosition = new MapPosition();
145 addMouseListener(this);
146 addMouseMotionListener(this);
147 addMouseWheelListener(this);
148 addKeyListener(this);
150 // Make listener for changes to controls
151 ItemListener itemListener = new ItemListener() {
152 public void itemStateChanged(ItemEvent e)
158 // Make special listener for changes to map checkbox
159 ItemListener mapCheckListener = new ItemListener() {
160 public void itemStateChanged(ItemEvent e)
162 _tileManager.clearMemoryCaches();
164 Config.setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
165 UpdateMessageBroker.informSubscribers(); // to let menu know
168 _topPanel = new OverlayPanel();
169 _topPanel.setLayout(new FlowLayout());
170 // Make slider for transparency
171 _transparencySlider = new JSlider(-6, 6, 0);
172 _transparencySlider.setPreferredSize(new Dimension(100, 20));
173 _transparencySlider.setMajorTickSpacing(1);
174 _transparencySlider.setSnapToTicks(true);
175 _transparencySlider.setOpaque(false);
176 _transparencySlider.setValue(0);
177 _transparencySlider.addChangeListener(new ChangeListener() {
178 public void stateChanged(ChangeEvent e)
180 int val = _transparencySlider.getValue();
181 if (val == 1 || val == -1)
182 _transparencySlider.setValue(0);
189 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
190 _topPanel.add(_transparencySlider);
191 // Add checkbox button for enabling scale bar
192 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
193 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
194 _scaleCheckBox.setOpaque(false);
195 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
196 _scaleCheckBox.addItemListener(new ItemListener() {
197 public void itemStateChanged(ItemEvent e) {
198 _scaleBar.setVisible(_scaleCheckBox.isSelected());
201 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
202 _topPanel.add(_scaleCheckBox);
203 // Add checkbox button for enabling maps or not
204 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
205 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
206 _mapCheckBox.setOpaque(false);
207 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
208 _mapCheckBox.addItemListener(mapCheckListener);
209 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
210 _topPanel.add(_mapCheckBox);
211 // Add checkbox button for enabling autopan or not
212 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
213 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
214 _autopanCheckBox.setOpaque(false);
215 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
216 _autopanCheckBox.addItemListener(itemListener);
217 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
218 _topPanel.add(_autopanCheckBox);
219 // Add checkbox button for connecting points or not
220 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
221 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
222 _connectCheckBox.setOpaque(false);
223 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
224 _connectCheckBox.addItemListener(itemListener);
225 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
226 _topPanel.add(_connectCheckBox);
228 // Add zoom in, zoom out buttons
229 _sidePanel = new OverlayPanel();
230 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
231 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
232 zoomInButton.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
233 zoomInButton.setContentAreaFilled(false);
234 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
235 zoomInButton.addActionListener(new ActionListener() {
236 public void actionPerformed(ActionEvent e)
241 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
242 _sidePanel.add(zoomInButton);
243 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
244 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
245 zoomOutButton.setContentAreaFilled(false);
246 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
247 zoomOutButton.addActionListener(new ActionListener() {
248 public void actionPerformed(ActionEvent e)
253 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
254 _sidePanel.add(zoomOutButton);
256 // Bottom panel for scale bar
257 _scaleBar = new ScaleBar();
259 // add control panels to this one
260 setLayout(new BorderLayout());
261 _topPanel.setVisible(false);
262 _sidePanel.setVisible(false);
263 add(_topPanel, BorderLayout.NORTH);
264 add(_sidePanel, BorderLayout.WEST);
265 add(_scaleBar, BorderLayout.SOUTH);
272 * Make the popup menu for right-clicking the map
274 private void makePopup()
276 _popup = new JPopupMenu();
277 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
278 zoomInItem.addActionListener(new ActionListener() {
279 public void actionPerformed(ActionEvent e)
281 panMap((_popupMenuX - getWidth()/2)/2, (_popupMenuY - getHeight()/2)/2);
284 _popup.add(zoomInItem);
285 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
286 zoomOutItem.addActionListener(new ActionListener() {
287 public void actionPerformed(ActionEvent e)
289 panMap(-(_popupMenuX - getWidth()/2), -(_popupMenuY - getHeight()/2));
292 _popup.add(zoomOutItem);
293 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
294 zoomFullItem.addActionListener(new ActionListener() {
295 public void actionPerformed(ActionEvent e)
301 _popup.add(zoomFullItem);
302 _popup.addSeparator();
304 JMenuItem setMapBgItem = new JMenuItem(
305 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
306 setMapBgItem.addActionListener(new ActionListener() {
307 public void actionPerformed(ActionEvent e)
309 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
311 _popup.add(setMapBgItem);
313 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
314 newPointItem.addActionListener(new ActionListener() {
315 public void actionPerformed(ActionEvent e)
317 _app.createPoint(createPointFromClick(_popupMenuX, _popupMenuY));
319 _popup.add(newPointItem);
321 JMenuItem drawPointsItem = new JMenuItem(I18nManager.getText("menu.map.drawpoints"));
322 drawPointsItem.addActionListener(new ActionListener() {
323 public void actionPerformed(ActionEvent e)
325 _drawMode = MODE_DRAW_POINTS_START;
328 _popup.add(drawPointsItem);
333 * Zoom to fit the current data area
335 private void zoomToFit()
337 _latRange = _track.getLatRange();
338 _lonRange = _track.getLonRange();
339 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
340 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
341 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
342 MapUtils.getYFromLatitude(_latRange.getMaximum()));
343 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
344 getWidth(), getHeight());
350 * @see java.awt.Canvas#paint(java.awt.Graphics)
352 public void paint(Graphics inG)
355 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
358 if (_track.getNumPoints() > 0)
360 // Check for autopan if enabled / necessary
361 if (_autopanCheckBox.isSelected())
363 int selectedPoint = _selection.getCurrentPointIndex();
364 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
366 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
367 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
370 if (px < PAN_DISTANCE) {
371 panX = px - AUTOPAN_DISTANCE;
373 else if (px > (getWidth()-PAN_DISTANCE)) {
374 panX = AUTOPAN_DISTANCE + px - getWidth();
376 if (py < PAN_DISTANCE) {
377 panY = py - AUTOPAN_DISTANCE;
379 if (py > (getHeight()-PAN_DISTANCE)) {
380 panY = AUTOPAN_DISTANCE + py - getHeight();
382 if (panX != 0 || panY != 0) {
383 _mapPosition.pan(panX, panY);
386 _prevSelectedPoint = selectedPoint;
389 // Draw the map contents if necessary
390 if ((_mapImage == null || _recalculate))
393 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
395 // Draw the prepared image onto the panel
396 if (_mapImage != null) {
397 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
399 // Draw the zoom rectangle if necessary
400 if (_drawMode == MODE_ZOOM_RECT)
402 inG.setColor(Color.RED);
403 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
404 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
405 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
406 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
408 else if (_drawMode == MODE_DRAW_POINTS_CONT)
410 // draw line to mouse position to show drawing mode
411 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT));
412 int prevIndex = _track.getNumPoints()-1;
413 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(prevIndex));
414 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(prevIndex));
415 inG.drawLine(px, py, _dragToX, _dragToY);
420 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
421 inG.fillRect(0, 0, getWidth(), getHeight());
422 inG.setColor(COLOR_MESSAGES);
423 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
424 _scaleBar.updateScale(-1, 0);
426 // Draw slider etc on top
432 * Paint the map tiles and the points on to the _mapImage
434 private void paintMapContents()
436 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
438 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
442 Graphics g = _mapImage.getGraphics();
443 // Clear to background
444 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
445 g.fillRect(0, 0, getWidth(), getHeight());
447 // Check whether maps are on or not
448 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
449 _mapCheckBox.setSelected(showMap);
451 // reset error message
452 if (!showMap) {_shownOsmErrorAlready = false;}
453 _recalculate = false;
454 // Only get map tiles if selected
458 _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
460 boolean loadingFailed = false;
461 if (_mapImage == null) return;
463 if (_tileManager.isOverzoomed())
465 // display overzoom message
466 g.setColor(COLOR_MESSAGES);
467 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
471 int numLayers = _tileManager.getNumLayers();
472 // Loop over tiles drawing each one
473 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
474 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
475 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
477 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
478 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
480 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
482 for (int l=0; l<numLayers; l++)
484 Image image = _tileManager.getTile(l, tileX, tileY);
486 g.drawImage(image, x, y, 256, 256, null);
492 // Make maps brighter / fainter according to slider
493 final int brightnessIndex = Math.max(1, _transparencySlider.getValue()) - 1;
494 if (brightnessIndex > 0)
496 final int[] alphas = {0, 40, 80, 120, 160, 210};
497 Color bgColor = Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND);
498 bgColor = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), alphas[brightnessIndex]);
500 g.fillRect(0, 0, getWidth(), getHeight());
505 // Paint the track points on top
506 int pointsPainted = 1;
509 pointsPainted = paintPoints(g);
511 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
517 // Zoom to fit if no points found
518 if (pointsPainted <= 0 && _checkBounds) {
523 _checkBounds = false;
524 // enable / disable transparency slider
525 _transparencySlider.setEnabled(showMap);
530 * Paint the points using the given graphics object
531 * @param inG Graphics object to use for painting
532 * @return number of points painted, if any
534 private int paintPoints(Graphics inG)
537 final ColourScheme cs = Config.getColourScheme();
538 final int[] opacities = {255, 190, 130, 80, 40, 0};
540 if (_transparencySlider.getValue() < 0)
541 opacity = opacities[-1 - _transparencySlider.getValue()];
542 final Color pointColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_POINT), opacity);
543 final Color rangeColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_SELECTION), opacity);
544 final Color currentColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_PRIMARY), opacity);
545 final Color secondColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_SECONDARY), opacity);
546 final Color textColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_TEXT), opacity);
548 // try to set line width for painting
549 if (inG instanceof Graphics2D)
551 int lineWidth = Config.getConfigInt(Config.KEY_LINE_WIDTH);
552 if (lineWidth < 1 || lineWidth > 4) {lineWidth = 2;}
553 ((Graphics2D) inG).setStroke(new BasicStroke(lineWidth));
555 int pointsPainted = 0;
557 inG.setColor(pointColour);
558 int prevX = -1, prevY = -1;
559 boolean connectPoints = _connectCheckBox.isSelected();
560 boolean prevPointVisible = false, currPointVisible = false;
561 boolean anyWaypoints = false;
562 boolean isWaypoint = false;
563 for (int i=0; i<_track.getNumPoints(); i++)
565 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
566 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
567 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
568 isWaypoint = _track.getPoint(i).isWaypoint();
569 anyWaypoints = anyWaypoints || isWaypoint;
570 if (currPointVisible)
574 // Draw rectangle for track point
575 if (_track.getPoint(i).getDeleteFlag()) {
576 inG.setColor(currentColour);
579 inG.setColor(pointColour);
581 inG.drawRect(px-2, py-2, 3, 3);
587 // Connect track points if either of them are visible
588 if (connectPoints && (currPointVisible || prevPointVisible)
589 && !(prevX == -1 && prevY == -1)
590 && !_track.getPoint(i).getSegmentStart())
592 inG.drawLine(prevX, prevY, px, py);
594 prevX = px; prevY = py;
596 prevPointVisible = currPointVisible;
599 // Loop over points, just drawing blobs for waypoints
600 inG.setColor(textColour);
601 FontMetrics fm = inG.getFontMetrics();
602 int nameHeight = fm.getHeight();
603 int width = getWidth();
604 int height = getHeight();
606 for (int i=0; i<_track.getNumPoints(); i++)
608 if (_track.getPoint(i).isWaypoint())
610 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
611 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
612 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
614 inG.fillRect(px-3, py-3, 6, 6);
619 // Loop over points again, now draw names for waypoints
620 for (int i=0; i<_track.getNumPoints(); i++)
622 if (_track.getPoint(i).isWaypoint())
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())
628 // Figure out where to draw waypoint name so it doesn't obscure track
629 String waypointName = _track.getPoint(i).getWaypointName();
630 int nameWidth = fm.stringWidth(waypointName);
631 boolean drawnName = false;
632 // Make arrays for coordinates right left up down
633 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
634 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
635 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
637 // Shift arrays for coordinates right left up down
638 nameXs[0] += 2; nameXs[1] -= 2;
639 nameYs[2] -= 2; nameYs[3] += 2;
640 // Check each direction in turn right left up down
641 for (int a=0; a<4; a++)
643 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
644 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
645 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
647 // Found a rectangle to fit - draw name here and quit
648 inG.drawString(waypointName, nameXs[a], nameYs[a]);
658 // Loop over points, drawing blobs for photo / audio points
659 inG.setColor(secondColour);
660 for (int i=0; i<_track.getNumPoints(); i++)
662 if (_track.getPoint(i).hasMedia())
664 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
665 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
666 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
668 inG.drawRect(px-1, py-1, 2, 2);
669 inG.drawRect(px-2, py-2, 4, 4);
675 // Draw selected range
676 if (_selection.hasRangeSelected())
678 inG.setColor(rangeColour);
679 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
681 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
682 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
683 inG.drawRect(px-1, py-1, 2, 2);
687 // Draw selected point, crosshairs
688 int selectedPoint = _selection.getCurrentPointIndex();
689 if (selectedPoint >= 0)
691 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
692 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
693 inG.setColor(currentColour);
695 inG.drawLine(px, 0, px, getHeight());
696 inG.drawLine(0, py, getWidth(), py);
698 inG.drawOval(px - 2, py - 2, 4, 4);
699 inG.drawOval(px - 3, py - 3, 6, 6);
701 // Return the number of points painted
702 return pointsPainted;
707 * Tests whether there are any dark pixels within the specified x,y rectangle
708 * @param inX left X coordinate
709 * @param inY bottom Y coordinate
710 * @param inWidth width of rectangle
711 * @param inHeight height of rectangle
712 * @param inTextColour colour of text
713 * @return true if the rectangle overlaps stuff too close to the given colour
715 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
717 // each of the colour channels must be further away than this to count as empty
718 final int BRIGHTNESS_LIMIT = 80;
719 final int textRGB = inTextColour.getRGB();
720 final int textLow = textRGB & 255;
721 final int textMid = (textRGB >> 8) & 255;
722 final int textHigh = (textRGB >> 16) & 255;
725 // loop over x coordinate of rectangle
726 for (int x=0; x<inWidth; x++)
728 // loop over y coordinate of rectangle
729 for (int y=0; y<inHeight; y++)
731 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
732 // split into four components rgba
733 int pixLow = pixelColor & 255;
734 int pixMid = (pixelColor >> 8) & 255;
735 int pixHigh = (pixelColor >> 16) & 255;
736 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
737 // If colours are too close in any channel then it's an overlap
738 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
739 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
740 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
744 catch (NullPointerException e) {
745 // ignore null pointers, just return false
751 * Make a semi-transparent colour for drawing with
752 * @param inColour base colour (fully opaque)
753 * @param inOpacity opacity where 0=invisible and 255=full
754 * @return new colour object
756 private static Color makeTransparentColour(Color inColour, int inOpacity)
758 if (inOpacity > 240) return inColour;
759 return new Color(inColour.getRed(), inColour.getGreen(), inColour.getBlue(), inOpacity);
763 * Inform that tiles have been updated and the map can be repainted
764 * @param inIsOk true if data loaded ok, false for error
766 public synchronized void tilesUpdated(boolean inIsOk)
768 // Show message if loading failed (but not too many times)
769 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
771 _shownOsmErrorAlready = true;
772 // use separate thread to show message about failing to load osm images
773 new Thread(new Runnable() {
775 try {Thread.sleep(500);} catch (InterruptedException ie) {}
776 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
785 * Zoom out, if not already at minimum zoom
787 public void zoomOut()
789 _mapPosition.zoomOut();
795 * Zoom in, if not already at maximum zoom
799 _mapPosition.zoomIn();
806 * @param inDeltaX x shift
807 * @param inDeltaY y shift
809 public void panMap(int inDeltaX, int inDeltaY)
811 _mapPosition.pan(inDeltaX, inDeltaY);
817 * Create a DataPoint object from the given click coordinates
818 * @param inX x coordinate of click
819 * @param inY y coordinate of click
820 * @return DataPoint with given coordinates and no altitude
822 private DataPoint createPointFromClick(int inX, int inY)
824 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(inY, getHeight()));
825 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(inX, getWidth()));
826 return new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
827 new Longitude(lon, Coordinate.FORMAT_NONE), null);
831 * @see javax.swing.JComponent#getMinimumSize()
833 public Dimension getMinimumSize()
835 final Dimension minSize = new Dimension(512, 300);
840 * @see javax.swing.JComponent#getPreferredSize()
842 public Dimension getPreferredSize()
844 return getMinimumSize();
849 * Respond to mouse click events
850 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
852 public void mouseClicked(MouseEvent inE)
854 if (_track != null && _track.getNumPoints() > 0)
856 // select point if it's a left-click
857 if (!inE.isMetaDown())
859 if (inE.getClickCount() == 1)
862 if (_drawMode == MODE_DEFAULT)
864 int pointIndex = _track.getNearestPointIndex(
865 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
866 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
867 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
868 // Extend selection for shift-click
869 if (inE.isShiftDown()) {
870 _trackInfo.extendSelection(pointIndex);
873 _trackInfo.selectPoint(pointIndex);
876 else if (_drawMode == MODE_DRAW_POINTS_START)
878 _app.createPoint(createPointFromClick(inE.getX(), inE.getY()));
879 _dragToX = inE.getX();
880 _dragToY = inE.getY();
881 _drawMode = MODE_DRAW_POINTS_CONT;
883 else if (_drawMode == MODE_DRAW_POINTS_CONT)
885 DataPoint point = createPointFromClick(inE.getX(), inE.getY());
886 _app.createPoint(point);
887 point.setSegmentStart(false);
890 else if (inE.getClickCount() == 2)
893 if (_drawMode == MODE_DEFAULT) {
894 panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
897 else if (_drawMode == MODE_DRAW_POINTS_START || _drawMode == MODE_DRAW_POINTS_CONT) {
898 _drawMode = MODE_DEFAULT;
904 // show the popup menu for right-clicks
905 _popupMenuX = inE.getX();
906 _popupMenuY = inE.getY();
907 _popup.show(this, _popupMenuX, _popupMenuY);
913 * Ignore mouse enter events
914 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
916 public void mouseEntered(MouseEvent inE)
922 * Ignore mouse exited events
923 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
925 public void mouseExited(MouseEvent inE)
931 * Ignore mouse pressed events
932 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
934 public void mousePressed(MouseEvent inE)
940 * Respond to mouse released events
941 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
943 public void mouseReleased(MouseEvent inE)
946 if (_drawMode == MODE_ZOOM_RECT && Math.abs(_dragToX - _dragFromX) > 20
947 && Math.abs(_dragToY - _dragFromY) > 20)
949 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
951 if (_drawMode == MODE_ZOOM_RECT) {
952 _drawMode = MODE_DEFAULT;
954 _dragFromX = _dragFromY = -1;
959 * Respond to mouse drag events
960 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
962 public void mouseDragged(MouseEvent inE)
964 if (!inE.isMetaDown())
966 // Left mouse drag - pan map by appropriate amount
967 if (_dragFromX != -1)
969 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
973 _dragFromX = _dragToX = inE.getX();
974 _dragFromY = _dragToY = inE.getY();
978 // Right-click and drag - draw rectangle and control zoom
979 _drawMode = MODE_ZOOM_RECT;
980 if (_dragFromX == -1) {
981 _dragFromX = inE.getX();
982 _dragFromY = inE.getY();
984 _dragToX = inE.getX();
985 _dragToY = inE.getY();
991 * Respond to mouse move events without button pressed
992 * @param inEvent ignored
994 public void mouseMoved(MouseEvent inEvent)
996 // Ignore unless we're drawing points
997 if (_drawMode == MODE_DRAW_POINTS_CONT)
999 _dragToX = inEvent.getX();
1000 _dragToY = inEvent.getY();
1006 * Respond to status bar message from broker
1007 * @param inMessage message, ignored
1009 public void actionCompleted(String inMessage)
1015 * Respond to data updated message from broker
1016 * @param inUpdateType type of update
1018 public void dataUpdated(byte inUpdateType)
1020 _recalculate = true;
1021 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
1022 _checkBounds = true;
1024 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
1025 _tileManager.resetConfig();
1028 // enable or disable components
1029 boolean hasData = _track.getNumPoints() > 0;
1030 _topPanel.setVisible(hasData);
1031 _sidePanel.setVisible(hasData);
1032 // grab focus for the key presses
1033 this.requestFocus();
1037 * Respond to key presses on the map canvas
1038 * @param inE key event
1040 public void keyPressed(KeyEvent inE)
1042 int code = inE.getKeyCode();
1043 int currPointIndex = _selection.getCurrentPointIndex();
1044 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
1045 if (inE.isControlDown() || inE.isMetaDown())
1047 // Shift as well makes things faster
1048 final int pointIncrement = inE.isShiftDown()?3:1;
1049 // Check for arrow keys to zoom in and out
1050 if (code == KeyEvent.VK_UP)
1052 else if (code == KeyEvent.VK_DOWN)
1054 // Key nav for next/prev point
1055 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
1056 _trackInfo.incrementPointIndex(-pointIncrement);
1057 else if (code == KeyEvent.VK_RIGHT)
1058 _trackInfo.incrementPointIndex(pointIncrement);
1059 else if (code == KeyEvent.VK_PAGE_UP)
1060 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
1061 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1062 else if (code == KeyEvent.VK_PAGE_DOWN)
1063 _trackInfo.selectPoint(Checker.getNextSegmentStart(
1064 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1065 // Check for home and end
1066 else if (code == KeyEvent.VK_HOME)
1067 _trackInfo.selectPoint(0);
1068 else if (code == KeyEvent.VK_END)
1069 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
1073 // Check for arrow keys to pan
1075 if (code == KeyEvent.VK_UP)
1076 upwardsPan = -PAN_DISTANCE;
1077 else if (code == KeyEvent.VK_DOWN)
1078 upwardsPan = PAN_DISTANCE;
1079 int rightwardsPan = 0;
1080 if (code == KeyEvent.VK_RIGHT)
1081 rightwardsPan = PAN_DISTANCE;
1082 else if (code == KeyEvent.VK_LEFT)
1083 rightwardsPan = -PAN_DISTANCE;
1084 panMap(rightwardsPan, upwardsPan);
1086 if (code == KeyEvent.VK_ESCAPE)
1087 _drawMode = MODE_DEFAULT;
1088 // Check for backspace key to delete current point (delete key already handled by menu)
1089 else if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
1090 _app.deleteCurrentPoint();
1096 * @param inE key released event, ignored
1098 public void keyReleased(KeyEvent e)
1104 * @param inE key typed event, ignored
1106 public void keyTyped(KeyEvent inE)
1112 * @param inE mouse wheel event indicating scroll direction
1114 public void mouseWheelMoved(MouseWheelEvent inE)
1116 int clicks = inE.getWheelRotation();
1118 panMap((inE.getX() - getWidth()/2)/2, (inE.getY() - getHeight()/2)/2);
1121 else if (clicks > 0) {
1122 panMap(-(inE.getX() - getWidth()/2), -(inE.getY() - getHeight()/2));
1128 * @return current map position
1130 public MapPosition getMapPosition()
1132 return _mapPosition;