<?xml version="1.0" encoding="UTF-8"?>

<Module>
<ModulePrefs
  title="Mappyttss"
  height="500">
  <Require feature="setprefs" />
</ModulePrefs>
<UserPref name="lat" default_value="33.55" datatype="hidden" />
<UserPref name="lng" default_value="151.14" datatype="hidden" />
<UserPref name="zoom" default_value="4" datatype="hidden" />
<UserPref name="type" default_value="MAP" datatype="hidden" />
<UserPref name="markers_comm" default_value="0" datatype="hidden" />
<UserPref name="cur_marker_id" default_value="0" datatype="hidden" />
<UserPref name="lines_comm" default_value="0" datatype="hidden" />
<UserPref name="cur_line_id" default_value="0" datatype="hidden" />


<Content type="html"><![CDATA[
    <script src="http://maps.google.com/maps?file=api&amp;v=2.x&amp;key=ABQIAAAAzr2EBOXUKnm_jVnk0OJI7xSosDVG8KKPE1-m51RBrvYughuyMxQ-i1QfUnH94QxWIa6N4U6MouMmBA"
            type="text/javascript"></script>
    <script type="text/javascript">


    /** Copyright 2008 Google Inc.  All Rights Reserved.

    * @fileoverview This is an XML gadget for Mappy, a collaborative map 
    *  annotation tool. It allows you to draw markers and lines on the 
    *  map, as well as annotating the markers. The state of the map is 
    *  recorded in the UserPrefs of the gadget and as such can be played 
    *  back and shared.
    * @author sushkov@google.com (Oleg Sushkov)
    */


    /** The global map reference.
     * @type {GMap2}
     */
    var map;

    /** The preference structure for the Gadget UserPrefs.
     * @type {_IG_Prefs}
     */
    var prefs;

    /** The current latitude of the map view
     * @type {number}
     */
    var lat;

    /** The current longitude of the map view
     * @type {number}
     */
    var lng;

    /** The current zoom level of the map view
     * @type {number}
     */
    var zoom;

    /** The current type of the map view
     * @type {string}
     */
    var type;


    /** Array of Markers, any change to this array must be reflected in the
     *  "markers_comm" string in the prefs.
     * @type {Array.<Marker>}
     */
    var markers;

    /** A pointer to the Marker which has its infoWindow open, is null if no
     *  infoWindow open.
     * @type {Marker}
     */
    var curActiveMarker;

    /** The id of the next newly created marker, any change must be reflected
     *  in the "markers_comm" string in the prefs.
     * @type {number}
     */
    var curMarkerId;

    /** Array of Lines, any change to this array must be reflected in the
     *  "lines_comm" string in the prefs.
     * @type {Array.<Line>}
     */
    var lines;

    /** The id of the next newly created line, any change must be reflected
     *  in the "lines_comm" string in the prefs.
     * @type {number}
     */
    var curLineId;

    /** Current click mode, ie: what happens when the user clicks on the map
     *  (select, new marker, new line, delete).
     * @type {string}
     */
    var mode;

    /** The latitude and longitude of the last click on the map in line mode.
     * @type {LatLng}
     */
    var prevLinePoint;

    /** A line which is drawn to represent where a new line will be placed if
     *  a use clicks on the map again.
     * @type {Line}
     */
    var curPotentialLine;

    /** Current event handler for a left click.
     * @type {function}
     */
    var clickListener;


////////////////////////////////////////////////////////////////


    /** Initialises the Gadget, initialises all of the global variables, sets
     *  the state according to the UserPrefs, adds all of the required panels
     *  to the map, registers all of the initial even handlers.
     */
    function initialize() {
      if (GBrowserIsCompatible()) {

        // Initialise all of the global variables.
        map = new GMap2(document.getElementById("map_canvas"));
        prefs = new _IG_Prefs(__MODULE_ID__);
        lat = parseFloat(prefs.getString("lat"));
        lng = parseFloat(prefs.getString("lng"));

        zoom = parseFloat(prefs.getString("zoom"));
        type = prefs.getString("type");

        markers = new Array();
        curActiveMarker = null;
        curMarkerId = prefs.getInt("curMarkerId");

        lines = new Array();
        curLineId = prefs.getInt("curLineId");

        mode = "marker_mode";
        prevLinePoint = null;
        curPotentialLine = null

        clickListener = null;

        // Initialise the map
        setMapType(map, type);
        map.setCenter(new GLatLng(lat, lng), zoom);
        map.addControl(new GSmallMapControl());
        map.addControl(new GMapTypeControl());
        map.addControl(new CustomButton());
        map.enableContinuousZoom();

        GEvent.addListener(map, "moveend", onMoveEnd);
        GEvent.addListener(map, "zoomend", onZoomEnd);
        GEvent.addListener(map, "maptypechanged", onMapTypeChange);
        GEvent.addListener(map, "singlerightclick", function(){
            resetLineDrawingState();
        });
        GEvent.addListener(map, "mousemove", onMouseMove);

        gadgets.rpc.register("set_pref", setPref);

        // Load the markers and lines from the stored prefs
        createMarkersFromString(prefs.getString("markers_comm"));
        createLinesFromString(prefs.getString("lines_comm"));
      }
    }


    /** Delete all Markers from the map and from the global array.
     *  This in effect clears the markers array.
     */
    function clearMarkers(){
      for(var i = 0; i < markers.length; i++){
        markers[i].GMMarker.hide();
        map.removeOverlay(markers[i].GMMarker);
        map.getPane(G_MAP_FLOAT_PANE).removeChild(markers[i].tooltip);

        GEvent.removeListener(markers[i].dragHandler);
        GEvent.removeListener(markers[i].clickHandler);

        delete markers[i].GMMarker;
        delete markers[i];
      }

      delete markers;
      markers = new Array();
    }


    /** Delete all Lines from the map and from the global array.
     *  This in effect clears the lines array.
     */
    function clearLines(){
      for(var i = 0; i < lines.length; i++){
        map.removeOverlay(lines[i].GMLine);
        delete lines[i].GMLine;
        delete lines[i];
      }

      delete lines;
      lines = new Array();
    }

    /** Delete the curActiveMarker from the map and from the array.
     *  The curActiveMarker is the marker which currently has its
     *  infoWindow open. This function should only ever be called
     *  when there exists such a marker.
     */
    function deleteActiveMarker(){
      if(curActiveMarker != null){
        deleteMarker(curActiveMarker);
      }
      curActiveMarker = null;
    }


    /** Deletes the given marker from the global "markers" array, as well as all
     *  associated data structures and event handlers.
     * @param {Marker} marker The refence to the marker which is to be deleted.
     */
    function deleteMarker(marker){
      marker.GMMarker.closeInfoWindow();
      map.removeOverlay(marker.GMMarker);
      map.getPane(G_MAP_FLOAT_PANE).removeChild(marker.tooltip);

      var markerIndex = -1;
      for(var i = 0; i < markers.length; i++){
        if(markers[i].id == marker.id){
          markerIndex = i;
          break;
        }
      }

      GEvent.removeListener(marker.dragHandler);
      GEvent.removeListener(marker.clickHandler);

      delete marker.GMMarker;
      delete marker;

      if(markerIndex != -1){
        markers.splice(markerIndex, 1);
      }

      setMarkersCommString();
    }


    /** Deletes the given line from the global "lines" array, as well as all
     *  associated data structures and event handlers.
     * @param {Line} line The refence to the line which is to be deleted.
     */
    function deleteLine(line){
      map.removeOverlay(line.GMLine);

      var lineIndex = -1;
      for(var i = 0; i < lines.length; i++){
        if(lines[i].id == line.id){
          lineIndex = i;
          break;
        }
      }

      GEvent.removeListener(line.clickHandler);

      delete line.GMMarker;
      delete line;

      if(lineIndex != -1){
        lines.splice(lineIndex, 1);
      }

      setLinesCommString();
    }


    /** Deleted the potentialLine, which is the semi-transparent line
     *  drawn on the map to represent the line about to be drawn on the
     *  next click.
     */
    function deletePotentialLine(){
      if(curPotentialLine != null){
        map.removeOverlay(curPotentialLine);
        delete curPotentialLine;
      }
    }


    /** Resets the line drawing state, sets the prevLinePoint to
     *  null, meaning that the next click in line mode will not
     *  draw a line but register that point as the beginning of the
     *  next line. In addition, if a potentialLine exists, it is removed
     *  from the map.
     */
    function resetLineDrawingState(){
      deletePotentialLine();
      prevLinePoint = null;
    }


////////////////////////////////////////////////////////////////


    /** This function gets called when a user clicks on the map in
     *  "marker_mode". As a result a marker is drawn at that point
     *  on the map, and correspondingly added to the global "markers"
     *  array.
     * @param {GOverlay} overlay The reference to an overlay object which was
     *  clicked, if clicked on the map this is null.
     * @param {LatLng} latlng The map coordinates of the click.
     * @param {LatLng} overlaylatlng The coordinates of the overlay element
     *  that was clicked, if click was on the map this is null.
     */
    function createMarker(overlay, latlng, overlaylatlng) {
      if(overlay != null){
        return;
      }

      var newMarker = new Marker(curMarkerId, latlng.lat(), latlng.lng(), "");
      curActiveMarker = newMarker;
      curMarkerId++;
      prefs.set("curMarkerId", curMarkerId);

      markers.push(newMarker);
      map.addOverlay(newMarker.GMMarker);

      openInfoWindowForMarker(newMarker);
      setMarkersCommString(); // write to the prefs about this new marker.
    }


    /** This function gets called when a user clicks on the map in "line_mode".
     *  As a result either a line gets drawn on the map, or if its the first
     *  click the position is recorded and used as the start of the line
     *  upon the next click.
     * @param {GOverlay} overlay The reference to an overlay object which was
     *  clicked, if clicked on the map this is null.
     * @param {LatLng} latlng The map coordinates of the click.
     * @param {LatLng} overlaylatlng The coordinates of the overlay element
     *  that was clicked, if click was on the map this is null.
     */
    function createPolyline(overlay, latlng, overlaylatlng){
      var useCoords = null;
      if(overlay != null){
        useCoords = overlaylatlng;
      }
      else{
        useCoords = latlng;
      }

      if(prevLinePoint != null){
        var newLine = new Line(curLineId,
                               prevLinePoint.lat(),
                               prevLinePoint.lng(),
                               useCoords.lat(),
                               useCoords.lng());

        curLineId = curLineId + 1;
        prefs.set("curLineId", curLineId);

        lines.push(newLine);
        map.addOverlay(newLine.GMLine);

        setLinesCommString();
      }

      prevLinePoint = useCoords;
    }


    /** Given a "markers_comm" string taken from gadget prefs, this function
     *  will create markers based on the string. The resultant markers are put
     *  into the "markers" global array.
     * @param {string} mstring The encoded string containing the marker info
     *  from UserPrefs.
     */
    function createMarkersFromString(mstring){
      var markersCommData = mstring.split("|");
      for(var i = 0; i < markersCommData[0]; i++){
        var curMarkerData = markersCommData[i+1].split("&");
        var id = curMarkerData[0];
        var latValue = parseFloat(curMarkerData[1]);
        var lngValue = parseFloat(curMarkerData[2]);
        var annotation = curMarkerData[3];

        var newMarker = new Marker(id, latValue, lngValue, annotation);
        markers.push(newMarker);
        map.addOverlay(newMarker.GMMarker);
      }

      repositionMarkerTooltips();
    }


    /** Given a "lines_comm" string taken from gadget prefs, this function
     *  will create lines based on the string. The resultant lines are put
     *  into the "lines" global array.
     * @param {string} lstring The encoded string containing the line info
     *  from UserPrefs.
     */
    function createLinesFromString(lstring){
      var linesCommData = lstring.split("|");
      for(var i = 0; i < linesCommData[0]; i++){
        var curLineData = linesCommData[i+1].split("&");
        var id = curLineData[0];
        var startLatValue = parseFloat(curLineData[1]);
        var startLngValue = parseFloat(curLineData[2]);
        var endLatValue = parseFloat(curLineData[3]);
        var endLngValue = parseFloat(curLineData[4]);

        var newLine = new Line(id, startLatValue, startLngValue,
                               endLatValue, endLngValue);
        lines.push(newLine);
        map.addOverlay(newLine.GMLine);
      }
    }


////////////////////////////////////////////////////////////////


    /** Opens an infoWindow over a clicked Marker. This infoWindow
     *  contains an annotation text area and a delete button.
     * @param {Marker} marker The reference to the clicked marker to which
     *  this infoWindow will belong to.
     */
    function openInfoWindowForMarker(marker){
      var infoWindow = document.createElement('div');
      infoWindow.style.width = '280px';
      infoWindow.style.height = '160px';
      infoWindow.id = 'info_window';

      curActiveMarker = marker;

      var topInfoMenu = getTopInfoWindowMenuElement('top_info_menu', marker);
      var annotateWindow = getAnnotateElement('annotate_window', marker);
      var bottomInfoMenu = getBottomInfoWindowMenuElement('bottom_info_menu',
                                                          marker);

      infoWindow.appendChild(topInfoMenu);
      infoWindow.appendChild(annotateWindow);
      infoWindow.appendChild(bottomInfoMenu);

      var tab = new GInfoWindowTab("Info Window", infoWindow);
      marker.GMMarker.openInfoWindowTabs([tab]);
    }


    /** Constructs the HTML element for the top part of the infoWindow
     *  of a marker.
     * @param {string} id The HTML id for the resulting div.
     * @param {Marker} parentMarker The reference to the marker to which the
     *  resultant infoWindow belongs.
     * @return {Element} The HTML div element corresponding to the top
     *  part of the infoWindow.
     */
    function getTopInfoWindowMenuElement(id, parentMarker) {
      var topMenu = document.createElement('div');
      topMenu.style.width = '280px';
      topMenu.style.height = '20px';
      topMenu.id = id;
      topMenu.innerHTML = '<div style="font-weight:bold;font-size:80%;color:#666666">' +
                          'Description' +
                          '</div>';
      return topMenu;
    }


    /** Constructs the HTML element for the annotation part of the infoWindow
     *  of a marker. This contains the text area used to change the annotation
     *  of the given marker.
     * @param {string} id The HTML id for the resulting div.
     * @param {Marker} parentMarker The reference to the marker to which the
     *  resultant infoWindow belongs.
     * @return {Element} The HTML div element corresponding to the annotation
     *  part of the infoWindow.
     */
    function getAnnotateElement(id, parentMarker) {
      var annotateWindow = document.createElement('div');
      annotateWindow.style.width = '280px';
      annotateWindow.style.height = '120px';
      annotateWindow.id = id;
      annotateWindow.innerHTML = '<form>' +
                                 '<textarea name="annotation" onkeyup="onAnnotationChange(this);" wrap="soft" style="width:280px;height:120px;">' +
                                 parentMarker.annotation +
                                 '</textarea></form>';
      return annotateWindow;
    }


    /** Constructs the HTML element for the bottom part of the infoWindow
     *  of a marker. This contains the Delete button for the given marker.
     * @param {string} id The HTML id for the resulting div.
     * @param {Marker} parentMarker The reference to the marker to which the
     *  resultant infoWindow belongs.
     * @return {Element} The HTML div element corresponding to the bottom
     *  part of the infoWindow.
     */
    function getBottomInfoWindowMenuElement(id, parentMarker) {
      var bottomMenu = document.createElement('div');
      bottomMenu.style.width = '280px';
      bottomMenu.style.height = '20px';
      bottomMenu.id = id;
      bottomMenu.innerHTML = '<span style="float:left;font-size:small;padding-top:4px">' +
                             '<a href="javascript:deleteActiveMarker();">Delete</a>' +
                             '</span>';
      return bottomMenu;
    }


////////////////////////////////////////////////////////////////


    /** Event handler for map pan, updates the UserPrefs of the gadget.
     */
    function onMoveEnd() {
      var latlng = map.getCenter();
      if ((latlng.lat() != lat) || (latlng.lng() != lng)) {
        lat = latlng.lat();
        lng = latlng.lng();
        prefs.set("lat", lat);
        prefs.set("lng", lng);

        repositionMarkerTooltips();
      }
    }


    /** Event handler for map zoom, updates the UserPrefs of the gadget.
     * @param {number} oldZoom The old zoom level of the map
     * @param {number} newZoom The new zoom level of the map
     */
    function onZoomEnd(oldZoom, newZoom) {
      if (zoom != newZoom) {
        zoom = newZoom;
        prefs.set("zoom", zoom);

        repositionMarkerTooltips();
      }
    }


    /** Event handler for map type change, updates the UserPrefs of the gadget.
     */
    function onMapTypeChange() {
      if (type != map.getCurrentMapType().getName()) {
        type = map.getCurrentMapType().getName();
        prefs.set("type", type);
      }
    }


    /** Event handler for a change of a marker annotation through the
     *  infoWindow.
     * @param {Element} textArea The HTML text area alement which was changed.
     */
    function onAnnotationChange(textArea){
      if(textArea.value != curActiveMarker.annotation){
        curActiveMarker.annotation = textArea.value;

        // Sanitise the annotation by removing control characters & and |,
        // this will go away once we move to better communication protocol
        // in the near future.
        curActiveMarker.annotation = curActiveMarker.annotation.replace(/&/g, '');
        curActiveMarker.annotation = curActiveMarker.annotation.replace(/\|/g, '');

        curActiveMarker.tooltip.innerHTML = '<div style="float:left;font-size:70%;border:1px #000000 solid;background-color:#eeeeee;padding:2px">' +
                                            curActiveMarker.annotation +
                                            '</div>';

        if(curActiveMarker.annotation.length > 0){
          curActiveMarker.tooltip.style.visibility = "visible";
        }
        else{
          curActiveMarker.tooltip.style.visibility = "hidden";
        }

        setMarkersCommString();
      }
    }


    /** Event handler for mouse movement when in "line_mode" and having
     *  clicked at least once (displays the potential line).
     * @param {LatLng} latlng The latitude and longitude of the current mouse
     *  position over the map.
     */
    function onMouseMove(latlng){
      if(mode == "line_mode" && prevLinePoint != null){
        deletePotentialLine();
        curPotentialLine = new GPolyline([prevLinePoint, latlng], "#ff1111",
                                         4, 0.3, {clickable : false});
        map.addOverlay(curPotentialLine);
      }
    }


    /** Event handler for dragging a marker.
     * @param {Marker} marker The reference for the Marker which was dragged.
     */
    function onDragEnd(marker) {
      var latlng = marker.GMMarker.getLatLng();
      setMarkersCommString();
      repositionMarkerTooltips();
    }



    /** Event handler for clicking on a marker.
     * @param {Marker} marker The reference for the Marker which was clicked.
     */
    function onMarkerClick(marker){
      if(mode == "marker_mode" || mode == "select_mode"){
        openInfoWindowForMarker(marker);
      }
      else if(mode == "delete_mode"){
        deleteMarker(marker);
      }
    }


    /** Event handler for clicking on a line.
     * @param {LatLng} latlng The latitude and longitude of the point on the
     *  map where the click occured.
     * @param {Line} line The reference for the Line which was clicked.
     */
    function onLineClick(latlng, line){
      if(mode == "delete_mode"){
        deleteLine(line);
      }
      else if(mode == "marker_mode"){
        createMarker(null, latlng, null);
      }
    }


////////////////////////////////////////////////////////////////


    /** Sets the prefs "markers_comm" string, used whenever markers change.
     */
    function setMarkersCommString() {
      var newMarkersString = "";
      var allMarkerData = new Array();
      allMarkerData.push(markers.length);

      for(var i = 0; i < markers.length; i++){
        var markerData = new Array();
        markerData.push(markers[i].id);
        markerData.push(markers[i].GMMarker.getLatLng().lat());
        markerData.push(markers[i].GMMarker.getLatLng().lng());
        markerData.push(markers[i].annotation);
        allMarkerData.push(markerData.join('&'));
        delete markerData;
      }

      newMarkersString = allMarkerData.join('|');
      delete allMarkerData;
      prefs.set("markers_comm", newMarkersString);
    }


    /** Sets the prefs "lines_comm" string, used whenever lines change.
     */
    function setLinesCommString() {
      var newLinesString = "";
      var allLineData = new Array();
      allLineData.push(lines.length);

      for(var i = 0; i < lines.length; i++){
        var lineData = new Array();
        lineData.push(lines[i].id);
        lineData.push(lines[i].startLat);
        lineData.push(lines[i].startLng);
        lineData.push(lines[i].endLat);
        lineData.push(lines[i].endLng);
        allLineData.push(lineData.join('&'));
        delete lineData;
      }

      newLinesString = allLineData.join('|');
      delete allLineData;
      prefs.set("lines_comm", newLinesString);
    }


    /** Check to see if new prefs data for markers differs from our current
     *  local markers state.
     * @param {string} markersString The string encoding of the markers info in
     *  the prefs string.
     * @return {boolean} Whether the markers data in the prefs differs from
     *  the local marker state.
     */
    function markersDontMatchData(markersString){
      var markersCommData = markersString.split("|");

      if(markersCommData.length != markers.length + 1){ return true; }
      if(markersCommData[0] != markers.length){ return true; }

      for(var i = 0; i < markers.length; i++){
        var curMarkerData = markersCommData[i+1].split("&");
        var id = curMarkerData[0];
        var latValue = parseFloat(curMarkerData[1]);
        var lngValue = parseFloat(curMarkerData[2]);
        var annotation = curMarkerData[3];

        if(markers[i].id != id ||
           markers[i].lat != latValue ||
           markers[i].lng != lngValue ||
           markers[i].annotation != annotation){
          return true;
        }
      }

      return false;
    }


    /** Check to see if new prefs data for lines differs from our current
     *  local lines state.
     * @param {string} linesString The string encoding of the lines info in
     *  the prefs string.
     * @return {boolean} Whether the lines data in the prefs differs from
     *  the local line state.
     */
    function linesDontMatchData(linesString) {
      var linesCommData = linesString.split("|");

      if(linesCommData.length != lines.length + 1){ return true; }
      if(linesCommData[0] != lines.length){ return true; }

      for(var i = 0; i < lines.length; i++){
        var curLineData = linesCommData[i+1].split("&");
        var id = curLineData[0];
        var startLatValue = parseFloat(curLineData[1]);
        var startLngValue = parseFloat(curLineData[2]);
        var endLatValue = parseFloat(curLineData[3]);
        var endLngValue = parseFloat(curLineData[4]);

        if(lines[i].id != id ||
           lines[i].startLat != startLatValue ||
           lines[i].startLng != startLngValue ||
           lines[i].endLat != endLatValue ||
           lines[i].endLng != endLngValue){
          return true;
        }
      }

      return false;
    }


    /** This function is called whenever a change in the Gadget prefs is
     *  detected. It then checks to see what has changed specifically and
     *  updates the local
     *  state accordingly.
     */
    function setPref() {
      var args = this["a"];
      var name = args[1];
      var value = args[2];
      var floatValue = parseFloat(value);
      var intValue = parseInt(value);
      switch (name) {
        case "lat":
          if (lat != floatValue) {
            lat = floatValue;
            map.setCenter(new GLatLng(lat, lng));
            repositionMarkerTooltips();
          }
          break;
        case "lng":
          if (lng != floatValue) {
            lng = floatValue;
            map.setCenter(new GLatLng(lat, lng));
            repositionMarkerTooltips();
          }
          break;
        case "zoom":
          if (zoom != floatValue) {
            zoom = floatValue;
            map.setZoom(zoom);
            repositionMarkerTooltips();
          }
          break;
        case "type":
          if (type != value) {
            type = value;
            setMapType(map, type);
          }
          break;
        case "curMarkerId":
          if(curMarkerId != intValue){
            curMarkerId = intValue;
          }
          break;
        case "curLineId":
          if(curLineId != intValue){
            curLineId = intValue;
          }
          break;
        case "markers_comm":
          if(markersDontMatchData(value)){
            clearMarkers();
            createMarkersFromString(value);
          }
          break;

        case "lines_comm":
          if(linesDontMatchData(value)){
            clearLines();
            createLinesFromString(value);
          }
          break;
      }
    }


    /** Sets the map type to the given type.
     * @param {GMap2} map The reference of the map whose type is set.
     * @param {string} name The name of the type to set on the given map.
     */
    function setMapType(map, name) {
      var mapTypes = map.getMapTypes();
      for (var i in mapTypes) {
        if (mapTypes[i].getName() == name) {
          map.setMapType(mapTypes[i]);
        }
      }
    }


    /** Repositions the marker tooltip when a marker is moved.
     */
    function repositionMarkerTooltips(){
      // adjust the positions of all marker tooltips
      for(var i = 0; i < markers.length; i++){
        var offset = map.fromLatLngToDivPixel(markers[i].GMMarker.getLatLng());
        var width = markers[i].GMMarker.getIcon().iconSize.width;
        var height = markers[i].tooltip.clientHeight;

        var pos = new GControlPosition(G_ANCHOR_TOP_LEFT,
                                       new GSize(offset.x+width*0.4,offset.y));

        pos.apply(markers[i].tooltip);
      }
    }


////////////////////////////////////////////////////////////////


    /**
     * Marker class used to represent all markers on the map. Contains the
     *  position and annotation of the corresponding marker. All markers are
     *  contained in the "markers" global array.
     * @param {number} markerId The id number of the new marker.
     * @param {number} markerLat The latitude of the marker.
     * @param {number} markerLng The longitude of the marker.
     * @param {string} annotation The annotation string of the new marker.
     * @constructor
     */
    function Marker(markerId, markerLat, markerLng, annotation) {
      var me = this;

      this.id  = markerId;
      this.lat = markerLat;
      this.lng = markerLng;
      this.annotation = annotation;

      this.GMMarker = new GMarker(new GLatLng(markerLat, markerLng),
                                  {draggable : true });

      this.tooltip = document.createElement("div");
      this.tooltip.style.width = "100px";

      map.getPane(G_MAP_FLOAT_PANE).appendChild(me.tooltip);

      if(this.annotation == "") {
        this.tooltip.style.visibility="hidden";
      }
      else {
        this.tooltip.style.visibility="visible";
      }

      this.tooltip.innerHTML = '<div style="float:left;font-size:70%;border:1px #000000 solid;background-color:#eeeeee;padding:2px">' +
                               me.annotation + '</div>';

      var offset = map.fromLatLngToDivPixel(me.GMMarker.getLatLng());
      var width = me.GMMarker.getIcon().iconSize.width;
      var height = me.tooltip.clientHeight;
      var pos = new GControlPosition(G_ANCHOR_TOP_LEFT,
                                     new GSize(offset.x+width*0.4, offset.y));
      pos.apply(me.tooltip);

      this.dragHandler = GEvent.addListener(me.GMMarker, "dragend", function(){
        onDragEnd(me);
      });

      this.clickHandler = GEvent.addListener(me.GMMarker, "click", function(){
        onMarkerClick(me);
      });
    }


    /**
     * Line is the class representing each line on the map. All lines are
     *  contained in the "lines" global array.
     * @param {number} lineId The id number of the new line.
     * @param {number} startLat The latitude of the start point of the line.
     * @param {number} startLng The longitude of the start point of the line.
     * @param {number} endLat The latitude of the end point of the line.
     * @param {number} endLng The longitude of the end point of the line.
     * @constructor
     */
    function Line(lineId, startLat, startLng, endLat, endLng){
      var me = this;

      this.id  = lineId;
      this.startLat = startLat;
      this.startLng = startLng;
      this.endLat = endLat;
      this.endLng = endLng;

      var GMLine = new GPolyline([new GLatLng(startLat, startLng),
                                 new GLatLng(endLat, endLng)],
                                 "#ff1111", 4, 0.7, {clickable : true});
      this.GMLine = GMLine;

      this.clickHandler = GEvent.addListener(me.GMLine, "click",
                                             function(latlng){
        onLineClick(latlng, me);
      });
    }



    /**
     * CustomButton provides the button panel on the map. The select button,
     *  the markers button, the line button, and the delete button are all
     *  provided by this panel.
     * @extends {GControl}
     * @constructor
     */
    function CustomButton() {
    }

    CustomButton.prototype = new GControl();


    /**
     * Initialises the button panel, creates the button divs and inserts them
     * into the map.
     * @param {GMap2} map The reference to the map into which the panel is
     *  inserted into.
     * @return {Element} The refenrence to the div element of the button panel.
     */
     CustomButton.prototype.initialize = function(map) {
       var me = this;
       var container = document.createElement("div");

       this.handActiveImg = '<img src="http://maps.google.com/intl/en_us/mapfiles/ms/t/Bsd.png" alt="Select/edit map features" />';
       this.handInactiveImg = '<img src="http://maps.google.com/intl/en_us/mapfiles/ms/t/Bsu.png" alt="Select/edit map features" />';
       this.markerActiveImg = '<img src="http://maps.google.com/intl/en_us/mapfiles/ms/t/Bmd.png" alt="Add a place mark" />';
       this.markerInactiveImg = '<img src="http://maps.google.com/intl/en_us/mapfiles/ms/t/Bmu.png" alt="Add a place mark" />';
       this.lineActiveImg = '<img src="http://maps.google.com/intl/en_us/mapfiles/ms/t/Bld.png" alt="Draw a line" />';
       this.lineInactiveImg = '<img src="http://maps.google.com/intl/en_us/mapfiles/ms/t/Blu.png" alt="Draw a line" />';
       this.delActiveImg = '<img src="http://www.cse.unsw.edu.au/~osushkov/Bdd.png" alt="Delete map features" />';
       this.delInactiveImg = '<img src="http://www.cse.unsw.edu.au/~osushkov/Bdu.png" alt="Delete map features" />';

       this.createButtonDiv(container, "hand_button", me.handActiveImg,
                            me.handActiveImg, null, "select_mode");

       this.createButtonDiv(container, "marker_button", me.markerActiveImg,
                            me.markerInactiveImg, createMarker, "marker_mode");

       this.createButtonDiv(container, "line_button", me.lineActiveImg,
                            me.lineInactiveImg, createPolyline, "line_mode");
       this.createButtonDiv(container, "del_button", me.delActiveImg,
                            me.delInactiveImg, null, "delete_mode");

       map.getContainer().appendChild(container);
       return container;
    }


    /**
     * Creates a single button inside the button panel container. Button when
     *  clicked calls the given callback and changes the draw mode to the
     *  specified mode.
     * @param {Element} container The HTML div container into which the
     *  button is inserted.
     * @param {string} id The string id of the button div.
     * @param {string} activeImgSrc The url of the image the button is to
     *  display when it is clicked to activate it.
     * @param {string} initImgSrc The starting image the button is to have
     *  at init time.
     * @param {function} newClickListener The function callback for when the
     *  button is clicked.
     * @param {string} resultingMode The mode name corresponding to the button
     *  being activated.
     */
    CustomButton.prototype.createButtonDiv = function(container,
                                                      id,
                                                      activeImgSrc,
                                                      initImgSrc,
                                                      newClickListener,
                                                      resultingMode) {
       var me = this;
       var newDiv = document.createElement("div");
       this.setButtonStyle_(newDiv);
       newDiv.id = id;
       newDiv.innerHTML = initImgSrc;
       container.appendChild(newDiv);
       GEvent.addDomListener(newDiv, "click", function() {
         me.resetClickListener(newClickListener);
         me.setButtonsInactive();
         document.getElementById(id).innerHTML = activeImgSrc;
         mode = resultingMode;
       });
    }


    /**
     * Sets all buttons in the panel to display their inactive images.
     * Also resets the line drawing state.
     */
    CustomButton.prototype.setButtonsInactive = function() {
      document.getElementById("del_button").innerHTML = this.delInactiveImg;
      document.getElementById("line_button").innerHTML = this.lineInactiveImg;
      document.getElementById("hand_button").innerHTML = this.handInactiveImg;

      document.getElementById("marker_button").innerHTML =
        this.markerInactiveImg;

      resetLineDrawingState();
    }


    /**
     * Removes the current click listener on the map if there is any, replaces
     * it with the given function callback.
     * @param {function} newListener The function callback of the new click
     *  listener for the map.
     */
    CustomButton.prototype.resetClickListener = function(newListener) {
      if(clickListener != null){
        GEvent.removeListener(clickListener);
        clickListener = null;
      }

      if(newListener != null){
        clickListener = GEvent.addListener(map, "click", newListener);
      }
    }


    /**
     * Sets the position of the button panel. By default, the control will
     *  appear in the bottom left corner of the map.
     */
    CustomButton.prototype.getDefaultPosition = function() {
      return new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(7, 37));
    }


    /**
     * Sets the proper CSS for the given button element.
     * @param {Element} button The HTML div element for which to set
     *  the CSS style.
     */
    CustomButton.prototype.setButtonStyle_ = function(button) {
      button.style.padding = "1px";
      button.style.cursor = "pointer";
    }


    </script>
  </head>
  <body onload="initialize()" onunload="GUnload()">
    <div id="map_canvas" style="width: 100%; height: 100%"></div>
  </body>

]]></Content>

</Module>

