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.Coordinate;
44 import tim.prune.data.DataPoint;
45 import tim.prune.data.DoubleRange;
46 import tim.prune.data.Latitude;
47 import tim.prune.data.Longitude;
48 import tim.prune.data.Selection;
49 import tim.prune.data.Track;
50 import tim.prune.data.TrackInfo;
51 import tim.prune.gui.IconManager;
54 * Class for the map canvas, to display a background map and draw on it
56 public class MapCanvas extends JPanel implements MouseListener, MouseMotionListener, DataSubscriber,
57 KeyListener, MouseWheelListener
59 /** App object for callbacks */
60 private App _app = null;
62 private Track _track = null;
63 /** TrackInfo object */
64 private TrackInfo _trackInfo = null;
65 /** Selection object */
66 private Selection _selection = null;
67 /** Previously selected point */
68 private int _prevSelectedPoint = -1;
70 private MapTileCacher _tileCacher = new MapTileCacher(this);
71 /** Image to display */
72 private BufferedImage _mapImage = null;
73 /** Slider for transparency */
74 private JSlider _transparencySlider = null;
75 /** Checkbox for scale bar */
76 private JCheckBox _scaleCheckBox = null;
77 /** Checkbox for maps */
78 private JCheckBox _mapCheckBox = null;
79 /** Checkbox for autopan */
80 private JCheckBox _autopanCheckBox = null;
81 /** Checkbox for connecting track points */
82 private JCheckBox _connectCheckBox = null;
83 /** Right-click popup menu */
84 private JPopupMenu _popup = null;
85 /** Top component panel */
86 private JPanel _topPanel = null;
87 /** Side component panel */
88 private JPanel _sidePanel = null;
90 private ScaleBar _scaleBar = null;
92 private DoubleRange _latRange = null, _lonRange = null;
93 private DoubleRange _xRange = null, _yRange = null;
94 private boolean _recalculate = false;
95 /** Flag to check bounds on next paint */
96 private boolean _checkBounds = false;
98 private MapPosition _mapPosition = null;
99 /** x coordinate of drag from point */
100 private int _dragFromX = -1;
101 /** y coordinate of drag from point */
102 private int _dragFromY = -1;
103 /** Flag set to true for right-click dragging */
104 private boolean _zoomDragging = false;
105 /** x coordinate of drag to point */
106 private int _dragToX = -1;
107 /** y coordinate of drag to point */
108 private int _dragToY = -1;
109 /** x coordinate of popup menu */
110 private int _popupMenuX = -1;
111 /** y coordinate of popup menu */
112 private int _popupMenuY = -1;
113 /** Flag to prevent showing too often the error message about loading maps */
114 private boolean _shownOsmErrorAlready = false;
116 /** Constant for click sensitivity when selecting nearest point */
117 private static final int CLICK_SENSITIVITY = 10;
118 /** Constant for pan distance from key presses */
119 private static final int PAN_DISTANCE = 20;
120 /** Constant for pan distance from autopan */
121 private static final int AUTOPAN_DISTANCE = 75;
124 private static final Color COLOR_MESSAGES = Color.GRAY;
129 * @param inApp App object for callbacks
130 * @param inTrackInfo track info object
132 public MapCanvas(App inApp, TrackInfo inTrackInfo)
135 _trackInfo = inTrackInfo;
136 _track = inTrackInfo.getTrack();
137 _selection = inTrackInfo.getSelection();
138 _mapPosition = new MapPosition();
139 addMouseListener(this);
140 addMouseMotionListener(this);
141 addMouseWheelListener(this);
142 addKeyListener(this);
144 // Make listener for changes to controls
145 ItemListener itemListener = new ItemListener() {
146 public void itemStateChanged(ItemEvent e)
152 // Make special listener for changes to map checkbox
153 ItemListener mapCheckListener = new ItemListener() {
154 public void itemStateChanged(ItemEvent e)
156 _tileCacher.clearAll();
158 Config.setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
159 UpdateMessageBroker.informSubscribers(); // to let menu know
162 _topPanel = new JPanel();
163 _topPanel.setLayout(new FlowLayout());
164 _topPanel.setOpaque(false);
165 // Make slider for transparency
166 _transparencySlider = new JSlider(0, 5, 0);
167 _transparencySlider.setPreferredSize(new Dimension(100, 20));
168 _transparencySlider.setMajorTickSpacing(1);
169 _transparencySlider.setSnapToTicks(true);
170 _transparencySlider.setOpaque(false);
171 _transparencySlider.addChangeListener(new ChangeListener() {
172 public void stateChanged(ChangeEvent e)
178 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
179 _topPanel.add(_transparencySlider);
180 // Add checkbox button for enabling scale bar
181 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
182 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
183 _scaleCheckBox.setOpaque(false);
184 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
185 _scaleCheckBox.addItemListener(new ItemListener() {
186 public void itemStateChanged(ItemEvent e) {
187 _scaleBar.setVisible(_scaleCheckBox.isSelected());
190 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
191 _topPanel.add(_scaleCheckBox);
192 // Add checkbox button for enabling maps or not
193 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
194 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
195 _mapCheckBox.setOpaque(false);
196 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
197 _mapCheckBox.addItemListener(mapCheckListener);
198 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
199 _topPanel.add(_mapCheckBox);
200 // Add checkbox button for enabling autopan or not
201 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
202 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
203 _autopanCheckBox.setOpaque(false);
204 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
205 _autopanCheckBox.addItemListener(itemListener);
206 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
207 _topPanel.add(_autopanCheckBox);
208 // Add checkbox button for connecting points or not
209 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
210 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
211 _connectCheckBox.setOpaque(false);
212 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
213 _connectCheckBox.addItemListener(itemListener);
214 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
215 _topPanel.add(_connectCheckBox);
217 // Add zoom in, zoom out buttons
218 _sidePanel = new JPanel();
219 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
220 _sidePanel.setOpaque(false);
221 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
222 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
223 zoomInButton.setContentAreaFilled(false);
224 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
225 zoomInButton.addActionListener(new ActionListener() {
226 public void actionPerformed(ActionEvent e)
231 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
232 _sidePanel.add(zoomInButton);
233 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
234 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
235 zoomOutButton.setContentAreaFilled(false);
236 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
237 zoomOutButton.addActionListener(new ActionListener() {
238 public void actionPerformed(ActionEvent e)
243 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
244 _sidePanel.add(zoomOutButton);
246 // Bottom panel for scale bar
247 _scaleBar = new ScaleBar();
249 // add control panels to this one
250 setLayout(new BorderLayout());
251 _topPanel.setVisible(false);
252 _sidePanel.setVisible(false);
253 add(_topPanel, BorderLayout.NORTH);
254 add(_sidePanel, BorderLayout.WEST);
255 add(_scaleBar, BorderLayout.SOUTH);
262 * Make the popup menu for right-clicking the map
264 private void makePopup()
266 _popup = new JPopupMenu();
267 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
268 zoomInItem.addActionListener(new ActionListener() {
269 public void actionPerformed(ActionEvent e)
273 zoomInItem.setEnabled(true);
274 _popup.add(zoomInItem);
275 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
276 zoomOutItem.addActionListener(new ActionListener() {
277 public void actionPerformed(ActionEvent e)
281 zoomOutItem.setEnabled(true);
282 _popup.add(zoomOutItem);
283 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
284 zoomFullItem.addActionListener(new ActionListener() {
285 public void actionPerformed(ActionEvent e)
291 zoomFullItem.setEnabled(true);
292 _popup.add(zoomFullItem);
293 _popup.addSeparator();
295 JMenuItem setMapBgItem = new JMenuItem(
296 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
297 setMapBgItem.addActionListener(new ActionListener() {
298 public void actionPerformed(ActionEvent e)
300 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
302 _popup.add(setMapBgItem);
304 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
305 newPointItem.addActionListener(new ActionListener() {
306 public void actionPerformed(ActionEvent e)
308 double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_popupMenuY, getHeight()));
309 double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_popupMenuX, getWidth()));
310 _app.createPoint(new DataPoint(new Latitude(lat, Coordinate.FORMAT_NONE),
311 new Longitude(lon, Coordinate.FORMAT_NONE), null));
313 newPointItem.setEnabled(true);
314 _popup.add(newPointItem);
319 * Zoom to fit the current data area
321 private void zoomToFit()
323 _latRange = _track.getLatRange();
324 _lonRange = _track.getLonRange();
325 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
326 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
327 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
328 MapUtils.getYFromLatitude(_latRange.getMaximum()));
329 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
330 getWidth(), getHeight());
336 * @see java.awt.Canvas#paint(java.awt.Graphics)
338 public void paint(Graphics inG)
341 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
344 if (_track.getNumPoints() > 0)
346 // Check for autopan if enabled / necessary
347 if (_autopanCheckBox.isSelected())
349 int selectedPoint = _selection.getCurrentPointIndex();
350 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
352 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
353 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
356 if (px < PAN_DISTANCE) {
357 panX = px - AUTOPAN_DISTANCE;
359 else if (px > (getWidth()-PAN_DISTANCE)) {
360 panX = AUTOPAN_DISTANCE + px - getWidth();
362 if (py < PAN_DISTANCE) {
363 panY = py - AUTOPAN_DISTANCE;
365 if (py > (getHeight()-PAN_DISTANCE)) {
366 panY = AUTOPAN_DISTANCE + py - getHeight();
368 if (panX != 0 || panY != 0) {
369 _mapPosition.pan(panX, panY);
372 _prevSelectedPoint = selectedPoint;
375 // Draw the mapImage if necessary
376 if ((_mapImage == null || _recalculate))
379 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getCentreTileY());
381 // Draw the prepared image onto the panel
382 if (_mapImage != null) {
383 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
385 // Draw the zoom rectangle if necessary
388 inG.setColor(Color.RED);
389 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
390 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
391 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
392 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
397 inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
398 inG.fillRect(0, 0, getWidth(), getHeight());
399 inG.setColor(COLOR_MESSAGES);
400 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
401 _scaleBar.updateScale(-1, 0);
403 // Draw slider etc on top
409 * Get the map tiles for the current zoom level and given tile parameters
411 private void getMapTiles()
413 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
415 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
419 Graphics g = _mapImage.getGraphics();
420 // Clear to background
421 g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
422 g.fillRect(0, 0, getWidth(), getHeight());
424 // Check whether maps are on or not
425 boolean showMap = Config.getConfigBoolean(Config.KEY_SHOW_MAP);
426 _mapCheckBox.setSelected(showMap);
428 // reset error message
429 if (!showMap) {_shownOsmErrorAlready = false;}
430 // Only get map tiles if selected
434 _tileCacher.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
436 boolean loadingFailed = false;
437 if (_mapImage == null) return;
439 if (_tileCacher.isOverzoomed())
441 // display overzoom message
442 g.setColor(COLOR_MESSAGES);
443 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
447 // Loop over tiles drawing each one
448 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
449 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
450 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
452 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
453 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
455 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
456 Image image = _tileCacher.getTile(tileX, tileY);
458 g.drawImage(image, x, y, 256, 256, null);
463 // Make maps brighter / fainter
464 float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.0f};
465 float scaleFactor = scaleFactors[_transparencySlider.getValue()];
466 if (scaleFactor > 1.0f)
468 RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
469 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
470 RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
471 op.filter(_mapImage, _mapImage);
476 // Paint the track points on top
477 int pointsPainted = 1;
480 pointsPainted = paintPoints(g);
482 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
488 _recalculate = false;
489 // Zoom to fit if no points found
490 if (pointsPainted <= 0 && _checkBounds) {
495 _checkBounds = false;
496 // enable / disable transparency slider
497 _transparencySlider.setEnabled(showMap);
502 * Paint the points using the given graphics object
503 * @param inG Graphics object to use for painting
504 * @return number of points painted, if any
506 private int paintPoints(Graphics inG)
509 final Color pointColour = Config.getColourScheme().getColour(ColourScheme.IDX_POINT);
510 final Color rangeColour = Config.getColourScheme().getColour(ColourScheme.IDX_SELECTION);
511 final Color currentColour = Config.getColourScheme().getColour(ColourScheme.IDX_PRIMARY);
512 final Color secondColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
513 final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
515 int pointsPainted = 0;
517 inG.setColor(pointColour);
518 int prevX = -1, prevY = -1;
519 boolean connectPoints = _connectCheckBox.isSelected();
520 boolean prevPointVisible = false, currPointVisible = false;
521 boolean anyWaypoints = false;
522 boolean isWaypoint = false;
523 for (int i=0; i<_track.getNumPoints(); i++)
525 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
526 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
527 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
528 isWaypoint = _track.getPoint(i).isWaypoint();
529 anyWaypoints = anyWaypoints || isWaypoint;
530 if (currPointVisible)
534 // Draw rectangle for track point
535 if (_track.getPoint(i).getDeleteFlag()) {
536 inG.setColor(currentColour);
539 inG.setColor(pointColour);
541 inG.drawRect(px-2, py-2, 3, 3);
547 // Connect track points if either of them are visible
548 if (connectPoints && (currPointVisible || prevPointVisible)
549 && !(prevX == -1 && prevY == -1)
550 && !_track.getPoint(i).getSegmentStart())
552 inG.drawLine(prevX, prevY, px, py);
554 prevX = px; prevY = py;
556 prevPointVisible = currPointVisible;
559 // Loop over points, just drawing blobs for waypoints
560 inG.setColor(textColour);
561 FontMetrics fm = inG.getFontMetrics();
562 int nameHeight = fm.getHeight();
563 int width = getWidth();
564 int height = getHeight();
566 for (int i=0; i<_track.getNumPoints(); i++)
568 if (_track.getPoint(i).isWaypoint())
570 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
571 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
572 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
574 inG.fillRect(px-3, py-3, 6, 6);
579 // Loop over points again, now draw names for waypoints
580 for (int i=0; i<_track.getNumPoints(); i++)
582 if (_track.getPoint(i).isWaypoint())
584 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
585 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
586 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
588 // Figure out where to draw waypoint name so it doesn't obscure track
589 String waypointName = _track.getPoint(i).getWaypointName();
590 int nameWidth = fm.stringWidth(waypointName);
591 boolean drawnName = false;
592 // Make arrays for coordinates right left up down
593 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
594 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
595 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
597 // Shift arrays for coordinates right left up down
598 nameXs[0] += 2; nameXs[1] -= 2;
599 nameYs[2] -= 2; nameYs[3] += 2;
600 // Check each direction in turn right left up down
601 for (int a=0; a<4; a++)
603 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
604 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
605 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
607 // Found a rectangle to fit - draw name here and quit
608 inG.drawString(waypointName, nameXs[a], nameYs[a]);
618 // Loop over points, drawing blobs for photo points
619 inG.setColor(secondColour);
620 for (int i=0; i<_track.getNumPoints(); i++)
622 if (_track.getPoint(i).getPhoto() != null)
624 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
625 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
626 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
628 inG.drawRect(px-1, py-1, 2, 2);
629 inG.drawRect(px-2, py-2, 4, 4);
635 // Draw selected range
636 if (_selection.hasRangeSelected())
638 inG.setColor(rangeColour);
639 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
641 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
642 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
643 inG.drawRect(px-1, py-1, 2, 2);
647 // Draw selected point, crosshairs
648 int selectedPoint = _selection.getCurrentPointIndex();
649 if (selectedPoint >= 0)
651 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
652 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
653 inG.setColor(currentColour);
655 inG.drawLine(px, 0, px, getHeight());
656 inG.drawLine(0, py, getWidth(), py);
658 inG.drawOval(px - 2, py - 2, 4, 4);
659 inG.drawOval(px - 3, py - 3, 6, 6);
661 // Return the number of points painted
662 return pointsPainted;
667 * Tests whether there are any dark pixels within the specified x,y rectangle
668 * @param inX left X coordinate
669 * @param inY bottom Y coordinate
670 * @param inWidth width of rectangle
671 * @param inHeight height of rectangle
672 * @param inTextColour colour of text
673 * @return true if the rectangle overlaps stuff too close to the given colour
675 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour)
677 // each of the colour channels must be further away than this to count as empty
678 final int BRIGHTNESS_LIMIT = 80;
679 final int textRGB = inTextColour.getRGB();
680 final int textLow = textRGB & 255;
681 final int textMid = (textRGB >> 8) & 255;
682 final int textHigh = (textRGB >> 16) & 255;
685 // loop over x coordinate of rectangle
686 for (int x=0; x<inWidth; x++)
688 // loop over y coordinate of rectangle
689 for (int y=0; y<inHeight; y++)
691 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
692 // split into four components rgba
693 int pixLow = pixelColor & 255;
694 int pixMid = (pixelColor >> 8) & 255;
695 int pixHigh = (pixelColor >> 16) & 255;
696 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
697 // If colours are too close in any channel then it's an overlap
698 if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT ||
699 Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT ||
700 Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;}
704 catch (NullPointerException e) {
705 // ignore null pointers, just return false
712 * Inform that tiles have been updated and the map can be repainted
713 * @param inIsOk true if data loaded ok, false for error
715 public synchronized void tilesUpdated(boolean inIsOk)
717 // Show message if loading failed (but not too many times)
718 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
720 _shownOsmErrorAlready = true;
721 // use separate thread to show message about failing to load osm images
722 new Thread(new Runnable() {
724 try {Thread.sleep(500);} catch (InterruptedException ie) {}
725 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
734 * Zoom out, if not already at minimum zoom
736 public void zoomOut()
738 _mapPosition.zoomOut();
744 * Zoom in, if not already at maximum zoom
748 _mapPosition.zoomIn();
755 * @param inDeltaX x shift
756 * @param inDeltaY y shift
758 public void panMap(int inDeltaX, int inDeltaY)
760 _mapPosition.pan(inDeltaX, inDeltaY);
766 * @see javax.swing.JComponent#getMinimumSize()
768 public Dimension getMinimumSize()
770 final Dimension minSize = new Dimension(512, 300);
775 * @see javax.swing.JComponent#getPreferredSize()
777 public Dimension getPreferredSize()
779 return getMinimumSize();
784 * Respond to mouse click events
785 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
787 public void mouseClicked(MouseEvent inE)
789 if (_track != null && _track.getNumPoints() > 0)
791 // select point if it's a left-click
792 if (!inE.isMetaDown())
794 int pointIndex = _track.getNearestPointIndex(
795 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
796 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
797 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
798 // Extend selection for shift-click
799 if (inE.isShiftDown()) {
800 _trackInfo.extendSelection(pointIndex);
803 _trackInfo.selectPoint(pointIndex);
808 // show the popup menu for right-clicks
809 _popupMenuX = inE.getX();
810 _popupMenuY = inE.getY();
811 _popup.show(this, _popupMenuX, _popupMenuY);
817 * Ignore mouse enter events
818 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
820 public void mouseEntered(MouseEvent inE)
826 * Ignore mouse exited events
827 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
829 public void mouseExited(MouseEvent inE)
835 * Ignore mouse pressed events
836 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
838 public void mousePressed(MouseEvent inE)
844 * Respond to mouse released events
845 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
847 public void mouseReleased(MouseEvent inE)
850 if (_zoomDragging && Math.abs(_dragToX - _dragFromX) > 20 && Math.abs(_dragToY - _dragFromY) > 20)
852 //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
853 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
855 _dragFromX = _dragFromY = -1;
856 _zoomDragging = false;
861 * Respond to mouse drag events
862 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
864 public void mouseDragged(MouseEvent inE)
866 if (!inE.isMetaDown())
868 // Left mouse drag - pan map by appropriate amount
869 _zoomDragging = false;
870 if (_dragFromX != -1)
872 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
876 _dragFromX = inE.getX();
877 _dragFromY = inE.getY();
881 // Right-click and drag - draw rectangle and control zoom
882 _zoomDragging = true;
883 if (_dragFromX == -1) {
884 _dragFromX = inE.getX();
885 _dragFromY = inE.getY();
887 _dragToX = inE.getX();
888 _dragToY = inE.getY();
894 * Respond to mouse move events without button pressed
895 * @param inEvent ignored
897 public void mouseMoved(MouseEvent inEvent)
903 * Respond to status bar message from broker
904 * @param inMessage message, ignored
906 public void actionCompleted(String inMessage)
912 * Respond to data updated message from broker
913 * @param inUpdateType type of update
915 public void dataUpdated(byte inUpdateType)
918 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
921 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
922 _tileCacher.setTileConfig(new MapTileConfig());
925 // enable or disable components
926 boolean hasData = _track.getNumPoints() > 0;
927 _topPanel.setVisible(hasData);
928 _sidePanel.setVisible(hasData);
929 // grab focus for the key presses
934 * Respond to key presses on the map canvas
935 * @param inE key event
937 public void keyPressed(KeyEvent inE)
939 int code = inE.getKeyCode();
940 int currPointIndex = _selection.getCurrentPointIndex();
941 // Check for meta key
942 if (inE.isControlDown())
944 // Check for arrow keys to zoom in and out
945 if (code == KeyEvent.VK_UP)
947 else if (code == KeyEvent.VK_DOWN)
949 // Key nav for next/prev point
950 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
951 _trackInfo.selectPoint(currPointIndex-1);
952 else if (code == KeyEvent.VK_RIGHT)
953 _trackInfo.selectPoint(currPointIndex+1);
957 // Check for arrow keys to pan
959 if (code == KeyEvent.VK_UP)
960 upwardsPan = -PAN_DISTANCE;
961 else if (code == KeyEvent.VK_DOWN)
962 upwardsPan = PAN_DISTANCE;
963 int rightwardsPan = 0;
964 if (code == KeyEvent.VK_RIGHT)
965 rightwardsPan = PAN_DISTANCE;
966 else if (code == KeyEvent.VK_LEFT)
967 rightwardsPan = -PAN_DISTANCE;
968 panMap(rightwardsPan, upwardsPan);
969 // Check for delete key to delete current point
970 if (code == KeyEvent.VK_DELETE && currPointIndex >= 0)
972 _app.deleteCurrentPoint();
978 * @param inE key released event, ignored
980 public void keyReleased(KeyEvent e)
986 * @param inE key typed event, ignored
988 public void keyTyped(KeyEvent inE)
994 * @param inE mouse wheel event indicating scroll direction
996 public void mouseWheelMoved(MouseWheelEvent inE)
998 int clicks = inE.getWheelRotation();
1001 else if (clicks > 0)
1006 * @return current map position
1008 public MapPosition getMapPosition()
1010 return _mapPosition;