/*
 * google-map.js
 *
 * $Id: google-map.js,v 1.7 2005/12/29 01:33:46 randy Exp $
 */

/**
 * IE doesn't define the DOM-standard constants on Node. Define the
 * ones we might use so we can actually use them.
 */
if ( ! Node )
{
   var Node =
   {
      ELEMENT_NODE:           1,
      ATTRIBUTE_NODE:         2,
      TEXT_NODE:              3,
      DOCUMENT_NODE:          9,
      DOCUMENT_FRAGMENT_NODE: 11
   };
}

var imageDir = "/images";

var map;
function init()
{
   map = new VendomeMap( document.getElementById( "map" ),
                         document.getElementById( "key" ),
                         document.getElementById( "coord" ) );
}

/****************************************************************************
 * CONSTRUCTOR
 ***************************************************************************/

function VendomeMap( mapArea, keyArea, coordArea )
{
   this.mapArea = mapArea;
   this.keyArea = keyArea;
   this.coordArea = coordArea;

   // display an almost-helpful message if G reports not compatible browser
   if ( ! GBrowserIsCompatible() )
   {
      this.reportIncompatible();
      return;
   }

   // start loading our neighborhood data async
   var thisObj = this;
   VendomeMap.loadURLInBackground( '/association/neighborhood.xml',
                                   function( url, request )
                                   {
                                      thisObj.annotateNeighborhood( request );
                                   },
                                   true
                                 );

   // create map and controls
   this.map = new GMap( this.mapArea );
   this.map.addControl( new GSmallMapControl() );
   this.map.addControl( new GMapTypeControl() );

   // center on Ayer and San Pedro
   var endOfAyer = new GPoint( -121.89976930618286, 37.34440262285598 );
   this.map.centerAndZoom( endOfAyer, 1 );

   // if given a display area for it, create latitude/longitude finder
   if ( this.coordArea )
   {
      GEvent.addListener( this.map,
                          'click',
                          function( overlay, point )
                          {
                             if ( ! overlay && point )
                                thisObj.showCoordinates( point );
                          }
                        );
   }
}

VendomeMap.prototype.showCoordinates = function( point )
{
   if ( ! this.coordArea )
      return;
   while ( this.coordArea.hasChildNodes() )
      this.coordArea.removeChild( this.coordArea.childNodes[ 0 ] );
   // y is latitude, x is longitude
   var lat = document.createElement( "p" );
   lat.appendChild( document.createTextNode( "Latitude: " + point.y ) );
   this.coordArea.appendChild( lat );
   var lng = document.createElement( "p" );
   lng.appendChild( document.createTextNode( "Longitude: " + point.x ) );
   this.coordArea.appendChild( lng );
}

VendomeMap.prototype.showMarker = function( title )
{
   var marker = this.markerByTitle[ title ];
   if ( ! marker )
      return;
   marker.openInfoWindow( this.infoByTitle[ title ] );
}

VendomeMap.prototype.showLandmarksOfType = function( name )
{
   // bail if landmark type not actually changed
   if ( this.curLandmarkType == name )
      return;

   // if we have an already selected landmark type, clear it out
   if ( this.curLandmarkType )
   {
      this.map.closeInfoWindow();
      // remove all existing markers from the map
      var oldMarks = this.markersByType[ this.curLandmarkType ];
      for ( var i = 0 ; i < oldMarks.length ; ++i )
         this.map.removeOverlay( oldMarks[ i ] );
      // remove all items from the key list
      while ( this.keyList.hasChildNodes() )
         this.keyList.removeChild( this.keyList.childNodes[ 0 ] );
   }

   // update the selected landmark type
   this.curLandmarkType = name;

   // add the appropriate markers to the map
   var marks = this.markersByType[ name ];
   for ( var j = 0 ; j < marks.length ; ++j )
      this.map.addOverlay( marks[ j ] );

   // add the corresponding keys to the key list
   var keys = this.keysByType[ name ];
   for ( j = 0 ; j < keys.length ; ++j )
      this.keyList.appendChild( keys[ j ] );

   // update the class name for the selected landmark type for styling
   if ( this.typeList )
   {
      var links = this.typeList.getElementsByTagName( "a" );
      for ( j = 0 ; j < links.length ; ++j )
      {
         if ( links[ j ].firstChild.data == name )
            links[ j ].parentNode.className = "selected";
         else
            links[ j ].parentNode.className = "";
      }
   }
}

VendomeMap.prototype.makePoint = function( elem )
{
   var lat = elem.getElementsByTagName( "latitude" );
   var lng = elem.getElementsByTagName( "longitude" );
   if ( lat.length != 1 || lng.length != 1 )
      return;
   return( new GPoint( parseFloat( this.getFirstText( lng ) ),
                       parseFloat( this.getFirstText( lat ) ) ) );
}

VendomeMap.prototype.getFirstText = function( nodeList )
{
   if ( ! nodeList )
      return;
   if ( nodeList.length == 1 )
      return( this.getTextContent( nodeList[ 0 ] ) );
   return;
}

VendomeMap.prototype.getTextContent = function( elem )
{
   var text = '';
   var children = elem.childNodes;
   for ( var i = 0 ; i < children.length ; ++i )
   {
      var child = children[ i ];
      if ( child.nodeType == Node.TEXT_NODE )
         text += child.data;
      else
         text += this.getTextContent( child );
   }
   return( text );
}

VendomeMap.prototype.finishInit = function()
{
   // check for a marker to show in query string
   var query = location.search.substring( 1 );
   var pairs = query.split( "&" );
   for ( var i = 0 ; i < pairs.length ; ++i )
   {
		var pos = pairs[ i ].indexOf( "=" );
		var keyword = pairs[ i ].substring( 0, pos );
		if ( keyword != "show" )
			continue;
		var title = decodeURIComponent( pairs[ i ].substring( pos + 1 ) )
		this.showMarker( title );
   }
}

VendomeMap.prototype.annotateNeighborhood = function( request )
{
   var xmlDoc = request.responseXML;
   if ( ! xmlDoc )
      return;

   // draw polyline around the neighborhood boundary
   var boundary = xmlDoc.getElementsByTagName( "boundary" )[ 0 ];
   var points = new Array();
   var locs = boundary.getElementsByTagName( "point" );
   var i;
   for ( i = 0 ; i < locs.length ; ++i )
   {
      var point = this.makePoint( locs[ i ] );
      if ( point )
         points.push( point );
   }
   points.push( points[ 0 ] ); // close the path
   this.map.addOverlay( new GPolyline( points ) );

   // initialize the various landmark structures
   this.landmarkTypes = new Array();
   this.markersByType = new Object();
   this.keysByType    = new Object();
   this.infoByTitle   = new Object();
   this.markerByTitle = new Object();
   this.iconsByName   = new Object();

   // create the markers, info elements and keys
   var marks = xmlDoc.getElementsByTagName( "landmarks" );
   for ( i = 0 ; i < marks.length ; ++i )
   {
      // initialize lists
      var name = marks[ i ].getAttribute( "name" );
      var iconName = marks[ i ].getAttribute( "icon" );
      this.landmarkTypes.push( name );
      this.markersByType[ name ] = new Array();
      this.keysByType[ name ] = new Array();

      // create objects for each landmark, by title
      var sites = marks[ i ].getElementsByTagName( "landmark" );
      for ( var j = 0 ; j < sites.length ; ++j )
      {
         var title = this.getFirstText(
                        sites[ j ].getElementsByTagName( "title" ) );

         // create marker for this landmark with right icon
         var marker = this.createMarker( title, sites[ j ], iconName );
         this.markerByTitle[ title ] = marker;
         this.markersByType[ name ].push( marker );

         // create info element for this landmark
         var info = this.createInfo( title, sites[ j ] );
         this.infoByTitle[ title ] = info;

         // create key element for this landmark
         var key = this.createKey( title, sites[ j ] );
         this.keysByType[ name ].push( key );
      }
   }

   // if we have more than one landmark type, add controls to switch
   if ( this.landmarkTypes.length > 1 )
   {
      this.typeList = document.createElement( "ul" );
      this.typeList.id = "typeList";
      this.keyArea.appendChild( this.typeList );
      var thisObj = this;
      for ( i = 0 ; i < this.landmarkTypes.length ; ++i )
      {
         var li = document.createElement( "li" );
         var a = document.createElement( "a" );
         a.appendChild( document.createTextNode( this.landmarkTypes[ i ] ) );
         a.href = "#";
         a.onclick = function()
                     {
                        thisObj.showLandmarksOfType( this.firstChild.data );
                        return( false );
                     }
         li.appendChild( a );
         this.typeList.appendChild( li );
      }
   }
   this.keyList = document.createElement( "ul" );
   this.keyList.id = "keyList";
   this.keyArea.appendChild( this.keyList );

   // show first type
   this.showLandmarksOfType( this.landmarkTypes[ 0 ] );
   
   // run finish routine
   this.finishInit();
}

VendomeMap.prototype.createMarker = function( title, mark, iconName )
{
   // extract point from landmark
   var point = this.makePoint( mark );

   // determine any custom icon to use for this landmark
   var icon;
   if ( iconName )
      icon = this.iconOfName( iconName );

   // create new marker, but don't (yet) add to the map
   var marker = new GMarker( point, icon );
   var thisObj = this;
   GEvent.addListener( marker,
                       'click',
                       function()
                       {
                          marker.openInfoWindow(
                             thisObj.infoByTitle[ title ] );
                       }
                     );

   return( marker );
}

VendomeMap.prototype.createKey = function( title, mark )
{
   var e1 = document.createElement( "li" );
   var e2 = document.createElement( "a" );
   e2.appendChild( document.createTextNode( title ) );
   e2.href = "#";
   var thisObj = this;
   e2.onclick = function()
               {
                  thisObj.markerByTitle[ title ].openInfoWindow(
                     thisObj.infoByTitle[ title ] );
                  return( false );
               }
   e1.appendChild( e2 );

   return( e1 );
}

VendomeMap.prototype.createInfo = function( title, mark )
{
   var d = this.getFirstText( mark.getElementsByTagName( "description" ) );
   var a = this.getFirstText( mark.getElementsByTagName( "address" ) );
   var l = this.getFirstText( mark.getElementsByTagName( "link" ) );

   var e1 = document.createElement( "div" );
   e1.className = "infoWindow";
   // apparently, the info window is sized accordingly to the 
   // dimensions of the element we provide; the dimensions need
   // to be set on the style attribute (not in the stylesheet)
   // or they don't seem to be seen by GMap
   e1.style.width = "200px";
   e1.style.height = "100px";
   e1.style.overflow = "auto"; // show scrollbar if needed
   e1.style.marginRight = "18px"; // allow space for scrollbar

   var e;
   var e2 = document.createElement( "h4" );
   if ( l )
   {
      e = document.createElement( "a" );
      e.appendChild( document.createTextNode( title ) );
      e.href = l;
      e.target = "_blank";
      e2.appendChild( e );
   }
   else
   {
      e2.appendChild( document.createTextNode( title ) );
   }
   e1.appendChild( e2 );

   if ( d )
   {
      e = document.createElement( "p" );
      e.className = "desc";
      e.appendChild( document.createTextNode( d ) );
      e1.appendChild( e );
   }

   if ( a )
   {
      e = document.createElement( "p" );
      e.className = "addr";
      e.appendChild( document.createTextNode( a ) );
      e1.appendChild( e );
   }

   return( e1 );
}

VendomeMap.prototype.iconOfName = function( name )
{
   if ( this.iconsByName[ name ] === 1 )
      return;
   else if ( this.iconsByName[ name ] )
      return( this.iconsByName[ name ] );

   var i = new GIcon();
   i.image = imageDir + "/" + name + "-marker.png";
   i.shadow = imageDir + "/" + name + "-shadow.png";
   i.iconSize = new GSize( 20, 34 );
   i.shadowSize = new GSize( 37, 34 );
   i.iconAnchor = new GPoint( 10, 34 );
   i.infoWindowAnchor = new GPoint( 9, 1 );
   this.iconsByName[ name ] = i;

   return( i )
}

VendomeMap.prototype.reportIncompatible = function()
{
   var link = document.createElement( "a" );
   link.href = "http://local.google.com/support/bin/topic.py?topic=1473";
   link.target = "_blank";
   link.appendChild( document.createTextNode( "Google Local Help" ) );
   var sorry = document.createElement( "p" );
   sorry.style.width = "250px";
   sorry.style.backgroundColor = "white";
   sorry.style.border = "1px solid black";
   sorry.style.padding = "10px";
   sorry.style.position = "absolute";
   sorry.style.bottom = sorry.style.left = "10px";
   sorry.appendChild( document.createTextNode(
      "The Vendome Neighborhood Association map is powered by "
      + "Google Maps, which does not support your browser. See the " ) );
   sorry.appendChild( link );
   sorry.appendChild( document.createTextNode(
      " for information about supported browsers. A non-interactive "
      + "map of the neighborhood is shown here instead." ) );
   this.mapArea.appendChild( sorry );
   this.mapArea.style.position = "relative";
   this.mapArea.style.background =
      "transparent url( " + imageDir + "/static-map.gif) 50% 50% no-repeat";
}

/**
 * Load XML URL asynchronously and invoke client handler when done.
 *
 * An exception is thrown if the browser doesn't support XMLHttpRequest
 * (or its IE6 equivalent). A failure to load is not handled via 
 * exceptions, because there is no place for them to be reasonably
 * caught. Instead, if dispatchError is set to true, a faux request
 * object will be dispatched to the handler, with a null responseXML
 * but with status and statusText set per the failure. This allows the
 * client to examine the failure and act accordingly. If dispatchError
 * is set to false (the default), an alert() will be issued on failure.
 *
 * @param url
 *    URL to be loaded
 * @param handler
 *    client handler to be invoked, of form function( url, request )
 * @param dispatchError
 *    if true, invoke handler with faux request on error; otherwise
 *    errors are reported to user through alert() 
 * @param useContentType
 *    if true, the content type of the URL as returned by the server
 *    will be believed; by default, it is forced to be XML, to deal
 *    with idiotic web servers that don't vendor proper MIME types
 */
VendomeMap.loadURLInBackground =
function( url, handler, dispatchError, useContentType )
{
   // create request using browser-appropriate method
   var request = false;
   if ( window.XMLHttpRequest )
   {
      try
      {
         request = new XMLHttpRequest();
      }
      catch ( e )
      {
         request = false;
      }
   }
   else if ( window.ActiveXObject )
   {
      try
      {
         request = new ActiveXObject( "Msxml2.XMLHTTP" );
      }
      catch ( e )
      {
         try
         {
            request = new ActiveXObject( "Microsoft.XMLHTTP" );
         }
         catch ( e )
         {
            request = false;
         }
      }
   }
   
   // throw an exception if we could not make the request
   if ( ! request )
   {
      var e = { type   : "XMLHttpRequest",
                URL    : url,
                message: "Unable to create XMLHttpRequest for this browser."
              }
      throw( e );
   }

   // invoke our own (private) handler when state changes; note that
   // the request object will be available through the magic of closures
   request.onreadystatechange = function ()
   {
      handleStateChange( url, request, handler );
   }

   // unless directed otherwise, force result to be interpreted as XML;
   // this only works in Gecko (more hacks for IE below)
   if ( ! useContentType && request.overrideMimeType )
      request.overrideMimeType( "text/xml" );

   // send off the request
   request.open( "GET", url, true );
   request.send( null );

   // private handler to response to state changes
   function handleStateChange( url, request, handler )
   {
      if ( request.readyState != 4 )
         return; // ignore non-completed states
      // alert( "Status: " + request.status + "\n"
      //        + request.getAllResponseHeaders() );
      if ( request.status == 200 )
      {
         // only way to force IE to treat text/plain as XML is to
         // reload the text into the (existing but empty) DOM given;
         // we make a fake request object to dispatch in this case
         var dispatchRequest = request;
         if ( window.ActiveXObject )
         {
            request.responseXML.loadXML( request.responseText );
            dispatchRequest = new Object();
            dispatchRequest.responseText = request.responseText;
            dispatchRequest.responseXML  = request.responseXML;
            dispatchRequest.status       = request.status;
         }
         handler( url, dispatchRequest );
      }
      else if ( dispatchError )
      {
         // create faux request object with no XML data but status code
         var errorRequest = new Object();
         errorRequest.responseXML = null;
         errorRequest.status      = request.status;
         errorRequest.statusText  = request.statusText;
         errorRequest.URL         = url;
         handler( url, errorRequest );
      }
      else
         // can't throw an exception because there's no place to catch it!
         alert( "Problem loading from URL " + url
                + " [" + request.status + "]" );
   }
}


