if(!dojo.global.GoogleMaps){
	
dojo.declare("GoogleMaps", null, {
	
	/**
	 * instances of google.maps.Map corresponding to node ID
	 */
	maps_instances: {},
	
	map_controls_instances: {},
	maps_markers: {},
	
	info_windows: {},
	marker_bounds: {}, 

	
	/**
	 * google.maps.Geocoder
	 */
	geocoder: null,
	
	/**
	 * Default options if none are set in getMapInstance()
	 */
	default_options: {
		zoom: 8,
	    center: new google.maps.LatLng(50.0878114, 14.4204598), //Praha, Česká republika
	    mapTypeId: google.maps.MapTypeId.ROADMAP
	},
	
	original_center: null,
	original_zoom: null,
	
	constructor: function(){

	},
	
	/**
	 * 
	 * @param int canvas_id
	 * @return google.maps.Map
	 */
	getMapInstance: function(canvas_id, options){
		if(!this.maps_instances[canvas_id]){
			if(typeof(google.maps.Map) == "undefined"){
				console.error("Google maps API not found");
				return null;
			}
			var el = dojo.byId(canvas_id);
			if(!el){
				console.error("Canvas element with id '"+canvas_id+"' not found");
				return null;
			} else {
				if(!options) options = this.default_options;
				this.original_center = options.center;
				this.original_zoom = options.zoom;
				this.maps_instances[canvas_id] = new google.maps.Map(el,options);
				this.info_windows[canvas_id] = new google.maps.InfoWindow();
				this.marker_bounds[canvas_id] = new google.maps.LatLngBounds();
			}
		}
		return this.maps_instances[canvas_id];
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param google.maps.LatLng | string location
	 * @param int zoom
	 */
	setMapCenter: function(map_ID, location, zoom){
		var _this = this;
		if(location instanceof google.maps.LatLng){
			_this.getMapInstance(map_ID).setCenter(location, zoom);
		} else {
			_this.getLocationByAddress(location, function(results, address){
				_this.getMapInstance(map_ID).setCenter(results[0].geometry.location, zoom);
			});
		}
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @return google.maps.LatLng
	 */
	getMapCenter: function(map_ID){
		return this.getMapInstance(map_ID).getCenter();
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param int zoom
	 */
	setMapZoom: function(map_ID, zoom){
		this.getMapInstance(map_ID).setZoom(zoom);
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @return int
	 */
	getMapZoom: function(map_ID){
		this.getMapInstance(map_ID).getZoom();
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param ['HYBRID', 'ROADMAP', 'SATELLITE', 'TERRAIN'] mapType 
	 * @return bool
	 */
	setMapType: function(map_ID, mapType){
		if(!google.maps.MapTypeId[mapType]){
			console.error("Invalid map type '"+mapType+"' selected");
			return false;
		}
		this.getMapInstance(map_ID).setMapTypeId(google.maps.MapTypeId[mapType]);
		return true;
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @return google.maps.MapTypeId.*
	 */
	getMapType: function(map_ID){
		return this.getMapInstance(map_ID).getMapTypeId();
	},
	
	/**
	 * 
	 * @param string address
	 * @param function onFetch(google.maps.GeocoderResult results[], google.maps.GeocoderStatus status)
	 * @return
	 */
	getLocationByAddress: function(address, onFetch){
		var _this = this;
		if(!this.geocoder) this.geocoder = new google.maps.Geocoder();
		this.geocoder.geocode( 
			{ 'address': address },
			/**
			 * results: google.maps.GeocoderResult object
			 * status: google.maps.GeocoderStatus
			 */
			function(results, status) {
				if (status == google.maps.GeocoderStatus.OK) {
					//console.debug(results);
					if(onFetch){
						onFetch(results, address);
					} else {
						console.log("Found "+results.length+" locations of '"+address+"':");
						var latlng;
						for(var i in results){
							latlng = results[i].geometry.location;
							console.log(i+") ["+latlng.lat()+","+latlng.lng()+"], full address: "+results[i].formatted_address);
						}
						console.debug(results);
					}
				} else {
					var reason="";
					switch(status){
						case google.maps.GeocoderStatus.ERROR:  reason="There was a problem contacting the Google servers."; break;
						case google.maps.GeocoderStatus.INVALID_REQUEST:  reason="GeocoderRequest was invalid."; break;
						case google.maps.GeocoderStatus.OVER_QUERY_LIMIT:  reason="The webpage has gone over the requests limit in too short a period of time."; break;
						case google.maps.GeocoderStatus.REQUEST_DENIED:  reason="The webpage is not allowed to use the geocoder."; break;
						case google.maps.GeocoderStatus.UNKNOWN_ERROR:  reason="A geocoding request could not be processed due to a server error. The request may succeed if you try again."; break;
						case google.maps.GeocoderStatus.ZERO_RESULTS:  reason="No result was found for this GeocoderRequest."; break;
						default: break;
					}
					console.error("Address '"+address+"' not found. Reason: "+reason);
				}
		});
	},
	
	getAddressByLocation: function(location, onFetch){
		var _this = this;
		if(!this.geocoder) this.geocoder = new google.maps.Geocoder();
		if(!(location instanceof google.maps.LatLng)){
			if(typeof(location) == "string"){
				var latlngStr = location.split(",",2);
			    var lat = parseFloat(dojo.trim(latlngStr[0]));
			    var lng = parseFloat(dojo.trim(latlngStr[1]));
			    if(isNaN(lat) || isNaN(lng)){
			    	console.error("Invalid location format - must be instance of google.maps.LatLng, {lat: ...,lng: ...} or string like '12.730885,-12.345678' (lat,lng)");
			    	return false;
			    }
			    location = new google.maps.LatLng(lat, lng); 
			} else {
				if(typeof(location["lat"]) == "undefined" || typeof(location["lng"]) == "undefined"){
					console.error("Invalid location format - must be instance of google.maps.LatLng, {lat: ...,lng: ...} or string like '12.730885,-12.345678' (lat,lng)");
					return false;
				}
				location = new google.maps.LatLng(location.lat, location.lng); 
			}
		}
		this.geocoder.geocode( 
			{ 'latLng': location },
			/**
			 * results: google.maps.GeocoderResult object
			 * status: google.maps.GeocoderStatus
			 */
			function(results, status) {
				if (status == google.maps.GeocoderStatus.OK) {
					//console.debug(results);
					if(onFetch){
						onFetch(results, location);
					} else { 
						console.log("Found "+results.length+" objects at location ["+location.lat()+","+location.lng()+"]:");
						for(var i in results){
							console.log(i+") "+results[i].formatted_address);
						}
						console.debug(results);
					}
				} else {
					var reason="";
					switch(status){
						case google.maps.GeocoderStatus.ERROR:  reason="There was a problem contacting the Google servers."; break;
						case google.maps.GeocoderStatus.INVALID_REQUEST:  reason="GeocoderRequest was invalid."; break;
						case google.maps.GeocoderStatus.OVER_QUERY_LIMIT:  reason="The webpage has gone over the requests limit in too short a period of time."; break;
						case google.maps.GeocoderStatus.REQUEST_DENIED:  reason="The webpage is not allowed to use the geocoder."; break;
						case google.maps.GeocoderStatus.UNKNOWN_ERROR:  reason="A geocoding request could not be processed due to a server error. The request may succeed if you try again."; break;
						case google.maps.GeocoderStatus.ZERO_RESULTS:  reason="No result was found for this GeocoderRequest."; break;
						default: break;
					}
					console.error("No objects found at location ["+location.lat()+","+location.lng()+"]. Reason: "+reason);
				}
		});
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param float x
	 * @param float y
	 */
	panMapBy: function(map_ID, x, y){
		this.getMapInstance(map_ID).panBy(x, y);
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param google.maps.LatLng | string location
	 */
	panMapTo: function(map_ID, location){
		var _this = this;
		if(location instanceof google.maps.LatLng){
			_this.getMapInstance(map_ID).panTo(location);
		} else {
			_this.getLocationByAddress(location, function(results, address){
				_this.getMapInstance(map_ID).panTo(results[0].geometry.location);
			});
		}
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param eventType event
	 * @param function onEvent
	 * @return eventHandlerID
	 * 
	 * Event types:
	Events  		Arguments  	Description
	bounds_changed 	None 		This event is fired when the viewport bounds have changed.
	center_changed 	None 		This event is fired when the map center property changes.
	click 			MouseEvent 	This event is fired when the user clicks on the map (but not when they click on a marker or infowindow).
	dblclick 		MouseEvent 	This event is fired when the user double-clicks on the map. Note that the click event will also fire, right before this one.
	drag 			None 		This event is repeatedly fired while the user drags the map.
	dragend 		None 		This event is fired when the user stops dragging the map.
	dragstart 		None 		This event is fired when the user starts dragging the map.
	idle 			None 		This event is fired when the map becomes idle after panning or zooming.
	maptypeid_changed 	None 	This event is fired when the mapTypeId property changes.
	mousemove 		MouseEvent 	This event is fired whenever the user's mouse moves over the map container.
	mouseout 		MouseEvent 	This event is fired when the user's mouse exits the map container.
	mouseover 		MouseEvent 	This event is fired when the user's mouse enters the map container.
	resize 			None 		Developers should trigger this event on the map when the div changes size: google.maps.event.trigger(map, 'resize') .
	rightclick 		MouseEvent 	This event is fired when the DOM contextmenu event is fired on the map container.
	tilesloaded 	None 		This event is fired when the visible tiles have finished loading.
	zoom_changed 	None 		This event is fired when the map zoom property changes.
	 */
	connectMapEvent: function(map_ID, event, onEvent){
		google.maps.event.addListener(this.getMapInstance(map_ID), event, onEvent);
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param bool navigationControl
	 * @param bool mapTypeControl
	 * @param bool scaleControl
	 */
	setMapControls: function(map_ID, navigationControl, mapTypeControl, scaleControl){
		if(navigationControl === undefined) navigationControl = true;
		if(mapTypeControl === undefined) mapTypeControl = true;
		if(scaleControl === undefined) scaleControl = true;
		this.getMapInstance(map_ID).setOptions({
			navigationControl: navigationControl,
			mapTypeControl: mapTypeControl,
			scaleControl: scaleControl
		});
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param ['SMALL','ZOOM_PAN','ANDROID','DEFAULT'] style
	 * @params ['TOP','TOP_LEFT','TOP_RIGHT','BOTTOM','BOTTOM_LEFT','BOTTOM_RIGHT','LEFT','RIGHT'] position
	 * @return bool
	 */
	setMapNavigationControlOptions: function(map_ID, style, position){
		var options={};
		if(style){
			if(typeof(google.maps.NavigationControlStyle[style]) == "undefined"){
				console.error("Invalid NavigationControlStyle '"+style+"' selected");
				return false;
			}
			options[style] = google.maps.NavigationControlStyle[style];
		}
		
		if(position){
			if(typeof(google.maps.ControlPosition[position]) == "undefined"){
				console.error("Invalid ControlPosition '"+position+"' selected");
				return false;
			}
			options[position] = google.maps.ControlPosition[position];
		}
		
		this.getMapInstance(map_ID).setOptions({
			navigationControlOptions: options
		});
		return true;
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param ['HORIZONTAL_BAR','DROPDOWN_MENU','DEFAULT '] style
	 * @params ['TOP','TOP_LEFT','TOP_RIGHT','BOTTOM','BOTTOM_LEFT','BOTTOM_RIGHT','LEFT','RIGHT'] position
	 * @return bool
	 */
	setMapTypeControlOptions: function(map_ID, style, position){
		var options = {};
		if(style){
			if(typeof(google.maps.MapTypeControlStyle[style]) == "undefined"){
				console.error("Invalid MapTypeControlStyle '"+style+"' selected");
				return false;
			}
			options[style] = google.maps.MapTypeControlStyle[style];
		}
		
		if(position){
			if(typeof(google.maps.ControlPosition[position]) == "undefined"){
				console.error("Invalid ControlPosition '"+position+"' selected");
				return false;
			}
			options[position] = google.maps.ControlPosition[position];
		}
		
		
		this.getMapInstance(map_ID).setOptions({
			mapTypeControlOptions: options
		});
		return true;
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param ['TOP','TOP_LEFT','TOP_RIGHT','BOTTOM','BOTTOM_LEFT','BOTTOM_RIGHT','LEFT','RIGHT'] position
	 * @return bool
	 */
	setMapScaleControlOptions: function(map_ID, position){
		var options = {};		
		if(position){
			if(typeof(google.maps.ControlPosition[position]) == "undefined"){
				console.error("Invalid ControlPosition '"+position+"' selected");
				return false;
			}
			options[position] = google.maps.ControlPosition[position];
		}
		
		
		this.getMapInstance(map_ID).setOptions({
			scaleControlOptions: options
		});
		return true;
	},
	
	/**
	 * 
	 * @param string map_ID
	 * @param string control_ID
	 * @param ['TOP','TOP_LEFT','TOP_RIGHT','BOTTOM','BOTTOM_LEFT','BOTTOM_RIGHT','LEFT','RIGHT'] position
	 * @param string title
	 * @param HTML content
	 * @param int index
	 * @param object options
	 * @param function onCreate
	 * @return control instance object
	 * 
	 * Options:
	 * 	ctrlDiv_style: array of CSS style for container
	 *  controlUI_style: array of CSS style for controlUI
	 *  controlContent_style: array of CSS style for controlContent
	 *  ctrlDiv_events: DOM events for container (like: {click: function(evt){ ... }, dblclick: function(evt){ ... })
	 *  controlUI_events: DOM events for controlUI (like: {click: function(evt){ ... }, dblclick: function(evt){ ... })
	 *  controlContent_events: DOM events for controlContent (like: {click: function(evt){ ... }, dblclick: function(evt){ ... })
	 */
	createMapCustomControl: function(map_ID, control_ID, position, title, content, index, options, onCreate){
		if(!index) index = 1;
		if(!options) options = {};
		if(!this.maps_instances[map_ID]) this.getMapInstance(map_ID);
		
		if(typeof(google.maps.ControlPosition[position]) == "undefined"){
			console.error("Invalid ControlPosition '"+position+"' selected");
			return false;
		}
		
		var ctrlDiv = dojo.doc.createElement("DIV");
		ctrlDiv.index = index;
		ctrlDiv.id = "map_"+map_ID+"_control_"+control_ID;
		
		if(options["ctrlDiv_style"]){
			for(var style in options.ctrlDiv_style){
				ctrlDiv.style[style] = options.ctrlDiv_style[style];
			}
		}
		
		var controlUI = dojo.doc.createElement('DIV');
		controlUI.title = title;
		controlUI.id = ctrlDiv.id+"_controlUI";
		
		if(options["controlUI_style"]){
			for(var style in options.controlUI_style){
				controlUI.style[style] = options.controlUI_style[style];
			}
		}
		
		
		ctrlDiv.appendChild(controlUI);
		
		var controlContent = dojo.doc.createElement('DIV');
		controlContent.id = ctrlDiv.id+"_controlContent";
		
		if(options["controlContent_style"]){
			for(var style in options.controlContent_style){
				controlContent.style[style] = options.controlContent_style[style];
			}
		}

		controlContent.innerHTML = content;
		controlUI.appendChild(controlContent);
		
		if(!this.map_controls_instances[map_ID]){
			this.map_controls_instances[map_ID] = {};
		}
		
		this.map_controls_instances[map_ID][control_ID] = {
			id: control_ID,
			map_ID: map_ID,
			index: index,
			title: title,
			options: options,
			container: ctrlDiv,
			controlUI: controlUI,
			controlContent: controlContent
		};
		
		if(options["ctrlDiv_events"]){
			for(var event in options.ctrlDiv_events){
				google.maps.event.addDomListener(ctrlDiv, event, options.ctrlDiv_events[event]);
			}
		}
		
		if(options["controlUI_events"]){
			for(var event in options.controlUI_events){
				google.maps.event.addDomListener(controlUI, event, options.controlUI_events[event]);
			}
		}
		
		if(options["controlContent_events"]){
			for(var event in options.controlContent_events){
				google.maps.event.addDomListener(controlContent, event, options.controlContent_events[event]);
			}
		}

		this.maps_instances[map_ID].controls[google.maps.ControlPosition[position]].push(ctrlDiv);
		if(onCreate){
			onCreate(this.map_controls_instances[map_ID][control_ID]);
		}
		return this.map_controls_instances[map_ID][control_ID];
	},
	
	getCustomMapControl: function(map_ID, control_ID){
		if(typeof(this.map_controls_instances[map_ID][control_ID]) == "undefined"){
			console.error("Map '"+map_ID+"' custom control '"+control_ID+"' not found");
			return null;
		}
		return this.map_controls_instances[map_ID][control_ID];
	},
	
	createMarker: function(map_ID, marker_ID, position, title, infowindow_content, markerOptions, infoWindowOptions, auto_fit_bounds){
		var _this = this;
		if(!this.maps_instances[map_ID]) this.getMapInstance(map_ID);
		if(!this.maps_markers[map_ID]) this.maps_markers[map_ID] = {};
		if(auto_fit_bounds === undefined) auto_fit_bounds = true;
		if(!markerOptions) markerOptions = {};
		markerOptions.title = title;
		markerOptions.position = position;
		markerOptions.map = this.maps_instances[map_ID];
						
		if(!infoWindowOptions) infoWindowOptions = {};
		infoWindowOptions.content = infowindow_content;
				
		this.maps_markers[map_ID][marker_ID] = new google.maps.Marker(markerOptions);
		google.maps.event.addListener(_this.maps_markers[map_ID][marker_ID], "click", function(){			
			_this.info_windows[map_ID].setOptions(infoWindowOptions);
			_this.info_windows[map_ID].open(_this.maps_instances[map_ID], _this.maps_markers[map_ID][marker_ID]);
		});
		
		this.marker_bounds[map_ID].extend(position);
		if(auto_fit_bounds){
			this.maps_instances[map_ID].fitBounds(this.marker_bounds[map_ID]);
		}

		return this.maps_markers[map_ID][marker_ID];
	},
	
	getMarkerExists: function(map_ID, marker_ID){
		return typeof(this.maps_markers[map_ID][marker_ID]) != "undefined" &&
				this.maps_markers[map_ID][marker_ID] instanceof google.maps.Marker;
	},
	
	getMarker: function(map_ID, marker_ID){
		if(!this.getMarkerExists(map_ID, marker_ID)){
			console.error("Marker '"+marker_ID+"' not found in map '"+map_ID+"'");
			return false;
		}
		return this.maps_markers[map_ID][marker_ID];
	},
	
	hideMarker: function(map_ID, marker_ID){
		this.getMarker(map_ID, marker_ID).setMap(null);
	},
	
	showMarker: function(map_ID, marker_ID){
		this.getMarker(map_ID, marker_ID).setMap(this.getMapInstance(map_ID));
	},
	
	destroyMarker: function(map_ID, marker_ID, auto_fit){
		this.hideMarker(map_ID, marker_ID);
		delete this.maps_markers[map_ID][marker_ID];
		this.refreshBounds(map_ID, auto_fit);
	},
	
	destroyMarkers: function(map_ID){
		if(this.maps_markers[map_ID]){
			for(var marker_ID in this.maps_markers[map_ID]){
				this.hideMarker(map_ID, marker_ID);
				this.destroyMarker(map_ID, marker_ID, false);
			}
		}
		this.setMapCenter(map_ID, this.original_center, this.original_zoom);
	},
	
	refreshBounds: function(map_ID, auto_fit){
		if(auto_fit === undefined) auto_fit = true;
		this.marker_bounds[map_ID] = new google.maps.LatLngBounds();
		if(this.maps_markers[map_ID]){
			for(var marker_ID in this.maps_markers[map_ID]){
				var marker = this.maps_markers[map_ID][marker_ID];
				if(marker instanceof google.maps.Marker){
					this.marker_bounds[map_ID].extend(marker.position);
				}
			}
		}
		if(auto_fit){
			this.maps_instances[map_ID].fitBounds(this.marker_bounds[map_ID]);
		}
	},
	
	
	createMarkerAtAddress: function(map_ID, marker_ID, address, title, infowindow_content, markerOptions, infoWindowOptions, auto_fit_bounds, show_all_matches, onFetch){
		var _this = this;
		if(show_all_matches === undefined) show_all_matches = true;
		this.getLocationByAddress(address, function(results, addr){
			for(var i in results){
				var location = results[i].geometry.location;
				if(!title) title = results[i].formatted_address;
				if(!infowindow_content) infowindow_content = results[i].formatted_address;
				
				_this.createMarker(map_ID, marker_ID, location, title, infowindow_content, markerOptions, infoWindowOptions, auto_fit_bounds);		
				if(i == 0 && !show_all_matches) break;
			}
			if(onFetch){
				onFetch(results);
			}
		});
	},
	
	createMarkers: function(map_ID, markers, auto_fit_bounds, show_all_matches){
		if(auto_fit_bounds === undefined) auto_fit_bounds = true;
		if(show_all_matches === undefined) show_all_matches = true;
		for(var marker_ID in markers){
			var marker = markers[marker_ID];
			if(marker.address !== undefined){
				this.createMarkerAtAddress(map_ID, marker_ID, marker.address, marker.title, marker.infowindow_content, marker.markerOptions, marker.infoWindowOptions, auto_fit_bounds, show_all_matches);
			} else if(marker.position !== undefined) {
				if(!(marker.position instanceof google.maps.LatLng)){
					
					if(typeof(marker.position) == "string"){
						var latlngStr = marker.position.split(",",2);
					    var lat = parseFloat(dojo.trim(latlngStr[0]));
					    var lng = parseFloat(dojo.trim(latlngStr[1]));
					    if(isNaN(lat) || isNaN(lng)){
					    	console.error("Invalid location format for marker '"+marker_ID+"' - must be instance of google.maps.LatLng, {lat: ...,lng: ...} or string like '12.730885,-12.345678' (lat,lng)");
					    	continue;
					    }
					    marker.position = new google.maps.LatLng(lat, lng); 
					} else {
						if(typeof(marker.position["lat"]) == "undefined" || typeof(marker.position["lng"]) == "undefined"){
							console.error("Invalid location format for marker '"+marker_ID+"' - must be instance of google.maps.LatLng, {lat: ...,lng: ...} or string like '12.730885,-12.345678' (lat,lng)");
							continue;
						}
						marker.position = new google.maps.LatLng(marker.position.lat, marker.position.lng); 
					}
				}
				this.createMarker(map_ID, marker_ID, marker.position, marker.title, marker.infowindow_content, marker.markerOptions, marker.infoWindowOptions, false);
			} else {
				console.error("Marker '"+marker_ID+"' has no address or position specified");
			}
		}
		if(auto_fit_bounds){
			this.maps_instances[map_ID].fitBounds(this.marker_bounds[map_ID]);
		}
	},
	
	createMarkersByJsonUrl: function(map_ID, url, onFetch, auto_fit_bounds, show_all_matches){
		var _this = this;
		dojo.xhrGet({
            url: url,
            handleAs: "json",
            timeout: 30000,
            load: function(response, ioArgs) {
                _this.createMarkers(map_ID, response, auto_fit_bounds, show_all_matches);
                if(onFetch){
                	onFetch(response, map_ID);
                }
                return response;
            },

            // Event handler on errors:
            error: function(response, ioArgs) {
                console.error("Failed getting JSON markers from '"+url+"'");
                console.debug(response);
                return response;
            }
        });
	}
	
	
	
	
	
	
});

dojo.global.GoogleMaps = new GoogleMaps();

}