From fc08a5c8803fd79d58126528fa4a593ccfb8d512 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 17 Nov 2009 23:23:13 -0500 Subject: [PATCH] first pass at Mapstraction plugin --- plugins/Mapstraction/MapstractionPlugin.php | 149 ++ .../Mapstraction/js/mxn.(provider).core.js | 289 +++ plugins/Mapstraction/js/mxn.cloudmade.core.js | 357 ++++ plugins/Mapstraction/js/mxn.core.js | 1758 +++++++++++++++++ .../Mapstraction/js/mxn.geocommons.core.js | 233 +++ plugins/Mapstraction/js/mxn.google.core.js | 519 +++++ .../Mapstraction/js/mxn.google.geocoder.js | 179 ++ plugins/Mapstraction/js/mxn.googlev3.core.js | 443 +++++ plugins/Mapstraction/js/mxn.js | 505 +++++ plugins/Mapstraction/js/mxn.microsoft.core.js | 402 ++++ .../Mapstraction/js/mxn.openlayers.core.js | 513 +++++ plugins/Mapstraction/js/mxn.yahoo.core.js | 391 ++++ 12 files changed, 5738 insertions(+) create mode 100644 plugins/Mapstraction/MapstractionPlugin.php create mode 100644 plugins/Mapstraction/js/mxn.(provider).core.js create mode 100644 plugins/Mapstraction/js/mxn.cloudmade.core.js create mode 100644 plugins/Mapstraction/js/mxn.core.js create mode 100644 plugins/Mapstraction/js/mxn.geocommons.core.js create mode 100644 plugins/Mapstraction/js/mxn.google.core.js create mode 100644 plugins/Mapstraction/js/mxn.google.geocoder.js create mode 100644 plugins/Mapstraction/js/mxn.googlev3.core.js create mode 100644 plugins/Mapstraction/js/mxn.js create mode 100644 plugins/Mapstraction/js/mxn.microsoft.core.js create mode 100644 plugins/Mapstraction/js/mxn.openlayers.core.js create mode 100644 plugins/Mapstraction/js/mxn.yahoo.core.js diff --git a/plugins/Mapstraction/MapstractionPlugin.php b/plugins/Mapstraction/MapstractionPlugin.php new file mode 100644 index 0000000000..9a99d67427 --- /dev/null +++ b/plugins/Mapstraction/MapstractionPlugin.php @@ -0,0 +1,149 @@ +. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @copyright 2009 StatusNet Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Plugin to provide map visualization of location data + * + * This plugin uses the Mapstraction JavaScript library to + * + * @category Plugin + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * + * @seeAlso Location + */ + +class MapstractionPlugin extends Plugin +{ + /** provider name, one of: + 'cloudmade', 'google', 'microsoft', 'openlayers', 'yahoo' */ + public $provider = 'openlayers'; + /** provider API key (or 'appid'), if required ('google' and 'yahoo' only) */ + public $apikey = null; + + /** + * Hook for new URLs + * + * The way to register new actions from a plugin. + * + * @param Router &$m reference to router + * + * @return boolean event handler return + */ + + function onRouterInitialized(&$m) + { + $m->connect(':nickname/all/map', + array('action' => 'allmap'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); + $m->connect(':nickname/map', + array('action' => 'usermap'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); + return true; + } + + /** + * Hook for autoloading classes + * + * This makes sure our classes get autoloaded from our directory + * + * @param string $cls name of class being used + * + * @return boolean event handler return + */ + + function onAutoload($cls) + { + switch ($cls) + { + case 'AllmapAction': + case 'UsermapAction': + include_once INSTALLDIR.'/plugins/Mapstraction/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + default: + return true; + } + } + + /** + * Hook for adding extra JavaScript + * + * This makes sure our scripts get loaded for map-related pages + * + * @param Action $action Action object for the page + * + * @return boolean event handler return + */ + + function onEndShowScripts($action) + { + // These are the ones that have maps on 'em + if (!in_array($action->trimmed('action'), + array('showstream', 'all', 'allmap', 'usermap'))) { + return true; + } + + switch ($this->provider) + { + case 'cloudmade': + $action->script('http://tile.cloudmade.com/wml/0.2/web-maps-lite.js'); + break; + case 'google': + $action->script(sprintf('http://maps.google.com/maps?file=api&v=2&sensor=false&key=%s', + $this->apikey)); + break; + case 'microsoft': + $action->script('http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6'); + break; + case 'openlayers': + // XXX: is this not nice...? + $action->script('http://openlayers.org/api/OpenLayers.js'); + break; + case 'yahoo': + $action->script(sprintf('http://api.maps.yahoo.com/ajaxymap?v=3.8&appid=%s', + $this->apikey)); + break; + case 'geocommons': // don't support this yet + default: + return true; + } + + $action->script(sprintf('%s?(%s)', + common_path('plugins/Mapstraction/js/mxn.js'), + $this->provider)); + + return true; + } +} diff --git a/plugins/Mapstraction/js/mxn.(provider).core.js b/plugins/Mapstraction/js/mxn.(provider).core.js new file mode 100644 index 0000000000..cc9752ffd4 --- /dev/null +++ b/plugins/Mapstraction/js/mxn.(provider).core.js @@ -0,0 +1,289 @@ +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(googlePoint) { + // 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 + } + +} + +}); \ No newline at end of file diff --git a/plugins/Mapstraction/js/mxn.cloudmade.core.js b/plugins/Mapstraction/js/mxn.cloudmade.core.js new file mode 100644 index 0000000000..b6ee70b8f2 --- /dev/null +++ b/plugins/Mapstraction/js/mxn.cloudmade.core.js @@ -0,0 +1,357 @@ +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 Google 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(); + } + + } + +}); diff --git a/plugins/Mapstraction/js/mxn.core.js b/plugins/Mapstraction/js/mxn.core.js new file mode 100644 index 0000000000..c75d0969e4 --- /dev/null +++ b/plugins/Mapstraction/js/mxn.core.js @@ -0,0 +1,1758 @@ +(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 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