code:
mxn.register('{{api_id}}', {

Mapstraction: {

	init: function(element, api) {
		var me = this;

		// TODO: Add provider code
	},

	applyOptions: function(){
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	resizeTo: function(width, height){
		// TODO: Add provider code
	},

	addControls: function( args ) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	addSmallControls: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	addLargeControls: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	addMapTypeControls: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	setCenterAndZoom: function(point, zoom) {
		var map = this.maps[this.api];
		var pt = point.toProprietary(this.api);

		// TODO: Add provider code
	},

	addMarker: function(marker, old) {
		var map = this.maps[this.api];
		var pin = marker.toProprietary(this.api);

		// TODO: Add provider code

		return pin;
	},

	removeMarker: function(marker) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	removeAllMarkers: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	declutterMarkers: function(opts) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	addPolyline: function(polyline, old) {
		var map = this.maps[this.api];
		var pl = polyline.toProprietary(this.api);

		// TODO: Add provider code

		return pl;
	},

	removePolyline: function(polyline) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	getCenter: function() {
		var point;
		var map = this.maps[this.api];

		// TODO: Add provider code

		return point;
	},

	setCenter: function(point, options) {
		var map = this.maps[this.api];
		var pt = point.toProprietary(this.api);
		if(options && options.pan) {
			// TODO: Add provider code
		}
		else {
			// TODO: Add provider code
		}
	},

	setZoom: function(zoom) {
		var map = this.maps[this.api];

		// TODO: Add provider code

	},

	getZoom: function() {
		var map = this.maps[this.api];
		var zoom;

		// TODO: Add provider code

		return zoom;
	},

	getZoomLevelForBoundingBox: function( bbox ) {
		var map = this.maps[this.api];
		// NE and SW points from the bounding box.
		var ne = bbox.getNorthEast();
		var sw = bbox.getSouthWest();
		var zoom;

		// TODO: Add provider code

		return zoom;
	},

	setMapType: function(type) {
		var map = this.maps[this.api];
		switch(type) {
			case mxn.Mapstraction.ROAD:
				// TODO: Add provider code
				break;
			case mxn.Mapstraction.SATELLITE:
				// TODO: Add provider code
				break;
			case mxn.Mapstraction.HYBRID:
				// TODO: Add provider code
				break;
			default:
				// TODO: Add provider code
		}
	},

	getMapType: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code

		//return mxn.Mapstraction.ROAD;
		//return mxn.Mapstraction.SATELLITE;
		//return mxn.Mapstraction.HYBRID;

	},

	getBounds: function () {
		var map = this.maps[this.api];

		// TODO: Add provider code

		//return new mxn.BoundingBox( ,  ,  ,  );
	},

	setBounds: function(bounds){
		var map = this.maps[this.api];
		var sw = bounds.getSouthWest();
		var ne = bounds.getNorthEast();

		// TODO: Add provider code

	},

	addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	setImagePosition: function(id, oContext) {
		var map = this.maps[this.api];
		var topLeftPoint; var bottomRightPoint;

		// TODO: Add provider code

		//oContext.pixels.top = ...;
		//oContext.pixels.left = ...;
		//oContext.pixels.bottom = ...;
		//oContext.pixels.right = ...;
	},

	addOverlay: function(url, autoCenterAndZoom) {
		var map = this.maps[this.api];

		// TODO: Add provider code

	},

	addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	toggleTileLayer: function(tile_url) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	getPixelRatio: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	mousePosition: function(element) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	}
},

LatLonPoint: {

	toProprietary: function() {
		// TODO: Add provider code
	},

	fromProprietary: function(point) {
		// TODO: Add provider code
	}

},

Marker: {

	toProprietary: function() {
		// TODO: Add provider code
	},

	openBubble: function() {
		// TODO: Add provider code
	},

	hide: function() {
		// TODO: Add provider code
	},

	show: function() {
		// TODO: Add provider code
	},

	update: function() {
		// TODO: Add provider code
	}

},

Polyline: {

	toProprietary: function() {
		// TODO: Add provider code
	},

	show: function() {
		// TODO: Add provider code
	},

	hide: function() {
		// TODO: Add provider code
	}

}

});


code:
mxn.register('cloudmade', {

    Mapstraction: {

        init: function(element, api) {
            var me = this;
            var cloudmade = new CM.Tiles.CloudMade.Web({key: cloudmade_key});
            this.maps[api] = new CM.Map(element, cloudmade);
            this.loaded[api] = true;

            CM.Event.addListener(this.maps[api], 'click', function(location,marker) {
                if ( marker && marker.mapstraction_marker ) {
                    marker.mapstraction_marker.click.fire();
                }
                else if ( location ) {
                    me.click.fire({'location': new mxn.LatLonPoint(location.lat(), location.lng())});
                }

                // If the user puts their own markers directly on the map
                // then there is no location and this event should not fire.
                if ( location ) {
                    me.clickHandler(location.lat(),location.lng(),location,me);
                }
            });
        },

        applyOptions: function(){
            var map = this.maps[this.api];
            if(this.options.enableScrollWheelZoom){
              map.enableScrollWheelZoom();
            }
        },

        resizeTo: function(width, height){
            this.maps[this.api].checkResize();
        },

        addControls: function( args ) {
            var map = this.maps[this.api];

	    var c = this.addControlsArgs;
	    switch (c.zoom) {
	      case 'large':
	        this.addLargeControls();
	      break;
	      case 'small':
	        this.addSmallControls();
	      break;
	    }

	    if (c.map_type) {
	      this.addMapTypeControls();
	    }
	    if (c.scale) {
	      map.addControl(new CM.ScaleControl());
	      this.addControlsArgs.scale = true;
	    }
        },

        addSmallControls: function() {
            var map = this.maps[this.api];
	    map.addControl(new CM.SmallMapControl());
	    this.addControlsArgs.zoom = 'small';
        },

        addLargeControls: function() {
            var map = this.maps[this.api];
	    map.addControl(new CM.LargeMapControl());
	    this.addControlsArgs.zoom = 'large';
        },

        addMapTypeControls: function() {
            var map = this.maps[this.api];

	    map.addControl(new CM.TileLayerControl());
	    this.addControlsArgs.map_type = true;
        },

        dragging: function(on) {
            var map = this.maps[this.api];

            if (on) {
              map.enableDragging();
            } else {
              map.disableDragging();
            }
        },

        setCenterAndZoom: function(point, zoom) {
            var map = this.maps[this.api];
            var pt = point.toProprietary(this.api);
            map.setCenter(pt, zoom);

        },

        addMarker: function(marker, old) {
            var map = this.maps[this.api];
            var pin = marker.toProprietary(this.api);
	    map.addOverlay(pin);
            return pin;
        },

        removeMarker: function(marker) {
            var map = this.maps[this.api];
            marker.proprietary_marker.closeInfoWindow();
	    map.removeOverlay(marker.proprietary_marker);
        },

        removeAllMarkers: function() {
	    // Done in mxn.core.js
        },

        declutterMarkers: function(opts) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        addPolyline: function(polyline, old) {
            var map = this.maps[this.api];
            var pl = polyline.toProprietary(this.api);
	    map.addOverlay(pl);
            return pl;
        },

        removePolyline: function(polyline) {
            var map = this.maps[this.api];
            map.removeOverlay(polyline.proprietary_polyline);
        },

        getCenter: function() {
            var map = this.maps[this.api];
            var pt = map.getCenter();

            return new mxn.LatLonPoint(pt.lat(), pt.lng());
        },

        setCenter: function(point, options) {
            var map = this.maps[this.api];
            var pt = point.toProprietary(this.api);
            if(options !== null && options.pan) { map.panTo(pt); }
            else { map.setCenter(pt); }
        },

        setZoom: function(zoom) {
            var map = this.maps[this.api];
	    map.setZoom(zoom);
        },

        getZoom: function() {
	    var map = this.maps[this.api];
	    return map.getZoom();
        },

        getZoomLevelForBoundingBox: function( bbox ) {
            var map = this.maps[this.api];
            // NE and SW points from the bounding box.
            var ne = bbox.getNorthEast();
            var sw = bbox.getSouthWest();

	    var zoom = map.getBoundsZoomLevel(new CM.LatLngBounds(sw.toProprietary(this.api), ne.toProprietary(this.api)));
            return zoom;
        },

        setMapType: function(type) {
            var map = this.maps[this.api];

            // TODO: Are there any MapTypes for Cloudmade?

            switch(type) {
                case mxn.Mapstraction.ROAD:
                // TODO: Add provider code
                break;
                case mxn.Mapstraction.SATELLITE:
                // TODO: Add provider code
                break;
                case mxn.Mapstraction.HYBRID:
                // TODO: Add provider code
                break;
                default:
                // TODO: Add provider code
            }
        },

        getMapType: function() {
            var map = this.maps[this.api];

            // TODO: Are there any MapTypes for Cloudmade?

            return mxn.Mapstraction.ROAD;
            //return mxn.Mapstraction.SATELLITE;
            //return mxn.Mapstraction.HYBRID;

        },

        getBounds: function () {
            var map = this.maps[this.api];

            var box = map.getBounds();
            var sw = box.getSouthWest();
            var ne = box.getNorthEast();

            return new mxn.BoundingBox(sw.lat(), sw.lng(), ne.lat(), ne.lng());
        },

        setBounds: function(bounds){
            var map = this.maps[this.api];
            var sw = bounds.getSouthWest();
            var ne = bounds.getNorthEast();

	    map.zoomToBounds(new CM.LatLngBounds(sw.toProprietary(this.api), ne.toProprietary(this.api)));
        },

        addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        setImagePosition: function(id, oContext) {
            var map = this.maps[this.api];
            var topLeftPoint; var bottomRightPoint;

            // TODO: Add provider code

        },

        addOverlay: function(url, autoCenterAndZoom) {
            var map = this.maps[this.api];

            // TODO: Add provider code

        },

        addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        toggleTileLayer: function(tile_url) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        getPixelRatio: function() {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        mousePosition: function(element) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        }
    },

    LatLonPoint: {

        toProprietary: function() {
	    var cll = new CM.LatLng(this.lat,this.lon);
	    return cll;
        },

        fromProprietary: function(point) {
	    return new mxn.LatLonPoint(point.lat(),point.lng());
        }

    },

    Marker: {

        toProprietary: function() {
	    var pt = this.location.toProprietary(this.api);
	    var options = {};

	    if (this.iconUrl) {
	      var cicon = new CM.Icon();
	      cicon.image = this.iconUrl;
	      if (this.iconSize) {
	        cicon.iconSize = new CM.Size(this.iconSize[0], this.iconSize[1]);
	        if (this.iconAnchor) {
	          cicon.iconAnchor = new CM.Point(this.iconAnchor[0], this.iconAnchor[1]);
	        }
	      }
	      if (this.iconShadowUrl) {
	        cicon.shadow = this.iconShadowUrl;
	        if (this.iconShadowSize) {
	          cicon.shadowSize = new CM.Size(this.iconShadowSize[0], this.iconShadowSize[1]);
	        }
	      }
	      options.icon = cicon;
	    }
	    if (this.labelText) {
 	      options.title = this.labelText;
	    }
	    var cmarker = new CM.Marker(pt, options);

	    if (this.infoBubble) {
	      cmarker.bindInfoWindow(this.infoBubble);
	    }

	    return cmarker;
        },

        openBubble: function() {
            var pin = this.proprietary_marker;
            pin.openInfoWindow(this.infoBubble);
        },

        hide: function() {
            var pin = this.proprietary_marker;
            pin.hide();
        },

        show: function() {
            var pin = this.proprietary_marker;
            pin.show();
        },

        update: function() {
            // TODO: Add provider code
        }

    },

    Polyline: {

        toProprietary: function() {
            var pts = [];
            var poly;

            for (var i = 0,  length = this.points.length ; i< length; i++){
              pts.push(this.points[i].toProprietary(this.api));
            }
            if (this.closed || pts[0].equals(pts[pts.length-1])) {
              poly = new CM.Polygon(pts, this.color, this.width, this.opacity, this.fillColor || "#5462E3", this.opacity || "0.3");
            } else {
              poly = new CM.Polyline(pts, this.color, this.width, this.opacity);
            }
            return poly;
        },

        show: function() {
	    this.proprietary_polyline.show();
        },

        hide: function() {
	    this.proprietary_polyline.hide();
        }

    }

});

repo_name: ksuudilla/gnu

license: mit


code:
(function(){

/**
 * @exports mxn.util.$m as $m
 */
var $m = mxn.util.$m;

/**
 * Initialise our provider. This function should only be called
 * from within mapstraction code, not exposed as part of the API.
 * @private
 */
var init = function() {
	this.invoker.go('init', [ this.currentElement, this.api ]);
	this.applyOptions();
};

/**
 * Mapstraction instantiates a map with some API choice into the HTML element given
 * @name mxn.Mapstraction
 * @constructor
 * @param {String} element The HTML element to replace with a map
 * @param {String} api The API to use, one of 'yahoo', 'microsoft', 'openstreetmap', 'multimap', 'map24', 'openlayers', 'mapquest'. If omitted, first loaded provider implementation is used.
 * @param {Bool} debug optional parameter to turn on debug support - this uses alert panels for unsupported actions
 * @exports Mapstraction as mxn.Mapstraction
 */
var Mapstraction = mxn.Mapstraction = function(element, api, debug) {
    if (!api){
		api = mxn.util.getAvailableProviders()[0];
	}
	this.api = api;
	this.maps = {};
	this.currentElement = $m(element);
	this.eventListeners = [];
	this.markers = [];
	this.layers = [];
	this.polylines = [];
	this.images = [];
        this.controls = [];
	this.loaded = {};
	this.onload = {};
	this.element = element;

	// option defaults
	this.options = {
		enableScrollWheelZoom: false,
		enableDragging: true
	};

	this.addControlsArgs = {};

	// set up our invoker for calling API methods
	this.invoker = new mxn.Invoker(this, 'Mapstraction', function(){ return this.api; });

	// Adding our events
	mxn.addEvents(this, [

		/**
		 * Map has loaded
		 * @name mxn.Mapstraction#load
		 * @event
		 */
		'load',

		/**
		 * Map is clicked {location: LatLonPoint}
		 * @name mxn.Mapstraction#click
		 * @event
		 */
		'click',

		/**
		 * Map is panned
		 * @name mxn.Mapstraction#endPan
		 * @event
		 */
		'endPan',

		/**
		 * Zoom is changed
		 * @name mxn.Mapstraction#changeZoom
		 * @event
		 */
		'changeZoom',

		/**
		 * Marker is removed {marker: Marker}
		 * @name mxn.Mapstraction#markerAdded
		 * @event
		 */
		'markerAdded',

		/**
		 * Marker is removed {marker: Marker}
		 * @name mxn.Mapstraction#markerRemoved
		 * @event
		 */
		'markerRemoved',

		/**
		 * Polyline is added {polyline: Polyline}
		 * @name mxn.Mapstraction#polylineAdded
		 * @event
		 */
		'polylineAdded',

		/**
		 * Polyline is removed {polyline: Polyline}
		 * @name mxn.Mapstraction#polylineRemoved
		 * @event
		 */
		'polylineRemoved'
	]);

	// finally initialize our proper API map
	init.apply(this);
};

// Map type constants
Mapstraction.ROAD = 1;
Mapstraction.SATELLITE = 2;
Mapstraction.HYBRID = 3;

// methods that have no implementation in mapstraction core
mxn.addProxyMethods(Mapstraction, [
	/**
	 * Adds a large map panning control and zoom buttons to the map
	 * @name mxn.Mapstraction#addLargeControls
	 * @function
	 */
	'addLargeControls',

	/**
	 * Adds a map type control to the map (streets, aerial imagery etc)
	 * @name mxn.Mapstraction#addMapTypeControls
	 * @function
	 */
	'addMapTypeControls',

	/**
	 * Adds a GeoRSS or KML overlay to the map
	 *  some flavors of GeoRSS and KML are not supported by some of the Map providers
	 * @name mxn.Mapstraction#addOverlay
	 * @function
	 * @param {String} url GeoRSS or KML feed URL
	 * @param {Boolean} autoCenterAndZoom Set true to auto center and zoom after the feed is loaded
	 */
	'addOverlay',

	/**
	 * Adds a small map panning control and zoom buttons to the map
	 * @name mxn.Mapstraction#addSmallControls
	 * @function
	 */
	'addSmallControls',

	/**
	 * Applies the current option settings
	 * @name mxn.Mapstraction#applyOptions
	 * @function
	 */
	'applyOptions',

	/**
	 * Gets the BoundingBox of the map
	 * @name mxn.Mapstraction#getBounds
	 * @function
	 * @returns {BoundingBox} The bounding box for the current map state
	 */
	'getBounds',

	/**
	 * Gets the central point of the map
	 * @name mxn.Mapstraction#getCenter
	 * @function
	 * @returns {LatLonPoint} The center point of the map
	 */
	'getCenter',

	/**
	 * Gets the imagery type for the map.
	 * The type can be one of:
	 *  mxn.Mapstraction.ROAD
	 *  mxn.Mapstraction.SATELLITE
	 *  mxn.Mapstraction.HYBRID
	 * @name mxn.Mapstraction#getMapType
	 * @function
	 * @returns {Number}
	 */
	'getMapType',

	/**
	 * Returns a ratio to turn distance into pixels based on current projection
	 * @name mxn.Mapstraction#getPixelRatio
	 * @function
	 * @returns {Float} ratio
	 */
	'getPixelRatio',

	/**
	 * Returns the zoom level of the map
	 * @name mxn.Mapstraction#getZoom
	 * @function
	 * @returns {Integer} The zoom level of the map
	 */
	'getZoom',

	/**
	 * Returns the best zoom level for bounds given
	 * @name mxn.Mapstraction#getZoomLevelForBoundingBox
	 * @function
	 * @param {BoundingBox} bbox The bounds to fit
	 * @returns {Integer} The closest zoom level that contains the bounding box
	 */
	'getZoomLevelForBoundingBox',

	/**
	 * Displays the coordinates of the cursor in the HTML element
	 * @name mxn.Mapstraction#mousePosition
	 * @function
	 * @param {String} element ID of the HTML element to display the coordinates in
	 */
	'mousePosition',

	/**
	 * Resize the current map to the specified width and height
	 * (since it is actually on a child div of the mapElement passed
	 * as argument to the Mapstraction constructor, the resizing of this
	 * mapElement may have no effect on the size of the actual map)
	 * @name mxn.Mapstraction#resizeTo
	 * @function
	 * @param {Integer} width The width the map should be.
	 * @param {Integer} height The width the map should be.
	 */
	'resizeTo',

	/**
	 * Sets the map to the appropriate location and zoom for a given BoundingBox
	 * @name mxn.Mapstraction#setBounds
	 * @function
	 * @param {BoundingBox} bounds The bounding box you want the map to show
	 */
	'setBounds',

	/**
	 * setCenter sets the central point of the map
	 * @name mxn.Mapstraction#setCenter
	 * @function
	 * @param {LatLonPoint} point The point at which to center the map
	 * @param {Object} options Optional parameters
	 * @param {Boolean} options.pan Whether the map should move to the locations using a pan or just jump straight there
	 */
	'setCenter',

	/**
	 * Centers the map to some place and zoom level
	 * @name mxn.Mapstraction#setCenterAndZoom
	 * @function
	 * @param {LatLonPoint} point Where the center of the map should be
	 * @param {Integer} zoom The zoom level where 0 is all the way out.
	 */
	'setCenterAndZoom',

	/**
	 * Sets the imagery type for the map
	 * The type can be one of:
	 *  mxn.Mapstraction.ROAD
	 *  mxn.Mapstraction.SATELLITE
	 *  mxn.Mapstraction.HYBRID
	 * @name mxn.Mapstraction#setMapType
	 * @function
	 * @param {Number} type
	 */
	'setMapType',

	/**
	 * Sets the zoom level for the map
	 * MS doesn't seem to do zoom=0, and Gg's sat goes closer than it's maps, and MS's sat goes closer than Y!'s
	 * TODO: Mapstraction.prototype.getZoomLevels or something.
	 * @name mxn.Mapstraction#setZoom
	 * @function
	 * @param {Number} zoom The (native to the map) level zoom the map to.
	 */
	'setZoom',

	/**
	 * Turns a Tile Layer on or off
	 * @name mxn.Mapstraction#toggleTileLayer
	 * @function
	 * @param {tile_url} url of the tile layer that was created.
	 */
	'toggleTileLayer'
]);

/**
 * Sets the current options to those specified in oOpts and applies them
 * @param {Object} oOpts Hash of options to set
 */
Mapstraction.prototype.setOptions = function(oOpts){
	mxn.util.merge(this.options, oOpts);
	this.applyOptions();
};

/**
 * Sets an option and applies it.
 * @param {String} sOptName Option name
 * @param vVal Option value
 */
Mapstraction.prototype.setOption = function(sOptName, vVal){
	this.options[sOptName] = vVal;
	this.applyOptions();
};

/**
 * Enable scroll wheel zooming
 * @deprecated Use setOption instead.
 */
Mapstraction.prototype.enableScrollWheelZoom = function() {
	this.setOption('enableScrollWheelZoom', true);
};

/**
 * Enable/disable dragging of the map
 * @param {Boolean} on
 * @deprecated Use setOption instead.
 */
Mapstraction.prototype.dragging = function(on) {
	this.setOption('enableDragging', on);
};

/**
 * Change the current api on the fly
 * @param {String} api The API to swap to
 * @param element
 */
Mapstraction.prototype.swap = function(element,api) {
	if (this.api === api) {
		return;
	}

	var center = this.getCenter();
	var zoom = this.getZoom();

	this.currentElement.style.visibility = 'hidden';
	this.currentElement.style.display = 'none';

	this.currentElement = $m(element);
	this.currentElement.style.visibility = 'visible';
	this.currentElement.style.display = 'block';

	this.api = api;

	if (this.maps[this.api] === undefined) {
		init.apply(this);

		this.setCenterAndZoom(center,zoom);

		for (var i = 0; i < this.markers.length; i++) {
			this.addMarker(this.markers[i], true);
		}

		for (var j = 0; j < this.polylines.length; j++) {
			this.addPolyline( this.polylines[j], true);
		}
	}
	else {

		//sync the view
		this.setCenterAndZoom(center,zoom);

		//TODO synchronize the markers and polylines too
		// (any overlays created after api instantiation are not sync'd)
	}

	this.addControls(this.addControlsArgs);

};

/**
 * Returns the loaded state of a Map Provider
 * @param {String} api Optional API to query for. If not specified, returns state of the originally created API
 */
Mapstraction.prototype.isLoaded = function(api){
	if (api === null) {
		api = this.api;
	}
	return this.loaded[api];
};

/**
 * Set the debugging on or off - shows alert panels for functions that don't exist in Mapstraction
 * @param {Boolean} debug true to turn on debugging, false to turn it off
 */
Mapstraction.prototype.setDebug = function(debug){
	if(debug !== null) {
		this.debug = debug;
	}
	return this.debug;
};

/////////////////////////
//
// Event Handling
//
// FIXME need to consolidate some of these handlers...
//
///////////////////////////

// Click handler attached to native API
Mapstraction.prototype.clickHandler = function(lat, lon, me) {
	this.callEventListeners('click', {
		location: new LatLonPoint(lat, lon)
	});
};

// Move and zoom handler attached to native API
Mapstraction.prototype.moveendHandler = function(me) {
	this.callEventListeners('moveend', {});
};

/**
 * Add a listener for an event.
 * @param {String} type Event type to attach listener to
 * @param {Function} func Callback function
 * @param {Object} caller Callback object
 */
Mapstraction.prototype.addEventListener = function() {
	var listener = {};
	listener.event_type = arguments[0];
	listener.callback_function = arguments[1];

	// added the calling object so we can retain scope of callback function
	if(arguments.length == 3) {
		listener.back_compat_mode = false;
		listener.callback_object = arguments[2];
	}
	else {
		listener.back_compat_mode = true;
		listener.callback_object = null;
	}
	this.eventListeners.push(listener);
};

/**
 * Call listeners for a particular event.
 * @param {String} sEventType Call listeners of this event type
 * @param {Object} oEventArgs Event args object to pass back to the callback
 */
Mapstraction.prototype.callEventListeners = function(sEventType, oEventArgs) {
	oEventArgs.source = this;
	for(var i = 0; i < this.eventListeners.length; i++) {
		var evLi = this.eventListeners[i];
		if(evLi.event_type == sEventType) {
			// only two cases for this, click and move
			if(evLi.back_compat_mode) {
				if(evLi.event_type == 'click') {
					evLi.callback_function(oEventArgs.location);
				}
				else {
					evLi.callback_function();
				}
			}
			else {
				var scope = evLi.callback_object || this;
				evLi.callback_function.call(scope, oEventArgs);
			}
		}
	}
};

////////////////////
//
// map manipulation
//
/////////////////////

/**
 * addControls adds controls to the map. You specify which controls to add in
 * the associative array that is the only argument.
 * addControls can be called multiple time, with different args, to dynamically change controls.
 *
 * args = {
 *	 pan:	  true,
 *	 zoom:	 'large' || 'small',
 *	 overview: true,
 *	 scale:	true,
 *	 map_type: true,
 * }
 * @param {array} args Which controls to switch on
 */
Mapstraction.prototype.addControls = function( args ) {
	this.addControlsArgs = args;
	this.invoker.go('addControls', arguments);
};

/**
 * Adds a marker pin to the map
 * @param {Marker} marker The marker to add
 * @param {Boolean} old If true, doesn't add this marker to the markers array. Used by the "swap" method
 */
Mapstraction.prototype.addMarker = function(marker, old) {
	marker.mapstraction = this;
	marker.api = this.api;
	marker.location.api = this.api;
	marker.map = this.maps[this.api];
	var propMarker = this.invoker.go('addMarker', arguments);
	marker.setChild(propMarker);
	if (!old) {
		this.markers.push(marker);
	}
	this.markerAdded.fire({'marker': marker});
};

/**
 * addMarkerWithData will addData to the marker, then add it to the map
 * @param {Marker} marker The marker to add
 * @param {Object} data A data has to add
 */
Mapstraction.prototype.addMarkerWithData = function(marker, data) {
	marker.addData(data);
	this.addMarker(marker);
};

/**
 * addPolylineWithData will addData to the polyline, then add it to the map
 * @param {Polyline} polyline The polyline to add
 * @param {Object} data A data has to add
 */
Mapstraction.prototype.addPolylineWithData = function(polyline, data) {
	polyline.addData(data);
	this.addPolyline(polyline);
};

/**
 * removeMarker removes a Marker from the map
 * @param {Marker} marker The marker to remove
 */
Mapstraction.prototype.removeMarker = function(marker) {
	var current_marker;
	for(var i = 0; i < this.markers.length; i++){
		current_marker = this.markers[i];
		if(marker == current_marker) {
			this.invoker.go('removeMarker', arguments);
			marker.onmap = false;
			this.markers.splice(i, 1);
			this.markerRemoved.fire({'marker': marker});
			break;
		}
	}
};

/**
 * removeAllMarkers removes all the Markers on a map
 */
Mapstraction.prototype.removeAllMarkers = function() {
	var current_marker;
	while(this.markers.length > 0) {
		current_marker = this.markers.pop();
		this.invoker.go('removeMarker', [current_marker]);
	}
};

/**
 * Declutter the markers on the map, group together overlapping markers.
 * @param {Object} opts Declutter options
 */
Mapstraction.prototype.declutterMarkers = function(opts) {
	if(this.loaded[this.api] === false) {
		var me = this;
		this.onload[this.api].push( function() {
			me.declutterMarkers(opts);
		} );
		return;
	}

	var map = this.maps[this.api];

	switch(this.api)
	{
		//	case 'yahoo':
		//
		//	  break;
		//	case 'openstreetmap':
		//
		//	  break;
		//	case 'microsoft':
		//
		//	  break;
		//	case 'openlayers':
		//
		//	  break;
		case 'multimap':
			/*
			 * Multimap supports quite a lot of decluttering options such as whether
			 * to use an accurate of fast declutter algorithm and what icon to use to
			 * represent a cluster. Using all this would mean abstracting all the enums
			 * etc so we're only implementing the group name function at the moment.
			 */
			map.declutterGroup(opts.groupName);
			break;
		//	case 'mapquest':
		//
		//	  break;
		//	case 'map24':
		//
		//	  break;
		case '  dummy':
			break;
		default:
			if(this.debug) {
				alert(this.api + ' not supported by Mapstraction.declutterMarkers');
			}
	}
};

/**
 * Add a polyline to the map
 * @param {Polyline} polyline The Polyline to add to the map
 * @param {Boolean} old If true replaces an existing Polyline
 */
Mapstraction.prototype.addPolyline = function(polyline, old) {
	polyline.api = this.api;
	polyline.map = this.maps[this.api];
	var propPoly = this.invoker.go('addPolyline', arguments);
	polyline.setChild(propPoly);
	if(!old) {
		this.polylines.push(polyline);
	}
	this.polylineAdded.fire({'polyline': polyline});
};

// Private remove implementation
var removePolylineImpl = function(polyline) {
	this.invoker.go('removePolyline', arguments);
	polyline.onmap = false;
	this.polylineRemoved.fire({'polyline': polyline});
};

/**
 * Remove the polyline from the map
 * @param {Polyline} polyline The Polyline to remove from the map
 */
Mapstraction.prototype.removePolyline = function(polyline) {
	var current_polyline;
	for(var i = 0; i < this.polylines.length; i++){
		current_polyline = this.polylines[i];
		if(polyline == current_polyline) {
			this.polylines.splice(i, 1);
			removePolylineImpl.call(this, polyline);
			break;
		}
	}
};

/**
 * Removes all polylines from the map
 */
Mapstraction.prototype.removeAllPolylines = function() {
	var current_polyline;
	while(this.polylines.length > 0) {
		current_polyline = this.polylines.pop();
		removePolylineImpl.call(this, current_polyline);
	}
};

/**
 * autoCenterAndZoom sets the center and zoom of the map to the smallest bounding box
 * containing all markers
 */
Mapstraction.prototype.autoCenterAndZoom = function() {
	var lat_max = -90;
	var lat_min = 90;
	var lon_max = -180;
	var lon_min = 180;
	var lat, lon;
	var checkMinMax = function(){
		if (lat > lat_max) {
			lat_max = lat;
		}
		if (lat < lat_min) {
			lat_min = lat;
		}
		if (lon > lon_max) {
			lon_max = lon;
		}
		if (lon < lon_min) {
			lon_min = lon;
		}
	};
	for (var i = 0; i < this.markers.length; i++) {
		lat = this.markers[i].location.lat;
		lon = this.markers[i].location.lon;
		checkMinMax();
	}
	for(i = 0; i < this.polylines.length; i++) {
		for (var j = 0; j < this.polylines[i].points.length; j++) {
			lat = this.polylines[i].points[j].lat;
			lon = this.polylines[i].points[j].lon;
			checkMinMax();
		}
	}
	this.setBounds( new BoundingBox(lat_min, lon_min, lat_max, lon_max) );
};

/**
 * centerAndZoomOnPoints sets the center and zoom of the map from an array of points
 *
 * This is useful if you don't want to have to add markers to the map
 */
Mapstraction.prototype.centerAndZoomOnPoints = function(points) {
	var bounds = new BoundingBox(points[0].lat,points[0].lon,points[0].lat,points[0].lon);

	for (var i=1, len = points.length ; i<len; i++) {
		bounds.extend(points[i]);
	}

	this.setBounds(bounds);
};

/**
 * Sets the center and zoom of the map to the smallest bounding box
 * containing all visible markers and polylines
 * will only include markers and polylines with an attribute of "visible"
 */
Mapstraction.prototype.visibleCenterAndZoom = function() {
	var lat_max = -90;
	var lat_min = 90;
	var lon_max = -180;
	var lon_min = 180;
	var lat, lon;
	var checkMinMax = function(){
		if (lat > lat_max) {
			lat_max = lat;
		}
		if (lat < lat_min) {
			lat_min = lat;
		}
		if (lon > lon_max) {
			lon_max = lon;
		}
		if (lon < lon_min) {
			lon_min = lon;
		}
	};
	for (var i=0; i<this.markers.length; i++) {
		if (this.markers[i].getAttribute("visible")) {
			lat = this.markers[i].location.lat;
			lon = this.markers[i].location.lon;
			checkMinMax();
		}
	}

	for (i=0; i<this.polylines.length; i++){
		if (this.polylines[i].getAttribute("visible")) {
			for (j=0; j<this.polylines[i].points.length; j++) {
				lat = this.polylines[i].points[j].lat;
				lon = this.polylines[i].points[j].lon;
				checkMinMax();
			}
		}
	}

	this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
};

/**
 * Automatically sets center and zoom level to show all polylines
 * Takes into account radious of polyline
 * @param {Int} radius
 */
Mapstraction.prototype.polylineCenterAndZoom = function(radius) {
	var lat_max = -90;
	var lat_min = 90;
	var lon_max = -180;
	var lon_min = 180;

	for (var i=0; i < mapstraction.polylines.length; i++)
	{
		for (var j=0; j<mapstraction.polylines[i].points.length; j++)
		{
			lat = mapstraction.polylines[i].points[j].lat;
			lon = mapstraction.polylines[i].points[j].lon;

			latConv = lonConv = radius;

			if (radius > 0)
			{
				latConv = (radius / mapstraction.polylines[i].points[j].latConv());
				lonConv = (radius / mapstraction.polylines[i].points[j].lonConv());
			}

			if ((lat + latConv) > lat_max) {
				lat_max = (lat + latConv);
			}
			if ((lat - latConv) < lat_min) {
				lat_min = (lat - latConv);
			}
			if ((lon + lonConv) > lon_max) {
				lon_max = (lon + lonConv);
			}
			if ((lon - lonConv) < lon_min) {
				lon_min = (lon - lonConv);
			}
		}
	}

	this.setBounds(new BoundingBox(lat_min, lon_min, lat_max, lon_max));
};

/**
 * addImageOverlay layers an georeferenced image over the map
 * @param {id} unique DOM identifier
 * @param {src} url of image
 * @param {opacity} opacity 0-100
 * @param {west} west boundary
 * @param {south} south boundary
 * @param {east} east boundary
 * @param {north} north boundary
 */
Mapstraction.prototype.addImageOverlay = function(id, src, opacity, west, south, east, north) {

	var b = document.createElement("img");
	b.style.display = 'block';
	b.setAttribute('id',id);
	b.setAttribute('src',src);
	b.style.position = 'absolute';
	b.style.zIndex = 1;
	b.setAttribute('west',west);
	b.setAttribute('south',south);
	b.setAttribute('east',east);
	b.setAttribute('north',north);

	var oContext = {
		imgElm: b
	};

	this.invoker.go('addImageOverlay', arguments, { context: oContext });
};

Mapstraction.prototype.setImageOpacity = function(id, opacity) {
	if (opacity < 0) {
		opacity = 0;
	}
	if (opacity >= 100) {
		opacity = 100;
	}
	var c = opacity / 100;
	var d = document.getElementById(id);
	if(typeof(d.style.filter)=='string'){
		d.style.filter='alpha(opacity:'+opacity+')';
	}
	if(typeof(d.style.KHTMLOpacity)=='string'){
		d.style.KHTMLOpacity=c;
	}
	if(typeof(d.style.MozOpacity)=='string'){
		d.style.MozOpacity=c;
	}
	if(typeof(d.style.opacity)=='string'){
		d.style.opacity=c;
	}
};

Mapstraction.prototype.setImagePosition = function(id) {
	var imgElement = document.getElementById(id);
	var oContext = {
		latLng: {
			top: imgElement.getAttribute('north'),
			left: imgElement.getAttribute('west'),
			bottom: imgElement.getAttribute('south'),
			right: imgElement.getAttribute('east')
		},
		pixels: { top: 0, right: 0, bottom: 0, left: 0 }
	};

	this.invoker.go('setImagePosition', arguments, { context: oContext });

	imgElement.style.top = oContext.pixels.top.toString() + 'px';
	imgElement.style.left = oContext.pixels.left.toString() + 'px';
	imgElement.style.width = (oContext.pixels.right - oContext.pixels.left).toString() + 'px';
	imgElement.style.height = (oContext.pixels.bottom - oContext.pixels.top).toString() + 'px';
};

Mapstraction.prototype.addJSON = function(json) {
	var features;
	if (typeof(json) == "string") {
		features = eval('(' + json + ')');
	} else {
		features = json;
	}
	features = features.features;
	var map = this.maps[this.api];
	var html = "";
	var item;
	var polyline;
	var marker;
	var markers = [];

	if(features.type == "FeatureCollection") {
		this.addJSON(features.features);
	}

	for (var i = 0; i < features.length; i++) {
		item = features[i];
		switch(item.geometry.type) {
			case "Point":
				html = "<strong>" + item.title + "</strong><p>" + item.description + "</p>";
				marker = new Marker(new LatLonPoint(item.geometry.coordinates[1],item.geometry.coordinates[0]));
				markers.push(marker);
				this.addMarkerWithData(marker,{
					infoBubble : html,
					label : item.title,
					date : "new Date(\""+item.date+"\")",
					iconShadow : item.icon_shadow,
					marker : item.id,
					iconShadowSize : item.icon_shadow_size,
					icon : "http://boston.openguides.org/markers/AQUA.png",
					iconSize : item.icon_size,
					category : item.source_id,
					draggable : false,
					hover : false
				});
				break;
			case "Polygon":
				var points = [];
				polyline = new Polyline(points);
				mapstraction.addPolylineWithData(polyline,{
					fillColor : item.poly_color,
					date : "new Date(\""+item.date+"\")",
					category : item.source_id,
					width : item.line_width,
					opacity : item.line_opacity,
					color : item.line_color,
					polygon : true
				});
				markers.push(polyline);
				break;
			default:
		// console.log("Geometry: " + features.items[i].geometry.type);
		}
	}
	return markers;
};

/**
 * Adds a Tile Layer to the map
 *
 * Requires providing a parameterized tile url. Use {Z}, {X}, and {Y} to specify where the parameters
 *  should go in the URL.
 *
 * For example, the OpenStreetMap tiles are:
 *  m.addTileLayer("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png", 1.0, "OSM", 1, 19, true);
 *
 * @param {tile_url} template url of the tiles.
 * @param {opacity} opacity of the tile layer - 0 is transparent, 1 is opaque. (default=0.6)
 * @param {copyright_text} copyright text to use for the tile layer. (default=Mapstraction)
 * @param {min_zoom} Minimum (furtherest out) zoom level that tiles are available (default=1)
 * @param {max_zoom} Maximum (closest) zoom level that the tiles are available (default=18)
 * @param {map_type} Should the tile layer be a selectable map type in the layers palette (default=false)
 */
Mapstraction.prototype.addTileLayer = function(tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type) {
	if(!tile_url) {
		return;
	}

	this.tileLayers = this.tileLayers || [];
	opacity = opacity || 0.6;
	copyright_text = copyright_text || "Mapstraction";
	min_zoom = min_zoom || 1;
	max_zoom = max_zoom || 18;
	map_type = map_type || false;

	return this.invoker.go('addTileLayer', [ tile_url, opacity, copyright_text, min_zoom, max_zoom, map_type] );
};

/**
 * addFilter adds a marker filter
 * @param {field} name of attribute to filter on
 * @param {operator} presently only "ge" or "le"
 * @param {value} the value to compare against
 */
Mapstraction.prototype.addFilter = function(field, operator, value) {
	if (!this.filters) {
		this.filters = [];
	}
	this.filters.push( [field, operator, value] );
};

/**
 * Remove the specified filter
 * @param {Object} field
 * @param {Object} operator
 * @param {Object} value
 */
Mapstraction.prototype.removeFilter = function(field, operator, value) {
	if (!this.filters) {
		return;
	}

	var del;
	for (var f=0; f<this.filters.length; f++) {
		if (this.filters[f][0] == field &&
			(! operator || (this.filters[f][1] == operator && this.filters[f][2] == value))) {
			this.filters.splice(f,1);
			f--; //array size decreased
		}
	}
};

/**
 * Delete the current filter if present; otherwise add it
 * @param {Object} field
 * @param {Object} operator
 * @param {Object} value
 */
Mapstraction.prototype.toggleFilter = function(field, operator, value) {
	if (!this.filters) {
		this.filters = [];
	}

	var found = false;
	for (var f = 0; f < this.filters.length; f++) {
		if (this.filters[f][0] == field && this.filters[f][1] == operator && this.filters[f][2] == value) {
			this.filters.splice(f,1);
			f--; //array size decreased
			found = true;
		}
	}

	if (! found) {
		this.addFilter(field, operator, value);
	}
};

/**
 * removeAllFilters
 */
Mapstraction.prototype.removeAllFilters = function() {
	this.filters = [];
};

/**
 * doFilter executes all filters added since last call
 * Now supports a callback function for when a marker is shown or hidden
 * @param {Function} showCallback
 * @param {Function} hideCallback
 * @returns {Int} count of visible markers
 */
Mapstraction.prototype.doFilter = function(showCallback, hideCallback) {
	var map = this.maps[this.api];
	var visibleCount = 0;
	var f;
	if (this.filters) {
		switch (this.api) {
			case 'multimap':
				/* TODO polylines aren't filtered in multimap */
				var mmfilters = [];
				for (f=0; f<this.filters.length; f++) {
					mmfilters.push( new MMSearchFilter( this.filters[f][0], this.filters[f][1], this.filters[f][2] ));
				}
				map.setMarkerFilters( mmfilters );
				map.redrawMap();
				break;
			case '  dummy':
				break;
			default:
				var vis;
				for (var m=0; m<this.markers.length; m++) {
					vis = true;
					for (f = 0; f < this.filters.length; f++) {
						if (! this.applyFilter(this.markers[m], this.filters[f])) {
							vis = false;
						}
					}
					if (vis) {
						visibleCount ++;
						if (showCallback){
							showCallback(this.markers[m]);
						}
						else {
							this.markers[m].show();
						}
					}
					else {
						if (hideCallback){
							hideCallback(this.markers[m]);
						}
						else {
							this.markers[m].hide();
						}
					}

					this.markers[m].setAttribute("visible", vis);
				}
				break;
		}
	}
	return visibleCount;
};

Mapstraction.prototype.applyFilter = function(o, f) {
	var vis = true;
	switch (f[1]) {
		case 'ge':
			if (o.getAttribute( f[0] ) < f[2]) {
				vis = false;
			}
			break;
		case 'le':
			if (o.getAttribute( f[0] ) > f[2]) {
				vis = false;
			}
			break;
		case 'eq':
			if (o.getAttribute( f[0] ) == f[2]) {
				vis = false;
			}
			break;
	}

	return vis;
};

/**
 * getAttributeExtremes returns the minimum/maximum of "field" from all markers
 * @param {field} name of "field" to query
 * @returns {array} of minimum/maximum
 */
Mapstraction.prototype.getAttributeExtremes = function(field) {
	var min;
	var max;
	for (var m=0; m<this.markers.length; m++) {
		if (! min || min > this.markers[m].getAttribute(field)) {
			min = this.markers[m].getAttribute(field);
		}
		if (! max || max < this.markers[m].getAttribute(field)) {
			max = this.markers[m].getAttribute(field);
		}
	}
	for (var p=0; m<this.polylines.length; m++) {
		if (! min || min > this.polylines[p].getAttribute(field)) {
			min = this.polylines[p].getAttribute(field);
		}
		if (! max || max < this.polylines[p].getAttribute(field)) {
			max = this.polylines[p].getAttribute(field);
		}
	}

	return [min, max];
};

/**
 * getMap returns the native map object that mapstraction is talking to
 * @returns the native map object mapstraction is using
 */
Mapstraction.prototype.getMap = function() {
	// FIXME in an ideal world this shouldn't exist right?
	return this.maps[this.api];
};

//////////////////////////////
//
//   LatLonPoint
//
/////////////////////////////

/**
 * LatLonPoint is a point containing a latitude and longitude with helper methods
 * @name mxn.LatLonPoint
 * @constructor
 * @param {double} lat is the latitude
 * @param {double} lon is the longitude
 * @exports LatLonPoint as mxn.LatLonPoint
 */
var LatLonPoint = mxn.LatLonPoint = function(lat, lon) {
	// TODO error if undefined?
	//  if (lat == undefined) alert('undefined lat');
	//  if (lon == undefined) alert('undefined lon');
	this.lat = lat;
	this.lon = lon;
	this.lng = lon; // lets be lon/lng agnostic

	this.invoker = new mxn.Invoker(this, 'LatLonPoint');
};

mxn.addProxyMethods(LatLonPoint, [
	'fromProprietary', 'toProprietary'
], true);

/**
 * toString returns a string represntation of a point
 * @returns a string like '51.23, -0.123'
 * @type String
 */
LatLonPoint.prototype.toString = function() {
	return this.lat + ', ' + this.lon;
};

/**
 * distance returns the distance in kilometers between two points
 * @param {LatLonPoint} otherPoint The other point to measure the distance from to this one
 * @returns the distance between the points in kilometers
 * @type double
 */
LatLonPoint.prototype.distance = function(otherPoint) {
	// Uses Haversine formula from http://www.movable-type.co.uk
	var rads = Math.PI / 180;
	var diffLat = (this.lat-otherPoint.lat) * rads;
	var diffLon = (this.lon-otherPoint.lon) * rads;
	var a = Math.sin(diffLat / 2) * Math.sin(diffLat / 2) +
		Math.cos(this.lat*rads) * Math.cos(otherPoint.lat*rads) *
		Math.sin(diffLon/2) * Math.sin(diffLon/2);
	return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) * 6371; // Earth's mean radius in km
};

/**
 * equals tests if this point is the same as some other one
 * @param {LatLonPoint} otherPoint The other point to test with
 * @returns true or false
 * @type boolean
 */
LatLonPoint.prototype.equals = function(otherPoint) {
	return this.lat == otherPoint.lat && this.lon == otherPoint.lon;
};

/**
 * Returns latitude conversion based on current projection
 * @returns {Float} conversion
 */
LatLonPoint.prototype.latConv = function() {
	return this.distance(new LatLonPoint(this.lat + 0.1, this.lon))*10;
};

/**
 * Returns longitude conversion based on current projection
 * @returns {Float} conversion
 */
LatLonPoint.prototype.lonConv = function() {
	return this.distance(new LatLonPoint(this.lat, this.lon + 0.1))*10;
};

//////////////////////////
//
//  BoundingBox
//
//////////////////////////

/**
 * BoundingBox creates a new bounding box object
 * @name mxn.BoundingBox
 * @constructor
 * @param {double} swlat the latitude of the south-west point
 * @param {double} swlon the longitude of the south-west point
 * @param {double} nelat the latitude of the north-east point
 * @param {double} nelon the longitude of the north-east point
 * @exports BoundingBox as mxn.BoundingBox
 */
var BoundingBox = mxn.BoundingBox = function(swlat, swlon, nelat, nelon) {
	//FIXME throw error if box bigger than world
	//alert('new bbox ' + swlat + ',' +  swlon + ',' +  nelat + ',' + nelon);
	this.sw = new LatLonPoint(swlat, swlon);
	this.ne = new LatLonPoint(nelat, nelon);
};

/**
 * getSouthWest returns a LatLonPoint of the south-west point of the bounding box
 * @returns the south-west point of the bounding box
 * @type LatLonPoint
 */
BoundingBox.prototype.getSouthWest = function() {
	return this.sw;
};

/**
 * getNorthEast returns a LatLonPoint of the north-east point of the bounding box
 * @returns the north-east point of the bounding box
 * @type LatLonPoint
 */
BoundingBox.prototype.getNorthEast = function() {
	return this.ne;
};

/**
 * isEmpty finds if this bounding box has zero area
 * @returns whether the north-east and south-west points of the bounding box are the same point
 * @type boolean
 */
BoundingBox.prototype.isEmpty = function() {
	return this.ne == this.sw; // is this right? FIXME
};

/**
 * contains finds whether a given point is within a bounding box
 * @param {LatLonPoint} point the point to test with
 * @returns whether point is within this bounding box
 * @type boolean
 */
BoundingBox.prototype.contains = function(point){
	return point.lat >= this.sw.lat && point.lat <= this.ne.lat && point.lon >= this.sw.lon && point.lon <= this.ne.lon;
};

/**
 * toSpan returns a LatLonPoint with the lat and lon as the height and width of the bounding box
 * @returns a LatLonPoint containing the height and width of this bounding box
 * @type LatLonPoint
 */
BoundingBox.prototype.toSpan = function() {
	return new LatLonPoint( Math.abs(this.sw.lat - this.ne.lat), Math.abs(this.sw.lon - this.ne.lon) );
};

/**
 * extend extends the bounding box to include the new point
 */
BoundingBox.prototype.extend = function(point) {
	if(this.sw.lat > point.lat) {
		this.sw.lat = point.lat;
	}
	if(this.sw.lon > point.lon) {
		this.sw.lon = point.lon;
	}
	if(this.ne.lat < point.lat) {
		this.ne.lat = point.lat;
	}
	if(this.ne.lon < point.lon) {
		this.ne.lon = point.lon;
	}
	return;
};

//////////////////////////////
//
//  Marker
//
///////////////////////////////

/**
 * Marker create's a new marker pin
 * @name mxn.Marker
 * @constructor
 * @param {LatLonPoint} point the point on the map where the marker should go
 * @exports Marker as mxn.Marker
 */
var Marker = mxn.Marker = function(point) {
	this.api = null;
	this.location = point;
	this.onmap = false;
	this.proprietary_marker = false;
	this.attributes = [];
	this.invoker = new mxn.Invoker(this, 'Marker', function(){return this.api;});
	mxn.addEvents(this, [
		'openInfoBubble',	// Info bubble opened
		'closeInfoBubble', 	// Info bubble closed
		'click'				// Marker clicked
	]);
};

mxn.addProxyMethods(Marker, [
	'fromProprietary',
	'hide',
	'openBubble',
	'show',
	'toProprietary',
	'update'
]);

Marker.prototype.setChild = function(some_proprietary_marker) {
	this.proprietary_marker = some_proprietary_marker;
	some_proprietary_marker.mapstraction_marker = this;
	this.onmap = true;
};

Marker.prototype.setLabel = function(labelText) {
	this.labelText = labelText;
};

/**
 * addData conviniently set a hash of options on a marker
 */
Marker.prototype.addData = function(options){
	for(var sOptKey in options) {
		if(options.hasOwnProperty(sOptKey)){
			switch(sOptKey) {
				case 'label':
					this.setLabel(options.label);
					break;
				case 'infoBubble':
					this.setInfoBubble(options.infoBubble);
					break;
				case 'icon':
					if(options.iconSize && options.iconAnchor) {
						this.setIcon(options.icon, options.iconSize, options.iconAnchor);
					}
					else if(options.iconSize) {
						this.setIcon(options.icon, options.iconSize);
					}
					else {
						this.setIcon(options.icon);
					}
					break;
				case 'iconShadow':
					if(options.iconShadowSize) {
						this.setShadowIcon(options.iconShadow, [ options.iconShadowSize[0], options.iconShadowSize[1] ]);
					}
					else {
						this.setIcon(options.iconShadow);
					}
					break;
				case 'infoDiv':
					this.setInfoDiv(options.infoDiv[0],options.infoDiv[1]);
					break;
				case 'draggable':
					this.setDraggable(options.draggable);
					break;
				case 'hover':
					this.setHover(options.hover);
					this.setHoverIcon(options.hoverIcon);
					break;
				case 'hoverIcon':
					this.setHoverIcon(options.hoverIcon);
					break;
				case 'openBubble':
					this.openBubble();
					break;
				case 'groupName':
					this.setGroupName(options.groupName);
					break;
				default:
					// don't have a specific action for this bit of
					// data so set a named attribute
					this.setAttribute(sOptKey, options[sOptKey]);
					break;
			}
		}
	}
};

/**
 * setInfoBubble sets the html/text content for a bubble popup for a marker
 * @param {String} infoBubble the html/text you want displayed
 */
Marker.prototype.setInfoBubble = function(infoBubble) {
	this.infoBubble = infoBubble;
};

/**
 * setInfoDiv sets the text and the id of the div element where to the information
 *  useful for putting information in a div outside of the map
 * @param {String} infoDiv the html/text you want displayed
 * @param {String} div the element id to use for displaying the text/html
 */
Marker.prototype.setInfoDiv = function(infoDiv,div){
	this.infoDiv = infoDiv;
	this.div = div;
};

/**
 * setIcon sets the icon for a marker
 * @param {String} iconUrl The URL of the image you want to be the icon
 */
Marker.prototype.setIcon = function(iconUrl, iconSize, iconAnchor) {
	this.iconUrl = iconUrl;
	if(iconSize) {
		this.iconSize = iconSize;
	}
	if(iconAnchor) {
		this.iconAnchor = iconAnchor;
	}
};

/**
 * setIconSize sets the size of the icon for a marker
 * @param {String} iconSize The array size in pixels of the marker image
 */
Marker.prototype.setIconSize = function(iconSize){
	if(iconSize) {
		this.iconSize = iconSize;
	}
};

/**
 * setIconAnchor sets the anchor point for a marker
 * @param {String} iconAnchor The array offset of the anchor point
 */
Marker.prototype.setIconAnchor = function(iconAnchor){
	if(iconAnchor) {
		this.iconAnchor = iconAnchor;
	}
};

/**
 * setShadowIcon sets the icon for a marker
 * @param {String} iconUrl The URL of the image you want to be the icon
 */
Marker.prototype.setShadowIcon = function(iconShadowUrl, iconShadowSize){
	this.iconShadowUrl = iconShadowUrl;
	if(iconShadowSize) {
		this.iconShadowSize = iconShadowSize;
	}
};

Marker.prototype.setHoverIcon = function(hoverIconUrl){
	this.hoverIconUrl = hoverIconUrl;
};

/**
 * setDraggable sets the draggable state of the marker
 * @param {Bool} draggable set to true if marker should be draggable by the user
 */
Marker.prototype.setDraggable = function(draggable) {
	this.draggable = draggable;
};

/**
 * setHover sets that the marker info is displayed on hover
 * @param {Bool} hover set to true if marker should display info on hover
 */
Marker.prototype.setHover = function(hover) {
	this.hover = hover;
};

/**
 * Markers are grouped up by this name. declutterGroup makes use of this.
 */
Marker.prototype.setGroupName = function(sGrpName) {
	this.groupName = sGrpName;
};

/**
 * setAttribute: set an arbitrary key/value pair on a marker
 * @arg(String) key
 * @arg value
 */
Marker.prototype.setAttribute = function(key,value) {
	this.attributes[key] = value;
};

/**
 * getAttribute: gets the value of "key"
 * @arg(String) key
 * @returns value
 */
Marker.prototype.getAttribute = function(key) {
	return this.attributes[key];
};

///////////////
// Polyline ///
///////////////

/**
 * Instantiates a new Polyline.
 * @name mxn.Polyline
 * @constructor
 * @param {Point[]} points Points that make up the Polyline.
 * @exports Polyline as mxn.Polyline
 */
var Polyline = mxn.Polyline = function(points) {
	this.api = null;
	this.points = points;
	this.attributes = [];
	this.onmap = false;
	this.proprietary_polyline = false;
	this.pllID = "mspll-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
	this.invoker = new mxn.Invoker(this, 'Polyline', function(){return this.api;});
};

mxn.addProxyMethods(Polyline, [
	'fromProprietary',
	'hide',
	'show',
	'toProprietary',
	'update'
]);

/**
 * addData conviniently set a hash of options on a polyline
 */
Polyline.prototype.addData = function(options){
	for(var sOpt in options) {
		if(options.hasOwnProperty(sOpt)){
			switch(sOpt) {
				case 'color':
					this.setColor(options.color);
					break;
				case 'width':
					this.setWidth(options.width);
					break;
				case 'opacity':
					this.setOpacity(options.opacity);
					break;
				case 'closed':
					this.setClosed(options.closed);
					break;
				case 'fillColor':
					this.setFillColor(options.fillColor);
					break;
				default:
					this.setAttribute(sOpt, options[sOpt]);
					break;
			}
		}
	}
};

Polyline.prototype.setChild = function(some_proprietary_polyline) {
	this.proprietary_polyline = some_proprietary_polyline;
	this.onmap = true;
};

/**
 * in the form: #RRGGBB
 * Note map24 insists on upper case, so we convert it.
 */
Polyline.prototype.setColor = function(color){
	this.color = (color.length==7 && color[0]=="#") ? color.toUpperCase() : color;
};

/**
 * Stroke width of the polyline
 * @param {Integer} width
 */
Polyline.prototype.setWidth = function(width){
	this.width = width;
};

/**
 * A float between 0.0 and 1.0
 * @param {Float} opacity
 */
Polyline.prototype.setOpacity = function(opacity){
	this.opacity = opacity;
};

/**
 * Marks the polyline as a closed polygon
 * @param {Boolean} bClosed
 */
Polyline.prototype.setClosed = function(bClosed){
	this.closed = bClosed;
};

/**
 * Fill color for a closed polyline as HTML color value e.g. #RRGGBB
 * @param {String} sFillColor HTML color value #RRGGBB
 */
Polyline.prototype.setFillColor = function(sFillColor) {
	this.fillColor = sFillColor;
};

/**
 * setAttribute: set an arbitrary key/value pair on a polyline
 * @arg(String) key
 * @arg value
 */
Polyline.prototype.setAttribute = function(key,value) {
	this.attributes[key] = value;
};

/**
 * getAttribute: gets the value of "key"
 * @arg(String) key
 * @returns value
 */
Polyline.prototype.getAttribute = function(key) {
	return this.attributes[key];
};

/**
 * Simplifies a polyline, averaging and reducing the points
 * @param {Integer} tolerance (1.0 is a good starting point)
 */
Polyline.prototype.simplify = function(tolerance) {
	var reduced = [];

	// First point
	reduced[0] = this.points[0];

	var markerPoint = 0;

	for (var i = 1; i < this.points.length-1; i++){
		if (this.points[i].distance(this.points[markerPoint]) >= tolerance)
		{
			reduced[reduced.length] = this.points[i];
			markerPoint = i;
		}
	}

	// Last point
	reduced[reduced.length] = this.points[this.points.length-1];

	// Revert
	this.points = reduced;
};

///////////////
// Radius    //
///////////////

/**
 * Creates a new radius object for drawing circles around a point, does a lot of initial calculation to increase load time
 * @returns a new Radius
 * @type Radius
 * @constructor
 * @classDescription Radius
 * @param {Object} Center LatLonPoint of the radius
 * @param {quality} Number of points that comprise the approximated circle (20 is a good starting point)
 */
var Radius = mxn.Radius = function(center, quality) {
	this.center = center;
	var latConv = center.latConv();
	var lonConv = center.lonConv();

	// Create Radian conversion constant
	var rad = Math.PI / 180;
	this.calcs = [];

	for(var i = 0; i < 360; i += quality){
	    this.calcs.push([Math.cos(i * rad) / latConv, Math.sin(i * rad) / lonConv]);
        }
};

/**
 * Returns polyline of a circle around the point based on new radius
 * @param {Radius} radius
 * @param {Colour} colour
 * @returns {Polyline} Polyline
 */
Radius.prototype.getPolyline = function(radius, colour) {
        var points = [];

	for(var i = 0; i < this.calcs.length; i++){
		var point = new LatLonPoint(
			this.center.lat + (radius * this.calcs[i][0]),
			this.center.lon + (radius * this.calcs[i][1])
		);
		points.push(point);
	}

	// Add first point
	points.push(points[0]);

	var line = new Polyline(points);
	line.setColor(colour);

	return line;
};

})();


code:
mxn.register('geocommons', {

    Mapstraction: {

        init: function(element, api) {
            var me = this;
            this.element = element;
            Maker.maker_host='http://maker.geocommons.com';
            Maker.finder_host='http://finder.geocommons.com';
            Maker.core_host='http://geocommons.com';
          },

        applyOptions: function(){
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        resizeTo: function(width, height){
            var map = this.maps[this.api];
            map.setSize(width,height);
        },

        addControls: function( args ) {
            var map = this.maps[this.api];
            map.showControl("Zoom", args.zoom || false);
            map.showControl("Layers", args.layers || false);
            map.showControl("Styles", args.styles || false);
            map.showControl("Basemap", args.map_type || false);
            map.showControl("Legend", args.legend || false, "open");
            // showControl("Legend", true, "close");
        },

        addSmallControls: function() {
            var map = this.maps[this.api];
            showControl("Zoom", args.zoom);
            showControl("Legend", args.legend, "open");
        },

        addLargeControls: function() {
            var map = this.maps[this.api];
            showControl("Zoom", args.zoom);
            showControl("Layers", args.layers);
            showControl("Legend", args.legend, "open");
        },

        addMapTypeControls: function() {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        dragging: function(on) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        setCenterAndZoom: function(point, zoom) {
            var map = this.maps[this.api];
            map.setCenterZoom(point.lat, point.lon,zoom);
        },

        getCenter: function() {
            var map = this.maps[this.api];
            var point = map.getCenterZoom()[0];
            return mxn.LatLonPoint(point.lat,point.lon);
        },

        setCenter: function(point, options) {
            var map = this.maps[this.api];
            map.setCenter(point.lat, point.lon);
        },

        setZoom: function(zoom) {
            var map = this.maps[this.api];
            map.setZoom(zoom);
        },

        getZoom: function() {
            var map = this.maps[this.api];
            return map.getZoom();
        },

        getZoomLevelForBoundingBox: function( bbox ) {
            var map = this.maps[this.api];
            // NE and SW points from the bounding box.
            var ne = bbox.getNorthEast();
            var sw = bbox.getSouthWest();
            var zoom;

            // TODO: Add provider code

            return zoom;
        },

        setMapType: function(type) {
            var map = this.maps[this.api];
            switch(type) {
                case mxn.Mapstraction.ROAD:
                map.setMapProvider("OpenStreetMap (road)");
                break;
                case mxn.Mapstraction.SATELLITE:
                map.setMapProvider("BlueMarble");
                break;
                default:
                map.setMapProvider(type);
            }
        },

        getMapType: function() {
            var map = this.maps[this.api];
            switch(map.getMapProvider) {
                case "OpenStreetMap (Road)":
                    retu

            }
            // TODO: Add provider code

            //return mxn.Mapstraction.ROAD;
            //return mxn.Mapstraction.SATELLITE;
            //return mxn.Mapstraction.HYBRID;

        },

        getBounds: function () {
            var map = this.maps[this.api];
            var extent = map.getExtent();
            return new mxn.BoundingBox( extent.northWest.lat, extent.southEast.lon, extent.southEast.lat, extent.northWest.lon);
        },

        setBounds: function(bounds){
            var map = this.maps[this.api];
            var sw = bounds.getSouthWest();
            var ne = bounds.getNorthEast();
            map.setExtent(ne.lat,sw.lat,ne.lon,sw.lon);

        },

        addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        addOverlay: function(url, autoCenterAndZoom) {
            var map = this.maps[this.api];
            var me = this;
            Maker.load_map(this.element.id, url);
            setTimeout(function() { me.maps[me.api] = swfobject.getObjectById(FlashMap.dom_id);}, 500);
        },

        addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        toggleTileLayer: function(tile_url) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        getPixelRatio: function() {
            var map = this.maps[this.api];

            // TODO: Add provider code
        },

        mousePosition: function(element) {
            var map = this.maps[this.api];

            // TODO: Add provider code
        }
    },

    LatLonPoint: {

        toProprietary: function() {
            // TODO: Add provider code
        },

        fromProprietary: function(point) {
            // TODO: Add provider code
        }

    },

    Marker: {

        toProprietary: function() {
            // TODO: Add provider code
        },

        openBubble: function() {
            // TODO: Add provider code
        },

        hide: function() {
            // TODO: Add provider code
        },

        show: function() {
            // TODO: Add provider code
        },

        update: function() {
            // TODO: Add provider code
        }

    },

    Polyline: {

        toProprietary: function() {
            // TODO: Add provider code
        },

        show: function() {
            // TODO: Add provider code
        },

        hide: function() {
            // TODO: Add provider code
        }

    }

});


code:
// Auto-load scripts
//
// specify which map providers to load by using
// <script src="mxn.js?(provider1,provider2,[module1,module2])" ...
// in your HTML
//
// for each provider mxn.provider.module.js and mxn.module.js will be loaded
// module 'core' is always loaded
//
// NOTE: if you call without providers
// <script src="mxn.js" ...
// no scripts will be loaded at all and it is then up to you to load the scripts independently
(function() {
	var providers = null;
	var modules = 'core';
	var scriptBase;
	var scripts = document.getElementsByTagName('script');

	// Determine which scripts we need to load
	for (var i = 0; i < scripts.length; i++) {
		var match = scripts[i].src.replace(/%20/g , '').match(/^(.*?)mxn\.js(\?\(\[?(.*?)\]?\))?$/);
		if (match != null) {
			scriptBase = match[1];
			if (match[3]) {
				var settings = match[3].split(',[');
				providers = settings[0].replace(']' , '');
				if (settings[1]) modules += ',' + settings[1];
			}
			break;
	   }
	}

    if (providers == null || providers == 'none') return; // Bail out if no auto-load has been found
	providers = providers.replace(/ /g, '').split(',');
	modules = modules.replace(/ /g, '').split(',');

	// Actually load the scripts
	for (i = 0; i < modules.length; i++) {
	    document.write("<script type='text/javascript' src='" + scriptBase + 'mxn.' + modules[i] + '.js' + "'></script>");
	    for (var j = 0; j < providers.length; j++) document.write("<script type='text/javascript' src='" + scriptBase + 'mxn.' + providers[j] + '.' + modules[i] + '.js' + "'></script>");
	}
})();

(function(){

// holds all our implementing functions
var apis = {};

// Our special private methods
/**
 * Calls the API specific implementation of a particular method.
 * @private
 */
var invoke = function(sApiId, sObjName, sFnName, oScope, args){
	if(!hasImplementation(sApiId, sObjName, sFnName)) {
		throw 'Method ' + sFnName + ' of object ' + sObjName + ' is not supported by API ' + sApiId + '. Are you missing a script tag?';
	}
	return apis[sApiId][sObjName][sFnName].apply(oScope, args);
};

/**
 * Determines whether the specified API provides an implementation for the
 * specified object and function name.
 * @private
 */
var hasImplementation = function(sApiId, sObjName, sFnName){
	if(typeof(apis[sApiId]) == 'undefined') {
		throw 'API ' + sApiId + ' not loaded. Are you missing a script tag?';
	}
	if(typeof(apis[sApiId][sObjName]) == 'undefined') {
		throw 'Object definition ' + sObjName + ' in API ' + sApiId + ' not loaded. Are you missing a script tag?';
	}
	return typeof(apis[sApiId][sObjName][sFnName]) == 'function';
};

/**
 * @name mxn
 * @namespace
 */
var mxn = window.mxn = /** @lends mxn */ {

	/**
	 * Registers a set of provider specific implementation functions.
	 * @function
	 * @param {String} sApiId The API ID to register implementing functions for.
	 * @param {Object} oApiImpl An object containing the API implementation.
	 */
	register: function(sApiId, oApiImpl){
		if(!apis.hasOwnProperty(sApiId)){
			apis[sApiId] = {};
		}
		mxn.util.merge(apis[sApiId], oApiImpl);
	},

	/**
	 * Adds a list of named proxy methods to the prototype of a
	 * specified constructor function.
	 * @function
	 * @param {Function} func Constructor function to add methods to
	 * @param {Array} aryMethods Array of method names to create
	 * @param {Boolean} bWithApiArg Optional. Whether the proxy methods will use an API argument
	 */
	addProxyMethods: function(func, aryMethods, bWithApiArg){
		for(var i = 0; i < aryMethods.length; i++) {
			var sMethodName = aryMethods[i];
			if(bWithApiArg){
				func.prototype[sMethodName] = new Function('return this.invoker.go(\'' + sMethodName + '\', arguments, { overrideApi: true } );');
			}
			else {
				func.prototype[sMethodName] = new Function('return this.invoker.go(\'' + sMethodName + '\', arguments);');
			}
		}
	},

	/*
	checkLoad: function(funcDetails){
		if(this.loaded[this.api] === false) {
			var scope = this;
			this.onload[this.api].push( function() { funcDetails.callee.apply(scope, funcDetails); } );
			return true;
		}
		return false;
	},
	*/

	/**
	 * Bulk add some named events to an object.
	 * @function
	 * @param {Object} oEvtSrc The event source object.
	 * @param {String[]} aEvtNames Event names to add.
	 */
	addEvents: function(oEvtSrc, aEvtNames){
		for(var i = 0; i < aEvtNames.length; i++){
			var sEvtName = aEvtNames[i];
			if(sEvtName in oEvtSrc){
				throw 'Event or method ' + sEvtName + ' already declared.';
			}
			oEvtSrc[sEvtName] = new mxn.Event(sEvtName, oEvtSrc);
		}
	}

};

/**
 * Instantiates a new Event
 * @constructor
 * @param {String} sEvtName The name of the event.
 * @param {Object} oEvtSource The source object of the event.
 */
mxn.Event = function(sEvtName, oEvtSource){
	var handlers = [];
	if(!sEvtName){
		throw 'Event name must be provided';
	}
	/**
	 * Add a handler to the Event.
	 * @param {Function} fn The handler function.
	 * @param {Object} ctx The context of the handler function.
	 */
	this.addHandler = function(fn, ctx){
		handlers.push({context: ctx, handler: fn});
	};
	/**
	 * Remove a handler from the Event.
	 * @param {Function} fn The handler function.
	 * @param {Object} ctx The context of the handler function.
	 */
	this.removeHandler = function(fn, ctx){
		for(var i = 0; i < handlers.length; i++){
			if(handlers[i].handler == fn && handlers[i].context == ctx){
				handlers.splice(i, 1);
			}
		}
	};
	/**
	 * Remove all handlers from the Event.
	 */
	this.removeAllHandlers = function(){
		handlers = [];
	};
	/**
	 * Fires the Event.
	 * @param {Object} oEvtArgs Event arguments object to be passed to the handlers.
	 */
	this.fire = function(oEvtArgs){
		var args = [sEvtName, oEvtSource, oEvtArgs];
		for(var i = 0; i < handlers.length; i++){
			handlers[i].handler.apply(handlers[i].context, args);
		}
	};
};

/**
 * Creates a new Invoker, a class which helps with on-the-fly
 * invocation of the correct API methods.
 * @constructor
 * @param {Object} aobj The core object whose methods will make cals to go()
 * @param {String} asClassName The name of the Mapstraction class to be invoked, normally the same name as aobj's constructor function
 * @param {Function} afnApiIdGetter The function on object aobj which will return the active API ID
 */
mxn.Invoker = function(aobj, asClassName, afnApiIdGetter){
	var obj = aobj;
	var sClassName = asClassName;
	var fnApiIdGetter = afnApiIdGetter;
	var defOpts = {
		overrideApi: false, // {Boolean} API ID is overridden by value in first argument
		context: null, // {Object} Local vars can be passed from the body of the method to the API method within this object
		fallback: null // {Function} If an API implementation doesn't exist this function is run instead
	};

	/**
	 * Invoke the API implementation of a specific method.
	 * @param {String} sMethodName The method name to invoke
	 * @param {Array} args Arguments to pass on
	 * @param {Object} oOptions Optional. Extra options for invocation
	 * @param {Boolean} oOptions.overrideApi When true the first argument is used as the API ID.
	 * @param {Object} oOptions.context A context object for passing extra information on to the provider implementation.
	 * @param {Function} oOptions.fallback A fallback function to run if the provider implementation is missing.
	 */
	this.go = function(sMethodName, args, oOptions){

		if(typeof(oOptions) == 'undefined'){
			oOptions = defOpts;
		}

		var sApiId = oOptions.overrideApi ? args[0] : fnApiIdGetter.apply(obj);

		if(typeof(sApiId) != 'string'){
			throw 'API ID not available.';
		}

		if(typeof(oOptions.context) != 'undefined' && oOptions.context !== null){
			// make sure args is an array
			args = Array.prototype.slice.apply(args);
			args.push(oOptions.context);
		}

		if(typeof(oOptions.fallback) == 'function' && !hasImplementation(sApiId, sClassName, sMethodName)){
			// we've got no implementation but have got a fallback function
			return oOptions.fallback.apply(obj, args);
		}
		else {
			return invoke(sApiId, sClassName, sMethodName, obj, args);
		}

	};

};

/**
 * @namespace
 */
mxn.util = {

	/**
	 * Merges properties of one object into another recursively.
	 * @param {Object} oRecv The object receiveing properties
	 * @param {Object} oGive The object donating properties
	 */
	merge: function(oRecv, oGive){
		for (var sPropName in oGive){
			if (oGive.hasOwnProperty(sPropName)) {
				if(!oRecv.hasOwnProperty(sPropName)){
					oRecv[sPropName] = oGive[sPropName];
				}
				else {
					mxn.util.merge(oRecv[sPropName], oGive[sPropName]);
				}
			}
		}
	},

	/**
	 * $m, the dollar function, elegantising getElementById()
	 * @return An HTML element or array of HTML elements
	 */
	$m: function() {
		var elements = [];
		for (var i = 0; i < arguments.length; i++) {
			var element = arguments[i];
			if (typeof(element) == 'string') {
				element = document.getElementById(element);
			}
			if (arguments.length == 1) {
				return element;
			}
			elements.push(element);
		}
		return elements;
	},

	/**
	 * loadScript is a JSON data fetcher
	 * @param {String} src URL to JSON file
	 * @param {Function} callback Callback function
	 */
	loadScript: function(src, callback) {
		var script = document.createElement('script');
		script.type = 'text/javascript';
		script.src = src;
		if (callback) {
			if(script.addEventListener){
				script.addEventListener('load', callback, true);
			}
			else if(script.attachEvent){
				var done = false;
				script.attachEvent("onreadystatechange",function(){
					if ( !done && document.readyState === "complete" ) {
						done = true;
						callback();
					}
				});
			}
		}
		var h = document.getElementsByTagName('head')[0];
		h.appendChild( script );
		return;
	},

	/**
	 *
	 * @param {Object} point
	 * @param {Object} level
	 */
	convertLatLonXY_Yahoo: function(point, level) { //Mercator
		var size = 1 << (26 - level);
		var pixel_per_degree = size / 360.0;
		var pixel_per_radian = size / (2 * Math.PI);
		var origin = new YCoordPoint(size / 2 , size / 2);
		var answer = new YCoordPoint();
		answer.x = Math.floor(origin.x + point.lon * pixel_per_degree);
		var sin = Math.sin(point.lat * Math.PI / 180.0);
		answer.y = Math.floor(origin.y + 0.5 * Math.log((1 + sin) / (1 - sin)) * -pixel_per_radian);
		return answer;
	},

	/**
	 * Load a stylesheet from a remote file.
	 * @param {String} href URL to the CSS file
	 */
	loadStyle: function(href) {
		var link = document.createElement('link');
		link.type = 'text/css';
		link.rel = 'stylesheet';
		link.href = href;
		document.getElementsByTagName('head')[0].appendChild(link);
		return;
	},

	/**
	 * getStyle provides cross-browser access to css
	 * @param {Object} el HTML Element
	 * @param {String} prop Style property name
	 */
	getStyle: function(el, prop) {
		var y;
		if (el.currentStyle) {
			y = el.currentStyle[prop];
		}
		else if (window.getComputedStyle) {
			y = window.getComputedStyle( el, '').getPropertyValue(prop);
		}
		return y;
	},

	/**
	 * Convert longitude to metres
	 * http://www.uwgb.edu/dutchs/UsefulData/UTMFormulas.HTM
	 * "A degree of longitude at the equator is 111.2km... For other latitudes,
	 * multiply by cos(lat)"
	 * assumes the earth is a sphere but good enough for our purposes
	 * @param {Float} lon
	 * @param {Float} lat
	 */
	lonToMetres: function(lon, lat) {
		return lon * (111200 * Math.cos(lat * (Math.PI / 180)));
	},

	/**
	 * Convert metres to longitude
	 * @param {Object} m
	 * @param {Object} lat
	 */
	metresToLon: function(m, lat) {
		return m / (111200 * Math.cos(lat * (Math.PI / 180)));
	},

	/**
	 * Convert kilometres to miles
	 * @param {Float} km
	 * @returns {Float} miles
	 */
	KMToMiles: function(km) {
		return km / 1.609344;
	},

	/**
	 * Convert miles to kilometres
	 * @param {Float} miles
	 * @returns {Float} km
	 */
	milesToKM: function(miles) {
		return miles * 1.609344;
	},

	/**
	 *
	 * @param {Object} number
	 * @param {Object} base
	 */
	logN: function(number, base) {
		return Math.log(number) / Math.log(base);
	},

	/**
	 * Returns array of loaded provider apis
	 * @returns {Array} providers
	 */
	getAvailableProviders : function () {
		var providers = [];
		for (var propertyName in apis){
			if (apis.hasOwnProperty(propertyName)) {
				providers.push(propertyName);
			}
		}
		return providers;
	}

};

/**
 * Class for converting between HTML and RGB integer color formats.
 * Accepts either a HTML color string argument or three integers for R, G and B.
 * @constructor
 */
mxn.util.Color = function() {
	if(arguments.length == 3) {
		this.red = arguments[0];
		this.green = arguments[1];
		this.blue = arguments[2];
	}
	else if(arguments.length == 1) {
		this.setHexColor(arguments[0]);
	}
};

mxn.util.Color.prototype.reHex = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;

/**
 * Set the color from the supplied HTML hex string.
 * @param {String} strHexColor A HTML hex color string e.g. '#00FF88'.
 */
mxn.util.Color.prototype.setHexColor = function(strHexColor) {
	var match = strHexColor.match(this.reHex);
	if(match) {
		strHexColor = match[1];
	}
	else {
		throw 'Invalid HEX color format, expected #000, 000, #000000 or 000000';
	}
	if(strHexColor.length == 3) {
		strHexColor = strHexColor.replace(/\w/g, function(str){return str.concat(str);});
	}
	this.red = parseInt(strHexColor.substr(0,2), 16);
	this.green = parseInt(strHexColor.substr(2,2), 16);
	this.blue = parseInt(strHexColor.substr(4,2), 16);
};

/**
 * Retrieve the color value as an HTML hex string.
 * @returns {String} Format '00FF88' - note no preceding #.
 */
mxn.util.Color.prototype.getHexColor = function() {
	var vals = [this.red.toString(16), this.green.toString(16), this.blue.toString(16)];
	for(var i = 0; i < vals.length; i++) {
		vals[i] = (vals[i].length == 1) ? '0' + vals[i] : vals[i];
		vals[i] = vals[i].toUpperCase();
	}
	return vals.join('');
};

})();


code:
mxn.register('microsoft', {

Mapstraction: {

	init: function(element, api) {
		var me = this;
		if (VEMap){
			this.maps[api] = new VEMap(element.id);

			this.maps[api].AttachEvent('onclick', function(event){
				me.clickHandler();
				var x = event.mapX;
				var y = event.mapY;
			    var pixel = new VEPixel(x,y);
				me.click.fire({'location': new mxn.LatLonPoint(pixel.Latitude, pixel.Longitude)});

			});
			this.maps[api].AttachEvent('onendzoom', function(event){
				me.moveendHandler(me);
				me.changeZoom.fire();

			});
			this.maps[api].AttachEvent('onendpan', function(event){
				me.moveendHandler(me);
				me.endPan.fire();

			});
			this.maps[api].AttachEvent('onchangeview', function(event){
				me.endPan.fire();

			});
			this.maps[api].LoadMap();
			document.getElementById("MSVE_obliqueNotification").style.visibility = "hidden";

			//removes the bird's eye pop-up
			this.loaded[api] = true;
			me.load.fire();
		}
		else{
			alert(api + ' map script not imported')

		}

	},

	applyOptions: function(){
		var map = this.maps[this.api];
		if(this.options.enableScrollWheelZoom){
			map.enableContinuousZoom();
			map.enableScrollWheelZoom();

		}

	},

	resizeTo: function(width, height){
		this.maps[this.api].Resize(width, height);
	},

	addControls: function( args ) {
		var map = this.maps[this.api];

	if (args.pan) {
			map.SetDashboardSize(VEDashboardSize.Normal);
		}
		else {
			map.SetDashboardSize(VEDashboardSize.Tiny)
		}

  	if (args.zoom == 'large') {
			map.SetDashboardSize(VEDashboardSize.Small)
		}
		else if ( args.zoom == 'small' ) {
			map.SetDashboardSize(VEDashboardSize.Tiny)
		}
		else {
			map.HideDashboard();
			map.HideScalebar();
		}

	},

	addSmallControls: function() {
		var map = this.maps[this.api];
		map.SetDashboardSize(VEDashboardSize.Tiny);

	},

	addLargeControls: function() {
		var map = this.maps[this.api];
		map.SetDashboardSize(VEDashboardSize.Normal);
		this.addControlsArgs.pan=true;
		this.addControlsArgs.zoom = 'large';
	},

	addMapTypeControls: function() {
		var map = this.maps[this.api];
		map.addTypeControl();

	},

	dragging: function(on) {
		var map = this.maps[this.api];
		if(on){
			map.enableDragMap();
		}
		else{
			map.disableDragMap();

		}
	},

	setCenterAndZoom: function(point, zoom) {
		var map = this.maps[this.api];
		var pt = point.toProprietary(this.api);
		var vzoom =  zoom;
		map.SetCenterAndZoom(new VELatLong(point.lat,point.lon), vzoom)

	},

	addMarker: function(marker, old) {
		var map = this.maps[this.api];
                marker.pinID = "mspin-"+new Date().getTime()+'-'+(Math.floor(Math.random()*Math.pow(2,16)));
		var pin = marker.toProprietary(this.api);
	        map.AddShape(pin);
		//give onclick event
		//give on double click event
		//give on close window
		//return the marker

		return pin;
	},

	removeMarker: function(marker) {
		var map = this.maps[this.api];
		var id = marker.proprietary_marker.GetID();
		var microsoftShape = map.GetShapeByID(id);
		map.DeleteShape(microsoftShape);
	},

	removeAllMarkers: function() {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	declutterMarkers: function(opts) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	addPolyline: function(polyline, old) {
		var map = this.maps[this.api];
		var pl = polyline.toProprietary(this.api);
		pl.HideIcon();//hide the icon VE automatically displays
		map.AddShape(pl);

		return pl;
	},

	removePolyline: function(polyline) {
		var map = this.maps[this.api];
		var id = polyline.proprietary_polyline.GetID();
		var microsoftShape = map.GetShapeByID(id);
		map.DeleteShape(microsoftShape);
	},

	getCenter: function() {
		var map = this.maps[this.api];
		var LL = map.GetCenter();
		var point = new mxn.LatLonPoint(LL.Latitude, LL.Longitude);
		return point;

	},

	setCenter: function(point, options) {
		var map = this.maps[this.api];
		var pt = point.toProprietary(this.api);
		map.SetCenter(new VELatLong(point.lat, point.lon));

	},

	setZoom: function(zoom) {
		var map = this.maps[this.api];
		map.SetZoomLevel(zoom);

	},

	getZoom: function() {
		var map = this.maps[this.api];
		var zoom = map.GetZoomLevel();

		return zoom;
	},

	getZoomLevelForBoundingBox: function( bbox ) {
		var map = this.maps[this.api];
		// NE and SW points from the bounding box.
		var ne = bbox.getNorthEast();
		var sw = bbox.getSouthWest();
		var zoom;

		// TODO: Add provider code

		return zoom;
	},

	setMapType: function(type) {
		var map = this.maps[this.api];
		switch(type) {
			case mxn.Mapstraction.ROAD:
				map.SetMapStyle(VEMapStyle.Road);
				break;
			case mxn.Mapstraction.SATELLITE:
				map.SetMapStyle(VEMapStyle.Aerial);
				break;
			case mxn.Mapstraction.HYBRID:
				map.SetMapStyle(VEMapStyle.Hybrid);
				break;
			default:
				map.SetMapStyle(VEMapStyle.Road);
		}
	},

	getMapType: function() {
		var map = this.maps[this.api];
		var mode = map.GetMapStyle();
		switch(mode){
			case VEMapStyle.Aerial:
				return mxn.Mapstraction.SATELLITE;
			case VEMapStyle.Road:
				return mxn.Mapstraction.ROAD;
			case VEMapStyle.Hybrid:
				return mxn.Mapstraction.HYBRID;
			default:
				return null;

		}

	},

	getBounds: function () {
		var map = this.maps[this.api];
		view = map.GetMapView();
		var topleft = view.TopLeftLatLong;
		var bottomright = view.BottomRightLatLong;

		return new mxn.BoundingBox(bottomright.Latitude,topleft.Longitude,topleft.Latitude, bottomright.Longitude );
	},

	setBounds: function(bounds){
		var map = this.maps[this.api];
		var sw = bounds.getSouthWest();
		var ne = bounds.getNorthEast();

		var rec = new VELatLongRectangle(new VELatLong(ne.lat, ne.lon), new VELatLong(sw.lat, sw.lon));
		map.SetMapView(rec);

	},

	addImageOverlay: function(id, src, opacity, west, south, east, north, oContext) {
		var map = this.maps[this.api];

		// TODO: Add provider code
	},

	setImagePosition: function(id, oContext) {
		var map = this.maps[this.api];
		var topLeftPoint; var bottomRightPoint;

		// TODO: Add provider code

	//	oContext.pixels.top = ...;
	//	oContext.pixels.left = ...;
	//	oContext.pixels.bottom = ...;
	//	oContext.pixels.right = ...;
	},

	addOverlay: function(url, autoCenterAndZoom) {
		var map = this.maps[this.api];
	    var layer = new VEShapeLayer();
	    var mlayerspec = new VEShapeSourceSpecification(VEDataType.GeoRSS, url, layer);
	 	map.AddShapeLayer(layer);

	},

	addTileLayer: function(tile_url, opacity, copyright_text, min_zoom, max_zoom) {
		throw 'Not implemented';
	},

	toggleTileLayer: function(tile_url) {
		throw 'Not implemented';
	},

	getPixelRatio: function() {
		throw 'Not implemented';
	},

	mousePosition: function(element) {
		var locDisp = document.getElementById(element);
		if (locDisp != null) {
			var map = this.maps[this.api];
			map.AttachEvent("onmousemove", function(veEvent){
				var latlon = map.PixelToLatLong(new VEPixel(veEvent.mapX, veEvent.mapY));
				var loc = latlon.Latitude.toFixed(4) + " / " + latlon.Longitude.toFixed(4);
				locDisp.innerHTML = loc;

			});
			locDisp.innerHTML = "0.0000 / 0.0000";
		}
	}
},

LatLonPoint: {

	toProprietary: function() {
		return  new VELatLong(this.lat, this.lon);
	},

	fromProprietary: function(mpoint) {
		this.lat =mpoint.Latitude;
		this.lon =mpoint.Longitude;
	}

},

Marker: {

	toProprietary: function() {
		var mmarker = new VEShape(VEShapeType.Pushpin, this.location.toProprietary('microsoft'));

		return mmarker;

	},

	openBubble: function() {
		var mmarker = this.proprietary_marker;
		map.ClearInfoBoxStyles();
		mmarker.SetTitle(this.infoBubble);
	},

	hide: function() {
		this.proprietary_marker.hide();
	},

	show: function() {
		this.proprietary_marker_unhide();
	},

	update: function() {
		throw 'Not implemented';
	}

},

Polyline: {

	toProprietary: function() {
		var mpoints =[];
		for(var i =0, length = this.points.length; i < length; i++)
		{
			mpoints.push(this.points[i].toProprietary('microsoft'));
		}
		var mpolyline = new VEShape(VEShapeType.Polyline, mpoints);
		if(this.color){
			var color = new mxn.util.Color(this.color);
			var opacity = (typeof(this.opacity) == 'undefined' || this.opacity === null) ? 1.0 : this.opacity;
			var vecolor = new VEColor(color.red, color.green, color.blue, opacity);
			mpolyline.SetLineColor(vecolor);
		}
		//	TODO ability to change line width
		return mpolyline;
	},

	show: function() {
			this.proprietary_polyline.Show();
	},

	hide: function() {
			this.proprietary_polyline.Hide();
	}

}

});