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 /** Flag set to true for right-click dragging */
107 private boolean _zoomDragging = false;
108 /** x coordinate of drag to point */
109 private int _dragToX = -1;
110 /** y coordinate of drag to point */
111 private int _dragToY = -1;
112 /** x coordinate of popup menu */
113 private int _popupMenuX = -1;
114 /** y coordinate of popup menu */
115 private int _popupMenuY = -1;
116 /** Flag to prevent showing too often the error message about loading maps */
117 private boolean _shownOsmErrorAlready = false;
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;
132 * @param inApp App object for callbacks
133 * @param inTrackInfo track info object
135 public MapCanvas(App inApp, TrackInfo inTrackInfo)
138 _trackInfo = inTrackInfo;
139 _track = inTrackInfo.getTrack();
140 _selection = inTrackInfo.getSelection();
141 _mapPosition = new MapPosition();
142 addMouseListener(this);
143 addMouseMotionListener(this);
144 addMouseWheelListener(this);
145 addKeyListener(this);
147 // Make listener for changes to controls
148 ItemListener itemListener = new ItemListener() {
149 public void itemStateChanged(ItemEvent e)
155 // Make special listener for changes to map checkbox
156 ItemListener mapCheckListener = new ItemListener() {
157 public void itemStateChanged(ItemEvent e)
159 _tileManager.clearMemoryCaches();
161 Config.setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
162 UpdateMessageBroker.informSubscribers(); // to let menu know
165 _topPanel = new JPanel();
166 _topPanel.setLayout(new FlowLayout());
167 _topPanel.setOpaque(false);
168 // Make slider for transparency
169 _transparencySlider = new JSlider(0, 5, 0);
170 _transparencySlider.setPreferredSize(new Dimension(100, 20));
171 _transparencySlider.setMajorTickSpacing(1);
172 _transparencySlider.setSnapToTicks(true);
173 _transparencySlider.setOpaque(false);
174 _transparencySlider.addChangeListener(new ChangeListener() {
175 public void stateChanged(ChangeEvent e)
181 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
182 _topPanel.add(_transparencySlider);
183 // Add checkbox button for enabling scale bar
184 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
185 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
186 _scaleCheckBox.setOpaque(false);
187 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
188 _scaleCheckBox.addItemListener(new ItemListener() {
189 public void itemStateChanged(ItemEvent e) {
190 _scaleBar.setVisible(_scaleCheckBox.isSelected());
193 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
194 _topPanel.add(_scaleCheckBox);
195 // Add checkbox button for enabling maps or not
196 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
197 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
198 _mapCheckBox.setOpaque(false);
199 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
200 _mapCheckBox.addItemListener(mapCheckListener);
201 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
202 _topPanel.add(_mapCheckBox);
203 // Add checkbox button for enabling autopan or not
204 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
205 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
206 _autopanCheckBox.setOpaque(false);
207 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
208 _autopanCheckBox.addItemListener(itemListener);
209 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
210 _topPanel.add(_autopanCheckBox);
211 // Add checkbox button for connecting points or not
212 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
213 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
214 _connectCheckBox.setOpaque(false);
215 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
216 _connectCheckBox.addItemListener(itemListener);
217 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
218 _topPanel.add(_connectCheckBox);
220 // Add zoom in, zoom out buttons
221 _sidePanel = new JPanel();
222 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
223 _sidePanel.setOpaque(false);
224 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
225 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
226 zoomInButton.setContentAreaFilled(false);
227 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
228 zoomInButton.addActionListener(new ActionListener() {
229 public void actionPerformed(ActionEvent e)
234 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
235 _sidePanel.add(zoomInButton);
236 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
237 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
238 zoomOutButton.setContentAreaFilled(false);
239 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
240 zoomOutButton.addActionListener(new ActionListener() {
241 public void actionPerformed(ActionEvent e)
246 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
247 _sidePanel.add(zoomOutButton);
249 // Bottom panel for scale bar
250 _scaleBar = new ScaleBar();
252 // add control panels to this one
253 setLayout(new BorderLayout());
254 _topPanel.setVisible(false);
255 _sidePanel.setVisible(false);
256 add(_topPanel, BorderLayout.NORTH);
257 add(_sidePanel, BorderLayout.WEST);
258 add(_scaleBar, BorderLayout.SOUTH);
265 * Make the popup menu for right-clicking the map
267 private void makePopup()
269 _popup = new JPopupMenu();
270 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
271 zoomInItem.addActionListener(new ActionListener() {
272 public void actionPerformed(ActionEvent e)
276 zoomInItem.setEnabled(true);
277 _popup.add(zoomInItem);
278 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
279 zoomOutItem.addActionListener(new ActionListener() {
280 public void actionPerformed(ActionEvent e)
284 zoomOutItem.setEnabled(true);
285 _popup.add(zoomOutItem);
286 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
287 zoomFullItem.addActionListener(new ActionListener() {
288 public void actionPerformed(ActionEvent e)
294 zoomFullItem.setEnabled(true);
295 _popup.add(zoomFullItem);
296 _popup.addSeparator();
298 JMenuItem setMapBgItem = new JMenuItem(
299 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
300 setMapBgItem.addActionListener(new ActionListener() {
301 public void actionPerformed(ActionEvent e)
303 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
305 _popup.add(setMapBgItem);
307 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
308 newPointItem.addActionListener(new ActionListener() {
309 public void actionPerformed(ActionEvent e)
311 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_popupMenuY, getHeight()));
312 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_popupMenuX, getWidth()));
313 _app.createPoint(new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
314 new Longitude(lon, Coordinate.FORMAT_NONE), null));
316 newPointItem.setEnabled(true);
317 _popup.add(newPointItem);
322 * Zoom to fit the current data area
324 private void zoomToFit()
326 _latRange = _track.getLatRange();
327 _lonRange = _track.getLonRange();
328 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
329 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
330 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
331 MapUtils.getYFromLatitude(_latRange.getMaximum()));
332 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
333 getWidth(), getHeight());
339 * @see java.awt.Canvas#paint(java.awt.Graphics)
341 public void paint(Graphics inG)
344 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
347 if (_track.getNumPoints() > 0)
349 // Check for autopan if enabled / necessary
350 if (_autopanCheckBox.isSelected())
352 int selectedPoint = _selection.getCurrentPointIndex();
353 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
355 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
356 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
359 if (px < PAN_DISTANCE) {
360 panX = px - AUTOPAN_DISTANCE;
362 else if (px > (getWidth()-PAN_DISTANCE)) {
363 panX = AUTOPAN_DISTANCE + px - getWidth();
365 if (py < PAN_DISTANCE) {
366 panY = py - AUTOPAN_DISTANCE;
368 if (py > (getHeight()-PAN_DISTANCE)) {
369 panY = AUTOPAN_DISTANCE + py - getHeight();
371 if (panX != 0 || panY != 0) {
372 _mapPosition.pan(panX, panY);
375 _prevSelectedPoint = selectedPoint;
378 // Draw the map contents if necessary
379 if ((_mapImage == null || _recalculate))
382 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
384 // Draw the prepared image onto the panel
385 if (_mapImage != null) {
386 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
388 // Draw the zoom rectangle if necessary
391 inG.setColor(Color.RED);
392 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
393 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
394 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
395 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
400 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
401 inG.fillRect(0, 0, getWidth(), getHeight());
402 inG.setColor(COLOR_MESSAGES);
403 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
404 _scaleBar.updateScale(-1, 0);
406 // Draw slider etc on top
412 * Paint the map tiles and the points on to the _mapImage
414 private void paintMapContents()
416 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
418 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
422 Graphics g = _mapImage.getGraphics();
423 // Clear to background
424 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
425 g.fillRect(0, 0, getWidth(), getHeight());
427 // Check whether maps are on or not
428 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
429 _mapCheckBox.setSelected(showMap);
431 // reset error message
432 if (!showMap) {_shownOsmErrorAlready = false;}
433 _recalculate = false;
434 // Only get map tiles if selected
438 _tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
440 boolean loadingFailed = false;
441 if (_mapImage == null) return;
443 if (_tileManager.isOverzoomed())
445 // display overzoom message
446 g.setColor(COLOR_MESSAGES);
447 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
451 int numLayers = _tileManager.getNumLayers();
452 // Loop over tiles drawing each one
453 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
454 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
455 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
457 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
458 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
460 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
462 for (int l=0; l<numLayers; l++)
464 Image image = _tileManager.getTile(l, tileX, tileY);
466 g.drawImage(image, x, y, 256, 256, null);
472 // Make maps brighter / fainter
473 final float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.2f};
474 final float scaleFactor = scaleFactors[_transparencySlider.getValue()];
475 if (scaleFactor > 1.0f)
477 RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
478 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
479 RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
480 op.filter(_mapImage, _mapImage);
485 // Paint the track points on top
486 int pointsPainted = 1;
489 pointsPainted = paintPoints(g);
491 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
497 // Zoom to fit if no points found
498 if (pointsPainted <= 0 && _checkBounds) {
503 _checkBounds = false;
504 // enable / disable transparency slider
505 _transparencySlider.setEnabled(showMap);
510 * Paint the points using the given graphics object
511 * @param inG Graphics object to use for painting
512 * @return number of points painted, if any
514 private int paintPoints(Graphics inG)
517 final Color pointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
518 final Color rangeColour = Config.getColourScheme().getColour(ColourScheme.IDX_SELECTION);
519 final Color currentColour = Config.getColourScheme().getColour(ColourScheme.IDX_PRIMARY);
520 final Color secondColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
521 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
523 // try to set double line width for painting
524 if (inG instanceof Graphics2D) {
525 ((Graphics2D) inG).setStroke(new BasicStroke(2.0f));
527 int pointsPainted = 0;
529 inG.setColor(pointColour);
530 int prevX = -1, prevY = -1;
531 boolean connectPoints = _connectCheckBox.isSelected();
532 boolean prevPointVisible = false, currPointVisible = false;
533 boolean anyWaypoints = false;
534 boolean isWaypoint = false;
535 for (int i=0; i<_track.getNumPoints(); i++)
537 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
538 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
539 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
540 isWaypoint = _track.getPoint(i).isWaypoint();
541 anyWaypoints = anyWaypoints || isWaypoint;
542 if (currPointVisible)
546 // Draw rectangle for track point
547 if (_track.getPoint(i).getDeleteFlag()) {
548 inG.setColor(currentColour);
551 inG.setColor(pointColour);
553 inG.drawRect(px-2, py-2, 3, 3);
559 // Connect track points if either of them are visible
560 if (connectPoints && (currPointVisible || prevPointVisible)
561 && !(prevX == -1 && prevY == -1)
562 && !_track.getPoint(i).getSegmentStart())
564 inG.drawLine(prevX, prevY, px, py);
566 prevX = px; prevY = py;
568 prevPointVisible = currPointVisible;
571 // Loop over points, just drawing blobs for waypoints
572 inG.setColor(textColour);
573 FontMetrics fm = inG.getFontMetrics();
574 int nameHeight = fm.getHeight();
575 int width = getWidth();
576 int height = getHeight();
578 for (int i=0; i<_track.getNumPoints(); i++)
580 if (_track.getPoint(i).isWaypoint())
582 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
583 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
584 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
586 inG.fillRect(px-3, py-3, 6, 6);
591 // Loop over points again, now draw names for waypoints
592 for (int i=0; i<_track.getNumPoints(); i++)
594 if (_track.getPoint(i).isWaypoint())
596 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
597 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
598 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
600 // Figure out where to draw waypoint name so it doesn't obscure track
601 String waypointName = _track.getPoint(i).getWaypointName();
602 int nameWidth = fm.stringWidth(waypointName);
603 boolean drawnName = false;
604 // Make arrays for coordinates right left up down
605 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
606 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
607 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
609 // Shift arrays for coordinates right left up down
610 nameXs[0] += 2; nameXs[1] -= 2;
611 nameYs[2] -= 2; nameYs[3] += 2;
612 // Check each direction in turn right left up down
613 for (int a=0; a<4; a++)
615 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
616 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
617 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
619 // Found a rectangle to fit - draw name here and quit
620 inG.drawString(waypointName, nameXs[a], nameYs[a]);
630 // Loop over points, drawing blobs for photo points
631 inG.setColor(secondColour);
632 for (int i=0; i<_track.getNumPoints(); i++)
634 if (_track.getPoint(i).getPhoto() != null)
636 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
637 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
638 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
640 inG.drawRect(px-1, py-1, 2, 2);
641 inG.drawRect(px-2, py-2, 4, 4);
647 // Draw selected range
648 if (_selection.hasRangeSelected())
650 inG.setColor(rangeColour);
651 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
653 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
654 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
655 inG.drawRect(px-1, py-1, 2, 2);
659 // Draw selected point, crosshairs
660 int selectedPoint = _selection.getCurrentPointIndex();
661 if (selectedPoint >= 0)
663 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
664 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
665 inG.setColor(currentColour);
667 inG.drawLine(px, 0, px, getHeight());
668 inG.drawLine(0, py, getWidth(), py);
670 inG.drawOval(px - 2, py - 2, 4, 4);
671 inG.drawOval(px - 3, py - 3, 6, 6);
673 // Return the number of points painted
674 return pointsPainted;
679 * Tests whether there are any dark pixels within the specified x,y rectangle
680 * @param inX left X coordinate
681 * @param inY bottom Y coordinate
682 * @param inWidth width of rectangle
683 * @param inHeight height of rectangle
684 * @param inTextColour colour of text
685 * @return true if the rectangle overlaps stuff too close to the given colour
687 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
689 // each of the colour channels must be further away than this to count as empty
690 final int BRIGHTNESS_LIMIT = 80;
691 final int textRGB = inTextColour.getRGB();
692 final int textLow = textRGB & 255;
693 final int textMid = (textRGB >> 8) & 255;
694 final int textHigh = (textRGB >> 16) & 255;
697 // loop over x coordinate of rectangle
698 for (int x=0; x<inWidth; x++)
700 // loop over y coordinate of rectangle
701 for (int y=0; y<inHeight; y++)
703 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
704 // split into four components rgba
705 int pixLow = pixelColor & 255;
706 int pixMid = (pixelColor >> 8) & 255;
707 int pixHigh = (pixelColor >> 16) & 255;
708 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
709 // If colours are too close in any channel then it's an overlap
710 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
711 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
712 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
716 catch (NullPointerException e) {
717 // ignore null pointers, just return false
724 * Inform that tiles have been updated and the map can be repainted
725 * @param inIsOk true if data loaded ok, false for error
727 public synchronized void tilesUpdated(boolean inIsOk)
729 // Show message if loading failed (but not too many times)
730 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
732 _shownOsmErrorAlready = true;
733 // use separate thread to show message about failing to load osm images
734 new Thread(new Runnable() {
736 try {Thread.sleep(500);} catch (InterruptedException ie) {}
737 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
746 * Zoom out, if not already at minimum zoom
748 public void zoomOut()
750 _mapPosition.zoomOut();
756 * Zoom in, if not already at maximum zoom
760 _mapPosition.zoomIn();
767 * @param inDeltaX x shift
768 * @param inDeltaY y shift
770 public void panMap(int inDeltaX, int inDeltaY)
772 _mapPosition.pan(inDeltaX, inDeltaY);
778 * @see javax.swing.JComponent#getMinimumSize()
780 public Dimension getMinimumSize()
782 final Dimension minSize = new Dimension(512, 300);
787 * @see javax.swing.JComponent#getPreferredSize()
789 public Dimension getPreferredSize()
791 return getMinimumSize();
796 * Respond to mouse click events
797 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
799 public void mouseClicked(MouseEvent inE)
801 if (_track != null && _track.getNumPoints() > 0)
803 // select point if it's a left-click
804 if (!inE.isMetaDown())
806 if (inE.getClickCount() == 1)
809 int pointIndex = _track.getNearestPointIndex(
810 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
811 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
812 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
813 // Extend selection for shift-click
814 if (inE.isShiftDown()) {
815 _trackInfo.extendSelection(pointIndex);
818 _trackInfo.selectPoint(pointIndex);
821 else if (inE.getClickCount() == 2) {
823 panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
829 // show the popup menu for right-clicks
830 _popupMenuX = inE.getX();
831 _popupMenuY = inE.getY();
832 _popup.show(this, _popupMenuX, _popupMenuY);
838 * Ignore mouse enter events
839 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
841 public void mouseEntered(MouseEvent inE)
847 * Ignore mouse exited events
848 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
850 public void mouseExited(MouseEvent inE)
856 * Ignore mouse pressed events
857 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
859 public void mousePressed(MouseEvent inE)
865 * Respond to mouse released events
866 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
868 public void mouseReleased(MouseEvent inE)
871 if (_zoomDragging && Math.abs(_dragToX - _dragFromX) > 20 && Math.abs(_dragToY - _dragFromY) > 20)
873 //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
874 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
876 _dragFromX = _dragFromY = -1;
877 _zoomDragging = false;
882 * Respond to mouse drag events
883 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
885 public void mouseDragged(MouseEvent inE)
887 if (!inE.isMetaDown())
889 // Left mouse drag - pan map by appropriate amount
890 _zoomDragging = false;
891 if (_dragFromX != -1)
893 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
897 _dragFromX = inE.getX();
898 _dragFromY = inE.getY();
902 // Right-click and drag - draw rectangle and control zoom
903 _zoomDragging = true;
904 if (_dragFromX == -1) {
905 _dragFromX = inE.getX();
906 _dragFromY = inE.getY();
908 _dragToX = inE.getX();
909 _dragToY = inE.getY();
915 * Respond to mouse move events without button pressed
916 * @param inEvent ignored
918 public void mouseMoved(MouseEvent inEvent)
924 * Respond to status bar message from broker
925 * @param inMessage message, ignored
927 public void actionCompleted(String inMessage)
933 * Respond to data updated message from broker
934 * @param inUpdateType type of update
936 public void dataUpdated(byte inUpdateType)
939 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
942 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
943 _tileManager.resetConfig();
946 // enable or disable components
947 boolean hasData = _track.getNumPoints() > 0;
948 _topPanel.setVisible(hasData);
949 _sidePanel.setVisible(hasData);
950 // grab focus for the key presses
955 * Respond to key presses on the map canvas
956 * @param inE key event
958 public void keyPressed(KeyEvent inE)
960 int code = inE.getKeyCode();
961 int currPointIndex = _selection.getCurrentPointIndex();
962 // Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
963 if (inE.isControlDown() || inE.isMetaDown())
965 // Check for arrow keys to zoom in and out
966 if (code == KeyEvent.VK_UP)
968 else if (code == KeyEvent.VK_DOWN)
970 // Key nav for next/prev point
971 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
972 _trackInfo.selectPoint(currPointIndex-1);
973 else if (code == KeyEvent.VK_RIGHT)
974 _trackInfo.selectPoint(currPointIndex+1);
975 else if (code == KeyEvent.VK_PAGE_UP)
976 _trackInfo.selectPoint(Checker.getPreviousSegmentStart(
977 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
978 else if (code == KeyEvent.VK_PAGE_DOWN)
979 _trackInfo.selectPoint(Checker.getNextSegmentStart(
980 _trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
981 // Check for home and end
982 else if (code == KeyEvent.VK_HOME)
983 _trackInfo.selectPoint(0);
984 else if (code == KeyEvent.VK_END)
985 _trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
989 // Check for arrow keys to pan
991 if (code == KeyEvent.VK_UP)
992 upwardsPan = -PAN_DISTANCE;
993 else if (code == KeyEvent.VK_DOWN)
994 upwardsPan = PAN_DISTANCE;
995 int rightwardsPan = 0;
996 if (code == KeyEvent.VK_RIGHT)
997 rightwardsPan = PAN_DISTANCE;
998 else if (code == KeyEvent.VK_LEFT)
999 rightwardsPan = -PAN_DISTANCE;
1000 panMap(rightwardsPan, upwardsPan);
1001 // Check for backspace key to delete current point (delete key already handled by menu)
1002 if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
1003 _app.deleteCurrentPoint();
1009 * @param inE key released event, ignored
1011 public void keyReleased(KeyEvent e)
1017 * @param inE key typed event, ignored
1019 public void keyTyped(KeyEvent inE)
1025 * @param inE mouse wheel event indicating scroll direction
1027 public void mouseWheelMoved(MouseWheelEvent inE)
1029 int clicks = inE.getWheelRotation();
1032 else if (clicks > 0)
1037 * @return current map position
1039 public MapPosition getMapPosition()
1041 return _mapPosition;