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 JPanel();
169 _topPanel.setLayout(new FlowLayout());
170 _topPanel.setOpaque(false);
171 // Make slider for transparency
172 _transparencySlider = new JSlider(-6, 6, 0);
173 _transparencySlider.setPreferredSize(new Dimension(100, 20));
174 _transparencySlider.setMajorTickSpacing(1);
175 _transparencySlider.setSnapToTicks(true);
176 _transparencySlider.setOpaque(false);
177 _transparencySlider.setValue(0);
178 _transparencySlider.addChangeListener(new ChangeListener() {
179 public void stateChanged(ChangeEvent e)
181 int val = _transparencySlider.getValue();
182 if (val == 1 || val == -1)
183 _transparencySlider.setValue(0);
190 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
191 _topPanel.add(_transparencySlider);
192 // Add checkbox button for enabling scale bar
193 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
194 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
195 _scaleCheckBox.setOpaque(false);
196 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
197 _scaleCheckBox.addItemListener(new ItemListener() {
198 public void itemStateChanged(ItemEvent e) {
199 _scaleBar.setVisible(_scaleCheckBox.isSelected());
202 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
203 _topPanel.add(_scaleCheckBox);
204 // Add checkbox button for enabling maps or not
205 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
206 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
207 _mapCheckBox.setOpaque(false);
208 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
209 _mapCheckBox.addItemListener(mapCheckListener);
210 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
211 _topPanel.add(_mapCheckBox);
212 // Add checkbox button for enabling autopan or not
213 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
214 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
215 _autopanCheckBox.setOpaque(false);
216 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
217 _autopanCheckBox.addItemListener(itemListener);
218 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
219 _topPanel.add(_autopanCheckBox);
220 // Add checkbox button for connecting points or not
221 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
222 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
223 _connectCheckBox.setOpaque(false);
224 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
225 _connectCheckBox.addItemListener(itemListener);
226 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
227 _topPanel.add(_connectCheckBox);
229 // Add zoom in, zoom out buttons
230 _sidePanel = new JPanel();
231 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
232 _sidePanel.setOpaque(false);
233 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
234 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
235 zoomInButton.setContentAreaFilled(false);
236 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
237 zoomInButton.addActionListener(new ActionListener() {
238 public void actionPerformed(ActionEvent e)
243 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
244 _sidePanel.add(zoomInButton);
245 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
246 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
247 zoomOutButton.setContentAreaFilled(false);
248 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
249 zoomOutButton.addActionListener(new ActionListener() {
250 public void actionPerformed(ActionEvent e)
255 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
256 _sidePanel.add(zoomOutButton);
258 // Bottom panel for scale bar
259 _scaleBar = new ScaleBar();
261 // add control panels to this one
262 setLayout(new BorderLayout());
263 _topPanel.setVisible(false);
264 _sidePanel.setVisible(false);
265 add(_topPanel, BorderLayout.NORTH);
266 add(_sidePanel, BorderLayout.WEST);
267 add(_scaleBar, BorderLayout.SOUTH);
274 * Make the popup menu for right-clicking the map
276 private void makePopup()
278 _popup = new JPopupMenu();
279 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
280 zoomInItem.addActionListener(new ActionListener() {
281 public void actionPerformed(ActionEvent e)
283 panMap((_popupMenuX - getWidth()/2)/2, (_popupMenuY - getHeight()/2)/2);
286 _popup.add(zoomInItem);
287 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
288 zoomOutItem.addActionListener(new ActionListener() {
289 public void actionPerformed(ActionEvent e)
291 panMap(-(_popupMenuX - getWidth()/2), -(_popupMenuY - getHeight()/2));
294 _popup.add(zoomOutItem);
295 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
296 zoomFullItem.addActionListener(new ActionListener() {
297 public void actionPerformed(ActionEvent e)
303 _popup.add(zoomFullItem);
304 _popup.addSeparator();
306 JMenuItem setMapBgItem = new JMenuItem(
307 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
308 setMapBgItem.addActionListener(new ActionListener() {
309 public void actionPerformed(ActionEvent e)
311 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
313 _popup.add(setMapBgItem);
315 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
316 newPointItem.addActionListener(new ActionListener() {
317 public void actionPerformed(ActionEvent e)
319 _app.createPoint(createPointFromClick(_popupMenuX, _popupMenuY));
321 _popup.add(newPointItem);
323 JMenuItem drawPointsItem = new JMenuItem(I18nManager.getText("menu.map.drawpoints"));
324 drawPointsItem.addActionListener(new ActionListener() {
325 public void actionPerformed(ActionEvent e)
327 _drawMode = MODE_DRAW_POINTS_START;
330 _popup.add(drawPointsItem);
335 * Zoom to fit the current data area
337 private void zoomToFit()
339 _latRange = _track.getLatRange();
340 _lonRange = _track.getLonRange();
341 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
342 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
343 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
344 MapUtils.getYFromLatitude(_latRange.getMaximum()));
345 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
346 getWidth(), getHeight());
352 * @see java.awt.Canvas#paint(java.awt.Graphics)
354 public void paint(Graphics inG)
357 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
360 if (_track.getNumPoints() > 0)
362 // Check for autopan if enabled / necessary
363 if (_autopanCheckBox.isSelected())
365 int selectedPoint = _selection.getCurrentPointIndex();
366 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
368 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
369 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
372 if (px < PAN_DISTANCE) {
373 panX = px - AUTOPAN_DISTANCE;
375 else if (px > (getWidth()-PAN_DISTANCE)) {
376 panX = AUTOPAN_DISTANCE + px - getWidth();
378 if (py < PAN_DISTANCE) {
379 panY = py - AUTOPAN_DISTANCE;
381 if (py > (getHeight()-PAN_DISTANCE)) {
382 panY = AUTOPAN_DISTANCE + py - getHeight();
384 if (panX != 0 || panY != 0) {
385 _mapPosition.pan(panX, panY);
388 _prevSelectedPoint = selectedPoint;
391 // Draw the map contents if necessary
392 if ((_mapImage == null || _recalculate))
395 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
397 // Draw the prepared image onto the panel
398 if (_mapImage != null) {
399 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
401 // Draw the zoom rectangle if necessary
402 if (_drawMode == MODE_ZOOM_RECT)
404 inG.setColor(Color.RED);
405 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
406 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
407 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
408 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
410 else if (_drawMode == MODE_DRAW_POINTS_CONT)
412 // draw line to mouse position to show drawing mode
413 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT));
414 int prevIndex = _track.getNumPoints()-1;
415 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(prevIndex));
416 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(prevIndex));
417 inG.drawLine(px, py, _dragToX, _dragToY);
422 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
423 inG.fillRect(0, 0, getWidth(), getHeight());
424 inG.setColor(COLOR_MESSAGES);
425 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
426 _scaleBar.updateScale(-1, 0);
428 // Draw slider etc on top
434 * Paint the map tiles and the points on to the _mapImage
436 private void paintMapContents()
438 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
440 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
444 Graphics g = _mapImage.getGraphics();
445 // Clear to background
446 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
447 g.fillRect(0, 0, getWidth(), getHeight());
449 // Check whether maps are on or not
450 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
451 _mapCheckBox.setSelected(showMap);
453 // reset error message
454 if (!showMap) {_shownOsmErrorAlready = false;}
455 _recalculate = false;
456 // Only get map tiles if selected
460 _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
462 boolean loadingFailed = false;
463 if (_mapImage == null) return;
465 if (_tileManager.isOverzoomed())
467 // display overzoom message
468 g.setColor(COLOR_MESSAGES);
469 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
473 int numLayers = _tileManager.getNumLayers();
474 // Loop over tiles drawing each one
475 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
476 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
477 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
479 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
480 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
482 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
484 for (int l=0; l<numLayers; l++)
486 Image image = _tileManager.getTile(l, tileX, tileY);
488 g.drawImage(image, x, y, 256, 256, null);
494 // Make maps brighter / fainter according to slider
495 final int brightnessIndex = Math.max(1, _transparencySlider.getValue()) - 1;
496 if (brightnessIndex > 0)
498 final int[] alphas = {0, 40, 80, 120, 160, 210};
499 Color bgColor = Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND);
500 bgColor = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), alphas[brightnessIndex]);
502 g.fillRect(0, 0, getWidth(), getHeight());
507 // Paint the track points on top
508 int pointsPainted = 1;
511 pointsPainted = paintPoints(g);
513 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
519 // Zoom to fit if no points found
520 if (pointsPainted <= 0 && _checkBounds) {
525 _checkBounds = false;
526 // enable / disable transparency slider
527 _transparencySlider.setEnabled(showMap);
532 * Paint the points using the given graphics object
533 * @param inG Graphics object to use for painting
534 * @return number of points painted, if any
536 private int paintPoints(Graphics inG)
539 final ColourScheme cs = Config.getColourScheme();
540 final int[] opacities = {255, 190, 130, 80, 40, 0};
542 if (_transparencySlider.getValue() < 0)
543 opacity = opacities[-1 - _transparencySlider.getValue()];
544 final Color pointColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_POINT), opacity);
545 final Color rangeColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_SELECTION), opacity);
546 final Color currentColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_PRIMARY), opacity);
547 final Color secondColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_SECONDARY), opacity);
548 final Color textColour = makeTransparentColour(cs.getColour(ColourScheme.IDX_TEXT), opacity);
550 // try to set line width for painting
551 if (inG instanceof Graphics2D)
553 int lineWidth = Config.getConfigInt(Config.KEY_LINE_WIDTH);
554 if (lineWidth < 1 || lineWidth > 4) {lineWidth = 2;}
555 ((Graphics2D) inG).setStroke(new BasicStroke(lineWidth));
557 int pointsPainted = 0;
559 inG.setColor(pointColour);
560 int prevX = -1, prevY = -1;
561 boolean connectPoints = _connectCheckBox.isSelected();
562 boolean prevPointVisible = false, currPointVisible = false;
563 boolean anyWaypoints = false;
564 boolean isWaypoint = false;
565 for (int i=0; i<_track.getNumPoints(); i++)
567 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
568 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
569 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
570 isWaypoint = _track.getPoint(i).isWaypoint();
571 anyWaypoints = anyWaypoints || isWaypoint;
572 if (currPointVisible)
576 // Draw rectangle for track point
577 if (_track.getPoint(i).getDeleteFlag()) {
578 inG.setColor(currentColour);
581 inG.setColor(pointColour);
583 inG.drawRect(px-2, py-2, 3, 3);
589 // Connect track points if either of them are visible
590 if (connectPoints && (currPointVisible || prevPointVisible)
591 && !(prevX == -1 && prevY == -1)
592 && !_track.getPoint(i).getSegmentStart())
594 inG.drawLine(prevX, prevY, px, py);
596 prevX = px; prevY = py;
598 prevPointVisible = currPointVisible;
601 // Loop over points, just drawing blobs for waypoints
602 inG.setColor(textColour);
603 FontMetrics fm = inG.getFontMetrics();
604 int nameHeight = fm.getHeight();
605 int width = getWidth();
606 int height = getHeight();
608 for (int i=0; i<_track.getNumPoints(); i++)
610 if (_track.getPoint(i).isWaypoint())
612 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
613 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
614 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
616 inG.fillRect(px-3, py-3, 6, 6);
621 // Loop over points again, now draw names for waypoints
622 for (int i=0; i<_track.getNumPoints(); i++)
624 if (_track.getPoint(i).isWaypoint())
626 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
627 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
628 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
630 // Figure out where to draw waypoint name so it doesn't obscure track
631 String waypointName = _track.getPoint(i).getWaypointName();
632 int nameWidth = fm.stringWidth(waypointName);
633 boolean drawnName = false;
634 // Make arrays for coordinates right left up down
635 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
636 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
637 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
639 // Shift arrays for coordinates right left up down
640 nameXs[0] += 2; nameXs[1] -= 2;
641 nameYs[2] -= 2; nameYs[3] += 2;
642 // Check each direction in turn right left up down
643 for (int a=0; a<4; a++)
645 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
646 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
647 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
649 // Found a rectangle to fit - draw name here and quit
650 inG.drawString(waypointName, nameXs[a], nameYs[a]);
660 // Loop over points, drawing blobs for photo / audio points
661 inG.setColor(secondColour);
662 for (int i=0; i<_track.getNumPoints(); i++)
664 if (_track.getPoint(i).hasMedia())
666 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
667 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
668 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
670 inG.drawRect(px-1, py-1, 2, 2);
671 inG.drawRect(px-2, py-2, 4, 4);
677 // Draw selected range
678 if (_selection.hasRangeSelected())
680 inG.setColor(rangeColour);
681 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
683 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
684 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
685 inG.drawRect(px-1, py-1, 2, 2);
689 // Draw selected point, crosshairs
690 int selectedPoint = _selection.getCurrentPointIndex();
691 if (selectedPoint >= 0)
693 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
694 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
695 inG.setColor(currentColour);
697 inG.drawLine(px, 0, px, getHeight());
698 inG.drawLine(0, py, getWidth(), py);
700 inG.drawOval(px - 2, py - 2, 4, 4);
701 inG.drawOval(px - 3, py - 3, 6, 6);
703 // Return the number of points painted
704 return pointsPainted;
709 * Tests whether there are any dark pixels within the specified x,y rectangle
710 * @param inX left X coordinate
711 * @param inY bottom Y coordinate
712 * @param inWidth width of rectangle
713 * @param inHeight height of rectangle
714 * @param inTextColour colour of text
715 * @return true if the rectangle overlaps stuff too close to the given colour
717 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
719 // each of the colour channels must be further away than this to count as empty
720 final int BRIGHTNESS_LIMIT = 80;
721 final int textRGB = inTextColour.getRGB();
722 final int textLow = textRGB & 255;
723 final int textMid = (textRGB >> 8) & 255;
724 final int textHigh = (textRGB >> 16) & 255;
727 // loop over x coordinate of rectangle
728 for (int x=0; x<inWidth; x++)
730 // loop over y coordinate of rectangle
731 for (int y=0; y<inHeight; y++)
733 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
734 // split into four components rgba
735 int pixLow = pixelColor & 255;
736 int pixMid = (pixelColor >> 8) & 255;
737 int pixHigh = (pixelColor >> 16) & 255;
738 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
739 // If colours are too close in any channel then it's an overlap
740 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
741 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
742 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
746 catch (NullPointerException e) {
747 // ignore null pointers, just return false
753 * Make a semi-transparent colour for drawing with
754 * @param inColour base colour (fully opaque)
755 * @param inOpacity opacity where 0=invisible and 255=full
756 * @return new colour object
758 private static Color makeTransparentColour(Color inColour, int inOpacity)
760 if (inOpacity > 240) return inColour;
761 return new Color(inColour.getRed(), inColour.getGreen(), inColour.getBlue(), inOpacity);
765 * Inform that tiles have been updated and the map can be repainted
766 * @param inIsOk true if data loaded ok, false for error
768 public synchronized void tilesUpdated(boolean inIsOk)
770 // Show message if loading failed (but not too many times)
771 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
773 _shownOsmErrorAlready = true;
774 // use separate thread to show message about failing to load osm images
775 new Thread(new Runnable() {
777 try {Thread.sleep(500);} catch (InterruptedException ie) {}
778 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
787 * Zoom out, if not already at minimum zoom
789 public void zoomOut()
791 _mapPosition.zoomOut();
797 * Zoom in, if not already at maximum zoom
801 _mapPosition.zoomIn();
808 * @param inDeltaX x shift
809 * @param inDeltaY y shift
811 public void panMap(int inDeltaX, int inDeltaY)
813 _mapPosition.pan(inDeltaX, inDeltaY);
819 * Create a DataPoint object from the given click coordinates
820 * @param inX x coordinate of click
821 * @param inY y coordinate of click
822 * @return DataPoint with given coordinates and no altitude
824 private DataPoint createPointFromClick(int inX, int inY)
826 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(inY, getHeight()));
827 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(inX, getWidth()));
828 return new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
829 new Longitude(lon, Coordinate.FORMAT_NONE), null);
833 * @see javax.swing.JComponent#getMinimumSize()
835 public Dimension getMinimumSize()
837 final Dimension minSize = new Dimension(512, 300);
842 * @see javax.swing.JComponent#getPreferredSize()
844 public Dimension getPreferredSize()
846 return getMinimumSize();
851 * Respond to mouse click events
852 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
854 public void mouseClicked(MouseEvent inE)
856 if (_track != null && _track.getNumPoints() > 0)
858 // select point if it's a left-click
859 if (!inE.isMetaDown())
861 if (inE.getClickCount() == 1)
864 if (_drawMode == MODE_DEFAULT)
866 int pointIndex = _track.getNearestPointIndex(
867 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
868 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
869 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
870 // Extend selection for shift-click
871 if (inE.isShiftDown()) {
872 _trackInfo.extendSelection(pointIndex);
875 _trackInfo.selectPoint(pointIndex);
878 else if (_drawMode == MODE_DRAW_POINTS_START)
880 _app.createPoint(createPointFromClick(inE.getX(), inE.getY()));
881 _dragToX = inE.getX();
882 _dragToY = inE.getY();
883 _drawMode = MODE_DRAW_POINTS_CONT;
885 else if (_drawMode == MODE_DRAW_POINTS_CONT)
887 DataPoint point = createPointFromClick(inE.getX(), inE.getY());
888 _app.createPoint(point);
889 point.setSegmentStart(false);
892 else if (inE.getClickCount() == 2)
895 if (_drawMode == MODE_DEFAULT) {
896 panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
899 else if (_drawMode == MODE_DRAW_POINTS_START || _drawMode == MODE_DRAW_POINTS_CONT) {
900 _drawMode = MODE_DEFAULT;
906 // show the popup menu for right-clicks
907 _popupMenuX = inE.getX();
908 _popupMenuY = inE.getY();
909 _popup.show(this, _popupMenuX, _popupMenuY);
915 * Ignore mouse enter events
916 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
918 public void mouseEntered(MouseEvent inE)
924 * Ignore mouse exited events
925 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
927 public void mouseExited(MouseEvent inE)
933 * Ignore mouse pressed events
934 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
936 public void mousePressed(MouseEvent inE)
942 * Respond to mouse released events
943 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
945 public void mouseReleased(MouseEvent inE)
948 if (_drawMode == MODE_ZOOM_RECT && Math.abs(_dragToX - _dragFromX) > 20
949 && Math.abs(_dragToY - _dragFromY) > 20)
951 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
953 if (_drawMode == MODE_ZOOM_RECT) {
954 _drawMode = MODE_DEFAULT;
956 _dragFromX = _dragFromY = -1;
961 * Respond to mouse drag events
962 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
964 public void mouseDragged(MouseEvent inE)
966 if (!inE.isMetaDown())
968 // Left mouse drag - pan map by appropriate amount
969 if (_dragFromX != -1)
971 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
975 _dragFromX = _dragToX = inE.getX();
976 _dragFromY = _dragToY = inE.getY();
980 // Right-click and drag - draw rectangle and control zoom
981 _drawMode = MODE_ZOOM_RECT;
982 if (_dragFromX == -1) {
983 _dragFromX = inE.getX();
984 _dragFromY = inE.getY();
986 _dragToX = inE.getX();
987 _dragToY = inE.getY();
993 * Respond to mouse move events without button pressed
994 * @param inEvent ignored
996 public void mouseMoved(MouseEvent inEvent)
998 // Ignore unless we're drawing points
999 if (_drawMode == MODE_DRAW_POINTS_CONT)
1001 _dragToX = inEvent.getX();
1002 _dragToY = inEvent.getY();
1008 * Respond to status bar message from broker
1009 * @param inMessage message, ignored
1011 public void actionCompleted(String inMessage)
1017 * Respond to data updated message from broker
1018 * @param inUpdateType type of update
1020 public void dataUpdated(byte inUpdateType)
1022 _recalculate = true;
1023 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
1024 _checkBounds = true;
1026 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
1027 _tileManager.resetConfig();
1030 // enable or disable components
1031 boolean hasData = _track.getNumPoints() > 0;
1032 _topPanel.setVisible(hasData);
1033 _sidePanel.setVisible(hasData);
1034 // grab focus for the key presses
1035 this.requestFocus();
1039 * Respond to key presses on the map canvas
1040 * @param inE key event
1042 public void keyPressed(KeyEvent inE)
1044 int code = inE.getKeyCode();
1045 int currPointIndex = _selection.getCurrentPointIndex();
1046 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
1047 if (inE.isControlDown() || inE.isMetaDown())
1049 // Shift as well makes things faster
1050 final int pointIncrement = inE.isShiftDown()?3:1;
1051 // Check for arrow keys to zoom in and out
1052 if (code == KeyEvent.VK_UP)
1054 else if (code == KeyEvent.VK_DOWN)
1056 // Key nav for next/prev point
1057 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
1058 _trackInfo.incrementPointIndex(-pointIncrement);
1059 else if (code == KeyEvent.VK_RIGHT)
1060 _trackInfo.incrementPointIndex(pointIncrement);
1061 else if (code == KeyEvent.VK_PAGE_UP)
1062 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
1063 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1064 else if (code == KeyEvent.VK_PAGE_DOWN)
1065 _trackInfo.selectPoint(Checker.getNextSegmentStart(
1066 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1067 // Check for home and end
1068 else if (code == KeyEvent.VK_HOME)
1069 _trackInfo.selectPoint(0);
1070 else if (code == KeyEvent.VK_END)
1071 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
1075 // Check for arrow keys to pan
1077 if (code == KeyEvent.VK_UP)
1078 upwardsPan = -PAN_DISTANCE;
1079 else if (code == KeyEvent.VK_DOWN)
1080 upwardsPan = PAN_DISTANCE;
1081 int rightwardsPan = 0;
1082 if (code == KeyEvent.VK_RIGHT)
1083 rightwardsPan = PAN_DISTANCE;
1084 else if (code == KeyEvent.VK_LEFT)
1085 rightwardsPan = -PAN_DISTANCE;
1086 panMap(rightwardsPan, upwardsPan);
1088 if (code == KeyEvent.VK_ESCAPE)
1089 _drawMode = MODE_DEFAULT;
1090 // Check for backspace key to delete current point (delete key already handled by menu)
1091 else if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
1092 _app.deleteCurrentPoint();
1098 * @param inE key released event, ignored
1100 public void keyReleased(KeyEvent e)
1106 * @param inE key typed event, ignored
1108 public void keyTyped(KeyEvent inE)
1114 * @param inE mouse wheel event indicating scroll direction
1116 public void mouseWheelMoved(MouseWheelEvent inE)
1118 int clicks = inE.getWheelRotation();
1120 panMap((inE.getX() - getWidth()/2)/2, (inE.getY() - getHeight()/2)/2);
1123 else if (clicks > 0) {
1124 panMap(-(inE.getX() - getWidth()/2), -(inE.getY() - getHeight()/2));
1130 * @return current map position
1132 public MapPosition getMapPosition()
1134 return _mapPosition;