// almost-100% GUI Google Maps Builder for MediaWiki.
// Copyright 2006-2007 Evan Miller, except as noted below.

// Man, this almost looks professional.

/*
 * Hello! Welcome to the source of the Editor's Map. This is broken down
 * into four classes:
 *
 * 1. EditorsMarker: a wrapper around the GMarker path that provides the
 *            references necessary for the linked list structures, as well
 *            as information about captions and tabs
 * 2. EditorsSingletons: a linked list of unaffiliated markers
 * 3. EditorsPath: a linked list representing a path of connected markers
 * 4. EditorsMap: the Big One. This is the application class that contains
 *            everything else.
 *
 * You'll also notice references to a hash called "_". That's a structure of
 * messages created by GoogleMapsMessages.php.
 *
 * Anyway, feel free to poke around. I put a lot of work into cleaning and
 * documenting this code so you can understand it. If you make improvements,
 * please take time to send me a patch, so the program is better for everyone.
 * You can reach me at emmiller@gmail.com. Thanks!
 *
 */

// TODO: make path joints draggable only in "edit this path" mode for performance.

// "Class" is taken from the Prototype library. This makes it so we can
// declare new classes with arguments, instead of having to
// call the initialize method ourselves.

var Class = { create: function() { return function() { this.initialize.apply(this, arguments); } } };

// The application object.
var emap;

// Used for measuring paths.
var conversion_factor = { 'kilometers':1 / 1000, 'meters':1, 'miles': 100 / 2.54 / 12 / 5280 };
var sigfigs = 4;
var abbreviations = { 'kilometers':'km', 'meters':'m', 'miles':'mi' };

var colorSelectorRegistry = Array();

// a wrapper around the GMarker class
// This adds the variables we need to play with
// EditorsPath, which also makes it easier
// for us to swap out the underlying GMarker.
var EditorsMarker = Class.create();
EditorsMarker.prototype = {
    initialize: function(point, emap, icon) {
                  this.gmarker = new GMarker(point, { 'draggable':true, 'icon':icon });
                  this.gmarker.emarker = this;
                  this.container = document.createElement("span");
                  this.container.appendChild(document.createTextNode(emap.round(this.gmarker.getPoint().lat())+', '+emap.round(this.gmarker.getPoint().lng())));
                  this.container.appendChild(document.createElement("br"));
                  this.emap = emap;
                  this.tabs = new Array();
                  var this_marker = this;
                  GEvent.addListener(this.gmarker, 'dragend', function() { this_marker.updateLocation() } );
                },

    getIcon: function() {
	       return this.gmarker.getIcon();
	     },

    setCaption: function(caption) {
		  if (caption && this.getIcon() == GME_SMALL_ICON ) {
		    this.setIcon(G_DEFAULT_ICON);
		  } else if (!caption && this.path && this.getIcon() == G_DEFAULT_ICON) {
		    this.setIcon(GME_SMALL_ICON);
		  }
		  this.caption = caption;
          this.dump();
		},

    dump: function() {
            this.container.innerHTML = '';
            var line = '';
            if (this.icon_name)
              line += '('+this.icon_name+') ';
            line += this.emap.round(this.getPoint().lat())+', '+this.emap.round(this.getPoint().lng());
            if (this.caption)
              line += ', '+this.caption;
            this.container.appendChild(document.createTextNode(line));
            this.container.appendChild(document.createElement('br'));
            for(var i=0;i<this.tabs.length;i++) {
              this.container.appendChild(document.createTextNode('/'+this.tabs[i].title+"\\ "+this.tabs[i].content));
              this.container.appendChild(document.createElement('br'));
            }
          },

    addTab: function(title, content) {
              this.tabs[this.tabs.length] = { 'title':title, 'content':content };
              if (this.getIcon() == GME_SMALL_ICON)
                this.setIcon(G_DEFAULT_ICON);
              this.dump();
            },

// The API doesn't let us change a marker's icon on the fly,
// so we need to instantiate a new GMarker. No problem, though,
// because all the references that this application uses are
// here in the wrapper class.
    setIcon: function(icon) {
	       this.emap.zapGMarker(this.gmarker);
	       this.gmarker = new GMarker(this.gmarker.getPoint(), { 'icon':icon, 'draggable':this.gmarker.draggable() });
	       this.emap.gmap.addOverlay(this.gmarker);
	       this.gmarker.emarker = this;
	       var this_marker = this;
	       GEvent.addListener(this.gmarker, 'dragend', function() { this_marker.updateLocation() } );
	     },

    getPoint: function() {
                return this.gmarker.getPoint();
	      },

    distanceFrom: function(marker) {
                    return this.getPoint().distanceFrom(marker.getPoint());
                  },

// Called at the end of a drag. We recalculate the
// path's distance and draw new Polyline segments.
    updateLocation: function() {
      if( this.path ) {
        this.path.redraw( );
        this.path.distance = 0;
      }
      this.dump();
      this.emap.dumpPaths();
    },

		    // we need this logic in a couple places, so might as well put it here.
    getBalloonFooter: function() {
      var message = '';
      if (this.search_result) {
          message += '<a href="javascript:void(0)" onclick="emap.removeActiveMarkerAndJumpBack()">'+_['back']+'</a>&nbsp;&nbsp;';
      }
      message += '<a href="javascript:void(0)" onclick="emap.updateActiveMarker()">'+_['save point']+'</a>'+
	  '&nbsp;&nbsp;<a href="javascript:void(0)" onclick="emap.removeActiveMarker()">'+_['remove']+'</a>';
      if (GME_PATHS_SUPPORTED && this.path == undefined) {
	  message += '&nbsp;&nbsp;<a href="javascript:void(0)" onclick="emap.startPath()">'+_['start path']+'</a>';
      }
      message += '<div style="color: #aaa; font-size: 10px;">'+
	  this.emap.round(this.getPoint().lat())+', '+
	  this.emap.round(this.getPoint().lng())+'</div>';
      return message;
    },

    openEditWindow: function() {
      if (this.tabs.length) {
	  var tabs = [];
	  for(var t=0; t < this.tabs.length; t++) {
              label = this.emap.rtl ? ((parseInt(t)+1)+' '+_['tab']) : _['tab']+' '+(parseInt(t)+1);
              content = _['tab title']+':<br />'+'<input size="24" id="tab_title_'+t+'" value="'+this.tabs[t].title+'" />'+
		      '<br />'+_['caption']+':<br />'+
		      '<textarea class="balloon_textarea" id="tab_content_'+t+'">'+
		      this.tabs[t].content+'</textarea><br />'+
		      this.getBalloonFooter();
              if (this.emap.rtl) {
                  content = '<div style="direction: rtl;">'+content+'</div>';
              }
              tabs[t] = new GInfoWindowTab( label, content );
	  }
	  this.gmarker.openInfoWindowTabsHtml(tabs);
      } else {
          var content = ''; 
          content += _['make marker'];
          content += '<br /><textarea id="balloon_textarea" class="balloon_textarea">';
          content += this.caption;
          content += '</textarea><br />';
          content += this.getBalloonFooter();

          if (this.emap.rtl) {
              content = '<div style="direction: rtl;">'+content+'</div>';
          }
          this.gmarker.openInfoWindowHtml( content, { maxWidth:270 });
      }
    },

    caption: ''
};

// It's: a very simple linked list
var EditorsSingletons = Class.create();
EditorsSingletons.prototype = {
    initialize: function() {
                  this.container = document.createElement("span");
                  document.getElementById("map_dump_body").appendChild(this.container);
                },

    reset: function() {
             this.head = undefined;
             this.container = document.createElement("span");
             document.getElementById("map_dump_body").appendChild(this.container);
           },

    removeMarker: function(doomed_marker) {
            if (!doomed_marker) {
              return;
            }
            var p = this.head;
            // First, remove it from the singletons' linked list.
            while(p) {
              if (p == doomed_marker) {
                if (p.previous_marker) {
                  p.previous_marker.next_marker = p.next_marker;
                }
                if (p.next_marker) {
                  p.next_marker.previous_marker = p.previous_marker;
                }
                this.container.removeChild(doomed_marker.container);
              }
              p = p.previous_marker;
            }

            // If we're removing the head of the list, update
            // the head reference
            if (this.head == doomed_marker) {
              this.head = doomed_marker.previous_marker;
            }

            doomed_marker.previous_marker = undefined;
            doomed_marker.next_marker = undefined;
                  },

    addMarker: function(marker) {
	     marker.previous_marker = this.head;
	     if (this.head) { this.head.next_marker = marker; }
	     this.head = marker;
         this.container.appendChild(marker.container);
	   }
};

// A slightly more complicated linked list. This object also stores
// information about the path, such as its color and total distance.
var EditorsPath = Class.create();
EditorsPath.prototype = {
    initialize: function(color, fill, stroke, map, units, isPoly) {
        this.poly = isPoly;
          this.container = document.createElement("span");
          this.container.appendChild( document.createTextNode( '' ) );
          this.container.appendChild( document.createElement( 'br' ) );
          document.getElementById("map_dump_body").appendChild(this.container);

          this.colorSelector = new color_select(color);
          colorSelectorRegistry[this.colorSelector.id] = { 'path':this, 'type':'line' };
          this.setColor(color);

          this.colorSelectorFill = new color_select(fill);
          colorSelectorRegistry[this.colorSelectorFill.id] = { 'path':this, 'type':'fill' };
          this.setColor(fill, true);

          this.gmap = map;
          this.units = units;
          this.stroke = stroke;
          this.size = 0;
          this.distance = 0;
          this.markers = Array( );
          this.overlay = null;
    },

    addFill: function() {
                 this.poly = true;
                 this.setColor(this.hex_color, true);
                 this.redraw();
                 emap.dumpPaths();
                 this.colorSelectorFill.toggle_color_select();
     },

    removeFill: function() {
                 this.poly = false;
                 this.setColor(this.hex_color);
                 this.redraw();
                 emap.dumpPaths();
     },

    jump: function() {
              bounds = this.overlay.getBounds();
              this.gmap.setCenter(bounds.getCenter(), zoom = this.gmap.getBoundsZoomLevel(bounds));
    },

    addMarker: function(marker) {
      marker.path = this;

      this.markers.push( marker );

         this.container.appendChild(marker.container);
         this.redraw( );
         this.distance = 0;
       },

    redraw: function( ) {

      var old_overlay = this.overlay;

      if( !this.poly ) {
        var points = Array( );
        for( var i = 0; i <  this.markers.length; i++ ) {
          points[i] = this.markers[i].getPoint( );
        }
        this.overlay = new GPolyline( points, this.color, this.stroke, this.opacity );
        if ( old_overlay ) {
            this.gmap.removeOverlay(old_overlay);
        }
        this.gmap.addOverlay( this.overlay );
      }
      else {
        if( this.markers.length > 1 ) {
          this.markers.push( this.markers[0] );
        }
        var points = Array( );
        for( var i = 0; i <  this.markers.length; i++ ) {
          points[i] = this.markers[i].getPoint( );
        }
        this.overlay = new GPolygon( points, this.color, this.stroke, this.opacity, this.fill_color, this.fill_opacity );
        if ( old_overlay ) {
            this.gmap.removeOverlay(old_overlay);
        }
        this.gmap.addOverlay( this.overlay );
        if( this.markers.length > 1 ) {
          this.markers.pop( );
        }
      }
    },

    removeMarker: function(doomed_marker) {

      for( var i = 0; i < this.markers.length; i++ ) {
        if( doomed_marker == this.markers[i] ) {
            doomed_marker.path = null;
          this.markers.splice( i, 1 );
        }
      }

      this.redraw();
      this.distance = 0;
    },

    sizeExpression: function() {
        return this.poly ? this.areaExpression() : this.distanceExpression();
    },

    distanceExpression: function() {
      if( this.distance == 0 ) {
        this.distance = this.overlay.getLength();
      }
      dist = this.distance * conversion_factor[this.units];
      return this.formatNumber(dist, sigfigs)+' '+abbreviations[this.units];
    },

    areaExpression: function() {
        if (this.distance == 0) {
            this.updateDistance();
        }
        area = this.overlay.getArea() * Math.pow(conversion_factor[this.units], 2);
        return this.formatNumber(area, sigfigs)+' '+abbreviations[this.units]+'<sup>2</sup>';
    },

    updateDistance: function() {
    // we calculate the perimeter for purposes of comparing with other lines
            for( var i = 1; i < this.markers.length; i ++ ) {
                this.distance += this.markers[i].getPoint().distanceFrom( this.markers[i-1].getPoint() );
            }
            this.distance += this.markers[0].getPoint().distanceFrom( this.markers[this.markers.length - 1].getPoint() );
    },

    formatNumber: function(num, sigfigs) {
      places = Math.max(sigfigs - (Math.round(num)+'').length, 0);
      return Math.round(num * Math.pow(10, places)) / Math.pow(10, places);
    },

    setColor: function(color, isFill) {
	      var normalized = this.hex2alpha(color);
        if(isFill || !this.poly) {
  	      this.fill_hex_color = color;
  	      this.fill_color = normalized[0];
  	      this.fill_opacity = normalized[1];
        }
        if(!isFill) {
  	      this.hex_color = color;
  	      this.color = normalized[0];
  	      this.opacity = normalized[1];
        }
      if(this.poly) {
        this.container.childNodes[0].nodeValue = this.hex_color + ' (' + this.fill_hex_color + ')';
      }
      else {
        this.container.childNodes[0].nodeValue = this.hex_color;
      }
  },

// This method treats whiteness as transparency,
// returning a renormalized hex value and the
// opacity level. Works well for the "map" view,
// not so well for others. Might need to do something
// different for the satellite imagery, in the future...
    hex2alpha: function(hex) {
                 hex = hex.replace(/#/, '');
                 hex = hex.toUpperCase();
                 var rgb = [ parseInt(hex.substring(0, 2), 16),
                     parseInt(hex.substring(2, 4), 16),
                     parseInt(hex.substring(4, 6), 16) ];
                 var lets = "0123456789abcdef".split('');
                 var min = 255;
                 for(var i=0; i<rgb.length; i++) {
                   if (rgb[i] < min) {
                     min = rgb[i];
                   }
                 }
                 for(var i=0; i<rgb.length; i++) {
                   // re-normalize
                   rgb[i] = (rgb[i] - min) * (256 / (256 - min));
                   // hexify
                   rgb[i] = lets[rgb[i] >> 4] + lets[rgb[i] & 0xf];
                 }
                 return( [ '#'+rgb.join(''), (256 - min) * 1.0 / 256 ] );
               },

    updateColor: function(new_color, isFill) {
                   this.setColor(new_color, isFill);
                   this.redraw();
                 }
};

var EditorsMap = Class.create();
EditorsMap.prototype = {

/********* Initialization *********/

    initialize: function(options) {
// There are some weird incompatibilities with Safari which
// make the Editor's Map about 50% broken.
// If you want to be a hero and debug Safari's problems,
// just change this condition and start playing with it.
// For now, I'm declaring it incompatible.
	if (!GBrowserIsCompatible() || navigator.vendor == "Apple Computer, Inc.") {
	    this.mother_div = document.createElement('div');
	    this.mother_div.innerHTML = _['no editor'];
	    document.getElementById(options.container).appendChild(this.mother_div);
	    return;
	}
          // Initialize
	this.icon_base = options.icons;
	this.precision = options.precision; // how many decimal places?
	this.paths_supported = GME_PATHS_SUPPORTED;
	this.default_color = options.color;
        this.stroke = options.stroke;
	this.paths = new Array();
	this.units = options.units;
        this.rtl = options.rtl;
	this.textbox = document.getElementById(options.textbox);
	this.defaults = options; // keep a copy for later

	this.mother_div = this.getEditorsMapNode(options); // build all the HTML

	// stick it somewhere useful
	document.getElementById(options.container).appendChild(this.mother_div);

	this.singletons = new EditorsSingletons();

	// Now make the API components and attach to the appropriate places.
	this.gmap = new GMap2(this.map_div);
	this.controls = { 'selector':new GMapTypeControl(), 'scale':new GScaleControl(), 'overview':new GOverviewMapControl() };
	this.active_controls = {};

	if (options.geocoder) {
	    this.geocoder = new GClientGeocoder();
	}
	if (options.localsearch) {
	    this.localSearch = new GlocalSearch();
	    this.localSearch.setCenterPoint(this.gmap);
	    this.localSearch.setSearchCompleteCallback(this, EditorsMap.prototype.populateResults);
	}

	this.configureMap( options );

	if (this.maps_in_article == 1)
	    this.loadMap(1);

	// Closures are great, but they make it harder to have short-ish functions.
	// Here's how we cheat and let the closure bind to "this" but stick the workhorse
	// function somewhere else.
	var this_map = this;

	// Keep the map's center up-to-date.
	GEvent.addListener(this.gmap, 'moveend', function() { this_map.dumpMapAttributes() });

	// one click listener to rule them all...
	GEvent.addListener(this.gmap, 'click', function(overlay, point) { this_map.clickMap(overlay, point); });
    },

    getEditorsMapNode: function(options) {
          // Crack your knuckles. It's time to build a big fat DOM node with everything you see.
	       this.map_div = document.createElement("div");
	       this.map_div.style.width = options.width+"px";
	       this.map_div.style.height = options.height+"px";
               this.map_div.style.direction = "ltr";

               this.search_div = document.createElement("div");
	       if (options.geocoder || options.localsearch) {
                   if (options.localsearch) {
                       this.search_div.innerHTML = _['search preface'];
                   } else {
                       this.search_div.innerHTML = _['geocode preface'];
                   }
                   this.search_div.innerHTML +=
		       '<br /><input type="text" size="40" id="address_input" onkeypress="emap.findAddressIfEnter(event)" />&nbsp;&nbsp;&nbsp;'+
		       '<a href="javascript:void(0)" onclick="emap.findAddress();">'+_['search']+'</a>&nbsp;&nbsp;&nbsp;'+
		       '<a href="javascript:void(0)" onclick="emap.clearResults()" id="clear_search_results" style="display: none;">'+
		       _['clear search']+'</a>';
                   this.searching_div = document.createElement("div");
                   this.searching_div.innerHTML = _['searching'];
                   this.searching_div.style.display = 'none';
	       } else {
                   this.search_div.innerHTML = _['no search preface'];
               }

	       if (options.localsearch) {
		   this.map_table = document.createElement("table");
		   this.map_table.setAttribute("cellspacing", "8");
		   this.map_table.style.display = 'none';

		   this.map_body = document.createElement("tbody");

		   this.local_search_results = document.createElement("tr");

		   this.map_table.appendChild(this.map_body);
		   this.map_body.appendChild(this.local_search_results);
	       }

	       // We need to hang on to this reference for later,
	       // so this is scoped for the object, not just the initializer
	       this.load_map_div = document.createElement("div");
	       this.load_map_div.style.padding = "10px 0px";
	       this.refreshMapList();

	       this.path_info_div = document.createElement("div");
	       this.path_info_div.style.padding = "15px 0px";

	       this.instructions_div = document.createElement("div");
	       this.instructions_div.innerHTML = '<p>'+_['instructions']+'&nbsp;&nbsp;&nbsp;<a href="javascript:void(0)" onclick="if(confirm(\''+_['are you sure']+'\')) { emap.clearMap(); }">'+_['clear all points']+'</a></p>';

	       this.map_dump_div = document.createElement("pre");

	       this.map_dump_attributes_div = document.createElement("span");
	       this.map_dump_body_div = document.createElement("span");
	       this.map_dump_body_div.setAttribute("id", "map_dump_body");

	       this.map_dump_div.appendChild(this.map_dump_attributes_div);
	       this.map_dump_div.appendChild(this.map_dump_body_div);
	       this.map_dump_div.appendChild(document.createTextNode('</googlemap>'));

	       this.note_div = document.createElement("div");
	       this.note_div.style.padding = "10px";
	       this.note_div.style.fontWeight = 'bold';
	       this.note_div.style.fontStyle = 'italic';
	       this.note_div.innerHTML = _['note'];

	       this.root_div = document.createElement("div");
	       this.root_div.setAttribute('id', 'mother_div');

               this.root_div.appendChild(this.search_div);

               if (this.searching_div) {
                   this.root_div.appendChild(this.searching_div);
               }
               if (this.map_table) {
                   this.root_div.appendChild(this.map_table);
               }

	       this.root_div.appendChild(this.path_info_div);
	       this.root_div.appendChild(this.map_div);
	       this.root_div.appendChild(this.getControlPanelNode());
	       this.root_div.appendChild(this.instructions_div);
	       this.root_div.appendChild(this.map_dump_div);
	       this.root_div.appendChild(this.note_div);
	       this.root_div.appendChild(this.load_map_div);
	       return this.root_div;
    },

    getControlPanelNode: function() {
		  /* of course, at some point, DOM methods are more pain than they're worth.
           That's when innerHTML comes to the rescue. (I like to think of it as the
           literal syntax for DOM objects.) */
          var control_panel_div = document.createElement("div");
		  control_panel_div.style.fontSize = "10px";
          var text_sep = '&nbsp;&nbsp;&nbsp;'+
              '&nbsp;&nbsp;&nbsp;'+
              '&nbsp;&nbsp;&nbsp;'+
              '&nbsp;&nbsp;&nbsp;';
		  var html = _['zoom control']+': '+
              this.getRadioOption('controls', 'large', _['large'])+
              this.getRadioOption('controls', 'medium', _['medium'])+
              this.getRadioOption('controls', 'small', _['small'])+
              this.getRadioOption('controls', 'none', _['no zoom control'])+
              '&nbsp;&nbsp;&nbsp;'+
              '&nbsp;&nbsp;&nbsp;'+
              '&nbsp;&nbsp;&nbsp;'+
		      _['width']+': '+
		      '<select id="select_width" onchange="emap.configureMap({\'width\':this.value})">'+
              '<option></option>';
          for(var i=50;i<=700;i+=25)
		      html += '<option value="'+i+'">'+i+'</option>';
		  html += '</select>'+
		      '&nbsp;&nbsp;&nbsp;'+
		      '&nbsp;&nbsp;&nbsp;'+
		      _['height']+': '+
		      '<select id="select_height" onchange="emap.configureMap({\'height\':this.value})">'+
              '<option></option>';
          for(var i=50;i<=600;i+=25)
		      html += '<option value="'+i+'">'+i+'</option>';
          html += '</select>';
          html += '<br />'+
              this.getControlSwitch('selector')+
              text_sep+
              this.getControlSwitch('scale')+
              text_sep+
              this.getControlSwitch('overview');
          control_panel_div.innerHTML = html;
          return control_panel_div;
                         },

    getRadioOption: function(key, value, label) {
		      return ' <input id="control_'+key+'_'+value+'" type="radio" '+
                      'name="control_'+key+'" '+
                      'onclick="this.blur()" '+
                      'onchange="emap.configureMap({\''+key+'\':\''+value+'\'});" />'+label;
                    },

    getControlSwitch: function(control) {
		      return _[control+' control']+': '+
              this.getRadioOption(control, 'yes', _['yes'])+
              this.getRadioOption(control, 'no', _['no']);
                      },

/********** Map methods ************/

// call this instead of the set* functions
// to update the printed map attributes at 
// the same time.
    configureMap: function(attrs) {
                    if (attrs.width)
                      this.setMapWidth(attrs.width);
                    if (attrs.height)
                      this.setMapHeight(attrs.height);
                    if (attrs.selector)
                      this.setControl('selector', attrs.selector);
                    if (attrs.scale)
                      this.setControl('scale', attrs.scale);
                    if (attrs.overview)
                      this.setControl('overview', attrs.overview);
                    if (attrs.controls)
                      this.setControlSize(attrs.controls);
                    if (attrs.lat && attrs.lon)
                      this.gmap.setCenter(new GLatLng(parseFloat(attrs.lat), parseFloat(attrs.lon)), attrs.zoom ? parseInt(attrs.zoom) : undefined, attrs.type ? this.translateMapNameToType(attrs.type) : undefined);
                    this.dumpMapAttributes();
                  },

// this gets called when you click the link by the toolbar
    toggleGoogleMap: function() {
		   if (this.mother_div.style.display == "") {
		     this.mother_div.style.display = "none";
		   } else {
		     this.mother_div.style.display = "";
		   }
		 },

// Intercepts clicks to the map. Click may be on a point ("overlay"),
// or not.
    clickMap: function(overlay, point) {
                if (overlay == undefined) {
                  if (this.active_path != undefined) {
                    // a new point along a path
                    var path_marker = new EditorsMarker(point, this, GME_SMALL_ICON);
                    this.addMarkerToActivePath(path_marker);
                  } else {
                    // Not along a path. This gets blown away if there is a click anywhere else.
                    var my_marker = new EditorsMarker(point, this);
                    this.newMarker(my_marker, '');
                    this.temp_marker = my_marker;
                  }
                } else if (overlay && overlay.emarker) {
                  this.active_marker = overlay.emarker;
                  overlay.emarker.openEditWindow();
                }
	      },

// Blow away everything. Or at least, get rid of the references.
// I think IE might suck at circular references, so there might
// be a memory leak here.
    clearMap: function() {
                this.gmap.clearOverlays();
                this.map_dump_body_div.innerHTML = '';
                this.singletons.reset();
                this.paths = [];
                this.dumpPaths();
	      },

/************* Path methods *************/

    addPath: function(color, fill, isPoly) {
	   var path = new EditorsPath(color, fill, this.stroke, this.gmap, this.units, isPoly);
	   this.active_path = this.paths.length;
	   this.paths[this.paths.length] = path;
	   return path;
	 },

    activatePath: function(which) {
		    this.active_path = which;
		    this.dumpPaths();
		  },

    startPath: function() {
                 this.singletons.removeMarker(this.active_marker);
                 this.addPath(this.default_color, this.default_color).addMarker(this.active_marker);
                 this.updateActiveMarker();
                 this.gmap.closeInfoWindow();
                 this.dumpPaths();
	       },

    endPath: function() {
               if (this.active_path == undefined) {
                 return;
               }

               this.pruneOneMarkerPaths();
               this.active_path = undefined;
               this.active_marker = undefined;
               this.gmap.closeInfoWindow();
               this.dumpPaths();
	     },

    pruneOneMarkerPaths: function() {
         for(var i=0;i<this.paths.length;i++) {
             if (this.paths[i] && this.paths[i].markers.length == 1) {
                 var marker = this.paths[i].markers[0];
                 this.paths[i].removeMarker(marker);
                 this.singletons.addMarker(marker);
                 this.map_dump_body_div.removeChild(this.paths[i].container);
                 this.paths[i] = undefined;
             }
         }
     },

/********** Marker methods ***********/

// Used when you "clip" a search result, get a geo-code result,
// or just click the map to make a new marker.
    newMarker: function(marker, text) {
                 this.zapTempMarker();
                 this.addMarkerToActivePath(marker);
                 marker.setCaption(text);
                 marker.openEditWindow();
               },

    addMarkerToActivePath: function(marker) {
			     this.active_marker = marker;
			     this.singletons.removeMarker(marker);
			     if (this.paths_supported && this.active_path != undefined) {
			       this.paths[this.active_path].addMarker(marker);
			       this.gmap.addOverlay(marker.gmarker);
			       this.dumpPaths();
			     } else {
			       this.singletons.addMarker(marker);
			       this.gmap.addOverlay(marker.gmarker);
			     }
			   },

// Could be removing from a path, or from the singletons
    updateActiveMarker: function() {
                            this.active_marker.search_result = false; // no more "back" button
                          // we could have a caption, or some tabs.
                          if (document.getElementById('balloon_textarea')) {
                            var caption = document.getElementById('balloon_textarea').value;
                            // we need to close the info window before setting the caption,
                            // because it gets upset when it tries to close an info window
                            // that sprung up from a now-defunct marker.
                            this.gmap.closeInfoWindow();
                            this.active_marker.setCaption(caption);
                          } else { // tabs
                            for(var i=0;document.getElementById("tab_title_"+i);i++) {
                              this.active_marker.tabs[i] = { 'title':document.getElementById("tab_title_"+i).value,
                                'content':document.getElementById("tab_content_"+i).value };
                            }
                            this.active_marker.dump();
                            this.gmap.closeInfoWindow();
                          }
                          this.temp_marker = undefined;
			},

    removeActiveMarker: function() {
        if (this.active_marker.path) {
            var path = this.active_marker.path;
            path.removeMarker(this.active_marker);
            this.zapGMarker(this.active_marker);
            this.pruneOneMarkerPaths();
        } else {
            this.singletons.removeMarker(this.active_marker);
        }
        this.dumpPaths();
        this.gmap.closeInfoWindow();
        this.zapGMarker(this.active_marker.gmarker);
    },

    removeActiveMarkerAndJumpBack: function() {
       this.removeActiveMarker();
       this.gmap.returnToSavedPosition();
    },

    zapTempMarker: function() {
		     if (this.temp_marker) {
		       this.singletons.removeMarker(this.temp_marker);
		       this.zapGMarker(this.temp_marker.gmarker);
		       this.temp_marker = undefined;
		     }
		   },

// I found myself doing these two things in the same
// order all the time, so I thought, what the hell.
    zapGMarker: function(marker) {
		  GEvent.clearInstanceListeners(marker);
		  this.gmap.removeOverlay(marker);
		},

/************** Search methods ***************/

// this is called on every keypress in the search box.
// If the user pressed "enter", kick off a search.
    findAddressIfEnter: function(e) {
                          if (e.keyCode == 13 || e.which == 13) { // keyCode => IE, which => Mozilla
                            this.findAddress();
                          }
                        },

// This is called when you click "Search". First we try to geo-code it,
// and, failing that, we do a local search.
    findAddress: function() {
	     var addr = document.getElementById('address_input').value;
             this.gmap.savePosition();
             if (this.localSearch) {
                 this.map_table.style.display = '';
             }
	     this.searching_div.style.display = ''; // "searching..."

	     // make some variables available to the closure
	     var editors_map = this;

	     if (!this.geocoder) {
		 if (this.localSearch) {
		     this.localSearch.execute(addr);
		 } 
		 return;
	     }
	     this.geocoder.getLocations(addr, function(response) {
		     if (!response || response.Status.code != 200) { // i.e., the geocode failed.
			 if (editors_map.localSearch) {
			     editors_map.localSearch.execute(addr);
			 } else {
                             editors_map.searching_div.style.display = 'none';
			     alert(_['no results']);
			 }
		     } else { // We have a geo-code!
			 editors_map.searching_div.style.display = 'none';
                         if (editors_map.map_table) {
                         editors_map.map_table.style.display = 'none';
                         }
			 var place = response.Placemark[0];
			 var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
			 editors_map.gmap.setCenter(point,
			     2 * place.AddressDetails.Accuracy + 3); // just a lazy guess on how zoomed in we should be.

			 var my_marker = new EditorsMarker(point, editors_map);
                         my_marker.search_result = true;

			 var address = '';
			 if (place.AddressDetails.Accuracy >= 6) { // take formatting into our own hands
			     var state =  place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName;
			     var city =   place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName;
			     var street = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare.ThoroughfareName;
			     address = street+"\r\n"+city+', '+state;
			 } else {
			     address = place.address;
			 }
			 editors_map.newMarker(my_marker, address);
		     }
			   });
		 },

// Called by the local search. Formats the results nice and pretty,
// and provides a link to "add marker here"
    populateResults: function() {
                       this.searching_div.style.display = 'none';
                       document.getElementById('clear_search_results').style.display = '';
                       for(var r=0; r<this.localSearch.results.length; r++) {
                         var text = '';
                         var result = this.localSearch.results[r];
                         var phone = '';
                         var result_div = document.createElement("td");
                         result_div.setAttribute("width", (100 / this.localSearch.results.length)+"%");
                         result_div.setAttribute("valign", "top");
                         for (var p=0; p < result.phoneNumbers.length; p++) {
                           if (result.phoneNumbers[p].type == "main") {
                             phone = result.phoneNumbers[p].number;
                           }
                         }
                         text += "<b>"+result.title+"</b><br />";
                         if (result.streetAddress)
                           text += result.streetAddress+"<br />";
                         text += result.city+", "+result.region+'<br />';
                         if (phone)
                           text += phone+'<br />';
                         text += '<a href="javascript:void(0)" onclick="emap.clipResult(\''+result.titleNoFormatting.replace(/'/g, '\\\'')+
                             '\', '+result.lat+', '+result.lng+')">'+_['clip result']+'</a>';
                         result_div.innerHTML = text;
                         this.local_search_results.appendChild(result_div);
                       }
                       if(!this.localSearch.results[0]) {
                         // probably shouldn't alert, but it gets attention.
                         alert(_['no results']);
                       }
                     },

// Called by the "clear search results" link. Shocking, I know.
    clearResults: function() {
		    while(this.local_search_results.hasChildNodes()) {
		      this.local_search_results.removeChild(this.local_search_results.childNodes[0]);
		    }
		    document.getElementById('clear_search_results').style.display = 'none';
		  },

    clipResult: function(title, lat, lng) {
          // this is starting to look like Java! If only there were a
          // GLatitude object and a GLongitude object that took GDoublePrecisionNumbers
          // for their constructors...
		  var my_marker = new EditorsMarker(new GLatLng(lat, lng), this);
		  this.gmap.setCenter(my_marker.getPoint());
          this.newMarker(my_marker, title);
		},

/************ Super-boring "set" methods **************/

    setControlSize: function(type) {
                      document.getElementById('control_controls_'+type).checked = true;
                      this.current_control_type = type;
                      this.gmap.removeControl(this.current_control);
                      if (this.current_control_type == "small") {
                        this.current_control = new GSmallZoomControl();
                      }
                      if (this.current_control_type == "medium") {
                        this.current_control = new GSmallMapControl();
                      }
                      if (this.current_control_type == "large") {
                        this.current_control = new GLargeMapControl();
                      }
                      if (this.current_control_type != "none") {
                        this.gmap.addControl(this.current_control);
                      }
                    },

    setControl: function(which, whether) {
              document.getElementById('control_'+which+'_'+whether).checked = true;
              this.active_controls[which] = whether;
              if (whether == "yes") {
                  this.gmap.addControl(this.controls[which]);
              } else {
                  this.gmap.removeControl(this.controls[which]);
              }
    },

    setUnitOfDistance: function(u) {
		     this.units = u;
		     this.dumpPaths();
		   },

    setMapWidth: function(width) {
           document.getElementById('select_width').value = width;
           if (width != parseInt(this.map_div.style.width)) {
             this.map_div.style.width = width+'px';
             this.gmap.checkResize();
           }
    },

    setMapHeight: function(height) {
            document.getElementById('select_height').value = height;
            if (height != parseInt(this.map_div.style.height)) {
                this.map_div.style.height = height+'px';
                this.gmap.checkResize();
            }
    },

/********* Slightly less boring "dump" methods *********/

    dumpMapAttributes: function() {
// but only those which differ from defaults
       var str = '';
       str += '&lt;googlemap'+
	       ' lat="'+this.round(this.gmap.getCenter().lat())+'"'+
	       ' lon="'+this.round(this.gmap.getCenter().lng())+'"';
       var type = this.translateMapTypeToName(this.gmap.getCurrentMapType());
       if (this.defaults.type != type)
	       str += ' type="'+type+'"';
       var zoom = this.gmap.getZoom();
       if (this.defaults.zoom != zoom)
	       str += ' zoom="'+zoom+'"';
       var width = parseInt(this.map_div.style.width);
       if (this.defaults.width != width)
	       str += ' width="'+width+'"';
       var height = parseInt(this.map_div.style.height);
       if (this.defaults.height != height)
	       str += ' height="'+height+'"';
       for(i in this.active_controls)
         if(this.defaults[i] != this.active_controls[i])
	       str += ' '+i+'="'+this.active_controls[i]+'"';
       if (this.defaults.controls != this.current_control_type)
	       str += ' controls="'+this.current_control_type+'"';
	   str += '&gt;<br />';
	   this.map_dump_attributes_div.innerHTML = str;
	 },

// a bit crude. This is called whenever any part
// of any path changes. It iterates through all
// the paths and spits out information about them
    dumpPaths: function() {
		 var do_show = false;
		 var str = '';
		 var max = 0;
		 // first, get the distances.
		 for(var p=0; p < this.paths.length; p++) {
		   if (this.paths[p]) {
                       if (this.paths[p].distance == 0) {
                           this.paths[p].updateDistance();
                       } 
                       if (this.paths[p].distance > max) {
                           max = this.paths[p].distance;
                       }
                       do_show = true;
		   }
		 }
		 if (!do_show) {
		   this.path_info_div.innerHTML = '';
		   return;
		 }
         str += '<div style="width: 450px;">';
		 for(var p=0; p<this.paths.length; p++) {
		   if (this.paths[p]) {
		     // This part lets us show the relative lengths of paths.
		     str += '<div style="' +
             'width: '+ ((max > 0 ? ((this.paths[p].distance*100)/max) : 100) - 25)+'%; '+
             'float: left; height: 4px; '+
             'font-size: 2px; '+
             'margin: 4px; '+
             'background-color: '+(this.paths[p].poly ? this.paths[p].fill_hex_color : this.paths[p].hex_color)+'; '+
             'border: 2px solid '+this.paths[p].hex_color+'; '+
             '" '+
             'id="path_'+p+'"></div>';
                     str += '<div style="float: left;">('+this.paths[p].sizeExpression()+')</div>';
		     if (p == this.active_path) {
                         str += '<div style="margin: 4px; clear: both;">'+
                             '<b>'+_['editing path']+'</b>&nbsp;&nbsp;'+
                             '<a href="javascript:void(0)" onclick="emap.endPath()">'+_['save path']+'</a>&nbsp;&nbsp;'+
                             '</div>';
                     } else {
                         str += '<div style="margin: 4px; clear: both;">'+
                             '<a onclick="javascript:emap.activatePath('+p+')" href="javascript:void(0)">'+_['edit path']+'</a> - '+
                             '<a onclick="javascript:emap.paths['+p+'].jump()" href="javascript:void(0)">'+_['show path']+'</a> - '+
                             '<a onclick="emap.paths['+p+'].colorSelector.toggle_color_select()"'+
                             ' href="javascript:void(0)" id="pick_color_'+p+'">'+_['color path']+'</a>';
                         if (this.paths[p].poly) {
                             str += ' - <a onclick="emap.paths['+p+'].colorSelectorFill.toggle_color_select()"'+
                                 ' href="javascript:void(0)" id="fill_color_'+p+'">'+_['color fill']+'</a>';
                             str += ' - <a onclick="emap.paths['+p+'].removeFill()"'+
                                 ' href="javascript:void(0)">'+_['remove fill']+'</a>&nbsp;&nbsp;';
                         } else {
                             str += ' - <a onclick="emap.paths['+p+'].addFill()"'+
                                 ' href="javascript:void(0)" id="fill_color_'+p+'">'+_['add fill']+'</a>&nbsp;&nbsp;';
                         }
                         str += '</div>';
                     }
		   }
		 }
         str += '</div>';
		 this.path_info_div.innerHTML = str;
		 this.path_info_div.style.display = '';
		 for(var p=0; p < this.paths.length; p++) {
		   if (this.paths[p] && document.getElementById('pick_color_'+p)) {
		     this.paths[p].colorSelector.attach_to_element(document.getElementById('pick_color_'+p));
                     if(this.paths[p].poly) {
                         this.paths[p].colorSelectorFill.attach_to_element(document.getElementById('fill_color_'+p));
                     }
		   }
		 }
	       },

/********** Parser methods ************/

    listMaps: function() {
                // Parse the existing article for maps that we might want to load,
                // since we're such nice guys.
                var text = this.textbox.value;
                var lines = text.split("\n");
                var existing_maps = [];
                var i = 0;
                for(var l=0; l < lines.length; l++) {
                  if (lines[l].match(/<googlemap/)) {
                    attrs = this.getXMLishAttributes(lines[l]);
                    if (attrs['name'] != undefined) {
                      existing_maps[i] = attrs['name'];
                    } else {
                      existing_maps[i] = _['map']+' #'+(i+1);
                    }
                    i++;
                  }
                }
                this.maps_in_article = i;
                if (existing_maps[0]) {
                  map_selector_html = _['load map from article']+' <select id="load_map_selector">';
                  for (var e=0; e < existing_maps.length; e++) {
                    map_selector_html += '<option value="'+(+e+1)+'">'+existing_maps[e]+"</option>";
                  }
                  map_selector_html += '</select>&nbsp;&nbsp;'+
                      '<a href="javascript:void(0)" onclick="emap.loadMap(document.getElementById(\'load_map_selector\').value)">'+
                      _['load map']+'</a>&nbsp;&nbsp;&nbsp;'+
                      '<a href="javascript:void(0)" onclick="javascript:emap.refreshMapList()">'+
                      _['refresh list']+'</a>';
                  return map_selector_html;
                }
                return _['no maps']+' <a href="javascript:void(0)" onclick="javascript:emap.refreshMapList()">'+_['refresh list']+'</a>';
	      },

    refreshMapList: function() {
		      this.load_map_div.innerHTML = this.listMaps();
		    },

// reads a <googlemap> opening tag and returns a hash of the
// attributes. Somewhat fragile, use with caution.
    getXMLishAttributes: function(line) {
       var attr_hash = {};
       var attrs = line.split(' ');
       for (var a=0; a < attrs.length; a++) {
           if (attrs[a].match(/(\w+)="(.*)"/))
               attr_hash[RegExp.$1] = RegExp.$2;
       }
       return attr_hash;
    },

    translateMapNameToType: function(type) {
        if (type == 'hybrid')
            return G_HYBRID_MAP;
        if (type == 'satellite')
            return G_SATELLITE_MAP;
        return G_NORMAL_MAP;
    },

    translateMapTypeToName: function(type) {
         if (type == G_HYBRID_MAP)
             return 'hybrid';
         if (type == G_SATELLITE_MAP)
             return 'satellite';
         return 'map';
     },

    loadMap: function(which) {
               var text = this.textbox.value;
               var lines = text.split("\n");
               var number_maps = 0;
               var map_mode = false;
               var color = undefined;
               var attrs = {};
               this.endPath(); // just in case.
               for (var l=0; l < lines.length; l++) {
                 if (lines[l].match(/^<googlemap/)) {
                   // OK, we have a map
                   number_maps++;
                   if (number_maps == which) {
                     this.clearMap();
                     map_mode = true;
                     attrs = this.getXMLishAttributes(lines[l]);
                     this.configureMap( attrs );
                   }
                 } else if (map_mode) {
                   if (lines[l].match(/^(#[0-9a-fA-F]{6})(?: \((#[0-9a-fA-F]{6})\))?/)) {
                     if (this.paths_supported) {
                         if (RegExp.$2.length) {
                             this.addPath(RegExp.$1, RegExp.$2, true);
                         } else {
                             this.addPath(RegExp.$1, RegExp.$1, false);
                         }
                     }
                   } else if (lines[l].match(/^<\/googlemap>/)) {
                     this.active_path = undefined;
                     this.dumpPaths();
                     // our work here is done.
                     return;
                   } else if (lines[l].match(/^\/(.*?)\\ *(.*)/)) {
                     this.active_marker.addTab(RegExp.$1, RegExp.$2);
                   } else { // It's a point, we hope?
                     lines[l].match(/^(?:\((.*?)\) *)?([^, ]+), *([^ ,]+)(?:, *(.+))?/);
                     var icon = RegExp.$1;
                     var lat = parseFloat(RegExp.$2);
                     var lon = parseFloat(RegExp.$3);
                     var caption = RegExp.$4;
                     if (icon && !mapIcons[icon]) { // Just-in-time icon creation
                       mapIcons[icon] = new GIcon(G_DEFAULT_ICON, this.icon_base.replace('{label}', icon));
                     }
                     if (lat && lon) {
                       var mkr = new EditorsMarker(new GLatLng(lat, lon), this, mapIcons[icon]);
                       mkr.icon_name = icon;
                       this.addMarkerToActivePath(mkr);
                       mkr.setCaption(caption);
                     }
                   }
                 }
               }
             },

/********** Math library! *********/  // <-- joke

    round: function(number) {
                  return Math.round(number * Math.pow(10, this.precision)) / Math.pow(10, this.precision);
                }
};

// These are required by color_select.js, which is a great tool,
// but rather rude javascript. I wish I just pass it a function reference.

function color_change_update(new_color, selector_id) {
  var info = colorSelectorRegistry[selector_id];
  info.path.updateColor( new_color, info.type == 'fill' );
  for( var i = 0; i <  emap.paths.length; i++ ) {
      if( emap.paths[i] == info.path ) {
          if( info.type == 'line' ) {
              document.getElementById('path_'+i).style.borderColor = new_color;
              if( !emap.paths[i].poly ) {
                  document.getElementById('path_'+i).style.backgroundColor = new_color;
              }
          }
          else {
              document.getElementById('path_'+i).style.backgroundColor = new_color;
          }
      }
  }
}

function color_hide_update(new_color, selector_id) {
  var info = colorSelectorRegistry[selector_id];
  info.path.updateColor( new_color, info.type == 'fill' );
}

