


// Utils.js - general utility routines
//
//
// The current version of this code is always available at:
// http://www.acme.com/javascript/
//
//
// Copyright ï¿½ 2005 by Jef Poskanzer <jef@mail.acme.com>.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// For commentary on this license please see http://www.acme.com/license.html

////////////////////////////////////////////////////////////////////////////////
//
// AJAX.

// Create an XMLHttpRequest on either standards-complying browsers or MSIE.
function CreateXMLHttpRequest() {
  var r;
  try {
    r = new XMLHttpRequest();
  } catch (e1) {
    try {
      r = new ActiveXObject('Microsoft.XMLHTTP');
    } catch (e2) {
      try {
        r = new ActiveXObject('Msxml2.XMLHTTP');
      } catch (e2) {
        r = null;
      }
    }
  }
  return r;
}

// Does an HTTP GET and calls a routine when it completes.
//
// The second and third callbacks are optional. If no failCallback is
// specified, a default one is used that pops up an alert box.
//
// All three callbacks are passed the XMLHttpRequest object. The progress
// callback is also passed a string describing how far the fetch has
// proceeded, either as a percentage or just a byte count.
//
function HttpGet(url, okCallback, failCallback, progressCallback) {
  if (failCallback == null)
    failCallback = DefaultFailCallback;
  var request = CreateXMLHttpRequest();
  request.open('GET', url, true);
  request.setRequestHeader('Referer', location.href); // really ought to be set
                                                      // automatically!
  var data = {
    okCallback :okCallback,
    failCallback :failCallback,
    progressCallback :progressCallback,
    prevProgressReport :null
  };
  request.onreadystatechange = MakeCaller(HttpGetRequestChecker, request, data);
  request.send(null);
}

function HttpGetRequestChecker(request, data) {
  if (request.readyState == 4)
    if (request.status == 200) {
      if (data.okCallback != null)
        data.okCallback(request);
    } else {
      if (data.failCallback != null)
        data.failCallback(request);
    }
  else {
    if (data.progressCallback != null) {
      var now = new Date();
      if (data.prevProgressReport == null
          || now.getTime() - data.prevProgressReport.getTime() >= 250) {
        if (request.responseText.length > 0) {
          var cl = null;
          try {
            cl = request.getResponseHeader('content-length');
          } catch (e) {
          }
          var progress;
          if (cl != null)
            progress = Math.round(request.responseText.length * 100 / cl) + '%';
          else
            progress = request.responseText.length + ' bytes';
          data.progressCallback(request, progress);
        }
        data.prevProgressReport = now;
      }
    }
  }
}

function DefaultFailCallback(request) {
  if (request == null)
    alert('XMLHttpRequest create failed!');
  else
    alert('XML fetch failed! (' + request.status + ' ' + request.statusText
        + ')');
}

// Do an HTTP GET via a proxy if necessary.
function HttpGetProxy(url, okCallback, failCallback, progressCallback) {
  if (url.substring(0, 7) == 'http://') {
    var host = url.substring(7);
    var slash = host.indexOf('/');
    if (slash != -1)
      host = host.substring(0, slash);
    if (host != window.location.host)
      url = '/resources/proxy.cgi?url=' + encodeURIComponent(url);
  }
  HttpGet(url, okCallback, failCallback, progressCallback);
}

// Rename this, and have it descend the full sub-tree and only grab
// nodes named "#text".
function GetXmlText(element) {
  var value = '';
  var child = element.firstChild;
  while (child != null) {
    if (value != '')
      value += ' ';
    value += child.nodeValue;
    child = child.nextSibling;
  }
  return value;
}

function GetXmlValue(elements) {
  var values = '';
  for ( var i = 0; i < elements.length; ++i) {
    if (elements[i] != null && elements[i].firstChild != null) {
      if (values != '')
        values += ' ';
      values += elements[i].firstChild.nodeValue;
    }
  }
  return values;
}

function FindChildNamed(element, name) {
  var child = element.firstChild;
  while (child != null) {
    if (child.nodeName == name)
      return child;
    child = child.nextSibling;
  }
  return null;
}

function FindDeepChildNamed(element, name) {
  if (element.nodeName == name)
    return element;
  var child = element.firstChild;
  while (child != null) {
    var d = FindDeepChildNamed(child, name);
    if (d != null)
      return d;
    child = child.nextSibling;
  }
  return null;
}

function CountNodes(element) {
  var count = 1;
  var child = element.firstChild;
  while (child != null) {
    count += CountNodes(child);
    child = child.nextSibling;
  }
  return count;
}

// //////////////////////////////////////////////////////////////////////////////
//
// Cookies.

var endOfTime = 'Tue, 19-Jan-2038 03:14:07 GMT';
var beginningOfTime = 'Thu, 01-Jan-1970 00:00:00 GMT';

function SaveCookie(cookieName, cookieValue) {
  document.cookie = cookieName + '=' + encodeURIComponent(cookieValue)
      + '; expires=' + endOfTime;
}

function ClearCookie(cookieName) {
  document.cookie = cookieName + '=; expires=' + beginningOfTime;
}

function GetCookie(cookieName) {
  if (document.cookie.length > 0) {
    var cookieNameEq = cookieName + '=';
    var cookies = document.cookie.split(';');
    for ( var i = 0; i < cookies.length; ++i) {
      // Skip blanks.
      while (cookies[i].charAt(0) == ' ')
        cookies[i] = cookies[i].substr(1);
      // Is this our cookie?
      if (cookies[i].indexOf(cookieNameEq) == 0)
        return decodeURIComponent(cookies[i].substr(cookieNameEq.length));
    }
  }
  return null;
}

// //////////////////////////////////////////////////////////////////////////////
//
// Charsets.

function EntityToIso8859(inStr) {
  var outStr = '';
  for ( var i = 0; i < inStr.length; ++i) {
    var c = inStr.charAt(i);
    if (c != '&')
      outStr += c;
    else {
      var semi = inStr.indexOf(';', i);
      if (semi == -1)
        outStr += c;
      else {
        var entity = inStr.substring(i + 1, semi);
        if (entity == 'iexcl')
          outStr += '\xa1';
        else if (entity == 'copy')
          outStr += '\xa9';
        else if (entity == 'laquo')
          outStr += '\xab';
        else if (entity == 'reg')
          outStr += '\xae';
        else if (entity == 'deg')
          outStr += '\xb0';
        else if (entity == 'raquo')
          outStr += '\xbb';
        else if (entity == 'iquest')
          outStr += '\xbf';
        else if (entity == 'Agrave')
          outStr += '\xc0';
        else if (entity == 'Aacute')
          outStr += '\xc1';
        else if (entity == 'Acirc')
          outStr += '\xc2';
        else if (entity == 'Atilde')
          outStr += '\xc3';
        else if (entity == 'Auml')
          outStr += '\xc4';
        else if (entity == 'Aring')
          outStr += '\xc5';
        else if (entity == 'AElig')
          outStr += '\xc6';
        else if (entity == 'Ccedil')
          outStr += '\xc7';
        else if (entity == 'Egrave')
          outStr += '\xc8';
        else if (entity == 'Eacute')
          outStr += '\xc9';
        else if (entity == 'Ecirc')
          outStr += '\xca';
        else if (entity == 'Euml')
          outStr += '\xcb';
        else if (entity == 'Igrave')
          outStr += '\xcc';
        else if (entity == 'Iacute')
          outStr += '\xcd';
        else if (entity == 'Icirc')
          outStr += '\xce';
        else if (entity == 'Iuml')
          outStr += '\xcf';
        else if (entity == 'Ntilde')
          outStr += '\xd1';
        else if (entity == 'Ograve')
          outStr += '\xd2';
        else if (entity == 'Oacute')
          outStr += '\xd3';
        else if (entity == 'Ocirc')
          outStr += '\xd4';
        else if (entity == 'Otilde')
          outStr += '\xd5';
        else if (entity == 'Ouml')
          outStr += '\xd6';
        else if (entity == 'Oslash')
          outStr += '\xd8';
        else if (entity == 'Ugrave')
          outStr += '\xd9';
        else if (entity == 'Uacute')
          outStr += '\xda';
        else if (entity == 'Ucirc')
          outStr += '\xdb';
        else if (entity == 'Uuml')
          outStr += '\xdc';
        else if (entity == 'Yacute')
          outStr += '\xdd';
        else if (entity == 'szlig')
          outStr += '\xdf';
        else if (entity == 'agrave')
          outStr += '\xe0';
        else if (entity == 'aacute')
          outStr += '\xe1';
        else if (entity == 'acirc')
          outStr += '\xe2';
        else if (entity == 'atilde')
          outStr += '\xe3';
        else if (entity == 'auml')
          outStr += '\xe4';
        else if (entity == 'aring')
          outStr += '\xe5';
        else if (entity == 'aelig')
          outStr += '\xe6';
        else if (entity == 'ccedil')
          outStr += '\xe7';
        else if (entity == 'egrave')
          outStr += '\xe8';
        else if (entity == 'eacute')
          outStr += '\xe9';
        else if (entity == 'ecirc')
          outStr += '\xea';
        else if (entity == 'euml')
          outStr += '\xeb';
        else if (entity == 'igrave')
          outStr += '\xec';
        else if (entity == 'iacute')
          outStr += '\xed';
        else if (entity == 'icirc')
          outStr += '\xee';
        else if (entity == 'iuml')
          outStr += '\xef';
        else if (entity == 'ntilde')
          outStr += '\xf1';
        else if (entity == 'ograve')
          outStr += '\xf2';
        else if (entity == 'oacute')
          outStr += '\xf3';
        else if (entity == 'ocirc')
          outStr += '\xf4';
        else if (entity == 'otilde')
          outStr += '\xf5';
        else if (entity == 'ouml')
          outStr += '\xf6';
        else if (entity == 'oslash')
          outStr += '\xf8';
        else if (entity == 'ugrave')
          outStr += '\xf9';
        else if (entity == 'uacute')
          outStr += '\xfa';
        else if (entity == 'ucirc')
          outStr += '\xfb';
        else if (entity == 'uuml')
          outStr += '\xfc';
        else if (entity == 'yacute')
          outStr += '\xfd';
        else if (entity == 'yuml')
          outStr += '\xff';
        else if (entity == 'nbsp')
          outStr += ' ';
        else if (entity == 'lt')
          outStr += '<';
        else if (entity == 'gt')
          outStr += '>';
        else if (entity == 'amp')
          outStr += '&';
        else
          outStr += '&' + entity + ';';
        i += entity.length + 1;
      }
    }
  }
  return outStr;
}

function DeEntityize(inStr) {
  var outStr = '';
  for ( var i = 0; i < inStr.length; ++i) {
    var c = inStr.charAt(i);
    if (c != '&')
      outStr += c;
    else {
      var semi = inStr.indexOf(';', i);
      if (semi != -1)
        i = semi;
    }
  }
  return outStr;
}

function DeElementize(inStr) {
  var outStr = '';
  for ( var i = 0; i < inStr.length; ++i) {
    var c = inStr.charAt(i);
    if (c != '<')
      outStr += c;
    else {
      var gt = inStr.indexOf('>', i);
      if (gt != -1)
        i = gt;
    }
  }
  return outStr;
}

function DeHtmlize(str) {
  return DeEntityize(EntityToIso8859(DeElementize(str)));
}

function QuoteHtml(inStr) {
  // This should really be a state machine.
  var outStr = '';
  for ( var i = 0; i < inStr.length; ++i) {
    var c = inStr.charAt(i);
    switch (c) {
    case '<':
      outStr += '&lt;';
      break;
    case '>':
      outStr += '&gt;';
      break;
    case '&':
      outStr += '&amp;';
      break;
    default:
      outStr += c;
      break;
    }
  }
  return outStr;
}

// //////////////////////////////////////////////////////////////////////////////
//
// Miscellaneous.

// This returns a function closure that calls the given routine with the
// specified args.
function MakeCaller(func, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9,
    arg10) {
  return function() {
    func(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
  };
}

// And here's a version for use with events.
function MakeEventCaller(func, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8,
    arg9, arg10) {
  return function(e) {
    func(e, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10);
  };
}

function GetEvent(e) {
  if (e)
    return e;
  if (event)
    return event;
  if (window.event)
    return window.event;
  return null;
}

// Parse this document's QUERY_STRING and return an associative array.
function GetParameters() {
  var query_string = location.search.substring(1, location.search.length);
  var namevals = query_string.split('&');
  var params = [];
  for ( var i = 0; i < namevals.length; ++i) {
    var nameval = namevals[i].split('=');
    if (nameval.length == 2)
      params[nameval[0]] = decodeURIComponent(nameval[1].replace(/\+/g, ' '));
    // (For some reason decodeURIComponent doesn't handle plus signs!)
  }
  return params;
}

function Substitute(str, from, to) {
  var fromLen = from.length;
  var newStr = '';
  while (str.length > 0) {
    if (str.substr(0, fromLen) == from) {
      newStr += to;
      str = str.substr(fromLen);
    } else {
      newStr += str.charAt(0);
      str = str.substr(1);
    }
  }
  return newStr;
}

function Props(o) {
  var s = '';
  for (p in o) {
    if (s.length != 0)
      s += '\n';
    s += p + ': ' + o[p];
  }
  return s;
}

// These don't actually work right.

function GetBrowserWidth() {
  var width = null;
  try {
    width = innerWidth;
  } catch (e1) {
    try {
      width = document.documentElement.offsetWidth;
    } catch (e2) {
      try {
        width = document.documentElement.clientWidth;
      } catch (e2) {
        try {
          width = document.body.clientWidth;
        } catch (e2) {
        }
      }
    }
  }
  return width;
}

function GetBrowserHeight() {
  var height = null;
  try {
    height = innerHeight;
  } catch (e1) {
    try {
      height = document.documentElement.offsetHeight;
    } catch (e2) {
      try {
        height = document.documentElement.clientHeight;
      } catch (e2) {
        try {
          height = document.body.clientHeight;
        } catch (e2) {
        }
      }
    }
  }
  return height;
}

// The standard idiom for creating new DOM elements is like so:
// var e = document.createElement( 'type' );
// e.prop1 = val1;
// e.prop2 = val2;
// parent.appendChild( e );
// With this routine you can replace that with a one-liner:
// var e = AppendElement( parent, 'type', { prop1: val1, prop2: val2 } );
// That stuff in the curly braces is a JavaScript anonymous object literal.
//
// There are two exceptional cases to be aware of.
// 1) If you need to set properties on the style sub-object, you can't
// just say { style.prop: val } because that would cause a syntax error.
// Instead, make a nested object of your own, like so:
// { style: { prop: val } }.
// 2) You will also get a syntax error if you try to set the class
// property, because class is a JavaScript reserved word. Use
// className instead.

function AppendElement(parent, elementType, properties) {
  var element = document.createElement(elementType);
  if (properties != null)
    for (property in properties)
      if (property == 'style')
        for (nestedProperty in properties[property])
          element.style[nestedProperty] = properties['style'][nestedProperty];
      else
        element[property] = properties[property];
  parent.appendChild(element);
  return element;
}

function AppendText(parent, text) {
  var element = document.createTextNode(text);
  parent.appendChild(element);
  return element;
}

function ClearElement(element) {
  while (element.firstChild != null) {
    ClearElement(element.firstChild);
    element.removeChild(element.firstChild);
  }
}

function DestroyElement(element) {
  ClearElement(element);
  element.parentNode.removeChild(element);
}

function instanceOf(object, clas) {
  while (object != null) {
    if (object == clas.prototype)
      return true;
    object = object.__proto__;
  }
  return false;
}

function GetComputedStyle(element, style) {
  var camelStyle = Camelize(style);
  if (element.currentStyle != null)
    return element.currentStyle[camelStyle]; // MSIE
  if (window.getComputedStyle != null)
    return window.getComputedStyle(element, null)[camelStyle];
  return null;
}

var camelizeTable = [];
camelizeTable['background-attachment'] = 'backgroundAttachment';
camelizeTable['background-color'] = 'backgroundColor';
camelizeTable['background-image'] = 'backgroundImage';
camelizeTable['background-position'] = 'backgroundPosition';
camelizeTable['background-repeat'] = 'backgroundRepeat';
camelizeTable['border-bottom'] = 'borderBottom';
camelizeTable['border-bottom-color'] = 'borderBottomColor';
camelizeTable['border-bottom-style'] = 'borderBottomStyle';
camelizeTable['border-bottom-width'] = 'borderBottomWidth';
camelizeTable['border-color'] = 'borderColor';
camelizeTable['border-left'] = 'borderLeft';
camelizeTable['border-left-color'] = 'borderLeftColor';
camelizeTable['border-left-style'] = 'borderLeftStyle';
camelizeTable['border-left-width'] = 'borderLeftWidth';
camelizeTable['border-right'] = 'borderRight';
camelizeTable['border-right-color'] = 'borderRightColor';
camelizeTable['border-right-style'] = 'borderRightStyle';
camelizeTable['border-right-width'] = 'borderRightWidth';
camelizeTable['border-style'] = 'borderStyle';
camelizeTable['border-top'] = 'borderTop';
camelizeTable['border-top-color'] = 'borderTopColor';
camelizeTable['border-top-style'] = 'borderTopStyle';
camelizeTable['border-top-width'] = 'borderTopWidth';
camelizeTable['border-width'] = 'borderWidth';
camelizeTable['font-family'] = 'fontFamily';
camelizeTable['font-size'] = 'fontSize';
camelizeTable['font-variant'] = 'fontVariant';
camelizeTable['font-weight'] = 'fontWeight';
camelizeTable['letter-spacing'] = 'letterSpacing';
camelizeTable['line-height'] = 'lineHeight';
camelizeTable['list-style'] = 'listStyle';
camelizeTable['list-style-image'] = 'listStyleImage';
camelizeTable['list-style-position'] = 'listStylePosition';
camelizeTable['list-style-type'] = 'listStyleType';
camelizeTable['margin-bottom'] = 'marginBottom';
camelizeTable['margin-left'] = 'marginLeft';
camelizeTable['margin-right'] = 'marginRight';
camelizeTable['margin-top'] = 'marginTop';
camelizeTable['padding-bottom'] = 'paddingBottom';
camelizeTable['padding-left'] = 'paddingLeft';
camelizeTable['padding-right'] = 'paddingRight';
camelizeTable['padding-top'] = 'paddingTop';
camelizeTable['page-break-after'] = 'pageBreakAfter';
camelizeTable['page-break-before'] = 'pageBreakBefore';
camelizeTable['float'] = 'cssFloat';
camelizeTable['text-align'] = 'textAlign';
camelizeTable['text-decoration'] = 'textDecoration';
camelizeTable['text-indent'] = 'textIndent';
camelizeTable['text-transform'] = 'textTransform';
camelizeTable['vertical-align'] = 'verticalAlign';
camelizeTable['z-index'] = 'zIndex';

function Camelize(styleName) {
  var camelStyleName = camelizeTable[styleName];
  if (camelStyleName != null)
    return camelStyleName;
  else
    return styleName;
}

// Sorts the rows of an HTML table!
//
// First parameter is the table element to sort, second parameter
// is which column to sort on (first column == 1 etc.).
//
// The third parameter has multiple meanings.
// - If it's missing, or false, then the sort is in descending order.
// - If it's true, then the sort is in ascending order.
// - Otherwise it should be a function that takes two parameters
// and returns a boolean.

var oldCursor;
var startTime, endTime, elapsed;

function SortTable(table, columnNumber, compare) {
  startTime = new Date(); // !!!

  // Change cursor.
  oldCursor = table.style.cursor;
  table.style.cursor = 'wait';
  setTimeout(MakeCaller(SortTableLater, table, columnNumber, compare), 100);
}

function SortTableLater(table, columnNumber, compare) {
  // Find the container to sort in. Is there a tbody?
  var container = table;
  var child = table.firstChild;
  while (child != null) {
    if (child.nodeName == 'TBODY') {
      container = child;
      break;
    }
    child = child.nextSibling;
  }

  // Figure out what comparison function to use.
  if (compare == null || compare == false)
    compare = function(v1, v2) {
      return v1 > v2;
    };
  else if (compare == true)
    compare = function(v1, v2) {
      return v1 < v2;
    };
  // (Else use the object passed in as the comparison function.)

  // Collect up the rows and columns into an array.
  var rows = [];
  var row = container.firstChild;
  while (row != null) {
    if (row.nodeName == 'TR') {
      var columns = [];
      var column = row.firstChild;
      while (column != null) {
        if (column.nodeName == 'TD')
          columns.push(column);
        column = column.nextSibling;
      }
      if (columns.length > 0)
        rows.push(columns);
    }
    row = row.nextSibling;
  }

  // Sort. This is just a dumb O(n^2) algorithm, but we do try to
  // minimize the actual swaps.
  for ( var i = 0; i < rows.length; ++i) {
    var m = i;
    for ( var j = i + 1; j < rows.length; ++j) {
      var jstr = rows[j][columnNumber - 1].value;
      if (jstr == undefined)
        jstr = rows[j][columnNumber - 1].innerHTML;
      var jnum = parseFloat(jstr);

      var mstr = rows[m][columnNumber - 1].value;
      if (mstr == undefined)
        mstr = rows[m][columnNumber - 1].innerHTML;
      mnum = parseFloat(mstr);

      if (isNaN(jnum) || isNaN(mnum)) {
        if (compare(jstr, mstr))
          m = j;
      } else {
        if (compare(jnum, mnum))
          m = j;
      }
    }

    if (m != i) {
      // Swap row contents.
      var t;
      for ( var k = 0; k < rows[i].length; ++k) {
        t = rows[i][k].innerHTML;
        rows[i][k].innerHTML = rows[m][k].innerHTML;
        rows[m][k].innerHTML = t;
        t = rows[i][k].value;
        rows[i][k].value = rows[m][k].value;
        rows[m][k].value = t;
        t = rows[i][k].className;
        rows[i][k].className = rows[m][k].className;
        rows[m][k].className = t;
        t = rows[i][k].onclick;
        rows[i][k].onclick = rows[m][k].onclick;
        rows[m][k].onclick = t;
      }
      t = rows[i].className;
      rows[i].className = rows[m].className;
      rows[m].className = t;
    }
  }

  // Restore cursor.
  table.style.cursor = oldCursor;

  endTime = new Date(); // !!!
  elapsed = endTime.getTime() - startTime.getTime(); // !!!
  // alert( elapsed ); // !!!
}

function ArrayEvery(boolfunc, list) {
  for ( var i in list)
    if (!boolfunc(list[i]))
      return false;
  return true;
}

function ArraySome(boolfunc, list) {
  for ( var i in list)
    if (boolfunc(list[i]))
      return true;
  return false;
}

function ArrayFilter(boolfunc, list) {
  var results = [];
  for ( var i in list)
    if (boolfunc(list[i]))
      results.push(list[i]);
  return results;
}

function ArrayForEach(func, list) {
  for ( var i in list)
    func(list[i]);
}

function ArrayMap(func, list) {
  var results = [];
  for ( var i in list)
    results[i] = func(list[i]);
  return results;
}

function ArrayReduce(func, list, initialValue) {
  var i;
  var result;
  if (initialValue != null) {
    result = initialValue;
    i = 0;
  } else {
    result = list[0];
    i = 1;
  }
  for (; i < list.length; ++i)
    result = func(result, list[i]);
  return result;
}

function Range(from, to, inc) {
  if (inc == null)
    inc = 1;
  if (to == null) {
    to = from;
    from = 0;
  }
  inc = Math.abs(inc);
  var results = [];
  if (from <= to)
    for ( var i = from; i < to; i += inc)
      results.push(i);
  else
    for ( var i = from; i > to; i -= inc)
      results.push(i);
  return results;
}

function ArraySearch(a, v) {
  for ( var i in a)
    if (a[i] == v)
      return i;
  return null;
}

function MergeObjects(o1, o2) {
  var o = {};
  if (o1 != null)
    for ( var n in o1)
      o[n] = o1[n];
  if (o2 != null)
    for ( var n in o2)
      o[n] = o2[n];
  return o;
}

var currentPopup = null;

function PopUpInternal(anchorElement, styles) {
  // Make sure the anchorElement is a container. If not, use the parent.
  var nonContainers = [ 'AREA', 'BASE', 'BR', 'COL', 'HR', 'IMG', 'INPUT',
      'LINK', 'META', 'PARAM', 'TITLE' ];
  if (ArraySearch(nonContainers, anchorElement.nodeName) != null)
    anchorElement = anchorElement.parentNode;

  PopDown();
  anchorElement.style.position = 'relative';
  var outerDiv = AppendElement(anchorElement, 'div', {
    onclick :NoBubble,
    style : {
      position :'relative'
    }
  });
  var innerStyles = MergeObjects( {
    position :'absolute',
    right :'25px',
    top :'3px',
    zIndex :'1000',
    backgroundColor :'#ffffff'
  }, styles);
  var innerDiv = AppendElement(outerDiv, 'div', {
    style :innerStyles
  });
  currentPopup = outerDiv;
  return innerDiv;
}

function PopUpAddClose(div) {
  AppendElement(div, 'img', {
    src :'http://www.acme.com/resources/images/close.gif',
    onclick :PopDown,
    style : {
      width :'14px',
      height :'13px',
      border :'0px solid #ffffff',
      position :'absolute',
      right :'2px',
      top :'2px'
    }
  });
}

function PopUp(anchorElement, styles) {
  var p = PopUpInternal(anchorElement, styles);
  PopUpAddClose(p);
  return p;
}

function PopUpHtml(anchorElement, html, styles) {
  var p = PopUpInternal(anchorElement, styles);
  p.innerHTML = html;
  PopUpAddClose(p);
  return p;
}

function PopDown() {
  if (!currentPopup)
    return;
  DestroyElement(currentPopup);
  currentPopup = null;
}

function NoBubble(e) {
  e = GetEvent(e);
  if (e)
    e.cancelBubble = true;
}

function PopUpColorPicker(anchorElement, styles, callback) {
  var hexits = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
      'c', 'd', 'e', 'f' ];
  var p = PopUpInternal(anchorElement, MergeObjects( {
    width :'144px',
    height :'96px'
  }, styles));
  var table = AppendElement(p, 'table', {
    border :'0',
    cellSpacing :'0',
    cellPadding :'0'
  });
  var tbody = AppendElement(table, 'tbody');
  r = g = b = 0;
  for ( var y = 0; y < 12; ++y) {
    var tr = AppendElement(tbody, 'tr');
    for ( var x = 0; x < 18; ++x) {
      var c = hexits[r >> 4] + hexits[r & 15] + hexits[g >> 4] + hexits[g & 15]
          + hexits[b >> 4] + hexits[b & 15];
      var td = AppendElement(tr, 'td', {
        title :c,
        style : {
          width :'8px',
          height :'8px',
          backgroundColor :'#' + c
        }
      });
      td.onclick = MakePopDownCallback(callback, c);
      b += 51;
      if (b >= 256) {
        b = 0;
        g += 51;
        if (g >= 256) {
          g = 0;
          r += 51;
        }
      }
    }
  }
}

function MakePopDownCallback(callback, arg) {
  return function() {
    PopDown();
    callback(arg);
  };
}

function Join(strArray, separator) {
  var results = '';
  for (i in strArray) {
    if (results != '')
      results += separator;
    results += strArray[i];
  }
  return results;
}

// MapUtils.js - utility routines for Google Maps apps
//
//
// The current version of this code is always available at:
// http://www.acme.com/javascript/
//
//
// Copyright ï¿½ 2005,2006 by Jef Poskanzer <jef@mail.acme.com>.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// For commentary on this license please see http://www.acme.com/license.html

////////////////////////////////////////////////////////////////////////////////
//
// Internationalization.

var LANG_UNKNOWN = 0;
var LANG_ENGLISH = 1;
var LANG_FRENCH = 2;
var currentLanguage = LANG_UNKNOWN;

var _mInstructions;

function SetLanguage(language) {
  if (language != currentLanguage) {
    switch (language) {
    case LANG_ENGLISH:
      // HTML strings.
      _mInstructions = 'Drag the map with your mouse, or double-click to center.';
      _mSiteName = 'Google Maps';
      _mDataCopy = 'Map data &#169;2005 ';
      _mZenrinCopy = 'Map &#169;2005 ';
      _mNormalMap = 'Map';
      _mNormalMapShort = 'Map';
      _mHybridMap = 'Hybrid';
      _mHybridMapShort = 'Hyb';
      _mKeyholeMap = 'Satellite';
      _mKeyholeMapShort = 'Sat';
      _mNew = 'New!';
      _mTerms = 'Terms of Use';
      _mKeyholeCopy = 'Imagery &#169;2005 ';
      _mDecimalPoint = '.';
      _mThousandsSeparator = ',';
      _mZoomIn = 'Zoom In';
      _mZoomOut = 'Zoom Out';
      _mZoomSet = 'Click to set zoom level';
      _mZoomDrag = 'Drag to zoom';
      _mPanWest = 'Go left';
      _mPanEast = 'Go right';
      _mPanNorth = 'Go up';
      _mPanSouth = 'Go down';
      _mLastResult = 'Return to the last result';
      _mScale = 'Scale at the center of the map';
      break;

    case LANG_FRENCH:
      // HTML strings.
      _mInstructions = 'Faites glisser la carte avec la souris ou double-cliquez sur un point pour la recentrer.';
      _mSiteName = 'Cartes Google';
      _mDataCopy = 'Donn&eacute;es cartographiques &#169;2005 ';
      _mZenrinCopy = 'Carte &#169;2005 ';
      _mNormalMap = 'Carte';
      _mNormalMapShort = 'Car';
      _mHybridMap = 'Mixte';
      _mHybridMapShort = 'Mix';
      _mKeyholeMap = 'Satellite';
      _mKeyholeMapShort = 'Sat';
      _mNew = 'Nouvelle!';
      _mTerms = 'Limites d\'utilisation';
      _mKeyholeCopy = 'Images &#169;2005 ';
      _mDecimalPoint = ',';
      _mThousandsSeparator = '.';
      _mZoomIn = 'Zoom avant';
      _mZoomOut = 'Zoom arri&egrave;re';
      _mZoomSet = 'Cliquez pour d&eacute;finir le facteur de zoom';
      _mZoomDrag = 'Faites glisser le curseur pour zoomer';
      _mPanWest = 'D&eacute;placer vers la gauche';
      _mPanEast = 'D&eacute;placer vers la droite';
      _mPanNorth = 'D&eacute;placer vers le haut';
      _mPanSouth = 'D&eacute;placer vers le bas';
      _mLastResult = 'Revenir au r&eacute;sultat pr&eacute;c&eacute;dent';
      _mScale = '&Eacute;chelle au centre de la carte';
      break;
    }

    // HTML entities don't work in tool-tips, so we convert all the
    // tool-tip strings to use ISO-8859-1 characters directly.
    _mZoomIn = EntityToIso8859(_mZoomIn);
    _mZoomOut = EntityToIso8859(_mZoomOut);
    _mZoomSet = EntityToIso8859(_mZoomSet);
    _mZoomDrag = EntityToIso8859(_mZoomDrag);
    _mPanWest = EntityToIso8859(_mPanWest);
    _mPanEast = EntityToIso8859(_mPanEast);
    _mPanNorth = EntityToIso8859(_mPanNorth);
    _mPanSouth = EntityToIso8859(_mPanSouth);
    _mLastResult = EntityToIso8859(_mLastResult);
    _mScale = EntityToIso8859(_mScale);

    currentLanguage = language;
  }
}

// //////////////////////////////////////////////////////////////////////////////
//
// Position/zoom/type cookie.

var pztCookieName = 'positionZoomType2';
var oldPztCookieName = 'positionZoomType';

function SavePositionZoomTypeCookie(map) {
  var mapCenter = map.getCenter();
  var mapZoom = map.getZoom();
  var mapTypeLetter = MapTypeToLetter(map.getCurrentMapType());
  var cookieValue = mapCenter.lat().toFixed(5) + ','
      + mapCenter.lng().toFixed(5) + ',' + mapZoom + ',' + mapTypeLetter;
  SaveCookie(pztCookieName, cookieValue);
  ClearCookie(oldPztCookieName);
}

function GetPositionZoomTypeCookie(map) {
  var cookieValue = GetCookie(pztCookieName);
  if (cookieValue == null)
    return false;
  var vals = cookieValue.split(',');
  if (vals.length != 4)
    return false;
  var mapY = parseFloat(vals[0]);
  var mapX = parseFloat(vals[1]);
  var mapZoomStr = vals[2];
  var mapTypeLetter = vals[3];
  var mapZoom = parseInt(mapZoomStr);
  map.setCenter(new GLatLng(mapY, mapX), mapZoom);
  map.setMapType(LetterToMapType(mapTypeLetter));
  return true;
}

function SavePositionZoomTypeCookieOnChanges(map) {
  var caller = MakeCaller(SavePositionZoomTypeCookie, map);
  // GEvent.addListener( map, 'moveend', caller );
  GEvent.addListener(map, 'move', MakeCaller(SpztcChecker, caller));
  GEvent.addListener(map, 'zoomend', caller);
  GEvent.addListener(map, 'maptypechanged ', caller);
}

var spztcMoveEndTimer = null;
var spztcMoveEndCheckMsecs = 700;
var spztcMoveCount, spztcPrevMoveCount;

function SpztcChecker(caller) {
  if (spztcMoveEndTimer == null) {
    spztcMoveEndTimer = setTimeout(MakeCaller(SpztcTimeChecker, caller),
        spztcMoveEndCheckMsecs);
    spztcPrevMoveCount = spztcMoveCount = 0;
  }
  ++spztcMoveCount;
}

function SpztcTimeChecker(caller) {
  if (spztcMoveCount == spztcPrevMoveCount) {
    spztcMoveEndTimer = null;
    caller();
  } else {
    spztcMoveEndTimer = setTimeout(MakeCaller(SpztcTimeChecker, caller),
        spztcMoveEndCheckMsecs);
    spztcPrevMoveCount = spztcMoveCount;
  }
}

function MapTypeToLetter(mapType) {
  switch (mapType) {
  case G_NORMAL_MAP:
    return 'M';
  case G_SATELLITE_MAP:
    return 'S';
  case G_HYBRID_MAP:
    return 'H';
    // case G_PHYSICAL_MAP : return 'R';
  case WMS_TOPO_MAP:
    return 'T';
  case WMS_DOQ_MAP:
    return 'O';
  case WMS_NEXRAD_MAP:
    return 'N';
  default:
    return '-';
  }
}

function LetterToMapType(letter) {
  switch (letter) {
  case 'M':
    return G_NORMAL_MAP;
  case 'S':
    return G_SATELLITE_MAP;
  case 'H':
    return G_HYBRID_MAP;
    // case 'R': return G_PHYSICAL_MAP;
  case 'T':
    return WMS_TOPO_MAP;
  case 'O':
    return WMS_DOQ_MAP;
  case 'N':
    return WMS_NEXRAD_MAP;
  default:
    return G_NORMAL_MAP;
  }
}

// //////////////////////////////////////////////////////////////////////////////
//
// Geometry.

var degreesPerRadian = 180.0 / Math.PI;
var radiansPerDegree = Math.PI / 180.0;

// Returns the bearing in degrees between two points.
// North = 0, East = 90, South = 180, West = 270.
function Bearing(from, to) {
  // See T. Vincenty, Survey Review, 23, No 176, p 88-93,1975.

  // Convert to radians.
  var lat1 = from.lat() * radiansPerDegree;
  var lon1 = from.lng() * radiansPerDegree;
  var lat2 = to.lat() * radiansPerDegree;
  var lon2 = to.lng() * radiansPerDegree;

  // Compute the angle.
  var angle = -Math.atan2(Math.sin(lon1 - lon2) * Math.cos(lat2), Math
      .cos(lat1)
      * Math.sin(lat2)
      - Math.sin(lat1)
      * Math.cos(lat2)
      * Math.cos(lon1 - lon2));
  if (angle < 0.0)
    angle += Math.PI * 2.0;

  // And convert result to degrees.
  angle = angle * degreesPerRadian;

  return angle;
}

// Returns the bearing in degrees between two points.
// North = 0, East = 90, South = 180, West = 270.
// This version is ill-conditioned.
function BadBearing(from, to) {
  // Formula from the 1982 ARRL antenna book.
  var a = from.lat();
  var b = to.lat();
  var l = to.lng() - from.lng();

  // Handle some degenerate cases.
  var episilon = 0.0000000001;
  if (Math.abs(l) <= episilon)
    if (a > b)
      return 180.0;
    else
      return 0.0;
  else if (Math.abs(Math.abs(l) - 180.0) <= episilon)
    if (a >= 0.0 && b >= 0.0)
      return 0.0;
    else if (a < 0.0 && b < 0.0)
      return 180.0;
    else if (a >= 0.0)
      if (a > -b)
        return 0.0;
      else
        return 180.0;
    else if (a > -b)
      return 180.0;
    else
      return 0.0;

  // Convert to radians.
  a *= radiansPerDegree;
  b *= radiansPerDegree;
  l *= radiansPerDegree;

  var d = Math.acos(Math.sin(a) * Math.sin(b) + Math.cos(a) * Math.cos(b)
      * Math.cos(l));
  var angle = Math.acos((Math.sin(b) - Math.sin(a) * Math.cos(d))
      / (Math.cos(a) * Math.sin(d)));
  angle = angle * degreesPerRadian;
  if (Math.sin(l) < 0)
    angle = 360.0 - angle;
  return angle;
}

function Direction(bearing) {
  if (bearing >= 348.75 || bearing < 11.25)
    return "N";
  if (bearing >= 11.25 && bearing < 33.75)
    return "NxNE";
  if (bearing >= 33.75 && bearing < 56.25)
    return "NE";
  if (bearing >= 56.25 && bearing < 78.75)
    return "ExNE";
  if (bearing >= 78.75 && bearing < 101.25)
    return "E";
  if (bearing >= 101.25 && bearing < 123.75)
    return "ExSE";
  if (bearing >= 123.75 && bearing < 146.25)
    return "SE";
  if (bearing >= 146.25 && bearing < 168.75)
    return "SxSE";
  if (bearing >= 168.75 && bearing < 191.25)
    return "S";
  if (bearing >= 191.25 && bearing < 213.75)
    return "SxSW";
  if (bearing >= 213.75 && bearing < 236.25)
    return "SW";
  if (bearing >= 236.25 && bearing < 258.75)
    return "WxSW";
  if (bearing >= 258.75 && bearing < 281.25)
    return "W";
  if (bearing >= 281.25 && bearing < 303.75)
    return "WxNW";
  if (bearing >= 303.75 && bearing < 326.25)
    return "NW";
  if (bearing >= 326.25 && bearing < 348.75)
    return "NxNW";
  return "???"
}

function Direction8(bearing) {
  if (bearing >= 337.5 || bearing < 22.5)
    return "N";
  if (bearing >= 22.5 && bearing < 67.5)
    return "NE";
  if (bearing >= 67.5 && bearing < 112.5)
    return "E";
  if (bearing >= 112.5 && bearing < 157.5)
    return "SE";
  if (bearing >= 157.5 && bearing < 202.5)
    return "S";
  if (bearing >= 202.5 && bearing < 247.5)
    return "SW";
  if (bearing >= 247.5 && bearing < 292.5)
    return "W";
  if (bearing >= 292.5 && bearing < 337.5)
    return "NW";
  return "???"
}

// //////////////////////////////////////////////////////////////////////////////
//
// Click to zoom.

var clickZoomMap = null;
var clickZoomListener;
var clickZoomClicked;
var clickZoomDoubleClicked;

function ClickZoom(map) {
  if (map == clickZoomMap)
    return;
  if (clickZoomMap != null)
    ClickZoomOff();
  clickZoomMap = map;
  clickZoomListener = GEvent.addListener(clickZoomMap, 'click',
      ClickZoomClickHandler);
  clickZoomClicked = false;
  clickZoomDoubleClicked = false;
}

function ClickZoomOff() {
  if (clickZoomMap != null) {
    GEvent.removeListener(clickZoomListener);
    clickZoomListener = null;
    clickZoomMap = null;
  }
}

function ClickZoomClickHandler(overlay, point) {
  if (overlay == null && point != null) {
    // We want to avoid triggering on double-clicks.
    if (clickZoomClicked)
      // We were already doing a click; set the double-click flag.
      clickZoomDoubleClicked = true;
    else {
      // A fresh click; set the click flag and then wait 1/4 second.
      clickZoomClicked = true;
      clickZoomDoubleClicked = false;
      setTimeout(MakeCaller(ClickZoomLaterHandler, point), 250);
    }
  }
}

function ClickZoomLaterHandler(point) {
  if (!clickZoomDoubleClicked)
    clickZoomMap.setCenter(point, clickZoomMap.getZoom() + 1);
  // And reset the flag for the next click.
  clickZoomClicked = false;
}

// //////////////////////////////////////////////////////////////////////////////
//
// Mouse wheel zooming.

var mouseWheelZoomMap = null;

function MouseWheelZoom(map) {
  if (map == mouseWheelZoomMap)
    return;
  if (mouseWheelZoomMap != null)
    MouseWheelZoomOff();
  mouseWheelZoomMap = map;
  var container = mouseWheelZoomMap.getContainer();
  if (container.addEventListener)
    container.addEventListener('DOMMouseScroll', MouseWheelZoomHandler, false);
  else
    container.onmousewheel = window.onmousewheel = document.onmousewheel = MouseWheelZoomHandler;
}

function MouseWheelZoomOff() {
  if (mouseWheelZoomMap != null) {
    var container = mouseWheelZoomMap.getContainer();
    if (container.removeEventListener)
      container.removeEventListener('DOMMouseScroll', MouseWheelZoomHandler,
          false);
    else
      container.onmousewheel = window.onmousewheel = document.onmousewheel = null;
    mouseWheelZoomMap = null;
  }
}

function MouseWheelZoomHandler(e) {
  e = GetEvent(e);

  if (e != null) {
    var data = 0;

    if (e.wheelData != null) // MSIE, Opera
    {
      data = e.wheelData
      if (window.opera)
        data = -data;
    } else if (e.detail != null) // Mozilla, Firefox
      data = -e.detail;

    if (data > 0)
      mouseWheelZoomMap.setZoom(mouseWheelZoomMap.getZoom() + 1);
    else if (data < 0)
      mouseWheelZoomMap.setZoom(mouseWheelZoomMap.getZoom() - 1);
  }
}

// //////////////////////////////////////////////////////////////////////////////
//
// Miscellaneous.

function GetPointFromIP() {
  var request = CreateXMLHttpRequest();
  if (request == null)
    return null;
  request.open('GET', '/resources/hostip_proxy.cgi', false);
  request.send(null);
  if (request.readyState != 4)
    return null;
  if (request.status != 200)
    return null;
  if (request.responseXML == null)
    return null;
  // !!! This doesn't work in MSIE.
  if (request.responseXML.documentElement == null)
    return null;
  var coordElement = FindDeepChildNamed(request.responseXML.documentElement,
      'gml:coordinates');
  if (coordElement == null)
    return null;
  var coordText = GetXmlText(coordElement);
  var coords = coordText.split(',');
  if (coords.length != 2)
    return null;
  var lng = parseFloat(coords[0]);
  var lat = parseFloat(coords[1]);
  return new GLatLng(lat, lng);
}

function ZoomToMarkers(map, markers) {
  if (markers.length == 0)
    return;

  var minLng = 9999.0, maxLng = -9999.0, minLat = 9999.0, maxLat = -9999.0;
  for ( var i in markers) {
    if (markers[i].getPoint().lng() < minLng)
      minLng = markers[i].getPoint().lng();
    if (markers[i].getPoint().lng() > maxLng)
      maxLng = markers[i].getPoint().lng();
    if (markers[i].getPoint().lat() < minLat)
      minLat = markers[i].getPoint().lat();
    if (markers[i].getPoint().lat() > maxLat)
      maxLat = markers[i].getPoint().lat();
  }

  var center = new GLatLng((minLat + maxLat) / 2.0, (minLng + maxLng) / 2.0);
  map.setCenter(center);

  var bounds = new GLatLngBounds(new GLatLng(minLat, minLng), new GLatLng(
      maxLat, maxLng));
  map.setZoom(map.getBoundsZoomLevel(bounds));
}

// Crosshairs div

var crosshairsSize = 19;

GMap2.prototype.addCrosshairs = function() {
  var container = this.getContainer();
  if (this.crosshairs)
    this.removeCrosshairs();
  var crosshairs = document.createElement("img");
  crosshairs.src = 'http://www.acme.com/resources/images/crosshairs.gif';
  crosshairs.style.width = crosshairsSize + 'px';
  crosshairs.style.height = crosshairsSize + 'px';
  crosshairs.style.border = '0';
  crosshairs.style.position = 'relative';
  crosshairs.style.top = ((container.clientHeight - crosshairsSize) / 2) + 'px';
  crosshairs.style.left = ((container.clientWidth - crosshairsSize) / 2) + 'px';
  crosshairs.style.zIndex = '500';
  container.appendChild(crosshairs);
  this.crosshairs = crosshairs;
  return crosshairs;
};

GMap2.prototype.removeCrosshairs = function() {
  if (this.crosshairs) {
    this.getContainer().removeChild(this.crosshairs);
    this.crosshairs = null;
  }
};
// OverlayMessage.js - HTML transparent overlaid message
//
// Using these routines is very easy.
//
// 1) In your HTML, make an element for the message to appear over.  Give
//    it whatever style attributes you like:
//
//        <div id="container" style="width: 100%; height: 7in;"></div>
//
// 2) Load the routines into your code:
//
//        <script src="http://www.acme.com/javascript/OverlayMessage.js" type="text/javascript"></script>
//
// 3) Create an OverlayMessage object, passing it the HTML element:
//
//        var om = new OverlayMessage( document.getElementById( 'container' ) );
//
// 4) Call the Set method when you want a message to appear:
//
//        om.Set( 'Loading...' );
//
// 5) Call the Clear method when you want the message to go away:
//
//        om.Clear();
//
// That's it!  Everything else happens automatically.
//
//
// The current version of this code is always available at:
// http://www.acme.com/javascript/
//
//
// Copyright ï¿½ 2006 by Jef Poskanzer <jef@mail.acme.com>.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// For commentary on this license please see http://www.acme.com/license.html

OverlayMessage = function(container) {
  // Terminology:
  // +-----------------+
  // |wrapper |
  // |+---------------+|
  // ||container ||
  // || +-------+ ||
  // || |overlay| ||
  // || +-------+ ||
  // || ||
  // |+---------------+|
  // +-----------------+

  // Get the parent.
  var parent = container.parentNode;

  // Make the wrapper div.
  var wrapper = document.createElement('div');
  wrapper.style.cssText = container.style.cssText;
  parent.insertBefore(wrapper, container);

  // Move the container into the wrapper.
  parent.removeChild(container);
  wrapper.appendChild(container);
  container.style.cssText = 'position: relative; width: 100%; height: 100%;';

  // Add the overlay div.
  this.overlay = document.createElement('div');
  wrapper.appendChild(this.overlay);
  this.visibleStyle = 'position: relative; top: -55%; background-color: '
      + OverlayMessage.backgroundColor
      + '; width: 40%; text-align: center; margin-left: auto; margin-right: auto; padding: 2em; border: 0.08in ridge '
      + OverlayMessage.borderColor
      + '; z-index: 100; opacity: .75; filter: alpha(opacity=75);';
  this.invisibleStyle = 'display: none;';
  this.overlay.style.cssText = this.invisibleStyle;
};

OverlayMessage.backgroundColor = '#9999cc';
OverlayMessage.borderColor = '#666699';

OverlayMessage.prototype.Set = function(message) {
  this.overlay.innerHTML = message;
  this.overlay.style.cssText = this.visibleStyle;
};

OverlayMessage.prototype.Clear = function() {
  this.overlay.style.cssText = this.invisibleStyle;
};

OverlayMessage.SetBackgroundColor = function(color) {
  OverlayMessage.backgroundColor = color;
};

OverlayMessage.SetBorderColor = function(color) {
  OverlayMessage.borderColor = color;
};

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

/*	Google Maps API HtmlControl v1.1.2
	based on code posted on Google Maps API discussion group
	last updated/modified by Martin Pearman 20th August 2008
	
	http://googlemapsapi.martinpearman.co.uk/htmlcontrol
	
	This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

function HtmlControl(html, options){
	this.html=html;
	this.isVisible=true;
	this.isPrintable=false;	
	this.isSelectable=false;
	if(options){
		this.isVisible=(options.visible===false)?false:true;
		this.isPrintable=(options.printable===true)?true:false;
		this.isSelectable=(options.selectable===true)?true:false;
	}
}

HtmlControl.prototype=new GControl();

HtmlControl.prototype.initialize=function(map){
	this.div=document.createElement('div');
	this.div.innerHTML=this.html;
	this.setVisible(this.isVisible);
	map.getContainer().appendChild(this.div);
	return this.div;
};

HtmlControl.prototype.getDefaultPosition=function(){
	return new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(7,7));
};

HtmlControl.prototype.selectable=function(){
	return this.isSelectable;
};

HtmlControl.prototype.printable=function(){
	return this.isPrintable;
};

HtmlControl.prototype.setVisible=function(bool){
	this.div.style.display=bool ? '':'none';
	this.isVisible=bool;
};

HtmlControl.prototype.visible=function(){
	return this.isVisible;
}
	
var DealerLocator = new Class({

  Implements: Options,

  Extends: Events,

  options: {
    lat: 37.7,
    lng: 0,
    zoom: 2,
    emptyImage: '/docroot/img/tmp/dealer-without-image.png'
  },

  initialize : function(options) {
    this.setOptions(options);

    var mapDiv = document.getElementById('map');

    this.icons = {
      'dealer' :this.icon(ctx + '/docroot/img/marker-dealer-medium.png',
          new GSize(27, 35), new GPoint(13, 33), new GPoint(-114, 0)),
      'service' :this.icon(
          ctx + '/docroot/img/marker-service-medium.png', new GSize(27,
              35), new GPoint(13, 33), new GPoint(-114, 0))
    };
    this.zIndexProcesses = {
      'dealer': function() { return 2; },
      'service': function() { return 1; }
    };

    try {
      // Check browser compatibility.
      if (!GBrowserIsCompatible()) {
        mapDiv.innerHTML = 'Sorry, your browser is not compatible with Google Maps.';
        return;
      }

      // Make the message overlay.
      this.om = new OverlayMessage(mapDiv);

      // Create the map.
      this.map = new GMap2(mapDiv, {
        draggableCursor :'default'
      });
      this.map.addControl(new GMapTypeControl(), new GControlPosition(
          G_ANCHOR_TOP_RIGHT, new GSize(80, 25)));
      this.map.addControl(new GLargeMapControl(), new GControlPosition(
          G_ANCHOR_TOP_RIGHT, new GSize(10, 10)));
      // this.map.addControl(new GScaleControl());

      this.directionsControl = new HtmlControl('<div id="directions" class="htmlControl">'
        + '<a href="#" class="close"></a>'
        + '<form action="">'
        + '<p>' + this.options.labels.labelAddressFrom + '<br />'
        + '<input type="text" name="from" size="40" /><br />'
        + '<input type="hidden" name="to" /><br />'
        + '<input type="submit" name="submit" value="' + this.options.labels.buttonGetDirections + '" /></p>'
        + '</form>'
        + '<div class="info"></div>'
        + '</div>', {visible: false});
      this.map.addControl(this.directionsControl, new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(254,7)));
      var info = $(this.directionsControl.div).getElement('.info');
      $(this.directionsControl.div).getElement('a.close').addEvent('click', function(e) {
        new Event(e).stop();
        this.directionsControl.setVisible(false);
        info.empty();
      }.bind(this));
      $(this.directionsControl.div).getElement('form').addEvent('submit', function(e) {
        new Event(e).stop();
        this.loadDirections();
      }.bind(this));

      this.directions = new GDirections(this.map, info);
      GEvent.addListener(this.directions, 'error', function() { info.set('html', this.options.labels.messageErrorDirections); }.bind(this));

      this.map.setCenter(new GLatLng(this.options.lat, this.options.lng),
          this.options.zoom);
      this.map.setMapType(G_NORMAL_MAP);

      this.addEvent('search', this.handleSearch.bind(this));
    } catch (e) {
      GLog.write('Setup:\n' + Props(e));
    }
  },

  addToMap: function(marker, item) {
    this.map.addOverlay(marker);
  },

  clearMap: function() {
    this.map.clearOverlays();
  },

  handleSearch: function(data) {
    this.map.closeExtInfoWindow();
    this.clearMap();

    // Start fetching the XML.
    this.om.Set('Loading...');
    var params = {};
    [ 'uuid', 'sell', 'assistance' ].each(function(k) {
      if (data[k]) params[k] = data[k];
    });
    HttpGet(ctx + '/maserati/dealers-xml.html?' + Hash.toQueryString(params), this.RequestChecker.bind(this));
  },

  RequestChecker : function(request) {
    try {
      var xmlDoc = request.responseXML.documentElement;
      var itemElements = xmlDoc.getElementsByTagName('item');
      this.items = [];
      for (var i = 0, j = itemElements.length; i < j; i++) {
        this.items.push(this.item(itemElements[i]));
      }

      this.items.each(function(item) {
       var type = item.sell == 'true' ? 'dealer' : 'service';

       var marker = new GMarker(item.location, {
          icon :this.icons[type],
          title :item.name,
          zIndexProcess: this.zIndexProcesses[type]
        });
        item.marker = marker;
        GEvent.addListener(marker, 'click', function() {
          this.marker.openExtInfoWindow(
            this.map,
            'dealer_layer',
            dealerLocator.itemHTML(this.item),
            { beakOffset :0 });
        }.bind( {
          map :this.map,
          marker :marker,
          item :item
        }));
        this.addToMap(marker, item);
      }, this);

      this.om.Clear();
    } catch (e) {
      GLog.write('RequestChecker:\n' + Props(e));
    }
  },

  icon : function(image, size, anchor, windowAnchor) {
    var icon = new GIcon(G_DEFAULT_ICON);
    icon.image = image;
    icon.iconSize = size;
    icon.iconAnchor = anchor;
    icon.infoWindowAnchor = windowAnchor;
    return icon;
  },

  itemProps: [ 'code', 'lat', 'lng', 'name', 'address', 'zip', 'city', 'email', 'phone', 'fax', 'url', 'image', 'fullAddress', 'sell', 'assistance' ],

  item: function(itemElement) {
    var item = new Object();
    for (var i = 0, j = this.itemProps.length; i < j; i++) {
      item[this.itemProps[i]] = itemElement.getAttribute(this.itemProps[i]);
    }
    item.location = new GLatLng(parseFloat(item.lat), parseFloat(item.lng));

    if (item.url && item.url.indexOf('http://') != 0) item.url = 'http://' + item.url;
    return item;
  },

  itemHTML: function(item) {
    var links = [];
    var linkPreowned = this.linkPreowned(item);
    if (linkPreowned) links.push('<a href="' + linkPreowned + '">' + this.options.labels.linkPreowned + '</a>');
    if (item.url) links.push('<a href="' + item.url + '" target="_blank">' + this.options.labels.linkGoTo + '</a>');
    if (this.options.links.carConfigurator) links.push('<a href="' + this.options.links.carConfigurator + '">' + this.options.labels.linkCarConfigurator + '</a>');
    if (this.options.links.ebrochure) links.push('<a href="' + this.options.links.ebrochure + '">' + this.options.labels.linkEbrochure + '</a>');
    if (this.options.labels.linkDirections) links.push('<a href="#" onclick="dealerLocator.showDirectionsControl(' + item.lat + ',' + item.lng + ')">' + this.options.labels.linkDirections + '</a>');
    return '<div class="inner">'
      + '<p class="title"><strong>'
      + item.name + ' ' + item.code
      + '</strong></p>'
      + '<div><img src="'
      + ctx
      + (item.image || this.options.emptyImage) + '" alt="" />'
      + item.fullAddress.replace(/\\n/g, '<br/>')
      + (item.email ? ('E-mail: <a href="mailto:' + item.email + '">' + item.email + '</a><br/>') : '')
      + (item.url ? ('<a href="' + item.url + '" target="_blank">' + item.url + '</a>') : '')
      + '<br class="clear" />'
      + '<p>' + links.join(' | ') + '</p>'
      + '</div>';
  },

  showDirectionsControl: function(lat, lng) {
    this.map.closeExtInfoWindow();
    $(this.directionsControl.div).getElement('input[name=to]').value = lat + ',' + lng;
    this.directionsControl.setVisible(true);
  },

  loadDirections: function() {
    // Via Gorizia, 20052 Monza, Milano
    var div = $(this.directionsControl.div);
    this.directions.load('from: ' + div.getElement('input[name=from]').value + '  to: ' + div.getElement('input[name=to]').value);
  },

  linkPreowned: function(item) {
    if (item.url) {
      if (item.url.indexOf('http://www.maseratidealers.com/') === 0) return item.url + '/maseratistock.php';
      if (item.url.match(/www\.maserati(\.\w+)+/)) return this.options.links.preowned;
    }
    return null;
  }

});

var RicercaDealer = new Class({

  initialize: function(uuid, codeItaly, codeUSA) {
    this.element = $('dealersearch');
    this.uuid = uuid;
    this.codeItaly = codeItaly;
    this.codeUSA = codeUSA;

    this.usa = $('searchForm').getElement('.usa');
    this.italy = $('searchForm').getElement('.italy');


    this.element.getElement('a.expand').addEvent('click', function(e) {
      new Event(e).stop($('state').value);
      this.expand();
    }.bind(this));


    this.element.getElement('.button').addEvent('click', function(e) {
      new Event(e).stop();
      this.doSearch();
    }.bind(this));
    $('country').addEvent('change', this.refreshDropdowns.bind(this));
    this.refreshDropdowns();
    this.doSearch.delay(1, this);
  },

  expand: function() {
    $('dealersearchres').addClass('collapsed');
    $('dealersearch').removeClass('collapsed');
  },

  refreshDropdowns: function() {
    var country = $('country').getSelected().map(function(opt){ return opt.value; });
    if (country != '' && country == this.codeUSA) {
      this.usa.setStyle('display', '');
      this.italy.setStyle('display', 'none');
      $('province').getElement('option').selected = true;
      $('cap').value = '';
    } else if (country != '' && country == this.codeItaly) {
      this.usa.setStyle('display', 'none');
      this.italy.setStyle('display', '');
      $('state').getElement('option').selected = true;
      $('zip').value = '';
    } else {
      this.usa.setStyle('display', 'none');
      this.italy.setStyle('display', 'none');
      $('province').getElement('option').selected = true;
      $('cap').value = '';
      $('state').getElement('option').selected = true;
      $('zip').value = '';
    }
  },

  doSearch: function() {
    if ($('zip').value) $('state').getElement('option').selected = true;
    if ($('cap').value) $('province').getElement('option').selected = true;

    var data = { uuid: this.uuid };
    this.element.getElements('select').each(function(select) {
      data[select.name] = select.getSelected().map(function(opt) {
        return opt.value;
      }).join(',');
    });
    this.element.getElements('input').each(function(input) {
      if (input.type != 'checkbox' || input.checked) data[input.name] = input.value;
    });
    dealerLocator.fireEvent('search', data);
  }

});

var EsitoRicercaDealer = new Class({

  initialize: function() {
    $('dealersearchres').getElement('a.expand').addEvent('click', function(e) {
      new Event(e).stop();
      this.expand();
    }.bind(this));

    dealerLocator.addEvent('search', this.handleSearch.bind(this));
  },

  expand: function() {
    $('dealersearch').addClass('collapsed');
    $('dealersearchres').removeClass('collapsed');
  },

  handleSearch: function(data) {
    new Request({
      url: CONTEXT_PATH + '/maserati/dealers-xml.html',
      method: 'get',
      data: Hash.toQueryString(data),
      onSuccess: function(text, xml) {
        var xmlDoc = xml.documentElement;
        var itemElements = xmlDoc.getElementsByTagName('item');
        var update = $('dealersearchres').getElement('.results');
        update.empty();
        if (itemElements.length == 0) {
          update.innerHTML = dealerLocator.options.labels.messageNoResults;
        } else {
          var bounds = new GLatLngBounds();
          var item, div;
          for (var i = 0, j = itemElements.length; i < j; i++) {
            item = dealerLocator.item(itemElements[i]);
            div = new Element('div');
            div.addClass('result');

            if (item.sell == 'true' && item.assistance == 'true') {
               type = 'dealer_service';
            } else if(item.sell == 'true' && item.assistance == 'false') {
               type = 'dealer';
            } else {
               type = 'service';
            }

            div.innerHTML = '<a href="javascript:void(dealerLocator.map.closeExtInfoWindow(), dealerLocator.map.setCenter(new GLatLng(' + item.lat + ', ' + item.lng + '), 12));" class="'
              +  type
              + '">' + item.name + '</a><img src="' + CONTEXT_PATH + '/docroot/img/marker-' + type + '.gif" alt="" />'
              + '<br/>' + item.fullAddress.replace(/\\n/g, '<br/>')
              + (item.email ? ('E-mail: <a href="mailto:' + item.email + '">' + item.email + '</a><br/>') : '')
              + (item.url ? ('Url: <a href="' + item.url + '" target="_blank">website</a></div>') : '');
            div.inject(update);
            bounds.extend(item.location);
          }
          var ne = bounds.getNorthEast();
          var sw = bounds.getSouthWest();
          bounds.extend(new GLatLng(ne.lat(), sw.lng() - (ne.lng() - sw.lng()) / 2));
          dealerLocator.map.setZoom(dealerLocator.map.getBoundsZoomLevel(bounds));
          dealerLocator.map.setCenter(bounds.getCenter());
        }

        var country = $('country').getSelected().map(function(opt){ return opt.value; })
        if(!(country == '' && (country != this.codeUSA || country != this.codeItaly)))
          this.expand();

      }.bind(this)
    }).send();
  }

});

var DealerLocatorWithClustering = {};

DealerLocatorWithClustering['clusterer2'] = new Class({

  Extends: DealerLocator,

  initialize: function(options) {
    this.parent.apply(this, arguments);

    Clusterer.PopUp = function(cluster) {
      var clusterer = cluster.clusterer;
      var html = '<div>';
      var latlng;
      for (var i = 0, l = Math.min(cluster.markers.length, 10); i < l; i++) {
        latlng = cluster.markers[i].getLatLng();
        html += '<a href="#" onclick="dealerLocator.map.closeExtInfoWindow(); dealerLocator.map.setCenter(new GLatLng(' + latlng.lat() + ', ' + latlng.lng() + '), 12); return false;">' + cluster.markers[i].title + '</a><br/>';
      }
      html += '</div>';
      cluster.marker.openExtInfoWindow(
          dealerLocator.map,
          'dealer_layer',
          html,
          {beakOffset: 1}
        );
      //clusterer.poppedUpCluster = cluster;
    };

    this.clusterers = {
      'dealer': this.clusterer(ctx + '/docroot/img/marker-dealer-big.png',
          new GSize(42, 55), new GPoint(21, 53), new GPoint(-104, 0)),
      'service': this.clusterer(ctx + '/docroot/img/marker-service-big.png',
          new GSize(42, 55), new GPoint(21, 53), new GPoint(-104, 0))
    };
  },

  clusterer: function(image, size, anchor, infoWindowAnchor) {
    var c = new Clusterer(this.map);
    c.icon = new GIcon();
    c.icon.image = image;
    c.icon.iconSize = size;
    c.icon.iconAnchor = anchor;
    c.icon.infoWindowAnchor = infoWindowAnchor;
    return c;
  },

  addToMap: function(marker, item) {
    this.clusterers[item.sell == 'true' ? 'dealer' : 'service'].AddMarker(marker, item.name);
  },

  clearMap: function() {
    var type, clusterer, i;
    for (type in this.clusterers) {
      clusterer = this.clusterers[type];
      for (i = 0; i < clusterer.clusters.length; i++) {
        if (clusterer.clusters[i] != null) {
          clusterer.ClearCluster(clusterer.clusters[i]);
          clusterer.clusters[i] = null;
        }
      }
      clusterer.clusters.length = 0;
      for (i = 0; i < clusterer.markers.length; i++) {
        if (clusterer.markers[i] != null) {
          clusterer.RemoveMarker(clusterer.markers[i]);
        }
      }
      clusterer.markers.length = 0;
    }
  }

});

DealerLocatorWithClustering['clustermarker'] = new Class({

  Extends: DealerLocator,

  initialize: function(options) {
    this.parent.apply(this, arguments);
    this.clusterMarkers = {
      'dealer': this.clusterMarker(ctx + '/docroot/img/marker-dealer-big.png',
          new GSize(42, 55), new GPoint(21, 53), new GPoint(-104, 0)),
      'service': this.clusterMarker(ctx + '/docroot/img/marker-service-big.png',
          new GSize(42, 55), new GPoint(21, 53), new GPoint(-104, 0))
    };
  },

  clusterMarker: function(image, size, anchor, infoWindowAnchor) {
    var icon = new GIcon();
    icon.image = image;
    icon.iconSize = size;
    icon.iconAnchor = anchor;
    icon.infoWindowAnchor = infoWindowAnchor;
    return new ClusterMarker(this.map, {
      clusterMarkerIcon: icon
    });
  },

  addToMap: function(marker, item) {
    this.clusterMarkers[item.sell == 'true' ? 'dealer' : 'service'].addMarkers([ marker ]);
  },

  clearMap: function() {
    for (var type in this.clusterMarkers) {
      this.clusterMarkers[type].removeMarkers();
    }
  },

  RequestChecker : function(request) {
    this.parent(request);
    for (var type in this.clusterMarkers) {
      this.clusterMarkers[type].refresh(true);
    }
  }

});

