/** 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();  }};