package org.greenstone.atlas.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.maps.client.InfoWindow;
import com.google.gwt.maps.client.InfoWindowContent;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.control.ControlAnchor;
import com.google.gwt.maps.client.control.ControlPosition;
import com.google.gwt.maps.client.control.LargeMapControl;
import com.google.gwt.maps.client.control.MapTypeControl;
import com.google.gwt.maps.client.control.Control.CustomControl;
import com.google.gwt.maps.client.event.MapClickHandler;
import com.google.gwt.maps.client.event.MapMoveHandler;
import com.google.gwt.maps.client.event.PolygonClickHandler;
import com.google.gwt.maps.client.geocode.Geocoder;
import com.google.gwt.maps.client.geocode.LatLngCallback;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.maps.client.overlay.MarkerOptions;
import com.google.gwt.maps.client.overlay.Polygon;
import com.google.gwt.maps.client.overlay.Polyline;
import com.google.gwt.maps.client.overlay.PolylineOptions;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FormPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Hidden;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.xml.client.NamedNodeMap;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.FormElement;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeCollection;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.OptionElement;
import com.google.gwt.dom.client.SelectElement;
import com.google.gwt.dom.client.TextAreaElement;

import java.util.ArrayList;
import java.util.HashMap;

public class GS3MapLibrary implements EntryPoint
{
	protected MapWidget _map;

	protected VerticalPanel _containerPanel = new VerticalPanel();
	protected VerticalPanel _scoreAdjustPanel = new VerticalPanel();
	protected VerticalPanel _headerDiv = new VerticalPanel();
	protected HorizontalPanel _viewPanel = new HorizontalPanel();

	protected StatusBar _statusBar = null;

	protected HTML _gsPanel = new HTML();
	protected HTML _columnViewBox = new HTML();

	protected HorizontalPanel _contentDiv = new HorizontalPanel();
	protected HorizontalPanel _footerDiv = new HorizontalPanel();

	protected Menu _currentDocumentMenu = null;
	protected Menu _lastCreatedPlaceMenu = null;

	protected FlowPanel _radioPanel = new FlowPanel();
	protected FlowPanel _checkBoxPanel = new FlowPanel();
	
	protected Label _loadingBox = new Label("");
	protected Label _mapLoadingLabel = new Label("Loading map content");

	protected ListBox _textViewSelector = new ListBox();

	protected Button _geoQueryButton = new Button("Specify Search Square");

	protected Timer _loadingTimer = null;
	protected Timer _resizeCheckTimer = null;
	protected Timer _scrollCheckTimer = null;
	protected Timer _placeMouseOverTimer = null;
	protected Timer _documentMouseOverTimer = null;
	protected Timer _menuOverTimer = null;
	protected Timer _menuOutTimer = null;

	protected Geocoder _geocoder = null;

	protected static FindPlaceServiceAsync _findPlaceService = GWT.create(FindPlaceService.class);

	protected ArrayList<Place> _currentPlaces = null;
	protected ArrayList<String> _highlightedPlaces = new ArrayList<String>();
	protected ArrayList<String> _highlightedTextPlaces = new ArrayList<String>();
	protected ArrayList<Boolean> _visiblePlaceSets = new ArrayList<Boolean>();
	protected ArrayList<LatLng> _areaPoints = new ArrayList<LatLng>();
	protected ArrayList<String> _removedPlaces = new ArrayList<String>();
	
	protected ArrayList<ArrayList<Place>> _currentMultiPlaces = null;
	
	protected HashMap<String, Place> _chosenPlaces = new HashMap<String, Place>();	

	protected HandlerRegistration _fishEyeHandlerReg = null;
	protected HandlerRegistration _columnViewHandlerReg = null;

	protected MapMoveHandler _mapMoveHandler = null;
	protected MapClickHandler _spatialSearchClickHandler = null;
	protected SpatialSearchControls _searchControls = null;

	protected final int _MAXVISIBLEMARKERS = 500;
	protected int _currentZoomLevel = 2;
	protected int _loadingCount = 0;
	protected int _currentBrowserPixelWidth = 0;
	protected int _currentBrowserPixelHeight = 0;
	protected int _currentScrollTopValue = 0;

	protected boolean _updatingMap = false;
	protected boolean _gazetteerLoaded = false;
	protected boolean _redraw = false;
	protected boolean _resize = false;
	protected boolean _mapVisible = false;
	protected boolean _gsVisible = true;
	protected boolean _frameLoaded = false;
	protected boolean _initialised = false;
	protected boolean _showAllPlaces = false;
	protected boolean _mouseEventPause = false;
	protected boolean _mapMoveHandlerOn = true;
	protected boolean _searchResultsPage = false;

	protected String _selectedPlaceName = "";
	protected String _currentCollection = "";
	protected String _currentURL = "";
	protected final String _GREENSTONEURL = "http://localhost:8486/greenstone3";
	//protected final String _GREENSTONEURL = "http://www.nzdl.org/atlasgreenstone3";
	protected final String _GREENSTONEDEVURL = _GREENSTONEURL + "/dev";

	protected com.google.gwt.xml.client.Document _currentNewPageDoc;

	/**
	 * GWT module entry point
	 */
	public void onModuleLoad()
	{
		_statusBar = new StatusBar(this);
		//Make the Firebug console so that browsers that don't have it don't break
		setUpConsole();
		
		//Get a Google geocoder to help find places that the Gazetteer does not know the coordinates for
		_geocoder = new Geocoder();

		((ServiceDefTarget) _findPlaceService).setServiceEntryPoint("http://localhost:8486/ATLAS/findPlace");
		//((ServiceDefTarget) _findPlaceService).setServiceEntryPoint("http://www.nzdl.org/ATLAS/findPlace");
		
		//Setup native Javascript functions
		setUpLoadPageFromUrl(this);
		setUpLoadPageFromForm(this);
		setUpLoadSpatialSearchPage(this);
		
		//Setup the map
		setUpMap();
		
		//Set up the list box that stores the text view options
		setUpTextViewSelector();

		//Load the Greenstone home page
		loadPageFromUrl(_GREENSTONEDEVURL + "?a=p&sa=home");
	}

	/**
	 * Adds the three options to the text view selector
	 */
	public void setUpTextViewSelector()
	{
		_textViewSelector.addItem("Normal");
		_textViewSelector.addItem("Fisheye");
		_textViewSelector.addItem("Column");
	}
	
	/**
	 * Loads the page at the given URL
	 * @param url is the url to load
	 */
	public void loadPageFromUrl(String url)
	{
		//Make sure a url was given 
		if(url == null)
		{
			return;
		}
		
		//Save the given url for later use
		_currentURL = url;

		//Set Greenstone to be visible
		_gsVisible = true;
		
		//Clear the map
		_map.clearOverlays();

		updateHandlers();
		clearStatusUpdates();

		//Check if the page being loaded is a search page 
		if (url.contains("s1.query="))
		{
			//Load the search page
			loadSearchPageFromUrl(url);	
		}
		else
		{
			//This page is not a search page
			_searchResultsPage = false;
		}

		//If the url contains a collection name
		if (url.contains("&c="))
		{
			//Store the collection name
			_currentCollection = getCollectionFromURL(url);
		}

		//Add the "Page loading" update to the status bar"		
		_statusBar.addUpdate("Loading new page", "LoadPage");
		
		getNewPage(url.contains("?") ? (url + "&excerpttag=body") : (url + "?excerpttag=body"), 
			new ResponseFunction(){
				public void function(String response){
					HTML newPage = new HTML(response);
					newPage.getElement().setId("newpage");
					Element oldNewPage = (Element) Document.get().getElementById("newpage");
					if(oldNewPage != null)
					{
						oldNewPage.removeFromParent();
					}
					getElementsByTagName("body").get(0).appendChild(newPage.getElement());
					newPage.setVisible(false);

					showLoadedPage();
				}
			}
		);
	}
	
	public void loadPageWithoutUrl()
	{
		//Set the current url to be empty
		_currentURL = "";
		
		//Set Greenstone to be visible
		_gsVisible = true;
		
		//Clear the map
		_map.clearOverlays();

		updateHandlers();
		clearStatusUpdates();

		//Add the "Page loading" update to the status bar"		
		_statusBar.addUpdate("Loading new page", "LoadPage");
	}
	
	public void getNewPage(String url, final ResponseFunction responseFunc)
	{
		RequestBuilder newPageRequest = new RequestBuilder(RequestBuilder.GET, url);
		try{
			newPageRequest.sendRequest(null, new RequestCallback()
			{
				public void onResponseReceived(Request request, Response response)
				{
					if(response.getStatusCode() == Response.SC_OK)
					{
						responseFunc.function(response.getText());
					}
				}
				
				public void onError(Request request, Throwable exception)
				{
					alert("Problem loading new page -> " + exception.getMessage());
				}
			});
		}
		catch(Exception ex)
		{
			alert("Exception while creating the new page request -> " + ex.getMessage());
		}
	}
	
	/**
	 * Clears several of the status updates that should not persist across pages
	 */
	public void clearStatusUpdates()
	{
		_statusBar.removeUpdate("DisPlaces");
		_statusBar.removeUpdate("SearchLoad");
		_statusBar.removeUpdate("TextSearch");
	}
	
	/**
	 * Gets a collection name from a given url
	 * @param url the url to search for a collection in
	 * @return the collection name
	 */
	public String getCollectionFromURL(String url)
	{
		int start = (url.indexOf("&c=") + 3);
		int end = -1;
		if (url.indexOf("&", (url.indexOf("&c=") + 1)) == -1)
		{
			end = url.length();
		}
		else
		{
			end = url.indexOf("&", (url.indexOf("&c=") + 1));
		}
		return url.substring(start, end);
	}
	
	/**
	 * Sets up various handlers for a new page
	 */
	public void updateHandlers()
	{
		if (_fishEyeHandlerReg != null)
		{
			_fishEyeHandlerReg.removeHandler();
			_fishEyeHandlerReg = null;
		}

		if (!_mapMoveHandlerOn)
		{
			_mapMoveHandlerOn = true;
			_map.addMapMoveHandler(_mapMoveHandler);
		}

		if (_searchControls != null)
		{
			_map.removeControl(_searchControls);
			_searchControls = null;
		}

		if (_spatialSearchClickHandler != null)
		{
			_map.removeMapClickHandler(_spatialSearchClickHandler);
			_spatialSearchClickHandler = null;
		}
	}
	
	/**
	 * Moves the loaded page from the temporary frame to the main page
	 */
	private void showLoadedPage()
	{	
		//If ATLAS has not yet been initialised then do so
		if (!_initialised)
		{
			//Fix webpage elements so that they point to the correct address
			fixForms();
			fixAnchors(); 
			fixImages();
			
			initialiseATLAS();
			
			//Add <link> and <script> elements to <head>
			getNewPage(_currentURL.contains("?") ? (_currentURL + "&excerpttag=head") : (_currentURL + "?excerpttag=head"), 
				new ResponseFunction(){
					public void function(String response){
						HTML newPage = new HTML(response);
						changeCSSLinksAndScripts(newPage.getElement());
					}
				}
			);
		}
		else
		{
			//Add <link> and <script> elements to <head>
			getNewPage(_currentURL.contains("?") ? (_currentURL + "&excerpttag=head") : (_currentURL + "?excerpttag=head"), 
				new ResponseFunction(){
					public void function(String response){
						HTML newPage = new HTML(response);
						changeCSSLinksAndScripts(newPage.getElement());
					}
				}
			);

			//Fix webpage elements so that they point to the correct address
			fixForms();
			fixAnchors(); 
			fixImages(); 
			
			Element newPageElem = getElementById("newpage");
			NodeList<com.google.gwt.dom.client.Element> divs = newPageElem.getElementsByTagName("div");
			
			Element newBanner = null;
			Element newContent = null;
			
			for(int i = 0; i < divs.getLength(); i++)
			{
				if(divs.getItem(i).getId().equals("gs_banner"))
				{
					newBanner = (Element) divs.getItem(i);
				}
				else if(divs.getItem(i).getId().equals("gs_content"))
				{
					newContent = (Element) divs.getItem(i);
				}
			}
			
			//Swap the current banner with the new banner
			Element oldBanner = getElementById("gs_banner");
			oldBanner.getParentElement().removeChild(oldBanner);
			_headerDiv.insert(HTML.wrap(newBanner), 0);

			//If the content div was in column view mode then reset it
			if (_contentDiv.getWidget(0).getElement().getId().equals("columnBox"))
			{
				_contentDiv.clear();
				_contentDiv.add(_gsPanel);
				_contentDiv.add(_map);
			}

			//Swap the current Greenstone content with the new Greenstone content
			_gsPanel.setHTML(HTML.wrap(newContent).getHTML());
		}

		//If the page is a collection home page then add spatial search to the list of options
		if (_currentURL.contains("sa=about"))
		{
			addSpatialSearchLinkToAboutPage();
		}

		//The page has now finished being updated
		_statusBar.removeUpdate("LoadPage");

		//Check the document's text for places
		checkDocText();
	}
	
	/**
	 * Add a link to spatial searching on the current page
	 */
	private void addSpatialSearchLinkToAboutPage()
	{
		//Get the list to add the link to
		Element list = getElementById("servicelist");

		//If the list does not exist then stop
		if(list == null)
		{
			return;
		}
		
		//Create the div for the link and explanation
		Element linkLabel = DOM.createDiv();
		list.appendChild(linkLabel);
		linkLabel.setClassName("paramLabel");

		//Create the link and append it to the div
		Element spatialLink = DOM.createAnchor();
		linkLabel.appendChild(spatialLink);
		spatialLink.setAttribute("href", "javascript:loadSpatialSearchPageJS()");
		spatialLink.setInnerText("Spatial Search");

		//Create the explanation div and add to the parent div
		Element explainationDiv = DOM.createDiv();
		list.appendChild(explainationDiv);
		explainationDiv.setClassName("paramLabel");
		explainationDiv.setInnerText("Search for documents mentioning places from a certain area");

		Element br = DOM.createElement("br");
		list.appendChild(br);
		br.setClassName("clear");
	}
	
	/**
	 * Initialise the webpage elements for ATLAS
	 */
	public void initialiseATLAS()
	{		
		//Reload the page when the view type is changed
		_textViewSelector.addChangeHandler(new ChangeHandler()
		{
			public void onChange(ChangeEvent event)
			{
				if (getDocTextElement() != null)
				{
					loadPageFromUrl(_currentURL);
				}
			}
		});

		//Set up content
		_containerPanel.add(_headerDiv);
		_containerPanel.add(_statusBar.getStatusBarDiv());
		_containerPanel.add(_contentDiv);
		_containerPanel.add(_footerDiv);

		_viewPanel.add(new Label("View: "));
		_viewPanel.add(_textViewSelector);
		_viewPanel.setVisible(false);
		
		Element newPageElem = getElementById("newpage");
		NodeList<com.google.gwt.dom.client.Element> divs = newPageElem.getElementsByTagName("div");
		
		Element headerElem = null;
		Element bodyElem = null;
		Element footerElem = null;
		
		for(int i = 0; i < divs.getLength(); i++)
		{
			if(divs.getItem(i).getId().equals("gs_banner"))
			{
				headerElem = (Element) divs.getItem(i);
			}
			else if(divs.getItem(i).getId().equals("gs_content"))
			{
				bodyElem = (Element) divs.getItem(i);
			}
			else if(divs.getItem(i).getId().equals("gs_footer"))
			{
				footerElem = (Element) divs.getItem(i);
			}
		}
		
		if(headerElem == null){alert("header is null");}
		if(bodyElem == null){alert("body is null");}	
		if(footerElem == null){alert("footer is null");}

		// Add header
		//Node newHeader = importNode(newPage, tempFrameGetElementById("banner"), true);
		_headerDiv.add(HTML.wrap(headerElem));
		_headerDiv.add(_viewPanel);
		_headerDiv.setWidth("100%");
		_headerDiv.getElement().setId("GSHeader");

		_gsPanel.getElement().getStyle().setProperty("overflow", "auto");

		_contentDiv.setWidth("100%");
		_contentDiv.getElement().getStyle().setProperty("backgroundColor", "#FFFFFF");
		_contentDiv.getElement().setId("GSContent");

		// Add content
		//Node newContent = importNode(Document.get(), tempFrameGetElementById("content"), true);
		_gsPanel = HTML.wrap(bodyElem);
		_contentDiv.add(_gsPanel);
		_contentDiv.add(_map);

		// Add footer
		//Node newFooter = importNode(Document.get(), tempFrameGetElementById("footer"), true);
		_footerDiv.add(HTML.wrap(footerElem));
		_footerDiv.setWidth("100%");
		_footerDiv.getElement().setId("GSFooter");

		RootPanel.get("mainFrame").add(_containerPanel);

		setUpBrowserResizeCheckTimer();

		// Make the call to check if the Gazetteer is loaded
		_findPlaceService.isGazetteerLoaded(setUpCheckGazetteerCallback());
		_initialised = true;
	}

	/**
	 * Loads a Greenstone search page from a url
	 * @param url is the url to load
	 */
	public void loadSearchPageFromUrl(String url)
	{
		_searchResultsPage = true;

		//Set up the request to get returned documents from the xml
		RequestBuilder xmlRequest = new RequestBuilder(RequestBuilder.GET, url);
		try
		{
			//Make the request
			xmlRequest.sendRequest(null, new RequestCallback()
			{
				public void onError(Request request, Throwable exception)
				{
					logToConsole("xml Request Error -> " + exception.getMessage());
				}

				public void onResponseReceived(Request request, Response response)
				{
					processResponse(response);
				}
			});
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}
	}
	
	public void processResponse(Response response)
	{
		//If the status code is not the page loaded code then stop
		if(response.getStatusCode() != 200)
		{
			return;
		}
		
		//Add the status update that indicates that the system is searching for places in the result document
		_statusBar.addUpdate("Finding places in result documents", "SearchLoad");

		//Get the response text
		String text = response.getText();
		
		//Get the hash codes for the documents
		final ArrayList<String> docURLs = getDocURLsFromResponse(text);		
		final String[] texts = new String[docURLs.size()];
		
		for(int i = 0; i < texts.length; i++)
		{
			texts[i] = null;
		}
		
		for(int i = 0; i < docURLs.size(); i++)
		{
			String url = docURLs.get(i);
			RequestBuilder xmlRequest = new RequestBuilder(RequestBuilder.GET, _GREENSTONEURL + "/" + (url.contains("?") ? (url + "&excerptid=gs-document-text") : (url + "?excerptid=gs-document-text")));
			try
			{
				final int index = i;
				xmlRequest.sendRequest(null, new RequestCallback()
				{
					public void onResponseReceived(Request request, Response response)
					{
						if(response.getStatusCode() == Response.SC_OK)
						{
							texts[index] = response.getText();
						}
					}
					
					public void onError(Request request, Throwable exception)
					{
						alert("Exception requesting new page -> " + exception.getMessage());
						texts[index] =  "";
					}
				});
			}
			catch(Exception ex)
			{
				alert("Exception requesting " + url + " -> " + ex.getMessage());
			}
		}
		
		Timer checkDoneTimer = new Timer()
		{
			int _count = 0;
			
			public void run()
			{
				boolean done = true;
				if(_count < 20)
				{
					for(int i = 0; i < texts.length; i++)
					{
						if(texts[i] == null)
						{
							done = false;
							break;
						}
					}
				}
				_count++;
				
				if(done)
				{
					this.cancel();
					findPlacesInTexts(texts);
				}
			}
		};
		checkDoneTimer.scheduleRepeating(500);
	}
	
	/**
	 * Makes a call to the server to look through multiple places for places
	 * @param texts is a list of texts from different documents
	 */
	private void findPlacesInTexts(String[] texts)
	{
		logToConsole("Finished gettings doc text and gazetteer loaded!");

		//Make the call to the server that finds the places
		_findPlaceService.findPlacesInMultipleTexts(texts, setUpFindMultiplePlacesCallback());

		//Set up the map for viewing multiple places
		_map.clearOverlays();
		_mapVisible = true;
		_map.setVisible(true);
		_contentDiv.add(_mapLoadingLabel);

		_mapLoadingLabel.getElement().getStyle().setProperty("left", ((int) (_headerDiv.getOffsetWidth() * (3.0 / 4.0) - _mapLoadingLabel.getOffsetWidth() / 2)) + "px");
		_mapLoadingLabel.getElement().getStyle().setProperty("top", (_headerDiv.getOffsetHeight() + _contentDiv.getOffsetHeight() / 3) + "px");

		_map.getElement().getStyle().setProperty("left", (Window.getClientWidth() / 2 + 8) + "px");
		_map.getElement().getStyle().setProperty("top", (_headerDiv.getOffsetHeight() + 28 + Window.getScrollTop()) + "px");

		_resize = true;
	}
	
	/**
	 * Takes Greenstone search response xml text and extracts the document hashes from it
	 * @param text is the xml to use
	 * @return a list of document hashes
	 */
	private ArrayList<String> getDocURLsFromResponse(String text)
	{
		ArrayList<String> docURLs = new ArrayList<String>();

		int index = text.indexOf("<ul id=\"results\"");
		int endIndex = text.indexOf("</ul>", index);
		while ((index = text.indexOf("<a href=\"", index)) != -1 && index < endIndex)
		{
			int urlStart = index + "<a href=\"".length();
			int urlEnd = text.indexOf("\"", urlStart + 1);

			docURLs.add(text.substring(urlStart, urlEnd));
			
			index++;
		}
		
		return docURLs;
	}

	/**
	 * Creates and shows a spatial search page
	 */
	public void loadSpatialSearchPage()
	{
		//Clear the Greenstone panel
		_gsPanel.setHTML((new HTML()).getHTML());

		//Turn off the map move handler if it is on
		if (_mapMoveHandlerOn)
		{
			_mapMoveHandlerOn = false;
			_map.removeMapMoveHandler(_mapMoveHandler);
		}

		//Set up the page
		_map.clearOverlays();
		_mapVisible = true;
		_gsVisible = false;
		_map.setVisible(true);

		//Create a new list that will store the points that the user clicks on
		_areaPoints = new ArrayList<LatLng>();

		//If the map does not currently have a spatial search handler
		if (_spatialSearchClickHandler == null)
		{
			//Create a new map spatial search handler
			_spatialSearchClickHandler = new MapClickHandler()
			{
				public void onClick(MapClickEvent event)
				{
					//Take the point that has been clicked and add it to the list
					LatLng point = event.getLatLng();
					_areaPoints.add(point);

					//Create a marker at the clicked point
					MarkerOptions mo = MarkerOptions.newInstance();
					mo.setClickable(false);
					Marker marker = new Marker(point, mo);
					_map.addOverlay(marker);

					//If this is not the first point then add a line between this and the previous point
					if (_areaPoints.size() > 1)
					{
						PolylineOptions po = PolylineOptions.newInstance();
						po.setClickable(false);

						Polyline line = new Polyline(new LatLng[] { point, _areaPoints.get(_areaPoints.size() - 2) }, "#FFFFFF", 3, 1, po);
						_map.addOverlay(line);
					}
				}
			};
		}
		_map.addMapClickHandler(_spatialSearchClickHandler);

		//If the map does not have search controls then add them
		if (_searchControls == null)
		{
			_searchControls = new SpatialSearchControls(new ControlPosition(ControlAnchor.BOTTOM_RIGHT, 0, 0));
		}
		_map.addControl(_searchControls);

		_resize = true;
	}

	/**
	 * Takes an HTML form element and loads a page using its content
	 * @param form is the form to use
	 */
	public void loadPageFromForm(Element form)
	{
		//Get the top level url
		StringBuilder url = new StringBuilder(_GREENSTONEDEVURL);
		
		//Get the elements from the form
		FormElement formElement = FormElement.as(form);
		NodeCollection<com.google.gwt.dom.client.Element> elements = formElement.getElements();

		//Go through each element
		for (int i = 0; i < elements.getLength(); i++)
		{
			Element currentItem = (Element) elements.getItem(i);

			//If the element is an input element
			if (currentItem.getTagName().equals("INPUT"))
			{
				InputElement input = InputElement.as(currentItem);
				
				//If the element is a text or hidden element
				if (input.getType().equals("text") || input.getType().equals("hidden"))
				{
					//Append the name and value of this element to the url
					url.append("&" + input.getName() + "=" + input.getValue());
				}
				//If the element is a radio or checkbox element
				else if (input.getType().equals("radio") || input.getType().equals("checkbox"))
				{
					//If the element has been checked
					if (input.isChecked())
					{
						//Append the name and value of this element to the url
						url.append("&" + input.getName() + "=" + input.getAttribute("value"));
					}
				}
			}
			//If the element is a select element (drop down box)
			else if (currentItem.getTagName().equals("SELECT"))
			{
				SelectElement select = SelectElement.as(currentItem);
				NodeList<OptionElement> options = select.getOptions();

				//Append the name of the element and the value of the currently selected index to the url
				url.append("&" + select.getName() + "=" + options.getItem(select.getSelectedIndex()).getAttribute("value"));
			}
			//If the element is a text area element 
			else if (currentItem.getTagName().equals("TEXTAREA"))
			{
				TextAreaElement text = TextAreaElement.as(currentItem);
				
				//Append the name and value of the text area to the url
				url.append("&" + text.getName() + "=" + text.getValue());
			}
		}
		//Replace the first & with a ?
		url.replace(url.indexOf("&"), url.indexOf("&") + 1, "?");
		
		//Load the page with the created url
		loadPageFromUrl(url.toString());
	}

	/**
	 * Remove old CSS links from the page and add new ones, the href attributes are also corrected
	 */
	public void changeCSSLinksAndScripts(Element newHead)
	{		
		Element head = getDocumentHead();
		
		//Get all of the <link> elements from the HTML header 
		NodeList<com.google.gwt.dom.client.Element> currentLinks = head.getElementsByTagName("link");
		
		//Get all of the <script> elements from the HTML header 
		NodeList<com.google.gwt.dom.client.Element> currentScripts = head.getElementsByTagName("script");
		
		//Add the new links
		NodeList<com.google.gwt.dom.client.Element> newLinks = newHead.getElementsByTagName("link");	
		for (int i = 0; i < newLinks.getLength(); i++)
		{
			com.google.gwt.dom.client.Element e = newLinks.getItem(i);
			boolean found = false;

			for(int j = 0; j < currentLinks.getLength(); j++)
			{
				Element elem = (Element) currentLinks.getItem(j);
				if(e.getAttribute("href").equals(elem.getAttribute("href"))
				&& e.getAttribute("type").equals(elem.getAttribute("type"))
				&& e.getAttribute("rel").equals(elem.getAttribute("rel")))
				{
					found = true;
				}
			}

			if(!found)
			{
				Element newElem = DOM.createElement("link");
				newElem.setAttribute("href", e.getAttribute("href"));
				newElem.setAttribute("type", e.getAttribute("type"));
				newElem.setAttribute("rel", e.getAttribute("rel"));
				
				if(!newElem.getAttribute("href").startsWith("http://"))
				{
					newElem.setAttribute("href", _GREENSTONEURL + "/" + newElem.getAttribute("href"));	
				}
				head.appendChild(newElem);
			}
		}
		
		//Add the new scripts on the page
		NodeList<com.google.gwt.dom.client.Element> newScripts = newHead.getElementsByTagName("script");	
		for (int i = 0; i < newScripts.getLength(); i++)
		{
			com.google.gwt.dom.client.Element e = newScripts.getItem(i);
			boolean found = false;
			for(int j = 0; j < currentScripts.getLength(); j++)
			{
				Element elem = (Element) currentLinks.getItem(j);
				if(e.getAttribute("src").equals(elem.getAttribute("src")) 
				&& e.getAttribute("type").equals(elem.getAttribute("type")))
				{
					found = true;
				}
			}
			
			if(!found)
			{
				Element newElem = DOM.createElement("script");
				newElem.setAttribute("src", e.getAttribute("src"));
				newElem.setAttribute("type", e.getAttribute("type"));
						
				if(!e.getAttribute("src").startsWith("http://"))
				{
					e.setAttribute("src", _GREENSTONEURL + "/" + e.getAttribute("src"));
				}
				head.appendChild(newElem);
			}
		}
	}

	/**
	 * Go through each form on the page and add a javascript function in place of its action attribute
	 */
	public void fixForms()
	{
		NodeList<com.google.gwt.dom.client.Element> forms = getElementById("newpage").getElementsByTagName("form");
		for (int i = 0; i < forms.getLength(); i++)
		{
			Element e = (Element)forms.getItem(i);
			e.setAttribute("id", "form" + i);

			if(e.getAttribute("action").isEmpty())
			{
				continue;
			}
			if (!e.getAttribute("action").startsWith("http://") && !e.getAttribute("action").startsWith("/"))
			{
				e.setAttribute("action", "javascript:loadPageFromFormJS(document.getElementById('form" + i + "'))");
			}
		}
	}

	/**
	 * Go through each anchor on the page and add a javascript function in place of the href attribute
	 */
	public void fixAnchors()
	{
		NodeList<com.google.gwt.dom.client.Element> anchors = getElementById("newpage").getElementsByTagName("a");
		for (int i = 0; i < anchors.getLength(); i++)
		{
			Element e = (Element)anchors.getItem(i);
			if(e.getAttribute("href").isEmpty())
			{
				continue;
			}
			
			if(e.getAttribute("href").startsWith("javascript:"))
			{
				continue;
			}
			
			if (!e.getAttribute("href").startsWith("http://") && !e.getAttribute("href").startsWith("/"))
			{
				e.setAttribute("href", _GREENSTONEURL + "/" + e.getAttribute("href"));
			}
			//e.setAttribute("onClick", "loadPageFromUrlJS('" + e.getAttribute("href") + "'); return false;");
			e.setAttribute("href", "javascript:loadPageFromUrlJS('" + e.getAttribute("href") + "');");
		}
	}

	/**
	 * Go through each image on the page and correct its src attribute
	 */
	public void fixImages()
	{
		NodeList<com.google.gwt.dom.client.Element> images = getElementById("newpage").getElementsByTagName("img");
		for (int i = 0; i < images.getLength(); i++)
		{
			Element e = (Element)images.getItem(i);
			
			if(e.getAttribute("src").isEmpty())
			{
				continue;
			}
			
			if (!e.getAttribute("src").startsWith("http://") && !e.getAttribute("src").startsWith("/"))
			{
				e.setAttribute("src", _GREENSTONEURL + "/" + e.getAttribute("src"));
			}
		}
	}

	/**
	 * Makes calls to console.log harmless on browsers that do not have Firebug
	 */
	public static native void setUpConsole()
	/*-{
		if (! ("console" in $wnd) || !("firebug" in console)) 
		{
			var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"];
			$wnd.console = {};
			for (var i = 0; i < names.length; ++i) $wnd.console[names[i]] = function() {};
		}
	}-*/;

	/**
	 * Creates the javascript function that calls the loadPageFromForm GWT function 
	 * @param ml is the instance of the GS3MapLibrary class that you wish to attach this function to
	 */
	public static native void setUpLoadPageFromForm(GS3MapLibrary ml)
	/*-{
		$wnd.loadPageFromFormJS = function(form)
		{
			ml.@org.greenstone.atlas.client.GS3MapLibrary::loadPageFromForm(Lcom/google/gwt/user/client/Element;) (form);
		};
	}-*/;

	/**
	 * Creates the javascript function that calls the loadPageFromURL GWT function 
	 * @param ml is the instance of the GS3MapLibrary class that you wish to attach this function to
	 */
	public static native void setUpLoadPageFromUrl(GS3MapLibrary ml)
	/*-{
		$wnd.loadPageFromUrlJS = function(url)
		{
			ml.@org.greenstone.atlas.client.GS3MapLibrary::loadPageFromUrl(Ljava/lang/String;) (url);
		};
	}-*/;

	/**
	 * Creates the javascript function that calls the loadSpatialSearchPage GWT function 
	 * @param ml is the instance of the GS3MapLibrary class that you wish to attach this function to
	 */
	public static native void setUpLoadSpatialSearchPage(GS3MapLibrary ml)
	/*-{
		$wnd.loadSpatialSearchPageJS = function()
		{
			ml.@org.greenstone.atlas.client.GS3MapLibrary::loadSpatialSearchPage() ();
		};
	}-*/;

	/**
	 * Gets all of the specified type of element in the given frame
	 * @param frameIndex is the index of the frame to search in
	 * @param tagname is the specific tag type to get
	 * @return the list of elements with that tag name
	 */
	public static native JsArray<Element> tempFrameGetElementsByTagName(int frameIndex, String tagname)
	/*-{
		return $wnd.frames[frameIndex].document.getElementsByTagName(tagname);
	}-*/;

	/**
	 * Gets a element specifed by a given id in the specified frame
	 * @param frameIndex is the index of the frame to search in
	 * @param id is the id of the element to find
	 * @return the specified element
	 */
	public static native Element tempFrameGetElementById(int frameIndex, String id)
	/*-{
		return $wnd.frames[frameIndex].document.getElementById(id);
	}-*/;

	/**
	 * Gets the total number of frames in the webpage
	 * @return the total number of frames
	 */
	public static native int getNumberOfFrames()
	/*-{
		return $wnd.frames.length;
	}-*/;

	/**
	 * Gets an element specified the given id in the intermediate frame
	 * @param id is the id of the element to find
	 * @return the specified element
	 */
	public static native Element tempFrameGetElementById(String id)
	/*-{
		return $wnd.frames[2].document.getElementById(id);
	}-*/;

	/**
	 * Gets all of the specified type of element in the intermediate frame
	 * @param tagname is the specific tag type to get
	 * @return the list of elements with that tag name
	 */
	public static native JsArray<Element> tempFrameGetElementsByTagName(String tagname)
	/*-{
		return $wnd.frames[2].document.getElementsByTagName(tagname);
	}-*/;

	/**
	 * Prints a given message to screen in a window
	 * @param msg is the message to print
	 */
	public static native void alert(String msg)
	/*-{
		alert(msg);
	}-*/;
	
	/**
	 * Prints a given message to the Firebug console
	 * @param msg is the message to print
	 */
	public static native void logToConsole(String msg)
	/*-{
		//alert(msg);
		//console.log(msg);
	}-*/;

	/**
	 * Gets the <head> element of the <html>
	 * @return the head element
	 */
	public static native Element getDocumentHead()
	/*-{
		return $doc.getElementsByTagName("head")[0];
	}-*/;
	
    /**
     * Patch GWT Document.importNode, which fails to return the imported nodes
     */
    public static final native Node importNode(Document doc, Node node, boolean deep) /*-{
            return doc.importNode(node, deep);
    }-*/;


	/**
	 * Gets an element specified by the given id in the document
	 * @param id the id of the element to find
	 * @return the specified element
	 */
	public static native Element getElementById(String id)
	/*-{
		return $doc.getElementById(id);
	}-*/;

	/**
	 * Gets all of the elements specified by the given tag name
	 * @param tagName is the tag name of the elements to find
	 * @return the list of elements specified by the given tag name
	 */
	public static native JsArray<Element> getElementsByTagName(String tagName)
	/*-{
		return $doc.getElementsByTagName(tagName);
	}-*/;

	/**
	 * Calls the method the initialises the fisheye viewing
	 */
	public static native void setUpFishEye()
	/*-{
		console.log("Fish eye set up");
		$wnd.fishEyeSetUp();
	}-*/;

	/**
	 * Passes the mouse y coordinates from the GWT handler to the fisheye handler
	 * @param y is the mouse y coordinate
	 */
	public static native void passMouseCoordsToFishEye(int y)
	/*-{
		$wnd.checkMousePos(y);
	}-*/;

	/**
	 * Sets up the timer that checks periodically whether or not the elements in the page need to be resized
	 */
	public void setUpBrowserResizeCheckTimer()
	{
		_resizeCheckTimer = new Timer()
		{
			public void run()
			{
				if (_currentBrowserPixelHeight != Window.getClientHeight() || _currentBrowserPixelWidth != Window.getClientWidth() || _resize)
				{
					_resize = false;
					resizeElements();
					_currentBrowserPixelHeight = Window.getClientHeight();
					_currentBrowserPixelWidth = Window.getClientWidth();
				}
			}
		};
		_resizeCheckTimer.scheduleRepeating(500);
	}

	/**
	 * Resizes elements based on the current width and height of the browser window
	 */
	public void resizeElements()
	{
		//Get the width and height we want to use for the content div
		int cwidth = Window.getClientWidth() - 45;
		int cheight = (int) (Window.getClientHeight() - _headerDiv.getOffsetHeight() - _footerDiv.getOffsetHeight() - _statusBar.getStatusBarDiv().getOffsetHeight() - 50);

		//If the height is less than 300 then set it to 300 
		if (cheight < 200)
		{
			cheight = 200;
		}

		//Set the widths of the header and footer divs
		_headerDiv.setWidth(cwidth + "px");
		_footerDiv.setWidth(cwidth + "px");
		
		//Set the size of the content div
		_contentDiv.setSize(cwidth + "px", cheight + "px");
		
		//Set up the style of the Greenstone panel
		_gsPanel.getElement().getStyle().setProperty("position", "absolute");
		_gsPanel.getElement().getStyle().setProperty("overflow", "auto");
		
		//Set up the style of the Map panel
		_map.getElement().getStyle().setProperty("position", "absolute");
		_map.getElement().getStyle().setProperty("top", _contentDiv.getAbsoluteTop() + "px");

		//If the map is visible and the Greenstone panel is visible
		if (_mapVisible && _gsVisible)
		{
			_map.setSize((cwidth / 2 - 60) + "px", (cheight - 25) + "px");
			_map.getElement().getStyle().setProperty("left", (cwidth / 2 + 50) + "px");
			_gsPanel.setSize((cwidth / 2) + "px", (cheight - 25) + "px");
		}

		//If the map is visible and the Greenstone panel is not visible
		if (_mapVisible && !_gsVisible)
		{
			_gsPanel.setSize("0px", "0px");
			_map.setSize((cwidth - 40) + "px", cheight + "px");
			_map.getElement().getStyle().setProperty("left", _gsPanel.getAbsoluteLeft() + "px");
		}

		//If the map is not visible and the Greenstone panel is visible
		if (!_mapVisible && _gsVisible)
		{
			_gsPanel.setSize(cwidth + "px", cheight + "px");
			_map.setSize("0px", "0px");
		}

		//If neither panel is visible
		if (!_mapVisible && !_gsVisible)
		{
			_gsPanel.setSize("0px", "0px");
			_map.setSize("0px", "0px");
		}
	}

	/**
	 * Initialises the Google Map view
	 */
	public void setUpMap()
	{
		//Set up the map size, style, starting point and zoom level
		LatLng startPoint = LatLng.newInstance(0, 0);
		_map = new MapWidget(startPoint, _currentZoomLevel);
		_map.setSize(Window.getClientWidth() / 2 + "px", "600px");
		_map.getElement().getStyle().setProperty("margin", "5px");
		_map.setStyleName("map");

		//Store the default map move handler
		_mapMoveHandler = new MapMoveHandler()
		{
			public void onMove(MapMoveEvent event)
			{
				if (_map.getZoomLevel() != _currentZoomLevel)
				{
					_currentZoomLevel = _map.getZoomLevel();
					_map.clearOverlays();
					
					//If this is not a search page then there is only one place set
					if(!_searchResultsPage)
					{
						addPlacesToMap(_currentPlaces, "#FFFFFF", "#00FF00", _searchResultsPage);
					}
					//If this is a search page then there are likely to be multiple place sets
					else
					{
						displayMultiplePlaceSets();
					}
				}
			}
		};
		_map.addMapMoveHandler(_mapMoveHandler);

		// Add some controls for the zoom level
		_map.addControl(new LargeMapControl());
		_map.addControl(new MapTypeControl());

		//Add the map loading label
		_mapLoadingLabel.setStyleName("mapLoadingLabel");
		_mapLoadingLabel.getElement().getStyle().setProperty("left", ((int) (_headerDiv.getOffsetWidth() * (3.0 / 4.0) - _mapLoadingLabel.getOffsetWidth() / 2)) + "px");
		_mapLoadingLabel.getElement().getStyle().setProperty("top", (_headerDiv.getOffsetHeight() + _contentDiv.getOffsetHeight() / 3) + "px");

		//Make the map initially invisible
		_map.setVisible(false);
	}

	/**
	 * Creates a marker at the given point
	 * @param p is the place to create a marker for
	 * @param point is the point to place the marker at (if this is null the place will be used to provide the coordinates)
	 * @param borderColour is the colour of the border to use
	 * @param fillColour is the colour to use to fill the marker
	 * @param colourInfo stores whether or not to show a coloured rectangle in the information box that is shown when the marker is clicked
	 * @return the marker
	 */
	public Polygon createMarkerAtPoint(Place p, LatLng point, String borderColour, String fillColour, Boolean colourInfo)
	{
		float lat = 0.0f;
		float lng = 0.0f;

		//Get the coordinates
		if (point == null)
		{
			lat = p.getLatitude();
			lng = p.getLongitude();
		}
		else
		{
			lat = (float) point.getLatitude();
			lng = (float) point.getLongitude();
		}

		LatLng newPoint = ((point == null) ? LatLng.newInstance(lat, lng) : point);

		//Work out the size of the new marker based on the current zoom level
		double zoomLevel = _map.getZoomLevel();
		double size = (8.0 * Math.pow(2.0, -zoomLevel)) + (0.05 * (zoomLevel / 20.0));
		
		//Work out the transparency of the new marker based on the current zoom level
		double transparency = 0.8 * ((20.0 - zoomLevel) / 20.0);
		
		//Create the corner of the polygon
		LatLng tl = LatLng.newInstance(lat - size, lng + size);
		LatLng tr = LatLng.newInstance(lat + size, lng + size);
		LatLng br = LatLng.newInstance(lat + size, lng - size);
		LatLng bl = LatLng.newInstance(lat - size, lng - size);

		//If the user has chosen to highlight this place then make the marker red
		Polygon newMarker = null;
		if (_highlightedPlaces.contains(p.getName()))
		{
			newMarker = new Polygon(new LatLng[] { tl, tr, br, bl, tl }, borderColour, 1, 1, "#FF7F27", transparency);
		}
		//Otherwise make it the normal colour
		else
		{
			newMarker = new Polygon(new LatLng[] { tl, tr, br, bl, tl }, borderColour, 1, 1, fillColour, transparency);
		}

		// When the marker is clicked it will show its address in a info box
		newMarker.addPolygonClickHandler(new MarkerClickHandler(fillColour, p, newPoint, colourInfo));

		return newMarker;
	}

	/**
	 * Shows an info box above the marker when the marker is clicked
	 */
	public class MarkerClickHandler implements PolygonClickHandler
	{
		String _fillColour = "";
		Boolean _colourInfo = false;
		LatLng _point = null;
		Place _place = null;

		public MarkerClickHandler(String fillColour, Place place, LatLng point, Boolean colourInfo)
		{
			_fillColour = fillColour;
			_place = place;
			_point = point;
			_colourInfo = colourInfo;
		}

		public void onClick(PolygonClickEvent event)
		{
			InfoWindow info = _map.getInfoWindow();
			if (_colourInfo)
			{
				info.open(_point, new InfoWindowContent(createPlaceInformationString(_place)));
			}
			else
			{
				info.open(_point, new InfoWindowContent("<span style=\"background: " + _fillColour + "; color: " + _fillColour + ";\">...</span> " + createPlaceInformationString(_place)));
			}
		}
	}

	/**
	 * Takes a place object and creates a string with information about that place
	 * @param p is the place to make an information string from
	 * @return the information string
	 */
	public String createPlaceInformationString(Place p)
	{
		//If we do not have a valid place then stop
		if(p == null)
		{
			return null;
		}
		
		//Add the place name to the string
		StringBuilder s = new StringBuilder(p.getName());
		
		//Add the full parent name to the string
		if (p.getParentPlaceName() != null)
		{
			s.append(" in " + p.getParentPlaceName());
		}

		//If we have population information then add it to the string
		if (p.getPopulation() > 0)
		{
			StringBuilder pop = new StringBuilder(Long.toString(p.getPopulation()));

			//Insert commas into long numbers
			for (int i = pop.length() - 3; i > 0; i -= 3)
			{
				pop.insert(i, ',');
			}

			s.append("<br/>Population: " + pop);
		}

		//If we have coordinates then add those to the string as well
		if (p.getLongitude() != null && p.getLatitude() != null)
		{
			StringBuilder lat = new StringBuilder(Float.toString(p.getLatitude()));
			StringBuilder lng = new StringBuilder(Float.toString(p.getLongitude()));

			//Shorten coordinates to 3 decimal places
			int i = 0;
			while (lat.charAt(i++) != '.');
			int j = 0;
			while (lng.charAt(j++) != '.');

			if ((i + 3) < lat.length())
			{
				lat.delete(i + 3, lat.length());
			}

			if ((j + 3) < lng.length())
			{
				lng.delete(j + 3, lng.length());
			}

			s.append("<br/>Coordinates: " + lat + ", " + lng);
		}

		return s.toString();
	}

	/**
	 * Make a call to the server that gets places that match the given criteria
	 */
	public void updateCurrentPlaces()
	{
		_findPlaceService.getPlaces(0, 100, 0, true, true, true, _MAXVISIBLEMARKERS, setUpGetPlacesCallback());
	}

	/**
	 * Displays all of the place sets created from a text search
	 */
	public void displayMultiplePlaceSets()
	{
		//Clear the map
		_map.clearOverlays();

		//Go through each place set and if it has been marked visible then add it
		for (int j = 0; j < _currentMultiPlaces.size(); j++)
		{
			logToConsole("Adding set " + j + " to the map");
			if (_visiblePlaceSets.get(j))
			{
				addPlacesToMap(_currentMultiPlaces.get(j), "#FFFFFF", numToColour(j, _currentMultiPlaces.size(), 100, 255), true);
			}
		}
		_statusBar.removeUpdate("DisPlaces");
	}

	/**
	 * Creates a colour from the given parameters
	 * @param num is the specified index
	 * @param max is the total number of indexes
	 * @param cmin is the minimum colour
	 * @param cmax is the maximum colour
	 * @return the colour string (e.g. #F35A22)
	 */
	public String numToColour(int num, int max, int cmin, int cmax)
	{
		logToConsole("num = " + num + ", max = " + max + ", cmin = " + cmin + ", cmax = " + cmax);
		//Make sure num and max are good numbers
		if(num < 0){num = 0;}
		if(max <= 0){return "#000000";}	
		if(max > 255){max = 255;}
		if(num > max){num = max;}
		
		//Work out where this colour will lie in the spectrum
		int colourNum = (int) ((((float) num) / ((float) max)) * ((cmax - cmin) * 8));

		String red = null;
		String green = null;
		String blue = null;

		//If the colour is in the first section then make it a mix between red and green
		if (colourNum <= (cmax - cmin) * 2)
		{
			if (colourNum <= (cmax - cmin))
			{
				red = Integer.toHexString(colourNum + cmin);
				green = "00";
				blue = "00";
			}
			else
			{
				colourNum -= (cmax - cmin);
				red = Integer.toHexString(cmax);
				green = Integer.toHexString(colourNum + cmin);
				blue = "00";
			}
		}
		//If the colour is in the second section then make it a mix between green and blue
		else if (colourNum <= (cmax - cmin) * 4)
		{
			colourNum -= ((cmax - cmin) * 2);
			if (colourNum <= (cmax - cmin))
			{
				red = "00";
				green = Integer.toHexString(colourNum + cmin);
				blue = "00";
			}
			else
			{
				colourNum -= (cmax - cmin);
				red = "00";
				green = Integer.toHexString(cmax);
				blue = Integer.toHexString(colourNum + cmin);
			}
		}
		//If the colour is in the third section then make it a mix between red and blue
		else if (colourNum <= (cmax - cmin) * 6)
		{
			colourNum -= ((cmax - cmin) * 4);
			if (colourNum <= (cmax - cmin))
			{
				red = "00";
				green = "00";
				blue = Integer.toHexString(colourNum + cmin);
			}
			else
			{
				colourNum -= (cmax - cmin);
				red = Integer.toHexString(colourNum + cmin);
				green = "00";
				blue = Integer.toHexString(cmax);
			}
		}
		//If the colour is in the fourth section then make it a mix between all colours
		else
		{
			colourNum -= ((cmax - cmin) * 6);
			colourNum /= 2;
			
			red = Integer.toHexString(colourNum + cmin);
			green = Integer.toHexString(colourNum + cmin);
			blue = Integer.toHexString(colourNum + cmin);
		}

		//If the hex strings are only one character long then add a zero the the front
		if (red.length() < 2){red = "0" + red;}
		if (green.length() < 2){green = "0" + green;}
		if (blue.length() < 2){blue = "0" + blue;}

		logToConsole("#" + red + green + blue);
		return "#" + red + green + blue;
	}

	/**
	 * Takes an array of places and displays them on the map
	 * @param places is the list of places to display
	 * @param borderColour is the colour of the marker borders
	 * @param fillColour is the main colour of the marker
	 * @param colourInfo is whether or not to display a coloured box in the information box of the marker
	 */
	public void addPlacesToMap(ArrayList<Place> places, String borderColour, String fillColour, boolean colourInfo)
	{
		//If we do not have a valid set of places then stop
		if(places == null || places.size() == 0)
		{
			return;
		}
		
		ArrayList<Place> placesSeen = new ArrayList<Place>();

		// Make the best places visible
		for (Place p : places)
		{
			if (!_showAllPlaces)
			{
				//If the place is not directly referenced then don't use it
				if (!p.isDirectlyReferenced())
				{
					continue;
				}

				//If a different place with this name has been chosen then don't use it
				if (_removedPlaces.contains(p.getName()) || (_chosenPlaces.containsKey(p.getName()) && !_chosenPlaces.get(p.getName()).equals(p)))
				{
					continue;
				}

				//Make sure we only add the place once
				boolean found = false;
				for (Place q : placesSeen)
				{
					if (p.getName().equals(q.getName()))
					{
						found = true;
					}
				}

				if (found)
				{
					continue;
				}
				placesSeen.add(p);
			}

			// If there is no latitude and longitude provided for this place
			if (p.getLatitude() == null)
			{
				// Use Google's geocoder to find the place
				findAndMarkAddress(p, borderColour, fillColour, colourInfo);
			}
			// Latitude and longitude are provided in the address
			else
			{
				Polygon marker = createMarkerAtPoint(p, null, borderColour, fillColour, colourInfo);
				// Add the marker to the map
				_map.addOverlay(marker);
			}
		}
	}

	/**
	 * Finds and puts a marker at the given address using Google's geocoder service
	 * @param address is the address to find and mark (e.g. Hamilton, Waikato, New Zealand)
	 * @param index is where in the list of place locations this is to be added
	 */
	private void findAndMarkAddress(final Place place, final String borderColour, final String fillColour, final boolean colourInfo)
	{
		String address = null;

		if (place.getParentPlaceName() != null)
		{
			address = place.getName() + ", " + place.getParentPlaceName();
		}
		else
		{
			address = place.getName();
		}

		// Make the call to the geocoder service
		_geocoder.getLatLng(address, new LatLngCallback()
		{
			// Ignore the place if it is not valid
			public void onFailure()
			{
				// LogToConsole("Failed");
			}

			// If the geocoder service found the place
			public void onSuccess(final LatLng point)
			{
				// LogToConsole("Found");
				Polygon marker = createMarkerAtPoint(place, point, borderColour, fillColour, colourInfo);

				place.setLatitude((float) point.getLatitude());
				place.setLongitude((float) point.getLongitude());

				// Add the marker to the map
				_map.addOverlay(marker);
			}
		});
	}

	public AsyncCallback<ArrayList<Place>> setUpGetPlacesCallback()
	{
		AsyncCallback<ArrayList<Place>> callback = new AsyncCallback<ArrayList<Place>>()
		{
			public void onFailure(Throwable caught)
			{
				logToConsole("Get Places FAIL" + caught.getMessage());
			}

			// This is called when a successful call to findAllPlaces() is
			// completed
			public void onSuccess(ArrayList<Place> places)
			{
				logToConsole("Get Places Success!");
				_currentPlaces = places;

				addPlacesToMap(places, "#FFFFFF", "#00FF00", _searchResultsPage);
				_updatingMap = false;
			}
		};

		return callback;
	}

	/**
	 * Creates the callback object that is used when the call to the server returns
	 * @return the callback object
	 */
	public AsyncCallback<Boolean> setUpFindPlacesCallback()
	{
		return new AsyncCallback<Boolean>()
		{
			/**
			 * This is called when the call to the server fails
			 */
			public void onFailure(Throwable caught)
			{
				_statusBar.removeUpdate("TextSearch");
				logToConsole("Find places fail" + caught.getMessage());
			}

			/**
			 * This is called when the call to the server succeeds
			 * @param result is a dummy variable that does not do anything
			 */
			public void onSuccess(Boolean result)
			{
				//Remove the "searching for places" status bar message
				_statusBar.removeUpdate("TextSearch");

				logToConsole("Find Places success!");
				
				//Get the marked up text from the server
				_findPlaceService.getMarkedUpText(setUpGetMarkedUpTextCallback());
				
				//Get the found places from the server
				updateCurrentPlaces();

				//Clear the map
				_map.clearOverlays();
				
				//Add the new places to the map
				addPlacesToMap(_currentPlaces, "#FFFFFF", "#00FF00", _searchResultsPage);
				
				//Remove the "map loading" message from the map
				_mapLoadingLabel.removeFromParent();
			}
		}; 
	}

	public AsyncCallback<ArrayList<ArrayList<Place>>> setUpFindMultiplePlacesCallback()
	{
		AsyncCallback<ArrayList<ArrayList<Place>>> callback = new AsyncCallback<ArrayList<ArrayList<Place>>>()
		{
			public void onFailure(Throwable caught)
			{
				alert("Find multi places fail" + caught.getMessage());
			}

			public void onSuccess(ArrayList<ArrayList<Place>> places)
			{
				_statusBar.removeUpdate("SearchLoad");
				_statusBar.addUpdate("Displaying places", "DisPlaces");

				_currentMultiPlaces = places;
				_visiblePlaceSets = new ArrayList<Boolean>();
				for (int i = 0; i < _currentMultiPlaces.size(); i++)
				{
					_visiblePlaceSets.add(true);
				}

				_mapLoadingLabel.removeFromParent();
				displayMultiplePlaceSets();
				addElementsToSearchResults(places);
			}
		};
		return callback;
	}

	public AsyncCallback<Boolean> setUpCheckGazetteerCallback()
	{
		AsyncCallback<Boolean> callback = new AsyncCallback<Boolean>()
		{
			public void onFailure(Throwable caught)
			{
				logToConsole("Check Gazetteer fail " + caught.getMessage());
			}

			public void onSuccess(Boolean result)
			{
				if (!result)
				{
					_statusBar.addUpdate("Server is loading gazetteer", "GazLoad");

					_findPlaceService.loadGazetteer(setUpLoadGazetteerCallback());
				}
				else
				{
					logToConsole("Gazetteer Already Loaded!");
					_gazetteerLoaded = true;
				}
			}
		};
		return callback;
	}

	/**
	 * Checks to see if the current page has text from a document so that it can be checked for places
	 */
	public void checkDocText()
	{
		//Get the document text element if it exists
		Element documentText = getDocTextElement();

		//If it does not exist then stop
		if (documentText == null)
		{
			logToConsole("document text div not found");
			if (_mapVisible)
			{
				_map.setVisible(false);
				_mapVisible = false;
			}
			_viewPanel.setVisible(false);
			return;
		}
		
		logToConsole("Document text found!");
		
		//Allow the user to change the method they wish to use view the text
		_viewPanel.setVisible(true);

		//If the view type is not set to normal then remove the TOC
		if (_textViewSelector.getSelectedIndex() > 0)
		{
			HTML.wrap(getElementById("rightSidebar")).setVisible(false);
		}

		//Create the document menu if we have not already done so
		if (_currentDocumentMenu == null)
		{
			createDocumentMenu();
		}

		//Create the timer that closes the document menu when it is not in focus
		_menuOutTimer = new Timer()
		{
			public void run()
			{
				_currentDocumentMenu.hideMenu();
			}
		};

		//Let the user know that the system is searching for places
		_statusBar.addUpdate("Searching text for places", "TextSearch");

		//Clear the map
		_map.clearOverlays();
		
		//Make the map visible
		_mapVisible = true;
		_map.setVisible(true);
		
		//Add the "Loading" message to the map
		_contentDiv.add(_mapLoadingLabel);

		_mapLoadingLabel.getElement().getStyle().setProperty("left", ((int) (_headerDiv.getOffsetWidth() * (3.0 / 4.0) - _mapLoadingLabel.getOffsetWidth() / 2)) + "px");
		_mapLoadingLabel.getElement().getStyle().setProperty("top", (_headerDiv.getOffsetHeight() + _contentDiv.getOffsetHeight() / 3) + "px");

		//Resize the page elemens
		_resize = true;

		//Send the document text to the server to be checked for places
		final Element finalDocumentText = documentText;
		Timer checkLoadTimer = new Timer()
		{
			public void run()
			{
				if (_gazetteerLoaded)
				{
					this.cancel();
					_findPlaceService.findPlacesInText(finalDocumentText.getInnerText(), finalDocumentText.getInnerHTML(), setUpFindPlacesCallback());
				}
			}
		};
		checkLoadTimer.scheduleRepeating(1000);
	}

	/**
	 * Creates and returns the callback object used when the call to the server returns
	 * @return the callback object
	 */
	public AsyncCallback<String> setUpGetMarkedUpTextCallback()
	{
		return new AsyncCallback<String>()
		{
			/**
			 * This is called when the call to the server fails
			 */
			public void onFailure(Throwable caught)
			{
				logToConsole("Mark up text failed -> " + caught.getMessage());
			}

			/**
			 * This is called when the call the to server succeeds
			 * @param markedUpText is the text (marked up with place names) returned from the server
			 */
			public void onSuccess(String markedUpText)
			{
				logToConsole("Mark up text success");

				//Get the unmodified document text element from the web page
				Element documentText = getDocTextElement();

				//Set the document text HTML to be the marked up text
				documentText.setInnerHTML(markedUpText);

				//Get the spans in the marked up text that are place names
				JsArray<Element> textPlaces = getElementsByTagName("span");
				ArrayList<Element> placeElements = new ArrayList<Element>();

				//Add the place spans to the array
				for (int j = 0; j < textPlaces.length(); j++)
				{
					if (textPlaces.get(j).getAttribute("class").equals("place"))
					{
						placeElements.add(textPlaces.get(j));
					}
				}

				logToConsole("Adding events to " + placeElements.size() + " place spans");

				//Add mouse over and mouse out handlers to each span
				for (Element e : placeElements)
				{
					Label newLabel = Label.wrap(e);
					newLabel.addMouseOverHandler(new PlaceMouseOverHandler());
					newLabel.addMouseOutHandler(new PlaceMouseOutHandler());
					newLabel.addMouseUpHandler(new PlaceMouseUpHandler());
				}

				//If Fisheye view is selected in the list box then use it
				if (_textViewSelector.getSelectedIndex() == 1)
				{
					_statusBar.addUpdate("Loading fisheye view", "FishLoad");
					setUpFishEye();
					_statusBar.removeUpdate("FishLoad");
					_fishEyeHandlerReg = _gsPanel.addMouseMoveHandler(new FishEyeMouseMoveHandler());
				}

				//If Column view is selected in the list box then use it
				if (_textViewSelector.getSelectedIndex() == 2)
				{
					setUpColumnView(documentText);
				}
			}
		};
	}

	public void setUpColumnView(Element documentText)
	{
		_statusBar.addUpdate("Loading column view", "ColLoad");

		logToConsole("Wrapping doc text");
		HTML docText = HTML.wrap(documentText);
		int width = docText.getOffsetWidth();
		String html = docText.getHTML();
		ArrayList<String> words = new ArrayList<String>();

		int inTag = 0;
		boolean inWord = false;

		StringBuilder currentWord = new StringBuilder();

		for (int i = 0; i < html.length(); i++)
		{
			boolean add = true;
			char c = html.charAt(i);

			if (inTag > 0 && c == '>')
			{
				inTag--;

				if (inTag == 0)
				{
					words.add(currentWord.toString() + ">");
					currentWord = new StringBuilder();
					add = false;
				}
			}
			else if (c == '<')
			{
				inTag++;

				if (inWord)
				{
					inWord = false;
					words.add(currentWord.toString());
					currentWord = new StringBuilder();
				}
			}
			else if (("" + c).matches("\\s"))
			{
				if (inTag == 0)
				{
					add = false;
				}

				if (inWord)
				{
					inWord = false;
					words.add(currentWord.toString());
					currentWord = new StringBuilder();
				}
			}
			else
			{
				if (inTag == 0 && !inWord)
				{
					inWord = true;
				}
			}

			if (add)
			{
				currentWord.append(c);
			}
		}

		docText.setHTML("");

		HorizontalPanel columns = new HorizontalPanel();
		ArrayList<Label> labels = new ArrayList<Label>();
		logToConsole("Adding columns to panel");

		if (_contentDiv.getWidget(0).getElement().getId() == "gs_content")
		{
			VerticalPanel newDocTextPanel = new VerticalPanel();
			newDocTextPanel.add(columns);
			newDocTextPanel.add(_contentDiv.getWidget(0));
			newDocTextPanel.getElement().setId("columnBox");
			_contentDiv.insert(newDocTextPanel, 0);
		}
		else
		{
			((VerticalPanel) (_contentDiv.getWidget(0))).remove(0);
			((VerticalPanel) (_contentDiv.getWidget(0))).insert(columns, 0);
		}

		float fontSize = 12;
		do
		{
			logToConsole("Looping");
			Label l = new Label();
			labels.add(l);
			columns.add(l);

			for (int j = 0; j < labels.size(); j++)
			{
				StringBuilder labelText = new StringBuilder();
				for (int i = (words.size() / labels.size()) * j; i < ((j < (labels.size() - 1)) ? (((words.size() / labels.size()) * (j + 1)) - 1) : (words.size())); i++)
				{
					labelText.append(words.get(i) + " ");
				}

				labels.get(j).getElement().setInnerHTML(labelText.toString());
				labels.get(j).setWidth((width / labels.size()) + "px");
				labels.get(j).getElement().getStyle().setProperty("fontSize", fontSize + "px");
			}
			fontSize *= 0.85;

			logToConsole("Font size = " + fontSize);
		} while (columns.getOffsetHeight() > (Window.getClientHeight() - _headerDiv.getOffsetHeight() - _footerDiv.getOffsetHeight() - 70));

		for (int i = 0; i < labels.size(); i++)
		{
			ArrayList<String> subList = new ArrayList<String>();
			for (int j = (words.size() / labels.size()) * i; j < ((i < (labels.size() - 1)) ? (((words.size() / labels.size()) * (i + 1)) - 1) : (words.size())); j++)
			{
				subList.add(words.get(j));
			}
			labels.get(i).addMouseMoveHandler(new ColumnViewMouseMoveHandler(labels.get(i), subList));
		}

		columns.add(_columnViewBox);
		_columnViewBox.getElement().getStyle().setProperty("position", "absolute");
		_columnViewBox.getElement().getStyle().setProperty("border", "2px solid");
		_columnViewBox.getElement().getStyle().setProperty("background", "#FFFFFF");
		_columnViewBox.getElement().getStyle().setProperty("fontSize", "10px");
		_columnViewBox.setPixelSize(250, 100);
		_columnViewBox.setVisible(false);

		_statusBar.removeUpdate("ColLoad");
	}

	class ColumnViewMouseMoveHandler implements MouseMoveHandler
	{
		Label _label = null;
		ArrayList<String> _words = null;

		public ColumnViewMouseMoveHandler(Label label, ArrayList<String> words)
		{
			_label = label;
			_words = words;
		}

		public void onMouseOut(MouseOutEvent event)
		{
			_columnViewBox.setVisible(false);
		}

		public void onMouseOver(MouseOverEvent event)
		{
			_columnViewBox.setVisible(true);
		}

		public void onMouseMove(MouseMoveEvent event)
		{
			_columnViewBox.setVisible(true);

			JsArray<Element> divs = getElementsByTagName("div");
			Element documentText = null;
			for (int i = 0; i < divs.length(); i++)
			{
				if (divs.get(i).getAttribute("class").equals("documenttext"))
				{
					documentText = divs.get(i);
					break;
				}
			}

			if (event.getY() < 125)
			{
				_columnViewBox.getElement().getStyle().setProperty("top", (event.getY() + _label.getAbsoluteTop() + 25) + "px");
			}
			else
			{
				_columnViewBox.getElement().getStyle().setProperty("top", (event.getY() + _label.getAbsoluteTop() - 125) + "px");
			}

			if (event.getX() + _label.getAbsoluteLeft() - 125 < documentText.getAbsoluteLeft())
			{
				_columnViewBox.getElement().getStyle().setProperty("left", documentText.getAbsoluteLeft() + "px");
			}
			else if (event.getX() + _label.getAbsoluteLeft() + 125 > documentText.getOffsetWidth() + documentText.getAbsoluteLeft())
			{
				_columnViewBox.getElement().getStyle().setProperty("left", documentText.getOffsetWidth() + documentText.getAbsoluteLeft() - 250 + "px");
			}
			else
			{
				_columnViewBox.getElement().getStyle().setProperty("left", (event.getX() + _label.getAbsoluteLeft() - 125) + "px");
			}

			int midWordIndex = (int) (((float) (event.getY()) / ((_label.getAbsoluteTop() + _label.getOffsetHeight()) - _label.getAbsoluteTop())) * _words.size());
			int topWord = midWordIndex - 25;
			int botWord = midWordIndex + 25;

			if (_words.size() < 50)
			{
				topWord = 0;
				botWord = _words.size();
			}
			else
			{
				if (topWord < 0)
				{
					topWord = 0;
				}
				else if (topWord > _words.size() - 50)
				{
					topWord = _words.size() - 50;
				}

				if (botWord > _words.size())
				{
					botWord = _words.size();
				}
				else if (botWord < 50)
				{
					botWord = 50;
				}
			}

			StringBuilder s = new StringBuilder();

			for (int i = topWord; i < botWord; i++)
			{
				s.append(_words.get(i) + " ");
			}

			_columnViewBox.getElement().setInnerHTML(s.toString());
		}
	}

	public AsyncCallback<Boolean> setUpLoadGazetteerCallback()
	{
		AsyncCallback<Boolean> callback = new AsyncCallback<Boolean>()
		{
			public void onFailure(Throwable caught)
			{
				logToConsole("Loading Gazetteer failed ->" + caught.getMessage());
			}

			public void onSuccess(Boolean success)
			{
				logToConsole("Loading Gazetteer success!");
				_statusBar.removeUpdate("GazLoad");
				_gazetteerLoaded = true;
			}
		};

		return callback;
	}

	public void createDocumentMenu()
	{
		// Menu
		final Menu documentMenu = new Menu(_footerDiv.getElement(), new MouseOverHandler()
		{
			public void onMouseOver(MouseOverEvent event)
			{
				if(_menuOverTimer != null)
				{
					_menuOverTimer.cancel();
					_menuOverTimer = null;
				}
				Label item = (Label) event.getSource();
				item.getElement().getStyle().setProperty("background", "#0000FF");
				item.getElement().getStyle().setProperty("color", "#FFFFFF");
				item.getElement().getStyle().setProperty("border", "1px solid");
				if (_menuOutTimer != null)
				{
					_menuOutTimer.cancel();
					_menuOutTimer = null;
				}
			}
		}, new MouseOutHandler()
		{
			public void onMouseOut(MouseOutEvent event)
			{
				// logToConsole("MENU OUT!");
				Label item = (Label) event.getSource();
				item.getElement().getStyle().setProperty("background", "#FFFFFF");
				item.getElement().getStyle().setProperty("color", "#000000");
				item.getElement().getStyle().setProperty("border", "1px solid");

				_menuOutTimer = new Timer()
				{
					public void run()
					{
						_currentDocumentMenu.hideMenu();
					}
				};

				_menuOutTimer.schedule(500);
			}
		});

		// Menu items
		ArrayList<MenuItem> menuItems = new ArrayList<MenuItem>();
		menuItems.add(new MenuItem("Centre this place on the map", new ClickHandler()
		{
			public void onClick(ClickEvent event)
			{
				Place place = null;

				logToConsole("Center clicked, selected place name = " + _selectedPlaceName);

				if ((place = _chosenPlaces.get(_selectedPlaceName)) == null)
				{
					for (int i = 0; i < _currentPlaces.size(); i++)
					{
						if (_currentPlaces.get(i).getName().equals(_selectedPlaceName))
						{
							place = _currentPlaces.get(i);
							break;
						}
					}
				}

				if (place != null && place.getLatitude() != null && place.getLongitude() != null)
				{
					logToConsole("Centering map");
					_map.setCenter(LatLng.newInstance(place.getLatitude(), place.getLongitude()));
				}

				_currentDocumentMenu.hideMenu();
			}
		}));

		menuItems.add(new MenuItem("Highlight this place on the map", new ClickHandler()
		{
			public void onClick(ClickEvent event)
			{
				_highlightedPlaces.add(_selectedPlaceName);
				_map.clearOverlays();
				logToConsole("Overlays cleared");
				addPlacesToMap(_currentPlaces, "#FFFFFF", "#00FF00", false);
				logToConsole("Places added");

				_currentDocumentMenu.hideMenu();
			}
		}));

		menuItems.add(new MenuItem("Highlight this place in the text", new ClickHandler()
		{
			public void onClick(ClickEvent event)
			{
				_highlightedTextPlaces.add(_selectedPlaceName);

				JsArray<Element> places = getElementsByTagName("span");

				for (int i = 0; i < places.length(); i++)
				{
					Element e = places.get(i);

					if (e.getClassName() == "place" && e.getInnerText() == _selectedPlaceName)
					{
						e.getStyle().setProperty("background", "#FF0000");
					}
				}

				_currentDocumentMenu.hideMenu();
			}
		}));
		
		menuItems.add(new MenuItem("Remove all highlights", new ClickHandler()
		{
			public void onClick(ClickEvent event)
			{
				_highlightedPlaces.clear();

				_map.clearOverlays();
				addPlacesToMap(_currentPlaces, "#FFFFFF", "#00FF00", false);

				JsArray<Element> places = getElementsByTagName("span");

				for (int i = 0; i < places.length(); i++)
				{
					Element e = places.get(i);

					if (e.getClassName() == "place" && _highlightedTextPlaces.contains(e.getInnerText()))
					{
						e.getStyle().setProperty("background", "#FFFF00");
					}
				}
				_highlightedTextPlaces.clear();

				_currentDocumentMenu.hideMenu();
			}
		}));
		
		final MenuItem prevMenuItem = new MenuItem("Remove this place", new ClickHandler()
		{
			public void onClick(ClickEvent event)
			{
				_removedPlaces.add(_selectedPlaceName);
				_currentDocumentMenu.hideMenu();
				
				JsArray<Element> textPlaces = getElementsByTagName("span");

				//Add the place spans to the array
				for (int j = 0; j < textPlaces.length(); j++)
				{
					Element e = textPlaces.get(j);
					if (e.getAttribute("class").equals("place") && e.getInnerText().equals(_selectedPlaceName))
					{
						e.getParentElement().insertBefore(e.getFirstChild(), e);
					}
				}
				
				_map.clearOverlays();
				addPlacesToMap(_currentPlaces, "#FFFFFF", "#00FF00", false);
			}
		});
		menuItems.add(prevMenuItem);

		menuItems.add(new MenuItem("Choose correct place >", new ClickHandler()
		{
			public void onClick(ClickEvent event)
			{
				logToConsole("MARKER!");
				ArrayList<MenuItem> places = new ArrayList<MenuItem>();

				ArrayList<Place> matchingPlaces = new ArrayList<Place>();

				for (Place p : _currentPlaces)
				{
					if (p.getName().equals(_selectedPlaceName))
					{
						matchingPlaces.add(p);
					}
				}
				
				for (final Place p : matchingPlaces)
				{
					places.add(new MenuItem(p.getName() + ((p.getParentPlaceName() != null) ? (", " + p.getParentPlaceName()) : (" (Country)")), new ClickHandler()
					{
						public void onClick(ClickEvent e)
						{
							_chosenPlaces.put(_selectedPlaceName, p);

							_map.clearOverlays();
							logToConsole("Chose a new place!!!");
							addPlacesToMap(_currentPlaces, "#FFFFFF", "#00FF00", false);
							logToConsole("Places added!!!");

							_currentDocumentMenu.hideMenu();
						}
					}));
				}

				Menu placeMenu = new Menu(_footerDiv.getElement(), new MouseOverHandler()
				{
					public void onMouseOver(MouseOverEvent event)
					{
						Label item = (Label) event.getSource();
						item.getElement().getStyle().setProperty("background", "#0000FF");
						item.getElement().getStyle().setProperty("color", "#FFFFFF");
						if (_menuOutTimer != null)
						{
							_menuOutTimer.cancel();
							_menuOutTimer = null;
						}
					}
				}, new MouseOutHandler()
				{
					public void onMouseOut(MouseOutEvent event)
					{
						Label item = (Label) event.getSource();
						item.getElement().getStyle().setProperty("background", "#FFFFFF");
						item.getElement().getStyle().setProperty("color", "#000000");

						_menuOutTimer = new Timer()
						{
							public void run()
							{
								_currentDocumentMenu.hideMenu();
							}
						};

						_menuOutTimer.schedule(500);
					}
				});

				documentMenu.addChildMenu(placeMenu);
				placeMenu.addMenuItems(places);
				placeMenu.moveMenu(_currentDocumentMenu.getMenuDiv().getAbsoluteLeft() + _currentDocumentMenu.getMenuDiv().getOffsetWidth(), prevMenuItem.getMenuElement().getAbsoluteTop() + prevMenuItem.getMenuElement().getOffsetHeight() - 2);
				placeMenu.showMenu();
			}
		}){
			public boolean condition()
			{
				ArrayList<Place> matchingPlaces = new ArrayList<Place>();

				if(_currentPlaces == null || _currentPlaces.size() == 0)
				{
					return false;
				}
				
				for (Place p : _currentPlaces)
				{
					if (p.getName().equals(_selectedPlaceName))
					{
						matchingPlaces.add(p);
					}
				}
				
				if(matchingPlaces.size() > 1)
				{
					return true;
				}
				return false;
			}
		});

		documentMenu.addMenuItems(menuItems);
		documentMenu.hideMenu();
		_currentDocumentMenu = documentMenu;
	}

	public void startDocumentMouseOverTimer(final int x, final int y)
	{
		_documentMouseOverTimer = new Timer()
		{
			public void run()
			{
				_currentDocumentMenu.moveMenu(x, y);
				_currentDocumentMenu.showMenu();

				_documentMouseOverTimer = null;
			}
		};

		_documentMouseOverTimer.schedule(1000);
	}

	/**
	 * Gets the document text element from the web page
	 * @return the document text element
	 */
	public Element getDocTextElement()
	{
		JsArray<Element> divs = getElementsByTagName("div");

		Element documentText = null;
		for (int i = 0; i < divs.length(); i++)
		{
			if (divs.get(i).getAttribute("class").equals("documenttext"))
			{
				documentText = divs.get(i);
				break;
			}
		}

		return documentText;
	}

	public void addElementsToSearchResults(ArrayList<ArrayList<Place>> places)
	{
		JsArray<Element> elements = getElementsByTagName("li");
		ArrayList<Element> listElements = new ArrayList<Element>();

		for (int i = 0; i < elements.length(); i++)
		{
			if (elements.get(i).getAttribute("class").equals("document"))
			{
				listElements.add(elements.get(i));
			}
		}

		for (int i = 0; i < listElements.size(); i++)
		{
			Element span = DOM.createSpan();
			listElements.get(i).insertBefore(span, listElements.get(i).getFirstChild());
			span.getStyle().setBackgroundColor(numToColour(i, listElements.size(), 100, 255));
			span.getStyle().setColor(numToColour(i, listElements.size(), 100, 255));
			span.setInnerText("...");

			Element checkBoxSpan = DOM.createSpan();
			listElements.get(i).insertBefore(checkBoxSpan, listElements.get(i).getFirstChild());
			checkBoxSpan.setId("Result" + i);

			CheckBox c = new CheckBox();
			c.setValue(true);
			c.addValueChangeHandler(new SearchResultsCheckBoxChangeHandler(i));
			RootPanel.get("Result" + i).add(c);
		}
	}

	public class SearchResultsCheckBoxChangeHandler implements ValueChangeHandler<Boolean>
	{
		int _index = -1;

		public SearchResultsCheckBoxChangeHandler(int index)
		{
			_index = index;
		}

		public void onValueChange(ValueChangeEvent<Boolean> event)
		{
			if (event.getValue())
			{
				_visiblePlaceSets.set(_index, true);
			}
			else
			{
				_visiblePlaceSets.set(_index, false);
			}

			_map.clearOverlays();
			displayMultiplePlaceSets();
		}
	}

	public class DocumentOverHandler implements MouseOverHandler
	{
		int _index = -1;

		public DocumentOverHandler(int index)
		{
			_index = index;
		}

		public void onMouseOver(MouseOverEvent event)
		{
			Label currentLabel = (Label) event.getSource();
			currentLabel.setStyleName("placeOver");

			startDocumentMouseOverTimer(event.getX() + currentLabel.getAbsoluteLeft(), event.getY() + currentLabel.getAbsoluteTop());
		}
	}

	public class PlaceMouseOverHandler implements MouseOverHandler
	{
		public void onMouseOver(MouseOverEvent event)
		{
			Label currentLabel = (Label) event.getSource();
			currentLabel.setStyleName("placeOver");
			
			_selectedPlaceName = currentLabel.getElement().getInnerText();

			startPlaceMouseOverTimer(event.getX() + currentLabel.getAbsoluteLeft(), event.getY() + currentLabel.getAbsoluteTop());
		}
	}

	public class PlaceMouseOutHandler implements MouseOutHandler
	{
		public void onMouseOut(MouseOutEvent event)
		{
			Label currentLabel = (Label) event.getSource();
			currentLabel.setStyleName("place");

			if (_placeMouseOverTimer != null)
			{
				_placeMouseOverTimer.cancel();
				_placeMouseOverTimer = null;
			}
		}
	}
	
	public class PlaceMouseUpHandler implements MouseUpHandler
	{
		public void onMouseUp(MouseUpEvent event)
		{
			Label currentLabel = (Label) event.getSource();
			currentLabel.setStyleName("place");
			if(event.getNativeButton() == NativeEvent.BUTTON_LEFT)
			{
				String placeName = currentLabel.getText();
				Place place = _chosenPlaces.get(placeName);
				
				if(place == null)
				{
					for(Place p : _currentPlaces)
					{
						if(p.getName().equals(placeName))
						{
							place = p;
							break;
						}
					}
				}
				
				if(place != null && place.getLatitude() != null && place.getLongitude() != null)
				{
					_map.setCenter(LatLng.newInstance(place.getLatitude(), place.getLongitude()));
				}
			}
			/*
			else if(event.getNativeButton() == NativeEvent.BUTTON_RIGHT)
			{
				_currentDocumentMenu.moveMenu(event.getX() + currentLabel.getAbsoluteLeft(), event.getY() + currentLabel.getAbsoluteTop());
				_currentDocumentMenu.showMenu();
			}
			*/
		}
	}
	
	public void startPlaceMouseOverTimer(final int x, final int y)
	{
		_placeMouseOverTimer = new Timer()
		{
			public void run()
			{
				_currentDocumentMenu.refreshMenu();
				_currentDocumentMenu.moveMenu(x, y);
				_currentDocumentMenu.showMenu();
				
				_menuOverTimer = new Timer()
				{
					public void run()
					{
						_menuOverTimer = null;
						_currentDocumentMenu.hideMenu();
					}
				};
				_menuOverTimer.schedule(4000);

				_placeMouseOverTimer = null;
			}
		};

		_placeMouseOverTimer.schedule(1000);
	}
	
	public class FishEyeMouseMoveHandler implements MouseMoveHandler
	{
		public void onMouseMove(MouseMoveEvent e)
		{
			if (!_mouseEventPause)
			{
				_mouseEventPause = true;
				passMouseCoordsToFishEye(e.getY());
				Timer pauseTimer = new Timer()
				{
					public void run()
					{
						_mouseEventPause = false;
					}
				};
				pauseTimer.schedule(50);
			}
		}
	}

	/**
	 * This class contains methods that initialise and control the spatial search controls present on the map during a spatial search 
	 */
	public class SpatialSearchControls extends CustomControl
	{
		/**
		 * Inherited parent constructor
		 */
		protected SpatialSearchControls(ControlPosition arg0)
		{
			super(arg0);
		}

		/**
		 * Second inherited parent constructor
		 */
		protected SpatialSearchControls(ControlPosition arg0, boolean arg1, boolean arg2)
		{
			super(arg0, arg1, arg2);
		}

		/**
		 * Initialises the control buttons
		 */
		protected Widget initialize(MapWidget map)
		{
			//Create the panel to store the buttons
			HorizontalPanel panel = new HorizontalPanel();
			
			//Create the button that clears the user's currently created points
			Button clearButton = new Button("Clear Points", new ClickHandler()
			{
				public void onClick(ClickEvent event)
				{
					_areaPoints.clear();
					_map.clearOverlays();
				}
			});
			
			//Create the button the performs the spatial search
			Button performSearch = new Button("Search Area", new ClickHandler()
			{
				public void onClick(ClickEvent event)
				{
					//If the users has marked less than 3 points on the map then we cannot perform a spatial search
					if (_areaPoints.size() < 3)
					{
						//Show an error message on the status bar
						_statusBar.removeUpdate("SpatialWarn");
						_statusBar.addUpdate("A minimum of 3 points is needed on the map to perform a spatial search", "SpatialWarn");

						//Remove the message after 5 seconds
						Timer warningRemoveTimer = new Timer()
						{
							public void run()
							{
								_statusBar.removeUpdate("SpatialWarn");
							}
						};
						warningRemoveTimer.schedule(5000);

						return;
					}

					ArrayList<Float[]> points = new ArrayList<Float[]>();

					//Turn the points into a list of float arrays
					for (LatLng point : _areaPoints)
					{
						points.add(new Float[] { (float) point.getLatitude(), (float) point.getLongitude() });
					}

					//Ask the server to perform the spatial search
					_findPlaceService.spatialSearch(points, getSpatialSearchCallback());
				}
			});

			//Add the two buttons to the panel
			panel.add(clearButton);
			panel.add(performSearch);

			return panel;
		}
		
		/**
		 * Creates and returns the callback necessary to perform a spatial search on the server
		 * @return the callback object
		 */
		public AsyncCallback<ArrayList<String>> getSpatialSearchCallback()
		{
			return new AsyncCallback<ArrayList<String>>()
			{
				/**
				 * If the call to the server failed then this is called
				 * @see com.google.gwt.user.client.rpc.AsyncCallback#onFailure(java.lang.Throwable)
				 */
				public void onFailure(Throwable arg0)
				{
					logToConsole("Spatial search FAIL!");
				}
				
				/**
				 * If the call to the server succeeded then this is called
				 * @param placeNames is the places found within the spatial search polygon
				 */
				public void onSuccess(ArrayList<String> placeNames)
				{
					logToConsole("Spatial search Success!");

					//Create the query to pass to Greenstone
					StringBuffer query = new StringBuffer();
					for (int j = 0; j < placeNames.size() && j < 20; j++)
					{
						if (j == 0)
						{
							query.append(placeNames.get(j).replaceAll(" ", "+").replaceAll("-", "+"));
						}
						else
						{
							query.append("+" + placeNames.get(j).replaceAll(" ", "+").replaceAll("-", "+"));
						}
					}

					logToConsole("Query -> " + query);
					logToConsole("Collection -> " + _currentCollection);

					//Create an invisible form
					FormPanel form = new FormPanel();
					form.setAction(_GREENSTONEDEVURL);
					form.setMethod(FormPanel.METHOD_POST);
					form.getElement().setAttribute("target", "_self");

					VerticalPanel tempForm = new VerticalPanel();
					form.setWidget(tempForm);

					getElementsByTagName("body").get(0).appendChild(form.getElement());

					logToConsole("Adding hidden form elements");

					Hidden a = new Hidden("a", "q");
					tempForm.add(a);
					Hidden sa = new Hidden("sa");
					tempForm.add(sa);
					Hidden rt = new Hidden("rt", "rd");
					tempForm.add(rt);
					Hidden s = new Hidden("s", "TextQuery");
					tempForm.add(s);
					Hidden c = new Hidden("c", _currentCollection);
					tempForm.add(c);
					Hidden startPage = new Hidden("startPage", "1");
					tempForm.add(startPage);
					Hidden s1level = new Hidden("s1.level", "Sec");
					tempForm.add(s1level);
					Hidden s1case = new Hidden("s1.case", "1");
					tempForm.add(s1case);
					Hidden s1stem = new Hidden("s1.stem", "0");
					tempForm.add(s1stem);
					Hidden s1accent = new Hidden("s1.accent", "0");
					tempForm.add(s1accent);
					Hidden s1matchMode = new Hidden("s1.matchMode", "some");
					tempForm.add(s1matchMode);
					Hidden s1sort = new Hidden("s1.sortBy", "1");
					tempForm.add(s1sort);
					Hidden s1index = new Hidden("s1.index", "ZZ");
					tempForm.add(s1index);
					Hidden s1maxDocs = new Hidden("s1.maxDocs", "100");
					tempForm.add(s1maxDocs);
					Hidden s1query = new Hidden("s1.query", query.toString());
					tempForm.add(s1query);

					logToConsole("Submitting");

					form.submit();

					logToConsole("Loading results page");

					loadPageWithoutUrl();
				}
			};
		}

		public boolean isSelectable()
		{
			return false;
		}
	}

	public static FindPlaceServiceAsync getServerConnection()
	{
		return _findPlaceService;
	}
}