(function(){ var $m = mxn.util.$m; /** * Mapstraction instantiates a map with some API choice into the HTML element given * @param {String} element The HTML element to replace with a map * @param {String} api The API to use, one of 'google', 'yahoo', 'microsoft', 'openstreetmap', 'multimap', 'map24', 'openlayers', 'mapquest' * @param {Bool} debug optional parameter to turn on debug support - this uses alert panels for unsupported actions * @constructor */ var Mapstraction = mxn.Mapstraction = function(element, api, debug) { this.api = api; // could detect this from imported scripts? this.maps = {}; this.currentElement = $m(element); this.eventListeners = []; this.markers = []; this.layers = []; this.polylines = []; this.images = []; this.loaded = {}; this.onload = {}; // option defaults this.options = { enableScrollWheelZoom: false } this.addControlsArgs = {}; // set up our invoker for calling API methods this.invoker = new mxn.Invoker(this, 'Mapstraction', function(){ return api; }); // TODO: Events mxn.addEvents(this, ['load', 'endPan', 'markerAdded', 'markerRemoved', 'polylineAdded', 'polylineRemoved']); // finally initialize our proper API map this.invoker.go('init', [ this.currentElement, api ]); } // Map type constants Mapstraction.ROAD = 1; Mapstraction.SATELLITE = 2; Mapstraction.HYBRID = 3; // methods that have no implementation in mapstraction core mxn.addProxyMethods(Mapstraction, [ 'addLargeControls', 'addMarker', 'addMapTypeControls', 'addOverlay', 'addPolyline', 'addSmallControls', 'applyOptions', 'dragging', 'getBounds', 'getCenter', 'getMapType', 'getPixelRatio', 'getZoom', 'getZoomLevelForBoundingBox', 'mousePosition', 'resizeTo', 'removeMarker', 'setBounds', 'setCenter', 'setCenterAndZoom', 'setMapType', 'setOption', 'setZoom', 'toggleTileLayer' ]); Mapstraction.prototype.setOptions = function(oOpts){ mxn.merge(this.options, oOpts); this.applyOptions(); }; Mapstraction.prototype.setOption = function(sOptName, vVal){ this.options[sOptName] = vVal; this.applyOptions(); }; /** * Enable scroll wheel zooming * Currently only supported by Google */ Mapstraction.prototype.enableScrollWheelZoom = function() { this.setOption('enableScrollWheelZoom', true); }; /** * 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) { this.addAPI($m(element),api); 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 * @type {Boolean} The state of the map loading */ 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 * @type {Boolean} The state of debugging */ 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); 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 'google': // // 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; 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); }; Mapstraction.prototype.removePolylineImpl = function(polyline) { this.invoker.go('removePolyline', arguments); polyline.onmap = false; this.polylineRemoved.fire(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); this.removePolylineImpl(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(); this.removePolylineImpl(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; for (var i=0; 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 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; } } } 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 (i=0; 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; } } } 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 (i=0; i < mapstraction.polylines.length; i++) { for (j=0; j 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, false, 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) { if(this.loaded[this.api] === false) { var me = this; this.onload[this.api].push( function() { me.setImagePosition(id); } ); return; } var map = this.maps[this.api]; var x = document.getElementById(id); var d; var e; switch (this.api) { case 'google': case 'openstreetmap': d = map.fromLatLngToDivPixel(new GLatLng(x.getAttribute('north'), x.getAttribute('west'))); e = map.fromLatLngToDivPixel(new GLatLng(x.getAttribute('south'), x.getAttribute('east'))); break; case 'multimap': d = map.geoPosToContainerPixels(new MMLatLon(x.getAttribute('north'), x.getAttribute('west'))); e = map.geoPosToContainerPixels(new MMLatLon(x.getAttribute('south'), x.getAttribute('east'))); break; case 'viamichelin': // TODO } x.style.top = d.y.toString() + 'px'; x.style.left = d.x.toString() + 'px'; x.style.width = (e.x - d.x).toString() + 'px'; x.style.height = (e.y - d.y).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, date : "new Date(\""+item.date+"\")", 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); 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: * http://tile.openstreetmap.org/{Z}/{X}/{Y}.png * * @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) */ Mapstraction.prototype.addTileLayer = function(tile_url, opacity, copyright_text, min_zoom, max_zoom) { 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; return this.invoker.go('addTileLayer', [ tile_url, opacity, copyright_text, min_zoom, max_zoom ] ); }; /** * 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