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)
281 _popup.add(zoomInItem);
282 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
283 zoomOutItem.addActionListener(new ActionListener() {
284 public void actionPerformed(ActionEvent e)
288 _popup.add(zoomOutItem);
289 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
290 zoomFullItem.addActionListener(new ActionListener() {
291 public void actionPerformed(ActionEvent e)
297 _popup.add(zoomFullItem);
298 _popup.addSeparator();
300 JMenuItem setMapBgItem = new JMenuItem(
301 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
302 setMapBgItem.addActionListener(new ActionListener() {
303 public void actionPerformed(ActionEvent e)
305 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
307 _popup.add(setMapBgItem);
309 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
310 newPointItem.addActionListener(new ActionListener() {
311 public void actionPerformed(ActionEvent e)
313 _app.createPoint(createPointFromClick(_popupMenuX, _popupMenuY));
315 _popup.add(newPointItem);
317 JMenuItem drawPointsItem = new JMenuItem(I18nManager.getText("menu.map.drawpoints"));
318 drawPointsItem.addActionListener(new ActionListener() {
319 public void actionPerformed(ActionEvent e)
321 _drawMode = MODE_DRAW_POINTS_START;
324 _popup.add(drawPointsItem);
329 * Zoom to fit the current data area
331 private void zoomToFit()
333 _latRange = _track.getLatRange();
334 _lonRange = _track.getLonRange();
335 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
336 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
337 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
338 MapUtils.getYFromLatitude(_latRange.getMaximum()));
339 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
340 getWidth(), getHeight());
346 * @see java.awt.Canvas#paint(java.awt.Graphics)
348 public void paint(Graphics inG)
351 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
354 if (_track.getNumPoints() > 0)
356 // Check for autopan if enabled / necessary
357 if (_autopanCheckBox.isSelected())
359 int selectedPoint = _selection.getCurrentPointIndex();
360 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
362 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
363 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
366 if (px < PAN_DISTANCE) {
367 panX = px - AUTOPAN_DISTANCE;
369 else if (px > (getWidth()-PAN_DISTANCE)) {
370 panX = AUTOPAN_DISTANCE + px - getWidth();
372 if (py < PAN_DISTANCE) {
373 panY = py - AUTOPAN_DISTANCE;
375 if (py > (getHeight()-PAN_DISTANCE)) {
376 panY = AUTOPAN_DISTANCE + py - getHeight();
378 if (panX != 0 || panY != 0) {
379 _mapPosition.pan(panX, panY);
382 _prevSelectedPoint = selectedPoint;
385 // Draw the map contents if necessary
386 if ((_mapImage == null || _recalculate))
389 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
391 // Draw the prepared image onto the panel
392 if (_mapImage != null) {
393 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
395 // Draw the zoom rectangle if necessary
396 if (_drawMode == MODE_ZOOM_RECT)
398 inG.setColor(Color.RED);
399 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
400 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
401 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
402 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
404 else if (_drawMode == MODE_DRAW_POINTS_CONT)
406 // draw line to mouse position to show drawing mode
407 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT));
408 int prevIndex = _track.getNumPoints()-1;
409 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(prevIndex));
410 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(prevIndex));
411 inG.drawLine(px, py, _dragToX, _dragToY);
416 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
417 inG.fillRect(0, 0, getWidth(), getHeight());
418 inG.setColor(COLOR_MESSAGES);
419 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
420 _scaleBar.updateScale(-1, 0);
422 // Draw slider etc on top
428 * Paint the map tiles and the points on to the _mapImage
430 private void paintMapContents()
432 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
434 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
438 Graphics g = _mapImage.getGraphics();
439 // Clear to background
440 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
441 g.fillRect(0, 0, getWidth(), getHeight());
443 // Check whether maps are on or not
444 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
445 _mapCheckBox.setSelected(showMap);
447 // reset error message
448 if (!showMap) {_shownOsmErrorAlready = false;}
449 _recalculate = false;
450 // Only get map tiles if selected
454 _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
456 boolean loadingFailed = false;
457 if (_mapImage == null) return;
459 if (_tileManager.isOverzoomed())
461 // display overzoom message
462 g.setColor(COLOR_MESSAGES);
463 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
467 int numLayers = _tileManager.getNumLayers();
468 // Loop over tiles drawing each one
469 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
470 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
471 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
473 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
474 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
476 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
478 for (int l=0; l<numLayers; l++)
480 Image image = _tileManager.getTile(l, tileX, tileY);
482 g.drawImage(image, x, y, 256, 256, null);
488 // Make maps brighter / fainter
489 final float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.2f};
490 final float scaleFactor = scaleFactors[_transparencySlider.getValue()];
491 if (scaleFactor > 1.0f)
493 RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
494 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
495 RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
496 op.filter(_mapImage, _mapImage);
501 // Paint the track points on top
502 int pointsPainted = 1;
505 pointsPainted = paintPoints(g);
507 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
513 // Zoom to fit if no points found
514 if (pointsPainted <= 0 && _checkBounds) {
519 _checkBounds = false;
520 // enable / disable transparency slider
521 _transparencySlider.setEnabled(showMap);
526 * Paint the points using the given graphics object
527 * @param inG Graphics object to use for painting
528 * @return number of points painted, if any
530 private int paintPoints(Graphics inG)
533 final Color pointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
534 final Color rangeColour = Config.getColourScheme().getColour(ColourScheme.IDX_SELECTION);
535 final Color currentColour = Config.getColourScheme().getColour(ColourScheme.IDX_PRIMARY);
536 final Color secondColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
537 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
539 // try to set line width for painting
540 if (inG instanceof Graphics2D)
542 int lineWidth = Config.getConfigInt(Config.KEY_LINE_WIDTH);
543 if (lineWidth < 1 || lineWidth > 4) {lineWidth = 2;}
544 ((Graphics2D) inG).setStroke(new BasicStroke(lineWidth));
546 int pointsPainted = 0;
548 inG.setColor(pointColour);
549 int prevX = -1, prevY = -1;
550 boolean connectPoints = _connectCheckBox.isSelected();
551 boolean prevPointVisible = false, currPointVisible = false;
552 boolean anyWaypoints = false;
553 boolean isWaypoint = false;
554 for (int i=0; i<_track.getNumPoints(); i++)
556 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
557 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
558 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
559 isWaypoint = _track.getPoint(i).isWaypoint();
560 anyWaypoints = anyWaypoints || isWaypoint;
561 if (currPointVisible)
565 // Draw rectangle for track point
566 if (_track.getPoint(i).getDeleteFlag()) {
567 inG.setColor(currentColour);
570 inG.setColor(pointColour);
572 inG.drawRect(px-2, py-2, 3, 3);
578 // Connect track points if either of them are visible
579 if (connectPoints && (currPointVisible || prevPointVisible)
580 && !(prevX == -1 && prevY == -1)
581 && !_track.getPoint(i).getSegmentStart())
583 inG.drawLine(prevX, prevY, px, py);
585 prevX = px; prevY = py;
587 prevPointVisible = currPointVisible;
590 // Loop over points, just drawing blobs for waypoints
591 inG.setColor(textColour);
592 FontMetrics fm = inG.getFontMetrics();
593 int nameHeight = fm.getHeight();
594 int width = getWidth();
595 int height = getHeight();
597 for (int i=0; i<_track.getNumPoints(); i++)
599 if (_track.getPoint(i).isWaypoint())
601 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
602 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
603 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
605 inG.fillRect(px-3, py-3, 6, 6);
610 // Loop over points again, now draw names for waypoints
611 for (int i=0; i<_track.getNumPoints(); i++)
613 if (_track.getPoint(i).isWaypoint())
615 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
616 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
617 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
619 // Figure out where to draw waypoint name so it doesn't obscure track
620 String waypointName = _track.getPoint(i).getWaypointName();
621 int nameWidth = fm.stringWidth(waypointName);
622 boolean drawnName = false;
623 // Make arrays for coordinates right left up down
624 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
625 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
626 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
628 // Shift arrays for coordinates right left up down
629 nameXs[0] += 2; nameXs[1] -= 2;
630 nameYs[2] -= 2; nameYs[3] += 2;
631 // Check each direction in turn right left up down
632 for (int a=0; a<4; a++)
634 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
635 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
636 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
638 // Found a rectangle to fit - draw name here and quit
639 inG.drawString(waypointName, nameXs[a], nameYs[a]);
649 // Loop over points, drawing blobs for photo / audio points
650 inG.setColor(secondColour);
651 for (int i=0; i<_track.getNumPoints(); i++)
653 if (_track.getPoint(i).hasMedia())
655 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
656 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
657 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
659 inG.drawRect(px-1, py-1, 2, 2);
660 inG.drawRect(px-2, py-2, 4, 4);
666 // Draw selected range
667 if (_selection.hasRangeSelected())
669 inG.setColor(rangeColour);
670 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
672 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
673 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
674 inG.drawRect(px-1, py-1, 2, 2);
678 // Draw selected point, crosshairs
679 int selectedPoint = _selection.getCurrentPointIndex();
680 if (selectedPoint >= 0)
682 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
683 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
684 inG.setColor(currentColour);
686 inG.drawLine(px, 0, px, getHeight());
687 inG.drawLine(0, py, getWidth(), py);
689 inG.drawOval(px - 2, py - 2, 4, 4);
690 inG.drawOval(px - 3, py - 3, 6, 6);
692 // Return the number of points painted
693 return pointsPainted;
698 * Tests whether there are any dark pixels within the specified x,y rectangle
699 * @param inX left X coordinate
700 * @param inY bottom Y coordinate
701 * @param inWidth width of rectangle
702 * @param inHeight height of rectangle
703 * @param inTextColour colour of text
704 * @return true if the rectangle overlaps stuff too close to the given colour
706 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
708 // each of the colour channels must be further away than this to count as empty
709 final int BRIGHTNESS_LIMIT = 80;
710 final int textRGB = inTextColour.getRGB();
711 final int textLow = textRGB & 255;
712 final int textMid = (textRGB >> 8) & 255;
713 final int textHigh = (textRGB >> 16) & 255;
716 // loop over x coordinate of rectangle
717 for (int x=0; x<inWidth; x++)
719 // loop over y coordinate of rectangle
720 for (int y=0; y<inHeight; y++)
722 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
723 // split into four components rgba
724 int pixLow = pixelColor & 255;
725 int pixMid = (pixelColor >> 8) & 255;
726 int pixHigh = (pixelColor >> 16) & 255;
727 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
728 // If colours are too close in any channel then it's an overlap
729 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
730 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
731 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
735 catch (NullPointerException e) {
736 // ignore null pointers, just return false
743 * Inform that tiles have been updated and the map can be repainted
744 * @param inIsOk true if data loaded ok, false for error
746 public synchronized void tilesUpdated(boolean inIsOk)
748 // Show message if loading failed (but not too many times)
749 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
751 _shownOsmErrorAlready = true;
752 // use separate thread to show message about failing to load osm images
753 new Thread(new Runnable() {
755 try {Thread.sleep(500);} catch (InterruptedException ie) {}
756 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
765 * Zoom out, if not already at minimum zoom
767 public void zoomOut()
769 _mapPosition.zoomOut();
775 * Zoom in, if not already at maximum zoom
779 _mapPosition.zoomIn();
786 * @param inDeltaX x shift
787 * @param inDeltaY y shift
789 public void panMap(int inDeltaX, int inDeltaY)
791 _mapPosition.pan(inDeltaX, inDeltaY);
797 * Create a DataPoint object from the given click coordinates
798 * @param inX x coordinate of click
799 * @param inY y coordinate of click
800 * @return DataPoint with given coordinates and no altitude
802 private DataPoint createPointFromClick(int inX, int inY)
804 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(inY, getHeight()));
805 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(inX, getWidth()));
806 return new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
807 new Longitude(lon, Coordinate.FORMAT_NONE), null);
811 * @see javax.swing.JComponent#getMinimumSize()
813 public Dimension getMinimumSize()
815 final Dimension minSize = new Dimension(512, 300);
820 * @see javax.swing.JComponent#getPreferredSize()
822 public Dimension getPreferredSize()
824 return getMinimumSize();
829 * Respond to mouse click events
830 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
832 public void mouseClicked(MouseEvent inE)
834 if (_track != null && _track.getNumPoints() > 0)
836 // select point if it's a left-click
837 if (!inE.isMetaDown())
839 if (inE.getClickCount() == 1)
842 if (_drawMode == MODE_DEFAULT)
844 int pointIndex = _track.getNearestPointIndex(
845 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
846 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
847 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
848 // Extend selection for shift-click
849 if (inE.isShiftDown()) {
850 _trackInfo.extendSelection(pointIndex);
853 _trackInfo.selectPoint(pointIndex);
856 else if (_drawMode == MODE_DRAW_POINTS_START)
858 _app.createPoint(createPointFromClick(inE.getX(), inE.getY()));
859 _dragToX = inE.getX();
860 _dragToY = inE.getY();
861 _drawMode = MODE_DRAW_POINTS_CONT;
863 else if (_drawMode == MODE_DRAW_POINTS_CONT)
865 DataPoint point = createPointFromClick(inE.getX(), inE.getY());
866 _app.createPoint(point);
867 point.setSegmentStart(false);
870 else if (inE.getClickCount() == 2) {
872 if (_drawMode == MODE_DEFAULT) {
873 panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
876 else if (_drawMode == MODE_DRAW_POINTS_START || _drawMode == MODE_DRAW_POINTS_CONT) {
877 _drawMode = MODE_DEFAULT;
883 // show the popup menu for right-clicks
884 _popupMenuX = inE.getX();
885 _popupMenuY = inE.getY();
886 _popup.show(this, _popupMenuX, _popupMenuY);
892 * Ignore mouse enter events
893 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
895 public void mouseEntered(MouseEvent inE)
901 * Ignore mouse exited events
902 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
904 public void mouseExited(MouseEvent inE)
910 * Ignore mouse pressed events
911 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
913 public void mousePressed(MouseEvent inE)
919 * Respond to mouse released events
920 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
922 public void mouseReleased(MouseEvent inE)
925 if (_drawMode == MODE_ZOOM_RECT && Math.abs(_dragToX - _dragFromX) > 20
926 && Math.abs(_dragToY - _dragFromY) > 20)
928 //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
929 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
930 _drawMode = MODE_DEFAULT;
932 _dragFromX = _dragFromY = -1;
937 * Respond to mouse drag events
938 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
940 public void mouseDragged(MouseEvent inE)
942 if (!inE.isMetaDown())
944 // Left mouse drag - pan map by appropriate amount
945 if (_dragFromX != -1)
947 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
951 _dragFromX = _dragToX = inE.getX();
952 _dragFromY = _dragToY = inE.getY();
956 // Right-click and drag - draw rectangle and control zoom
957 _drawMode = MODE_ZOOM_RECT;
958 if (_dragFromX == -1) {
959 _dragFromX = inE.getX();
960 _dragFromY = inE.getY();
962 _dragToX = inE.getX();
963 _dragToY = inE.getY();
969 * Respond to mouse move events without button pressed
970 * @param inEvent ignored
972 public void mouseMoved(MouseEvent inEvent)
974 // Ignore unless we're drawing points
975 if (_drawMode == MODE_DRAW_POINTS_CONT)
977 _dragToX = inEvent.getX();
978 _dragToY = inEvent.getY();
984 * Respond to status bar message from broker
985 * @param inMessage message, ignored
987 public void actionCompleted(String inMessage)
993 * Respond to data updated message from broker
994 * @param inUpdateType type of update
996 public void dataUpdated(byte inUpdateType)
999 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
1000 _checkBounds = true;
1002 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
1003 _tileManager.resetConfig();
1006 // enable or disable components
1007 boolean hasData = _track.getNumPoints() > 0;
1008 _topPanel.setVisible(hasData);
1009 _sidePanel.setVisible(hasData);
1010 // grab focus for the key presses
1011 this.requestFocus();
1015 * Respond to key presses on the map canvas
1016 * @param inE key event
1018 public void keyPressed(KeyEvent inE)
1020 int code = inE.getKeyCode();
1021 int currPointIndex = _selection.getCurrentPointIndex();
1022 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
1023 if (inE.isControlDown() || inE.isMetaDown())
1025 // Check for arrow keys to zoom in and out
1026 if (code == KeyEvent.VK_UP)
1028 else if (code == KeyEvent.VK_DOWN)
1030 // Key nav for next/prev point
1031 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
1032 _trackInfo.selectPoint(currPointIndex-1);
1033 else if (code == KeyEvent.VK_RIGHT)
1034 _trackInfo.selectPoint(currPointIndex+1);
1035 else if (code == KeyEvent.VK_PAGE_UP)
1036 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
1037 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1038 else if (code == KeyEvent.VK_PAGE_DOWN)
1039 _trackInfo.selectPoint(Checker.getNextSegmentStart(
1040 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
1041 // Check for home and end
1042 else if (code == KeyEvent.VK_HOME)
1043 _trackInfo.selectPoint(0);
1044 else if (code == KeyEvent.VK_END)
1045 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
1049 // Check for arrow keys to pan
1051 if (code == KeyEvent.VK_UP)
1052 upwardsPan = -PAN_DISTANCE;
1053 else if (code == KeyEvent.VK_DOWN)
1054 upwardsPan = PAN_DISTANCE;
1055 int rightwardsPan = 0;
1056 if (code == KeyEvent.VK_RIGHT)
1057 rightwardsPan = PAN_DISTANCE;
1058 else if (code == KeyEvent.VK_LEFT)
1059 rightwardsPan = -PAN_DISTANCE;
1060 panMap(rightwardsPan, upwardsPan);
1062 if (code == KeyEvent.VK_ESCAPE)
1063 _drawMode = MODE_DEFAULT;
1064 // Check for backspace key to delete current point (delete key already handled by menu)
1065 else if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
1066 _app.deleteCurrentPoint();
1072 * @param inE key released event, ignored
1074 public void keyReleased(KeyEvent e)
1080 * @param inE key typed event, ignored
1082 public void keyTyped(KeyEvent inE)
1088 * @param inE mouse wheel event indicating scroll direction
1090 public void mouseWheelMoved(MouseWheelEvent inE)
1092 int clicks = inE.getWheelRotation();
1095 else if (clicks > 0)
1100 * @return current map position
1102 public MapPosition getMapPosition()
1104 return _mapPosition;