function Map_constructor(fhiOptions){
	var mapCanvas = fhiOptions.mapCanvas || alert('must provide a map canvas');
	var initLat = fhiOptions.lat || 41.765; //initial lat 
	var initLng = fhiOptions.lng || -72.683; //initial lng
   	var initLatLng = new google.maps.LatLng(initLat,initLng); //initial point (center)
	var initZoom = fhiOptions.zoom || 12 //initial zoom
	var initMapType = fhiOptions.mapType || google.maps.MapTypeId.ROADMAP; //initial map type
	var language = fhiOptions.language || "English"; //map language
	
	var thisMap = null; //reference to the actual gMap object - NOT the JS object created by this constructor!!!
	var markers = []; //array of marker objects for map (worksites)
	var searchMarker = null; //set if the user searches for an address or town
	var geocoder = new google.maps.Geocoder(); //gMap geocoder util
	var infobox = new InfoBox(); //placeholder for custom infowindow (infobox), initialized on marker click
	//var infowindow = new google.maps.InfoWindow(); //gMap info window
	//this.closeInfoWindow = function(){ //method to close info window (created above)
	//	infowindow.close();
	//}

	this.trafficLayer = new google.maps.TrafficLayer(); //reference to the traffic overlay
	this.cleanWaterLayer = new google.maps.KmlLayer('http://fhitestsite.com/mdcroadwork/kml/locations.kmz'); //set the clean water KMZ location here
	this.tilesLoaded = false; //reference to the tiles loaded state
	
	this.initMap = function(){ //method to setup map
	    var mapOptions = { //various map options
			zoom: initZoom,
			center: initLatLng,
			mapTypeId: initMapType,
			disableDefaultUI: true,
  			keyboardShortcuts: false,
   			mapTypeControl:true,
   			mapTypeControlOptions: {"mapTypeIds": [google.maps.MapTypeId.TERRAIN,google.maps.MapTypeId.ROADMAP,google.maps.MapTypeId.SATELLITE]},
			minZoom:9,
			zoomControl: true,
   			zoomControlOptions: {"style": [google.maps.ZoomControlStyle.LARGE]}
		};
		
		thisMap = new google.maps.Map(document.getElementById(mapCanvas), mapOptions); //create the map and set thisMap

		this.tilesLoaded = google.maps.event.addListener(thisMap, 'tilesloaded', function() { //once tiles are loaded, show today's markers
			var today = $('#constructionDateCalendar').val(); //get today from the datepicker
			showMarkers(today);
		});
	
  		var trafficControlDiv = document.createElement('DIV'); //create the traffic overlay control
  		var trafficControl = new TrafficControl(trafficControlDiv, thisMap);
		thisMap.controls[google.maps.ControlPosition.RIGHT_TOP].push(trafficControlDiv); //add the traffic overlay
		//if ($('#cleanWaterProjects')[0].checked){ //if the cleanWater checkbox happens to be checked on load, show the cleanWater layer
		//	this.toggleCleanWaterLayer(true);
		//}
	}

	this.getLatLng = function(addressString){
		//this.clearMarkers(); --this is commented out for now, because we don't want to clear the markers when a search is performed, might want to uncomment later, if we want to show only markers for one town
		if (searchMarker){ //if there's an old search marker clear it for the new search
			this.clearSearchMarker();
		}
		$('#fhiNotification').dialog('close'); //hide the notification window if it's open
		if (!addressString){ //if there was no address string passed, show everything (reset)
			showMarkers();
			thisMap.panTo(initLatLng);
			return;
		}
		geocoder.geocode({ address:addressString }, function(results,status){ //send the address to google, get back "results" (location) and "status" (of the request)
			if(status==="OK"){
				if (results.length==1){
					addMarker(results[0]); //TODO: if a marker already exists in the stack for this location, use it rather than creating a new one
					//TODO: this is where we could handle less accurate results (not "ROOFTOP" from google)
				} else if (results.length>1){
					refineResults(results); //if there's more than 1 result, let the user choose which to use...
				}
			} else { //if the status isn't "OK" handle it as an error
				showError(status);
			}
		});
	}

	var refineResults = function(results){
		var resultsTable = $('<table id="refineResults">');
		$.each(results,function(n){ //put each possible address in a table to show the user
			var refineLink = $('<a>').text(results[n].formatted_address).attr('href','#'); //TODO: do we need the HREF here?
			refineLink.bind('click',function(){
				fhiMap.getLatLng(results[n].formatted_address);
				return false; //prevent page jump
			});
			$('<tr>').append($('<td>').html(refineLink)).appendTo(resultsTable);
		});
		var dialogOptions = {
			"buttons" : {
				"Cancel" : function(){
					$(this).dialog('close');
				}
			},
			"modal" : false,
			"width" : "auto", //TODO: fix the width
			"title" : "Please choose an address"
		}
		$("#fhiNotification").html(resultsTable).dialog(dialogOptions); //show the user the dialog box
	}

	var showError = function(error){
		var errorMessage = "An error has occurred. Please try again."; //generic error
		var title = "Error";
		if (error=="ZERO_RESULTS"){ //for specific types (as returned by google) add a stanza here
			errorMessage = "Please check the address and try again.";
			title = "We couldn't find that address.";
		}
		if (error=="NO_DATA"){ //for specific types (as returned by google) add a stanza here
			errorMessage = "We have no information for this date. Either there is no work planned, or it has not been reported yet. Please check back soon as we update this site often.";
			title = "No Data";
		}
		var dialogOptions = {
			"buttons" : {
				"OK" : function(){
						$(this).dialog('close');
				}
			},
			"modal" : true,
			"width" : "400px",
			"title" : title,
			"resizable" : false
		}
		$("#fhiNotification").html(errorMessage).dialog(dialogOptions); //show the user the error
	}
 
	this.clearMarkers = function(){ //utility method to remove markers (TODO: make smarter? remember markers?)
		for (var m=0;m<markers.length;m++){
			markers[m].setMap(null);
		}
		markers.length = 0;
	}
	
	this.clearSearchMarker = function(){ //utility method to remove search marker
		searchMarker.setMap(null);
	}

	var addMarker = function(point){ //add markers to an array container
		if(point.geometry){ //if this property exists, it means it's a point returned from google so...
			point.lat = point.geometry.location.lat(); //...update the properties of the gmap point, so we can use this function the same way we use it for our own points (worksites)
			point.lng = point.geometry.location.lng();
			point.work_type = false;
			point.street = "Your search location";
		}
		var pointLatLng = new google.maps.LatLng(point.lat,point.lng);
		
		//var workTypeIcon = getWorkIcon(point.work_type);
		var marker = new google.maps.Marker({ //create the marker
			position: pointLatLng,
			title:point.street
		});
		if (point.work_type){ //if this property exists, it means it's a worksite
			marker.icon = getWorkIcon(point.work_type); //add the work type icon to the marker
			google.maps.event.addListener(marker, 'click', function() {//add the click listener to the marker
				var windowTitle = '<img class="turnOffInfobox" src="' + getWorkIcon(point.work_type) + '" /><span class="street">' + point.street + '</span>';
				var windowContent = '<div class="worksite_details">';
				windowContent += '<div class="no_wrap">' + point.work_type;
				if (point.from_street && point.to_street){
					windowContent += ' between ' + point.from_street + ' and ' + point.to_street + '.';
				}
				windowContent += '</div>';
				if(point.from_date==point.to_date){
					windowContent += '<div class="no_wrap">On: ' + point.from_date + ', ' + point.work_hours + '</div>';
				} else {
					windowContent += '<div class="no_wrap">From: ' + point.from_date + ' to ' + point.to_date + ', ' + point.work_hours + '</div>';
				}
				if (point.notes){
					windowContent += '<div class="notes">' + point.notes + '</div>';
				}
				if (point.link){
					windowContent += '<div class="link">For more information: <a href="' + point.link + '">' + point.link + '</a></div>';
				}
				if (point.image){
					windowContent += '<div class="map_img"><img src="' + point.image + '" /></div>';
				}
				windowContent += '</div>';
				infobox.setContent(windowContent);
				infobox.setTitle(windowTitle);
				infobox.open(marker, thisMap);
			});
			markers.push(marker); //add the marker to the container
			marker.setMap(thisMap); //show the marker
		}
		else { //no work type, so this is a search point
			thisMap.panTo(point.geometry.location); //center the map
			searchMarker = marker; //update the searchMarker property for later
			marker.setMap(thisMap); //show the marker
			//the following is commented out because we do not hide the markers on search (hence no reason to show them again)
			//showMarkers(point.address_components[0].long_name); //TODO: CAN WE DEPEND ON THIS RETURN VALUE FROM GOOGLE?!?!?!
		}
	}
	
	var getWorkIcon = function(workType){
		switch (workType.toLowerCase()){
			case "road closed":
				workTypeIcon = workIcons.IMG_ROAD_CLOSED;
				break;
			case "lane closed":
				workTypeIcon = workIcons.IMG_LANE_CLOSED;
				break;
			case "work in roadway":
				workTypeIcon = workIcons.IMG_WORK_IN_ROADWAY;
				break;
			case "utility work by others":
				workTypeIcon = workIcons.IMG_UTILITY_WORK_BY_OTHERS;
				break;
			case "paving":
				workTypeIcon = workIcons.IMG_PAVING;
				break;
			case "night work":
				workTypeIcon = workIcons.IMG_NIGHT_WORK;
				break;
			default:
				workTypeIcon = workIcons.IMG_ROAD_CLOSED; //TODO: should there be a default?
				break;
		}
		return workTypeIcon;
	}
	
	var showMarkers = function(date,city){ //NOTE: city is not used right now
		if (fhiMap.tilesLoaded){ //if tilesLoaded is anything but false, it means it's our first time using this function so...
			google.maps.event.removeListener(fhiMap.tilesLoaded); //...remove the tilesLoaded listener (only needed onLoad)
		}
		date = parseInt(date,10);
		var totalWorksites = 0;
		$.each(workLocations.worksites,
			function(index,worksite){
				if (date){ //if date is provided, use it to filter
					if (worksite.start_date_MS13<=date && date<=worksite.end_date_MS13){ //date is between the start and end date inclusive
						addMarker(worksite); //TODO: don't create new markers every time, use existing
						totalWorksites++;
					}
				} else {
					addMarker(worksite);
				}
			}
		);
		if (!totalWorksites){
			showError('NO_DATA');
		}
	}
	
	this.getMap = function(){ //return a reference to the Gmap object
		return thisMap;
	}
	
	//this.getInfowindow = function(){ //return a reference to this map's info window
	//	return infowindow;
	//}
	
	this.getInfobox = function(){ //return a reference to this map's info window
		return infobox;
	}
	
	this.filterDate = function(date){ //external method to fiter markers by date
		this.clearMarkers();
		showMarkers(date);
	}
	
	this.toggleCleanWaterLayer = function(isChecked){ //external method to toggle clean water layer
		if (isChecked){
			this.cleanWaterLayer.setMap(thisMap);
		} else {
			this.cleanWaterLayer.setMap(null);
		}
	}
}

function TrafficControl(controlDiv, map) {
	controlDiv.style.padding = '5px'; //set from edge of map
	
	var controlUI = document.createElement('DIV'); // create the control button
	controlUI.className = 'trafficButton';
	controlUI.title = 'Toggle Traffic Display';
	controlDiv.appendChild(controlUI);
	
	var controlText = document.createElement('DIV'); // create the control button text
	controlText.className = 'trafficButtonText';
	controlText.innerHTML = 'Traffic';
	controlUI.appendChild(controlText);

	var visibility = false; //hold the visibility state
	
	google.maps.event.addDomListener(controlUI, 'click', function() { // toggle traffic overlay
		if (visibility){
			controlUI.className = 'trafficButton'; //TODO: we can save some code by using jquery to add/remove a highlight class as needed (reduces css too)
			controlText.className = 'trafficButtonText'; //TODO: we can save some code by using jquery to add/remove a highlight class as needed (reduces css too)
			fhiMap.trafficLayer.setMap(null);
			visibility=false;
		} else {
			controlUI.className = 'trafficButtonOn'; //TODO: we can save some code by using jquery to add/remove a highlight class as needed (reduces css too)
			controlText.className = 'trafficButtonTextOn'; //TODO: we can save some code by using jquery to add/remove a highlight class as needed (reduces css too)
			fhiMap.trafficLayer.setMap(map);
			visibility=true;
		}
	});
}

var workIcons = {
	IMG_ROAD_CLOSED : "images/rwr.png",
	IMG_LANE_CLOSED : "images/rwy.png",
	IMG_WORK_IN_ROADWAY : "images/rwg.png",
	IMG_UTILITY_WORK_BY_OTHERS : "images/rwb.png",
	IMG_PAVING : "images/rwp.png",
	IMG_NIGHT_WORK : "images/rwk.png"
}
