// License: GPL. For details, see Readme.txt file.
package org.openstreetmap.gui.jmapviewer;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent;
import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent.COMMAND;
import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener;
import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
import org.openstreetmap.gui.jmapviewer.interfaces.MapPolygon;
import org.openstreetmap.gui.jmapviewer.interfaces.MapRectangle;
import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;

import course_generator.utils.CgLog;

/**
 * Provides a simple panel that displays pre-rendered map tiles loaded from the
 * OpenStreetMap project.
 *
 * @author Jan Peter Stotz
 *
 */
public class JMapViewer extends JPanel implements TileLoaderListener {

	private static final long serialVersionUID = 1L;

	public static boolean debug = false;

	/**
	 * Vectors for clock-wise tile painting
	 */
	protected static final Point[] move = { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) };

	public static final int MAX_ZOOM = 22;
	public static final int MIN_ZOOM = 0;

	protected List<MapMarker> mapMarkerList;
	protected List<MapRectangle> mapRectangleList;
	protected List<MapPolygon> mapPolygonList;

	protected boolean mapMarkersVisible;
	protected boolean mapRectanglesVisible;
	protected boolean mapPolygonsVisible;

	protected boolean tileGridVisible;
	protected boolean scrollWrapEnabled;

	protected TileController tileController;

	/**
	 * x- and y-position of the center of this map-panel on the world map denoted in
	 * screen pixel regarding the current zoom level.
	 */
	protected Point center;

	/**
	 * Current zoom level
	 */
	protected int zoom;

	protected JSlider zoomSlider;
	protected JButton zoomInButton;
	protected JButton zoomOutButton;

	public static enum ZOOM_BUTTON_STYLE {
		HORIZONTAL, VERTICAL
	}

	protected ZOOM_BUTTON_STYLE zoomButtonStyle;

	protected TileSource tileSource;

	protected AttributionSupport attribution = new AttributionSupport();

	/**
	 * Creates a standard {@link JMapViewer} instance that can be controlled via
	 * mouse: hold right mouse button for moving, double click left mouse button or
	 * use mouse wheel for zooming. Loaded tiles are stored the
	 * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for
	 * retrieving the tiles.
	 */
	public JMapViewer() {
		this(new MemoryTileCache(), 8);
		new DefaultMapController(this);
	}

	public JMapViewer(TileCache tileCache, int downloadThreadCount) {
		super();
		JobDispatcher.setMaxWorkers(downloadThreadCount);
		tileSource = new OsmTileSource.Mapnik();
		tileController = new TileController(tileSource, tileCache, this);
		mapMarkerList = new LinkedList<>();
		mapPolygonList = new LinkedList<>();
		mapRectangleList = new LinkedList<>();
		mapMarkersVisible = true;
		mapRectanglesVisible = true;
		mapPolygonsVisible = true;
		tileGridVisible = false;
		setLayout(null);
		initializeZoomSlider();
		setMinimumSize(new Dimension(tileSource.getTileSize(), tileSource.getTileSize()));
		setPreferredSize(new Dimension(400, 400));
		setDisplayPosition(new Coordinate(50, 9), 3);
		// setToolTipText("");
	}

	@Override
	public String getToolTipText(MouseEvent event) {
		// Point screenPoint = event.getLocationOnScreen();
		// Coordinate c = getPosition(screenPoint);
		return super.getToolTipText(event);
	}

	protected void initializeZoomSlider() {
		zoomSlider = new JSlider(MIN_ZOOM, tileController.getTileSource().getMaxZoom());
		zoomSlider.setOrientation(JSlider.VERTICAL);
		zoomSlider.setBounds(10, 10, 30, 150);
		zoomSlider.setOpaque(false);
		zoomSlider.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent e) {
				setZoom(zoomSlider.getValue());
			}
		});
		zoomSlider.setFocusable(false);
		add(zoomSlider);
		int size = 18;
		try {
			ImageIcon icon = new ImageIcon(JMapViewer.class.getResource("images/plus.png"));
			zoomInButton = new JButton(icon);
		} catch (Exception e) {
			zoomInButton = new JButton("+");
			zoomInButton.setFont(new Font("sansserif", Font.BOLD, 9));
			zoomInButton.setMargin(new Insets(0, 0, 0, 0));
		}
		zoomInButton.setBounds(4, 155, size, size);
		zoomInButton.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				zoomIn();
			}
		});
		zoomInButton.setFocusable(false);
		add(zoomInButton);
		try {
			ImageIcon icon = new ImageIcon(JMapViewer.class.getResource("images/minus.png"));
			zoomOutButton = new JButton(icon);
		} catch (Exception e) {
			zoomOutButton = new JButton("-");
			zoomOutButton.setFont(new Font("sansserif", Font.BOLD, 9));
			zoomOutButton.setMargin(new Insets(0, 0, 0, 0));
		}
		zoomOutButton.setBounds(8 + size, 155, size, size);
		zoomOutButton.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				zoomOut();
			}
		});
		zoomOutButton.setFocusable(false);
		add(zoomOutButton);
	}

	/**
	 * Changes the map pane so that it is centered on the specified coordinate at
	 * the given zoom level.
	 *
	 * @param to   specified coordinate
	 * @param zoom {@link #MIN_ZOOM} &lt;= zoom level &lt;= {@link #MAX_ZOOM}
	 */
	public void setDisplayPosition(Coordinate to, int zoom) {
		setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), to, zoom);
	}

	/**
	 * Changes the map pane so that the specified coordinate at the given zoom level
	 * is displayed on the map at the screen coordinate <code>mapPoint</code>.
	 *
	 * @param mapPoint point on the map denoted in pixels where the coordinate
	 *                 should be set
	 * @param to       specified coordinate
	 * @param zoom     {@link #MIN_ZOOM} &lt;= zoom level &lt;=
	 *                 {@link TileSource#getMaxZoom()}
	 */
	public void setDisplayPosition(Point mapPoint, Coordinate to, int zoom) {
		int x = tileSource.LonToX(to.getLon(), zoom);
		int y = tileSource.LatToY(to.getLat(), zoom);
		setDisplayPosition(mapPoint, x, y, zoom);
	}

	public void setDisplayPosition(int x, int y, int zoom) {
		setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), x, y, zoom);
	}

	public void setDisplayPosition(Point mapPoint, int x, int y, int zoom) {
		if (zoom > tileController.getTileSource().getMaxZoom() || zoom < MIN_ZOOM)
			return;

		// Get the plain tile number
		Point p = new Point();
		p.x = x - mapPoint.x + getWidth() / 2;
		p.y = y - mapPoint.y + getHeight() / 2;
		center = p;
		setIgnoreRepaint(true);
		try {
			int oldZoom = this.zoom;
			this.zoom = zoom;
			if (oldZoom != zoom) {
				zoomChanged(oldZoom);
			}
			if (zoomSlider.getValue() != zoom) {
				zoomSlider.setValue(zoom);
			}
		} finally {
			setIgnoreRepaint(false);
			repaint();
		}
	}

	/**
	 * Sets the displayed map pane and zoom level so that all chosen map elements
	 * are visible.
	 */
	public void setDisplayToFitMapElements(boolean markers, boolean rectangles, boolean polygons) {
		int nbElemToCheck = 0;
		if (markers && mapMarkerList != null)
			nbElemToCheck += mapMarkerList.size();
		if (rectangles && mapRectangleList != null)
			nbElemToCheck += mapRectangleList.size();
		if (polygons && mapPolygonList != null)
			nbElemToCheck += mapPolygonList.size();
		if (nbElemToCheck == 0)
			return;

		int x_min = Integer.MAX_VALUE;
		int y_min = Integer.MAX_VALUE;
		int x_max = Integer.MIN_VALUE;
		int y_max = Integer.MIN_VALUE;
		int mapZoomMax = tileController.getTileSource().getMaxZoom();

		if (markers) {
			for (MapMarker marker : mapMarkerList) {
				if (marker.isVisible()) {
					int x = tileSource.LonToX(marker.getLon(), mapZoomMax);
					int y = tileSource.LatToY(marker.getLat(), mapZoomMax);
					x_max = Math.max(x_max, x);
					y_max = Math.max(y_max, y);
					x_min = Math.min(x_min, x);
					y_min = Math.min(y_min, y);
				}
			}
		}

		if (rectangles) {
			for (MapRectangle rectangle : mapRectangleList) {
				if (rectangle.isVisible()) {
					x_max = Math.max(x_max, tileSource.LonToX(rectangle.getBottomRight().getLon(), mapZoomMax));
					y_max = Math.max(y_max, tileSource.LatToY(rectangle.getTopLeft().getLat(), mapZoomMax));
					x_min = Math.min(x_min, tileSource.LonToX(rectangle.getTopLeft().getLon(), mapZoomMax));
					y_min = Math.min(y_min, tileSource.LatToY(rectangle.getBottomRight().getLat(), mapZoomMax));
				}
			}
		}

		if (polygons) {
			for (MapPolygon polygon : mapPolygonList) {
				if (polygon.isVisible()) {
					for (ICoordinate c : polygon.getPoints()) {
						int x = tileSource.LonToX(c.getLon(), mapZoomMax);
						int y = tileSource.LatToY(c.getLat(), mapZoomMax);
						x_max = Math.max(x_max, x);
						y_max = Math.max(y_max, y);
						x_min = Math.min(x_min, x);
						y_min = Math.min(y_min, y);
					}
				}
			}
		}

		int height = Math.max(0, getHeight());
		int width = Math.max(0, getWidth());
		int newZoom = mapZoomMax;
		int x = x_max - x_min;
		int y = y_max - y_min;
		while (x > width || y > height) {
			newZoom--;
			x >>= 1;
			y >>= 1;
		}
		x = x_min + (x_max - x_min) / 2;
		y = y_min + (y_max - y_min) / 2;
		int z = 1 << (mapZoomMax - newZoom);
		x /= z;
		y /= z;
		setDisplayPosition(x, y, newZoom);
	}

	/**
	 * Sets the displayed map pane and zoom level so that all map markers are
	 * visible.
	 */
	public void setDisplayToFitMapMarkers() {
		setDisplayToFitMapElements(true, false, false);
	}

	/**
	 * Sets the displayed map pane and zoom level so that all map rectangles are
	 * visible.
	 */
	public void setDisplayToFitMapRectangles() {
		setDisplayToFitMapElements(false, true, false);
	}

	/**
	 * Sets the displayed map pane and zoom level so that all map polygons are
	 * visible.
	 */
	public void setDisplayToFitMapPolygons() {
		setDisplayToFitMapElements(false, false, true);
	}

	/**
	 * Sets the displayed map pane and zoom level so that all map polygons are
	 * visible.
	 */
	public void setDisplayToFitMapMarkersAndPolygons() {
		setDisplayToFitMapElements(true, false, true);
	}

	/**
	 * @return the center
	 */
	public Point getCenter() {
		return center;
	}

	/**
	 * @param center the center to set
	 */
	public void setCenter(Point center) {
		this.center = center;
	}

	/**
	 * Calculates the latitude/longitude coordinate of the center of the currently
	 * displayed map area.
	 *
	 * @return latitude / longitude
	 */
	public Coordinate getPosition() {
		double lon = tileSource.XToLon(center.x, zoom);
		double lat = tileSource.YToLat(center.y, zoom);
		return new Coordinate(lat, lon);
	}

	/**
	 * Converts the relative pixel coordinate (regarding the top left corner of the
	 * displayed map) into a latitude / longitude coordinate
	 *
	 * @param mapPoint relative pixel coordinate regarding the top left corner of
	 *                 the displayed map
	 * @return latitude / longitude
	 */
	public Coordinate getPosition(Point mapPoint) {
		return getPosition(mapPoint.x, mapPoint.y);
	}

	/**
	 * Converts the relative pixel coordinate (regarding the top left corner of the
	 * displayed map) into a latitude / longitude coordinate
	 *
	 * @param mapPointX
	 * @param mapPointY
	 * @return latitude / longitude
	 */
	public Coordinate getPosition(int mapPointX, int mapPointY) {
		int x = center.x + mapPointX - getWidth() / 2;
		int y = center.y + mapPointY - getHeight() / 2;
		double lon = tileSource.XToLon(x, zoom);
		double lat = tileSource.YToLat(y, zoom);
		return new Coordinate(lat, lon);
	}

	/**
	 * Calculates the position on the map of a given coordinate
	 *
	 * @param lat
	 * @param lon
	 * @param checkOutside
	 * @return point on the map or <code>null</code> if the point is not visible and
	 *         checkOutside set to <code>true</code>
	 */
	public Point getMapPosition(double lat, double lon, boolean checkOutside) {
		int x = tileSource.LonToX(lon, zoom);
		int y = tileSource.LatToY(lat, zoom);
		x -= center.x - getWidth() / 2;
		y -= center.y - getHeight() / 2;
		if (checkOutside) {
			if (x < 0 || y < 0 || x > getWidth() || y > getHeight())
				return null;
		}
		return new Point(x, y);
	}

	/**
	 * Calculates the position on the map of a given coordinate
	 *
	 * @param lat          Latitude
	 * @param offset       Offset respect Latitude
	 * @param checkOutside
	 * @return Integer the radius in pixels
	 */
	public Integer getLatOffset(double lat, double offset, boolean checkOutside) {
		int y = tileSource.LatToY(lat + offset, zoom);
		y -= center.y - getHeight() / 2;
		if (checkOutside) {
			if (y < 0 || y > getHeight())
				return null;
		}
		return y;
	}

	/**
	 * Calculates the position on the map of a given coordinate
	 *
	 * @param lat
	 * @param lon
	 * @return point on the map or <code>null</code> if the point is not visible
	 */
	public Point getMapPosition(double lat, double lon) {
		return getMapPosition(lat, lon, true);
	}

	/**
	 * Calculates the position on the map of a given coordinate
	 *
	 * @param marker MapMarker object that define the x,y coordinate
	 * @return Integer the radius in pixels
	 */
	public Integer getRadius(MapMarker marker, Point p) {
		if (marker.getMarkerStyle() == MapMarker.STYLE.FIXED)
			return (int) marker.getRadius();
		else if (p != null) {
			Integer radius = getLatOffset(marker.getLat(), marker.getRadius(), false);
			radius = radius == null ? null : p.y - radius.intValue();
			return radius;
		} else
			return null;
	}

	/**
	 * Calculates the position on the map of a given coordinate
	 *
	 * @param coord
	 * @return point on the map or <code>null</code> if the point is not visible
	 */
	public Point getMapPosition(Coordinate coord) {
		if (coord != null)
			return getMapPosition(coord.getLat(), coord.getLon());
		else
			return null;
	}

	/**
	 * Calculates the position on the map of a given coordinate
	 *
	 * @param coord
	 * @return point on the map or <code>null</code> if the point is not visible and
	 *         checkOutside set to <code>true</code>
	 */
	public Point getMapPosition(ICoordinate coord, boolean checkOutside) {
		if (coord != null)
			return getMapPosition(coord.getLat(), coord.getLon(), checkOutside);
		else
			return null;
	}

	/**
	 * Gets the meter per pixel.
	 *
	 * @return the meter per pixel
	 * @author Jason Huntley
	 */
	public double getMeterPerPixel() {
		Point origin = new Point(5, 5);
		Point center = new Point(getWidth() / 2, getHeight() / 2);

		double pDistance = center.distance(origin);

		Coordinate originCoord = getPosition(origin);
		Coordinate centerCoord = getPosition(center);

		double mDistance = tileSource.getDistance(originCoord.getLat(), originCoord.getLon(), centerCoord.getLat(),
				centerCoord.getLon());

		return mDistance / pDistance;
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);

		int iMove = 0;

		int tilesize = tileSource.getTileSize();
		int tilex = center.x / tilesize;
		int tiley = center.y / tilesize;
		int off_x = (center.x % tilesize);
		int off_y = (center.y % tilesize);

		int w2 = getWidth() / 2;
		int h2 = getHeight() / 2;
		int posx = w2 - off_x;
		int posy = h2 - off_y;

		int diff_left = off_x;
		int diff_right = tilesize - off_x;
		int diff_top = off_y;
		int diff_bottom = tilesize - off_y;

		boolean start_left = diff_left < diff_right;
		boolean start_top = diff_top < diff_bottom;

		if (start_top) {
			if (start_left) {
				iMove = 2;
			} else {
				iMove = 3;
			}
		} else {
			if (start_left) {
				iMove = 1;
			} else {
				iMove = 0;
			}
		} // calculate the visibility borders
		int x_min = -tilesize;
		int y_min = -tilesize;
		int x_max = getWidth();
		int y_max = getHeight();

		// calculate the length of the grid (number of squares per edge)
		int gridLength = 1 << zoom;

		// paint the tiles in a spiral, starting from center of the map
		boolean painted = true;
		int x = 0;
		while (painted) {
			painted = false;
			for (int i = 0; i < 4; i++) {
				if (i % 2 == 0) {
					x++;
				}
				for (int j = 0; j < x; j++) {
					if (x_min <= posx && posx <= x_max && y_min <= posy && posy <= y_max) {
						// tile is visible
						Tile tile;
						if (scrollWrapEnabled) {
							// in case tilex is out of bounds, grab the tile to use for wrapping
							int tilexWrap = (((tilex % gridLength) + gridLength) % gridLength);
							tile = tileController.getTile(tilexWrap, tiley, zoom);
						} else {
							tile = tileController.getTile(tilex, tiley, zoom);
						}
						if (tile != null) {
							tile.paint(g, posx, posy);
							if (tileGridVisible) {
								g.drawRect(posx, posy, tilesize, tilesize);
							}
						}
						painted = true;
					}
					Point p = move[iMove];
					posx += p.x * tilesize;
					posy += p.y * tilesize;
					tilex += p.x;
					tiley += p.y;
				}
				iMove = (iMove + 1) % move.length;
			}
		}
		// outer border of the map
		int mapSize = tilesize << zoom;
		if (scrollWrapEnabled) {
			g.drawLine(0, h2 - center.y, getWidth(), h2 - center.y);
			g.drawLine(0, h2 - center.y + mapSize, getWidth(), h2 - center.y + mapSize);
		} else {
			g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize);
		}

		// g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20);

		// keep x-coordinates from growing without bound if scroll-wrap is enabled
		if (scrollWrapEnabled) {
			center.x = center.x % mapSize;
		}

		if (mapPolygonsVisible && mapPolygonList != null) {
			for (MapPolygon polygon : mapPolygonList) {
				if (polygon.isVisible())
					paintPolygon(g, polygon);
			}
		}

		if (mapRectanglesVisible && mapRectangleList != null) {
			for (MapRectangle rectangle : mapRectangleList) {
				if (rectangle.isVisible())
					paintRectangle(g, rectangle);
			}
		}

		if (mapMarkersVisible && mapMarkerList != null) {
			for (MapMarker marker : mapMarkerList) {
				if (marker.isVisible())
					paintMarker(g, marker);
			}
		}

		attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0),
				getPosition(getWidth(), getHeight()), zoom, this);
	}

	/**
	 * Paint a single marker.
	 */
	protected void paintMarker(Graphics g, MapMarker marker) {
		Point p = getMapPosition(marker.getLat(), marker.getLon(), marker.getMarkerStyle() == MapMarker.STYLE.FIXED);
		Integer radius = getRadius(marker, p);
		if (scrollWrapEnabled) {
			int tilesize = tileSource.getTileSize();
			int mapSize = tilesize << zoom;
			if (p == null) {
				p = getMapPosition(marker.getLat(), marker.getLon(), false);
				radius = getRadius(marker, p);
			}
			marker.paint(g, p, radius);
			int xSave = p.x;
			int xWrap = xSave;
			// overscan of 15 allows up to 30-pixel markers to gracefully scroll off the
			// edge of the panel
			while ((xWrap -= mapSize) >= -15) {
				p.x = xWrap;
				marker.paint(g, p, radius);
			}
			xWrap = xSave;
			while ((xWrap += mapSize) <= getWidth() + 15) {
				p.x = xWrap;
				marker.paint(g, p, radius);
			}
		} else {
			if (p != null) {
				marker.paint(g, p, radius);
			}
		}
	}

	/**
	 * Paint a single rectangle.
	 */
	protected void paintRectangle(Graphics g, MapRectangle rectangle) {
		Coordinate topLeft = rectangle.getTopLeft();
		Coordinate bottomRight = rectangle.getBottomRight();
		if (topLeft != null && bottomRight != null) {
			Point pTopLeft = getMapPosition(topLeft, false);
			Point pBottomRight = getMapPosition(bottomRight, false);
			if (pTopLeft != null && pBottomRight != null) {
				rectangle.paint(g, pTopLeft, pBottomRight);
				if (scrollWrapEnabled) {
					int tilesize = tileSource.getTileSize();
					int mapSize = tilesize << zoom;
					int xTopLeftSave = pTopLeft.x;
					int xTopLeftWrap = xTopLeftSave;
					int xBottomRightSave = pBottomRight.x;
					int xBottomRightWrap = xBottomRightSave;
					while ((xBottomRightWrap -= mapSize) >= 0) {
						xTopLeftWrap -= mapSize;
						pTopLeft.x = xTopLeftWrap;
						pBottomRight.x = xBottomRightWrap;
						rectangle.paint(g, pTopLeft, pBottomRight);
					}
					xTopLeftWrap = xTopLeftSave;
					xBottomRightWrap = xBottomRightSave;
					while ((xTopLeftWrap += mapSize) <= getWidth()) {
						xBottomRightWrap += mapSize;
						pTopLeft.x = xTopLeftWrap;
						pBottomRight.x = xBottomRightWrap;
						rectangle.paint(g, pTopLeft, pBottomRight);
					}

				}
			}
		}
	}

	/**
	 * Paint a single polygon.
	 */
	protected void paintPolygon(Graphics g, MapPolygon polygon) {
		List<? extends ICoordinate> coords = polygon.getPoints();
		if (coords != null && coords.size() >= 3) {
			List<Point> points = new LinkedList<>();
			for (ICoordinate c : coords) {
				Point p = getMapPosition(c, false);
				if (p == null) {
					return;
				}
				points.add(p);
			}
			polygon.paint(g, points);
			if (scrollWrapEnabled) {
				int tilesize = tileSource.getTileSize();
				int mapSize = tilesize << zoom;
				List<Point> pointsWrapped = new LinkedList<>(points);
				boolean keepWrapping = true;
				while (keepWrapping) {
					for (Point p : pointsWrapped) {
						p.x -= mapSize;
						if (p.x < 0) {
							keepWrapping = false;
						}
					}
					polygon.paint(g, pointsWrapped);
				}
				pointsWrapped = new LinkedList<>(points);
				keepWrapping = true;
				while (keepWrapping) {
					for (Point p : pointsWrapped) {
						p.x += mapSize;
						if (p.x > getWidth()) {
							keepWrapping = false;
						}
					}
					polygon.paint(g, pointsWrapped);
				}
			}
		}
	}

	/**
	 * Moves the visible map pane.
	 *
	 * @param x horizontal movement in pixel.
	 * @param y vertical movement in pixel
	 */
	public void moveMap(int x, int y) {
		tileController.cancelOutstandingJobs(); // Clear outstanding load
		center.x += x;
		center.y += y;
		repaint();
		this.fireJMVEvent(new JMVCommandEvent(COMMAND.MOVE, this));
	}

	/**
	 * @return the current zoom level
	 */
	public int getZoom() {
		return zoom;
	}

	/**
	 * Increases the current zoom level by one
	 */
	public void zoomIn() {
		setZoom(zoom + 1);
	}

	/**
	 * Increases the current zoom level by one
	 */
	public void zoomIn(Point mapPoint) {
		setZoom(zoom + 1, mapPoint);
	}

	/**
	 * Decreases the current zoom level by one
	 */
	public void zoomOut() {
		setZoom(zoom - 1);
	}

	/**
	 * Decreases the current zoom level by one
	 *
	 * @param mapPoint point to choose as center for new zoom level
	 */
	public void zoomOut(Point mapPoint) {
		setZoom(zoom - 1, mapPoint);
	}

	/**
	 * Set the zoom level and center point for display
	 *
	 * @param zoom     new zoom level
	 * @param mapPoint point to choose as center for new zoom level
	 */
	public void setZoom(int zoom, Point mapPoint) {
		if (zoom > tileController.getTileSource().getMaxZoom() || zoom < tileController.getTileSource().getMinZoom()
				|| zoom == this.zoom)
			return;
		Coordinate zoomPos = getPosition(mapPoint);
		tileController.cancelOutstandingJobs(); // Clearing outstanding load
		// requests
		setDisplayPosition(mapPoint, zoomPos, zoom);

		this.fireJMVEvent(new JMVCommandEvent(COMMAND.ZOOM, this));
	}

	/**
	 * Set the zoom level
	 *
	 * @param zoom new zoom level
	 */
	public void setZoom(int zoom) {
		setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2));
	}

	/**
	 * Every time the zoom level changes this method is called. Override it in
	 * derived implementations for adapting zoom dependent values. The new zoom
	 * level can be obtained via {@link #getZoom()}.
	 *
	 * @param oldZoom the previous zoom level
	 */
	protected void zoomChanged(int oldZoom) {
		zoomSlider.setToolTipText("Zoom level " + zoom);
		zoomInButton.setToolTipText("Zoom to level " + (zoom + 1));
		zoomOutButton.setToolTipText("Zoom to level " + (zoom - 1));
		zoomOutButton.setEnabled(zoom > tileController.getTileSource().getMinZoom());
		zoomInButton.setEnabled(zoom < tileController.getTileSource().getMaxZoom());
	}

	public boolean isTileGridVisible() {
		return tileGridVisible;
	}

	public void setTileGridVisible(boolean tileGridVisible) {
		this.tileGridVisible = tileGridVisible;
		repaint();
	}

	public boolean getMapMarkersVisible() {
		return mapMarkersVisible;
	}

	/**
	 * Enables or disables painting of the {@link MapMarker}
	 *
	 * @param mapMarkersVisible
	 * @see #addMapMarker(MapMarker)
	 * @see #getMapMarkerList()
	 */
	public void setMapMarkerVisible(boolean mapMarkersVisible) {
		this.mapMarkersVisible = mapMarkersVisible;
		repaint();
	}

	public void setMapMarkerList(List<MapMarker> mapMarkerList) {
		this.mapMarkerList = mapMarkerList;
		repaint();
	}

	public List<MapMarker> getMapMarkerList() {
		return mapMarkerList;
	}

	public void setMapRectangleList(List<MapRectangle> mapRectangleList) {
		this.mapRectangleList = mapRectangleList;
		repaint();
	}

	public List<MapRectangle> getMapRectangleList() {
		return mapRectangleList;
	}

	public void setMapPolygonList(List<MapPolygon> mapPolygonList) {
		this.mapPolygonList = mapPolygonList;
		repaint();
	}

	public List<MapPolygon> getMapPolygonList() {
		return mapPolygonList;
	}

	public void addMapMarker(MapMarker marker) {
		mapMarkerList.add(marker);
		repaint();
	}

	public void removeMapMarker(MapMarker marker) {
		mapMarkerList.remove(marker);
		repaint();
	}

	public void removeAllMapMarkers() {
		mapMarkerList.clear();
		repaint();
	}

	public void addMapRectangle(MapRectangle rectangle) {
		mapRectangleList.add(rectangle);
		repaint();
	}

	public void removeMapRectangle(MapRectangle rectangle) {
		mapRectangleList.remove(rectangle);
		repaint();
	}

	public void removeAllMapRectangles() {
		mapRectangleList.clear();
		repaint();
	}

	public void addMapPolygon(MapPolygon polygon) {
		mapPolygonList.add(polygon);
		repaint();
	}

	public void removeMapPolygon(MapPolygon polygon) {
		mapPolygonList.remove(polygon);
		repaint();
	}

	public void removeAllMapPolygons() {
		mapPolygonList.clear();
		repaint();
	}

	public void setZoomContolsVisible(boolean visible) {
		zoomSlider.setVisible(visible);
		zoomInButton.setVisible(visible);
		zoomOutButton.setVisible(visible);
	}

	public boolean getZoomContolsVisible() {
		return zoomSlider.isVisible();
	}

	public void setTileSource(TileSource tileSource) {
		if (tileSource.getMaxZoom() > MAX_ZOOM)
			throw new RuntimeException("Maximum zoom level too high");
		if (tileSource.getMinZoom() < MIN_ZOOM)
			throw new RuntimeException("Minumim zoom level too low");
		Coordinate position = getPosition();
		this.tileSource = tileSource;
		tileController.setTileSource(tileSource);
		zoomSlider.setMinimum(tileSource.getMinZoom());
		zoomSlider.setMaximum(tileSource.getMaxZoom());
		tileController.cancelOutstandingJobs();
		if (zoom > tileSource.getMaxZoom()) {
			setZoom(tileSource.getMaxZoom());
		}
		attribution.initialize(tileSource);
		setDisplayPosition(position, zoom);
		repaint();
	}

	public void tileLoadingFinished(Tile tile, boolean success) {
		repaint();
	}

	public boolean isMapRectanglesVisible() {
		return mapRectanglesVisible;
	}

	/**
	 * Enables or disables painting of the {@link MapRectangle}
	 *
	 * @param mapRectanglesVisible
	 * @see #addMapRectangle(MapRectangle)
	 * @see #getMapRectangleList()
	 */
	public void setMapRectanglesVisible(boolean mapRectanglesVisible) {
		this.mapRectanglesVisible = mapRectanglesVisible;
		repaint();
	}

	public boolean isMapPolygonsVisible() {
		return mapPolygonsVisible;
	}

	/**
	 * Enables or disables painting of the {@link MapPolygon}
	 *
	 * @param mapPolygonsVisible
	 * @see #addMapPolygon(MapPolygon)
	 * @see #getMapPolygonList()
	 */
	public void setMapPolygonsVisible(boolean mapPolygonsVisible) {
		this.mapPolygonsVisible = mapPolygonsVisible;
		repaint();
	}

	public boolean isScrollWrapEnabled() {
		return scrollWrapEnabled;
	}

	public void setScrollWrapEnabled(boolean scrollWrapEnabled) {
		this.scrollWrapEnabled = scrollWrapEnabled;
		repaint();
	}

	public ZOOM_BUTTON_STYLE getZoomButtonStyle() {
		return zoomButtonStyle;
	}

	public void setZoomButtonStyle(ZOOM_BUTTON_STYLE style) {
		zoomButtonStyle = style;
		if (zoomSlider == null || zoomInButton == null || zoomOutButton == null) {
			return;
		}
		switch (style) {
		case HORIZONTAL:
			zoomSlider.setBounds(10, 10, 30, 150);
			zoomInButton.setBounds(4, 155, 18, 18);
			zoomOutButton.setBounds(26, 155, 18, 18);
			break;
		case VERTICAL:
			zoomSlider.setBounds(10, 27, 30, 150);
			zoomInButton.setBounds(14, 8, 20, 20);
			zoomOutButton.setBounds(14, 176, 20, 20);
			break;
		default:
			zoomSlider.setBounds(10, 10, 30, 150);
			zoomInButton.setBounds(4, 155, 18, 18);
			zoomOutButton.setBounds(26, 155, 18, 18);
			break;
		}
		repaint();
	}

	public TileController getTileController() {
		return tileController;
	}

	/**
	 * Return tile information caching class
	 * 
	 * @see TileLoaderListener#getTileCache()
	 */
	public TileCache getTileCache() {
		return tileController.getTileCache();
	}

	public void setTileLoader(TileLoader loader) {
		tileController.setTileLoader(loader);
	}

	public AttributionSupport getAttribution() {
		return attribution;
	}

	protected EventListenerList listenerList = new EventListenerList();

	/**
	 * @param listener listener to set
	 */
	public void addJMVListener(JMapViewerEventListener listener) {
		listenerList.add(JMapViewerEventListener.class, listener);
	}

	/**
	 * @param listener listener to remove
	 */
	public void removeJMVListener(JMapViewerEventListener listener) {
		listenerList.remove(JMapViewerEventListener.class, listener);
	}

	/**
	 * Send an update to all objects registered with viewer
	 *
	 * @param evt event to dispatch
	 */
	void fireJMVEvent(JMVCommandEvent evt) {
		Object[] listeners = listenerList.getListenerList();
		for (int i = 0; i < listeners.length; i += 2) {
			if (listeners[i] == JMapViewerEventListener.class) {
				((JMapViewerEventListener) listeners[i + 1]).processCommand(evt);
			}
		}
	}

	/**
	 * Save the map on the disk
	 * 
	 * @param filename file name with path
	 */
	public void saveToFile(String filename) {
		BufferedImage image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics g = image.getGraphics();
		paint(g);
		try {
			ImageIO.write(image, "png", new File(filename));
			CgLog.info("Save map to disk : " + filename);
		} catch (IOException ex) {
			CgLog.info(ex.toString());
		}
	}

}