var MapperSourceFolderPath = "scripts/Mapper/";

var MapTypes = { Road:1, Satellite:2, Hybrid:3, Physical:4 };

var MapUnits = { Miles:1, Kilometers:2 };

var MapLineStyles = { Solid:1, Dashed:2, Dotted:3 };

var MapPoint = Class.extend({
	Latitude: null,
	Longitude: null,
	init: function(Latitude, Longitude) {
		this.Latitude = parseFloat(Latitude) || 0;
		this.Longitude = parseFloat(Longitude) || 0;
	}
});

var MapPixel = Class.extend({
	X: null,
	Y: null,
	init: function(X, Y) {
		this.X = parseInt(X) || 0;
		this.Y = parseInt(Y) || 0;
	}
});

var MapColor = Class.extend({
	R: null,
	G: null,
	B: null,
	A: null,
	init: function(R, G, B, A) {
		this.R = parseInt(R) || 0;
		this.G = parseInt(G) || 0;
		this.B = parseInt(B) || 0;
		this.A = parseFloat(A) || 1;
	}
});

var MapMarkerIcon = Class.extend({
	Image: null,
	Size: null,
	Offset: null,
	Opacity: null,
	init: function(Image, Size, Offset, Opacity) {
		this.Image = Image;
		this.Size = Size;
		this.Offset = Offset || new MapPixel(Math.round(Size.X / 2), Math.round(Size.Y / 2));
		this.Opacity = parseFloat(Opacity) || 1;
	}
});

var _MapMarkerNextID = 1;
var MapMarker = Class.extend({
	init: function(Point, Content) {
		$.extend(this, {
			id: _MapMarkerNextID++,
			point: Point || new MapPoint(0, 0),
			content: Content || "",
			events: {},
			visible: true,
			icon: null,
			clickable: true,
			draggable: false,
			tooltip: null,
			printable: true,
			data: null,
			map: null
		});
	},
	ID: function() {
		return this.id;
	},
	Point: function(Point) {
		return this._Property("Point", Point);
	},
	Content: function(Content) {
		return this._Property("Content", Content);
	},
	Visible: function(Visible) {
		return this._Property("Visible", Visible);
	},
	Icon: function(Icon) {
		return this._Property("Icon", Icon);
	},
	Clickable: function(Clickable) {
		return this._Property("Clickable", Clickable);
	},
	Draggable: function(Draggable) {
		return this._Property("Draggable", Draggable);
	},
	Tooltip: function(Tooltip) {
		return this._Property("Tooltip", Tooltip);
	},
	Printable: function(Printable) {
		return this._Property("Printable", Printable);
	},
	Data: function(Data) {
		return this._Property("Data", Data);
	},
	Bind: function(Event, Callback) {
		if (this.map) {
			return this.map.MarkerBind(this.id, Event, Callback);
		} else {
			return this._Bind(Event, Callback);
		}
	},
	Unbind: function(Event, Callback) {
		if (this.map) {
			return this.map.MarkerUnbind(this.id, Event, Callback);
		} else {
			return this._Unbind(Event, Callback);
		}
	},
	ShowContent: function(Content) {
		if (this.map) {
			return this.map.MarkerShowContent(this.id, Content);
		} else {
			return this;
		}
	},
	HideContent: function() {
		return this._Method("HideContent");
	},
	ResizeContent: function() {
		return this._Method("ResizeContent");
	},
	PositionContent: function() {
		return this._Method("PositionContent");
	},
	VisibleContent: function() {
		return this._Method("VisibleContent");
	},
	ToFront: function() {
		return this._Method("ToFront");
	},
	Clone: function() {
		var m = new MapMarker();
		$.extend(m, this, {id:m.id,map:null});
		return m;
	},
	_Property: function(Name, Value) {
		if (this.map) {
			return this.map["Marker" + Name](this.id, Value);
		} else {
			if ($.defined(Value)) {
				this[Name.toLowerCase()] = Value;
				return this;
			} else {
				return this[Name.toLowerCase()];
			}
		}
	},
	_Method: function(Name) {
		if (this.map) {
			return this.map["Marker" + Name](this.id);
		} else {
			return this;
		}
	},
	_Bind: function(Event, Callback) {
		if (!this.events[Event]) {
			this.events[Event] = [];
		}
		this.events[Event].push(Callback);
		return this;
	},
	_Unbind: function(Event, Callback) {
		if (Event) {
			if (this.events[Event]) {
				if (Callback) {
					for (var i = this.events[Event].length - 1; i >= 0; i--) {
						if (this.events[Event][i] === Callback) {
							this.events[Event].splice(i, 1);
						}
					}
					if (!this.events[Event].length) {
						delete this.events[Event];
					}
				} else {
					delete this.events[Event];
				}
			}
		} else {
			this.events = {};
		}
		return this;
	}
});

var _MapLineNextID = 1;
var MapLine = Class.extend({
	init: function(Points, Color, Width, Style) {
		$.extend(this, {
			id: _MapLineNextID++,
			points: Points,
			color: Color,
			width: Width,
			style: Style || MapLineStyles.Solid
		});
	},
	ID: function() {
		return this.id;
	}
});

var _MapLayerNextID = 1;
var MapLayer = Class.extend({
	init: function(Service, Layers, Definitions, Opacity, ZIndex) {
		$.extend(this, {
			id: _MapLayerNextID++,
			service: Service,
			layers: Layers || [],
			definitions: Definitions || {},
			opacity: Opacity || 1,
			zIndex: ZIndex
		});
	},
	ID: function() {
		return this.id;
	}
});

var MapDirectionsStep = Class.extend({
	Instruction: null,
	Distance: null,
	Time: null,
	Point: null,
	init: function(Instruction, Distance, Time, Point) {
		this.Instruction = Instruction;
		this.Distance = Distance;
		this.Time = Time;
		this.Point = Point;
	}
});

var MapDirections = Class.extend({
	IDs: null,
	Callback: null,
	Distance: null,
	Time: null,
	Steps: null,
	Visible: null,
	init: function(IDs, Callback, Visible) {
		this.IDs = IDs;
		this.Callback = Callback;
		this.Visible = (Visible == false) ? false : true;
		this.Steps = [];
	}
});

var MapEvent = Class.extend({
	Marker: null,
	Point: null,
	init: function(Marker, Point) {
		this.Marker = Marker;
		this.Point = Point || (Marker ? Marker.Point() : null);
	}
});

var IMap = Class.extend({
	init: function(id) {
		$.extend(this, {
			id: id,
			container: $("#" + id),
			loaded: false,
			unit: this.DefaultParameters.unit,
			events: this.DefaultParameters.events,
			markers: this.DefaultParameters.markers,
			lines: this.DefaultParameters.lines,
			layers: this.DefaultParameters.layers,
			directions: this.DefaultParameters.directions,
			balloon: $([])
		});
	},
	Loaded: function() {
		return this.loaded;
	},
	Type: function(Type) {
		return $.defined(Type) ? this : this.DefaultParameters.type;
	},
	Unit: function(Unit) {
		if ($.defined(Unit)) {
			this.unit = Unit;
			return this;
		} else {
			return this.unit;
		}
	},
	Latitude: function(Latitude) {
		if ($.defined(Latitude)) {
			this.Point(new MapPoint(Latitude, this.Point().Longitude));
			return this;
		} else {
			return this.Point().Latitude;
		}
	},
	Longitude: function(Longitude) {
		if ($.defined(Longitude)) {
			this.Point(new MapPoint(this.Point().Latitude, Longitude));
			return this;
		} else {
			return this.Point().Longitude;
		}
	},
	Point: function(Point) {
		return $.defined(Point) ? this : this.DefaultParameters.point;
	},
	Zoom: function(Zoom) {
		return $.defined(Zoom) ? this : this.DefaultParameters.zoom;
	},
	Position: function(Point, Zoom) {
		if ($.defined(Point)) {
			this.Point(Point);
			if ($.defined(Zoom)) {
				this.Zoom(Zoom);
			}
			return this;
		} else {
			return this.Point();
		}
	},
	Extent: function(Extent) {
		if ($.defined(Extent)) {
			return this.Frame(Extent);
		} else {
			return [this.PixelToPoint(new MapPixel(0, 0)),this.PixelToPoint(new MapPixel(this.container.width(), this.container.height()))];
		}
	},
	Parameters: function(Parameters) {
		if ($.defined(Parameters)) {
			this.Type(Parameters.type);
			this.Unit(Parameters.unit);

			this.Unbind();
			for (var i in Parameters.events) {
				var e = Parameters.events[i];
				for (var j = 0; j < e.length; j++) {
					this.Bind(i, e[j]);
				}
			}

			this.ClearDirections();

			this.ClearMark();
			if (Parameters.markers.length) {
				this.Mark(Parameters.markers);
			}

			this.ClearLine();
			for (var i = 0; i < Parameters.lines.length; i++) {
				this.Line(Parameters.lines[i]);
			}

			this.ClearLayer();
			for (var i = 0; i < Parameters.layers.length; i++) {
				this.Layer(Parameters.layers[i]);
			}

			this.DirectionsReset(Parameters.directions);
			this.Position(Parameters.point, Parameters.zoom);
			return this;
		} else {
			var params = {
				type: this.Type(),
				unit: this.Unit(),
				events: this.events,
				markers: this.markers,
				lines: this.lines,
				layers: this.layers,
				directions: this.directions,
				point: this.Point(),
				zoom: this.Zoom()
			};
			return params;
		}
	},
	Load: function(Parameters) {
		if (!this.balloon.length) {
			this.balloon = this._BalloonGenerate();
		}
		this.Parameters(Parameters);
		this.loaded = true;
		var e = this.events["load"];
		if (e) {
			for (var i = 0; i < e.length; i++) {
				e[i]();
			}
		}
		return this;
	},
	Unload: function() {
		this.Hide();
		return this;
	},
	Pan: function(Pixel) {
		return this;
	},
	Show: function() {
		this.container.show();
		return this;
	},
	Hide: function() {
		this.HideContent();
		this.container.hide();
		return this;
	},
	Resize: function() {
		this._BalloonReset();
		return this;
	},
	Clear: function() {
		this.ClearLayer();
		this.ClearDirections();
		this.ClearLine();
		this.ClearMark();
		return this;
	},
	Reset: function() {
		this.Parameters(this.DefaultParameters);
		return this;
	},
	Bind: function(Event, Callback) {
		if (!this.events[Event]) {
			this.events[Event] = [];
		}
		this.events[Event].push(Callback);
		return this;
	},
	Unbind: function(Event, Callback) {
		if (Event) {
			if (this.events[Event]) {
				if (Callback) {
					for (var i = this.events[Event].length - 1; i >= 0; i--) {
						if (this.events[Event][i] === Callback) {
							this.events[Event].splice(i, 1);
						}
					}
					if (!this.events[Event].length) {
						delete this.events[Event];
					}
				} else {
					delete this.events[Event];
				}
			}
		} else {
			this.events = {};
		}
		return this;
	},
	Mark: function(Marker) {
		if ($.isArray(Marker)) {
			for (var i = 0; i < Marker.length; i++) {
				Marker[i].map = this;
				this.markers.push(Marker[i]);
			}
		} else {
			Marker.map = this;
			this.markers.push(Marker);
		}
		return this;
	},
	ClearMark: function(ID) {
		if (ID) {
			for (var i = 0; i < this.markers.length; i++) {
				if (this.markers[i].id == ID) {
					this.markers[i].map = null;
					this.markers.splice(i, 1);
					break;
				}
			}
		} else {
			this.markers = [];
		}
		this.MarkerResetDependents(ID);
		return this;
	},
	MarkerPoint: function(ID, Point) {
		var m = this._MarkerProperty(ID, "Point", Point);
		if ($.defined(Point)) {
			this.MarkerLinesReset(ID);
		}
		return m;
	},
	MarkerContent: function(ID, Content) {
		if ($.defined(Content)) {
			if (this.balloon.data("mid") == ID) {
				this.balloon.find(".mapper-balloon-content").empty().append(Content || "");
				this.ResizeContent();
				this.PositionContent();
			}
		}
		return this._MarkerProperty(ID, "Content", Content);
	},
	MarkerVisible: function(ID, Visible) {
		if (Visible == false) {
			this.MarkerResetDependents(ID);
		}
		return this._MarkerProperty(ID, "Visible", Visible);
	},
	MarkerIcon: function(ID, Icon) {
		return this._MarkerProperty(ID, "Icon", Icon);
	},
	MarkerClickable: function(ID, Clickable) {
		return this._MarkerProperty(ID, "Clickable", Clickable);
	},
	MarkerDraggable: function(ID, Draggable) {
		if ($.defined(Draggable) && Draggable == false) {
			var m = this.Markers(ID);
			m.Unbind("dragstart");
			m.Unbind("drag");
			m.Unbind("dragend");
		}
		return this._MarkerProperty(ID, "Draggable", Draggable);
	},
	MarkerTooltip: function(ID, Tooltip) {
		return this._MarkerProperty(ID, "Tooltip", Tooltip);
	},
	MarkerPrintable: function(ID, Printable) {
		return this._MarkerProperty(ID, "Printable", Printable);
	},
	MarkerData: function(ID, Data) {
		return this._MarkerProperty(ID, "Data", Data);
	},
	MarkerBind: function(ID, Event, Callback) {
		var m = this.Markers(ID);
		return m._Bind(Event, Callback);
	},
	MarkerUnbind: function(ID, Event, Callback) {
		var m = this.Markers(ID);
		return m._Unbind(Event, Callback);
	},
	MarkerShowContent: function(ID, Content) {
		this.HideContent();
		var m = this.MarkerToFront(ID);
		m.Content(Content);
		if (m.content && !m.VisibleContent()) {
			this.balloon.data("loaded", true).data("mid", ID).find(".mapper-balloon-content").empty().append(m.content);
			if (!m.printable) {
				this.balloon.addClass(Mapper.Classes.NoPrint);
			}
			this.ResizeContent();
			this.PositionContent();
			this.balloon.show().focusInput();
			var e = m.events["showcontent"];
			if (e) {
				for (var i = 0; i < e.length; i++) {
					e[i](new MapEvent(m));
				}
			}
			var e = this.events["showcontent"];
			if (e) {
				for (var i = 0; i < e.length; i++) {
					e[i](new MapEvent(m));
				}
			}
		}
		return m;
	},
	MarkerHideContent: function(ID) {
		if (this.balloon.data("mid") == ID) {
			this.HideContent();
		}
		return this.Markers(ID);
	},
	MarkerResizeContent: function(ID) {
		if (this.balloon.data("mid") == ID) {
			this.ResizeContent();
		}
		return this.Markers(ID);
	},
	MarkerPositionContent: function(ID) {
		if (this.balloon.data("mid") == ID) {
			this.PositionContent();
		}
		return this.Markers(ID);
	},
	MarkerVisibleContent: function(ID) {
		return this.balloon.data("mid") == ID ? this.VisibleContent() : false;
	},
	MarkerToFront: function(ID) {
		return this.Markers(ID);
	},
	Markers: function(ID) {
		if ($.defined(ID)) {
			var m = null;
			for (var i = 0; i < this.markers.length; i++) {
				if (this.markers[i].id == ID) {
					m = this.markers[i];
					break;
				}
			}
			return m;
		} else {
			return this.markers;
		}
	},
	MarkersFrame: function(IDs) {
		var points = [];
		for (var i = 0; i < this.markers.length; i++) {
			if (this.markers[i].visible && (!IDs || $.inArray(this.markers[i].id, IDs) >= 0)) {
				points.push(new MapPoint(this.markers[i].point.Latitude, this.markers[i].point.Longitude));
			}
		}
		return this.Frame(points);
	},
	MarkersInProximity: function(PointOrID, Pixels) {
		var prox = [];
		var isID = typeof(PointOrID) == "number";
		var px = this.PointToPixel(isID ? this.MarkerPoint(PointOrID) : PointOrID);
		for (var i = 0; i < this.markers.length; i++) {
			if (PointOrID != this.markers[i].id) {
				if (Mapper.CalculatePixelDistance(px, this.PointToPixel(this.markers[i].point)) <= Pixels) {
					prox.push(this.markers[i].id);
				}
			}
		}
		return prox;
	},
	MarkersVisibleContent: function() {
		return (this.balloon.data("mid") && this.VisibleContent()) ? this.Markers(this.balloon.data("mid")) : null;
	},
	MarkerLines: function(ID) {
		var ls = [];
		for (var i = 0; i < this.lines.length; i++) {
			var ln = this.lines[i];
			if ($.inArray(ID, ln.points) >= 0) {
				ls.push(ln.id);
			}
		}
		return ls;
	},
	MarkerLinesReset: function(ID) {
		var ls = this.MarkerLines(ID);
		for (var i = 0; i < ls.length; i++) {
			this.LinesReset(ls[i]);
		}
		return this;
	},
	MarkerDirectionsReset: function(ID) {
		if (this.directions) {
			for (var i = 0; i < this.directions.IDs.length; i++) {
				if (this.directions.IDs[i] == ID) {
					this.DirectionsReset();
					break;
				}
			}
		}
		return this;
	},
	MarkerResetDependents: function(ID) {
		if (ID) {
			this.MarkerHideContent(ID);
			this.MarkerLinesReset(ID);
			this.MarkerDirectionsReset(ID);
		} else {
			this.HideContent();
			this.LinesReset();
			this.DirectionsReset();
		}
	},
	MarkerPrimitive: function(ID) {
		return null;
	},
	Line: function(Line) {
		this.lines.push(Line);
		return this;
	},
	ClearLine: function(ID) {
		if (ID) {
			for (var i = 0; i < this.lines.length; i++) {
				if (this.lines[i].id == ID) {
					this.lines.splice(i, 1);
					break;
				}
			}
		} else {
			this.lines = [];
		}
		return this;
	},
	LinePoint: function(ID, Index, Point) {
		var ln = this.Lines(ID);
		if ($.defined(Point)) {
			ln.points[Index] = Point;
			return ln;
		} else {
			return ln.points[Index];
		}
	},
	Lines: function(ID) {
		if (ID) {
			var ln = null;
			for (var i = 0; i < this.lines.length; i++) {
				if (this.lines[i].id == ID) {
					ln = this.lines[i];
					break;
				}
			}
			return ln;
		} else {
			return this.lines;
		}
	},
	LinesReset: function(ID) {
		if (ID) {
			var ln = this.Lines(ID);
			for (var i = ln.points.length - 1; i >= 0; i--) {
				var p = ln.points[i];
				if (typeof(p) == "number") {
					var m = this.Markers(p);
					if (!m) {
						ln.points.splice(i, 1);
					}
				}
			}
			if (!ln.points.length) {
				this.ClearLine(ID);
			}
		} else {
			for (var i = this.lines - 1; i >= 0; i--) {
				this.LinesReset(this.lines[i].id);
			}
		}
		return this;
	},
	Layer: function(Layer) {
		this.layers.push(Layer);
		return this;
	},
	ClearLayer: function(ID) {
		if (ID) {
			for (var i = 0; i < this.layers.length; i++) {
				if (this.layers[i].id == ID) {
					this.layers.splice(i, 1);
					break;
				}
			}
		} else {
			this.layers = [];
		}
		return this;
	},
	Layers: function(ID) {
		if (ID) {
			var ly = null;
			for (var i = 0; i < this.layers.length; i++) {
				if (this.layers[i].id == ID) {
					ly = this.layers[i];
					break;
				}
			}
			return ly;
		} else {
			return this.layers;
		}
	},
	Directions: function(Directions) {
		if ($.defined(Directions)) {
			this.ClearDirections();
			this.directions = Directions;
			return this;
		} else {
			return this.directions;
		}
	},
	ClearDirections: function() {
		this.directions = null;
		return this;
	},
	DirectionsReset: function(Directions) {
		var d = Directions ? Directions : this.directions;
		return this.Directions(d ? new MapDirections(d.IDs, d.Callback, d.Visible) : null);
	},
	DirectionsVisible: function(Visible) {
		if ($.defined(Visible)) {
			this.directions.Visible = Visible;
			return this;
		} else {
			return this.directions.Visible;
		}
	},
	DirectionsFrame: function() {
		var points = [];
		for (var i = 0; i < this.directions.IDs.length; i++) {
			points.push(this.MarkerPoint(this.directions.IDs[i]));
		}
		for (var i = 0; i < this.directions.Steps.length; i++) {
			points.push(this.directions.Steps[i].Point);
		}
		return this.Frame(points);
	},
	ShowContent: function(Content) {
		this.HideContent();
		this.balloon.data("loaded", true).addClass("mapper-balloon-notail").find(".mapper-balloon-content").append(Content || "");
		this.ResizeContent();
		this.PositionContent();
		this.balloon.show().focusInput();
		var e = this.events["showcontent"];
		if (e) {
			for (var i = 0; i < e.length; i++) {
				e[i]();
			}
		}
		return this;
	},
	HideContent: function() {
		if (this.VisibleContent()) {
			this.balloon.data("loaded", false);
			if (this.balloon.data("mid")) {
				var m = this.Markers(this.balloon.data("mid"));
				if (m) {
					var e = m.events["hidecontent"];
					if (e) {
						for (var i = 0; i < e.length; i++) {
							e[i](new MapEvent(m));
						}
					}
				}
			}
			this.balloon.hide().removeClass("mapper-balloon-notail " + Mapper.Classes.NoPrint).css({bottom:"",left:"",width:""}).removeData().find(".mapper-balloon-content").empty();
			var e = this.events["hidecontent"];
			if (e) {
				for (var i = 0; i < e.length; i++) {
					e[i]();
				}
			}
		}
		return this;
	},
	ResizeContent: function() {
		if (this.balloon.is(":visible")) {
			var c = this.balloon.find(".mapper-balloon-content");
			this.balloon.width("").width(c.outerWidth());
			if (!this.balloon.closest(".mapper-balloon-fill").length) {
				var f = c.find(".mapper-balloon-fill:first");
				if (f.css("max-height") === "none") {
					f.height(Math.max(Math.ceil((c.innerHeight() + c.height()) / 2) - c.children().height() + f.height(), 50));
				}
			}
		} else {
			var left = this.balloon.data("mid") ? this.balloon.css("left") : "";
			this.balloon.css("left", -10000).show();
			this.ResizeContent();
			this.balloon.hide().css("left", left);
		}
		return this;
	},
	PositionContent: function() {
		var id = this.balloon.data("mid");
		if (id) {
			var p = this._BalloonReset();
			if (p) {
				var m = this.Markers(id);
				var x0 = this._BalloonOffsetX(p, m.icon) - 10;
				var x1 = x0 + this.balloon.outerWidth() + 20;
				var y0 = this._BalloonOffsetY(p, m.icon) - 10 - this.balloon.outerHeight();
				var y1 = p.Y + (m.icon ? m.icon.Size.Y - m.icon.Offset.Y : 0);
				if (x1 > this.container.width() || x0 < 0 || y1 > this.container.height() || y0 < 0) {
					var c = this.PointToPixel(this.Point());
					if (x1 > this.container.width()) {
						c.X = x1 - this.container.width() / 2;
					} else if (x0 < 0) {
						c.X += x0;
					}
					if (y1 > this.container.height()) {
						c.Y = y1 - this.container.height() / 2;
					} else if (y0 < 0) {
						c.Y += y0;
					}
					this.Position(this.PixelToPoint(c));
				}
			}
		}
		return this;
	},
	VisibleContent: function() {
		return this.balloon.data("loaded") === true;
	},
	Frame: function(Points) {
		return this;
	},
	PointToPixel: function(Point) {
		return new MapPixel(0, 0);
	},
	PixelToPoint: function(Pixel) {
		return new MapPoint(0, 0);
	},
	PixelsToMeters: function(PixelDistance) {
		PixelDistance = PixelDistance || 1;
		var x = parseInt((this.container.width() - PixelDistance) / 2);
		var y = parseInt(this.container.height() / 2);
		var p1 = this.PixelToPoint(new MapPixel(x, y));
		var p2 = this.PixelToPoint(new MapPixel(x + PixelDistance, y));
		return Mapper.CalculateDistance(p1, p2);
	},
	_MarkerProperty: function(ID, Name, Value) {
		var m = this.Markers(ID);
		if ($.defined(Value)) {
			m[Name.toLowerCase()] = Value;
			return m;
		} else {
			return m[Name.toLowerCase()];
		}
	},
	_BalloonReset: function() {
		var mid = this.balloon.data("mid");
		if (mid) {
			var m = this.Markers(mid);
			var p = this.PointToPixel(m.point);
			this.balloon.css({bottom:this.container.height() - this._BalloonOffsetY(p, m.icon), left:this._BalloonOffsetX(p, m.icon)});
			return p;
		}
		return null;
	},
	_BalloonOffsetX: function(pixels, icon) {
		return (pixels.X - (icon ? icon.Offset.X : 0) - 87 + 1);
	},
	_BalloonOffsetY: function(pixels, icon) {
		return (pixels.Y - (icon ? icon.Offset.Y : 0) - 58 + 1);
	},
	_BalloonMapMoveStart: function() {
		if (this.balloon.data("mid")) {
			this.balloon.hide();
		}
	},
	_BalloonMapMoveEnd: function() {
		if (this._BalloonReset()) {
			this.balloon.show();
		}
	},
	_BalloonGenerate: function() {
		var _this = this;
		var cls = Mapper.Resource("close");
		var x = $("<a/>").addClass("mapper-balloon-close").text(cls).attr({href:"#",title:cls}).click(function() {
			_this.HideContent();
			return false;
		});
		var c = $("<div/>").addClass("mapper-balloon-content");
		var t = $("<div/>").addClass("mapper-balloon-tail");
		for (var i = 0; i < 4; i++) {
			t.append($("<div/>"));
		}
		return $("<div/>").addClass("mapper-balloon").append(c).append(x).append(t).hide().appendTo(this.container);
	},
	DefaultParameters: {
		type: MapTypes.Road,
		unit: MapUnits.Miles,
		events: {},
		markers: [],
		lines: [],
		layers: [],
		directions: null,
		point: new MapPoint(0, 0),
		zoom: 2
	}
});

var Mapper = IMap.extend({
	init: function(id) {
		this._super(id);

		this.container.addClass(Mapper.Classes.Mapper);
		$.extend(this, {
			current: null,
			provider: null,
			providers: []
		});
	},
	Type: function(Type) {
		return this.current.Type(Type);
	},
	Unit: function(Unit) {
		return this.current.Unit(Unit);
	},
	Latitude: function(Latitude) {
		return this.current.Latitude(Latitude);
	},
	Longitude: function(Longitude) {
		return this.current.Longitude(Longitude);
	},
	Point: function(Point) {
		return this.current.Point(Point);
	},
	Zoom: function(Zoom) {
		return this.current.Zoom(Zoom);
	},
	Position: function(Point, Zoom) {
		return this.current.Position(Point, Zoom);
	},
	Extent: function(Extent) {
		return this.current.Extent(Extent);
	},
	Parameters: function(Parameters) {
		return this.current.Parameters(Parameters);
	},
	Provider: function() {
		return this.provider;
	},
	Load: function(Provider, Parameters) {
		this.loaded = false;
		Provider = $.initCase(Provider);
		if (this.current) {
			Parameters = $.extend({}, this.Parameters(), Parameters);
			this.current.Unload();
		} else {
			Parameters = $.extend({}, this.DefaultParameters, Parameters);
		}

		if (this.providers[Provider]) {
			this._LoadComplete(Provider, Parameters);
		} else {
			var _this = this;
			$.getScript(Mapper.GetPath($.format("{0}/Mapper.{0}.js", Provider)), function() {
				_this._Loading(Provider, Parameters);
			});
		}
		return this;
	},
	Unload: function() {
		return this.current.Unload();
	},
	Pan: function(Pixel) {
		return this.current.Pan(Pixel);
	},
	Show: function() {
		return this.current.Show();
	},
	Hide: function() {
		return this.current.Hide();
	},
	Resize: function() {
		this.current.container.height(this.container.height()).width(this.container.width());
		return this.current.Resize();
	},
	Clear: function() {
		return this.current.Clear();
	},
	Reset: function() {
		return this.current.Reset();
	},
	Bind: function(Event, Callback) {
		return this.current.Bind(Event, Callback);
	},
	Unbind: function(Event, Callback) {
		return this.current.Unbind(Event, Callback);
	},
	Mark: function(Marker) {
		var val = this.current.Mark(Marker);
		if ($.isArray(Marker)) {
			for (var i = 0; i < Marker.length; i++) {
				Marker[i].map = this;
			}
		} else {
			Marker.map = this;
		}
		return val;
	},
	ClearMark: function(ID) {
		return this.current.ClearMark(ID);
	},
	MarkerPoint: function(ID, Point) {
		return this.current.MarkerPoint(ID, Point);
	},
	MarkerContent: function(ID, Content) {
		return this.current.MarkerContent(ID, Content);
	},
	MarkerVisible: function(ID, Visible) {
		return this.current.MarkerVisible(ID, Visible);
	},
	MarkerIcon: function(ID, Icon) {
		return this.current.MarkerIcon(ID, Icon);
	},
	MarkerClickable: function(ID, Clickable) {
		return this.current.MarkerClickable(ID, Clickable);
	},
	MarkerDraggable: function(ID, Draggable) {
		return this.current.MarkerDraggable(ID, Draggable);
	},
	MarkerTooltip: function(ID, Tooltip) {
		return this.current.MarkerTooltip(ID, Tooltip);
	},
	MarkerPrintable: function(ID, Printable) {
		return this.current.MarkerPrintable(ID, Printable);
	},
	MarkerData: function(ID, Data) {
		return this.current.MarkerData(ID, Data);
	},
	MarkerBind: function(ID, Event, Callback) {
		return this.current.MarkerBind(ID, Event, Callback);
	},
	MarkerUnbind: function(ID, Event, Callback) {
		return this.current.MarkerUnbind(ID, Event, Callback);
	},
	MarkerShowContent: function(ID, Content) {
		return this.current.MarkerShowContent(ID, Content);
	},
	MarkerHideContent: function(ID) {
		return this.current.MarkerHideContent(ID);
	},
	MarkerResizeContent: function(ID) {
		return this.current.MarkerResizeContent(ID);
	},
	MarkerPositionContent: function(ID) {
		return this.current.MarkerPositionContent(ID);
	},
	MarkerVisibleContent: function(ID) {
		return this.current.MarkerVisibleContent(ID);
	},
	MarkerToFront: function(ID) {
		return this.current.MarkerToFront(ID);
	},
	Markers: function(ID) {
		return this.current.Markers(ID);
	},
	MarkersFrame: function(IDs) {
		return this.current.MarkersFrame(IDs);
	},
	MarkersInProximity: function(PointOrID, Pixels) {
		return this.current.MarkersInProximity(PointOrID, Pixels);
	},
	MarkersVisibleContent: function() {
		return this.current.MarkersVisibleContent();
	},
	MarkerLines: function(ID) {
		return this.current.MarkerLines(ID);
	},
	MarkerLinesReset: function(ID) {
		return this.current.MarkerLinesReset(ID);
	},
	MarkerDirectionsReset: function(ID) {
		return this.current.MarkerDirectionsReset(ID);
	},
	MarkerResetDependents: function(ID) {
		return this.current.MarkerResetDependents(ID);
	},
	MarkerPrimitive: function(ID) {
		return this.current.MarkerPrimitive(ID);
	},
	Line: function(Line) {
		return this.current.Line(Line);
	},
	ClearLine: function(ID) {
		return this.current.ClearLine(ID);
	},
	LinePoint: function(ID, Index, Point) {
		return this.current.LinePoint(ID, Index, Point);
	},
	Lines: function(ID) {
		return this.current.Lines(ID);
	},
	LinesReset: function(ID) {
		return this.current.LinesReset(ID);
	},
	Layer: function(Layer) {
		return this.current.Layer(Layer);
	},
	ClearLayer: function(ID) {
		return this.current.ClearLayer(ID);
	},
	Layers: function(ID) {
		return this.current.Layers(ID);
	},
	Directions: function(Directions) {
		return this.current.Directions(Directions);
	},
	ClearDirections: function() {
		return this.current.ClearDirections();
	},
	DirectionsReset: function(Directions) {
		return this.current.DirectionsReset(Directions);
	},
	DirectionsVisible: function(Visible) {
		return this.current.DirectionsVisible(Visible);
	},
	DirectionsFrame: function() {
		return this.current.DirectionsFrame();
	},
	ShowContent: function(Content) {
		return this.current.ShowContent(Content);
	},
	HideContent: function() {
		return this.current.HideContent();
	},
	ResizeContent: function() {
		return this.current.ResizeContent();
	},
	PositionContent: function() {
		return this.current.PositionContent();
	},
	VisibleContent: function() {
		return this.current.VisibleContent();
	},
	Frame: function(Points) {
		return this.current.Frame(Points);
	},
	PointToPixel: function(Point) {
		return this.current.PointToPixel(Point);
	},
	PixelToPoint: function(Pixel) {
		return this.current.PixelToPoint(Pixel);
	},
	PixelsToMeters: function(PixelDistance) {
		return this.current.PixelsToMeters(PixelDistance);
	},
	_Loading: function(Provider, Parameters) {
		var p = Provider + "Mapper";
		if (window[p]) {
			var pid = this.id + "_" + Provider;
			this.container.prepend($("<div/>").attr("id", pid).addClass(Mapper.Classes.Map).hide());
			this.providers[Provider] = new window[p](pid);
			this._LoadComplete(Provider, Parameters);
		} else {
			var _this = this;
			$.thread(function() {
				_this._Loading(Provider, Parameters);
			}, 100);
		}
	},
	_LoadComplete: function(Provider, Parameters) {
		this.current = this.providers[Provider];
		this.current.container.height(this.container.height()).width(this.container.width());
		this.current.Show();
		this.current.Load(Parameters);
		this.provider = Provider;
		this.loaded = true;
	}
});


// Mapper Utilities
$.extend(Mapper, {
	SourceFolderPath: $.resolveUrl(MapperSourceFolderPath),
	EarthRadius: 6372795, //approximate radius in meters
	Factor: (Math.PI / 180),
	Precision: 0.0000000000001,
	Extents: {North:90.0, East:180.0, South:-90.0, West:-180.0},
	Classes: {Mapper:"mapper",Map:"mapper-map",NoPrint:"no-print"},
	ZoomLevels: {Point:20, Street:16, City:12, Zip:11, State:7, Country:5},
	GetPath: function(FilePath) {
		return Mapper.SourceFolderPath + FilePath;
	},
	GetLocale: function(SupportedLocales, Parameter) {
		var key = null;
		var lang = $("meta[httpEquiv='Content-Language']").attr("content");
		while (lang) {
			var re = new RegExp(lang, "i");
			for (var i = 0; i < SupportedLocales.length; i++) {
				var loc = SupportedLocales[i];
				if (loc.search(re) == 0) {
					key = loc;
					break;
				}
			}
			if (key) {
				key = "&" + Parameter + "=" + key;
				break;
			} else {
				lang = lang.substr(0, lang.lastIndexOf("-"));
			}
		}
		return key || "";
	},
	GetKey: function(SupportedKeys) {
		var key = SupportedKeys[window.location.hostname];
		if (!key) {
			for (var i in SupportedKeys) {
				var re = new RegExp(".*\." + i + "$", "i");
				if (re.test(window.location.hostname)) {
					key = SupportedKeys[i];
					break;
				}
			}
		}
		return key || "";
	},
	GetService: function(Service) {
		return $.format("{0}//{1}/ArcGIS/rest/services/{2}/MapServer", window.location.protocol, $.trimEnd(Mapper.Resource("graphics_server"), "/"), Service);
	},
	CalculateDistance: function(PointA, PointB) {
		var dLat = Mapper.ToRadians(PointB.Latitude - PointA.Latitude);
		var dLon = Mapper.ToRadians(PointB.Longitude - PointA.Longitude);
		var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Mapper.ToRadians(PointA.Latitude)) *
				Math.cos(Mapper.ToRadians(PointB.Latitude)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
		var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
		var d = Mapper.EarthRadius * c;
		return d;
	},
	CalculatePixelDistance: function(PixelA, PixelB) {
		return Math.sqrt(Math.pow(PixelB.X - PixelA.X, 2) + Math.pow(PixelB.Y - PixelA.Y, 2))
	},
	CalculatePoint: function(PointA, Distance, Bearing) {
		var d = Distance / Mapper.EarthRadius;
		var b = Mapper.ToRadians(Bearing);
		var lat1 = Mapper.ToRadians(PointA.Latitude);
		var lng1 = Mapper.ToRadians(PointA.Longitude);
		var lat2 = Math.asin(Math.sin(lat1) * Math.cos(d) +
				Math.cos(lat1) * Math.sin(d) * Math.cos(b));
		var lng2 = lng1 + Math.atan2(Math.sin(b) * Math.sin(d) * Math.cos(lat1),
				Math.cos(d) - Math.sin(lat1) * Math.sin(lat2));
		lng2 = (lng2 + 3 * Math.PI) % (2 * Math.PI) - Math.PI;	// normalise to -180...+180
		return new MapPoint(Mapper.ToDegrees(lat2), Mapper.ToDegrees(lng2));
	},
	PrintDistance: function(Meters, Unit, Full) {
		var distance = 0;
		var name = Mapper.Resource(Full ? "meters" : "m");
		switch (Unit) {
			case MapUnits.Miles:
				if (Meters > 160.9344) {
					distance = (Meters / 1609.344).toFixed(1);
					name = Mapper.Resource(Full ? "miles" : "mi");
				} else {
					distance = (Meters / 0.3048).toFixed(0);
					name = Mapper.Resource(Full ? "feet" : "ft");
				}
				break;
			case MapUnits.Kilometers:
				if (Meters > 100) {
					distance = (Meters / 1000).toFixed(1);
					name = Mapper.Resource(Full ? "kilometers" : "km");
				} else {
					distance = Meters.toFixed(0);
				}
				break;
		}
		return distance + " " + name;
	},
	PrintTime: function(Seconds) {
		var time = "";
		if (!$.defined(Seconds) || Seconds == null || isNaN(Seconds)) {
			Seconds = 0;
		}
		var s = Seconds % 60;
		var m = Seconds - s;
		m = (m / 60) + (s > 0 ? 1 : 0);
		if (m >= 60) {
			var mRemain = m % 60;
			var h = m - mRemain;
			h = h / 60;
			time = (h + " " + Mapper.Resource(h > 1 ? "hours" : "hour") + (mRemain > 0 ? (", " + mRemain + " " + Mapper.Resource(mRemain > 1 ? "minutes" : "minute")) : ""));
		} else {
			time = (m + " " + Mapper.Resource(m > 1 ? "minutes" : "minute"));
		}
		return time;
	},
	PrintCoordinate: function(Decimal, IsLongitude) {
		var coord = "";
		Decimal = parseFloat(Decimal);
		if ($.defined(IsLongitude)) {
			var isPositive = (Decimal >= 0);
			var direction = Mapper.Resource(IsLongitude ? (isPositive ? "east" : "west") : (isPositive ? "north" : "south"));
			Decimal = Math.abs(Decimal);
			var degrees = Math.floor(Decimal);
			Decimal = (Decimal - degrees) * 60;
			var minutes = Math.floor(Decimal);
			Decimal = (Decimal - minutes) * 60;
			var seconds = Math.round(Decimal);
			coord = (degrees + "\u00B0" + (minutes < 10 ? "0" : "") + minutes + "\u0027" + (seconds < 10 ? "0" : "") + seconds + "\u0022" + (degrees || minutes || seconds ? " " + direction : ""));
		} else {
			coord = Decimal.toFixed(6);
		}
		return coord;
	},
	SortPoints:function(Data, Rows, Cols) {
		if (!Mapper.SortPoints._Init) {
			$.extend(Mapper.SortPoints, {
				_Init: true,
				Bounds: null,
				Sections: null,
				Sort: function(a, b) {
					var pa = Mapper.GetPoint(a);
					var pb = Mapper.GetPoint(b);
					if (pa && pb) {
						var ra = Mapper.SortPoints.SortRow(pa);
						var rb = Mapper.SortPoints.SortRow(pb);
						if (ra == rb) {
							var ca = Mapper.SortPoints.SortCol(pa);
							var cb = Mapper.SortPoints.SortCol(pb);
							if (ca == cb) {
								return pa.Longitude - pb.Longitude;
							} else {
								return ca - cb;
							}
						} else {
							return rb - ra;
						}
					} else if (!pa && !pb) {
						return 0;
					} else {
						return pa ? -1 : 1;
					}
				},
				SortRow: function(Point) {
					return Math.ceil((Point.Latitude - Mapper.SortPoints.Bounds.BottomRight.Latitude) / Mapper.SortPoints.Sections.Y);
				},
				SortCol: function(Point) {
					return Math.ceil((Point.Longitude - Mapper.SortPoints.Bounds.TopLeft.Longitude) / Mapper.SortPoints.Sections.X);
				}
			});
		}
		Mapper.SortPoints.Bounds = Mapper.GetBounds(Data);
		Mapper.SortPoints.Sections = {X:(Mapper.SortPoints.Bounds.BottomRight.Longitude - Mapper.SortPoints.Bounds.TopLeft.Longitude) / $.nz(Rows, 5),
			Y:(Mapper.SortPoints.Bounds.TopLeft.Latitude - Mapper.SortPoints.Bounds.BottomRight.Latitude) / $.nz(Cols, 5)};
		Data.sort(Mapper.SortPoints.Sort);
	},
	GetBounds:function(Data) {
		var t = r = Number.NEGATIVE_INFINITY;
		var b = l = Number.POSITIVE_INFINITY;
		for (var i = 0; i < Data.length; i++) {
			var p = Mapper.GetPoint(Data[i]);
			if (p) {
				if (p.Latitude > t) {
					t = p.Latitude;
				}
				if (p.Latitude < b) {
					b = p.Latitude;
				}
				if (p.Longitude > r) {
					r = p.Longitude;
				}
				if (p.Longitude < l) {
					l = p.Longitude;
				}
			}
		}
		t = Mapper.Increment(t);
		r = Mapper.Increment(r);
		b = Mapper.Decrement(b);
		l = Mapper.Decrement(l);
		return {TopLeft:new MapPoint(t, l), BottomRight:new MapPoint(b, r)};
	},
	GetPoint:function(Object) {
		var p = null;
		var lat = $.nz(Object.Latitude, Object.latitude);
		var lng = $.nz(Object.Longitude, Object.longitude);
		if (!$.empty(lat) && !$.empty(lng)) {
			p = new MapPoint(lat, lng);
		}
		return p;
	},
	GetExtent:function(Data) {
		var x = [];
		for (var i = 0; i < Data.length; i = i + 2) {
			x.push(new MapPoint(Data[i], Data[i + 1]));
		}
		return x;
	},
	CleanZoom: function(Zoom) {
		var z = parseInt(Zoom);
		return isNaN(z) ? 2 : z;
	},
	CleanPoint: function(Point) {
		Point.Latitude = Math.min(Math.max(Point.Latitude, Mapper.Extents.South), Mapper.Extents.North);
		Point.Longitude = Math.min(Math.max(Point.Longitude, Mapper.Extents.West), Mapper.Extents.East);
		return Point;
	},
	PointsEqual: function(a, b) {
		var pa = Mapper.GetPoint(a);
		var pb = Mapper.GetPoint(b);
		if (pa && pb) {
			return pa.Latitude == pb.Latitude && pa.Longitude == pb.Longitude;
		} else {
			return (!pa && !pb);
		}
	},
	Increment: function(Decimal) {
		return Decimal + Mapper.Precision;
	},
	Decrement: function(Decimal) {
		return Decimal - Mapper.Precision;
	},
	ToDegrees: function(Radians) {
		return Radians / Mapper.Factor;
	},
	ToRadians: function(Degrees) {
		return Degrees * Mapper.Factor;
	},
	Resource: function(Resource) {
		return $.resource("m_" + Resource);
	}
});

$.includeStyleSheet(Mapper.GetPath("Mapper.css"));

