1 package tim.prune.gui.map;
3 import java.awt.BorderLayout;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.FontMetrics;
8 import java.awt.Graphics;
10 import java.awt.RenderingHints;
11 import java.awt.event.ActionEvent;
12 import java.awt.event.ActionListener;
13 import java.awt.event.ItemEvent;
14 import java.awt.event.ItemListener;
15 import java.awt.event.KeyEvent;
16 import java.awt.event.KeyListener;
17 import java.awt.event.MouseEvent;
18 import java.awt.event.MouseListener;
19 import java.awt.event.MouseMotionListener;
20 import java.awt.event.MouseWheelEvent;
21 import java.awt.event.MouseWheelListener;
22 import java.awt.image.BufferedImage;
23 import java.awt.image.RescaleOp;
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 /** Flag set to true for right-click dragging */
105 private boolean _zoomDragging = false;
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;
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;
130 * @param inApp App object for callbacks
131 * @param inTrackInfo track info object
133 public MapCanvas(App inApp, TrackInfo inTrackInfo)
136 _trackInfo = inTrackInfo;
137 _track = inTrackInfo.getTrack();
138 _selection = inTrackInfo.getSelection();
139 _mapPosition = new MapPosition();
140 addMouseListener(this);
141 addMouseMotionListener(this);
142 addMouseWheelListener(this);
143 addKeyListener(this);
145 // Make listener for changes to controls
146 ItemListener itemListener = new ItemListener() {
147 public void itemStateChanged(ItemEvent e)
153 // Make special listener for changes to map checkbox
154 ItemListener mapCheckListener = new ItemListener() {
155 public void itemStateChanged(ItemEvent e)
157 _tileManager.clearMemoryCaches();
159 Config.setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
160 UpdateMessageBroker.informSubscribers(); // to let menu know
163 _topPanel = new JPanel();
164 _topPanel.setLayout(new FlowLayout());
165 _topPanel.setOpaque(false);
166 // Make slider for transparency
167 _transparencySlider = new JSlider(0, 5, 0);
168 _transparencySlider.setPreferredSize(new Dimension(100, 20));
169 _transparencySlider.setMajorTickSpacing(1);
170 _transparencySlider.setSnapToTicks(true);
171 _transparencySlider.setOpaque(false);
172 _transparencySlider.addChangeListener(new ChangeListener() {
173 public void stateChanged(ChangeEvent e)
179 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
180 _topPanel.add(_transparencySlider);
181 // Add checkbox button for enabling scale bar
182 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
183 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
184 _scaleCheckBox.setOpaque(false);
185 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
186 _scaleCheckBox.addItemListener(new ItemListener() {
187 public void itemStateChanged(ItemEvent e) {
188 _scaleBar.setVisible(_scaleCheckBox.isSelected());
191 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
192 _topPanel.add(_scaleCheckBox);
193 // Add checkbox button for enabling maps or not
194 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
195 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
196 _mapCheckBox.setOpaque(false);
197 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
198 _mapCheckBox.addItemListener(mapCheckListener);
199 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
200 _topPanel.add(_mapCheckBox);
201 // Add checkbox button for enabling autopan or not
202 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
203 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
204 _autopanCheckBox.setOpaque(false);
205 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
206 _autopanCheckBox.addItemListener(itemListener);
207 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
208 _topPanel.add(_autopanCheckBox);
209 // Add checkbox button for connecting points or not
210 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
211 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
212 _connectCheckBox.setOpaque(false);
213 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
214 _connectCheckBox.addItemListener(itemListener);
215 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
216 _topPanel.add(_connectCheckBox);
218 // Add zoom in, zoom out buttons
219 _sidePanel = new JPanel();
220 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
221 _sidePanel.setOpaque(false);
222 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
223 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
224 zoomInButton.setContentAreaFilled(false);
225 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
226 zoomInButton.addActionListener(new ActionListener() {
227 public void actionPerformed(ActionEvent e)
232 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
233 _sidePanel.add(zoomInButton);
234 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
235 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
236 zoomOutButton.setContentAreaFilled(false);
237 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
238 zoomOutButton.addActionListener(new ActionListener() {
239 public void actionPerformed(ActionEvent e)
244 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
245 _sidePanel.add(zoomOutButton);
247 // Bottom panel for scale bar
248 _scaleBar = new ScaleBar();
250 // add control panels to this one
251 setLayout(new BorderLayout());
252 _topPanel.setVisible(false);
253 _sidePanel.setVisible(false);
254 add(_topPanel, BorderLayout.NORTH);
255 add(_sidePanel, BorderLayout.WEST);
256 add(_scaleBar, BorderLayout.SOUTH);
263 * Make the popup menu for right-clicking the map
265 private void makePopup()
267 _popup = new JPopupMenu();
268 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
269 zoomInItem.addActionListener(new ActionListener() {
270 public void actionPerformed(ActionEvent e)
274 zoomInItem.setEnabled(true);
275 _popup.add(zoomInItem);
276 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
277 zoomOutItem.addActionListener(new ActionListener() {
278 public void actionPerformed(ActionEvent e)
282 zoomOutItem.setEnabled(true);
283 _popup.add(zoomOutItem);
284 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
285 zoomFullItem.addActionListener(new ActionListener() {
286 public void actionPerformed(ActionEvent e)
292 zoomFullItem.setEnabled(true);
293 _popup.add(zoomFullItem);
294 _popup.addSeparator();
296 JMenuItem setMapBgItem = new JMenuItem(
297 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
298 setMapBgItem.addActionListener(new ActionListener() {
299 public void actionPerformed(ActionEvent e)
301 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
303 _popup.add(setMapBgItem);
305 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
306 newPointItem.addActionListener(new ActionListener() {
307 public void actionPerformed(ActionEvent e)
309 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_popupMenuY, getHeight()));
310 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_popupMenuX, getWidth()));
311 _app.createPoint(new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
312 new Longitude(lon, Coordinate.FORMAT_NONE), null));
314 newPointItem.setEnabled(true);
315 _popup.add(newPointItem);
320 * Zoom to fit the current data area
322 private void zoomToFit()
324 _latRange = _track.getLatRange();
325 _lonRange = _track.getLonRange();
326 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
327 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
328 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
329 MapUtils.getYFromLatitude(_latRange.getMaximum()));
330 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
331 getWidth(), getHeight());
337 * @see java.awt.Canvas#paint(java.awt.Graphics)
339 public void paint(Graphics inG)
342 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
345 if (_track.getNumPoints() > 0)
347 // Check for autopan if enabled / necessary
348 if (_autopanCheckBox.isSelected())
350 int selectedPoint = _selection.getCurrentPointIndex();
351 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
353 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
354 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
357 if (px < PAN_DISTANCE) {
358 panX = px - AUTOPAN_DISTANCE;
360 else if (px > (getWidth()-PAN_DISTANCE)) {
361 panX = AUTOPAN_DISTANCE + px - getWidth();
363 if (py < PAN_DISTANCE) {
364 panY = py - AUTOPAN_DISTANCE;
366 if (py > (getHeight()-PAN_DISTANCE)) {
367 panY = AUTOPAN_DISTANCE + py - getHeight();
369 if (panX != 0 || panY != 0) {
370 _mapPosition.pan(panX, panY);
373 _prevSelectedPoint = selectedPoint;
376 // Draw the map contents if necessary
377 if ((_mapImage == null || _recalculate))
380 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getCentreTileY());
382 // Draw the prepared image onto the panel
383 if (_mapImage != null) {
384 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
386 // Draw the zoom rectangle if necessary
389 inG.setColor(Color.RED);
390 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
391 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
392 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
393 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
398 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
399 inG.fillRect(0, 0, getWidth(), getHeight());
400 inG.setColor(COLOR_MESSAGES);
401 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
402 _scaleBar.updateScale(-1, 0);
404 // Draw slider etc on top
410 * Paint the map tiles and the points on to the _mapImage
412 private void paintMapContents()
414 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
416 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
420 Graphics g = _mapImage.getGraphics();
421 // Clear to background
422 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
423 g.fillRect(0, 0, getWidth(), getHeight());
425 // Check whether maps are on or not
426 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
427 _mapCheckBox.setSelected(showMap);
429 // reset error message
430 if (!showMap) {_shownOsmErrorAlready = false;}
431 _recalculate = false;
432 // Only get map tiles if selected
436 _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
438 boolean loadingFailed = false;
439 if (_mapImage == null) return;
441 if (_tileManager.isOverzoomed())
443 // display overzoom message
444 g.setColor(COLOR_MESSAGES);
445 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
449 int numLayers = _tileManager.getNumLayers();
450 // Loop over tiles drawing each one
451 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
452 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
453 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
455 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
456 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
458 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
460 for (int l=0; l<numLayers; l++)
462 Image image = _tileManager.getTile(l, tileX, tileY);
464 g.drawImage(image, x, y, 256, 256, null);
470 // Make maps brighter / fainter
471 float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.0f};
472 float scaleFactor = scaleFactors[_transparencySlider.getValue()];
473 if (scaleFactor > 1.0f)
475 RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
476 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
477 RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
478 op.filter(_mapImage, _mapImage);
483 // Paint the track points on top
484 int pointsPainted = 1;
487 pointsPainted = paintPoints(g);
489 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
495 // Zoom to fit if no points found
496 if (pointsPainted <= 0 && _checkBounds) {
501 _checkBounds = false;
502 // enable / disable transparency slider
503 _transparencySlider.setEnabled(showMap);
508 * Paint the points using the given graphics object
509 * @param inG Graphics object to use for painting
510 * @return number of points painted, if any
512 private int paintPoints(Graphics inG)
515 final Color pointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
516 final Color rangeColour = Config.getColourScheme().getColour(ColourScheme.IDX_SELECTION);
517 final Color currentColour = Config.getColourScheme().getColour(ColourScheme.IDX_PRIMARY);
518 final Color secondColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
519 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
521 int pointsPainted = 0;
523 inG.setColor(pointColour);
524 int prevX = -1, prevY = -1;
525 boolean connectPoints = _connectCheckBox.isSelected();
526 boolean prevPointVisible = false, currPointVisible = false;
527 boolean anyWaypoints = false;
528 boolean isWaypoint = false;
529 for (int i=0; i<_track.getNumPoints(); i++)
531 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
532 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
533 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
534 isWaypoint = _track.getPoint(i).isWaypoint();
535 anyWaypoints = anyWaypoints || isWaypoint;
536 if (currPointVisible)
540 // Draw rectangle for track point
541 if (_track.getPoint(i).getDeleteFlag()) {
542 inG.setColor(currentColour);
545 inG.setColor(pointColour);
547 inG.drawRect(px-2, py-2, 3, 3);
553 // Connect track points if either of them are visible
554 if (connectPoints && (currPointVisible || prevPointVisible)
555 && !(prevX == -1 && prevY == -1)
556 && !_track.getPoint(i).getSegmentStart())
558 inG.drawLine(prevX, prevY, px, py);
560 prevX = px; prevY = py;
562 prevPointVisible = currPointVisible;
565 // Loop over points, just drawing blobs for waypoints
566 inG.setColor(textColour);
567 FontMetrics fm = inG.getFontMetrics();
568 int nameHeight = fm.getHeight();
569 int width = getWidth();
570 int height = getHeight();
572 for (int i=0; i<_track.getNumPoints(); i++)
574 if (_track.getPoint(i).isWaypoint())
576 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
577 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
578 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
580 inG.fillRect(px-3, py-3, 6, 6);
585 // Loop over points again, now draw names for waypoints
586 for (int i=0; i<_track.getNumPoints(); i++)
588 if (_track.getPoint(i).isWaypoint())
590 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
591 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
592 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
594 // Figure out where to draw waypoint name so it doesn't obscure track
595 String waypointName = _track.getPoint(i).getWaypointName();
596 int nameWidth = fm.stringWidth(waypointName);
597 boolean drawnName = false;
598 // Make arrays for coordinates right left up down
599 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
600 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
601 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
603 // Shift arrays for coordinates right left up down
604 nameXs[0] += 2; nameXs[1] -= 2;
605 nameYs[2] -= 2; nameYs[3] += 2;
606 // Check each direction in turn right left up down
607 for (int a=0; a<4; a++)
609 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
610 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
611 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
613 // Found a rectangle to fit - draw name here and quit
614 inG.drawString(waypointName, nameXs[a], nameYs[a]);
624 // Loop over points, drawing blobs for photo points
625 inG.setColor(secondColour);
626 for (int i=0; i<_track.getNumPoints(); i++)
628 if (_track.getPoint(i).getPhoto() != null)
630 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
631 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
632 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
634 inG.drawRect(px-1, py-1, 2, 2);
635 inG.drawRect(px-2, py-2, 4, 4);
641 // Draw selected range
642 if (_selection.hasRangeSelected())
644 inG.setColor(rangeColour);
645 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
647 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
648 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
649 inG.drawRect(px-1, py-1, 2, 2);
653 // Draw selected point, crosshairs
654 int selectedPoint = _selection.getCurrentPointIndex();
655 if (selectedPoint >= 0)
657 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
658 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
659 inG.setColor(currentColour);
661 inG.drawLine(px, 0, px, getHeight());
662 inG.drawLine(0, py, getWidth(), py);
664 inG.drawOval(px - 2, py - 2, 4, 4);
665 inG.drawOval(px - 3, py - 3, 6, 6);
667 // Return the number of points painted
668 return pointsPainted;
673 * Tests whether there are any dark pixels within the specified x,y rectangle
674 * @param inX left X coordinate
675 * @param inY bottom Y coordinate
676 * @param inWidth width of rectangle
677 * @param inHeight height of rectangle
678 * @param inTextColour colour of text
679 * @return true if the rectangle overlaps stuff too close to the given colour
681 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
683 // each of the colour channels must be further away than this to count as empty
684 final int BRIGHTNESS_LIMIT = 80;
685 final int textRGB = inTextColour.getRGB();
686 final int textLow = textRGB & 255;
687 final int textMid = (textRGB >> 8) & 255;
688 final int textHigh = (textRGB >> 16) & 255;
691 // loop over x coordinate of rectangle
692 for (int x=0; x<inWidth; x++)
694 // loop over y coordinate of rectangle
695 for (int y=0; y<inHeight; y++)
697 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
698 // split into four components rgba
699 int pixLow = pixelColor & 255;
700 int pixMid = (pixelColor >> 8) & 255;
701 int pixHigh = (pixelColor >> 16) & 255;
702 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
703 // If colours are too close in any channel then it's an overlap
704 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
705 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
706 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
710 catch (NullPointerException e) {
711 // ignore null pointers, just return false
718 * Inform that tiles have been updated and the map can be repainted
719 * @param inIsOk true if data loaded ok, false for error
721 public synchronized void tilesUpdated(boolean inIsOk)
723 // Show message if loading failed (but not too many times)
724 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
726 _shownOsmErrorAlready = true;
727 // use separate thread to show message about failing to load osm images
728 new Thread(new Runnable() {
730 try {Thread.sleep(500);} catch (InterruptedException ie) {}
731 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
740 * Zoom out, if not already at minimum zoom
742 public void zoomOut()
744 _mapPosition.zoomOut();
750 * Zoom in, if not already at maximum zoom
754 _mapPosition.zoomIn();
761 * @param inDeltaX x shift
762 * @param inDeltaY y shift
764 public void panMap(int inDeltaX, int inDeltaY)
766 _mapPosition.pan(inDeltaX, inDeltaY);
772 * @see javax.swing.JComponent#getMinimumSize()
774 public Dimension getMinimumSize()
776 final Dimension minSize = new Dimension(512, 300);
781 * @see javax.swing.JComponent#getPreferredSize()
783 public Dimension getPreferredSize()
785 return getMinimumSize();
790 * Respond to mouse click events
791 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
793 public void mouseClicked(MouseEvent inE)
795 if (_track != null && _track.getNumPoints() > 0)
797 // select point if it's a left-click
798 if (!inE.isMetaDown())
800 int pointIndex = _track.getNearestPointIndex(
801 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
802 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
803 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
804 // Extend selection for shift-click
805 if (inE.isShiftDown()) {
806 _trackInfo.extendSelection(pointIndex);
809 _trackInfo.selectPoint(pointIndex);
814 // show the popup menu for right-clicks
815 _popupMenuX = inE.getX();
816 _popupMenuY = inE.getY();
817 _popup.show(this, _popupMenuX, _popupMenuY);
823 * Ignore mouse enter events
824 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
826 public void mouseEntered(MouseEvent inE)
832 * Ignore mouse exited events
833 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
835 public void mouseExited(MouseEvent inE)
841 * Ignore mouse pressed events
842 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
844 public void mousePressed(MouseEvent inE)
850 * Respond to mouse released events
851 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
853 public void mouseReleased(MouseEvent inE)
856 if (_zoomDragging && Math.abs(_dragToX - _dragFromX) > 20 && Math.abs(_dragToY - _dragFromY) > 20)
858 //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
859 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
861 _dragFromX = _dragFromY = -1;
862 _zoomDragging = false;
867 * Respond to mouse drag events
868 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
870 public void mouseDragged(MouseEvent inE)
872 if (!inE.isMetaDown())
874 // Left mouse drag - pan map by appropriate amount
875 _zoomDragging = false;
876 if (_dragFromX != -1)
878 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
882 _dragFromX = inE.getX();
883 _dragFromY = inE.getY();
887 // Right-click and drag - draw rectangle and control zoom
888 _zoomDragging = true;
889 if (_dragFromX == -1) {
890 _dragFromX = inE.getX();
891 _dragFromY = inE.getY();
893 _dragToX = inE.getX();
894 _dragToY = inE.getY();
900 * Respond to mouse move events without button pressed
901 * @param inEvent ignored
903 public void mouseMoved(MouseEvent inEvent)
909 * Respond to status bar message from broker
910 * @param inMessage message, ignored
912 public void actionCompleted(String inMessage)
918 * Respond to data updated message from broker
919 * @param inUpdateType type of update
921 public void dataUpdated(byte inUpdateType)
924 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
927 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
928 _tileManager.resetConfig();
931 // enable or disable components
932 boolean hasData = _track.getNumPoints() > 0;
933 _topPanel.setVisible(hasData);
934 _sidePanel.setVisible(hasData);
935 // grab focus for the key presses
940 * Respond to key presses on the map canvas
941 * @param inE key event
943 public void keyPressed(KeyEvent inE)
945 int code = inE.getKeyCode();
946 int currPointIndex = _selection.getCurrentPointIndex();
947 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
948 if (inE.isControlDown() || inE.isMetaDown())
950 // Check for arrow keys to zoom in and out
951 if (code == KeyEvent.VK_UP)
953 else if (code == KeyEvent.VK_DOWN)
955 // Key nav for next/prev point
956 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
957 _trackInfo.selectPoint(currPointIndex-1);
958 else if (code == KeyEvent.VK_RIGHT)
959 _trackInfo.selectPoint(currPointIndex+1);
960 else if (code == KeyEvent.VK_PAGE_UP)
961 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
962 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
963 else if (code == KeyEvent.VK_PAGE_DOWN)
964 _trackInfo.selectPoint(Checker.getNextSegmentStart(
965 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
966 // Check for home and end
967 else if (code == KeyEvent.VK_HOME)
968 _trackInfo.selectPoint(0);
969 else if (code == KeyEvent.VK_END)
970 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
974 // Check for arrow keys to pan
976 if (code == KeyEvent.VK_UP)
977 upwardsPan = -PAN_DISTANCE;
978 else if (code == KeyEvent.VK_DOWN)
979 upwardsPan = PAN_DISTANCE;
980 int rightwardsPan = 0;
981 if (code == KeyEvent.VK_RIGHT)
982 rightwardsPan = PAN_DISTANCE;
983 else if (code == KeyEvent.VK_LEFT)
984 rightwardsPan = -PAN_DISTANCE;
985 panMap(rightwardsPan, upwardsPan);
986 // Check for backspace key to delete current point (delete key already handled by menu)
987 if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
988 _app.deleteCurrentPoint();
994 * @param inE key released event, ignored
996 public void keyReleased(KeyEvent e)
1002 * @param inE key typed event, ignored
1004 public void keyTyped(KeyEvent inE)
1010 * @param inE mouse wheel event indicating scroll direction
1012 public void mouseWheelMoved(MouseWheelEvent inE)
1014 int clicks = inE.getWheelRotation();
1017 else if (clicks > 0)
1022 * @return current map position
1024 public MapPosition getMapPosition()
1026 return _mapPosition;