User:George Drummond/EditorsMap js

From MediaWiki.org
Jump to: navigation, search
THIS IS ONLY A PROTOTYPE, FEATURES DO NOT WORK
 // almost-100% GUI Google Maps Builder for MediaWiki.
// Copyright 2006 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 = { 'meters':1, 'miles': 100 / 2.54 / 12 / 5280 };
var decimal_places = { 'meters':0, 'miles':2 };

// 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(gmarker, emap) {
                                  // one day I'm going to buy a leather jacket that says
                                  // "F*ck the DOM" on the back of it, and wear it in public.
                                  this.container = document.createElement("span");
                                  this.container.appendChild(document.createTextNode(emap.round(gmarker.getPoint().lat())+', '+emap.round(gmarker.getPoint().lng())));
                                  this.container.appendChild(document.createElement("br"));
                                  this.gmarker = gmarker;
                                  this.gmarker.emarker = this;
                                  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();
                },
        
        setTitle: function(title) {
                  this.title = title;
                  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.title){
                          line += ', '+this.title;
                        }
                        if (this.caption){
                          line += '\n'+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.incoming_polyline) {
          var prev = this.incoming_polyline.getVertex(0);
          this.path.distance -= prev.distanceFrom(this.incoming_polyline.getVertex(1));
          this.path.distance += prev.distanceFrom(this.getPoint());
          this.path.gmap.removeOverlay(this.incoming_polyline);
          var newpline = new GPolyline([ prev, this.getPoint() ],
                  this.path.color, this.path.weight, this.path.opacity);
          this.incoming_polyline = newpline;
          this.previous_marker.outgoing_polyline = newpline;
          this.path.gmap.addOverlay(newpline);
          }
          if (this.outgoing_polyline) {
          var nex = this.outgoing_polyline.getVertex(1);
          this.path.distance -= nex.distanceFrom(this.outgoing_polyline.getVertex(0));
          this.path.distance += nex.distanceFrom(this.getPoint());
          this.path.gmap.removeOverlay(this.outgoing_polyline);
          var nextpline = new GPolyline([ this.getPoint(), nex ],
                  this.path.color, this.path.weight, this.path.opacity);
          this.outgoing_polyline = nextpline;
          this.next_marker.incoming_polyline = nextpline;
          this.path.gmap.addOverlay(nextpline);
          }
          this.dump();
          this.emap.dumpPaths();
        },

                        // we need this logic in a couple places, so might as well put it here.
        getBalloonFooter: function() {
          var message = '<a href="javascript:emap.updateActiveMarker()">'+_['save point']+'</a>'+
          '  <a href="javascript:emap.removeActiveMarker()">'+_['remove']+'</a>';
          if (GME_PATHS_SUPPORTED && this.path == undefined) {
          message += '  <a href="javascript: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(t in this.tabs) {
                  tabs[t] = new GInfoWindowTab(_['tab']+' '+(parseInt(t)+1), _['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());
          }
          this.gmarker.openInfoWindowTabsHtml(tabs);
          } else { /* CODE FOR TITLE BOX */
          this.gmarker.openInfoWindowHtml( 'Title:<br /><input id="balloon_title" value="'
                   +((this.title == undefined) ? '' : this.title)+
                   '" /><br />'+_['make marker']+
                  '<br /><textarea id="balloon_textarea" class="balloon_textarea">'+
                  ((this.caption == undefined) ? '' : this.caption)+
                  '</textarea><br />'+this.getBalloonFooter(),
                  { maxWidth:270 });
          }
        }
};

// 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, map, units) {
                  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);
                  this.setColor(color);
                  this.gmap = map;
                  this.units = units;
                  this.weight = 6;
                  this.size = 0;
                  this.distance = 0;
                },

        addMarker: function(marker) {
  // Add mutual references
                 var polyline;
                 marker.path = this;
                 if (this.head != undefined) {
                   this.distance += this.head.distanceFrom(marker);
                   // add the polyline
                   polyline = new GPolyline([ this.head.getPoint(), marker.getPoint() ], this.color, this.weight, this.opacity);
                   this.head.outgoing_polyline = polyline;
                   marker.incoming_polyline = polyline;
                   marker.previous_marker = this.head;
                   this.head.next_marker = marker;

                   this.gmap.addOverlay(polyline);
                 }
                 this.head = marker;
                 this.size++;
                 this.container.appendChild(marker.container);
//           this.gmap.addOverlay(marker.gmarker);
           },

// 1. remove the marker's references
// 2. update its reference's references
// 3. recreate polylines
// 4. update distance
// 5. Alles
        removeMarker: function(doomed_marker) {
                var p = this.head;
                while(p) {
                  if (p == doomed_marker) {
                        if (p.previous_marker && p.next_marker) { // from the middle
                                this.distance = this.distance -
                                        p.distanceFrom(p.previous_marker)-
                                        p.distanceFrom(p.next_marker) +
                                        p.previous_marker.distanceFrom(p.next_marker);
                                p.previous_marker.next_marker = p.next_marker;
                                p.next_marker.previous_marker = p.previous_marker;
                                this.gmap.removeOverlay(p.outgoing_polyline);
                                this.gmap.removeOverlay(p.incoming_polyline);
                                var newpline = new GPolyline([ p.previous_marker.getPoint(), p.next_marker.getPoint() ],
                                                this.color, this.weight, this.opacity);
                                p.previous_marker.outgoing_polyline = newpline;
                                p.next_marker.incoming_polyline = newpline;
                                this.gmap.addOverlay(newpline);
                        } else if (p.previous_marker && !p.next_marker) { // the head
                          this.distance -= p.distanceFrom(p.previous_marker);
                          this.head = p.previous_marker;
                          p.previous_marker.outgoing_polyline = undefined;
                          p.previous_marker.next_marker = undefined;
                          this.gmap.removeOverlay(doomed_marker.incoming_polyline);
                        } else if (!p.previous_marker && p.next_marker) { // the tail
                          this.distance -= p.distanceFrom(p.next_marker);
                          p.next_marker.previous_marker = undefined;
                          p.next_marker.incoming_polyline = undefined;
                          this.gmap.removeOverlay(p.outgoing_polyline);
                        }
                  }
                  p = p.previous_marker;
                }
                doomed_marker.previous_marker = undefined;
                doomed_marker.next_marker = undefined;
                doomed_marker.path = undefined;
                this.size--;
                                this.container.removeChild(doomed_marker.container);
                return doomed_marker.gmarker;
                  },

        distanceExpression: function() {
                          return Math.round(this.distance *
                                  conversion_factor[this.units] *
                                  Math.pow(10, decimal_places[this.units])) /
                          Math.pow(10, decimal_places[this.units])+' '+_[this.units];
                        },

        setColor: function(color) {
                  this.hex_color = color;
                  var normalized = this.hex2alpha(color);
                  this.color = normalized[0];
                  this.opacity = normalized[1];
                  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...

// Aren't you amazed how short this function is? I am,
// considering this is JavaScript.
        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(i in rgb) {
                                   if (rgb[i] < min) {
                                         min = rgb[i];
                                   }
                                 }
                                 for(i in rgb) {
                                   // 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 ] );
                           },

// Here we walk along the EditorsPath and update each segment.
        updateColor: function(new_color) {
                                   this.setColor(new_color);
                                   var m = this.head;
                                   while(m) {
                                         if (m.incoming_polyline) {
                                           var newpline = new GPolyline([ m.incoming_polyline.getVertex(0), m.incoming_polyline.getVertex(1) ],
                                                           this.color, this.weight, this.opacity);
                                           this.gmap.removeOverlay(m.incoming_polyline);
                                           this.gmap.addOverlay(newpline);
                                           m.incoming_polyline = newpline;
                                           m.previous_marker.outgoing_polyline = newpline;
                                         }
                                         m = m.previous_marker;
                                   }
                                 }
};

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.") {
                var message_div = document.createElement('div');
                message_div.innerHTML = _['no editor'];
                document.getElementById(options.container).appendChild(message_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.paths = new Array();
        this.units = options.units;
        this.toggle_link = document.getElementById(options.toggle);
        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.toggle_link.style.display = "";

        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.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)" />   '+
                           '<a href="javascript:emap.findAddress();">'+_['search']+'</a>   '+
                           '<a href="javascript: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']+'   <a href="javascript:if(confirm(\''+_['are you sure']+'\')) { emap.clearMap(); }">'+_['clear all points']+'</a></p>';

                   this.map_dump_div = document.createElement("pre");
                   //this.map_dump_div.setAttribute('id', 'map_dump');
                   //this.map_dump_div.style.padding = "0px 10px";
                   //this.map_dump_div.style.fontFamily = "Courier";

                   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 = '   '+
                          '   '+
                          '   '+
                          '   ';
                  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'])+
                          '   '+
                          '   '+
                          '   '+
                          _['width']+': '+
                          '<select id="select_width" onchange="emap.configureMap({\'width\':this.value})">'+
                          '<option></option>';
                  for(i=50;i<=700;i+=25)
                          html += '<option value="'+i+'">'+i+'</option>';
                  html += '</select>'+
                          '   '+
                          '   '+
                          _['height']+': '+
                          '<select id="select_height" onchange="emap.configureMap({\'height\':this.value})">'+
                          '<option></option>';
                  for(i=50;i<=600;i+=25)
                          html += '<option value="'+i+'">'+i+'</option>';
                  html += '</select>'+
                          '<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";
                         this.toggle_link.innerHTML = _['make map'];
                   } else {
                         this.mother_div.style.display = "";
                         this.toggle_link.innerHTML = _['hide map'];
                   }
                 },

// 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(new GMarker(point, { 'icon':GME_SMALL_ICON, 'draggable':true }), this);
                                        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(new GMarker(point, { 'draggable':true }), 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. Fancy that.
        clearMap: function() {
                                this.gmap.clearOverlays();
                                this.map_dump_body_div.innerHTML = '';
                                this.singletons.reset();
                                this.paths = [];
                                this.dumpPaths();
                  },

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

        addPath: function(color) {
           var path = new EditorsPath(color, this.gmap, this.units);
           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).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].size == 1) {
                                   var marker = this.paths[i].head;
                                   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() {
                                                  // we could have a title, title & caption, or some tabs.
                                                  if (document.getElementById('balloon_title')) {
                                                         var title = document.getElementById('balloon_title').value;
                                                         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);
                                                        this.active_marker.setTitle(title);

                                                  } else { // tabs
                                                        for(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.gmap.closeInfoWindow();
                                                  }
                                                  this.temp_marker = undefined;
                        },

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

        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;
                         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(new GMarker(point, { 'draggable':true }), editors_map);

                         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(r in this.localSearch.results) {
                                                 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 (p in result.phoneNumbers) {
                                                   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: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 GMarker(new GLatLng(lat, lng), { 'draggable':true }), 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 += '<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 += '><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 > 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]) {
                         if (p == this.active_path) {
                   str += '<div style="margin: 4px; clear: both;">'+
                           '('+this.paths[p].distanceExpression()+') '+
                           '<b>'+_['editing path']+'</b>  '+
                           '<a href="javascript:emap.endPath()">'+_['save path']+'</a>  '+
                           '</div>';
                         } else {
                           str += '<div style="margin: 4px; clear: both;">'+
                           '('+this.paths[p].distanceExpression()+') '+
                           '<a href="javascript:emap.activatePath('+p+')">'+_['edit path']+'</a>  '+
                           '<a onclick="color_selects['+p+'].toggle_color_select()"'+
                           ' href="javascript:void(0)" id="pick_color_'+p+'">'+_['color path']+'</a>  '+
                           '</div>';
                         }
                         // This part lets us show the relative lengths of paths.
                         str += '<div style="' +
                         'width: '+ (max > 0 ? ((this.paths[p].distance*100)/max) : '100')+'%; '+
                         'float: left; height: 6px; '+
                         'font-size: 2px; '+
                         'margin: 4px; '+
                         'background-color: '+this.paths[p].hex_color+';" '+
                         'id="path_'+p+'"></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));
                   }
                 }
                   },

/********** 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(l in lines) {
                                  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 (e in existing_maps) {
                                        map_selector_html += '<option value="'+(+e+1)+'">'+existing_maps[e]+"</option>";
                                  }
                                  map_selector_html += '</select>  '+
                                          '<a href="javascript:emap.loadMap(document.getElementById(\'load_map_selector\').value)">'+
                                          _['load map']+'</a>   '+
                                          '<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 (a in attrs) {
                                 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 (l in lines) {
                                 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(/^#/)) {
                                         if (this.paths_supported) {
                                           color = lines[l].substring(0, 7);
                                           this.addPath(color);
                                         }
                                   } 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 GMarker(new GLatLng(lat, lon),
                                                                   { 'draggable':true, 'icon':mapIcons[icon] }), this);
                                           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 id = selector_id.match(/(\d+)/);
  id = RegExp.$1;
  if (document.getElementById('path_'+id)) {
        document.getElementById('path_'+id).style.backgroundColor = new_color;
  }
}

function color_hide_update(new_color, selector_id) {
  var id = selector_id.match(/(\d+)/);
  id = RegExp.$1;
  emap.paths[id].updateColor(new_color);
}

Personal tools
Namespaces

Variants
Actions
Navigation
Support
Download
Development
Communication
Print/export
Toolbox