// ---- Constants ----

var LINE_OPACITY=0.8;


// ---- Global Variables ----

// the GMap2 object itself
var map = null;

// the node objects
var nodes = [];

// hostname of the node that is currently selected (clicked on)
var selectedHost = "";

// currently selected link-type
var selectedLinkType = "";


// ---- Methods ----

// Node object constructor
function Node(hostname, wired_ip, wired_mac, testbed, latitude, longitude, location, wired, sensor, mark) {
    this.hostname = hostname;
    this.wired_ip = wired_ip;
    this.wired_mac = wired_mac;
    this.testbed = testbed;
    this.latlong = new GLatLng(latitude, longitude);
    this.location = location;
    this.wired = wired;
    this.sensor = sensor;
    this.mark = mark;

    var myIcon = getIcon(this);
    this.marker = new GMarker(this.latlong, { icon:myIcon, noshadow:true });

    // hash of linktypes (string) to linksets
    // a linkset is a hash of destination nodes (string) to GPolyline objects
    this.linksets = [];

    // list of items that provide content for this node's info window; these
    // items can either be strings or functions that return strings (for dynamic
    // content)
    this.custom_content = [];

    // object methods
    this.show = function() {
        if (node.marker.isHidden()) {
            node.marker.show();
        }
    };

    this.hide = function() {
        if (!node.marker.isHidden()) {
            node.marker.hide();
        }
    };

    this.showLinks = function(linktype) {
        if (linktype in this.linksets) {
            var linkset = this.linksets[linktype];
            for (var dst in linkset) {
                linkset[dst].show();
            }
        }
    }

    this.hideLinks = function() {
        for (var linktype in this.linksets) {
            var linkset = this.linksets[linktype];
            for (var dst in linkset) {
                linkset[dst].hide();
            }
        }
    }
}

// convenience function; adds a new GPolyline link to the specified source node
// IFF the source and destination nodes both exist
function addLink(linktype, src_host, dst_host, color, weight) {
    if ((src_host in nodes) && (dst_host in nodes)) {
        var src = nodes[src_host];
        var dst = nodes[dst_host];
        if (!(linktype in src.linksets)) {
            src.linksets[linktype] = [];
        }
        var gp = new GPolyline([src.latlong, dst.latlong], color, weight, LINE_OPACITY);
        src.linksets[linktype][dst_host] = gp;
        gp.hide();  // hide() must be called before addOverlay()
        map.addOverlay(gp);
    }
}

// returns HTML content for a node's popup window
function getContent(node) {
    content = '';
    content += '<div style="font-size: 10pt">';
    content += "<b><u>" + node.hostname + "</u></b><br>";
    if (node.location.length > 0) {
        content += "&nbsp;&nbsp;<i>location:</i> " + node.location + "<br>";
    }
    content += "&nbsp;&nbsp;<i>testbed:</i> " +
        (node.testbed == "citymd" ? "indoor" : "outdoor") + "<br>";
    content += "&nbsp;&nbsp;<i>ip:</i> " + node.wired_ip + "<br>";
    content += "&nbsp;&nbsp;<i>mac:</i> " + node.wired_mac + "<br>";

    if (node.sensor) {
        content += "&nbsp;&nbsp;sensor-equipped<br>";
    }

    if (node.wired) {
        content += "&nbsp;&nbsp;wired<br>";
    }

    if (node.sensor) {
        var now = new Date();
        var start = new Date();
        start.setTime(now.getTime() - 1*24*60*60*1000);  // 1 day

        content += '<a href="http://citysense.bbn.com/DataQuery/?nodeNames=' + node.hostname +
            '&dateRange=' +
            start.getFullYear() + '-' + (start.getMonth()+1) + '-' + start.getDate() +
            "," +
            now.getFullYear() + '-' + (now.getMonth()+1) + '-' + now.getDate() +
            '" target="_blank">view sensor data</a><br>';
    }

    for (var i=0; i < node.custom_content.length; i++) {
        elt = node.custom_content[i];
        if (typeof(elt) == 'function') {
            content += elt();
        } else {
            content += elt;
        }
    }

    content += '</div>';
    return content;
}

// choose the nodes' marker icons according to testbed
function getIcon(node) {
    var icon = new GIcon(G_DEFAULT_ICON);
    //icon.iconSize = new GSize(32, 32);
    //icon.iconAnchor = new GPoint(16, 32);
    icon.shadow = null;

    if (node.testbed == "citymd") {
        icon.image = "images/icons/google/purple_Marker"+node.mark+".png";
    } else {
        icon.image = "images/icons/google/paleblue_Marker"+node.mark+".png";
    }
    return icon;
}

// tries various methods to create an XMLHttpRequest object
function getXMLHttpRequestObj() {
    try {
        // Firefox, Opera 8.0+, Safari
        return new XMLHttpRequest();
    }
    catch (e) {
        // Internet Explorer
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        }
        catch (e) {
            try {
                return new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch (e) {
                alert("Your browser does not support AJAX!");
                return false;
            }
        }
    }
}

// put a marker on the map and set event handlers
function placeNodeMarker(map, node) {
    map.addOverlay(node.marker);
    GEvent.addListener(node.marker, "click",
        function() {
            // clear any previously selected host
            if (selectedHost != "") {
                nodes[selectedHost].hideLinks();
            }
            selectedHost = node.hostname;
            node.marker.openInfoWindowHtml(getContent(node));
            node.showLinks(selectedLinkType);
        });
    GEvent.addListener(node.marker, "mouseover",
        function() {
            node.showLinks(selectedLinkType);
        });
    GEvent.addListener(node.marker, "mouseout",
        function() {
            // do not hide links if this node is the selected node
            if (selectedHost != node.hostname) {
                node.hideLinks();
            }
        });
}

// filter which nodes are shown by testbed
function filterNodesByTestbed(testbed) {
    for (var hostname in nodes) {
        node = nodes[hostname];
        if (node.testbed == 'citymd') {
            tb = 'indoor';
        } else {
            tb = 'outdoor';
        }
        if ((tb == testbed) || (testbed == 'all')) {
            node.show();
        } else {
            node.hide();
            node.hideLinks();
        }
    }
}

// choose what kind of links to show between nodes
function selectLinkType(linktype) {
    // set the linktype variable, call hideLinks() on all nodes (should only be
    // necessary for the selected node, if there is one, but to be safe we just
    // do it on everything), then call showLinks() on the select node if there
    // is one
    selectedLinkType = linktype;

    for (var hostname in nodes) {
        nodes[hostname].hideLinks();
    }

    if (selectedHost != "") {
        nodes[selectedHost].showLinks(linktype);
    }
}

// called when the page finishes loading
function load() {
    if (! GBrowserIsCompatible()) {
        alert("Your browser is not compatible with Google Maps!");
        return;
    }
    
    // set up the map in general
    map = new GMap2(document.getElementById("map"));
    // center of Harvard nodes:
    // map.setCenter(new GLatLng(42.37880461471607, -71.11757040023804), 16);
    // center of Harvard+BBN nodes:
    map.setCenter(new GLatLng(42.38006389992038, -71.1150405670166), 13);
    map.setMapType(G_SATELLITE_MAP);
    // add controls AFTER changing the map type
    map.addControl(new GLargeMapControl());
    map.addControl(new GMapTypeControl());
    map.enableScrollWheelZoom();

    // add a click event-handler so that if the user clicks somewhere on the map
    // *other than* on a node, any currently selected node is deselected
    GEvent.addListener(map, "click",
        function(overlay, latlng) {
            // latlng argument is defined only if the user did NOT click on some
            // kind of overlay object
            if (latlng) {
                if (selectedHost != "") {
                    nodes[selectedHost].hideLinks();
                    selectedHost = "";
                }
            }
        });

    // this function creates Node objects for all of the nodes that need to be
    // displayed on the map; the function itself is defined in the javascript
    // that is spit out by getnodes.cgi
    nodes = getnodes();  // hashed by hostname
    
    // now that we got all of the Node objects, we can put markers on the map
    for (var hostname in nodes) {
        placeNodeMarker(map, nodes[hostname]);
    }

    // Now we start asynchronously filling in other node parameters (presumably,
    // these parameters are all slow to fetch for some reason - e.g. they
    // require a database query - and thus we don't want to hold up show the
    // initial map view).  They way we do this is to simply put together a pair
    // of arrays, one for the URLs to call and one for the functions to process
    // the results.  Then we execute them in order.
    var ajaxUrls = [];  // the urls to request from
    var ajaxHandlers = [];  // the functions to process the result

    // *************************************************************************
    // HERE IS WHERE YOU ADD ALL AJAX COMMANDS (for brevity, recommend you do
    // not declare handler function inline; instead declare them at the end of
    // this file)
    // *************************************************************************

    // Dead Nodes (do this first since its the most visible change)
    ajaxUrls.push("/maps/gmap/deadnodes.cgi");
    ajaxHandlers.push(deadnode_handler);

    // OLSR
    ajaxUrls.push("/maps/gmap/olsr.cgi");
    ajaxHandlers.push(link_handler);

    // TCP iperf data
    ajaxUrls.push("/maps/gmap/tcp.cgi");
    ajaxHandlers.push(link_handler);

    // Ping latency data
    ajaxUrls.push("/maps/gmap/ping.cgi");
    ajaxHandlers.push(link_handler);
    
    // *************************************************************************
    // END OF AJAX COMMANDS
    // *************************************************************************

    // sanity check things
    if (ajaxUrls.length != ajaxHandlers.length) {
        return;
    }
    
    var xmlHttp = getXMLHttpRequestObj();
    
    // we cannot assign this function direction to the onreadystatechange
    // property of the XMLHttpRequest object because (for some bizarre reason)
    // if we want to reuse this object we need to RE-assign the
    // onreadystatechange property after each time we call open() -- so we
    // declare a variable to act as a handle for this function so we can use it
    // over and over again
    var stateChange = function() {
        if (xmlHttp.readyState != 4) return;
        if (xmlHttp.status != 200) return;

        process = ajaxHandlers[0];
        process(xmlHttp.responseText);
        
        // ok now we are done with this fetch; throw away the URL and
        // handler for this resource
        ajaxUrls.shift();
        ajaxHandlers.shift();
        
        // is there another AJAX request to perform?  if so, start it
        if (ajaxUrls.length > 0) {
            xmlHttp.open("GET", ajaxUrls[0], true);
            xmlHttp.onreadystatechange = stateChange;
            xmlHttp.send(null);
        }
    }

    // kick things off with the first request
    xmlHttp.open("GET", ajaxUrls[0], true);
    xmlHttp.onreadystatechange = stateChange;
    xmlHttp.send(null);
}

function deadnode_handler(input) {
    var lines = input.split("\n");
    for (var i=0; i < lines.length; i++) {
        // trim the line
        var line = lines[i].replace(/^\s+|\s+$/g, "");

        if (line.length == 0) {
            // skip blank lines
            continue;
        }

        if (line in nodes) {
            nodes[line].marker.setImage("images/icons/google/red_Marker"+nodes[line].mark+".png");
        }
    }
}

// common handler for multiple AJAX requests that all return the same format of
// data
function link_handler(input) {
    var lines = input.split("\n");
    for (var i=0; i < lines.length; i++) {
        // trim the line
        var line = lines[i].replace(/^\s+|\s+$/g, "");

        if (line.length == 0) {
            // skip blank lines
            continue;
        }

        var args = line.split("\t");
        addLink(args[0], args[1], args[2], args[3], parseFloat(args[4]));
    }
}

function debug_clear() {
    elt = document.getElementById("key_panel");
    if (elt != null) {
        elt.innerHTML = "";
    }
}

function debug_print(str) {
    elt = document.getElementById("key_panel");
    if (elt != null) {
        elt.innerHTML += str + "<br>";
    }
}

/* Constructor */
function LabeledMarker(latlng, options){
    this.latlng = latlng;
    this.labelText = options.labelText || "";
    this.labelClass = options.labelClass || "markerLabel";
    this.labelOffset = options.labelOffset || new GSize(0, 0);
    
    this.clickable = options.clickable || true;
    
    if (options.draggable) {
    	// This version of LabeledMarker doesn't support dragging.
    	options.draggable = false;
    }
    
    GMarker.apply(this, arguments);
}


/* It's a limitation of JavaScript inheritance that we can't conveniently
   extend GMarker without having to run its constructor. In order for the
   constructor to run, it requires some dummy GLatLng. */
LabeledMarker.prototype = new GMarker(new GLatLng(0, 0));


// Creates the text div that goes over the marker.
LabeledMarker.prototype.initialize = function(map) {
	// Do the GMarker constructor first.
	GMarker.prototype.initialize.apply(this, arguments);
	
	var div = document.createElement("div");
	div.className = this.labelClass;
	div.innerHTML = this.labelText;
	div.style.position = "absolute";
	map.getPane(G_MAP_MARKER_PANE).appendChild(div);

	if (this.clickable) {
		// Pass through events fired on the text div to the marker.
		var eventPassthrus = ['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'];
		for(var i = 0; i < eventPassthrus.length; i++) {
			var name = eventPassthrus[i];
			GEvent.addDomListener(div, name, newEventPassthru(this, name));
		}

		// Mouseover behaviour for the cursor.
		div.style.cursor = "pointer";
	}
	
	this.map = map;
	this.div = div;
}

function newEventPassthru(obj, event) {
	return function() { 
		GEvent.trigger(obj, event);
	};
}

// Redraw the rectangle based on the current projection and zoom level
LabeledMarker.prototype.redraw = function(force) {
	GMarker.prototype.redraw.apply(this, arguments);
	
	// We only need to do anything if the coordinate system has changed
	if (!force) return;
	
	// Calculate the DIV coordinates of two opposite corners of our bounds to
	// get the size and position of our rectangle
	var p = this.map.fromLatLngToDivPixel(this.latlng);
	var z = GOverlay.getZIndex(this.latlng.lat());
	
	// Now position our DIV based on the DIV coordinates of our bounds
	this.div.style.left = (p.x + this.labelOffset.width) + "px";
	this.div.style.top = (p.y + this.labelOffset.height) + "px";
	this.div.style.zIndex = z + 1; // in front of the marker
}

// Remove the main DIV from the map pane, destroy event handlers
LabeledMarker.prototype.remove = function() {
	GEvent.clearInstanceListeners(this.div);
	this.div.parentNode.removeChild(this.div);
	this.div = null;
	GMarker.prototype.remove.apply(this, arguments);
}
