/*
    geoXML3.js

    Renders KML on the Google Maps JavaScript API Version 3 
    http://code.google.com/p/geoxml3/


    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

// Extend the global String with a method to remove leading and trailing whitespace
if (!String.prototype.trim) {
  String.prototype.trim = function () {
    return this.replace(/^\s+|\s+$/g, '');
  };
}

// Declare namespace
geoXML3 = window.geoXML3 || {};

// Constructor for the root KML parser object
geoXML3.parser = function (options) {
  // Private variables
  var parserOptions = geoXML3.combineOptions(options, {
    singleInfoWindow: false,
    processStyles: true,
    zoom: true
  });
  var docs = []; // Individual KML documents
  var lastMarker;

  // Private methods

  var parse = function (urls) {
    // Process one or more KML documents
  
    if (typeof urls === 'string') {
      // Single KML document
      urls = [urls];
    }

    // Internal values for the set of documents as a whole
    var internals = {
      docSet: [],
      remaining: urls.length,
      parserOnly: !parserOptions.afterParse
    }; 

    var thisDoc;
    for (var i = 0; i < urls.length; i++) {
      thisDoc = {
        url: urls[i], 
        internals: internals
      };
      internals.docSet.push(thisDoc);
      geoXML3.fetchXML(thisDoc.url, function (responseXML) {render(responseXML, thisDoc);});
    }
  };

  var hideDocument = function (doc) {
    // Hide the map objects associated with a document 
    var i;
    for (i = 0; i < doc.markers.length; i++) {
      this.markers[i].set_visible(false);
    }
    for (i = 0; i < doc.overlays.length; i++) {
      doc.overlays[i].setOpacity(0);
    }
  };
  
  var showDocument = function (doc) {
    // Show the map objects associated with a document 
    var i;
    for (i = 0; i < doc.markers.length; i++) {
      doc.markers[i].set_visible(true);
    }
    for (i = 0; i < doc.overlays.length; i++) {
      doc.overlays[i].setOpacity(doc.overlays[i].percentOpacity_);
    }
  };

  var render = function (responseXML, doc) {
    // Callback for retrieving a KML document: parse the KML and display it on the map

    if (!responseXML) {
      // Error retrieving the data
      geoXML3.log('Unable to retrieve ' + doc.url);
      if (parserOptions.failedParse) {
        parserOptions.failedParse(doc);
      }
    } else if (!doc) {
      throw 'geoXML3 internal error: render called with null document';
    } else {
      doc.styles = {};
      doc.placemarks = [];
      doc.groundOverlays = [];
      if (parserOptions.zoom && !!parserOptions.map)
        doc.bounds = new google.maps.LatLngBounds();
  
      // Parse styles
      var styleID, iconNodes, i;
      var styleNodes = responseXML.getElementsByTagName('Style');
      for (i = 0; i < styleNodes.length; i++) {
        styleID   = styleNodes[i].getAttribute('id');
        iconNodes = styleNodes[i].getElementsByTagName('Icon');
        if (!!iconNodes.length) {
          doc.styles['#' + styleID] = {
            href: geoXML3.nodeValue(iconNodes[0].getElementsByTagName('href')[0])
          };
        }
      }
      if (!!parserOptions.processStyles || !parserOptions.createMarker) {
        // Convert parsed styles into GMaps equivalents
        processStyles(doc);
      }
      
      // Parse placemarks
      var placemark, node, coords, path;
      var placemarkNodes = responseXML.getElementsByTagName('Placemark');
      for (i = 0; i < placemarkNodes.length; i++) {
        // Init the placemark object
        node = placemarkNodes[i];
        placemark = {
          name:  geoXML3.nodeValue(node.getElementsByTagName('name')[0]),
          description: geoXML3.nodeValue(node.getElementsByTagName('description')[0]),
          styleUrl: geoXML3.nodeValue(node.getElementsByTagName('styleUrl')[0])
        };
        placemark.style = doc.styles[placemark.styleUrl] || {};
        if (/^https?:\/\//.test(placemark.description)) {
          placemark.description = '<a href="' + placemark.description + '">' + placemark.description + '</a>';
        }

        // Extract the coordinates
        coords = geoXML3.nodeValue(node.getElementsByTagName('coordinates')[0]).trim();
        coords = coords.replace(/\s+/g, ' ').replace(/, /g, ',');
        path = coords.split(' ');

        // What sort of placemark?
        if (path.length === 1) {
          // Polygons/lines not supported in v3, so only plot markers
          coords = path[0].split(',');
          placemark.point = {
            lat: parseFloat(coords[1]), 
            lng: parseFloat(coords[0]), 
            alt: parseFloat(coords[2])
          };
          if (!!doc.bounds) {
            doc.bounds.extend(new google.maps.LatLng(placemark.point.lat, placemark.point.lng));
          }

          // Call the appropriate function to create the marker
          if (!!parserOptions.createMarker) {
            parserOptions.createMarker(placemark, doc);
          } else {
            createMarker(placemark, doc);
          }
        }
      }
  
      // Parse ground overlays
      var groundOverlay, color, transparency;
      var groundNodes = responseXML.getElementsByTagName('GroundOverlay');
      for (i = 0; i < groundNodes.length; i++) {
        node = groundNodes[i];
        
        // Init the ground overlay object
        groundOverlay = {
          name:        geoXML3.nodeValue(node.getElementsByTagName('name')[0]),
          description: geoXML3.nodeValue(node.getElementsByTagName('description')[0]),
          icon: {href: geoXML3.nodeValue(node.getElementsByTagName('href')[0])},
          latLonBox: {
            north: parseFloat(geoXML3.nodeValue(node.getElementsByTagName('north')[0])),
            east:  parseFloat(geoXML3.nodeValue(node.getElementsByTagName('east')[0])),
            south: parseFloat(geoXML3.nodeValue(node.getElementsByTagName('south')[0])),
            west:  parseFloat(geoXML3.nodeValue(node.getElementsByTagName('west')[0]))
          }
        };
        if (!!doc.bounds) {
          doc.bounds.union(new google.maps.LatLngBounds(
            new google.maps.LatLng(groundOverlay.latLonBox.south, groundOverlay.latLonBox.west),
            new google.maps.LatLng(groundOverlay.latLonBox.north, groundOverlay.latLonBox.east)
          ));
        }

        // Opacity is encoded in the color node
        color = geoXML3.nodeValue(node.getElementsByTagName('color')[0]);
        if ((color !== '') && (color.length == 8)) {
          transparency = parseInt(color.substring(0, 2), 16);
          groundOverlay.opacity = Math.round((255 - transparency) / 2.55);
        } else {
          groundOverlay.opacity = 100;
        }
  
        // Call the appropriate function to create the overlay
        if (!!parserOptions.createOverlay) {
          parserOptions.createOverlay(groundOverlay, doc);
        } else {
          createOverlay(groundOverlay, doc);
        }
      }

      if (!!doc.bounds) {
        doc.internals.bounds = doc.internals.bounds || new google.maps.LatLngBounds();
        doc.internals.bounds.union(doc.bounds); 
      }
      if (!!doc.styles || !!doc.markers || !!doc.overlays) {
        doc.internals.parserOnly = false;
      }

      doc.internals.remaining -= 1;
      if (doc.internals.remaining === 0) {
        // We're done processing this set of KML documents

        // Options that get invoked after parsing completes
        if (!!doc.internals.bounds) {
          parserOptions.map.fitBounds(doc.internals.bounds); 
        }
        if (parserOptions.afterParse) {
          parserOptions.afterParse(doc.internals.docSet);
        }

        if (!doc.internals.parserOnly) {
          // geoXML3 is not being used only as a real-time parser, so keep the parsed documents around
          docs.concat(doc.internals.docSet);
        }
      }
    }
  };
    
  var processStyles = function (doc) {
    var stdRegEx = /\/(red|blue|green|yellow|lightblue|purple|pink|orange)(-dot)?\.png/;
    for (var styleID in doc.styles) {
      if (!!doc.styles[styleID].href) {
        // Init the style object with a standard KML icon
        doc.styles[styleID].icon =  new google.maps.MarkerImage(
          doc.styles[styleID].href,
          new google.maps.Size(32, 32),
          new google.maps.Point(0, 0),
          new google.maps.Point(16, 12)
        );

        // Look for a predictable shadow
        if (stdRegEx.test(doc.styles[styleID].href)) {
          // A standard GMap-style marker icon
          doc.styles[styleID].shadow = new google.maps.MarkerImage(
              'http://maps.google.com/mapfiles/ms/micons/msmarker.shadow.png',
              new google.maps.Size(59, 32),
              new google.maps.Point(0, 0),
              new google.maps.Point(16, 12));
        } else if (doc.styles[styleID].href.indexOf('-pushpin.png') > -1) {
          // Pushpin marker icon
          doc.styles[styleID].shadow = new google.maps.MarkerImage(
            'http://maps.google.com/mapfiles/ms/micons/pushpin_shadow.png',
            new google.maps.Size(59, 32),
            new google.maps.Point(0, 0),
            new google.maps.Point(16, 12));
        } else {
          // Other MyMaps KML standard icon
          doc.styles[styleID].shadow = new google.maps.MarkerImage(
            doc.styles[styleID].href.replace('.png', '.shadow.png'),
            new google.maps.Size(59, 32),
            new google.maps.Point(0, 0),
            new google.maps.Point(16, 12));
        }
      }
    }
  };

  var createMarker = function (placemark, doc) {
    // create a Marker to the map from a placemark KML object

    // Load basic marker properties
    var markerOptions = geoXML3.combineOptions(parserOptions.markerOptions, {
      map:      parserOptions.map,
      position: new google.maps.LatLng(placemark.point.lat, placemark.point.lng),
      title:    placemark.name,
      zIndex:   Math.round(-placemark.point.lat * 100000),
      icon:     placemark.style.icon,
      shadow:   placemark.style.shadow 
    });
  
    // Create the marker on the map
    var marker = new google.maps.Marker(markerOptions);

    // Set up and create the infowindow
    var infoWindowOptions = geoXML3.combineOptions(parserOptions.infoWindowOptions, {
      content: '<div class="infowindow"><h3>' + placemark.name + 
               '</h3><div>' + placemark.description + '</div></div>',
      pixelOffset: new google.maps.Size(0, 2)
    });
    marker.infoWindow = new google.maps.InfoWindow(infoWindowOptions);

    // Infowindow-opening event handler
    google.maps.event.addListener(marker, 'click', function() {
      if (!!parserOptions.singleInfoWindow) {
        if (!!lastMarker && !!lastMarker.infoWindow) {
          lastMarker.infoWindow.close();
        }
        lastMarker = this;
      }
      this.infoWindow.open(this.map, this);
    });
    
    if (!!doc) {
      doc.markers = doc.markers || [];
      doc.markers.push(marker);
    }

    return marker;
  };
  
  var createOverlay = function (groundOverlay, doc) {
    // Add a ProjectedOverlay to the map from a groundOverlay KML object

    if (!window.ProjectedOverlay) {
      throw 'geoXML3 error: ProjectedOverlay not found while rendering GroundOverlay from KML';
    }

    var bounds = new google.maps.LatLngBounds(
        new google.maps.LatLng(groundOverlay.latLonBox.south, groundOverlay.latLonBox.west),
        new google.maps.LatLng(groundOverlay.latLonBox.north, groundOverlay.latLonBox.east)
    );
    var overlayOptions = geoXML3.combineOptions(parserOptions.overlayOptions, {percentOpacity: groundOverlay.opacity});
    var overlay = new ProjectedOverlay(parserOptions.map, groundOverlay.icon.href, bounds, overlayOptions);
    
    if (!!doc) {
      doc.overlays = doc.overlays || [];
      doc.overlays.push(overlay);
    }

    return 
  };

  return {
    // Expose some properties and methods

    options: parserOptions,
    docs:    docs,
    
    parse:         parse,
    hideDocument:  hideDocument,
    showDocument:  showDocument,
    processStyles: processStyles, 
    createMarker:  createMarker,
    createOverlay: createOverlay
  };
};
// End of KML Parser

// Helper objects and functions

// Log a message to the debugging console, if one exists
geoXML3.log = function(msg) {
  if (!!window.console) {
    console.log(msg);
  }
};

// Combine two options objects, a set of default values and a set of override values 
geoXML3.combineOptions = function (overrides, defaults) {
  var result = {};
  if (!!overrides) {
    for (var prop in overrides) {
      if (overrides.hasOwnProperty(prop)) {
        result[prop] = overrides[prop];
      }
    }
  }
  if (!!defaults) {
    for (prop in defaults) {
      if (defaults.hasOwnProperty(prop) && (result[prop] === undefined)) {
        result[prop] = defaults[prop];
      }
    }
  }
  return result;
};

// Retrieve a text document from url and pass it to callback as a string
geoXML3.fetchers = [];
geoXML3.fetchXML = function (url, callback) {
  function timeoutHandler() {
    callback();
  };

  var xhrFetcher;
  if (!!geoXML3.fetchers.length) {
    xhrFetcher = geoXML3.fetchers.pop();
  } else {
    if (!!window.XMLHttpRequest) {
      xhrFetcher = new window.XMLHttpRequest(); // Most browsers
    } else if (!!window.ActiveXObject) {
      xhrFetcher = new window.ActiveXObject('Microsoft.XMLHTTP'); // Some IE
    }
  }

  if (!xhrFetcher) {
    geoXML3.log('Unable to create XHR object');
    callback(null);
  } else {
    xhrFetcher.open('GET', url, true);
    xhrFetcher.onreadystatechange = function () {
      if (xhrFetcher.readyState === 4) {
        // Retrieval complete
        if (!!xhrFetcher.timeout)
          clearTimeout(xhrFetcher.timeout);
        if (xhrFetcher.status >= 400) {
          geoXML3.log('HTTP error ' + xhrFetcher.status + ' retrieving ' + url);
          callback();
        } else {
          // Returned successfully
          callback(xhrFetcher.responseXML);
        }
        // We're done with this fetcher object
        geoXML3.fetchers.push(xhrFetcher);
      }
    };
    xhrFetcher.timeout = setTimeout(timeoutHandler, 60000);
    xhrFetcher.send(null);
  }
};

//nodeValue: Extract the text value of a DOM node, with leading and trailing whitespace trimmed
geoXML3.nodeValue = function(node) {
  if (!node) {
    return '';
  } else {
    return (node.innerText || node.text || node.textContent).trim();
  }
};
