if (typeof(GMap2) != 'undefined') {

var CustomGoogleMap = Class.create(PageWidget, {
	gmap : null, // DOM node of google map
	locations_container : null, // DOM node of element which contains list of locations
	location_nodes : null, // array of DOM nodes for each location
	map_markers : null, // array of map markers

	initialize : function(map_el, locations_container_id, config) {
		// basic google map sanity check
		if (!GBrowserIsCompatible()) { return; }

		// get the basic DOM nodes we need in order to function
		// and bail out if they are not present
		this.node = $(map_el);
		if (!this.node) { return; }

		this.setOptions(config);

		this.instantiateGmap();

		// clear everything up
		this.clearMapData();

		// add the locations from the doc
		this.locations_container = $(locations_container_id);

		if (this.locations_container) {
			this.populateMapFromDOM();

		} else if (this.CONFIG['map_data_url']) {
			this.loadMapData(this.CONFIG['map_data_url']);
		}

		// fit everything on the map (even if there's no data at the moment)
	  this.zoomToMarkers(this.CONFIG['zoom_slop_percentage']);

	}, // END: initialize

	instantiateGmap : function() {
		// create the google map instance
		this.gmap = new GMap2(this.node);

		// add controls to the map
		try {
			this.CONFIG['map_controls'].each(function(map_control){
				this.gmap.addControl(eval("new " + map_control + "();"));
			}.bind(this));
		} catch(e) { }

		// set the center point
	  this.gmap.setCenter(this.CONFIG['map_center_point']);

		this.gmap.enableDoubleClickZoom();
	},

	populateMapFromDOM : function(){
		this.location_nodes = $$S(this.locations_container, " ." + this.CONFIG['location_item_class']) || [];

		this.location_nodes.each(function(loc_node, index){
			// parse the location's DOM nodes for data
			var data = this.extractLocationDataFromDOM(loc_node);

			// pull the ID off the element
			// FIXME: not sure quite how to handle no ID on location yet
			var regex_result = this.CONFIG['dom_id_regex'].exec(loc_node.id);
			if (regex_result.length) {
				data.id = parseInt(regex_result[1], 10);
			} else {
				data.id = -1;
			}

			// store the loop index on the location node and the marker data
			loc_node.marker_index = index;
			data.index = index;

			// set default coords if the lat/lng is missing
			data.lat = data.lat || this.CONFIG['default_marker_lat'];
			data.lng = data.lng || this.CONFIG['default_marker_lng'];

			// pull the marker image from the location node (as a DOM node)
			var current_maker_img = "";
			try {
				current_maker_img = $$S(loc_node, "." + this.CONFIG['map_marker_class'])[0];
			} catch(e) { }

			// create the marker using our data and add it to the map
			this.addMarkerToMap(this.createMarker(data, current_maker_img));

			// add event handlers to the location node for nifty effects
			this.attachLocationEventListeners(loc_node);

		}.bind(this));
	}, // END: populateMapFromDOM

	populateMapFromJSON : function(markers) {
		this.location_nodes = [];

		markers.each(function(marker){
			// create the marker using our data and add it to the map
			this.addMarkerToMap(this.createMarker(marker));
		}.bind(this));

	}, // END: populateMapFromJSON

	loadMapData : function(url) {
		new Ajax.Request(url, {
			'method' : 'get', 
			'onSuccess' : this.loadMapDataCallback.bind(this),
			'onFailure' : Prototype.emptyFunction
		});
	}, // END: loadMapData

	loadMapDataCallback : function(transport) {
		var json = transport.responseText.evalJSON();

		if (json && json.markers && json.markers.length) {
			this.clearMapData();
			this.populateMapFromJSON(json.markers);
			this.zoomToMarkers(this.CONFIG['zoom_slop_percentage']);
		}
	}, // END: loadMapDataCallback

	clearMapData : function() {
		this.gmap.clearOverlays();
		this.map_markers = [];
	}, // END: clearMapData

	zoomToMarkers : function(slopPercentage, heightOffsetPct) {
		var x, y, min_x, max_x, min_y, max_y;

		this.map_markers.each(function(marker, index){
			var point = marker.getPoint();

			x = parseFloat(point.lat());
			y = parseFloat(point.lng());

			if (index == 0) {
				min_x = x; max_x = x; min_y = y; max_y = y;

			} else {
				if (x < min_x) min_x = x;
				if (x > max_x) max_x = x;
				if (y < min_y) min_y = y;
				if (y > max_y) max_y = y;
			}
		}.bind(this));

		// if there are no points, then center of the default center point
		if (this.map_markers.length == 0) {
		  this.gmap.setCenter(this.CONFIG['map_center_point_default'], this.CONFIG['map_default_zoom']);

		// else center over the single point
		} else if (this.map_markers.length == 1) {
			this.gmap.setCenter(new GLatLng(x,y), this.CONFIG['map_default_zoom']);

		// else center over all of the points with some slack
		} else if (this.map_markers.length > 1) {
			var center = new GLatLng((min_x + max_x) / 2, (min_y + max_y) / 2);
			var span = new GSize(Math.abs(max_x - min_x), Math.abs(max_y - min_y));

			var slopWid = 0;
			var slopHgt = 0;

			if (typeof(slopPercentage) != "undefined") {
				slopWid = span.width * slopPercentage / 200;
				slopHgt = span.height * slopPercentage / 200;
				span.width  *= 1 + slopPercentage / 100;
				span.height *= 1 + slopPercentage / 100;
			}

			var deltaHgt = 0;

			if (typeof(heightOffsetPct) != "undefined") {
				deltaHgt = span.height * heightOffsetPct / 100;
				center = new GLatLng(center.lat() + deltaHgt, center.lng());
			}

			// needs slop
			var bounds = new GLatLngBounds(new GLatLng(min_x - slopHgt, min_y - slopWid), new GLatLng(max_x + slopHgt, max_y + slopWid)); // sw, ne
			var zoom = this.gmap.getBoundsZoomLevel(bounds);

			this.gmap.setCenter(center, zoom);
		}
	}, // END: zoomToMarkers


	/* -- BEGIN: location functions ----------------------------------------- */

	extractLocationDataFromDOM : function(element) {
		return {
			// required
			lat : parseFloat(this.getTextValue(element, this.CONFIG['lat_class'])),
			lng : parseFloat(this.getTextValue(element, this.CONFIG['lng_class'])),

			// suggested
			title       : this.getTextValue(element, this.CONFIG['title_class']),
			url         : this.getAttributeValue(element, this.CONFIG['url_class'], 'href'),
			address     : this.getHTMLValue(element, this.CONFIG['address_class']),
			phone       : this.getHTMLValue(element, this.CONFIG['phone_class']),
			description : this.getHTMLValue(element, this.CONFIG['desc_class']),
			img         : this.getAttributeValue(element, this.CONFIG['image_class'], 'src')

			// custom properties can go here
//			info_window_width : this.getCustomMarkerProperty(element, "info_window_width")
		};
	}, // END: extractLocationDataFromDOM

	attachLocationEventListeners : function(loc) {
		loc.observe('click',     this.handleLocationClick.bindAsEventListener(this, loc));
		loc.observe('mouseover', this.handleLocationMouseover.bindAsEventListener(this, loc));
		loc.observe('mouseout',  this.handleLocationMouseout.bindAsEventListener(this, loc));
	}, // END: attachLocationEventListeners

	/* ------------------------------------------- END: location functions -- */


	/* -- BEGIN: marker functions ------------------------------------------- */

	createMarker : function(marker_data) {
		// create the marker with its custom props
		var marker = new GMarker(new GLatLng(marker_data['lat'], marker_data['lng']), {
			icon : this.createMarkerIcon(marker_data),
			title : marker_data['title']
		});
		// store the data we parsed on the marker object
		marker.data = marker_data;

		this.attachMarkerEventListeners(marker);

		return marker;
	}, // END: createMarker

	createMarkerIcon : function(marker_data, marker_img) {
		// make the icon for the marker, using a filename template
		var icon = new GIcon();

		// marker_data.icon = {
		// 	icon_img : "",
		// 	icon_img_width : 0,
		// 	icon_img_height : 0,
		// 	icon_anchor_x : 0,
		// 	icon_anchor_y : 0,
		// 	icon_win_anchor_x : 0,
		// 	icon_win_anchor_y : 0,
		// 	icon_shadow_img : "",
		// 	icon_shadow_img_width : 0,
		// 	icon_shadow_img_height : 0
		// }

		if (marker_data.icon) {
			icon.image            = marker_data.icon.icon_img.replace(this.CONFIG['index_regex'], "." + (marker_data.index + 1) + ".");
			icon.iconSize         = new GSize(marker_data.icon.icon_img_width, marker_data.icon.icon_img_height);
			icon.iconAnchor       = new GPoint(marker_data.icon.icon_anchor_x || 0, marker_data.icon.icon_anchor_y || 0);
			icon.infoWindowAnchor = new GPoint(marker_data.icon.icon_win_anchor_x || 0, marker_data.icon.icon_win_anchor_y || 0);

			if (marker_data.icon.icon_shadow_img) {
				icon.shadow = marker_data.icon.icon_shadow_img;
				icon.shadowSize = new GSize(marker_data.icon.icon_shadow_img_width, marker_data.icon.icon_shadow_img_height);
			}

		} else {
			// if we're passing in the marker image, use it
			if (marker_img) {
				icon.image = marker_img.src;
				icon.iconSize = new GSize(img.width, img.height);

			} else {
				// otherwise, check if we're in bounds for our max numbered marker
				if (this.CONFIG['max_numbered_icons'] && (marker_data.index <= this.CONFIG['max_numbered_icons']) ) {
					icon.image = this.CONFIG['marker_base_image'].replace(this.CONFIG['index_regex'], "." + (marker_data.index + 1) + ".");

				// else use the base marker image
				} else {
					icon.image = this.CONFIG['marker_base_image'];
				}
			}

			icon.iconSize         = new GSize(this.CONFIG['marker_icon_w'], this.CONFIG['marker_icon_h']);
			icon.iconAnchor       = new GPoint(this.CONFIG['marker_icon_anchor_x'], this.CONFIG['marker_icon_anchor_y']);
			icon.infoWindowAnchor = new GPoint(this.CONFIG['marker_icon_win_anchor_x'], this.CONFIG['marker_icon_win_anchor_y']);

			if (this.CONFIG['marker_shadow_image']) {
				icon.shadow = this.CONFIG['marker_shadow_image'];
				icon.shadowSize = new GSize(this.CONFIG['marker_shadow_icon_w'], this.CONFIG['marker_shadow_icon_h']);
			}
		}

		return icon;
	}, // END: createMarkerIcon

	generateMarkerInfoWindowContent : function(marker) {
		var template_markup = [];
		
		this.CONFIG['info_window_markup_order'].each(function(item){
			if (marker.data[item] && !marker.data[item].blank())
				template_markup.push(this.CONFIG['info_window_templates'][item]);
		}.bind(this));

		return (new Template(template_markup.join("\n"))).evaluate(marker.data);
	}, // END: generateMarkerInfoWindowContent

	attachMarkerEventListeners : function(marker) {
		// set up event listeners for the markers and info window
		GEvent.addListener(marker, "click", this.handleMarkerClick.bind(this, marker));
		GEvent.addListener(marker, "mouseover", this.handleMarkerMouseover.bind(this, marker));
		GEvent.addListener(marker, "mouseout", this.handleMarkerMouseout.bind(this, marker));
		GEvent.addListener(marker, "infowindowopen", this.handleInfoWindowOpen.bind(this, marker));
		GEvent.addListener(marker, "infowindowclose", this.handleInfoWindowClose.bind(this, marker));
	}, // END: attachMarkerEventListeners

	addMarkerToMap : function(marker) {
		// store the marker in an array and add the marker to the map
		this.map_markers.push(marker);
		this.gmap.addOverlay(marker);
	}, // END: addMarkerToMap
	
	markerInfoWindowCallback : function(marker, transport) {
		var json = transport.responseText.evalJSON();

		if (json && json.marker && json.marker.info_window_html) {
			marker.openInfoWindowHtml(this.wrapInfoWindowHTML(marker, json.marker.info_window_html));
		}
	}, // END: markerInfoWindowCallback

	wrapInfoWindowHTML : function(marker, html) {
		// FIXME: refactor this
		var inline_style = "";

		var width = marker.data['info_window_width'] || this.CONFIG['info_window_width'];
		if (width) inline_style += "width: " + width + "px;";

		var height = marker.data['info_window_height'] || this.CONFIG['info_window_height'];
		if (height) inline_style += (Prototype.Browser.IE6 ? '' : 'min-') + "height: " + height + "px;";

		return [
			'<div class="' + this.CONFIG['info_window_class'] + '" style="' + inline_style + '">',
			html,
			'<\/div>'
		].join('\n');
	}, // END: wrapInfoWindowHTML

	/* --------------------------------------------- END: marker functions -- */


	/* -- BEGIN: utility functions ------------------------------------------ */

	getTextValue : function(el, class_name) {
		try {
			var node = $$S(el, "." + class_name)[0];
			return node.innerHTML.stripTags();
//			return node.textContent || node.innerText;
		} catch(e) {
			return null;
		}
	}, // END: getTextValue

	getHTMLValue : function(el, class_name) {
		try {
			var node = $$S(el, "." + class_name)[0];
			return node.innerHTML;
//			return node.textContent || node.innerText;
		} catch(e) {
			return null;
		}
	}, // END: getHTMLValue

	getAttributeValue : function(el, class_name, attrib) {
		try {
			var node = $$S(el, "." + class_name)[0];
			return node.getAttribute(attrib);
		} catch(e) {
			return null;
		}
	}, // END: getAttributeValue

	getCustomMarkerProperty : function(el, attrib) {
		try {
			el = $(el);

			// if we lack a config or a desired attrib, then bail out
			if (!Object.keys(this.CONFIG['marker_class_properties']).length || !el || !attrib) { return null; }

			// get classes on the element (without the standard class)
			var el_classes = el.classNames().toArray().without(this.CONFIG['location_item_class']);

			// if there's a class on it, use it
			if (el_classes.length) {
				var el_class = el_classes[0];

				// find the matching class and property
				for (var config_class in this.CONFIG['marker_class_properties']) {
					if (el_class == config_class) {
						return this.CONFIG['marker_class_properties'][el_class][attrib];
					}
				}

			} else {
				return null;
			}
			
		} catch(e) {
			return null;
		}

	}, // END: getCustomMarkerProperty

	/* -------------------------------------------- END: utility functions -- */


	/* -- BEGIN: event handlers --------------------------------------------- */

	handleLocationClick : function(e, loc) {
		Event.stop(e);
		try {
			GEvent.trigger(this.map_markers[loc.marker_index], 'click');
		} catch(e) { }
	}, // END: handleLocationClick

	handleLocationMouseover : function(e, loc) {
		try {
			GEvent.trigger(this.map_markers[loc.marker_index], 'mouseover');
		} catch(e) { }
	}, // END: handleLocationMouseover

	handleLocationMouseout : function(e, loc) {
		try {
			GEvent.trigger(this.map_markers[loc.marker_index], 'mouseout');
		} catch(e) { }
	}, // END: handleLocationMouseout

	handleMarkerClick : function(marker) {
		// remove the selected class on all the location markers
		this.location_nodes.each(function(node){
			node.removeClassName(this.CONFIG['selected_class']);
		}.bind(this));

		try {
			// activate the current location
			$(this.CONFIG['location_id_prefix'] + marker.data['id']).addClassName(this.CONFIG['selected_class']);
		} catch(e) { }

		// if a url is available for the info window contents, retrieve it
		if (!this.CONFIG['info_window_url'].blank()) {

			new Ajax.Request(this.CONFIG['info_window_url'], {
				'method' : 'get', 
				'parameters' : { 'id' : marker.data.id },
				'onSuccess' : this.markerInfoWindowCallback.bind(this, marker),
				'onFailure' : Prototype.emptyFunction
			});

		// else use the data gathered during the DOM extraction
		} else {
			marker.openInfoWindowHtml(this.wrapInfoWindowHTML(marker, this.generateMarkerInfoWindowContent(marker)));
		}
	}, // END: handleMarkerClick

	handleMarkerMouseover : function(marker) {
		try {
			$(this.CONFIG['location_id_prefix'] + marker.data['id']).addClassName(this.CONFIG['hover_class']);
		} catch(e) { }
	}, // END: handleMarkerMouseover

	handleMarkerMouseout : function(marker) {
		try {
			$(this.CONFIG['location_id_prefix'] + marker.data['id']).removeClassName(this.CONFIG['hover_class']);
		} catch(e) { }
	}, // END: handleMarkerMouseout

	handleInfoWindowOpen : function(marker) {
		try {
			$(this.CONFIG['location_id_prefix'] + marker.data['id']).addClassName(this.CONFIG['selected_class']);
		} catch(e) { }
	}, // END: handleInfoWindowOpen

	handleInfoWindowClose : function(marker) {
		try {
			$(this.CONFIG['location_id_prefix'] + marker.data['id']).removeClassName(this.CONFIG['selected_class']);
		} catch(e) { }
	} // END: handleInfoWindowClose

	/* ----------------------------------------------- END: event handlers -- */

}); // END: CustomGoogleMap

CustomGoogleMap.CONFIG = {
	location_item_class : "vcard", // the individual location
	location_id_prefix  : "item" + "_", // the individual location

	map_marker_class : 'MapMarker', // the marker image in the location item

	title_class : "fn",
	url_class : "url",
	desc_class : "title",
	geo_class : "geo",
	lat_class : "latitude",
	lng_class : "longitude",
	address_class : "adr",
	phone_class : "tel",
	image_class : "Thumbnail",

	hover_class : "Hover",
	selected_class : "Selected",

	info_window_class : "MapInfoWindow",

	// internal stuff
	dom_id_regex : /(\d+)$/, // pulls the index out of the location dom element
	index_regex  : /\.default\./, // replaces the placeholders in the marker image URLs

	info_window_url : "",
	info_window_width : null, // in pixels; google maps min is 217px
	info_window_height : null, // in pixels;
	info_window_markup_order : $w('title img description address phone url'),
	info_window_templates : {
		// we're using DIVs and SPANs because this markup may be nested deep 
		// within the DOM and we have no idea what the CSS rules may be for 
		// other elements
		'title'       : '<div class="Title">#{title}<\/div>',
		'img'         : '<div class="Image"><img src="#{img}" class="Thumbnail" alt="#{title}" \/><\/div>',
		'description' : '<div class="Description">#{description}<\/div>',
		'address'     : '<div class="Address">#{address}<\/div>',
		'phone'       : '<div class="Phone">#{phone}<\/div>',
		'url'         : '<div class="Website"><a href="#{url}">#{url}<\/a></div>'
	},

	map_controls : ["GLargeMapControl", "GMapTypeControl"],
	map_center_point : new GLatLng(0,0), // the map has to be centered in order for it to work
	map_center_point_default : new GLatLng(0,0), // the map has to be centered in order for it to work
	map_default_zoom : 4, // this is used if there is just one point being mapped

	zoom_slop_percentage : 10,

	marker_base_image   : "/images/icon.map-marker.default.png",
	marker_icon_w : 20,
	marker_icon_h : 34,
	marker_icon_anchor_x : 9,
	marker_icon_anchor_y : 34,
	marker_icon_win_anchor_x : 9,
	marker_icon_win_anchor_y : 2,
	marker_shadow_image : "/images/icon.map-marker.shadow.png",
	marker_shadow_icon_w : 37,
	marker_shadow_icon_h : 34,

	max_numbered_icons : 20,

	// these properties will have to be pulled individually out via 
	// getCustomMarkerProperty used inside the extractDataFromDOM method
	marker_class_properties : {
		// "ClassName" : {
		// 	info_window_width : 200
		// },
		// "OtherClassName" : {
		// 	info_window_width : 400
		// }
	},

	description_max_chars : 130, // in characters

	// default coords for a marker if we can't extract its geo data from the DOM
	default_marker_lat : 0,
	default_marker_lng : 0
};

fixWebKitInheritanceBug(CustomGoogleMap);


// remove this in production
window.onerror = function(){};

Event.observe(window, 'unload', function(){
	// to workaround an error in the GMaps API
	// http://groups.google.com/group/Google-Maps-API/browse_thread/thread/d6a4d6069607b37e
	try { GUnload(); } catch(e) {}
}, false);

}