(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 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 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 = "" + item.title + "

" + item.description + "

"; 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