// definitions of map types
var MAP_TRAIL = {
	model: 'Trail',
	associations: ['region', 'activity'],
	fields: ['name', 'region.id', 'activity.id', 'distance_filter', 'rating'],
	name: 'name',
	locationData: 'latitude/longitude',
	iconBase: '/img/maps/pins/',
	infoWindow: {
		markup: '<h3 rel="name"></h3>' +
				'<dl>' +
					'<dt>Activity:</dt><dd rel="activity.name"></dd>' +
					'<dt>Distance:</dt><dd rel="distance"></dd>' +
					'<dt>Rating:</dt><dd rel="rating"></dd>' +
				'</dl>',
		offsets: { x: -47, y: 10 }
	},
	overlay: G_PHYSICAL_MAP,
	setFormElements: {}
},

MAP_GETAWAY = {
	model: 'Getaway',
	fields: ['name', 'nwregion', 'distance', 'outdoors', 'wine_country', 'urban', 'small_town', 'mountain', 'coast', 'ski_snowboard'],
	associations: [],
	name: 'name',
	locationData: 'location.lat/lng',
	iconBase: '/img/maps/pins/',
	infoWindow: {
		markup: '<h3 rel="name"></h3>' +
				'<div><em rel="esubhead"></em></div>',
		offsets: { x: -47, y: 10 }
	},
	overlay: G_PHYSICAL_MAP,
	setFormElements: {
		nwregion: function (setting) {
			var region = $('region'),
				controls = $('controls');

			// determine which classname to set on fieldset, which controls display of "miles from portland"
			if (setting == 'true' || setting == "*" || setting == "none") {
				if (region.hasClassName('outside')) {
					region.removeClassName('outside');
					region.addClassName('inside');
				}
				controls['nwregion'][0].checked = true;
			} else {
				if (region.hasClassName('inside')) {
					if (controls['distance'].value) {
						controls['distance'].value = '';
					}
					region.removeClassName('inside');
					region.addClassName('outside');
				}
				controls['nwregion'][1].checked = true;
			}
		}
	}
},

MAP_RESTAURANT = {
	model: 'Restaurant',
	associations: ['business_listing', 'neighborhood_quadrant', 'neighborhood', 'cuisine'],
	fields: ['business_listing.name', 'business_listing.neighborhood$', 'cuisine.id',
			 'breakfast', 'lunch', 'dinner', 'brunch', 'parking',
			 'reservations_suggested', 'kid_friendly', 'outdoor_dining', 'carryout',
			 'late_night_dining'],
	name: 'business_listing.name',
	locationData: 'business_listing.address.lat/lng',
	iconBase: '/img/maps/pins/',
	infoWindow: {
		markup: '<h3 rel="business_listing.name"></h3>' +
				'<dl>' +
					'<dt>Cuisine:</dt><dd rel="cuisine.name"></dd>' +
					'<dt>Hours:</dt><dd rel="business_listing.hours"></dd>' +
				'<dl>',
		offsets: { x: -47, y: 10 }
	},
	overlay: G_NORMAL_MAP,
	setFormElements: {}
},

MAP_BAR = {
	model: 'Bar',
	associations: ['business_listing', 'neighborhood_quadrant', 'neighborhood'],
	fields: ['business_listing.name', 'business_listing.neighborhood$',
			 'singles_scene', 'recommended_menu', 'sports_bar', 'karaoke', 'romantic', 'live_entertainment',
			 'scenic_view', 'outdoor_patio', 'recommended_beer_selection', 'late_night',
			 'private_parties', 'pub', 'wine_bar', 'dancing', 'dives', 'billiards', 'brewery',
			 'gay_bar', 'happy_hour', 'hotel_bar'],
	name: 'business_listing.name',
	locationData: 'business_listing.address.lat/lng',
	iconBase: '/img/maps/pins/',
	infoWindow: {
		markup: '<h3 rel="business_listing.name"></h3>' +
				'<dl>' +
					'<dt>Type:</dt><dd rel="display_type"></dd>' +
					'<dt>Hours:</dt><dd rel="business_listing.hours"></dd>' +
				'</dl>',
		offsets: { x: -47, y: 10 }
	},
	overlay: G_NORMAL_MAP,
	setFormElements: {}
},

MAP_SHOP = {
	model: 'Shop',
	associations: ['business_listing', 'neighborhood_quadrant', 'neighborhood'],
	fields: ['business_listing.name', 'business_listing.neighborhood$',
			 'gifts_accessories', 'jewelry', 'antiques', 'books_paper_goods', 'home_garden', 'kids_clothes_toys', 'mens_apparel',
			 'pet_accessories', 'shoes', 'womens_apparel', 'bath_beauty', 'gourmet_specialty_foods', 'outdoor_sporting_goods', 'spas'],
	name: 'business_listing.name',
	locationData: 'business_listing.address.lat/lng',
	iconBase: '/img/maps/pins/',
	infoWindow: {
		markup: '<h3 rel="business_listing.name"></h3>' +
				'<dl>' +
					'<dt>Type:</dt><dd rel="display_type"></dd>' +
					'<dt>Hours:</dt><dd rel="business_listing.hours"></dd>' +
				'</dl>',
		offsets: { x: -47, y: 10 }
	},
	overlay: G_NORMAL_MAP,
	setFormElements: {}
};



// Wires up search controls, results list, "more info" windows and the map for a particular map type
var mapSearch = function (mapType) {

	// contains all GMap api calls; controls functionality for markers and associated info bubbles
	var map = function () {
		var gmap,
			// shortcut to info window definition
			info = mapType.infoWindow,
			// default lat/lon and zoom (Seattle)
			startLat = 47.62005, startLon = -122.34787, startZoom = 12, 
			markers = [],  // list of currently placed markers
			currentMarker, // currently active marker
			icon;          // base GIcon object definition


		// define custom info window object/methods
		var InfoWindow = function(marker, data) {
			this.marker = marker;
			this.content = new Element('div', { className: 'info-window', style: 'display:none;' } ).
				insert( '<div class="info-top"></div><div class="info-content">' + info.markup + '<a class="more">Details</a><a class="close">Close</a></div><div class="info-bottom"></div>');
			$(gmap.getPane(G_MAP_FLOAT_PANE)).insert(this.content);

			// populate the markup
			injectData(this.content, data);

			// attach click handlers to close and more anchors
			var more = this.content.getElementsByClassName('more')[0];
			more.writeAttribute({ href: '#' + data.id });
			this.moreHandler = (function (e) { 
				e.stop();
				addressing.setURL({info: e.target.hash.slice(1)});
				this.hide();
			}).bindAsEventListener(this);
			more.observe('click', this.moreHandler)

			var close = this.content.getElementsByClassName('close')[0];
			this.closeHandler = activateMarker.bindAsEventListener(this.marker);
			close.observe('click', this.closeHandler);
		}
		InfoWindow.prototype = new GOverlay();
		InfoWindow.prototype.initialize = function (map) {
			// size and position the window based on supplied info type
			this.content.setStyle({ 
				left: ( gmap.fromLatLngToDivPixel( this.marker.getLatLng() ).x + ( info.offsets.x || 0 ) ) + 'px',
				bottom: ( -1 * gmap.fromLatLngToDivPixel( this.marker.getLatLng() ).y + ( info.offsets.y ) ) + 'px'
			});
		}
		InfoWindow.prototype.remove = function () {
			this.content.getElementsByClassName('more')[0].stopObserving('click', this.moreHandler);
			this.content.getElementsByClassName('close')[0].stopObserving('click', this.closeHandler);
			this.closeHandler = this.moreHandler = undefined;
			this.marker = null;
			this.content.remove();
		}
		InfoWindow.prototype.copy = function () {
			//TODO: may not be needed?
		}
		InfoWindow.prototype.redraw = function (force) {
			if (!force) return;

			// map the info window to the new pixel coords on zoom
			this.content.setStyle({
				left: ( gmap.fromLatLngToDivPixel( this.marker.getLatLng() ).x + ( info.offsets.x || 0 ) ) + 'px',
				bottom: ( -1 * gmap.fromLatLngToDivPixel( this.marker.getLatLng() ).y + ( info.offsets.y ) ) + 'px'
			});
		}
		InfoWindow.prototype.show = function () {
			el = this.content;
			el.show();
		}
		InfoWindow.prototype.hide = function () {
			el = this.content;
			el.hide();
		}

		// shorthand for lat/longs in GLatLng format
		var point = function (lat, lon) {
			return new GLatLng(lat, lon);
		}

		// finds and creates a view that will show all markers in the marker list
		var recenter = function () {
			var bounds = new GLatLngBounds();

			// iterate over each marker until we have a bounding box that contains them all
			markers.each( function (marker) {
				bounds.extend( marker.getLatLng() );
			});
			gmap.setZoom( gmap.getBoundsZoomLevel(bounds) );	
			gmap.panTo( bounds.getCenter() );
		}

		// on marker click, pan to marker and toggle its info window on and off, as needed;
		// also highlight cooresponding listing in location list
		var activateMarker = function (e) {
			map.goToMarker(this.index);
			locationList.goToLocation(this.index);
		}

		// interface to map object
		return {
			init: function () {
				gmap = $('map');

				if (gmap && GBrowserIsCompatible()) {
					// create custom icon class, from which we'll clone markers
					if (mapType.iconBase) {
						icon = new GIcon();
						icon.image = mapType.iconBase + '01.png';
						icon.iconSize = new GSize(27, 28);
						icon.transparent = mapType.iconBase + 'trans.png';
						icon.iconAnchor = new GPoint(11, 22);
						icon.infoWindowAnchor = new GPoint(11, 5);
						icon.imageMap = [11,0, 0,0, 0,11, 2,21, 11,24, 21,21, 24,11, 21,2];
					}

					gmap = new GMap2(gmap, { mapTypes: [G_NORMAL_MAP, G_SATELLITE_MAP, G_PHYSICAL_MAP] });
					gmap.addControl(new GLargeMapControl(), new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(10,10)));
					gmap.addControl(new GMapTypeControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(10,10)));
					gmap.setCenter(point(startLat, startLon), startZoom, mapType.overlay);

					// preload the info-window images
					var top = new Image(), mid = new Image(), bot = new Image();
					top.src = 'bg-trail-window-top.png';
					mid.src = 'bg-trail-window-content.png';
					bot.src = 'bg-trail-window-bottom.png';
				}
			},

			// remove all markers from the map, and clear the marker list
			clear: function () {
				markers.each( function (marker) {
					GEvent.removeListener(marker.handle);
					marker.handle = null;
				});

				gmap.clearOverlays();

				markers = [];
				currentMarker = null;
			},

			// create a marker list, generate info windows for each marker, and set a new zoom/center to show
			// all markers in the list on the map
			update: function (locs, offset) {
				// generate the marker array from locations, using supplied map and info types
				(locs.length > 25 || offset > 0) ? iconType = 'generic' : iconType = 'num';
				locs.each( function (loc, index) {
					var marker = new GMarker(
						point(parseFloat(loc.lat), parseFloat(loc.lon)),
						{
							icon: icon ?
								new GIcon(icon, mapType.iconBase + ((iconType == 'num') ? (index < 9 ? '0' + (index + 1) : index + 1) : iconType) + '.png') : G_DEFAULT_ICON,
							title: loc[mapType.name]
						}
					);					
					
					// this value is used to allow markers to reference themselves in the markers array
					marker.index = index;
					
					gmap.addOverlay(marker);
					marker.handle = GEvent.addListener(marker, 'click', activateMarker);
					marker.infoWindow = new InfoWindow(marker, loc);
					gmap.addOverlay(marker.infoWindow);

					markers.push(marker);
				});

				// pan and zoom to a viewport that will show all markers
				recenter();
			},

			// pan to a marker in the marker list
			goToMarker: function (index) {
				var marker = markers[index];
				gmap.panTo( marker.getLatLng() );

				// toggle the info window
 				if (currentMarker) {
					currentMarker.infoWindow.hide();
					if (marker == currentMarker) {
						currentMarker = undefined;
						return;
					}
				}
				marker.infoWindow.show();
				currentMarker = marker;
			},

			closeInfoWindow: function () {
				if (currentMarker) {
					currentMarker.infoWindow.hide();
					currentMarker = undefined;
				}
			},

			reset: function () {
				map.clear();
				gmap.setZoom(startZoom);
				gmap.panTo(point(startLat, startLon));
			},

			unload: function () {
				if (gmap && gmap.isLoaded()) {
					map.clear();
					GUnload();
				}
				gmap = info = null;
			}
		};
	}(); // end map
	


	// manages results from the search object, and calls markers in the map object on list item click
	var locationList = function () {
		var terms, results, list, fail, instructions,
			currentLocation,
			locations = [];

		// build a results list from the JSON object passed in by search
		var buildResults = function (offset) {
			list.update();
			list.show();

			locations.each( function (loc, index) {
				list.insert(
					'<li id="' + loc.id + '"><a href="#' + (index) + '"><span>' + (index + offset + 1) + '</span>' + loc.name + '</a></li>'
				);
			});
		}

		// toggle on state for this result list item
		var activateLocation = function (e) {
			var index;
			
			if (e.target.tagName.toLowerCase() == 'a') {
				e.stop();
				index = parseInt(e.target.hash.slice(1), 10);
				locationList.goToLocation(index);
				map.goToMarker(index);
			}
		}

		// figure out how much vertical room is left for the results list after term bucket is populated
		var remainingHeight = function () {
			var parent = $('map-results');
			return (parseInt(parent.getStyle('height')) - 
					(terms.getStyle('display') == 'block' ? terms.offsetHeight + parseInt(terms.getStyle('margin-top')) + parseInt(terms.getStyle('margin-bottom')) : 0))
				+ 'px';
		}

		return {
			init: function () {
				terms = $('terms');
				instructions = $('instructions');
				list = new Element('ol');
				fail = new Element('p', { id: 'fail' }).insert('Sorry, but there were no results that matched your search criteria.');
				
				results = $('results');
				results.insert({top: fail});
				results.insert({top: list});
				list.observe('click', activateLocation);

				terms.hide();
				fail.hide();
				list.hide();

				results.select('a')[0].observe('click', function (e) {
					e.stop();
					$('sidebar').className = "search";
				});

				results.setStyle({ height: remainingHeight() });
			},

  		// Build pagination links for resultsets greater than the max size.
  		buildPagination: function(mapType, searchParams, maxResults, offset) {
        searchParams += '&result=count';
		  
  		  // submit the search; pass results to the location list on success
				new Ajax.Request('/api/' + mapType.model + '/search.json?' + searchParams, {
					method: 'get',
					onSuccess: function (data) {
						var totalResults = parseInt(data.responseText.evalJSON().count);

						if (totalResults > maxResults || offset > 0) {
						  $('terms').insert({bottom : '&nbsp;(' + totalResults + ')'});
						  
						  var pageCount = (totalResults / maxResults).ceil();

				      // Construct the pagination links.
						  var paginationLinks = "<span class='left'>Pages:&nbsp;&nbsp;</span>";

						  for (var i = 0; i < pageCount; ++i) {
                ((offset / maxResults).ceil() == i) ? className = 'active' : className = 'inactive';
                paginationLinks += '<a href="#" title="'+ i +'" class="'+ className +'">' + (i+1) + '</a>';
              }
              
						  $('pagination').update(paginationLinks);
						  
						  $$('#pagination a').each(function(el) {
						    el.observe('click', switchPages);
						  });

						}
						
					},
					onFailure: function () {
					  alert('/api/' + mapType.model + '/search.json?' + searchParams);
						alert('Ajax.Request: search failed');
					}
				});
  		  
  		  function switchPages(e) {
          e.preventDefault();
          toPage = e.target.readAttribute('title')

          // Set all links back to red.
          $$('#pagination a').each(function(el) {
  			    el.setStyle({color: '#c11530'});
  			  });

          search.submit(SWFAddress.getValue().match(/search:([\w\s\=\*\.\$,&-']+)\//)[1], toPage * maxResults);
        }
  		  
  		},

			// remove all overlays and clear out the location list
			clear: function (showLoader) {
				if (showLoader) {
					$('map-results').addClassName('loading');
				}

				terms.hide();
				fail.hide();

				list.update();
				currentLocation = null;
				locations = [];

				// get the more info window out of the way
				if (moreInfo.isOpen()) {
					addressing.setURL({info: null});
				}
			}, 

			// update the results list with the JSON data returned from the server
			update: function (data, offset) {	
				var locationData, name, latlon, ref;
				
				if (data) {
					locationData = mapType.locationData.split('.');
					latlon = locationData.last().split('/');
					locationData = locationData.slice(0, -1);

					name = mapType.name.split('.');

					data.each( function (loc) {
						// normalize the location of lat/lon
						ref = loc;
						locationData.each( function (assoc) {
							ref = ref ? ref[assoc] : null;
						});

						// safeguard against lat/lng associations that didn't have values set; they will be weeded out in the loop below
						if (ref) {
							loc.lat = ref[latlon[0]];
							loc.lon = ref[latlon[1]];
						}

						// normalize location name
						if (!loc.name) {
							ref = loc;
							name.each( function (assoc) {
								ref = ref ? ref[assoc] : null;
							});
							if (ref) {
								loc.name = ref;
							}
						}
					});
					
					// weed out all invalid data
					locations = data.select( function(loc) {
						return (loc.lat != undefined) && (loc.lon != undefined) && (loc.name != undefined);
					});

					// sort the location array by name
					// locations.sort(function (a,b) {
					// 	return a.name < b.name ? -1 : 1;
					// });
				}

				// clear spinner and set height of scrollable result container
				terms.show();
				$('map-results').removeClassName('loading');
				results.setStyle({ height: remainingHeight() });

				// if there are any locations, build result list and tell map to draw new markers
				if (locations.length) {
					// populate locations across map and result list
					buildResults(offset);
					map.update(locations, offset);

					// attach a class name to fix potential horizontal scrolling issues
					// because IE6/7 are being stupid about overflow:auto and horizontal scrollbars
					if (Prototype.Browser.IE && results.getHeight() < (list.getHeight() + instructions.getHeight())) {
						list.addClassName('resize');
					}
				} else {
					// no results were returned
					list.hide();
					fail.show();
				}

				results.show();
			},

			// toggle a result list item on or off, and scroll it into view
			goToLocation: function (index) {
				var loc = locations[index],
					li = $('' + loc.id);    // grab li container based on id saved in location
				
				// close "more info" window, if it's open
				if (moreInfo.isOpen()) {
					addressing.setURL({info: null});
				}

				// scroll this list element into view...
				// check if it's below current window...
				if (results.scrollTop + results.clientHeight < li.offsetTop + li.getHeight()) {
					results.scrollTop = (li.offsetTop + li.getHeight()) - results.clientHeight;

					// or check if it's above
 				} else if (li.offsetTop < results.scrollTop) {
					results.scrollTop = li.offsetTop;
				}

				// toggle the on state
 				if (currentLocation) {
					$('' + currentLocation.id).removeClassName('on');
					if (loc == currentLocation) {
						currentLocation = undefined;
						return;
					}
				}
				li.addClassName('on');
				currentLocation = loc;
			},

			unload: function () {
				$('map-results').update();
				results = null;
			}
		};
	}(); // end locationList



	// handles the search functionality and interfaces with form controls
	var search = function () {
		var controls, terms, toggle;

		// serialize form values
		var buildSearchURL = function () {
			var searchString = mapType.fields.slice(0).map( function (field) {

				var tagName = controls[field].tagName ? controls[field].tagName.toLowerCase() : controls[field][0].tagName.toLowerCase(),
					type = controls[field].type ? controls[field].type : controls[field][0].type;
				
				switch (tagName) {
				case 'input':
					switch (type) {
					case 'text':
						if (controls[field].value) {
							return field + '=' + controls[field].value;
						}
						break;
					case 'checkbox':
						// is this the only control by that name?
						if (!controls[field].length) {
							if (controls[field].checked) {
								return field + "=" + controls[field].value;
							}

						// if field name is not unique, iterate over the node list
						} else {
							var list = [];
							for (var i = 0, len = controls[field].length; i < len; i++) {
								if (controls[field][i].checked) {
									list.push(controls[field][i].value);
								}
							}
							if (list) return field + "=" + list.join(",");
						}
						break;
					case 'radio':
						// iterate over the node list
						for (var i = 0, len = controls[field].length; i < len; i++) {
							if (controls[field][i].checked) {
								return field + "=" + controls[field][i].value;
							}
						}
						break;
					default:
						return null;
					}
					break;
				case 'select':
					if (controls[field].value && controls[field].value != '') {
						return field + '=' + controls[field].selectedIndex;
					}
					break;
				default:
					return null;
				}
			}).without(null).join('&');

			if (!searchString.length) {
				searchString = '*';
			}
			
			addressing.setURL({search: searchString, info: null});
		}

		return {
			init: function () {
				toggle = $('sidebar');
				controls = $('controls');
				terms = $('terms');

				// handlers
				controls.observe('submit', function(e) {
					e.stop();
					buildSearchURL()
					addressing.setURL({info: null});
				});
				toggle.select('ul')[0].observe('click', function (e) {
					e.stop();
					toggle.className = e.target.hash.slice(1);
				});

				// turn on the form
				$('submit').disabled = false;
			},

			// submit search to server and pass results to location list
			submit: function (string, offset) {
				var searchParams = '', // parameter string sent to server
					searchList = [],   // input values collected from search form
					termString = [],   // search terms that are shown to user
					multiSelect = [],  // used to join checkbox option sets with different names
					multiGroups = {},   // used to concat multi-select checkbox options with the same name
					associations = mapType.associations, // array of associations to include in search
					special_offers = false,
					max_results = 100;

				// grab values and gather search terms from all search fields
				searchList = mapType.fields.inject([], function (array, field) {
					var name = field,
						tagName = controls[field].tagName ? controls[field].tagName.toLowerCase() : controls[field][0].tagName.toLowerCase(),
						value = controls[field].value,
						type = controls[field].type ? controls[field].type : controls[field][0].type,
						selectedIndex = controls[field].selectedIndex;
					
					switch (tagName) {
					case 'input':
						switch (type) {
						case 'text':
							if (value) {
								// check for a rel attribute on the field; this indicates that the db query needs to be
								// specially formatted based on the value of the rel attribute
								if (controls[field].getAttribute('rel')) {
									value = controls[field].getAttribute('rel').replace(/\$/g, value);
									array.push( value );
									// use the label for this control as the search term, if it's not an overlabel
									var label = controls.select('label[for='+controls[field].id+']')[0];
									if (label.className != 'overlabel-apply') {
										termString.push( '<em>"' + controls[field].value + ' ' + label.firstChild.nodeValue + '"</em>' );
									} else {
										termString.push( '<em>"' + controls[field].value + '"</em>');
									}
								} else {
									array.push( escape(name) + ':' + escape(value) );
									termString.push( '<em>"' + value + '"</em>');
								}
							}
							break;
						case 'checkbox':
							if (!controls[field].length) {
								if (controls[field].checked) {
									if (controls[field].id == 'special_offers') {
										special_offers = true;
									} else {
										multiSelect.push( escape(name) + '!' + controls[field].value );
									}
									// use the label for this control as the search term
									termString.push( '<em>"' + controls.select('label[for='+controls[field].id+']')[0].firstChild.nodeValue + '"</em>' );
								}
							} else {
								var el;
								for (var i = 0, len = controls[field].length; i < len; i++) {
									el = controls[field][i];
									if (el.checked) {
										if (el.id == 'special_offers') {
											special_offers = true;
										} else {
											if (!multiGroups[field]) multiGroups[field] = [];
											multiGroups[field].push( escape(name) + '!' + controls[field][i].value );
										}
										// use the label for this control as the search term
										termString.push( '<em>"' + controls.select('label[for='+el.id+']')[0].firstChild.nodeValue + '"</em>');
									}
								}
							}
							break;
						case 'radio':
							// iterate over the node list
							var el;
							for (var i = 0, len = controls[field].length; i < len; i++) {
								el = controls[field][i];
								if (el.checked) {
									array.push( escape(name) + '!' + el.value );
									// use the label for this control as the search term
									termString.push( '<em>"' + controls.select('label[for='+el.id+']')[0].firstChild.nodeValue + '"</em>');
								}
							}
							break;
						}
						break;
					case 'select':
						if (value && value != '') {
							// check for a dollar sign in the field name; this indicates that the data can be pulled
							// from multiple locations; we need to grab and concatenate the full name/association 
							// from the selected value's title field
							if (field.indexOf('$') != -1) {
								name = field.replace('\$', controls[field][selectedIndex].getAttribute('rel'));
							}
							array.push( escape(name) + '!' + escape(value) );
							termString.push( '<em>"' + controls[field][selectedIndex].text + '"</em>');
						}
						break;
					}
					
					return array;
				});

				// if there are any single-select checkbox options, join those up based on map type
				if (multiSelect.length) {
					if (mapType.model == 'Restaurant') {
						searchList.push( '(' + multiSelect.join('@') + ')' );
					} else {
						searchList.push( '(' + multiSelect.join('|') + ')' );
					}
				}

				// join any multi-select option groups
				multiGroups = $H(multiGroups);
				multiGroups.each( function (pair) {
					searchList.push( 
						'(' + pair[1].join('|') + ')'
					);
				});

				if (special_offers) {
					searchList.push( 'special_offers!1' );
				}

				// create query
				if (searchList.length) {
				  searchParams = 'logic=(' + searchList.join('@') + ')'
				} else {
				  searchParams = '';
				}
				
				if (associations.length) {
				  if (searchParams != '') { searchParams += '&'; }
					searchParams += 'include=' + associations.join(',');
				}

				// searchParams += '&order=asc&order_by=id';

				// display the search terms
				terms.update( '<strong>Results for:</strong> ' + (termString.join(' and ') || '<em>all ' + mapType.model + 's</em>') );

				// clear map and location list
				locationList.clear(true);
				map.clear();

				limitedParams = searchParams + '&limit=' + max_results + '&offset=' + offset;

				// submit the search; pass results to the location list on success
				new Ajax.Request('/api/' + mapType.model + '/search.json?' + limitedParams, {
					method: 'get',
					onSuccess: function (data) {
						var resultList = data.responseText.evalJSON();
						locationList.update(resultList, offset);

            // Build pagination links if this is the first view.
						if (resultList.size() == max_results || offset > 0) {
						  locationList.buildPagination(mapType, searchParams, max_results, offset);
						} else {
						  $('pagination').update('');
						}
						
					},
					onFailure: function () {
						alert('Ajax.Request: search failed');
						locationList.update();
					}
				});
							
			},

			checkFormState: function (params) {
				var changed = false,
					states = {};

				// see if we're doing an all-inclusive search (or no search)
				if (params == '*' || params == 'none') {

					mapType.fields.each( function(field) {

						if (typeof mapType.setFormElements[field] === "function") {
							mapType.setFormElements[field]('*');

						} else {
							var tagName = controls[field].tagName ? controls[field].tagName.toLowerCase() : controls[field][0].tagName.toLowerCase(),
								type = controls[field].type ? controls[field].type : controls[field][0].type;

							switch (tagName) {
							case 'input':
								switch (type) {
								case 'text':
									if (controls[field].value !== '' ) {
										controls[field].value = '';
										changed = true;
									}
									break;
								case 'checkbox':
									if (!controls[field].length) {
										if (controls[field].checked) {
											controls[field].checked = false;
											changed = true;
										}

									} else {
										for (var i = 0, len = controls[field].length; i < len; i++) {
											if (controls[field][i].checked) {
												controls[field][i].checked = false;
												changed = true;
											}
										}
									}
									break;
								case 'radio':
									if (controls[field][0].checked != true) {
										controls[field][0].checked = true;
										changed = true;
									}
									break;
								}
								break;
							case 'select':
								if (controls[field].selectedIndex) {
									controls[field].selectedIndex = 0;
									changed = true;
								}
								break;
							}
						}
					});

				} else {

					// break up each param into a key/value pair of field states
					params.split('&').each( function (param) {
						var pair = param.split('=');
						states[unescape(pair[0])] = unescape(pair[1]);
					});

					// loop over each form field and look for matching field state names
					mapType.fields.each( function(field) {

						if (typeof mapType.setFormElements[field] === "function") {
							mapType.setFormElements[field](states[field]);

						} else {
							var tagName = controls[field].tagName ? controls[field].tagName.toLowerCase() : controls[field][0].tagName.toLowerCase(),
								type = controls[field].type ? controls[field].type : controls[field][0].type;

							// check to see if fields that should be set are
							if (states[field] !== undefined) {

								switch (tagName) {
								case 'input':
									switch (type) {
									case 'text':
										if (controls[field].value !== states[field]) {
											controls[field].value = states[field];
											changed = true;
										}
										break;
									case 'checkbox':
										if (!controls[field].length) {
											if (!controls[field].checked) {
												controls[field].checked = true;
												changed = true;
											}
										} else {
											var list = states[field].split(','),
												el;
											// loop through each element, and try to find it in the array
											for (var i = 0, len = controls[field].length; i < len; i++) {
												el = controls[field][i];
												list.each( function (value) {
													if (el.value == value && !el.checked) {
														el.checked = true;
														changed = true;
													}
												});
											}
										}
										break;
									case 'radio':
										var el;
										for (var i = 0, len = controls[field].length; i < len; i++) {
											el = controls[field][i];
											if (el.value == states[field] && !el.checked) {
												el.checked = true;
												changed = true;
											}
										}
										break;
									}
									break;
								case 'select':
									if (controls[field].selectedIndex != parseInt(states[field])) {
										controls[field].selectedIndex = states[field];
										changed = true;
									}
									break;
								}

							// otherwise, check to see if fields that shouldn't be set aren't
							} else {

								switch (tagName) {
								case 'input':
									switch (type) {
									case 'text':
										if (controls[field].value !== states[field] ) {
											controls[field].value = '';
											changed = true;
										}
										break;
									case 'checkbox':
										if (!controls[field].length) {
											if (controls[field].checked) {
												controls[field].checked = false;
												changed = true;
											}
										} else {
											var list = states[field].split(','),
												el;
											// if element is checked but is not in the array, uncheck it
											for (var i = 0, len = controls[field].length; i < len; i++) {
												el = controls[field][i];
												if (list.indexOf(el.value) == -1 && el.checked) {
													el.checked = false;
													changed = true;
												}
											}
										}

										if (controls[field].checked) {
											controls[field].checked = false;
											changed = true;
										}
										break;
									case 'radio':
										if (!controls[field][0].checked) {
											controls[field][0].checked = true;
											changed = true;
										}
										break;
									}
									break;
								case 'select':
									if (controls[field].selectedIndex) {
										controls[field].selectedIndex = 0;
										changed = true;
									}
									break;
								}

							}
						}
					});
				}

				resetOverLabels(controls);
				if (params != 'none') {
					toggle.className = 'results';
				}
			},

			clear: function () {
				controls.select('*[name]').each( function(field) {
					var tagname = field.tagName.toLowerCase(),
						type = field.getAttribute('type');

					if (tagname == 'select') {
						field.selectedIndex = 0;
					} else if (tagname == 'input' && (type == 'radio' || type == 'checkbox')) {
						field.checked = false;
					} else if (tagname == 'input' && type == 'text') {
						field.value = '';
					}
				});

				resetOverLabels(controls);
				terms.update();
			},
			
			unload: function () {
				controls = terms = null;
			}
		};
	}(); // end search



	// controls the "more info" window display for the current location, grabbing info and comments from server as needed
	var moreInfo = function () {
		var overlay, wrapper, 
			content, options, print, ad, actions,
			error,
			open = false;

		// email the current info window url to a friend
		var shareThis = function (e) {
			var error = false,
				fields = $A(e.target.getElementsByClassName('req'));

			e.stop();

			// check for required fields (not currently checking validity of fields)
			if (fields.length) {
				fields.each( function (field) {
					if (field.value === '') {
						$('email-response').hide();
						field.addClassName('error');
						error = true;
					} else {
						field.removeClassName('error');
					}
				});
			}

			if (error) {
				e.target.addClassName('error');
			} else {
				// send email request
				new Ajax.Request('/api/pokoencephalon', {
					method: 'post',
					parameters: Form.serialize(e.target) + '&email[url]=' + escape('<' + window.location.toString() + '>') + '&' + tok.k() + '=' + tok.v(),
					onLoading: function () { $('share-spinner').setStyle({visibility: 'visible'}); },
					onSuccess: function () {
						// clear the form
						e.target.reset();
						$('share-spinner').setStyle({visibility: 'hidden'});
						e.target.removeClassName('error');
						resetOverLabels(e.target);
						$('email-response').show();
						$('email-response').update('Your message has been sent. Thank you.');
						tok.r();
					},
					onFailure: function () {
						$('share-spinner').setStyle({visibility: 'hidden'});
						$('email-response').addClassName('error').insert('An error occurred while attempting to send your message');
					}
				});
			}
		}

		// post a new comment back to the server, which will reply with JSON data containing the new comment
		var postDiscussComment = function (e) {
			var params = Form.serialize(e.target) + '&' + tok.k() + '=' + tok.v();

			e.stop();

			// post the comment
			new Ajax.Request('/api/comment.json', {
				method: 'post',
				parameters: params,
				onSuccess: function (data) {
					e.target.reset();
					resetOverLabels(e.target);
					tok.r(); 
					updateDiscussComments(data.responseText.evalJSON());
				},
				onFailure: function () {
					alert('Ajax.Request: comment post failed');
				}
			});
		}

		// update the currently displayed comment list with a JSON comment object called from postDiscussComment function
		var updateDiscussComments = function (comment) {
			var comments = $('comments'),
				list = $('comment-list');

			if (!list) {
				comments.select('p')[0].remove();
				list = new Element('dl', { id: 'comment-list' });
				$('comments').insert({ bottom: list });
			}

			list.insert({ top: '<dt><em>' + comment.name + '</em> wrote:</dt>' + '<dd>' + comment.text + '</dd>' });
		}

		return {
			init: function () {
				var close = new Element('a', { className: 'close', href: '#info' }).insert('Close');

				// create the more info window skeleton
				overlay = new Element('div', { id: 'more-info-overlay', style: 'display:none;' });
				wrapper = new Element('div', { id: 'more-info-wrapper', className: 'description' });
				options = new Element('ul', { id: 'more-info-options' }).insert(
					new Element('li', { className: 'description' }).insert(
						new Element('a', { href: '#description' }).insert('Description')))
				.insert(
					new Element('li', { className: 'discuss' }).insert(
						new Element('a', { href: '#discuss' }).insert('Discuss')))
				.insert(
					new Element('li', { className: 'email' }).insert(
						new Element('a', { href: '#email' }).insert('Email To A Friend')))
    		.insert(
    			new Element('li', { className: 'sharethis' }).insert(
    				new Element('a', { href: '#sharethis' }).insert('Share This')));

				
				// they'd rather the user print the pdf on the trails map
				if (mapType.model != "Trail") {
					print = new Element('a', { href: '#', id: 'more-info-print' }).insert('Print');
				}

				wrapper.insert(options);
				wrapper.insert(close);
				wrapper.insert(print);
				overlay.insert(wrapper);
				$('map-wrapper').insert({ top: overlay });

				// close button
				close.observe('click', function (e) {
					addressing.setURL({info: null});
					e.stop();
				});

				// option links
				options.observe('click', function (e) {
					e.stop();
					if (e.target.tagName.toLowerCase() == 'a') {
						wrapper.className = e.target.hash.slice('1');
					}
				});

				// print link
				if (print) {
					print.observe('click', function (e) {
						e.stop();
						moreInfo.print();
					});
				}

				// preload the background png
				var bg = new Image();
				bg.src = '/img/more-info/bg-wrapper.png';
			},

			isOpen: function () {
				return open;
			},

			show: function (id) {
				if (!open) {
					new Ajax.Request('/snippets/' + mapType.model.toLowerCase() + '/more-info/' + id + '/', {
						method: 'get',
						onSuccess: function (data) {
							wrapper.insert(data.responseText);
							initOverLabels();
							external_links();

							content = $('more-info-content');
							actions = $('event-actions');
							ad = $('more-info-ad');

							// capture form submits
							var discuss = wrapper.select('#more-info-discuss form')
							if (discuss && discuss.length) discuss[0].observe('submit', postDiscussComment);
							wrapper.select('#more-info-email form')[0].observe('submit', shareThis);

              // build title for meta-description tag on redirect for share popovers, then construct the share links.
						  build_share_links("http://"+window.location.hostname+"/extensions/popover/?type="+$("popover-type").innerHTML+"&id=" + $("popover-id").innerHTML, ($$("#title h3")[0].innerHTML.replace(/&amp;/g, 'and')));
						},
						onFailure: function () {
							error = new Element('p', { className: 'error' }).insert(
								'There was a problem getting your request from the server.'
							);
							wrapper.insert(error);
						}
					});

					overlay.show();
					open = true;
				}
			},
			
			hide: function () {
				if (moreInfo.isOpen()) {
					overlay.hide();

					// reset pre-display defaults
					if (content) content.remove();
					if (actions) actions.remove();
					if (ad) ad.remove();
					if (error) error.remove();
					content = ad = actions = error = undefined;
					
					wrapper.className = 'description';
					open = false;
				}
			},

			print: function () {
				$(document.body).addClassName('print-info');
				window.print();
			},

			unload: function () {

			}
		}
	}();

	// swfaddress utilities / event dispatching
	var addressing = function () {
		var currentSearch, currentInfo;

		return {
			init: function () {
				currentSearch = 'none',
				SWFAddress.addEventListener(SWFAddressEvent.CHANGE, addressing.dispatch);
			},

			setURL: function (params) {
				var state = unescape(SWFAddress.getValue()),
					changed = false;

				params = $H(params);
				params.each( function (pair) {
					var value, test,
						search = new RegExp(pair.key + ':([^/]+)/');

					value = pair.value ? pair.key + ':' + pair.value + '/' : '';
					test = state.match(search);

					if ( test && test[0] ) {
						if (test[0] != value) {
							state = state.replace(test[0], value);
							changed = true;
						}
					} else {
						state += value;
						changed = true;
					}
				});

				if (changed) {
					SWFAddress.setValue(state);
				}
			},

			dispatch: function () {
				var states = unescape(SWFAddress.getValue()),
					searchString, info;

				// check for search and filter settings
				searchString = states.match(/search:([\w\s\=\*\.\$,&-']+)\//);
				if (searchString && searchString[1]) {
					searchString = searchString[1];
					search.checkFormState(searchString);
				} else {
					searchString = 'none';
				}

				if (currentSearch != searchString) {
					currentSearch = searchString;

					if (searchString == 'none') {
						locationList.clear();
						search.clear();
						moreInfo.hide();
						map.reset();
					} else {
						search.submit(searchString, 0);
					}
				}

				// check for info box
				info = states.match(/info:([\d]+)\//);
				if (info && info[1]) {
					// make sure there's something else set in the URL first
					if (!addressing.checkURL('search')) {
						addressing.setURL({search: search});
					} else {
						info = parseInt(info[1]);
						if (currentInfo != info) {
							currentInfo = info;
							moreInfo.show(info);
						}
					}
				} else {
					if (currentInfo != undefined) {
						currentInfo = undefined;
						moreInfo.hide();
					}
				}
			},

			checkURL: function (param) {
				return unescape(SWFAddress.getValue()).match(param);
			}
		}
	}();



	// finally, load the object interfaces on domready
	document.observe('dom:loaded', function () {
		addressing.init();
		moreInfo.init()
		search.init();
		locationList.init();
		map.init();
	});

	Element.observe(window, 'unload', function () {
		locationList.unload();
		search.unload()
		moreInfo.unload();
		map.unload();
	});



	// utility functions

	// preload png backgrounds for floating windows to avoid weird visual artifacts when displayed
	var preloadBG = function (node) {
		// TODO
	}

	// injects JSON 'data' into HTML elements within 'node' which are tagged with rel attributes that matches
	// a key in 'data'
	var injectData = function (node, data) {
		// pick nodes to populate based on the 'rel' attribute
		node.getElementsBySelector('*[rel]').each( function (el) {
			var ptr = data,
				suffix = el.select('.suffix');

			// the data might have associations, resulting in nested objects represented by .'s in the rel string;
			// split on .'s and resolve reference to the data object from the outside in
			el.readAttribute('rel').split('.').each( function (assoc) {
				if (ptr && ptr[assoc]) {
					ptr = ptr[assoc];

				// if it's not a valid reference, then there's something wrong with the rel name;
				// skip this particular HTML element
				} else {
					ptr = null;
					return;
				}
			});

			if (ptr) {
				// if there's suffix info, insert data before it
				if (suffix && suffix[0]) {
					suffix[0].insert({ before: ptr });
				} else {
					el.insert(ptr);
				}
			}
		});
	}

	// Douglas Crawford's function to fix some memory leak issues in pre-patched IE6
	function purge(d) {
		var a = d.attributes, i, l, n;
		if (a) {
			l = a.length;
			for (i = 0; i < l; i += 1) {
				n = a[i].name;
				if (typeof d[n] === 'function') {
					d[n] = null;
				}
			}
		}
		a = d.childNodes;
		if (a) {
			l = a.length;
			for (i = 0; i < l; i += 1) {
				purge(d.childNodes[i]);
			}
		}
	}

} // end mapSearch
