/**
 *  Wayfinder UI functions
 *
 *  Client-side functionality for NSCC wayfinder service.
 *  Uses Google map, so be sure to have that API library loaded
 *  and parsed prior to this one.
 *
 *  changelog
 *  + 2009/01/09 Fixed: Safari 3.x error using room# input field.
 *               Added: XMLHttpRequest factory object & refactored.
 *               Appended 'XMLHttpRequest' to User-Agent header for stats.
 *  + 2008/05/27 Clears markers and InfoWindow on search, button-use, and 404;
 *               clears room search-field when either a named-location button is
 *               used or the field is given focus; search-field focused on 404;
 *               room number displayed (if apropos) in room search-field upon
 *               successful map update; and added permalink generator for current
 *               map state.
 *  + 2008/05/23 Related named-location links grouped into accordian-type submenus.
 *  + 2007/09/01 Created.
 *
 *  @author Michael Vellines <mvellines@sccd.ctc.edu>
 *  @version 2009/01/09
 *
 *  @todo Replace alert() with more elegant solution for not-found location messages
 */
 
var gMap;  // Google Map instance.
var compatMap = false; // will this browser work with Google Map?
var compatAjax = false; // does this browser have Ajax capability?
var campusMiddle = new GLatLng(47.699466, -122.333005);
var infoWinOpts = {'maxWidth': '200'};
var infoWinHtml = '';
var mapOverlays = {};  // GMap overlays
var markerOverlay;  // GMap location marker overlay.


// Cross-platform XMLHttpRequest factory.
var XHR = {
  '_xhr' : null,
  '_factory' : [
    function() { return new ActiveXObject('Msxml2.XMLHTTP'); },
    function() { return new ActiveXObject('Microsoft.XMLHTTP'); },
    function() { return new XMLHttpRequest(); }
  ],
  'newRequest' : function() {
    if (XHR._xhr != null) return XHR._xhr();
    for (var i = 0; i < XHR._factory.length; i++) {
      try {
        var fac= XHR._factory[i];
        var req = fac();
        if (req != null) {
          XHR._xhr = fac;
          return req;
        }
      } catch(ex) { continue; }
    }
      
    // No viable factory if we get here.
    XHR._xhr = function() {
      compatAjax = false;
      throw new Error('XMLHttpRequest not supported');
    }
    XHR._xhr();  // throw error.
  }
};



// Ensures all prior page-load handlers run plus the specifed one.
function addLoadEvent(fn) {
  var old = window.onload;
  if(typeof window.onload != 'function') {
    window.onload = fn;
  } else {
    window.onload = function() {
      old();
      fn();
    }
  }
}



// Ensures all prior page-unload handlers run plus the specified one.
function addUnloadEvent(fn) {
  var old = window.onunload;
  if(typeof window.onunload != 'function') {
    window.onunload = fn;
  } else {
    window.onunload = function() {
      old();
      fn();
    }
  }
}



//  Attaches submit-handler to input form and click-handlers to named-location
//  links, then either loads a Google Map centered on the NSCC campus in hybrid-mode
//  (or a notice about browser incompatibility) into the map container.
//  If location data was sent with the page, the map (or text-info) will be updated
//  based on the data.
function initWayfinder(e) {

  // Attach click-handlers to location-category headings,
  // and hide the associated locations submenu.
  var subMenuHeadings = document.getElementsByTagName('h4');
  for(var i = 0; i < subMenuHeadings.length; i++) {
    var heading = subMenuHeadings[i];
    if(! heading.id) { continue; }
    heading.onclick = toggleSubMenu;
    var subMenu = document.getElementById(heading.id + 'List');
    subMenu.className = 'hiddenMenu';
    heading.style.borderBottom = 'solid 1px';
  }
  
  // Ajax compatible browser?
  if(typeof(XMLHttpRequest) != 'undefined'
    || typeof(ActiveXObject) != 'undefined') compatAjax = true;
  
  // Is UA compatible with the Google Map?
  if (GBrowserIsCompatible()) {
    compatMap = true;
    mapOverlays.bldgs = getBldgOverlay();
    setMapHeight();
    gMap = new GMap2(document.getElementById('map'));
    gMap.enableContinuousZoom();
//    gMap.enableScrollWheelZoom();
    gMap.addControl(new GLargeMapControl());
    gMap.addControl(new GMapTypeControl());
    gMap.addControl(new GScaleControl());
    gMap.addControl(new GOverviewMapControl());
    gMap.setCenter(campusMiddle, 16, G_HYBRID_MAP);
    gMap.addOverlay(mapOverlays.bldgs);
    
    // Display a location? (loc would be defined in script-block on page)
    if(typeof(window.loc) != 'undefined') { updateMap(loc); }
  }
  
  // Otherwise, display text-only information.
  else {
    var noMap = document.getElementById('map');
    var info;
    
    // Display location info?
    if(typeof(window.loc) != 'undefined') {
      info = makeTextLocation(loc);
    } else {
      info = '<h3>Map Not Available</h3><p>The NSCC Wayfinder application '
           + 'uses a Google Map to display locations, but your browser '
           + 'does not appear to be compatible with the Google Map service. '
           + 'Therefore, you will not be able to see map locations; instead, '
           + 'you will receive text information for requested locations.</p>';
    }
    noMap.innerHTML = info;
  }
  
  // Update hidden form field to indicate preferred means of response.
  var field = document.getElementById('viaField');
  field.value = compatAjax ? '2' : '1'; // server constant-values
  
  // Attach click-handlers and preferred means of response parameters to named-room links.
  var nameLinks = document.getElementsByTagName('a');
  for(var i = 0; i < nameLinks.length; i++) {
    var link = nameLinks[i];
//    if(link.href.indexOf('?loc=') < 0) { continue; }  // skip other links.
    if(link.href.indexOf('?loc=') < 0 && link.href.indexOf('?bldg=') < 0) { continue; }
    link.href += compatAjax ? '&via=2' : '&via=1';  // server constant-values
    link.onclick = getLocData;    
  }
  
  // Attach resize handler to change height of map to fit window.
  window.onresize = setMapHeight;
  
  // Attach focus-handler to room search-field.
  document.getElementById('room').onfocus = function() { this.value = ''; }

  // Attach submit-handler to room-search form.
  var form = document.getElementById('wayfinderForm');
  form.onsubmit = getLocData;
  form.room.focus();

  return true;
}



// Toggle display of specific location-category submenu.
function toggleSubMenu(e) {
  // Retrieve event (IE only).
  if(!e) e = window.event;
  if(e.type == 'click') {
    var category;
    if(e.currentTarget) {
      category = (e.currentTarget.nodeType == 3 || e.currentTarget.nodeType == 4)
               ? e.parentNode.currentTarget.id
               : e.currentTarget.id;
    } else if(e.srcElement) { category = e.srcElement.id; }
    var heading = document.getElementById(category);
    var subMenu = document.getElementById(category + 'List');
    if(subMenu.className == 'hiddenMenu') {
      heading.style.borderBottomColor = 'rgb(220, 220, 220)';
      subMenu.className = '';
    } else {
      subMenu.className = 'hiddenMenu';
      heading.style.borderBottomColor = 'rgb(0, 51, 102)';
    }
  }
  return true;
}



//  Handle request to find specified location.
function getLocData(e) {
  // Retrieve event (IE only).
  if(!e) e = window.event;
  
  // Clear map display as needed.
  if(gMap) {
    if(! gMap.getInfoWindow().isHidden()) { gMap.closeInfoWindow(); }
    if(mapOverlays.loc) gMap.removeOverlay(mapOverlays.loc);
    if(mapOverlays.marker) gMap.removeOverlay(mapOverlays.marker);
//    gMap.clearOverlays();
  }
  
  // Non-Ajax browser? Don't bother with fancy stuff.
  if(! compatAjax) return true; 
  
  // Prevent default behavior.
  if(e.preventDefault) e.preventDefault();
  else e.returnValue = false;
  
  // Searching for a room number?
  if(e.type == 'submit') {
    queryServerByRoom();
  
  // Or, requesting by name?
  } else if(e.type == 'click') {
    // Clear the room-search field.
    var field = document.getElementById('room').value = ''; 
    
    // Get the href value of the event-generating element.
    var url;
    if (e.currentTarget) {
      // Safari and Konqueror sometimes reference text and cdata node instead of <a>.
      url = (e.currentTarget.nodeType == 3 || e.currentTarget.nodeType == 4)
          ? e.parentNode.currentTarget.href
          : e.currentTarget.href;  
    // Naturally, IE must do it differently.
    } else if (e.srcElement) { url = getHrefForIE(e.srcElement); }
    // Send request via Ajax.
    queryServerByLocation(url);
  }
  return false;
}



//  Request data for the specified room from the server via Ajax.
function queryServerByRoom() {
  // URL-encode and serialize the form fields.
  var roomVal = document.getElementById('room').value;
  var encVal = encodeURIComponent(roomVal).replace(/%20/g, '+')
  var params = 'via=' + document.getElementById('viaField').value + '&room=' + encVal;
  var ua = navigator.userAgent + ' XMLHttpRequest'; // for stats tracking
  
  // Get a new XHR and send form data via POST.
  msgr = XHR.newRequest();
  msgr.open('post', '/maps/wayfinder.php', true);
  msgr.onreadystatechange = ajax_listener;
  msgr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  msgr.setRequestHeader('User-Agent', ua);
  msgr.send(params);
  // TODO: Advise user that request has been made and we are awaiting server response.
  
  return null;
}



//  Request data for the specified location from the server via Ajax.
function queryServerByLocation(url) {
  if(! url) { return false; }
  var ua = navigator.userAgent + ' XMLHttpRequest'; // for stats tracking

  // Make a new XHR and send a GET request.
  msgr = XHR.newRequest();
  msgr.open('get', url, true);
  msgr.onreadystatechange = ajax_listener;
  msgr.setRequestHeader('User-Agent', ua);
  msgr.send(null);  
  // TODO: Advise user that request has been made and we are awaiting server response.
  
  return null;
}



//  AJAX response handler.
function ajax_listener() {
  var loc;  // Ref to parsed location data.

  // Advise user of response status.
  switch(msgr.readyState) {
    case 1 :
      break;
    case 2 :
      break;
    case 3 :
      break;
    case 4 :
      if(msgr.status == 404) {
        updateMap(false);
      } else if(msgr.status == 200) {
        try {
          loc = window.eval('(' + msgr.responseText + ');');
//          loc = eval(msgr.responseText);
          updateMap(loc);
        } catch (x) {
          alert('Error processing update request\n\n' + x.name + ': ' + x.message + '\n\n\nReponse:\n\n' + msgr.responseText);
        }
        // Found one possible location?
//         if(! (loc instanceof Array)) updateMap(loc);
        
        // Otherwise need user to clarify which one.
      }
      break;
  }
  return true;
}



// Updates the Google map to display a polygon overlay of the general location
// and an infoWindow containing location details.
function updateMap(loc) {

  // Reset link to map URL.
  var mapLink = document.getElementById('mapLink');
  var qi = mapLink.href.indexOf('?');
  mapLink.href = (qi < 0) ? mapLink.href : mapLink.href.substring(0, qi);

  if(!loc) {
    alert('Location not found');
    document.getElementById('room').focus();
    return false;
  }
  var infoWinHtml = '';
  if(mapOverlays.loc) gMap.removeOverlay(mapOverlays.loc);
  if(mapOverlays.marker) gMap.removeOverlay(mapOverlays.marker);
//  gMap.addOverlay(bldgOverlay);
  
  // Add highlight to map.
  mapOverlays.loc = new GPolygon.fromEncoded({
    polylines: [
      {points: loc.encPolygon.points,
       levels: loc.encPolygon.levels,
       color: loc.encPolygon.lineColor,
       opacity: loc.encPolygon.lineOpacity,
       weight: loc.encPolygon.lineWeight,
       numLevels: loc.encPolygon.numLevels,
       zoomFactor: loc.encPolygon.zoomFactor}
    ],
    fill: loc.encPolygon.fill,
    color: loc.encPolygon.fillColor,
    opacity: loc.encPolygon.fillOpacity,
    outline: loc.encPolygon.outline
  });
  gMap.addOverlay(mapOverlays.loc);
  
  // Create HTML text to display in the map infoWindow and update mapLink URL.
  if(loc.name != '' && loc.abbr != '') {
    infoWinHtml = loc.name + ' (<abbr title="' + loc.name + '">' + loc.abbr
                + '</abbr>)<br />';
  } else if(loc.name != '') {
    infoWinHtml = loc.name + '<br />';
  }
  if(loc.number != '') {
    infoWinHtml += '<acronym title="' + loc.building.name + '">'
                + loc.building.abbr + '</acronym> ' + loc.number + '<br />';
    
    // Display room number in room search-field.
    document.getElementById('room').value = loc.building.abbr + ' ' + loc.number;
    
    // Add room parameter to mapLink URL.
    mapLink.href += '?room=' + loc.number;
  } else if(loc.id != '') {
    mapLink.href += '?loc=' + loc.id;
  } else {
    mapLink.href += '?bldg=' + loc.building.id;
  }
  if(loc.building.name != '') {
    infoWinHtml += loc.building.name + ' (<acronym title="' + loc.building.name
                + '">' + loc.building.abbr + '</acronym>)';
  }
  infoWinHtml += (loc.wing != '') ? ', ' + loc.wing + ' wing' : '';
  infoWinHtml += '<br />';
  infoWinHtml += (loc.sect != '') ? loc.sect + ' section<br />' : '';
  if(loc.floor != '') {
    infoWinHtml += loc.floor;
    switch(loc.floor) {
      case '1' : infoWinHtml += 'st floor'; break;
      case '2' : infoWinHtml += 'nd floor'; break;
      case '3' : infoWinHtml += 'rd floor'; break;
      default : infoWinHtml += 'th floor';
    }
    infoWinHtml += '<br />';
  }
  if(loc.desc != '') infoWinHtml += '<hr />' + loc.desc;
  
  // Create a marker to be displayed when infoWindow is closed.
  mapOverlays.marker = new GMarker(mapOverlays.loc.getBounds().getCenter());
  gMap.addOverlay(mapOverlays.marker);
  mapOverlays.marker.hide();

  // Add closeclick event-handler to the infoWindow.
  GEvent.addListener(gMap.getInfoWindow(), 'closeclick',
    function() {
      gMap.closeInfoWindow();
      mapOverlays.marker.show();
    }
  );

  // Add click event-handler to the marker to open the infoWindow.
  GEvent.addListener(mapOverlays.marker, 'click',
    function() {
      gMap.openInfoWindowHtml(mapOverlays.loc.getBounds().getCenter(), infoWinHtml, infoWinOpts);
      mapOverlays.marker.hide();
    }
  );
  
  // Add event handler to the map to display marker when infowindow closes.
  GEvent.addListener(gMap, 'infowindowclose', function() { mapOverlays.marker.show(); });
  
  // Open infoWindow.
  gMap.openInfoWindowHtml(mapOverlays.loc.getBounds().getCenter(), infoWinHtml, infoWinOpts);
  
  return null;
}



// Creates a text version (HTML) of the location information. Used by browsers
// that are not compatible with Google Map.
function makeTextLocation(loc) {
  var html = '<h3>';
  if(loc.name) { html += loc.name; }
  else if(loc.number) { html += loc.number; }
  else { html += 'Location'; }
  html += '</h3>\n<ul class="locInfo">';
  if(loc.number) { html += '\n  <li>Room: ' + loc.number + '</li>'; }
  if(loc.bldg) { html += '\n  <li>Building: ' + loc.bldg + '</li>'; }
  if(loc.floor) { html += '\n  <li>Floor: ' + loc.floor + '</li>'; }
  if(loc.sect) { html += '\n  <li>Sector: ' + loc.sect + '</li>'; }
  html += '\n</ul>';
  if(loc.desc) { html += '\n<p>' + loc.desc + '</p>' };  
  return html;
}



// Retrieve the href value of the anchor element whence the event occurred.
// This function will recursively climb the element hierarchy until the anchor
// element's href value can be read or the document element is reached. It is
// needed because child elements can reside inside the anchor element which may
// be the source of the click event.
function getHrefForIE(elem) {
  if(elem.nodeType == 9) { return null; } // failed: reached top of document.
  else if(elem.nodeType == 1 && elem.href) { return elem.href; } // Found it!
  else { return getHrefForIE(elem.parentNode); }
}



// Window resize handler.
// Sets the height of the map's container whenever the window is resized so the
// map will always occupy the maximum amount of screen space possible without
// requiring user to scroll page.
function setMapHeight() {
  if(! compatMap) return false;
  var mapBox = document.getElementById('map');
  var inUse = 158;  // vertical pixels already allocated to page elements.
  var minHeight = 220;  // minimum map height based on 640x480
  var availHeight;   // height of window excluding chrome and controls.
  
  // Std-compliant browsers
  if(window.innerHeight) {
    availHeight = (window.innerHeight > minHeight + inUse)
                ? window.innerHeight - inUse
                : minHeight;
  
  // IE6 in compiance-mode
  } else if(document.documentElement && document.documentElement.clientHeight) {
    availHeight = (document.documentElement.clientHeight > minHeight + inUse)
                ? document.documentElement.clientHeight - inUse
                : minHeight;
  
  // Dumber IEs
  } else {
    availHeight = (document.body.clientHeight > minHeight + inUse)
                ? document.body.clientHeight - inUse
                : minHeight;
  }
  
  mapBox.style.height = availHeight + 'px';
  return true;
}



function getBldgOverlay() {
  var overlay;
  var polylines = [];
  
  // Building data. Points & levels extracted from DB and hard-coded to reduce
  // server overhead for data that is unlikely to change.
  var bldg_data = {
    'lcolor': '#6F6A61',
    'lopacity': 0.9,
    'lweight': 1,
    'numLevels': 18,
    'zoomFactor': 2,
    'fill': true,
    'fcolor': '#ffffff',
    'fopacity': 0.5,
    'outline': true,
    'lines': [
      {'points': 'gpcbHrhtiVbGE?kAcGD?jA',
       'levels': 'PDFDP',
       'bldg': 'IB'},
      {'points': 'oscbHnntiV?cBX??b@N??X^??d@iA?',
       'levels': 'PBDAABBDP',
       'bldg': 'CH'},
      {'points': '{lcbHnatiVbDC@vBeD??sB',
       'levels': 'PDEDP',
       'bldg': 'AS'},
      {'points': 'ufcbHlatiVdDC?xBeD??uB',
       'levels': 'PDEDP',
       'bldg': 'TB'},
      {'points': 'qfcbHh}siVx@A?TR??~@mA??sA',
       'levels': 'PCAADCP',
       'bldg': 'ED'},
      {'points': 'ajcbHz|siVdAA?pBeA??oB',
       'levels': 'PCECP',
       'bldg': 'PE'},
      {'points': 'gicbHbitiVnBA?dBmB@?GQ??eAN??W',
       'levels': 'PDE@@DAAP',
       'bldg': 'LB'},
      {'points': 'yecbH`ftiVlDA?`E_B??kAT??k@cBB?kA',
       'levels': 'PEFDABDCP',
       'bldg': 'CC'},
      {'points': 'opabHlswiV`@??bBa@??cB',
       'levels': 'PCDCP',
       'bldg': 'GRW'},
      {'points': 'mi~aHnlfiVFeAvB`@WdEeAYNwBq@O',
       'levels': 'PCDCP',
       'bldg': 'SPT'},
      {'points': 'y}bbHty|iVdIF?rDgI?@{D',
       'levels': 'PCDCP',
       'bldg': 'WHT'}
    ]  
  };
  
  // Build polylines for overlay.
  for(var i=0; i < bldg_data.lines.length; i++) {
    polylines[i] = {
      points: bldg_data.lines[i]['points'],
      levels: bldg_data.lines[i]['levels'],
      color: bldg_data.lcolor,
      opacity: bldg_data.lopacity,
      weight: bldg_data.lweight,
      numLevels: bldg_data.numLevels,
      zoomFactor: bldg_data.zoomFactor
    };
  }
  
  // Build overlay.
  overlay = new GPolygon.fromEncoded({
    polylines : polylines,
    fill: bldg_data.fill,
    color: bldg_data.fcolor,
    opacity: bldg_data.fopacity,
    outline: bldg_data.outline});

  return overlay;
}



// Attach event listeners to the load and unload events.
addLoadEvent(initWayfinder);
addUnloadEvent(GUnload);
