//>>built
define("dojox/geo/charting/Map", ["dojo/_base/lang","dojo/_base/array","dojo/_base/declare","dojo/_base/html","dojo/dom",
		"dojo/dom-geometry","dojo/dom-class", "dojo/_base/xhr","dojo/_base/connect","dojo/_base/window", "dojox/gfx",
		"dojox/geo/charting/_base","dojox/geo/charting/Feature","dojox/geo/charting/_Marker","dojo/number","dojo/_base/sniff"],
  function(lang, arr, declare, html, dom, domGeom, domClass, xhr, connect, win, gfx, base, 
		   Feature, Marker, number, has) {

	return declare("dojox.geo.charting.Map", null, {
	//	summary:
	//		Map widget interacted with charting.
	//	description:
	//		Support rendering Americas, AsiaPacific, ContinentalEurope, EuropeMiddleEastAfrica,
	//		USStates, WorldCountries, and WorldCountriesMercator by default.
	//	example:
	//	|	var usaMap = new dojox.geo.charting.Map(srcNode, "dojotoolkit/dojox/geo/charting/resources/data/USStates.json");
	//	|	<div id="map" style="width:600px;height:400px;"></div>
	
	//	defaultColor: String
	//		Default map feature color, e.g: "#B7B7B7"
	defaultColor:"#B7B7B7",
	//	highlightColor: String
	//		Map feature color when mouse over it, e.g: "#"
	highlightColor:"#D5D5D5",
	//	series: Array
	//		stack to data range, e.g: [{name:'label 1', min:20, max:70, color:'#DDDDDD'},{...},...]
	series:[],
	dataBindingAttribute:null,
	dataBindingValueFunction:null,
	dataStore:null,
	showTooltips: true,
	enableFeatureZoom: true,
	colorAnimationDuration:0,
	_idAttributes:null,
	_onSetListener:null,
	_onNewListener:null,
	_onDeleteListener:null,
	constructor: function(/*HTML Node*/container, /*String or Json object*/shapeData){
		//	container:
		//		map container html node/id
		//	shapeData:
		//		map shape data json object, or url to json file
		
		html.style(container, "display", "block");
		
		this.container = container;
		var containerBounds = this._getContainerBounds();
		// get map container coords
		this.surface = gfx.createSurface(container, containerBounds.w, containerBounds.h);
		
		this._createZoomingCursor();
		
		// add transparent background for event capture
		this.mapBackground = this.surface.createRect({x: 0, y: 0, width: containerBounds.w, height: containerBounds.w}).setFill("rgba(0,0,0,0)");
		
		this.mapObj = this.surface.createGroup();
		this.mapObj.features = {};
		
		if (typeof shapeData == "object") {
			this._init(shapeData);
		} else {
	        // load map shape file
			if (typeof shapeData == "string" && shapeData.length > 0) {
				xhr.get({
					url: shapeData,
					handleAs: "json",
					sync: true,
					load: lang.hitch(this, "_init")
				});
			}
		}
	},
	
	_getContainerBounds: function() {
		//	summary: 
		//		returns the bounds {x:, y:, w: ,h:} of the DOM node container in absolute coordinates 
		//	tags:
		//		private
		
		var position = domGeom.position(this.container,true);
		var marginBox = domGeom.getMarginBox(this.container);
		// use contentBox for correct width and height - surface spans outside border otherwise
		var contentBox = domGeom.getContentBox(this.container);
		this._storedContainerBounds = {
				x: position.x,
				y: position.y,
				w: contentBox.w || 100,
				h: contentBox.h || 100
			};
		return this._storedContainerBounds;
	},
	
	resize: function(/**boolean**/ adjustMapCenter/**boolean**/,adjustMapScale,/**boolean**/ animate) {
		//	summary: 
		//		resize the underlying GFX surface to accommodate to parent DOM Node size change
		//	adjustMapCenter: boolean
		//		keeps the center of the map when resizing the surface
		//	adjustMapScale: boolean
		//		adjusts the map scale to keep the visible portion of the map as much as possible 
		
		var oldBounds = this._storedContainerBounds; 
		var newBounds = this._getContainerBounds();
		
		if ((oldBounds.w == newBounds.w) && (oldBounds.h == newBounds.h)) {
			return;
		}
		
		// set surface dimensions, and background
		this.mapBackground.setShape({width:newBounds.w, height:newBounds.h});
		this.surface.setDimensions(newBounds.w,newBounds.h);
		
		this.mapObj.marker.hide();
		this.mapObj.marker._needTooltipRefresh = true;
		
		if (adjustMapCenter) {
			
			var mapScale = this.getMapScale();
			var newScale = mapScale;
			
			if (adjustMapScale) {
				var bbox = this.mapObj.boundBox;
				var widthFactor = newBounds.w / oldBounds.w;
				var heightFactor = newBounds.h / oldBounds.h;
				newScale = mapScale * Math.sqrt(widthFactor * heightFactor);
			}
			
			//	current map center
			var invariantMapPoint = this.screenCoordsToMapCoords(oldBounds.w/2,oldBounds.h/2);

			//	apply new parameters
			this.setMapCenterAndScale(invariantMapPoint.x,invariantMapPoint.y,newScale,animate);
		}
	},
	
	_isMobileDevice: function() {
		//	summary: 
		//		tests whether the application is running on a mobile device (android or iOS)
		//	tags:
		//		private
		return (has("safari")
				&& (navigator.userAgent.indexOf("iPhone") > -1 ||
					navigator.userAgent.indexOf("iPod") > -1 ||
					navigator.userAgent.indexOf("iPad") > -1
				)) || (navigator.userAgent.toLowerCase().indexOf("android") > -1);
	},
	
	
	setMarkerData: function(/*String*/ markerFile){
		//	summary:
		//		import markers from outside file, associate with map feature by feature id
		//		which identified in map shape file, e.g: "NY":"New York"
		//	markerFile:
		//		outside marker data url, handled as json style.
		//		data format: {"NY":"New York",.....}
		xhr.get({
			url: markerFile,
			handleAs: "json",
			handle: lang.hitch(this, "_appendMarker")
		});
	},
	
	setDataBindingAttribute: function(/*String*/prop) {
		//  summary:
		//		sets the property name of the dataStore items to use as value (see Feature.setValue function)
		//	prop:
		//		the property
		this.dataBindingAttribute = prop;
		
		// refresh data
		if (this.dataStore) {
			this._queryDataStore();
		}
	},
	
	setDataBindingValueFunction: function(/* function */valueFunction) {
		//  summary:
		//		sets the function that extracts values from dataStore items,to use as Feature values (see Feature.setValue function)
		//	prop:
		//		the function
		this.dataBindingValueFunction = valueFunction;
		
		// refresh data
		if (this.dataStore) {
			this._queryDataStore();
		}
	},
	
	
	
	_queryDataStore: function() {
		if (!this.dataBindingAttribute || (this.dataBindingAttribute.length == 0))
			return;
		
		var mapInstance = this;
		this.dataStore.fetch({
			scope: this,
			onComplete: function(items){
				this._idAttributes = mapInstance.dataStore.getIdentityAttributes({});
				arr.forEach(items, function(item) {
					var id = mapInstance.dataStore.getValue(item, this._idAttributes[0]);
					if(mapInstance.mapObj.features[id]){
						var val = null;
						var itemVal = mapInstance.dataStore.getValue(item, mapInstance.dataBindingAttribute);
						if (itemVal) {
							if (this.dataBindingValueFunction) {
								val = this.dataBindingValueFunction(itemVal);
							} else {
								if (isNaN(val)) {
									// regular parse
									val=number.parse(itemVal);
								} else {
									val = itemVal;
								}
							}
						}
						if (val)
							mapInstance.mapObj.features[id].setValue(val);
					}
				},this);						
			}
		});
	},
	
	_onSet:function(item,attribute,oldValue,newValue){
		// look for matching feature
		var id = this.dataStore.getValue(item, this._idAttributes[0]);
		var feature = this.mapObj.features[id];
		if (feature && (attribute == this.dataBindingAttribute)) {
			if (newValue)
				feature.setValue(newValue);
			else
				feature.unsetValue();
		}
	},

	_onNew:function(newItem,  parentItem){
		var id = this.dataStore.getValue(item, this._idAttributes[0]);
		var feature = this.mapObj.features[id];
		if (feature && (attribute == this.dataBindingAttribute)) {
			feature.setValue(newValue);
		}
	},
	
	_onDelete:function(item){
		var id = item[this._idAttributes[0]];
		var feature = this.mapObj.features[id];
		if (feature) {
			feature.unsetValue();
		}
	},
	
	setDataStore: function(/*ItemFileReadStore*/ dataStore, /*String*/ dataBindingProp){
		//	summary:
		//		populate data for each map feature from fetched data store
		//	dataStore:
		//		the dataStore to fetch the information from
		//	dataBindingProp:
		//		sets the property name of the dataStore items to use as value
		if (this.dataStore != dataStore) {
			// disconnect previous listener if any
			if (this._onSetListener) {
				connect.disconnect(this._onSetListener);
				connect.disconnect(this._onNewListener);
				connect.disconnect(this._onDeleteListener);
			}
			
			// set new dataStore
			this.dataStore = dataStore;
			
			// install listener on new dataStore
			if (dataStore) {
				_onSetListener = connect.connect(this.dataStore,"onSet",this,this._onSet);
				_onNewListener = connect.connect(this.dataStore,"onNew",this,this._onNew);
				_onDeleteListener = connect.connect(this.dataStore,"onDelete",this,this._onDelete);
			}
		}
		if (dataBindingProp)
			this.setDataBindingAttribute(dataBindingProp);

	},
	
	
	
	addSeries: function(/*url or Json Object*/ series){
		//	summary: 
		//		sets ranges of data values (associated with label, color) to style map data values
		//	series:
		//		array of range objects such as : [{name:'label 1', min:20, max:70, color:'#DDDDDD'},{...},...]
		
		if (typeof series == "object") {
			this._addSeriesImpl(series);
		} else {
	        // load series file
			if (typeof series == "string" && series.length > 0) {
				xhr.get({
					url: series,
					handleAs: "json",
					sync: true,
					load: lang.hitch(this, function(content){
						this._addSeriesImpl(content.series);
					})
				});
			}
		}
		
	},
	
	_addSeriesImpl: function(/*Json object*/series) {
		
		this.series = series;
		
		// refresh color scheme
		for (var item in this.mapObj.features) {
			var feature = this.mapObj.features[item];
			feature.setValue(feature.value);
		}
	},

	
	fitToMapArea: function(/*bbox: {x,y,w,h}*/mapArea,pixelMargin,animate,/* callback function */onAnimationEnd){
		//	summary: 
		//		set this component's transformation so that the specified area fits in the component (centered)
		//	mapArea: 
		//		the map area that needs to fill the component
		//	pixelMargin: int
		//		a margin (in pixels) from the borders of the Map component.
		//	animate: boolean
		//		true if the transform change should be animated
		//	onAnimationEnd: function
		//		a callback function to be executed when the animation completes (if animate set to true).
		
		if(!pixelMargin){
			pixelMargin = 0;
		}
		var width = mapArea.w,
			height = mapArea.h,
			containerBounds = this._getContainerBounds(),
			scale = Math.min((containerBounds.w - 2 * pixelMargin) / width,
							(containerBounds.h - 2 * pixelMargin) / height);
		
		this.setMapCenterAndScale(mapArea.x + mapArea.w / 2,mapArea.y + mapArea.h / 2,scale,animate,onAnimationEnd);
	},
	
	fitToMapContents: function(pixelMargin,animate,/* callback function */onAnimationEnd){
		//	summary: 
		//		set this component's transformation so that the whole map data fits in the component (centered)
		//	pixelMargin: int
		//		a margin (in pixels) from the borders of the Map component.
		//	animate: boolean
		//		true if the transform change should be animated
		//	onAnimationEnd: function
		//		a callback function to be executed when the animation completes (if animate set to true).
		
		//transform map to fit container
		var bbox = this.mapObj.boundBox;
		this.fitToMapArea(bbox,pixelMargin,animate,onAnimationEnd);
	},
	
	setMapCenter: function(centerX,centerY,animate,/* callback function */onAnimationEnd) {
		//	summary: 
		//		set this component's transformation so that the map is centered on the specified map coordinates
		//	centerX: float
		//		the X coordinate (in map coordinates) of the new center
		//	centerY: float
		//		the Y coordinate (in map coordinates) of the new center
		//	animate: boolean
		//		true if the transform change should be animated
		//	onAnimationEnd: function
		//		a callback function to be executed when the animation completes (if animate set to true).
		
		// call setMapCenterAndScale with current map scale 
		var currentScale = this.getMapScale();
		this.setMapCenterAndScale(centerX,centerY,currentScale,animate,onAnimationEnd);
		
	},
	
	_createAnimation: function(onShape,fromTransform,toTransform,/* callback function */onAnimationEnd) {
		//	summary: 
		//		creates a transform animation object (between two transforms) used internally
		//	fromTransform: dojox.gfx.matrix.Matrix2D
		//		the start transformation (when animation begins)
		//	toTransform: dojox.gfx.matrix.Matrix2D
		//		the end transormation (when animation ends)
		//	onAnimationEnd: function
		//		callback function to be executed when the animation completes.
		var fromDx = fromTransform.dx?fromTransform.dx:0;
		var fromDy = fromTransform.dy?fromTransform.dy:0;
		var toDx = toTransform.dx?toTransform.dx:0;
		var toDy = toTransform.dy?toTransform.dy:0;
		var fromScale = fromTransform.xx?fromTransform.xx:1.0;
		var toScale = toTransform.xx?toTransform.xx:1.0;
		
		var anim = gfx.fx.animateTransform({
			duration: 1000,
			shape: onShape,
			transform: [{
				name: "translate",
				start: [fromDx,fromDy],
				end: [toDx,toDy]
			},
			{
				name: "scale",
				start: [fromScale],
				end: [toScale]
			}
			]
		});

		//install callback
		if (onAnimationEnd) {
			var listener = connect.connect(anim,"onEnd",this,function(event){
				onAnimationEnd(event);
				connect.disconnect(listener);
			});
		}
		
		return anim;
	},

	
	setMapCenterAndScale: function(centerX,centerY,scale, animate,/* callback function */onAnimationEnd) {
		
		//	summary: 
		//		set this component's transformation so that the map is centered on the specified map coordinates
		//		and scaled to the specified scale.
		//	centerX: float
		//		the X coordinate (in map coordinates) of the new center
		//	centerY: float
		//		the Y coordinate (in map coordinates) of the new center
		//	scale: float
		//		the scale of the map
		//	animate: boolean
		//		true if the transform change should be animated
		//	onAnimationEnd: function
		//		a callback function to be executed when the animation completes (if animate set to true).
		
		
		// compute matrix parameters
		var bbox = this.mapObj.boundBox;
		var containerBounds = this._getContainerBounds();
		var offsetX = containerBounds.w/2 - scale * (centerX - bbox.x);
		var offsetY = containerBounds.h/2 - scale * (centerY - bbox.y);
		var newTransform = new gfx.matrix.Matrix2D({xx: scale, yy: scale, dx:offsetX, dy:offsetY});
		
		
		var currentTransform = this.mapObj.getTransform();
		
		// can animate only if specified AND curentTransform exists
		if (!animate || !currentTransform) {
			this.mapObj.setTransform(newTransform);
		} else {
			var anim = this._createAnimation(this.mapObj,currentTransform,newTransform,onAnimationEnd);
			anim.play();
		}
	},
	
	getMapCenter: function() {
		//	summary: 
		//		returns the map coordinates of the center of this Map component.
		//	returns: {x:,y:}
		//		the center in map coordinates
		var containerBounds = this._getContainerBounds();
		return this.screenCoordsToMapCoords(containerBounds.w/2,containerBounds.h/2);
	},
	
	setMapScale: function(scale,animate,/* callback function */onAnimationEnd) {
		//	summary: 
		//		set this component's transformation so that the map is scaled to the specified scale.
		//	animate: boolean
		//		true if the transform change should be animated
		//	onAnimationEnd: function
		//		a callback function to be executed when the animation completes (if animate set to true).
		
		
		// default invariant is map center
		var containerBounds = this._getContainerBounds();
		var invariantMapPoint = this.screenCoordsToMapCoords(containerBounds.w/2,containerBounds.h/2);
		this.setMapScaleAt(scale,invariantMapPoint.x,invariantMapPoint.y,animate,onAnimationEnd);
	},
	
	setMapScaleAt: function(scale,fixedMapX,fixedMapY,animate,/* callback function */onAnimationEnd) {
		//	summary: 
	    //		set this component's transformation so that the map is scaled to the specified scale, and the specified 
		//		point (in map coordinates) stays fixed on this Map component
		//	fixedMapX: float
		//		the X coordinate (in map coordinates) of the fixed screen point
		//	fixedMapY: float
		//		the Y coordinate (in map coordinates) of the fixed screen point
		//	animate: boolean
		//		true if the transform change should be animated
		//	onAnimationEnd: function
		//		a callback function to be executed when the animation completes (if animate set to true).
		
		
		var invariantMapPoint = null;
		var invariantScreenPoint = null;

		invariantMapPoint = {x: fixedMapX, y: fixedMapY};
		invariantScreenPoint = this.mapCoordsToScreenCoords(invariantMapPoint.x,invariantMapPoint.y);
		
		// compute matrix parameters
		var bbox = this.mapObj.boundBox;
		var offsetX = invariantScreenPoint.x - scale * (invariantMapPoint.x - bbox.x);
		var offsetY = invariantScreenPoint.y - scale * (invariantMapPoint.y - bbox.y);
		var newTransform = new gfx.matrix.Matrix2D({xx: scale, yy: scale, dx:offsetX, dy:offsetY});

		var currentTransform = this.mapObj.getTransform();

		// can animate only if specified AND curentTransform exists
		if (!animate || !currentTransform) {
			this.mapObj.setTransform(newTransform);
		} else {
			var anim = this._createAnimation(this.mapObj,currentTransform,newTransform,onAnimationEnd);
			anim.play();
		}
	},
	
	getMapScale: function() {
		//	summary: 
		//		returns the scale of this Map component.
		//	returns: float
		//		the scale
		var mat = this.mapObj.getTransform();
		var scale = mat?mat.xx:1.0;
		return scale;
	},
	
	mapCoordsToScreenCoords: function(mapX,mapY) {
		//	summary: 
		//		converts map coordinates to screen coordinates given the current transform of this Map component
		//	returns: {x:,y:}
		//		the screen coordinates correspondig to the specified map coordinates.
		var matrix = this.mapObj.getTransform();
		var screenPoint = gfx.matrix.multiplyPoint(matrix, mapX, mapY);
		return screenPoint;
	},
	
	screenCoordsToMapCoords: function(screenX, screenY) {
		//	summary: 
		//		converts screen coordinates to map coordinates given the current transform of this Map component
		//	returns: {x:,y:}
		//		the map coordinates corresponding to the specified screen coordinates.
		var invMatrix = gfx.matrix.invert(this.mapObj.getTransform());
		var mapPoint = gfx.matrix.multiplyPoint(invMatrix, screenX, screenY);
		return mapPoint;
	},
	deselectAll: function(){
		//	summary:
		//		deselect all features of map
		for(var name in this.mapObj.features){
			this.mapObj.features[name].select(false);
		}
		this.selectedFeature = null;
		this.focused = false;
	},
	
	_init: function(shapeData){
		
		//	summary: 
		//		inits this Map component.
		
		//transform map to fit container
		this.mapObj.boundBox = {x: shapeData.layerExtent[0],
								y: shapeData.layerExtent[1],
								w: (shapeData.layerExtent[2] - shapeData.layerExtent[0]),
								h: shapeData.layerExtent[3] - shapeData.layerExtent[1]};
		this.fitToMapContents(3);


		//	if there are "features", then implement them now.
		arr.forEach(shapeData.featureNames, function(item){
			var featureShape = shapeData.features[item];
			featureShape.bbox.x = featureShape.bbox[0];
			featureShape.bbox.y = featureShape.bbox[1];
			featureShape.bbox.w = featureShape.bbox[2];
			featureShape.bbox.h = featureShape.bbox[3];
			var feature = new Feature(this, item, featureShape);
			feature.init();
			this.mapObj.features[item] = feature;
		}, this);
		

		//	set up a marker.
		this.mapObj.marker = new Marker({}, this);
	},
	_appendMarker: function(markerData){
		this.mapObj.marker = new Marker(markerData, this);
	},
	_createZoomingCursor: function(){
		if(!dom.byId("mapZoomCursor")){
			var mapZoomCursor = win.doc.createElement("div");
			html.attr(mapZoomCursor,"id","mapZoomCursor");
			domClass.add(mapZoomCursor,"mapZoomIn");
			html.style(mapZoomCursor,"display","none");
			win.body().appendChild(mapZoomCursor);
		}
	},
	onFeatureClick: function(feature){
	},
	onFeatureOver: function(feature){
	},
	onZoomEnd:function(feature){
	}
});
});