package ui;

import classes.Layer;
import classes.Project;
import classes.ProjectElement;
import enums.EditingMode;
import enums.SelectionType;
import enums.ToolType;
import helpers.Broadcaster;
import helpers.CursorProvider;
import interfaces.CurrentLayerChangedListener;
import interfaces.ProjectChangedListener;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.RectangularShape;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JScrollBar;
import roadnetwork.Edge;
import roadnetwork.RoadNetwork;
import roadnetwork.RoadNetworkElement;
import trafficdefinition.Accident;
import trafficdefinition.AreaFlow;
import trafficdefinition.Flow;
import trafficdefinition.Handle;
import trafficdefinition.HotSpot;
import trafficdefinition.School;
import trafficdefinition.TrafficArea;
import trafficdefinition.TrafficDefinitionElement;
import trafficdefinition.TrafficDefinitionLayer;

/**
 * @author  PapaleonLe01
 */
public class Display extends JComponent implements CurrentLayerChangedListener, ProjectChangedListener, MouseListener, MouseMotionListener, MouseWheelListener, AdjustmentListener, KeyListener {

	private static final long serialVersionUID = 7162061244639610972L;

	private JScrollBar scrlVertical = null;

	private JScrollBar scrlHorizontal = null;

	/**
	 * The currently selected traffic layer
	 */
	private TrafficDefinitionLayer currentTrafficLayer;

	/**
	 * The current project's road network
	 */
	private RoadNetwork roadNetwork;

	/**
	 * Flag that states whether a project is open
	 */
	private boolean projectLoaded = false;

	/**
	 * The horizontal map translation
	 */
	private double translateX;

	/**
	 * The vertical map translation
	 */
	private double translateY;

	/**
	 * The minimum and maximum allowe transalation values
	 */
	private double mintranslateX;
	/**
	 * The minimum and maximum allowe transalation values
	 */
	private double maxtranslateX;
	/**
	 * The minimum and maximum allowe transalation values
	 */
	private double mintranslateY;
	/**
	 * The minimum and maximum allowe transalation values
	 */
	private double maxtranslateY;

	/**
	 * Scaling factor (zoom in - out)
	 */
	private double scale;

	private Point previousMouseLocation;

	private boolean areaZoom;

	/**
	 * The current tool
	 */
	private ToolType tool = ToolType.None;

	/**
	 * The current selection type
	 */
	private SelectionType selectionType = SelectionType.New;

	/**
	 * Helper variable to store a new element during its creation process
	 */
	private TrafficDefinitionElement newElement;

	/**
	 * Flag to know whether to draw all traffic layers or just the currently
	 * selected one
	 */
	private boolean drawAllTrafficLayers = false;

	private GeneralPath areaPath;

	private boolean creatingNewElement = false;

	private Handle currentHandle = null;

	private Rectangle marquee;

	private Point marqueeStart;

	private Point marqueeEnd;

	/**
	 * The transformation used while drawing the map and traffic layers
	 */
	private AffineTransform tx = new AffineTransform();

	private EditingMode editingMode;

	private boolean manipulatingHandle = false;

	/**
	 * Default constructor.
	 * 
	 * @param scrlvertical
	 * @param scrlhorizontal
	 */
	public Display(JScrollBar scrlvertical, JScrollBar scrlhorizontal) {

		scrlVertical = scrlvertical;
		scrlHorizontal = scrlhorizontal;

		// Initialize transformation variables
		translateX = 0;
		translateY = 0;
		scale = 1;

		setOpaque(true);
		setDoubleBuffered(true);

		// Register event handlers
		addMouseListener(this);
		addMouseMotionListener(this);
		addMouseWheelListener(this);
		addKeyListener(this);

		scrlVertical.addAdjustmentListener(this);
		scrlHorizontal.addAdjustmentListener(this);

		// Be notified when the user opens or closes the project
		Broadcaster.addProjectChangedListener(this);
	}

	/**
	 * Event handler for scroll bar events. Sets the translation variables so
	 * that the map moves when the user scrolls
	 */
	public void adjustmentValueChanged(AdjustmentEvent e) {
		// Horizontal scrolling
		if (e.getSource() == scrlHorizontal) {
			translateX = Math.max(maxtranslateX, mintranslateX) - e.getValue() + Math.min(maxtranslateX, mintranslateX);
			repaint();
		}
		// Vertical scrolling
		else if (e.getSource() == scrlVertical) {
			translateY = Math.max(maxtranslateY, mintranslateY) - e.getValue() + Math.min(maxtranslateY, mintranslateY);
			repaint();
		}

	}

	/**
	 * Handler for the event of the current traffic layer changed.
	 */
	public void CurrentLayerChanged(Layer<? extends ProjectElement> newCurrentLayer) {
		// Deselect all elements before changing the layer
		if (currentTrafficLayer != null) {
			currentTrafficLayer.SelectNoneElements();
		}

		if (roadNetwork != null) {
			roadNetwork.SelectNoneElements();
		}

		// If the user has selected the map layer then hide any previously
		// selected traffic layer
		if (newCurrentLayer instanceof RoadNetwork) {
			setEditingMode(EditingMode.Map);

			currentTrafficLayer = null;
		} else if (newCurrentLayer instanceof TrafficDefinitionLayer) {
			currentTrafficLayer = (TrafficDefinitionLayer) newCurrentLayer;

			setEditingMode(EditingMode.GenericTraffic);
		}

		repaint();
	}

	/**
	 * Deletes the selected elements of the currently editing map or traffic
	 * layer
	 * 
	 */
	public void DeleteSelectedObjects() {
		switch (editingMode) {
		case Map:
			roadNetwork.DeleteSelectedElements();
			break;
		case GenericTraffic:
			currentTrafficLayer.DeleteSelectedElements();
		}

		repaint();
	}

	/**
	 * Updates the mouse cursor when a key is pressed, or deletes selected
	 * elements when the delete key is pressed,or cancels the creation of a new
	 * object if the Esc key was pressed
	 */
	public void keyPressed(KeyEvent e) {
		if (!projectLoaded) {
			return;
		}

		// Delete the selected elements if the Delete key was pressed
		if (e.getKeyCode() == KeyEvent.VK_DELETE) {
			DeleteSelectedObjects();

			return;
		}
		// Cancel the creation of a new element if the Escape key was pressed
		else if ((e.getKeyCode() == KeyEvent.VK_ESCAPE) && (creatingNewElement)) {
			creatingNewElement = false;
			newElement = null;
			repaint();

			return;
		}

		if ((tool == ToolType.Select) && (e.getKeyCode() == KeyEvent.VK_CONTROL)) {
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.SelectAdd));

			return;
		}

		if ((tool == ToolType.Select) && (e.getKeyCode() == KeyEvent.VK_SHIFT)) {
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.SelectRemove));

			return;
		}

		if ((tool == ToolType.Zoom) && (e.getKeyCode() == KeyEvent.VK_CONTROL)) {
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.ZoomOut));

			return;
		}
	}

	/**
	 * Updates the mouse cursor when a key is released
	 */
	public void keyReleased(KeyEvent e) {
		if (!projectLoaded) {
			return;
		}

		if ((tool == ToolType.Select) && ((e.getKeyCode() == KeyEvent.VK_CONTROL) || (e.getKeyCode() == KeyEvent.VK_SHIFT))) {
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.Select));

			return;
		}

		if ((tool == ToolType.Zoom) && (e.getKeyCode() == KeyEvent.VK_CONTROL)) {
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.ZoomIn));

			return;
		}
	}

	public void keyTyped(KeyEvent e) {
	}

	public void mouseClicked(MouseEvent e) {
		if (!projectLoaded) {
			return;
		}

		// Transform the point to map coordinates
		Point2D.Double transformedPoint = TransformPoint(e.getPoint());

		if (transformedPoint == null) {
			return;
		}

		switch (tool) {
		// Create school
		case School:
			if (e.getButton() == MouseEvent.BUTTON1) {
				currentTrafficLayer.AddTrafficElement(new School(transformedPoint));
			}

			repaint();
			break;
		case Hotspot:
			// If we are in the middle of creating a new hotspot and the user
			// right clicks then cancel the creation
			if ((creatingNewElement) && (e.getButton() == MouseEvent.BUTTON3)) {
				creatingNewElement = false;
				newElement = null;
				repaint();

				return;
			}

			// If this is the second click (we were creating a hot spot)
			if (creatingNewElement) {
				// Finalize
				((HotSpot) newElement).createHandles();

				// Add the hotspot to the layer
				currentTrafficLayer.AddTrafficElement(newElement);

				// Reset helper variables
				newElement = null;
				creatingNewElement = false;

				repaint();
			}
			// If this is the first click
			else {
				// Create a temporary hotspot and set its center
				newElement = new HotSpot(transformedPoint);

				creatingNewElement = true;
			}

			return;
		case AreaFlow:
			// If we are in the middle of creating a new area flow and the user
			// right clicks then cancel the creation
			if ((creatingNewElement) && (e.getButton() == MouseEvent.BUTTON3)) {
				creatingNewElement = false;
				newElement = null;
				repaint();

				return;
			}

			// If we are in the middle of creating an area flow
			if (creatingNewElement) {
				AreaFlow newFlow = (AreaFlow) newElement;

				newFlow.constructionPhase++;

				// If the construction has finished (4 clicks needed)
				if (newFlow.constructionPhase == 3) {
					newFlow.createHandles();

					currentTrafficLayer.AddTrafficElement(newElement);

					// Reset helper variables
					creatingNewElement = false;
					newElement = null;

					repaint();
				}
			}
			// First click - construction beginning
			else {
				// Create temporary area flow and set its starting location
				newElement = new AreaFlow();
				((AreaFlow) newElement).setStartAreaCenter(transformedPoint);

				((AreaFlow) newElement).constructionPhase = 0;

				creatingNewElement = true;
			}

			return;
		case Flow:
			// Get the map element below the mouse
			RoadNetworkElement elementBelowMouse = roadNetwork.PointHitTest(transformedPoint);

			// If we are in the middle of creating a new flow and the user right
			// clicks or clicks somewhere that is not an edge cancel the
			// operation
			if ((creatingNewElement) && ((e.getButton() == MouseEvent.BUTTON3) || (elementBelowMouse == null) || !(elementBelowMouse instanceof Edge))) {
				creatingNewElement = false;
				newElement = null;
				repaint();

				return;
			}

			if ((elementBelowMouse != null) && (elementBelowMouse instanceof Edge)) {
				// Set the flow's ending point and add it to the traffic layer
				if (creatingNewElement) {
					((Flow) newElement).setTo(new Point2D.Double(elementBelowMouse.getBounds().getCenterX(), elementBelowMouse.getBounds().getCenterY()));

					((Flow) newElement).setEnd((Edge) elementBelowMouse);

					((Flow) newElement).SetDependencies();

					currentTrafficLayer.AddTrafficElement(newElement);

					creatingNewElement = false;
					newElement = null;

					repaint();
				}
				// Start creating the flow
				else {
					newElement = new Flow();
					((Flow) newElement).setStart((Edge) elementBelowMouse);
					((Flow) newElement).setFrom(new Point2D.Float((float) elementBelowMouse.getBounds().getCenterX(), (float) elementBelowMouse.getBounds().getCenterY()));

					creatingNewElement = true;
				}
			}

			return;
		case Accident:
			// Get the map element below the mouse
			RoadNetworkElement elementBelowMouse2 = roadNetwork.PointHitTest(transformedPoint);

			if ((elementBelowMouse2 != null) && (elementBelowMouse2 instanceof Edge) && (e.getButton() == MouseEvent.BUTTON1)) {
				currentTrafficLayer.AddTrafficElement(new Accident((Edge) elementBelowMouse2, transformedPoint));
			}

			repaint();

			break;
		case Select:
			switch (editingMode) {
			case Map:
				if (e.isControlDown()) {
					roadNetwork.AlterSelection(new Rectangle((int) Math.round(transformedPoint.x), (int) Math.round(transformedPoint.y), 1, 1), SelectionType.Add);
				} else if (e.isShiftDown()) {
					roadNetwork.AlterSelection(new Rectangle((int) Math.round(transformedPoint.x), (int) Math.round(transformedPoint.y), 1, 1), SelectionType.Remove);
				} else {
					roadNetwork.AlterSelection(new Rectangle((int) Math.round(transformedPoint.x), (int) Math.round(transformedPoint.y), 1, 1), selectionType);
				}

				repaint();

				break;
			case GenericTraffic:
				if (e.isControlDown()) {
					currentTrafficLayer.AlterSelection(new Rectangle((int) Math.round(transformedPoint.x), (int) Math.round(transformedPoint.y), 1, 1), SelectionType.Add);
				} else if (e.isShiftDown()) {
					currentTrafficLayer.AlterSelection(new Rectangle((int) Math.round(transformedPoint.x), (int) Math.round(transformedPoint.y), 1, 1), SelectionType.Remove);
				} else {
					currentTrafficLayer.AlterSelection(new Rectangle((int) Math.round(transformedPoint.x), (int) Math.round(transformedPoint.y), 1, 1), selectionType);
				}

				repaint();

				break;
			}
			break;
		case Zoom:
			// Zoom out
			if (e.isControlDown()) {
				// If the map fits in the display then disable further zoom out
				if ((getWidth() > roadNetwork.getBounds().width * scale) && (getHeight() > roadNetwork.getBounds().height * scale)) {
					return;
				} else {
					scale -= 0.5;
				}
			}
			// Zoom in
			else {
				scale += 0.5;
			}

			// Move the previous center of the map to the center of the display
			MoveMapPointToDisplayCenter(transformedPoint);

			UpdateScrollBars();

			repaint();

			break;
		}
	}

	public void mouseDragged(MouseEvent e) {
		if (!projectLoaded) {
			return;
		}

		// Transform from device coordinates to map coordinates
		Point2D.Double transformedPoint = TransformPoint(e.getPoint());

		if (transformedPoint == null) {
			return;
		}

		switch (tool) {
		case Pan:
			// Update the translation variables
			translateX += e.getX() - previousMouseLocation.getX();
			translateY += e.getY() - previousMouseLocation.getY();

			// Save the new mouse position
			previousMouseLocation = e.getPoint();

			UpdateScrollBars();

			repaint();

			break;
		case Zoom:
			// Marquee zooming
			areaZoom = true;

			// Update the marquee
			marqueeEnd = e.getPoint();

			marquee.setFrameFromDiagonal(marqueeStart, marqueeEnd);

			repaint();

			break;
		case Select:
			// If we are moving a handle then change the handle's location
			if (manipulatingHandle) {
				currentHandle.setLocationAndNotify(transformedPoint);
			}
			// If we are creating a marquee
			else {
				areaZoom = true;

				// Update the marquee
				marqueeEnd = e.getPoint();

				marquee.setFrameFromDiagonal(marqueeStart, marqueeEnd);
			}
			repaint();

			break;
		case Area:
			// If we are creating a traffic area then update its path
			areaPath.lineTo((float) transformedPoint.x, (float) transformedPoint.y);

			repaint();

			break;
		}

	}

	/**
	 * Set the focus on the display so that we can catch key events
	 */
	public void mouseEntered(MouseEvent e) {
		setFocusable(true);
		requestFocus(true);
	}

	public void mouseExited(MouseEvent e) {
		manipulatingHandle = false;
	}

	public void mouseMoved(MouseEvent e) {
		if (!projectLoaded) {
			return;
		}

		// Transform the mouse's location to map coordinates
		Point2D.Double transformedPoint = TransformPoint(e.getPoint());

		if (transformedPoint == null) {
			return;
		}

		// Find if an element is below the mouse location to show its tooltip
		TrafficDefinitionElement trafficElementBelowMouse = null;
		RoadNetworkElement roadElementBelowMouse = roadNetwork.PointHitTest(transformedPoint);

		if (currentTrafficLayer != null) {
			trafficElementBelowMouse = currentTrafficLayer.PointHitTest(transformedPoint);
		}

		// Set the tooltip of the element below the mouse
		if (trafficElementBelowMouse != null) {
			setToolTipText(trafficElementBelowMouse.getToolTip());
		} else if (roadElementBelowMouse != null) {
			setToolTipText(roadElementBelowMouse.getToolTip());
		} else {
			setToolTipText(null);
		}

		switch (tool) {
		case Select:
			// Check to see if the mouse is over a selected element's handle
			currentHandle = null;

			boolean overHandle = false;

			if (currentTrafficLayer != null) {
				List<TrafficDefinitionElement> selectedTrafficElements = currentTrafficLayer.getSelectedElements();

				for (TrafficDefinitionElement t : selectedTrafficElements) {
					currentHandle = t.handleHit(e.getPoint(), tx);

					if (currentHandle != null) {
						overHandle = true;
						break;
					}
				}
			}

			// If it is, update its cursor
			if (overHandle) {
				this.setCursor(CursorProvider.getCursor(CursorProvider.Cursors.Hand));
			} else {
				this.setCursor(CursorProvider.getCursor(CursorProvider.Cursors.Select));
			}
			break;
		case Hotspot:
			// Set the hotspot's radius
			if (creatingNewElement) {
				HotSpot newHotSpot = (HotSpot) newElement;

				newHotSpot.setRadius(transformedPoint);

				repaint();
			}

			break;
		case AreaFlow:
			// Set the flow's properties based on the construction phase
			if (creatingNewElement) {
				AreaFlow newFlow = (AreaFlow) newElement;

				switch (newFlow.constructionPhase) {
				case 0:
					newFlow.setStartAreaRadius(transformedPoint);
					break;
				case 1:
					newFlow.setEndAreaCenter(transformedPoint);
					break;
				case 2:
					newFlow.setEndAreaRadius(transformedPoint);
					break;
				}

				repaint();
			}

			break;
		case Flow:
			// Update the cursor if the mouse is over an edge
			if ((roadElementBelowMouse != null) && (roadElementBelowMouse instanceof Edge)) {
				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
			} else {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}

			if (!creatingNewElement) {
				return;
			}

			// Show flow while creating it
			((Flow) newElement).setTo(transformedPoint);

			repaint();

			break;
		case Accident:
			// Update the cursor if the mouse is over an edge
			if ((roadElementBelowMouse != null) && (roadElementBelowMouse instanceof Edge)) {
				setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
			} else {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}

			break;
		}
	}

	public void mousePressed(MouseEvent e) {
		if (!projectLoaded) {
			return;
		}

		// Capture starting point
		previousMouseLocation = e.getPoint();

		// Transform the point to map coordinates
		Point2D.Double transformedPoint = TransformPoint(e.getPoint());

		if (transformedPoint == null) {
			return;
		}

		switch (tool) {
		case Select:

			// If the mouse was over a handle then start manipulating it
			if (currentHandle != null) {
				this.setCursor(CursorProvider.getCursor(CursorProvider.Cursors.HandClosed));

				manipulatingHandle = true;
			} else {
				// If the user is making a new selection first deselect all
				// previous elements
				if ((selectionType == SelectionType.New) && (!e.isShiftDown()) && (!e.isControlDown())) {
					switch (editingMode) {
					case Map:
						roadNetwork.SelectNoneElements();
						break;
					case GenericTraffic:
						currentTrafficLayer.SelectNoneElements();
						break;
					}
				}

				areaZoom = false;

				// Create the marquee rectangle
				marquee = new Rectangle();
				marqueeStart = e.getPoint();

				manipulatingHandle = false;
			}
			break;
		case Zoom:
			areaZoom = false;

			// Create the marquee rectangle
			marquee = new Rectangle();
			marqueeStart = e.getPoint();

			break;
		case Pan:
			// Update cursor
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.HandClosed));

			break;
		case Area:
			areaPath = new GeneralPath();

			areaPath.moveTo((float) transformedPoint.x, (float) transformedPoint.y);

			break;
		}
	}

	public void mouseReleased(MouseEvent e) {
		if (!projectLoaded) {
			return;
		}

		switch (tool) {
		case Pan:
			// Restore cursor
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.Hand));
			break;
		case Zoom:
			if ((!areaZoom) || (marquee.height * marquee.width < 10)) {
				return;
			}

			// Transform the marquee to map coordinates
			Point2D.Double transformedMarqueeStart = TransformPoint(marqueeStart);
			Point2D.Double transformedMarqueeEnd = TransformPoint(marqueeEnd);

			if ((transformedMarqueeStart == null) || (transformedMarqueeEnd == null)) {
				return;
			}

			Rectangle transformedRectangle = new Rectangle();
			transformedRectangle.setFrameFromDiagonal(transformedMarqueeStart, transformedMarqueeEnd);

			// Zoom the map to the rectangle specified by the marquee
			ZoomToRectangleAndCenter(transformedRectangle);

			UpdateScrollBars();

			marquee = null;

			repaint();

			break;
		case Select:
			// Release the handle if we were manipulating it
			if (manipulatingHandle) {
				manipulatingHandle = false;
			} else {
				if ((!areaZoom) || (marquee.height * marquee.width < 10)) {
					return;
				}

				// Transform the marquee to map coordinates
				Point2D.Double transformedMarqueeStart1 = TransformPoint(marqueeStart);
				Point2D.Double transformedMarqueeEnd1 = TransformPoint(marqueeEnd);

				if ((transformedMarqueeStart1 == null) || (transformedMarqueeEnd1 == null)) {
					return;
				}

				Rectangle transformedRectangle1 = new Rectangle();
				transformedRectangle1.setFrameFromDiagonal(transformedMarqueeStart1, transformedMarqueeEnd1);

				// Select the current layer's elements based on the selection
				// type and the marquee
				switch (editingMode) {
				case Map:
					if (e.isControlDown()) {
						roadNetwork.AlterSelection(transformedRectangle1, SelectionType.Add);
					} else if (e.isShiftDown()) {
						roadNetwork.AlterSelection(transformedRectangle1, SelectionType.Remove);
					} else {
						roadNetwork.AlterSelection(transformedRectangle1, selectionType);
					}

					repaint();

					break;
				case GenericTraffic:
					if (e.isControlDown()) {
						currentTrafficLayer.AlterSelection(transformedRectangle1, SelectionType.Add);
					} else if (e.isShiftDown()) {
						currentTrafficLayer.AlterSelection(transformedRectangle1, SelectionType.Remove);
					} else {
						currentTrafficLayer.AlterSelection(transformedRectangle1, selectionType);
					}

					repaint();

					break;
				}

				marquee = null;

				repaint();
			}

			break;
		case Area:
			// Finalize the area's shape
			areaPath.closePath();

			currentTrafficLayer.AddTrafficElement(new TrafficArea(areaPath));

			areaPath = null;

			repaint();
			break;
		}

	}

	/**
	 * Zooms in or out when the user scrolls the mouse wheel
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {
		if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

			// If we are zooming out
			if (e.getWheelRotation() < 0) {
				// If the map fits in the display then disable further zoom out
				if ((getWidth() > roadNetwork.getBounds().width * scale) && (getHeight() > roadNetwork.getBounds().height * scale)) {
					return;
				}
			}

			Point2D.Double transformedPoint = TransformPoint(e.getPoint());

			if (transformedPoint == null) {
				return;
			}

			// Make it a reasonable amount of zoom
			// 0.01 gives a nice slow transition
			scale += 0.01 * e.getWheelRotation();

			MoveMapPointToDisplayPoint(transformedPoint, e.getPoint());

			UpdateScrollBars();

			repaint();
		}
	}

	/**
	 * Moves the map point (expressed in map coordinates) to the specified
	 * display point (expressed in device coordinates)
	 * 
	 * @param mapPoint
	 * @param displayPoint
	 */
	private void MoveMapPointToDisplayPoint(Point2D.Double mapPoint, Point displayPoint) {
		translateX = displayPoint.x - (mapPoint.x * scale);
		translateY = displayPoint.y + (mapPoint.y * scale);
	}

	/**
	 * Translates the map so that the specified point is at the center of the
	 * display
	 * 
	 * @param mapPoint
	 */
	public void MoveMapPointToDisplayCenter(Point2D.Double mapPoint) {
		MoveMapPointToDisplayPoint(mapPoint, new Point(getWidth() / 2, getHeight() / 2));
	}

	/**
	 * Responsible for the drawing of the component. Draws the map and traffic
	 * layers.
	 */
	@Override
	public void paintComponent(Graphics g) {
		Graphics2D graphics2D = (Graphics2D) g;

		// Get the clip bounds
		Rectangle clip = graphics2D.getClipBounds();

		// If only part of the component needs repainting then repaint the whole
		// display anyway.
		// No mechanism yet for painting only part of the component.
		if ((clip.x != 0) || (clip.y != 0)) {
			repaint();
			return;
		}

		// Draw gray background
		graphics2D.setColor(Color.GRAY);
		graphics2D.fillRect(0, 0, getWidth(), getHeight());

		// If no project is loaded don't continue
		if (!projectLoaded) {
			return;
		}

		// Set transformation matrix (for pan and zoom)
		tx.setToIdentity();
		tx.translate(translateX, translateY);

		// Minus is for y-axis reversal
		tx.scale(scale, -scale);

		graphics2D.setTransform(tx);

		// Set rendering hints
		graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

		// Draw white page
		graphics2D.setColor(Color.WHITE);
		graphics2D.fill(roadNetwork.getBounds());

		// Draw page border
		graphics2D.setColor(Color.BLACK);
		graphics2D.draw(roadNetwork.getBounds());

		// Draw the road network
		roadNetwork.Draw(graphics2D, scale);

		if(drawAllTrafficLayers){
			// Draw all the project traffic definition layers
			for (TrafficDefinitionLayer tdl : Project.getCurrentlyLoadedProject().getTrafficLayers()) {
				tdl.Draw(graphics2D, scale);
			}
		}
		else{
			 //Draw the current traffic definition layer
			 if (currentTrafficLayer != null) {
				 currentTrafficLayer.Draw(graphics2D, scale);
			 }
		}
		
		// Draw the currently creating new element
		if (newElement != null) {
			graphics2D.setColor(Color.RED);
			newElement.Draw(graphics2D, scale, false, false);
		}

		// Draw area
		if (areaPath != null) {
			graphics2D.setColor(new Color(120, 65, 87, 125));
			graphics2D.fill(areaPath);
		}

		// Draw marquee
		if (marquee != null) {
			graphics2D.setTransform(new AffineTransform());
			graphics2D.setColor(Color.RED);
			graphics2D.draw(marquee);
		}
	}

	/**
	 * Handler for the event of the project change.
	 * 
	 * @param project
	 *            the newly loaded project or null if the project was closed
	 */
	public void ProjectChanged(Project project) {
		// If the new project is null it means that the project was closed.
		if (project == null) {
			projectLoaded = false;

			roadNetwork = null;
			currentTrafficLayer = null;

			repaint();
		}
		// Else it means the project was opened or created
		else {
			projectLoaded = true;

			roadNetwork = project.getRoadNetwork();

			ZoomToFitAndCenter();

			UpdateScrollBars();

			setEditingMode(EditingMode.Map);
		}
	}

	/**
	 * Repaints the display
	 * 
	 */

	// Refreshes the display
	public void Refresh() {
		repaint();
	}

	/**
	 * Selects all elements on the currently editing map or traffic layer
	 * 
	 */
	public void SelectAll() {
		switch (editingMode) {
		case Map:
			roadNetwork.SelectAllElements();
			break;
		case GenericTraffic:
			currentTrafficLayer.SelectAllElements();
		}

		repaint();
	}

	/**
	 * Inverts the selection on the currently editing map or traffic layer
	 * 
	 */
	public void SelectInverse() {
		switch (editingMode) {
		case Map:
			roadNetwork.SelectInverseElements();
			break;
		case GenericTraffic:
			currentTrafficLayer.SelectInverseElements();
			break;
		}

		repaint();
	}

	/**
	 * Deselects all elements on the currently editing map or traffic layer
	 * 
	 */
	public void SelectNone() {
		switch (editingMode) {
		case Map:
			roadNetwork.SelectNoneElements();
			break;
		case GenericTraffic:
			currentTrafficLayer.SelectNoneElements();
			break;
		}

		repaint();

	}

	/**
	 * Sets the editing mode (Map or Traffic)
	 * @param  newMode
	 * @uml.property  name="editingMode"
	 */
	public void setEditingMode(EditingMode newMode) {
		editingMode = newMode;
	}

	/**
	 * Sets the type of selection (New, Add or Remove)
	 * @param  selectionType
	 * @uml.property  name="selectionType"
	 */
	public void setSelectionType(SelectionType selectionType) {
		this.selectionType = selectionType;
	}

	/**
	 * Shows or hides the map's deleted elements
	 * 
	 * @param b
	 */
	public void setShowDeleted(boolean b) {
		if (!projectLoaded) {
			return;
		}

		roadNetwork.setShowDeleted(b);

		repaint();
	}

	/**
	 * Sets the drawAllTrafficLayers flag
	 * @param  b
	 * @uml.property  name="drawAllTrafficLayers"
	 */
	public void setDrawAllTrafficLayers(boolean b){
		drawAllTrafficLayers = b;
	}
	
	/**
	 * Sets the current tool
	 * @param  tool
	 * @uml.property  name="tool"
	 */
	public void setTool(ToolType tool) {
		this.tool = tool;

		// Set the cursor based on the tool
		switch (tool) {
		case Pan:
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.Hand));
			break;
		case Zoom:
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.ZoomIn));
			break;
		case Select:
			setCursor(CursorProvider.getCursor(CursorProvider.Cursors.Select));
			break;
		default:
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			break;
		}
	}

	/**
	 * Undeletes the selected deleted elements of the map
	 * 
	 */
	public void UndeleteSelectedObjects() {
		roadNetwork.UndeleteSelectedMapElements();

		repaint();
	}

	/**
	 * Centers the map on the display and zooms so that the whole map fits in
	 * the display.
	 * 
	 */
	public void ZoomToFitAndCenter() {
		if (!projectLoaded) {
			return;
		}

		ZoomToRectangleAndCenter(roadNetwork.getBounds());

		UpdateScrollBars();

		repaint();
	}

	/**
	 * Transforms the specified point from device coordinates to map coordinates
	 * 
	 * @param p
	 *            the point to transform
	 * @return the transformed point
	 */
	private Point2D.Double TransformPoint(Point p) {
		try {
			return (Point2D.Double) tx.inverseTransform(new Point2D.Double(p.getX(), p.getY()), null);
		} catch (NoninvertibleTransformException e) {
			return null;
		}
	}

	/**
	 * Updates the scrollbars based on the scaling and translation variables
	 * 
	 */
	private void UpdateScrollBars() {
		mintranslateX = (getWidth() / 2) - (roadNetwork.getBounds().getMinX() * scale);
		maxtranslateX = (getWidth() / 2) - (roadNetwork.getBounds().getMaxX() * scale);

		mintranslateY = (getHeight() / 2) + (roadNetwork.getBounds().getMinY() * scale);
		maxtranslateY = (getHeight() / 2) + (roadNetwork.getBounds().getMaxY() * scale);

		scrlVertical.removeAdjustmentListener(this);

		scrlVertical.setMinimum((int) Math.min(mintranslateY, maxtranslateY));
		scrlVertical.setMaximum((int) Math.max(mintranslateY, maxtranslateY));

		scrlVertical.setValue((int) -translateY + (int) Math.max(maxtranslateY, mintranslateY) + (int) Math.min(maxtranslateY, mintranslateY));

		scrlVertical.addAdjustmentListener(this);

		scrlHorizontal.removeAdjustmentListener(this);

		scrlHorizontal.setMinimum((int) Math.min(mintranslateX, maxtranslateX));
		scrlHorizontal.setMaximum((int) Math.max(mintranslateX, maxtranslateX));

		scrlHorizontal.setValue((int) -translateX + (int) Math.max(maxtranslateX, mintranslateX) + (int) Math.min(maxtranslateX, mintranslateX));

		scrlHorizontal.addAdjustmentListener(this);
	}

	/**
	 * Zooms the map so that it fits in the specified rectangle and centers it
	 * on the display
	 * 
	 * @param rectangle
	 */
	private void ZoomToRectangleAndCenter(RectangularShape rectangle) {
		// Fit in height
		if (rectangle.getHeight() > rectangle.getWidth()) {
			scale = (getHeight()) / rectangle.getHeight();
		}
		// Fit in width
		else if (rectangle.getHeight() < rectangle.getWidth()) {
			scale = (getWidth()) / rectangle.getWidth();
		}
		// If the map is square then fit it based on the display's minimum
		// dimension
		else {
			int minDimension = Math.min(getWidth(), getHeight());

			scale = (minDimension) / rectangle.getWidth();
		}

		// Move map to the center of the display
		MoveMapPointToDisplayCenter(new Point2D.Double(rectangle.getCenterX(), rectangle.getCenterY()));
	}

	/**
	 * Zooms and centers on the specified element. This method is called when
	 * the user selects an element from the property panel combo box
	 * 
	 * @param element
	 */
	public void CenterAndZoomOnElement(ProjectElement element) {
		// Move map to the center of the display
		MoveMapPointToDisplayCenter(new Point2D.Double(element.getBounds().getCenterX(), element.getBounds().getCenterY()));

		// TODO: Zooming correctly
		// ZoomToRectangleAndCenter(element.getBounds());

		UpdateScrollBars();

		repaint();
	}
}
