(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 'google', 'googlev3', '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 '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; 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; }; })();