/* 
 * ExtInfoWindow Class, v1.0  
 *  Copyright (c) 2007, Joe Monahan (http://www.seejoecode.com) 
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *       http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 * This class lets you add an info window to the map which mimics GInfoWindow 
 * and allows for users to skin it via CSS.  Additionally it has options to 
 * pull in HTML content from an ajax request, triggered when a user clicks on 
 * the associated marker. 
 */ 
  
  
 /** 
  * Creates a new ExtInfoWindow that will initialize by reading styles from css 
  * 
  * @constructor 
  * @param {GMarker} marker The marker associated with the info window 
  * @param {String} windowId The DOM Id we will use to reference the info window 
  * @param {String} html The HTML contents 
  * @param {Object} opt_opts A contianer for optional arguments: 
  *    {String} ajaxUrl The Url to hit on the server to request some contents  
  *    {Number} paddingX The padding size in pixels that the info window will leave on  
  *                    the left and right sides of the map when panning is involved. 
  *    {Number} paddingY The padding size in pixels that the info window will leave on  
  *                    the top and bottom sides of the map when panning is involved. 
  *    {Number} beakOffset The repositioning offset for when aligning the beak element.  
  *                    This is used to make sure the beak lines up correcting if the  
  *                    info window styling containers a border. 
  */ 
 function ExtInfoWindow(marker, windowId, html, opt_opts) { 
   this.html_ = html; 
   this.marker_ = marker; 
   this.infoWindowId_ = windowId; 
  
   this.options_ = opt_opts == null ? {} : opt_opts; 
   this.ajaxUrl_ = this.options_.ajaxUrl == null ? null : this.options_.ajaxUrl; 
   this.callback_ = this.options_.ajaxCallback == null ? null : this.options_.ajaxCallback; 
  
   this.borderSize_ = this.options_.beakOffset == null ? 0 : this.options_.beakOffset; 
   this.paddingX_ = this.options_.paddingX == null ? 0 + this.borderSize_ : this.options_.paddingX + this.borderSize_; 
   this.paddingY_ = this.options_.paddingY == null ? 0 + this.borderSize_ : this.options_.paddingY + this.borderSize_; 
  
   this.map_ = null; 
  
   this.container_ = document.createElement('div'); 
   this.container_.style.position = 'relative'; 
   this.container_.style.display = 'none'; 
  
   this.contentDiv_ = document.createElement('div'); 
   this.contentDiv_.id = this.infoWindowId_ + '_contents'; 
   this.contentDiv_.innerHTML = this.html_; 
   this.contentDiv_.style.display = 'block'; 
   this.contentDiv_.style.visibility = 'hidden'; 
  
   this.wrapperDiv_ = document.createElement('div'); 
 }; 
  
 //use the GOverlay class 
 ExtInfoWindow.prototype = new GOverlay(); 
  
 /** 
  * Called by GMap2's addOverlay method.  Creates the wrapping div for our info window and adds 
  * it to the relevant map pane.  Also binds mousedown event to a private function so that they 
  * are not passed to the underlying map.  Finally, performs ajax request if set up to use ajax 
  * in the constructor. 
  * @param {GMap2} map The map that has had this extInfoWindow is added to it. 
  */ 
 ExtInfoWindow.prototype.initialize = function(map) { 
   this.map_ = map; 
  
   this.defaultStyles = { 
     containerWidth: this.map_.getSize().width / 2, 
     borderSize: 1 
   }; 
  
   this.wrapperParts = { 
     tl:{t:0, l:0, w:0, h:0, domElement: null}, 
     t:{t:0, l:0, w:0, h:0, domElement: null}, 
     tr:{t:0, l:0, w:0, h:0, domElement: null}, 
     l:{t:0, l:0, w:0, h:0, domElement: null}, 
     r:{t:0, l:0, w:0, h:0, domElement: null}, 
     bl:{t:0, l:0, w:0, h:0, domElement: null}, 
     b:{t:0, l:0, w:0, h:0, domElement: null}, 
     br:{t:0, l:0, w:0, h:0, domElement: null}, 
     beak:{t:0, l:0, w:0, h:0, domElement: null}, 
     close:{t:0, l:0, w:0, h:0, domElement: null} 
   }; 
  
   for (var i in this.wrapperParts ) { 
     var tempElement = document.createElement('div'); 
     tempElement.id = this.infoWindowId_ + '_' + i; 
     tempElement.style.visibility = 'hidden'; 
     document.body.appendChild(tempElement); 
     tempElement = document.getElementById(this.infoWindowId_ + '_' + i); 
     var tempWrapperPart = eval('this.wrapperParts.' + i);     
     tempWrapperPart.w = parseInt(this.getStyle_(tempElement, 'width')); 
     tempWrapperPart.h = parseInt(this.getStyle_(tempElement, 'height')); 
     document.body.removeChild(tempElement); 
   } 
   for (var i in this.wrapperParts) { 
     if (i == 'close' ) { 
       //first append the content so the close button is layered above it 
       this.wrapperDiv_.appendChild(this.contentDiv_); 
     } 
     var wrapperPartsDiv = null; 
     if (this.wrapperParts[i].domElement == null) { 
       wrapperPartsDiv = document.createElement('div'); 
       this.wrapperDiv_.appendChild(wrapperPartsDiv); 
     } else { 
       wrapperPartsDiv = this.wrapperParts[i].domElement; 
     } 
     wrapperPartsDiv.id = this.infoWindowId_ + '_' + i; 
     wrapperPartsDiv.style.position = 'absolute'; 
     wrapperPartsDiv.style.width = this.wrapperParts[i].w + 'px'; 
     wrapperPartsDiv.style.height = this.wrapperParts[i].h + 'px'; 
     wrapperPartsDiv.style.top = this.wrapperParts[i].t + 'px'; 
     wrapperPartsDiv.style.left = this.wrapperParts[i].l + 'px'; 
     this.wrapperParts[i].domElement = wrapperPartsDiv; 
   } 
    
   this.map_.getPane(G_MAP_FLOAT_PANE).appendChild(this.container_); 
   this.container_.id = this.infoWindowId_; 
   var containerWidth  = this.getStyle_(document.getElementById(this.infoWindowId_), 'width'); 
   this.container_.style.width = (containerWidth == null ? this.defaultStyles.containerWidth : containerWidth); 
  
   this.map_.getContainer().appendChild(this.contentDiv_); 
   this.contentWidth = this.getDimensions_(this.container_).width; 
   this.contentDiv_.style.width = this.contentWidth + 'px'; 
   this.contentDiv_.style.position = 'absolute'; 
  
   this.container_.appendChild(this.wrapperDiv_); 
  
   GEvent.bindDom(this.container_, 'mousedown', this,this.onClick_); 
   GEvent.bindDom(this.container_, 'dblclick', this,this.onClick_); 
   GEvent.bindDom(this.container_, 'DOMMouseScroll', this, this.onClick_); 
    
  
   GEvent.trigger(this.map_, 'extinfowindowopen'); 
   if (this.ajaxUrl_ != null ) { 
     this.ajaxRequest_(this.ajaxUrl_); 
   } 
 }; 
  
 /** 
  * Private function to steal mouse click events to prevent it from returning to the map. 
  * Without this links in the ExtInfoWindow would not work, and you could click to zoom or drag  
  * the map behind it. 
  * @private 
  * @param {MouseEvent} e The mouse event caught by this function 
  */ 
 ExtInfoWindow.prototype.onClick_ = function(e) { 
   if(navigator.userAgent.toLowerCase().indexOf('msie') != -1 && document.all) { 
     window.event.cancelBubble = true; 
     window.event.returnValue = false; 
   } else { 
     //e.preventDefault(); 
     e.stopPropagation(); 
   } 
 }; 
  
 /** 
  * Remove the extInfoWindow container from the map pane.  
  */ 
 ExtInfoWindow.prototype.remove = function() { 
   if (this.map_.getExtInfoWindow() != null) { 
     GEvent.trigger(this.map_, 'extinfowindowbeforeclose'); 
      
     GEvent.clearInstanceListeners(this.container_); 
     if (this.container_.outerHTML) { 
       this.container_.outerHTML = ''; //prevent pseudo-leak in IE 
     } 
     if (this.container_.parentNode) { 
       this.container_.parentNode.removeChild(this.container_); 
     } 
     this.container_ = null; 
     GEvent.trigger(this.map_, 'extinfowindowclose'); 
     this.map_.setExtInfoWindow_(null); 
   } 
 }; 
  
 /** 
  * Return a copy of this overlay, for the parent Map to duplicate itself in full. This 
  * is part of the Overlay interface and is used, for example, to copy everything in the  
  * main view into the mini-map. 
  * @return {GOverlay} 
  */ 
 ExtInfoWindow.prototype.copy = function() { 
   return new ExtInfoWindow(this.marker_, this.infoWindowId_, this.html_, this.options_); 
 }; 
  
 /** 
  * Draw extInfoWindow and wrapping decorators onto the map.  Resize and reposition 
  * the map as necessary.  
  * @param {Boolean} force Will be true when pixel coordinates need to be recomputed. 
  */ 
 ExtInfoWindow.prototype.redraw = function(force) { 
   if (!force || this.container_ == null) return; 
  
   //set the content section's height, needed so  browser font resizing does not affect the window's dimensions 
   var contentHeight = this.contentDiv_.offsetHeight; 
   this.contentDiv_.style.height = contentHeight + 'px'; 
  
   //reposition contents depending on wrapper parts. 
   //this is necessary for content that is pulled in via ajax 
   this.contentDiv_.style.left = this.wrapperParts.l.w + 'px'; 
   this.contentDiv_.style.top = this.wrapperParts.tl.h + 'px'; 
   this.contentDiv_.style.visibility = 'visible'; 
  
   //Finish configuring wrapper parts that were not set in initialization 
   this.wrapperParts.tl.t = 0; 
   this.wrapperParts.tl.l = 0; 
   this.wrapperParts.t.l = this.wrapperParts.tl.w; 
   this.wrapperParts.t.w = (this.wrapperParts.l.w + this.contentWidth + this.wrapperParts.r.w) - this.wrapperParts.tl.w - this.wrapperParts.tr.w; 
   this.wrapperParts.t.h = this.wrapperParts.tl.h; 
   this.wrapperParts.tr.l = this.wrapperParts.t.w + this.wrapperParts.tl.w; 
   this.wrapperParts.l.t = this.wrapperParts.tl.h; 
   this.wrapperParts.l.h = contentHeight; 
   this.wrapperParts.r.l = this.contentWidth + this.wrapperParts.l.w; 
   this.wrapperParts.r.t = this.wrapperParts.tr.h; 
   this.wrapperParts.r.h = contentHeight; 
   this.wrapperParts.bl.t = contentHeight + this.wrapperParts.tl.h; 
   this.wrapperParts.b.l = this.wrapperParts.bl.w; 
   this.wrapperParts.b.t = contentHeight + this.wrapperParts.tl.h; 
   this.wrapperParts.b.w = (this.wrapperParts.l.w + this.contentWidth + this.wrapperParts.r.w) - this.wrapperParts.bl.w - this.wrapperParts.br.w; 
   this.wrapperParts.b.h = this.wrapperParts.bl.h; 
   this.wrapperParts.br.l = this.wrapperParts.b.w + this.wrapperParts.bl.w; 
   this.wrapperParts.br.t = contentHeight + this.wrapperParts.tr.h; 
   this.wrapperParts.close.l = this.wrapperParts.tr.l +this.wrapperParts.tr.w - this.wrapperParts.close.w - this.borderSize_; 
   this.wrapperParts.close.t = this.borderSize_; 
   this.wrapperParts.beak.l = this.borderSize_ + (this.contentWidth / 2) - (this.wrapperParts.beak.w / 2); 
   this.wrapperParts.beak.t = this.wrapperParts.bl.t + this.wrapperParts.bl.h - this.borderSize_; 
  
   //create the decoration wrapper DOM objects 
   //append the styled info window to the container 
   for (var i in this.wrapperParts) { 
     if (i == 'close' ) { 
       //first append the content so the close button is layered above it 
       this.wrapperDiv_.insertBefore(this.contentDiv_, this.wrapperParts[i].domElement); 
     } 
     var wrapperPartsDiv = null; 
     if (this.wrapperParts[i].domElement == null) { 
       wrapperPartsDiv = document.createElement('div'); 
       this.wrapperDiv_.appendChild(wrapperPartsDiv); 
     } else { 
       wrapperPartsDiv = this.wrapperParts[i].domElement; 
     } 
     wrapperPartsDiv.id = this.infoWindowId_ + '_' + i; 
     wrapperPartsDiv.style.position='absolute'; 
     wrapperPartsDiv.style.width = this.wrapperParts[i].w + 'px'; 
     wrapperPartsDiv.style.height = this.wrapperParts[i].h + 'px'; 
     wrapperPartsDiv.style.top = this.wrapperParts[i].t + 'px'; 
     wrapperPartsDiv.style.left = this.wrapperParts[i].l + 'px'; 
     this.wrapperParts[i].domElement = wrapperPartsDiv; 
   } 
  
   //add event handler for the close box 
   var currentMarker = this.marker_; 
   var thisMap = this.map_; 
   GEvent.addDomListener(this.wrapperParts.close.domElement, 'click',  
     function() { 
       thisMap.closeExtInfoWindow(); 
     } 
   ); 
  
   //position the container on the map, over the marker 
   var pixelLocation = this.map_.fromLatLngToDivPixel(this.marker_.getPoint()); 
   this.container_.style.position = 'absolute'; 
   var markerIcon = this.marker_.getIcon(); 
   this.container_.style.left = (pixelLocation.x  
     - (this.contentWidth / 2)  
     - markerIcon.iconAnchor.x  
     + markerIcon.infoWindowAnchor.x 
   ) + 'px'; 
  
   this.container_.style.top = (pixelLocation.y 
     - this.wrapperParts.bl.h 
     - contentHeight 
     - this.wrapperParts.tl.h 
     - this.wrapperParts.beak.h 
     - markerIcon.iconAnchor.y 
     + markerIcon.infoWindowAnchor.y 
     + this.borderSize_ 
   ) + 'px'; 
  
   this.container_.style.display = 'block'; 
  
   if(this.map_.getExtInfoWindow() != null) { 
     this.repositionMap_(); 
   } 
 }; 
  
 /** 
  * Determine the dimensions of the contents to recalculate and reposition the  
  * wrapping decorator elements accordingly. 
  */ 
 ExtInfoWindow.prototype.resize = function(){ 
    
   //Create temporary DOM node for new contents to get new height 
   //This is done because if you manipulate this.contentDiv_ directly it causes visual errors in IE6 
   var tempElement = this.contentDiv_.cloneNode(true); 
   tempElement.id = this.infoWindowId_ + '_tempContents'; 
   tempElement.style.visibility = 'hidden';       
   tempElement.style.height = 'auto'; 
   document.body.appendChild(tempElement); 
   tempElement = document.getElementById(this.infoWindowId_ + '_tempContents'); 
   var contentHeight = tempElement.offsetHeight; 
   document.body.removeChild(tempElement); 
  
   //Set the new height to eliminate visual defects that can be caused by font resizing in browser 
   this.contentDiv_.style.height = contentHeight + 'px'; 
  
   var contentWidth = this.contentDiv_.offsetWidth; 
   var pixelLocation = this.map_.fromLatLngToDivPixel(this.marker_.getPoint()); 
  
   var oldWindowHeight = this.wrapperParts.t.domElement.offsetHeight + this.wrapperParts.l.domElement.offsetHeight + this.wrapperParts.b.domElement.offsetHeight;         
   var oldWindowPosTop = this.wrapperParts.t.domElement.offsetTop; 
  
   //resize info window to look correct for new height 
   this.wrapperParts.l.domElement.style.height = contentHeight + 'px'; 
   this.wrapperParts.r.domElement.style.height = contentHeight + 'px'; 
   var newPosTop = this.wrapperParts.b.domElement.offsetTop - contentHeight; 
   this.wrapperParts.l.domElement.style.top = newPosTop + 'px'; 
   this.wrapperParts.r.domElement.style.top = newPosTop + 'px'; 
   this.contentDiv_.style.top = newPosTop + 'px'; 
   windowTHeight = parseInt(this.wrapperParts.t.domElement.style.height); 
   newPosTop -= windowTHeight; 
   this.wrapperParts.close.domElement.style.top = newPosTop + this.borderSize_ + 'px'; 
   this.wrapperParts.tl.domElement.style.top = newPosTop + 'px'; 
   this.wrapperParts.t.domElement.style.top = newPosTop + 'px'; 
   this.wrapperParts.tr.domElement.style.top = newPosTop + 'px'; 
  
   this.repositionMap_(); 
 }; 
  
 /** 
  * Check to see if the displayed extInfoWindow is positioned off the viewable  
  * map region and by how much.  Use that information to pan the map so that  
  * the extInfoWindow is completely displayed. 
  * @private 
  */ 
 ExtInfoWindow.prototype.repositionMap_ = function(){ 
   //pan if necessary so it shows on the screen 
   var mapNE = this.map_.fromLatLngToDivPixel( 
     this.map_.getBounds().getNorthEast() 
   ); 
   var mapSW = this.map_.fromLatLngToDivPixel( 
     this.map_.getBounds().getSouthWest() 
   ); 
   var markerPosition = this.map_.fromLatLngToDivPixel( 
     this.marker_.getPoint() 
   ); 
  
   var panX = 0; 
   var panY = 0; 
   var paddingX = this.paddingX_; 
   var paddingY = this.paddingY_; 
   var infoWindowAnchor = this.marker_.getIcon().infoWindowAnchor; 
   var iconAnchor = this.marker_.getIcon().iconAnchor; 
  
   //test top of screen   
   var windowT = this.wrapperParts.t.domElement; 
   var windowL = this.wrapperParts.l.domElement; 
   var windowB = this.wrapperParts.b.domElement; 
   var windowR = this.wrapperParts.r.domElement; 
   var windowBeak = this.wrapperParts.beak.domElement; 
  
   var offsetTop = markerPosition.y - ( -infoWindowAnchor.y + iconAnchor.y +  this.getDimensions_(windowBeak).height + this.getDimensions_(windowB).height + this.getDimensions_(windowL).height + this.getDimensions_(windowT).height + this.paddingY_); 
   if (offsetTop < mapNE.y) { 
     panY = mapNE.y - offsetTop; 
   } else { 
     //test bottom of screen 
     var offsetBottom = markerPosition.y + this.paddingY_; 
     if (offsetBottom >= mapSW.y) { 
       panY = -(offsetBottom - mapSW.y); 
     } 
   } 
  
   //test right of screen 
   var offsetRight = Math.round(markerPosition.x + this.getDimensions_(this.container_).width/2 + this.getDimensions_(windowR).width + this.paddingX_ + infoWindowAnchor.x - iconAnchor.x); 
   if (offsetRight > mapNE.x) { 
     panX = -( offsetRight - mapNE.x); 
   } else { 
     //test left of screen 
     var offsetLeft = - (Math.round( (this.getDimensions_(this.container_).width/2 - this.marker_.getIcon().iconSize.width/2) + this.getDimensions_(windowL).width + this.borderSize_ + this.paddingX_) - markerPosition.x - infoWindowAnchor.x + iconAnchor.x); 
     if( offsetLeft < mapSW.x) { 
       panX = mapSW.x - offsetLeft; 
     } 
   } 
  
   if (panX != 0 || panY != 0 && this.map_.getExtInfoWindow() != null ) { 
     this.map_.panBy(new GSize(panX,panY)); 
   } 
 }; 
  
 /** 
  * Private function that handles performing an ajax request to the server.  The response 
  * information is assumed to be HTML and is placed inside this extInfoWindow's contents region. 
  * Last, check to see if the height has changed, and resize the extInfoWindow accordingly. 
  * @private 
  * @param {String} url The Url of where to make the ajax request on the server 
  */ 
 ExtInfoWindow.prototype.ajaxRequest_ = function(url){ 
   var thisMap = this.map_; 
   var thisCallback = this.callback_; 
   GDownloadUrl(url, function(response, status){ 
     var infoWindow = document.getElementById(thisMap.getExtInfoWindow().infoWindowId_ + '_contents'); 
     if (response == null || status == -1 ) { 
       infoWindow.innerHTML = '<span class="error">ERROR: The Ajax request failed to get HTML content from "' + url + '"</span>'; 
     } else { 
       infoWindow.innerHTML = response; 
     } 
     if (thisCallback != null ) { 
       thisCallback(); 
     } 
     thisMap.getExtInfoWindow().resize(); 
     GEvent.trigger(thisMap, 'extinfowindowupdate'); 
   }); 
 }; 
  
 /** 
  * Private function derived from Prototype.js to get a given element's 
  * height and width 
  * @private 
  * @param {Object} element The DOM element that will have height and  
  *                    width will be calculated for it. 
  * @return {Object} Object with keys: width, height 
  */ 
 ExtInfoWindow.prototype.getDimensions_ = function(element) { 
   var display = this.getStyle_(element, 'display'); 
   if (display != 'none' && display != null) { // Safari bug 
     return {width: element.offsetWidth, height: element.offsetHeight}; 
   } 
  
   // All *Width and *Height properties give 0 on elements with display none, 
   // so enable the element temporarily 
   var els = element.style; 
   var originalVisibility = els.visibility; 
   var originalPosition = els.position; 
   var originalDisplay = els.display; 
   els.visibility = 'hidden'; 
   els.position = 'absolute'; 
   els.display = 'block'; 
   var originalWidth = element.clientWidth; 
   var originalHeight = element.clientHeight; 
   els.display = originalDisplay; 
   els.position = originalPosition; 
   els.visibility = originalVisibility; 
   return {width: originalWidth, height: originalHeight}; 
 }; 
  
 /** 
  * Private function derived from Prototype.js to get a given element's 
  * value that is associated with the passed style 
  * @private 
  * @param {Object} element The DOM element that will be checked. 
  * @param {String} style The style name that will be have it's value returned. 
  * @return {Object} 
  */ 
 ExtInfoWindow.prototype.getStyle_ = function(element, style) { 
   var found = false; 
   style = this.camelize_(style); 
   var value = element.style[style]; 
   if (!value) { 
     if (document.defaultView && document.defaultView.getComputedStyle) { 
       var css = document.defaultView.getComputedStyle(element, null); 
       value = css ? css[style] : null; 
     } else if (element.currentStyle) { 
       value = element.currentStyle[style]; 
     } 
   } 
   if((value == 'auto') && (style == 'width' || style == 'height') && (this.getStyle_(element, 'display') != 'none')) { 
     if( style == 'width' ) { 
       value = element.offsetWidth; 
     }else { 
       value = element.offsetHeight; 
     } 
   } 
   return (value == 'auto') ? null : value; 
 }; 
  
 /** 
  * Private function pulled from Prototype.js that will change a hyphened 
  * style name into camel case. 
  * @private 
  * @param {String} element The string that will be parsed and made into camel case 
  * @return {String} 
  */ 
 ExtInfoWindow.prototype.camelize_ = function(element) { 
   var parts = element.split('-'), len = parts.length; 
   if (len == 1) return parts[0]; 
   var camelized = element.charAt(0) == '-' 
     ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) 
     : parts[0]; 
  
   for (var i = 1; i < len; i++) { 
     camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); 
   } 
   return camelized; 
 }; 
  
 GMap.prototype.ExtInfoWindowInstance_ = null; 
 GMap.prototype.ClickListener_ = null; 
 GMap.prototype.InfoWindowListener_ = null; 
  
 /** 
  * Creates a new instance of ExtInfoWindow for the GMarker.  Register the newly created  
  * instance with the map, ensuring only one window is open at a time. If this is the first 
  * ExtInfoWindow ever opened, add event listeners to the map to close the ExtInfoWindow on  
  * zoom and click, to mimic the default GInfoWindow behavior. 
  * 
  * @param {GMap} map The GMap2 object where the ExtInfoWindow will open 
  * @param {String} cssId The id we will use to reference the info window 
  * @param {String} html The HTML contents 
  * @param {Object} opt_opts A contianer for optional arguments: 
  *    {String} ajaxUrl The Url to hit on the server to request some contents  
  *    {Number} paddingX The padding size in pixels that the info window will leave on  
  *                    the left and right sides of the map when panning is involved. 
  *    {Number} paddingX The padding size in pixels that the info window will leave on  
  *                    the top and bottom sides of the map when panning is involved. 
  *    {Number} beakOffset The repositioning offset for when aligning the beak element.  
  *                    This is used to make sure the beak lines up correcting if the  
  *                    info window styling containers a border. 
  */ 
 GMarker.prototype.openExtInfoWindow = function(map, cssId, html, opt_opts) { 
   if (map == null) { 
     throw 'Error in GMarker.openExtInfoWindow: map cannot be null'; 
     return false; 
   } 
   if (cssId == null || cssId == '') { 
     throw 'Error in GMarker.openExtInfoWindow: must specify a cssId'; 
     return false; 
   } 
    
   map.closeInfoWindow(); 
   if (map.getExtInfoWindow() != null) { 
     map.closeExtInfoWindow(); 
   } 
   if (map.getExtInfoWindow() == null) { 
     map.setExtInfoWindow_( new ExtInfoWindow( 
       this, 
       cssId, 
       html, 
       opt_opts 
     ) ); 
     if (map.ClickListener_ == null) { 
       //listen for map click, close ExtInfoWindow if open 
       map.ClickListener_ = GEvent.addListener(map, 'click', 
       function(event) { 
           if( !event && map.getExtInfoWindow() != null ){ 
             map.closeExtInfoWindow(); 
           } 
         } 
       ); 
     } 
     if (map.InfoWindowListener_ == null) { 
       //listen for default info window open, close ExtInfoWindow if open 
       map.InfoWindowListener_ = GEvent.addListener(map, 'infowindowopen',  
       function(event) { 
           if (map.getExtInfoWindow() != null) { 
             map.closeExtInfoWindow(); 
           } 
         } 
       ); 
     } 
     map.addOverlay(map.getExtInfoWindow()); 
   } 
 }; 
  
 /** 
  * Remove the ExtInfoWindow instance 
  * @param {GMap2} map The map where the GMarker and ExtInfoWindow exist 
  */ 
 GMarker.prototype.closeExtInfoWindow = function(map) { 
   if( map.getExtInfWindow() != null ){ 
     map.closeExtInfoWindow(); 
   } 
 }; 
  
 /** 
  * Get the ExtInfoWindow instance from the map 
  */ 
 GMap2.prototype.getExtInfoWindow = function(){ 
   return this.ExtInfoWindowInstance_; 
 }; 
 /** 
  * Set the ExtInfoWindow instance for the map 
  * @private 
  */ 
 GMap2.prototype.setExtInfoWindow_ = function( extInfoWindow ){ 
   this.ExtInfoWindowInstance_ = extInfoWindow; 
 } 
 /** 
  * Remove the ExtInfoWindow from the map 
  */ 
 GMap2.prototype.closeExtInfoWindow = function(){ 
   if( this.getExtInfoWindow() != null ){ 
     this.ExtInfoWindowInstance_.remove(); 
   } 
 }; 
 
