//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
    
//-------------------------------------------- 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         = [];
    
    //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.lat                   = this.hV(".latitude" );
    this.lng                   = this.hV(".longitude");
    this.zoom                  = this.hV(".zoom");
    this.user_centered         = this.hX(".user_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
    
    //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));
    if (this.user_centered && this.user && this.user.hasGEO) this.ggMap.setCenter(new GLatLng(this.user.lat, this.user.lng), this.zoom);
    //apply 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, "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.defered_setup = function defered_setup() {     
    var self = this;

    //decide which marker manager to use (different functions available)
    if      (window.MarkerClusterer) this.ggMgr = new MarkerClusterer(this.ggMap); //, markers, {maxZoom: zoom, gridSize: size, styles: styles[style]});
    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();
    setTimeout(function(){$(document).trigger("mapready")}, 0);
}

//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.addOverlay    = function addOverlay(ggOverlay)    {return this.ggMap.addOverlay(ggOverlay);}
Map.prototype.removeOverlay = function removeOverlay(ggOverlay) {return this.ggMap.removeOverlay(ggOverlay);}
Map.prototype.zoomToBounds  = function zoomToBounds(bounds) {
    this.ggMap.setZoom(this.ggMap.getBoundsZoomLevel(bounds));
    this.ggMap.setCenter(bounds.getCenter());
}

//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.ggMap.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.infowindowclose = function infowindowclose() {
}
Map.prototype.infowindowopen = function infowindowopen() {
}
Map.prototype.showFirstAddress = function showFirstAddress(searchKey, callback) {
    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){
            if (ggLocalSearch && ggLocalSearch.results && ggLocalSearch.results.length) {
                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);
            }
        });
    }
}

Map.prototype.findAddresses = function findAddresses(searchKey, callback) {
    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 (callback) callback(self.ggLocalSearch, searchKey, self);
        });
        if (searchKey.replace(/^\s+|\s+$/gim, '')) {
            this.ggLocalSearch.execute(searchKey);
            this.lastSearchKey = searchKey;
        }
    }
}

Map.prototype.followAddress = function followAddress(jContainer, callback, immediate) {
    //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
        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 (window.searching) window.searching(this);
                if (self.addressFollowTimer) clearTimeout(self.addressFollowTimer);
                self.addressFollowTimer = setTimeout(function(){
                    self.sourceAddressChanged(jContainer, callback);
                    self.addressFollowTimer = null;
                }, 2000);
            }
        });
        //for selects and radio buttons
        jContainer.change(function(){
            if (self.addressFollowTimer) clearTimeout(self.addressFollowTimer);
            if (window.searching) window.searching(this);
            self.sourceAddressChanged(jContainer, callback);
        });
        //immediate (deferred) default = true
        if (immediate != false && this.immediate_follow == 'true') setTimeout(function(){
            if (window.searching) window.searching(this);
            self.sourceAddressChanged(jContainer, callback);
        }, 0);
    }
}

Map.prototype.sourceAddressChanged = function sourceAddressChanged(jContainer, callback) {
    var self = this;
    var searchKey = '';
    
    //construct address
    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);
        }
    );
}

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.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;
    //for direct private friend operations on my ggMarker
    this.map               = null;       //direct access to map
    this.add_markers       = null;       //temp array of ggMarkers
    this.rem_markers       = null;       //temp array of ggMarkers
    this.mgr_updateMarkers = null;       //defered addition timer

    //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());
    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);});
    }
    if (_jInfoWindow) this.bindInfoWindow(_jInfoWindow); //sets base value

    if (window.addGEOMarker_after) addGEOMarker_after(this);
}
Marker.prototype.gLetterid = 1;

//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) {
    if (!jIW || !jIW.length) jIW = this.jInfoWindow;
    if (jIW && jIW.length) return this.ggMarker.openInfoWindow(jIW.get(0));
}

//other
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) {
    //does not copy constructor properties, do that manually
    //this is because Google Maps does not allow access to them separately
    var self = this;
    //base manager marker removal - swap new marker in
    if (this.map && this.map.ggMgr) {
        if (this.map.ggMgr.addMarkers) {
            //arrays
            if (!this.add_markers) this.add_markers = [];
            if (!this.rem_markers) this.rem_markers = [];
            this.add_markers.push(ggNewMarker);
            this.rem_markers.push(this.ggMarker);
            //defered addition
            if (!this.mgr_updateMarkers) this.mgr_updateMarkers = setTimeout(function(){
                self.map.ggMgr.addMarkers(self.add_markers, 0);
                for (var i = 0; i < self.rem_markers.length; i++) self.map.ggMgr.removeMarker(self.rem_markers[i]);
                self.map.refresh();
                //reset ready for next round
                cinfo("update [" + self.add_markers.length + "] added, [" + self.rem_markers.length + "] removed");
                self.add_markers       = null;
                self.rem_markers       = null;
                self.mgr_updateMarkers = null;
            }, 0);
        } else {
            //normal map version
            this.map.ggMgr.removeOverlay(this.ggMarker);
            this.map.ggMgr.addOverlay(ggNewMarker, 0);
        }
    } else cwarn("no map or manager!");
    delete this.ggMarker;
    this.ggMarker = ggNewMarker;
    //attach events and objects
    if (this.jInfoWindow) this.bindInfoWindow(this.jInfoWindow);
    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);});
    }
    return this;
}

Marker.prototype.bindInfoWindow = function bindInfoWindow(jInfoWindow) {
    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
        var oinfo = this.jInfoWindow.get(0);           //raw DOM object
        this.ggMarker.bindInfoWindow(oinfo);           //align events to show and hide it
        this.ggMarker.infowindow = oinfo;              //attach 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
    if (!this.circle && this.ggMarker) {
        var ggLatLng     = this.ggMarker.getLatLng();
        var ggNewLL      = new GLatLng(ggLatLng.lat() + 0.00001, ggLatLng.lng());
        this.circle      = new GMarker(ggNewLL, {title:"highlight", icon:Marker.highlightIcon});
        this.map.addMarker(this.circle);
        //this.bounce();
    }
}
Marker.prototype.dehighlight = function dehighlight() {
    if (this.circle) {
        this.map.removeMarker(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();