function GMCMenuManager (menuBarId, mainPaneId, GMapObj) {
  // make sure we instantiate only once
  if (window.GMCMenuManagerInstance) {
    alert ('GMCMenuManager must not be instantiated twice!');
    return;
  }

  window.GMCMenuManagerInstance = this;

  // basic properties
  this.menuBarPane = document.getElementById (menuBarId);
  this.mainPane = document.getElementById (mainPaneId);
  this.menuBar = Array ();		// horizontal menu bar w/ menu objects
  this.menuBarOrdered = Array ();	// same, in order of appearance
  this.activeDropDown = null;		// active drop-down DOM object
  this.activeMenus = Array ();		// depth-sorted act. menu obj hierarchy
  this.mhdrclass = 'menuheader';	// style class for menuBar menus
  this.clearMenusTimeout = null;	// pending timeout for clearing menus
  this.showSubMenuTimeout = null;	// pending timeout for showing submenu
  this.clearSubMenusTimeout = null;	// pending timeout for clearing submenus

  // timeouts
  this.clearMenusTimeoutMsec	= 50;	// time to wait before clearing any						// menus after exiting main pane
  this.showSubMenuTimeoutMsec	= 600;	// time to wait when cursor stops on						// a menu item with a submenu before
					// displaying the submenu
  this.clearSubMenusTimeoutMsec	= 1200;	// time to wait when cursor stops on						// a menu item without submenus before
					// hiding submenus open by another item
					
  this.clearSubMenusTim

  // geometry (used?) and depth
  this.width = 0;			// bounding box width
  this.height = 0;			// bounding box height
  this.beginZIndex = 30;

  // standard menu item heights
  this.menuItemHeight = 24;
  this.menuHrHeight = 3;

  this.addToMenuBar = function (menu, width) {
    var l = 0;
    if (this.menuBarOrdered.length) {
      var m = this.menuBarOrdered [this.menuBarOrdered.length - 1];
      var d = m.parentDomObj;
      l = m.left + d.width;
    }
    menu.left = l;
    if (this.menuBarPane.nodeName == 'TR') {
      // menu.top  = - this.menuItemHeight;
    }
    else {
      menu.top  = 0; // has to be tested
    }
    menu.positioning = 'down';
    menu.relocate = false;
    menu.depth = 0;

    var d;
    if (this.menuBarPane.nodeName == 'TR') {
      d = document.createElement ('TD');
    }
    else {
      d = document.createElement ('DIV'); // has to be tested
    }

    d.id = menu.name;
    d.width = ((width)? width : 70);
    d.className = this.mhdrclass;
    d.onmouseover = this.mouseOverMenuBarItem;
    d.onmouseout = this.mouseOutOfMenuBarItem;
    d.onclick    = this.mouseClickMenuBarItem;
    d.innerHTML = '&nbsp;&nbsp;' + menu.name;

    menu.parentDomObj = d;

    this.menuBarPane.appendChild (d);

    this.menuBar [menu.name] = menu;
    this.menuBarOrdered.push (menu);
  }


  this.closeMenuBar = function (name) {
    // HERE: produce a final TD with the necessary formatting & event observers
    // and add it
    var l = 0;
    if (this.menuBarOrdered.length) {
      var m = this.menuBarOrdered [this.menuBarOrdered.length - 1];
      var d = m.parentDomObj;
      l = parseInt (m.left) + parseInt (d.width);
    }

    var d;
    if (this.menuBarPane.nodeName == 'TR') {
      d = document.createElement ('TD');
    }
    else {
      d = document.createElement ('DIV');
    }

    d.id = name;
    d.width = 700 - l;				// FIXME: get from Map
    d.onmouseover = this.mouseOverPane;
    d.onmouseout = this.mouseOutOfPane;
    d.className = 'menuheaderright';
    this.menuBarPane.appendChild (d);
  }

  /* if we need to reconstruct the menu bar
  this.resetMenuBar = function (name) {
  }
  */

  this.cancelClearMenusTimeout = function () {
    var mm = window.GMCMenuManagerInstance;

    if (mm.clearMenusTimeout) {
      window.clearTimeout (mm.clearMenusTimeout);
    }
    mm.clearMenusTimeout = null;
  }

  this.cancelShowSubMenuTimeout = function () {
    var mm = window.GMCMenuManagerInstance;

    if (mm.showSubMenuTimeout) {
      window.clearTimeout (mm.showSubMenuTimeout);
    }
    mm.showSubMenuTimeout = null;
  }

  this.cancelClearSubMenusTimeout = function () {
    var mm = window.GMCMenuManagerInstance;

    if (mm.clearSubMenusTimeout) {
      window.clearTimeout (mm.clearSubMenusTimeout);
    }
    mm.clearSubMenusTimeout = null;
  }

  this.cancelSubMenuTimeouts = function () {
    var mm = window.GMCMenuManagerInstance;
    mm.cancelShowSubMenuTimeout ();
    mm.cancelClearSubMenusTimeout ();
  }

  this.clearSubMenus = function (i) {
    var mm = window.GMCMenuManagerInstance;

    mm.cancelClearSubMenusTimeout ();

    while (this.activeMenus.length > i) {
      var m = this.activeMenus.pop();
      mm.mainPane.removeChild (m.divObj);
      m.divObj = null;
      if (m.lazyfunc) m.resetMenuItems();	// leave back an empty menu
    }
  }

  this.clearMenuBar = function () {
    var mm = window.GMCMenuManagerInstance;
    for (var i in mm.menuBar) {
      mm.menuBar[i].parentDomObj.className = mm.mhdrclass;
    }
  }

  this.clearMenus = function () {
    var mm = window.GMCMenuManagerInstance;

    mm.cancelClearMenusTimeout ();
    mm.clearSubMenus (0);
    mm.clearMenuBar ();
  }

  this.mouseOverPane = function () {
    var mm = window.GMCMenuManagerInstance;
    // due to a race condition (oject has been created but not stored under
    // window.GMCMenuManagerInstance), this can be null in cases the mouseOver
    // event is fired during initialization ; avoid warning by just doing
    // nothing, we 'll get the chance to see a second mouse over
    if (!mm) return;

    mm.cancelClearMenusTimeout ();
    mm.mhdrclass = 'menu';
    for (var i in mm.menuBar) {
      mm.menuBar[i].parentDomObj.className = mm.mhdrclass;
    }
  }

  this.mouseOutOfPane = function () {
    var mm = window.GMCMenuManagerInstance;
    if (!mm) return;		// see above -- should be rarer than mouseover
    mm.clearMenusTimeout = window.setTimeout (mm.clearMenus,
     mm.clearMenusTimeoutMsec);
    mm.mhdrclass = 'menuheader';
    for (var i in mm.menuBar) {
      mm.menuBar[i].parentDomObj.className = mm.mhdrclass;
    }
  }

  this.mouseOverMenuBarItem = function () {
    var mm = window.GMCMenuManagerInstance;
    var m = mm.menuBar [this.id];

    mm.mouseOverPane ();

    if (mm.activeMenus.length > 0) {
      mm.clearMenus ();
      mm.expandVerticallyMenu (m);
    }
    else {
      mm.clearMenus ();
    }
    this.className = 'menuhighlight'
  }

  this.mouseOutOfMenuBarItem = function () {
    window.GMCMenuManagerInstance.mouseOutOfPane ();
  }

  this.mouseClickMenuBarItem = function () {
    var mm = window.GMCMenuManagerInstance;
    var m = mm.menuBar [this.id];
    mm.clearMenus();
    mm.expandVerticallyMenu (m);
    this.className = 'menuhighlight'
  }

  this.mouseOverMenuItem = function () {
    var mm = window.GMCMenuManagerInstance;
    var l = mm.activeMenus.length - 1;
    var menudepth = this.menudepth;

    mm.cancelClearMenusTimeout ();	// this only makes sense for transitions
					// from the menubar to a menu item

    mm.cancelClearSubMenusTimeout ();
    mm.cancelShowSubMenuTimeout();

    // alert ('l='+l+', menudepth='+menudepth);
    if (l > this.menudepth) {
      mm.clearSubMenusTimeout = window.setTimeout (function () {
	mm.clearSubMenus (menudepth + 1);
      }, mm.clearSubMenusTimeoutMsec);
    }
    this.className = 'menuhighlight';
  }

  this.mouseOutOfMenuItem = function () {
    var mm = window.GMCMenuManagerInstance;

    mm.cancelShowSubMenuTimeout ();
    mm.cancelClearSubMenusTimeout ();
    this.className = 'menu';
  }

  this.mouseOverMenuItemWithSubMenu = function () {
    var mm = window.GMCMenuManagerInstance;
    var l = mm.activeMenus.length - 1;

    var thistd = this;			// has a context of invoking <td> obj

    if (l <= this.menudepth ||		// no deeper sub-menus open
     mm.activeMenus [l] != thistd.submenu) {	// other owners' menus are open
      mm.cancelShowSubMenuTimeout ();
      mm.showSubMenuTimeout = window.setTimeout (function () {
	thistd.onclick();
      }, mm.showSubMenuTimeoutMsec);
    }
    this.className = 'menuhighlight';
  }

  this.clickMenuItemWithSubMenu = function () {
    var mm = window.GMCMenuManagerInstance;

    mm.cancelShowSubMenuTimeout ();
    mm.cancelClearSubMenusTimeout ();

    mm.clearSubMenus (this.menudepth + 1);	// someone else's submenu
    mm.expandVerticallyMenu (this.submenu);
  }

  this.mouseClickFinal = function (funcref, event) {
    var mm = window.GMCMenuManagerInstance;
    mm.clearMenus();
    funcref (event);
  }

  this.expandVerticallyMenu = function (m, p) {
    var mm = window.GMCMenuManagerInstance;

    var top = m.top;
    var left= m.left;
    if (p) {
      top = p.y;
      left= p.x;
    }

    // evaluate dynamically menu contents
    if (m.lazyfunc && m.calllazy) {
      m.lazyfunc ();
      // if a callback function has been supplied, don't expand now
      // (but store location for later redisplaying)
      if (m.asyncbak) {
	m.top = top;
	m.left = left;
	return;
      }
    }

    m.depth = mm.activeMenus.length;
    // alert ('depth='+m.depth);

    switch (m.positioning) {
     case 'down':
      // top += mm.menuItemHeight;
      break;
     case 'right':
      left += m.parentDomObj.width;
      break;
     default:
      // position right there
      break;;
    }
    
    m.divObj = m.render (top, left);

    mm.activeMenus.push (m);
    mm.mainPane.appendChild (m.divObj);
  }

  this.expandHorizontallyMenuItem = function (m) {
    // etc.
  }

  if (GMapObj instanceof GMap2) {
    GEvent.addListener (GMapObj, "mouseover", this.mouseOverPane);
    GEvent.addListener (GMapObj, "mouseout", this.mouseOutOfPane);

    this.width = GMapObj.getSize().width;
    this.height= GMapObj.getSize().height;
  }
}

function GMCompatMenu (name, width, lazyfunc /*_or_options */, asyncbak) {
  var mm = window.GMCMenuManagerInstance;

  this.name = name;			// menu name
  this.divObj = null;			// div object for displaying this menu
  this.parentDomObj = null;		// dom object (usually a TD on a parent
					// menu) that triggered this menu
  this.depth = 0;			// 0=menu, 1=submenu, 2=sub-submenu,etc.

  this.positioning = null;		// whether to display 'down' or 'right'
  this.relocate = false;		// relocate to fit to main pane?
  this.noindent = false;		// avoid inserting &nbsp;s at start?

  this.top = 0;				// current location
  this.left = 0;

  this.parentleft = 0;			// useful in relocating

  this.width = (width)?width : 100;	// width of menu in pixels
  this.nexttoprel = 0;			// relative top for next item to add

  this.bottom = 0;			// absolute Y position of bottom pixel
  this.right = 0;			// absolute X position of rigthmost pixl

  this.items = Array();			// current menu items
  this.hasSub = false;			// has submenus?

  this.preplaced = false;		// already preplaced

  this.lazyfunc = null;			// lazy (onMouseOver) creation function
  this.asyncbak	= null;			// callback for async lazy functions
  this.calllazy = true;			// flag for ignoring lazy when callback
					// is being fired

  // third argument is either a literal object with options, or a function
  // reference for lazy menu evaluation
  if (lazyfunc && typeof(lazyfunc) == 'function') {
    this.lazyfunc = lazyfunc;
    if (asyncbak) {
      this.asyncbak = function () {
	var mm = window.GMCMenuManagerInstance;
	this.calllazy = false;
	mm.expandVerticallyMenu (this);
	this.calllazy = true;;
      }
    }
  }
  else if (lazyfunc) {			// if not a function, it is a flag
    var options = lazyfunc;
    if (options.relocate) this.relocate = true;
    if (options.noindent) this.noindent = true;
  }

  this.addItem = function (/*GMCompatMenuItem*/ item) {
    item.toprel = this.nexttoprel;
    this.nexttoprel += item.height;
    if (item.width) {
      this.width = Math.Max (item.width, this.width);
    }
    this.items.push (item);
    if (item.submenu) this.hasSub = true;
  }

  this.resetMenuItems = function () {
    // this.top = 0;
    // this.left = 0;
    this.nexttoprel = 0;
    this.bottom = 0;
    this.right = 0;
    this.items = Array ();
    this.hasSub= false;
    // this.preplaced = false;
  }

  this.preplace = function (top, left) {
    this.top = top;
    this.left = left;
    this.preplaced = true;
  }

  this.clearpreplace = function () {
    this.top = 0;
    this.left = 0;
    this.preplaced = false;
  }

  this.render = function (top, left) {

    if (this.preplaced) {
      top = this.top;
      left= this.left;
    }

    // alert ('Rendering '+this.name+' at '+top+','+left);

    var mm = window.GMCMenuManagerInstance;
    var d = document.createElement ('div');

    if (this.relocate) {
      if (mm.height && top + this.nexttoprel > mm.height) {
	// alert ('v-overflow');
	top = mm.height - this.nexttoprel;
	if (top < 0) top = 0;
      }

      if (mm.width && left + this.width > mm.width) {
	// alert ('h-overflow');
	left = this.parentleft - this.width - 2;
	if (left < 0) {
	  left = mm.width - this.width - 2 - this.parentleft; 
	  if (left < 0) {
	    left = 0;
	    // in all these cases, sub-menu will overlap horizontally,
	    // so try to place it over or below current parent menu item
	    // FIXME: do it...
	    top += 2 * mm.menuItemHeight;
	    if (mm.height && top + this.nexttoprel > mm.height) {
	      // fix this...
	      top = mm.height - this.nexttoprel - 2 * mm.menuItemHeight;
	      if (top < 0) top = 0;
	    }
	  }
	}
      }
    }

    this.bottom = top + this.nexttoprel;
    this.right  = left + this.width;
    if (this.hasSub) this.right += 5;

    if (this.relocate) {
      // check if menu fits in pane and relocate
    }

    if (!this.items.length) {
      d.innerHTML = '(empty menu)';
      return;
    }

    // we need a row to create a TBODY, doesn't get created otherwise
    d.innerHTML = '<table border="1px" bordercolor="#A0A0A0" cellspacing="0" cellpadding="0"><tr><td><table class="menu" border="0" cellpadding="0" cellspacing="0"><tr><!--this must be here so that a tbody is created--></tr></table></td></tr></table>';
    var tbody = d.childNodes[0].tBodies[0].rows[0].cells[0].childNodes[0].tBodies[0];

    for (var i in this.items) {
      var it = this.items[i];

      it.top = top + it.toprel;
      it.left= left;
      it.width = this.width;
      if (this.hasSub) it.width += 5;

      var tr = document.createElement ('TR');
      tbody.appendChild (tr);

      var td = document.createElement ('TD');
      td.className = it.className;
      td.width = it.width + 'px';
      td.innerHTML = it.text;
      td.menudepth = this.depth;

      if (it.onmouseover) td.onmouseover = it.onmouseover;
      if (it.onmouseout) td.onmouseout = it.onmouseout;
      if (it.onclick) td.onclick = it.onclick;

      if (it.submenu) {
	td.innerHTML = '<table border="0" cellpadding="0" cellspacing="0"><tr><td width="'+this.width+'">'+it.text+'</td><td width="5">&gt;</td></tr></table>';
	td.submenu = it.submenu;
	it.submenu.preplace (it.top, it.left + it.width + 3);
	it.submenu.parentleft = left;
      }
      tr.appendChild (td);
    }

    d.id = 'menu:' + this.name + '@' + this.top + ',' + this.left;
    d.className = 'menu';
    s = d.style;
    s.position = 'absolute';
    s.top = top + 'px';
    s.left = left + 'px';
    s.zindex = mm.beginzindex + this.depth;

    return d;
  }
}

function GMCompatMenuItem (text, onclick) {
  var mm = window.GMCMenuManagerInstance;

  this.className = 'menu';
  this.height = 24;
  this.width = null;		// overwritten when added to menu

  this.top = 0;
  this.toprel = 0;
  this.left= 0;
  this.leftrel= 0;

  this.text = text;
  this.onmouseover = null;
  this.onmouseout = null;
  this.onclick = null;

  this.submenu = null;
  

  if (text == '<hr>' || text == '<HR>') {
    this.height = 3;
    this.className = 'menuhr';
    return;
  }

  if (onclick instanceof GMCompatMenu) {
    this.submenu = onclick;
    // these MUST CHANGE
    this.onmouseover = mm.mouseOverMenuItemWithSubMenu;
    this.onmouseout = mm.mouseOutOfMenuItem;
    this.onclick = mm.clickMenuItemWithSubMenu;
    this.text = '&nbsp;&nbsp;&nbsp;&nbsp;' + text;
  }
  else if (onclick) {
    this.onmouseover = mm.mouseOverMenuItem;
    this.onmouseout = mm.mouseOutOfMenuItem;
    this.onclick = function (ev) { mm.mouseClickFinal (onclick, ev);};
    this.text = '&nbsp;&nbsp;&nbsp;&nbsp;' + text;
  }
  else {
    this.className = 'menuinert';
    this.text = '&nbsp;&nbsp;' + text;
  }
}
