//uses a #map div and works off various "microformats" in the map div to control display
//all files are auto-included by the dir:googlemaps
//requires item.js in order to make Javascript Items for the item_class's that then plot themselves on the map
//requires jQuery
//requires Google Maps
//requires the OpenSource MarkerManager
//j* = jQuery object, g* = global, gg* = GoogleMaps object
//http://code.google.com/apis/maps/articles/toomanymarkers.html#markerclusterer
    
//-------------------------------------------- Map --------------------------------------------
function Map(_jHTMLElement, _user, _noautoadd) {
    HTMLObject.apply(this, arguments);
    if (!arguments.length) return this;
    var self = this;
    
    //base properties
    this.ggMgr                  = null;
    this.ggLocalSearch          = null;
    this.lastSearchKey          = null;
    this.gShowMarker            = null;
    this.user                   = _user;
    this.noautoadd              = _noautoadd; //don't automatically add items from the page
    this.items                  = [];
    this.addressFollowKeyTimer  = null;       //for starting address lookup when keys are being pressed
    this.addressSearching       = null;       //address lookup has started
    this.isclustering           = false;      //this can change with maxZoom levels
    this.clustering_maxZoom     = 11;
    this.clustering_gridSize    = 30;         //in pixels

    
    //defered call timeouts
    this.mgr_addItems = null;
    this.mgr_refresh  = null;
    this.items_temp   = null; //temporary item array for 1 execution period to use addItems instead of addItem

    //extended map functions in the HTML microformat style
    //these DIVs are all lost when the map is initialised because it clears the inards of <div id="map" />
    this.geo_type              = this.hV(".geo .type" );
    this.lat                   = this.hV(".geo .latitude" );
    this.lng                   = this.hV(".geo .longitude");
    this.zoom                  = this.hV(".geo .zoom");
    this.user_centered         = this.hX(".user_centred");
    this.gps_centered          = this.hX(".gps_centred");
    this.GLargeMapControl      = this.hX(".GLargeMapControl");
    this.enableDoubleClickZoom = this.hX(".enableDoubleClickZoom");
    this.GScaleControl         = this.hX(".GScaleControl");
    this.immediate_follow      = this.hV(".immediate_follow"); //only relevant to address form following

    //optional
    this.opennow               = this.hB(".geo .infowindow"); //immediate display of infowindow of item (geo_type = itemsearch only)
    this.openhid               = this.hV(".geo .hid");
    
    //defaults        
    //defaulting to London (custom location is encoded in the map div using microformats)
    if (!this.zoom) this.zoom = 11;        else this.zoom = parseInt(this.zoom);  //city level
    if (!this.lat)  this.lat  = 51.510452; else this.lat  = parseFloat(this.lat); //London
    if (!this.lng)  this.lng  = -0.126171; else this.lng  = parseFloat(this.lng); //London

    //initialise the map and centre it
    //user, subdomain, map default and global default
    //if user logged in so center on there location. need to do this first for the map to work
    this.ggMap = new GMap2(this.jHTMLElement.get(0));
    switch (this.geo_type) {
        case 'locationsearch': 
        case 'itemsearch': {
            break;
        }
        default: { //geo_type = subsite and default (both overrideable by user and mobile device positioning)
            //user centre
            if (this.user_centered && this.user && this.user.hasGEO) {
                this.ggMap.setCenter(new GLatLng(this.user.lat, this.user.lng), 13);
                this.geo_type = 'user';
            }
            //mobile device center
            //can throw an exception if the geo feature is turned off
            try {
                if (this.gps_centered && navigator && navigator.geolocation) navigator.geolocation.getCurrentPosition(
                    function(position) {
                        self.gps_foundLocation(position);
                        self.geo_type = 'gps';
                    }, 
                    function()         {self.gps_noLocation()}
                );
            } catch (ex) {}
        }
    }
    //apply server supplied default if we don't have a centre yet
    if (!this.ggMap.getCenter()) this.ggMap.setCenter(new GLatLng(this.lat, this.lng), this.zoom);
    
    //using closure for object call
    GEvent.addListener(this.ggMap, "moveend",         function (){self.mapmoved();});  
    GEvent.addListener(this.ggMap, "zoomend",         function (oldLevel, newLevel){self.zoomed(oldLevel, newLevel);});
    GEvent.addListener(this.ggMap, "infowindowopen",  function (){self.infowindowopen();});
    GEvent.addListener(this.ggMap, "infowindowclose", function (){self.infowindowclose();});

    //add the zoom control (by default)
    if (this.GLargeMapControl)      this.ggMap.addControl(new GLargeMapControl());
    if (this.enableDoubleClickZoom) this.ggMap.enableDoubleClickZoom();
    if (this.GScaleControl)         this.ggMap.addControl(new GScaleControl());
    
    //address and postcode lookup capability
    if (window.GlocalSearch)        this.ggLocalSearch = new GlocalSearch();

    //geometry controls with quick access extendos to controls[] array
    if (window.GeometryControls) {    
        this.geometryControls = new GeometryControls({
            infoWindowHtmlURL:'/includes/gmaps/geometrycontrols/examples/data/geometry_html_template.html',
            stylesheets:[],
            autoSave:false,
            debug:false
        });
        if (window.MarkerControl)   this.geometryControls.addControl(this.markerControl   = new MarkerControl());
        if (window.PolygonControl)  this.geometryControls.addControl(this.polygonControl  = new PolygonControl());
        if (window.PolylineControl) this.geometryControls.addControl(this.polylineControl = new PolylineControl());
        this.ggMap.addControl(this.geometryControls);
    }

    //deferred setup
    //allow the Google Map to initialise properly
    //and clear its DIV along with the directives
    setTimeout(function(){self.defered_setup()}, 0);
}
Map.prototype = new HTMLObject();
Map.prototype.toString = function toString() {return '[Map Object with [' + this.items.length + '] items]';}

Map.prototype.defered_setup = function defered_setup() {     
    var self = this;

    //decide which marker manager to use (different functions available)
    //MarkerClusterer:   http://code.google.com/apis/maps/articles/toomanymarkers.html#markerclusterer
    //ClusterManager_v1: http://cm.qfox.nl/
    if      (window.MarkerClusterer)   this.ggMgr = new MarkerClusterer(  this.ggMap, this.clustering_maxZoom, this.clustering_gridSize);
    else if (window.ClusterManager_v1) this.ggMgr = new ClusterManager_v1(this.ggMap); //objClusterIcon:, boolNotClickable:
    else if (window.MarkerManager)     this.ggMgr = new MarkerManager(    this.ggMap); //always included by dir:googlemaps
    else                               this.ggMgr = this.ggMap;                        //no manager

    //auto add types: runs off HTML markup (microformats) in order to maintain progressive enhancement
    //additions here, after the manager has been created
    if (!this.noautoadd && window.createItemClass) {
        $(".item_class").each(function(i, itemClass){
            self.addItem(createItemClass($(itemClass), self, i)); //will be deferred by addItem
        });
        this.refresh_deferedOnce(); //just in case we add other map changing stuff after
    }

    //defered mapmove event (all happens after addItem deferred and after map initialised)
    this.mapmoved();
    this.zoomed(null, this.getZoom());
    if (this.geo_type == 'itemsearch' && this.opennow) setTimeout(function(){self.itemsearch_go();}, 0);
    setTimeout(function(){$(document).trigger("mapready")}, 0);
}

Map.prototype.itemsearch_go = function itemsearch_go() {
    //all items should be loaded, but mapready has not fired yet
    //map should already be zoomed in on the item, so we just need to access it and display it
    var item = Item.fromHID(this.openhid);
    if (item) item.openInfoWindow();
}

//Facade: expose some of the GMap interface:
Map.prototype.setCenter          = function setCenter(ggLatLng)                         {return this.ggMap.setCenter(ggLatLng);}
Map.prototype.panTo              = function panTo(ggLatLng)                             {return this.ggMap.panTo(ggLatLng);}
Map.prototype.getCenter          = function getCenter()                                 {return this.ggMap.getCenter();}
Map.prototype.getBounds          = function getBounds()                                 {return this.ggMap.getBounds();}
Map.prototype.getZoom            = function getZoom()                                   {return this.ggMap.getZoom();}
Map.prototype.setZoom            = function setZoom(newlevel)                           {return this.ggMap.setZoom(newlevel);}
Map.prototype.checkResize        = function checkResize()                               {return this.ggMap.checkResize();}
Map.prototype.addOverlay         = function addOverlay(ggOverlay)                       {return this.ggMap.addOverlay(ggOverlay);}
Map.prototype.removeOverlay      = function removeOverlay(ggOverlay)                    {return this.ggMap.removeOverlay(ggOverlay);}
Map.prototype.openInfoWindowHtml = function openInfoWindowHtml(ggLatLng, html, options) {return this.ggMap.openInfoWindowHtml(ggLatLng, html, options);}
Map.prototype.openInfoWindow     = function openInfoWindow(ggLatLng, node, options)     {return this.ggMap.openInfoWindow(ggLatLng, node, options);}
Map.prototype.closeInfoWindow    = function closeInfoWindow()                           {return this.ggMap.closeInfoWindow();}
Map.prototype.getInfoWindow      = function getInfoWindow()                             {return this.ggMap.getInfoWindow();}
Map.prototype.zoomToBounds       = function zoomToBounds(bounds) {
    this.ggMap.setZoom(this.ggMap.getBoundsZoomLevel(bounds));
    this.ggMap.setCenter(bounds.getCenter());
}


Map.prototype.isInfoWindowOpen   = function isInfoWindowOpen()                          {return !this.ggMap.getInfoWindow().isHidden();}

//mobile phone gps functions
Map.prototype.gps_foundLocation = function gps_foundLocation(position) {
    var coords;
    if (position && (coords = position.coords)) {
        this.zoom = 15;
        this.ggMap.setCenter(new GLatLng(coords.latitude, coords.longitude), this.zoom);
    }
}
Map.prototype.gps_noLocation = function gps_noLocation() {}

//Facade: expose manager functionality
//defered calls
Map.prototype.refresh_deferedOnce = function refresh_deferedOnce() {
    var self = this;
    if (!this.mgr_refresh) this.mgr_refresh = setTimeout(function(){
        self.refresh();
        self.mgr_refresh = null;
    }, 0);
}

Map.prototype.addItems_deferedOnce = function addItems_deferedOnce(items) {
    var self = this;
    if (!this.mgr_addItems) this.mgr_addItems = setTimeout(function(){
        self.addItems(items);
        self.mgr_addItems = null;
    }, 0);
}
Map.prototype.refresh = function refresh() {
    //see if the manager has refresh capability first
    if (this.ggMgr.refresh) {
        this.ggMgr.refresh();
        cinfo("manager refresh");
    }
}

Map.prototype.addItems = function addItems(items) {
    //add items through the manager
    if (!this.ggMgr) cerror("manager not available yet!");
    var ggMarkers = Item.ggMarkers(items);
    if      (this.ggMgr.addMarkers) this.ggMgr.addMarkers(ggMarkers, 0); //minimum zoom = world map (0)
    else if (this.ggMgr.addOverlay) for (var i = 0; i < ggMarkers.length; i++) this.ggMap.addOverlay(ggMarkers[i]);
    
    //notify everyone Item(s) is now on a map
    for (var i = 0; i < items.length; i++) items[i].setMap(this);
    this.items.concat(items);                             //add new items to map's list
    cinfo("[" + items.length + "] items added");
    if (items == this.items_temp) this.items_temp = null; //restart temporary array of items
    return items;
}

Map.prototype.removeItem = function removeItem(item) {
    if      (this.ggMgr.removeMarker)  this.ggMgr.removeMarker(item.marker.ggMarker);
    else if (this.ggMgr.removeOverlay) this.ggMgr.removeOverlay(item.marker.ggMarker);
    this.items.remove(item);
    item.setMap();
}

Map.prototype.addItem = function addItem(item) {
    //addItem is slow for many items, so temp item list compiled and then addItems called
    var self = this;
    if (item && item.hasGEO) {
        if (!this.items_temp) this.items_temp = []; //null array indicates first addition
        this.items_temp.push(item);
        this.addItems_deferedOnce(this.items_temp);
    }
    return item;
}

//direct marker additions
Map.prototype.addMarker = function addMarker(marker) {
    if      (this.ggMgr.addMarker)  this.ggMgr.addMarker(marker, 0); //minimum zoom = world map (0)
    else if (this.ggMgr.addOverlay) this.ggMgr.addOverlay(marker);
}
Map.prototype.removeMarker = function removeMarker(marker) {
    if      (this.ggMgr.removeMarker)  this.ggMgr.removeMarker(marker);
    else if (this.ggMgr.removeOverlay) this.ggMgr.removeOverlay(marker);
}


//events
Map.prototype.mapmoved = function mapmoved() {
    var self = this;
    if (window.mapmoved_after && this.ggMap) 
        window.setTimeout(function(){
            window.mapmoved_after(self);
        }, 0);
}
Map.prototype.zoomed = function zoomed(oldlevel, newlevel) {
    var self = this;
    if (window.zoomed && this.ggMap) 
        window.setTimeout(function(){
            window.zoomed(self, oldlevel, newlevel);
        }, 0);
    this.isclustering = (this.clustering_maxZoom > newlevel);
}
Map.prototype.infowindowclose = function infowindowclose() {
}
Map.prototype.infowindowopen = function infowindowopen() {
}
Map.prototype.showFirstAddress = function showFirstAddress(searchKey, callback, callback_notfound) {
    var self = this;
    if (!this.ggLocalSearch) alert('local search disabled!');
    else {
        if (!this.gShowMarker) {
            this.gShowMarker = new Marker(51.511734, -0.101452, 'location', new Icon('/societycard/images/marker_packs/green_medium/blank.png', 20, 34), true, null, window); //draggeable
            this.addMarker(this.gShowMarker.ggMarker);
        }

        this.findAddresses(searchKey, function(ggLocalSearch, searchKey){
            var firstresult  = ggLocalSearch.results[0];
            var firstpoint   = new GLatLng(firstresult.lat, firstresult.lng);
            var accuracy     = firstresult.accuracy;
            var accuracyname;
            
            switch (accuracy) {
                case 0:	 {accuracyname = 'Unknown location'; break;}
                case 1:	 {accuracyname = 'Country'; break;}
                case 2:	 {accuracyname = 'Region'; break;}           //(state, province, prefecture, etc.)
                case 3:	 {accuracyname = 'Sub-region'; break;}       //(county, municipality, etc.)
                case 4:	 {accuracyname = 'Town'; break;}             //(city, village)
                case 5:	 {accuracyname = 'Post code'; break;}        //(zip code)
                case 6:	 {accuracyname = 'Street'; break;}
                case 7:	 {accuracyname = 'Intersection'; break;}
                case 8:	 {accuracyname = 'Address'; break;}
                case 9:	 {accuracyname = 'Premise'; break;}          //(building name, property name, shopping center, etc.)
            }
            if (accuracy > 1) { //google map does not show the country level for some reason
                self.panTo(firstpoint);
                self.gShowMarker.setLatLng(firstpoint);
                self.gShowMarker.ggMarker.openInfoWindowHtml(searchKey);
            }
            if (callback) callback(ggLocalSearch, searchKey, firstresult, firstpoint, accuracy, accuracyname, self);
        }, 
        callback_notfound
        );
    }
}

Map.prototype.findAddresses = function findAddresses(searchKey, callback, callback_notfound) {
    var self = this;
    if (!this.ggLocalSearch) alert('local search disabled!');
    else {
        this.ggLocalSearch.setSearchCompleteCallback(self, function(searchControl, searcher){
            //searchControl, searcher are null in this case
            if (self.addressSearching) clearTimeout(self.addressSearching);
            self.addressSearching = null;
            if (self.ggLocalSearch && self.ggLocalSearch.results && self.ggLocalSearch.results.length) {
                if (callback)          callback(         self.ggLocalSearch, searchKey, self);
                self.jHTMLElement.addClass("map_active").removeClass("map_inactive");
            } else {
                if (callback_notfound) callback_notfound(self.ggLocalSearch, searchKey, self);
                self.gShowMarker.ggMarker.closeInfoWindow();
                self.jHTMLElement.addClass("map_inactive").removeClass("map_active");
            }
        });
        if (searchKey.replace(/^\s+|\s+$/gim, '')) {
            //3 seconds to find address before the notfound is called
            //this is because GMaps does not return the call if no address id found
            this.addressSearching = setTimeout(function(){
                self.addressSearching = false;
                if (callback_notfound) callback_notfound(self.ggLocalSearch, searchKey, self);
                self.gShowMarker.ggMarker.closeInfoWindow();
                self.jHTMLElement.addClass("map_inactive").removeClass("map_active");
            }, 3000);
            if (window.searching) window.searching(self.ggLocalSearch, searchKey, self);
            this.ggLocalSearch.execute(searchKey);
            this.lastSearchKey = searchKey;
        }
    }
}

Map.prototype.followAddress = function followAddress(jContainer, callback, immediate, callback_notfound) {
    //use closure so that multiple address groups can be watched
    var self = this;
    if (!this.ggLocalSearch) alert('local search disabled!');
    else {
        //map updates
        //for key presses: note that a timer triggers address lookup after key presses (addressFollowKeyTimer)
        jContainer.keyup(function(e){
            //ignore some keys
            var keycode = e.which;
            if (keycode != 9  //tab 
             && keycode != 13 //return
             && keycode != 16 //shift
             && keycode != 17 //ctrl
             && keycode != 27 //escape
            ) {
                if (self.addressFollowKeyTimer) clearTimeout(self.addressFollowKeyTimer);
                self.addressFollowKeyTimer = setTimeout(function(){
                    self.addressFollowKeyTimer = null;
                    self.sourceAddressChanged(jContainer, callback, callback_notfound);
                }, 2000);
            }
        });
        //for selects and radio buttons
        jContainer.change(function(){
            if (self.addressFollowKeyTimer) {
                clearTimeout(self.addressFollowKeyTimer);
                self.addressFollowKeyTimer = null;
            }
            self.sourceAddressChanged(jContainer, callback, callback_notfound);
        });
        //immediate (deferred) default = true
        if (immediate != false && this.immediate_follow == 'true') setTimeout(function(){
            self.sourceAddressChanged(jContainer, callback, callback_notfound);
        }, 0);
    }
    self.jHTMLElement.addClass("map_inactive").removeClass("map_active");
}

Map.prototype.sourceAddressChanged = function sourceAddressChanged(jContainer, callback, callback_notfound) {
    var self = this;
    var searchKey = '';
    
    //construct address
    if (jContainer.is(":input")) searchKey = jContainer.val();
    else {
      jContainer.find(":input").each(function(){
          var val = $(this).val().replace(/^\s+|\s+$/gim, '');
          if (val) {
              if (searchKey) searchKey += ', ';
              searchKey += val;
          }
      });
    }
    searchKey = searchKey.replace(/^\s+|\s+$/gim, ''); //trim

    //show and callback
    if (searchKey && this.lastSearchKey != searchKey) this.showFirstAddress(
        searchKey,
        function(ggLocalSearch, searchKey, firstresult, firstpoint, accuracy, accuracyname, map){
            self.sourceAddressFound(ggLocalSearch, searchKey, firstresult, firstpoint, accuracy, accuracyname, map, callback);
        }, 
        callback_notfound
    );
}

Map.prototype.sourceAddressFound = function sourceAddressFound(ggLocalSearch, searchKey, firstresult, firstpoint, accuracy, accuracyname, map, callback) {
    if (callback) callback(ggLocalSearch, searchKey, firstresult, firstpoint, accuracy, accuracyname, map);
}

//-------------------------------------------- Marker --------------------------------------------
function Marker(_lat, _lng, _title, _icon, _draggable, _jInfoWindow, _owner) {
    if (!arguments.length) return this;
    var self = this;

    this.id                = Marker.id++; //global identifier
    Marker.items[this.id]  = this;
    this.lat               = _lat;
    this.lng               = _lng;
    this.title             = _title;
    this.icon              = _icon;       //Icon: might be null
    this.draggable         = _draggable;
    this.owner             = _owner;      //an Item or the Map or sumink
    this.circle            = null;
    this.jInfoWindow       = _jInfoWindow;
    //for direct private friend operations on my ggMarker
    this.map               = null;        //direct access to map

    //default icon if null: incrementing default green medium
    if (!this.icon) {
        var src_def = '/images/map_default/marker_packs/green_medium/' + this.gLetterid + '.png';
        Marker.prototype.gLetterid++;
        this.icon = new Icon(src_def, 20, 34);
    }

    //objects and events    
    this.ggMarker = new GMarker(new GLatLng(this.lat, this.lng), this.ggOptions());
    this.addEvents();
    if (this.jInfoWindow) this.bindInfoWindow(this.jInfoWindow);

    if (window.addGEOMarker_after) addGEOMarker_after(this);
}
Marker.prototype.gLetterid = 1;
Marker.id = 1;
Marker.items = [];

Marker.prototype.addEvents = function addEvents() {
    var self = this;
    if (self.owner) {
        if (this.draggable && self.owner.moved) GEvent.addListener(this.ggMarker, "dragend",         function (latlng){self.owner.moved(latlng, self);});
        if (self.owner.mouseover)               GEvent.addListener(this.ggMarker, "mouseover",       function (latlng){self.owner.mouseover(false);});
        if (self.owner.mouseout)                GEvent.addListener(this.ggMarker, "mouseout",        function (latlng){self.owner.mouseout(false);});
        if (self.owner.click)                   GEvent.addListener(this.ggMarker, "click",           function (latlng){self.owner.click(latlng);});
        if (self.owner.infowindowopen)          GEvent.addListener(this.ggMarker, "infowindowopen",  function (latlng){self.owner.infowindowopen(latlng);});
        if (self.owner.infowindowclose)         GEvent.addListener(this.ggMarker, "infowindowclose", function (latlng){self.owner.infowindowclose(latlng);});
    }
}

//static
//default highlight icon marker (can be overridden)
//note that IE and Google Maps API may have an issue with animated images and the (mb is null) error
Marker.highlightIcon            = new GIcon(G_DEFAULT_ICON);
Marker.highlightIcon.image      = window.isie ? "/images/map_default/circle_highlight_static.png" : "/images/map_default/circle_highlight.gif";
Marker.highlightIcon.shadow     = 'none';
Marker.highlightIcon.iconSize   = new GSize(34, 16);
Marker.highlightIcon.iconAnchor = new GPoint(17, 8);

//Facade: expose some of the GMarker interface:
Marker.prototype.setLatLng =      function setLatLng(gLatLng) {return this.ggMarker.setLatLng(gLatLng);}
Marker.prototype.getLatLng =      function getLatLng()        {return this.ggMarker.getLatLng();}
Marker.prototype.showMapBlowup =  function showMapBlowup()    {return this.ggMarker.showMapBlowup();}
Marker.prototype.panTo =          function panTo()            {if (this.map) return this.map.panTo(this.getLatLng());}
Marker.prototype.openInfoWindow = function openInfoWindow(jIW, options) {
    if (!jIW) GEvent.trigger(this.ggMarker, 'click'); //open existing bound window (no other way!)
    else {
        //we have a different, specific window to show
        //clustering open
        var ggMarker;
        if (this.ggMarker.cluster) ggMarker = this.ggMarker.cluster; //if clustering is enabled and we are part of a cluster then we need to grab that marker
        else                       ggMarker = this.ggMarker;
        ggMarker.openInfoWindow(jIW.get(0), options);
    }
}
Marker.prototype.closeInfoWindow = function closeInfoWindow() {
    var marker;
    if (this.ggMarker.cluster) marker = this.ggMarker.cluster; //if clustering is enabled and we are part of a cluster then we need to grab that marker
    else                       marker = this.ggMarker;
    return marker.closeInfoWindow();
}

//other
Marker.prototype.toString  = function toString()   {return '[Marker Object [' + this.title +'] @ (' + this.lat + ',' + this.lng + ')';}
Marker.prototype.setMap    = function setMap(_map) {return this.map = _map;}
Marker.prototype.ggOptions = function ggOptions()  {return {title:this.title, icon:(this.icon ? this.icon.ggIcon : null), draggable:this.draggable, bouncy:true};}

Marker.prototype.change_size = function change_size(newwidth, newheight, passive) {
    //need to construct replacement marker with all constructor properties
    //note that the icon size attributes are saved for reversion
    if (this.icon && (this.icon.width != newwidth || this.icon.height != newheight)) {
        this.icon.setHeight(newheight);
        this.icon.setWidth(newwidth);
        this.icon.generate();
        var ggNewMarker = new GMarker(this.ggMarker.getLatLng(), this.ggOptions());
        return this.change_marker(ggNewMarker);
    }
    return false;
}

Marker.prototype.revert_original = function revert_original() {
    //need to construct replacement marker with all constructor properties
    //note that the icon size attributes are saved for reversion
    if (this.icon && this.icon.revert_original()) {
        var ggNewMarker = new GMarker(this.ggMarker.getLatLng(), this.ggOptions());
        return this.change_marker(ggNewMarker);
    }
    return false;
}

Marker.prototype.revert_last = function revert_last() {
    //need to construct replacement marker with all constructor properties
    //note that the icon size attributes are saved for reversion
    if (this.icon && this.icon.revert_last()) {
        var ggNewMarker = new GMarker(this.ggMarker.getLatLng(), this.ggOptions());
        return this.change_marker(ggNewMarker);
    }
    return false;
}

Marker.prototype.change_shadow = function change_shadow(newshadow) {
    //need to construct replacement marker with all constructor properties
    if (this.icon && this.icon.shadow != newshadow) {
        this.icon.setShadow(newshadow);
        this.icon.generate();
        var ggNewMarker = new GMarker(this.ggMarker.getLatLng(), this.ggOptions());
        return this.change_marker(ggNewMarker);
    }
    return false;
}

Marker.prototype.change_icon = function change_icon(newsrc) {
    //need to construct replacement marker with all constructor properties
    if (this.icon && this.icon.src != newsrc) {
        this.icon.setSRC(newsrc);
        this.icon.generate();
        var ggNewMarker = new GMarker(this.ggMarker.getLatLng(), this.ggOptions());
        return this.change_marker(ggNewMarker);
    }
    return false;
}

Marker.prototype.change_marker = function change_marker(ggNewMarker) {
    //MULTIPLE calls may come through to this function for the same marker
    // update only to the last one using a deferred function
    //Some map managers have an addMarkers function that we can also use
    //does not copy constructor properties, do that manually
    // this is because Google Maps does not allow access to them separately
    //defered arrays and function call
    if (!Marker.mgr_updateMarkers) Marker.mgr_updateMarkers = setTimeout(function(){Marker.updateMarkers();}, 0);

    //add the marker to the defered list
    //keyed on this pointer, singular
    //Updates to new one if called twice, old is garbage collected
    Marker.marker_changes[this.id] = ggNewMarker; 
    
    return this;
}

//static functions for Marker update
Marker.marker_changes = new Object();
Marker.mgr_updateMarkers = null;
Marker.updateMarkers = function updateMarkers() {
    var ggNewMarker, map, anymap;

    //loop through all the markers to be changed and update them
    //TODO: incorporate marker manager addMarkers() logic here...
    for (var id in Marker.marker_changes) {
        marker      = Marker.items[id];
        ggNewMarker = Marker.marker_changes[id];
        map         = marker.map;
        anymap      = map ? map : anymap;
        
        //base manager marker removal - swap new marker in
        if (map && map.ggMgr) {
            if (map.ggMgr.addMarker && map.ggMgr.removeMarker) {
                map.ggMgr.removeMarker(marker.ggMarker);
                map.ggMgr.addMarker(ggNewMarker);
            } else {
                map.ggMgr.removeOverlay(marker.ggMarker);
                map.ggMgr.addOverlay(ggNewMarker, 0);
            }
        } else cwarn("no map or manager!");

        //other additions and changes
        delete marker.ggMarker;
        marker.ggMarker = ggNewMarker;
        
        //attach events and objects to the marker
        //bindInfoWindow re-clones the infowindow object, copying all the events
        marker.addEvents();
        if (marker.jInfoWindow) marker.bindInfoWindow(marker.jInfoWindow);
    }
    //some marker managers like a refresh
    if (anymap && anymap.refresh) anymap.refresh();
    
    //clear marker update mechanism
    Marker.marker_changes = new Object(); 
    Marker.mgr_updateMarkers = null;
    return Marker;
}

Marker.prototype.bindInfoWindow = function bindInfoWindow(jInfoWindow, options) {
    if (jInfoWindow && jInfoWindow.length && this.ggMarker) {
        this.jInfoWindow = jInfoWindow;
        //separate function so that client pages can access
        //and also display needs to be controlled
        this.jInfoWindow.addClass("display_block");    //ensure that the infowindow is visible so its size can be assessed
        //tabs and call
        var ggTabs, jInfowindowtabs = jInfoWindow.find(".infowindowtab");
        if (jInfowindowtabs.length <= 1) {
            //no tabs, open window with full node
            //clone so that the origonal is not removed
            this.ggMarker.bindInfoWindow(jInfoWindow.clone(true, true).get(0), options);
        } else {
            //we have a tabbed info window with at least two tabs
            var parentClasses = jInfoWindow.attr("class");
            ggTabs = [];
            jInfowindowtabs.each(function(){
                $(this).addClass(parentClasses);
                //clone so that the origonal is not removed
                ggTabs.push(new GInfoWindowTab($(this).find(".tab_name").text(), $(this).clone(true, true).get(0)));
            });
            this.ggMarker.bindInfoWindowTabs(ggTabs, options);
        }
        this.ggMarker.infowindow = this.jInfoWindow.get(0); //attach raw to GCM
        this.jInfoWindow.removeClass("display_block");      //hide DIV from main DOM again
    }
    return jInfoWindow;
}

Marker.prototype.highlight = function highlight() {
    //image is 34 x 16
    //using overlay to avoid extra markers in the marker management system
    if (!this.map.isclustering && !this.circle && this.ggMarker) {
        var ggLatLng     = this.ggMarker.getLatLng();
        var ggNewLatLng  = new GLatLng(ggLatLng.lat() + 0.00001, ggLatLng.lng()); //to place the circle just behind its marker
        this.circle      = new GMarker(ggNewLatLng, {title:"highlight", icon:Marker.highlightIcon});
        this.map.addOverlay(this.circle);
        //this.bounce();
    }
}
Marker.prototype.dehighlight = function dehighlight() {
    if (!this.map.isclustering && this.circle) {
        this.map.removeOverlay(this.circle);
        this.circle = null;
    }
}

Marker.prototype.bounce = function bounce() {
    if (!this.draggable && this.ggMarker) { //doesn't work with draggable markers
        if (!this.ggMarker.Xa) {
            this.ggMarker.Xa = true;
            this.ggMarker.qo(false);
        }
        this.ggMarker.Pa = 20; //Current height
        this.ggMarker.ri = 20; //Max height
        this.ggMarker.av = 1;  //Direction (+ = down)
        this.ggMarker.tc();    //Go baby!
    }
}

//-------------------------------------------- Icon --------------------------------------------
function Icon(_jHTMLElement, _width, _height) { //or src, width, height
    //translates an img tag to a GIcon for a marker
    //the idea is a concise definition for an icon built by the XSL layer
    //so that the XSL can specify lots of icons and the javascript simply references them
    //JavaScript does no calcs, all done in the XSL
    if (arguments.length == 1) HTMLObject.apply(this, arguments);
    if (!arguments.length) return this;

    //base values
    if (arguments.length == 1) { //generate values from the HTML
        this.src         = this.hA("src");    //required
        this.shadow      = this.hA("alt");    //required: always specify the title on these images
        this.width       = this.hA("width");
        this.height      = this.hA("height");
    } else {                     //values passed in directly
        this.src         = arguments[0];
        this.width       = arguments[1];
        this.height      = arguments[2];
    }

    //defaults
    if (this.width  == '')      this.width       = 20;
    if (this.height == '')      this.height      = 34;
    
    //original
    this.original_src         = this.src;  
    this.original_shadow      = this.shadow;
    this.original_width       = this.width;
    this.original_height      = this.height;
    
    //previous
    //this.last_src         = null;  
    //this.last_shadow      = null;
    //this.last_width       = null;
    //this.last_height      = null;

    this.generate();
}
Icon.prototype.toString = function toString() {return "[" + this.src + " (" + this.width + "," + this.height + ")]";}
Icon.prototype = new HTMLObject();

Icon.prototype.setHeight = function setHeight(_newheight) {
    this.last_height = this.height;
    this.height      = _newheight;
    return this;
}

Icon.prototype.setWidth = function setWidth(_newwidth) {
    this.last_width = this.width;
    this.width      = _newwidth;
    return this;
}
Icon.prototype.setShadow = function setShadow(_newshadow) {
    this.last_shadow = this.shadow;
    this.shadow      = _newshadow;
    return this;
}

Icon.prototype.setSRC = function setSRC(_newsrc) {
    this.last_src  = this.src;
    this.src       = _newsrc;
    return this;
}

Icon.prototype.generate = function generate() {
    //construct GIcon
    if (window.GIcon) {
        this.ggIcon                      = new GIcon(G_DEFAULT_ICON);
        var middle                       = parseInt(this.width / 2);
        var shadowwidth                  = parseInt(this.width * 1.6);
        this.ggIcon.image                = this.src;
        this.ggIcon.shadow               = this.shadow;
        this.ggIcon.iconSize             = new GSize(this.width, this.height);
        this.ggIcon.shadowSize           = new GSize(shadowwidth, this.height);
        this.ggIcon.iconAnchor           = new GPoint(middle, this.height);
        this.ggIcon.infoWindowAnchor     = new GPoint(middle, 2);
    }
    return this;
}

Icon.prototype.revert_original = function revert_original() {
    if (this.src != this.original_src || this.shadow != this.original_shadow || this.width != this.original_width || this.height != this.original_height) {
        this.src         = this.original_src;
        this.shadow      = this.original_shadow;
        this.width       = this.original_width;
        this.height      = this.original_height;
        this.generate();
        return true;
    } else return false;
}

Icon.prototype.revert_last = function revert_last() {
    if (this.src != this.last_src || this.shadow != this.last_shadow || this.width != this.last_width || this.height != this.last_height) {
        this.src         = this.last_src;
        this.shadow      = this.last_shadow;
        this.width       = this.last_width;
        this.height      = this.last_height;
        if (!this.src) this.revertOriginal();
        this.generate();
        return true;
    } else return false;
}

//-------------------------------------------- HTML Pane --------------------------------------------
function HTMLPane(_node, _ggPoint) {
    if (!arguments.length) return this;
    
    this.node    = _node;
    this.ggPoint = _ggPoint;

    this.ggMap   = null;
}
HTMLPane.prototype = new GOverlay();

HTMLPane.prototype.initialize = function initialize(_ggMap) {
    //Called by the map after the overlay is added to the map using GMap2.addOverlay()
    this.ggMap = _ggMap;

    var pane = this.ggMap.getPane(G_MAP_MARKER_SHADOW_PANE); //returns a DIV
    pane.appendChild(this.node);
    
    //required attributes
    this.node.style.position = 'absolute';
    this.node.style.display  = 'block';
    
    //position
    this.redraw(true);
}
HTMLPane.prototype.copy   = function copy() {return this;}
HTMLPane.prototype.redraw = function redraw(force) {
    var position         = this.ggMap.fromLatLngToDivPixel(this.ggPoint);
    position.y += $(this.node).height();
    this.node.style.left = position.x + 'px';
    this.node.style.top  = position.y + 'px';
}
HTMLPane.prototype.remove = function remove() {
    if (this.node.parentNode) this.node.parentNode.removeChild(this.node);
}

//-------------------------------------------- Text Label --------------------------------------------
function Label(_html, _ggPoint, _classname) {
    if (!arguments.length) return this;
    
    this.html      = _html;
    this.classname = _classname;
    var node = document.createElement('div');
    node.className = 'google_label ' + this.classname;
    node.innerHTML = this.html;
    HTMLPane.call(this, node, _ggPoint);
}
Label.prototype = new HTMLPane();
