﻿// JScript File
// Contains the main code to run the PDC Control
// This object is instantiated for each copy of the control on the page

function pdc(controlPrefix) {

// Global variables
var prefix = controlPrefix;
var mapDiv;
var busyDiv;
var myMap;
var myDataset;
var xmlHttp;
var prevZoom;
var zoomChanged;
var maxZoom = 10;
var minZoom = 5;
var showPopUps = false;
var webServiceURL;
var XMLConfigURL;
var markerList = new Object;
var arrLLPairs;
var markerFactoryList = [];
var infowindow = new google.maps.InfoWindow();
var configDone = false;
var errorLabel;
var performanceLabel;

// Performance counters
var displayPerformance = true;
var performanceResponse = "";
var timeAjax = 0;
var timeInScript = 0;
var scriptStopwatch = 0;
var ajaxStopwatch = 0;

// Trace
var trace = "";

// Now get Started
initialise();

this.reinitialise = function(){initialise();}       // Privileged Function

function initialise() {  trace += "pdc_initialise:" + new Date().getTime() + "; "; 
    prevZoom = 0;
    zoomChanged = true;
    
    mapDiv = document.getElementById(prefix + "mapCanvas");
    busyDiv = document.getElementById(prefix + "busyOverlay");
    busyDiv.style.display = 'block';
    errorLabel = document.getElementById(prefix + "txtError");
    errorLabel.style.display = 'none';
    performanceLabel = document.getElementById(prefix + "txtPerformance");
    performanceLabel.style.display = 'none';

    myDataset = document.getElementById(prefix + "txtDataset").value;
    var mapLat = parseFloat(document.getElementById(prefix + "txtInitialLat").value);
    var mapLng = parseFloat(document.getElementById(prefix + "txtInitialLng").value);
    var mapZoom = parseInt(document.getElementById(prefix + "txtInitialZoom").value);
    var additionalOptions = document.getElementById(prefix + "txtMapOptions").value;
    webServiceURL = document.getElementById(prefix + "txtWebServiceURL").value;
    XMLConfigURL = document.getElementById(prefix + "txtXMLConfigURL").value;
        
    // Get Configuration file and set up markerFactories
    var configFilename = myDataset + ".xml";
    ajaxRequestConfig(configFilename, parseConfig);
    
    //Default Map Options
    var myOptions = {
        zoom: mapZoom,
        center: new google.maps.LatLng(mapLat, mapLng),
        mapTypeId: google.maps.MapTypeId.ROADMAP
    }
    // Add configured additional options
    var addOptions = eval('(' + document.getElementById(prefix + "txtMapOptions").value + ')');
    for(optKey in addOptions) {
         myOptions[optKey] = addOptions[optKey];
    }
  
    // Set up new map
    myMap = new google.maps.Map(mapDiv, myOptions);
    
    // Attach listener on map changes
    google.maps.event.addListener(myMap, "bounds_changed", function(){doBoundsChanged();});
}

// Function to parse the Configuration details
function parseConfig(xmlHttpConfig) {   trace += "parseConfig:" + new Date().getTime() + "; "; 
    try {
        // alert("Config:: " + xmlHttpConfig.responseText);
        var r1 = xmlHttpConfig.responseXML.getElementsByTagName("pdcconfig")[0];
        var r2 = r1.getElementsByTagName("markerlist")[0].getElementsByTagName("marker");
        for (i=0; i<r2.length; i++) {
            var markerId = r2[i].attributes.getNamedItem("id").nodeValue;
            var markerParams = r2[i].childNodes[0].nodeValue;
            // alert(markerParams);
            markerFactoryList[Number(markerId)] = new RSPBMarkerFactory(eval('(' + markerParams + ')'));
        }   
//         alert(r1.toSource());

        // Get the displayPerformance marker
        var dpString = findXmlTag("displayperformance", xmlHttpConfig.responseText);
        if (dpString.toLowerCase() == "true") {
            displayPerformance = true; 
        }
        else {
            displayPerformance = false; 
        }
        // Min/Max Zoom Levels,  Show Popups
        var zoom = findXmlTag("minzoom", xmlHttpConfig.responseText);
        if (zoom != "") {minZoom = Number(zoom);}
        zoom = findXmlTag("maxzoom", xmlHttpConfig.responseText);
        if (zoom != "") {maxZoom = Number(zoom);}
        var popups = findXmlTag("showpopups", xmlHttpConfig.responseText);
        if (popups.toLowerCase() == "true") {showPopUps = true;}
        
        configDone = true;
    }
    catch (err) {
        // Try to log error if faulty Config file
        if (err.description) {
            ajaxLogError("Error reading Configuration details: " + err.description);
           }
        else {
            ajaxLogError("Error reading Configuration details: No description available");
        }
        errorLabel.style.display = 'block';
        errorLabel.innerHTML = "Sorry, there has been an error reading the map configuration details.";
    }
}

// Function called whenever bounds changed.  Decides if necessary to get new markers
function doBoundsChanged() {    trace += "doBoundsChanged:" + new Date().getTime() + "; "; 
    if (configDone) {
        if (displayPerformance) {scriptStopwatch = new Date().getTime() ; timeInScript = 0;}
        errorLabel.style.display = 'none';
        busyDiv.style.display = 'block';

        // Restrain Zoom level within bounds       
        var zoom = myMap.getZoom();
        if (zoom > maxZoom) {myMap.setZoom(maxZoom); return;}
        if (zoom < minZoom) {myMap.setZoom(minZoom); return;}

        // alert("Lat:" + myMap.get_center().lat() + "  Lng:" + myMap.get_center().lng() + "  Zoom:" + myMap.get_zoom());
        var myBounds = myMap.getBounds();
        var sw = myBounds.getSouthWest();
        var ne = myBounds.getNorthEast();
        if (zoom != prevZoom) {
            zoomChanged = true;
            prevZoom = zoom;
        }
        // alert(sw.lat() + "  " + ne.lat() + "  " +  sw.lng() + "  " +  ne.lng());
        
        // Kill the display of any existing set of markers
        // First Abort any operation currently in progress
        if (xmlHttp) xmlHttp.abort();
        arrLLPairs = [];
        if (displayPerformance) {ajaxStopwatch = new Date().getTime() ; timeAjax = 0;}
        xmlHttp = ajaxRequestMarkers(myDataset, sw.lat(), ne.lat(), sw.lng(), ne.lng(), zoom, displayResponse);
    }
    else {
        // Try again after short delay
        window.setTimeout(function() {doBoundsChanged()}, 100);
    }
}

function displayResponse(xmlHttp) {   trace += "displayResponse:" + xmlHttp.readyState + ":" + new Date().getTime() + "; "; 
// Decodes the AJAX response, and puts markers on map.  
    try {
        if (xmlHttp.responseText.length > 0) {

            // alert(xmlHttp.responseText);
            if (displayPerformance) timeAjax += (new Date().getTime() - ajaxStopwatch); 
            
            // Decode the response string here
            // Strip the XML by hand.  Using the built-in decoder blows up in IE6
            var txt = xmlHttp.responseText.toLowerCase();
            var index1 = txt.indexOf("<markerlist>");
            
            // Get the list of markers
            var responseBody = findXmlTag("markerList", xmlHttp.responseText);
            // Get the Performance details
            performanceResponse = findXmlTag("performanceDetails", xmlHttp.responseText);
            
            arrLLPairs = [];                 // Array of Lat, Lng pairs
              
            // Clear existing markers if the Zoom level has changed
            // ********** Expect there to be a method such as myMap.clear_overlays();
            // but maybe API V3 has not implemented it yet.  
            // So instead:
            if (zoomChanged) {
                for (m in markerList) {
                    markerList[m][0].setMap(null);
                }
                infowindow.close();
                markerList = new Object;
                zoomChanged = false;
            }

            // Draw new markers
            arrLLPairs = [];
            if (responseBody) {
                if (responseBody.length > 0) {
                    arrLLPairs = responseBody.split("|");
                }
            }
            drawMarkers();
        }
    }
    catch (err) {
        // Try to log error if faulty Response
        if (err.description) {
            ajaxLogError("Error reading Marker details: " + err.description);
           }
        else {
            ajaxLogError("Error reading Marker details: No description available");
        }
        errorLabel.style.display = 'block';
        errorLabel.innerHTML = "Sorry, there has been an error obtaining the marker information";
    }
}

// Recursive function to set up Markers
// with timeout interspersed to allow repainting to happen
function drawMarkers() {    trace += "drawMarkers:" + new Date().getTime() + "; "; 

    var arrLL;          // Holds a single pair of Lat/Lng
    if (arrLLPairs.length > 0) {
        // shift off marker and draw it
        arrLL = arrLLPairs.shift().split(",");
        // If marker does not already exist
        if (!(markerList[arrLL[0] + arrLL[1]])) {
            // Create Marker and add to map
            var point = new google.maps.LatLng(Number(arrLL[0]), Number(arrLL[1]));   
            var marker;
            var markertype = Number(arrLL[2]);
            var info = arrLL[3];
            
            // NOTE: The text arrives HTMLEncoded.  This is a bit of a problem with odd characters (e.g. < >) in Tooltips
            // but necessary for PopUps.  Have to live with restrictions on tooltips.
            
            marker = markerFactoryList[markertype].makeMarker({lat:Number(arrLL[0]), lng:Number(arrLL[1]), label:info});
            marker.setMap(myMap);

            // Add Event Listener 
            // If Cluster Marker then centre and zoom map on marker
            if (markertype > 0) {
                google.maps.event.addListener(marker, 'click', function() {
                    myMap.setCenter(marker.getPosition());
                    var zoom = myMap.getZoom();
                    if (zoom < maxZoom) {myMap.setZoom(zoom+1);}
                });
            }
            else {
                // If Single Marker, and Popups allowed, then show info in popup
                if (showPopUps) {
                    google.maps.event.addListener(marker, 'click', function() {
                        var position = marker.getPosition();                 
                        var markerKey = String(position.lat()) + String(position.lng());            
                        infowindow.setContent(markerList[markerKey][1]);
                        infowindow.open(myMap, marker);
                    });
                }
            }
            
            // Keep so (a) can retrieve the Info field, and (b) can clear it later
            var markerListEntry = [marker, info];
            markerList[arrLL[0] + arrLL[1]] = markerListEntry;           
        }
        
        // Recursively call self to do next marker,
        // ... after a timeout to let the first one be drawn.        
        window.setTimeout(function() {drawMarkers()}, 0);
    }
    else {
    // finish off action if required
        if (displayPerformance) {
            timeInScript += (new Date().getTime() - scriptStopwatch);
            performanceLabel.style.display = 'block';
            performanceLabel.innerHTML = performanceResponse + ",<br/>AJAXTime: " + timeAjax + ", ScriptTime: " + timeInScript;
        }
        // Turn off the busy indicator
        busyDiv.style.display = 'none';
        
//      Print trace string if required          
//        errorLabel.style.display = 'block';
//        errorLabel.innerHTML = trace;
        
    }
}

// Function to find content of simple, loweset level XML tag in XML coded string.
// Used because standard XML Object routines seem not to be reliable in IE6
// Returns full content of the tag, or empty string if not found.
// Only finds first instance in the string supplied.
// Won't cope with tags that have attributes
function findXmlTag(tag, str) {
    // Test for empty tag -- <tag/>
    var regExp = eval("/<\\s*" + tag + "\\s*\\/>/i");
    var a = (str.search(regExp));
    if (a >= 0) return "";              // Empty tag found

    // else look for opening tag itself
    regExp = eval("/<\\s*" + tag + "\\s*>/i");
    var b = (str.search(regExp));
    str = str.substring(b);             // Throw away everything before the opening tag
    // Look for end of the opening tag
    var c = (str.search(/>/));
    str = str.substring(c+1);           // Throw away the tag itself
    // Look for the closing tag
    regExp = eval("/<\\/\\s*" + tag + "\\s*>/i");
    var d = (str.search(regExp));
    str = str.substring(0, d);          // Throw away the rest of the string

    return str;
}

///////////////////////////////////////////////////////////////////////////
// AJAX Routines to get the Marker detail information, etc

// Get hold of AJAX Request object
function createXMLHttpRequest() {
   try { return new XMLHttpRequest(); } catch(e) {}
   try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
   try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
   alert("Sorry, but your browser does not support the necessary AJAX capability to show this map");
   return null;
}

// Formats the request parameters and sends Ajax request to Server 
// dataset      reference to the configuration file which defines parameters of this dataset
// minLat etc.  minimum and maximum Latitude and Longitude of window required
// zoom         Zoom level to select markers for.
// callback     Function to callback with response
function ajaxRequestMarkers(dataset, minLat, maxLat, minLng, maxLng, zoom, callback) { 
    var xmlHttp;    
    xmlHttp = createXMLHttpRequest();

    var url = webServiceURL + "RSPBMapService.asmx/getClusterMarkers";
    var body = "dataset=" + dataset + "&minLat=" + minLat + "&maxLat=" + maxLat;
    body += "&minLng=" + minLng + "&maxLng=" + maxLng;
    body += "&zoom=" + zoom;
    
    xmlHttp.open("POST",url,true);                  // Must be before onreadystatechange for IE6
    xmlHttp.onreadystatechange = function() { 
        if (xmlHttp.readyState==4 && xmlHttp.status == 200) {
            callback(xmlHttp); 
        }
    };

    //Send the proper header information along with the request
    xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xmlHttp.setRequestHeader("Content-length", body.length);
    xmlHttp.setRequestHeader("Connection", "close");

    xmlHttp.send(body);
    
    // Return the xmlHttpObject to allow ait to be aborted before re-calling the routine
    return xmlHttp;
}

// Fetches the XML configuration file from the server
// filename     File to fetch
// callback     Function to callback with response
function ajaxRequestConfig (filename, callback) {  
    var xmlHttpConfig = createXMLHttpRequest();
    var url = XMLConfigURL + filename;    
    xmlHttpConfig.open("GET",url,true);     
    xmlHttpConfig.onreadystatechange = function() { 
        if (xmlHttpConfig.readyState==4 && xmlHttpConfig.status == 200) {
            callback(xmlHttpConfig); 
        }
    };
 
    xmlHttpConfig.send(null);
}

// Reports an error message to the Server - assuming this is possible
function ajaxLogError(message) {
    var xmlHttpLogging = createXMLHttpRequest();        
    var url = webServiceURL + "RSPBMapService.asmx/logClientError";
    xmlHttpLogging.open("POST",url,true);
    xmlHttpLogging.onreadystatechange = function() {};      // Ignore any response
    
    var body = "message=" + message;
    
    //Send the proper header information along with the request
    xmlHttpLogging.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    xmlHttpLogging.setRequestHeader("Content-length", body.length);
    xmlHttpLogging.setRequestHeader("Connection", "close");

    xmlHttpLogging.send(body);
}

}

