/** * vim:et sts=4 sw=4 cindent: * @ignore * * @author migurski * @author darren * @author tom * * com.modestmaps.Map is the base class and interface for Modest Maps. * * @description Map is the base class and interface for Modest Maps. * Correctly attaching an instance of this Sprite subclass * should result in a pannable map. Controls and event * handlers must be added separately. * * @usage * import com.modestmaps.Map; * import com.modestmaps.geo.Location; * import com.modestmaps.mapproviders.BlueMarbleMapProvider; * ... * var map:Map = new Map(640, 480, true, new BlueMarbleMapProvider()); * addChild(map); * * */ package com.modestmaps { import com.modestmaps.core.*; import com.modestmaps.events.*; import com.modestmaps.geo.*; import com.modestmaps.mapproviders.IMapProvider; import com.modestmaps.mapproviders.microsoft.MicrosoftProvider; import com.modestmaps.overlays.MarkerClip; import flash.display.DisplayObject; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; [Event(name="startZooming", type="com.modestmaps.events.MapEvent")] [Event(name="stopZooming", type="com.modestmaps.events.MapEvent")] [Event(name="zoomedBy", type="com.modestmaps.events.MapEvent")] [Event(name="startPanning", type="com.modestmaps.events.MapEvent")] [Event(name="stopPanning", type="com.modestmaps.events.MapEvent")] [Event(name="panned", type="com.modestmaps.events.MapEvent")] [Event(name="resized", type="com.modestmaps.events.MapEvent")] [Event(name="mapProviderChanged",type="com.modestmaps.events.MapEvent")] [Event(name="beginExtentChange", type="com.modestmaps.events.MapEvent")] [Event(name="extentChanged", type="com.modestmaps.events.MapEvent")] [Event(name="beginTileLoading", type="com.modestmaps.events.MapEvent")] [Event(name="allTilesLoaded", type="com.modestmaps.events.MapEvent")] [Event(name="rendered", type="com.modestmaps.events.MapEvent")] [Event(name="markerRollOver", type="com.modestmaps.events.MarkerEvent")] [Event(name="markerRollOut", type="com.modestmaps.events.MarkerEvent")] [Event(name="markerClick", type="com.modestmaps.events.MarkerEvent")] public class Map extends Sprite { protected var mapWidth:Number = 320; protected var mapHeight:Number = 240; protected var __draggable:Boolean = true; /** das grid */ public var grid:TileGrid; /** markers are attached here */ public var markerClip:MarkerClip; /** Who do we get our Map urls from? How far can we pan? */ protected var mapProvider:IMapProvider; /** fraction of width/height to pan panLeft, panRight, panUp, panDown * @default 0.333333333 */ public var panFraction:Number = 0.333333333; /** * Initialize the map: set properties, add a tile grid, draw it. * Default extent covers the entire globe, (+/-85, +/-180). * * @param Width of map, in pixels. * @param Height of map, in pixels. * @param Whether the map can be dragged or not. * @param Desired map provider, e.g. Blue Marble. * @param Either a MapExtent or a Location and zoom (comma separated) * * @see com.modestmaps.core.TileGrid */ public function Map(width:Number=320, height:Number=240, draggable:Boolean=true, mapProvider:IMapProvider=null, ... rest) { if (!mapProvider) mapProvider = new MicrosoftProvider(MicrosoftProvider.ROAD); // TODO getter/setter for this that disables interaction in TileGrid __draggable = draggable; // don't call setMapProvider here // the extent calculations are all squirrely this.mapProvider = mapProvider; // initialize the grid (so point/location/coordinate functions should be valid after this) grid = new TileGrid(mapWidth, mapHeight, draggable, mapProvider); grid.addEventListener(Event.CHANGE, onExtentChanged); addChild(grid); setSize(width, height); markerClip = new MarkerClip(this); addChild(markerClip); // if rest was passed in from super constructor in a subclass, // it will be an array... if (rest && rest.length > 0 && rest[0] is Array) { rest = rest[0]; } // (doing that is OK because none of the arguments we're expecting are Arrays) // look at ... rest arguments for MapExtent or Location/zoom if (rest && rest.length > 0 && rest[0] is MapExtent) { setExtent(rest[0] as MapExtent); } else if (rest && rest.length > 1 && rest[0] is Location && rest[1] is Number) { setCenterZoom(rest[0] as Location, rest[1] as Number); } else { // use the whole world as a default var extent:MapExtent = new MapExtent(85, -85, 180, -180); // but adjust to fit the mapprovider's outer limits if there are any: var l1:Location = mapProvider.coordinateLocation(mapProvider.outerLimits()[0]); var l2:Location = mapProvider.coordinateLocation(mapProvider.outerLimits()[1]); if (!isNaN(l1.lat) && Math.abs(l1.lat) != Infinity) { extent.north = l1.lat; } if (!isNaN(l2.lat) && Math.abs(l2.lat) != Infinity) { extent.south = l2.lat; } if (!isNaN(l1.lon) && Math.abs(l1.lon) != Infinity) { extent.west = l1.lon; } if (!isNaN(l2.lon) && Math.abs(l2.lon) != Infinity) { extent.east = l2.lon; } setExtent(extent); } //addChild(grid.debugField); } /** * Based on an array of locations, determine appropriate map * bounds using calculateMapExtent(), and inform the grid of * tile coordinate and point by calling grid.resetTiles(). * Resulting map extent will ensure that all passed locations * are visible. * * @param extent the minimum area to fit inside the map view * * @see com.modestmaps.Map#calculateMapExtent * @see com.modestmaps.core.TileGrid#resetTiles */ public function setExtent(extent:MapExtent):void { onExtentChanging(); // tell grid what the rock is cooking grid.resetTiles(locationsCoordinate( [ extent.northWest, extent.southEast ] )); onExtentChanged(); } /** * Based on a location and zoom level, determine appropriate initial * tile coordinate and point using calculateMapCenter(), and inform * the grid of tile coordinate and point by calling grid.resetTiles(). * * @param Location of center. * @param Desired zoom level. * * @see com.modestmaps.Map#calculateMapExtent * @see com.modestmaps.core.TileGrid#resetTiles */ public function setCenterZoom(location:Location, zoom:Number):void { if (zoom == grid.zoomLevel) { setCenter(location); } else { onExtentChanging(); zoom = Math.min(Math.max(zoom, grid.minZoom), grid.maxZoom); // tell grid what the rock is cooking grid.resetTiles(mapProvider.locationCoordinate(location).zoomTo(zoom)); onExtentChanged(); } } /** * Based on a zoom level, determine appropriate initial * tile coordinate and point using calculateMapCenter(), and inform * the grid of tile coordinate and point by calling grid.resetTiles(). * * @param Desired zoom level. * * @see com.modestmaps.Map#calculateMapExtent * @see com.modestmaps.core.TileGrid#resetTiles */ public function setZoom(zoom:Number):void { if (zoom != grid.zoomLevel) { // TODO: if grid enforces this in enforceBounds, do we need to do it here too? grid.zoomLevel = Math.min(Math.max(zoom, grid.minZoom), grid.maxZoom); } } public function extentCoordinate(extent:MapExtent):Coordinate { return locationsCoordinate([ extent.northWest, extent.southEast ]); } public function locationsCoordinate(locations:Array, fitWidth:Number=0, fitHeight:Number=0):Coordinate { if (!fitWidth) fitWidth = mapWidth; if (!fitHeight) fitHeight = mapHeight; var TL:Coordinate = mapProvider.locationCoordinate(locations[0]); var BR:Coordinate = TL.copy(); // get outermost top left and bottom right coordinates to cover all locations for (var i:int = 1; i < locations.length; i++) { var coordinate:Coordinate = mapProvider.locationCoordinate(locations[i]); TL.row = Math.min(TL.row, coordinate.row); TL.column = Math.min(TL.column, coordinate.column), TL.zoom = Math.min(TL.zoom, coordinate.zoom); BR.row = Math.max(BR.row, coordinate.row), BR.column = Math.max(BR.column, coordinate.column), BR.zoom = Math.max(BR.zoom, coordinate.zoom); } // multiplication factor between horizontal span and map width var hFactor:Number = (BR.column - TL.column) / (fitWidth / mapProvider.tileWidth); // multiplication factor expressed as base-2 logarithm, for zoom difference var hZoomDiff:Number = Math.log(hFactor) / Math.LN2; // possible horizontal zoom to fit geographical extent in map width var hPossibleZoom:Number = TL.zoom - Math.ceil(hZoomDiff); // multiplication factor between vertical span and map height var vFactor:Number = (BR.row - TL.row) / (fitHeight / mapProvider.tileHeight); // multiplication factor expressed as base-2 logarithm, for zoom difference var vZoomDiff:Number = Math.log(vFactor) / Math.LN2; // possible vertical zoom to fit geographical extent in map height var vPossibleZoom:Number = TL.zoom - Math.ceil(vZoomDiff); // initial zoom to fit extent vertically and horizontally // additionally, make sure it's not outside the boundaries set by provider limits var initZoom:Number = Math.min(hPossibleZoom, vPossibleZoom); initZoom = Math.min(initZoom, mapProvider.outerLimits()[1].zoom); initZoom = Math.max(initZoom, mapProvider.outerLimits()[0].zoom); // coordinate of extent center var centerRow:Number = (TL.row + BR.row) / 2; var centerColumn:Number = (TL.column + BR.column) / 2; var centerZoom:Number = (TL.zoom + BR.zoom) / 2; var centerCoord:Coordinate = (new Coordinate(centerRow, centerColumn, centerZoom)).zoomTo(initZoom); return centerCoord; } /* * Return a MapExtent for the current map view. * TODO: MapExtent needs adapting to deal with non-rectangular map projections * * @return MapExtent object */ public function getExtent():MapExtent { var extent:MapExtent = new MapExtent(); if(!mapProvider) { throw new Error("WHOAH, no mapProvider in getExtent!"); } extent.northWest = mapProvider.coordinateLocation(grid.topLeftCoordinate); extent.southEast = mapProvider.coordinateLocation(grid.bottomRightCoordinate); return extent; } /* * Return the current center location and zoom of the map. * * @return Array of center and zoom: [center location, zoom number]. */ public function getCenterZoom():Array { return [ mapProvider.coordinateLocation(grid.centerCoordinate), grid.zoomLevel ]; } /* * Return the current center location of the map. * * @return center Location */ public function getCenter():Location { return mapProvider.coordinateLocation(grid.centerCoordinate); } /* * Return the current zoom level of the map. * * @return zoom number */ public function getZoom():int { return Math.floor(grid.zoomLevel); } /** * Set new map size, dispatch MapEvent.RESIZED. * The MapEvent includes the newSize. * * @param w New map width. * @param h New map height. * * @see com.modestmaps.events.MapEvent.RESIZED */ public function setSize(w:Number, h:Number):void { if (w != mapWidth || h != mapHeight) { mapWidth = w; mapHeight = h; // mask out out of bounds marker remnants scrollRect = new Rectangle(0,0,mapWidth,mapHeight); grid.resizeTo(new Point(mapWidth, mapHeight)); dispatchEvent(new MapEvent(MapEvent.RESIZED, this.getSize())); } } /** * Get map size. * * @return Array of [width, height]. */ public function getSize():/*Number*/Array { var size:/*Number*/Array = [mapWidth, mapHeight]; return size; } public function get size():Point { return new Point(mapWidth, mapHeight); } public function set size(value:Point):void { setSize(value.x, value.y); } /** Get map width. */ public function getWidth():Number { return mapWidth; } /** Get map height. */ public function getHeight():Number { return mapHeight; } /** * Get a reference to the current map provider. * * @return Map provider. * * @see com.modestmaps.mapproviders.IMapProvider */ public function getMapProvider():IMapProvider { return mapProvider; } /** * Set a new map provider, repainting tiles and changing bounding box if necessary. * * @param Map provider. * * @see com.modestmaps.mapproviders.IMapProvider */ public function setMapProvider(newProvider:IMapProvider):void { var previousGeometry:String; if (mapProvider) { previousGeometry = mapProvider.geometry(); } var extent:MapExtent = getExtent(); mapProvider = newProvider; if (grid) { grid.setMapProvider(mapProvider); } if (mapProvider.geometry() != previousGeometry) { setExtent(extent); } // among other things this will notify the marker clip that its cached coordinates are invalid dispatchEvent(new MapEvent(MapEvent.MAP_PROVIDER_CHANGED, newProvider)); } /** * Get a point (x, y) for a location (lat, lon) in the context of a given clip. * * @param Location to match. * @param Movie clip context in which returned point should make sense. * * @return Matching point. */ public function locationPoint(location:Location, context:DisplayObject=null):Point { var coord:Coordinate = mapProvider.locationCoordinate(location); return grid.coordinatePoint(coord, context); } /** * Get a location (lat, lon) for a point (x, y) in the context of a given clip. * * @param Point to match. * @param Movie clip context in which passed point should make sense. * * @return Matching location. */ public function pointLocation(point:Point, context:DisplayObject=null):Location { var coord:Coordinate = grid.pointCoordinate(point, context); return mapProvider.coordinateLocation(coord); } /** Pan up by 1/3 (or panFraction) of the map height. */ public function panUp(event:Event=null):void { panBy(0, mapHeight*panFraction); } /** Pan down by 1/3 (or panFraction) of the map height. */ public function panDown(event:Event=null):void { panBy(0, -mapHeight*panFraction); } /** Pan left by 1/3 (or panFraction) of the map width. */ public function panLeft(event:Event=null):void { panBy((mapWidth*panFraction), 0); } /** Pan left by 1/3 (or panFraction) of the map width. */ public function panRight(event:Event=null):void { panBy(-(mapWidth*panFraction), 0); } public function panBy(px:Number, py:Number):void { if (!grid.panning && !grid.zooming) { grid.prepareForPanning(); grid.tx += px; grid.ty += py; grid.donePanning(); } } /** zoom in, keeping the requested point in the same place */ public function zoomInAbout(targetPoint:Point=null, duration:Number=-1):void { zoomByAbout(1, targetPoint, duration); } /** zoom out, keeping the requested point in the same place */ public function zoomOutAbout(targetPoint:Point=null, duration:Number=-1):void { zoomByAbout(-1, targetPoint, duration); } /** zoom in or out by zoomDelta, keeping the requested point in the same place */ public function zoomByAbout(zoomDelta:Number, targetPoint:Point=null, duration:Number=-1):void { if (!targetPoint) targetPoint = new Point(mapWidth/2, mapHeight/2); if (grid.zoomLevel + zoomDelta < grid.minZoom) { zoomDelta = grid.minZoom - grid.zoomLevel; } else if (grid.zoomLevel + zoomDelta > grid.maxZoom) { zoomDelta = grid.maxZoom - grid.zoomLevel; } var sc:Number = Math.pow(2, zoomDelta); grid.prepareForZooming(); grid.prepareForPanning(); var m:Matrix = grid.getMatrix(); m.translate(-targetPoint.x, -targetPoint.y); m.scale(sc, sc); m.translate(targetPoint.x, targetPoint.y); grid.setMatrix(m); grid.doneZooming(); grid.donePanning(); } /** zoom in and put the given location in the center of the screen, or optionally at the given targetPoint */ public function panAndZoomIn(location:Location, targetPoint:Point=null):void { panAndZoomBy(2, location, targetPoint); } /** zoom out and put the given location in the center of the screen, or optionally at the given targetPoint */ public function panAndZoomOut(location:Location, targetPoint:Point=null):void { panAndZoomBy(0.5, location, targetPoint); } /** zoom in or out by sc, moving the given location to the requested target */ public function panAndZoomBy(sc:Number, location:Location, targetPoint:Point=null, duration:Number=-1):void { if (!targetPoint) targetPoint = new Point(mapWidth/2, mapHeight/2); var p:Point = locationPoint(location); grid.prepareForZooming(); grid.prepareForPanning(); var m:Matrix = grid.getMatrix(); m.translate(-p.x, -p.y); m.scale(sc, sc); m.translate(targetPoint.x, targetPoint.y); grid.setMatrix(m); grid.donePanning(); grid.doneZooming(); } /** put the given location in the middle of the map */ public function setCenter(location:Location):void { onExtentChanging(); // tell grid what the rock is cooking grid.resetTiles(mapProvider.locationCoordinate(location).zoomTo(grid.zoomLevel)); onExtentChanged(); } /** * Zoom in by one zoom level (to 200%) immediately, * rounding up to the nearest zoom level if we're currently between zooms. * *

Triggers MapEvent.START_ZOOMING and MapEvent.STOP_ZOOMING events.

* * @param event an optional event so that zoomIn can directly function as an event listener. */ public function zoomIn(event:Event=null):void { zoomBy(1); } /** * Zoom out by one zoom level (to 50%) immediately, * rounding down to the nearest zoom level if we're currently between zooms. * *

Triggers MapEvent.START_ZOOMING and MapEvent.STOP_ZOOMING events.

* * @param event an optional event so that zoomOut can directly function as an event listener. */ public function zoomOut(event:Event=null):void { zoomBy(-1); } /** * Adds dir to grid.zoomLevel, and rounds up or down to the nearest whole number. * Used internally by zoomIn and zoomOut (keeping it DRY, as they say) * and overridden by TweenMap for animation. * *

grid.zoomLevel calls the grid.scale setter for us * which will call grid.prepareForZooming if we didn't already * and grid.doneZooming after modifying the zoom level.

* *

Animating/tweening grid.scale fires START_ZOOMING, and STOP_ZOOMING * MapEvents unless you call grid.prepareForZooming first. Be sure * to also call grid.stopZooming at the end of your animation. * * @param dir the direction of zoom, generally 1 for zooming in, or -1 for zooming out * */ protected function zoomBy(dir:int):void { if (!grid.panning) { var target:Number = dir < 0 ? Math.floor(grid.zoomLevel+dir) : Math.ceil(grid.zoomLevel+dir); grid.zoomLevel = Math.min(Math.max(grid.minZoom, target), grid.maxZoom); } } /** * Add a marker at the given location (lat, lon) * * @param Location of marker. * @param optionally, a sprite (where sprite.name=id) that will always be in the right place */ public function putMarker(location:Location, marker:DisplayObject=null):void { markerClip.attachMarker(marker, location); } /** * Get a marker with the given id if one was created. * * @param ID of marker, opaque string. */ public function getMarker(id:String):DisplayObject { return markerClip.getMarker(id); } /** * Remove a marker with the given id. * * @param ID of marker, opaque string. */ public function removeMarker(id:String):void { markerClip.removeMarker(id); // also calls grid.removeMarker } public function removeAllMarkers():void { markerClip.removeAllMarkers() } /** * Dispatches MapEvent.EXTENT_CHANGED when the map is recentered. * The MapEvent includes the new extent. * * TODO: dispatch this on resize? * TODO: should we move Map to com.modestmaps.core so that this could be made internal instead of public? * * @see com.modestmaps.events.MapEvent.EXTENT_CHANGED */ protected function onExtentChanged(event:Event=null):void { if (hasEventListener(MapEvent.EXTENT_CHANGED)) { dispatchEvent(new MapEvent(MapEvent.EXTENT_CHANGED, getExtent())); } } /** * Dispatches MapEvent.BEGIN_EXTENT_CHANGE when the map is about to be resized. * The MapEvent includes the current. * * @see com.modestmaps.events.MapEvent.BEGIN_EXTENT_CHANGE */ protected function onExtentChanging():void { if (hasEventListener(MapEvent.BEGIN_EXTENT_CHANGE)) { dispatchEvent(new MapEvent(MapEvent.BEGIN_EXTENT_CHANGE, getExtent())); } } override public function set doubleClickEnabled(enabled:Boolean):void { super.doubleClickEnabled = enabled; trace("doubleClickEnabled on Map is no longer necessary!"); trace("\tto enable useful defaults, use:"); trace("\tmap.addEventListener(MouseEvent.DOUBLE_CLICK, map.onDoubleClick);"); } /** pans and zooms in on double clicked location */ public function onDoubleClick(event:MouseEvent):void { if (!__draggable) return; var p:Point = grid.globalToLocal(new Point(event.stageX, event.stageY)); if (event.shiftKey) { if (grid.zoomLevel > grid.minZoom) { zoomOutAbout(p); } else { panBy(mapWidth/2 - p.x, mapHeight/2 - p.y); } } else if (event.ctrlKey) { panAndZoomIn(pointLocation(p)); } else { if (grid.zoomLevel < grid.maxZoom) { zoomInAbout(p); } else { panBy(mapWidth/2 - p.x, mapHeight/2 - p.y); } } } } }