/**
 * Scripts for displaying the GTA4 map using the Google Maps API.
 */

/**
 * Create the map in a given div.
 * @param HTMLElement mapDiv
 * @return GMap2|false
 */
function createMap(mapDiv)
{
    // Check for browser incompatiblity
    if (!GBrowserIsCompatible()) {
        return false;
    }

    // Start creating our custom map. We need a custom map type consisting
    // our own custom tile layer. Google needs us to provide Copyright information
    // for tiles so we start by setting that up.
    var myCopyright = new GCopyrightCollection("(c) ");
    myCopyright.addCopyright(new GCopyright(1, new GLatLngBounds(new GLatLng(-90,-180), new GLatLng(90,180)), 0, '2008 Rockstar Games, GTANet'));
    
    // Create a new tile layer with our copyright information.
    // We have tiles for zoom levels 1 to 5.
    var tiles = new GTileLayer(myCopyright, 1, 5);

    // Implement the abstract functions
    // Generate URLs to tiles given a specific point at a specific zoom level.
    tiles.getTileUrl = function(point, zoom) {
        var len = Math.pow(2, zoom);
        if ((point.x > len-1) || (point.y > len-1)) return "";
        var prefix_id = "";
        var tile_id = (point.y * len) + point.x + 1;
        if (tile_id < 10) prefix_id = "0";
        return "http://media.gtanet.com/gta4/images/map/tiles/"+ zoom + "_" + prefix_id + tile_id + ".jpg";
    };
    // Our tiles are not PNGs and we want full opacity
    tiles.isPng      = function() { return false; };
    tiles.getOpacity = function() { return 1.0;  };

    // Using the custom tile layer, create a custom map type. Also using the euclidean
    // projection for a flat earth. Projection needs 18 levels for polylines to work.
    var gta4map = new GMapType([tiles], new EuclideanProjection(18), "Euclidean");

    // Create a Google Map in our map div, using the gta4 custom maptype
    var map = new GMap2(mapDiv, {mapTypes: [gta4map], backgroundColor: '#5d7c8e'});
    
    // Center it (required)
    map.setCenter(new GLatLng(0,0), 0);
    
    // Prevent memory leak
    Event.observe(window, 'unload', GUnload);
    
    // Return the GMap2
    return map;
}


/**
* "Euclidean" projection allows us to plot our points on a flat world,
* as in the case for the GTA4 map.
* Unclear exactly how this works and few comments were provided in the
* original example from which this was taken.
*
* http://econym.org.uk/gmap/customflat.htm
* http://www.columbia.edu/~nlevitt/googlemaps-gwt/doc/com/mapitz/gwt/googleMaps/client/GProjection.html
*/
EuclideanProjection = Class.create(new GProjection, {

    /**
    * Constructor - takes the number of zoom levels to be considered in the projection.
    * @param int a
    */
    initialize: function(a) {

        this.pixelsPerLonDegree = [];
        this.pixelsPerLonRadian = [];
        this.pixelOrigo = [];
        this.tileBounds = [];

        var b = 256;
        var c = 1;

        for( var d = 0; d < a; d++ ) {
            var e = b/2;

            this.pixelsPerLonDegree.push(b/360);
            this.pixelsPerLonRadian.push(b/(2*Math.PI));
            this.pixelOrigo.push(new GPoint(e,e));
            this.tileBounds.push(c);

            b *= 2;
            c *= 2;
        }

    },

    /**
    * A method for converting latitudes and longitudes to pixel coordinates
    * @param GLatLng object a position
    * @param int b zoom level
    * @return GPoint object
    */
    fromLatLngToPixel: function(a, b) {
        var c = Math.round(this.pixelOrigo[b].x + a.lng() * this.pixelsPerLonDegree[b]);
        var d = Math.round(this.pixelOrigo[b].y + (-2 * a.lat()) * this.pixelsPerLonDegree[b]);
        return new GPoint(c,d);
    },
    

    /**
    * A method for converting pixel coordinates to latitudes and longitudes
    * (inverse of above)
    * @param GPoint object a
    * @param b int zoom level
    * @param c bool true for "unbounded" - i.e. does not wrap beyond -180 or 180 degrees
    * @return GLatLng object
    */
    fromPixelToLatLng: function(a, b, c) {
        var d = (a.x - this.pixelOrigo[b].x) / this.pixelsPerLonDegree[b];
        var e = -0.5 * (a.y - this.pixelOrigo[b].y) / this.pixelsPerLonDegree[b];
        return new GLatLng(e, d, true)
    },

    /**
    * A method that checks if the x and y values are in range
    * @param GPoint a object position of tile
    * @param int b zoom level
    * @param int tilesize c
    * @return bool
    */
    tileCheckRange: function(a, b, c){
        var d=this.tileBounds[b];
        if ( a.y < 0 || a.y >= d || a.x < 0 || a.x >= d ){
            return false;
        }
        return true;
    },

    /**
    * A method that returns the width of the tilespace. Set to pseudo-infinite
    * here to prevent tiling.
    * @param int zoom
    */
    getWrapWidth: function(zoom) {
        return 99999999999999;
    }

});


/**
 * Load data with AJAX and run registered callbacks on completion.
 * Many tasks will require the data file to be loaded. Instead of
 * mixing up all the code so that things occur in the right order,
 * callbacks can be added to this object. If a callback is added
 * before the request has finished, it is added to the stack and all
 * are called upon response. If a callback is added after the response
 * has been received, it is run immediately.
 */
Loader = Class.create({

    /**
     * Success callbacks.
     * @var array
     */
    _successCallbacks: null,
    
    /**
     * Fail callbacks
     * @var array
     */
    _failCallbacks: null,
    
    /**
     * Prototype's Ajax.Request object
     * @var object
     */
    _req: null,
    
    /**
     * Is the request complete?
     * @var object
     */
    _completedRequest: null,
    
    
    /**
     * Start the request.
     */
    initialize: function(url)
    {
        // Set defaults
        this._successCallbacks = new Array();
        this._failCallbacks    = new Array();
        this._complete         = null;
        
        // Create the AJAX options object
        var options = {
            method: 'get'
        };
        options.onFailure = this._failed.bind(this);
        options.onSuccess = this._success.bind(this);
        
        // Make the request with prototype
        this._req = new Ajax.Request(url, options);
    },
    
    
    /**
     * Register a success callback.
     * @param function callback
     */
    onSuccess: function(callback)
    {
        if (this._completedRequest) {
            if (this._req.success()) {
                callback.curry(this._completedRequest).defer();
            }
            return;
        }
        
        this._successCallbacks.push(callback);
    },
    
    
    /**
     * Register a failure callback.
     * @param function callback
     */
    onFailure: function(callback)
    {
        if (this._completedRequest) {
            if (!this._req.success()) {
                callback.curry(this._completedRequest).defer();
            }
            return;
        }
        
        this._failCallbacks.push(callback);
    },
    
    
    /**
     * Run the failure callbacks.
     * @param Ajax.Response response
     */
    _failed: function(response) 
    {
        // Mark as complete
        this._completedRequest = response;
        
        // Call registered callbacks
        for (var i = 0, l = this._failCallbacks.length; i < l; ++i) {
            (this._failCallbacks[i])(response);
        }
    },
    
    
    /**
     * Run the success callbacks.
     * @param Ajax.Response response
     */
    _success: function(response)
    {
        // Mark as complete
        this._completedRequest = response;
        
        // Call registered callbacks
        for (var i = 0, l = this._successCallbacks.length; i < l; ++i) {
            (this._successCallbacks[i])(response);
        }
    }
    
});


/**
 * Manage markers/points on the map.
 */
Points = {
    
    /**
     * GMap2 instance
     */
    _map: null,
    
    /**
     * Markers currently on the map
     */
    _onMap: new Array(),
    
    
    /**
     * Set the map.
     * @param GMap2 map
     */
    setMap: function(map)
    {
        this._map = map;
    },
    
    
    /**
     * Create an icon.
     * @param string src URL to icon image
     * @param array size [width, height] of icon image
     * @return GIcon
     */
    createIcon: function(src, size)
    {
        var icon        = new GIcon(G_DEFAULT_ICON);
        
        icon.image      = src;
        icon.iconSize   = new GSize(size[0], size[1]);
        icon.shadow     = null;
        icon.shadowSize = new GSize(0,0);
        icon.iconAnchor = new GPoint(icon.iconSize.width/2, icon.iconSize.height/2);
        icon.infoWindowAnchor = new GPoint(icon.iconSize.width/2, 0);
        
        return icon;
    },
    
    
    /**
     * Create a marker.
     * @param object|array position can be either GLatLng object, object with .lat and .lng
     *                              properties or array of [lat,lng]
     * @param object icon either GIcon object or object with .src (string) url to icon image
     *                                                       .size (array) [width, height] of image
     * @param GMarkerOptions options
     * @return GMarker
     */
    createMarker: function(position, icon, options)
    {
        // Create options if none supplied
        if (!options) {
            options = {};
        }

        // Get the GLatLng
        if (!(position instanceof GLatLng)) {
            if (position.lat) {
                position = new GLatLng(position.lat, position.lng);
            } else {
                position = new GLatLng(position[0], position[1]);
            }
        }

        // Get the GIcon
        if (icon) {
            if (!(icon instanceof GIcon)) {
                icon = this.createIcon(icon.src, icon.size);
            }
        } else {
            icon = G_DEFAULT_ICON;
        }
        
        // Add the icon to the options
        options.icon = icon;

        // Create the marker and return it
        return new GMarker(position, options);
    },
    
    
    /**
     * Add an info window to appear on clicking a marker.
     * @param GMarker marker
     * @param function contentCallback
     * @param string customStyle the CSS prefix for custom info window styles (see extinfowindow)
     *                           omit this to use the default Google Maps infowindow
     * @return void
     */
    addInfoWindowToMarker: function(marker, contentCallback, customStyle)
    {
        GEvent.addListener(marker, 'click', this._openInfoWindow.bind(this, marker, contentCallback, customStyle));
    },
    
    
    /**
     * Remove an info window from a marker.
     * @param GMarker marker
     */
    removeInfoWindowFromMarker: function(marker)
    {
        GEvent.clearListeners(marker, 'click');
    },
    
    
    /**
     * Open an info window.
     * @see addInfoWindowToMarker
     */
    _openInfoWindow: function(marker, contentCallback, customStyle)
    {   
        // Pan properly (extinfowindow is a bit weird and pans off all over the place)
        this._map.panTo(marker.getLatLng());
        ExtInfoWindow.prototype.repositionMap_ = function(){};
        
        // Check we have some content
        var content = (contentCallback)();
        if (content === null) {
            return;
        }
        
        // Open the window
        if (customStyle) {
            marker.openExtInfoWindow(
              this._map,
              customStyle,
              content,
              {beakOffset: 3, paddingX: 10, paddingY: 20}
            );
            this._map.getExtInfoWindow().marker = marker;
        } else {
            marker.openInfoWindowHtml(content);
            this._map.getInfoWindow().marker = marker;
        }
    },
    
    
    /**
     * Add a marker to the map.
     * @param GMarker marker
     */
    addToMap: function(marker)
    {
        this._map.addOverlay(marker);
        this._onMap.push(marker);
    },
    
    
    /**
     * Remove a marker from the map.
     * @param GMarker marker
     */
    removeFromMap: function(marker)
    {
        this._map.removeOverlay(marker);
        this._onMap = this._onMap.without(marker);
    },
    
    
    /**
     * Get the markers which are currently on the map.
     * @return array
     */
    getMarkersOnMap: function()
    {
        return this._onMap;
    },
    
    
    /**
     * Get the marker for the currently open window (or null if none exists)
     * @return GMarker|null
     */
    getOpenWindowMarker: function()
    {
        var gInfoWindow   = this._map.getInfoWindow();
        var extInfoWindow = this._map.getExtInfoWindow();
        
        if (gInfoWindow && !gInfoWindow.isHidden()) {
            return gInfoWindow.marker;
        } else if (extInfoWindow) {
            return extInfoWindow.marker;
        }
        return null;
    },
    
    
    /**
     * Close the currently open info window.
     * @return void
     */
    closeWindow: function()
    {
        var gInfoWindow   = this._map.getInfoWindow();
        var extInfoWindow = this._map.getExtInfoWindow();
        
        if (gInfoWindow && !gInfoWindow.isHidden()) {
            gInfoWindow.marker = null;
            this._map.closeInfoWindow();
        } else if (extInfoWindow) {
            extInfoWindow.marker = null
            this._map.closeExtInfoWindow();
        }
    }
};


/**
 * GTA4 map overview control 
 */
OverviewControl = Class.create(new GControl(), {
    overviewSize:   [200, 127], // Width Height
    overviewBounds: {lat:[-53, 56], lng: [-168, 175]}, // Bottom Top Left Right
    overviewUrl: 'http://media.gtanet.com/gta4/images/map/overview.gif',
    
    initialize: function(map)
    {
        if (!(map instanceof GMap2)) {
            return false;
        }
        
        // Store map reference
        this._map = map;
        
        // Create the overview div and add to map
        this._containerDiv = new Element('div',{id:'ee'});
        this._containerDiv.setStyle({background: '#5d7c8d url('+this.overviewUrl+') no-repeat top left', 
                                     height: this.overviewSize[1]+'px',
                                     width:  this.overviewSize[0]+'px',
                                     border: '3px solid #eee', borderRight: 0, borderBottom: 0,
                                     overflow: 'hidden'});                               
        this._map.getContainer().appendChild(this._containerDiv);

        // Create the view rectangle
        this._viewDiv = new Element('div');
        this._viewDiv.setStyle({position: 'relative', border: '1px solid #ce0000'});
        this._containerDiv.appendChild(this._viewDiv);
        
        // Colour it
        this._viewDiv.appendChild(new Element('div').setStyle({background: '#7a0000', opacity: 0.4, height: '100%'}));

        // Work out where to position the rectangle
        this.reposition();

        // Reposition when map changes
        GEvent.addListener(this._map, 'move', this.reposition.bind(this));
        GEvent.addListener(this._map, 'zoomend', this.reposition.bind(this));
        Event.observe(window, 'resize', this.reposition.bind(this));
        
        return this._containerDiv;
    },
    reposition: function()
    {
        // Hide at low zoom levels
        if (this._map.getZoom() > 2) {
            this._containerDiv.show();
        } else {
            this._containerDiv.hide();
            return;
        }
    
        var xScaleFactor = this.overviewSize[0]/(Math.abs(this.overviewBounds.lng[0])+Math.abs(this.overviewBounds.lng[1]));
        var yScaleFactor = this.overviewSize[1]/(Math.abs(this.overviewBounds.lat[0])+Math.abs(this.overviewBounds.lat[1]));

        var bounds = this._map.getBounds();
        var size   = bounds.toSpan();
        var width  = Math.round( (size.lng() / (Math.abs(this.overviewBounds.lng[0]) + Math.abs(this.overviewBounds.lng[1]))) * this.overviewSize[0]);
        var height = Math.round( (size.lat() / (Math.abs(this.overviewBounds.lat[0]) + Math.abs(this.overviewBounds.lat[1]))) * this.overviewSize[1]);
        var top    = Math.round( (this.overviewBounds.lat[1] + (bounds.getNorthEast().lat()*-1)) * yScaleFactor );
        var left   = Math.round( (this.overviewBounds.lng[1] + (bounds.getSouthWest().lng()))    * xScaleFactor );
       
        this._viewDiv.setStyle({width: width+'px', height: height+'px', top: top+'px', left: left-3+'px'});
    },
    getDefaultPosition: function()
    {
        return new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(0, 0));
    }
});


/*
* 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.
 *    {Number} maxPanning The maximum panning distance when the marker is not 
 *                    in screen. This is used to make sure the map will not pan to
 *                     much when opening a marker outside the viewport
 */
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.maxContent_ = this.options_.maxContent == null ? null : this.options_.maxContent;
  this.maximizeEnabled_ = this.maxContent_ == null ? false : true;
  this.isMaximized_ = false;

  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.maxPanning_ = this.options_.maxPanning == null ? 500 : this.options_.maxPanning;

  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;

  if( this.maximizeEnabled_ ){
    this.maxWidth_ = this.map_.getSize().width * 0.9;
    this.maxHeight_ = this.map_.getSize().height * 0.9;
  }

  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}
  };
  if( this.maximizeEnabled_ ){
    this.wrapperParts.max = {t:0, l:0, w:0, h:0, domElement: null};
    this.wrapperParts.min = {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 = this.wrapperParts[i];    
    tempWrapperPart.w = parseInt(this.getStyle_(tempElement, 'width'), 10);
    tempWrapperPart.h = parseInt(this.getStyle_(tempElement, 'height'), 10);
    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_);
  
  if( this.maximizeEnabled_ ){
    this.minWidth_ = this.getDimensions_(this.container_).width;
  }
  
  if (this.maximizeEnabled_) {
    thisMap = this.map_;
    thisMaxWidth = this.maxWidth_;
    thisMaxHeight = this.maxHeight_;
    thisContainer = this.container_;
    thisMaxContent = this.maxContent_;
    if(this.marker_) {
      GEvent.trigger(this.marker_, 'extinfowindowbeforeclose'); 
    }

    thisMinWidth = this.container_.style.width;
    thisMinHeight = this.container_.style.height;
    //add event handler for maximize and minimize icons
    GEvent.addDomListener(this.wrapperParts.max.domElement, 'click', 
      function() {
        var infoWindow = thisMap.getExtInfoWindow();
        infoWindow.container_.style.width = thisMaxWidth + 'px';
        infoWindow.ajaxRequest_(thisMaxContent);

    if(this.marker_) {
      GEvent.trigger(this.marker_, 'extinfowindowclose');
    }
        infoWindow.isMaximized_ = true;
        infoWindow.redraw(true);
      
        //swap min/max icons
        infoWindow.toggleMaxMin_();
      }
    );
    GEvent.addDomListener(this.wrapperParts.min.domElement, 'click', 
      function() {
        var infoWindow = thisMap.getExtInfoWindow();
        infoWindow.container_.style.width = thisMinWidth;
        infoWindow.container_.style.height = thisMinHeight;
        if (infoWindow.ajaxUrl_ != null ) {
           infoWindow.ajaxRequest_(this.ajaxUrl_);
        }else{
          infoWindow.contentDiv_.innerHTML = infoWindow.html_;
        }
        
        infoWindow.isMaximized_ = false;
        infoWindow.redraw(true);
        infoWindow.resize();

        //swap min/max icons
        infoWindow.toggleMaxMin_();
      }
    );
    
    this.toggleMaxMin_();
    
  }

  var stealEvents = ['mousedown', 'dblclick', 'DOMMouseScroll'];
  for( i=0; i < stealEvents.length; i++ ){
    GEvent.bindDom(this.container_, stealEvents[i], 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';
  
  this.contentWidth = this.getDimensions_(this.container_).width;
  this.contentDiv_.style.width = this.container_.style.width;

  //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.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_;
  this.wrapperParts.close.l = this.wrapperParts.tr.l +this.wrapperParts.tr.w - this.wrapperParts.close.w - this.borderSize_;
  this.wrapperParts.close.t = this.borderSize_;
  if( this.maximizeEnabled_ ){
    this.wrapperParts.max.l = this.wrapperParts.close.l - this.wrapperParts.max.w - 5;
    this.wrapperParts.max.t = this.wrapperParts.close.t;
    this.wrapperParts.min.l = this.wrapperParts.max.l;
    this.wrapperParts.min.t = this.wrapperParts.max.t;
  }

  //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 icon
  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_();
  }
};

ExtInfoWindow.prototype.toggleMaxMin_ = function(){
  if( this.wrapperParts.max.domElement != null && this.wrapperParts.min.domElement != null ){
    if (this.isMaximized_) {
      this.wrapperParts.max.domElement.style.display = 'none';
      this.wrapperParts.min.domElement.style.display = 'block';
    } else {
      this.wrapperParts.max.domElement.style.display = 'block';
      this.wrapperParts.min.domElement.style.display = 'none';
    }
  }
};

/**
 * 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.container_.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, 10);
  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 ) {
      if ((panY < 0 - this.maxPanning_ || panY > this.maxPanning_) && (panX < 0 - this.maxPanning_ || panX > this.maxPanning_)) {
        this.map_.setCenter(this.marker_.getPoint());
      }else {
        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){
    if (thisMap.getExtInfoWindow() !== null) {
      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);
  if (element.id == this.infoWindowId_ && style == 'width' && element.style.display == 'none') {
  	element.style.visibility = 'hidden';
	  element.style.display = '';
  }
  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;
    }
  }
  if (element.id == this.infoWindowId_ && style == 'width' && element.style.display != 'none') {
  	element.style.display = 'none';
  	element.style.visibility = 'visible';	
  }
  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.getExtInfoWindow() != 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();
  }
};

/**
 * END OF extinfowindow.js
 */
 
 /**
  * labeledmarker.js
  */
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('5 4(a,b){1.o=b;1.q=b.u||"";1.v=b.O||"P";1.r=b.Q||k R(0,0);1.s=(b.S==l)?l:m;1.w=b.A||"";1.9=m;1.d=m;6(b.B){b.B=l}7.e(1,f)}4.3=k 7(k T(0,0));4.3.C=5(a){7.3.C.e(1,f);1.D=a;1.2=U.V("W");1.2.X=1.v;1.2.E=1.q;1.2.8.Y="Z";6(1.s){1.2.8.10="11"}1.2.A=1.w;6(!1.d){1.t()}1.h();a.12(13).14(1.2);6(1.s){g b=[\'15\',\'16\',\'17\',\'18\',\'19\',\'1a\'];1b(g i=0;i<b.1c;i++){g c=b[i];j.1d(1.2,c,j.1e(j,j.1f,1,c,1.n()))}}};4.3.F=5(a){7.3.F.e(1,f);1.G()};4.3.G=5(){g p=1.D.1g(1.n());g z=1h.1i(1.n().1j());1.2.8.1k=(p.x+1.r.1l)+"H";1.2.8.1m=(p.y+1.r.1n)+"H";1.2.8.1o=z};4.3.I=5(){j.1p(1.2);6(1.2.J){1.2.J=""}6(1.2.K){1.2.K.1q(1.2)}1.2=1r;7.3.I.e(1,f)};4.3.1s=5(){g a=k 4(1.n(),1.o);a.9=1.9;a.d=1.d;L a};4.3.M=5(){7.3.M.e(1,f);1.d=m;1.h()};4.3.t=5(){7.3.t.e(1,f);1.d=l;1.h()};4.3.1t=5(a){1.9=a;1.h()};4.3.1u=5(){L 1.9};4.3.h=5(){6(1.2){6((!1.1v())&&1.9){1.2.8.N=\'1w\'}1x{1.2.8.N=\'1y\'}}};4.3.1z=5(a){1.q=a;1.2.E=a;1.o.u=a};',62,98,'|this|div_|prototype|LabeledMarker|function|if|GMarker|style|labelVisibility_||||ownVisibility_|apply|arguments|var|applyLabelVisibility_||GEvent|new|false|true|getLatLng|opts_||labelText_|labelOffset_|clickable_|hide|labelText|labelClass_|title_||||title|draggable|initialize|map_|innerHTML|redraw|redrawLabel_|px|remove|outerHTML|parentNode|return|show|display|labelClass|LabeledMarker_markerLabel|labelOffset|GSize|clickable|GLatLng|document|createElement|div|className|position|absolute|cursor|pointer|getPane|G_MAP_MARKER_PANE|appendChild|click|dblclick|mousedown|mouseup|mouseover|mouseout|for|length|addDomListener|callback|trigger|fromLatLngToDivPixel|GOverlay|getZIndex|lat|left|width|top|height|zIndex|clearInstanceListeners|removeChild|null|copy|setLabelVisibility|getLabelVisibility|isHidden|block|else|none|setLabelText'.split('|'),0,{}));
