content: package tim.prune.gui.map;
  
  import java.awt.*;
  import java.awt.event.*;
  import java.awt.geom.AffineTransform;
  import java.awt.image.BufferedImage;
  import java.awt.image.ImageObserver;
  import java.util.List;
  
  import javax.swing.*;
  
  import tim.prune.App;
  import tim.prune.DataSubscriber;
  import tim.prune.FunctionLibrary;
  import tim.prune.GenericFunction;
  import tim.prune.I18nManager;
  import tim.prune.UpdateMessageBroker;
  import tim.prune.cmd.EditPointCmd;
  import tim.prune.cmd.InsertPointCmd;
  import tim.prune.config.ColourScheme;
  import tim.prune.config.Config;
  import tim.prune.data.*;
  import tim.prune.function.DeleteCurrentPoint;
  import tim.prune.function.Describer;
  import tim.prune.function.compress.MarkPointsInRectangleFunction;
  import tim.prune.function.edit.FieldEdit;
  import tim.prune.gui.IconManager;
  import tim.prune.gui.MultiStateCheckBox;
  import tim.prune.gui.colour.PointColourer;
  import tim.prune.gui.colour.WaypointColours;
  import tim.prune.gui.colour.WaypointSymbolPainter;
  import tim.prune.tips.TipManager;
  
  /**
   * Class for the map canvas, to display a background map and draw on it
   */
  public class MapCanvas extends JPanel implements MouseListener, MouseMotionListener, DataSubscriber,
  	KeyListener, MouseWheelListener, TileConsumer
  {
  	/** App object for callbacks */
  	private final App _app;
  	/** Track object */
  	private final Track _track;
  	/** TrackInfo object */
  	private final TrackInfo _trackInfo;
  	/** Selection object */
  	private final Selection _selection;
  	/** Object to keep track of midpoints */
  	private final MidpointData _midpoints;
  	/** Index of point clicked at mouseDown */
  	private int _clickedPoint = -1;
  	/** Previously selected point */
  	private int _prevSelectedPoint = -1;
  	/** Tile manager */
  	private final MapTileManager _tileManager = new MapTileManager(this);
  	/** Image to display */
  	private BufferedImage _mapImage = null;
  	/** Second image for drawing track (only needed for alpha blending) */
  	private BufferedImage _trackImage = null;
  	/** Slider for transparency */
  	private JSlider _transparencySlider = null;
  	/** Checkbox for scale bar */
  	private final JCheckBox _scaleCheckBox;
  	/** Checkbox for maps */
  	private final JCheckBox _mapCheckBox;
  	/** Checkbox for autopan */
  	private final JCheckBox _autopanCheckBox;
  	/** Checkbox for connecting track points */
  	private final MultiStateCheckBox _connectCheckBox;
  	/** Checkbox for enable edit mode */
  	private final JCheckBox _editmodeCheckBox;
  	/** Right-click popup menu */
  	private JPopupMenu _popup = null;
  	/** Top component panel */
  	private final JPanel _topPanel;
  	/** Side component panel */
  	private final JPanel _sidePanel;
  	/** Scale bar */
  	private ScaleBar _scaleBar = null;
  	/* Data */
  	private DoubleRange _latRange = null, _lonRange = null;
  
  	private boolean _recalculate = false;
  	/** Flag to check bounds on next paint */
  	private boolean _checkBounds = false;
  	/** Map position */
  	private final MapPosition _mapPosition;
  	/** coordinates of drag from point */
  	private int _dragFromX = -1, _dragFromY = -1;
  	/** coordinates of drag to point */
  	private int _dragToX = -1, _dragToY = -1;
  	/** coordinates of popup menu */
  	private int _popupMenuX = -1, _popupMenuY = -1;
  	/** Current drawing mode */
  	private DrawMode _drawMode = DrawMode.DEFAULT;
  	/** Current waypoint icon definition */
  	private WpIconDefinition _waypointIconDefinition = null;
  	/** Colours for waypoint icons */
  	private final WaypointColours _waypointColours = new WaypointColours();
  	/** Remember whether map is being drawn with empty track or not */
  	private boolean _emptyTrack = true;
  
  	/** Scaling factor of display from OS */
  	private double _lastScale = 1.0;
  
  	/** Constant for click sensitivity when selecting nearest point */
  	private static final int CLICK_SENSITIVITY = 10;
  	/** Constant for pan distance from key presses */
  	private static final int PAN_DISTANCE = 20;
  	/** Constant for pan distance from autopan */
  	private static final int AUTOPAN_DISTANCE = 75;
  
  	// Drawing modes
  	private enum DrawMode {DEFAULT, ZOOM_RECT, DRAW_POINTS_START, DRAW_POINTS_CONT, DRAG_POINT,
  		CREATE_MIDPOINT, MARK_RECTANGLE_INSIDE, MARK_RECTANGLE_OUTSIDE};
  
  	private static final int INDEX_UNKNOWN  = -2;
  
  
  	/**
  	 * Constructor
  	 * @param inApp App object for callbacks
  	 */
  	public MapCanvas(App inApp)
  	{
  		_app = inApp;
  		_trackInfo = _app.getTrackInfo();
  		_track = _trackInfo.getTrack();
  		_selection = _trackInfo.getSelection();
  		_midpoints = new MidpointData();
  		_mapPosition = new MapPosition();
  		addMouseListener(this);
  		addMouseMotionListener(this);
  		addMouseWheelListener(this);
  		addKeyListener(this);
  
  		// Make listener for changes to controls
  		ItemListener itemListener = e -> {
  			_recalculate = true;
  			repaint();
  		};
  		// Make special listener for changes to map checkbox
  		ItemListener mapCheckListener = e -> {
  			_tileManager.clearMemoryCaches();
  			_recalculate = true;
  			_app.getConfig().setConfigBoolean(Config.KEY_SHOW_MAP, e.getStateChange() == ItemEvent.SELECTED);
  			UpdateMessageBroker.informSubscribers(); // to let menu know
  			// If the track is only partially visible and you turn the map off, make the track fully visible again
  			if (e.getStateChange() == ItemEvent.DESELECTED && _transparencySlider.getValue() < 0) {
  				_transparencySlider.setValue(0);
  			}
  		};
  		_topPanel = new OverlayPanel();
  		_topPanel.setLayout(new FlowLayout());
  		// Make slider for transparency
  		_transparencySlider = new JSlider(-6, 6, 0);
  		_transparencySlider.setPreferredSize(new Dimension(100, 20));
  		_transparencySlider.setMajorTickSpacing(1);
  		_transparencySlider.setSnapToTicks(true);
  		_transparencySlider.setOpaque(false);
  		_transparencySlider.setValue(0);
  		_transparencySlider.addChangeListener(e -> {
  			int val = _transparencySlider.getValue();
  			if (val == 1 || val == -1) {
  				_transparencySlider.setValue(0);
  			} else {
  				_recalculate = true;
  				repaint();
  			}
  		});
  		_transparencySlider.setFocusable(false); // stop slider from stealing keyboard focus
  		_topPanel.add(_transparencySlider);
  		IconManager iconManager = _app.getIconManager();
  		// Add checkbox button for enabling scale bar
  		_scaleCheckBox = new JCheckBox(iconManager.getImageIcon(IconManager.SCALEBAR_BUTTON), true);
  		_scaleCheckBox.setSelectedIcon(iconManager.getImageIcon(IconManager.SCALEBAR_BUTTON_ON));
  		_scaleCheckBox.setOpaque(false);
  		_scaleCheckBox.setToolTipText(I18nManager.getText("menu.map.showscalebar"));
  		_scaleCheckBox.addItemListener(e -> _scaleBar.setVisible(_scaleCheckBox.isSelected()));
  		_scaleCheckBox.setFocusable(false); // stop button from stealing keyboard focus
  		_topPanel.add(_scaleCheckBox);
  		// Add checkbox button for enabling maps or not
  		_mapCheckBox = new JCheckBox(iconManager.getImageIcon(IconManager.MAP_BUTTON), false);
  		_mapCheckBox.setSelectedIcon(iconManager.getImageIcon(IconManager.MAP_BUTTON_ON));
  		_mapCheckBox.setOpaque(false);
  		_mapCheckBox.setToolTipText(I18nManager.getText("menu.map.showmap"));
  		_mapCheckBox.addItemListener(mapCheckListener);
  		_mapCheckBox.setFocusable(false); // stop button from stealing keyboard focus
  		_topPanel.add(_mapCheckBox);
  		// Add checkbox button for enabling autopan or not
  		_autopanCheckBox = new JCheckBox(iconManager.getImageIcon(IconManager.AUTOPAN_BUTTON), true);
  		_autopanCheckBox.setSelectedIcon(iconManager.getImageIcon(IconManager.AUTOPAN_BUTTON_ON));
  		_autopanCheckBox.setOpaque(false);
  		_autopanCheckBox.setToolTipText(I18nManager.getText("menu.map.autopan"));
  		_autopanCheckBox.addItemListener(itemListener);
  		_autopanCheckBox.setFocusable(false); // stop button from stealing keyboard focus
  		_topPanel.add(_autopanCheckBox);
  		// Add checkbox button for connecting points or not
  		_connectCheckBox = new MultiStateCheckBox(4);
  		_connectCheckBox.setIcon(0, iconManager.getImageIcon(IconManager.POINTS_WITH_ARROWS_BUTTON));
  		_connectCheckBox.setIcon(1, iconManager.getImageIcon(IconManager.POINTS_HIDDEN_BUTTON));
  		_connectCheckBox.setIcon(2, iconManager.getImageIcon(IconManager.POINTS_CONNECTED_BUTTON));
  		_connectCheckBox.setIcon(3, iconManager.getImageIcon(IconManager.POINTS_DISCONNECTED_BUTTON));
  		_connectCheckBox.setCurrentState(0);
  		_connectCheckBox.setOpaque(false);
  		_connectCheckBox.setToolTipText(I18nManager.getText("menu.map.connect"));
  		_connectCheckBox.addItemListener(itemListener);
  		_connectCheckBox.setFocusable(false); // stop button from stealing keyboard focus
  		_topPanel.add(_connectCheckBox);
  
  		// Add checkbox button for edit mode or not
  		_editmodeCheckBox = new JCheckBox(iconManager.getImageIcon(IconManager.EDIT_MODE_BUTTON), false);
  		_editmodeCheckBox.setSelectedIcon(iconManager.getImageIcon(IconManager.EDIT_MODE_BUTTON_ON));
  		_editmodeCheckBox.setOpaque(false);
  		_editmodeCheckBox.setToolTipText(I18nManager.getText("menu.map.editmode"));
  		_editmodeCheckBox.addItemListener(itemListener);
  		_editmodeCheckBox.setFocusable(false); // stop button from stealing keyboard focus
  		_topPanel.add(_editmodeCheckBox);
  
  		// Add zoom in, zoom out buttons
  		_sidePanel = new OverlayPanel();
  		_sidePanel.setLayout(new BoxLayout(_sidePanel, BoxLayout.Y_AXIS));
  		JButton zoomInButton = new JButton(iconManager.getImageIcon(IconManager.ZOOM_IN_BUTTON));
  		zoomInButton.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
  		zoomInButton.setContentAreaFilled(false);
  		zoomInButton.setToolTipText(I18nManager.getText("menu.map.zoomin"));
  		zoomInButton.addActionListener(e -> zoomIn());
  		zoomInButton.setFocusable(false); // stop button from stealing keyboard focus
  		_sidePanel.add(zoomInButton);
  		JButton zoomOutButton = new JButton(iconManager.getImageIcon(IconManager.ZOOM_OUT_BUTTON));
  		zoomOutButton.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
  		zoomOutButton.setContentAreaFilled(false);
  		zoomOutButton.setToolTipText(I18nManager.getText("menu.map.zoomout"));
  		zoomOutButton.addActionListener(e -> zoomOut());
  		zoomOutButton.setFocusable(false); // stop button from stealing keyboard focus
  		_sidePanel.add(zoomOutButton);
  
  		// Bottom panel for scale bar
  		_scaleBar = new ScaleBar(_app.getConfig());
  
  		// add control panels to this one
  		setLayout(new BorderLayout());
  		_topPanel.setVisible(false);
  		_sidePanel.setVisible(false);
  		add(_topPanel, BorderLayout.NORTH);
  		add(_sidePanel, BorderLayout.WEST);
  		add(_scaleBar, BorderLayout.SOUTH);
  		// Make popup menu
  		makePopup();
  		// Get currently selected map from Config, pass to MapTileManager
  		_tileManager.setMapSource(_app.getConfig().getConfigInt(Config.KEY_MAPSOURCE_INDEX));
  		// Update display settings
  		dataUpdated(MAPSERVER_CHANGED);
  	}
  
  
  	/**
  	 * Make the popup menu for right-clicking the map
  	 */
  	private void makePopup()
  	{
  		_popup = new JPopupMenu();
  		JMenuItem zoomInItem = new JMenuItem(I18nManager.getText("menu.map.zoomin"));
  		zoomInItem.addActionListener(e -> {
  			panMap((_popupMenuX - getScaledWidth()/2)/2, (_popupMenuY - getScaledHeight()/2)/2);
  			zoomIn();
  		});
  		_popup.add(zoomInItem);
  		JMenuItem zoomOutItem = new JMenuItem(I18nManager.getText("menu.map.zoomout"));
  		zoomOutItem.addActionListener(e -> {
  			panMap(-(_popupMenuX - getScaledWidth()/2), -(_popupMenuY - getScaledHeight()/2));
  			zoomOut();
  		});
  		_popup.add(zoomOutItem);
  		JMenuItem zoomFullItem = new JMenuItem(I18nManager.getText("menu.map.zoomfull"));
  		zoomFullItem.addActionListener(e -> {
  			zoomToFit();
  			_recalculate = true;
  			repaint();
  		});
  		_popup.add(zoomFullItem);
  		_popup.addSeparator();
  		// Set background
  		JMenuItem setMapBgItem = new JMenuItem(
  			I18nManager.getText(FunctionLibrary.FUNCTION_SET_MAP_BG.getNameKey()));
  		setMapBgItem.addActionListener(e -> FunctionLibrary.FUNCTION_SET_MAP_BG.begin());
  		_popup.add(setMapBgItem);
  		// new point option
  		JMenuItem newPointItem = new JMenuItem(I18nManager.getText("menu.map.newpoint"));
  		newPointItem.addActionListener(e -> insertPoint(createPointFromClick(_popupMenuX, _popupMenuY, true), -1));
  		_popup.add(newPointItem);
  		// draw point series
  		JMenuItem drawPointsItem = new JMenuItem(I18nManager.getText("menu.map.drawpoints"));
  		drawPointsItem.addActionListener(e -> _drawMode = DrawMode.DRAW_POINTS_START);
  		_popup.add(drawPointsItem);
  	}
  
  
  	/**
  	 * Zoom to fit the current data area
  	 */
  	private void zoomToFit()
  	{
  		if (_track.getNumPoints() > 0)
  		{
  			_latRange = _track.getLatRange();
  			_lonRange = _track.getLonRange();
  		}
  		if (_latRange == null || _lonRange == null
  			|| !_latRange.hasData() || !_lonRange.hasData())
  		{
  			setDefaultLatLonRange();
  		}
  		DoubleRange xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
  			MapUtils.getXFromLongitude(_lonRange.getMaximum()));
  		DoubleRange yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
  			MapUtils.getYFromLatitude(_latRange.getMaximum()));
  		_mapPosition.zoomToXY(xRange.getMinimum(), xRange.getMaximum(), yRange.getMinimum(), yRange.getMaximum(),
  			getScaledWidth(), getScaledHeight());
  		showZoomLevel();
  	}
  
  	/**
  	 * Track data is empty, so find a default area on the map to show
  	 */
  	private void setDefaultLatLonRange()
  	{
  		String storedRange = _app.getConfig().getConfigString(Config.KEY_LATLON_RANGE);
  		// Parse it into four latlon values
  		try
  		{
  			String[] values = storedRange.split(";");
  			if (values.length == 4)
  			{
  				final double lat1 = Double.parseDouble(values[0]);
  				final double lat2 = Double.parseDouble(values[1]);
  				if (lat1 >= -90.0 && lat1 <= 90.0 && lat2 >= -90.0 && lat2 <= 90.0 && lat1 != lat2)
  				{
  					_latRange = new DoubleRange(lat1, lat2);
  					final double lon1 = Double.parseDouble(values[2]);
  					final double lon2 = Double.parseDouble(values[3]);
  					if (lon1 >= -180.0 && lon1 <= 180.0 && lon2 >= -180.0 && lon2 <= 180.0 && lon1 != lon2)
  					{
  						_lonRange = new DoubleRange(lon1, lon2);
  						return;
  					}
  				}
  			}
  		}
  		catch (Exception ignored) {}
  		_latRange = new DoubleRange(45.8, 47.9);
  		_lonRange = new DoubleRange(5.9, 10.6);
  	}
  
  	/**
  	 * Paint method
  	 * @see java.awt.Canvas#paint(java.awt.Graphics)
  	 */
  	public void paint(Graphics inG)
  	{
  		super.paint(inG);
  		if (_mapImage != null && (_mapImage.getWidth() != getScaledWidth() || _mapImage.getHeight() != getScaledHeight())) {
  			_mapImage = null;
  		}
  		final boolean showMap = _app.getConfig().getConfigBoolean(Config.KEY_SHOW_MAP);
  		final boolean showSomething = _track.getNumPoints() > 0 || showMap;
  		if (showSomething)
  		{
  			// Check for autopan if enabled / necessary
  			if (_autopanCheckBox.isSelected())
  			{
  				int selectedPoint = _selection.getCurrentPointIndex();
  				if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
  				{
  					autopanToPoint(selectedPoint);
  				}
  				_prevSelectedPoint = selectedPoint;
  			}
  
  			// Recognise empty map position, if no data has been loaded
  			if (_emptyTrack && _track.getNumPoints() > 1)
  			{
  				zoomToFit();
  				_recalculate = true;
  			}
  			_emptyTrack = (_track.getNumPoints() == 0);
  
  			// Draw the map contents if necessary
  			if (_mapImage == null || _recalculate)
  			{
  				paintMapContents();
  				_scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
  			}
  			// Draw the prepared image onto the panel
  			if (_mapImage != null)
  			{
  				Graphics2D g2 = (Graphics2D) (inG.create());
  				final boolean allowScaling = _app.getConfig().getConfigBoolean(Config.KEY_OSSCALING);
  				final double preScale = allowScaling ? 1.0 : g2.getTransform().getScaleX();
  				if (preScale != _lastScale)
  				{
  					_lastScale = preScale;
  					_mapPosition.setDisplayScaling(_lastScale);
  					_scaleBar.setDisplayScaling(_lastScale);
  				}
  				if (!allowScaling)
  				{
  					AffineTransform at = g2.getTransform();
  					final double xTranslate = at.getTranslateX();
  					final double yTranslate = at.getTranslateY();
  					at.setToScale(1.0, 1.0);
  					at.translate(xTranslate, yTranslate);
  					g2.setTransform(at);
  				}
  				g2.drawImage(_mapImage, 0, 0, getScaledWidth(), getScaledHeight(), null);
  			}
  
  			switch (_drawMode)
  			{
  				case DRAG_POINT:
  					DataPoint currPoint = _track.getPoint(_selection.getCurrentPointIndex());
  					if (currPoint != null)
  					{
  						final int currPointIndex = _selection.getCurrentPointIndex();
  						if (currPoint.isWaypoint()) {
  							drawDragLines(inG, currPointIndex, currPointIndex);
  						}
  						else {
  							drawDragLines(inG, currPointIndex - 1, currPointIndex + 1);
  						}
  					}
  					break;
  
  				case CREATE_MIDPOINT:
  					drawDragLines(inG, _clickedPoint-1, _clickedPoint);
  					break;
  
  				case ZOOM_RECT:
  				case MARK_RECTANGLE_INSIDE:
  				case MARK_RECTANGLE_OUTSIDE:
  					if (_dragFromX != -1 && _dragFromY != -1)
  					{
  						// Draw the zoom rectangle if necessary
  						inG.setColor(Color.RED);
  						inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
  						inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
  						inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
  						inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY);
  					}
  					break;
  
  				case DRAW_POINTS_CONT:
  					int prevIndex = _track.getNumPoints() - 1;
  					if (prevIndex >= 0)
  					{
  						// draw line to mouse position to show drawing mode
  						inG.setColor(_app.getConfig().getColourScheme().getColour(ColourScheme.IDX_POINT));
  						int px = getWidth() / 2 + (int) (_mapPosition.getXFromCentre(_track.getX(prevIndex)) / _lastScale);
  						int py = getHeight() / 2 + (int) (_mapPosition.getYFromCentre(_track.getY(prevIndex)) / _lastScale);
  						inG.drawLine(px, py, _dragToX, _dragToY);
  					}
  					break;
  				case DRAW_POINTS_START:
  				case DEFAULT:
  					break;
  			}
  		}
  		else
  		{
  			final ColourScheme colourScheme = _app.getConfig().getColourScheme();
  			inG.setColor(colourScheme.getColour(ColourScheme.IDX_BACKGROUND));
  			inG.fillRect(0, 0, getScaledWidth(), getScaledHeight());
  			inG.setColor(colourScheme.getColour(ColourScheme.IDX_TEXT));
  			inG.drawString(I18nManager.getText("display.nodata"), 50, getScaledHeight()/2);
  			_scaleBar.updateScale(-1, 0);
  		}
  		// enable or disable panels
  		_topPanel.setVisible(showSomething);
  		_sidePanel.setVisible(showSomething);
  		// Draw slider etc on top
  		paintChildren(inG);
  	}
  
  	/**
  	 * @return true if the currently selected point is visible, false if off-screen or nothing selected
  	 */
  	private boolean isCurrentPointVisible()
  	{
  		if (_trackInfo.getCurrentPoint() == null) {return false;}
  		final int selectedPoint = _selection.getCurrentPointIndex();
  		final int xFromCentre = Math.abs(_mapPosition.getXFromCentre(_track.getX(selectedPoint)));
  		if (xFromCentre > (getScaledWidth()/2)) {return false;}
  		final int yFromCentre = Math.abs(_mapPosition.getYFromCentre(_track.getY(selectedPoint)));
  		return yFromCentre < (getScaledHeight()/2);
  	}
  
  	/**
  	 * If the specified point isn't visible, pan to it
  	 * @param inIndex index of selected point
  	 */
  	private void autopanToPoint(int inIndex)
  	{
  		int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(inIndex));
  		int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(inIndex));
  		int panX = 0;
  		int panY = 0;
  		if (px < PAN_DISTANCE) {
  			panX = px - AUTOPAN_DISTANCE;
  		}
  		else if (px > (getWidth()-PAN_DISTANCE)) {
  			panX = AUTOPAN_DISTANCE + px - getWidth();
  		}
  		if (py < (2*PAN_DISTANCE)) {
  			panY = py - AUTOPAN_DISTANCE;
  		}
  		if (py > (getHeight()-PAN_DISTANCE)) {
  			panY = AUTOPAN_DISTANCE + py - getHeight();
  		}
  		if (panX != 0 || panY != 0) {
  			_mapPosition.pan(panX, panY);
  		}
  	}
  
  	/**
  	 * Paint the map tiles and the points on to the _mapImage
  	 */
  	private void paintMapContents()
  	{
  		if (_mapImage == null || _mapImage.getWidth() != getScaledWidth() || _mapImage.getHeight() != getScaledHeight())
  		{
  			_mapImage = new BufferedImage(getScaledWidth(), getScaledHeight(), BufferedImage.TYPE_INT_RGB);
  		}
  
  		Graphics g = _mapImage.getGraphics();
  		// Set antialiasing according to config
  		final Config config = _app.getConfig();
  		final ColourScheme colourScheme = config.getColourScheme();
  		((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
  			config.getConfigBoolean(Config.KEY_ANTIALIAS) ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
  		// Clear to background
  		g.setColor(colourScheme.getColour(ColourScheme.IDX_BACKGROUND));
  		g.fillRect(0, 0, getScaledWidth(), getScaledHeight());
  
  		// Check whether maps are on or not
  		final boolean showMap = config.getConfigBoolean(Config.KEY_SHOW_MAP);
  		_mapCheckBox.setSelected(showMap);
  		// Check whether disk cache is on or not
  		final boolean usingDiskCache = config.getConfigString(Config.KEY_DISK_CACHE) != null;
  		// Show tip to recommend setting up a cache
  		if (showMap && !usingDiskCache && config.getConfigBoolean(Config.KEY_ONLINE_MODE))
  		{
  			SwingUtilities.invokeLater(() -> _app.showTip(TipManager.Tip_UseAMapCache));
  		}
  
  		_recalculate = false;
  		// Only get map tiles if selected
  		if (showMap)
  		{
  			// init tile cacher
  			_tileManager.centreMap(_mapPosition.getZoom(), _mapPosition.getCentreTileX(), _mapPosition.getCentreTileY());
  
  			if (_mapImage == null) return;
  
  			if (_tileManager.isOverzoomed())
  			{
  				// display overzoom message
  				g.setColor(colourScheme.getColour(ColourScheme.IDX_TEXT));
  				g.drawString(I18nManager.getText("map.overzoom"), 50, getScaledHeight()/2);
  			}
  			else
  			{
  				int numLayers = _tileManager.getNumLayers();
  				// Loop over tiles drawing each one
  				int[] tileIndices = _mapPosition.getTileIndices(getScaledWidth(), getScaledHeight());
  				int[] pixelOffsets = _mapPosition.getDisplayOffsets(getScaledWidth(), getScaledHeight());
  				for (int tileX = tileIndices[0]; tileX <= tileIndices[1]; tileX++)
  				{
  					int x = (tileX - tileIndices[0]) * 256 - pixelOffsets[0];
  					for (int tileY = tileIndices[2]; tileY <= tileIndices[3]; tileY++)
  					{
  						int y = (tileY - tileIndices[2]) * 256 - pixelOffsets[1];
  						// Loop over layers
  						for (int l=0; l<numLayers; l++)
  						{
  							Image image = _tileManager.getTile(l, tileX, tileY, true, _app.getConfig());
  							if (image != null) {
  								g.drawImage(image, x, y, 256, 256, (img, flags, px, py, width, height) -> checkPaintedTile(flags));
  							}
  						}
  					}
  				}
  
  				// Make maps brighter / fainter according to slider
  				final int brightnessIndex = Math.max(1, _transparencySlider.getValue()) - 1;
  				if (brightnessIndex > 0)
  				{
  					final int[] alphas = {0, 40, 80, 120, 160, 210};
  					Color bgColor = colourScheme.getColour(ColourScheme.IDX_BACKGROUND);
  					bgColor = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), alphas[brightnessIndex]);
  					g.setColor(bgColor);
  					g.fillRect(0, 0, getScaledWidth(), getScaledHeight());
  				}
  			}
  		}
  
  		// Work out track opacity according to slider
  		final float[] opacities = {1.0f, 0.75f, 0.5f, 0.3f, 0.15f, 0.0f};
  		float trackOpacity = 1.0f;
  		if (_transparencySlider.getValue() < 0) {
  			trackOpacity = opacities[-1 - _transparencySlider.getValue()];
  		}
  
  		if (trackOpacity > 0.0f)
  		{
  			// Paint the track points on top
  			boolean pointsPainted = true;
  			try
  			{
  				if (trackOpacity > 0.9f)
  				{
  					// Track is fully opaque, just draw it directly
  					pointsPainted = paintPoints(g);
  					_trackImage = null;
  				}
  				else
  				{
  					// Track is partly transparent, so use a separate BufferedImage
  					if (_trackImage == null || _trackImage.getWidth() != getScaledWidth() || _trackImage.getHeight() != getScaledHeight())
  					{
  						_trackImage = new BufferedImage(getScaledWidth(), getScaledHeight(), BufferedImage.TYPE_INT_ARGB);
  					}
  					// Clear to transparent
  					Graphics2D gTrack = _trackImage.createGraphics();
  					gTrack.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, 0.0f));
  					gTrack.fillRect(0, 0, getScaledWidth(), getScaledHeight());
  					gTrack.setPaintMode();
  					// Draw the track onto this separate image
  					pointsPainted = paintPoints(gTrack);
  					((Graphics2D) g).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, trackOpacity));
  					g.drawImage(_trackImage, 0, 0, null);
  				}
  			}
  			catch (NullPointerException | ArrayIndexOutOfBoundsException ignored) {}
  			// ignore, probably due to data being changed during drawing
  
  			// Zoom to fit if no points found
  			if (!pointsPainted && _checkBounds)
  			{
  				zoomToFit();
  				_recalculate = true;
  				repaint();
  			}
  		}
  
  		// free g
  		g.dispose();
  
  		_checkBounds = false;
  		// enable / disable transparency slider
  		_transparencySlider.setEnabled(showMap);
  	}
  
  
  	private boolean checkPaintedTile(int flags)
  	{
  		if ((flags & ImageObserver.ALLBITS) == 0) {
  			tilesUpdated(true);
  			return true;
  		}
  		return false;
  	}
  
  
  	/**
  	 * Paint the points using the given graphics object
  	 * @param inG Graphics object to use for painting
  	 * @return true if any points or lines painted
  	 */
  	private boolean paintPoints(Graphics inG)
  	{
  		// Set up colours
  		final ColourScheme cs = _app.getConfig().getColourScheme();
  		final Color pointColour  = cs.getColour(ColourScheme.IDX_POINT);
  		final Color rangeColour  = cs.getColour(ColourScheme.IDX_SELECTION);
  		final Color currentColour = cs.getColour(ColourScheme.IDX_PRIMARY);
  		final Color secondColour = cs.getColour(ColourScheme.IDX_SECONDARY);
  		final Color textColour   = cs.getColour(ColourScheme.IDX_TEXT);
  		final PointColourer pointColourer = _app.getPointColourer();
  
  		final int winWidth  = getScaledWidth();
  		final int winHeight = getScaledHeight();
  		final int halfWinWidth  = winWidth / 2;
  		final int halfWinHeight = winHeight / 2;
  
  		final int numPoints = _track.getNumPoints();
  		final int[] xPixels = new int[numPoints];
  		final int[] yPixels = new int[numPoints];
  
  		final int pointSeparationForArrowsSqd = 400;
  		final int pointSeparation1dForArrows = (int) (Math.sqrt(pointSeparationForArrowsSqd) * 0.7);
  		final int hugePointSeparationForArrows = 120;
  
  		// try to set line width for painting
  		if (inG instanceof Graphics2D)
  		{
  			int lineWidth = _app.getConfig().getConfigInt(Config.KEY_LINE_WIDTH);
  			if (lineWidth < 1 || lineWidth > 4) {lineWidth = 2;}
  			((Graphics2D) inG).setStroke(new BasicStroke(lineWidth));
  		}
  
  		boolean pointsPainted = false;
  		// draw track points
  		inG.setColor(pointColour);
  		int prevX = -1, prevY = -1;
  		final int connectState = _connectCheckBox.getCurrentState();
  		final boolean drawLines = (connectState != 3);  // 0, 1 or 2
  		final boolean drawPoints = (connectState != 1); // 0, 2 or 3
  		final boolean drawArrows = (connectState == 0); // 0
  
  		boolean prevPointVisible = false;
  		boolean anyWaypoints = false;
  		boolean drawnLastArrow = false;	// avoid painting arrows on adjacent lines, looks too busy
  		for (int i=0; i<numPoints; i++)
  		{
  			// Calculate pixel position of point from its x, y coordinates
  			int px = halfWinWidth  + _mapPosition.getXFromCentre(_track.getX(i));
  			int py = halfWinHeight + _mapPosition.getYFromCentre(_track.getY(i));
  			px = wrapLongitudeValue(px, winWidth, _mapPosition.getZoom());
  			// Remember these calculated pixel values so they don't have to be recalculated
  			xPixels[i] = px; yPixels[i] = py;
  
  			final boolean currPointVisible = px >= 0 && px < winWidth && py >= 0 && py < winHeight;
  			final boolean isWaypoint = _track.getPoint(i).isWaypoint();
  			anyWaypoints = anyWaypoints || isWaypoint;
  			if (!isWaypoint)
  			{
  				if (currPointVisible || (drawLines && prevPointVisible))
  				{
  					// For track points, work out which colour to use
  					if (_trackInfo.isPointMarkedForDeletion(i)) {
  						inG.setColor(currentColour);
  					}
  					else if (pointColourer != null)
  					{  // use the point colourer if there is one
  						Color trackColour = pointColourer.getColour(i);
  						inG.setColor(trackColour);
  					}
  					else
  					{
  						inG.setColor(pointColour);
  					}
  
  					// Draw rectangle for track point if it's visible
  					if (currPointVisible)
  					{
  						if (drawPoints) {
  							inG.drawRect(px-2, py-2, 3, 3);
  						}
  						pointsPainted = true;
  					}
  				}
  
  				// Connect track points if either of them are visible
  				if (drawLines
  				 && (currPointVisible || prevPointVisible)
  				 && !(prevX == -1 && prevY == -1)
  				 && !_track.getPoint(i).getSegmentStart())
  				{
  					inG.drawLine(prevX, prevY, px, py);
  					pointsPainted = true;
  
  					// Now consider whether we need to draw an arrow as well
  					if (drawArrows)
  					{
  						final double pointDist = Math.max(Math.abs(prevX - px), Math.abs(prevY - py));
  						final int separationLimit = (drawnLastArrow ? hugePointSeparationForArrows : pointSeparation1dForArrows);
  						if (pointDist > separationLimit)
  						{
  							final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py);
  							if (pointSeparationSqd > pointSeparationForArrowsSqd)
  							{
  								final double midX = (prevX + px) / 2.0;
  								final double midY = (prevY + py) / 2.0;
  								final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight;
  								if (midPointVisible)
  								{
  									final double alpha = Math.atan2(py - prevY, px - prevX);
  									final double MID_TO_VERTEX = 3.0;
  									final double arrowX = MID_TO_VERTEX * Math.cos(alpha);
  									final double arrowY = MID_TO_VERTEX * Math.sin(alpha);
  									final double vertexX = midX + arrowX;
  									final double vertexY = midY + arrowY;
  									inG.drawLine((int)(midX-arrowX-2*arrowY), (int)(midY-arrowY+2*arrowX), (int)vertexX, (int)vertexY);
  									inG.drawLine((int)(midX-arrowX+2*arrowY), (int)(midY-arrowY-2*arrowX), (int)vertexX, (int)vertexY);
  								}
  								drawnLastArrow = midPointVisible;
  							}
  						}
  						else
  						{
  							drawnLastArrow = false;
  						}
  					}
  				}
  				prevX = px; prevY = py;
  			}
  			prevPointVisible = currPointVisible;
  		}
  
  		// Loop over points, just drawing blobs for waypoints
  		inG.setColor(textColour);
  		if (anyWaypoints)
  		{
  			// TODO: Expand font size of inG using _lastScale
  			FontMetrics fm = inG.getFontMetrics();
  			final int nameHeight = fm.getHeight();
  			_waypointColours.setSalt(_app.getConfig().getConfigInt(Config.KEY_WPICON_SALT));
  			int numWaypoints = 0;
  			for (int i=0; i<_track.getNumPoints(); i++)
  			{
  				if (_track.getPoint(i).isWaypoint())
  				{
  					int px = xPixels[i];
  					int py = yPixels[i];
  					if (px >= 0 && px < winWidth && py >= 0 && py < winHeight)
  					{
  						if (_waypointIconDefinition == null) {
  							inG.fillRect(px-3, py-3, 6, 6);
  						}
  						else
  						{
  							ImageIcon icon = _waypointIconDefinition.getImageIcon();
  							Color paintColor = _waypointColours.getColourForType(_track.getPoint(i).getFieldValue(Field.WAYPT_TYPE));
  							Image painted = WaypointSymbolPainter.paintSymbol(icon, paintColor);
  							if (painted != null)
  							{
  								inG.drawImage(painted, px-_waypointIconDefinition.getXOffset(),
  									py-_waypointIconDefinition.getYOffset(), null);
  							}
  						}
  						pointsPainted = true;
  						numWaypoints++;
  					}
  				}
  			}
  			// Take more care with waypoint names if less than 100 are visible
  			final int numNameSteps = (numWaypoints > 100 ? 1 : 4);
  			final int numPointSteps = (numWaypoints > 1000 ? 2 : 1);
  
  			// Loop over points again, now draw names for waypoints
  			int[] nameXs = {0, 0, 0, 0};
  			int[] nameYs = {0, 0, 0, 0};
  			for (int i=0; i<_track.getNumPoints(); i += numPointSteps)
  			{
  				if (_track.getPoint(i).isWaypoint())
  				{
  					int px = xPixels[i];
  					int py = yPixels[i];
  					if (px >= 0 && px < winWidth && py >= 0 && py < winHeight)
  					{
  						// Figure out where to draw waypoint name so it doesn't obscure track
  						String waypointName = _track.getPoint(i).getWaypointName();
  						int nameWidth = fm.stringWidth(waypointName);
  						boolean drawnName = false;
  						// Make arrays for coordinates right left up down
  						nameXs[0] = px + 2; nameXs[1] = px - nameWidth - 2;
  						nameXs[2] = nameXs[3] = px - nameWidth/2;
  						nameYs[0] = nameYs[1] = py + (nameHeight/2);
  						nameYs[2] = py - 2; nameYs[3] = py + nameHeight + 2;
  						for (int extraSpace = 0; extraSpace < numNameSteps && !drawnName; extraSpace++)
  						{
  							// Shift arrays for coordinates right left up down
  							nameXs[0] += 3; nameXs[1] -= 3;
  							nameYs[2] -= 3; nameYs[3] += 3;
  							// Check each direction in turn right left up down
  							for (int a=0; a<4; a++)
  							{
  								if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < winWidth
  									&& nameYs[a] < winHeight && (nameYs[a] - nameHeight) > 0
  									&& !MapUtils.overlapsPoints(_mapImage, nameXs[a], nameYs[a], nameWidth, nameHeight, textColour))
  								{
  									// Found a rectangle to fit - draw name here and quit
  									inG.drawString(waypointName, nameXs[a], nameYs[a]);
  									drawnName = true;
  									break;
  								}
  							}
  						}
  					}
  				}
  			}
  		}
  		// Loop over points, drawing blobs for photo / audio points
  		inG.setColor(secondColour);
  		for (int i=0; i<_track.getNumPoints(); i++)
  		{
  			if (_track.getPoint(i).hasMedia())
  			{
  				int px = xPixels[i];
  				int py = yPixels[i];
  				if (px >= 0 && px < winWidth && py >= 0 && py < winHeight)
  				{
  					inG.drawRect(px-1, py-1, 2, 2);
  					inG.drawRect(px-2, py-2, 4, 4);
  					pointsPainted = true;
  				}
  			}
  		}
  
  		// Draw selected range
  		if (_selection.hasRangeSelected())
  		{
  			inG.setColor(rangeColour);
  			for (int i=_selection.getStart(); i<=_selection.getEnd(); i++)
  			{
  				int px = xPixels[i];
  				int py = yPixels[i];
  				inG.drawRect(px-1, py-1, 2, 2);
  			}
  		}
  
  		// Draw crosshairs at selected point
  		int selectedPoint = _selection.getCurrentPointIndex();
  		if (selectedPoint >= 0)
  		{
  			int px = xPixels[selectedPoint];
  			int py = yPixels[selectedPoint];
  			inG.setColor(currentColour);
  			// crosshairs
  			inG.drawLine(px, 0, px, winHeight);
  			inG.drawLine(0, py, winWidth, py);
  		}
  		// Return the number of points painted
  		return pointsPainted;
  	}
  
  	/**
  	 * Wrap the given pixel value if appropriate and possible
  	 * @param inPx Pixel x coordinate
  	 * @param inWinWidth window width in pixels
  	 * @param inZoom zoom level
  	 * @return modified pixel x coordinate
  	 */
  	private static int wrapLongitudeValue(int inPx, int inWinWidth, int inZoom)
  	{
  		if (inPx > inWinWidth)
  		{
  			// Pixel is too far right, could we wrap it back onto the screen?
  			int px = inPx;
  			while (px > inWinWidth) {
  				px -= (256 << inZoom);
  			}
  			if (px >= 0) {
  				return px; // successfully wrapped back onto the screen
  			}
  		}
  		else if (inPx < 0)
  		{
  			// Pixel is too far left, could we wrap it back onto the screen?
  			int px = inPx;
  			while (px < 0) {
  				px += (256 << inZoom);
  			}
  			if (px < inWinWidth) {
  				return px; // successfully wrapped back onto the screen
  			}
  		}
  		// Either it's already on the screen or couldn't be wrapped
  		return inPx;
  	}
  
  	/**
  	 * Draw the lines while dragging a point
  	 * @param inG graphics object
  	 * @param inPrevIndex index of point to draw from
  	 * @param inNextIndex index of point to draw to
  	 */
  	private void drawDragLines(Graphics inG, int inPrevIndex, int inNextIndex)
  	{
  		inG.setColor(_app.getConfig().getColourScheme().getColour(ColourScheme.IDX_POINT));
  		// line from prev point to cursor
  		if ((inPrevIndex == inNextIndex) || (inPrevIndex > -1 && !_track.getPoint(inPrevIndex+1).getSegmentStart()))
  		{
  			final int px = getWidth() / 2 + (int) (_mapPosition.getXFromCentre(_track.getX(inPrevIndex)) / _lastScale);
  			final int py = getHeight() / 2 + (int) (_mapPosition.getYFromCentre(_track.getY(inPrevIndex)) / _lastScale);
  			inG.drawLine(px, py, _dragToX, _dragToY);
  		}
  		if (inNextIndex < _track.getNumPoints() && !_track.getPoint(inNextIndex).getSegmentStart())
  		{
  			final int px = getWidth() / 2 + (int) (_mapPosition.getXFromCentre(_track.getX(inNextIndex)) / _lastScale);
  			final int py = getHeight() / 2 + (int) (_mapPosition.getYFromCentre(_track.getY(inNextIndex)) / _lastScale);
  			inG.drawLine(px, py, _dragToX, _dragToY);
  		}
  	}
  
  	/**
  	 * Inform that tiles have been updated and the map can be repainted
  	 * @param inIsOk true if data loaded ok, false for error
  	 */
  	public void tilesUpdated(boolean inIsOk)
  	{
  		_recalculate = true;
  		repaint();
  	}
  
  	/**
  	 * Inform that a cache failure occurred
  	 */
  	public void reportCacheFailure()
  	{
  		// Cache can't be used, so disable it - user will be reminded to set it up by the tips
  		_app.getConfig().setConfigString(Config.KEY_DISK_CACHE, null);
  	}
  
  	/**
  	 * Zoom out, if not already at minimum zoom
  	 */
  	public final void zoomOut()
  	{
  		_mapPosition.zoomOut();
  		_recalculate = true;
  		repaint();
  		showZoomLevel();
  	}
  
  	/**
  	 * Zoom in, if not already at maximum zoom
  	 */
  	public final void zoomIn()
  	{
  		// See if selected point is currently visible, if so (and autopan on) then autopan after zoom to keep it visible
  		boolean wasVisible = _autopanCheckBox.isSelected() && isCurrentPointVisible();
  		_mapPosition.zoomIn();
  		if (wasVisible && !isCurrentPointVisible()) {
  			autopanToPoint(_selection.getCurrentPointIndex());
  		}
  		_recalculate = true;
  		repaint();
  		showZoomLevel();
  	}
  
  	private void showZoomLevel()
  	{
  		if (_app.getConfig().getConfigBoolean(Config.KEY_SHOW_ZOOMLEVEL)) {
  			UpdateMessageBroker.informSubscribers(I18nManager.getText("display.zoomlevel") + ": " + _mapPosition.getZoom());
  		}
  	}
  
  	/**
  	 * Pan map
  	 * @param inDeltaX x shift
  	 * @param inDeltaY y shift
  	 */
  	public void panMap(int inDeltaX, int inDeltaY)
  	{
  		_mapPosition.pan(inDeltaX, inDeltaY);
  		_recalculate = true;
  		repaint();
  	}
  
  	/**
  	 * Create a DataPoint object from the given click coordinates
  	 * @param inX x coordinate of click
  	 * @param inY y coordinate of click
  	 * @param inNewSegment true to start a new segment, false to continue
  	 * @return DataPoint with given coordinates and no altitude, or null if values are invalid
  	 */
  	private DataPoint createPointFromClick(int inX, int inY, boolean inNewSegment)
  	{
  		double lat = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(inY, getHeight()));
  		double lon = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(inX, getWidth()));
  		Coordinate latitude = Latitude.make(lat);
  		Coordinate longitude = Longitude.make(lon);
  		if (latitude != null && longitude != null)
  		{
  			DataPoint point = new DataPoint(Latitude.make(lat), Longitude.make(lon));
  			point.setSegmentStart(inNewSegment);
  			return point;
  		}
  		return null;
  	}
  
  	/**
  	 * Move a DataPoint object to the given mouse coordinates
  	 * @param startX start x coordinate of mouse
  	 * @param startY start y coordinate of mouse
  	 * @param endX end x coordinate of mouse
  	 * @param endY end y coordinate of mouse
  	 */
  	private void movePointToMouse(int startX, int startY, int endX, int endY )
  	{
  		double lat1 = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(startY, getHeight()));
  		double lon1 = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(startX, getWidth()));
  		double lat_delta = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(endY, getHeight())) - lat1;
  		double lon_delta = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(endX, getWidth())) - lon1;
  
  		DataPoint point = _trackInfo.getCurrentPoint();
  		if (point == null) {
  			return;
  		}
  
  		final String latitude = String.format("%.12f", point.getLatitude().getDouble() + lat_delta);
  		final String longitude = String.format("%.12f", point.getLongitude().getDouble() + lon_delta);
  		List<FieldEdit> edits = List.of(new FieldEdit(Field.LATITUDE, latitude),
  			new FieldEdit(Field.LONGITUDE, longitude));
  		int pointIndex = _app.getTrackInfo().getSelection().getCurrentPointIndex();
  		EditPointCmd command = new EditPointCmd(pointIndex, edits);
  		Describer undoDescriber = new Describer("undo.editpoint", "undo.editpoint.withname");
  		command.setDescription(undoDescriber.getDescriptionWithNameOrNot(point.getWaypointName()));
  		command.setConfirmText(I18nManager.getText("confirm.point.edit"));
  		_app.execute(command);
  	}
  
  
  	/**
  	 * @see javax.swing.JComponent#getMinimumSize()
  	 */
  	public Dimension getMinimumSize() {
  		return new Dimension(512, 300);
  	}
  
  	/**
  	 * @see javax.swing.JComponent#getPreferredSize()
  	 */
  	public Dimension getPreferredSize() {
  		return getMinimumSize();
  	}
  
  
  	/**
  	 * Respond to mouse click events
  	 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
  	 */
  	public void mouseClicked(MouseEvent inE)
  	{
  		final boolean showMap = _app.getConfig().getConfigBoolean(Config.KEY_SHOW_MAP);
  		final boolean hasPoints = _track != null && _track.getNumPoints() > 0;
  		if (showMap || hasPoints)
  		{
  			// select point if it's a left-click
  			if (!inE.isMetaDown())
  			{
  				if (inE.getClickCount() == 1)
  				{
  					// single left click
  					if (_drawMode == DrawMode.DEFAULT && hasPoints)
  					{
  						int pointIndex = _clickedPoint;
  						if (pointIndex == INDEX_UNKNOWN)
  						{
  							// index hasn't been calculated yet
  							pointIndex = _track.getNearestPointIndex(
  							 _mapPosition.getXFromPixels(inE.getX(), getWidth()),
  							 _mapPosition.getYFromPixels(inE.getY(), getHeight()),
  							 _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
  						}
  						// Extend selection for shift-click
  						if (inE.isShiftDown()) {
  							_trackInfo.extendSelection(pointIndex);
  						}
  						else {
  							_trackInfo.selectPoint(pointIndex);
  						}
  					}
  					else if (_drawMode == DrawMode.DRAW_POINTS_START)
  					{
  						DataPoint point = createPointFromClick(inE.getX(), inE.getY(), true);
  						insertPoint(point, -1);
  						_dragToX = inE.getX();
  						_dragToY = inE.getY();
  						_drawMode = DrawMode.DRAW_POINTS_CONT;
  					}
  					else if (_drawMode == DrawMode.DRAW_POINTS_CONT)
  					{
  						DataPoint point = createPointFromClick(inE.getX(), inE.getY(), false);
  						insertPoint(point, -1); // append
  					}
  				}
  				else if (inE.getClickCount() == 2)
  				{
  					// double click
  					if (_drawMode == DrawMode.DEFAULT) {
  						panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
  						zoomIn();
  					}
  					else if (_drawMode == DrawMode.DRAW_POINTS_START || _drawMode == DrawMode.DRAW_POINTS_CONT) {
  						_drawMode = DrawMode.DEFAULT;
  					}
  				}
  			}
  			else
  			{
  				// show the popup menu for right-clicks
  				_popupMenuX = inE.getX();
  				_popupMenuY = inE.getY();
  				_popup.show(this, _popupMenuX, _popupMenuY);
  			}
  		}
  		// Reset app mode
  		_app.setCurrentMode(App.AppMode.NORMAL);
  		if (_drawMode == DrawMode.MARK_RECTANGLE_INSIDE || _drawMode == DrawMode.MARK_RECTANGLE_OUTSIDE) {
  			_drawMode = DrawMode.DEFAULT;
  		}
  	}
  
  	/**
  	 * Insert the given point into the track
  	 * @param inPoint point to insert
  	 * @param inInsertIndex index of insertion, or -1 for append
  	 */
  	private void insertPoint(DataPoint inPoint, int inInsertIndex)
  	{
  		if (inInsertIndex < -1 || inPoint == null) {
  			return;
  		}
  		InsertPointCmd command = new InsertPointCmd(inPoint, inInsertIndex);
  		command.setDescription(I18nManager.getText("undo.createpoint"));
  		command.setConfirmText(I18nManager.getText("confirm.createpoint"));
  		_app.execute(command);
  	}
  
  
  	/**
  	 * Ignore mouse enter events
  	 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
  	 */
  	public void mouseEntered(MouseEvent inE) {
  		// ignore
  	}
  
  	/**
  	 * Ignore mouse exited events
  	 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
  	 */
  	public void mouseExited(MouseEvent inE) {
  		// ignore
  	}
  
  	/**
  	 * React to mouse pressed events to initiate a point drag
  	 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
  	 */
  	public void mousePressed(MouseEvent inE)
  	{
  		_clickedPoint = INDEX_UNKNOWN;
  		if (_track == null || _track.getNumPoints() <= 0) {
  			return;
  		}
  		if (inE.isMetaDown()) {
  			return; // right-press ignored
  		}
  		// Left mouse drag - check if point is near; if so select it for dragging
  		if (_drawMode == DrawMode.DEFAULT)
  		{
  			/* Drag points if edit mode is enabled OR ALT is pressed */
  			if (_editmodeCheckBox.isSelected() || inE.isAltDown() || inE.isAltGraphDown())
  			{
  				final double clickX = _mapPosition.getXFromPixels(inE.getX(), getWidth());
  				final double clickY = _mapPosition.getYFromPixels(inE.getY(), getHeight());
  				final double clickSens = _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY);
  				_clickedPoint = _track.getNearestPointIndex(clickX, clickY, clickSens, false);
  
  				if (_clickedPoint >= 0)
  				{
  					// TODO: maybe use another color of the cross or remove the cross while dragging???
  
  					_trackInfo.selectPoint(_clickedPoint);
  					if (_trackInfo.getCurrentPoint() != null)
  					{
  						_drawMode = DrawMode.DRAG_POINT;
  						_dragFromX = _dragToX = inE.getX();
  						_dragFromY = _dragToY = inE.getY();
  					}
  				}
  				else
  				{
  					// Not a click on a point, so check half-way between two (connected) trackpoints
  					int midpointIndex = _midpoints.getNearestPointIndex(clickX, clickY, clickSens);
  					if (midpointIndex > 0)
  					{
  						_drawMode = DrawMode.CREATE_MIDPOINT;
  						_clickedPoint = midpointIndex;
  						_dragFromX = _dragToX = inE.getX();
  						_dragFromY = _dragToY = inE.getY();
  					}
  				}
  			}
  		}
  	}
  
  	/**
  	 * Respond to mouse released events
  	 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
  	 */
  	public void mouseReleased(MouseEvent inE)
  	{
  		_recalculate = true;
  
  		if (_drawMode == DrawMode.DRAG_POINT)
  		{
  			if (inE.isMetaDown()) {
  				return;
  			}
  			if (Math.abs(_dragToX - _dragFromX) > 2
  				|| Math.abs(_dragToY - _dragFromY) > 2)
  			{
  				movePointToMouse(_dragFromX, _dragFromY, _dragToX, _dragToY );
  			}
  			_drawMode = DrawMode.DEFAULT;
  		}
  		else if (_drawMode == DrawMode.CREATE_MIDPOINT)
  		{
  			if (inE.isMetaDown()) {
  				return;
  			}
  			_drawMode = DrawMode.DEFAULT;
  			insertPoint(createPointFromClick(_dragToX, _dragToY, false), _clickedPoint);
  		}
  		else if (_drawMode == DrawMode.ZOOM_RECT)
  		{
  			if (Math.abs(_dragToX - _dragFromX) > 20
  			 && Math.abs(_dragToY - _dragFromY) > 20)
  			{
  				_mapPosition.zoomToPixels(_dragFromX, _dragToX, _dragFromY, _dragToY, getWidth(), getHeight());
  				showZoomLevel();
  			}
  			_drawMode = DrawMode.DEFAULT;
  		}
  		else if (_drawMode == DrawMode.MARK_RECTANGLE_INSIDE || _drawMode == DrawMode.MARK_RECTANGLE_OUTSIDE)
  		{
  			if (inE.isMetaDown()) {
  				return;
  			}
  			final boolean markInside = _drawMode == DrawMode.MARK_RECTANGLE_INSIDE;
  			// Reset app mode
  			_app.setCurrentMode(App.AppMode.NORMAL);
  			_drawMode = DrawMode.DEFAULT;
  			// Call a function to mark the points
  			double lon1 = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_dragFromX, getWidth()));
  			double lat1 = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_dragFromY, getHeight()));
  			double lon2 = MapUtils.getLongitudeFromX(_mapPosition.getXFromPixels(_dragToX, getWidth()));
  			double lat2 = MapUtils.getLatitudeFromY(_mapPosition.getYFromPixels(_dragToY, getHeight()));
  			// Invalidate rectangle if pixel coords are (-1,-1)
  			if (_dragFromX < 0 || _dragFromY < 0) {
  				lon1 = lon2;
  				lat1 = lat2;
  			}
  			MarkPointsInRectangleFunction marker = getMarkRectangleFunction(markInside);
  			marker.setRectCoords(lon1, lat1, lon2, lat2);
  			marker.begin();
  		}
  		_dragFromX = _dragFromY = -1;
  		repaint();
  	}
  
  	/** Get the appropriate function from the library instead of creating a new one */
  	private MarkPointsInRectangleFunction getMarkRectangleFunction(boolean inMarkInside)
  	{
  		GenericFunction function = inMarkInside ? FunctionLibrary.FUNCTION_MARK_INSIDE_RECTANGLE
  			: FunctionLibrary.FUNCTION_MARK_OUTSIDE_RECTANGLE;
  		return (MarkPointsInRectangleFunction) function;
  	}
  
  
  	/**
  	 * Respond to mouse drag events
  	 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
  	 */
  	public void mouseDragged(MouseEvent inE)
  	{
  		// Note: One would expect inE.isMetaDown() to give information about whether this is a
  		//       drag with the right mouse button or not - but since java 9 this is buggy,
  		//       so we use the beautifully-named getModifiersEx() instead.
  		//       And logically BUTTON3 refers to the secondary mouse button, not the tertiary one!
  		final boolean isRightDrag = (inE.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
  		if (isRightDrag)
  		{
  			// Right-click and drag - update rectangle
  			_drawMode = DrawMode.ZOOM_RECT;
  			if (_dragFromX == -1) {
  				_dragFromX = inE.getX();
  				_dragFromY = inE.getY();
  			}
  			_dragToX = inE.getX();
  			_dragToY = inE.getY();
  			repaint();
  		}
  		else
  		{
  			// Left mouse drag - decide whether to drag the point, drag the
  			// marking rectangle or pan the map
  			if (_drawMode == DrawMode.DRAG_POINT || _drawMode == DrawMode.CREATE_MIDPOINT)
  			{
  				// move point
  				_dragToX = inE.getX();
  				_dragToY = inE.getY();
  				_recalculate = true;
  				repaint();
  			}
  			else if (_drawMode == DrawMode.MARK_RECTANGLE_INSIDE || _drawMode == DrawMode.MARK_RECTANGLE_OUTSIDE)
  			{
  				// draw a rectangle for marking points
  				if (_dragFromX == -1) {
  					_dragFromX = inE.getX();
  					_dragFromY = inE.getY();
  				}
  				_dragToX = inE.getX();
  				_dragToY = inE.getY();
  				repaint();
  			}
  			else
  			{
  				// regular left-drag pans map by appropriate amount
  				if (_dragFromX != -1)
  				{
  					panMap(_dragFromX - inE.getX(), _dragFromY - inE.getY());
  				}
  				_dragFromX = _dragToX = inE.getX();
  				_dragFromY = _dragToY = inE.getY();
  			}
  		}
  	}
  
  	/**
  	 * Respond to mouse move events without button pressed
  	 * @param inEvent ignored
  	 */
  	public void mouseMoved(MouseEvent inEvent)
  	{
  		boolean useCrosshairs = false;
  		boolean useResize     = false;
  		// Ignore unless we're drawing points
  		if (_drawMode == DrawMode.DRAW_POINTS_CONT)
  		{
  			_dragToX = inEvent.getX();
  			_dragToY = inEvent.getY();
  			repaint();
  		}
  		else if (_drawMode == DrawMode.MARK_RECTANGLE_INSIDE || _drawMode == DrawMode.MARK_RECTANGLE_OUTSIDE) {
  			useResize = true;
  		}
  		else if ((_editmodeCheckBox.isSelected() || inEvent.isAltDown() || inEvent.isAltGraphDown())
  				&& _track.getNumPoints() > 0)
  		{
  			// Try to find a point or a midpoint at this location, and if there is one
  			// then change the cursor to crosshairs
  			final double clickX = _mapPosition.getXFromPixels(inEvent.getX(), getWidth());
  			final double clickY = _mapPosition.getYFromPixels(inEvent.getY(), getHeight());
  			final double clickSens = _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY);
  			useCrosshairs = (_track.getNearestPointIndex(clickX, clickY, clickSens, false) >= 0
  				|| _midpoints.getNearestPointIndex(clickX, clickY, clickSens) >= 0
  			);
  		}
  		if (useCrosshairs && !isCursorSet()) {
  			setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
  		}
  		else if (useResize && !isCursorSet()) {
  			setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
  		}
  		else if (!useCrosshairs && !useResize && isCursorSet()) {
  			setCursor(null);
  		}
  	}
  
  	/**
  	 * Respond to status bar message from broker
  	 * @param inMessage message, ignored
  	 */
  	public void actionCompleted(String inMessage) {
  	}
  
  	/**
  	 * Respond to data updated message from broker
  	 * @param inUpdateType type of update
  	 */
  	public void dataUpdated(int inUpdateType)
  	{
  		_recalculate = true;
  		if ((inUpdateType & DataSubscriber.DATA_ADDED_OR_REMOVED) > 0) {
  			_checkBounds = true;
  		}
  		if ((inUpdateType & DataSubscriber.MAPSERVER_CHANGED) > 0)
  		{
  			// Get the selected map source index and pass to tile manager
  			Config config = _app.getConfig();
  			_tileManager.setMapSource(config.getConfigInt(Config.KEY_MAPSOURCE_INDEX));
  			_waypointIconDefinition = WaypointIcons.getDefinition(config, _app.getIconManager());
  		}
  		if ((inUpdateType & (DataSubscriber.DATA_ADDED_OR_REMOVED + DataSubscriber.DATA_EDITED)) > 0) {
  			_midpoints.updateData(_track);
  		}
  		// See if rect mode has been activated
  		final App.AppMode appMode = _app.getCurrentMode();
  		if (appMode == App.AppMode.DRAWRECT_INSIDE || appMode == App.AppMode.DRAWRECT_OUTSIDE)
  		{
  			_drawMode = appMode == App.AppMode.DRAWRECT_INSIDE ? DrawMode.MARK_RECTANGLE_INSIDE : DrawMode.MARK_RECTANGLE_OUTSIDE;
  			if (!isCursorSet()) {
  				setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
  			}
  		}
  		repaint();
  		// grab focus for the key presses
  		this.requestFocus();
  	}
  
  	/**
  	 * Respond to key presses on the map canvas
  	 * @param inE key event
  	 */
  	public void keyPressed(KeyEvent inE)
  	{
  		int code = inE.getKeyCode();
  		int currPointIndex = _selection.getCurrentPointIndex();
  		// Check for Ctrl key (for Linux/Win) or meta key (Clover key for Mac)
  		if (inE.isControlDown() || inE.isMetaDown())
  		{
  			// Shift as well makes things faster
  			final int pointIncrement = inE.isShiftDown()?3:1;
  			// Check for arrow keys to zoom in and out
  			if (code == KeyEvent.VK_UP)
  				zoomIn();
  			else if (code == KeyEvent.VK_DOWN)
  				zoomOut();
  			// Key nav for next/prev point
  			else if (code == KeyEvent.VK_LEFT && currPointIndex > 0)
  				_trackInfo.incrementPointIndex(-pointIncrement);
  			else if (code == KeyEvent.VK_RIGHT)
  				_trackInfo.incrementPointIndex(pointIncrement);
  			else if (code == KeyEvent.VK_PAGE_UP)
  				_trackInfo.selectPoint(Checker.getPreviousSegmentStart(
  					_trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
  			else if (code == KeyEvent.VK_PAGE_DOWN)
  				_trackInfo.selectPoint(Checker.getNextSegmentStart(
  					_trackInfo.getTrack(), _trackInfo.getSelection().getCurrentPointIndex()));
  			// Check for home and end
  			else if (code == KeyEvent.VK_HOME)
  				_trackInfo.selectPoint(0);
  			else if (code == KeyEvent.VK_END)
  				_trackInfo.selectPoint(_trackInfo.getTrack().getNumPoints()-1);
  		}
  		else
  		{
  			// Check for arrow keys to pan
  			int upwardsPan = 0;
  			if (code == KeyEvent.VK_UP)
  				upwardsPan = -PAN_DISTANCE;
  			else if (code == KeyEvent.VK_DOWN)
  				upwardsPan = PAN_DISTANCE;
  			int rightwardsPan = 0;
  			if (code == KeyEvent.VK_RIGHT)
  				rightwardsPan = PAN_DISTANCE;
  			else if (code == KeyEvent.VK_LEFT)
  				rightwardsPan = -PAN_DISTANCE;
  			panMap(rightwardsPan, upwardsPan);
  			// Check for escape
  			if (code == KeyEvent.VK_ESCAPE) {
  				_drawMode = DrawMode.DEFAULT;
  			}
  			// Check for backspace key to delete current point (delete key already handled by menu)
  			else if (code == KeyEvent.VK_BACK_SPACE && currPointIndex >= 0) {
  				new DeleteCurrentPoint(_app).begin();
  			}
  			// Check for home and end (without Ctrl)
  			else if (code == KeyEvent.VK_HOME && _trackInfo.getSelection().hasRangeSelected()) {
  				_trackInfo.selectPoint(_trackInfo.getSelection().getStart());
  			}
  			else if (code == KeyEvent.VK_END && _trackInfo.getSelection().hasRangeSelected()) {
  				_trackInfo.selectPoint(_trackInfo.getSelection().getEnd());
  			}
  		}
  	}
  
  	/**
  	 * @param e key released event, ignored
  	 */
  	public void keyReleased(KeyEvent e) {
  	}
  
  	/**
  	 * @param inE key typed event, ignored
  	 */
  	public void keyTyped(KeyEvent inE) {
  	}
  
  	/**
  	 * @param inE mouse wheel event indicating scroll direction
  	 */
  	public void mouseWheelMoved(MouseWheelEvent inE)
  	{
  		final int clicks = inE.getWheelRotation();
  		final int deltaX = (inE.getX() - getWidth()/2) / 2;
  		final int deltaY = (inE.getY() - getHeight()/2) / 2;
  		if (clicks < 0) {
  			panMap(deltaX, deltaY);
  			zoomIn();
  		}
  		else if (clicks > 0) {
  			panMap(-deltaX, -deltaY);
  			zoomOut();
  		}
  	}
  
  	private int getScaledHeight() {
  		final int normalHeight = super.getHeight();
  		return (int) (normalHeight * _lastScale);
  	}
  
  	private int getScaledWidth() {
  		final int normalWidth = super.getWidth();
  		return (int) (normalWidth * _lastScale);
  	}
  
  	public double getMinYValue() {return _mapPosition.getYFromPixels(getHeight(), getHeight());}
  	public double getMaxYValue() {return _mapPosition.getYFromPixels(0, getHeight());}
  	public double getMinXValue() {return _mapPosition.getXFromPixels(0, getWidth());}
  	public double getMaxXValue() {return _mapPosition.getXFromPixels(getWidth(), getWidth());}
  }
  