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.data.DoubleRange;
41 import tim.prune.data.Selection;
42 import tim.prune.data.Track;
43 import tim.prune.data.TrackInfo;
44 import tim.prune.gui.IconManager;
47 * Class for the map canvas, to display a background map and draw on it
49 public class MapCanvas extends JPanel implements MouseListener, MouseMotionListener, DataSubscriber,
50 KeyListener, MouseWheelListener
52 /** App object for callbacks */
53 private App _app = null;
55 private Track _track = null;
56 /** TrackInfo object */
57 private TrackInfo _trackInfo = null;
58 /** Selection object */
59 private Selection _selection = null;
60 /** Previously selected point */
61 private int _prevSelectedPoint = -1;
63 private MapTileCacher _tileCacher = new MapTileCacher(this);
64 /** Image to display */
65 private BufferedImage _mapImage = null;
66 /** Slider for transparency */
67 private JSlider _transparencySlider = null;
68 /** Checkbox for scale bar */
69 private JCheckBox _scaleCheckBox = null;
70 /** Checkbox for maps */
71 private JCheckBox _mapCheckBox = null;
72 /** Checkbox for autopan */
73 private JCheckBox _autopanCheckBox = null;
74 /** Checkbox for connecting track points */
75 private JCheckBox _connectCheckBox = null;
76 /** Right-click popup menu */
77 private JPopupMenu _popup = null;
78 /** Top component panel */
79 private JPanel _topPanel = null;
80 /** Side component panel */
81 private JPanel _sidePanel = null;
83 private ScaleBar _scaleBar = null;
85 private DoubleRange _latRange = null, _lonRange = null;
86 private DoubleRange _xRange = null, _yRange = null;
87 private boolean _recalculate = false;
88 /** Flag to check bounds on next paint */
89 private boolean _checkBounds = false;
91 private MapPosition _mapPosition = null;
92 /** x coordinate of drag from point */
93 private int _dragFromX = -1;
94 /** y coordinate of drag from point */
95 private int _dragFromY = -1;
96 /** Flag set to true for right-click dragging */
97 private boolean _zoomDragging = false;
98 /** x coordinate of drag to point */
99 private int _dragToX = -1;
100 /** y coordinate of drag to point */
101 private int _dragToY = -1;
102 /** x coordinate of popup menu */
103 private int _popupMenuX = -1;
104 /** y coordinate of popup menu */
105 private int _popupMenuY = -1;
106 /** Flag to prevent showing too often the error message about loading maps */
107 private boolean _shownOsmErrorAlready = false;
109 /** Constant for click sensitivity when selecting nearest point */
110 private static final int CLICK_SENSITIVITY = 10;
111 /** Constant for pan distance from key presses */
112 private static final int PAN_DISTANCE = 20;
113 /** Constant for pan distance from autopan */
114 private static final int AUTOPAN_DISTANCE = 75;
117 private static final Color COLOR_BG = Color.WHITE;
118 private static final Color COLOR_MESSAGES = Color.GRAY;
119 private static final Color COLOR_POINT = Color.BLUE;
120 private static final Color COLOR_POINT_DELETED = Color.RED;
121 private static final Color COLOR_CURR_RANGE = Color.GREEN;
122 private static final Color COLOR_CROSSHAIRS = Color.RED;
123 private static final Color COLOR_WAYPT_NAME = Color.BLACK;
124 private static final Color COLOR_PHOTO_PT = Color.ORANGE;
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();
161 _topPanel = new JPanel();
162 _topPanel.setLayout(new FlowLayout());
163 _topPanel.setOpaque(false);
164 // Make slider for transparency
165 _transparencySlider = new JSlider(0, 5, 0);
166 _transparencySlider.setPreferredSize(new Dimension(100, 20));
167 _transparencySlider.setMajorTickSpacing(1);
168 _transparencySlider.setSnapToTicks(true);
169 _transparencySlider.setOpaque(false);
170 _transparencySlider.addChangeListener(new ChangeListener() {
171 public void stateChanged(ChangeEvent e)
177 _transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
178 _topPanel.add(_transparencySlider);
179 // Add checkbox button for enabling scale bar
180 _scaleCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
181 _scaleCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
182 _scaleCheckBox.setOpaque(false);
183 _scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
184 _scaleCheckBox.addItemListener(new ItemListener() {
185 public void itemStateChanged(ItemEvent e) {
186 _scaleBar.setVisible(_scaleCheckBox.isSelected());
189 _scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
190 _topPanel.add(_scaleCheckBox);
191 // Add checkbox button for enabling maps or not
192 _mapCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.MAP_BUTTON), false);
193 _mapCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
194 _mapCheckBox.setOpaque(false);
195 _mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
196 _mapCheckBox.addItemListener(mapCheckListener);
197 _mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
198 _topPanel.add(_mapCheckBox);
199 // Add checkbox button for enabling autopan or not
200 _autopanCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
201 _autopanCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
202 _autopanCheckBox.setOpaque(false);
203 _autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
204 _autopanCheckBox.addItemListener(itemListener);
205 _autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
206 _topPanel.add(_autopanCheckBox);
207 // Add checkbox button for connecting points or not
208 _connectCheckBox = new JCheckBox(IconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON), true);
209 _connectCheckBox.setSelectedIcon(IconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
210 _connectCheckBox.setOpaque(false);
211 _connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
212 _connectCheckBox.addItemListener(itemListener);
213 _connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
214 _topPanel.add(_connectCheckBox);
216 // Add zoom in, zoom out buttons
217 _sidePanel = new JPanel();
218 _sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
219 _sidePanel.setOpaque(false);
220 JButton zoomInButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
221 zoomInButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
222 zoomInButton.setContentAreaFilled(false);
223 zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
224 zoomInButton.addActionListener(new ActionListener() {
225 public void actionPerformed(ActionEvent e)
230 zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
231 _sidePanel.add(zoomInButton);
232 JButton zoomOutButton = new JButton(IconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
233 zoomOutButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
234 zoomOutButton.setContentAreaFilled(false);
235 zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
236 zoomOutButton.addActionListener(new ActionListener() {
237 public void actionPerformed(ActionEvent e)
242 zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
243 _sidePanel.add(zoomOutButton);
245 // Bottom panel for scale bar
246 _scaleBar = new ScaleBar();
248 // add control panels to this one
249 setLayout(new BorderLayout());
250 _topPanel.setVisible(false);
251 _sidePanel.setVisible(false);
252 add(_topPanel, BorderLayout.NORTH);
253 add(_sidePanel, BorderLayout.WEST);
254 add(_scaleBar, BorderLayout.SOUTH);
261 * Make the popup menu for right-clicking the map
263 private void makePopup()
265 _popup = new JPopupMenu();
266 JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
267 zoomInItem.addActionListener(new ActionListener() {
268 public void actionPerformed(ActionEvent e)
272 zoomInItem.setEnabled(true);
273 _popup.add(zoomInItem);
274 JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
275 zoomOutItem.addActionListener(new ActionListener() {
276 public void actionPerformed(ActionEvent e)
280 zoomOutItem.setEnabled(true);
281 _popup.add(zoomOutItem);
282 JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
283 zoomFullItem.addActionListener(new ActionListener() {
284 public void actionPerformed(ActionEvent e)
290 zoomFullItem.setEnabled(true);
291 _popup.add(zoomFullItem);
292 _popup.addSeparator();
294 JMenuItem setMapBgItem = new JMenuItem(
295 I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
296 setMapBgItem.addActionListener(new ActionListener() {
297 public void actionPerformed(ActionEvent e)
299 FunctionLibrary.FUNCTION_SET_MAP_BG.begin();
301 _popup.add(setMapBgItem);
303 JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
304 newPointItem.addActionListener(new ActionListener() {
305 public void actionPerformed(ActionEvent e)
307 _app.createPoint(MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_popupMenuY, getHeight())),
308 MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_popupMenuX, getWidth())));
310 newPointItem.setEnabled(true);
311 _popup.add(newPointItem);
316 * Zoom to fit the current data area
318 private void zoomToFit()
320 _latRange = _track.getLatRange();
321 _lonRange = _track.getLonRange();
322 _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
323 MapUtils.getXFromLongitude(_lonRange.getMaximum()));
324 _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
325 MapUtils.getYFromLatitude(_latRange.getMaximum()));
326 _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
327 getWidth(), getHeight());
333 * @see java.awt.Canvas#paint(java.awt.Graphics)
335 public void paint(Graphics inG)
338 if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
341 if (_track.getNumPoints() > 0)
343 // Check for autopan if enabled / necessary
344 if (_autopanCheckBox.isSelected())
346 int selectedPoint = _selection.getCurrentPointIndex();
347 if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
349 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
350 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
353 if (px < PAN_DISTANCE) {
354 panX = px - AUTOPAN_DISTANCE;
356 else if (px > (getWidth()-PAN_DISTANCE)) {
357 panX = AUTOPAN_DISTANCE + px - getWidth();
359 if (py < PAN_DISTANCE) {
360 panY = py - AUTOPAN_DISTANCE;
362 if (py > (getHeight()-PAN_DISTANCE)) {
363 panY = AUTOPAN_DISTANCE + py - getHeight();
365 if (panX != 0 || panY != 0) {
366 _mapPosition.pan(panX, panY);
369 _prevSelectedPoint = selectedPoint;
372 // Draw the mapImage if necessary
373 if ((_mapImage == null || _recalculate))
376 _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getCentreTileY());
378 // Draw the prepared image onto the panel
379 if (_mapImage != null) {
380 inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
382 // Draw the zoom rectangle if necessary
385 inG.setColor(Color.RED);
386 inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
387 inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
388 inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
389 inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
394 inG.setColor(COLOR_BG);
395 inG.fillRect(0, 0, getWidth(), getHeight());
396 inG.setColor(COLOR_MESSAGES);
397 inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
398 _scaleBar.updateScale(-1, 0);
400 // Draw slider etc on top
406 * Get the map tiles for the current zoom level and given tile parameters
408 private void getMapTiles()
410 if (_mapImage == null || _mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())
412 _mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
416 Graphics g = _mapImage.getGraphics();
418 g.setColor(COLOR_BG);
419 g.fillRect(0, 0, getWidth(), getHeight());
421 // reset error message
422 if (!_mapCheckBox.isSelected()) {_shownOsmErrorAlready = false;}
423 // Only get map tiles if selected
424 if (_mapCheckBox.isSelected())
427 _tileCacher.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
429 boolean loadingFailed = false;
430 if (_mapImage == null) return;
432 if (_tileCacher.isOverzoomed())
434 // display overzoom message
435 g.setColor(COLOR_MESSAGES);
436 g.drawString(I18nManager.getText("map.overzoom"), 50, getHeight()/2);
440 // Loop over tiles drawing each one
441 int[] tileIndices = _mapPosition.getTileIndices(getWidth(), getHeight());
442 int[] pixelOffsets = _mapPosition.getDisplayOffsets(getWidth(), getHeight());
443 for (int tileX = tileIndices[0]; tileX <= tileIndices[1] && !loadingFailed; tileX++)
445 int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
446 for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
448 int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
449 Image image = _tileCacher.getTile(tileX, tileY);
451 g.drawImage(image, x, y, 256, 256, null);
456 // Make maps brighter / fainter
457 float[] scaleFactors = {1.0f, 1.05f, 1.1f, 1.2f, 1.6f, 2.0f};
458 float scaleFactor = scaleFactors[_transparencySlider.getValue()];
459 if (scaleFactor > 1.0f)
461 RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
462 hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
463 RescaleOp op = new RescaleOp(scaleFactor, 0, hints);
464 op.filter(_mapImage, _mapImage);
469 // Paint the track points on top
470 int pointsPainted = 1;
473 pointsPainted = paintPoints(g);
475 catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing
481 _recalculate = false;
482 // Zoom to fit if no points found
483 if (pointsPainted <= 0 && _checkBounds) {
488 _checkBounds = false;
489 // enable / disable transparency slider
490 _transparencySlider.setEnabled(_mapCheckBox.isSelected());
495 * Paint the points using the given graphics object
496 * @param inG Graphics object to use for painting
497 * @return number of points painted, if any
499 private int paintPoints(Graphics inG)
501 int pointsPainted = 0;
503 inG.setColor(COLOR_POINT);
504 int prevX = -1, prevY = -1;
505 boolean connectPoints = _connectCheckBox.isSelected();
506 boolean prevPointVisible = false, currPointVisible = false;
507 for (int i=0; i<_track.getNumPoints(); i++)
509 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
510 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
511 currPointVisible = px >= 0 && px < getWidth() && py >= 0 && py < getHeight();
512 if (currPointVisible)
514 if (!_track.getPoint(i).isWaypoint())
516 // Draw rectangle for track point
517 if (_track.getPoint(i).getDeleteFlag()) {
518 inG.setColor(COLOR_POINT_DELETED);
521 inG.setColor(COLOR_POINT);
523 inG.drawRect(px-2, py-2, 3, 3);
527 if (!_track.getPoint(i).isWaypoint())
529 // Connect track points if either of them are visible
530 if (connectPoints && (currPointVisible || prevPointVisible)
531 && !(prevX == -1 && prevY == -1)
532 && !_track.getPoint(i).getSegmentStart())
534 inG.drawLine(prevX, prevY, px, py);
536 prevX = px; prevY = py;
538 prevPointVisible = currPointVisible;
541 // Loop over points, just drawing blobs for waypoints
542 inG.setColor(COLOR_WAYPT_NAME);
543 FontMetrics fm = inG.getFontMetrics();
544 int nameHeight = fm.getHeight();
545 int width = getWidth();
546 int height = getHeight();
547 for (int i=0; i<_track.getNumPoints(); i++)
549 if (_track.getPoint(i).isWaypoint())
551 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
552 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
553 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
555 inG.fillRect(px-3, py-3, 6, 6);
560 // Loop over points again, now draw names for waypoints
561 for (int i=0; i<_track.getNumPoints(); i++)
563 if (_track.getPoint(i).isWaypoint())
565 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
566 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
567 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
569 // Figure out where to draw waypoint name so it doesn't obscure track
570 String waypointName = _track.getPoint(i).getWaypointName();
571 int nameWidth = fm.stringWidth(waypointName);
572 boolean drawnName = false;
573 // Make arrays for coordinates right left up down
574 int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2};
575 int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2};
576 for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
578 // Shift arrays for coordinates right left up down
579 nameXs[0] += 2; nameXs[1] -= 2;
580 nameYs[2] -= 2; nameYs[3] += 2;
581 // Check each direction in turn right left up down
582 for (int a=0; a<4; a++)
584 if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < width
585 && nameYs[a] < height && (nameYs[a] - nameHeight) > 0
586 && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight))
588 // Found a rectangle to fit - draw name here and quit
589 inG.drawString(waypointName, nameXs[a], nameYs[a]);
598 // Loop over points, drawing blobs for photo points
599 inG.setColor(COLOR_PHOTO_PT);
600 for (int i=0; i<_track.getNumPoints(); i++)
602 if (_track.getPoint(i).getPhoto() != null)
604 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
605 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
606 if (px >= 0 && px < getWidth() && py >= 0 && py < getHeight())
608 inG.drawRect(px-1, py-1, 2, 2);
609 inG.drawRect(px-2, py-2, 4, 4);
615 // Draw selected range
616 if (_selection.hasRangeSelected())
618 inG.setColor(COLOR_CURR_RANGE);
619 for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
621 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(i));
622 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(i));
623 inG.drawRect(px-1, py-1, 2, 2);
627 // Draw selected point, crosshairs
628 int selectedPoint = _selection.getCurrentPointIndex();
629 if (selectedPoint >= 0)
631 int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint));
632 int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint));
633 inG.setColor(COLOR_CROSSHAIRS);
635 inG.drawLine(px, 0, px, getHeight());
636 inG.drawLine(0, py, getWidth(), py);
638 inG.drawOval(px - 2, py - 2, 4, 4);
639 inG.drawOval(px - 3, py - 3, 6, 6);
641 // Return the number of points painted
642 return pointsPainted;
647 * Tests whether there are any dark pixels within the specified x,y rectangle
648 * @param inX left X coordinate
649 * @param inY bottom Y coordinate
650 * @param inWidth width of rectangle
651 * @param inHeight height of rectangle
652 * @return true if there's at least one data point in the rectangle
654 private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight)
656 // each of the colour channels must be brighter than this to count as empty
657 final int BRIGHTNESS_LIMIT = 210;
660 // loop over x coordinate of rectangle
661 for (int x=0; x<inWidth; x++)
663 // loop over y coordinate of rectangle
664 for (int y=0; y<inHeight; y++)
666 int pixelColor = _mapImage.getRGB(inX + x, inY - y);
667 // split into four components rgba
668 int lowestBit = pixelColor & 255;
669 int secondBit = (pixelColor >> 8) & 255;
670 int thirdBit = (pixelColor >> 16) & 255;
671 //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored
672 if (lowestBit < BRIGHTNESS_LIMIT || secondBit < BRIGHTNESS_LIMIT || thirdBit < BRIGHTNESS_LIMIT) return true;
676 catch (NullPointerException e) {
677 // ignore null pointers, just return false
684 * Inform that tiles have been updated and the map can be repainted
685 * @param inIsOk true if data loaded ok, false for error
687 public synchronized void tilesUpdated(boolean inIsOk)
689 // Show message if loading failed (but not too many times)
690 if (!inIsOk && !_shownOsmErrorAlready && _mapCheckBox.isSelected())
692 _shownOsmErrorAlready = true;
693 // use separate thread to show message about failing to load osm images
694 new Thread(new Runnable() {
696 try {Thread.sleep(500);} catch (InterruptedException ie) {}
697 _app.showErrorMessage("error.osmimage.dialogtitle", "error.osmimage.failed");
706 * Zoom out, if not already at minimum zoom
708 public void zoomOut()
710 _mapPosition.zoomOut();
716 * Zoom in, if not already at maximum zoom
720 _mapPosition.zoomIn();
727 * @param inDeltaX x shift
728 * @param inDeltaY y shift
730 public void panMap(int inDeltaX, int inDeltaY)
732 _mapPosition.pan(inDeltaX, inDeltaY);
738 * @see javax.swing.JComponent#getMinimumSize()
740 public Dimension getMinimumSize()
742 final Dimension minSize = new Dimension(512, 300);
747 * @see javax.swing.JComponent#getPreferredSize()
749 public Dimension getPreferredSize()
751 return getMinimumSize();
756 * Respond to mouse click events
757 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
759 public void mouseClicked(MouseEvent inE)
761 if (_track != null && _track.getNumPoints() > 0)
763 // select point if it's a left-click
764 if (!inE.isMetaDown())
766 int pointIndex = _track.getNearestPointIndex(
767 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
768 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
769 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
770 _trackInfo.selectPoint(pointIndex);
774 // show the popup menu for right-clicks
775 _popupMenuX = inE.getX();
776 _popupMenuY = inE.getY();
777 _popup.show(this, _popupMenuX, _popupMenuY);
783 * Ignore mouse enter events
784 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
786 public void mouseEntered(MouseEvent inE)
792 * Ignore mouse exited events
793 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
795 public void mouseExited(MouseEvent inE)
801 * Ignore mouse pressed events
802 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
804 public void mousePressed(MouseEvent inE)
810 * Respond to mouse released events
811 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
813 public void mouseReleased(MouseEvent inE)
816 if (_zoomDragging && Math.abs(_dragToX - _dragFromX) > 20 && Math.abs(_dragToY - _dragFromY) > 20)
818 //System.out.println("Finished zoom: " + _dragFromX + ", " + _dragFromY + " to " + _dragToX + ", " + _dragToY);
819 _mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
821 _dragFromX = _dragFromY = -1;
822 _zoomDragging = false;
827 * Respond to mouse drag events
828 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
830 public void mouseDragged(MouseEvent inE)
832 if (!inE.isMetaDown())
834 // Left mouse drag - pan map by appropriate amount
835 _zoomDragging = false;
836 if (_dragFromX != -1)
838 panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
842 _dragFromX = inE.getX();
843 _dragFromY = inE.getY();
847 // Right-click and drag - draw rectangle and control zoom
848 _zoomDragging = true;
849 if (_dragFromX == -1) {
850 _dragFromX = inE.getX();
851 _dragFromY = inE.getY();
853 _dragToX = inE.getX();
854 _dragToY = inE.getY();
860 * Respond to mouse move events without button pressed
861 * @param inEvent ignored
863 public void mouseMoved(MouseEvent inEvent)
869 * Respond to status bar message from broker
870 * @param inMessage message, ignored
872 public void actionCompleted(String inMessage)
878 * Respond to data updated message from broker
879 * @param inUpdateType type of update
881 public void dataUpdated(byte inUpdateType)
884 if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
887 if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0) {
888 _tileCacher.setTileConfig(new MapTileConfig());
891 // enable or disable components
892 boolean hasData = _track.getNumPoints() > 0;
893 _topPanel.setVisible(hasData);
894 _sidePanel.setVisible(hasData);
895 // grab focus for the key presses
900 * Respond to key presses on the map canvas
901 * @param inE key event
903 public void keyPressed(KeyEvent inE)
905 int code = inE.getKeyCode();
906 int currPointIndex = _selection.getCurrentPointIndex();
907 // Check for meta key
908 if (inE.isControlDown())
910 // Check for arrow keys to zoom in and out
911 if (code == KeyEvent.VK_UP)
913 else if (code == KeyEvent.VK_DOWN)
915 // Key nav for next/prev point
916 else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
917 _trackInfo.selectPoint(currPointIndex-1);
918 else if (code == KeyEvent.VK_RIGHT)
919 _trackInfo.selectPoint(currPointIndex+1);
923 // Check for arrow keys to pan
925 if (code == KeyEvent.VK_UP)
926 upwardsPan = -PAN_DISTANCE;
927 else if (code == KeyEvent.VK_DOWN)
928 upwardsPan = PAN_DISTANCE;
929 int rightwardsPan = 0;
930 if (code == KeyEvent.VK_RIGHT)
931 rightwardsPan = PAN_DISTANCE;
932 else if (code == KeyEvent.VK_LEFT)
933 rightwardsPan = -PAN_DISTANCE;
934 panMap(rightwardsPan, upwardsPan);
935 // Check for delete key to delete current point
936 if (code == KeyEvent.VK_DELETE && currPointIndex >= 0)
938 _app.deleteCurrentPoint();
944 * @param inE key released event, ignored
946 public void keyReleased(KeyEvent e)
952 * @param inE key typed event, ignored
954 public void keyTyped(KeyEvent inE)
960 * @param inE mouse wheel event indicating scroll direction
962 public void mouseWheelMoved(MouseWheelEvent inE)
964 int clicks = inE.getWheelRotation();
972 * @return current map position
974 public MapPosition getMapPosition()